grid2: respect s.BaseGridNum and add a failing test case

This commit is contained in:
c9s 2023-11-03 18:12:42 +08:00
parent cdebcc9a58
commit 6cce5a2268
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
2 changed files with 114 additions and 21 deletions

View File

@ -796,6 +796,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
if numberOfSellOrders > 0 { if numberOfSellOrders > 0 {
numberOfSellOrders-- 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 // 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) s.Market.MinQuantity)
if baseQuantity.Compare(minBaseQuantity) <= 0 { 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) baseQuantity = s.Market.RoundUpQuantityByPrecision(minBaseQuantity)
numberOfSellOrders = int(math.Floor(baseInvestment.Div(baseQuantity).Float64())) 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) s.logger.Infof("grid base investment sell orders: %d", numberOfSellOrders)
@ -824,7 +830,8 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
// quoteInvestment = (p1 + p2 + p3) * q // quoteInvestment = (p1 + p2 + p3) * q
// maxBuyQuantity = quoteInvestment / (p1 + p2 + p3) // maxBuyQuantity = quoteInvestment / (p1 + p2 + p3)
si := -1 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] pin := pins[i]
price := fixedpoint.Value(pin) price := fixedpoint.Value(pin)
@ -844,6 +851,7 @@ func (s *Strategy) calculateBaseQuoteInvestmentQuantity(
// requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice)) // requiredQuote = requiredQuote.Add(quantity.Mul(nextLowerPrice))
totalQuotePrice = totalQuotePrice.Add(nextLowerPrice) totalQuotePrice = totalQuotePrice.Add(nextLowerPrice)
} }
} else { } else {
// for orders that buy // for orders that buy
if s.ProfitSpread.IsZero() && i+1 == si { 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 // should never place a buy order at the upper price
if i == len(pins)-1 { if i == end {
continue 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 { if totalQuotePrice.Sign() > 0 && quoteInvestment.Sign() > 0 {
quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) quoteSideQuantity := quoteInvestment.Div(totalQuotePrice)
s.logger.Infof("quote side quantity: %f = %f / %f", quoteSideQuantity.Float64(), quoteInvestment.Float64(), totalQuotePrice.Float64())
if numberOfSellOrders > 0 { if numberOfSellOrders > 0 {
return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil return fixedpoint.Min(quoteSideQuantity, baseQuantity), nil
} }
@ -1058,6 +1069,11 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession)
return err2 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 // check if base and quote are enough
var totalBase = fixedpoint.Zero var totalBase = fixedpoint.Zero
var totalQuote = 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-- { for i := len(pins) - 2; i >= 0; i-- {
pin := pins[i] pin := pins[i]
price := fixedpoint.Value(pin) price := fixedpoint.Value(pin)
// TODO: should we round the quote here before adding?
totalQuote = totalQuote.Add(price.Mul(minQuantity)) totalQuote = totalQuote.Add(price.Mul(minQuantity))
} }

View File

@ -204,6 +204,65 @@ func TestStrategy_generateGridOrders(t *testing.T) {
}, orders) }, orders)
}) })
t.Run("base and quote #2", 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 with pre-calculated baseGridNumber", func(t *testing.T) { t.Run("base and quote with pre-calculated baseGridNumber", func(t *testing.T) {
s := newTestStrategy() s := newTestStrategy()
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
@ -519,11 +578,11 @@ func newTestMarket(symbol string) types.Market {
BaseCurrency: "BTC", BaseCurrency: "BTC",
QuoteCurrency: "USDT", QuoteCurrency: "USDT",
TickSize: number(0.01), TickSize: number(0.01),
StepSize: number(0.00001), StepSize: number(0.000001),
PricePrecision: 2, PricePrecision: 2,
VolumePrecision: 8, VolumePrecision: 8,
MinNotional: number(10.0), MinNotional: number(8.0),
MinQuantity: number(0.001), MinQuantity: number(0.0003),
} }
case "ETHUSDT": case "ETHUSDT":
return types.Market{ return types.Market{
@ -534,7 +593,7 @@ func newTestMarket(symbol string) types.Market {
PricePrecision: 2, PricePrecision: 2,
VolumePrecision: 6, VolumePrecision: 6,
MinNotional: number(8.000), MinNotional: number(8.000),
MinQuantity: number(0.00030), MinQuantity: number(0.0046),
} }
} }
@ -577,12 +636,17 @@ func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.O
} }
} }
func newTestStrategy() *Strategy { func newTestStrategy(va ...string) *Strategy {
market := newTestMarket("BTCUSDT") symbol := "BTCUSDT"
if len(va) > 0 {
symbol = va[0]
}
market := newTestMarket(symbol)
s := &Strategy{ s := &Strategy{
logger: logrus.NewEntry(logrus.New()), logger: logrus.NewEntry(logrus.New()),
Symbol: "BTCUSDT", Symbol: symbol,
Market: market, Market: market,
GridProfitStats: newGridProfitStats(market), GridProfitStats: newGridProfitStats(market),
UpperPrice: number(20_000), UpperPrice: number(20_000),
@ -790,7 +854,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
} }
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) 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) assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
return []types.Order{ return []types.Order{
{SubmitOrder: expectedSubmitOrder}, {SubmitOrder: expectedSubmitOrder},
@ -858,7 +924,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
} }
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) 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) assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
return []types.Order{ return []types.Order{
{SubmitOrder: expectedSubmitOrder}, {SubmitOrder: expectedSubmitOrder},
@ -946,7 +1014,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
Market: s.Market, Market: s.Market,
Tag: orderTag, 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) assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
return []types.Order{ return []types.Order{
{SubmitOrder: expectedSubmitOrder}, {SubmitOrder: expectedSubmitOrder},
@ -963,7 +1033,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
Market: s.Market, Market: s.Market,
Tag: orderTag, 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) assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2)
return []types.Order{ return []types.Order{
{SubmitOrder: expectedSubmitOrder2}, {SubmitOrder: expectedSubmitOrder2},
@ -1060,7 +1132,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
} }
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl) 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) assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder, order), "%+v is not equal to %+v", order, expectedSubmitOrder)
return []types.Order{ return []types.Order{
{SubmitOrder: expectedSubmitOrder}, {SubmitOrder: expectedSubmitOrder},
@ -1078,7 +1152,9 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
Tag: orderTag, 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) assert.True(t, equalOrdersIgnoreClientOrderID(expectedSubmitOrder2, order), "%+v is not equal to %+v", order, expectedSubmitOrder2)
return []types.Order{ return []types.Order{
{SubmitOrder: expectedSubmitOrder2}, {SubmitOrder: expectedSubmitOrder2},
@ -1190,14 +1266,14 @@ func TestStrategy_aggregateOrderQuoteAmountAndFeeRetry(t *testing.T) {
func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
t.Run("7 grids", func(t *testing.T) { t.Run("7 grids", func(t *testing.T) {
s := newTestStrategy() s := newTestStrategy("ETHUSDT")
s.UpperPrice = number(1660) s.UpperPrice = number(1660)
s.LowerPrice = number(1630) s.LowerPrice = number(1630)
s.QuoteInvestment = number(61) s.QuoteInvestment = number(61)
s.GridNum = 7 s.GridNum = 7
grid := s.newGrid() grid := s.newGrid()
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) 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) err := s.checkMinimalQuoteInvestment(grid)
assert.NoError(t, err) assert.NoError(t, err)
@ -1207,12 +1283,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
s := newTestStrategy() s := newTestStrategy()
// 10_000 * 0.001 = 10USDT // 10_000 * 0.001 = 10USDT
// 20_000 * 0.001 = 20USDT // 20_000 * 0.001 = 20USDT
// hence we should have at least: 20USDT * 10 grids
s.QuoteInvestment = number(10_000) s.QuoteInvestment = number(10_000)
s.GridNum = 10 s.GridNum = 10
grid := s.newGrid() grid := s.newGrid()
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) 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) err := s.checkMinimalQuoteInvestment(grid)
assert.NoError(t, err) assert.NoError(t, err)
@ -1225,11 +1300,11 @@ func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
grid := s.newGrid() grid := s.newGrid()
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid) 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) err := s.checkMinimalQuoteInvestment(grid)
assert.Error(t, err) 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")
}) })
} }