backtest: fix stop limit order matching

Signed-off-by: c9s <yoanlin93@gmail.com>
This commit is contained in:
c9s 2022-06-27 19:48:14 +08:00
parent 9631a2e551
commit 10d5a8a4f2
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
4 changed files with 209 additions and 51 deletions

View File

@ -124,7 +124,8 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
return o, nil return o, nil
} }
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *types.Order, trades *types.Trade, err error) { // PlaceOrder returns the created order object, executed trade (if any) and error
func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *types.Trade, error) {
// price for checking account balance, default price // price for checking account balance, default price
price := o.Price price := o.Price
@ -135,7 +136,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
} }
price = m.LastPrice price = m.LastPrice
case types.OrderTypeLimit, types.OrderTypeLimitMaker: case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker:
price = o.Price price = o.Price
} }
@ -301,11 +302,42 @@ func (m *SimplePriceMatching) newTradeFromOrder(order *types.Order, isMaker bool
} }
} }
// BuyToPrice means price go up and the limit sell should be triggered
func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) { func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders []types.Order, trades []types.Trade) {
klineMatchingLogger.Debugf("kline buy to price %s", price.String()) klineMatchingLogger.Debugf("kline buy to price %s", price.String())
var askOrders []types.Order var bidOrders []types.Order
for _, o := range m.bidOrders {
switch o.Type {
case types.OrderTypeStopLimit:
// should we trigger the order?
if price.Compare(o.StopPrice) <= 0 {
bidOrders = append(bidOrders, o)
break
}
// convert this order to limit order
// we use value object here, so it's a copy
o.Type = types.OrderTypeLimit
// 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
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
} else {
// keep it as a maker order
bidOrders = append(bidOrders, o)
}
default:
bidOrders = append(bidOrders, o)
}
}
m.bidOrders = bidOrders
var askOrders []types.Order
for _, o := range m.askOrders { for _, o := range m.askOrders {
switch o.Type { switch o.Type {
@ -333,10 +365,13 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
o.Type = types.OrderTypeLimit o.Type = types.OrderTypeLimit
// is it a taker order? // is it a taker order?
if price.Compare(o.Price) >= 0 { // 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 { if o.Price.Compare(m.LastKLine.Low) < 0 {
o.Price = m.LastKLine.Low 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)
@ -386,6 +421,37 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
klineMatchingLogger.Debugf("kline sell to price %s", price.String()) klineMatchingLogger.Debugf("kline sell to price %s", price.String())
var sellPrice = price var sellPrice = price
var askOrders []types.Order
for _, o := range m.askOrders {
switch o.Type {
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 {
askOrders = append(askOrders, o)
break
}
o.Type = types.OrderTypeLimit
// if the order price is lower than the current price
// it's a taker order
if o.Price.Compare(sellPrice) <= 0 {
o.ExecutedQuantity = o.Quantity
o.Status = types.OrderStatusFilled
closedOrders = append(closedOrders, o)
} else {
askOrders = append(askOrders, o)
}
default:
askOrders = append(askOrders, o)
}
}
m.askOrders = askOrders
var bidOrders []types.Order var bidOrders []types.Order
for _, o := range m.bidOrders { for _, o := range m.bidOrders {
switch o.Type { switch o.Type {
@ -402,11 +468,12 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
} }
case types.OrderTypeStopLimit: case types.OrderTypeStopLimit:
// should we trigger the order // if the price is lower than the stop price
// we should trigger the stop order
if sellPrice.Compare(o.StopPrice) <= 0 { if sellPrice.Compare(o.StopPrice) <= 0 {
o.Type = types.OrderTypeLimit o.Type = types.OrderTypeLimit
if sellPrice.Compare(o.Price) <= 0 { if o.Price.Compare(sellPrice) <= 0 {
if o.Price.Compare(m.LastKLine.High) > 0 { if o.Price.Compare(m.LastKLine.High) > 0 {
o.Price = m.LastKLine.High o.Price = m.LastKLine.High
} }

View File

@ -151,17 +151,7 @@ func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h,
} }
} }
func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) { func getTestMarket() types.Market {
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)},
})
market := types.Market{ market := types.Market{
Symbol: "BTCUSDT", Symbol: "BTCUSDT",
PricePrecision: 8, PricePrecision: 8,
@ -172,7 +162,98 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
MinAmount: fixedpoint.MustNewFromString("10.0"), MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("0.001"), MinQuantity: fixedpoint.MustNewFromString("0.001"),
} }
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_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),
}
stopOrder := 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(stopOrder)
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")
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")
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, stopOrder.Price, trades[0].Price)
}
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),
}
stopOrder := 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(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.OrderTypeLimit, closedOrders[0].Type)
assert.Equal(t, stopOrder.Price, trades[0].Price)
}
func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
account := getTestAccount()
market := getTestMarket()
engine := &SimplePriceMatching{ engine := &SimplePriceMatching{
Account: account, Account: account,
Market: market, Market: market,

View File

@ -220,16 +220,38 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.orderExecutor.Bind() s.orderExecutor.Bind()
store, _ := session.MarketDataStore(s.Symbol) store, _ := session.MarketDataStore(s.Symbol)
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow} s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
s.pivot.Bind(store) s.pivot.Bind(store)
if kLinesP, ok := store.KLinesOfInterval(s.IntervalWindow.Interval); ok {
s.pivot.Update(*kLinesP)
}
// update pivot low data
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
return
}
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
if lastLow.IsZero() {
return
}
if lastLow.Compare(s.lastLow) != 0 {
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
}
s.lastLow = lastLow
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
})
if s.BounceShort != nil && s.BounceShort.Enabled { if s.BounceShort != nil && s.BounceShort.Enabled {
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow} s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow}
s.resistancePivot.Bind(store) s.resistancePivot.Bind(store)
} }
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
if s.BreakLow.StopEMA != nil { if s.BreakLow.StopEMA != nil {
s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA) s.stopEWMA = standardIndicator.EWMA(*s.BreakLow.StopEMA)
} }
@ -298,6 +320,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:] s.pivotLowPrices = s.pivotLowPrices[len(s.pivotLowPrices)-10:]
} }
ratio := fixedpoint.One.Add(s.BreakLow.Ratio)
breakPrice := previousLow.Mul(ratio)
closePrice := kline.Close
// if previous low is not break, skip
if closePrice.Compare(breakPrice) >= 0 {
return
}
log.Infof("%s breakLow signal detected, closed price %f < breakPrice %f", kline.Symbol, closePrice.Float64(), breakPrice.Float64())
// stop EMA protection // stop EMA protection
if s.stopEWMA != nil && !s.BreakLow.StopEMARange.IsZero() { if s.stopEWMA != nil && !s.BreakLow.StopEMARange.IsZero() {
ema := fixedpoint.NewFromFloat(s.stopEWMA.Last()) ema := fixedpoint.NewFromFloat(s.stopEWMA.Last())
@ -306,19 +339,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
} }
emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.BreakLow.StopEMARange)) emaStopShortPrice := ema.Mul(fixedpoint.One.Sub(s.BreakLow.StopEMARange))
if kline.Close.Compare(emaStopShortPrice) < 0 { if closePrice.Compare(emaStopShortPrice) < 0 {
log.Infof("stopEMA protection: close price %f < EMA(%v) = %f", closePrice.Float64(), s.BreakLow.StopEMA, ema.Float64())
return return
} }
} }
ratio := fixedpoint.One.Add(s.BreakLow.Ratio)
breakPrice := previousLow.Mul(ratio)
// if previous low is not break, skip
if kline.Close.Compare(breakPrice) >= 0 {
return
}
_ = s.orderExecutor.GracefulCancel(ctx) _ = s.orderExecutor.GracefulCancel(ctx)
quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity) quantity := s.useQuantityOrBaseBalance(s.BreakLow.Quantity)
@ -374,27 +400,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}) })
} }
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
// StrategyController
if s.Status != types.StrategyStatusRunning {
return
}
if kline.Symbol != s.Symbol || kline.Interval != s.Interval {
return
}
if s.pivot.LastLow() > 0.0 {
lastLow := fixedpoint.NewFromFloat(s.pivot.LastLow())
if lastLow.Compare(s.lastLow) != 0 {
log.Infof("new pivot low detected: %f %s", s.pivot.LastLow(), kline.EndTime.Time())
}
s.lastLow = lastLow
s.pivotLowPrices = append(s.pivotLowPrices, s.lastLow)
}
})
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
wg.Done() wg.Done()

View File

@ -271,7 +271,7 @@ func (o Order) String() string {
orderID = strconv.FormatUint(o.OrderID, 10) orderID = strconv.FormatUint(o.OrderID, 10)
} }
return fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s | %s", desc := fmt.Sprintf("ORDER %s | %s | %s | %s | %s %-4s | %s/%s @ %s",
o.Exchange.String(), o.Exchange.String(),
o.CreationTime.Time().Local().Format(time.RFC1123), o.CreationTime.Time().Local().Format(time.RFC1123),
orderID, orderID,
@ -280,8 +280,13 @@ func (o Order) String() string {
o.Side, o.Side,
o.ExecutedQuantity.String(), o.ExecutedQuantity.String(),
o.Quantity.String(), o.Quantity.String(),
o.Price.String(), o.Price.String())
o.Status)
if o.Type == OrderTypeStopLimit {
desc += " Stop @ " + o.StopPrice.String()
}
return desc + " | " + string(o.Status)
} }
// PlainText is used for telegram-styled messages // PlainText is used for telegram-styled messages