均值方差优化
一般来说,数学优化是一个非常困难的问题,特别是当我们处理复杂的目标和约束条件时。 然而,凸式优化问题是一类被深入研究过的问题,正好被金融所用。凸问题有以下形式:
其中 \(\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
- inttickers
- str listbounds
- float tuple 或 (float tuple) listcov_matrix
- np.ndarrayexpected_returns
- np.ndarraysolver
- strsolver_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 arrayTypeError – 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 floatValueError – 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 floatValueError – 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_returns
或 cov_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
)。例如,考虑到最小方差目标,我们有:
请注意, \(w^T w\) 与权重平方之和相同(我没有明确写出这一点,以减少因 \(\Sigma\) 同时表示协方差矩阵和求和运算符而造成的混淆)。 这个项减少了零权重的数量,因为当所有权重平均分配时,它有一个最小值,而在整个投资组合分配给一种资产的极限情况下,它有最大值。 我把它称为 L2 正则化,因为它与机器学习中的 L2 正则化项的形式完全相同,尽管目的略有不同(在机器学习中,它被用来保持小的权重,而在这里,它被用来使它们更大)。
备注
在实践中, \(\gamma\) 必须进行调整以达到你想要的正则化水平。
然而,如果资产很少(少于 20 个资产),那么 gamma=1
是一个很好的起点。
对于更大的资产范围,或者如果你想在最终的投资组合中获得更多非零权重,那么就增加 gamma
。