diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index f4b253d3f..b65211f9f 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -17,7 +17,7 @@ backtest: # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp startTime: "2022-01-01" - endTime: "2022-05-12" + endTime: "2022-07-18" sessions: - binance symbols: @@ -133,28 +133,24 @@ exchangeStrategies: # buyBelowNeutralSMA: when this set, it will only place buy order when the current price is below the SMA line. buyBelowNeutralSMA: false - # Set up your stop order, this is optional - # sometimes the stop order might decrease your total profit. - # you can setup multiple stop, - stops: - # use trailing stop order - - trailingStop: - # callbackRate: when the price reaches -1% from the previous highest, we trigger the stop - callbackRate: 5.1% + exits: - # closePosition is how much position do you want to close - closePosition: 20% + # roiTakeProfit is used to force taking profit by percentage of the position ROI (currently the price change) + # force to take the profit ROI exceeded the percentage. + - roiTakeProfit: + percentage: 3% - # minProfit is how much profit you want to take. - # if you set this option, your stop will only be triggered above the average cost. - minProfit: 5% + - protectiveStopLoss: + activationRatio: 1% + stopLossRatio: 0.2% + placeStopOrder: false - # interval is the time interval for checking your stop - interval: 1m - - # virtual means we don't place a a REAL stop order - # when virtual is on - # the strategy won't place a REAL stop order, instead if watches the close price, - # and if the condition matches, it submits a market order to close your position. - virtual: true + - protectiveStopLoss: + activationRatio: 2% + stopLossRatio: 1% + placeStopOrder: false + - protectiveStopLoss: + activationRatio: 5% + stopLossRatio: 3% + placeStopOrder: false diff --git a/pkg/bbgo/exit_trailing_stop_test.go b/pkg/bbgo/exit_trailing_stop_test.go index 385d89363..e585899ce 100644 --- a/pkg/bbgo/exit_trailing_stop_test.go +++ b/pkg/bbgo/exit_trailing_stop_test.go @@ -36,12 +36,13 @@ func TestTrailingStop_ShortPosition(t *testing.T) { mockEx := mocks.NewMockExchange(mockCtrl) mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{ - Symbol: "BTCUSDT", - Side: types.SideTypeBuy, - Type: types.OrderTypeMarket, - Market: market, - Quantity: fixedpoint.NewFromFloat(1.0), - Tag: "trailingStop", + Symbol: "BTCUSDT", + Side: types.SideTypeBuy, + Type: types.OrderTypeMarket, + Market: market, + Quantity: fixedpoint.NewFromFloat(1.0), + Tag: "trailingStop", + MarginSideEffect: types.SideEffectTypeAutoRepay, }) session := NewExchangeSession("test", mockEx) @@ -113,12 +114,13 @@ func TestTrailingStop_LongPosition(t *testing.T) { mockEx := mocks.NewMockExchange(mockCtrl) mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2) mockEx.EXPECT().SubmitOrders(gomock.Any(), types.SubmitOrder{ - Symbol: "BTCUSDT", - Side: types.SideTypeSell, - Type: types.OrderTypeMarket, - Market: market, - Quantity: fixedpoint.NewFromFloat(1.0), - Tag: "trailingStop", + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Market: market, + Quantity: fixedpoint.NewFromFloat(1.0), + Tag: "trailingStop", + MarginSideEffect: types.SideEffectTypeAutoRepay, }) session := NewExchangeSession("test", mockEx) diff --git a/pkg/bbgo/smart_stops.go b/pkg/bbgo/smart_stops.go deleted file mode 100644 index 17a3ff8a8..000000000 --- a/pkg/bbgo/smart_stops.go +++ /dev/null @@ -1,287 +0,0 @@ -package bbgo - -import ( - "context" - "errors" - - log "github.com/sirupsen/logrus" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" -) - -type TrailingStop struct { - // CallbackRate is the callback rate from the previous high price - CallbackRate fixedpoint.Value `json:"callbackRate,omitempty"` - - // ClosePosition is a percentage of the position to be closed - ClosePosition fixedpoint.Value `json:"closePosition,omitempty"` - - // MinProfit is the percentage of the minimum profit ratio. - // Stop order will be activiated only when the price reaches above this threshold. - MinProfit fixedpoint.Value `json:"minProfit,omitempty"` - - // Interval is the time resolution to update the stop order - // KLine per Interval will be used for updating the stop order - Interval types.Interval `json:"interval,omitempty"` - - // Virtual is used when you don't want to place the real order on the exchange and lock the balance. - // You want to handle the stop order by the strategy itself. - Virtual bool `json:"virtual,omitempty"` -} - -type TrailingStopController struct { - *TrailingStop - - Symbol string - - position *types.Position - latestHigh fixedpoint.Value - averageCost fixedpoint.Value - - // activated: when the price reaches the min profit price, we set the activated to true to enable trailing stop - activated bool -} - -func NewTrailingStopController(symbol string, config *TrailingStop) *TrailingStopController { - return &TrailingStopController{ - TrailingStop: config, - Symbol: symbol, - } -} - -func (c *TrailingStopController) Subscribe(session *ExchangeSession) { - session.Subscribe(types.KLineChannel, c.Symbol, types.SubscribeOptions{ - Interval: c.Interval, - }) -} - -func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) { - // store the position - c.position = tradeCollector.Position() - c.averageCost = c.position.AverageCost - - // Use trade collector to get the position update event - tradeCollector.OnPositionUpdate(func(position *types.Position) { - // update average cost if we have it. - c.averageCost = position.AverageCost - }) - - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { - if kline.Symbol != c.Symbol || kline.Interval != c.Interval { - return - } - - // if average cost is zero, we don't need trailing stop - if c.averageCost.IsZero() || c.position == nil { - return - } - - closePrice := kline.Close - - // if we don't hold position, we just skip dust position - if c.position.Base.Abs().Compare(c.position.Market.MinQuantity) < 0 || c.position.Base.Abs().Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 { - return - } - - if c.MinProfit.Sign() <= 0 { - // when minProfit is not set, we should always activate the trailing stop order - c.activated = true - } else if closePrice.Compare(c.averageCost) > 0 || - changeRate(closePrice, c.averageCost).Compare(c.MinProfit) > 0 { - - if !c.activated { - log.Infof("%s trailing stop activated at price %s", c.Symbol, closePrice.String()) - c.activated = true - } - } else { - return - } - - if !c.activated { - return - } - - // if the trailing stop order is activated, we should update the latest high - // update the latest high - c.latestHigh = fixedpoint.Max(closePrice, c.latestHigh) - - // if it's in the callback rate, we don't want to trigger stop - if closePrice.Compare(c.latestHigh) < 0 && changeRate(closePrice, c.latestHigh).Compare(c.CallbackRate) < 0 { - return - } - - if c.Virtual { - // if the profit rate is defined, and it is less than our minimum profit rate, we skip stop - if c.MinProfit.Sign() > 0 && - closePrice.Compare(c.averageCost) < 0 || - changeRate(closePrice, c.averageCost).Compare(c.MinProfit) < 0 { - return - } - - log.Infof("%s trailing stop emitted, latest high: %s, closed price: %s, average cost: %s, profit spread: %s", - c.Symbol, - c.latestHigh.String(), - closePrice.String(), - c.averageCost.String(), - closePrice.Sub(c.averageCost).String()) - - log.Infof("current %s position: %s", c.Symbol, c.position.String()) - - marketOrder := c.position.NewMarketCloseOrder(c.ClosePosition) - if marketOrder != nil { - log.Infof("submitting %s market order to stop: %+v", c.Symbol, marketOrder) - - // skip dust order - if marketOrder.Quantity.Mul(closePrice).Compare(c.position.Market.MinNotional) < 0 { - log.Warnf("%s market order quote quantity %s < min notional %s, skip placing order", c.Symbol, marketOrder.Quantity.Mul(closePrice).String(), c.position.Market.MinNotional.String()) - return - } - - createdOrders, err := session.Exchange.SubmitOrders(ctx, *marketOrder) - if err != nil { - log.WithError(err).Errorf("stop market order place error") - return - } - tradeCollector.OrderStore().Add(createdOrders...) - tradeCollector.Process() - - // reset the state - c.latestHigh = fixedpoint.Zero - c.activated = false - } - } else { - // place stop order only when the closed price is greater than the current average cost - if c.MinProfit.Sign() > 0 && closePrice.Compare(c.averageCost) > 0 && - changeRate(closePrice, c.averageCost).Compare(c.MinProfit) >= 0 { - - stopPrice := c.averageCost.Mul(fixedpoint.One.Add(c.MinProfit)) - orderForm := c.GenerateStopOrder(stopPrice, c.averageCost) - if orderForm != nil { - log.Infof("updating %s stop limit order to simulate trailing stop order...", c.Symbol) - - createdOrders, err := session.Exchange.SubmitOrders(ctx, *orderForm) - if err != nil { - log.WithError(err).Errorf("%s stop order place error", c.Symbol) - return - } - - tradeCollector.OrderStore().Add(createdOrders...) - tradeCollector.Process() - } - } - } - }) -} - -func (c *TrailingStopController) GenerateStopOrder(stopPrice, price fixedpoint.Value) *types.SubmitOrder { - base := c.position.GetBase() - if base.IsZero() { - return nil - } - - quantity := base.Abs() - quoteQuantity := price.Mul(quantity) - - if c.ClosePosition.Sign() > 0 { - quantity = quantity.Mul(c.ClosePosition) - } - - // skip dust orders - if quantity.Compare(c.position.Market.MinQuantity) < 0 || - quoteQuantity.Compare(c.position.Market.MinNotional) < 0 { - return nil - } - - side := types.SideTypeSell - if base.Sign() < 0 { - side = types.SideTypeBuy - } - - return &types.SubmitOrder{ - Symbol: c.Symbol, - Market: c.position.Market, - Type: types.OrderTypeStopLimit, - Side: side, - StopPrice: stopPrice, - Price: price, - Quantity: quantity, - } -} - -type FixedStop struct{} - -type Stop struct { - TrailingStop *TrailingStop `json:"trailingStop,omitempty"` - FixedStop *FixedStop `json:"fixedStop,omitempty"` -} - -// SmartStops shares the stop order logics between different strategies -// -// See also: -// - Stop-Loss order: https://www.investopedia.com/terms/s/stop-lossorder.asp -// - Trailing Stop-loss order: https://www.investopedia.com/articles/trading/08/trailing-stop-loss.asp -// -// How to integrate this into your strategy? -// -// To use the stop controllers, you can embed this struct into your Strategy struct -// -// func (s *Strategy) Initialize() error { -// return s.SmartStops.InitializeStopControllers(s.Symbol) -// } -// func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { -// s.SmartStops.Subscribe(session) -// } -// -// func (s *Strategy) Run() { -// s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector) -// } -// -type SmartStops struct { - // Stops is the slice of the stop order config - Stops []Stop `json:"stops,omitempty"` - - // StopControllers are constructed from the stop config - StopControllers []StopController `json:"-"` -} - -type StopController interface { - Subscribe(session *ExchangeSession) - Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) -} - -func (s *SmartStops) newStopController(symbol string, config Stop) (StopController, error) { - if config.TrailingStop != nil { - return NewTrailingStopController(symbol, config.TrailingStop), nil - } - - return nil, errors.New("incorrect stop controller setup") -} - -func (s *SmartStops) InitializeStopControllers(symbol string) error { - for _, stop := range s.Stops { - controller, err := s.newStopController(symbol, stop) - if err != nil { - return err - } - - s.StopControllers = append(s.StopControllers, controller) - } - return nil -} - -func (s *SmartStops) Subscribe(session *ExchangeSession) { - for _, stopController := range s.StopControllers { - stopController.Subscribe(session) - } -} - -func (s *SmartStops) RunStopControllers(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) { - for _, stopController := range s.StopControllers { - stopController.Run(ctx, session, tradeCollector) - } -} - -func changeRate(a, b fixedpoint.Value) fixedpoint.Value { - return a.Sub(b).Div(b).Abs() -} diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 564f35124..711e33f55 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -23,8 +23,6 @@ import ( const ID = "bollmaker" -const stateKey = "state-v1" - var notionModifier = fixedpoint.NewFromFloat(1.1) var two = fixedpoint.NewFromInt(2) @@ -58,8 +56,7 @@ type Strategy struct { // Symbol is the market symbol you want to trade Symbol string `json:"symbol"` - // Interval is how long do you want to update your order price and quantity - Interval types.Interval `json:"interval"` + types.IntervalWindow bbgo.QuantityOrAmount @@ -142,12 +139,10 @@ type Strategy struct { ShadowProtection bool `json:"shadowProtection"` ShadowProtectionRatio fixedpoint.Value `json:"shadowProtectionRatio"` - bbgo.SmartStops - session *bbgo.ExchangeSession book *types.StreamOrderBook - state *State + ExitMethods bbgo.ExitMethodSet `json:"exits"` // persistence fields Position *types.Position `json:"position,omitempty" persistence:"position"` @@ -175,10 +170,6 @@ func (s *Strategy) InstanceID() string { return fmt.Sprintf("%s:%s", ID, s.Symbol) } -func (s *Strategy) Initialize() error { - return s.SmartStops.InitializeStopControllers(s.Symbol) -} - func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.Interval, @@ -196,7 +187,7 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { }) } - s.SmartStops.Subscribe(session) + s.ExitMethods.SetAndSubscribe(session, s) } func (s *Strategy) Validate() error { @@ -449,18 +440,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.DynamicSpread.DynamicAskSpread = &indicator.SMA{IntervalWindow: types.IntervalWindow{s.Interval, s.DynamicSpread.Window}} } - s.OnSuspend(func() { - s.Status = types.StrategyStatusStopped - _ = s.orderExecutor.GracefulCancel(ctx) - bbgo.Sync(s) - }) - - s.OnEmergencyStop(func() { - // Close 100% position - percentage := fixedpoint.NewFromFloat(1.0) - _ = s.ClosePosition(ctx, percentage) - }) - if s.DisableShort { s.Long = &[]bool{true}[0] } @@ -515,18 +494,27 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.orderExecutor.BindEnvironment(s.Environment) s.orderExecutor.BindProfitStats(s.ProfitStats) s.orderExecutor.Bind() - s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(s) }) - - s.SmartStops.RunStopControllers(ctx, session, s.orderExecutor.TradeCollector()) + s.ExitMethods.Bind(session, s.orderExecutor) if bbgo.IsBackTesting { log.Warn("turning of useTickerPrice option in the back-testing environment...") s.UseTickerPrice = false } + s.OnSuspend(func() { + _ = s.orderExecutor.GracefulCancel(ctx) + bbgo.Sync(s) + }) + + s.OnEmergencyStop(func() { + // Close 100% position + percentage := fixedpoint.NewFromFloat(1.0) + _ = s.ClosePosition(ctx, percentage) + }) + session.UserDataStream.OnStart(func() { if s.UseTickerPrice { ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol) @@ -543,28 +531,24 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { + session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { // StrategyController if s.Status != types.StrategyStatusRunning { return } - if kline.Symbol != s.Symbol || kline.Interval != s.Interval { - return - } - // Update spreads with dynamic spread if s.DynamicSpread.Enabled { s.DynamicSpread.Update(kline) dynamicBidSpread, err := s.DynamicSpread.GetBidSpread() if err == nil && dynamicBidSpread > 0 { s.BidSpread = fixedpoint.NewFromFloat(dynamicBidSpread) - log.Infof("new bid spread: %v", s.BidSpread.Percentage()) + log.Infof("%s dynamic bid spread updated: %s", s.Symbol, s.BidSpread.Percentage()) } dynamicAskSpread, err := s.DynamicSpread.GetAskSpread() if err == nil && dynamicAskSpread > 0 { s.AskSpread = fixedpoint.NewFromFloat(dynamicAskSpread) - log.Infof("new ask spread: %v", s.AskSpread.Percentage()) + log.Infof("%s dynamic ask spread updated: %s", s.Symbol, s.AskSpread.Percentage()) } } @@ -582,7 +566,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } else { s.placeOrders(ctx, kline.Close, &kline) } - }) + })) // s.book = types.NewStreamBook(s.Symbol) // s.book.BindStreamForBackground(session.MarketDataStream) diff --git a/pkg/strategy/rsmaker/strategy.go b/pkg/strategy/rsmaker/strategy.go index 0a860bea0..a9fb5b8ad 100644 --- a/pkg/strategy/rsmaker/strategy.go +++ b/pkg/strategy/rsmaker/strategy.go @@ -121,8 +121,6 @@ type Strategy struct { ProfitStats *types.ProfitStats `persistence:"profit_stats"` TradeStats *types.TradeStats `persistence:"trade_stats"` - bbgo.SmartStops - session *bbgo.ExchangeSession orderExecutor *bbgo.GeneralOrderExecutor book *types.StreamOrderBook @@ -143,15 +141,10 @@ func (s *Strategy) ID() string { return ID } -func (s *Strategy) Initialize() error { - return s.SmartStops.InitializeStopControllers(s.Symbol) -} - func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{ Interval: s.Interval, }) - // s.SmartStops.Subscribe(session) } func (s *Strategy) Validate() error { @@ -430,8 +423,6 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. s.neutralBoll = s.StandardIndicatorSet.BOLL(s.NeutralBollinger.IntervalWindow, s.NeutralBollinger.BandWidth) s.defaultBoll = s.StandardIndicatorSet.BOLL(s.DefaultBollinger.IntervalWindow, s.DefaultBollinger.BandWidth) - // s.SmartStops.RunStopControllers(ctx, session, s.tradeCollector) - var klines []*types.KLine session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { // StrategyController