examples/portfolioOptimizationUsingModernPortfolioTheory.ipynb
This notebook utilizes OpenBB’s data for portfolio optimization based on MPT principles. We would be optimizing a portfolio of top 10 crypto assets, using the daily close data from 1st october 2023 to 1st october 2024.
The portfolio optimization would be done using the mean-variance approach. The mean-variance approach helps determine the optimal allocation of assets in a portfolio to minimize overall risk while maximizing expected returns.
Modern Portfolio Theory (MPT) is a mathematical framework for constructing a portfolio of assets to maximize expected return based on a given level of risk. In this notebook, we will implement MPT to construct an optimal portfolio with minimum volatility using a selection of the top cryptocurrencies as our assets. The notebook will fetch historical price data for these assets, calculate the portfolio's expected return and risk, and visualize the optimal portfolio based on risk-return trade-offs.
Install external packages
!pip install openbb
!pip install PyPortfolioOpt
Import necessary packages
from openbb import obb
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pypfopt import EfficientFrontier
from pypfopt import CovarianceShrinkage, CLA, expected_returns
Fetch the daily data of the top crypto currencies for a period of one year using openbb
top_crypto= ['ADA-USD', 'BNB-USD', 'BTC-USD', 'DOT-USD', 'ETH-USD', 'LTC-USD','MATIC-USD', 'SOL-USD', 'TRX-USD', 'XRP-USD']
ohlc_data= obb.crypto.price.historical(top_crypto, provider="yfinance", interval='1d', start_date='2023-10-01', end_date='2024-10-01').to_df()
ohlc_data
Select the close data from ohlc_data for each crypto currency
close_symbol= ohlc_data[['close', 'symbol']]
# Setting the symbol as the second index level
close_symbol = close_symbol.set_index('symbol', append= True)
# Unstack 'symbol' to make each unique symbol a separate column
close_symbol_unstacked= close_symbol.unstack(level='symbol')
# Flatten the column headers
close_symbol_unstacked.columns = close_symbol_unstacked.columns.get_level_values(1)
prices= close_symbol_unstacked
prices
The expected returns serve as the basis for evaluating different asset combinations and determining the optimal portfolio allocation. In the next steps, these returns will be used along with the covariance matrix to analyze risk-return profiles.
$$return=\frac{Σ\space r_{i}}{N}⋅365$$
where $r_i$ is the daily return of a particular asset and $N$ is the number of days in the data, we multiply by 365 so as to annualize the result
# Expected returns of crypto assets using the mean historical return.
assets_expected_returns = expected_returns.mean_historical_return(prices, frequency=365, compounding=False)
assets_expected_returns
The frequency parameter indicates the number of trading periods in a year. For crypto daily data, it is set to 365, since crypto can be traded everyday.
The compounding parameter calculate returns using simple or compounded growth. Setting compounding=False results in simple annualized returns. If compounding=True, the function would compute geometric (compounded) returns, which consider reinvested returns over time.
The covariance matrix represents the relationship between the returns of the assets. It measures how returns of one asset(e.g btc) vary in relation to another(e.g eth), which is essential for understanding the overall risk of a portfolio. Assets with high positive covariance tend to move in the same direction, while those with negative covariance move in opposite directions.
The formula below uses an example of btc and eth to express how covariance is calculated:
$$Cov({r_{btc}, r_{eth} })= \frac{Σ(r_{btc}-\bar r_{btc})(r_{eth}-\bar r_{eth})}{N}$$
Where $r$ is the daily returns of the assets and $\bar r$ is the average daily returns of the assets.
# Covariance matrix of crypto assets using the Ledoit-Wolf shrinkage method.
covariance = CovarianceShrinkage(prices).ledoit_wolf()
covariance
Ledoit-Wolf Shrinkage:is a technique used to improve the estimation of covariance matrices, especially when dealing with high-dimensional data (like multiple crypto assets) relative to the number of observations.
Portfolio risk, also known as portfolio volatility, is determined by calculating the variance of the returns of the assets. The variance is calculated using the below equation:
$$ \sigma^2= W⋅Cov⋅W^T $$
Where $W$ is the weights of the asstes and $Cov$ is the covariance of the returns of the assets in the portfolio.
The Critical Line Algorithm (CLA) optimizes asset weights in a portfolio by calculating the risk and expected return for various combinations of these weights. It systematically varies the weights assigned to each asset, assessing how each combination affects overall portfolio performance. This process helps identify efficient portfolios that maximize expected returns for a given level of risk or minimize risk for a desired return.
# Create a Critical Line Algorithm (CLA) object using the calculated expected returns and covariance matrix.
cla = CLA(assets_expected_returns, covariance)
The Efficient Frontier is a curve showing optimal portfolios with the best risk-return tradeoffs. It's important because it helps us identify portfolios that maximize return for a given risk level or minimize risk for a desired return.
# Calculate the efficient frontier, obtaining the returns, volatility, and weights for various portfolios.
(returns, volatility, weights) = cla.efficient_frontier()
efficient_frontier_portfolios= pd.DataFrame([returns, volatility, weights]).T
efficient_frontier_portfolios.columns=['returns', 'volatility', 'weights']
efficient_frontier_portfolios
plt.figure(figsize=(10,5))
plt.scatter(volatility, returns, label='Portfolios on efficient frontier')
plt.legend()
plt.ylabel('Expected Reward')
plt.xlabel('Volatiity')
plt.show()
Get the weights (in perentage) of the portfolio with the lowest volatility
optimized_weight=np.array(list(cla.max_sharpe().values()))
optimized_weight= np.round(optimized_weight, 4)
pie_df=pd.DataFrame(optimized_weight*100, index=prices.columns, columns=['weights'])
pie_df= pie_df.sort_values(by=['weights'], ascending=False)
pie_df
Display Pie Chart of the weights
pie_df= pie_df.query('weights != 0.000000')
fig, ax = plt.subplots()
ax.pie(pie_df.weights, labels=pie_df.index.values.tolist(), autopct='%1.1f%%', radius=2)
plt.show()
The above weights forms a portfolio with maximum sharpe ratio