Multi-asset trading strategy with different start-dates (IPOs)
-
I just got started with backtrader about a week back and was looking to implement a particular strategy described on this blog (https://www.backtrader.com/blog/2019-07-19-rebalancing-conservative/rebalancing-conservative/) using a similar multi-asset trading strategy. However, my data had data for stocks with different start dates - some due to IPOs and some due to data availability. Obviously this meant that the platform implemented the strategy only from when all the buffers could be guaranteed. The
prenext
andnextstart
methods obviously were going to be the way to tackle this. While just implementingself.next()
within theprenext
method would be a simple straightforward fix to most trading strategies, this wasn't going to be in this case. The__init__
method initialised the ranking lines and there was no way to control it from theprenext
method. So I implemented a a small workaround as this below.class rebalancing(bt.Strategy): params = dict( selcperc=0.10, # percentage of stocks to select from the universe rperiod=1, # period for the returns calculation, default 1 period vperiod=36, # lookback period for volatility - default 36 periods mperiod=12, # lookback period for momentum - default 12 periods reserve=0.05 # 5% reserve capital ) def log(self, arg): print('{} {}'.format(self.datetime.date(), arg)) # return def prenext(self): self.d_with_len = [d for d in self.datas if len(d)>self.p.vperiod] if len(self.d_with_len)>self.selnum: self.next() def nextstart(self): self.d_with_len = self.datas self.next()
The above fix came straight from https://www.backtrader.com/blog/2019-05-20-momentum-strategy/momentum-strategy/
However, further tweaking required inside the
self.next()
method still had to be addressed - which I did so.def next(self): comps_in_d_with_len = [] for d in self.d_with_len: comps_in_d_with_len.append(d._name) sub_ranks = self.ranks.copy() for key in list(sub_ranks): if key._name not in comps_in_d_with_len: del sub_ranks[key] ranks = sorted( sub_ranks.items(), # get the (d, rank), pair key=lambda x: x[1][0], # use rank (elem 1) and current time "0" reverse=True, # highest ranked 1st ... please ) # put top ranked in dict with data as key to test for presence rtop = dict(ranks[:self.selnum]) # For logging purposes of stocks leaving the portfolio rbot = dict(ranks[self.selnum:]) # prepare quick lookup list of stocks currently holding a position posdata = [d for d, pos in self.getpositions().items() if pos] # remove those no longer top ranked # do this first to issue sell orders and free cash for d in (d for d in posdata if d not in rtop): self.log('Leave {} - Rank {:.2f}'.format(d._name, rbot[d][0])) self.order_target_percent(d, target=0.0) # rebalance those already top ranked and still there for d in (d for d in posdata if d in rtop): self.log('Rebal {} - Rank {:.2f}'.format(d._name, rtop[d][0])) self.order_target_percent(d, target=self.perctarget) del rtop[d] # remove it, to simplify next iteration # issue a target order for the newly top ranked stocks # do this last, as this will generate buy orders consuming cash for d in rtop: self.log('Enter {} - Rank {:.2f}'.format(d._name, rtop[d][0])) self.order_target_percent(d, target=self.perctarget)
Much of the code remains the same, but it was quite tricky to figure out how to get the platform to go as far back as possible with the available dataset and the
minimum period
buffer constraint in order to start implementation. A saw a lot of discussion about this multi-period multi-start date trading strategy in the forum, but nothing quite concrete. So perhaps this is helpful.