通用有效前沿
只要你有一个期望收益向量和一个协方差矩阵,就可以使用前面描述的均值-方差优化方法。目标和约束将是投资组合收益率和投资组合波动率的某种组合。
然而,你可能想为一个完全不同类型的风险模型(一个不依赖于协方差矩阵的模型)构建有效边界,或者优化一个与投资组合收益无关的目标(如跟踪误差)。 PyPortfolioOpt 带有几个流行的替代方案,并提供对自定义优化问题的支持。
有效半方差
这种均值-均方差优化不对整个波动性的惩罚,而是寻求只对下行波动性进行惩罚,因为上行波动性是可以接受的。
有两种方法来解决均值-半方差优化问题。第一种是使用启发式(有点快,但也有点脏)的解决方案:假装半方差矩阵(在 risk_models
中实现)是一个典型的协方差矩阵并进行标准的平均方差优化。
可以证明,这并不能产生一个在均值-半方差空间内有效的投资组合(尽管它可能是一个足够好的近似)。
幸运的是,有可能将均值-半方差优化写成一个凸问题(尽管有许多变量),可以通过求解得到一个精确解。 例如,为了在给定目标半方差 \(s^*\) 下收益最大化(只做多),我们将解决以下问题:
这里, B = (returns - benchmark) / sqrt(T)
超额收益的 \(T \times N\) (缩放)矩阵。可以添加额外的线性平等约束和凸不平等约束。
PyPortfolioOpt 允许用户使用 EfficientSemivariance
类顺着有效半方差前沿进行优化。
EfficientSemivariance
继承自 EfficientFrontier
,所以它有相同的实用方法(比如 add_constraint()
, portfolio_performance()
)。
注意,一些父方法,如 max_sharpe()
和 min_volatility()
不适用于均值-半方差投资组合,所以调用它们会返回 NotImplementedError
。
EfficientSemivariance
的 API 与 EfficientFrontier
略有不同。
不应该传入协方差矩阵,而应该传入一个历史/模拟收益的 dataframe(可以使用辅助方法 expected_returns.returns_from_prices()
从价格 dataframe 中构建)。
下面是一个完整的例子,在这个例子中,我们寻求在 20% 的目标年收益率下,使半方差最小的投资组合:
from pypfopt import expected_returns, EfficientSemivariance
df = ... # your dataframe of prices
mu = expected_returns.mean_historical_returns(df)
historical_returns = expected_returns.returns_from_prices(df)
es = EfficientSemivariance(mu, historical_returns)
es.efficient_return(0.20)
# We can use the same helper methods as before
weights = es.clean_weights()
print(weights)
es.portfolio_performance(verbose=True)
portfolio_performance
方法输出的是投资组合的期望收益率、半方差和 Sortino 比率(类似夏普比率,但只针对下行偏差)。
有兴趣的读者可以参考 Estrada (2007) [1] ,以了解更多细节。 我要感谢 Philipp Schiele 编写了大部分的有效半方差功能和文档(所有错误我负责)。 该实现是基于 Markowitz 等人(2019) [2] 。
小心
在均值-半方差前沿寻找投资组合,在计算上比标准的均值-协方差优化更难:我们的实现使用了 2T + N
个优化变量,这意味着对于 50 种资产和 3 年的数据,有大约 1500 个变量。
虽然 EfficientSemivariance
原则上允许额外的约束和目标,但更有可能遇到求解器错误。
我建议将 EfficientSemivariance
问题保持在较小的范围内,并尽量减少约束条件。
- class pypfopt.efficient_frontier.EfficientSemivariance(expected_returns, returns, frequency=252, benchmark=0, weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None)[源代码]
EfficientSemivariance 对象帮助沿着均值-均值边界进行优化。这适合那些更关注下行偏差的用户。
可用变量:
输入:
n_assets
- inttickers
- str listbounds
- float tuple OR (float tuple) listreturns
- pd.DataFrameexpected_returns
- np.ndarraysolver
- strsolver_options
- {str: str} dict
输出:
weights
- np.ndarray
公共方法:
min_semivariance()
最小化投资组合的 semivariance(下行偏差)。max_quadratic_utility()
在一定的风险规避下,最大化”下行二次方效用”。efficient_risk()
在给定的目标半偏差下,使收益最大化。efficient_return()
在给定的目标收益率下,最小化半偏差。add_objective()
为优化问题添加一个(凸)目标。add_constraint()
在优化问题中加入一个约束条件。convex_objective()
解决一个带有线性约束的通用凸目标。portfolio_performance()
计算优化后的投资组合期望收益率、半偏差和索提诺比率。set_weights()
从一个权重字典中创建 self.weights (np.ndarray)。clean_weights()
对权重进行四舍五入,并剪掉接近零的部分。save_weights_to_file()
将权重保存为 csv、json 或 txt。
- efficient_return(target_return, market_neutral=False)[源代码]
在给定的目标收益下,使半方差最小。
- 参数:
target_return (float) – the desired return of the resulting portfolio.
market_neutral (bool, optional) – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
- 抛出:
ValueError – if
target_return
is not a positive floatValueError – if no portfolio can be found with return equal to
target_return
- 返回:
asset weights for the optimal portfolio
- 返回类型:
OrderedDict
- efficient_risk(target_semideviation, market_neutral=False)[源代码]
使目标半标准差(下行标准偏差)的收益最大化。由此产生的投资组合将有一个小于目标的半标准差(但不保证相等)。
- 参数:
target_semideviation (float) – the desired maximum semideviation of the resulting portfolio.
market_neutral – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
market_neutral – bool, optional
- 返回:
asset weights for the efficient risk portfolio
- 返回类型:
OrderedDict
- max_quadratic_utility(risk_aversion=1, market_neutral=False)[源代码]
最大化给定的二次效用,使用半方差而不是方差。
- 参数:
risk_aversion (positive float) – risk aversion parameter (must be greater than 0), defaults to 1
market_neutral – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
market_neutral – bool, optional
- 返回:
asset weights for the maximum-utility portfolio
- 返回类型:
OrderedDict
- min_semivariance(market_neutral=False)[源代码]
最小化投资组合的半方差(进一步解释见文档)。
- 参数:
market_neutral – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
market_neutral – bool, optional
- 返回:
asset weights for the volatility-minimising portfolio
- 返回类型:
OrderedDict
- portfolio_performance(verbose=False, risk_free_rate=0.02)[源代码]
优化后,计算(并可选择打印)最优投资组合的表现,具体包括:期望收益率、半离差和索提诺比率。
- 参数:
verbose (bool, optional) – whether performance should be printed, defaults to False
risk_free_rate (float, optional) – 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.
- 抛出:
ValueError – if weights have not been calculated yet
- 返回:
expected return, semideviation, Sortino ratio.
- 返回类型:
(float, float, float)
有效 CVaR
Conditional Value-at-Risk 条件在险价值(又称预期亏损)是衡量尾部风险的一个常用指标。 CVaR 可以被认为是在非常糟糕的日子发生的损失的平均值,其中非常糟糕是由参数 \(\beta\) 来量化的。
例如,如果我们计算出的 CVaR 是 10%,则 \(\beta = 0.95\) 的情况下,我们可以有 95% 的把握认为最坏情况下的日均损失将是 3%。 换句话说,CVaR 是所有损失的平均数,它们只发生在 \((1-\beta)\%\) 的时间。
虽然 CVaR 是一个相当直观的概念,但需要很多新的符号来对其进行数学表述(详见维基百科的 预期亏损 )。 我们将采用以下公式:
w 为投资组合的权重向量
r 为资产收益向量(日频), 其概率分布为 \(p(r)\)
\(L(w, r) = - w^T r\) 投资组合的损失
\(\alpha\) 代表投资组合的风险值(VaR),置信度为 \(\beta\).
CVaR 可以被记为:
这是一个令人讨厌的优化表达式,因为我们基本上是在整合 VaR 值。Rockafellar and Uryasev (2001) [3] 的关键见解是,我们可以等价地优化以下凸函数:
其中 \([x]^+ = \max(x, 0)\) 。作者证明,最小化 \(F_\beta(w, \alpha)\) over \(w, \alpha\) 可以使 CVaR 最小化。假设我们有一个 T 个每日收益的样本(可以是历史的或模拟的)。 表达式中的积分变成了一个总和,因此 CVaR 优化问题减少为一个线性问题:
这个公式为每个数据点引入了一个新的变量(类似于有效半方差),所以 dataframe 数据过长可能会遇到性能问题。 同时,应该致力于提供一个足够大的数据样本,以包括尾部事件。
感谢 Nicolas Knudde 提供的初稿(所有错误我负责)。该实现基于 Rockafellar and Uryasev (2001) [3] 。
- class pypfopt.efficient_frontier.EfficientCVaR(expected_returns, returns, beta=0.95, weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None)[源代码]
EfficientCVaR 类允许沿平均 CVaR 前沿进行优化,使用R ockafellar and Ursayev(2001)的表述。
可用变量:
输入:
n_assets
- inttickers
- str listbounds
- float tuple OR (float tuple) listreturns
- pd.DataFrameexpected_returns
- np.ndarraysolver
- strsolver_options
- {str: str} dict
输出:
weights
- np.ndarray
公共方法:
min_cvar()
使 CVaR 最小化efficient_risk()
在给定的 CVaR 的情况下,使收益最大化。efficient_return()
在给定的目标收益率下,使 CVaR 最小。add_objective()
为优化问题添加一个(凸)目标add_constraint()
在优化问题中加入一个约束条件。portfolio_performance()
计算投资组合的期望收益和 CVaR。set_weights()
从 weights dict 创建 self.weights (np.ndarray)。clean_weights()
对权重进行四舍五入,并剪掉接近零的部分。save_weights_to_file()
将权重保存为 csv、json 或 txt。
- efficient_return(target_return, market_neutral=False)[源代码]
在给定的目标收益下,使 CVaR 最小化。
- 参数:
target_return (float) – the desired return of the resulting portfolio.
market_neutral (bool, optional) – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
- 抛出:
ValueError – if
target_return
is not a positive floatValueError – if no portfolio can be found with return equal to
target_return
- 返回:
asset weights for the optimal portfolio
- 返回类型:
OrderedDict
- efficient_risk(target_cvar, market_neutral=False)[源代码]
使目标 CVaR 的收益最大化。最终的投资组合的 CVaR 将低于目标值(但不保证相等)。
- 参数:
target_cvar (float) – the desired conditional value at risk of the resulting portfolio.
market_neutral – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
market_neutral – bool, optional
- 返回:
asset weights for the efficient risk portfolio
- 返回类型:
OrderedDict
- min_cvar(market_neutral=False)[源代码]
尽量减少投资组合的 CVaR(进一步解释见文件)。
- 参数:
market_neutral – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
market_neutral – bool, optional
- 返回:
asset weights for the volatility-minimising portfolio
- 返回类型:
OrderedDict
有效 CDaR
Conditional Drawdown at Risk 条件在险回撤(CDaR)是一种比较特殊的尾部风险衡量方法。 它试图缓解有效半方差和有效 CVaR 的问题,因为它考虑了资产大幅下降的时间范围。 CDaR 可以被认为是在非常糟糕的时期发生的损失的平均值,其中非常糟糕是由参数 \(\beta\) 来量化的。 回撤被定义为非复利下收益率与前一峰值的差异。
换句话说,CDaR 是只发生在 \((1-\beta)\%\) 的时间回撤的平均数。 当 \(\beta = 1\) 时,CDaR 就仅仅是最大的回撤。
虽然回撤是一个相当直观的概念,但需要很多新的符号来对其进行数学表述(详见维基百科的 回撤)。 我们将采用以下公式:
w 为投资组合的权重向量
r 为累积资产收益(日频)的向量,其概率分布为 \(p(r(t))\)
\(D(w, r, t) = \max_{\tau<t}(w^T r(\tau))-w^T r(t)\) 为投资组合的回撤
\(\alpha\) 代表投资组合的回撤(DaR),置信度为 \(\beta\).
CDaR 可以被记为:
这是一个令人讨厌的优化表达式,与 VaR 的情况类似。 Chekhlov, Rockafellar and Uryasev (2005) [4] 的关键见解是,可以等价地优化一个凸函数,它可以被转化为一个线性问题(与 CVaR 的方式相同)。
- class pypfopt.efficient_frontier.EfficientCDaR(expected_returns, returns, beta=0.95, weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None)[源代码]
EfficientCDaR 类允许沿平均 CDaR 前沿进行优化,使用 Chekhlov, Ursayev and Zabarankin(2005)的表述。
可用变量:
输入:
n_assets
- inttickers
- str listbounds
- float tuple OR (float tuple) listreturns
- pd.DataFrameexpected_returns
- np.ndarraysolver
- strsolver_options
- {str: str} dict
输出:
weights
- np.ndarray
公共方法:
min_cdar()
使CDaR最小化efficient_risk()
在给定的 CDaR 的情况下,使收益最大化。efficient_return()
在给定的目标收益率下,使 CDaR 最小。add_objective()
为优化问题添加一个(凸)目标。add_constraint()
在优化问题中加入一个(线性)约束条件。portfolio_performance()
计算投资组合的期望收益和 CDaR。set_weights()
从 weights dict 创建 self.weights (np.ndarray)。clean_weights()
对权重进行四舍五入,并剪掉接近零的部分。save_weights_to_file()
将权重保存为 csv、json 或 txt。
- efficient_return(target_return, market_neutral=False)[源代码]
在给定的目标收益下,使 CDaR 最小化。
- 参数:
target_return (float) – the desired return of the resulting portfolio.
market_neutral (bool, optional) – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
- 抛出:
ValueError – if
target_return
is not a positive floatValueError – if no portfolio can be found with return equal to
target_return
- 返回:
asset weights for the optimal portfolio
- 返回类型:
OrderedDict
- efficient_risk(target_cdar, market_neutral=False)[源代码]
使目标 CDaR 的收益率最大化。最终的投资组合的 CDaR 将低于目标值(但不保证相等)。
- 参数:
target_cdar (float) – the desired maximum CDaR of the resulting portfolio.
market_neutral – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
market_neutral – bool, optional
- 返回:
asset weights for the efficient risk portfolio
- 返回类型:
OrderedDict
- min_cdar(market_neutral=False)[源代码]
尽量减少投资组合的 CDaR(进一步解释见文件)。
- 参数:
market_neutral – whether the portfolio should be market neutral (weights sum to zero), defaults to False. Requires negative lower weight bound.
market_neutral – bool, optional
- 返回:
asset weights for the volatility-minimising portfolio
- 返回类型:
OrderedDict
非常感谢 Nicolas Knudde 实现了这个功能。
定制优化问题
我们之前已经看到,向 EfficientFrontier
对象(以及扩展到其他一般的有效前沿对象,如 EfficientSemivariance
)添加约束条件是很容易的。
但是,如果你对 max_sharpe()
, min_volatility()
, efficient_risk()
等相关的目标不感兴趣,而想设置一个全新的问题来优化一些自定义的目标呢?
例如,也许我们的目标是构建一个最能复制特定指数的一篮子资产,换句话说,就是使跟踪误差最小化。 这并不适合均值-方差优化范式,我们仍然可以在 PyPortfolioOpt 中实现它:
from pypfopt.base_optimizer import BaseConvexOptimizer
from pypfopt.objective_functions import ex_post_tracking_error
historic_rets = ... # dataframe of historic asset returns
benchmark_rets = ... # pd.Series of historic benchmark returns (same index as historic)
opt = BaseConvexOptimizer(
n_assets=len(historic_returns.columns),
tickers=historic_returns.columns,
weight_bounds=(0, 1)
)
opt.convex_objective(
ex_post_tracking_error,
historic_returns=historic_rets,
benchmark_returns=benchmark_rets,
)
weights = opt.clean_weights()
EfficientFrontier
类继承于 BaseConvexOptimizer
。
从 EfficientFrontier
实例中调用 convex_objective
可能比从 BaseConvexOptimizer
中调用更方便,特别是当目标依赖于平均收益或协方差矩阵时。
可以优化一些通用的 convex_objective
(必须使用 cvxpy
原子函数来构建,见 这里 )或 nonconvex_objective
,它们使用 scipy.optimize
作为后台,因此有一个完全不同的API。关于更多的例子,请看这个 cookbook 。
- class pypfopt.base_optimizer.BaseConvexOptimizer
- BaseConvexOptimizer.convex_objective(custom_objective, weights_sum_to_one=True, **kwargs)
优化一个自定义的凸型目标函数。应使用
ef.add_constraint()
添加约束条件。优化器的参数必须作为 keyword-args 传递。例如:# Could define as a lambda function instead def logarithmic_barrier(w, cov_matrix, k=0.1): # 60 Years of Portfolio Optimization, Kolm et al (2014) return cp.quad_form(w, cov_matrix) - k * cp.sum(cp.log(w)) w = ef.convex_objective(logarithmic_barrier, cov_matrix=ef.cov_matrix)
- 参数:
custom_objective (function with signature (cp.Variable, **kwargs) -> cp.Expression) – an objective function to be MINIMISED. This should be written using cvxpy atoms Should map (w, **kwargs) -> float.
weights_sum_to_one (bool, optional) – whether to add the default objective, defaults to True
- 抛出:
OptimizationError – if the objective is nonconvex or constraints nonlinear.
- 返回:
asset weights for the efficient risk portfolio
- 返回类型:
OrderedDict
- BaseConvexOptimizer.nonconvex_objective(custom_objective, objective_args=None, weights_sum_to_one=True, constraints=None, solver='SLSQP', initial_guess=None)
使用 scipy 后端优化一些目标函数。这可以支持非凸目标和非线性约束,但可能会卡在局部最小值。例如:
# Market-neutral efficient risk constraints = [ {"type": "eq", "fun": lambda w: np.sum(w)}, # weights sum to zero { "type": "eq", "fun": lambda w: target_risk ** 2 - np.dot(w.T, np.dot(ef.cov_matrix, w)), }, # risk = target_risk ] ef.nonconvex_objective( lambda w, mu: -w.T.dot(mu), # min negative return (i.e maximise return) objective_args=(ef.expected_returns,), weights_sum_to_one=False, constraints=constraints, )
- 参数:
objective_function (function with signature (np.ndarray, args) -> float) – an objective function to be MINIMISED. This function should map (weight, args) -> cost
objective_args (tuple of np.ndarrays) – arguments for the objective function (excluding weight)
weights_sum_to_one (bool, optional) – whether to add the default objective, defaults to True
constraints (dict list) – list of constraints in the scipy format (i.e dicts)
solver (string) – which SCIPY solver to use, e.g “SLSQP”, “COBYLA”, “BFGS”. User beware: different optimizers require different inputs.
initial_guess (np.ndarray) – the initial guess for the weights, shape (n,) or (n, 1)
- 返回:
asset weights that optimize the custom objective
- 返回类型:
OrderedDict