Backtrader Community

    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search
    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)

    General Discussion
    2
    3
    1250
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • B
      barton05 last edited by barton05

      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
      
      1 Reply Last reply Reply Quote 1
      • A
        ada_34 last edited by

        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?

        1 Reply Last reply Reply Quote 0
        • B
          barton05 last edited by barton05

          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)]
          
          1 Reply Last reply Reply Quote 0
          • 1 / 1
          • First post
            Last post
          Copyright © 2016, 2017, 2018, 2019, 2020, 2021 NodeBB Forums | Contributors