diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index ce3f77dc2..0c8248d93 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -796,6 +796,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity( if numberOfSellOrders > 0 { numberOfSellOrders-- } + + s.logger.Infof("calculated number of sell orders: %d", numberOfSellOrders) } // if the maxBaseQuantity is less than minQuantity, then we need to reduce the number of the sell orders @@ -810,8 +812,12 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity( s.Market.MinQuantity) if baseQuantity.Compare(minBaseQuantity) <= 0 { + s.logger.Infof("base quantity %s is less than min base quantity: %s, adjusting...", baseQuantity.String(), minBaseQuantity.String()) + baseQuantity = s.Market.RoundUpQuantityByPrecision(minBaseQuantity) numberOfSellOrders = int(math.Floor(baseInvestment.Div(baseQuantity).Float64())) + + s.logger.Infof("adjusted base quantity to %s", baseQuantity.String()) } s.logger.Infof("grid base investment sell orders: %d", numberOfSellOrders) @@ -824,7 +830,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity( // quoteInvestment = (p1 + p2 + p3) * q // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) si := -1 - for i := len(pins) - 1 - numberOfSellOrders; i >= 0; i-- { + end := len(pins) - 1 + for i := end - numberOfSellOrders - 1; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) @@ -844,6 +851,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity( // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) } + } else { // for orders that buy if s.ProfitSpread.IsZero() && i+1 == si { @@ -851,7 +859,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity( } // should never place a buy order at the upper price - if i == len(pins)-1 { + if i == end { continue } @@ -859,8 +867,11 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity( } } + s.logger.Infof("total quote price: %f", totalQuotePrice.Float64()) if totalQuotePrice.Sign() > 0 && quoteInvestment.Sign() > 0 { quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) + + s.logger.Infof("quote side quantity: %f = %f / %f", quoteSideQuantity.Float64(), quoteInvestment.Float64(), totalQuotePrice.Float64()) if numberOfSellOrders > 0 { return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil } @@ -1058,6 +1069,11 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return err2 } + if s.BaseGridNum > 0 { + sell1 := fixedpoint.Value(s.grid.Pins[len(s.grid.Pins)-1-s.BaseGridNum]) + lastPrice = sell1.Sub(s.Market.TickSize) + } + // check if base and quote are enough var totalBase = fixedpoint.Zero var totalQuote = fixedpoint.Zero @@ -1432,6 +1448,8 @@ func calculateMinimalQuoteInvestment(market types.Market, grid *Grid) fixedpoint for i := len(pins) - 2; i >= 0; i-- { pin := pins[i] price := fixedpoint.Value(pin) + + // TODO: should we round the quote here before adding? totalQuote = totalQuote.Add(price.Mul(minQuantity)) } diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index 9f592959f..70d6f97ef 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -204,6 +204,123 @@ func TestStrategy_generateGridOrders(t *testing.T) { }, orders) }) + t.Run("base and quote with predefined base grid num", func(t *testing.T) { + gridNum := int64(22) + upperPrice := number(35500.000000) + lowerPrice := number(34450.000000) + quoteInvestment := number(18.47) + baseInvestment := number(0.010700) + lastPrice := number(34522.930000) + baseGridNum := int(20) + + s := newTestStrategy() + s.GridNum = gridNum + s.BaseGridNum = baseGridNum + s.LowerPrice = lowerPrice + s.UpperPrice = upperPrice + s.grid = NewGrid(lowerPrice, upperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + assert.Equal(t, 22, len(s.grid.Pins)) + + quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, s.grid.Pins) + assert.NoError(t, err) + assert.Equal(t, "0.000535", quantity.String()) + + s.QuantityOrAmount.Quantity = quantity + + orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 21, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(35500.0), types.SideTypeSell}, + {number(35450.0), types.SideTypeSell}, + {number(35400.0), types.SideTypeSell}, + {number(35350.0), types.SideTypeSell}, + {number(35300.0), types.SideTypeSell}, + {number(35250.0), types.SideTypeSell}, + {number(35200.0), types.SideTypeSell}, + {number(35150.0), types.SideTypeSell}, + {number(35100.0), types.SideTypeSell}, + {number(35050.0), types.SideTypeSell}, + {number(35000.0), types.SideTypeSell}, + {number(34950.0), types.SideTypeSell}, + {number(34900.0), types.SideTypeSell}, + {number(34850.0), types.SideTypeSell}, + {number(34800.0), types.SideTypeSell}, + {number(34750.0), types.SideTypeSell}, + {number(34700.0), types.SideTypeSell}, + {number(34650.0), types.SideTypeSell}, + {number(34600.0), types.SideTypeSell}, + {number(34550.0), types.SideTypeSell}, + // -- fake trade price at 34549.9 + // -- 34500 should be empty + {number(34450.0), types.SideTypeBuy}, + }, orders) + }) + + t.Run("base and quote", func(t *testing.T) { + gridNum := int64(22) + upperPrice := number(35500.000000) + lowerPrice := number(34450.000000) + quoteInvestment := number(20.0) + baseInvestment := number(0.010700) + lastPrice := number(34522.930000) + baseGridNum := int(0) + + s := newTestStrategy() + s.GridNum = gridNum + s.BaseGridNum = baseGridNum + s.LowerPrice = lowerPrice + s.UpperPrice = upperPrice + s.grid = NewGrid(lowerPrice, upperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) + s.grid.CalculateArithmeticPins() + assert.Equal(t, 22, len(s.grid.Pins)) + + quantity, err := s.calculateBaseQuoteInvestmentQuantity(quoteInvestment, baseInvestment, lastPrice, s.grid.Pins) + assert.NoError(t, err) + assert.Equal(t, "0.00029006", quantity.String()) + + s.QuantityOrAmount.Quantity = quantity + + orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice) + assert.NoError(t, err) + if !assert.Equal(t, 21, len(orders)) { + for _, o := range orders { + t.Logf("- %s %s", o.Price.String(), o.Side) + } + } + + assertPriceSide(t, []PriceSideAssert{ + {number(35500.0), types.SideTypeSell}, + {number(35450.0), types.SideTypeSell}, + {number(35400.0), types.SideTypeSell}, + {number(35350.0), types.SideTypeSell}, + {number(35300.0), types.SideTypeSell}, + {number(35250.0), types.SideTypeSell}, + {number(35200.0), types.SideTypeSell}, + {number(35150.0), types.SideTypeSell}, + {number(35100.0), types.SideTypeSell}, + {number(35050.0), types.SideTypeSell}, + {number(35000.0), types.SideTypeSell}, + {number(34950.0), types.SideTypeSell}, + {number(34900.0), types.SideTypeSell}, + {number(34850.0), types.SideTypeSell}, + {number(34800.0), types.SideTypeSell}, + {number(34750.0), types.SideTypeSell}, + {number(34700.0), types.SideTypeSell}, + {number(34650.0), types.SideTypeSell}, + {number(34600.0), types.SideTypeSell}, + {number(34550.0), types.SideTypeSell}, + // -- 34500 should be empty + {number(34450.0), types.SideTypeBuy}, + }, orders) + }) + t.Run("base and quote with pre-calculated baseGridNumber", func(t *testing.T) { s := newTestStrategy() s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) @@ -519,11 +636,11 @@ func newTestMarket(symbol string) types.Market { BaseCurrency: "BTC", QuoteCurrency: "USDT", TickSize: number(0.01), - StepSize: number(0.00001), + StepSize: number(0.000001), PricePrecision: 2, VolumePrecision: 8, - MinNotional: number(10.0), - MinQuantity: number(0.001), + MinNotional: number(8.0), + MinQuantity: number(0.0003), } case "ETHUSDT": return types.Market{ @@ -534,7 +651,7 @@ func newTestMarket(symbol string) types.Market { PricePrecision: 2, VolumePrecision: 6, MinNotional: number(8.000), - MinQuantity: number(0.00030), + MinQuantity: number(0.0046), } } @@ -577,12 +694,17 @@ func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.O } } -func newTestStrategy() *Strategy { - market := newTestMarket("BTCUSDT") +func newTestStrategy(va ...string) *Strategy { + symbol := "BTCUSDT" + if len(va) > 0 { + symbol = va[0] + } + + market := newTestMarket(symbol) s := &Strategy{ logger: logrus.NewEntry(logrus.New()), - Symbol: "BTCUSDT", + Symbol: symbol, Market: market, GridProfitStats: newGridProfitStats(market), UpperPrice: number(20_000), @@ -790,7 +912,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) { } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) - orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func( + ctx context.Context, order types.SubmitOrder, + ) (types.OrderSlice, error) { assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) return []types.Order{ {SubmitOrder: expectedSubmitOrder}, @@ -858,7 +982,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) { } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) - orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func( + ctx context.Context, order types.SubmitOrder, + ) (types.OrderSlice, error) { assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) return []types.Order{ {SubmitOrder: expectedSubmitOrder}, @@ -946,7 +1072,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Market: s.Market, Tag: orderTag, } - orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func( + ctx context.Context, order types.SubmitOrder, + ) (types.OrderSlice, error) { assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) return []types.Order{ {SubmitOrder: expectedSubmitOrder}, @@ -963,7 +1091,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Market: s.Market, Tag: orderTag, } - orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func( + ctx context.Context, order types.SubmitOrder, + ) (types.OrderSlice, error) { assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2) return []types.Order{ {SubmitOrder: expectedSubmitOrder2}, @@ -1060,7 +1190,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) { } orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) - orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func( + ctx context.Context, order types.SubmitOrder, + ) (types.OrderSlice, error) { assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder) return []types.Order{ {SubmitOrder: expectedSubmitOrder}, @@ -1078,7 +1210,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) { Tag: orderTag, } - orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, order types.SubmitOrder) (types.OrderSlice, error) { + orderExecutor.EXPECT().SubmitOrders(ctx, gomock.Any()).DoAndReturn(func( + ctx context.Context, order types.SubmitOrder, + ) (types.OrderSlice, error) { assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2) return []types.Order{ {SubmitOrder: expectedSubmitOrder2}, @@ -1190,14 +1324,14 @@ func TestStrategy_aggregateOrderQuoteAmountAndFeeRetry(t *testing.T) { func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { t.Run("7 grids", func(t *testing.T) { - s := newTestStrategy() + s := newTestStrategy("ETHUSDT") s.UpperPrice = number(1660) s.LowerPrice = number(1630) s.QuoteInvestment = number(61) s.GridNum = 7 grid := s.newGrid() minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) - assert.InDelta(t, 60.46, minQuoteInvestment.Float64(), 0.01) + assert.InDelta(t, 48.36, minQuoteInvestment.Float64(), 0.01) err := s.checkMinimalQuoteInvestment(grid) assert.NoError(t, err) @@ -1207,12 +1341,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { s := newTestStrategy() // 10_000 * 0.001 = 10USDT // 20_000 * 0.001 = 20USDT - // hence we should have at least: 20USDT * 10 grids s.QuoteInvestment = number(10_000) s.GridNum = 10 grid := s.newGrid() minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) - assert.InDelta(t, 129.9999, minQuoteInvestment.Float64(), 0.01) + assert.InDelta(t, 103.999, minQuoteInvestment.Float64(), 0.01) err := s.checkMinimalQuoteInvestment(grid) assert.NoError(t, err) @@ -1225,11 +1358,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { grid := s.newGrid() minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) - assert.InDelta(t, 14979.995499, minQuoteInvestment.Float64(), 0.001) + assert.InDelta(t, 11983.996400, minQuoteInvestment.Float64(), 0.001) err := s.checkMinimalQuoteInvestment(grid) assert.Error(t, err) - assert.EqualError(t, err, "need at least 14979.995500 USDT for quote investment, 10000.000000 USDT given") + assert.EqualError(t, err, "need at least 11983.996400 USDT for quote investment, 10000.000000 USDT given") }) }