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%,这样结果更稳健。
  • 再平衡频率: 别天天调权重,交易成本会吃掉收益。我个人习惯月度或季度再平衡。
一个小技巧: 如果你发现优化出来的权重全是极端值(比如0或1),试试用 L2正则化 或者 Black-Litterman模型 来平滑权重。我在做FOF组合时经常用这招。

2.8 本章小结

均值方差模型虽然经典,但用起来要小心。它假设收益服从正态分布,但实际市场有肥尾效应;它假设协方差矩阵稳定,但市场风格切换时协方差会变。不过作为入门模型,它给了我们一个很好的分析框架。

代码层面,记住几个关键点:scipy.optimize.minimize 是核心工具,约束条件要写对,边界条件要设好。有效前沿的计算本质上是多次优化,而最大夏普比率组合是那个「最优中的最优」。

下一章咱们聊聊怎么用蒙特卡洛模拟来验证这些结果的稳定性。嗯,先到这吧。


专注资料整理