How to backtest using portfolio compositions
-
I have a csv file / pandas dataframe which looks like this. It contains various portfolio compositions for different strategies. Mostly based on different optimisation methods, max sharpe, min VaR etc..
date asset percentage strategy 4-Jan-21 AAPL 12.00% strategy1 4-Jan-21 TSM 1.00% strategy1 4-Jan-21 IBM 31.00% strategy1 4-Jan-21 KO 15.00% strategy1 4-Jan-21 AMD 41.00% strategy1 4-Jan-21 DELL 23.00% strategy2 4-Jan-21 TSM 12.20% strategy2 4-Jan-21 IBM 15.24% strategy2 4-Jan-21 KO 1.50% strategy2 4-Jan-21 NKE 7.50% strategy2 4-Jan-21 TSLA 9.50% strategy2 4-Jan-21 CSCO 3.30% strategy2 4-Jan-21 JPM 27.76% strategy2 4-Jan-21 AMD 45% strategy3 4-Jan-21 BA 0.50% strategy3 4-Jan-21 ORCL 54.50% strategy3 5-Jan-21 … … strategy1
I want to test a bunch of strategies with a 30 asset portfolio.
So lets say on 4Jan2021, strategy 1 asks me to hold 12% in apple, 1% in TSM.. etc. I want to be able to check the prices and know howmany I should be holding. I can easily create a function for that.
The code I have seen in the repo mostly shows how to do stuff with indicators, like SMA cross over, RSI...
My question is, is it possible to create and test portfolios based on these compositions I have so I can test which strategy is the most profitable one? So if I'm testing strategy1, it would check this frame, and know howmany stocks in apple to buy or sell on that particular day.
Is there example of code anywhere that would let me test my strategy based on the frame I have?
I am new to backtesting and backtrader, any help would be greatly appreciated.
-
@anarchy1989
The first thing you will want to do it load your targets with your datas. I like personally to attach the target to the dataline I as I add it to backtrader.tickers = {"FB": 0.25, "MSFT": 0.4, "TSLA": 0.35} for ticker, target in tickers.items(): data = bt.feeds.YahooFinanceData( dataname=ticker, timeframe=bt.TimeFrame.Days, fromdate=datetime.datetime(2019, 1, 1), todate=datetime.datetime(2020, 12, 31), reverse=False, ) data.target = target cerebro.adddata(data, name=ticker)
In next you will want to go through each data, and determine the current allocation. If the current allocation is too far from the desired allocation (threshold) you trade all datas.
Notice there is a buffer variable. This will reduce the overall value of the account for calculating units to trade. This helps avoid margin.
You will use a dictionary to track this information.
def next(self): track_trades = dict() total_value = self.broker.get_value() * (1 - self.p.buffer) for d in self.datas: track_trades[d] = dict() value = self.broker.get_value(datas=[d]) allocation = value / total_value units_to_trade = (d.target - allocation) * total_value / d.close[0] track_trades[d]["units"] = units_to_trade # Can check to make sure there is enough distance away from ideal to trade. track_trades[d]["threshold"] = abs(d.target - allocation) > self.p.threshold
Check all the thresholds to determine if trading. If any of datas need trading, then all need trading.
rebalance = False for values in track_trades.values(): if values['threshold']: rebalance = True if not rebalance: return
Finally, execute your trades. Always sell first to generate cash in the account and avoid margins.
# Sell shares first for d, value in track_trades.items(): if value["units"] < 0: self.sell(d, size=value["units"]) # Buy shares second for d, value in track_trades.items(): if value["units"] > 0: self.buy(d, size=value["units"])
Here is the all of the code for your reference.
import datetime import backtrader as bt class Strategy(bt.Strategy): params = ( ("buffer", 0.05), ("threshold", 0.025), ) def log(self, txt, dt=None): """ Logging function fot this strategy""" dt = dt or self.data.datetime[0] if isinstance(dt, float): dt = bt.num2date(dt) print("%s, %s" % (dt.date(), txt)) def print_signal(self): self.log( f"o {self.datas[0].open[0]:7.2f} " f"h {self.datas[0].high[0]:7.2f} " f"l {self.datas[0].low[0]:7.2f} " f"c {self.datas[0].close[0]:7.2f} " f"v {self.datas[0].volume[0]:7.0f} " ) def notify_order(self, order): """ Triggered upon changes to orders. """ # Suppress notification if it is just a submitted order. if order.status == order.Submitted: return # Print out the date, security name, order number and status. type = "Buy" if order.isbuy() else "Sell" self.log( f"{order.data._name:<6} Order: {order.ref:3d} " f"Type: {type:<5}\tStatus" f" {order.getstatusname():<8} \t" f"Size: {order.created.size:9.4f} Price: {order.created.price:9.4f} " f"Position: {self.getposition(order.data).size:5.2f}" ) if order.status == order.Margin: return # Check if an order has been completed if order.status in [order.Completed]: self.log( f"{order.data._name:<6} {('BUY' if order.isbuy() else 'SELL'):<5} " # f"EXECUTED for: {dn} " f"Price: {order.executed.price:6.2f} " f"Cost: {order.executed.value:6.2f} " f"Comm: {order.executed.comm:4.2f} " f"Size: {order.created.size:9.4f} " ) def notify_trade(self, trade): """Provides notification of closed trades.""" if trade.isclosed: self.log( "{} Closed: PnL Gross {}, Net {},".format( trade.data._name, round(trade.pnl, 2), round(trade.pnlcomm, 1), ) ) def next(self): track_trades = dict() total_value = self.broker.get_value() * (1 - self.p.buffer) for d in self.datas: track_trades[d] = dict() value = self.broker.get_value(datas=[d]) allocation = value / total_value units_to_trade = (d.target - allocation) * total_value / d.close[0] track_trades[d]["units"] = units_to_trade # Can check to make sure there is enough distance away from ideal to trade. track_trades[d]["threshold"] = abs(d.target - allocation) > self.p.threshold rebalance = False for values in track_trades.values(): if values['threshold']: rebalance = True if not rebalance: return # Sell shares first for d, value in track_trades.items(): if value["units"] < 0: self.sell(d, size=value["units"]) # Buy shares second for d, value in track_trades.items(): if value["units"] > 0: self.buy(d, size=value["units"]) if __name__ == "__main__": cerebro = bt.Cerebro() tickers = {"FB": 0.25, "MSFT": 0.4, "TSLA": 0.35} for ticker, target in tickers.items(): data = bt.feeds.YahooFinanceData( dataname=ticker, timeframe=bt.TimeFrame.Days, fromdate=datetime.datetime(2019, 1, 1), todate=datetime.datetime(2020, 12, 31), reverse=False, ) data.target = target cerebro.adddata(data, name=ticker) cerebro.addstrategy(Strategy) # Execute cerebro.run()