diff --git a/pkg/strategy/pivotshort/resistance.go b/pkg/strategy/pivotshort/resistance.go new file mode 100644 index 000000000..b32af2e42 --- /dev/null +++ b/pkg/strategy/pivotshort/resistance.go @@ -0,0 +1,145 @@ +package pivotshort + +import ( + "context" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +type ResistanceShort struct { + Enabled bool `json:"enabled"` + Symbol string `json:"-"` + Market types.Market `json:"-"` + + types.IntervalWindow + + MinDistance fixedpoint.Value `json:"minDistance"` + NumLayers int `json:"numLayers"` + 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 + + activeOrders *bbgo.ActiveOrderBook +} + +func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { + s.session = session + s.orderExecutor = orderExecutor + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + s.activeOrders.BindStream(session.UserDataStream) + + store, _ := session.MarketDataStore(s.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 + if lastKLine != nil { + s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) + } + + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + position := s.orderExecutor.Position() + if position.IsOpened(kline.Close) { + return + } + + s.findNextResistancePriceAndPlaceOrders(kline.Close) + })) +} + +func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { + + 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 { + bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) + 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 s.activeOrders.NumOfOrders() > 0 { + if err := s.orderExecutor.GracefulCancelActiveOrderBook(ctx, s.activeOrders); err != nil { + log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) + } + } + + 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.activeOrders.Add(createdOrders...) +} diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 034eea81b..814a3bca1 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -48,139 +48,6 @@ type BreakLow struct { StopEMA *types.IntervalWindow `json:"stopEMA"` } -type ResistanceShort struct { - Enabled bool `json:"enabled"` - Symbol string `json:"-"` - Market types.Market `json:"-"` - - types.IntervalWindow - - MinDistance fixedpoint.Value `json:"minDistance"` - NumLayers int `json:"numLayers"` - 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 - - activeOrders *bbgo.ActiveOrderBook -} - -func (s *ResistanceShort) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.GeneralOrderExecutor) { - s.session = session - s.orderExecutor = orderExecutor - s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) - s.activeOrders.BindStream(session.UserDataStream) - - store, _ := session.MarketDataStore(s.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 - if lastKLine != nil { - s.findNextResistancePriceAndPlaceOrders(lastKLine.Close) - } - - session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { - position := s.orderExecutor.Position() - if position.IsOpened(kline.Close) { - return - } - - s.findNextResistancePriceAndPlaceOrders(kline.Close) - })) -} - -func (s *ResistanceShort) findNextResistancePriceAndPlaceOrders(closePrice fixedpoint.Value) { - - 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 { - bbgo.Notify("Found next resistance price: %f", nextResistancePrice.Float64()) - 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.activeOrders.Orders()...); err != nil { - log.WithError(err).Errorf("can not cancel resistance orders: %+v", s.activeOrders.Orders()) - } - - 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.activeOrders.Add(createdOrders...) -} - type Strategy struct { Environment *bbgo.Environment Symbol string `json:"symbol"` @@ -356,8 +223,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se return } - // we need the price cross the break line - // or we do nothing + // we need the price cross the break line or we do nothing if !(openPrice.Compare(breakPrice) > 0 && closePrice.Compare(breakPrice) < 0) { return } @@ -378,6 +244,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } } + // graceful cancel all active orders _ = s.orderExecutor.GracefulCancel(ctx) quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity)