mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
New-Portfolio-Optimization-Menu
This commit is contained in:
@@ -9,11 +9,15 @@ from typing import List, Tuple
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import yfinance as yf
|
||||
import riskfolio as rp
|
||||
|
||||
from openbb_terminal.decorators import log_start_end
|
||||
from openbb_terminal.portfolio.portfolio_optimization import yahoo_finance_model
|
||||
from openbb_terminal.rich_config import console
|
||||
from openbb_terminal.portfolio.portfolio_optimization.riskfolio import (
|
||||
Portfolio,
|
||||
HCPortfolio,
|
||||
AuxFunctions,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -330,7 +334,7 @@ def get_mean_risk_portfolio(
|
||||
risk_free_rate = risk_free_rate / time_factor[freq.upper()]
|
||||
|
||||
# Building the portfolio object
|
||||
port = rp.Portfolio(returns=stock_returns, alpha=alpha)
|
||||
port = Portfolio.Portfolio(returns=stock_returns, alpha=alpha)
|
||||
|
||||
# Estimate input parameters:
|
||||
port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma)
|
||||
@@ -462,7 +466,7 @@ def get_max_diversification_portfolio(
|
||||
)
|
||||
|
||||
# Building the portfolio object
|
||||
port = rp.Portfolio(returns=stock_returns)
|
||||
port = Portfolio.Portfolio(returns=stock_returns)
|
||||
|
||||
# Estimate input parameters:
|
||||
port.assets_stats(method_mu="hist", method_cov=covariance, d=d_ewma)
|
||||
@@ -572,11 +576,11 @@ def get_max_decorrelation_portfolio(
|
||||
)
|
||||
|
||||
# Building the portfolio object
|
||||
port = rp.Portfolio(returns=stock_returns)
|
||||
port = Portfolio.Portfolio(returns=stock_returns)
|
||||
|
||||
# Estimate input parameters:
|
||||
port.assets_stats(method_mu="hist", method_cov=covariance, d=d_ewma)
|
||||
port.cov = rp.cov2corr(port.cov)
|
||||
port.cov = AuxFunctions.cov2corr(port.cov)
|
||||
|
||||
# Budget constraints
|
||||
port.upperlng = value
|
||||
@@ -726,7 +730,7 @@ def get_risk_parity_portfolio(
|
||||
risk_free_rate = risk_free_rate / time_factor[freq.upper()]
|
||||
|
||||
# Building the portfolio object
|
||||
port = rp.Portfolio(returns=stock_returns, alpha=alpha)
|
||||
port = Portfolio.Portfolio(returns=stock_returns, alpha=alpha)
|
||||
|
||||
# Calculating optimal portfolio
|
||||
port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma)
|
||||
@@ -872,7 +876,7 @@ def get_rel_risk_parity_portfolio(
|
||||
)
|
||||
|
||||
# Building the portfolio object
|
||||
port = rp.Portfolio(returns=stock_returns)
|
||||
port = Portfolio.Portfolio(returns=stock_returns)
|
||||
|
||||
# Calculating optimal portfolio
|
||||
port.assets_stats(method_mu=mean, method_cov=covariance, d=d_ewma)
|
||||
@@ -1129,7 +1133,7 @@ def get_hcp_portfolio(
|
||||
|
||||
risk_free_rate = risk_free_rate / time_factor[freq.upper()]
|
||||
# Building the portfolio object
|
||||
port = rp.HCPortfolio(
|
||||
port = HCPortfolio.HCPortfolio(
|
||||
returns=stock_returns,
|
||||
alpha=alpha,
|
||||
a_sim=a_sim,
|
||||
|
||||
@@ -12,7 +12,6 @@ from datetime import date
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import riskfolio as rp
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib.gridspec import GridSpec
|
||||
|
||||
@@ -26,6 +25,12 @@ from openbb_terminal.portfolio.portfolio_optimization import (
|
||||
optimizer_model,
|
||||
yahoo_finance_model,
|
||||
)
|
||||
from openbb_terminal.portfolio.portfolio_optimization.riskfolio import (
|
||||
Portfolio,
|
||||
PlotFunctions,
|
||||
RiskFunctions,
|
||||
)
|
||||
|
||||
from openbb_terminal.rich_config import console
|
||||
from openbb_terminal.config_terminal import theme
|
||||
|
||||
@@ -105,6 +110,7 @@ time_factor = {
|
||||
"M": 12.0,
|
||||
}
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def d_period(period: str, start: str, end: str):
|
||||
"""
|
||||
@@ -149,6 +155,7 @@ def d_period(period: str, start: str, end: str):
|
||||
|
||||
return p
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def portfolio_performance(
|
||||
weights: dict,
|
||||
@@ -236,7 +243,7 @@ def portfolio_performance(
|
||||
print(f"Sharpe ratio: {sharpe:.4f}")
|
||||
|
||||
if risk_measure != "MV":
|
||||
risk = rp.Sharpe_Risk(
|
||||
risk = RiskFunctions.Sharpe_Risk(
|
||||
weights,
|
||||
cov=stock_returns.cov(),
|
||||
returns=stock_returns,
|
||||
@@ -285,6 +292,7 @@ def portfolio_performance(
|
||||
"Return / " + risk_names[risk_measure.lower()] + f" ratio: {sharpe_2:.4f}"
|
||||
)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_weights(weights: dict, market_neutral: bool = False):
|
||||
"""
|
||||
@@ -320,6 +328,7 @@ def display_weights(weights: dict, market_neutral: bool = False):
|
||||
header = "Value ($)" if tot_value > 1.01 else "Value (%)"
|
||||
print_rich_table(weight_df, headers=[header], show_index=True, title="Weights")
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_equal_weight(
|
||||
stocks: List[str],
|
||||
@@ -425,6 +434,7 @@ def display_equal_weight(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_property_weighting(
|
||||
stocks: List[str],
|
||||
@@ -533,6 +543,7 @@ def display_property_weighting(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_mean_risk(
|
||||
stocks: List[str],
|
||||
@@ -719,6 +730,7 @@ def display_mean_risk(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_max_sharpe(
|
||||
stocks: List[str],
|
||||
@@ -860,6 +872,7 @@ def display_max_sharpe(
|
||||
)
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_min_risk(
|
||||
stocks: List[str],
|
||||
@@ -1001,6 +1014,7 @@ def display_min_risk(
|
||||
)
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_max_util(
|
||||
stocks: List[str],
|
||||
@@ -1147,6 +1161,7 @@ def display_max_util(
|
||||
)
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_max_ret(
|
||||
stocks: List[str],
|
||||
@@ -1288,6 +1303,7 @@ def display_max_ret(
|
||||
)
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_max_div(
|
||||
stocks: List[str],
|
||||
@@ -1400,6 +1416,7 @@ def display_max_div(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_max_decorr(
|
||||
stocks: List[str],
|
||||
@@ -1513,6 +1530,7 @@ def display_max_decorr(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_ef(
|
||||
stocks: List[str],
|
||||
@@ -1616,7 +1634,7 @@ def display_ef(
|
||||
risk_free_rate = risk_free_rate / time_factor[freq.upper()]
|
||||
|
||||
# Building the portfolio object
|
||||
port = rp.Portfolio(returns=stock_returns, alpha=alpha)
|
||||
port = Portfolio.Portfolio(returns=stock_returns, alpha=alpha)
|
||||
|
||||
# Estimate input parameters:
|
||||
port.assets_stats(method_mu="hist", method_cov="hist")
|
||||
@@ -1662,7 +1680,7 @@ def display_ef(
|
||||
|
||||
for i in range(frontier.shape[1]):
|
||||
w = np.array(frontier.iloc[:, i], ndmin=2).T
|
||||
risk = rp.Sharpe_Risk(
|
||||
risk = RiskFunctions.Sharpe_Risk(
|
||||
w,
|
||||
cov=cov,
|
||||
returns=stock_returns,
|
||||
@@ -1687,7 +1705,7 @@ def display_ef(
|
||||
ax = external_axes[0]
|
||||
|
||||
frontier = pd.concat([frontier, random_weights], axis=1)
|
||||
ax = rp.plot_frontier(
|
||||
ax = PlotFunctions.plot_frontier(
|
||||
w_frontier=frontier,
|
||||
mu=mu,
|
||||
cov=cov,
|
||||
@@ -1708,7 +1726,7 @@ def display_ef(
|
||||
# Add risk free line
|
||||
if tangency:
|
||||
ret_sharpe = (mu @ weights).to_numpy().item() * time_factor[freq.upper()]
|
||||
risk_sharpe = rp.Sharpe_Risk(
|
||||
risk_sharpe = RiskFunctions.Sharpe_Risk(
|
||||
weights,
|
||||
cov=cov,
|
||||
returns=stock_returns,
|
||||
@@ -1745,6 +1763,7 @@ def display_ef(
|
||||
if external_axes is None:
|
||||
theme.visualize_output(force_tight_layout=False)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_risk_parity(
|
||||
stocks: List[str],
|
||||
@@ -1898,6 +1917,7 @@ def display_risk_parity(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_rel_risk_parity(
|
||||
stocks: List[str],
|
||||
@@ -2038,6 +2058,7 @@ def display_rel_risk_parity(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_hcp(
|
||||
stocks: List[str],
|
||||
@@ -2311,6 +2332,7 @@ def display_hcp(
|
||||
console.print("")
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_hrp(
|
||||
stocks: List[str],
|
||||
@@ -2528,6 +2550,7 @@ def display_hrp(
|
||||
)
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_herc(
|
||||
stocks: List[str],
|
||||
@@ -2753,6 +2776,7 @@ def display_herc(
|
||||
)
|
||||
return weights
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_nco(
|
||||
stocks: List[str],
|
||||
@@ -3092,6 +3116,7 @@ def pie_chart_weights(
|
||||
if external_axes is None:
|
||||
theme.visualize_output()
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def additional_plots(
|
||||
weights: Dict,
|
||||
@@ -3196,7 +3221,7 @@ def additional_plots(
|
||||
else:
|
||||
ax = external_axes[0]
|
||||
|
||||
ax = rp.plot_hist(
|
||||
ax = PlotFunctions.plot_hist(
|
||||
stock_returns, w=pd.Series(weights).to_frame(), alpha=alpha, ax=ax
|
||||
)
|
||||
ax.legend(fontsize="x-small", loc="best")
|
||||
@@ -3215,7 +3240,7 @@ def additional_plots(
|
||||
ax = external_axes[0]
|
||||
|
||||
nav = stock_returns.cumsum()
|
||||
ax = rp.plot_drawdown(
|
||||
ax = PlotFunctions.plot_drawdown(
|
||||
nav=nav, w=pd.Series(weights).to_frame(), alpha=alpha, ax=ax
|
||||
)
|
||||
|
||||
@@ -3239,7 +3264,7 @@ def additional_plots(
|
||||
else:
|
||||
ax = external_axes[0]
|
||||
|
||||
ax = rp.plot_risk_con(
|
||||
ax = PlotFunctions.plot_risk_con(
|
||||
w=pd.Series(weights).to_frame(),
|
||||
cov=stock_returns.cov(),
|
||||
returns=stock_returns,
|
||||
@@ -3267,7 +3292,7 @@ def additional_plots(
|
||||
else:
|
||||
ax = external_axes[0]
|
||||
|
||||
ax = rp.plot_clusters(
|
||||
ax = PlotFunctions.plot_clusters(
|
||||
returns=stock_returns,
|
||||
codependence="pearson",
|
||||
linkage="ward",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,964 @@
|
||||
"""""" #
|
||||
"""
|
||||
Copyright (c) 2020-2022, Dany Cajas
|
||||
All rights reserved.
|
||||
This work is licensed under BSD 3-Clause "New" or "Revised" License.
|
||||
License available at https://github.com/dcajasn/Riskfolio-Lib/blob/master/LICENSE.txt
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import scipy.cluster.hierarchy as hr
|
||||
from scipy.spatial.distance import squareform
|
||||
import openbb_terminal.portfolio.portfolio_optimization.riskfolio.AuxFunctions as af
|
||||
import openbb_terminal.portfolio.portfolio_optimization.riskfolio.DBHT as db
|
||||
|
||||
|
||||
def assets_constraints(constraints, asset_classes):
|
||||
r"""
|
||||
Create the linear constraints matrixes A and B of the constraint
|
||||
:math:`Aw \geq B`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraints : DataFrame of shape (n_constraints, n_fields)
|
||||
Constraints matrix, where n_constraints is the number of constraints
|
||||
and n_fields is the number of fields of constraints matrix, the fields
|
||||
are:
|
||||
|
||||
- Disabled: (bool) indicates if the constraint is enable.
|
||||
- Type: (str) can be: 'Assets', 'Classes', 'All Assets', 'Each asset in a class' and 'All Classes'.
|
||||
- Set: (str) if Type is 'Classes', 'Each asset in a class' or 'All Classes'specified the name of the asset's classes set.
|
||||
- Position: (str) the name of the asset or asset class of the constraint.
|
||||
- Sign: (str) can be '>=' or '<='.
|
||||
- Weight: (scalar) is the maximum or minimum weight of the absolute constraint.
|
||||
- Type Relative: (str) can be: 'Assets' or 'Classes'.
|
||||
- Relative Set: (str) if Type Relative is 'Classes' specified the name of the set of asset classes.
|
||||
- Relative: (str) the name of the asset or asset class of the relative constraint.
|
||||
- Factor: (scalar) is the factor of the relative constraint.
|
||||
|
||||
asset_classes : DataFrame of shape (n_assets, n_cols)
|
||||
Asset's classes matrix, where n_assets is the number of assets and
|
||||
n_cols is the number of columns of the matrix where the first column
|
||||
is the asset list and the next columns are the different asset's
|
||||
classes sets.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A : nd-array
|
||||
The matrix A of :math:`Aw \geq B`.
|
||||
|
||||
B : nd-array
|
||||
The matrix B of :math:`Aw \geq B`.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError when the value cannot be calculated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
import riskfolio as rp
|
||||
|
||||
asset_classes = {'Assets': ['FB', 'GOOGL', 'NTFX', 'BAC', 'WFC', 'TLT', 'SHV'],
|
||||
'Class 1': ['Equity', 'Equity', 'Equity', 'Equity', 'Equity',
|
||||
'Fixed Income', 'Fixed Income'],
|
||||
'Class 2': ['Technology', 'Technology', 'Technology',
|
||||
'Financial', 'Financial', 'Treasury', 'Treasury'],}
|
||||
|
||||
asset_classes = pd.DataFrame(asset_classes)
|
||||
asset_classes = asset_classes.sort_values(by=['Assets'])
|
||||
|
||||
constraints = {'Disabled': [False, False, False, False, False, False, False],
|
||||
'Type': ['Classes', 'Classes', 'Assets', 'Assets', 'Classes',
|
||||
'All Assets', 'Each asset in a class'],
|
||||
'Set': ['Class 1', 'Class 1', '', '', 'Class 2', '', 'Class 1'],
|
||||
'Position': ['Equity', 'Fixed Income', 'BAC', 'WFC', 'Financial',
|
||||
'', 'Equity'],
|
||||
'Sign': ['<=', '<=', '<=', '<=', '>=', '>=', '>='],
|
||||
'Weight': [0.6, 0.5, 0.1, '', '', 0.02, ''],
|
||||
'Type Relative': ['', '', '', 'Assets', 'Classes', '', 'Assets'],
|
||||
'Relative Set': ['', '', '', '', 'Class 1', '', ''],
|
||||
'Relative': ['', '', '', 'FB', 'Fixed Income', '', 'TLT'],
|
||||
'Factor': ['', '', '', 1.2, 0.5, '', 0.4]}
|
||||
|
||||
constraints = pd.DataFrame(constraints)
|
||||
|
||||
|
||||
The constraint looks like this:
|
||||
|
||||
.. image:: images/Constraints.png
|
||||
|
||||
It is easier to construct the constraints in excel and then upload to a
|
||||
dataframe.
|
||||
|
||||
To create the matrixes A and B we use the following command:
|
||||
|
||||
::
|
||||
|
||||
A, B = rp.assets_constraints(constraints, asset_classes)
|
||||
|
||||
|
||||
The matrixes A and B looks like this (all constraints were converted to a linear
|
||||
constraint):
|
||||
|
||||
.. image:: images/AxB.png
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(constraints, pd.DataFrame) and not isinstance(
|
||||
asset_classes, pd.DataFrame
|
||||
):
|
||||
raise ValueError("constraints and asset_classes must be DataFrames")
|
||||
|
||||
if constraints.shape[1] != 10:
|
||||
raise ValueError("constraints must have ten columns")
|
||||
|
||||
n = len(constraints)
|
||||
m = len(asset_classes)
|
||||
data = constraints.fillna("")
|
||||
data = data.values.tolist()
|
||||
assetslist = asset_classes.iloc[:, 0].values.tolist()
|
||||
|
||||
A = []
|
||||
B = []
|
||||
for i in range(0, n):
|
||||
if data[i][0] == False:
|
||||
if data[i][1] == "Assets":
|
||||
item = assetslist.index(data[i][3])
|
||||
if data[i][4] == ">=":
|
||||
d = 1
|
||||
elif data[i][4] == "<=":
|
||||
d = -1
|
||||
if data[i][5] != "":
|
||||
A1 = [0] * m
|
||||
A1[item] = d
|
||||
A.append(A1)
|
||||
B.append([data[i][5] * d])
|
||||
else:
|
||||
A1 = [0] * m
|
||||
A1[item] = 1
|
||||
if data[i][6] == "Assets":
|
||||
item2 = assetslist.index(data[i][8])
|
||||
A2 = [0] * m
|
||||
A2[item2] = 1
|
||||
elif data[i][6] == "Classes":
|
||||
A2 = np.where(
|
||||
asset_classes[data[i][7]].values == data[i][8], 1, 0
|
||||
)
|
||||
A1 = ((np.array(A1) + np.array(A2) * data[i][9] * -1) * d).tolist()
|
||||
A.append(A1)
|
||||
B.append([0])
|
||||
elif data[i][1] == "All Assets":
|
||||
item = len(assetslist)
|
||||
if data[i][4] == ">=":
|
||||
d = 1
|
||||
elif data[i][4] == "<=":
|
||||
d = -1
|
||||
if data[i][5] != "":
|
||||
A1 = np.identity(item) * d
|
||||
A1 = A1.tolist()
|
||||
B1 = np.ones((item, 1)) * d * data[i][5]
|
||||
for i in range(0, item):
|
||||
A.append(A1[i])
|
||||
B.append(B1.tolist()[0])
|
||||
else:
|
||||
A1 = np.identity(item)
|
||||
if data[i][6] == "Assets":
|
||||
item2 = assetslist.index(data[i][8])
|
||||
A2 = np.zeros((item, item - 1))
|
||||
A2 = np.insert(A2, item2 - 1, 1, axis=1)
|
||||
elif data[i][6] == "Classes":
|
||||
A1 = np.identity(item)
|
||||
A2 = np.where(
|
||||
asset_classes[data[i][7]].values == data[i][8], 1, 0
|
||||
)
|
||||
A2 = np.ones((item, item)) * np.array(A2)
|
||||
A1 = ((np.array(A1) + np.array(A2) * data[i][9] * -1) * d).tolist()
|
||||
for i in range(0, item):
|
||||
A.append(A1[i])
|
||||
B.append([0])
|
||||
elif data[i][1] == "Classes":
|
||||
if data[i][4] == ">=":
|
||||
d = 1
|
||||
elif data[i][4] == "<=":
|
||||
d = -1
|
||||
if data[i][5] != "":
|
||||
A1 = np.where(asset_classes[data[i][2]].values == data[i][3], 1, 0)
|
||||
A1 = np.array(A1) * d
|
||||
A1 = A1.tolist()
|
||||
A.append(A1)
|
||||
B.append([data[i][5] * d])
|
||||
else:
|
||||
A1 = np.where(asset_classes[data[i][2]].values == data[i][3], 1, 0)
|
||||
if data[i][6] == "Assets":
|
||||
item2 = assetslist.index(data[i][8])
|
||||
A2 = [0] * m
|
||||
A2[item2] = 1
|
||||
elif data[i][6] == "Classes":
|
||||
A2 = np.where(
|
||||
asset_classes[data[i][7]].values == data[i][8], 1, 0
|
||||
)
|
||||
A1 = ((np.array(A1) + np.array(A2) * data[i][9] * -1) * d).tolist()
|
||||
A.append(A1)
|
||||
B.append([0])
|
||||
elif data[i][1] == "Each asset in a class":
|
||||
if data[i][4] == ">=":
|
||||
d = 1
|
||||
elif data[i][4] == "<=":
|
||||
d = -1
|
||||
if data[i][5] != "":
|
||||
A1 = np.where(asset_classes[data[i][2]].values == data[i][3], 1, 0)
|
||||
l = 0
|
||||
for k in A1:
|
||||
if k == 1:
|
||||
A3 = [0] * m
|
||||
A3[l] = 1 * d
|
||||
A.append(A3)
|
||||
B.append([data[i][5] * d])
|
||||
l = l + 1
|
||||
else:
|
||||
A1 = np.where(asset_classes[data[i][2]].values == data[i][3], 1, 0)
|
||||
l = 0
|
||||
for k in A1:
|
||||
if k == 1:
|
||||
A3 = [0] * m
|
||||
A3[l] = 1
|
||||
if data[i][6] == "Assets":
|
||||
item2 = assetslist.index(data[i][8])
|
||||
A2 = [0] * m
|
||||
A2[item2] = 1
|
||||
elif data[i][6] == "Classes":
|
||||
A2 = np.where(
|
||||
asset_classes[data[i][7]].values == data[i][8], 1, 0
|
||||
)
|
||||
A3 = (
|
||||
(np.array(A3) + np.array(A2) * data[i][9] * -1) * d
|
||||
).tolist()
|
||||
A.append(A3)
|
||||
B.append([0])
|
||||
l = l + 1
|
||||
elif data[i][1] == "All Classes":
|
||||
if data[i][4] == ">=":
|
||||
d = 1
|
||||
elif data[i][4] == "<=":
|
||||
d = -1
|
||||
if data[i][5] != "":
|
||||
for k in np.unique(asset_classes[data[i][2]].values):
|
||||
A1 = np.where(asset_classes[data[i][2]].values == k, 1, 0) * d
|
||||
A1 = A1.tolist()
|
||||
A.append(A1)
|
||||
B.append([data[i][5] * d])
|
||||
else:
|
||||
for k in np.unique(asset_classes[data[i][2]].values):
|
||||
A1 = np.where(asset_classes[data[i][2]].values == k, 1, 0)
|
||||
if data[i][6] == "Assets":
|
||||
item2 = assetslist.index(data[i][8])
|
||||
A2 = [0] * m
|
||||
A2[item2] = 1
|
||||
elif data[i][6] == "Classes":
|
||||
A2 = np.where(
|
||||
asset_classes[data[i][7]].values == data[i][8], 1, 0
|
||||
)
|
||||
A3 = (
|
||||
(np.array(A1) + np.array(A2) * data[i][9] * -1) * d
|
||||
).tolist()
|
||||
A.append(A3)
|
||||
B.append([0])
|
||||
|
||||
A = np.array(A, ndmin=2)
|
||||
B = np.array(B, ndmin=2)
|
||||
|
||||
return A, B
|
||||
|
||||
|
||||
def factors_constraints(constraints, loadings):
|
||||
r"""
|
||||
Create the factors constraints matrixes C and D of the constraint
|
||||
:math:`Cw \geq D`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraints : DataFrame of shape (n_constraints, n_fields)
|
||||
Constraints matrix, where n_constraints is the number of constraints
|
||||
and n_fields is the number of fields of constraints matrix, the fields
|
||||
are:
|
||||
|
||||
- Disabled: (bool) indicates if the constraint is enable.
|
||||
- Factor: (str) the name of the factor of the constraint.
|
||||
- Sign: (str) can be '>=' or '<='.
|
||||
- Value: (scalar) is the maximum or minimum value of the factor.
|
||||
|
||||
loadings : DataFrame of shape (n_assets, n_features)
|
||||
The loadings matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
C : nd-array
|
||||
The matrix C of :math:`Cw \geq D`.
|
||||
|
||||
D : nd-array
|
||||
The matrix D of :math:`Cw \geq D`.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError when the value cannot be calculated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
loadings = {'const': [0.0004, 0.0002, 0.0000, 0.0006, 0.0001, 0.0003, -0.0003],
|
||||
'MTUM': [0.1916, 1.0061, 0.8695, 1.9996, 0.0000, 0.0000, 0.0000],
|
||||
'QUAL': [0.0000, 2.0129, 1.4301, 0.0000, 0.0000, 0.0000, 0.0000],
|
||||
'SIZE': [0.0000, 0.0000, 0.0000, 0.4717, 0.0000, -0.1857, 0.0000],
|
||||
'USMV': [-0.7838, -1.6439, -1.0176, -1.4407, 0.0055, 0.5781, 0.0000],
|
||||
'VLUE': [1.4772, -0.7590, -0.4090, 0.0000, -0.0054, -0.4844, 0.9435]}
|
||||
|
||||
loadings = pd.DataFrame(loadings)
|
||||
|
||||
constraints = {'Disabled': [False, False, False],
|
||||
'Factor': ['MTUM', 'USMV', 'VLUE'],
|
||||
'Sign': ['<=', '<=', '>='],
|
||||
'Value': [0.9, -1.2, 0.3],
|
||||
'Relative Factor': ['USMV', '', '']}
|
||||
|
||||
constraints = pd.DataFrame(constraints)
|
||||
|
||||
|
||||
The constraint looks like this:
|
||||
|
||||
.. image:: images/Constraints2.png
|
||||
|
||||
It is easier to construct the constraints in excel and then upload to a
|
||||
dataframe.
|
||||
|
||||
To create the matrixes C and D we use the following command:
|
||||
|
||||
::
|
||||
|
||||
C, D = rp.factors_constraints(constraints, loadings)
|
||||
|
||||
|
||||
The matrixes C and D looks like this (all constraints were converted to a linear
|
||||
constraint):
|
||||
|
||||
.. image:: images/CxD.png
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(constraints, pd.DataFrame) and not isinstance(
|
||||
loadings, pd.DataFrame
|
||||
):
|
||||
raise ValueError("constraints and loadings must be DataFrames")
|
||||
|
||||
if constraints.shape[1] != 5:
|
||||
raise ValueError("constraints must have five columns")
|
||||
|
||||
n = len(constraints)
|
||||
data = constraints.fillna("")
|
||||
data = data.values.tolist()
|
||||
|
||||
C = []
|
||||
D = []
|
||||
for i in range(0, n):
|
||||
if data[i][0] == False:
|
||||
if data[i][2] == ">=":
|
||||
d = 1
|
||||
elif data[i][2] == "<=":
|
||||
d = -1
|
||||
C1 = loadings[data[i][1]].values
|
||||
if data[i][4] != "":
|
||||
C2 = loadings[data[i][4]].values
|
||||
C1 = C1 - C2
|
||||
C.append(C1 * d)
|
||||
D.append([data[i][3] * d])
|
||||
|
||||
C = np.array(C, ndmin=2)
|
||||
D = np.array(D, ndmin=2)
|
||||
|
||||
return C, D
|
||||
|
||||
|
||||
def assets_views(views, asset_classes):
|
||||
r"""
|
||||
Create the assets views matrixes P and Q of the views :math:`Pw = Q`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
views : DataFrame of shape (n_views, n_fields)
|
||||
Constraints matrix, where n_views is the number of views
|
||||
and n_fields is the number of fields of views matrix, the fields
|
||||
are:
|
||||
|
||||
- Disabled: (bool) indicates if the constraint is enable.
|
||||
- Type: (str) can be: 'Assets' or 'Classes'.
|
||||
- Set: (str) if Type is 'Classes' specified the name of the set of asset classes.
|
||||
- Position: (str) the name of the asset or asset class of the view.
|
||||
- Sign: (str) can be '>=' or '<='.
|
||||
- Return: (scalar) is the return of the view.
|
||||
- Type Relative: (str) can be: 'Assets' or 'Classes'.
|
||||
- Relative Set: (str) if Type Relative is 'Classes' specified the name of the set of asset classes.
|
||||
- Relative: (str) the name of the asset or asset class of the relative view.
|
||||
|
||||
asset_classes : DataFrame of shape (n_assets, n_cols)
|
||||
Asset's classes matrix, where n_assets is the number of assets and
|
||||
n_cols is the number of columns of the matrix where the first column
|
||||
is the asset list and the next columns are the different asset's
|
||||
classes sets.
|
||||
|
||||
Returns
|
||||
-------
|
||||
P : nd-array
|
||||
The matrix P that shows the relation among assets in each view.
|
||||
|
||||
Q : nd-array
|
||||
The matrix Q that shows the expected return of each view.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError when the value cannot be calculated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
asset_classes = {'Assets': ['FB', 'GOOGL', 'NTFX', 'BAC', 'WFC', 'TLT', 'SHV'],
|
||||
'Class 1': ['Equity', 'Equity', 'Equity', 'Equity', 'Equity',
|
||||
'Fixed Income', 'Fixed Income'],
|
||||
'Class 2': ['Technology', 'Technology', 'Technology',
|
||||
'Financial', 'Financial', 'Treasury', 'Treasury'],}
|
||||
|
||||
asset_classes = pd.DataFrame(asset_classes)
|
||||
asset_classes = asset_classes.sort_values(by=['Assets'])
|
||||
|
||||
views = {'Disabled': [False, False, False, False],
|
||||
'Type': ['Assets', 'Classes', 'Classes', 'Assets'],
|
||||
'Set': ['', 'Class 2','Class 1', ''],
|
||||
'Position': ['WFC', 'Financial', 'Equity', 'FB'],
|
||||
'Sign': ['<=', '>=', '>=', '>='],
|
||||
'Return': [ 0.3, 0.1, 0.05, 0.03 ],
|
||||
'Type Relative': [ 'Assets', 'Classes', 'Assets', ''],
|
||||
'Relative Set': [ '', 'Class 1', '', ''],
|
||||
'Relative': ['FB', 'Fixed Income', 'TLT', '']}
|
||||
|
||||
views = pd.DataFrame(views)
|
||||
|
||||
|
||||
The constraint looks like this:
|
||||
|
||||
.. image:: images/Views.png
|
||||
|
||||
It is easier to construct the constraints in excel and then upload to a
|
||||
dataframe.
|
||||
|
||||
To create the matrixes P and Q we use the following command:
|
||||
|
||||
::
|
||||
|
||||
P, Q = rp.assets_views(views, asset_classes)
|
||||
|
||||
|
||||
The matrixes P and Q looks like this:
|
||||
|
||||
.. image:: images/PxQ.png
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(views, pd.DataFrame) and not isinstance(
|
||||
asset_classes, pd.DataFrame
|
||||
):
|
||||
raise ValueError("constraints and asset_classes must be DataFrames")
|
||||
|
||||
if views.shape[1] != 9:
|
||||
raise ValueError("constraints must have nine columns")
|
||||
|
||||
n = len(views)
|
||||
m = len(asset_classes)
|
||||
data = views.fillna("")
|
||||
data = data.values.tolist()
|
||||
assetslist = asset_classes.iloc[:, 0].values.tolist()
|
||||
|
||||
P = []
|
||||
Q = []
|
||||
for i in range(0, n):
|
||||
valid = False
|
||||
if data[i][0] == False:
|
||||
if data[i][1] == "Assets":
|
||||
item = assetslist.index(data[i][3])
|
||||
if data[i][4] == ">=":
|
||||
d = 1
|
||||
elif data[i][4] == "<=":
|
||||
d = -1
|
||||
if data[i][5] != "":
|
||||
P1 = [0] * m
|
||||
P1[item] = 1
|
||||
if data[i][6] == "Assets" and data[i][8] != "":
|
||||
item2 = assetslist.index(data[i][8])
|
||||
P2 = [0] * m
|
||||
P2[item2] = 1
|
||||
valid = True
|
||||
elif (
|
||||
data[i][6] == "Classes"
|
||||
and data[i][7] != ""
|
||||
and data[i][8] != ""
|
||||
):
|
||||
P2 = np.where(
|
||||
asset_classes[data[i][7]].values == data[i][8], 1, 0
|
||||
)
|
||||
P2 = P2 / np.sum(P2)
|
||||
valid = True
|
||||
elif data[i][6] == "" and data[i][7] == "" and data[i][8] == "":
|
||||
P2 = [0] * m
|
||||
valid = True
|
||||
if valid == True:
|
||||
P1 = ((np.array(P1) - np.array(P2)) * d).tolist()
|
||||
P.append(P1)
|
||||
Q.append([data[i][5] * d])
|
||||
elif data[i][1] == "Classes":
|
||||
if data[i][4] == ">=":
|
||||
d = 1
|
||||
else:
|
||||
d = -1
|
||||
if data[i][5] != "":
|
||||
P1 = np.where(asset_classes[data[i][2]].values == data[i][3], 1, 0)
|
||||
P1 = P1 / np.sum(P1)
|
||||
if data[i][6] == "Assets" and data[i][8] != "":
|
||||
item2 = assetslist.index(data[i][8])
|
||||
P2 = [0] * m
|
||||
P2[item2] = 1
|
||||
valid = True
|
||||
elif (
|
||||
data[i][6] == "Classes"
|
||||
and data[i][7] != ""
|
||||
and data[i][8] != ""
|
||||
):
|
||||
P2 = np.where(
|
||||
asset_classes[data[i][7]].values == data[i][8], 1, 0
|
||||
)
|
||||
P2 = P2 / np.sum(P2)
|
||||
valid = True
|
||||
elif data[i][6] == "" and data[i][7] == "" and data[i][8] == "":
|
||||
P2 = [0] * m
|
||||
valid = True
|
||||
if valid == True:
|
||||
P1 = ((np.array(P1) - np.array(P2)) * d).tolist()
|
||||
P.append(P1)
|
||||
Q.append([data[i][5] * d])
|
||||
|
||||
P = np.array(P, ndmin=2)
|
||||
Q = np.array(Q, ndmin=2)
|
||||
|
||||
for i in range(len(Q)):
|
||||
if Q[i, 0] < 0:
|
||||
P[i, :] = -1 * P[i, :]
|
||||
Q[i, :] = -1 * Q[i, :]
|
||||
|
||||
return P, Q
|
||||
|
||||
|
||||
def factors_views(views, loadings, const=True):
|
||||
r"""
|
||||
Create the factors constraints matrixes C and D of the constraint
|
||||
:math:`Cw \geq D`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraints : DataFrame of shape (n_constraints, n_fields)
|
||||
Constraints matrix, where n_constraints is the number of constraints
|
||||
and n_fields is the number of fields of constraints matrix, the fields
|
||||
are:
|
||||
|
||||
- Disabled: (bool) indicates if the constraint is enable.
|
||||
- Factor: (str) the name of the factor of the constraint.
|
||||
- Sign: (str) can be '>=' or '<='.
|
||||
- Value: (scalar) is the maximum or minimum value of the factor.
|
||||
|
||||
loadings : DataFrame of shape (n_assets, n_features)
|
||||
The loadings matrix.
|
||||
|
||||
Returns
|
||||
-------
|
||||
P : nd-array
|
||||
The matrix P that shows the relation among factors in each factor view.
|
||||
|
||||
Q : nd-array
|
||||
The matrix Q that shows the expected return of each factor view.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError when the value cannot be calculated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
loadings = {'const': [0.0004, 0.0002, 0.0000, 0.0006, 0.0001, 0.0003, -0.0003],
|
||||
'MTUM': [0.1916, 1.0061, 0.8695, 1.9996, 0.0000, 0.0000, 0.0000],
|
||||
'QUAL': [0.0000, 2.0129, 1.4301, 0.0000, 0.0000, 0.0000, 0.0000],
|
||||
'SIZE': [0.0000, 0.0000, 0.0000, 0.4717, 0.0000, -0.1857, 0.0000],
|
||||
'USMV': [-0.7838, -1.6439, -1.0176, -1.4407, 0.0055, 0.5781, 0.0000],
|
||||
'VLUE': [1.4772, -0.7590, -0.4090, 0.0000, -0.0054, -0.4844, 0.9435]}
|
||||
|
||||
loadings = pd.DataFrame(loadings)
|
||||
|
||||
factorsviews = {'Disabled': [False, False, False],
|
||||
'Factor': ['MTUM', 'USMV', 'VLUE'],
|
||||
'Sign': ['<=', '<=', '>='],
|
||||
'Value': [0.9, -1.2, 0.3],
|
||||
'Relative Factor': ['USMV', '', '']}
|
||||
|
||||
factorsviews = pd.DataFrame(factorsviews)
|
||||
|
||||
|
||||
The constraint looks like this:
|
||||
|
||||
.. image:: images/factorsviews.png
|
||||
|
||||
It is easier to construct the constraints in excel and then upload to a
|
||||
dataframe.
|
||||
|
||||
To create the matrixes P and Q we use the following command:
|
||||
|
||||
::
|
||||
|
||||
P, Q = rp.factors_views(factorsviews, loadings, const=True)
|
||||
|
||||
|
||||
The matrixes P and Q looks like this:
|
||||
|
||||
.. image:: images/P_fxQ_f.png
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(views, pd.DataFrame) and not isinstance(loadings, pd.DataFrame):
|
||||
raise ValueError("constraints and loadings must be DataFrames")
|
||||
|
||||
if views.shape[1] != 5:
|
||||
raise ValueError("constraints must have five columns")
|
||||
|
||||
n = len(views)
|
||||
data = views.fillna("")
|
||||
data = data.values.tolist()
|
||||
factorslist = loadings.columns.tolist()
|
||||
if const == True:
|
||||
factorslist = factorslist[1:]
|
||||
m = len(factorslist)
|
||||
|
||||
P = []
|
||||
Q = []
|
||||
for i in range(0, n):
|
||||
if data[i][0] == False:
|
||||
item = factorslist.index(data[i][1])
|
||||
if data[i][2] == ">=":
|
||||
d = 1
|
||||
elif data[i][2] == "<=":
|
||||
d = -1
|
||||
P1 = [0] * m
|
||||
P1[item] = d
|
||||
if data[i][4] != "":
|
||||
item = factorslist.index(data[i][4])
|
||||
P1[item] = -d
|
||||
P.append(P1)
|
||||
Q.append([data[i][3] * d])
|
||||
|
||||
P = np.array(P, ndmin=2)
|
||||
Q = np.array(Q, ndmin=2)
|
||||
|
||||
return P, Q
|
||||
|
||||
|
||||
def assets_clusters(
|
||||
returns,
|
||||
codependence="pearson",
|
||||
linkage="ward",
|
||||
k=None,
|
||||
max_k=10,
|
||||
bins_info="KN",
|
||||
alpha_tail=0.05,
|
||||
leaf_order=True,
|
||||
):
|
||||
r"""
|
||||
Create asset classes based on hierarchical clustering.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
returns : DataFrame
|
||||
Assets returns.
|
||||
codependence : str, can be {'pearson', 'spearman', 'abs_pearson', 'abs_spearman', 'distance', 'mutual_info' or 'tail'}
|
||||
The codependence or similarity matrix used to build the distance
|
||||
metric and clusters. The default is 'pearson'. Posible values are:
|
||||
|
||||
- 'pearson': pearson correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{0.5(1-\rho^{pearson}_{i,j})}`.
|
||||
- 'spearman': spearman correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{0.5(1-\rho^{spearman}_{i,j})}`.
|
||||
- 'abs_pearson': absolute value pearson correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{(1-|\rho^{pearson}_{i,j}|)}`.
|
||||
- 'abs_spearman': absolute value spearman correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{(1-|\rho^{spearman}_{i,j}|)}`.
|
||||
- 'distance': distance correlation matrix. Distance formula :math:`D_{i,j} = \sqrt{(1-\rho^{distance}_{i,j})}`.
|
||||
- 'mutual_info': mutual information matrix. Distance used is variation information matrix.
|
||||
- 'tail': lower tail dependence index matrix. Dissimilarity formula :math:`D_{i,j} = -\log{\lambda_{i,j}}`.
|
||||
|
||||
linkage : string, optional
|
||||
Linkage method of hierarchical clustering, see `linkage <https://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html?highlight=linkage#scipy.cluster.hierarchy.linkage>`_ for more details.
|
||||
The default is 'ward'. Posible values are:
|
||||
|
||||
- 'single'.
|
||||
- 'complete'.
|
||||
- 'average'.
|
||||
- 'weighted'.
|
||||
- 'centroid'.
|
||||
- 'median'.
|
||||
- 'ward'.
|
||||
- 'DBHT'. Direct Bubble Hierarchical Tree.
|
||||
|
||||
k : int, optional
|
||||
Number of clusters. This value is took instead of the optimal number
|
||||
of clusters calculated with the two difference gap statistic.
|
||||
The default is None.
|
||||
max_k : int, optional
|
||||
Max number of clusters used by the two difference gap statistic
|
||||
to find the optimal number of clusters. The default is 10.
|
||||
bins_info: int or str
|
||||
Number of bins used to calculate variation of information. The default
|
||||
value is 'KN'. Posible values are:
|
||||
|
||||
- 'KN': Knuth's choice method. See more in `knuth_bin_width <https://docs.astropy.org/en/stable/api/astropy.stats.knuth_bin_width.html>`_.
|
||||
- 'FD': Freedman–Diaconis' choice method. See more in `freedman_bin_width <https://docs.astropy.org/en/stable/api/astropy.stats.freedman_bin_width.html>`_.
|
||||
- 'SC': Scotts' choice method. See more in `scott_bin_width <https://docs.astropy.org/en/stable/api/astropy.stats.scott_bin_width.html>`_.
|
||||
- 'HGR': Hacine-Gharbi and Ravier' choice method.
|
||||
- int: integer value choice by user.
|
||||
|
||||
alpha_tail : float, optional
|
||||
Significance level for lower tail dependence index. The default is 0.05.
|
||||
leaf_order : bool, optional
|
||||
Indicates if the cluster are ordered so that the distance between
|
||||
successive leaves is minimal. The default is True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
clusters : DataFrame
|
||||
A dataframe with asset classes based on hierarchical clustering.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError when the value cannot be calculated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
::
|
||||
|
||||
clusters = rp.assets_clusters(returns, codependence='pearson',
|
||||
linkage='ward', k=None, max_k=10,
|
||||
alpha_tail=0.05, leaf_order=True)
|
||||
|
||||
|
||||
The clusters dataframe looks like this:
|
||||
|
||||
.. image:: images/clusters_df.png
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(returns, pd.DataFrame):
|
||||
raise ValueError("returns must be a DataFrame")
|
||||
|
||||
# Calculating codependence matrix and distance metric
|
||||
if codependence in {"pearson", "spearman"}:
|
||||
codep = returns.corr(method=codependence)
|
||||
dist = np.sqrt(np.clip((1 - codep) / 2, a_min=0.0, a_max=1.0))
|
||||
elif codependence in {"abs_pearson", "abs_spearman"}:
|
||||
codep = np.abs(returns.corr(method=codependence[4:]))
|
||||
dist = np.sqrt(np.clip((1 - codep), a_min=0.0, a_max=1.0))
|
||||
elif codependence in {"distance"}:
|
||||
codep = af.dcorr_matrix(returns).astype(float)
|
||||
dist = np.sqrt(np.clip((1 - codep), a_min=0.0, a_max=1.0))
|
||||
elif codependence in {"mutual_info"}:
|
||||
codep = af.mutual_info_matrix(returns, bins_info).astype(float)
|
||||
dist = af.var_info_matrix(returns, bins_info).astype(float)
|
||||
elif codependence in {"tail"}:
|
||||
codep = af.ltdi_matrix(returns, alpha_tail).astype(float)
|
||||
dist = -np.log(codep)
|
||||
|
||||
# Hierarchcial clustering
|
||||
dist = dist.to_numpy()
|
||||
dist = pd.DataFrame(dist, columns=codep.columns, index=codep.index)
|
||||
if linkage == "DBHT":
|
||||
# different choices for D, S give different outputs!
|
||||
D = dist.to_numpy() # dissimilatity matrix
|
||||
if codependence in {"pearson", "spearman"}:
|
||||
S = (1 - dist**2).to_numpy()
|
||||
else:
|
||||
S = codep.copy().to_numpy() # similarity matrix
|
||||
(_, _, _, _, _, clustering) = db.DBHTs(
|
||||
D, S, leaf_order=leaf_order
|
||||
) # DBHT clustering
|
||||
else:
|
||||
p_dist = squareform(dist, checks=False)
|
||||
clustering = hr.linkage(p_dist, method=linkage, optimal_ordering=leaf_order)
|
||||
|
||||
# Optimal number of clusters
|
||||
if k is None:
|
||||
k = af.two_diff_gap_stat(codep, dist, clustering, max_k)
|
||||
|
||||
# Building clusters
|
||||
clusters_inds = hr.fcluster(clustering, k, criterion="maxclust")
|
||||
labels = np.array(returns.columns.tolist())
|
||||
|
||||
clusters = {"Assets": [], "Clusters": []}
|
||||
|
||||
for i, v in enumerate(clusters_inds):
|
||||
clusters["Assets"].append(labels[i])
|
||||
clusters["Clusters"].append("Cluster " + str(v))
|
||||
|
||||
clusters = pd.DataFrame(clusters)
|
||||
clusters = clusters.sort_values(by=["Assets"])
|
||||
|
||||
return clusters
|
||||
|
||||
|
||||
def hrp_constraints(constraints, asset_classes):
|
||||
r"""
|
||||
Create the upper and lower bounds constraints for hierarchical risk parity
|
||||
model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
constraints : DataFrame of shape (n_constraints, n_fields)
|
||||
Constraints matrix, where n_constraints is the number of constraints
|
||||
and n_fields is the number of fields of constraints matrix, the fields
|
||||
are:
|
||||
|
||||
- Disabled: (bool) indicates if the constraint is enable.
|
||||
- Type: (str) can be: 'Assets', All Assets' and 'Each asset in a class'.
|
||||
- Position: (str) the name of the asset or asset class of the constraint.
|
||||
- Sign: (str) can be '>=' or '<='.
|
||||
- Weight: (scalar) is the maximum or minimum weight of the absolute constraint.
|
||||
|
||||
asset_classes : DataFrame of shape (n_assets, n_cols)
|
||||
Asset's classes matrix, where n_assets is the number of assets and
|
||||
n_cols is the number of columns of the matrix where the first column
|
||||
is the asset list and the next columns are the different asset's
|
||||
classes sets.
|
||||
|
||||
Returns
|
||||
-------
|
||||
w_max : pd.Series
|
||||
The upper bound of hierarchical risk parity weights constraints.
|
||||
|
||||
w_min : pd.Series
|
||||
The lower bound of hierarchical risk parity weights constraints.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError when the value cannot be calculated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
::
|
||||
|
||||
asset_classes = {'Assets': ['FB', 'GOOGL', 'NTFX', 'BAC', 'WFC', 'TLT', 'SHV'],
|
||||
'Class 1': ['Equity', 'Equity', 'Equity', 'Equity', 'Equity',
|
||||
'Fixed Income', 'Fixed Income'],
|
||||
'Class 2': ['Technology', 'Technology', 'Technology',
|
||||
'Financial', 'Financial', 'Treasury', 'Treasury'],}
|
||||
|
||||
asset_classes = pd.DataFrame(asset_classes)
|
||||
asset_classes = asset_classes.sort_values(by=['Assets'])
|
||||
|
||||
constraints = {'Disabled': [False, False, False, False, False, False],
|
||||
'Type': ['Assets', 'Assets', 'All Assets', 'All Assets',
|
||||
'Each asset in a class', 'Each asset in a class'],
|
||||
'Set': ['', '', '', '','Class 1', 'Class 2'],
|
||||
'Position': ['BAC', 'FB', '', '', 'Equity', 'Treasury'],
|
||||
'Sign': ['>=', '<=', '<=', '>=', '<=', '<='],
|
||||
'Weight': [0.02, 0.085, 0.09, 0.01, 0.07, 0.06]}
|
||||
|
||||
constraints = pd.DataFrame(constraints)
|
||||
|
||||
The constraint looks like this:
|
||||
|
||||
.. image:: images/HRPConstraints.png
|
||||
|
||||
It is easier to construct the constraints in excel and then upload to a
|
||||
dataframe.
|
||||
|
||||
To create the pd.Series w_max and w_min we use the following command:
|
||||
|
||||
::
|
||||
|
||||
w_max, w_min = rp.hrp_constraints(constraints, asset_classes)
|
||||
|
||||
|
||||
The pd.Series w_max and w_min looks like this (all constraints were
|
||||
merged to a single upper bound for each asset):
|
||||
|
||||
.. image:: images/HRP_Bounds.png
|
||||
|
||||
"""
|
||||
|
||||
if not isinstance(constraints, pd.DataFrame) and not isinstance(
|
||||
asset_classes, pd.DataFrame
|
||||
):
|
||||
raise ValueError("constraints and asset_classes must be DataFrames")
|
||||
|
||||
if constraints.shape[1] != 6:
|
||||
raise ValueError("constraints must have ten columns")
|
||||
|
||||
n = len(constraints)
|
||||
data = constraints.fillna("").copy()
|
||||
assetslist = asset_classes.iloc[:, 0].values.tolist()
|
||||
|
||||
w_max = pd.Series(1, index=assetslist)
|
||||
w_min = pd.Series(0, index=assetslist)
|
||||
|
||||
for i in range(0, n):
|
||||
if data.loc[i, "Disabled"] == False:
|
||||
if data.loc[i, "Type"] == "Assets":
|
||||
assets = data.loc[i, "Position"]
|
||||
if data.loc[i, "Sign"] == ">=":
|
||||
if w_min.loc[assets] <= data.loc[i, "Weight"]:
|
||||
w_min.loc[assets] = data.loc[i, "Weight"]
|
||||
elif data.loc[i, "Sign"] == "<=":
|
||||
if w_max.loc[assets] >= data.loc[i, "Weight"]:
|
||||
w_max.loc[assets] = data.loc[i, "Weight"]
|
||||
elif data.loc[i, "Type"] == "All Assets":
|
||||
if data.loc[i, "Sign"] == ">=":
|
||||
if w_min[w_min <= data.loc[i, "Weight"]].shape[0] != 0:
|
||||
w_min[w_min <= data.loc[i, "Weight"]] = data.loc[i, "Weight"]
|
||||
elif data.loc[i, "Sign"] == "<=":
|
||||
if w_max[w_max >= data.loc[i, "Weight"]].shape[0] != 0:
|
||||
w_max[w_max >= data.loc[i, "Weight"]] = data.loc[i, "Weight"]
|
||||
elif data.loc[i, "Type"] == "Each asset in a class":
|
||||
label_0 = asset_classes.columns.tolist()[0]
|
||||
label_1 = data.loc[i, "Set"]
|
||||
label_2 = data.loc[i, "Position"]
|
||||
assets = asset_classes[[label_0, label_1]][
|
||||
asset_classes[label_1] == label_2
|
||||
]
|
||||
assets = assets["Assets"].tolist()
|
||||
if data.loc[i, "Sign"] == ">=":
|
||||
if (
|
||||
w_min.loc[assets][
|
||||
w_min.loc[assets] <= data.loc[i, "Weight"]
|
||||
].shape[0]
|
||||
!= 0
|
||||
):
|
||||
w_min.loc[assets] = np.where(
|
||||
w_min.loc[assets] <= data.loc[i, "Weight"],
|
||||
data.loc[i, "Weight"],
|
||||
w_min.loc[assets],
|
||||
)
|
||||
elif data.loc[i, "Sign"] == "<=":
|
||||
if (
|
||||
w_max.loc[assets][
|
||||
w_max.loc[assets] >= data.loc[i, "Weight"]
|
||||
].shape[0]
|
||||
!= 0
|
||||
):
|
||||
w_max.loc[assets] = np.where(
|
||||
w_max.loc[assets] >= data.loc[i, "Weight"],
|
||||
data.loc[i, "Weight"],
|
||||
w_max.loc[assets],
|
||||
)
|
||||
|
||||
return w_max, w_min
|
||||
1068
openbb_terminal/portfolio/portfolio_optimization/riskfolio/DBHT.py
Normal file
1068
openbb_terminal/portfolio/portfolio_optimization/riskfolio/DBHT.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,799 @@
|
||||
"""""" #
|
||||
"""
|
||||
Copyright (c) 2020-2022, Dany Cajas
|
||||
All rights reserved.
|
||||
This work is licensed under BSD 3-Clause "New" or "Revised" License.
|
||||
License available at https://github.com/dcajasn/Riskfolio-Lib/blob/master/LICENSE.txt
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import scipy.cluster.hierarchy as hr
|
||||
from scipy.spatial.distance import squareform
|
||||
import openbb_terminal.portfolio.portfolio_optimization.riskfolio.Portfolio as pf
|
||||
import openbb_terminal.portfolio.portfolio_optimization.riskfolio.RiskFunctions as rk
|
||||
import openbb_terminal.portfolio.portfolio_optimization.riskfolio.AuxFunctions as af
|
||||
import openbb_terminal.portfolio.portfolio_optimization.riskfolio.ParamsEstimation as pe
|
||||
import openbb_terminal.portfolio.portfolio_optimization.riskfolio.DBHT as db
|
||||
|
||||
|
||||
class HCPortfolio(object):
|
||||
r"""
|
||||
Class that creates a portfolio object with all properties needed to
|
||||
calculate optimal portfolios.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
returns : DataFrame, optional
|
||||
A dataframe that containts the returns of the assets.
|
||||
The default is None.
|
||||
alpha : float, optional
|
||||
Significance level of VaR, CVaR, EVaR, DaR, CDaR, EDaR and Tail Gini of losses.
|
||||
The default is 0.05.
|
||||
a_sim : float, optional
|
||||
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
||||
beta : float, optional
|
||||
Significance level of CVaR and Tail Gini of gains. If None it duplicates alpha value.
|
||||
The default is None.
|
||||
b_sim : float, optional
|
||||
Number of CVaRs used to approximate Tail Gini of gains. If None it duplicates a_sim value.
|
||||
The default is None.
|
||||
w_max : Series, optional
|
||||
Upper bound constraint for hierarchical risk parity weights :cite:`c-Pfitzinger`.
|
||||
w_min : Series, optional
|
||||
Lower bound constraint for hierarchical risk parity weights :cite:`c-Pfitzinger`.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
returns=None,
|
||||
alpha=0.05,
|
||||
a_sim=100,
|
||||
beta=None,
|
||||
b_sim=None,
|
||||
w_max=None,
|
||||
w_min=None,
|
||||
alpha_tail=0.05,
|
||||
bins_info=None,
|
||||
):
|
||||
self._returns = returns
|
||||
self.alpha = alpha
|
||||
self.a_sim = a_sim
|
||||
self.beta = beta
|
||||
self.b_sim = b_sim
|
||||
self.alpha_tail = alpha_tail
|
||||
self.bins_info = bins_info
|
||||
self.asset_order = None
|
||||
self.clusters = None
|
||||
self.cov = None
|
||||
self.mu = None
|
||||
self.codep = None
|
||||
self.codep_sorted = None
|
||||
self.w_max = w_max
|
||||
self.w_min = w_min
|
||||
|
||||
@property
|
||||
def returns(self):
|
||||
if self._returns is not None and isinstance(self._returns, pd.DataFrame):
|
||||
return self._returns
|
||||
else:
|
||||
raise NameError("returns must be a DataFrame")
|
||||
|
||||
@returns.setter
|
||||
def returns(self, value):
|
||||
if value is not None and isinstance(value, pd.DataFrame):
|
||||
self._returns = value
|
||||
else:
|
||||
raise NameError("returns must be a DataFrame")
|
||||
|
||||
@property
|
||||
def assetslist(self):
|
||||
if self._returns is not None and isinstance(self._returns, pd.DataFrame):
|
||||
return self._returns.columns.tolist()
|
||||
|
||||
# get naive-risk weights
|
||||
def _naive_risk(self, returns, cov, rm="MV", rf=0):
|
||||
assets = returns.columns.tolist()
|
||||
n = len(assets)
|
||||
|
||||
if rm == "equal":
|
||||
weight = np.ones((n, 1)) * 1 / n
|
||||
else:
|
||||
inv_risk = np.zeros((n, 1))
|
||||
for i in assets:
|
||||
k = assets.index(i)
|
||||
w = np.zeros((n, 1))
|
||||
w[k, 0] = 1
|
||||
w = pd.DataFrame(w, columns=["weights"], index=assets)
|
||||
if rm == "vol":
|
||||
risk = rk.Sharpe_Risk(
|
||||
w,
|
||||
cov=cov,
|
||||
returns=returns,
|
||||
rm="MV",
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
else:
|
||||
risk = rk.Sharpe_Risk(
|
||||
w,
|
||||
cov=cov,
|
||||
returns=returns,
|
||||
rm=rm,
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
inv_risk[k, 0] = risk
|
||||
|
||||
if rm == "MV":
|
||||
inv_risk = 1 / np.power(inv_risk, 2)
|
||||
else:
|
||||
inv_risk = 1 / inv_risk
|
||||
weight = inv_risk * (1 / np.sum(inv_risk))
|
||||
|
||||
weight = weight.reshape(-1, 1)
|
||||
|
||||
return weight
|
||||
|
||||
# get optimal weights
|
||||
def _opt_w(self, returns, mu, cov, obj="MinRisk", rm="MV", rf=0, l=2):
|
||||
if returns.shape[1] == 1:
|
||||
weight = np.array([1]).reshape(-1, 1)
|
||||
else:
|
||||
if obj in {"MinRisk", "Utility", "Sharpe"}:
|
||||
port = pf.Portfolio(returns=returns)
|
||||
port.assets_stats(method_mu="hist", method_cov="hist", d=0.94)
|
||||
port.cov = cov
|
||||
if mu is not None:
|
||||
port.mu = mu
|
||||
weight = port.optimization(
|
||||
model="Classic", rm=rm, obj=obj, rf=rf, l=l, hist=True
|
||||
).to_numpy()
|
||||
elif obj in {"ERC"}:
|
||||
port = pf.Portfolio(returns=returns)
|
||||
port.assets_stats(method_mu="hist", method_cov="hist", d=0.94)
|
||||
port.cov = cov
|
||||
weight = port.rp_optimization(
|
||||
model="Classic", rm=rm, rf=rf, b=None, hist=True
|
||||
).to_numpy()
|
||||
|
||||
weight = weight.reshape(-1, 1)
|
||||
|
||||
return weight
|
||||
|
||||
# Create hierarchical clustering
|
||||
def _hierarchical_clustering(
|
||||
self,
|
||||
model="HRP",
|
||||
linkage="ward",
|
||||
codependence="pearson",
|
||||
max_k=10,
|
||||
leaf_order=True,
|
||||
):
|
||||
|
||||
# Calculating distance
|
||||
if codependence in {"pearson", "spearman", "custom_cov"}:
|
||||
dist = np.sqrt(np.clip((1 - self.codep) / 2, a_min=0.0, a_max=1.0))
|
||||
elif codependence in {"abs_pearson", "abs_spearman", "distance"}:
|
||||
dist = np.sqrt(np.clip((1 - self.codep), a_min=0.0, a_max=1.0))
|
||||
elif codependence in {"mutual_info"}:
|
||||
dist = af.var_info_matrix(self.returns, self.bins_info).astype(float)
|
||||
elif codependence in {"tail"}:
|
||||
dist = -np.log(self.codep).astype(float)
|
||||
|
||||
# Hierarchcial clustering
|
||||
dist = dist.to_numpy()
|
||||
dist = pd.DataFrame(dist, columns=self.codep.columns, index=self.codep.index)
|
||||
if linkage == "DBHT":
|
||||
# different choices for D, S give different outputs!
|
||||
D = dist.to_numpy() # dissimilatity matrix
|
||||
if codependence in {"pearson", "spearman", "custom_cov"}:
|
||||
codep = 1 - dist**2
|
||||
S = codep.to_numpy() # similarity matrix
|
||||
else:
|
||||
S = self.codep.to_numpy() # similarity matrix
|
||||
(_, _, _, _, _, clustering) = db.DBHTs(
|
||||
D, S, leaf_order=leaf_order
|
||||
) # DBHT clustering
|
||||
else:
|
||||
p_dist = squareform(dist, checks=False)
|
||||
clustering = hr.linkage(p_dist, method=linkage, optimal_ordering=leaf_order)
|
||||
|
||||
if model in {"HERC", "HERC2", "NCO"}:
|
||||
# optimal number of clusters
|
||||
k = af.two_diff_gap_stat(self.codep, dist, clustering, max_k)
|
||||
else:
|
||||
k = None
|
||||
|
||||
return clustering, k
|
||||
|
||||
# sort clustered items by distance
|
||||
def _seriation(self, clusters):
|
||||
return hr.leaves_list(clusters)
|
||||
|
||||
# compute HRP weight allocation through recursive bisection
|
||||
def _recursive_bisection(self, sort_order, rm="MV", rf=0):
|
||||
|
||||
if isinstance(self.w_max, pd.Series) and isinstance(self.w_min, pd.Series):
|
||||
if (self.w_max.all() >= self.w_min.all()).item():
|
||||
flag = True
|
||||
else:
|
||||
raise NameError("All upper bounds must be higher than lower bounds")
|
||||
else:
|
||||
flag = False
|
||||
|
||||
weight = pd.Series(1, index=sort_order) # set initial weights to 1
|
||||
items = [sort_order]
|
||||
|
||||
while len(items) > 0: # loop while weights is under 100%
|
||||
items = [
|
||||
i[j:k]
|
||||
for i in items
|
||||
for j, k in (
|
||||
(0, len(i) // 2),
|
||||
(len(i) // 2, len(i)),
|
||||
) # get cluster indi
|
||||
if len(i) > 1
|
||||
]
|
||||
|
||||
# allocate weight to left and right cluster
|
||||
for i in range(0, len(items), 2):
|
||||
left_cluster = items[i]
|
||||
right_cluster = items[i + 1]
|
||||
|
||||
# Left cluster
|
||||
left_cov = self.cov.iloc[left_cluster, left_cluster]
|
||||
left_returns = self.returns.iloc[:, left_cluster]
|
||||
left_weight = self._naive_risk(left_returns, left_cov, rm=rm, rf=rf)
|
||||
|
||||
if rm == "vol":
|
||||
left_risk = rk.Sharpe_Risk(
|
||||
left_weight,
|
||||
cov=left_cov,
|
||||
returns=left_returns,
|
||||
rm="MV",
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
else:
|
||||
left_risk = rk.Sharpe_Risk(
|
||||
left_weight,
|
||||
cov=left_cov,
|
||||
returns=left_returns,
|
||||
rm=rm,
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
if rm == "MV":
|
||||
left_risk = np.power(left_risk, 2)
|
||||
|
||||
# Right cluster
|
||||
right_cov = self.cov.iloc[right_cluster, right_cluster]
|
||||
right_returns = self.returns.iloc[:, right_cluster]
|
||||
right_weight = self._naive_risk(right_returns, right_cov, rm=rm, rf=rf)
|
||||
|
||||
if rm == "vol":
|
||||
right_risk = rk.Sharpe_Risk(
|
||||
right_weight,
|
||||
cov=right_cov,
|
||||
returns=right_returns,
|
||||
rm="MV",
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
else:
|
||||
right_risk = rk.Sharpe_Risk(
|
||||
right_weight,
|
||||
cov=right_cov,
|
||||
returns=right_returns,
|
||||
rm=rm,
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
if rm == "MV":
|
||||
right_risk = np.power(right_risk, 2)
|
||||
|
||||
# Allocate weight to clusters
|
||||
alpha_1 = 1 - left_risk / (left_risk + right_risk)
|
||||
|
||||
# Weights constraints
|
||||
if flag:
|
||||
a1 = np.sum(self.w_max[left_cluster]) / weight[left_cluster[0]]
|
||||
a2 = np.max(
|
||||
[
|
||||
np.sum(self.w_min[left_cluster]) / weight[left_cluster[0]],
|
||||
alpha_1,
|
||||
]
|
||||
)
|
||||
alpha_1 = np.min([a1, a2])
|
||||
a1 = np.sum(self.w_max[right_cluster]) / weight[right_cluster[0]]
|
||||
a2 = np.max(
|
||||
[
|
||||
np.sum(self.w_min[right_cluster])
|
||||
/ weight[right_cluster[0]],
|
||||
1 - alpha_1,
|
||||
]
|
||||
)
|
||||
alpha_1 = 1 - np.min([a1, a2])
|
||||
|
||||
weight[left_cluster] *= alpha_1 # weight 1
|
||||
weight[right_cluster] *= 1 - alpha_1 # weight 2
|
||||
|
||||
weight.index = self.asset_order
|
||||
|
||||
return weight
|
||||
|
||||
# compute HRP weight allocation through cluster-based bisection
|
||||
def _hierarchical_recursive_bisection(
|
||||
self, Z, rm="MV", rf=0, linkage="ward", model="HERC"
|
||||
):
|
||||
|
||||
# Transform linkage to tree and reverse order
|
||||
root, nodes = hr.to_tree(Z, rd=True)
|
||||
nodes = np.array(nodes)
|
||||
nodes_1 = np.array([i.dist for i in nodes])
|
||||
idx = np.argsort(nodes_1)
|
||||
nodes = nodes[idx][::-1].tolist()
|
||||
|
||||
weight = pd.Series(1, index=self.cov.index) # Set initial weights to 1
|
||||
|
||||
clustering_inds = hr.fcluster(Z, self.k, criterion="maxclust")
|
||||
clusters = {
|
||||
i: [] for i in range(min(clustering_inds), max(clustering_inds) + 1)
|
||||
}
|
||||
for i, v in enumerate(clustering_inds):
|
||||
clusters[v].append(i)
|
||||
|
||||
# Loop through k clusters
|
||||
for i in nodes[: self.k - 1]:
|
||||
if i.is_leaf() == False: # skip leaf-nodes
|
||||
left = i.get_left().pre_order() # lambda i: i.id) # get left cluster
|
||||
right = i.get_right().pre_order() # lambda i: i.id) # get right cluster
|
||||
left_set = set(left)
|
||||
right_set = set(right)
|
||||
left_risk = 0
|
||||
right_risk = 0
|
||||
|
||||
# Allocate weight to clusters
|
||||
if rm == "equal":
|
||||
alpha_1 = 0.5
|
||||
|
||||
else:
|
||||
for j in clusters.keys():
|
||||
if set(clusters[j]).issubset(left_set):
|
||||
# Left cluster
|
||||
left_cov = self.cov.iloc[clusters[j], clusters[j]]
|
||||
left_returns = self.returns.iloc[:, clusters[j]]
|
||||
left_weight = self._naive_risk(
|
||||
left_returns, left_cov, rm=rm, rf=rf
|
||||
)
|
||||
|
||||
if rm == "vol":
|
||||
left_risk_ = rk.Sharpe_Risk(
|
||||
left_weight,
|
||||
cov=left_cov,
|
||||
returns=left_returns,
|
||||
rm="MV",
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
else:
|
||||
left_risk_ = rk.Sharpe_Risk(
|
||||
left_weight,
|
||||
cov=left_cov,
|
||||
returns=left_returns,
|
||||
rm=rm,
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
if rm == "MV":
|
||||
left_risk_ = np.power(left_risk_, 2)
|
||||
|
||||
left_risk += left_risk_
|
||||
|
||||
elif set(clusters[j]).issubset(right_set):
|
||||
# Right cluster
|
||||
right_cov = self.cov.iloc[clusters[j], clusters[j]]
|
||||
right_returns = self.returns.iloc[:, clusters[j]]
|
||||
right_weight = self._naive_risk(
|
||||
right_returns, right_cov, rm=rm, rf=rf
|
||||
)
|
||||
|
||||
if rm == "vol":
|
||||
right_risk_ = rk.Sharpe_Risk(
|
||||
right_weight,
|
||||
cov=right_cov,
|
||||
returns=right_returns,
|
||||
rm="MV",
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
else:
|
||||
right_risk_ = rk.Sharpe_Risk(
|
||||
right_weight,
|
||||
cov=right_cov,
|
||||
returns=right_returns,
|
||||
rm=rm,
|
||||
rf=rf,
|
||||
alpha=self.alpha,
|
||||
a_sim=self.a_sim,
|
||||
beta=self.beta,
|
||||
b_sim=self.b_sim,
|
||||
)
|
||||
if rm == "MV":
|
||||
right_risk_ = np.power(right_risk_, 2)
|
||||
|
||||
right_risk += right_risk_
|
||||
|
||||
alpha_1 = 1 - left_risk / (left_risk + right_risk)
|
||||
|
||||
weight[left] *= alpha_1 # weight 1
|
||||
weight[right] *= 1 - alpha_1 # weight 2
|
||||
|
||||
# Get constituents of k clusters
|
||||
clustered_assets = pd.Series(
|
||||
hr.cut_tree(Z, n_clusters=self.k).flatten(), index=self.cov.index
|
||||
)
|
||||
# Multiply within-cluster weight with inter-cluster weight
|
||||
for i in range(self.k):
|
||||
cluster = clustered_assets.loc[clustered_assets == i]
|
||||
cluster_cov = self.cov.loc[cluster.index, cluster.index]
|
||||
cluster_returns = self.returns.loc[:, cluster.index]
|
||||
if model == "HERC":
|
||||
cluster_weights = pd.Series(
|
||||
self._naive_risk(
|
||||
cluster_returns, cluster_cov, rm=rm, rf=rf
|
||||
).flatten(),
|
||||
index=cluster_cov.index,
|
||||
)
|
||||
elif model == "HERC2":
|
||||
cluster_weights = pd.Series(
|
||||
self._naive_risk(
|
||||
cluster_returns, cluster_cov, rm="equal", rf=rf
|
||||
).flatten(),
|
||||
index=cluster_cov.index,
|
||||
)
|
||||
weight.loc[cluster_weights.index] *= cluster_weights
|
||||
|
||||
return weight
|
||||
|
||||
# compute intra-cluster weights
|
||||
def _intra_weights(self, Z, obj="MinRisk", rm="MV", rf=0, l=2):
|
||||
# Get constituents of k clusters
|
||||
clustered_assets = pd.Series(
|
||||
hr.cut_tree(Z, n_clusters=self.k).flatten(), index=self.cov.index
|
||||
)
|
||||
|
||||
# get covariance matrices for each cluster
|
||||
intra_weights = pd.DataFrame(index=clustered_assets.index)
|
||||
for i in range(self.k):
|
||||
cluster = clustered_assets.loc[clustered_assets == i]
|
||||
if self.mu is not None:
|
||||
cluster_mu = self.mu.loc[:, cluster.index]
|
||||
else:
|
||||
cluster_mu = None
|
||||
cluster_cov = self.cov.loc[cluster.index, cluster.index]
|
||||
cluster_returns = self.returns.loc[:, cluster.index]
|
||||
weights = pd.Series(
|
||||
self._opt_w(
|
||||
cluster_returns, cluster_mu, cluster_cov, obj=obj, rm=rm, rf=rf, l=l
|
||||
).flatten(),
|
||||
index=cluster_cov.index,
|
||||
)
|
||||
intra_weights[i] = weights
|
||||
|
||||
intra_weights = intra_weights.fillna(0)
|
||||
return intra_weights
|
||||
|
||||
def _inter_weights(self, intra_weights, obj="MinRisk", rm="MV", rf=0, l=2):
|
||||
# inter-cluster mean vector
|
||||
if self.mu is not None:
|
||||
tot_mu = self.mu @ intra_weights
|
||||
else:
|
||||
tot_mu = None
|
||||
# inter-cluster covariance matrix
|
||||
tot_cov = intra_weights.T.dot(np.dot(self.cov, intra_weights))
|
||||
# inter-cluster returns matrix
|
||||
tot_ret = self.returns @ intra_weights
|
||||
|
||||
# inter-cluster weights
|
||||
inter_weights = pd.Series(
|
||||
self._opt_w(tot_ret, tot_mu, tot_cov, obj=obj, rm=rm, rf=rf, l=l).flatten(),
|
||||
index=intra_weights.columns,
|
||||
)
|
||||
# determine the weight on each cluster by multiplying the intra-cluster weight with the inter-cluster weight
|
||||
weights = intra_weights.mul(inter_weights, axis=1).sum(axis=1).sort_index()
|
||||
return weights
|
||||
|
||||
# Allocate weights
|
||||
def optimization(
|
||||
self,
|
||||
model="HRP",
|
||||
codependence="pearson",
|
||||
covariance="hist",
|
||||
obj="MinRisk",
|
||||
rm="MV",
|
||||
rf=0,
|
||||
l=2,
|
||||
custom_cov=None,
|
||||
custom_mu=None,
|
||||
linkage="single",
|
||||
k=None,
|
||||
max_k=10,
|
||||
bins_info="KN",
|
||||
alpha_tail=0.05,
|
||||
leaf_order=True,
|
||||
d=0.94,
|
||||
**kwargs,
|
||||
):
|
||||
r"""
|
||||
This method calculates the optimal portfolio according to the
|
||||
optimization model selected by the user.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model : str, can be {'HRP', 'HERC' or 'HERC2'}
|
||||
The hierarchical cluster portfolio model used for optimize the
|
||||
portfolio. The default is 'HRP'. Posible values are:
|
||||
|
||||
- 'HRP': Hierarchical Risk Parity.
|
||||
- 'HERC': Hierarchical Equal Risk Contribution.
|
||||
- 'HERC2': HERC but splitting weights equally within clusters.
|
||||
- 'NCO': Nested Clustered Optimization.
|
||||
|
||||
codependence : str, optional
|
||||
The codependence or similarity matrix used to build the distance
|
||||
metric and clusters. The default is 'pearson'. Posible values are:
|
||||
|
||||
- 'pearson': pearson correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{0.5(1-\rho^{pearson}_{i,j})}`.
|
||||
- 'spearman': spearman correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{0.5(1-\rho^{spearman}_{i,j})}`.
|
||||
- 'abs_pearson': absolute value pearson correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{(1-|\rho^{pearson}_{i,j}|)}`.
|
||||
- 'abs_spearman': absolute value spearman correlation matrix. Distance formula: :math:`D_{i,j} = \sqrt{(1-|\rho^{spearman}_{i,j}|)}`.
|
||||
- 'distance': distance correlation matrix. Distance formula :math:`D_{i,j} = \sqrt{(1-\rho^{distance}_{i,j})}`.
|
||||
- 'mutual_info': mutual information matrix. Distance used is variation information matrix.
|
||||
- 'tail': lower tail dependence index matrix. Dissimilarity formula :math:`D_{i,j} = -\log{\lambda_{i,j}}`.
|
||||
- 'custom_cov': use custom correlation matrix based on the custom_cov parameter. Distance formula: :math:`D_{i,j} = \sqrt{0.5(1-\rho^{pearson}_{i,j})}`.
|
||||
|
||||
covariance : str, optional
|
||||
The method used to estimate the covariance matrix:
|
||||
The default is 'hist'. Posible values are:
|
||||
|
||||
- 'hist': use historical estimates.
|
||||
- 'ewma1': use ewma with adjust=True. For more information see `EWM <https://pandas.pydata.org/pandas-docs/stable/user_guide/window.html#exponentially-weighted-window>`_.
|
||||
- 'ewma2': use ewma with adjust=False. For more information see `EWM <https://pandas.pydata.org/pandas-docs/stable/user_guide/window.html#exponentially-weighted-window>`_.
|
||||
- 'ledoit': use the Ledoit and Wolf Shrinkage method.
|
||||
- 'oas': use the Oracle Approximation Shrinkage method.
|
||||
- 'shrunk': use the basic Shrunk Covariance method.
|
||||
- 'gl': use the basic Graphical Lasso Covariance method.
|
||||
- 'jlogo': use the j-LoGo Covariance method. For more information see: :cite:`c-jLogo`.
|
||||
- 'fixed': denoise using fixed method. For more information see chapter 2 of :cite:`c-MLforAM`.
|
||||
- 'spectral': denoise using spectral method. For more information see chapter 2 of :cite:`c-MLforAM`.
|
||||
- 'shrink': denoise using shrink method. For more information see chapter 2 of :cite:`c-MLforAM`.
|
||||
- 'custom_cov': use custom covariance matrix.
|
||||
|
||||
obj : str can be {'MinRisk', 'Utility', 'Sharpe' or 'ERC'}.
|
||||
Objective function used by the NCO model.
|
||||
The default is 'MinRisk'. Posible values are:
|
||||
|
||||
- 'MinRisk': Minimize the selected risk measure.
|
||||
- 'Utility': Maximize the Utility function :math:`\mu w - l \phi_{i}(w)`.
|
||||
- 'Sharpe': Maximize the risk adjusted return ratio based on the selected risk measure.
|
||||
- 'ERC': Equally risk contribution portfolio of the selected risk measure.
|
||||
|
||||
rm : str, optional
|
||||
The risk measure used to optimze the portfolio. If model is 'NCO',
|
||||
the risk measures available depends on the objective functon.
|
||||
The default is 'MV'. Posible values are:
|
||||
|
||||
- 'equal': Equally weighted.
|
||||
- 'vol': Standard Deviation.
|
||||
- 'MV': Variance.
|
||||
- 'MAD': Mean Absolute Deviation.
|
||||
- 'MSV': Semi Standard Deviation.
|
||||
- 'FLPM': First Lower Partial Moment (Omega Ratio).
|
||||
- 'SLPM': Second Lower Partial Moment (Sortino Ratio).
|
||||
- 'VaR': Value at Risk.
|
||||
- 'CVaR': Conditional Value at Risk.
|
||||
- 'TG': Tail Gini.
|
||||
- 'EVaR': Entropic Value at Risk.
|
||||
- 'WR': Worst Realization (Minimax).
|
||||
- 'RG': Range of returns.
|
||||
- 'CVRG': CVaR range of returns.
|
||||
- 'TGRG': Tail Gini range of returns.
|
||||
- 'MDD': Maximum Drawdown of uncompounded cumulative returns (Calmar Ratio).
|
||||
- 'ADD': Average Drawdown of uncompounded cumulative returns.
|
||||
- 'DaR': Drawdown at Risk of uncompounded cumulative returns.
|
||||
- 'CDaR': Conditional Drawdown at Risk of uncompounded cumulative returns.
|
||||
- 'EDaR': Entropic Drawdown at Risk of uncompounded cumulative returns.
|
||||
- 'UCI': Ulcer Index of uncompounded cumulative returns.
|
||||
- 'MDD_Rel': Maximum Drawdown of compounded cumulative returns (Calmar Ratio).
|
||||
- 'ADD_Rel': Average Drawdown of compounded cumulative returns.
|
||||
- 'DaR_Rel': Drawdown at Risk of compounded cumulative returns.
|
||||
- 'CDaR_Rel': Conditional Drawdown at Risk of compounded cumulative returns.
|
||||
- 'EDaR_Rel': Entropic Drawdown at Risk of compounded cumulative returns.
|
||||
- 'UCI_Rel': Ulcer Index of compounded cumulative returns.
|
||||
|
||||
rf : float, optional
|
||||
Risk free rate, must be in the same period of assets returns.
|
||||
The default is 0.
|
||||
l : scalar, optional
|
||||
Risk aversion factor of the 'Utility' objective function.
|
||||
The default is 2.
|
||||
custom_cov : DataFrame or None, optional
|
||||
Custom covariance matrix, used when codependence or covariance
|
||||
parameters have value 'custom_cov'. The default is None.
|
||||
custom_mu : DataFrame or None, optional
|
||||
Custom mean vector when NCO objective is 'Utility' or 'Sharpe'.
|
||||
The default is None.
|
||||
linkage : string, optional
|
||||
Linkage method of hierarchical clustering. For more information see `linkage <https://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html>`_.
|
||||
The default is 'single'. Posible values are:
|
||||
|
||||
- 'single'.
|
||||
- 'complete'.
|
||||
- 'average'.
|
||||
- 'weighted'.
|
||||
- 'centroid'.
|
||||
- 'median'.
|
||||
- 'ward'.
|
||||
- 'DBHT': Direct Bubble Hierarchical Tree.
|
||||
|
||||
k : int, optional
|
||||
Number of clusters. This value is took instead of the optimal number
|
||||
of clusters calculated with the two difference gap statistic.
|
||||
The default is None.
|
||||
max_k : int, optional
|
||||
Max number of clusters used by the two difference gap statistic
|
||||
to find the optimal number of clusters. The default is 10.
|
||||
bins_info: int or str
|
||||
Number of bins used to calculate variation of information. The default
|
||||
value is 'KN'. Posible values are:
|
||||
|
||||
- 'KN': Knuth's choice method. See more in `knuth_bin_width <https://docs.astropy.org/en/stable/api/astropy.stats.knuth_bin_width.html>`_.
|
||||
- 'FD': Freedman–Diaconis' choice method. See more in `freedman_bin_width <https://docs.astropy.org/en/stable/api/astropy.stats.freedman_bin_width.html>`_.
|
||||
- 'SC': Scotts' choice method. See more in `scott_bin_width <https://docs.astropy.org/en/stable/api/astropy.stats.scott_bin_width.html>`_.
|
||||
- 'HGR': Hacine-Gharbi and Ravier' choice method.
|
||||
- int: integer value choice by user.
|
||||
|
||||
alpha_tail : float, optional
|
||||
Significance level for lower tail dependence index. The default is 0.05.
|
||||
leaf_order : bool, optional
|
||||
Indicates if the cluster are ordered so that the distance between
|
||||
successive leaves is minimal. The default is True.
|
||||
d : scalar
|
||||
The smoothing factor of ewma methods.
|
||||
The default is 0.94.
|
||||
**kwargs:
|
||||
Other variables related to covariance estimation. See
|
||||
`Scikit Learn <https://scikit-learn.org/stable/modules/covariance.html>`_
|
||||
and chapter 2 of :cite:`d-MLforAM` for more details.
|
||||
|
||||
Returns
|
||||
-------
|
||||
w : DataFrame
|
||||
The weights of optimal portfolio.
|
||||
|
||||
"""
|
||||
|
||||
# Covariance matrix
|
||||
if covariance == "custom_cov":
|
||||
self.cov = custom_cov.copy()
|
||||
else:
|
||||
self.cov = pe.covar_matrix(
|
||||
self.returns, method=covariance, d=0.94, **kwargs
|
||||
)
|
||||
|
||||
# Custom mean vector
|
||||
if custom_mu is not None:
|
||||
if isinstance(custom_mu, pd.Series) == True:
|
||||
self.mu = custom_mu.to_frame().T
|
||||
elif isinstance(custom_mu, pd.DataFrame) == True:
|
||||
if custom_mu.shape[0] > 1 and custom_mu.shape[1] == 1:
|
||||
self.mu = custom_mu.T
|
||||
elif custom_mu.shape[0] == 1 and custom_mu.shape[1] > 1:
|
||||
self.mu = custom_mu
|
||||
else:
|
||||
raise NameError("custom_mu must be a column DataFrame")
|
||||
else:
|
||||
raise NameError("custom_mu must be a column DataFrame or Series")
|
||||
|
||||
self.alpha_tail = alpha_tail
|
||||
self.bins_info = bins_info
|
||||
|
||||
# Codependence matrix
|
||||
if codependence in {"pearson", "spearman"}:
|
||||
self.codep = self.returns.corr(method=codependence).astype(float)
|
||||
elif codependence in {"abs_pearson", "abs_spearman"}:
|
||||
self.codep = np.abs(self.returns.corr(method=codependence[4:])).astype(
|
||||
float
|
||||
)
|
||||
elif codependence in {"distance"}:
|
||||
self.codep = af.dcorr_matrix(self.returns).astype(float)
|
||||
elif codependence in {"mutual_info"}:
|
||||
self.codep = af.mutual_info_matrix(self.returns, self.bins_info).astype(
|
||||
float
|
||||
)
|
||||
elif codependence in {"tail"}:
|
||||
self.codep = af.ltdi_matrix(self.returns, alpha=self.alpha_tail).astype(
|
||||
float
|
||||
)
|
||||
elif codependence in {"custom_cov"}:
|
||||
self.codep = af.cov2corr(custom_cov).astype(float)
|
||||
|
||||
# Step-1: Tree clustering
|
||||
self.clusters, self.k = self._hierarchical_clustering(
|
||||
model, linkage, codependence, max_k, leaf_order=leaf_order
|
||||
)
|
||||
if k is not None:
|
||||
self.k = int(k)
|
||||
|
||||
# Step-2: Seriation (Quasi-Diagnalization)
|
||||
self.sort_order = self._seriation(self.clusters)
|
||||
asset_order = self.assetslist
|
||||
asset_order[:] = [self.assetslist[i] for i in self.sort_order]
|
||||
self.asset_order = asset_order.copy()
|
||||
self.codep_sorted = self.codep.reindex(
|
||||
index=self.asset_order, columns=self.asset_order
|
||||
)
|
||||
|
||||
if isinstance(self.w_max, pd.Series) and isinstance(self.w_min, pd.Series):
|
||||
self.w_max = self.w_max.reindex(index=self.asset_order)
|
||||
self.w_max.index = self.sort_order
|
||||
self.w_min = self.w_min.reindex(index=self.asset_order)
|
||||
self.w_min.index = self.sort_order
|
||||
|
||||
# Step-3: Recursive bisection
|
||||
if model == "HRP":
|
||||
# Recursive bisection
|
||||
weights = self._recursive_bisection(self.sort_order, rm=rm, rf=rf)
|
||||
elif model in ["HERC", "HERC2"]:
|
||||
# Cluster-based Recursive bisection
|
||||
weights = self._hierarchical_recursive_bisection(
|
||||
self.clusters, rm=rm, rf=rf, linkage=linkage, model=model
|
||||
)
|
||||
elif model == "NCO":
|
||||
# Step-3.1: Determine intra-cluster weights
|
||||
intra_weights = self._intra_weights(
|
||||
self.clusters, obj=obj, rm=rm, rf=rf, l=l
|
||||
)
|
||||
|
||||
# Step-3.2: Determine inter-cluster weights and multiply with → intra-cluster weights
|
||||
weights = self._inter_weights(intra_weights, obj=obj, rm=rm, rf=rf, l=l)
|
||||
|
||||
if isinstance(self.w_max, pd.Series) and isinstance(self.w_min, pd.Series):
|
||||
self.w_max = self.w_max.sort_index()
|
||||
self.w_max.index = self.assetslist
|
||||
self.w_min = self.w_min.sort_index()
|
||||
self.w_max.index = self.assetslist
|
||||
|
||||
weights = weights.loc[self.assetslist].to_frame()
|
||||
weights.columns = ["weights"]
|
||||
|
||||
return weights
|
||||
@@ -0,0 +1,257 @@
|
||||
"""""" #
|
||||
"""
|
||||
Copyright (c) 2020-2022, Dany Cajas
|
||||
All rights reserved.
|
||||
This work is licensed under BSD 3-Clause "New" or "Revised" License.
|
||||
License available at https://github.com/dcajasn/Riskfolio-Lib/blob/master/LICENSE.txt
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
def owa_gmd(T):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the Gini mean difference (GMD)
|
||||
of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
An OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
w_ = []
|
||||
for i in range(1, T + 1):
|
||||
w_.append(2 * i - 1 - T)
|
||||
w_ = 2 * np.array(w_) / (T * (T - 1))
|
||||
w_ = w_.reshape(-1, 1)
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_cvar(T, alpha=0.05):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the Conditional Value at Risk (CVaR)
|
||||
of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
alpha : float, optional
|
||||
Significance level of CVaR. The default is 0.05.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
An OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
k = int(np.ceil(T * alpha)) - 1
|
||||
w_ = np.zeros((T, 1))
|
||||
w_[:k, :] = -1 / (T * alpha)
|
||||
w_[k, :] = -1 - np.sum(w_[:k, :])
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_wcvar(T, alphas, weights):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the Weighted Conditional Value at
|
||||
Risk (WCVaR) of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
alphas : list
|
||||
List of significance levels of each CVaR model.
|
||||
weights : list
|
||||
List of weights of each CVaR model.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
An OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
w_ = 0
|
||||
for i, j in zip(alphas, weights):
|
||||
w_ += owa_cvar(T, i) * j
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_tg(T, alpha=0.05, a_sim=100):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the Tail Gini of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
alpha : float, optional
|
||||
Significance level of TaiL Gini. The default is 0.05.
|
||||
a_sim : float, optional
|
||||
Number of CVaRs used to approximate the Tail Gini. The default is 100.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
A OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
alphas = np.linspace(alpha, 0.0001, a_sim)[::-1]
|
||||
w_ = [(alphas[1] - 0) * alphas[0] / alphas[-1] ** 2]
|
||||
for i in range(1, len(alphas) - 1):
|
||||
w_.append((alphas[i + 1] - alphas[i - 1]) * alphas[i] / alphas[-1] ** 2)
|
||||
w_.append((alphas[-1] - alphas[-2]) / alphas[-1])
|
||||
w_ = owa_wcvar(T, alphas, w_)
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_wr(T):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the Worst realization (minimum) of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
A OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
w_ = np.zeros((T, 1))
|
||||
w_[0, :] = -1
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_rg(T):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the range of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
A OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
w_ = np.zeros((T, 1))
|
||||
w_[0, :] = -1
|
||||
w_[-1, :] = 1
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_cvrg(T, alpha=0.05, beta=None):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the CVaR range of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
alpha : float, optional
|
||||
Significance level of CVaR of losses. The default is 0.05.
|
||||
beta : float, optional
|
||||
Significance level of CVaR of gains. If None it duplicates alpha.
|
||||
The default is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
A OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
if beta is None:
|
||||
beta = alpha
|
||||
|
||||
w_ = owa_cvar(T, alpha) - owa_cvar(T, beta)[::-1]
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_wcvrg(T, alphas, weights_a, betas=None, weights_b=None):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the WCVaR range of a returns series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
alphas : list
|
||||
List of significance levels of each CVaR of losses model.
|
||||
weights_a : list
|
||||
List of weights of each CVaR of losses model.
|
||||
betas : list, optional
|
||||
List of significance levels of each CVaR of gains model. If None it duplicates alpha.
|
||||
The default is None.
|
||||
weights_b : list, optional
|
||||
List of weights of each CVaR of gains model. If None it duplicates weights_a.
|
||||
The default is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
A OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
if betas is None or weights_b is None:
|
||||
betas = alphas
|
||||
weights_b = weights_a
|
||||
|
||||
w_ = owa_wcvar(T, alphas, weights_a) - owa_wcvar(T, betas, weights_b)[::-1]
|
||||
|
||||
return w_
|
||||
|
||||
|
||||
def owa_tgrg(T, alpha=0.05, a_sim=100, beta=None, b_sim=None):
|
||||
r"""
|
||||
Calculate the OWA weights to calculate the Tail Gini range of a returns
|
||||
series.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
T : int
|
||||
Number of observations of the returns series.
|
||||
alpha : float, optional
|
||||
Significance level of Tail Gini of losses. The default is 0.05.
|
||||
a_sim : float, optional
|
||||
Number of CVaRs used to approximate Tail Gini of losses. The default is 100.
|
||||
beta : float, optional
|
||||
Significance level of Tail Gini of gains. If None it duplicates alpha value.
|
||||
The default is None.
|
||||
b_sim : float, optional
|
||||
Number of CVaRs used to approximate Tail Gini of gains. If None it duplicates a_sim value.
|
||||
The default is None.
|
||||
|
||||
Returns
|
||||
-------
|
||||
value : 1d-array
|
||||
A OWA weights vector of size Tx1.
|
||||
"""
|
||||
|
||||
if beta is None:
|
||||
beta = alpha
|
||||
if b_sim is None:
|
||||
b_sim = a_sim
|
||||
|
||||
w_ = owa_tg(T, alpha, a_sim) - owa_tg(T, beta, b_sim)[::-1]
|
||||
|
||||
return w_
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1
requirements-full.txt
vendored
1
requirements-full.txt
vendored
@@ -238,7 +238,6 @@ reportlab==3.6.9; python_version >= "3.7" and python_version < "4"
|
||||
requests-oauthlib==1.3.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6"
|
||||
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
|
||||
rich==10.16.2; python_full_version >= "3.6.2" and python_full_version < "4.0.0"
|
||||
riskfolio-lib==3.0.2; python_full_version >= "3.7"
|
||||
robin-stocks==2.1.0; python_version >= "3"
|
||||
rsa==4.8; python_version >= "3.6" and python_version < "4" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.6")
|
||||
scikit-learn==1.0.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7"
|
||||
|
||||
1
requirements.txt
vendored
1
requirements.txt
vendored
@@ -190,7 +190,6 @@ reportlab==3.6.9; python_version >= "3.7" and python_version < "4"
|
||||
requests-oauthlib==1.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0"
|
||||
requests==2.27.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.6.0")
|
||||
rich==10.16.2; python_full_version >= "3.6.2" and python_full_version < "4.0.0"
|
||||
riskfolio-lib==3.0.2; python_full_version >= "3.7"
|
||||
robin-stocks==2.1.0; python_version >= "3"
|
||||
scikit-learn==1.0.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7"
|
||||
scipy==1.8.0; python_version >= "3.8" and python_version < "3.11" and python_full_version >= "3.7.1" and python_full_version < "4.0.0" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0") and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.6.0" and python_version >= "3.7")
|
||||
|
||||
Reference in New Issue
Block a user