bbgo_origin/pkg/strategy/grid2/strategy_test.go

1144 lines
32 KiB
Go
Raw Normal View History

2022-11-25 10:07:02 +00:00
//go:build !dnum
package grid2
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
2023-03-01 13:49:15 +00:00
gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/types/mocks"
)
2023-02-23 16:44:50 +00:00
func init() {
registerMetrics()
}
func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) {
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Market: types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
},
}
t.Run("quote to base balance conversion check", func(t *testing.T) {
_, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(10_000.0), number(0.1), number(13_500.0), []Pin{
Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy)
Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy)
Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy)
Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy)
Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy)
Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD
})
assert.NoError(t, err)
assert.Equal(t, number(6000.0), requiredQuote)
})
t.Run("quote to base balance conversion not enough", func(t *testing.T) {
_, requiredQuote, err := s.checkRequiredInvestmentByQuantity(number(0.0), number(5_000.0), number(0.1), number(13_500.0), []Pin{
Pin(number(10_000.0)), // 0.1 * 10_000 = 1000 USD (buy)
Pin(number(11_000.0)), // 0.1 * 11_000 = 1100 USD (buy)
Pin(number(12_000.0)), // 0.1 * 12_000 = 1200 USD (buy)
Pin(number(13_000.0)), // 0.1 * 13_000 = 1300 USD (buy)
Pin(number(14_000.0)), // 0.1 * 14_000 = 1400 USD (buy)
Pin(number(15_000.0)), // 0.1 * 15_000 = 1500 USD
})
assert.EqualError(t, err, "quote balance (5000.000000 USDT) is not enough, required = quote 6000.000000")
assert.Equal(t, number(6000.0), requiredQuote)
})
}
type PriceSideAssert struct {
Price fixedpoint.Value
Side types.SideType
}
func assertPriceSide(t *testing.T, priceSideAsserts []PriceSideAssert, orders []types.SubmitOrder) {
for i, a := range priceSideAsserts {
assert.Equalf(t, a.Price, orders[i].Price, "order #%d price should be %f", i+1, a.Price.Float64())
assert.Equalf(t, a.Side, orders[i].Side, "order at price %f should be %s", a.Price.Float64(), a.Side)
}
}
func TestStrategy_generateGridOrders(t *testing.T) {
t.Run("quote only", func(t *testing.T) {
s := newTestStrategy()
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
2023-02-15 08:51:12 +00:00
quoteInvestment := number(10000.0)
baseInvestment := number(0)
orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 10, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(19000.0), types.SideTypeBuy},
{number(18000.0), types.SideTypeBuy},
{number(17000.0), types.SideTypeBuy},
{number(16000.0), types.SideTypeBuy},
{number(15000.0), types.SideTypeBuy},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
2023-02-15 08:51:12 +00:00
t.Run("quote only + buy only", func(t *testing.T) {
s := newTestStrategy()
s.UpperPrice = number(0.9)
s.LowerPrice = number(0.1)
s.GridNum = 7
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
assert.Equal(t, []Pin{
Pin(number(0.1)),
Pin(number(0.23)),
Pin(number(0.36)),
Pin(number(0.50)),
Pin(number(0.63)),
Pin(number(0.76)),
Pin(number(0.9)),
}, s.grid.Pins, "pins are correct")
lastPrice := number(22100)
quoteInvestment := number(100.0)
baseInvestment := number(0)
quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, s.grid.Pins)
assert.NoError(t, err)
assert.InDelta(t, 38.7364341, quantity.Float64(), 0.00001)
2023-02-15 08:51:12 +00:00
s.QuantityOrAmount.Quantity = quantity
orders, err := s.generateGridOrders(quoteInvestment, baseInvestment, lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 6, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(0.76), types.SideTypeBuy},
{number(0.63), types.SideTypeBuy},
{number(0.5), types.SideTypeBuy},
{number(0.36), types.SideTypeBuy},
{number(0.23), types.SideTypeBuy},
{number(0.1), types.SideTypeBuy},
}, orders)
})
t.Run("base + quote", func(t *testing.T) {
s := newTestStrategy()
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
orders, err := s.generateGridOrders(number(10000.0), number(0.021), lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 10, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(20000.0), types.SideTypeSell},
{number(19000.0), types.SideTypeSell},
{number(17000.0), types.SideTypeBuy},
{number(16000.0), types.SideTypeBuy},
{number(15000.0), types.SideTypeBuy},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
t.Run("enough base + quote", func(t *testing.T) {
s := newTestStrategy()
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
orders, err := s.generateGridOrders(number(10000.0), number(1.0), lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 10, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(20000.0), types.SideTypeSell},
{number(19000.0), types.SideTypeSell},
{number(18000.0), types.SideTypeSell},
{number(17000.0), types.SideTypeSell},
{number(16000.0), types.SideTypeSell},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
t.Run("enough base + quote + profitSpread", func(t *testing.T) {
s := newTestStrategy()
s.ProfitSpread = number(1_000)
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
s.grid.CalculateArithmeticPins()
s.QuantityOrAmount.Quantity = number(0.01)
lastPrice := number(15300)
orders, err := s.generateGridOrders(number(10000.0), number(1.0), lastPrice)
assert.NoError(t, err)
if !assert.Equal(t, 11, len(orders)) {
for _, o := range orders {
t.Logf("- %s %s", o.Price.String(), o.Side)
}
}
assertPriceSide(t, []PriceSideAssert{
{number(21000.0), types.SideTypeSell},
{number(20000.0), types.SideTypeSell},
{number(19000.0), types.SideTypeSell},
{number(18000.0), types.SideTypeSell},
{number(17000.0), types.SideTypeSell},
{number(15000.0), types.SideTypeBuy},
{number(14000.0), types.SideTypeBuy},
{number(13000.0), types.SideTypeBuy},
{number(12000.0), types.SideTypeBuy},
{number(11000.0), types.SideTypeBuy},
{number(10000.0), types.SideTypeBuy},
}, orders)
})
}
func TestStrategy_checkRequiredInvestmentByAmount(t *testing.T) {
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Market: types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
},
}
t.Run("quote to base balance conversion", func(t *testing.T) {
2022-11-25 07:42:45 +00:00
_, requiredQuote, err := s.checkRequiredInvestmentByAmount(
number(0.0), number(3_000.0),
number(1000.0),
number(13_500.0), []Pin{
Pin(number(10_000.0)),
Pin(number(11_000.0)),
Pin(number(12_000.0)),
Pin(number(13_000.0)),
Pin(number(14_000.0)),
Pin(number(15_000.0)),
})
assert.EqualError(t, err, "quote balance (3000.000000 USDT) is not enough, required = quote 4999.999890")
assert.InDelta(t, 4999.999890, requiredQuote.Float64(), number(0.001).Float64())
})
}
func TestStrategy_calculateQuoteInvestmentQuantity(t *testing.T) {
t.Run("quote quantity", func(t *testing.T) {
// quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000) * q
// q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000)
// q = 12_000 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000)
// q = 0.2
s := newTestStrategy()
lastPrice := number(13_500.0)
quoteInvestment := number(12_000.0)
quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, []Pin{
Pin(number(10_000.0)), // buy
Pin(number(11_000.0)), // buy
Pin(number(12_000.0)), // buy
Pin(number(13_000.0)), // buy
Pin(number(14_000.0)), // buy
Pin(number(15_000.0)),
})
assert.NoError(t, err)
assert.InDelta(t, 0.199999916, quantity.Float64(), 0.0001)
})
t.Run("quote quantity #2", func(t *testing.T) {
s := newTestStrategy()
lastPrice := number(160.0)
quoteInvestment := number(1_000.0)
quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, []Pin{
Pin(number(100.0)), // buy
Pin(number(116.67)), // buy
Pin(number(133.33)), // buy
Pin(number(150.00)), // buy
Pin(number(166.67)), // buy
Pin(number(183.33)),
Pin(number(200.00)),
})
assert.NoError(t, err)
assert.InDelta(t, 1.1764, quantity.Float64(), 0.00001)
})
2023-02-16 10:11:04 +00:00
t.Run("quote quantity #3", func(t *testing.T) {
s := newTestStrategy()
lastPrice := number(22000.0)
quoteInvestment := number(100.0)
pins := []Pin{
Pin(number(0.1)),
Pin(number(0.23)),
Pin(number(0.36)),
Pin(number(0.50)),
Pin(number(0.63)),
Pin(number(0.76)),
Pin(number(0.90)),
}
quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, pins)
assert.NoError(t, err)
assert.InDelta(t, 38.736434, quantity.Float64(), 0.0001)
2023-02-16 10:11:04 +00:00
var totalQuoteUsed = fixedpoint.Zero
for i, pin := range pins {
if i == len(pins)-1 {
continue
}
price := fixedpoint.Value(pin)
totalQuoteUsed = totalQuoteUsed.Add(price.Mul(quantity))
}
assert.LessOrEqualf(t, totalQuoteUsed, number(100.0), "total quote used: %f", totalQuoteUsed.Float64())
})
t.Run("profit spread", func(t *testing.T) {
// quoteInvestment = (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000) * q
// q = quoteInvestment / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000)
// q = 7500 / (10,000 + 11,000 + 12,000 + 13,000 + 14,000 + 15,000)
// q = 0.1
s := newTestStrategy()
s.ProfitSpread = number(2000.0)
lastPrice := number(13_500.0)
quoteInvestment := number(7500.0)
quantity, err := s.calculateQuoteInvestmentQuantity(quoteInvestment, lastPrice, []Pin{
Pin(number(10_000.0)), // sell order @ 12_000
Pin(number(11_000.0)), // sell order @ 13_000
Pin(number(12_000.0)), // sell order @ 14_000
Pin(number(13_000.0)), // sell order @ 15_000
Pin(number(14_000.0)), // sell order @ 16_000
Pin(number(15_000.0)), // sell order @ 17_000
})
assert.NoError(t, err)
assert.InDelta(t, 0.099992, quantity.Float64(), 0.0001)
})
}
func newTestMarket() types.Market {
return types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
TickSize: number(0.01),
PricePrecision: 2,
VolumePrecision: 8,
MinNotional: number(10.0),
MinQuantity: number(0.001),
}
}
var testOrderID = uint64(0)
func newTestOrder(price, quantity fixedpoint.Value, side types.SideType) types.Order {
market := newTestMarket()
testOrderID++
return types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: side,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
AveragePrice: fixedpoint.Zero,
StopPrice: fixedpoint.Zero,
Market: market,
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
GID: testOrderID,
OrderID: testOrderID,
Status: types.OrderStatusNew,
ExecutedQuantity: fixedpoint.Zero,
IsWorking: true,
}
}
func newTestStrategy() *Strategy {
market := newTestMarket()
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Symbol: "BTCUSDT",
Market: market,
GridProfitStats: newGridProfitStats(market),
UpperPrice: number(20_000),
LowerPrice: number(10_000),
2023-01-16 10:34:08 +00:00
GridNum: 11,
historicalTrades: bbgo.NewTradeStore(),
// QuoteInvestment: number(9000.0),
}
return s
}
func TestStrategy_calculateProfit(t *testing.T) {
t.Run("earn quote without compound", func(t *testing.T) {
s := newTestStrategy()
profit := s.calculateProfit(types.Order{
SubmitOrder: types.SubmitOrder{
Price: number(13_000),
Quantity: number(1.0),
},
}, number(12_000), number(1.0))
assert.NotNil(t, profit)
assert.Equal(t, "USDT", profit.Currency)
assert.InDelta(t, 1000.0, profit.Profit.Float64(), 0.1)
})
t.Run("earn quote with compound", func(t *testing.T) {
s := newTestStrategy()
s.Compound = true
profit := s.calculateProfit(types.Order{
SubmitOrder: types.SubmitOrder{
Price: number(13_000),
Quantity: number(1.0),
},
}, number(12_000), number(1.0))
assert.NotNil(t, profit)
assert.Equal(t, "USDT", profit.Currency)
assert.InDelta(t, 1000.0, profit.Profit.Float64(), 0.1)
})
t.Run("earn base without compound", func(t *testing.T) {
s := newTestStrategy()
s.EarnBase = true
s.Compound = false
quoteQuantity := number(12_000).Mul(number(1.0))
sellQuantity := quoteQuantity.Div(number(13_000.0))
buyOrder := types.SubmitOrder{
Price: number(12_000.0),
Quantity: number(1.0),
}
profit := s.calculateProfit(types.Order{
SubmitOrder: types.SubmitOrder{
Price: number(13_000.0),
Quantity: sellQuantity,
},
}, buyOrder.Price, buyOrder.Quantity)
assert.NotNil(t, profit)
assert.Equal(t, "BTC", profit.Currency)
assert.InDelta(t, sellQuantity.Float64()-buyOrder.Quantity.Float64(), profit.Profit.Float64(), 0.001)
})
}
func TestStrategy_aggregateOrderBaseFee(t *testing.T) {
s := newTestStrategy()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
s.orderQueryService = mockService
ctx := context.Background()
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
Symbol: "BTCUSDT",
OrderID: "3",
}).Return([]types.Trade{
{
ID: 1,
OrderID: 3,
Exchange: "binance",
Price: number(20000.0),
Quantity: number(0.2),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
Fee: number(0.2 * 0.01),
},
{
ID: 1,
OrderID: 3,
Exchange: "binance",
Price: number(20000.0),
Quantity: number(0.8),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
Fee: number(0.8 * 0.01),
},
}, nil)
2023-03-03 05:13:12 +00:00
baseFee, _ := s.aggregateOrderFee(types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: number(1.0),
Price: number(20000.0),
AveragePrice: number(0),
StopPrice: number(0),
Market: types.Market{},
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
GID: 1,
OrderID: 3,
Status: types.OrderStatusFilled,
ExecutedQuantity: number(1.0),
IsWorking: false,
})
assert.Equal(t, "0.01", baseFee.String())
}
func TestStrategy_findDuplicatedPriceOpenOrders(t *testing.T) {
t.Run("no duplicated open orders", func(t *testing.T) {
s := newTestStrategy()
s.grid = s.newGrid()
dupOrders := s.findDuplicatedPriceOpenOrders([]types.Order{
newTestOrder(number(1900.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1800.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1700.0), number(0.1), types.SideTypeSell),
})
assert.Empty(t, dupOrders)
assert.Len(t, dupOrders, 0)
})
t.Run("1 duplicated open order SELL", func(t *testing.T) {
s := newTestStrategy()
s.grid = s.newGrid()
dupOrders := s.findDuplicatedPriceOpenOrders([]types.Order{
newTestOrder(number(1900.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1900.0), number(0.1), types.SideTypeSell),
newTestOrder(number(1800.0), number(0.1), types.SideTypeSell),
})
assert.Len(t, dupOrders, 1)
})
}
func TestStrategy_handleOrderFilled(t *testing.T) {
ctx := context.Background()
t.Run("no fee token", func(t *testing.T) {
gridQuantity := number(0.1)
orderID := uint64(1)
s := newTestStrategy()
s.Quantity = gridQuantity
s.grid = s.newGrid()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
Symbol: "BTCUSDT",
OrderID: "1",
}).Return([]types.Trade{
{
ID: 1,
OrderID: orderID,
Exchange: "binance",
Price: number(11000.0),
Quantity: gridQuantity,
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
Fee: number(gridQuantity.Float64() * 0.1 * 0.01),
},
}, nil)
s.orderQueryService = mockService
expectedSubmitOrder := types.SubmitOrder{
Symbol: "BTCUSDT",
Type: types.OrderTypeLimit,
Price: number(12_000.0),
Quantity: number(0.0999),
Side: types.SideTypeSell,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
2022-12-19 10:51:39 +00:00
Tag: orderTag,
}
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{
{SubmitOrder: expectedSubmitOrder},
}, nil)
s.orderExecutor = orderExecutor
s.handleOrderFilled(types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: gridQuantity,
Price: number(11000.0),
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
OrderID: orderID,
Status: types.OrderStatusFilled,
ExecutedQuantity: gridQuantity,
})
})
2022-12-07 06:42:06 +00:00
t.Run("with fee token", func(t *testing.T) {
gridQuantity := number(0.1)
orderID := uint64(1)
s := newTestStrategy()
s.Quantity = gridQuantity
s.grid = s.newGrid()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
Symbol: "BTCUSDT",
OrderID: "1",
}).Return([]types.Trade{
{
ID: 1,
OrderID: orderID,
Exchange: "binance",
Price: number(11000.0),
Quantity: gridQuantity,
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
Fee: fixedpoint.Zero,
},
}, nil)
s.orderQueryService = mockService
expectedSubmitOrder := types.SubmitOrder{
Symbol: "BTCUSDT",
Type: types.OrderTypeLimit,
Price: number(12_000.0),
Quantity: gridQuantity,
Side: types.SideTypeSell,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
2022-12-19 10:51:39 +00:00
Tag: orderTag,
2022-12-07 06:42:06 +00:00
}
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{
{SubmitOrder: expectedSubmitOrder},
}, nil)
s.orderExecutor = orderExecutor
s.handleOrderFilled(types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: gridQuantity,
Price: number(11000.0),
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
OrderID: orderID,
Status: types.OrderStatusFilled,
ExecutedQuantity: gridQuantity,
})
})
2022-12-07 06:48:51 +00:00
t.Run("with fee token and EarnBase", func(t *testing.T) {
gridQuantity := number(0.1)
orderID := uint64(1)
s := newTestStrategy()
s.Quantity = gridQuantity
s.EarnBase = true
s.grid = s.newGrid()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
2023-03-03 05:51:50 +00:00
2022-12-07 06:48:51 +00:00
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
Symbol: "BTCUSDT",
OrderID: "1",
}).Return([]types.Trade{
{
ID: 1,
OrderID: orderID,
Exchange: "binance",
Price: number(11000.0),
2023-03-03 05:51:50 +00:00
Quantity: number("0.1"),
2022-12-07 06:48:51 +00:00
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
Fee: fixedpoint.Zero,
},
2023-03-03 05:51:50 +00:00
}, 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)
2022-12-07 06:48:51 +00:00
s.orderQueryService = mockService
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
expectedSubmitOrder := types.SubmitOrder{
Symbol: "BTCUSDT",
Type: types.OrderTypeLimit,
Side: types.SideTypeSell,
Price: number(12_000.0),
Quantity: number(0.09166666),
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
2022-12-19 10:51:39 +00:00
Tag: orderTag,
2022-12-07 06:48:51 +00:00
}
orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{
{SubmitOrder: expectedSubmitOrder},
}, nil)
expectedSubmitOrder2 := types.SubmitOrder{
Symbol: "BTCUSDT",
Type: types.OrderTypeLimit,
Side: types.SideTypeBuy,
Price: number(11_000.0),
Quantity: number(0.09999909),
2022-12-07 06:48:51 +00:00
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
2022-12-19 10:51:39 +00:00
Tag: orderTag,
2022-12-07 06:48:51 +00:00
}
orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{
{SubmitOrder: expectedSubmitOrder2},
}, nil)
s.orderExecutor = orderExecutor
s.handleOrderFilled(types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: gridQuantity,
Price: number(11000.0),
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
OrderID: 1,
Status: types.OrderStatusFilled,
ExecutedQuantity: gridQuantity,
})
s.handleOrderFilled(types.Order{
SubmitOrder: expectedSubmitOrder,
Exchange: "binance",
OrderID: 2,
Status: types.OrderStatusFilled,
ExecutedQuantity: expectedSubmitOrder.Quantity,
})
})
2022-12-07 06:42:06 +00:00
t.Run("with fee token and compound", func(t *testing.T) {
gridQuantity := number(0.1)
orderID := uint64(1)
s := newTestStrategy()
s.Quantity = gridQuantity
s.Compound = true
s.grid = s.newGrid()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
2023-03-03 05:51:50 +00:00
2022-12-07 06:42:06 +00:00
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
Symbol: "BTCUSDT",
OrderID: "1",
}).Return([]types.Trade{
{
ID: 1,
OrderID: orderID,
Exchange: "binance",
Price: number(11000.0),
Quantity: gridQuantity,
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
2023-03-03 05:51:50 +00:00
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"),
2022-12-07 06:42:06 +00:00
},
}, nil)
s.orderQueryService = mockService
expectedSubmitOrder := types.SubmitOrder{
Symbol: "BTCUSDT",
Type: types.OrderTypeLimit,
Price: number(12_000.0),
Quantity: number(0.09999),
2022-12-07 06:42:06 +00:00
Side: types.SideTypeSell,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
2022-12-19 10:51:39 +00:00
Tag: orderTag,
2022-12-07 06:42:06 +00:00
}
orderExecutor := gridmocks.NewMockOrderExecutor(mockCtrl)
orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder).Return([]types.Order{
{SubmitOrder: expectedSubmitOrder},
}, nil)
expectedSubmitOrder2 := types.SubmitOrder{
Symbol: "BTCUSDT",
Type: types.OrderTypeLimit,
Price: number(11_000.0),
2023-03-03 05:51:50 +00:00
Quantity: number(0.10909),
2022-12-07 06:42:06 +00:00
Side: types.SideTypeBuy,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
2022-12-19 10:51:39 +00:00
Tag: orderTag,
2022-12-07 06:42:06 +00:00
}
orderExecutor.EXPECT().SubmitOrders(ctx, expectedSubmitOrder2).Return([]types.Order{
{SubmitOrder: expectedSubmitOrder2},
}, nil)
s.orderExecutor = orderExecutor
s.handleOrderFilled(types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: gridQuantity,
Price: number(11000.0),
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
OrderID: 1,
Status: types.OrderStatusFilled,
ExecutedQuantity: gridQuantity,
})
s.handleOrderFilled(types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Quantity: gridQuantity,
Price: number(12000.0),
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
OrderID: 2,
Status: types.OrderStatusFilled,
ExecutedQuantity: gridQuantity,
})
})
}
func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) {
s := newTestStrategy()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
s.orderQueryService = mockService
ctx := context.Background()
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
Symbol: "BTCUSDT",
OrderID: "3",
}).Return(nil, errors.New("api error"))
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
Symbol: "BTCUSDT",
OrderID: "3",
}).Return([]types.Trade{
{
ID: 1,
OrderID: 3,
Exchange: "binance",
Price: number(20000.0),
Quantity: number(0.2),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
Fee: number(0.2 * 0.01),
},
{
ID: 1,
OrderID: 3,
Exchange: "binance",
Price: number(20000.0),
Quantity: number(0.8),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
FeeCurrency: "BTC",
Fee: number(0.8 * 0.01),
},
}, nil)
2023-03-03 05:13:12 +00:00
baseFee, _ := s.aggregateOrderFee(types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: number(1.0),
Price: number(20000.0),
AveragePrice: number(0),
StopPrice: number(0),
Market: types.Market{},
TimeInForce: types.TimeInForceGTC,
},
Exchange: "binance",
GID: 1,
OrderID: 3,
Status: types.OrderStatusFilled,
ExecutedQuantity: number(1.0),
IsWorking: false,
})
assert.Equal(t, "0.01", baseFee.String())
}
func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {
t.Run("7 grids", func(t *testing.T) {
s := newTestStrategy()
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)
err := s.checkMinimalQuoteInvestment(grid)
assert.NoError(t, err)
})
t.Run("10 grids", func(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)
err := s.checkMinimalQuoteInvestment(grid)
assert.NoError(t, err)
})
t.Run("1000 grids", func(t *testing.T) {
s := newTestStrategy()
s.QuoteInvestment = number(10_000)
s.GridNum = 1000
grid := s.newGrid()
minQuoteInvestment := calculateMinimalQuoteInvestment(s.Market, grid)
assert.InDelta(t, 14979.995499, 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")
})
}
2023-03-01 14:21:24 +00:00
func Test_buildPinOrderMap(t *testing.T) {
assert := assert.New(t)
s := newTestStrategy()
s.UpperPrice = number(2000.0)
s.LowerPrice = number(1000.0)
s.GridNum = 11
s.grid = s.newGrid()
t.Run("successful case", func(t *testing.T) {
openOrders := []types.Order{
types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: number(1.0),
Price: number(1000.0),
AveragePrice: number(0),
StopPrice: number(0),
Market: s.Market,
TimeInForce: types.TimeInForceGTC,
},
Exchange: "max",
GID: 1,
OrderID: 1,
Status: types.OrderStatusNew,
ExecutedQuantity: number(0.0),
IsWorking: false,
},
}
m, err := s.buildPinOrderMap(s.grid, openOrders)
assert.NoError(err)
assert.Len(m, 11)
for pin, order := range m {
if pin == s.FormatPrice(openOrders[0].Price) {
assert.Equal(openOrders[0].OrderID, order.OrderID)
} else {
assert.Equal(uint64(0), order.OrderID)
}
}
})
t.Run("there is one order with non-pin price in openOrders", func(t *testing.T) {
openOrders := []types.Order{
types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: number(1.0),
Price: number(1111.0),
AveragePrice: number(0),
StopPrice: number(0),
Market: s.Market,
TimeInForce: types.TimeInForceGTC,
},
Exchange: "max",
GID: 1,
OrderID: 1,
Status: types.OrderStatusNew,
ExecutedQuantity: number(0.0),
IsWorking: false,
},
}
_, err := s.buildPinOrderMap(s.grid, openOrders)
assert.Error(err)
})
t.Run("there are duplicated open orders at same pin", func(t *testing.T) {
openOrders := []types.Order{
types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: number(1.0),
Price: number(1000.0),
AveragePrice: number(0),
StopPrice: number(0),
Market: s.Market,
TimeInForce: types.TimeInForceGTC,
},
Exchange: "max",
GID: 1,
OrderID: 1,
Status: types.OrderStatusNew,
ExecutedQuantity: number(0.0),
IsWorking: false,
},
types.Order{
SubmitOrder: types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Quantity: number(1.0),
Price: number(1000.0),
AveragePrice: number(0),
StopPrice: number(0),
Market: s.Market,
TimeInForce: types.TimeInForceGTC,
},
Exchange: "max",
GID: 2,
OrderID: 2,
Status: types.OrderStatusNew,
ExecutedQuantity: number(0.0),
IsWorking: false,
},
}
_, err := s.buildPinOrderMap(s.grid, openOrders)
assert.Error(err)
})
}