diff --git a/pkg/strategy/grid2/mocks/order_executor.go b/pkg/strategy/grid2/mocks/order_executor.go new file mode 100644 index 000000000..0d25bc1d6 --- /dev/null +++ b/pkg/strategy/grid2/mocks/order_executor.go @@ -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...) +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index a8e58f00c..8cb34f488 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -28,6 +28,13 @@ func init() { 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 { Environment *bbgo.Environment @@ -102,7 +109,7 @@ type Strategy struct { session *bbgo.ExchangeSession orderQueryService types.ExchangeOrderQueryService - orderExecutor *bbgo.GeneralOrderExecutor + orderExecutor OrderExecutor historicalTrades *bbgo.TradeStore // groupID is the group ID used for the strategy instance for canceling orders @@ -705,6 +712,12 @@ func (s *Strategy) closeGrid(ctx context.Context) error { 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 // 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. @@ -715,9 +728,7 @@ func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) return nil } - s.grid = NewGrid(s.LowerPrice, s.UpperPrice, fixedpoint.NewFromInt(s.GridNum), s.Market.TickSize) - s.grid.CalculateArithmeticPins() - + s.grid = s.newGrid() s.logger.Info("OPENING GRID: ", s.grid.String()) lastPrice, err := s.getLastTradePrice(ctx, session) @@ -957,7 +968,7 @@ func (s *Strategy) checkMinimalQuoteInvestment() error { 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() s.session = session @@ -1004,19 +1015,18 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.historicalTrades.EnablePrune = true s.historicalTrades.BindStream(session.UserDataStream) - s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.orderExecutor.BindEnvironment(s.Environment) - s.orderExecutor.BindProfitStats(s.ProfitStats) - s.orderExecutor.Bind() - - s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { + orderExecutor := bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + orderExecutor.BindEnvironment(s.Environment) + orderExecutor.BindProfitStats(s.ProfitStats) + orderExecutor.Bind() + orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _, _ fixedpoint.Value) { s.GridProfitStats.AddTrade(trade) }) - s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { bbgo.Sync(ctx, s) }) - - s.orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + orderExecutor.ActiveMakerOrders().OnFilled(s.handleOrderFilled) + s.orderExecutor = orderExecutor // TODO: detect if there are previous grid orders on the order book if s.ClearOpenOrdersWhenStart { diff --git a/pkg/strategy/grid2/strategy_test.go b/pkg/strategy/grid2/strategy_test.go index adcd8de1b..2bd8b9147 100644 --- a/pkg/strategy/grid2/strategy_test.go +++ b/pkg/strategy/grid2/strategy_test.go @@ -15,6 +15,8 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types/mocks" + + gridmocks "github.com/c9s/bbgo/pkg/strategy/grid2/mocks" ) func TestStrategy_checkRequiredInvestmentByQuantity(t *testing.T) { @@ -269,6 +271,7 @@ func newTestStrategy() *Strategy { s := &Strategy{ logger: logrus.NewEntry(logrus.New()), + Symbol: "BTCUSDT", Market: market, GridProfitStats: newGridProfitStats(market), UpperPrice: number(20_000), @@ -397,6 +400,75 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) { 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, + }) + }) +} + func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { s := newTestStrategy()