Navigation

    Backtrader Community

    • Register
    • 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/

    Multi-asset rebalancing strategy

    Indicators/Strategies/Analyzers
    1
    1
    262
    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
      brettelliot last edited by

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