package backtest import ( "testing" "time" "github.com/stretchr/testify/assert" "git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint" "git.qtrade.icu/lychiyu/qbtrade/pkg/types" ) func newLimitOrder(symbol string, side types.SideType, price, quantity float64) types.SubmitOrder { return types.SubmitOrder{ Symbol: symbol, Side: side, Type: types.OrderTypeLimit, Quantity: fixedpoint.NewFromFloat(quantity), Price: fixedpoint.NewFromFloat(price), TimeInForce: types.TimeInForceGTC, } } func TestSimplePriceMatching_orderUpdate(t *testing.T) { account := &types.Account{ MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), } account.UpdateBalances(types.BalanceMap{ "USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(10000.0)}, }) market := types.Market{ Symbol: "BTCUSDT", PricePrecision: 8, VolumePrecision: 8, QuoteCurrency: "USDT", BaseCurrency: "BTC", MinNotional: fixedpoint.MustNewFromString("0.001"), MinAmount: fixedpoint.MustNewFromString("10.0"), MinQuantity: fixedpoint.MustNewFromString("0.001"), StepSize: fixedpoint.MustNewFromString("0.00001"), TickSize: fixedpoint.MustNewFromString("0.01"), } t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC) engine := &SimplePriceMatching{ account: account, Market: market, currentTime: t1, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(25000), } orderUpdateCnt := 0 orderUpdateNewStatusCnt := 0 orderUpdateFilledStatusCnt := 0 var lastOrder types.Order engine.OnOrderUpdate(func(order types.Order) { lastOrder = order orderUpdateCnt++ switch order.Status { case types.OrderStatusNew: orderUpdateNewStatusCnt++ case types.OrderStatusFilled: orderUpdateFilledStatusCnt++ } }) // maker order _, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 24000.0, 0.1)) assert.NoError(t, err) assert.Equal(t, 1, orderUpdateCnt) // should get new status assert.Equal(t, 1, orderUpdateNewStatusCnt) // should get new status assert.Equal(t, 0, orderUpdateFilledStatusCnt) // should get new status assert.Equal(t, types.OrderStatusNew, lastOrder.Status) assert.Equal(t, fixedpoint.NewFromFloat(0.0), lastOrder.ExecutedQuantity) t2 := t1.Add(time.Minute) // should match 25000, 24000 k := newKLine("BTCUSDT", types.Interval1m, t2, 26000, 27000, 23000, 25000) engine.processKLine(k) assert.Equal(t, 2, orderUpdateCnt) // should got new and filled assert.Equal(t, 1, orderUpdateNewStatusCnt) // should got new status assert.Equal(t, 1, orderUpdateFilledStatusCnt) // should got new status assert.Equal(t, types.OrderStatusFilled, lastOrder.Status) assert.Equal(t, "0.1", lastOrder.ExecutedQuantity.String()) assert.Equal(t, lastOrder.Quantity.String(), lastOrder.ExecutedQuantity.String()) } func TestSimplePriceMatching_CancelOrder(t *testing.T) { account := getTestAccount() market := getTestMarket() t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC) engine := &SimplePriceMatching{ account: account, Market: market, currentTime: t1, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(30000.0), } createdOrder1, trade1, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 20000.0, 0.1)) assert.NoError(t, err) assert.Nil(t, trade1) assert.Len(t, engine.bidOrders, 1) assert.Len(t, engine.askOrders, 0) createdOrder2, trade2, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 40000.0, 0.1)) assert.NoError(t, err) assert.Nil(t, trade2) assert.Len(t, engine.bidOrders, 1) assert.Len(t, engine.askOrders, 1) if assert.NotNil(t, createdOrder1) { retOrder, err := engine.CancelOrder(*createdOrder1) assert.NoError(t, err) assert.NotNil(t, retOrder) assert.Len(t, engine.bidOrders, 0) assert.Len(t, engine.askOrders, 1) } if assert.NotNil(t, createdOrder2) { retOrder, err := engine.CancelOrder(*createdOrder2) assert.NoError(t, err) assert.NotNil(t, retOrder) assert.Len(t, engine.bidOrders, 0) assert.Len(t, engine.askOrders, 0) } } func TestSimplePriceMatching_processKLine(t *testing.T) { account := getTestAccount() market := getTestMarket() t1 := time.Date(2021, 7, 1, 0, 0, 0, 0, time.UTC) engine := &SimplePriceMatching{ account: account, Market: market, currentTime: t1, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(30000.0), } for i := 0; i <= 5; i++ { var p = 20000.0 + float64(i)*1000.0 _, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, p, 0.001)) assert.NoError(t, err) } t2 := t1.Add(time.Minute) // should match 25000, 24000 k := newKLine("BTCUSDT", types.Interval1m, t2, 30000, 27000, 23000, 25000) assert.Equal(t, t2.Add(time.Minute-time.Millisecond), k.EndTime.Time()) engine.processKLine(k) assert.Equal(t, 3, len(engine.bidOrders)) assert.Len(t, engine.bidOrders, 3) assert.Equal(t, 3, len(engine.closedOrders)) for _, o := range engine.closedOrders { assert.Equal(t, k.EndTime.Time(), o.UpdateTime.Time()) } } func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h, l, c float64) types.KLine { return types.KLine{ Symbol: symbol, StartTime: types.Time(startTime), EndTime: types.Time(startTime.Add(interval.Duration() - time.Millisecond)), Interval: interval, Open: fixedpoint.NewFromFloat(o), High: fixedpoint.NewFromFloat(h), Low: fixedpoint.NewFromFloat(l), Close: fixedpoint.NewFromFloat(c), Closed: true, } } // getTestMarket returns the BTCUSDT market information // for tests, we always use BTCUSDT func getTestMarket() types.Market { market := types.Market{ Symbol: "BTCUSDT", PricePrecision: 8, VolumePrecision: 8, QuoteCurrency: "USDT", BaseCurrency: "BTC", MinNotional: fixedpoint.MustNewFromString("0.001"), MinAmount: fixedpoint.MustNewFromString("10.0"), MinQuantity: fixedpoint.MustNewFromString("0.001"), StepSize: fixedpoint.MustNewFromString("0.00001"), TickSize: fixedpoint.MustNewFromString("0.01"), } return market } func getTestAccount() *types.Account { account := &types.Account{ MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01), } account.UpdateBalances(types.BalanceMap{ "USDT": {Currency: "USDT", Available: fixedpoint.NewFromFloat(1000000.0)}, "BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)}, }) return account } func TestSimplePriceMatching_LimitBuyTakerOrder(t *testing.T) { account := getTestAccount() market := getTestMarket() engine := &SimplePriceMatching{ account: account, Market: market, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(19000.0), } takerOrder := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeLimit, Quantity: fixedpoint.NewFromFloat(0.1), Price: fixedpoint.NewFromFloat(20000.0), TimeInForce: types.TimeInForceGTC, } createdOrder, trade, err := engine.PlaceOrder(takerOrder) assert.NoError(t, err) t.Logf("created order: %+v", createdOrder) t.Logf("executed trade: %+v", trade) assert.Equal(t, "19000", trade.Price.String()) assert.Equal(t, "19000", createdOrder.AveragePrice.String()) assert.Equal(t, "20000", createdOrder.Price.String()) usdt, ok := account.Balance("USDT") assert.True(t, ok) assert.True(t, usdt.Locked.IsZero()) btc, ok := account.Balance("BTC") assert.True(t, ok) assert.True(t, btc.Locked.IsZero()) assert.Equal(t, fixedpoint.NewFromFloat(100.0).Add(createdOrder.Quantity).String(), btc.Available.String()) usedQuoteAmount := createdOrder.AveragePrice.Mul(createdOrder.Quantity) assert.Equal(t, "USDT", trade.FeeCurrency) assert.Equal(t, usdt.Available.String(), fixedpoint.NewFromFloat(1000000.0).Sub(usedQuoteAmount).Sub(trade.Fee).String()) } func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) { account := getTestAccount() market := getTestMarket() engine := &SimplePriceMatching{ account: account, Market: market, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(19000.0), } stopBuyOrder := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeStopLimit, Quantity: fixedpoint.NewFromFloat(0.1), Price: fixedpoint.NewFromFloat(22000.0), StopPrice: fixedpoint.NewFromFloat(21000.0), TimeInForce: types.TimeInForceGTC, } createdOrder, trade, err := engine.PlaceOrder(stopBuyOrder) assert.NoError(t, err) assert.Nil(t, trade, "place stop order should not trigger the stop buy") assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy") // place some limit orders, so we ensure that the remaining orders are not removed. _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeBuy, 18000, 0.01)) assert.NoError(t, err) _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeSell, 32000, 0.01)) assert.NoError(t, err) assert.Equal(t, 2, len(engine.bidOrders)) assert.Equal(t, 1, len(engine.askOrders)) closedOrders, trades := engine.buyToPrice(fixedpoint.NewFromFloat(20000.0)) assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") assert.Equal(t, 2, len(engine.bidOrders), "bid orders should be the same") assert.Equal(t, 1, len(engine.askOrders), "ask orders should be the same") closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21001.0)) assert.Len(t, closedOrders, 1, "should trigger the stop buy order") assert.Len(t, trades, 1, "should have stop order trade executed") assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) assert.Equal(t, "21001", trades[0].Price.String()) assert.Equal(t, "22000", closedOrders[0].Price.String(), "order.Price should not be adjusted") assert.Equal(t, fixedpoint.NewFromFloat(21001.0).String(), engine.lastPrice.String()) stopOrder2 := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeStopLimit, Quantity: fixedpoint.NewFromFloat(0.1), Price: fixedpoint.NewFromFloat(22000.0), StopPrice: fixedpoint.NewFromFloat(21000.0), TimeInForce: types.TimeInForceGTC, } createdOrder, trade, err = engine.PlaceOrder(stopOrder2) assert.NoError(t, err) assert.Nil(t, trade, "place stop order should not trigger the stop buy") assert.NotNil(t, createdOrder, "place stop order should not trigger the stop buy") assert.Len(t, engine.bidOrders, 2) closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20500.0)) assert.Len(t, closedOrders, 1, "should trigger the stop buy order") assert.Len(t, trades, 1, "should have stop order trade executed") assert.Len(t, engine.bidOrders, 1, "should left one bid order") } func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) { account := getTestAccount() market := getTestMarket() engine := &SimplePriceMatching{ account: account, Market: market, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(22000.0), } stopSellOrder := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeStopLimit, Quantity: fixedpoint.NewFromFloat(0.1), Price: fixedpoint.NewFromFloat(20000.0), StopPrice: fixedpoint.NewFromFloat(21000.0), TimeInForce: types.TimeInForceGTC, } createdOrder, trade, err := engine.PlaceOrder(stopSellOrder) assert.NoError(t, err) assert.Nil(t, trade, "place stop order should not trigger the stop sell") assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") // place some limit orders, so we ensure that the remaining orders are not removed. _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeBuy, 18000, 0.01)) assert.NoError(t, err) _, _, err = engine.PlaceOrder(newLimitOrder(market.Symbol, types.SideTypeSell, 32000, 0.01)) assert.NoError(t, err) assert.Equal(t, 1, len(engine.bidOrders)) assert.Equal(t, 2, len(engine.askOrders)) closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0)) assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") assert.Equal(t, 1, len(engine.bidOrders)) assert.Equal(t, 2, len(engine.askOrders)) closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0)) assert.Len(t, closedOrders, 1, "should trigger the stop sell order") assert.Len(t, trades, 1, "should have stop order trade executed") assert.Equal(t, 1, len(engine.bidOrders)) assert.Equal(t, 1, len(engine.askOrders)) assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) assert.Equal(t, "20000", closedOrders[0].Price.String(), "limit order price should not be changed") assert.Equal(t, "20990", trades[0].Price.String()) assert.Equal(t, "20990", engine.lastPrice.String()) // place a stop limit sell order with a higher price than the current price stopOrder2 := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeStopLimit, Quantity: fixedpoint.NewFromFloat(0.1), Price: fixedpoint.NewFromFloat(20000.0), StopPrice: fixedpoint.NewFromFloat(21000.0), TimeInForce: types.TimeInForceGTC, } createdOrder, trade, err = engine.PlaceOrder(stopOrder2) assert.NoError(t, err) assert.Nil(t, trade, "place stop order should not trigger the stop sell") assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(21000.0)) if assert.Len(t, closedOrders, 1, "should trigger the stop sell order") { assert.Len(t, trades, 1, "should have stop order trade executed") assert.Equal(t, types.SideTypeSell, closedOrders[0].Side) assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) assert.Equal(t, "21000", trades[0].Price.String(), "trade price should be the kline price not the order price") assert.Equal(t, "21000", engine.lastPrice.String(), "engine last price should be updated correctly") } } func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) { account := getTestAccount() market := getTestMarket() engine := &SimplePriceMatching{ account: account, Market: market, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(22000.0), } stopOrder := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeStopMarket, Quantity: fixedpoint.NewFromFloat(0.1), Price: fixedpoint.NewFromFloat(20000.0), StopPrice: fixedpoint.NewFromFloat(21000.0), TimeInForce: types.TimeInForceGTC, } createdOrder, trade, err := engine.PlaceOrder(stopOrder) assert.NoError(t, err) assert.Nil(t, trade, "place stop order should not trigger the stop sell") assert.NotNil(t, createdOrder, "place stop order should not trigger the stop sell") closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(21500.0)) assert.Len(t, closedOrders, 0, "price change far from the price should not trigger the stop buy") assert.Len(t, trades, 0, "price change far from the price should not trigger the stop buy") closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(20990.0)) assert.Len(t, closedOrders, 1, "should trigger the stop sell order") assert.Len(t, trades, 1, "should have stop order trade executed") assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) assert.Equal(t, types.OrderTypeMarket, closedOrders[0].Type) assert.Equal(t, fixedpoint.NewFromFloat(20990.0), trades[0].Price, "trade price should be adjusted to the last price") } func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) { account := getTestAccount() market := getTestMarket() engine := &SimplePriceMatching{ account: account, Market: market, closedOrders: make(map[uint64]types.Order), } for i := 0; i < 5; i++ { _, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 8000.0-float64(i), 1.0)) assert.NoError(t, err) } assert.Len(t, engine.bidOrders, 5) assert.Len(t, engine.askOrders, 0) for i := 0; i < 5; i++ { _, _, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 9000.0+float64(i), 1.0)) assert.NoError(t, err) } assert.Len(t, engine.bidOrders, 5) assert.Len(t, engine.askOrders, 5) closedOrders, trades := engine.sellToPrice(fixedpoint.NewFromFloat(8100.0)) assert.Len(t, closedOrders, 0) assert.Len(t, trades, 0) closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(8000.0)) assert.Len(t, closedOrders, 1) assert.Len(t, trades, 1) for _, trade := range trades { assert.True(t, trade.IsBuyer) } for _, o := range closedOrders { assert.Equal(t, types.SideTypeBuy, o.Side) } closedOrders, trades = engine.sellToPrice(fixedpoint.NewFromFloat(7000.0)) assert.Len(t, closedOrders, 4) assert.Len(t, trades, 4) closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(8900.0)) assert.Len(t, closedOrders, 0) assert.Len(t, trades, 0) closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9000.0)) assert.Len(t, closedOrders, 1) assert.Len(t, trades, 1) for _, o := range closedOrders { assert.Equal(t, types.SideTypeSell, o.Side) } for _, trade := range trades { assert.Equal(t, types.SideTypeSell, trade.Side) } closedOrders, trades = engine.buyToPrice(fixedpoint.NewFromFloat(9500.0)) assert.Len(t, closedOrders, 4) assert.Len(t, trades, 4) } func TestSimplePriceMatching_LimitTakerOrder(t *testing.T) { account := getTestAccount() market := getTestMarket() engine := &SimplePriceMatching{ account: account, Market: market, closedOrders: make(map[uint64]types.Order), lastPrice: fixedpoint.NewFromFloat(20000.0), } closedOrder, trade, err := engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeBuy, 21000.0, 1.0)) assert.NoError(t, err) if assert.NotNil(t, closedOrder) { if assert.NotNil(t, trade) { assert.Equal(t, "20000", trade.Price.String()) assert.False(t, trade.IsMaker, "should be taker") } } closedOrder, trade, err = engine.PlaceOrder(newLimitOrder("BTCUSDT", types.SideTypeSell, 19000.0, 1.0)) assert.NoError(t, err) if assert.NotNil(t, closedOrder) { assert.Equal(t, "19000", closedOrder.Price.String()) if assert.NotNil(t, trade) { assert.Equal(t, "20000", trade.Price.String()) assert.False(t, trade.IsMaker, "should be taker") } } }