backtest: fix stop order backtest, add more test cases and assertions

This commit is contained in:
c9s 2022-06-28 14:34:56 +08:00
parent 09e98eed82
commit abee61cdc4
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
2 changed files with 137 additions and 59 deletions

View File

@ -311,8 +311,8 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
switch o.Type { switch o.Type {
case types.OrderTypeStopMarket: case types.OrderTypeStopMarket:
// should we trigger the order // the price is still lower than the stop price, we will put the order back to the list
if o.StopPrice.Compare(price) <= 0 { if price.Compare(o.StopPrice) < 0 {
// not triggering it, put it back // not triggering it, put it back
bidOrders = append(bidOrders, o) bidOrders = append(bidOrders, o)
break break
@ -325,8 +325,8 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
closedOrders = append(closedOrders, o) closedOrders = append(closedOrders, o)
case types.OrderTypeStopLimit: case types.OrderTypeStopLimit:
// should we trigger the order? // the price is still lower than the stop price, we will put the order back to the list
if price.Compare(o.StopPrice) <= 0 { if price.Compare(o.StopPrice) < 0 {
bidOrders = append(bidOrders, o) bidOrders = append(bidOrders, o)
break break
} }
@ -338,7 +338,10 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
// is it a taker order? // is it a taker order?
// higher than the current price, then it's a taker order // higher than the current price, then it's a taker order
if o.Price.Compare(price) >= 0 { 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.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o) closedOrders = append(closedOrders, o)
@ -358,7 +361,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
case types.OrderTypeStopMarket: case types.OrderTypeStopMarket:
// should we trigger the order // should we trigger the order
if price.Compare(o.StopPrice) <= 0 { if price.Compare(o.StopPrice) < 0 {
// not triggering it, put it back // not triggering it, put it back
askOrders = append(askOrders, o) askOrders = append(askOrders, o)
break break
@ -372,7 +375,7 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
case types.OrderTypeStopLimit: case types.OrderTypeStopLimit:
// should we trigger the order? // should we trigger the order?
if price.Compare(o.StopPrice) <= 0 { if price.Compare(o.StopPrice) < 0 {
askOrders = append(askOrders, o) askOrders = append(askOrders, o)
break break
} }
@ -381,11 +384,11 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
// is it a taker order? // is it a taker order?
// higher than the current price, then it's a taker order // higher than the current price, then it's a taker order
if o.Price.Compare(price) >= 0 { if o.Price.Compare(price) <= 0 {
// price protection, added by @zenix // limit sell order as taker, move it to the closed order
if o.Price.Compare(m.LastKLine.Low) < 0 { // we assume that we have no price slippage here, so the latest price will be the executed price
o.Price = m.LastKLine.Low // TODO: simulate slippage here
} o.Price = price
o.ExecutedQuantity = o.Quantity o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled o.Status = types.OrderStatusFilled
@ -397,9 +400,6 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
case types.OrderTypeLimit, types.OrderTypeLimitMaker: case types.OrderTypeLimit, types.OrderTypeLimitMaker:
if price.Compare(o.Price) >= 0 { if price.Compare(o.Price) >= 0 {
if o.Price.Compare(m.LastKLine.Low) < 0 {
o.Price = m.LastKLine.Low
}
o.ExecutedQuantity = o.Quantity o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o) closedOrders = append(closedOrders, o)
@ -432,40 +432,44 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
return closedOrders, trades 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) { func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
klineMatchingLogger.Debugf("kline sell to price %s", price.String()) 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 var askOrders []types.Order
for _, o := range m.askOrders { for _, o := range m.askOrders {
switch o.Type { switch o.Type {
case types.OrderTypeStopMarket: case types.OrderTypeStopMarket:
// should we trigger the order // should we trigger the order
if o.StopPrice.Compare(sellPrice) >= 0 { if price.Compare(o.StopPrice) > 0 {
o.Type = types.OrderTypeMarket
o.ExecutedQuantity = o.Quantity
o.Price = sellPrice
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
} else {
askOrders = append(askOrders, o) 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: case types.OrderTypeStopLimit:
// if the price is lower than the stop price // if the price is lower than the stop price
// we should trigger the stop sell order // we should trigger the stop sell order
if sellPrice.Compare(o.StopPrice) > 0 { if price.Compare(o.StopPrice) > 0 {
askOrders = append(askOrders, o) askOrders = append(askOrders, o)
break break
} }
o.Type = types.OrderTypeLimit o.Type = types.OrderTypeLimit
// handle TAKER SELL
// if the order price is lower than the current price // if the order price is lower than the current price
// it's a taker order // 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.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o) closedOrders = append(closedOrders, o)
@ -485,38 +489,39 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
case types.OrderTypeStopMarket: case types.OrderTypeStopMarket:
// should we trigger the order // should we trigger the order
if o.StopPrice.Compare(sellPrice) >= 0 { if o.StopPrice.Compare(price) < 0 {
o.Type = types.OrderTypeMarket 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.ExecutedQuantity = o.Quantity
o.Price = sellPrice
o.Status = types.OrderStatusFilled o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o) closedOrders = append(closedOrders, o)
} else { } else {
bidOrders = append(bidOrders, o) 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: case types.OrderTypeLimit, types.OrderTypeLimitMaker:
if sellPrice.Compare(o.Price) <= 0 { if price.Compare(o.Price) <= 0 {
o.ExecutedQuantity = o.Quantity o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o) closedOrders = append(closedOrders, o)
@ -570,7 +575,6 @@ func (m *SimplePriceMatching) getOrder(orderID uint64) (types.Order, bool) {
func (m *SimplePriceMatching) processKLine(kline types.KLine) { func (m *SimplePriceMatching) processKLine(kline types.KLine) {
m.CurrentTime = kline.EndTime.Time() m.CurrentTime = kline.EndTime.Time()
m.LastKLine = kline
if m.LastPrice.IsZero() { if m.LastPrice.IsZero() {
m.LastPrice = kline.Open m.LastPrice = kline.Open
} else { } else {
@ -610,8 +614,9 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) {
if m.LastPrice.IsZero() { if m.LastPrice.IsZero() {
m.BuyToPrice(kline.Close) m.BuyToPrice(kline.Close)
} }
} }
m.LastKLine = kline
} }
func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order { func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order {

View File

@ -187,7 +187,7 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
LastPrice: fixedpoint.NewFromFloat(19000.0), LastPrice: fixedpoint.NewFromFloat(19000.0),
} }
stopOrder := types.SubmitOrder{ stopBuyOrder := types.SubmitOrder{
Symbol: market.Symbol, Symbol: market.Symbol,
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
Type: types.OrderTypeStopLimit, Type: types.OrderTypeStopLimit,
@ -196,14 +196,24 @@ func TestSimplePriceMatching_StopLimitOrderBuy(t *testing.T) {
StopPrice: fixedpoint.NewFromFloat(21000.0), StopPrice: fixedpoint.NewFromFloat(21000.0),
TimeInForce: types.TimeInForceGTC, TimeInForce: types.TimeInForceGTC,
} }
createdOrder, trade, err := engine.PlaceOrder(stopOrder) createdOrder, trade, err := engine.PlaceOrder(stopBuyOrder)
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, trade, "place stop order should not trigger the stop buy") 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.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)) 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, 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.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)) closedOrders, trades = engine.BuyToPrice(fixedpoint.NewFromFloat(21001.0))
assert.Len(t, closedOrders, 1, "should trigger the stop buy order") 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.OrderStatusFilled, closedOrders[0].Status)
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) 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) { func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
@ -224,7 +257,7 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
LastPrice: fixedpoint.NewFromFloat(22000.0), LastPrice: fixedpoint.NewFromFloat(22000.0),
} }
stopOrder := types.SubmitOrder{ stopSellOrder := types.SubmitOrder{
Symbol: market.Symbol, Symbol: market.Symbol,
Side: types.SideTypeSell, Side: types.SideTypeSell,
Type: types.OrderTypeStopLimit, Type: types.OrderTypeStopLimit,
@ -233,22 +266,62 @@ func TestSimplePriceMatching_StopLimitOrderSell(t *testing.T) {
StopPrice: fixedpoint.NewFromFloat(21000.0), StopPrice: fixedpoint.NewFromFloat(21000.0),
TimeInForce: types.TimeInForceGTC, TimeInForce: types.TimeInForceGTC,
} }
createdOrder, trade, err := engine.PlaceOrder(stopOrder) createdOrder, trade, err := engine.PlaceOrder(stopSellOrder)
assert.NoError(t, err) assert.NoError(t, err)
assert.Nil(t, trade, "place stop order should not trigger the stop sell") 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") 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)) 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, 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.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)) closedOrders, trades = engine.SellToPrice(fixedpoint.NewFromFloat(20990.0))
assert.Len(t, closedOrders, 1, "should trigger the stop sell order") assert.Len(t, closedOrders, 1, "should trigger the stop sell order")
assert.Len(t, trades, 1, "should have stop order trade executed") 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.OrderStatusFilled, closedOrders[0].Status)
assert.Equal(t, types.OrderTypeLimit, closedOrders[0].Type) 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) { 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.OrderStatusFilled, closedOrders[0].Status)
assert.Equal(t, types.OrderTypeMarket, closedOrders[0].Type) 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) { func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {