Merge pull request #898 from c9s/refactor/pivotshort

Refactor/pivotshort
This commit is contained in:
Yo-An Lin 2022-08-26 19:09:00 +08:00 committed by GitHub
commit f17249ba89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 280 additions and 206 deletions

View File

@ -32,9 +32,13 @@ type ExitMethod struct {
RoiStopLoss *RoiStopLoss `json:"roiStopLoss"`
ProtectiveStopLoss *ProtectiveStopLoss `json:"protectiveStopLoss"`
RoiTakeProfit *RoiTakeProfit `json:"roiTakeProfit"`
TrailingStop *TrailingStop2 `json:"trailingStop"`
// Exit methods for short positions
// =================================================
LowerShadowTakeProfit *LowerShadowTakeProfit `json:"lowerShadowTakeProfit"`
CumulatedVolumeTakeProfit *CumulatedVolumeTakeProfit `json:"cumulatedVolumeTakeProfit"`
TrailingStop *TrailingStop2 `json:"trailingStop"`
SupportTakeProfit *SupportTakeProfit `json:"supportTakeProfit"`
}
func (e ExitMethod) String() string {
@ -43,26 +47,37 @@ func (e ExitMethod) String() string {
b, _ := json.Marshal(e.RoiStopLoss)
buf.WriteString("roiStopLoss: " + string(b) + ", ")
}
if e.ProtectiveStopLoss != nil {
b, _ := json.Marshal(e.ProtectiveStopLoss)
buf.WriteString("protectiveStopLoss: " + string(b) + ", ")
}
if e.RoiTakeProfit != nil {
b, _ := json.Marshal(e.RoiTakeProfit)
buf.WriteString("rioTakeProft: " + string(b) + ", ")
}
if e.LowerShadowTakeProfit != nil {
b, _ := json.Marshal(e.LowerShadowTakeProfit)
buf.WriteString("lowerShadowTakeProft: " + string(b) + ", ")
}
if e.CumulatedVolumeTakeProfit != nil {
b, _ := json.Marshal(e.CumulatedVolumeTakeProfit)
buf.WriteString("cumulatedVolumeTakeProfit: " + string(b) + ", ")
}
if e.TrailingStop != nil {
b, _ := json.Marshal(e.TrailingStop)
buf.WriteString("trailingStop: " + string(b) + ", ")
}
if e.SupportTakeProfit != nil {
b, _ := json.Marshal(e.SupportTakeProfit)
buf.WriteString("supportTakeProfit: " + string(b) + ", ")
}
return buf.String()
}
@ -113,6 +128,10 @@ func (m *ExitMethod) Bind(session *ExchangeSession, orderExecutor *GeneralOrderE
m.CumulatedVolumeTakeProfit.Bind(session, orderExecutor)
}
if m.SupportTakeProfit != nil {
m.SupportTakeProfit.Bind(session, orderExecutor)
}
if m.TrailingStop != nil {
m.TrailingStop.Bind(session, orderExecutor)
}

View File

@ -11,6 +11,12 @@ import (
const enableMarketTradeStop = false
// ProtectiveStopLoss provides a way to protect your profit but also keep a room for the price volatility
// Set ActivationRatio to 1% means if the price is away from your average cost by 1%, we will activate the protective stop loss
// and the StopLossRatio is the minimal profit ratio you want to keep for your position.
// If you set StopLossRatio to 0.1% and ActivationRatio to 1%,
// when the price goes away from your average cost by 1% and then goes back to below your (average_cost * (1 - 0.1%))
// The stop will trigger.
type ProtectiveStopLoss struct {
Symbol string `json:"symbol"`

View File

@ -0,0 +1,132 @@
package bbgo
import (
"context"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
// SupportTakeProfit finds the previous support price and take profit at the previous low.
type SupportTakeProfit struct {
Symbol string
types.IntervalWindow
Ratio fixedpoint.Value `json:"ratio"`
pivot *indicator.PivotLow
orderExecutor *GeneralOrderExecutor
session *ExchangeSession
activeOrders *ActiveOrderBook
currentSupportPrice fixedpoint.Value
triggeredPrices []fixedpoint.Value
}
func (s *SupportTakeProfit) Subscribe(session *ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
}
func (s *SupportTakeProfit) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
s.activeOrders = NewActiveOrderBook(s.Symbol)
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if s.activeOrders.Exists(order) {
if !s.currentSupportPrice.IsZero() {
s.triggeredPrices = append(s.triggeredPrices, s.currentSupportPrice)
}
}
})
s.activeOrders.BindStream(session.UserDataStream)
position := orderExecutor.Position()
s.pivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow)
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
if !s.updateSupportPrice(kline.Close) {
return
}
if !position.IsOpened(kline.Close) {
logrus.Infof("position is not opened, skip updating support take profit order")
return
}
buyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio))
quantity := position.GetQuantity()
ctx := context.Background()
if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil {
logrus.WithError(err).Errorf("cancel order failed")
}
Notify("placing %s take profit order at price %f", s.Symbol, buyPrice.Float64())
createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Type: types.OrderTypeLimitMaker,
Side: types.SideTypeBuy,
Price: buyPrice,
Quantity: quantity,
Tag: "supportTakeProfit",
MarginSideEffect: types.SideEffectTypeAutoRepay,
})
if err != nil {
logrus.WithError(err).Errorf("can not submit orders: %+v", createdOrders)
}
s.activeOrders.Add(createdOrders...)
}))
}
func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool {
logrus.Infof("[supportTakeProfit] lows: %v", s.pivot.Values)
groupDistance := 0.01
minDistance := 0.05
supportPrices := findPossibleSupportPrices(closePrice.Float64()*(1.0-minDistance), groupDistance, s.pivot.Values)
if len(supportPrices) == 0 {
return false
}
logrus.Infof("[supportTakeProfit] found possible support prices: %v", supportPrices)
// nextSupportPrice are sorted in increasing order
nextSupportPrice := fixedpoint.NewFromFloat(supportPrices[len(supportPrices)-1])
// it's price that we have been used to take profit
for _, p := range s.triggeredPrices {
var l = p.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
var h = p.Mul(one.Add(fixedpoint.NewFromFloat(0.01)))
if p.Compare(l) > 0 && p.Compare(h) < 0 {
return false
}
}
currentBuyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio))
if s.currentSupportPrice.IsZero() {
logrus.Infof("setup next support take profit price at %f", nextSupportPrice.Float64())
s.currentSupportPrice = nextSupportPrice
return true
}
// the close price is already lower than the support price, than we should update
if closePrice.Compare(currentBuyPrice) < 0 || nextSupportPrice.Compare(s.currentSupportPrice) > 0 {
logrus.Infof("setup next support take profit price at %f", nextSupportPrice.Float64())
s.currentSupportPrice = nextSupportPrice
return true
}
return false
}
func findPossibleSupportPrices(closePrice float64, groupDistance float64, lows []float64) []float64 {
return floats.Group(floats.Lower(lows, closePrice), groupDistance)
}

42
pkg/bbgo/stop_ema.go Normal file
View File

@ -0,0 +1,42 @@
package bbgo
import (
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type StopEMA struct {
types.IntervalWindow
Range fixedpoint.Value `json:"range"`
stopEWMA *indicator.EWMA
}
func (s *StopEMA) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
symbol := orderExecutor.Position().Symbol
s.stopEWMA = session.StandardIndicatorSet(symbol).EWMA(s.IntervalWindow)
}
func (s *StopEMA) Allowed(closePrice fixedpoint.Value) bool {
ema := fixedpoint.NewFromFloat(s.stopEWMA.Last())
if ema.IsZero() {
logrus.Infof("stopEMA protection: value is zero, skip")
return false
}
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.Range))
if closePrice.Compare(emaStopShortPrice) < 0 {
Notify("stopEMA protection: close price %f less than stopEMA %f = EMA(%f) * (1 - RANGE %f)",
closePrice.Float64(),
s.IntervalWindow.String(),
emaStopShortPrice.Float64(),
ema.Float64(),
s.Range.Float64())
return false
}
return true
}

70
pkg/bbgo/trend_ema.go Normal file
View File

@ -0,0 +1,70 @@
package bbgo
import (
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type TrendEMA struct {
types.IntervalWindow
// MaxGradient is the maximum gradient allowed for the entry.
MaxGradient float64 `json:"maxGradient"`
MinGradient float64 `json:"minGradient"`
ewma *indicator.EWMA
last, current float64
}
func (s *TrendEMA) Bind(session *ExchangeSession, orderExecutor *GeneralOrderExecutor) {
if s.MaxGradient == 0.0 {
s.MaxGradient = 1.0
}
symbol := orderExecutor.Position().Symbol
s.ewma = session.StandardIndicatorSet(symbol).EWMA(s.IntervalWindow)
session.MarketDataStream.OnStart(func() {
if s.ewma.Length() < 2 {
return
}
s.last = s.ewma.Values[s.ewma.Length()-2]
s.current = s.ewma.Last()
})
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
s.last = s.current
s.current = s.ewma.Last()
}))
}
func (s *TrendEMA) Gradient() float64 {
if s.last > 0.0 && s.current > 0.0 {
return s.last / s.current
}
return 0.0
}
func (s *TrendEMA) GradientAllowed() bool {
gradient := s.Gradient()
logrus.Infof("trendEMA %+v current=%f last=%f gradient=%f", s, s.current, s.last, gradient)
if gradient == .0 {
return false
}
if s.MaxGradient > 0.0 && gradient < s.MaxGradient {
return true
}
if s.MinGradient > 0.0 && gradient > s.MinGradient {
return true
}
return false
}

View File

@ -10,11 +10,6 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
type StopEMA struct {
types.IntervalWindow
Range fixedpoint.Value `json:"range"`
}
type FakeBreakStop struct {
types.IntervalWindow
}
@ -38,9 +33,9 @@ type BreakLow struct {
Leverage fixedpoint.Value `json:"leverage"`
Quantity fixedpoint.Value `json:"quantity"`
StopEMA *StopEMA `json:"stopEMA"`
StopEMA *bbgo.StopEMA `json:"stopEMA"`
TrendEMA *TrendEMA `json:"trendEMA"`
TrendEMA *bbgo.TrendEMA `json:"trendEMA"`
FakeBreakStop *FakeBreakStop `json:"fakeBreakStop"`
@ -52,8 +47,6 @@ type BreakLow struct {
pivotLow *indicator.PivotLow
pivotLowPrices []fixedpoint.Value
stopEWMA *indicator.EWMA
trendEWMALast, trendEWMACurrent float64
orderExecutor *bbgo.GeneralOrderExecutor
@ -90,13 +83,10 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
s.pivotLow = standardIndicator.PivotLow(s.IntervalWindow)
if s.StopEMA != nil {
s.stopEWMA = standardIndicator.EWMA(s.StopEMA.IntervalWindow)
s.StopEMA.Bind(session, orderExecutor)
}
if s.TrendEMA != nil {
if s.TrendEMA.MaxGradient == 0.0 {
s.TrendEMA.MaxGradient = 1.0
}
s.TrendEMA.Bind(session, orderExecutor)
}
@ -196,16 +186,8 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
}
// stop EMA protection
if s.stopEWMA != nil {
ema := fixedpoint.NewFromFloat(s.stopEWMA.Last())
if ema.IsZero() {
log.Infof("stopEMA protection: value is zero, skip")
return
}
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.StopEMA.Range))
if closePrice.Compare(emaStopShortPrice) < 0 {
bbgo.Notify("stopEMA protection: close price %f < EMA(%v %f) * (1 - RANGE %f) = %f", closePrice.Float64(), s.StopEMA, ema.Float64(), s.StopEMA.Range.Float64(), emaStopShortPrice.Float64())
if s.StopEMA != nil {
if !s.StopEMA.Allowed(closePrice) {
return
}
}

View File

@ -26,7 +26,7 @@ type ResistanceShort struct {
Leverage fixedpoint.Value `json:"leverage"`
Ratio fixedpoint.Value `json:"ratio"`
TrendEMA *TrendEMA `json:"trendEMA"`
TrendEMA *bbgo.TrendEMA `json:"trendEMA"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
@ -39,6 +39,8 @@ type ResistanceShort struct {
}
func (s *ResistanceShort) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
if s.TrendEMA != nil {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.TrendEMA.Interval})
}
@ -59,9 +61,6 @@ func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
s.activeOrders.BindStream(session.UserDataStream)
if s.TrendEMA != nil {
if s.TrendEMA.MaxGradient == 0.0 {
s.TrendEMA.MaxGradient = 1.0
}
s.TrendEMA.Bind(session, orderExecutor)
}

View File

@ -11,7 +11,6 @@ import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/dynamic"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
@ -25,121 +24,6 @@ func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type SupportTakeProfit struct {
Symbol string
types.IntervalWindow
Ratio fixedpoint.Value `json:"ratio"`
pivot *indicator.PivotLow
orderExecutor *bbgo.GeneralOrderExecutor
session *bbgo.ExchangeSession
activeOrders *bbgo.ActiveOrderBook
currentSupportPrice fixedpoint.Value
triggeredPrices []fixedpoint.Value
}
func (s *SupportTakeProfit) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
}
func (s *SupportTakeProfit) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
s.session = session
s.orderExecutor = orderExecutor
s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol)
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if s.activeOrders.Exists(order) {
if !s.currentSupportPrice.IsZero() {
s.triggeredPrices = append(s.triggeredPrices, s.currentSupportPrice)
}
}
})
s.activeOrders.BindStream(session.UserDataStream)
position := orderExecutor.Position()
s.pivot = session.StandardIndicatorSet(s.Symbol).PivotLow(s.IntervalWindow)
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) {
if !s.updateSupportPrice(kline.Close) {
return
}
if !position.IsOpened(kline.Close) {
log.Infof("position is not opened, skip updating support take profit order")
return
}
buyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio))
quantity := position.GetQuantity()
ctx := context.Background()
if err := orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil {
log.WithError(err).Errorf("cancel order failed")
}
bbgo.Notify("placing %s take profit order at price %f", s.Symbol, buyPrice.Float64())
createdOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Type: types.OrderTypeLimitMaker,
Side: types.SideTypeBuy,
Price: buyPrice,
Quantity: quantity,
Tag: "supportTakeProfit",
MarginSideEffect: types.SideEffectTypeAutoRepay,
})
if err != nil {
log.WithError(err).Errorf("can not submit orders: %+v", createdOrders)
}
s.activeOrders.Add(createdOrders...)
}))
}
func (s *SupportTakeProfit) updateSupportPrice(closePrice fixedpoint.Value) bool {
log.Infof("[supportTakeProfit] lows: %v", s.pivot.Values)
groupDistance := 0.01
minDistance := 0.05
supportPrices := findPossibleSupportPrices(closePrice.Float64()*(1.0-minDistance), groupDistance, s.pivot.Values)
if len(supportPrices) == 0 {
return false
}
log.Infof("[supportTakeProfit] found possible support prices: %v", supportPrices)
// nextSupportPrice are sorted in increasing order
nextSupportPrice := fixedpoint.NewFromFloat(supportPrices[len(supportPrices)-1])
// it's price that we have been used to take profit
for _, p := range s.triggeredPrices {
var l = p.Mul(one.Sub(fixedpoint.NewFromFloat(0.01)))
var h = p.Mul(one.Add(fixedpoint.NewFromFloat(0.01)))
if p.Compare(l) > 0 && p.Compare(h) < 0 {
return false
}
}
currentBuyPrice := s.currentSupportPrice.Mul(one.Add(s.Ratio))
if s.currentSupportPrice.IsZero() {
log.Infof("setup next support take profit price at %f", nextSupportPrice.Float64())
s.currentSupportPrice = nextSupportPrice
return true
}
// the close price is already lower than the support price, than we should update
if closePrice.Compare(currentBuyPrice) < 0 || nextSupportPrice.Compare(s.currentSupportPrice) > 0 {
log.Infof("setup next support take profit price at %f", nextSupportPrice.Float64())
s.currentSupportPrice = nextSupportPrice
return true
}
return false
}
type Strategy struct {
Environment *bbgo.Environment
Symbol string `json:"symbol"`
@ -163,7 +47,7 @@ type Strategy struct {
// ResistanceShort is one of the entry method
ResistanceShort *ResistanceShort `json:"resistanceShort"`
SupportTakeProfit []*SupportTakeProfit `json:"supportTakeProfit"`
SupportTakeProfit []*bbgo.SupportTakeProfit `json:"supportTakeProfit"`
ExitMethods bbgo.ExitMethodSet `json:"exits"`
@ -188,7 +72,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
if s.ResistanceShort != nil && s.ResistanceShort.Enabled {
dynamic.InheritStructValues(s.ResistanceShort, s)
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval})
s.ResistanceShort.Subscribe(session)
}
if s.BreakLow != nil {

View File

@ -1,60 +0,0 @@
package pivotshort
import (
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type TrendEMA struct {
types.IntervalWindow
// MaxGradient is the maximum gradient allowed for the entry.
MaxGradient float64 `json:"maxGradient"`
MinGradient float64 `json:"minGradient"`
trendEWMA *indicator.EWMA
trendEWMALast, trendEWMACurrent, trendGradient float64
}
func (s *TrendEMA) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) {
symbol := orderExecutor.Position().Symbol
s.trendEWMA = session.StandardIndicatorSet(symbol).EWMA(s.IntervalWindow)
session.MarketDataStream.OnStart(func() {
if s.trendEWMA.Length() > 1 {
s.trendEWMALast = s.trendEWMA.Values[s.trendEWMA.Length()-2]
s.trendEWMACurrent = s.trendEWMA.Last()
}
})
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
s.trendEWMALast = s.trendEWMACurrent
s.trendEWMACurrent = s.trendEWMA.Last()
}))
}
func (s *TrendEMA) Gradient() float64 {
return s.trendGradient
}
func (s *TrendEMA) GradientAllowed() bool {
if s.trendEWMALast > 0.0 && s.trendEWMACurrent > 0.0 {
s.trendGradient = s.trendEWMALast / s.trendEWMACurrent
}
log.Infof("trendEMA %+v current=%f last=%f gradient=%f", s, s.trendEWMACurrent, s.trendEWMALast, s.trendGradient)
if s.trendGradient == .0 {
return false
}
if s.MaxGradient > 0.0 && s.trendGradient < s.MaxGradient {
return true
}
if s.MinGradient > 0.0 && s.trendGradient > s.MinGradient {
return true
}
return false
}