opening range breakout strategy for multiple stocks
-
Hi,
I am trying to backtest opening range breakout strategy for a list of 50 stocks together. With an idea that the trades across stocks are executed till the cash is there in the system for the day and all positions closed at the end of the day. I am placing the strategy code below -
--
class OpeningRangeBreakout(bt.Strategy):
params = dict(
num_opening_bars=3,
fast_length= 5126,
slow_length= 25126
)def __init__(self): self.opening_range_low = 0 self.opening_range_high = 0 self.opening_range = 0 self.bought_today = False self.order = None self.crossovers = [] # for d in self.datas: # ma_fast = bt.ind.SMA(d, period = self.params.fast_length) # ma_slow = bt.ind.SMA(d, period = self.params.slow_length) # self.crossovers.append(bt.ind.CrossOver(ma_fast, ma_slow)) def log(self, txt, dt=None): if dt is None: dt = self.datas[0].datetime.datetime() # print('%s, %s' % (dt, txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed if order.status in [order.Completed]: order_details = f"{order.executed.price}, Cost: {order.executed.value}, Comm {order.executed.comm}" if order.isbuy(): self.log(f"BUY EXECUTED, Price: {order_details}") else: # Sell self.log(f"SELL EXECUTED, Price: {order_details}") elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') self.order = None def next(self): self.opening_range_low = {} self.opening_range_high = {} self.bought_today = {} self.opening_range = {} for i, d in enumerate(self.datas): current_bar_datetime = d.num2date(d.datetime[0]) previous_bar_datetime = d.num2date(d.datetime[-1]) print("current_bar_datetime",current_bar_datetime, "previous_bar_datetime", previous_bar_datetime) if (current_bar_datetime.date() != previous_bar_datetime.date()) : self.opening_range_low[i] = d.low[0] self.opening_range_high[i] = d.high[0] self.bought_today[i] = False opening_range_start_time = time(9, 30, 0) dt = datetime.combine(date.today(), opening_range_start_time) + \ timedelta(minutes=self.p.num_opening_bars * 5) opening_range_end_time = dt.time() print("self.opening_range_high",self.opening_range_high) if (current_bar_datetime.time() >= opening_range_start_time) \ and (current_bar_datetime.time() < opening_range_end_time): self.opening_range_high[i] = max(d.high[0], self.opening_range_high[i]) self.opening_range_low[i] = min(d.low[0], self.opening_range_low[i]) self.opening_range[i] = self.opening_range_high[i] - self.opening_range_low[i] else: if self.order: return if self.getposition(d).size and (d.close[0] > (self.opening_range_high[i] + self.opening_range[i])): self.close() if d.close[0] > self.opening_range_high[i] and not self.getposition(d).size and not self.bought_today[i]: self.order = self.buy() self.bought_today[i] = True if self.getposition(d).size and (self.data.close[0] < (self.opening_range_high[i] - self.opening_range[i])): self.order = self.close() if self.getposition(d).size and current_bar_datetime.time() >= time(15, 45, 0): self.log("RUNNING OUT OF TIME - LIQUIDATING POSITION") self.close() def stop(self): self.roi = (self.broker.get_value() / 100000) - 1.0
This however results in an error after the first run where the opening range is stored in this dict - self.opening_range_high, but when it moves to the next time indicator the value of self.opening_range_high fro the previous run is not retained.
Below is the last bit of error log. Can post the entire code if needed -
current_bar_datetime 2021-01-15 09:30:00 previous_bar_datetime 2021-03-09 15:25:00
self.opening_range_high {0: 536.25, 1: 2631.1, 2: 676.8, 3: 3563.6, 4: 8775.0, 5: 4889.9, 6: 603.6, 7: 425.45, 8: 3649.45, 9: 838.6, 10:
147.2, 11: 5257.9, 12: 2885.35, 13: 145.6, 14: 1011.6, 15: 1025.85, 16: 2680.0, 17: 1462.55, 18: 3207.0, 19: 262.95, 20: 2386.35, 21: 550.4, 22: 965.0, 23: 1359.8, 24: 103.7, 25: 214.1, 26: 401.65, 27: 1886.55, 28: 1378.0, 29: 828.15, 30: 8131.0, 31: 18243.2, 32: 102.7, 33: 106.1, 34: 204.0, 35: 1964.5, 36: 310.65, 37: 24532.0, 38: 604.5, 39: 244.75, 40: 721.5, 41: 3224.95, 42: 1045.0, 43: 1505.0, 44: 5527.95, 45: 524.0, 46: 178.85, 47: 454.2, 48: 232.95}
1,ADANIPORTS,1,2021-01-15 09:30:00,536.25,536.25,534.3,535.0,70646.0,,ASIANPAINT,1,2021-01-15 09:30:00,2627.4,2631.1,2626.0,2628.8,34444.0,,AXISBANK,1,2021-01-15 09:30:00,676.7,676.8,675.1,675.8,211308.0,,BAJAJ-AUTO,1,2021-01-15 09:30:00,3560.0,3563.6,3552.8,3559.8,6547.0,,BAJAJFINSV,1,2021-01-15 09:30:00,8767.35,8775.0,8751.1,8757.45,10775.0,,BAJFINANCE,1,2021-01-15 09:30:00,4889.0,4889.9,4865.5,4873.9,52116.0,,BHARTIARTL,1,2021-01-15 09:30:00,603.5,603.6,600.2,602.2,2011855.0,,BPCL,1,2021-01-15 09:30:00,425.3,425.45,423.25,423.4,162279.0,,BRITANNIA,1,2021-01-15 09:30:00,3646.25,3649.45,3640.05,3641.95,5971.0,,CIPLA,1,2021-01-15 09:30:00,837.7,838.6,835.7,837.5,38658.0,,COALINDIA,1,2021-01-15 09:30:00,147.1,147.2,146.75,146.9,130012.0,,DRREDDY,1,2021-01-15 09:30:00,5255.0,5257.9,5243.0,5254.4,16992.0,,EICHERMOT,1,2021-01-15 09:30:00,2885.35,2885.35,2871.0,2875.0,23501.0,,GAIL,1,2021-01-15 09:30:00,145.35,145.6,145.0,145.3,621437.0,,GRASIM,1,2021-01-15 09:30:00,1011.05,1011.6,1009.55,1010.0,29951.0,,HCLTECH,1,2021-01-15 09:30:00,1022.0,1025.85,1020.65,1024.35,495943.0,,HDFC,1,2021-01-15 09:30:00,2679.6,2680.0,2676.7,2678.55,20000.0,,HDFCBANK,1,2021-01-15 09:30:00,1460.7,1462.55,1459.25,1460.25,122419.0,,HEROMOTOCO,1,2021-01-15 09:30:00,3194.75,3207.0,3182.45,3187.7,63657.0,,HINDALCO,1,2021-01-15 09:30:00,262.55,262.95,261.35,262.2,157124.0,,HINDUNILVR,1,2021-01-15 09:30:00,2384.65,2386.35,2383.2,2383.55,17541.0,,ICICIBANK,1,2021-01-15 09:30:00,550.2,550.4,549.05,549.15,250540.0,,INDUSINDBK,1,2021-01-15 09:30:00,965.0,965.0,957.2,958.95,329196.0,,INFY,1,2021-01-15 09:30:00,1357.0,1359.8,1355.3,1355.8,307078.0,,IOC,1,2021-01-15 09:30:00,103.7,103.7,102.95,103.0,920844.0,,ITC,1,2021-01-15 09:30:00,214.0,214.1,213.55,213.8,583160.0,,JSWSTEEL,1,2021-01-15
09:30:00,401.0,401.65,399.35,401.65,169314.0,,KOTAKBANK,1,2021-01-15 09:30:00,1886.0,1886.55,1878.6,1880.75,57796.0,,LT,1,2021-01-15 09:30:00,1377.8,1378.0,1374.0,1374.15,51607.0,,M&M,1,2021-01-15 09:30:00,828.15,828.15,825.5,826.35,76797.0,,MARUTI,1,2021-01-15 09:30:00,8126.05,8131.0,8106.0,8120.0,21054.0,,NESTLEIND,1,2021-01-15 09:30:00,18243.2,18243.2,18200.3,18200.4,1607.0,,NTPC,1,2021-01-15 09:30:00,102.7,102.7,102.4,102.55,295404.0,,ONGC,1,2021-01-15 09:30:00,105.8,106.1,105.5,105.8,524289.0,,POWERGRID,1,2021-01-15 09:30:00,204.0,204.0,203.1,203.3,161929.0,,RELIANCE,1,2021-01-15 09:30:00,1964.4,1964.5,1961.9,1961.9,153914.0,,SBIN,1,2021-01-15 09:30:00,310.65,310.65,308.25,308.45,1273422.0,,SHREECEM,1,2021-01-15 09:30:00,24520.0,24532.0,24500.0,24526.7,679.0,,SUNPHARMA,1,2021-01-15 09:30:00,602.9,604.5,600.8,603.55,138630.0,,TATAMOTORS,1,2021-01-15 09:30:00,244.75,244.75,243.65,244.0,1420424.0,,TATASTEEL,1,2021-01-15 09:30:00,718.0,721.5,716.65,721.25,1274300.0,,TCS,1,2021-01-15 09:30:00,3219.6,3224.95,3210.5,3219.95,89570.0,,TECHM,1,2021-01-15 09:30:00,1044.85,1045.0,1036.2,1036.4,137201.0,,TITAN,1,2021-01-15 09:30:00,1504.35,1505.0,1501.2,1504.9,28613.0,,ULTRACEMCO,1,2021-01-15 09:30:00,5517.95,5527.95,5501.95,5515.05,12135.0,,UPL,1,2021-01-15 09:30:00,523.5,524.0,521.2,521.5,381937.0,,VEDL,1,2021-01-15 09:30:00,178.85,178.85,178.0,178.35,265411.0,,WIPRO,1,2021-01-15 09:30:00,453.8,454.2,452.8,453.6,367378.0,,ZEEL,1,2021-01-15 09:30:00,232.7,232.95,231.45,232.5,487195.0,,OpeningRangeBreakout,1,737805.3958333334,Broker,1,100000.0,100000.0,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,BuySell,1,,,DataTrades,1,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
current_bar_datetime 2021-01-15 09:35:00 previous_bar_datetime 2021-01-15 09:30:00
self.opening_range_high {}
Traceback (most recent call last):
File "c:\Users\40100147\Abhishek\Projects\fullstack-trading-app\backtest.py", line 185, in <module>
strategies = cerebro.run()
File "c:\ProgramData\Anaconda3\envs\backtrader\lib\site-packages\backtrader\cerebro.py", line 1127, in run
runstrat = self.runstrategies(iterstrat)
File "c:\ProgramData\Anaconda3\envs\backtrader\lib\site-packages\backtrader\cerebro.py", line 1293, in runstrategies
self._runonce(runstrats)
File "c:\ProgramData\Anaconda3\envs\backtrader\lib\site-packages\backtrader\cerebro.py", line 1695, in _runonce
strat._oncepost(dt0)
File "c:\ProgramData\Anaconda3\envs\backtrader\lib\site-packages\backtrader\strategy.py", line 309, in _oncepost
self.next()
File "c:\Users\40100147\Abhishek\Projects\fullstack-trading-app\backtest.py", line 84, in next
self.opening_range_high[i] = max(d.high[0], self.opening_range_high[i])
KeyError: 0 -
@abhishek-anand-1 said in opening range breakout strategy for multiple stocks:
self.opening_range_high[i] = max(d.high[0], self.opening_range_high[i])
You are trying to compare maximum with
d.high[0]
andself.opening_range_high[i]
. In reading your code, it is possible thatself.opening_range_high[i]
is not created yet at this point in the code. If you look for this key in the dictionary and it's not there, you getKeyError: 0
.Two options. You could set the value of
self.opening_range_high[i]
to zero at the beginning of the enumeration, or you can check to see if it is in the keys before proceeding, like this:if i in self.opening_range_high: self.opening_range_high[i] = max(d.high[0], self.opening_range_high[i]) else: self.opening_range_high[i] = d.high[0]
-
@run-out Thanks for the response. I was able to find out the erro. Just posting the corrected code here -
import con_config as conf import backtrader as bt import pandas as pd import sqlite3 from datetime import date, datetime, time, timedelta import matplotlib import psycopg2 import os import rootpath class OpeningRangeBreakout(bt.Strategy): params = dict( num_opening_bars=3, fast_length= 5*12*6, slow_length= 25*12*6 ) def __init__(self): self.bought_today = False self.order = None self.crossovers = [] self.opening_range_low = {} self.opening_range_high = {} self.bought_today = {} self.opening_range = {} def log(self, txt, dt=None): if dt is None: dt = self.datas[0].datetime.datetime() # print('%s, %s' % (dt, txt)) def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: # Buy/Sell order submitted/accepted to/by broker - Nothing to do return # Check if an order has been completed if order.status in [order.Completed]: order_details = f"{order.executed.price}, Cost: {order.executed.value}, Comm {order.executed.comm}" if order.isbuy(): self.log(f"BUY EXECUTED, Price: {order_details}") else: # Sell self.log(f"SELL EXECUTED, Price: {order_details}") elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') self.order = None def next(self): for i, d in enumerate(self.datas): current_bar_datetime = d.num2date(d.datetime[0]) previous_bar_datetime = d.num2date(d.datetime[-1]) print("current_bar_datetime",current_bar_datetime, "previous_bar_datetime", previous_bar_datetime) if (current_bar_datetime.date() != previous_bar_datetime.date()) : self.opening_range_low[i] = d.low[0] self.opening_range_high[i] = d.high[0] self.bought_today[i] = False opening_range_start_time = time(9, 30, 0) dt = datetime.combine(date.today(), opening_range_start_time) + \ timedelta(minutes=self.p.num_opening_bars * 5) opening_range_end_time = dt.time() print("self.opening_range_high",self.opening_range_high) if (current_bar_datetime.time() >= opening_range_start_time) \ and (current_bar_datetime.time() < opening_range_end_time): self.opening_range_high[i] = max(d.high[0], self.opening_range_high[i]) self.opening_range_low[i] = min(d.low[0], self.opening_range_low[i]) self.opening_range[i] = self.opening_range_high[i] - self.opening_range_low[i] else: if self.order: return if self.getposition(d).size and (d.close[0] > (self.opening_range_high[i] + self.opening_range[i])): self.close() if d.close[0] > self.opening_range_high[i] and not self.getposition(d).size and not self.bought_today[i]: self.order = self.buy() self.bought_today[i] = True if self.getposition(d).size and (self.data.close[0] < (self.opening_range_high[i] - self.opening_range[i])): self.order = self.close() if self.getposition(d).size and current_bar_datetime.time() >= time(15, 45, 0): self.log("RUNNING OUT OF TIME - LIQUIDATING POSITION") self.close() def stop(self): self.roi = (self.broker.get_value() / 100000) - 1.0 if __name__ == '__main__': # Reading in the csv # Getting the rootpath of the working directory working_dir = rootpath.detect(__file__) # working_dir = "C:\\Users\\40100147\\Abhishek\\Projects\\fullstack-trading-app" nifty50_path = os.path.join(working_dir, "data\\nifty50.csv") outputpath = os.path.join(working_dir, "data\\result_df_2.csv") nifty50 = pd.read_csv(nifty50_path, header=0, sep='\t') nifty50_list = tuple(nifty50['Symbol'].tolist()) conn = psycopg2.connect(**conf.con_dict) query = f"""--sql select a.tradingsymbol from ( select tradingsymbol, avg(Volume) as volume from equities.candlestick where candle_date_time >= '01-15-2021' and cast(candle_date_time as time(0)) > '09:30:00' and cast(candle_date_time as time(0)) < '15:30:00' and candle_length = '5minute' and tradingsymbol in {nifty50_list} group by tradingsymbol) as a; """ stocks = pd.read_sql(query, con=conn) stocks_list = stocks['tradingsymbol'] result_df = [] cerebro = bt.Cerebro() for stock in stocks_list: query = f"""--sql select * from equities.candlestick where candle_date_time >= '01-15-2021' and cast(candle_date_time as time(0)) >= '09:30:00' and cast(candle_date_time as time(0)) <= '15:30:00' and tradingsymbol = '{stock}' and candle_length = '5minute'; """ dataframe = pd.read_sql(query, con=conn, index_col='candle_date_time', parse_dates=['datetime']) data = bt.feeds.PandasData(dataname=dataframe) cerebro.adddata(data, name = stock) temp_list = [] initial_cash = 100000.0 cerebro.broker.setcash(initial_cash) # 0.01% of the operation value cerebro.broker.setcommission(commission=0.0001) cerebro.addsizer(bt.sizers.PercentSizer, percents=95) cerebro.addwriter(bt.WriterFile, csv=True) cerebro.addstrategy(OpeningRangeBreakout) strats = cerebro.optstrategy(OpeningRangeBreakout, num_opening_bars=\ [15, 30, 60]) cerebro.run() # Get final portfolio Value portvalue = cerebro.broker.getvalue() # Print out the final result print('Final Portfolio Value: ${}'.format(portvalue))
-
@abhishek-anand-1 This appears to be the code from the Part Time Larry youtube channel video titled "Opening Range Breakout Backtest with Python and Backtrader". Just be warned that his original code has some issues. I could be wrong but it appears that you have not made the corrections to the code.
-
The code would define the first 15 minutes of the opening range to generate the high. There is no problem with this.
-
The code would trigger an order outside of the 15 minute opening range. This is no longer an opening range breakout if the purchase is at 1 pm. You can validate that his code is not functioning correctly at the 37:14 of the video that shows purchases outside of the 15 minute opening range.
-
If the purchase occurs after the 15:45, it is held overnight. This is now a swing trade strategy.
-
-
@krispy @ abhishek.anand 1
Come to think about it. This code can't trigger an order in the opening range. This is why.The data.high is placed into variable opening_range_high. Then data.close (the same bar) is compared against opening_range_high to trigger an order. How can data.close (of the same bar) be > data.high (of the same bar)? They can be equal, but not greater.
To summarize, within the opening range the same bar is compared to itself to see if it is greater than itself. This is why it can't trigger an order within the opening range.
How do you go and create a YouTube video about this strategy when it is clearly not tested?
-
@abhishek.anand @krispy have you tried to specify which asset should be sold / closed? In the code i see:
self.buy()
insteadself.buy(data=d)
self.close()
insteadself.close(data=d)
Let me know if works better.