第四节:均值-方差优化——马科维茨模型回顾、有效前沿计算、最大夏普比率组合调仓

均值-方差优化,说白了就是「在风险和收益之间找平衡」。我刚开始做量化那会儿,觉得这玩意儿太理论了,直到有一次实盘回撤超过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]
我的小技巧:计算有效前沿时,别从0收益开始。先算全局最小方差组合(GMV),然后从那里往上扫。这样前沿更平滑,也更容易找到合理的起点。

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
避坑指南:我曾经直接用过去3年的数据算协方差矩阵,结果调仓后组合波动率比预期高了30%。后来发现,市场结构变了,历史协方差根本不能代表未来。建议用滚动窗口+指数加权协方差,或者干脆用半衰期衰减。

4.4 调仓实战中的三个关键问题

  1. 参数估计误差——预期收益和协方差矩阵都是估计值,误差会放大到权重上。我习惯用「收缩估计」或「贝叶斯先验」来平滑。
  2. 调仓频率——理论上每天调最好,但交易成本会吃掉收益。我个人建议:月度调仓+阈值触发(偏离超过5%才调)。
  3. 约束条件——实际中不能完全按理论来。比如单只基金持仓不能超过20%,行业集中度不能太高。这些约束会让有效前沿「缩水」。

4.5 知识体系总览

下面这张图,是我自己梳理的均值-方差优化核心逻辑。你看一眼,基本就能把握住整个章节的脉络:

均值-方差优化知识体系 输入数据 历史收益率 | 协方差矩阵 马科维茨模型 min w^T Σ w | s.t. w^T μ = μ_target 有效前沿 风险-收益最优组合 参数估计方法 历史法 | 收缩估计 | 贝叶斯 优化求解 SCIPY | CVXOPT 调仓应用 最大夏普 | 最小方差 | 目标风险 实战要点与避坑指南 • 协方差矩阵估计:样本量不足时用收缩估计 • 调仓频率:月度调仓 + 阈值触发(偏离5%) • 约束条件:单只基金上限20%,行业集中度控制 • 最大夏普比率对输入参数敏感,建议做敏感性分析

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