Mutable variables not responding with Backtrader (replication of zipline strategy)
-
Hi everyone,
First I wanted to thank everyone in this community. I've been reading the forum for a while and found help for many issues I had. However, I've been running into an issue in the last weeks and cannot seem to find the answer. I've already spent days on it but really cannot find what's wrong. I want to clarify that I'm not a programming super star either so it might be something stupid but I just can't get my hands on it.
My problem is the following. I have been trying to replicate my zipline backtesting strategy in backtrader. It's a backtesting strategy on a pool of crypto currencies. However, I'm obtaining completely different results. There might be different issues with my code but I've identified at least one of them which is that some mutable variables I use do not seem to be responding or reacting well as they do not generate any signals or difference when I delete them from the strategy. Those variables are more specifically: entry_price, entry_date and topped.Please find my entire code below. Only thing you need to run it are Binance API keys. If you have any idea what's wrong please let me know.
''' Thanks in advance!! ''' import pandas as pd from binance.client import Client import binance from datetime import datetime as dt import pytz from strategy import * import pandas_market_calendars as mcal import backtrader as bt import backtrader.indicators as btind from datetime import datetime import matplotlib import numpy as np tickers = ['ETHUSDT','BTCUSDT','LTCUSDT','XLMUSDT','EOSUSDT','XTZUSDT', 'BNBUSDT','IOTAUSDT'] class maCross(bt.Strategy): ''' For an official backtrader blog on this topic please take a look at: https://www.backtrader.com/blog/posts/2017-04-09-multi-example/multi-example.html oneplot = Force all datas to plot on the same master. ''' params = ( ('sma7', 7), ('sma10', 10), ('sma20', 20), ('rsi5', 5), ('rsi10', 10), ('natr', 10), ('sma_vol', 10), ('adosc_f', 7), ('adosc_s', 20), ('oneplot', True) ) def __init__(self): ''' Create an dictionary of indicators so that we can dynamically add the indicators to the strategy using a loop. This mean the strategy will work with any numner of data feeds. ''' self.inds = dict() self.mutables = dict() for i, d in enumerate(self.datas): print() self.mutables[d] = dict() self.mutables[d]['entry_price'] = np.nan self.mutables[d]['entry_date'] = 0 self.mutables[d]['topped'] = False self.mutables[d]['close'] = d.close self.inds[d] = dict() self.inds[d]['sma7'] = bt.talib.SMA(d.close, timeperiod=self.params.sma7) self.inds[d]['sma7'].csv = True self.inds[d]['sma10'] = bt.talib.SMA(d.close, timeperiod=self.params.sma10) self.inds[d]['sma10'].csv = True self.inds[d]['sma20'] = bt.talib.SMA(d.close, timeperiod=self.params.sma20) self.inds[d]['sma20'].csv = True self.inds[d]['rsi5'] = bt.talib.RSI(d.close, timeperiod=self.params.rsi5) self.inds[d]['rsi5'].csv = True self.inds[d]['rsi10'] = bt.talib.SMA(d.close, timeperiod=self.params.rsi10) self.inds[d]['rsi10'].csv = True self.inds[d]['natr'] = bt.talib.NATR(d.high, d.low, d.close, timeperiod=self.params.natr) self.inds[d]['sma_vol'] = bt.talib.SMA(self.inds[d]['natr'], timeperiod=self.params.sma_vol) self.inds[d]['sma_vol'].csv = True self.inds[d]['adosc'] = bt.talib.ADOSC(d.high, d.low, d.close, d.volume, fastperiod=self.params.adosc_f, slowperiod=self.params.adosc_s) self.inds[d]['adosc'].csv = True if i > 0: #Check we are not on the first loop of data feed: if self.p.oneplot == True: d.plotinfo.plotmaster = self.datas[0] def next(self): for i, d in enumerate(self.datas): dt, dn = self.datetime.date(), d._name pos = self.getposition(d).size self.mutables[d]['entry_date'] = self.mutables[d]['entry_date'] + 1 # Entry functions ha1_reac = self.inds[d]['rsi5'][0] < 60 and self.inds[d]['rsi5'][0] > 45 ha1_cond = self.inds[d]['sma7'][-5] > self.inds[d]['sma10'][-5] and self.inds[d]['sma10'][-5] > self.inds[d]['sma20'][-5] and self.inds[d]['sma_vol'][0] < 20 vol_ind = self.inds[d]['adosc'][0] > 0 entry = (vol_ind and ha1_reac and ha1_cond and self.mutables[d]['close'][0] > self.mutables[d]['close'][-10]*0.98) or self.mutables[d]['topped']==1 # Exit functions v1 = self.inds[d]['rsi10'][0] > 80 v2 = self.mutables[d]['entry_price']*0.93 > self.mutables[d]['close'][0] v3 = self.mutables[d]['close'][0] > self.mutables[d]['entry_price']*1.1 v4 = self.mutables[d]['entry_date'] >= 30 if not pos: # no market / no orders if entry: self.mutables[d]['entry_price'] = self.mutables[d]['close'][0] self.mutables[d]['entry_date'] = 0 self.order = self.order_target_percent(data=d, target=1) elif pos > 0: if v1 or v2 or v3 or v4: self.order = self.order_target_size(data=d, target=0) if v1 or v2 or v4: self.mutables[d]['topped'] = 0 if v3: self.mutables[d]['topped'] = 1 def notify_trade(self, trade): dt = self.data.datetime.date() if trade.isclosed: print('{} {} Closed: PnL Gross {}, Net {}'.format( dt, trade.data._name, round(trade.pnl,2), round(trade.pnlcomm,2))) #Variable for our starting cash startcash = 2000 pct_per_stock = 1.0 / len(tickers) #Create an instance of cerebro cerebro = bt.Cerebro() #For annual returns cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years) cerebro.addobserver(bt.observers.Value) cerebro.addanalyzer(bt.analyzers.SharpeRatio, riskfreerate=0.0) cerebro.addanalyzer(bt.analyzers.Returns) cerebro.addanalyzer(bt.analyzers.AnnualReturn) cerebro.addanalyzer(bt.analyzers.DrawDown) #Add our strategy cerebro.addstrategy(maCross, oneplot=False) data = {} # Prepare dataframes with needed columns for ticker in tickers: klines = client.get_historical_klines(ticker, client.KLINE_INTERVAL_1DAY, "29 Sep, 2017", "28 Dec, 2020") df = pd.DataFrame.from_records(klines) df = df.rename(columns={0:"date", 1:"open", 2:"high", 3:"low", 4:"close",5:"volume"}) df = df[['date','open','high','low','close','volume']] df['date'] = pd.to_datetime(df['date'], unit='ms') df['date'] = df['date'].dt.tz_localize('UTC') df = df.set_index('date') df["open"] = pd.to_numeric(df["open"], downcast="float") df["high"] = pd.to_numeric(df["high"], downcast="float") df["low"] = pd.to_numeric(df["low"], downcast="float") df["close"] = pd.to_numeric(df["close"], downcast="float") df["volume"] = pd.to_numeric(df["volume"], downcast="float") data = bt.feeds.PandasData(dataname=df, calendar='NYSE') cerebro.adddata(data, name=ticker) # Set our desired cash start cerebro.broker.setcash(startcash) cerebro.addsizer(bt.sizers.PercentSizer, percents = 10) #cerebro.broker.set_checksubmit(False) cerebro.broker.setcommission(commission=0.0015) # Write data cerebro.addwriter(bt.WriterFile, csv=True, out="trade_history.csv") #cerebro.addwriter(bt.WriterFile, csv=False) # Run over everything #cerebro.run(runonce=False) results = cerebro.run(runonce=False) cerebro.plot() #Get final portfolio Value portvalue = cerebro.broker.getvalue() pnl = portvalue - startcash #Print out the final result print('Final Portfolio Value: ${}'.format(portvalue)) print(f"Sharpe: {results[0].analyzers.sharperatio.get_analysis()['sharperatio']:.3f}") print(f"Norm. Annual Return: {results[0].analyzers.returns.get_analysis()['rnorm100']:.2f}%") print(f"Max Drawdown: {results[0].analyzers.drawdown.get_analysis()['max']['drawdown']:.2f}%") print(f"Annual Return:", results[0].analyzers.annualreturn.get_analysis()) #cerebro.plot(iplot=False, volume=False, width=20)