From 3e6b975c2c89b84089c7384bbe88948afd019cc4 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 30 Jun 2022 18:29:02 +0800 Subject: [PATCH] pivotshort: refactor ResistanceShort entry method --- config/pivotshort.yaml | 8 +- pkg/bbgo/exit.go | 2 +- pkg/dynamic/merge.go | 4 +- pkg/dynamic/merge_test.go | 10 +- pkg/strategy/pivotshort/strategy.go | 246 ++++++++++++++-------------- pkg/types/position.go | 4 + 6 files changed, 144 insertions(+), 130 deletions(-) diff --git a/config/pivotshort.yaml b/config/pivotshort.yaml index 3a9bb4ad0..ea5aa6f61 100644 --- a/config/pivotshort.yaml +++ b/config/pivotshort.yaml @@ -46,12 +46,14 @@ exchangeStrategies: window: 99 resistanceShort: - enabled: false + enabled: true interval: 1h - window: 10 + window: 8 + quantity: 10.0 + + # minDistance is used to ignore the place that is too near to the current price minDistance: 3% - # stopLossPercentage: 1% # ratio is the ratio of the resistance price, # higher the ratio, lower the price diff --git a/pkg/bbgo/exit.go b/pkg/bbgo/exit.go index 5b99ce982..2701fdb94 100644 --- a/pkg/bbgo/exit.go +++ b/pkg/bbgo/exit.go @@ -44,7 +44,7 @@ func (m *ExitMethod) Inherit(parent interface{}) { continue } - dynamic.MergeStructValues(fieldValue.Interface(), parent) + dynamic.InheritStructValues(fieldValue.Interface(), parent) } } diff --git a/pkg/dynamic/merge.go b/pkg/dynamic/merge.go index aed5a442f..8e44bd333 100644 --- a/pkg/dynamic/merge.go +++ b/pkg/dynamic/merge.go @@ -2,9 +2,9 @@ package dynamic import "reflect" -// MergeStructValues merges the field value from the source struct to the dest struct. +// InheritStructValues merges the field value from the source struct to the dest struct. // Only fields with the same type and the same name will be updated. -func MergeStructValues(dst, src interface{}) { +func InheritStructValues(dst, src interface{}) { if dst == nil { return } diff --git a/pkg/dynamic/merge_test.go b/pkg/dynamic/merge_test.go index be7c2ee25..ca61355b0 100644 --- a/pkg/dynamic/merge_test.go +++ b/pkg/dynamic/merge_test.go @@ -21,14 +21,14 @@ func Test_reflectMergeStructFields(t *testing.T) { t.Run("zero value", func(t *testing.T) { a := &TestStrategy{Symbol: "BTCUSDT"} b := &struct{ Symbol string }{Symbol: ""} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, "BTCUSDT", b.Symbol) }) t.Run("non-zero value", func(t *testing.T) { a := &TestStrategy{Symbol: "BTCUSDT"} b := &struct{ Symbol string }{Symbol: "ETHUSDT"} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, "ETHUSDT", b.Symbol, "should be the original value") }) @@ -45,7 +45,7 @@ func Test_reflectMergeStructFields(t *testing.T) { Symbol string types.IntervalWindow }{} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, iw, b.IntervalWindow) assert.Equal(t, "BTCUSDT", b.Symbol) }) @@ -62,7 +62,7 @@ func Test_reflectMergeStructFields(t *testing.T) { }{ IntervalWindow: types.IntervalWindow{Interval: types.Interval5m, Window: 9}, } - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, types.IntervalWindow{Interval: types.Interval5m, Window: 9}, b.IntervalWindow) }) @@ -75,7 +75,7 @@ func Test_reflectMergeStructFields(t *testing.T) { b := &struct { A string }{} - MergeStructValues(b, a) + InheritStructValues(b, a) assert.Equal(t, "", b.A) assert.Equal(t, 1.99, a.A) }) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 5cdb40661..a55094833 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -10,6 +10,7 @@ import ( "github.com/sirupsen/logrus" "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" @@ -48,7 +49,9 @@ type BreakLow struct { } type ResistanceShort struct { - Enabled bool `json:"enabled"` + Enabled bool `json:"enabled"` + Symbol string `json:"-"` + Market types.Market `json:"-"` types.IntervalWindow @@ -57,15 +60,126 @@ type ResistanceShort struct { LayerSpread fixedpoint.Value `json:"layerSpread"` Quantity fixedpoint.Value `json:"quantity"` Ratio fixedpoint.Value `json:"ratio"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + resistancePivot *indicator.Pivot + resistancePrices []float64 + nextResistancePrice fixedpoint.Value + + resistanceOrders []types.Order } -type Entry struct { - CatBounceRatio fixedpoint.Value `json:"catBounceRatio"` - NumLayers int `json:"numLayers"` - TotalQuantity fixedpoint.Value `json:"totalQuantity"` +func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor - Quantity fixedpoint.Value `json:"quantity"` - MarginSideEffect types.MarginOrderSideEffectType `json:"marginOrderSideEffect"` + position := orderExecutor.Position() + symbol := position.Symbol + store, _ := session.MarketDataStore(symbol) + + s.resistancePivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} + s.resistancePivot.Bind(store) + + // preload history kline data to the resistance pivot indicator + // we use the last kline to find the higher lows + lastKLine := preloadPivot(s.resistancePivot, store) + + // use the last kline from the history before we get the next closed kline + s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + + session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + if kline.Symbol != s.Symbol || kline.Interval != s.Interval { + return + } + + s.findNextResistancePriceAndPlaceOrders(kline.Close) + }) +} + +func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { + position := s.orderExecutor.Position() + if position.IsOpened(closePrice) { + return + } + + minDistance := s.MinDistance.Float64() + lows := s.resistancePivot.Lows + resistancePrices := findPossibleResistancePrices(closePrice.Float64(), minDistance, lows) + + log.Infof("last price: %f, possible resistance prices: %+v", closePrice.Float64(), resistancePrices) + + ctx := context.Background() + if len(resistancePrices) > 0 { + nextResistancePrice := fixedpoint.NewFromFloat(resistancePrices[0]) + if nextResistancePrice.Compare(s.nextResistancePrice) != 0 { + s.nextResistancePrice = nextResistancePrice + s.placeResistanceOrders(ctx, nextResistancePrice) + } + } +} + +func (s *ResistanceShort) placeResistanceOrders(ctx context.Context, resistancePrice fixedpoint.Value) { + futuresMode := s.session.Futures || s.session.IsolatedFutures + _ = futuresMode + + totalQuantity := s.Quantity + numLayers := s.NumLayers + if numLayers == 0 { + numLayers = 1 + } + + numLayersF := fixedpoint.NewFromInt(int64(numLayers)) + layerSpread := s.LayerSpread + quantity := totalQuantity.Div(numLayersF) + + if err := s.orderExecutor.CancelOrders(ctx, s.resistanceOrders...); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.resistanceOrders) + } + s.resistanceOrders = nil + + log.Infof("placing resistance orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) + + var orderForms []types.SubmitOrder + for i := 0; i < numLayers; i++ { + balances := s.session.GetAccount().Balances() + quoteBalance := balances[s.Market.QuoteCurrency] + baseBalance := balances[s.Market.BaseCurrency] + _ = quoteBalance + _ = baseBalance + + // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) + price := resistancePrice.Mul(fixedpoint.One.Add(s.Ratio)) + spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) + price = price.Add(spread) + log.Infof("price = %f", price.Float64()) + + log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) + + orderForms = append(orderForms, types.SubmitOrder{ + Symbol: s.Symbol, + Side: types.SideTypeSell, + Type: types.OrderTypeLimitMaker, + Price: price, + Quantity: quantity, + Tag: "resistanceShort", + }) + + // TODO: fix futures mode later + /* + if futuresMode { + if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { + } + } + */ + } + + createdOrders, err := s.orderExecutor.SubmitOrders(ctx, orderForms...) + if err != nil { + log.WithError(err).Errorf("can not place resistance order") + } + s.resistanceOrders = createdOrders } type Strategy struct { @@ -81,11 +195,12 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` + // BreakLow is one of the entry method BreakLow BreakLow `json:"breakLow"` + // ResistanceShort is one of the entry method ResistanceShort *ResistanceShort `json:"resistanceShort"` - Entry Entry `json:"entry"` ExitMethods bbgo.ExitMethodSet `json:"exits"` session *bbgo.ExchangeSession @@ -96,7 +211,6 @@ type Strategy struct { resistancePivot *indicator.Pivot stopEWMA *indicator.EWMA pivotLowPrices []fixedpoint.Value - resistancePrices []float64 currentBounceShortPrice fixedpoint.Value // StrategyController @@ -112,6 +226,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) if s.ResistanceShort != nil && s.ResistanceShort.Enabled { + dynamic.InheritStructValues(s.ResistanceShort, s) session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ResistanceShort.Interval}) } @@ -182,8 +297,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} s.pivot.Bind(store) - - lastKLine := preloadPivot(s.pivot, store) + preloadPivot(s.pivot, store) // update pivot low data session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { @@ -204,11 +318,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow) }) - if s.ResistanceShort != nil && s.ResistanceShort.Enabled { - s.resistancePivot = &indicator.Pivot{IntervalWindow: s.ResistanceShort.IntervalWindow} - s.resistancePivot.Bind(store) - } - if s.BreakLow.StopEMA != nil { s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) } @@ -218,35 +327,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } if s.ResistanceShort != nil && s.ResistanceShort.Enabled { - if s.resistancePivot != nil { - preloadPivot(s.resistancePivot, store) - } - - session.UserDataStream.OnStart(func() { - if lastKLine == nil { - return - } - - if s.resistancePivot != nil { - lows := s.resistancePivot.Lows - minDistance := s.ResistanceShort.MinDistance.Float64() - closePrice := lastKLine.Close.Float64() - s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) - log.Infof("last price: %f, possible resistance prices: %+v", closePrice, s.resistancePrices) - - if len(s.resistancePrices) > 0 { - resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0]) - if resistancePrice.Compare(s.currentBounceShortPrice) != 0 { - log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices) - - _ = s.orderExecutor.GracefulCancel(ctx) - - s.currentBounceShortPrice = resistancePrice - s.placeBounceSellOrders(ctx, s.currentBounceShortPrice) - } - } - } - }) + s.ResistanceShort.Bind(session, s.orderExecutor) } // Always check whether you can open a short position or not @@ -321,40 +402,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - // StrategyController - if s.Status != types.StrategyStatusRunning { - return - } - - if s.ResistanceShort == nil || !s.ResistanceShort.Enabled { - return - } - - if kline.Symbol != s.Symbol || kline.Interval != s.ResistanceShort.Interval { - return - } - - if s.resistancePivot != nil { - closePrice := kline.Close.Float64() - minDistance := s.ResistanceShort.MinDistance.Float64() - lows := s.resistancePivot.Lows - s.resistancePrices = findPossibleResistancePrices(closePrice, minDistance, lows) - - if len(s.resistancePrices) > 0 { - resistancePrice := fixedpoint.NewFromFloat(s.resistancePrices[0]) - if resistancePrice.Compare(s.currentBounceShortPrice) != 0 { - log.Infof("updating resistance price... possible resistance prices: %+v", s.resistancePrices) - - _ = s.orderExecutor.GracefulCancel(ctx) - - s.currentBounceShortPrice = resistancePrice - s.placeBounceSellOrders(ctx, s.currentBounceShortPrice) - } - } - } - }) - if !bbgo.IsBackTesting { // use market trade to submit short order session.MarketDataStream.OnMarketTrade(func(trade types.Trade) { @@ -380,46 +427,6 @@ func (s *Strategy) findHigherPivotLow(price fixedpoint.Value) (fixedpoint.Value, return price, false } -func (s *Strategy) placeBounceSellOrders(ctx context.Context, resistancePrice fixedpoint.Value) { - futuresMode := s.session.Futures || s.session.IsolatedFutures - totalQuantity := s.ResistanceShort.Quantity - numLayers := s.ResistanceShort.NumLayers - if numLayers == 0 { - numLayers = 1 - } - - numLayersF := fixedpoint.NewFromInt(int64(numLayers)) - - layerSpread := s.ResistanceShort.LayerSpread - quantity := totalQuantity.Div(numLayersF) - - log.Infof("placing bounce short orders: resistance price = %f, layer quantity = %f, num of layers = %d", resistancePrice.Float64(), quantity.Float64(), numLayers) - - for i := 0; i < numLayers; i++ { - balances := s.session.GetAccount().Balances() - quoteBalance := balances[s.Market.QuoteCurrency] - baseBalance := balances[s.Market.BaseCurrency] - - // price = (resistance_price * (1.0 + ratio)) * ((1.0 + layerSpread) * i) - price := resistancePrice.Mul(fixedpoint.One.Add(s.ResistanceShort.Ratio)) - spread := layerSpread.Mul(fixedpoint.NewFromInt(int64(i))) - price = price.Add(spread) - log.Infof("price = %f", price.Float64()) - - log.Infof("placing bounce short order #%d: price = %f, quantity = %f", i, price.Float64(), quantity.Float64()) - - if futuresMode { - if quantity.Mul(price).Compare(quoteBalance.Available) <= 0 { - s.placeOrder(ctx, price, quantity) - } - } else { - if quantity.Compare(baseBalance.Available) <= 0 { - s.placeOrder(ctx, price, quantity) - } - } - } -} - func (s *Strategy) placeOrder(ctx context.Context, price fixedpoint.Value, quantity fixedpoint.Value) { _, _ = s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: s.Symbol, @@ -474,6 +481,7 @@ func (s *Strategy) placeMarketSell(ctx context.Context, quantity fixedpoint.Valu func findPossibleResistancePrices(closePrice float64, minDistance float64, lows []float64) []float64 { // sort float64 in increasing order + // lower to higher prices sort.Float64s(lows) var resistancePrices []float64 diff --git a/pkg/types/position.go b/pkg/types/position.go index 185106572..e96423006 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -273,6 +273,10 @@ func (p *Position) IsClosed() bool { return p.Base.Sign() == 0 } +func (p *Position) IsOpened(currentPrice fixedpoint.Value) bool { + return p.IsClosed() || !p.IsDust(currentPrice) +} + func (p *Position) Type() PositionType { if p.Base.Sign() > 0 { return PositionLong