For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/
Multi-asset rebalancing strategy
-
Hi everyone,
I struggled for awhile to build a multi-asset rebalancing strategy that didn't spend on margin. I figured i'd shared the results for someone else. It's worth mentioning that I'm using this with monthly data and only for research purposes, not live trading, so Im using cheat-on-open. Hope this helps someone!
from __future__ import (absolute_import, division, print_function, unicode_literals) import pandas as pd import backtrader as bt import math import random import numpy as np class RandomAssetData(object): def __init__(self, start_price=100.00, start_date='2000-01-01', end_date='2020-12-31', scale=1.5): self._start_price = start_price self._start_date = start_date self._end_date = end_date self._scale = scale # Geneate dates dates = pd.date_range(self._start_date, self._end_date, freq='1M') - pd.offsets.MonthBegin(1) # Generate random steps steps = np.random.normal(loc=0, scale=1.5, size=len(dates)) # Set first element to 0 so that the first price will be the starting stock price steps[0]=0 # Simulate stock prices, by adding the steps to a starting price prices = self._start_price + np.cumsum(steps) self._prices_df = pd.DataFrame({'date': dates, 'open': prices}) self._prices_df['open'] = self._prices_df['open'].abs() self._prices_df['close'] = self._prices_df['open'].abs() self._prices_df = self._prices_df.set_index('date') def get_prices(self): return self._prices_df class PandasData(bt.feeds.PandasData): params = ( ('datetime', None), ('open', 'open'), ('high', -1), ('low', -1), ('close', 'close'), ('volume', -1), ('openinterest', -1), ('adj_close', -1), ) class RebalanceStrategy(bt.Strategy): params = (('target_allocations', list()), ('rebalance_months', [1]),) def __init__(self): self.rebalance_dict = dict() for i, d in enumerate(self.datas): self.rebalance_dict[d] = dict() for asset in self.p.target_allocations: if asset[0] == d._name: self.rebalance_dict[d]['target_allocation'] = asset[1] # print(f"setting target percent of {d._name} to {asset[1]}") def prenext_open(self): dt = self.datas[0].datetime.date(0) if dt.month in self.p.rebalance_months: self.rebalance() def next_open(self): dt = self.datas[0].datetime.date(0) if dt.month in self.p.rebalance_months: self.rebalance() def rebalance(self): date = self.data.datetime.datetime().date() # Calculate my own port_value because we're using coo port_value = self.broker.get_cash() for i, d in enumerate(self.datas): port_value += self.getposition(d).size * d.open[0] # print(f"{date} port_value=${round(port_value,2)}") # Calculate the current allocation of each asset and clear all other data for i, d in enumerate(self.datas): current_value = self.getposition(d).size * d.open[0] self.rebalance_dict[d]['current_value'] = current_value self.rebalance_dict[d]['current_allocation'] = round(math.ceil(current_value / port_value * 100) / 100, 2) self.rebalance_dict[d]['allocation_change'] = 0.0 self.rebalance_dict[d]['amount_to_sell'] = 0.0 self.rebalance_dict[d]['share_price'] = 0.0 self.rebalance_dict[d]['num_shares_to_sell'] = 0 self.rebalance_dict[d]['actual_selling_amount'] = 0.0 # Calculate the change in the allocation needed for each asset # change = target allocation - current allocation for i, d in enumerate(self.datas): self.rebalance_dict[d]['allocation_change'] = round( self.rebalance_dict[d]['target_allocation'] - self.rebalance_dict[d]['current_allocation'], 3) # Calculate the sells for i, d in enumerate(self.datas): if self.rebalance_dict[d]['allocation_change'] < 0: amount_to_sell = (math.ceil(abs(port_value * self.rebalance_dict[d]['allocation_change']))) share_price = d.open[0] num_shares_to_sell = math.ceil(amount_to_sell / share_price) actual_selling_amount = num_shares_to_sell * share_price self.rebalance_dict[d]['amount_to_sell'] = amount_to_sell self.rebalance_dict[d]['share_price'] = share_price self.rebalance_dict[d]['num_shares_to_sell'] = num_shares_to_sell self.rebalance_dict[d]['actual_selling_amount'] = actual_selling_amount # Calculate the buys for i, d in enumerate(self.datas): if self.rebalance_dict[d]['allocation_change'] > 0: amount_to_buy = port_value * self.rebalance_dict[d]['allocation_change'] share_price = d.open[0] num_shares_to_buy = math.floor(amount_to_buy / share_price) actual_buying_amount = num_shares_to_buy * share_price self.rebalance_dict[d]['amount_to_buy'] = amount_to_buy self.rebalance_dict[d]['share_price'] = share_price self.rebalance_dict[d]['num_shares_to_buy'] = num_shares_to_buy self.rebalance_dict[d]['actual_buying_amount'] = actual_buying_amount # Sell stuff for i, d in enumerate(self.datas): if self.rebalance_dict[d]['allocation_change'] < 0: # print(f" {d._name} current_value={self.rebalance_dict[d]['current_value']:.2f} current_allocation={self.rebalance_dict[d]['current_allocation']} allocation_change={self.rebalance_dict[d]['allocation_change']} num_shares_to_sell={self.rebalance_dict[d]['num_shares_to_sell']} share_price={self.rebalance_dict[d]['share_price']:.2f} amount_to_sell={self.rebalance_dict[d]['amount_to_sell']:.2f} actual_selling_amount={self.rebalance_dict[d]['actual_selling_amount']:.2f}") self.sell(d, size=self.rebalance_dict[d]['num_shares_to_sell']) # Buy stuff for i, d in enumerate(self.datas): if self.rebalance_dict[d]['allocation_change'] > 0: # print(f" {d._name} current_value={self.rebalance_dict[d]['current_value']:.2f} current_allocation={self.rebalance_dict[d]['current_allocation']} allocation_change={self.rebalance_dict[d]['allocation_change']} num_shares_to_buy={self.rebalance_dict[d]['num_shares_to_buy']} share_price={self.rebalance_dict[d]['share_price']:.2f} amount_to_buy={self.rebalance_dict[d]['amount_to_buy']:.2f} actual_buying_amount={self.rebalance_dict[d]['actual_buying_amount']:.2f}") self.buy(d, size=self.rebalance_dict[d]['num_shares_to_buy']) def notify_order(self, order): date = self.data.datetime.datetime().date() price = 'NA' if not order.price else round(order.price,5) if order.status not in [order.Accepted, order.Completed]: print(f"{date} >> {order.Status[order.status]} !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! >> {order.data._name}, Ref: {order.ref}, Size: {order.size}, Price: {price}") # elif order.status == order.Completed: # print(f"{date} >> Order Completed >> {order.data._name}, Ref: {order.ref}, Size: {order.size}, Price: {price}") def notify_trade(self, trade): date = self.data.datetime.datetime().date() if trade.isclosed: print('{} >> Notify Trade >> Stock: {}, Close Price: {}, Profit, Gross {}, Net {}'.format( date, trade.data._name, trade.price, round(trade.pnl,2), round(trade.pnlcomm,2))) if __name__ == '__main__': cerebro = bt.Cerebro(stdstats=False, cheat_on_open=True) # # strategy Params # allocations = [ ('data0', .25), ('data1', .25), ('data2', .25), ('data3', .25), ] # Add a strategy cerebro.addstrategy(RebalanceStrategy, target_allocations=allocations) # Add the data feeds (random walks) for x in range(4): df = RandomAssetData(random.randint(1, 200)).get_prices() data = bt.feeds.PandasData(dataname=df, name=f"data{x}") cerebro.adddata(data) cerebro.broker.set_checksubmit(False) cerebro.broker.setcommission(commission=0.0) start_cash = 10000 cerebro.broker.setcash(start_cash) print(f"Starting Portfolio Value: ${cerebro.broker.getvalue():,.0f}") strategies = cerebro.run() strategy = strategies[0] # Get final portfolio Value port_value = cerebro.broker.getvalue() pnl = port_value - start_cash roi = (port_value - start_cash) / start_cash * 100 # aka cumulative return print(f"Final Portfolio Value: ${port_value:,.0f}") print(f"P/L: ${pnl:,.0f}") print(f"ROI: {roi:,.2f}%")