Test a strategy over a list of stocks (50+)
-
Hello,
I'm currently triying to test a strategy over a list of stocks (30+) and then generate the results of the strategy for each stock.
I tried to use a for loop to go through every stock and get the results, but it doesn't work.
The code only prints the results for the first stock in the list.Here ist the code:
import backtrader as bt import os.path import sys from datetime import datetime, timedelta import math import warnings period = 3650 strategyResults = {} lastBuy = None optimization = True tickers =['SH', 'VXX', 'EEM', 'QQQ', 'PSQ', 'XLF', 'GDX', 'HYG', 'EFA', 'IAU', 'XOP', 'IWM', 'FXI', 'SLV', 'USO', 'XLE', 'IEMG', 'AMLP', 'EWZ', 'XLK', 'XLI', 'VWO', 'GLD', 'XLP', 'JNK', 'EWJ', 'XLU', 'VEA', 'IEFA', 'XLV', 'PFF', 'VIXY', 'TLT', 'GDXJ', 'LQD', 'XLB', 'BKLN', 'XLY', 'SMH', 'OIH', 'ASHR', 'RSX', 'MCHI', 'VTI', 'EWH', 'SPLV', 'KRE', 'IVV', 'DIA', 'IEF', 'EZU', 'EWT', 'SPDW', 'VOO', 'SCHF', 'EWY', 'MYY', 'DOG', 'EUM'] data_path = r"C:\Users\XXXXX" # define the resolution(s) of the chart(s) & the strategy timeframes = { '1D': 1 } # New Class to define the content of the strategy class EMAStack(bt.Strategy): # Define the parameters of the strategy params = ( ('portfolio_expo', 0.10), # Max 15% of the Portfolio per trade ('trade_risk', 0.02), # Max 2% risk per trade (stop loss) ('atrdist', 3.0) # ATR based Stop loss distance ) def notify_order(self, order): if order.status == order.Completed: pass if not order.alive(): self.order = None # indicate no order is pending # Initialize the elements which are needed for the strategy (indicators, etc...) def __init__(self): # Define the indicators self.ema50 = bt.indicators.EMA(self.data.close, period=10) self.ema200 = bt.indicators.EMA(self.data.close, period=30) self.atr = bt.indicators.ATR(period=14) # Define the crossover signals self.bull_cross = bt.indicators.CrossOver(self.ema50, self.ema200) self.bull_cross.plotinfo.subplot = False self.bear_cross = bt.indicators.CrossOver(self.ema200, self.ema50) self.bear_cross.plotinfo.subplot = False def start(self): self.order = None # sentinel to avoid operations on pending order def prenext(self): self.next() def next(self): # Get the Amount of cash in the Portfolio cash = self.broker.get_cash() if self.order: return # pending order execution if not self.position: # check if we already have an open position on the stock if self.bull_cross > 0.0: # Calculation of the Stock Qty to buy depending on our risk strategy pdist = self.atr[0] * self.p.atrdist self.pstop = self.data.close[0] - pdist qty = math.floor((cash * self.p.trade_risk) / pdist) portfolio_exposure_calc = qty * self.data.close[0] portfolio_exposure_strategy = cash * self.p.portfolio_expo if portfolio_exposure_calc <= portfolio_exposure_strategy: self.order = self.buy(size=qty) else: qty = math.floor(portfolio_exposure_strategy / self.data.close[0]) self.order = self.buy(size=qty) elif self.bear_cross > 0.0: # Calculation of the Stock Qty to buy depending on our risk strategy pdist = self.atr[0] * self.p.atrdist self.pstop = self.data.close[0] - pdist qty = math.floor((cash * self.p.trade_risk) / pdist) portfolio_exposure_calc = qty * self.data.close[0] portfolio_exposure_strategy = cash * self.p.portfolio_expo if portfolio_exposure_calc <= portfolio_exposure_strategy: self.order = self.sell(size=qty) else: qty = math.floor(portfolio_exposure_strategy / self.data.close[0]) self.order = self.sell(size=qty) else: # in the market pclose = self.data.close[0] pstop = self.pstop # Add detection if we are already short or long if pclose < pstop or self.bear_cross or self.bull_cross: self.close() # stop met - get out else: pdist = self.atr[0] * self.p.atrdist # Update only if greater than self.pstop = max(pstop, pclose - pdist) ''' def notify_trade(self, trade): date = self.data.datetime.datetime() if trade.isclosed: print('-' * 32, ' NOTIFY TRADE ', '-' * 32) print('{}, Avg Price: {}, Profit, Gross {}, Net {}'.format( date, trade.price, round(trade.pnl, 2), round(trade.pnlcomm, 2))) ''' def printTradeAnalysis(analyzer): ''' Function to print the Technical Analysis results in a nice format. ''' # Get the results we are interested in total_open = analyzer.total.open total_closed = analyzer.total.closed total_won = analyzer.won.total total_lost = analyzer.lost.total win_streak = analyzer.streak.won.longest lose_streak = analyzer.streak.lost.longest pnl_net = round(analyzer.pnl.net.total, 2) strike_rate = round(((total_won / total_closed) * 100), 2) profit_factor = round((total_won / total_lost), 2) # Designate the rows h1 = ['Total Open', 'Total Closed', 'Total Won', 'Total Lost'] h2 = ['Strike Rate', 'Win Streak', 'Losing Streak', 'PnL Net'] r1 = [total_open, total_closed, total_won, total_lost] r2 = [strike_rate, win_streak, lose_streak, pnl_net] # Check which set of headers is the longest. if len(h1) > len(h2): header_length = len(h1) else: header_length = len(h2) # Print the rows print_list = [h1, r1, h2, r2] row_format = "{:<15}" * (header_length + 1) print("Trade Analysis Results:") for row in print_list: print(row_format.format('', *row)) print('Profit Factor: {}'.format(profit_factor)) def printSQN(analyzer): sqn = round(analyzer.sqn, 2) print('SQN: {}'.format(sqn)) if __name__ == '__main__': cerebro = bt.Cerebro() for ticker in tickers: endDate = datetime.today().strftime('%Y-%m-%d') fromDate = (datetime.today() - timedelta(days=period)) startDate = fromDate.strftime('%Y-%m-%d') filename = '%s_%s_%s.txt' % (ticker, startDate, endDate) datapath = os.path.join(data_path, filename) print(os.path.abspath(datapath)) #''' if not os.path.isfile(datapath): print('file: %s not found' % filename) sys.exit() #''' data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime(fromDate.year, fromDate.month, fromDate.day), # Do not pass values before this date todate=datetime(datetime.today().year, datetime.today().month, datetime.today().day), # Do not pass values after this date reverse=False) cerebro.adddata(data) # Set the Cash for the Strategy cerebro.broker.setcash(10000) # Set the comissions cerebro.broker.setcommission(commission=0.005) # Add the analyzers we are interested in cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='ta') cerebro.addanalyzer(bt.analyzers.SQN, _name="sqn") cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name="annualreturn") cerebro.addanalyzer(bt.analyzers.PyFolio, _name="pyfolio") cerebro.addstrategy(EMAStack) # Run over everything strategies = cerebro.run() EMAStack = strategies[0] #cerebro.run() # print the analyzers printTradeAnalysis(EMAStack.analyzers.ta.get_analysis()) printSQN(EMAStack.analyzers.sqn.get_analysis()) print("Final Portfolio Value: %.2f" % cerebro.broker.getvalue())
I hope that someone can help
-
@marketwizard said in Test a strategy over a list of stocks (50+):
self.ema50 = bt.indicators.EMA(self.data.close, period=10)
self.pstop = self.data.close[0] - pdist
self.order = self.buy(size=qty)
You strategy is only using the first data feed added.
self.data
is a shortcut for the first datafeed.self.buy(size=qty)
will also create the buy order for the first data feed.Please take a look at the docs:
https://www.backtrader.com/docu/concepts/#shortcuts-for-data-feeds
explain the various shortcuts available for the strategy
https://www.backtrader.com/docu/strategy/#reference-strategy
shows how to pass the arbitrary data feed (and not only the first one) to the
buy
method.One more useful article explaining how to work with multiple data feeds:
https://backtest-rookies.com/2017/08/22/backtrader-multiple-data-feeds-indicators/
-
@vladisld thank you for your answer.
I went through the documentations and the article from backtest-rookies and I also tested their code.
It seems that it is the same portfolio which is investing on every FX pair
What I'm trying to do is a bit different:
I want to backtest my strategy on each stock in order to generate the KPI's for each stock individually (and plot them idivually).
And in the next steps, the goal would be to generate a table / file with datas like:'AAPL' | strike rate = 24% | sharpe ratio = 0.36 | final portfolio value = XXX
'JNJ' | strike rate = 35% | sharpe ratio = 0.52 | final portfolio value = XXX
'AMZN' | strike rate = 60% | sharpe ratio = 1.236 | final portfolio value = XXX
.....
...
-
In this case you should have created the Cerebro instance for each ticket and call its
run
method in the loop. The code you've presented above just adds multiple symbols to the same Cerebro instance and runs all of them together afterwards. Please correct me if I'm wrong
-
I modified the code in accordance to your recommandation:
- I moved the creation of the cerebro instance, the "run" and "addstrategy" method into the loop.
Then, when I run the code, the backtests results (and the plot) for the first stock in the loop are generated, then I receive the following error:
Traceback (most recent call last): File "C:/Users/marketwizard/PycharmProjects/MW_Backtests/EMA_universeV2/main.py", line 262, in <module> strategies = cerebro.run() File "C:\Users\marketwizard\PycharmProjects\Algotrading_libraries1\lib\site-packages\backtrader\cerebro.py", line 1127, in run runstrat = self.runstrategies(iterstrat) File "C:\Users\marketwizard\PycharmProjects\Algotrading_libraries1\lib\site-packages\backtrader\cerebro.py", line 1238, in runstrategies strat._addobserver(False, observers.Broker) File "C:\Users\marketwizard\PycharmProjects\Algotrading_libraries1\lib\site-packages\backtrader\lineseries.py", line 461, in __getattr__ return getattr(self.lines, name) AttributeError: 'Lines_LineSeries_LineIterator_DataAccessor_Strateg' object has no attribute '_addobserver' Process finished with exit code 1
Here the code with the modifications described above
import backtrader as bt import pandas as pd import os.path import sys from datetime import datetime, timedelta, date import math period = 3650 strategyResults = {} lastBuy = None icap = 10000 tickers =['SH', 'VXX', 'EEM', 'QQQ', 'PSQ', 'XLF', 'GDX', 'HYG', 'EFA', 'IAU', 'XOP', 'IWM', 'FXI', 'SLV', 'USO', 'XLE', 'IEMG', 'AMLP', 'EWZ', 'XLK', 'XLI', 'VWO', 'GLD', 'XLP', 'JNK', 'EWJ', 'XLU', 'VEA', 'IEFA', 'XLV', 'PFF', 'VIXY', 'TLT', 'GDXJ', 'LQD', 'XLB', 'BKLN', 'XLY', 'SMH', 'OIH', 'ASHR', 'RSX', 'MCHI', 'VTI', 'EWH', 'SPLV', 'KRE', 'IVV', 'DIA', 'IEF', 'EZU', 'EWT', 'SPDW', 'VOO', 'SCHF', 'EWY', 'MYY', 'DOG', 'EUM'] data_path = r"C:\XXX" # define the resolution(s) of the chart(s) & the strategy timeframes = { '1D': 1 } # New Class to define the content of the strategy class EMAStack(bt.Strategy): # Define the parameters of the strategy params = ( ('portfolio_expo', 0.10), # Max 15% of the Portfolio per trade ('trade_risk', 0.02), # Max 2% risk per trade (stop loss) ('atrdist', 3.0) # ATR based Stop loss distance ) def notify_order(self, order): if order.status == order.Completed: pass if not order.alive(): self.order = None # indicate no order is pending # Initialize the elements which are needed for the strategy (indicators, etc...) def __init__(self): # Define the indicators self.ema50 = bt.indicators.EMA(self.data.close, period=50) self.ema200 = bt.indicators.EMA(self.data.close, period=200) self.atr = bt.indicators.ATR(period=14) # self.log_pnl = [] # Define the crossover signals self.bull_cross = bt.indicators.CrossOver(self.ema50, self.ema200) self.bull_cross.plotinfo.subplot = False self.bear_cross = bt.indicators.CrossOver(self.ema200, self.ema50) self.bear_cross.plotinfo.subplot = False def start(self): self.order = None # sentinel to avoid operations on pending order self.curpos = None def prenext(self): self.next() def next(self): # Get the Amount of cash in the Portfolio cash = self.broker.get_cash() if self.order: return # pending order execution if not self.position: # check if we already have an open position on the stock if self.bull_cross > 0.0: # Calculation of the Stock Qty to buy depending on our risk strategy pdist = self.atr[0] * self.p.atrdist self.pstop = self.data.close[0] - pdist qty = math.floor((cash * self.p.trade_risk) / pdist) portfolio_exposure_calc = qty * self.data.close[0] portfolio_exposure_strategy = cash * self.p.portfolio_expo if portfolio_exposure_calc <= portfolio_exposure_strategy: self.order = self.buy(size=qty) else: qty = math.floor(portfolio_exposure_strategy / self.data.close[0]) self.order = self.buy(size=qty) ''' elif self.bear_cross > 0.0: # Calculation of the Stock Qty to buy depending on our risk strategy pdist = self.atr[0] * self.p.atrdist self.pstop = self.data.close[0] - pdist qty = math.floor((cash * self.p.trade_risk) / pdist) portfolio_exposure_calc = qty * self.data.close[0] portfolio_exposure_strategy = cash * self.p.portfolio_expo if portfolio_exposure_calc <= portfolio_exposure_strategy: self.order = self.sell(size=qty) else: qty = math.floor(portfolio_exposure_strategy / self.data.close[0]) self.order = self.sell(size=qty) ''' else: # in the market pclose = self.data.close[0] pstop = self.pstop # Add detection if we are already short or long if pclose < pstop or self.bear_cross or self.bull_cross: self.close() # stop met - get out else: pdist = self.atr[0] * self.p.atrdist # Update only if greater than self.pstop = max(pstop, pclose - pdist) ''' def notify_trade(self, trade): date = self.data.datetime.datetime() if trade.isclosed: print('-' * 32, ' NOTIFY TRADE ', '-' * 32) print('{}, Avg Price: {}, Profit, Gross {}, Net {}'.format( date, trade.price, round(trade.pnl, 2), round(trade.pnlcomm, 2))) ''' def printSQN(analyzer): sqn = round(analyzer.sqn, 2) print('SQN: {}'.format(sqn)) def pretty_print(format, *args): print(format.format(*args)) def exists(object, *properties): for property in properties: if not property in object: return False object = object.get(property) return True def printTradeAnalysis(cerebro, analyzers): format = " {:<24} : {:<24}" NA = '-' print('Backtesting Results') if hasattr(analyzers, 'ta'): ta = analyzers.ta.get_analysis() openTotal = ta.total.open if exists(ta, 'total', 'open' ) else None closedTotal = ta.total.closed if exists(ta, 'total', 'closed') else None wonTotal = ta.won.total if exists(ta, 'won', 'total' ) else None lostTotal = ta.lost.total if exists(ta, 'lost', 'total' ) else None streakWonLongest = ta.streak.won.longest if exists(ta, 'streak', 'won', 'longest') else None streakLostLongest = ta.streak.lost.longest if exists(ta, 'streak', 'lost', 'longest') else None pnlNetTotal = ta.pnl.net.total if exists(ta, 'pnl', 'net', 'total' ) else None pnlNetAverage = ta.pnl.net.average if exists(ta, 'pnl', 'net', 'average') else None pretty_print(format, 'Open Positions', openTotal or NA) pretty_print(format, 'Closed Trades', closedTotal or NA) pretty_print(format, 'Winning Trades', wonTotal or NA) pretty_print(format, 'Loosing Trades', lostTotal or NA) print('\n') pretty_print(format, 'Longest Winning Streak', streakWonLongest or NA) pretty_print(format, 'Longest Loosing Streak', streakLostLongest or NA) pretty_print(format, 'Strike Rate (Win/closed)', (wonTotal / closedTotal) * 100 if wonTotal and closedTotal else NA) print('\n') pretty_print(format, 'Inital Portfolio Value', '${}'.format(icap)) pretty_print(format, 'Final Portfolio Value', '${}'.format(cerebro.broker.getvalue())) pretty_print(format, 'Net P/L', '${}'.format(round(pnlNetTotal, 2)) if pnlNetTotal else NA) pretty_print(format, 'P/L Average per trade', '${}'.format(round(pnlNetAverage, 2)) if pnlNetAverage else NA) print('\n') if hasattr(analyzers, 'drawdown'): pretty_print(format, 'Drawdown', '${}'.format(analyzers.drawdown.get_analysis()['drawdown'])) if hasattr(analyzers, 'sharpe'): pretty_print(format, 'Sharpe Ratio:', analyzers.sharpe.get_analysis()['sharperatio']) if hasattr(analyzers, 'vwr'): pretty_print(format, 'VRW', analyzers.vwr.get_analysis()['vwr']) if hasattr(analyzers, 'sqn'): pretty_print(format, 'SQN', analyzers.sqn.get_analysis()['sqn']) print('\n') print('Transactions') format = " {:<24} {:<24} {:<16} {:<8} {:<8} {:<16}" pretty_print(format, 'Date', 'Amount', 'Price', 'SID', 'Symbol', 'Value') for key, value in analyzers.txn.get_analysis().items(): pretty_print(format, key.strftime("%Y/%m/%d %H:%M:%S"), value[0][0], value[0][1], value[0][2], value[0][3], value[0][4]) if __name__ == '__main__': for ticker in tickers: cerebro = bt.Cerebro() endDate = date(2021, 1, 10) fromDate = (endDate - timedelta(days=period)) startDate = fromDate.strftime('%Y-%m-%d') filename = '%s_%s_%s.txt' % (ticker, startDate, endDate) datapath = os.path.join(data_path, filename) print(os.path.abspath(datapath)) #''' if not os.path.isfile(datapath): print('file: %s not found' % filename) sys.exit() #''' data = bt.feeds.YahooFinanceCSVData( dataname=datapath, # Do not pass values before this date fromdate=datetime(fromDate.year, fromDate.month, fromDate.day), # Do not pass values before this date todate=datetime(datetime.today().year, datetime.today().month, datetime.today().day), # Do not pass values after this date reverse=False) cerebro.adddata(data) # Set the Cash for the Strategy cerebro.broker.setcash(icap) # Set the comissions cerebro.broker.setcommission(commission=0.005) # Add the analyzers we are interested in cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='ta') cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown') cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.0, annualize=True, timeframe=bt.TimeFrame.Days) cerebro.addanalyzer(bt.analyzers.VWR, _name='vwr') cerebro.addanalyzer(bt.analyzers.SQN, _name='sqn') cerebro.addanalyzer(bt.analyzers.Transactions, _name='txn') cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio') cerebro.addstrategy(EMAStack) # Run over everything strategies = cerebro.run() EMAStack = strategies[0] # print the analyzers printTradeAnalysis(cerebro, EMAStack.analyzers) cerebro.plot(style='candlestick', barup='green', bardown='red')
-
Could you please try to run the Cerebro engine using:
cerebro.run(stdstats=False)
-
I tried with
cerebro.run(stdstats=False)
But I received following error:
Traceback (most recent call last): File "C:/Users/marketwizard/PycharmProjects/MW_Backtests/EMA_universeV2/main.py", line 262, in <module> strategies = cerebro.run(stdstats=False) File "C:\Users\marketwizard\PycharmProjects\Algotrading_libraries1\lib\site-packages\backtrader\cerebro.py", line 1127, in run runstrat = self.runstrategies(iterstrat) File "C:\Users\marketwizard\PycharmProjects\Algotrading_libraries1\lib\site-packages\backtrader\cerebro.py", line 1257, in runstrategies strat._addanalyzer(ancls, *anargs, **ankwargs) File "C:\Users\marketwizard\PycharmProjects\Algotrading_libraries1\lib\site-packages\backtrader\lineseries.py", line 461, in __getattr__ return getattr(self.lines, name) AttributeError: 'Lines_LineSeries_LineIterator_DataAccessor_Strateg' object has no attribute '_addanalyzer'
-
@marketwizard said in Test a strategy over a list of stocks (50+):
cerebro.addstrategy(EMAStack)
# Run over everything strategies = cerebro.run() EMAStack = strategies[0]
You are overriding the EMAStack name - which should be a class of your strategy with the strategy instance returned from the
Cerebro.run()
method. So the next iteration in thecerebro.addstrategy(EMAStack)
the strategy instance is passed instead of strategy class.
-
@vladisld thank you, now it works !