通用有效前沿

只要你有一个期望收益向量和一个协方差矩阵,就可以使用前面描述的均值-方差优化方法。目标和约束将是投资组合收益率和投资组合波动率的某种组合。

然而,你可能想为一个完全不同类型的风险模型(一个不依赖于协方差矩阵的模型)构建有效边界,或者优化一个与投资组合收益无关的目标(如跟踪误差)。 PyPortfolioOpt 带有几个流行的替代方案,并提供对自定义优化问题的支持。

有效半方差

这种均值-均方差优化不对整个波动性的惩罚,而是寻求只对下行波动性进行惩罚,因为上行波动性是可以接受的。

有两种方法来解决均值-半方差优化问题。第一种是使用启发式(有点快,但也有点脏)的解决方案:假装半方差矩阵(在 risk_models 中实现)是一个典型的协方差矩阵并进行标准的平均方差优化。 可以证明,这并不能产生一个在均值-半方差空间内有效的投资组合(尽管它可能是一个足够好的近似)。

幸运的是,有可能将均值-半方差优化写成一个凸问题(尽管有许多变量),可以通过求解得到一个精确解。 例如,为了在给定目标半方差 \(s^*\) 下收益最大化(只做多),我们将解决以下问题:

\[\begin{split}\begin{equation*} \begin{aligned} & \underset{w}{\text{maximise}} & & w^T \mu \\ & \text{subject to} & & n^T n \leq s^* \\ &&& B w - p + n = 0 \\ &&& w^T \mathbf{1} = 1 \\ &&& n \geq 0 \\ &&& p \geq 0. \\ \end{aligned} \end{equation*}\end{split}\]

这里, 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 - int

    • tickers - str list

    • bounds - float tuple OR (float tuple) list

    • returns - pd.DataFrame

    • expected_returns - np.ndarray

    • solver - str

    • solver_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 float

  • ValueError – 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 可以被记为:

\[CVaR(w, \beta) = \frac{1}{1-\beta} \int_{L(w, r) \geq \alpha (w)} L(w, r) p(r)dr.\]

这是一个令人讨厌的优化表达式,因为我们基本上是在整合 VaR 值。Rockafellar and Uryasev (2001) [3] 的关键见解是,我们可以等价地优化以下凸函数:

\[F_\beta (w, \alpha) = \alpha + \frac{1}{1-\beta} \int [-w^T r - \alpha]^+ p(r) dr,\]

其中 \([x]^+ = \max(x, 0)\) 。作者证明,最小化 \(F_\beta(w, \alpha)\) over \(w, \alpha\) 可以使 CVaR 最小化。假设我们有一个 T 个每日收益的样本(可以是历史的或模拟的)。 表达式中的积分变成了一个总和,因此 CVaR 优化问题减少为一个线性问题:

\[\begin{split}\begin{equation*} \begin{aligned} & \underset{w, \alpha}{\text{minimise}} & & \alpha + \frac{1}{1-\beta} \frac 1 T \sum_{i=1}^T u_i \\ & \text{subject to} & & u_i \geq 0 \\ &&& u_i \geq -w^T r_i - \alpha. \\ \end{aligned} \end{equation*}\end{split}\]

这个公式为每个数据点引入了一个新的变量(类似于有效半方差),所以 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 - int

    • tickers - str list

    • bounds - float tuple OR (float tuple) list

    • returns - pd.DataFrame

    • expected_returns - np.ndarray

    • solver - str

    • solver_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 float

  • ValueError – 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

portfolio_performance(verbose=False)[源代码]

优化后,计算(并可选择打印)最优投资组合的表现,具体是:期望收益率、CVaR

参数:

verbose (bool, optional) – whether performance should be printed, defaults to False

抛出:

ValueError – if weights have not been calculated yet

返回:

expected return, CVaR.

返回类型:

(float, float)

set_weights(input_weights)[源代码]

设置用户输入的权重属性(np.array)的实用函数

参数:

input_weights (dict) – {ticker: weight} dict

有效 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 可以被记为:

\[CDaR(w, \beta) = \frac{1}{1-\beta} \int_{D(w, r, t) \geq \alpha (w)} D(w, r, t) p(r(t))dr(t).\]

这是一个令人讨厌的优化表达式,与 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 - int

    • tickers - str list

    • bounds - float tuple OR (float tuple) list

    • returns - pd.DataFrame

    • expected_returns - np.ndarray

    • solver - str

    • solver_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 float

  • ValueError – 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

portfolio_performance(verbose=False)[源代码]

优化后,计算(并可选择打印)最佳投资组合的表现,特别是:期望收益率、CDaR

参数:

verbose (bool, optional) – whether performance should be printed, defaults to False

抛出:

ValueError – if weights have not been calculated yet

返回:

expected return, CDaR.

返回类型:

(float, float)

set_weights(input_weights)[源代码]

设置用户输入的权重属性(np.array)的实用函数

参数:

input_weights (dict) – {ticker: weight} dict

非常感谢 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

参考文献