2、均值方差模型实战:Python实现均值方差优化、计算有效前沿、寻找最优权重
好了,咱们直接进入正题。
上一章我们把均值方差的理论框架搭好了,什么期望收益、协方差矩阵、有效前沿,听着挺唬人。但说实话,理论归理论,真正上手跑代码的时候,坑多得很。我当年第一次实现这个模型,跑出来的权重全是负数,差点以为公式抄错了。
这一章,咱们就用 Python 把均值方差模型完整撸一遍。从数据准备到优化求解,再到画出那条漂亮的有效前沿曲线,最后找到那个传说中的「最优权重」。
2.1 准备工作:数据与工具
先说说工具。我个人习惯用 numpy 做矩阵运算,pandas 处理数据,scipy.optimize 做优化求解。画图用 matplotlib,够用了。
数据方面,我选了几只常见的 ETF 作为示例:沪深300(000300.SH)、中证500(000905.SH)、创业板指(399006.SZ)、国债指数(000012.SH)。你想想看,这四类资产基本覆盖了股票和债券,做配置练习再合适不过。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import minimize
# 模拟收益率数据(实际项目中从wind或tushare获取)
np.random.seed(42)
assets = ['沪深300', '中证500', '创业板', '国债']
n_assets = len(assets)
n_obs = 252 # 一年交易日
# 生成模拟收益率
returns = pd.DataFrame(
np.random.randn(n_obs, n_assets) * 0.01 + [0.0005, 0.0006, 0.0008, 0.0002],
columns=assets
)
# 计算年化收益率和协方差矩阵
mean_returns = returns.mean() * 252
cov_matrix = returns.cov() * 252
print("年化收益率:")
print(mean_returns)
print("\n年化协方差矩阵:")
print(cov_matrix)
2.2 均值方差优化的数学表达
说白了,均值方差优化就是在给定预期收益水平下,最小化组合方差。或者反过来,在给定风险水平下,最大化收益。
数学上长这样:
目标函数:min w^T Σ w
约束条件:w^T μ = μ_target
sum(w) = 1
0 ≤ w_i ≤ 1 (可选,不加就是允许做空)
其中 w 是权重向量,Σ 是协方差矩阵,μ 是预期收益向量。
嗯,这里要注意:如果不加权重上下界约束,优化结果很可能出现极端值。我曾经在实盘回测中遇到过,优化出来的权重是 [2.3, -1.1, 0.8, -1.0],你敢信?这在实际交易中根本没法执行。
2.3 代码实现:求解最优权重
咱们先写一个函数,给定目标收益,返回最小方差组合的权重。
def min_variance_portfolio(target_return, mean_ret, cov_mat, allow_short=True):
n = len(mean_ret)
# 目标函数:组合方差
def portfolio_variance(weights):
return weights.T @ cov_mat @ weights
# 约束条件
constraints = [
{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}, # 权重和为1
{'type': 'eq', 'fun': lambda w: w @ mean_ret - target_return} # 目标收益
]
# 边界条件
if allow_short:
bounds = tuple([(-1, 1) for _ in range(n)])
else:
bounds = tuple([(0, 1) for _ in range(n)])
# 初始权重(等权)
init_guess = np.array([1/n] * n)
# 求解
result = minimize(portfolio_variance, init_guess,
method='SLSQP', bounds=bounds,
constraints=constraints)
return result.x
# 测试:求目标收益为15%的最小方差组合
target = 0.15
weights = min_variance_portfolio(target, mean_returns, cov_matrix, allow_short=False)
print(f"目标收益 {target:.2%} 的最优权重:")
for asset, w in zip(assets, weights):
print(f"{asset}: {w:.4f}")
print(f"组合预期收益:{weights @ mean_returns:.4f}")
print(f"组合方差:{weights.T @ cov_matrix @ weights:.6f}")
method='SLSQP',结果默认用了 L-BFGS-B,这个算法不支持等式约束,直接报错。所以记得指定优化方法。
2.4 计算有效前沿
有效前沿就是所有「给定风险下收益最高」或「给定收益下风险最低」的组合的集合。说白了,就是那条最优的曲线。
计算思路很简单:在最小收益和最大收益之间均匀取点,对每个目标收益求解一次优化问题。
def efficient_frontier(mean_ret, cov_mat, n_points=50, allow_short=True):
n = len(mean_ret)
# 计算最小和最大收益组合
# 最小收益:全局最小方差组合
def min_var_no_target(weights):
return weights.T @ cov_mat @ weights
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = tuple([(0, 1) for _ in range(n)]) if not allow_short else None
result_min = minimize(min_var_no_target, np.array([1/n]*n),
method='SLSQP', bounds=bounds,
constraints=constraints)
min_ret = result_min.x @ mean_ret
# 最大收益:全仓收益最高的资产
max_ret = mean_ret.max()
# 在区间内均匀取点
target_returns = np.linspace(min_ret, max_ret, n_points)
frontier_vols = []
frontier_weights = []
for target in target_returns:
w = min_variance_portfolio(target, mean_ret, cov_mat, allow_short)
vol = np.sqrt(w.T @ cov_mat @ w)
frontier_vols.append(vol)
frontier_weights.append(w)
return target_returns, frontier_vols, frontier_weights
# 计算有效前沿
target_rets, vols, weights_list = efficient_frontier(mean_returns, cov_matrix, n_points=30)
# 绘制有效前沿
plt.figure(figsize=(10, 6))
plt.plot(vols, target_rets, 'b-', linewidth=2, label='有效前沿')
plt.xlabel('风险(标准差)')
plt.ylabel('预期收益')
plt.title('均值方差模型有效前沿')
plt.grid(True, alpha=0.3)
plt.legend()
plt.show()
2.5 寻找最优权重:最大化夏普比率
有效前沿上的点都是「有效」的,但哪个最好?这就要看你的风险偏好了。不过有一个客观标准:夏普比率最大的组合。
夏普比率 = (组合收益 - 无风险利率) / 组合波动率。说白了,就是每承担一单位风险,能多拿多少超额收益。
def max_sharpe_portfolio(mean_ret, cov_mat, risk_free_rate=0.03, allow_short=True):
n = len(mean_ret)
def neg_sharpe(weights):
port_ret = weights @ mean_ret
port_vol = np.sqrt(weights.T @ cov_mat @ weights)
return -(port_ret - risk_free_rate) / port_vol
constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
bounds = tuple([(0, 1) for _ in range(n)]) if not allow_short else None
result = minimize(neg_sharpe, np.array([1/n]*n),
method='SLSQP', bounds=bounds,
constraints=constraints)
return result.x
# 求解最大夏普组合
sharpe_weights = max_sharpe_portfolio(mean_returns, cov_matrix, risk_free_rate=0.03)
sharpe_ret = sharpe_weights @ mean_returns
sharpe_vol = np.sqrt(sharpe_weights.T @ cov_matrix @ sharpe_weights)
sharpe_ratio = (sharpe_ret - 0.03) / sharpe_vol
print("最大夏普比率组合权重:")
for asset, w in zip(assets, sharpe_weights):
print(f"{asset}: {w:.4f}")
print(f"组合收益:{sharpe_ret:.4f}")
print(f"组合波动率:{sharpe_vol:.4f}")
print(f"夏普比率:{sharpe_ratio:.4f}")
2.6 可视化:有效前沿与最优组合
光看数字不过瘾,咱们把有效前沿、最大夏普组合、全局最小方差组合都画在一张图上。
# 全局最小方差组合
def global_min_variance(cov_mat):
n = cov_mat.shape[0]
ones = np.ones(n)
inv_cov = np.linalg.inv(cov_mat)
w = inv_cov @ ones / (ones.T @ inv_cov @ ones)
return w
gmv_weights = global_min_variance(cov_matrix)
gmv_ret = gmv_weights @ mean_returns
gmv_vol = np.sqrt(gmv_weights.T @ cov_matrix @ gmv_weights)
# 绘图
plt.figure(figsize=(12, 7))
plt.plot(vols, target_rets, 'b-', linewidth=2, label='有效前沿')
plt.scatter(gmv_vol, gmv_ret, c='green', s=100, marker='o', label='全局最小方差组合')
plt.scatter(sharpe_vol, sharpe_ret, c='red', s=100, marker='*', label='最大夏普比率组合')
# 标注权重
for i, (vol, ret, w) in enumerate(zip(vols[::5], target_rets[::5], weights_list[::5])):
if i % 2 == 0:
plt.annotate(f'w={np.round(w, 2)}', (vol, ret), fontsize=8, alpha=0.6)
plt.xlabel('风险(标准差)')
plt.ylabel('预期收益')
plt.title('均值方差模型:有效前沿与最优组合')
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
2.7 实战中的坑与经验
代码跑通了,但别高兴太早。我在实际项目中遇到过几个大坑,分享给你:
- 输入敏感性问题: 均值方差模型对预期收益的估计极其敏感。你稍微改一下收益数据,权重就天翻地覆。我建议用多期滚动窗口来估计,别只用一期数据。
- 协方差矩阵的稳定性: 当资产数量多的时候,协方差矩阵可能接近奇异,导致优化失败。可以用 shrinkage 方法或者 PCA 降维来处理。
- 权重约束的重要性: 不加约束的优化结果往往不可执行。我一般会加个上下界,比如单只股票不超过20%,这样结果更稳健。
- 再平衡频率: 别天天调权重,交易成本会吃掉收益。我个人习惯月度或季度再平衡。
L2正则化 或者 Black-Litterman模型 来平滑权重。我在做FOF组合时经常用这招。
2.8 本章小结
均值方差模型虽然经典,但用起来要小心。它假设收益服从正态分布,但实际市场有肥尾效应;它假设协方差矩阵稳定,但市场风格切换时协方差会变。不过作为入门模型,它给了我们一个很好的分析框架。
代码层面,记住几个关键点:scipy.optimize.minimize 是核心工具,约束条件要写对,边界条件要设好。有效前沿的计算本质上是多次优化,而最大夏普比率组合是那个「最优中的最优」。
下一章咱们聊聊怎么用蒙特卡洛模拟来验证这些结果的稳定性。嗯,先到这吧。