From 5cbc6f191fac9950ad8bcad10e4304366deb381b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 13:11:56 +0800 Subject: [PATCH 1/5] grid2: aggregate order fee instead of only base fee --- pkg/strategy/grid2/strategy.go | 13 +++++++++---- pkg/strategy/grid2/strategy_test.go | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 1f356d23e..7d01c0dab 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -330,21 +330,26 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { return true } -// aggregateOrderBaseFee collects the base fee quantity from the given order +// aggregateOrderFee collects the base fee quantity from the given order // it falls back to query the trades via the RESTful API when the websocket trades are not all received. -func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value { +func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { // try to get the received trades (websocket trades) orderTrades := s.historicalTrades.GetOrderTrades(o) if len(orderTrades) > 0 { s.logger.Infof("found filled order trades: %+v", orderTrades) } + feeCurrency := s.Market.BaseCurrency + if o.Side == types.SideTypeSell { + feeCurrency = s.Market.QuoteCurrency + } + for maxTries := maxNumberOfOrderTradesQueryTries; maxTries > 0; maxTries-- { // if one of the trades is missing, we need to query the trades from the RESTful API if s.verifyOrderTrades(o, orderTrades) { // if trades are verified fees := collectTradeFee(orderTrades) - if fee, ok := fees[s.Market.BaseCurrency]; ok { + if fee, ok := fees[feeCurrency]; ok { return fee } return fixedpoint.Zero @@ -417,7 +422,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { // baseSellQuantityReduction calculation should be only for BUY order // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC // if we don't reduce the sell quantity, than we might fail to place the sell order - baseSellQuantityReduction = s.aggregateOrderBaseFee(o) + baseSellQuantityReduction = s.aggregateOrderFee(o) s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 15b2a94f1..bdb3ff222 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -478,7 +478,7 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderBaseFee(types.Order{ + baseFee := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, @@ -865,7 +865,7 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderBaseFee(types.Order{ + baseFee := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, From bd86a8966763af5fca9fdcf7a2af7fffd40ed41b Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 13:13:12 +0800 Subject: [PATCH 2/5] grid2: return fee currency --- pkg/strategy/grid2/strategy.go | 12 ++++++------ pkg/strategy/grid2/strategy_test.go | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 7d01c0dab..2d37496d9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -332,7 +332,7 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { // aggregateOrderFee collects the base fee quantity from the given order // it falls back to query the trades via the RESTful API when the websocket trades are not all received. -func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { +func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { // try to get the received trades (websocket trades) orderTrades := s.historicalTrades.GetOrderTrades(o) if len(orderTrades) > 0 { @@ -350,14 +350,14 @@ func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { // if trades are verified fees := collectTradeFee(orderTrades) if fee, ok := fees[feeCurrency]; ok { - return fee + return fee, "" } - return fixedpoint.Zero + return fixedpoint.Zero, feeCurrency } // if we don't support orderQueryService, then we should just skip if s.orderQueryService == nil { - return fixedpoint.Zero + return fixedpoint.Zero, feeCurrency } s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API") @@ -375,7 +375,7 @@ func (s *Strategy) aggregateOrderFee(o types.Order) fixedpoint.Value { } } - return fixedpoint.Zero + return fixedpoint.Zero, feeCurrency } func (s *Strategy) processFilledOrder(o types.Order) { @@ -422,7 +422,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { // baseSellQuantityReduction calculation should be only for BUY order // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC // if we don't reduce the sell quantity, than we might fail to place the sell order - baseSellQuantityReduction = s.aggregateOrderFee(o) + baseSellQuantityReduction, _ = s.aggregateOrderFee(o) s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction) diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index bdb3ff222..4136e4d87 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -478,7 +478,7 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderFee(types.Order{ + baseFee, _ := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, @@ -865,7 +865,7 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { }, }, nil) - baseFee := s.aggregateOrderFee(types.Order{ + baseFee, _ := s.aggregateOrderFee(types.Order{ SubmitOrder: types.SubmitOrder{ Symbol: "BTCUSDT", Side: types.SideTypeBuy, From 9a89237c241ecf43af64894775dbbe8c370e7578 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 13:51:50 +0800 Subject: [PATCH 3/5] grid2: fix base/quote fee reduction --- pkg/strategy/grid2/strategy.go | 59 ++++++++++++++++++----------- pkg/strategy/grid2/strategy_test.go | 53 ++++++++++++++++++++++---- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2d37496d9..747398a34 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -321,9 +321,9 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { tq := aggregateTradesQuantity(trades) if tq.Compare(o.Quantity) != 0 { - s.logger.Warnf("order trades missing. expected: %f actual: %f", - o.Quantity.Float64(), - tq.Float64()) + s.logger.Warnf("order trades missing. expected: %s got: %s", + o.Quantity.String(), + tq.String()) return false } @@ -350,7 +350,7 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { // if trades are verified fees := collectTradeFee(orderTrades) if fee, ok := fees[feeCurrency]; ok { - return fee, "" + return fee, feeCurrency } return fixedpoint.Zero, feeCurrency } @@ -394,7 +394,24 @@ func (s *Strategy) processFilledOrder(o types.Order) { orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice) // collect trades - baseSellQuantityReduction := fixedpoint.Zero + feeQuantityReduction := fixedpoint.Zero + feeCurrency := "" + feePrec := 2 + + // feeQuantityReduction calculation is used to reduce the order quantity + // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC + // if we don't reduce the sell quantity, than we might fail to place the sell order + feeQuantityReduction, feeCurrency = s.aggregateOrderFee(o) + s.logger.Infof("GRID ORDER #%d %s FEE: %s %s", + o.OrderID, o.Side, + feeQuantityReduction.String(), feeCurrency) + + feeQuantityReduction, feePrec = roundUpMarketQuantity(s.Market, feeQuantityReduction, feeCurrency) + s.logger.Infof("GRID ORDER #%d %s FEE (rounding precision %d): %s %s", + o.OrderID, o.Side, + feePrec, + feeQuantityReduction.String(), + feeCurrency) switch o.Side { case types.SideTypeSell: @@ -410,6 +427,11 @@ func (s *Strategy) processFilledOrder(o types.Order) { // use the profit to buy more inventory in the grid if s.Compound || s.EarnBase { + // if it's not using the platform fee currency, reduce the quote quantity for the buy order + if feeCurrency == s.Market.QuoteCurrency { + orderExecutedQuoteAmount = orderExecutedQuoteAmount.Sub(feeQuantityReduction) + } + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice), s.Market.MinQuantity) } else if s.QuantityOrAmount.Quantity.Sign() > 0 { newQuantity = s.QuantityOrAmount.Quantity @@ -419,19 +441,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { profit = s.calculateProfit(o, newPrice, newQuantity) case types.SideTypeBuy: - // baseSellQuantityReduction calculation should be only for BUY order - // because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC - // if we don't reduce the sell quantity, than we might fail to place the sell order - baseSellQuantityReduction, _ = s.aggregateOrderFee(o) - s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency) - - baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction) - s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding with precision %d): %s %s", - s.Market.VolumePrecision, - baseSellQuantityReduction.String(), - s.Market.BaseCurrency) - - newQuantity = newQuantity.Sub(baseSellQuantityReduction) + newQuantity = newQuantity.Sub(feeQuantityReduction) newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { @@ -443,7 +453,7 @@ func (s *Strategy) processFilledOrder(o types.Order) { } if s.EarnBase { - newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity) + newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(feeQuantityReduction), s.Market.MinQuantity) } } @@ -1794,6 +1804,11 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha return false, nil } -func roundUpMarketQuantity(market types.Market, v fixedpoint.Value) fixedpoint.Value { - return v.Round(market.VolumePrecision, fixedpoint.Up) -} \ No newline at end of file +func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (fixedpoint.Value, int) { + prec := market.VolumePrecision + if c == market.QuoteCurrency { + prec = market.PricePrecision + } + + return v.Round(prec, fixedpoint.Up), prec +} diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 4136e4d87..a1f65d418 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -646,6 +646,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { defer mockCtrl.Finish() mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ Symbol: "BTCUSDT", OrderID: "1", @@ -655,14 +656,32 @@ func TestStrategy_handleOrderFilled(t *testing.T) { OrderID: orderID, Exchange: "binance", Price: number(11000.0), - Quantity: gridQuantity, + Quantity: number("0.1"), Symbol: "BTCUSDT", Side: types.SideTypeBuy, IsBuyer: true, FeeCurrency: "BTC", Fee: fixedpoint.Zero, }, - }, nil) + }, nil).Times(1) + + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "2", + }).Return([]types.Trade{ + { + ID: 2, + OrderID: orderID, + Exchange: "binance", + Price: number(12000.0), + Quantity: number(0.09166666666), + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + IsBuyer: true, + FeeCurrency: "BTC", + Fee: fixedpoint.Zero, + }, + }, nil).Times(1) s.orderQueryService = mockService @@ -735,6 +754,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { defer mockCtrl.Finish() mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl) + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ Symbol: "BTCUSDT", OrderID: "1", @@ -749,7 +769,25 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Side: types.SideTypeBuy, IsBuyer: true, FeeCurrency: "BTC", - Fee: fixedpoint.Zero, + Fee: number("0.00001"), + }, + }, nil) + + mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{ + Symbol: "BTCUSDT", + OrderID: "2", + }).Return([]types.Trade{ + { + ID: 2, + OrderID: orderID, + Exchange: "binance", + Price: number(12000.0), + Quantity: gridQuantity, + Symbol: "BTCUSDT", + Side: types.SideTypeSell, + IsBuyer: true, + FeeCurrency: "USDT", + Fee: number("0.01"), }, }, nil) @@ -759,7 +797,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Symbol: "BTCUSDT", Type: types.OrderTypeLimit, Price: number(12_000.0), - Quantity: gridQuantity, + Quantity: number(0.09998999), Side: types.SideTypeSell, TimeInForce: types.TimeInForceGTC, Market: s.Market, @@ -775,7 +813,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Symbol: "BTCUSDT", Type: types.OrderTypeLimit, Price: number(11_000.0), - Quantity: number(0.1090909), + Quantity: number(0.10909), Side: types.SideTypeBuy, TimeInForce: types.TimeInForceGTC, Market: s.Market, @@ -937,8 +975,9 @@ func Test_roundUpMarketQuantity(t *testing.T) { q := number("0.00000003") assert.Equal(t, "0.00000003", q.String()) - q3 := roundUpMarketQuantity(types.Market{ + q3, prec := roundUpMarketQuantity(types.Market{ VolumePrecision: 8, - }, q) + }, q, "BTC") assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8") + assert.Equal(t, 8, prec) } From ca741f91eb62f3306cc392a7fca0dca6d318ee91 Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 14:18:37 +0800 Subject: [PATCH 4/5] grid2: add fee currency check for buy order --- pkg/strategy/grid2/strategy.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 747398a34..6fb5303f2 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -386,9 +386,12 @@ func (s *Strategy) processFilledOrder(o types.Order) { newPrice := o.Price newQuantity := o.Quantity executedPrice := o.Price - if o.AveragePrice.Sign() > 0 { - executedPrice = o.AveragePrice - } + + /* + if o.AveragePrice.Sign() > 0 { + executedPrice = o.AveragePrice + } + */ // will be used for calculating quantity orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice) @@ -441,7 +444,9 @@ func (s *Strategy) processFilledOrder(o types.Order) { profit = s.calculateProfit(o, newPrice, newQuantity) case types.SideTypeBuy: - newQuantity = newQuantity.Sub(feeQuantityReduction) + if feeCurrency == s.Market.BaseCurrency { + newQuantity = newQuantity.Sub(feeQuantityReduction) + } newSide = types.SideTypeSell if !s.ProfitSpread.IsZero() { From bf4553d767b3d77debe1311f76db68b7a87c945e Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 3 Mar 2023 14:10:34 +0800 Subject: [PATCH 5/5] grid2: add OrderFillDelay option --- pkg/strategy/grid2/strategy.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 6fb5303f2..e490980d9 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -143,6 +143,8 @@ type Strategy struct { // StopIfLessThanMinimalQuoteInvestment stops the strategy if the quote investment does not match StopIfLessThanMinimalQuoteInvestment bool `json:"stopIfLessThanMinimalQuoteInvestment"` + OrderFillDelay types.Duration `json:"orderFillDelay"` + // PrometheusLabels will be used as the base prometheus labels PrometheusLabels prometheus.Labels `json:"prometheusLabels"` @@ -785,6 +787,10 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.ExchangeSession) func(o types.Order) { return func(o types.Order) { + if s.OrderFillDelay > 0 { + time.Sleep(s.OrderFillDelay.Duration()) + } + s.handleOrderFilled(o) // sync the profits to redis