Portfolio Trading using PyPortfolioOpt (A Python Library for Portfolio Optimisation)

Neelakash Chatterjee
Cloudcraftz
Published in
6 min readMar 27, 2022

--

Hello readers, I’m back with our most beloved finance series . Today we are going to see how to trade a portfolio of stocks using a very famous python library called PyPortfolioOpt .

When we actually trade in real life we don’t only buy or sell a single stock we buy various stocks and each one varying in quantity . But how do we know, which ones to invest in and how much to invest ? Don’t worry PyPortfolioOpt makes our lives simple . Let’s see how .

Today in our trading scenario we take a portfolio of 4 stocks from the banking sector , namely AXISBANK HDFC ICICI and KOTAK . We’ll we buying various combinations of the above stocks based on some calculations .

Import Statements

from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices
from pypfopt import EfficientFrontier
from pypfopt import risk_models
from pypfopt import expected_returns

import numpy as np
import pandas as pd
import warnings
import matplotlib.pyplot as plt
warnings.filterwarnings('ignore')

Data Manipulations

Here we are loading the historical stock price data of the above mentioned stocks .

df_1, df_2 = pd.read_csv("./BANK SECTOR/AXISBANK.csv"), pd.read_csv("./BANK SECTOR/HDFCBANK.csv")
df_3, df_4 = pd.read_csv("./BANK SECTOR/ICICIBANK.csv"), pd.read_csv("./BANK SECTOR/KOTAKBANK.csv")


df_1['TIC'] = 'AXISBANK'
df_2['TIC'] = 'HDFCBANK'
df_3['TIC'] = 'ICICIBANK'
df_4['TIC'] = 'KOTAKBANK'
df = pd.concat([df_1, df_2, df_3, df_4], axis=0)

df.reset_index(drop=True, inplace=True)
df.sort_values(['Date', 'TIC'], inplace=True)
df.set_index('Date', inplace=True)
df.head()
df.tail()

Since we are interested in building a portfolio of the above stocks, we will only consider the Adj Close of the above stocks and create a new dataframe .

combined_df = df[['Adj Close', 'TIC']]
combined_df.reset_index(inplace=True)
combined_df.loc[:, 'Date'] = pd.to_datetime(combined_df.loc[:, 'Date'])
combined_df = combined_df.set_index(['Date', 'TIC']).unstack(level=-1)
combined_df.columns = combined_df.columns.droplevel()
combined_df.columns.name = None
combined_df.head()

Below is the format of the DataFrame which we should use while we are planning to trade a portfolio using PyPortfolioOpt library .

Historical Stock Prices

As we do for any other ML task we have to check for missing values as well , in case they are present we’ve to deal with them as well.

Below we are simply filling up the missing values using , ffill method .

combined_df.isnull().sum()
AXISBANK       0
HDFCBANK 204
ICICIBANK 0
KOTAKBANK 0
dtype: int64
combined_df.fillna(method='ffill', inplace=True)
combined_df.isnull().sum()
AXISBANK     0
HDFCBANK 0
ICICIBANK 0
KOTAKBANK 0
dtype: int64

Data Exploration and Analysis

A moving average plot of 3 months for all stocks in our portfolio

Here we are trying get an idea as to how the stocks are related to each other . You may clearly see that there is some pattern that all of these stocks follow, and that is quite obvious as they are all from the same sector i.e. Banking .

Note: We are taking a moving mean of 63 days to see the performance over a period of 3 months . (Each month approximately has 22 trading days) .

Another interesting observation is that , all these stocks had a great fall in the 2020–21 period , that was most probably due to the effect of COVID-19 . Data Visualisation can tell us so many things isn’t it ?

combined_df.rolling(63).mean().plot(figsize=(12, 6), ylabel='Price in Rs', fontsize=13)
<AxesSubplot:xlabel='Date', ylabel='Price in Rs'>

Since all stocks are from banking domain they are highly correlated

combined_df.corr()

Weight Optimisation for a period of 6 months

Now comes the most important part , how much money should I invest in each stock out of the total money that I’m willing to invest . What the PyPortfolioOpt does is , it takes the prices of each of the stock on a daily basis , calculates the returns for the given period (here the parameter frequency=126 is used , since there are 126 trading days in 6 months) , calculates the covariance matrix between all the stock involved in our portfolio and calculates the EfficientFrontier . But it is not necessary for us to understand all of this calculations , all we need to do is supply the prices, the period over which we want to trade and the amount of money or cash that we are willing to invest .

Note : EfficientFrontier is a concept used in finance, which in simple language means the portfolios that offer the lowest volatility for a given level of return. They also represent the portfolio of highest expected return for a given level of volatility.

Discrete Allocation , tells us how many stocks to buy for each stock in the portfolio , what would be the expected return and what would be the expected volatility for that period .

Note : Sharpe Ratio is Annual Return / Annual Volatility , taking risk free return as 0 .

# Calculate expected returns and sample covariance
mu = expected_returns.mean_historical_return(combined_df, frequency=126)
S = risk_models.sample_cov(combined_df)

# Optimize for maximal Sharpe ratio
ef = EfficientFrontier(mu, S)
raw_weights = ef.max_sharpe()
print("These are the raw weights: ", raw_weights)
cleaned_weights = ef.clean_weights()
print("These are the cleaned weights: ", cleaned_weights)
ef.portfolio_performance(verbose=True)
These are the raw weights:  OrderedDict([('AXISBANK', 0.0), ('HDFCBANK', 0.5931326198045054), ('ICICIBANK', 0.223813532713209), ('KOTAKBANK', 0.1830538474822856)])
These are the cleaned weights: OrderedDict([('AXISBANK', 0.0), ('HDFCBANK', 0.59313), ('ICICIBANK', 0.22381), ('KOTAKBANK', 0.18305)])
Expected annual return: 9.7%
Annual volatility: 21.9%
Sharpe Ratio: 0.35





(0.09745366188013037, 0.2185292774713777, 0.3544315103969307)
latest_prices = get_latest_prices(combined_df)

da = DiscreteAllocation(cleaned_weights, latest_prices, total_portfolio_value=100000)
allocation, leftover = da.greedy_portfolio()
print("Discrete allocation:", allocation)
print("Funds remaining: Rs.{:.2f}".format(leftover))
Discrete allocation: {'HDFCBANK': 41, 'ICICIBANK': 28, 'KOTAKBANK': 9}
Funds remaining: Rs.1086.98

Weight Optimisation for a period of 3 months

We did the same thing for 3 months, say we start trading today i.e. we buy the given stocks for each stock that our library spits out for us and we sell them after 3 months.

# Calculate expected returns and sample covariance
mu = expected_returns.mean_historical_return(combined_df, frequency=63)
S = risk_models.sample_cov(combined_df)

# Optimize for maximal Sharpe ratio
ef = EfficientFrontier(mu, S)
raw_weights = ef.max_sharpe()
print("These are the raw weights: ", raw_weights)
cleaned_weights = ef.clean_weights()
print("These are the cleaned weights: ", cleaned_weights)
ef.portfolio_performance(verbose=True)
These are the raw weights:  OrderedDict([('AXISBANK', 0.0), ('HDFCBANK', 0.5392578268766164), ('ICICIBANK', 0.2805247970633728), ('KOTAKBANK', 0.1802173760600108)])
These are the cleaned weights: OrderedDict([('AXISBANK', 0.0), ('HDFCBANK', 0.53926), ('ICICIBANK', 0.28052), ('KOTAKBANK', 0.18022)])
Expected annual return: 4.8%
Annual volatility: 22.3%
Sharpe Ratio: 0.13





(0.04828287293307085, 0.22338234337078958, 0.12661194482199722)
latest_prices = get_latest_prices(combined_df)

da = DiscreteAllocation(cleaned_weights, latest_prices, total_portfolio_value=100000)
allocation, leftover = da.greedy_portfolio()
print("Discrete allocation:", allocation)
print("Funds remaining: Rs.{:.2f}".format(leftover))
Discrete allocation: {'HDFCBANK': 38, 'ICICIBANK': 34, 'KOTAKBANK': 9}
Funds remaining: Rs.531.41

Easy, isn’t it ? But what did we observe ? we saw that the Sharperatio is not at all good for both the cases , so if you ask me …. I’m not investing in this portfolio , I’m out . So, although the process is simple but making money out of the market has never been easy . Invest carefully !

Thank you geeks , that is it for this weekend and I’ll be back with another story the next weekend . See ya geeks !!

Credits: (Who helped me in creating this article)
1. Nabojyoti Pandey (My colleague at Cloudcraftz Solutions Pvt. Ltd.)
2. Amrit Kumar Sarkar (My colleague at Cloudcraftz Solutions Pvt. Ltd.)

Credits: (Who helped me in creating this article)
1. Nabojyoti Pandey (My colleague at Cloudcraftz Solutions Pvt. Ltd.)
2. Amrit Kumar Sarkar (My colleague at Cloudcraftz Solutions Pvt. Ltd.)

--

--

Neelakash Chatterjee
Cloudcraftz

Data Scientist @NX Block Trades Pvt Ltd | Ex-Juniour Data Scientist @Cloudcraftz Solutions Pvt Ltd | MBA (FinTech) @BITS https://www.linkedin.com/in/neelakash-c