For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

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 the cerebro.addstrategy(EMAStack) the strategy instance is passed instead of strategy class.



  • @vladisld thank you, now it works !


Log in to reply
 

});