Black–Litterman Model for Asset Allocation for Top 20 Indian Companies by Market Capitalization and Backtesting — Part 2

Sabir Jana, CFA
Analytics Vidhya
Published in
9 min readJun 26, 2020

by Sabir Jana

In this article, we will go through a step by step process to backtest the portfolio created from Black–Litterman Model for Asset Allocation for Top 20 Indian Companies by Market Capitalization and Backtesting — Part 1. Before we move to the next step, let’s quickly recapture the summary till now. As the first step, we gathered the market capitalization and daily pricing data for 20 Indian companies by market capitalization. Next, with the help of PyPortfolioOpt (python open-source library), we calculated market-implied returns. Then, we calculated the posterior estimates of returns and the covariance matrix and also factored analyst views and confidence regarding these views. With posterior estimates of returns and the covariance matrix as inputs, we ran the efficient frontier optimizer to get optimal weights for minimum volatility and no more than 10% weight constraints for any single stock.

To backtest this portfolio I thought of using two powerful python libraries, ziplineand Backtrader. In my opinion, both have their pros and cons however I decided in favor of Backtrader. With Backtrader it will be easier for you to follow along with this article and run the code on your machine without indulging in custom bundle complications of zipline. However, if you want to use zipline then you can refer to my article on How to Import Indian equities Data to zipline on your local machine? as zipline will require you to create a custom bundle to backtest it. You can find the code and data for this article at my Github repository.

Our approach will be as follows:

  1. Gathering Data.
  2. Define our Backtesting Strategy in Backtrader.
  3. Extract Performance Statistics to Compare and Contrast with the Benchmark.

Gathering Data

As a first step, we will need our portfolio weights. We saved this information as a csv file in the first part of the article so we are going to read it from the csv file. Apart from this, we will need an OHLCV dataset for these 20 tickers from 2010–01–01 to 2020–05–29. You can get this dataset from a data source of your choice however as a jump start I have provided a csv file for this dataset.

Let’s get hands-on with the code and perform the following tasks:

  1. Read the tickers and corresponding weights from wt_min_vola_wts.csv and plot it for quick verification.
  2. Define backtest date from 2010–01–01 to 2020–05–29.
  3. Read prices_all.csv file and create a stack dataframe with OHLCV columns.
  4. Create a list of tuples of ticker and corresponding weight, we will need this at a later stage.
# necessary imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
import yfinance as yf
import pandas_datareader.data as web
import pyfolio as pf
import backtrader as bt
idx = pd.IndexSlice
%matplotlib inline
%config InlineBackend.figure_format = ‘retina’
# Read our 20 tickers weights
wt_min_vola = pd.read_csv('data/wt_min_vola_wts.csv', index_col=[0])
print(wt_min_vola)
# plot the weights
wt_min_vola.plot.bar(figsize=(14,6),
title = 'Asset Allocation Based on BL with Confidence Matrix', grid=True,legend=False);
plt.ylabel('Percentage')
plt.savefig('images/chart6', dpi=300)
# date range for the backtesting
fromdate=datetime.datetime(2010, 1, 1)
todate=datetime.datetime(2020, 5, 29)
# Read daily prices from csv
prices = pd.read_csv('data/prices_all.csv', index_col=['ticker','date'], parse_dates=True)
prices.head(2)
prices.info()# create a tuple list of ticker and weight
assets_param = [(ind, wt_min_vola.loc[ind][0]) for ind in wt_min_vola.index]
print(assets_param)
Asset Allocation Based on BL with Confidence Matrix

We have a well-diversified portfolio of the top 20 Indian companies by market capitalization. If you check the output of efficient frontier from Part 1, the expected annual return is around 8% with an annual volatility of 9.8% and the Sharpe ratio is 0.61. However, we need to understand this performance expectation is in the case of ‘buy and hold’ and does not account for any rebalancing for the duration of backtesting.

Define our Backtesting Strategy in Backtrader

Our backtesting strategy is as follows:

  1. We will start with 500,000 currency cash and stocks universe of our 20 stocks and corresponding weights.
  2. Every quarter we are going to rebalance and reset the weights to their initial value.
  3. As we are rebalancing quarterly, I have accounted for the trading expense which is 0.4%

Let’s jump to the code:

  1. In Backtrader, our strategy class needs to inherit from bt.Strategy. It has input parameters as assets and rebalance_months. The parameter assets is used to provide the list of tuples of tickers and corresponding weights. The second parameterrebalance_month is used to provide a list of rebalancing months. As an example, January is represented by number 1 on the list.
  2. In the__init__ function we create a dictionary of stocks and corresponding weights with an additional flag of ‘rebalanced’.
  3. In the next function, we enumerate all 20 stocks and check if the current month is in therebalance_months list and the‘rebalanced’ flag is ‘False’. In that case, we order the target percentage and set the flag as ‘True’.
  4. If the current month is not on the list we keep the ‘rebalanced’ flag as ‘False’.
  5. In the notify_order function, we check if the order is completed.
  6. If the order is completed we notify the trade with thenotify_trade function which completes the Strategy class.
  7. Next, we create the instance of bt.Cerebro which is the main engine behind Backtrader and set the starting cash as 500,000 and trading commission as 0.004 (0.4%).
  8. The parameter set_checksubmit is set as ‘False’. It will ensure that orders will not be checked to see if we can afford it before submitting them.
  9. In the next step, we write a loop to load the backtesting data for these 20 stocks in memory.
  10. Now, we need to add the Strategy to cerebro with input as the list of tuples of stock and weight that we created in the previous section.
  11. We add returns and pyfolio analyzers that we will use to extract the performance of our strategy.
  12. Finally, we run the strategy and capture the output in the results variable.
# define the strategy
class Strategy(bt.Strategy):
# parameters for inputs
params = dict(
assets = [],
rebalance_months = [1,3,6,9]
)

# initialize
def __init__(self):
# create a dictionary of ticker:{'rebalanced': False, 'target_percent': target%}
self.rebalance_dict = dict()
for i, d in enumerate(self.datas):
self.rebalance_dict[d] = dict()
self.rebalance_dict[d]['rebalanced'] = False
for asset in self.p.assets:
if asset[0] == d._name:
self.rebalance_dict[d]['target_percent'] = asset[1]

def next(self):
# rebalance for the month in the list
for i, d in enumerate(self.datas):
dt = d.datetime.datetime()
dname = d._name
pos = self.getposition(d).size

if dt.month in self.p.rebalance_months and self.rebalance_dict[d]['rebalanced'] == False:
print('{} Sending Order: {} | Month {} | Rebalanced: {} | Pos: {}'.
format(dt, dname, dt.month,
self.rebalance_dict[d]['rebalanced'], pos ))

self.order_target_percent(d, target=self.rebalance_dict[d]['target_percent']/100)
self.rebalance_dict[d]['rebalanced'] = True

# Reset the flage
if dt.month not in self.p.rebalance_months:
self.rebalance_dict[d]['rebalanced'] = False

# notify the order if completed
def notify_order(self, order):
date = self.data.datetime.datetime().date()

if order.status == order.Completed:
print('{} >> Order Completed >> Stock: {}, Ref: {}, Size: {}, Price: {}'.
format(date, order.data._name, order.ref, order.size,
'NA' if not order.price else round(order.price,5)
))
# notify the trade if completed
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))
)
# starting cash
startcash = 500000
# 0.4% commission
commission = 0.004
#Create an instance of cerebro
cerebro = bt.Cerebro()
cerebro.broker.setcash(startcash)# orders will not be checked to see if you can afford it before submitting them
cerebro.broker.set_checksubmit(False)
cerebro.broker.setcommission(commission=commission)TICKERS = list(prices.index.get_level_values('ticker').unique())
print(TICKERS)
# load the data
for ticker, data in prices.groupby(level=0):
if ticker in TICKERS:
print(f"Adding ticker: {ticker}")
data = bt.feeds.PandasData(dataname=data.droplevel(level=0),
name=str(ticker),
fromdate=fromdate,
todate=todate,
plot=False)
cerebro.adddata(data)
#Add our strategy
cerebro.addstrategy(Strategy, assets=assets_param)
# add analyzers
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='time_return')
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())# Run the strategy.
results = cerebro.run(stdstats=True, tradehistory=False)
# Print out the final result
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

Extract Performance Statistics to Compare and Contrast with the Benchmark

Finally, it is time to see how well we did after the long process of portfolio identification and subsequent backtesting. In this section, we will take the backtesting results and compare them to the benchmark performance. We are going to use S&P BSE-Sensex as our benchmark. This is considering our 20 stocks are all large-cap companies. We use the pyfolio Python library for performance analysis.

Let’s code it for the following:

  1. Extract the inputs needed for pyfolio including returns, positions, and transactions from backtesting results variable.
  2. Download benchmark — S&P BSE-Sensex daily pricing data with the help of python library yfinance and convert it to daily returns. We need to make sure to use the right timezone else pyfolio will throw an error. It is also important to avoid any lookahead bias. I prefer to convert all data to the UTC time zone.
  3. Plot the performance of the strategy and benchmark to compare and contrast the results. I prefer plotting individual parameters as per need rather than using the cheatsheet option in pyfolio. If you are new to python then please use the code for plotting as is without putting too much time and energy as you will come around on this with time and experience.

Let’s code it:

# Extract inputs for pyfolio
strat = results[0]
pyfoliozer = strat.analyzers.getbyname(‘pyfolio’)
# Extract inputs for pyfolio
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
returns.name = 'Strategy'
returns.head(2)
# Get the benchmark returns for comparison
benchmark = '^BSESN' # S&P BSE Sensex
benchmark_rets= web.DataReader(benchmark, 'yahoo', start='2010-01-01',)['Adj Close'].pct_change().dropna()
benchmark_rets.index = benchmark_rets.index.tz_localize('UTC')
benchmark_rets = benchmark_rets.filter(returns.index)
benchmark_rets.name = 'S&P BSE-SENSEX'
benchmark_rets.head(2)
# Get the benchmark prices for comparison
benchmark = '^BSESN' # S&P BSE Sensex
benchmark_prices = web.DataReader(benchmark, 'yahoo', start='2010-01-01',)['Adj Close']
benchmark_prices = benchmark_prices.asfreq('D', method='ffill')
benchmark_prices.index = benchmark_prices.index.tz_localize('UTC')
benchmark_prices = benchmark_prices.filter(returns.index)
benchmark_prices.head(5)
# Rebase the benchmark prices for comparison
benchmark_prices = (benchmark_prices/benchmark_prices.iloc[0]) * startcash
benchmark_prices.head()
portfolio_value = returns.cumsum().apply(np.exp) * startcash# Visulize the output
fig, ax = plt.subplots(2, 1, sharex=True, figsize=[14, 8])
# portfolio value
portfolio_value.plot(ax=ax[0], label='Strategy')
benchmark_prices.plot(ax=ax[0], label='Benchmark - S&P BSE-Sensex')
ax[0].set_ylabel('Portfolio Value')
ax[0].grid(True)
ax[0].legend()
# daily returns
returns.plot(ax=ax[1], label='Strategy', alpha=0.5)
benchmark_rets.plot(ax=ax[1], label='Benchmark - S&P BSE-Sensex', alpha=0.5)
ax[1].set_ylabel('Daily Returns')
fig.suptitle('Black–Litterman Portfolio Allocation vs S&P BSE-Sensex', fontsize=16)
plt.grid(True)
plt.legend()
plt.show()
fig.savefig('images/chart9', dpi=300)
import seaborn as sns
import warnings
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
warnings.filterwarnings('ignore')
# get performance statistics for strategy
pf.show_perf_stats(returns,)
# get performance statistics for benchmark
pf.show_perf_stats(benchmark_rets)
# plot performance for strategy
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(14, 8),constrained_layout=True)
axes = ax.flatten()
pf.plot_drawdown_periods(returns=returns, ax=axes[0])
axes[0].grid(True)
pf.plot_rolling_returns(returns=returns,
factor_returns=benchmark_rets,
ax=axes[1], title='Strategy vs BSE-SENSEX')
axes[1].grid(True)
pf.plot_drawdown_underwater(returns=returns, ax=axes[2])
axes[2].grid(True)
pf.plot_rolling_sharpe(returns=returns, ax=axes[3])
axes[3].grid(True)
fig.suptitle('BL Portfolio vs BSE-SENSEX - 1', fontsize=16, y=0.990)
plt.grid(True)
plt.legend()
plt.tight_layout()
plt.savefig('images/chart7', dpi=300)
# plot performance
fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(16, 9),constrained_layout=True)
axes = ax.flatten()
pf.plot_rolling_beta(returns=returns, factor_returns=benchmark_rets, ax=axes[0])
axes[0].grid(True)
pf.plot_rolling_volatility(returns=returns, factor_returns=benchmark_rets,ax=axes[1])
axes[1].grid(True)
pf.plot_annual_returns(returns=returns, ax=axes[2])
axes[2].grid(True)
pf.plot_monthly_returns_heatmap(returns=returns, ax=axes[3],)
fig.suptitle('BL Portfolio vs BSE-SENSEX - 2', fontsize=16, y=0.950)
plt.tight_layout()
plt.savefig('images/chart8', dpi=300)
Black–Litterman Portfolio vs S&P BSE-Sexsex
Strategy vs S&P BSE-Sensex Performance

It can be seen from the image above that we did pretty good and beat the benchmark on some of the key parameters such as annual returns, Sharpe ratio, Sortino ratio, Stability, Max drawdown, etc. The annual volatility and daily value at risk are slightly higher than the benchmark. We haven’t performed well on a few other parameters such as very high kurtosis and negative skew of returns. Let’s look at the graphical representation of performance on these and a few other parameters.

BL Portfolio vs Benchmark — 1
BL Portfolio vs Benchmark — 2

We can see that the cumulative return for the strategy is way ahead of the benchmark. 6 months rolling volatility is in line with the benchmark and both 6 and 12 months rolling beta is less than 1. However, there are high fluctuations in 6 months rolling Sharpe ratio which is worrisome.

It might be a coincidence that overall our strategy performed very well in comparison to S&P BSE-Sensex as I didn’t use a scientific approach to define my view and confidence in it. They were based on my gut feel. Also, please be aware that I have used free pricing data and there might be data quality issues. The objective is to go through the entire process with you and highlight the kind of flexibility and options this model offers. In my opinion, we need this to ensure enough confidence in our decisions regarding asset allocation for us and our clients.

Happy investing and do leave your feedback on the article :)

Thanks

Please Note: This analysis is only for educational purposes and the author is not liable for any of your investment decisions.

--

--