第四节:均值-方差优化——马科维茨模型回顾、有效前沿计算、最大夏普比率组合调仓
均值-方差优化,说白了就是「在风险和收益之间找平衡」。我刚开始做量化那会儿,觉得这玩意儿太理论了,直到有一次实盘回撤超过20%,才老老实实回来啃马科维茨。嗯,真香。
4.1 马科维茨模型核心回顾
马科维茨在1952年提出的这个框架,核心就一句话:给定预期收益水平,最小化组合方差;或者给定风险容忍度,最大化预期收益。你想想看,这不就是我们每天纠结的事吗?
数学上,我们要解这个优化问题:
min w^T Σ w
s.t. w^T μ = μ_target
sum(w) = 1
w_i ≥ 0 (可选,不加就是允许做空)
其中 w 是权重向量,Σ 是协方差矩阵,μ 是预期收益向量。我个人习惯用 scipy.optimize 来解,比手动推导拉格朗日乘子方便多了。
- 收益服从正态分布——实际中尾部风险经常被低估
- 投资者是风险厌恶的——但不同人的厌恶程度不一样
- 用历史数据估计参数——我踩过最大的坑就在这
4.2 有效前沿计算——从理论到代码
有效前沿就是所有「最优组合」的集合。怎么算?说白了就是不断改变目标收益 μ_target,重复求解上面的优化问题。
我在项目中遇到过一个问题:直接用历史协方差矩阵,结果前沿形状特别奇怪。后来发现是样本量不够,协方差矩阵估计不稳定。解决办法?用收缩估计(Shrinkage Estimator)或者直接上贝叶斯方法。
下面是我常用的计算代码:
import numpy as np
import pandas as pd
from scipy.optimize import minimize
def efficient_frontier(returns, target_ret):
"""
计算给定目标收益下的最小方差组合
returns: DataFrame, 各资产历史收益率
target_ret: float, 目标年化收益
"""
mu = returns.mean() * 252 # 年化
Sigma = returns.cov() * 252
n = len(mu)
w0 = np.ones(n) / n # 初始等权
# 约束条件
constraints = [
{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}, # 权重和为1
{'type': 'eq', 'fun': lambda w: w @ mu - target_ret} # 目标收益
]
bounds = [(0, 1)] * n # 不允许做空
res = minimize(lambda w: w @ Sigma @ w, w0,
method='SLSQP', bounds=bounds, constraints=constraints)
return res.x
# 生成有效前沿
target_rets = np.linspace(0.05, 0.25, 50)
frontier_weights = [efficient_frontier(returns, r) for r in target_rets]
4.3 最大夏普比率组合——调仓的终极目标?
最大夏普比率组合,就是有效前沿上那个「性价比最高」的点。数学上,它对应着从原点(无风险利率)到有效前沿的切线。
为什么要用它调仓?因为理论上,这是风险调整后收益最高的组合。但注意,我说的是「理论上」。
我曾经用最大夏普比率组合做调仓,结果发现每次调仓后权重变化特别大。为什么?因为夏普比率对输入参数极其敏感。你想想看,预期收益差0.5%,最优权重可能就差20%。
计算最大夏普比率组合的代码:
def max_sharpe_ratio(returns, rf=0.03):
"""
计算最大夏普比率组合
rf: 无风险利率,默认3%
"""
mu = returns.mean() * 252
Sigma = returns.cov() * 252
n = len(mu)
def neg_sharpe(w):
port_ret = w @ mu
port_vol = np.sqrt(w @ Sigma @ w)
return -(port_ret - rf) / port_vol
w0 = np.ones(n) / n
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = [(0, 1)] * n
res = minimize(neg_sharpe, w0, method='SLSQP', bounds=bounds, constraints=constraints)
return res.x
4.4 调仓实战中的三个关键问题
- 参数估计误差——预期收益和协方差矩阵都是估计值,误差会放大到权重上。我习惯用「收缩估计」或「贝叶斯先验」来平滑。
- 调仓频率——理论上每天调最好,但交易成本会吃掉收益。我个人建议:月度调仓+阈值触发(偏离超过5%才调)。
- 约束条件——实际中不能完全按理论来。比如单只基金持仓不能超过20%,行业集中度不能太高。这些约束会让有效前沿「缩水」。
4.5 知识体系总览
下面这张图,是我自己梳理的均值-方差优化核心逻辑。你看一眼,基本就能把握住整个章节的脉络:
4.6 一个完整的调仓示例
假设我们有5只基金,过去1年的日收益率数据。我们想用最大夏普比率组合做月度调仓。完整流程如下:
import pandas as pd
import numpy as np
from scipy.optimize import minimize
# 模拟数据
np.random.seed(42)
dates = pd.date_range('2023-01-01', '2023-12-31', freq='D')
funds = ['基金A', '基金B', '基金C', '基金D', '基金E']
returns = pd.DataFrame(np.random.randn(len(dates), 5) * 0.02 + 0.001,
index=dates, columns=funds)
# 月度调仓函数
def monthly_rebalance(returns, rf=0.03):
# 按月度分组
monthly = returns.resample('M').apply(lambda x: x.mean() * 252)
weights_history = []
for month, data in monthly.iterrows():
# 用过去60个交易日估计协方差
hist = returns.loc[:month].tail(60)
mu = hist.mean() * 252
Sigma = hist.cov() * 252
# 求解最大夏普
n = len(mu)
def neg_sharpe(w):
port_ret = w @ mu
port_vol = np.sqrt(w @ Sigma @ w)
return -(port_ret - rf) / port_vol
w0 = np.ones(n) / n
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = [(0.05, 0.30)] * n # 单只基金5%-30%
res = minimize(neg_sharpe, w0, method='SLSQP', bounds=bounds, constraints=constraints)
weights_history.append(res.x)
return pd.DataFrame(weights_history, index=monthly.index, columns=funds)
weights = monthly_rebalance(returns)
print(weights.tail())
均值-方差优化是个好工具,但它不是万能的。参数估计误差、市场结构变化、交易成本——这些现实问题都会让理论打折扣。我的建议是:用它做参考,别用它做唯一决策。结合基本面判断和风险预算,效果会好很多。
公众号:蓝海资料掘金营,微信deep3321