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

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.

    alt text
    alt text
    alt text

    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 and SellPrice were pre-recorded in previous WFA simulation (tf or tradeFeed 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)]
    

Log in to reply
 

});