diff --git a/config/grid2-max.yaml b/config/grid2-max.yaml index f9c751cb42..dea3da8097 100644 --- a/config/grid2-max.yaml +++ b/config/grid2-max.yaml @@ -36,7 +36,7 @@ exchangeStrategies: symbol: BTCUSDT upperPrice: 18_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. ## when compound is disabled, fixed quantity is used for each grid order. @@ -70,12 +70,12 @@ exchangeStrategies: # amount: 10.0 ## 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. ## 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. - quoteInvestment: 10_000 + # quoteInvestment: 10_000 # baseInvestment: 1.0 feeRate: 0.075% diff --git a/pkg/strategy/grid2/mocks/order_executor.go b/pkg/strategy/grid2/mocks/order_executor.go new file mode 100644 index 0000000000..0d25bc1d68 --- /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 ddeb411cca..8cb34f488b 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 @@ -132,31 +139,12 @@ func (s *Strategy) Validate() error { return fmt.Errorf("gridNum can not be zero") } - if s.FeeRate.IsZero() { - s.FeeRate = fixedpoint.NewFromFloat(0.1 * 0.01) // 0.1%, 0.075% with BNB + if err := s.checkSpread(); err != nil { + 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() { - // the min fee rate from 2 maker/taker orders (with 0.1 rate for profit) - 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") + if !s.QuantityOrAmount.IsSet() && s.QuoteInvestment.IsZero() { + return fmt.Errorf("either quantity, amount or quoteInvestment must be set") } 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()) } +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) { s.logger.Infof("GRID ORDER CANCELED: %s", o.String()) @@ -606,7 +620,7 @@ func (s *Strategy) calculateQuoteBaseInvestmentQuantity(quoteInvestment, baseInv quoteSideQuantity := quoteInvestment.Div(totalQuotePrice) if maxNumberOfSellOrders > 0 { - return fixedpoint.Max(quoteSideQuantity, maxBaseQuantity), nil + return fixedpoint.Min(quoteSideQuantity, maxBaseQuantity), nil } return quoteSideQuantity, nil @@ -698,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. @@ -708,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) @@ -950,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 @@ -997,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 adcd8de1b5..a2f73a5d7d 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,326 @@ 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, + }) + }) + + 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) { s := newTestStrategy()