diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index f4b253d3f6..e32af6ace0 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-05-31" sessions: - binance symbols: @@ -38,12 +38,21 @@ exchangeStrategies: interval: 1m # quantity is the base order quantity for your buy/sell order. - quantity: 0.05 + # quantity: 0.05 + amount: 20 + + # Position Stack, with longer stack length, may need more capital. + # Push position in stack is initiating a position to calculate base, average cost, etc. + # Pop position in stack is loading a previous position back. + positionStack: + enabled: true + pushThreshold: 25% + popThreshold: 5% # useTickerPrice use the ticker api to get the mid price instead of the closed kline price. # The back-test engine is kline-based, so the ticker price api is not supported. # Turn this on if you want to do real trading. - useTickerPrice: true + useTickerPrice: false # spread is the price spread from the middle price. # For ask orders, the ask price is ((bestAsk + bestBid) / 2 * (1.0 + spread)) @@ -103,7 +112,7 @@ exchangeStrategies: domain: [ -1, 1 ] # when in down band, holds 1.0 by maximum # when in up band, holds 0.05 by maximum - range: [ 10.0, 1.0 ] + range: [ 3.0, 0.5] # DisableShort means you can don't want short position during the market making # THe short here means you might sell some of your existing inventory. @@ -136,25 +145,25 @@ exchangeStrategies: # Set up your stop order, this is optional # sometimes the stop order might decrease your total profit. # you can setup multiple stop, - stops: +# stops: # use trailing stop order - - trailingStop: - # callbackRate: when the price reaches -1% from the previous highest, we trigger the stop - callbackRate: 5.1% - - # closePosition is how much position do you want to close - closePosition: 20% - - # 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% - - # 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 +# - trailingStop: +# # callbackRate: when the price reaches -1% from the previous highest, we trigger the stop +# callbackRate: 5.1% +# +# # closePosition is how much position do you want to close +# closePosition: 20% +# +# # 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% +# +# # 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 diff --git a/pkg/bbgo/smart_stops.go b/pkg/bbgo/smart_stops.go index 7a17e1e38e..8555b80651 100644 --- a/pkg/bbgo/smart_stops.go +++ b/pkg/bbgo/smart_stops.go @@ -58,13 +58,13 @@ func (c *TrailingStopController) Subscribe(session *ExchangeSession) { func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSession, tradeCollector *TradeCollector) { // store the position - c.position = tradeCollector.Position() + c.position = tradeCollector.Position().(*types.Position) c.averageCost = c.position.AverageCost // Use trade collector to get the position update event - tradeCollector.OnPositionUpdate(func(position *types.Position) { + tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { // update average cost if we have it. - c.averageCost = position.AverageCost + c.averageCost = position.(*types.Position).AverageCost }) session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index c648ec48c3..fb79679a46 100644 --- a/pkg/bbgo/tradecollector.go +++ b/pkg/bbgo/tradecollector.go @@ -18,17 +18,17 @@ type TradeCollector struct { tradeStore *TradeStore tradeC chan types.Trade - position *types.Position + position types.AnyPosition orderStore *OrderStore doneTrades map[types.TradeKey]struct{} recoverCallbacks []func(trade types.Trade) tradeCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) - positionUpdateCallbacks []func(position *types.Position) + positionUpdateCallbacks []func(position types.AnyPosition) profitCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) } -func NewTradeCollector(symbol string, position *types.Position, orderStore *OrderStore) *TradeCollector { +func NewTradeCollector(symbol string, position types.AnyPosition, orderStore *OrderStore) *TradeCollector { return &TradeCollector{ Symbol: symbol, orderSig: sigchan.New(1), @@ -47,7 +47,7 @@ func (c *TradeCollector) OrderStore() *OrderStore { } // Position returns the position used by the trade collector -func (c *TradeCollector) Position() *types.Position { +func (c *TradeCollector) Position() types.AnyPosition { return c.position } diff --git a/pkg/bbgo/tradecollector_callbacks.go b/pkg/bbgo/tradecollector_callbacks.go index af8bf1bd13..5273b2b77f 100644 --- a/pkg/bbgo/tradecollector_callbacks.go +++ b/pkg/bbgo/tradecollector_callbacks.go @@ -27,11 +27,11 @@ func (c *TradeCollector) EmitTrade(trade types.Trade, profit fixedpoint.Value, n } } -func (c *TradeCollector) OnPositionUpdate(cb func(position *types.Position)) { +func (c *TradeCollector) OnPositionUpdate(cb func(position types.AnyPosition)) { c.positionUpdateCallbacks = append(c.positionUpdateCallbacks, cb) } -func (c *TradeCollector) EmitPositionUpdate(position *types.Position) { +func (c *TradeCollector) EmitPositionUpdate(position types.AnyPosition) { for _, cb := range c.positionUpdateCallbacks { cb(position) } diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 038f2c9015..9483e57b64 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -44,6 +44,12 @@ type State struct { ProfitStats types.ProfitStats `json:"profitStats,omitempty"` } +type PositionStack struct { + Enabled bool `json:"enabled,omitempty"` + PushThreshold fixedpoint.Value `json:"pushThreshold,omitempty"` + PopThreshold fixedpoint.Value `json:"popThreshold,omitempty"` +} + type BollingerSetting struct { types.IntervalWindow BandWidth float64 `json:"bandWidth"` @@ -225,11 +231,12 @@ type Strategy struct { session *bbgo.ExchangeSession book *types.StreamOrderBook - state *State + state *State + PositionStack PositionStack // persistence fields - Position *types.Position `json:"position,omitempty" persistence:"position"` - ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` + Position *types.PositionStack `json:"position,omitempty" persistence:"position"` + ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` activeMakerOrders *bbgo.LocalActiveOrderBook orderStore *bbgo.OrderStore @@ -289,7 +296,7 @@ func (s *Strategy) Validate() error { return nil } -func (s *Strategy) CurrentPosition() *types.Position { +func (s *Strategy) CurrentPosition() *types.PositionStack { return s.Position } @@ -652,9 +659,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if s.Position == nil { // fallback to the legacy position struct in the state if s.state != nil && s.state.Position != nil { - s.Position = s.state.Position + s.Position.Position = s.state.Position } else { - s.Position = types.NewPositionFromMarket(s.Market) + s.Position = types.NewPositionStackFromMarket(s.Market) } } @@ -699,7 +706,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ProfitStats.AddTrade(trade) if profit.Compare(fixedpoint.Zero) == 0 { - s.Environment.RecordPosition(s.Position, trade, nil) + s.Environment.RecordPosition(s.Position.Position, trade, nil) } else { log.Infof("%s generated profit: %v", s.Symbol, profit) p := s.Position.NewProfit(trade, profit, netProfit) @@ -710,11 +717,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.ProfitStats.AddProfit(p) s.Notify(&s.ProfitStats) - s.Environment.RecordPosition(s.Position, trade, &p) + s.Environment.RecordPosition(s.Position.Position, trade, &p) } }) - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { log.Infof("position changed: %s", s.Position) s.Notify(s.Position) }) @@ -773,6 +780,32 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Errorf("graceful cancel order error") } + //log.Error(len(s.Position.Stack), s.Position.AverageCost, kline.Close) + if s.Position.Position.AverageCost.Div(kline.Close).Compare(fixedpoint.One.Add(s.PositionStack.PushThreshold)) > 0 { + log.Infof("push position %s", s.Position) + s.Position = s.Position.Push(types.NewPositionFromMarket(s.Market)) + } + // make it dust naturally by bollmaker + if len(s.Position.Stack) > 1 && s.Position.Stack[len(s.Position.Stack)-2].AverageCost.Compare(kline.Close) < 0 && s.Market.IsDustQuantity(s.Position.GetBase(), kline.Close) { + log.Infof("pop position %s", s.Position) + s.Position = s.Position.Pop() + } + // make it dust by TP + if !s.PositionStack.PopThreshold.IsZero() { + if len(s.Position.Stack) > 1 && s.Position.Stack[len(s.Position.Stack)-2].AverageCost.Compare(kline.Close) < 0 && s.Position.AverageCost.Div(kline.Close).Compare(fixedpoint.One.Sub(s.PositionStack.PopThreshold)) < 0 { + s.ClosePosition(ctx, fixedpoint.One) + log.Infof("pop position %s", s.Position) + log.Error("pop position") + s.Position = s.Position.Pop() + } + } + + //if s.Position.AverageCost.Div(kline.Close).Compare(fixedpoint.One.Sub(s.PopThreshold)) < 0 && && !s.Position.AverageCost.IsZero() { + // //log.Error(len(s.Position.Stack), s.Position.AverageCost, kline.Close) + // log.Errorf("pop") + // s.ClosePosition(ctx, fixedpoint.One) + // s.Position = s.Position.Pop() + //} // check if there is a canceled order had partially filled. s.tradeCollector.Process() diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 4c3defc070..665b3f9740 100644 --- a/pkg/strategy/ewoDgtrd/strategy.go +++ b/pkg/strategy/ewoDgtrd/strategy.go @@ -834,7 +834,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { log.Infof("position changed: %s", position) s.Notify(s.Position) }) diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index af5a2bcab3..384ae6082a 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -620,7 +620,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } */ - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { s.Notifiability.Notify(position) }) s.tradeCollector.BindStream(session.UserDataStream) diff --git a/pkg/strategy/pivotshort/strategy.go b/pkg/strategy/pivotshort/strategy.go index 6976ee4d8e..1986de805a 100644 --- a/pkg/strategy/pivotshort/strategy.go +++ b/pkg/strategy/pivotshort/strategy.go @@ -179,7 +179,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { log.Infof("position changed: %s", s.Position) s.Notify(s.Position) }) diff --git a/pkg/strategy/support/strategy.go b/pkg/strategy/support/strategy.go index 83768c56b3..d569644ffa 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -499,13 +499,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if !s.TrailingStopTarget.TrailingStopCallbackRatio.IsZero() { // Update trailing stop when the position changes - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { // StrategyController if s.Status != types.StrategyStatusRunning { return } - if position.Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position + if position.(*types.Position).Base.Compare(s.Market.MinQuantity) > 0 { // Update order if we have a position // Cancel the original order if err := s.cancelOrder(s.trailingStopControl.OrderID, ctx, orderExecutor); err != nil { log.WithError(err).Errorf("Can not cancel the original trailing stop order!") @@ -515,12 +515,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se // Calculate minimum target price var minTargetPrice = fixedpoint.Zero if s.trailingStopControl.minimumProfitPercentage.Sign() > 0 { - minTargetPrice = position.AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) + minTargetPrice = position.(*types.Position).AverageCost.Mul(fixedpoint.One.Add(s.trailingStopControl.minimumProfitPercentage)) } // Place new order if the target price is higher than the minimum target price if s.trailingStopControl.IsHigherThanMin(minTargetPrice) { - orderForm := s.trailingStopControl.GenerateStopOrder(position.Base) + orderForm := s.trailingStopControl.GenerateStopOrder(position.(*types.Position).Base) orders, err := s.submitOrders(ctx, orderExecutor, orderForm) if err != nil { log.WithError(err).Error("submit profit trailing stop order error") diff --git a/pkg/strategy/wall/strategy.go b/pkg/strategy/wall/strategy.go index 7fed0615cb..4635092b41 100644 --- a/pkg/strategy/wall/strategy.go +++ b/pkg/strategy/wall/strategy.go @@ -302,7 +302,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { log.Infof("position changed: %s", s.Position) s.Notify(s.Position) }) @@ -340,7 +340,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Errorf("can not place order") } - if err := s.activeAdjustmentOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { log.WithError(err).Errorf("graceful cancel order error") } diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index 53ae10fdec..71896ed66e 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -781,7 +781,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order } }) - s.tradeCollector.OnPositionUpdate(func(position *types.Position) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { s.Notifiability.Notify(position) }) s.tradeCollector.OnRecover(func(trade types.Trade) { diff --git a/pkg/types/position.go b/pkg/types/position.go index c02c358ac1..6c138d82e4 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -29,6 +29,11 @@ type PositionRisk struct { LiquidationPrice fixedpoint.Value `json:"liquidationPrice"` } +type AnyPosition interface { + AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) + GetBase() (base fixedpoint.Value) +} + type Position struct { Symbol string `json:"symbol" db:"symbol"` BaseCurrency string `json:"baseCurrency" db:"base"` @@ -453,3 +458,32 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp return fixedpoint.Zero, fixedpoint.Zero, false } + +type PositionStack struct { + *Position + Stack []*Position +} + +func (stack *PositionStack) Push(pos *Position) *PositionStack { + stack.Position = pos + stack.Stack = append(stack.Stack, pos) + return stack + +} + +func (stack *PositionStack) Pop() *PositionStack { + if len(stack.Stack) < 1 { + return nil + } + stack.Position = stack.Stack[len(stack.Stack)-1] + stack.Stack = stack.Stack[:len(stack.Stack)-1] + return stack +} + +func NewPositionStackFromMarket(market Market) *PositionStack { + pos := NewPositionFromMarket(market) + return &PositionStack{ + Position: pos, + Stack: []*Position{pos}, + } +}