I am trying to backtest a rebalancing strategy using performance of SPX. The (simplified) idea is: if price is above 200day moving average, then buy top 10 tickers on a pre-loaded list. If price is below 200 day moving average, sell everything in the portfolio.
Below is the code. Daily SPX data was loaded to cerebro initially to calculate indicators. Then the weekly list comprised of dates and a pre-generated list of 10 tickers for each week (Friday) was loaded.
Only when the date on the SPX dataframe match the date of the list, the 10 tickers on the list will be picked and their respective price info on that day and the next day will be fetched from yfinance (the reason for this is because the list could change every week therefore preloading all data for all tickers for the whole period does not seem to be efficient). The price info of tickers with existing positions will also be loaded.
Then the strategy will continue to check if the condition (close > MA200) is satisfied, if yes, using cash to buy the 10 tickers on the list. If not, sell all existing positions.
I got the following error message:
==============================Traceback (most recent call last): File "c:\Users\abc\Documents\BackTrader\testcode4.py", line 123, in <module> cerebro.run() ~~~~~~~~~~~^^ File "C:\Users\abc\Documents\BackTrader\.venv\Lib\site-packages\backtrader\cerebro.py", line 1132, in run runstrat = self.runstrategies(iterstrat) File "C:\Users\abc\Documents\BackTrader\.venv\Lib\site-packages\backtrader\cerebro.py", line 1298, in runstrategies self._runonce(runstrats) ~~~~~~~~~~~~~^^^^^^^^^^^ File "C:\Users\abc\Documents\BackTrader\.venv\Lib\site-packages\backtrader\cerebro.py", line 1700, in _runonce strat._oncepost(dt0) ~~~~~~~~~~~~~~~^^^^^ File "C:\Users\abc\Documents\BackTrader\.venv\Lib\site-packages\backtrader\strategy.py", line 309, in _oncepost self.next() ~~~~~~~~~^^ File "c:\Users\abc\Documents\BackTrader\testcode4.py", line 91, in next size = self.broker.get_cash() / 10 / data.close[0] ~~~~~~~~~~^^^ File "C:\Users\abc\Documents\BackTrader\.venv\Lib\site-packages\backtrader\linebuffer.py", line 163, in __getitem__ return self.array[self.idx + ago] ~~~~~~~~~~^^^^^^^^^^^^^^^^IndexError: array index out of range==============================I am new to Python and BackTrader, so any help will be appreciated. Below are the simplified codes.
import backtrader as btimport pandas as pdimport yfinance as yf# Define strategyclass WeeklyRotation(bt.Strategy): params = ( ('printlog', False), ) def __init__(self, cerebro): self.cerebro = cerebro self.dataclose = self.datas[0].close # Add indicators self.ma200 = bt.indicators.SimpleMovingAverage(self.dataclose, period=200) def notify_order(self, order): # Buy/Sell order submitted/accepted to/by broker - Nothing to do if order.status in [order.Submitted, order.Accepted]: return # Check if an order has been completed # Attention: broker could reject order if not enough cash if order.status in [order.Completed]: # Log long trade execution info if order.isbuy(): self.log('BUY EXECUTED, Ticker: %s, Price: %.2f, Size: %d, Cost: %.2f, Comm: %.2f' % (order.data._name, order.executed.price, order.executed.size, order.executed.value, order.executed.comm,)) else: self.log('SELL EXECUTED, Price: %.2f, Size: %d, Cost: %.2f, Comm: %.2f' % (order.executed.price, order.executed.size, order.executed.value, order.executed.comm)) self.log(f'Portfolio value: {self.broker.get_value()}') for position in self.getpositions(): self.log(f'Ticker: {position.data._name}, Price: {position.price}, Size: {position.size}, Value: {position.size * position.price}') elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.log('Order Canceled/Margin/Rejected') def next(self): # Get the current date current_date = self.datas[0].datetime.date(0) # Load the pregenerated weekly list for strategy. portfolio_df = pd.read_csv('weekly_list.csv', index_col=0) portfolio_df.index = pd.to_datetime(portfolio_df.index) # Check if the current date is in the portfolio if current_date.strftime('%Y-%m-%d') in portfolio_df.index.strftime('%Y-%m-%d'): # Get the list of tickers for the current date tickers = portfolio_df.loc[current_date.strftime('%Y-%m-%d')].dropna().tolist() # Load data for tickers on the weekly ticker list for the current date and next trading day for ticker in tickers: new_ticker_dataname = yf.download(ticker, start=current_date, end=current_date + pd.Timedelta(days=4)))) new_ticker_data = bt.feeds.PandasData(dataname=new_ticker_dataname) self.cerebro.adddata(new_ticker_data, name=ticker) # Check existing positions for position in self.getpositions(): ticker = position.data._name if ticker not in tickers: existing_ticker_data = bt.feeds.PandasData(dataname=yf.download(ticker, start=current_date, end=current_date + pd.Timedelta(days=4))) self.cerebro.adddata(existing_ticker_data, name=ticker) # Create a sell order for tickers not in the portfolio list self.sell(data=position.data) self.log(f'Sell order created for {ticker}, size: {position.size}') # Up to here everything seems to be fine # # Buy top 10 stocks if price is over 200-day moving average if (self.dataclose[0] >= self.ma200[0]): for ticker in tickers: data = self.cerebro.datasbyname[ticker] size = self.broker.get_cash() / 10 / data.close[0] self.buy(data=data, size=size) else: # Just sell everything for position in self.getpositions(): ticker = position.data._name self.sell(ticker) else: returnif __name__ == '__main__': # Create a cerebro entity cerebro = bt.Cerebro() # Co-pilot suggested to add cerebro as a parameter below so that data feeds can be added # inside the strategy, but I don't know whether it works or not. # Add a strategy cerebro.addstrategy(WeeklyRotation, cerebro=cerebro) # Create a DataFrame from SPX historical data df = pd.read_csv('SPX_data.csv', index_col=0) # Convert the index to datetime format df.index = pd.to_datetime(df.index) # Create a Data Feed using the DataFrame data = bt.feeds.PandasData(dataname=df) # Add the Data Feed to Cerebro cerebro.adddata(data) # Set cash start and commission rate cerebro.broker.set_cash(1000000) cerebro.broker.setcommission(commission=0.001) # Run over everything cerebro.run()Tried to print out the value of new_ticker_dataname, new_ticker_data and self.cerebro.datasbyname[ticker]. Here is what I get:
new_ticker_dataname: Price Close High Low Open VolumeTicker EBAY EBAY EBAY EBAY EBAYDate2020-06-26 47.061386 47.422045 46.312327 47.061386 177624002020-06-29 47.449795 47.477537 46.349320 47.227849 8933900, lenth: 2new_ticker_data: <backtrader.feeds.pandafeed.PandasData object at 0x00000186138B2350>, lenth: 0self.cerebro.datasbyname[ticker]: <backtrader.feeds.pandafeed.PandasData object at 0x000001861385E350>What I plan to do is to get the closing price of the particular ticker (in above case it's EBAY), so that I can calculate number of shares to buy and place an order. Also need to have the open price for the next trading day in order to know what price the trade will be executed at (based on market order). Since I can't find a way to locate the closing price (and other prices), don't know how to proceed.
Many thanks in advance.