Commit f549e333 authored by 李宗熹's avatar 李宗熹

添加基金诊断模块

parent 27d4cde4
from .black_litterman import (
market_implied_prior_returns,
market_implied_risk_aversion,
BlackLittermanModel,
)
from .cla import CLA
from .discrete_allocation import get_latest_prices, DiscreteAllocation
from .efficient_frontier import EfficientFrontier
from .hierarchical_portfolio import HRPOpt
from .risk_models import CovarianceShrinkage
__version__ = "1.2.6"
__all__ = [
"market_implied_prior_returns",
"market_implied_risk_aversion",
"BlackLittermanModel",
"CLA",
"get_latest_prices",
"DiscreteAllocation",
"EfficientFrontier",
"HRPOpt",
"CovarianceShrinkage",
]
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
"""
The ``exceptions`` module houses custom exceptions. Currently implemented:
- OptimizationError
"""
class OptimizationError(Exception):
"""
When an optimization routine fails – usually, this means
that cvxpy has not returned the "optimal" flag.
"""
def __init__(self, *args, **kwargs):
default_message = (
"Please check your objectives/constraints or use a different solver."
)
super().__init__(default_message, *args, **kwargs)
This diff is collapsed.
"""
The ``hierarchical_portfolio`` module seeks to implement one of the recent advances in
portfolio optimisation – the application of hierarchical clustering models in allocation.
All of the hierarchical classes have a similar API to ``EfficientFrontier``, though since
many hierarchical models currently don't support different objectives, the actual allocation
happens with a call to `optimize()`.
Currently implemented:
- ``HRPOpt`` implements the Hierarchical Risk Parity (HRP) portfolio. Code reproduced with
permission from Marcos Lopez de Prado (2016).
"""
import collections
import numpy as np
import pandas as pd
import scipy.cluster.hierarchy as sch
import scipy.spatial.distance as ssd
from . import base_optimizer, risk_models
class HRPOpt(base_optimizer.BaseOptimizer):
"""
A HRPOpt object (inheriting from BaseOptimizer) constructs a hierarchical
risk parity portfolio.
Instance variables:
- Inputs
- ``n_assets`` - int
- ``tickers`` - str list
- ``returns`` - pd.Series
- Output:
- ``weights`` - np.ndarray
- ``clusters`` - linkage matrix corresponding to clustered assets.
Public methods:
- ``optimize()`` calculates weights using HRP
- ``portfolio_performance()`` calculates the expected return, volatility and Sharpe ratio for
the optimised portfolio.
- ``set_weights()`` creates self.weights (np.ndarray) from a weights dict
- ``clean_weights()`` rounds the weights and clips near-zeros.
- ``save_weights_to_file()`` saves the weights to csv, json, or txt.
"""
def __init__(self, returns=None, cov_matrix=None):
"""
:param returns: asset historical returns
:type returns: pd.DataFrame
:param cov_matrix: covariance of asset returns
:type cov_matrix: pd.DataFrame.
:raises TypeError: if ``returns`` is not a dataframe
"""
if returns is None and cov_matrix is None:
raise ValueError("Either returns or cov_matrix must be provided")
if returns is not None and not isinstance(returns, pd.DataFrame):
raise TypeError("returns are not a dataframe")
self.returns = returns
self.cov_matrix = cov_matrix
self.clusters = None
if returns is None:
tickers = list(cov_matrix.columns)
else:
tickers = list(returns.columns)
super().__init__(len(tickers), tickers)
@staticmethod
def _get_cluster_var(cov, cluster_items):
"""
Compute the variance per cluster
:param cov: covariance matrix
:type cov: np.ndarray
:param cluster_items: tickers in the cluster
:type cluster_items: list
:return: the variance per cluster
:rtype: float
"""
# Compute variance per cluster
cov_slice = cov.loc[cluster_items, cluster_items]
weights = 1 / np.diag(cov_slice) # Inverse variance weights
weights /= weights.sum()
return np.linalg.multi_dot((weights, cov_slice, weights))
@staticmethod
def _get_quasi_diag(link):
"""
Sort clustered items by distance
:param link: linkage matrix after clustering
:type link: np.ndarray
:return: sorted list of indices
:rtype: list
"""
return sch.to_tree(link, rd=False).pre_order()
@staticmethod
def _raw_hrp_allocation(cov, ordered_tickers):
"""
Given the clusters, compute the portfolio that minimises risk by
recursively traversing the hierarchical tree from the top.
:param cov: covariance matrix
:type cov: np.ndarray
:param ordered_tickers: list of tickers ordered by distance
:type ordered_tickers: str list
:return: raw portfolio weights
:rtype: pd.Series
"""
w = pd.Series(1, index=ordered_tickers)
cluster_items = [ordered_tickers] # initialize all items in one cluster
while len(cluster_items) > 0:
cluster_items = [
i[j:k]
for i in cluster_items
for j, k in ((0, len(i) // 2), (len(i) // 2, len(i)))
if len(i) > 1
] # bi-section
# For each pair, optimise locally.
for i in range(0, len(cluster_items), 2):
first_cluster = cluster_items[i]
second_cluster = cluster_items[i + 1]
# Form the inverse variance portfolio for this pair
first_variance = HRPOpt._get_cluster_var(cov, first_cluster)
second_variance = HRPOpt._get_cluster_var(cov, second_cluster)
alpha = 1 - first_variance / (first_variance + second_variance)
w[first_cluster] *= alpha # weight 1
w[second_cluster] *= 1 - alpha # weight 2
return w
def optimize(self, linkage_method="single"):
"""
Construct a hierarchical risk parity portfolio, using Scipy hierarchical clustering
(see `here <https://docs.scipy.org/doc/scipy/reference/generated/scipy.cluster.hierarchy.linkage.html>`_)
:param linkage_method: which scipy linkage method to use
:type linkage_method: str
:return: weights for the HRP portfolio
:rtype: OrderedDict
"""
if linkage_method not in sch._LINKAGE_METHODS:
raise ValueError("linkage_method must be one recognised by scipy")
if self.returns is None:
cov = self.cov_matrix
corr = risk_models.cov_to_corr(self.cov_matrix).round(6)
else:
corr, cov = self.returns.corr(), self.returns.cov()
# Compute distance matrix, with ClusterWarning fix as
# per https://stackoverflow.com/questions/18952587/
# this can avoid some nasty floating point issues
matrix = np.sqrt(np.clip((1.0 - corr) / 2.0, a_min=0.0, a_max=1.0))
dist = ssd.squareform(matrix, checks=False)
self.clusters = sch.linkage(dist, linkage_method)
sort_ix = HRPOpt._get_quasi_diag(self.clusters)
ordered_tickers = corr.index[sort_ix].tolist()
hrp = HRPOpt._raw_hrp_allocation(cov, ordered_tickers)
weights = collections.OrderedDict(hrp.sort_index())
self.set_weights(weights)
return weights
def portfolio_performance(self, verbose=False, risk_free_rate=0.02, frequency=252):
"""
After optimising, calculate (and optionally print) the performance of the optimal
portfolio. Currently calculates expected return, volatility, and the Sharpe ratio
assuming returns are daily
:param verbose: whether performance should be printed, defaults to False
:type verbose: bool, optional
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
:param frequency: number of time periods in a year, defaults to 252 (the number
of trading days in a year)
:type frequency: int, optional
:raises ValueError: if weights have not been calculated yet
:return: expected return, volatility, Sharpe ratio.
:rtype: (float, float, float)
"""
if self.returns is None:
cov = self.cov_matrix
mu = None
else:
cov = self.returns.cov() * frequency
mu = self.returns.mean() * frequency
return base_optimizer.portfolio_performance(
self.weights, mu, cov, verbose, risk_free_rate
)
"""
The ``objective_functions`` module provides optimisation objectives, including the actual
objective functions called by the ``EfficientFrontier`` object's optimisation methods.
These methods are primarily designed for internal use during optimisation and each requires
a different signature (which is why they have not been factored into a class).
For obvious reasons, any objective function must accept ``weights``
as an argument, and must also have at least one of ``expected_returns`` or ``cov_matrix``.
The objective functions either compute the objective given a numpy array of weights, or they
return a cvxpy *expression* when weights are a ``cp.Variable``. In this way, the same objective
function can be used both internally for optimisation and externally for computing the objective
given weights. ``_objective_value()`` automatically chooses between the two behaviours.
``objective_functions`` defaults to objectives for minimisation. In the cases of objectives
that clearly should be maximised (e.g Sharpe Ratio, portfolio return), the objective function
actually returns the negative quantity, since minimising the negative is equivalent to maximising
the positive. This behaviour is controlled by the ``negative=True`` optional argument.
Currently implemented:
- Portfolio variance (i.e square of volatility)
- Portfolio return
- Sharpe ratio
- L2 regularisation (minimising this reduces nonzero weights)
- Quadratic utility
- Transaction cost model (a simple one)
"""
import numpy as np
import cvxpy as cp
def _objective_value(w, obj):
"""
Helper method to return either the value of the objective function
or the objective function as a cvxpy object depending on whether
w is a cvxpy variable or np array.
:param w: weights
:type w: np.ndarray OR cp.Variable
:param obj: objective function expression
:type obj: cp.Expression
:return: value of the objective function OR objective function expression
:rtype: float OR cp.Expression
"""
if isinstance(w, np.ndarray):
if np.isscalar(obj):
return obj
elif np.isscalar(obj.value):
return obj.value
else:
return obj.value.item()
else:
return obj
def portfolio_variance(w, cov_matrix):
"""
Calculate the total portfolio variance (i.e square volatility).
:param w: asset weights in the portfolio
:type w: np.ndarray OR cp.Variable
:param cov_matrix: covariance matrix
:type cov_matrix: np.ndarray
:return: value of the objective function OR objective function expression
:rtype: float OR cp.Expression
"""
variance = cp.quad_form(w, cov_matrix)
return _objective_value(w, variance)
def portfolio_return(w, expected_returns, negative=True):
"""
Calculate the (negative) mean return of a portfolio
:param w: asset weights in the portfolio
:type w: np.ndarray OR cp.Variable
:param expected_returns: expected return of each asset
:type expected_returns: np.ndarray
:param negative: whether quantity should be made negative (so we can minimise)
:type negative: boolean
:return: negative mean return
:rtype: float
"""
sign = -1 if negative else 1
mu = w @ expected_returns
return _objective_value(w, sign * mu)
def sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.02, negative=True):
"""
Calculate the (negative) Sharpe ratio of a portfolio
:param w: asset weights in the portfolio
:type w: np.ndarray OR cp.Variable
:param expected_returns: expected return of each asset
:type expected_returns: np.ndarray
:param cov_matrix: covariance matrix
:type cov_matrix: np.ndarray
:param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02.
The period of the risk-free rate should correspond to the
frequency of expected returns.
:type risk_free_rate: float, optional
:param negative: whether quantity should be made negative (so we can minimise)
:type negative: boolean
:return: (negative) Sharpe ratio
:rtype: float
"""
mu = w @ expected_returns
sigma = cp.sqrt(cp.quad_form(w, cov_matrix))
sign = -1 if negative else 1
sharpe = (mu - risk_free_rate) / sigma
return _objective_value(w, sign * sharpe)
def L2_reg(w, gamma=1):
r"""
L2 regularisation, i.e :math:`\gamma ||w||^2`, to increase the number of nonzero weights.
Example::
ef = EfficientFrontier(mu, S)
ef.add_objective(objective_functions.L2_reg, gamma=2)
ef.min_volatility()
:param w: asset weights in the portfolio
:type w: np.ndarray OR cp.Variable
:param gamma: L2 regularisation parameter, defaults to 1. Increase if you want more
non-negligible weights
:type gamma: float, optional
:return: value of the objective function OR objective function expression
:rtype: float OR cp.Expression
"""
L2_reg = gamma * cp.sum_squares(w)
return _objective_value(w, L2_reg)
def quadratic_utility(w, expected_returns, cov_matrix, risk_aversion, negative=True):
r"""
Quadratic utility function, i.e :math:`\mu - \frac 1 2 \delta w^T \Sigma w`.
:param w: asset weights in the portfolio
:type w: np.ndarray OR cp.Variable
:param expected_returns: expected return of each asset
:type expected_returns: np.ndarray
:param cov_matrix: covariance matrix
:type cov_matrix: np.ndarray
:param risk_aversion: risk aversion coefficient. Increase to reduce risk.
:type risk_aversion: float
:param negative: whether quantity should be made negative (so we can minimise).
:type negative: boolean
:return: value of the objective function OR objective function expression
:rtype: float OR cp.Expression
"""
sign = -1 if negative else 1
mu = w @ expected_returns
variance = cp.quad_form(w, cov_matrix)
utility = mu - 0.5 * risk_aversion * variance
return _objective_value(w, sign * utility)
def transaction_cost(w, w_prev, k=0.001):
"""
A very simple transaction cost model: sum all the weight changes
and multiply by a given fraction (default to 10bps). This simulates
a fixed percentage commission from your broker.
:param w: asset weights in the portfolio
:type w: np.ndarray OR cp.Variable
:param w_prev: previous weights
:type w_prev: np.ndarray
:param k: fractional cost per unit weight exchanged
:type k: float
:return: value of the objective function OR objective function expression
:rtype: float OR cp.Expression
"""
return _objective_value(w, k * cp.norm(w - w_prev, 1))
"""
The ``plotting`` module houses all the functions to generate various plots.
Currently implemented:
- ``plot_covariance`` - plot a correlation matrix
- ``plot_dendrogram`` - plot the hierarchical clusters in a portfolio
- ``plot_efficient_frontier`` – plot the efficient frontier, using the CLA algorithm.
- ``plot_weights`` - bar chart of weights
"""
import numpy as np
from . import risk_models
import scipy.cluster.hierarchy as sch
try:
import matplotlib.pyplot as plt
plt.style.use("seaborn-deep")
except (ModuleNotFoundError, ImportError):
raise ImportError("Please install matplotlib via pip or poetry")
def _plot_io(**kwargs):
"""
Helper method to optionally save the figure to file.
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param dpi: dpi of figure to save or plot, defaults to 300
:type dpi: int (between 50-500)
:param showfig: whether to plt.show() the figure, defaults to True
:type showfig: bool, optional
"""
filename = kwargs.get("filename", None)
showfig = kwargs.get("showfig", True)
dpi = kwargs.get("dpi", 300)
plt.tight_layout()
if filename:
plt.savefig(fname=filename, dpi=dpi)
if showfig:
plt.show()
def plot_covariance(cov_matrix, plot_correlation=False, show_tickers=True, **kwargs):
"""
Generate a basic plot of the covariance (or correlation) matrix, given a
covariance matrix.
:param cov_matrix: covariance matrix
:type cov_matrix: pd.DataFrame or np.ndarray
:param plot_correlation: whether to plot the correlation matrix instead, defaults to False.
:type plot_correlation: bool, optional
:param show_tickers: whether to use tickers as labels (not recommended for large portfolios),
defaults to True
:type show_tickers: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
if plot_correlation:
matrix = risk_models.cov_to_corr(cov_matrix)
else:
matrix = cov_matrix
fig, ax = plt.subplots()
cax = ax.imshow(matrix)
fig.colorbar(cax)
if show_tickers:
ax.set_xticks(np.arange(0, matrix.shape[0], 1))
ax.set_xticklabels(matrix.index)
ax.set_yticks(np.arange(0, matrix.shape[0], 1))
ax.set_yticklabels(matrix.index)
plt.xticks(rotation=90)
_plot_io(**kwargs)
return ax
def plot_dendrogram(hrp, show_tickers=True, **kwargs):
"""
Plot the clusters in the form of a dendrogram.
:param hrp: HRPpt object that has already been optimized.
:type hrp: object
:param show_tickers: whether to use tickers as labels (not recommended for large portfolios),
defaults to True
:type show_tickers: bool, optional
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param showfig: whether to plt.show() the figure, defaults to True
:type showfig: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
if hrp.clusters is None:
hrp.optimize()
fig, ax = plt.subplots()
if show_tickers:
sch.dendrogram(hrp.clusters, labels=hrp.tickers, ax=ax, orientation="top")
plt.xticks(rotation=90)
plt.tight_layout()
else:
sch.dendrogram(hrp.clusters, no_labels=True, ax=ax)
_plot_io(**kwargs)
return ax
def plot_efficient_frontier(cla, points=100, show_assets=True, **kwargs):
"""
Plot the efficient frontier based on a CLA object
:param points: number of points to plot, defaults to 100
:type points: int, optional
:param show_assets: whether we should plot the asset risks/returns also, defaults to True
:type show_assets: bool, optional
:param filename: name of the file to save to, defaults to None (doesn't save)
:type filename: str, optional
:param showfig: whether to plt.show() the figure, defaults to True
:type showfig: bool, optional
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
if cla.weights is None:
cla.max_sharpe()
optimal_ret, optimal_risk, _ = cla.portfolio_performance()
if cla.frontier_values is None:
cla.efficient_frontier(points=points)
mus, sigmas, _ = cla.frontier_values
fig, ax = plt.subplots()
ax.plot(sigmas, mus, label="Efficient frontier")
if show_assets:
ax.scatter(
np.sqrt(np.diag(cla.cov_matrix)),
cla.expected_returns,
s=30,
color="k",
label="assets",
)
ax.scatter(optimal_risk, optimal_ret, marker="x", s=100, color="r", label="optimal")
ax.legend()
ax.set_xlabel("Volatility")
ax.set_ylabel("Return")
_plot_io(**kwargs)
return ax
def plot_weights(weights, **kwargs):
"""
Plot the portfolio weights as a horizontal bar chart
:param weights: the weights outputted by any PyPortfolioOpt optimiser
:type weights: {ticker: weight} dict
:return: matplotlib axis
:rtype: matplotlib.axes object
"""
desc = sorted(weights.items(), key=lambda x: x[1], reverse=True)
labels = [i[0] for i in desc]
vals = [i[1] for i in desc]
y_pos = np.arange(len(labels))
fig, ax = plt.subplots()
ax.barh(y_pos, vals)
ax.set_xlabel("Weight")
ax.set_yticks(y_pos)
ax.set_yticklabels(labels)
ax.invert_yaxis()
_plot_io(**kwargs)
return ax
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
import numpy as np
import pandas as pd
from scipy.optimize import minimize
# 风险预算优化
def calculate_portfolio_var(w, S):
# 计算组合风险的函数
w = np.mat(w)
return w * S * w.T
def calculate_risk_contribution(w, S):
# 计算单个资产对总体风险贡献度的函数
w = np.mat(w)
sigma = np.sqrt(calculate_portfolio_var(w, S))
# 边际风险贡献
MRC = S * w.T / sigma
# 风险贡献
RC = np.multiply(MRC, w.T)
return RC
def risk_budget_objective(w, pars):
# 计算组合风险
S = pars[0] # 协方差矩阵
risk_target = pars[1] # 组合中资产预期风险贡献度的目标向量
sig_p = np.sqrt(calculate_portfolio_var(w, S)) # portfolio sigma
risk_target = np.asmatrix(np.multiply(sig_p, risk_target))
asset_RC = calculate_risk_contribution(w, S)
J = sum(np.square(asset_RC - risk_target.T))[0, 0] # sum of squared error
return J
def total_weight_constraint(x):
return np.sum(x) - 1.0
def long_only_constraint(x):
return x
# 根据资产预期目标风险贡献度来计算各资产的权重
def calcu_w(w_origin, S, risk_target):
# w0 = [0.2, 0.2, 0.2, 0.6]
# x_t = [0.25, 0.25, 0.25, 0.25] # 目标是让四个资产风险贡献度相等,即都为25%
cons = ({'type': 'eq', 'fun': total_weight_constraint},)
# {'type': 'ineq', 'fun': long_only_constraint})
res = minimize(risk_budget_objective, w_origin, args=[S, risk_target], method='SLSQP',
bounds=[[0,1]] * len(w_origin),
constraints=cons, options={'disp': True})
w_rb = np.asmatrix(res.x)
return w_rb
# return res.x
# 将各资产风险贡献度绘制成柱状图
def plot_rc(w, S):
rc = calculate_risk_contribution(w, S)
print(rc)
if __name__ == '__main__':
P = pd.read_csv("tests/resources/stock_prices.csv", parse_dates=True, index_col="date")
w_origin, S, mu = optim_drawdown(P, 0.6, [0.001, 1])
S = np.asmatrix(S)
w_origin = np.asarray([i for i in w_origin.values()])
risk_target = np.asarray([1/len(w_origin)] * len(w_origin))
print(risk_target)
w_rb = calcu_w(w_origin, S, risk_target)
print('各资产权重:', w_rb)
print(w_rb @ mu)
plot_rc(w_rb)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment