Merge pull request #1391 from c9s/c9s/grid2-base-quote-fix

This commit is contained in:
c9s 2023-11-06 10:37:21 +08:00 committed by GitHub
commit e773bb0e52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 172 additions and 21 deletions

View File

@ -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))
}

View File

@ -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")
})
}