mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
Merge pull request #1022 from c9s/feature/grid2
strategy: grid2: more refactoring, fix bugs and add more tests
This commit is contained in:
commit
37f4435aba
|
@ -36,7 +36,7 @@ exchangeStrategies:
|
||||||
symbol: BTCUSDT
|
symbol: BTCUSDT
|
||||||
upperPrice: 18_000.0
|
upperPrice: 18_000.0
|
||||||
lowerPrice: 16_000.0
|
lowerPrice: 16_000.0
|
||||||
gridNumber: 200
|
gridNumber: 100
|
||||||
|
|
||||||
## compound is used for buying more inventory when the profit is made by the filled SELL order.
|
## compound is used for buying more inventory when the profit is made by the filled SELL order.
|
||||||
## when compound is disabled, fixed quantity is used for each grid order.
|
## when compound is disabled, fixed quantity is used for each grid order.
|
||||||
|
@ -70,12 +70,12 @@ exchangeStrategies:
|
||||||
# amount: 10.0
|
# amount: 10.0
|
||||||
|
|
||||||
## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC
|
## 2) fixed quantity: it will use your balance to place orders with the fixed quantity. e.g. 0.001 BTC
|
||||||
# quantity: 0.001
|
quantity: 0.001
|
||||||
|
|
||||||
## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid.
|
## 3) quoteInvestment and baseInvestment: when using quoteInvestment, the strategy will automatically calculate your best quantity for the whole grid.
|
||||||
## quoteInvestment is required, and baseInvestment is optional (could be zero)
|
## quoteInvestment is required, and baseInvestment is optional (could be zero)
|
||||||
## if you have existing BTC position and want to reuse it you can set the baseInvestment.
|
## if you have existing BTC position and want to reuse it you can set the baseInvestment.
|
||||||
quoteInvestment: 10_000
|
# quoteInvestment: 10_000
|
||||||
# baseInvestment: 1.0
|
# baseInvestment: 1.0
|
||||||
|
|
||||||
feeRate: 0.075%
|
feeRate: 0.075%
|
||||||
|
|
95
pkg/strategy/grid2/mocks/order_executor.go
Normal file
95
pkg/strategy/grid2/mocks/order_executor.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/c9s/bbgo/pkg/strategy/grid2 (interfaces: OrderExecutor)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
reflect "reflect"
|
||||||
|
|
||||||
|
fixedpoint "github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
types "github.com/c9s/bbgo/pkg/types"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockOrderExecutor is a mock of OrderExecutor interface.
|
||||||
|
type MockOrderExecutor struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockOrderExecutorMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockOrderExecutorMockRecorder is the mock recorder for MockOrderExecutor.
|
||||||
|
type MockOrderExecutorMockRecorder struct {
|
||||||
|
mock *MockOrderExecutor
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockOrderExecutor creates a new mock instance.
|
||||||
|
func NewMockOrderExecutor(ctrl *gomock.Controller) *MockOrderExecutor {
|
||||||
|
mock := &MockOrderExecutor{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockOrderExecutorMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||||
|
func (m *MockOrderExecutor) EXPECT() *MockOrderExecutorMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosePosition mocks base method.
|
||||||
|
func (m *MockOrderExecutor) ClosePosition(arg0 context.Context, arg1 fixedpoint.Value, arg2 ...string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
varargs := []interface{}{arg0, arg1}
|
||||||
|
for _, a := range arg2 {
|
||||||
|
varargs = append(varargs, a)
|
||||||
|
}
|
||||||
|
ret := m.ctrl.Call(m, "ClosePosition", varargs...)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClosePosition indicates an expected call of ClosePosition.
|
||||||
|
func (mr *MockOrderExecutorMockRecorder) ClosePosition(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
varargs := append([]interface{}{arg0, arg1}, arg2...)
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClosePosition", reflect.TypeOf((*MockOrderExecutor)(nil).ClosePosition), varargs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GracefulCancel mocks base method.
|
||||||
|
func (m *MockOrderExecutor) GracefulCancel(arg0 context.Context, arg1 ...types.Order) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
varargs := []interface{}{arg0}
|
||||||
|
for _, a := range arg1 {
|
||||||
|
varargs = append(varargs, a)
|
||||||
|
}
|
||||||
|
ret := m.ctrl.Call(m, "GracefulCancel", varargs...)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GracefulCancel indicates an expected call of GracefulCancel.
|
||||||
|
func (mr *MockOrderExecutorMockRecorder) GracefulCancel(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
varargs := append([]interface{}{arg0}, arg1...)
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GracefulCancel", reflect.TypeOf((*MockOrderExecutor)(nil).GracefulCancel), varargs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitOrders mocks base method.
|
||||||
|
func (m *MockOrderExecutor) SubmitOrders(arg0 context.Context, arg1 ...types.SubmitOrder) (types.OrderSlice, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
varargs := []interface{}{arg0}
|
||||||
|
for _, a := range arg1 {
|
||||||
|
varargs = append(varargs, a)
|
||||||
|
}
|
||||||
|
ret := m.ctrl.Call(m, "SubmitOrders", varargs...)
|
||||||
|
ret0, _ := ret[0].(types.OrderSlice)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitOrders indicates an expected call of SubmitOrders.
|
||||||
|
func (mr *MockOrderExecutorMockRecorder) SubmitOrders(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
varargs := append([]interface{}{arg0}, arg1...)
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitOrders", reflect.TypeOf((*MockOrderExecutor)(nil).SubmitOrders), varargs...)
|
||||||
|
}
|
|
@ -28,6 +28,13 @@ func init() {
|
||||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks/order_executor.go -package=mocks . OrderExecutor
|
||||||
|
type OrderExecutor interface {
|
||||||
|
SubmitOrders(ctx context.Context, submitOrders ...types.SubmitOrder) (types.OrderSlice, error)
|
||||||
|
ClosePosition(ctx context.Context, percentage fixedpoint.Value, tags ...string) error
|
||||||
|
GracefulCancel(ctx context.Context, orders ...types.Order) error
|
||||||
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
Environment *bbgo.Environment
|
Environment *bbgo.Environment
|
||||||
|
|
||||||
|
@ -102,7 +109,7 @@ type Strategy struct {
|
||||||
session *bbgo.ExchangeSession
|
session *bbgo.ExchangeSession
|
||||||
orderQueryService types.ExchangeOrderQueryService
|
orderQueryService types.ExchangeOrderQueryService
|
||||||
|
|
||||||
orderExecutor *bbgo.GeneralOrderExecutor
|
orderExecutor OrderExecutor
|
||||||
historicalTrades *bbgo.TradeStore
|
historicalTrades *bbgo.TradeStore
|
||||||
|
|
||||||
// groupID is the group ID used for the strategy instance for canceling orders
|
// groupID is the group ID used for the strategy instance for canceling orders
|
||||||
|
@ -132,31 +139,12 @@ func (s *Strategy) Validate() error {
|
||||||
return fmt.Errorf("gridNum can not be zero")
|
return fmt.Errorf("gridNum can not be zero")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.FeeRate.IsZero() {
|
if err := s.checkSpread(); err != nil {
|
||||||
s.FeeRate = fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB
|
return errors.Wrapf(err, "spread is too small, please try to reduce your gridNum or increase the price range (upperPrice and lowerPrice)")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.ProfitSpread.IsZero() {
|
if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() {
|
||||||
// the min fee rate from 2 maker/taker orders (with 0.1 rate for profit)
|
return fmt.Errorf("either quantity, amount or quoteInvestment must be set")
|
||||||
gridFeeRate := s.FeeRate.Mul(fixedpoint.NewFromFloat(2.01))
|
|
||||||
|
|
||||||
if s.ProfitSpread.Div(s.LowerPrice).Compare(gridFeeRate) < 0 {
|
|
||||||
return fmt.Errorf("profitSpread %f %s is too small for lower price, less than the fee rate: %s", s.ProfitSpread.Float64(), s.ProfitSpread.Div(s.LowerPrice).Percentage(), s.FeeRate.Percentage())
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ProfitSpread.Div(s.UpperPrice).Compare(gridFeeRate) < 0 {
|
|
||||||
return fmt.Errorf("profitSpread %f %s is too small for upper price, less than the fee rate: %s", s.ProfitSpread.Float64(), s.ProfitSpread.Div(s.UpperPrice).Percentage(), s.FeeRate.Percentage())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.QuantityOrAmount.Validate(); err != nil {
|
|
||||||
if s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() && s.BaseInvestment.IsZero() {
|
|
||||||
return fmt.Errorf("one of quantity, amount, quoteInvestment must be set")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -171,6 +159,32 @@ func (s *Strategy) InstanceID() string {
|
||||||
return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int())
|
return fmt.Sprintf("%s-%s-%d-%d-%d", ID, s.Symbol, s.GridNum, s.UpperPrice.Int(), s.LowerPrice.Int())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) checkSpread() error {
|
||||||
|
gridNum := fixedpoint.NewFromInt(s.GridNum)
|
||||||
|
spread := s.ProfitSpread
|
||||||
|
if spread.IsZero() {
|
||||||
|
spread = s.UpperPrice.Sub(s.LowerPrice).Div(gridNum)
|
||||||
|
}
|
||||||
|
|
||||||
|
feeRate := s.FeeRate
|
||||||
|
if feeRate.IsZero() {
|
||||||
|
feeRate = fixedpoint.NewFromFloat(0.075 * 0.01)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the min fee rate from 2 maker/taker orders (with 0.1 rate for profit)
|
||||||
|
gridFeeRate := feeRate.Mul(fixedpoint.NewFromFloat(2.01))
|
||||||
|
|
||||||
|
if spread.Div(s.LowerPrice).Compare(gridFeeRate) < 0 {
|
||||||
|
return fmt.Errorf("profitSpread %f %s is too small for lower price, less than the grid fee rate: %s", spread.Float64(), spread.Div(s.LowerPrice).Percentage(), gridFeeRate.Percentage())
|
||||||
|
}
|
||||||
|
|
||||||
|
if spread.Div(s.UpperPrice).Compare(gridFeeRate) < 0 {
|
||||||
|
return fmt.Errorf("profitSpread %f %s is too small for upper price, less than the grid fee rate: %s", spread.Float64(), spread.Div(s.UpperPrice).Percentage(), gridFeeRate.Percentage())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Strategy) handleOrderCanceled(o types.Order) {
|
func (s *Strategy) handleOrderCanceled(o types.Order) {
|
||||||
s.logger.Infof("GRID ORDER CANCELED: %s", o.String())
|
s.logger.Infof("GRID ORDER CANCELED: %s", o.String())
|
||||||
|
|
||||||
|
@ -606,7 +620,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv
|
||||||
|
|
||||||
quoteSideQuantity := quoteInvestment.Div(totalQuotePrice)
|
quoteSideQuantity := quoteInvestment.Div(totalQuotePrice)
|
||||||
if maxNumberOfSellOrders > 0 {
|
if maxNumberOfSellOrders > 0 {
|
||||||
return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil
|
return fixedpoint.Min(quoteSideQuantity, maxBaseQuantity), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return quoteSideQuantity, nil
|
return quoteSideQuantity, nil
|
||||||
|
@ -698,6 +712,12 @@ func (s *Strategy) closeGrid(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Strategy) newGrid() *Grid {
|
||||||
|
grid := NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
|
||||||
|
grid.CalculateArithmeticPins()
|
||||||
|
return grid
|
||||||
|
}
|
||||||
|
|
||||||
// openGrid
|
// openGrid
|
||||||
// 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate.
|
// 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate.
|
||||||
// 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment.
|
// 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment.
|
||||||
|
@ -708,9 +728,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize)
|
s.grid = s.newGrid()
|
||||||
s.grid.CalculateArithmeticPins()
|
|
||||||
|
|
||||||
s.logger.Info("OPENING GRID: ", s.grid.String())
|
s.logger.Info("OPENING GRID: ", s.grid.String())
|
||||||
|
|
||||||
lastPrice, err := s.getLastTradePrice(ctx, session)
|
lastPrice, err := s.getLastTradePrice(ctx, session)
|
||||||
|
@ -950,7 +968,7 @@ func (s *Strategy) checkMinimalQuoteInvestment() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||||
instanceID := s.InstanceID()
|
instanceID := s.InstanceID()
|
||||||
|
|
||||||
s.session = session
|
s.session = session
|
||||||
|
@ -997,19 +1015,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.historicalTrades.EnablePrune = true
|
s.historicalTrades.EnablePrune = true
|
||||||
s.historicalTrades.BindStream(session.UserDataStream)
|
s.historicalTrades.BindStream(session.UserDataStream)
|
||||||
|
|
||||||
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
|
orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
|
||||||
s.orderExecutor.BindEnvironment(s.Environment)
|
orderExecutor.BindEnvironment(s.Environment)
|
||||||
s.orderExecutor.BindProfitStats(s.ProfitStats)
|
orderExecutor.BindProfitStats(s.ProfitStats)
|
||||||
s.orderExecutor.Bind()
|
orderExecutor.Bind()
|
||||||
|
orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) {
|
||||||
s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) {
|
|
||||||
s.GridProfitStats.AddTrade(trade)
|
s.GridProfitStats.AddTrade(trade)
|
||||||
})
|
})
|
||||||
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
|
orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
|
||||||
bbgo.Sync(ctx, s)
|
bbgo.Sync(ctx, s)
|
||||||
})
|
})
|
||||||
|
orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled)
|
||||||
s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled)
|
s.orderExecutor = orderExecutor
|
||||||
|
|
||||||
// TODO: detect if there are previous grid orders on the order book
|
// TODO: detect if there are previous grid orders on the order book
|
||||||
if s.ClearOpenOrdersWhenStart {
|
if s.ClearOpenOrdersWhenStart {
|
||||||
|
|
|
@ -15,6 +15,8 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
"github.com/c9s/bbgo/pkg/types/mocks"
|
"github.com/c9s/bbgo/pkg/types/mocks"
|
||||||
|
|
||||||
|
gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) {
|
func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) {
|
||||||
|
@ -269,6 +271,7 @@ func newTestStrategy() *Strategy {
|
||||||
|
|
||||||
s := &Strategy{
|
s := &Strategy{
|
||||||
logger: logrus.NewEntry(logrus.New()),
|
logger: logrus.NewEntry(logrus.New()),
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
Market: market,
|
Market: market,
|
||||||
GridProfitStats: newGridProfitStats(market),
|
GridProfitStats: newGridProfitStats(market),
|
||||||
UpperPrice: number(20_000),
|
UpperPrice: number(20_000),
|
||||||
|
@ -397,6 +400,326 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) {
|
||||||
assert.Equal(t, "0.01", baseFee.String())
|
assert.Equal(t, "0.01", baseFee.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Tag: "grid",
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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,
|
||||||
|
Tag: "grid",
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
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,
|
||||||
|
Tag: "grid",
|
||||||
|
}
|
||||||
|
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.09999999),
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
Market: s.Market,
|
||||||
|
Tag: "grid",
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
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,
|
||||||
|
Tag: "grid",
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
Quantity: number(0.1090909),
|
||||||
|
Side: types.SideTypeBuy,
|
||||||
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
Market: s.Market,
|
||||||
|
Tag: "grid",
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) {
|
||||||
s := newTestStrategy()
|
s := newTestStrategy()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user