import numpy as np
import pandas as pd
import yfinance as yf
import warnings
warnings.filterwarnings("ignore")
pd.options.display.float_format = '{:.4%}'.format
# Date range
start = '2016-01-01'
end = '2019-12-30'
# Tickers of assets
assets = ['JCI', 'AMZN', 'CMCSA', 'CPB', 'MO', 'APA', 'MMC', 'JPM',
'ZION', 'AAPL', 'BAX', 'BMY', 'LUV', 'PCAR', 'TXT', 'TMO',
'DE', 'MSFT', 'HPQ', 'SEE', 'VZ', 'CNP', 'NI', 'T', 'BA']
assets.sort()
# Downloading data
data = yf.download(assets, start = start, end = end)
data = data.loc[:,('Adj Close', slice(None))]
data.columns = assets
[*********************100%***********************] 25 of 25 completed
# Calculating returns
Y = data[assets].iloc[-300:,:].pct_change().dropna()
display(Y.head())
AAPL | AMZN | APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | ... | MO | MSFT | NI | PCAR | SEE | T | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Date | |||||||||||||||||||||
2018-10-19 00:00:00+00:00 | 1.5230% | -0.3778% | 0.0475% | -0.8599% | -1.4333% | -3.0011% | 0.1113% | 1.2968% | 3.4360% | -0.8764% | ... | 1.6741% | 0.1475% | 0.6339% | -0.1823% | -0.7728% | 1.1384% | -1.1145% | -1.2872% | 0.4574% | -0.8025% |
2018-10-22 00:00:00+00:00 | 0.6110% | 1.4325% | -1.9240% | -0.0786% | -0.6334% | -6.2983% | -0.6392% | -1.1025% | 0.0528% | -0.3221% | ... | -1.0331% | 0.8927% | -0.8661% | 0.4483% | -2.8972% | -0.6084% | -0.6075% | -0.8635% | 0.1457% | -3.4490% |
2018-10-23 00:00:00+00:00 | 0.9427% | -1.1513% | -3.6571% | -1.6658% | -0.4202% | -0.4521% | -0.2797% | -0.5034% | 0.1844% | -3.9948% | ... | 0.8808% | -1.3956% | 0.4766% | -5.1240% | -0.0321% | 1.0713% | -1.0807% | -1.8308% | 4.0560% | 4.0353% |
2018-10-24 00:00:00+00:00 | -3.4302% | -5.9083% | -4.5501% | 1.3141% | -1.8042% | -3.5933% | -4.2917% | 0.8674% | 0.9995% | -4.1108% | ... | 0.7437% | -5.3469% | 3.5178% | -4.2683% | -1.3479% | -8.0558% | -1.2403% | -4.2187% | 0.3671% | -3.3065% |
2018-10-25 00:00:00+00:00 | 2.1898% | 7.0887% | 0.4741% | 2.5716% | 0.5186% | 0.7782% | 5.0410% | -0.5733% | -1.1718% | 2.1585% | ... | 1.3642% | 5.8444% | -1.0310% | 0.4914% | 0.9109% | -1.2516% | 4.3662% | 1.3800% | -1.7241% | 3.3538% |
5 rows × 25 columns
The Kurtosis portfolio model proposed by Cajas (2022) shows how to optimize the fourth moment of portfolio returns in a similar way than portfolio variance.
It is recommended to use MOSEK to optimize Kurtosis for a large number of assets due the model use semidefinite programming. Also, for a large number of assets is recommended to use the relaxed version of this model based only on second order cone programming. To use the relaxed version we have to use a number of assets higher than the property n_max_kurt, so for example if number of assets is 30 and we set port.n_max_kurt = 25, riskfolio-lib is going to use the relaxed version.
Instructions to install MOSEK are in this link, is better to install using Anaconda. Also you will need a license, I recommend you that ask for an academic license here.
import riskfolio as rp
import mosek
# Building the portfolio object
port = rp.Portfolio(returns=Y)
# Calculating optimum portfolio
# Select method and estimate input parameters:
method_mu='hist' # Method to estimate expected returns based on historical data.
method_cov='hist' # Method to estimate covariance matrix based on historical data.
method_kurt='hist' # Method to estimate cokurtosis square matrix based on historical data.
port.assets_stats(method_mu=method_mu,
method_cov=method_cov,
method_kurt=method_kurt,
)
port.solvers = ['MOSEK'] # It is recommended to use mosek when optimizing Kurtosis
# Estimate optimal portfolio:
model ='Classic' # Could be Classic (historical), BL (Black Litterman) or FM (Factor Model)
rm = 'KT' # Risk measure used, this time will be Tail Gini Range
obj = 'Sharpe' # Objective function, could be MinRisk, MaxRet, Utility or Sharpe
hist = True # Use historical scenarios for risk measures that depend on scenarios
rf = 0 # Risk free rate
l = 0 # Risk aversion factor, only useful when obj is 'Utility'
w = port.optimization(model=model, rm=rm, obj=obj, rf=rf, l=l, hist=hist)
display(w.T)
You must convert self.kurt to a positive definite matrix You must convert self.skurt to a positive definite matrix
AAPL | AMZN | APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | ... | MO | MSFT | NI | PCAR | SEE | T | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
weights | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 4.7028% | 0.0000% | 0.0000% | 11.1102% | 0.0000% | ... | 0.0000% | 0.0000% | 2.9795% | 5.4946% | 8.0196% | 4.8792% | 1.2582% | 0.0000% | 23.3527% | 0.0000% |
1 rows × 25 columns
# Plotting the composition of the portfolio
ax = rp.plot_pie(w=w,
title='Sharpe Mean - Kurtosis',
others=0.05,
nrow=25,
cmap = "tab20",
height=6,
width=10,
ax=None)
ax = rp.plot_hist(returns=Y,
w=w,
alpha=0.05,
bins=50,
height=6,
width=10,
ax=None)
points = 50 # Number of points of the frontier
frontier = port.efficient_frontier(model=model, rm=rm, points=points, rf=rf, hist=hist)
display(frontier.T.head())
AAPL | AMZN | APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | ... | MO | MSFT | NI | PCAR | SEE | T | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 0.6894% | 6.6516% | 4.7593% | 17.4514% | 0.0000% | 0.0000% | ... | 11.7019% | 0.0000% | 10.5489% | 0.0000% | 8.5918% | 0.5055% | 0.0000% | 0.0000% | 26.1992% | 3.3163% |
1 | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 6.6966% | 1.6972% | 6.4671% | 3.6874% | 0.0000% | ... | 0.9022% | 0.0000% | 10.1653% | 0.0000% | 10.1175% | 4.9220% | 0.0000% | 0.0000% | 27.3730% | 0.0000% |
2 | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 6.1761% | 0.0000% | 0.0000% | 7.7140% | 0.0000% | ... | 0.0000% | 0.0000% | 8.0389% | 1.2117% | 9.4790% | 5.8509% | 0.0000% | 0.0000% | 26.0669% | 0.0000% |
3 | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 4.9226% | 0.0000% | 0.0000% | 10.8113% | 0.0000% | ... | 0.0000% | 0.0000% | 3.4604% | 5.0189% | 8.1531% | 4.9600% | 0.8984% | 0.0000% | 23.6468% | 0.0000% |
4 | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 0.0000% | 3.4452% | 0.0000% | 0.0000% | 12.6920% | 0.0000% | ... | 0.0000% | 0.0000% | 0.5352% | 8.2394% | 7.3098% | 4.4609% | 3.2102% | 0.0000% | 21.7050% | 0.0000% |
5 rows × 25 columns
# Plotting the efficient frontier
label = 'Max Risk Adjusted Return Portfolio' # Title of point
mu = port.mu # Expected returns
cov = port.cov # Covariance matrix
returns = port.returns # Returns of the assets
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm=rm,
rf=rf, alpha=0.05, cmap='viridis', w=w, label=label,
marker='*', s=16, c='r', height=6, width=10, ax=None)
# Plotting efficient frontier composition
ax = rp.plot_frontier_area(w_frontier=frontier, cmap="tab20", height=6, width=10, ax=None)
b = None # Risk contribution constraints vector
w_rp = port.rp_optimization(model=model, rm=rm, rf=rf, b=b, hist=hist)
display(w_rp.T)
AAPL | AMZN | APA | BA | BAX | BMY | CMCSA | CNP | CPB | DE | ... | MO | MSFT | NI | PCAR | SEE | T | TMO | TXT | VZ | ZION | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
weights | 2.4551% | 2.3390% | 2.3561% | 2.9710% | 3.8989% | 4.2350% | 4.0183% | 6.7929% | 5.1240% | 2.6398% | ... | 5.7290% | 2.7138% | 7.0910% | 3.2300% | 4.6015% | 4.4500% | 3.2760% | 3.3235% | 7.4288% | 3.4346% |
1 rows × 25 columns
ax = rp.plot_pie(w=w_rp,
title='Risk Parity Square Root Kurtosis',
others=0.05,
nrow=25,
cmap="tab20",
height=6,
width=10,
ax=None)
ax = rp.plot_risk_con(w_rp, cov=port.cov, returns=port.returns, rm=rm, rf=0, alpha=0.05,
color="tab:blue", height=6, width=10, ax=None)
# Plotting the efficient frontier
ws = pd.concat([w, w_rp],axis=1)
ws.columns = ["Max Return/Kurtosis", "Risk Parity Kurtosis"]
mu = port.mu # Expected returns
cov = port.cov # Covariance matrix
returns = port.returns # Returns of the assets
ax = rp.plot_frontier(w_frontier=frontier, mu=mu, cov=cov, returns=returns, rm=rm,
rf=rf, alpha=0.05, cmap='viridis', w=ws,
marker='*', s=16, height=6, width=10, ax=None)