Merge pull request #772 from c9s/fix/backtest-stop-order

backtest: fix stop order backtest, add more test cases and assertions
This commit is contained in:
Yo-An Lin 2022-06-28 15:30:19 +08:00 committed by GitHub
commit 74d74a2d0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 {
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 {

View File

@ -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) {