diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index 437c3f80f..e32af6ace 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -16,7 +16,7 @@ backtest: # for testing max draw down (MDD) at 03-12 # see here for more details # https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp - startTime: "2022-05-01" + startTime: "2022-01-01" endTime: "2022-05-31" sessions: - binance @@ -26,7 +26,7 @@ backtest: binance: balances: ETH: 0.0 - USDT: 100_000.0 + USDT: 10_000.0 exchangeStrategies: @@ -44,13 +44,15 @@ exchangeStrategies: # 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. - pushThreshold: 10% - # popThreshold : 1% + 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)) @@ -110,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. diff --git a/pkg/bbgo/smart_stops.go b/pkg/bbgo/smart_stops.go index ddf2ba505..8555b8065 100644 --- a/pkg/bbgo/smart_stops.go +++ b/pkg/bbgo/smart_stops.go @@ -62,7 +62,7 @@ func (c *TrailingStopController) Run(ctx context.Context, session *ExchangeSessi c.averageCost = c.position.AverageCost // Use trade collector to get the position update event - tradeCollector.OnPositionUpdate(func(position types.PositionInterface) { + tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { // update average cost if we have it. c.averageCost = position.(*types.Position).AverageCost }) diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index 88dd20533..fb79679a4 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.PositionInterface + 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.PositionInterface) + positionUpdateCallbacks []func(position types.AnyPosition) profitCallbacks []func(trade types.Trade, profit, netProfit fixedpoint.Value) } -func NewTradeCollector(symbol string, position types.PositionInterface, 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.PositionInterface { +func (c *TradeCollector) Position() types.AnyPosition { return c.position } diff --git a/pkg/bbgo/tradecollector_callbacks.go b/pkg/bbgo/tradecollector_callbacks.go index 4a1d4255c..5273b2b77 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.PositionInterface)) { +func (c *TradeCollector) OnPositionUpdate(cb func(position types.AnyPosition)) { c.positionUpdateCallbacks = append(c.positionUpdateCallbacks, cb) } -func (c *TradeCollector) EmitPositionUpdate(position types.PositionInterface) { +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 6db8e1119..9483e57b6 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,15 +231,13 @@ type Strategy struct { session *bbgo.ExchangeSession book *types.StreamOrderBook - state *State + state *State + PositionStack PositionStack // persistence fields Position *types.PositionStack `json:"position,omitempty" persistence:"position"` ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"` - PushThreshold fixedpoint.Value `json:"pushThreshold,omitempty"` - PopThreshold fixedpoint.Value `json:"popThreshold,omitempty"` - activeMakerOrders *bbgo.LocalActiveOrderBook orderStore *bbgo.OrderStore tradeCollector *bbgo.TradeCollector @@ -717,7 +721,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se } }) - s.tradeCollector.OnPositionUpdate(func(position types.PositionInterface) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { log.Infof("position changed: %s", s.Position) s.Notify(s.Position) }) @@ -775,19 +779,27 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se if err := s.activeMakerOrders.GracefulCancel(ctx, s.session.Exchange); err != nil { 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.PushThreshold)) > 0 { - log.Errorf("push") - log.Error(s.Position) + //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)) } - // && - if len(s.Position.Stack) > 1 && s.Position.Stack[len(s.Position.Stack)-2].AverageCost.Compare(kline.Close) < 0 && s.Market.IsDustQuantity(s.Position.Position.GetBase(), kline.Close) { - log.Errorf("pop") - log.Error(s.Position) + // 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") diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index aea493531..665b3f974 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.PositionInterface) { + 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 5fb872bcc..384ae6082 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.PositionInterface) { + 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 baf79a264..1986de805 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.PositionInterface) { + 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 29392ddc1..d569644ff 100644 --- a/pkg/strategy/support/strategy.go +++ b/pkg/strategy/support/strategy.go @@ -499,7 +499,7 @@ 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.PositionInterface) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { // StrategyController if s.Status != types.StrategyStatusRunning { return diff --git a/pkg/strategy/wall/strategy.go b/pkg/strategy/wall/strategy.go index a31d6c44d..4635092b4 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.PositionInterface) { + s.tradeCollector.OnPositionUpdate(func(position types.AnyPosition) { log.Infof("position changed: %s", s.Position) s.Notify(s.Position) }) diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index a10a1d4bf..71896ed66 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.PositionInterface) { + 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 ca962db7b..6c138d82e 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -29,8 +29,9 @@ type PositionRisk struct { LiquidationPrice fixedpoint.Value `json:"liquidationPrice"` } -type PositionInterface interface { +type AnyPosition interface { AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) + GetBase() (base fixedpoint.Value) } type Position struct { @@ -480,140 +481,9 @@ func (stack *PositionStack) Pop() *PositionStack { } func NewPositionStackFromMarket(market Market) *PositionStack { - pos := &Position{ - Symbol: market.Symbol, - BaseCurrency: market.BaseCurrency, - QuoteCurrency: market.QuoteCurrency, - Market: market, - TotalFee: make(map[string]fixedpoint.Value), - } + pos := NewPositionFromMarket(market) return &PositionStack{ Position: pos, Stack: []*Position{pos}, } } - -func (p *PositionStack) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) { - price := td.Price - quantity := td.Quantity - quoteQuantity := td.QuoteQuantity - fee := td.Fee - - // calculated fee in quote (some exchange accounts may enable platform currency fee discount, like BNB) - // convert platform fee token into USD values - var feeInQuote fixedpoint.Value = fixedpoint.Zero - - switch td.FeeCurrency { - - case p.BaseCurrency: - quantity = quantity.Sub(fee) - - case p.QuoteCurrency: - quoteQuantity = quoteQuantity.Sub(fee) - - default: - if p.ExchangeFeeRates != nil { - if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok { - if td.IsMaker { - feeInQuote = feeInQuote.Add(exchangeFee.MakerFeeRate.Mul(quoteQuantity)) - } else { - feeInQuote = feeInQuote.Add(exchangeFee.TakerFeeRate.Mul(quoteQuantity)) - } - } - } else if p.FeeRate != nil { - if td.IsMaker { - feeInQuote = feeInQuote.Add(p.FeeRate.MakerFeeRate.Mul(quoteQuantity)) - } else { - feeInQuote = feeInQuote.Add(p.FeeRate.TakerFeeRate.Mul(quoteQuantity)) - } - } - } - - p.Lock() - defer p.Unlock() - - // update changedAt field before we unlock in the defer func - defer func() { - p.ChangedAt = td.Time.Time() - }() - - p.addTradeFee(td) - - // Base > 0 means we're in long position - // Base < 0 means we're in short position - switch td.Side { - - case SideTypeBuy: - if p.Base.Sign() < 0 { - // convert short position to long position - if p.Base.Add(quantity).Sign() > 0 { - profit = p.AverageCost.Sub(price).Mul(p.Base.Neg()) - netProfit = p.ApproximateAverageCost.Sub(price).Mul(p.Base.Neg()).Sub(feeInQuote) - p.Base = p.Base.Add(quantity) - p.Quote = p.Quote.Sub(quoteQuantity) - p.AverageCost = price - p.ApproximateAverageCost = price - p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) - return profit, netProfit, true - } else { - // covering short position - p.Base = p.Base.Add(quantity) - p.Quote = p.Quote.Sub(quoteQuantity) - profit = p.AverageCost.Sub(price).Mul(quantity) - netProfit = p.ApproximateAverageCost.Sub(price).Mul(quantity).Sub(feeInQuote) - p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) - return profit, netProfit, true - } - } - - divisor := p.Base.Add(quantity) - p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base). - Add(quoteQuantity). - Add(feeInQuote). - Div(divisor) - p.AverageCost = p.AverageCost.Mul(p.Base).Add(quoteQuantity).Div(divisor) - p.Base = p.Base.Add(quantity) - p.Quote = p.Quote.Sub(quoteQuantity) - - return fixedpoint.Zero, fixedpoint.Zero, false - - case SideTypeSell: - if p.Base.Sign() > 0 { - // convert long position to short position - if p.Base.Compare(quantity) < 0 { - profit = price.Sub(p.AverageCost).Mul(p.Base) - netProfit = price.Sub(p.ApproximateAverageCost).Mul(p.Base).Sub(feeInQuote) - p.Base = p.Base.Sub(quantity) - p.Quote = p.Quote.Add(quoteQuantity) - p.AverageCost = price - p.ApproximateAverageCost = price - p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) - return profit, netProfit, true - } else { - p.Base = p.Base.Sub(quantity) - p.Quote = p.Quote.Add(quoteQuantity) - profit = price.Sub(p.AverageCost).Mul(quantity) - netProfit = price.Sub(p.ApproximateAverageCost).Mul(quantity).Sub(feeInQuote) - p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) - return profit, netProfit, true - } - } - - // handling short position, since Base here is negative we need to reverse the sign - divisor := quantity.Sub(p.Base) - p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base.Neg()). - Add(quoteQuantity). - Sub(feeInQuote). - Div(divisor) - - p.AverageCost = p.AverageCost.Mul(p.Base.Neg()). - Add(quoteQuantity). - Div(divisor) - p.Base = p.Base.Sub(quantity) - p.Quote = p.Quote.Add(quoteQuantity) - - return fixedpoint.Zero, fixedpoint.Zero, false - } - - return fixedpoint.Zero, fixedpoint.Zero, false -}