From abee61cdc46bc37ef2692c76966e6097b06dd687 Mon Sep 17 00:00:00 2001 From: c9s Date: Tue, 28 Jun 2022 14:34:56 +0800 Subject: [PATCH] backtest: fix stop order backtest, add more test cases and assertions --- pkg/backtest/matching.go | 109 ++++++++++++++++++---------------- pkg/backtest/matching_test.go | 87 ++++++++++++++++++++++++--- 2 files changed, 137 insertions(+), 59 deletions(-) diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index dd8f057a8..a89c72ded 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -311,8 +311,8 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ switch o.Type { case types.OrderTypeStopMarket: - // should we trigger the order - if o.StopPrice.Compare(price) <= 0 { + // the price is still lower than the stop price, we will put the order back to the list + if price.Compare(o.StopPrice) < 0 { // not triggering it, put it back bidOrders = append(bidOrders, o) break @@ -325,8 +325,8 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ closedOrders = append(closedOrders, o) case types.OrderTypeStopLimit: - // should we trigger the order? - if price.Compare(o.StopPrice) <= 0 { + // the price is still lower than the stop price, we will put the order back to the list + if price.Compare(o.StopPrice) < 0 { bidOrders = append(bidOrders, o) break } @@ -338,7 +338,10 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ // is it a taker order? // higher than the current price, then it's a taker order if o.Price.Compare(price) >= 0 { - // taker order, move it to the closed order + // limit buy taker order, move it to the closed order + // we assume that we have no price slippage here, so the latest price will be the executed price + // TODO: simulate slippage here + o.Price = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -358,7 +361,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ case types.OrderTypeStopMarket: // should we trigger the order - if price.Compare(o.StopPrice) <= 0 { + if price.Compare(o.StopPrice) < 0 { // not triggering it, put it back askOrders = append(askOrders, o) break @@ -372,7 +375,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ case types.OrderTypeStopLimit: // should we trigger the order? - if price.Compare(o.StopPrice) <= 0 { + if price.Compare(o.StopPrice) < 0 { askOrders = append(askOrders, o) break } @@ -381,11 +384,11 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ // is it a taker order? // higher than the current price, then it's a taker order - if o.Price.Compare(price) >= 0 { - // price protection, added by @zenix - if o.Price.Compare(m.LastKLine.Low) < 0 { - o.Price = m.LastKLine.Low - } + if o.Price.Compare(price) <= 0 { + // limit sell order as taker, move it to the closed order + // we assume that we have no price slippage here, so the latest price will be the executed price + // TODO: simulate slippage here + o.Price = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled @@ -397,9 +400,6 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ case types.OrderTypeLimit, types.OrderTypeLimitMaker: if price.Compare(o.Price) >= 0 { - if o.Price.Compare(m.LastKLine.Low) < 0 { - o.Price = m.LastKLine.Low - } o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -432,40 +432,44 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [ return closedOrders, trades } +// SellToPrice simulates the price trend in down direction. +// When price goes down, buy orders should be executed, and the stop orders should be triggered. func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { klineMatchingLogger.Debugf("kline sell to price %s", price.String()) - var sellPrice = price - + // in this section we handle --- the price goes lower, and we trigger the stop sell var askOrders []types.Order for _, o := range m.askOrders { switch o.Type { case types.OrderTypeStopMarket: // should we trigger the order - if o.StopPrice.Compare(sellPrice) >= 0 { - o.Type = types.OrderTypeMarket - o.ExecutedQuantity = o.Quantity - o.Price = sellPrice - o.Status = types.OrderStatusFilled - closedOrders = append(closedOrders, o) - } else { + if price.Compare(o.StopPrice) > 0 { askOrders = append(askOrders, o) + break } + o.Type = types.OrderTypeMarket + o.ExecutedQuantity = o.Quantity + o.Price = price + o.Status = types.OrderStatusFilled + closedOrders = append(closedOrders, o) + case types.OrderTypeStopLimit: // if the price is lower than the stop price // we should trigger the stop sell order - if sellPrice.Compare(o.StopPrice) > 0 { + if price.Compare(o.StopPrice) > 0 { askOrders = append(askOrders, o) break } o.Type = types.OrderTypeLimit + // handle TAKER SELL // if the order price is lower than the current price // it's a taker order - if o.Price.Compare(sellPrice) <= 0 { + if o.Price.Compare(price) <= 0 { + o.Price = price o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -485,38 +489,39 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders case types.OrderTypeStopMarket: // should we trigger the order - if o.StopPrice.Compare(sellPrice) >= 0 { - o.Type = types.OrderTypeMarket + if o.StopPrice.Compare(price) < 0 { + bidOrders = append(bidOrders, o) + break + } + + o.Type = types.OrderTypeMarket + o.ExecutedQuantity = o.Quantity + o.Price = price + o.Status = types.OrderStatusFilled + closedOrders = append(closedOrders, o) + + case types.OrderTypeStopLimit: + // if the price is lower than the stop order price + // we should trigger the stop order + if o.StopPrice.Compare(price) < 0 { + bidOrders = append(bidOrders, o) + break + } + + o.Type = types.OrderTypeLimit + + // taker order? + if o.Price.Compare(price) >= 0 { + o.Price = price o.ExecutedQuantity = o.Quantity - o.Price = sellPrice o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) } else { bidOrders = append(bidOrders, o) } - case types.OrderTypeStopLimit: - // if the price is lower than the stop price - // we should trigger the stop order - if sellPrice.Compare(o.StopPrice) <= 0 { - o.Type = types.OrderTypeLimit - - if o.Price.Compare(sellPrice) <= 0 { - if o.Price.Compare(m.LastKLine.High) > 0 { - o.Price = m.LastKLine.High - } - o.ExecutedQuantity = o.Quantity - o.Status = types.OrderStatusFilled - closedOrders = append(closedOrders, o) - } else { - bidOrders = append(bidOrders, o) - } - } else { - bidOrders = append(bidOrders, o) - } - case types.OrderTypeLimit, types.OrderTypeLimitMaker: - if sellPrice.Compare(o.Price) <= 0 { + if price.Compare(o.Price) <= 0 { o.ExecutedQuantity = o.Quantity o.Status = types.OrderStatusFilled closedOrders = append(closedOrders, o) @@ -570,7 +575,6 @@ func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) { func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.CurrentTime = kline.EndTime.Time() - m.LastKLine = kline if m.LastPrice.IsZero() { m.LastPrice = kline.Open } else { @@ -610,8 +614,9 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) { if m.LastPrice.IsZero() { m.BuyToPrice(kline.Close) } - } + + m.LastKLine = kline } func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order { diff --git a/pkg/backtest/matching_test.go b/pkg/backtest/matching_test.go index 269f52563..2c642c9f6 100644 --- a/pkg/backtest/matching_test.go +++ b/pkg/backtest/matching_test.go @@ -187,7 +187,7 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) { LastPrice: fixedpoint.NewFromFloat(19000.0), } - stopOrder := types.SubmitOrder{ + stopBuyOrder := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeStopLimit, @@ -196,14 +196,24 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) { StopPrice: fixedpoint.NewFromFloat(21000.0), TimeInForce: types.TimeInForceGTC, } - createdOrder, trade, err := engine.PlaceOrder(stopOrder) + 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") @@ -211,7 +221,30 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) { assert.Equal(t, types.OrderStatusFilled, closedOrders[0].Status) assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) - assert.Equal(t, stopOrder.Price, trades[0].Price) + assert.Equal(t, "21001", trades[0].Price.String()) + assert.Equal(t, "21001", closedOrders[0].Price.String(), "order.Price should 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) { @@ -224,7 +257,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) { LastPrice: fixedpoint.NewFromFloat(22000.0), } - stopOrder := types.SubmitOrder{ + stopSellOrder := types.SubmitOrder{ Symbol: market.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeStopLimit, @@ -233,22 +266,62 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) { StopPrice: fixedpoint.NewFromFloat(21000.0), TimeInForce: types.TimeInForceGTC, } - createdOrder, trade, err := engine.PlaceOrder(stopOrder) + 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, stopOrder.Price, trades[0].Price) + assert.Equal(t, "20990", closedOrders[0].Price.String()) + 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) { @@ -285,7 +358,7 @@ func TestSimplePriceMatching_StopMarketOrderSell(t *testing.T) { 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) + 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) {