mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
backtest: fix stop limit order matching
Signed-off-by: c9s <yoanlin93@gmail.com>
This commit is contained in:
parent
9631a2e551
commit
10d5a8a4f2
|
@ -124,7 +124,8 @@ func (m *SimplePriceMatching) CancelOrder(o types.Order) (types.Order, error) {
|
|||
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 := o.Price
|
||||
|
||||
|
@ -135,7 +136,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (closedOrders *typ
|
|||
}
|
||||
|
||||
price = m.LastPrice
|
||||
case types.OrderTypeLimit, types.OrderTypeLimitMaker:
|
||||
case types.OrderTypeLimit, types.OrderTypeStopLimit, types.OrderTypeLimitMaker:
|
||||
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) {
|
||||
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 {
|
||||
switch o.Type {
|
||||
|
||||
|
@ -333,10 +365,13 @@ func (m *SimplePriceMatching) BuyToPrice(price fixedpoint.Value) (closedOrders [
|
|||
o.Type = types.OrderTypeLimit
|
||||
|
||||
// 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 {
|
||||
o.Price = m.LastKLine.Low
|
||||
}
|
||||
|
||||
o.ExecutedQuantity = o.Quantity
|
||||
o.Status = types.OrderStatusFilled
|
||||
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())
|
||||
|
||||
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
|
||||
for _, o := range m.bidOrders {
|
||||
switch o.Type {
|
||||
|
@ -402,11 +468,12 @@ func (m *SimplePriceMatching) SellToPrice(price fixedpoint.Value) (closedOrders
|
|||
}
|
||||
|
||||
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 {
|
||||
o.Type = types.OrderTypeLimit
|
||||
|
||||
if sellPrice.Compare(o.Price) <= 0 {
|
||||
if o.Price.Compare(sellPrice) <= 0 {
|
||||
if o.Price.Compare(m.LastKLine.High) > 0 {
|
||||
o.Price = m.LastKLine.High
|
||||
}
|
||||
|
|
|
@ -151,17 +151,7 @@ func newKLine(symbol string, interval types.Interval, startTime time.Time, o, h,
|
|||
}
|
||||
}
|
||||
|
||||
func TestSimplePriceMatching_PlaceLimitOrder(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(1000000.0)},
|
||||
"BTC": {Currency: "BTC", Available: fixedpoint.NewFromFloat(100.0)},
|
||||
})
|
||||
|
||||
func getTestMarket() types.Market {
|
||||
market := types.Market{
|
||||
Symbol: "BTCUSDT",
|
||||
PricePrecision: 8,
|
||||
|
@ -172,7 +162,98 @@ func TestSimplePriceMatching_PlaceLimitOrder(t *testing.T) {
|
|||
MinAmount: fixedpoint.MustNewFromString("10.0"),
|
||||
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{
|
||||
Account: account,
|
||||
Market: market,
|
||||
|
|
|
@ -220,16 +220,38 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.orderExecutor.Bind()
|
||||
|
||||
store, _ := session.MarketDataStore(s.Symbol)
|
||||
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
|
||||
|
||||
s.pivot = &indicator.Pivot{IntervalWindow: s.IntervalWindow}
|
||||
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 {
|
||||
s.resistancePivot = &indicator.Pivot{IntervalWindow: s.BounceShort.IntervalWindow}
|
||||
s.resistancePivot.Bind(store)
|
||||
}
|
||||
|
||||
standardIndicator, _ := session.StandardIndicatorSet(s.Symbol)
|
||||
if s.BreakLow.StopEMA != nil {
|
||||
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:]
|
||||
}
|
||||
|
||||
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
|
||||
if s.stopEWMA != nil && !s.BreakLow.StopEMARange.IsZero() {
|
||||
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))
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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) {
|
||||
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
|
||||
wg.Done()
|
||||
|
|
|
@ -271,7 +271,7 @@ func (o Order) String() string {
|
|||
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.CreationTime.Time().Local().Format(time.RFC1123),
|
||||
orderID,
|
||||
|
@ -280,8 +280,13 @@ func (o Order) String() string {
|
|||
o.Side,
|
||||
o.ExecutedQuantity.String(),
|
||||
o.Quantity.String(),
|
||||
o.Price.String(),
|
||||
o.Status)
|
||||
o.Price.String())
|
||||
|
||||
if o.Type == OrderTypeStopLimit {
|
||||
desc += " Stop @ " + o.StopPrice.String()
|
||||
}
|
||||
|
||||
return desc + " | " + string(o.Status)
|
||||
}
|
||||
|
||||
// PlainText is used for telegram-styled messages
|
||||
|
|
Loading…
Reference in New Issue
Block a user