I'm confused about the behavior of stop orders. Consider the example below, in which the stop price is reached on the same bar on which the order is entered. However, the stop is not executed.
In this example, both the buy and the stop orders are placed on the 2022-01-02 bar, with a stop of 10.0. The buy is executed as expected, at a market price of 11.1 (the open price of the bar). However, the stop order is not executed, even though the low of the bar is 10.0.
In general in my tests it appears that stops are never executed on the same bar they are entered on, even though conceptually the orders are placed (and executed) at bar open, an presumably any low price of the bar happens after bar open.
Is this expected behavior and a limitation in Backtrader, and if so, is there a workaround?
Here's some sample output which illustrates this behavior:
Starting Portfolio Value: 10000.00
2022-01-01 OHLC: 10.1, 10.3, 10.0, 10.2
2022-01-01 placing buy order
2022-01-02 1: Buy Market: Submitted: 10 @ None
2022-01-02 2: Sell Stop: Submitted: -10 @ 10
2022-01-02 3: Sell Limit: Submitted: -10 @ None
2022-01-02 1: Buy Market: Completed: 10 @ 11.1
2022-01-02 OHLC: 11.1, 11.3, 10.0, 11.2
2022-01-03 3: Sell Limit: Completed: -10 @ 12.1
2022-01-03 2: Sell Stop: Canceled
2022-01-03 OHLC: 12.1, 12.3, 12.0, 12.2
2022-01-03 placing buy order
Final Portfolio Value: 10010.00
Here's the code that generates this output:
import backtrader as bt
import pandas as pd
from io import StringIO
class TestStrategy(bt.Strategy):
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f"{dt.isoformat()} {txt}")
def next(self):
d = self.data
self.log(f"OHLC: {d.open[0]}, {d.high[0]}, {d.low[0]}, {d.close[0]}")
if self.position.size == 0:
self.log("placing buy order")
self.buy_bracket(size=10, stopprice=10, exectype=bt.Order.Market)
def notify_order(self, order):
id = order.ref
created = bt.num2date(order.created.dt).strftime('%Y-%m-%d %H:%M')
status_name = order.Status[order.status]
ordtype_name = order.ordtypename()
exectype_name = order.ExecTypes[order.exectype]
order_header = f"{id}: {ordtype_name} {exectype_name}: {status_name}"
order_info = f"{order.size} @ {order.price}"
exec_info = f"{order.executed.size} @ {order.executed.price}"
if order.status in [order.Completed]:
self.log(f"{order_header}: {exec_info}")
elif order.status in [order.Submitted, order.Accepted]:
if order.status == order.Submitted:
self.log(f"{order_header}: {order_info}")
else:
self.log(f"{order_header}")
if __name__ == '__main__':
#make sure you use tabs as separators, not spaces in this input string, or change the separator in `read_csv` below
data ="""
datetime open high low close
2022-01-01 10.1 10.3 10.0 10.2
2022-01-02 11.1 11.3 10.0 11.2
2022-01-03 12.1 12.3 12.0 12.2
"""
prices = StringIO(data)
df = pd.read_csv(prices, sep = '\t', parse_dates=True, index_col=0)
pricedata = bt.feeds.PandasData(dataname = df)
cerebro = bt.Cerebro()
cerebro.addstrategy(TestStrategy)
cerebro.adddata(pricedata)
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())