Monte Carlos and CAR25 Simulation by Backtrader (Credit to Howard Bandy)
-
Credit to CAR25 by Howard Bandy. I think Howard provides a good way to measure the required fraction (sizing) for any stock simulation.
I have created the monte carlos as Howard Bandy mentioned in his book. Hope it helps BT-ers as a quick start to have the Monte Carlos simulation. :)Figures as follow:
I have randomly distributed the trade and set the data started as 1-1-1990. It will need some entry and exit price in your previous trade record as reference point. It will adjust the monte['fraction'] according to the simulated DD90 value.
Code as follow:
def runOneMonte(_,row,consoFeeds,monte,params): stockName,exchange,indName=row[:3] cerebro = bt.Cerebro() cerebro.broker.setcash(100000) simParams={'stockName':stockName,'exchange':exchange,'indicator':indName} #analyzer cerebro.addanalyzer(continueITDDAnalyzer,_name='contITDD') #addCommission IB = params['commission'](commission=0.0) cerebro.broker.addcommissioninfo(IB) #SetBroker cerebro.broker.set_checksubmit(False) #Shuffle and reindex Data #random.seed(1) datas=list(itertools.chain.from_iterable(itertools.repeat(consoFeeds,max(int(monte['tradeTime']/len(consoFeeds)),1)))) shuffledDatas=random.sample(datas, len(datas)) barlen = sum(len(f)-1 for f in shuffledDatas) consoFeed=pd.concat(shuffledDatas) mcIndex=pd.date_range(start=datetime.datetime.strptime('1990-01-01','%Y-%m-%d'),periods=len(consoFeed),freq='D') consoFeed.set_index(mcIndex,drop=True,inplace=True) _feed=bt.feeds.PandasData(dataname=consoFeed[['open','high','low','close','volume']]) cerebro.adddata(_feed,name=stockName) tradeFeed = consoFeed[['BuyPrice','SellPrice']] cerebro.addstrategy(sizingStra, monte['fraction'],tradeFeed) cerebro.strats[0][0][0].contITDDSet={'ITDD':0,'ITDDLength':0,'initV':100000,'peak':float('-inf'),'ddlen':-1,'dd':0} strategies=cerebro.run() contITDDSet=strategies[0].analyzers.contITDD.get_analysis() contITDDSet['CARReturn']=(cerebro.broker.getvalue()/contITDDSet['initV'] - 1)*100*252/barlen return contITDDSet
def calculateDD90(row,params,monte,consoFeeds,q,loopRound=6): stockName=row.Index masterLogger=logging.getLogger('Master') loopTime=0 while loopTime<loopRound: _prevFraction=0 _loopCount=0 if params['numMulti'] >1: print('{}-{}: Start Pooling with Monte fraction:{}'.format(stockName,row.indicator, monte['fraction'])) pool = multiprocessing.Pool(params['numMulti'], worker_init, [q]) #Run 1000 monte carlos startTime=datetime.datetime.now() with pool as p: simResSet=pd.DataFrame.from_dict(pool.starmap(runOneMonte,zip(range(0,monte['NumberSim']),itertools.repeat(list(row)),itertools.repeat(consoFeeds),itertools.repeat(monte),itertools.repeat(params)))) pool.close() pool.join() timeRequired=datetime.datetime.now()-startTime print('{}-{}: Loop Time required for runOneMonte:{}'.format(stockName,row.indicator,timeRequired)) else: print('{}:Start Single Thread Simulation'.format(stockName)) simResSet = pd.DataFrame([runOneMonte(i,list(row), consoFeeds,monte,params) for i in range(0,monte['NumberSim'])]) DD90 = np.quantile(simResSet['ITDD'] , 0.9, interpolation='nearest') DD90 = simResSet[simResSet['ITDD']==DD90].to_dict(orient='records')[0] print('{}-{}:Test Result: Fraction ={}, DD90={}, CAR25={}'.format(stockName,row.indicator,monte['fraction']*100,DD90['ITDD'],DD90['CARReturn'])) if abs(monte['drawdownLimit'] - DD90['ITDD']) < monte['accuracyTolerance'] or abs(monte['fraction'] - _prevFraction) < 0.01 or _loopCount >5: return DD90 else: _prevFraction = monte['fraction'] _loopCount+=1 monte['fraction']=monte['fraction']*monte['drawdownLimit']/(DD90['ITDD']) if monte['fraction']>1 : print('Overwhelming sizing Occurred') return DD90 loopTime+=1 return DD90
def setMonte(row,params,monte): #Read Order time and Trade Price and prepare the feeds, dont follow below script as it is minimized consoFeeds=[] for record in records: consoFeeds.append(consoFeed) #Multi-processing _stockStartTime=datetime.datetime.now() DD90=calculateDD90(row,params,monte,consoFeeds,q) timeRequired=datetime.datetime.now()-_stockStartTime print('{}: Total Loop Time required:{}\n================================='.format(row.Index,timeRequired)) DD90.update({'fraction':monte['fraction'],'stockName':row.Index,'exchange':row.exchange,'indicator':row.indicator}) return DD90
-
I also like Howard Bandy’s trade sizing strategy, described in his book « Quantitative Technical Analysis ». I recently discovered backtrader and I think it is a good python platform for trade analysis and backtesting strategies. You had the excellent idea to adapt the sizing strategy of Howard Bandy to backtrader and providing us with your code. I am new to python and needs some help. In your function RunningOneMontecarlo you add the analyser class continueITDDAnalyser and the strategy class sizingSTRA. Is it possible to know what you put into these two classes?
-
Yeah, his QTA book is a good book to study and it makes sense to me.
For continueITDDAnalyzer:
I have created one analyzer called ITDDAnalyzer and another called continueITDDAnalyzer. Former one is to compute ITDD at the end of ONE simulation while continueITDDAnalyzer compute succeeding simulations.
from __future__ import (absolute_import, division, print_function, unicode_literals) import backtrader as bt from backtrader.analyzers import returns as returns import pandas as pd from backtrader.analyzers import TimeReturn import logging class ITDDAnalyzer(bt.TimeFrameAnalyzerBase): def CalculateITDD(self): value = self.strategy.broker.getvalue() if value > self.peak: self.peak = value self.ddlen = 0 # start of streak self.dd=dd= 100.0 * (self.peak - value) / self.peak self.ddlen+=bool(dd) self.ITDD=max(self.ITDD, self.dd) self.ITDDLength=max(self.ITDDLength,self.ddlen) def start(self,*args,**kwargs): self.ITDD=0 self.ITDDLength=0 self.initV=self.strategy.broker.getvalue() self.peak=float('-inf') self.ddlen= -1 self.dd = 0 self.ITDDRecord=[] def __init__(self): self._returns = TimeReturn(timeframe=bt.TimeFrame.Months) def notify_trade(self,trade): if self.strategy.getposition(self.data).size==0: #Calculate the ITDD together with commission included self.CalculateITDD() self.CARreturn=(self.strategy.broker.getvalue()/self.initV - 1)*100*252/max(trade.barlen,1) self.ITDDRecord.append([self.ITDD, self.ITDDLength,self.CARreturn]) #Reset Parameters for next trade self.ITDD=0 self.ITDDLength=int(0) self.initV=self.strategy.broker.getvalue() self.peak=float('-inf') self.ddlen= -1 self.dd = 0 def on_dt_over(self): if self.strategy.getposition(self.data).size==0: self.CalculateITDD() class continueITDDAnalyzer(ITDDAnalyzer): def __init__(self): self._returns = TimeReturn(timeframe=bt.TimeFrame.Months) self.ITDDSet=self.strategy.contITDDSet def start(self): self.ITDD=self.ITDDSet['ITDD'] self.ITDDLength=self.ITDDSet['ITDDLength'] self.initV=self.ITDDSet['initV'] self.value=self.strategy.broker.getvalue() self.peak=float(self.ITDDSet['peak']) self.ddlen=self.ITDDSet['ddlen'] self.dd =self.ITDDSet['dd'] def get_analysis(self): ITDDSet = {'ITDD':self.ITDD, 'ITDDLength':self.ITDDLength, 'initV':self.initV, 'value':self.strategy.broker.getvalue(), 'peak':self.peak, 'ddlen':self.ddlen, 'dd':self.dd} return ITDDSet
For SizingStra, it is a simple strategy to buy and sell. One point to note is that the
BuyPrice
andSellPrice
were pre-recorded in previous WFA simulation (tf
ortradeFeed
in below code). It has its own buy/sell price instead of bar open/close bar price particularly when strategy has trail stop that is NOT using bar close price to close the transaction in previous trading. I did it on purpose giving I want to have monte on my trade results. I extracted the most important part for your reference as well.def __init__(self,targetRatio,tradeFeed): self.historicOrder=[] self.totalCommission=0 # This is to record total commission paid in simulation self.o=dict() # orders per data (main, stop, limit, manual-close) self.targetRatio = targetRatio #Target ratio is to calculate the fraction self.tf= tradeFeed def next(self): self.analyzers.contITDD.CalculateITDD() if (self.tf.ix[self.datas[0].datetime.datetime()]['BuyPrice'])>0: PValue=self.broker.getvalue() calSize=int(PValue*self.targetRatio/self.datas[0].open[0]) targetSize=max(calSize,0) BuyPrice = self.tf.ix[self.datas[0].datetime.datetime()]['BuyPrice'] oneSimLogger.debug('PValue:{}, TargetSize:{}, BuyPrice:{}'.format(self.broker.getvalue(),targetSize,BuyPrice)) self.o = [self.buy(data=self.datas[0],exectype=bt.Order.Limit, size=targetSize,price=BuyPrice)] elif (self.tf.ix[self.datas[0].datetime.datetime()]['SellPrice'])>0: pos = self.getposition(self.data).size SellPrice = self.tf.ix[self.datas[0].datetime.datetime()]['SellPrice'] oneSimLogger.debug('PValue:{}, TargetSize:{}, SellPrice:{}'.format(self.broker.getvalue(),pos,SellPrice)) self.o = [self.sell(data=self.datas[0],size=pos,exectype=bt.Order.Stop,price=SellPrice)]