均值方差优化

一般来说,数学优化是一个非常困难的问题,特别是当我们处理复杂的目标和约束条件时。 然而,凸式优化问题是一类被深入研究过的问题,正好被金融所用。凸问题有以下形式:

\[\begin{split}\begin{equation*} \begin{aligned} & \underset{\mathbf{x}}{\text{minimise}} & & f(\mathbf{x}) \\ & \text{subject to} & & g_i(\mathbf{x}) \leq 0, i = 1, \ldots, m\\ &&& A\mathbf{x} = b,\\ \end{aligned} \end{equation*}\end{split}\]

其中 \(\mathbf{x} \in \mathbb{R}^n\) ,并且 \(f(\mathbf{x}), g_i(\mathbf{x})\) 是凸函数。 [1]

幸运的是,投资组合优化问题(具有标准目标和约束)是凸的。这使得我们可以立即应用大量的理论以及精炼的求解程序。相应地,主要的困难是将我们的具体问题输入求解器。

PyPortfolioOpt 承担这项困难的工作,允许像 ef.min_volatility() 这样的单行代码来生成一个最小化波动率的投资组合,同时允许从中派生更复杂的问题。 这一切都要归功于 cvxpy ,它是一个很棒的 python 凸优化建模库,PyPortfolioOpt 的有效前沿功能正是基于它。

小技巧

你可以在相关 cookbook 中找到完整的例子。

结构

如凸问题的定义所示,我们基本上需要指定两件事:优化目标和优化约束。 例如,经典的投资组合优化问题是,在收益约束条件下使风险最小化(即投资组合的收益必须超过某一数额)。 然而,从实施的角度来看,优化目标和约束条件之间并没有太大的区别。 考虑一个类似的问题,在风险约束下实现收益最大化,这时风险和收益的角色与上面的相比,进行了交换。

为此,PyPortfolioOpt 定义了一个 objective_functions 模块,其中包含目标函数(正如我们刚才看到的,它也可以作为约束条件)。 实际的优化发生在 efficient_frontier.EfficientFrontier 类中。这个类为优化不同的目标提供了直接的方法(下面都有说明)。

PyPortfolioOpt 的设计使你可以很容易地在现有问题上添加新的约束或目标项。 例如,在最小波动率目标中增加一个正则化目标(下文有解释)就很简单:

ef = EfficientFrontier(expected_returns, cov_matrix)  # 初始化
ef.add_objective(objective_functions.L2_reg)  # 添加第二个目标函数
ef.min_volatility()  # 寻找最小化波动率和 L2_reg 的投资组合

小技巧

如果想绘制有效前沿,请看一下 绘图 模块。

基本用法

efficient_frontier 模块包含 EfficientFrontier 类和它的子类,它们为各种可能的目标函数和参数生成最优投资组合。

class pypfopt.efficient_frontier.EfficientFrontier(expected_returns, cov_matrix, weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None)[源代码]

一个 EfficientFrontier 对象(继承自 BaseConvexOptimizer)包含多个优化方法,可以用不同的参数调用(对应不同的目标函数)。 注意:如果要对目标/约束条件/边界/参数做任何改变,应该实例化一个新的 EfficientFrontier 对象。

可用变量:

  • Inputs:

    • n_assets - int

    • tickers - str list

    • bounds - float tuple 或 (float tuple) list

    • cov_matrix - np.ndarray

    • expected_returns - np.ndarray

    • solver - str

    • solver_options - {str: str} dict

  • Output: weights - np.ndarray

Public methods:

  • min_volatility() 优化最小波动率

  • max_sharpe() 对最大夏普比率进行优化(又称切线组合)

  • 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 格式。

__init__(expected_returns, cov_matrix, weight_bounds=(0, 1), solver=None, verbose=False, solver_options=None)[源代码]
参数:
  • expected_returns (pd.Series, list, np.ndarray) – expected returns for each asset. Can be None if optimising for volatility only (but not recommended).

  • cov_matrix (pd.DataFrame or np.array) – covariance of returns for each asset. This must be positive semidefinite, otherwise optimization will fail.

  • weight_bounds (tuple OR tuple list, optional) – minimum and maximum weight of each asset OR single min/max pair if all identical, defaults to (0, 1). Must be changed to (-1, 1) for portfolios with shorting.

  • solver (str) – name of solver. list available solvers with: cvxpy.installed_solvers()

  • verbose (bool, optional) – whether performance and debugging info should be printed, defaults to False

  • solver_options (dict, optional) – parameters for the given solver

抛出:
  • TypeError – if expected_returns is not a series, list or array

  • TypeError – if cov_matrix is not a dataframe or array

备注

从 v0.5.0 开始,可以传递一个 (min, max) 对的集合(list 或 tuple),代表不同资产的不同界限。

小技巧

如果想产生只做空头的投资组合,有一个快速的解决方法。将期望收益乘以 -1,然后优化一个只做多的投资组合。

min_volatility()[源代码]

最小化波动率。

返回:

asset weights for the volatility-minimising portfolio

返回类型:

OrderedDict

max_sharpe(risk_free_rate=0.02)[源代码]

最大化夏普比率。这个结果也被称为切线组合,因为它是资本市场线与有效前沿相切的组合。 这是一个进行了一定的变量替换后的凸优化问题。详见 Cornuejols and Tutuncu (2006)

参数:

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 risk_free_rate is non-numeric

返回:

asset weights for the Sharpe-maximising portfolio

返回类型:

OrderedDict

小心

因为 max_sharpe() 做了一个变量替换,额外的目标可能无法按预期工作。

max_quadratic_utility(risk_aversion=1, market_neutral=False)[源代码]

使给定的二次方效用最大化,即:

\[\max_w w^T \mu - \frac \delta 2 w^T \Sigma w\]
参数:
  • 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

备注

pypfopt.black_litterman 提供了一种计算市场隐含风险偏好参数的方法,在没有其他信息的情况下,它可以提供一个有用的估计值。

efficient_risk(target_volatility, market_neutral=False)[源代码]

在目标风险下实现收益最大化。由此产生的投资组合的波动率将小于目标值(但不保证相等)。

参数:
  • target_volatility (float) – the desired maximum volatility 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

抛出:
  • ValueError – if target_volatility is not a positive float

  • ValueError – if no portfolio can be found with volatility equal to target_volatility

  • ValueError – if risk_free_rate is non-numeric

返回:

asset weights for the efficient risk portfolio

返回类型:

OrderedDict

小心

如果你在 efficient_risk()efficient_return() 中传递了一个不合理的目标, 优化器将无声地失败,并返回奇怪的权重。谨慎使用!

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 Markowitz 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, volatility, Sharpe ratio.

返回类型:

(float, float, float)

小技巧

如果想独立于任何优化器使用 portfolio_performance 函数(例如用于调试),可以使用:

from pypfopt import base_optimizer

base_optimizer.portfolio_performance(
    weights, expected_returns, cov_matrix, verbose=True, risk_free_rate=0.02
)

备注

PyPortfolioOpt 默认选择 cvxpy 的求解器。 如果你想明确地选择求解器,只需将可选的 solver = "ECOS" kwargs 传递给构造函数。 你可以从 支持的求解器 中选择,并通过 solver_options``(一个 ``dict)传入求解器参数。

添加目标和约束

EfficientFrontier 继承自 BaseConvexOptimizer 类。添加约束条件和目标的函数文档如下:

class pypfopt.base_optimizer.BaseConvexOptimizer
BaseConvexOptimizer.add_constraint(new_constraint)

在优化问题中加入一个新的约束条件。这个约束必须满足 DCP 规则,即要么是一个线性平等约束,要么是凸不平等约束。

例如:

ef.add_constraint(lambda x : x[0] == 0.02)
ef.add_constraint(lambda x : x >= 0.01)
ef.add_constraint(lambda x: x <= np.array([0.01, 0.08, ..., 0.5]))
参数:

new_constraint (callable (e.g lambda function)) – the constraint to be added

BaseConvexOptimizer.add_sector_constraints(sector_mapper, sector_lower, sector_upper)

对不同资产组的权重总和添加限制。最常见的是行业限制,例如,投资组合中的科技风险必须低于 x%:

sector_mapper = {
    "GOOG": "tech",
    "FB": "tech",,
    "XOM": "Oil/Gas",
    "RRC": "Oil/Gas",
    "MA": "Financials",
    "JPM": "Financials",
}

sector_lower = {"tech": 0.1}  # at least 10% to tech
sector_upper = {
    "tech": 0.4, # less than 40% tech
    "Oil/Gas": 0.1 # less than 10% oil and gas
}
参数:
  • sector_mapper ({str: str} dict) – dict that maps tickers to sectors

  • sector_lower ({str: float} dict) – lower bounds for each sector

  • sector_upper ({str:float} dict) – upper bounds for each sector

BaseConvexOptimizer.add_objective(new_objective, **kwargs)

在目标函数中添加一个新项。这个项必须是凸的,并由 cvxpy 原子函数构建。

例如:

def L1_norm(w, k=1):
    return k * cp.norm(w, 1)

ef.add_objective(L1_norm, k=2)
参数:

new_objective (cp.Expression (i.e function of cp.Variable)) – the objective to be added

目标函数

objective_functions 模块提供优化目标,包括 EfficientFrontier 对象的优化方法所调用的实际目标函数。 这些方法主要是为优化过程中的内部使用而设计的,每个方法都需要不同的参数(这就是为什么它们没有被归入一个类的原因)。 由于显而易见的原因,任何目标函数都必须接受 weights 作为参数,并且必须至少有 expected_returnscov_matrix 中的一个。

目标函数要么计算给定权重的 numpy 数组的目标,要么当权重是 cp.Variable 时,它返回一个 cvxpy 表达式。 这样一来,同一个目标函数既可以在内部用于优化,也可以在外部用于计算给定权重的目标。 _objective_value() 会自动在这两种行为之间选择。

objective_functions 默认为用于最小化的优化。 在明显应该最大化的目标的情况下(例如夏普比率,投资组合收益),目标函数实际上返回负数,因为最小化负数等同于最大化正数。 这种行为可由 negative=True 的可选参数控制。

目前已经实现:

  • 投资组合方差(即波动率的平方)。

  • 投资组合收益率

  • 夏普比率

  • L2 正则化(最小化它可以减少非零权重)。

  • 二次方效用

  • 交易成本模型(简单的模型)

  • 事前(平方)跟踪误差

  • 事后(平方)跟踪误差

pypfopt.objective_functions.L2_reg(w, gamma=1)[源代码]

L2 正则化,即 \(\gamma ||w||^2\) ,来增加非零权重的数量。

例如:

ef = EfficientFrontier(mu, S)
ef.add_objective(objective_functions.L2_reg, gamma=2)
ef.min_volatility()
参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • gamma (float, optional) – L2 regularisation parameter, defaults to 1. Increase if you want more non-negligible weights

返回:

value of the objective function OR objective function expression

返回类型:

float OR cp.Expression

pypfopt.objective_functions.ex_ante_tracking_error(w, cov_matrix, benchmark_weights)[源代码]

计算事前跟踪误差的(平方),即 \((w - w_b)^T \Sigma (w-w_b)\)

参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • cov_matrix (np.ndarray) – covariance matrix

  • benchmark_weights (np.ndarray) – asset weights in the benchmark

返回:

value of the objective function OR objective function expression

返回类型:

float OR cp.Expression

pypfopt.objective_functions.ex_post_tracking_error(w, historic_returns, benchmark_returns)[源代码]

计算事后跟踪误差的(平方),即 \(Var(r - r_b)\)

参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • historic_returns (np.ndarray) – historic asset returns

  • benchmark_returns (pd.Series or np.ndarray) – historic benchmark returns

返回:

value of the objective function OR objective function expression

返回类型:

float OR cp.Expression

pypfopt.objective_functions.portfolio_return(w, expected_returns, negative=True)[源代码]

计算一个投资组合的(负)平均收益率

参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • expected_returns (np.ndarray) – expected return of each asset

  • negative (boolean) – whether quantity should be made negative (so we can minimise)

返回:

negative mean return

返回类型:

float

pypfopt.objective_functions.portfolio_variance(w, cov_matrix)[源代码]

计算投资组合的总方差(即平方波动率)。

参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • cov_matrix (np.ndarray) – covariance matrix

返回:

value of the objective function OR objective function expression

返回类型:

float OR cp.Expression

pypfopt.objective_functions.quadratic_utility(w, expected_returns, cov_matrix, risk_aversion, negative=True)[源代码]

二次方效用函数,即 \(\mu - \frac 1 2 \delta w^T \Sigma w\)

参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • expected_returns (np.ndarray) – expected return of each asset

  • cov_matrix (np.ndarray) – covariance matrix

  • risk_aversion (float) – risk aversion coefficient. Increase to reduce risk.

  • negative (boolean) – whether quantity should be made negative (so we can minimise).

返回:

value of the objective function OR objective function expression

返回类型:

float OR cp.Expression

pypfopt.objective_functions.sharpe_ratio(w, expected_returns, cov_matrix, risk_free_rate=0.02, negative=True)[源代码]

计算一个投资组合的(负)夏普比率

参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • expected_returns (np.ndarray) – expected return of each asset

  • cov_matrix (np.ndarray) – covariance matrix

  • 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.

  • negative (boolean) – whether quantity should be made negative (so we can minimise)

返回:

(negative) Sharpe ratio

返回类型:

float

pypfopt.objective_functions.transaction_cost(w, w_prev, k=0.001)[源代码]

一个非常简单的交易成本模型:将所有的权重变化相加,再乘以一个给定的分数(默认为 10bps)。这模拟了经纪人的固定百分比佣金。

参数:
  • w (np.ndarray OR cp.Variable) – asset weights in the portfolio

  • w_prev (np.ndarray) – previous weights

  • k (float) – fractional cost per unit weight exchanged

返回:

value of the objective function OR objective function expression

返回类型:

float OR cp.Expression

更多关于 L2 正则化的内容

正如在用户指南中所讨论的那样,均值-方差优化经常导致许多零权重,也就是说,有效的投资组合最终并不包括大多数资产。 这是可以预期的行为,如果你的投资组合中需要一定数量的资产,这可能是不能接受的。

为了迫使均值-方差优化器产生更多非零权重,我们在所有的目标函数中加入了“小权重惩罚”,参数为 \(\gamma\) (gamma)。例如,考虑到最小方差目标,我们有:

\[\underset{w}{\text{minimise}} ~ \left\{w^T \Sigma w \right\} ~~~ \longrightarrow ~~~ \underset{w}{\text{minimise}} ~ \left\{w^T \Sigma w + \gamma w^T w \right\}\]

请注意, \(w^T w\) 与权重平方之和相同(我没有明确写出这一点,以减少因 \(\Sigma\) 同时表示协方差矩阵和求和运算符而造成的混淆)。 这个项减少了零权重的数量,因为当所有权重平均分配时,它有一个最小值,而在整个投资组合分配给一种资产的极限情况下,它有最大值。 我把它称为 L2 正则化,因为它与机器学习中的 L2 正则化项的形式完全相同,尽管目的略有不同(在机器学习中,它被用来保持小的权重,而在这里,它被用来使它们更大)。

备注

在实践中, \(\gamma\) 必须进行调整以达到你想要的正则化水平。 然而,如果资产很少(少于 20 个资产),那么 gamma=1 是一个很好的起点。 对于更大的资产范围,或者如果你想在最终的投资组合中获得更多非零权重,那么就增加 gamma

参考文献