From a5e1ae48674c7437d295e073fe7e353aee32e10d Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 30 May 2022 15:15:44 +0800 Subject: [PATCH 1/5] bbgo: refactor trade collector using position interface --- pkg/bbgo/tradecollector.go | 8 +- pkg/bbgo/tradecollector_callbacks.go | 4 +- pkg/types/position.go | 164 +++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 6 deletions(-) diff --git a/pkg/bbgo/tradecollector.go b/pkg/bbgo/tradecollector.go index c648ec48c3..88dd205331 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.PositionInterface 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.PositionInterface) 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.PositionInterface, 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.PositionInterface { return c.position } diff --git a/pkg/bbgo/tradecollector_callbacks.go b/pkg/bbgo/tradecollector_callbacks.go index af8bf1bd13..4a1d4255c4 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.PositionInterface)) { c.positionUpdateCallbacks = append(c.positionUpdateCallbacks, cb) } -func (c *TradeCollector) EmitPositionUpdate(position *types.Position) { +func (c *TradeCollector) EmitPositionUpdate(position types.PositionInterface) { for _, cb := range c.positionUpdateCallbacks { cb(position) } diff --git a/pkg/types/position.go b/pkg/types/position.go index c02c358ac1..ca962db7bc 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -29,6 +29,10 @@ type PositionRisk struct { LiquidationPrice fixedpoint.Value `json:"liquidationPrice"` } +type PositionInterface interface { + AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) +} + type Position struct { Symbol string `json:"symbol" db:"symbol"` BaseCurrency string `json:"baseCurrency" db:"base"` @@ -453,3 +457,163 @@ 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 := &Position{ + Symbol: market.Symbol, + BaseCurrency: market.BaseCurrency, + QuoteCurrency: market.QuoteCurrency, + Market: market, + TotalFee: make(map[string]fixedpoint.Value), + } + 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 +} From ae17e0ddbe224dd9bc0ff8582b972c1a1eb0d9c2 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 30 May 2022 15:16:52 +0800 Subject: [PATCH 2/5] bbgo: refactor trailing stop using position interface --- pkg/bbgo/smart_stops.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/bbgo/smart_stops.go b/pkg/bbgo/smart_stops.go index 7a17e1e38e..ddf2ba5051 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.PositionInterface) { // update average cost if we have it. - c.averageCost = position.AverageCost + c.averageCost = position.(*types.Position).AverageCost }) session.MarketDataStream.OnKLineClosed(func(kline types.KLine) { From a3ca8326f2ddae90de17821bce8cd4f5bc8b2a00 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 30 May 2022 15:18:50 +0800 Subject: [PATCH 3/5] strategy: refactor strategies using position interface --- pkg/strategy/ewoDgtrd/strategy.go | 2 +- pkg/strategy/grid/strategy.go | 2 +- pkg/strategy/pivotshort/strategy.go | 2 +- pkg/strategy/support/strategy.go | 8 ++++---- pkg/strategy/wall/strategy.go | 3 +-- pkg/strategy/xmaker/strategy.go | 2 +- 6 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/ewoDgtrd/strategy.go b/pkg/strategy/ewoDgtrd/strategy.go index 4c3defc070..aea4935311 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.PositionInterface) { 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..5fb872bcc7 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.PositionInterface) { 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..baf79a2643 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.PositionInterface) { 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..29392ddc18 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.PositionInterface) { // 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..a31d6c44dc 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.PositionInterface) { 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..a10a1d4bf8 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.PositionInterface) { s.Notifiability.Notify(position) }) s.tradeCollector.OnRecover(func(trade types.Trade) { From 54d60b98907c7d08ce15146df8f36e789b1aaba5 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 30 May 2022 15:20:07 +0800 Subject: [PATCH 4/5] bollmaker: add position stack --- config/bollmaker.yaml | 57 +++++++++++++++++------------- pkg/strategy/bollmaker/strategy.go | 37 ++++++++++++++----- 2 files changed, 61 insertions(+), 33 deletions(-) diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index f4b253d3f6..437c3f80f4 100644 --- a/config/bollmaker.yaml +++ b/config/bollmaker.yaml @@ -16,8 +16,8 @@ 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-01-01" - endTime: "2022-05-12" + startTime: "2022-05-01" + endTime: "2022-05-31" sessions: - binance symbols: @@ -26,7 +26,7 @@ backtest: binance: balances: ETH: 0.0 - USDT: 10_000.0 + USDT: 100_000.0 exchangeStrategies: @@ -38,7 +38,14 @@ 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. + pushThreshold: 10% + # popThreshold : 1% # 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. @@ -103,7 +110,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: [10.0, 1.0 ] # 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 +143,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/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index 038f2c9015..6db8e11191 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -228,8 +228,11 @@ type Strategy struct { state *State // 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"` + + PushThreshold fixedpoint.Value `json:"pushThreshold,omitempty"` + PopThreshold fixedpoint.Value `json:"popThreshold,omitempty"` activeMakerOrders *bbgo.LocalActiveOrderBook orderStore *bbgo.OrderStore @@ -289,7 +292,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 +655,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 +702,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 +713,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.PositionInterface) { log.Infof("position changed: %s", s.Position) s.Notify(s.Position) }) @@ -772,7 +775,25 @@ 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) + 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) + 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() From 3f939461cf3fec80b6d3195c6972fdc7d62ede24 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 30 May 2022 18:16:20 +0800 Subject: [PATCH 5/5] bollmaker: clean up position stack --- config/bollmaker.yaml | 14 +-- pkg/bbgo/smart_stops.go | 2 +- pkg/bbgo/tradecollector.go | 8 +- pkg/bbgo/tradecollector_callbacks.go | 4 +- pkg/strategy/bollmaker/strategy.go | 38 +++++--- pkg/strategy/ewoDgtrd/strategy.go | 2 +- pkg/strategy/grid/strategy.go | 2 +- pkg/strategy/pivotshort/strategy.go | 2 +- pkg/strategy/support/strategy.go | 2 +- pkg/strategy/wall/strategy.go | 2 +- pkg/strategy/xmaker/strategy.go | 2 +- pkg/types/position.go | 136 +-------------------------- 12 files changed, 49 insertions(+), 165 deletions(-) diff --git a/config/bollmaker.yaml b/config/bollmaker.yaml index 437c3f80f4..e32af6ace0 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 ddf2ba5051..8555b80651 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 88dd205331..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.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 4a1d4255c4..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.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 6db8e11191..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,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 aea4935311..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.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 5fb872bcc7..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.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 baf79a2643..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.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 29392ddc18..d569644ffa 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 a31d6c44dc..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.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 a10a1d4bf8..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.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 ca962db7bc..6c138d82e4 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 -}