均值方差优化进阶:用Python教你最大化夏普比率模型

在投资组合管理中,均值‑方差优化是最经典的工具之一。
上一讲讲了「最小化波动率」,得到了一个相对保守的组合。
今天这篇往前走一步:追求「风险调整后收益最大化」,也就是从“让组合更稳”变成“让组合更划算”。 本文依然以中证港股通科技(931573)、中证绿色电力(931897)等五只指数为实战标的,结合Python代码,完整演示如何通过权重边界引导优化器朝着你想要的风格收敛。

从最小化波动率到最大化夏普

夏普比率 =(组合收益–无风险利率)/ 组合波动率。它衡量的是每承受一单位波动,能换来多少超额回报。
换句话说:一只夏普为0.5的组合,风险调整后收益比不上夏普为0.8的组合——哪怕后者波动更大。
这正是均值方差框架的进阶用法:不在“收益”和“风险”之间二选一,而是在二者之间找平衡点。
而max_sharpe函数内部的目标函数neg_sharpe_ratio正是将夏普取负,配合scipy.optimize.minimize的SLSQP算法进行求解。这种基于梯度的优化方法天然支持复杂的权重边界和等式约束,能够自动将“最大化夏普”转化为标准的凸优化问题。

代码实现分步拆解

整个优化流程分为五个关键步骤:

Step 1:准备收益率数据

  • 读取Excel文件,日期列作为索引
  • returns Dataframe:每列=一个资产,每行=一个交易日

Step 2:估计参数

  • returns_method:预期收益率估计方法,支持算术平均、指数加权、几何平均等
  • covariance_method:协方差矩阵估计方法,支持样本协方差、Ledoit‑Wolf收缩等
  • 年化因子自动推断(日数据252,周数据52,月数据12)

Step 3:设置权重边界

  • weight_bounds:为每个资产单独设定(min, max)
  • 无特殊要求时,允许用max_weight/min_weight设定统一边界
  • 必须保证下限总和≤1且上限总和≥1,否则优化不可行

Step 4:定义目标函数与约束

  • 目标函数neg_sharpe_ratio(w) → 最大化夏普
  • 权重和约束:Σw_i = 1

Step 5:执行优化并提取结果

  • SLSQP方法在满足边界和等式约束的前提下迭代求解
  • 返回最优权重、预期年化收益、波动率和夏普比率
  • 代码中嵌入了回调函数callback_func,每次迭代会动态打印当前夏普值,方便观察收敛过程。

工作流程图

流程图

实战示例:五只指数的配置

选取了5只代表性指数:

指数 风格
中证绿色电力 (931897) 低波动、防御性强
中证港股通科技 (931573) 高波动、高成长
中证红利低波动 (H30269) 低波动、稳定分红
中证小盘500 (000905) 中小盘成长
上证科创板50 (000688) 高波动、高弹性

下面通过两种不同权重的边界设置,演示如何让优化器给出不同类型的组合。

场景一:追求稳定收益(低风险)

  • 边界策略:提高绿色电力和红利低波的上下限
  • 压缩科技、小盘、科创的上限
  • 确保低波动资产合计至少占40%。
stable_bounds = [
    (0.10, 0.20),   # 绿色电力: 10% ~ 20%
    (0.05, 0.10),   # 港股通科技: 5% ~ 10%
    (0.30, 0.50),   # 红利低波: 30% ~ 50%
    (0.05, 0.15),   # 小盘500: 5% ~ 15%
    (0.05, 0.15)    # 科创50: 5% ~ 15%
]

场景二:追求高额收益(高风险)

  • 边界策略:大幅提高科技、小盘、科创的上限(最高40%)
  • 降低绿色电力和红利低波的下限至0
  • 允许集中配置高风险资产。
high_return_bounds = [
    (0.00, 0.10),   # 绿色电力: 0% ~ 10%
    (0.15, 0.40),   # 港股通科技: 15% ~ 40%
    (0.00, 0.30),   # 红利低波: 0% ~ 30%
    (0.10, 0.40),   # 小盘500: 10% ~ 40%
    (0.10, 0.40)    # 科创50: 10% ~ 40%
]

如何自己动手尝试

Step 1:准备数据

把各个指数的日度涨跌幅整理成Excel文件,列名为“日期Date”和“涨跌幅(%)Change(%)”。也可以直接用pandas-datareader在线获取。

Step 2:调整边界

根据你对每个资产的判断,修改weight_bounds列表。务必确保下限总和≤1且上限总和≥1,否则优化会因不可行而失败。

Step 3:运行优化

调用max_sharpe,选择你需要的returns_method和covariance_method。

Step 4:分析结果

查看输出中的权重分布和组合收益/波动率/夏普,判断是否符合预期。

完整代码

import pandas as pd
import numpy as np
from scipy.optimize import minimize
from typing import List, Tuple, Optional, Dict, Any


def max_sharpe(
        returns: pd.DataFrame,
        allow_short: bool = False,
        max_weight: Optional[float] = None,
        min_weight: Optional[float] = None,
        weight_bounds: Optional[List[Tuple[float, float]]] = None,
        risk_free_rate: float = 0.03,
        sample_length: Optional[int] = None,
        returns_method: str = 'mean',
        covariance_method: str = 'sample',
        **estimation_params: Any
) -> Dict[str, Any]:
    """
    最大化夏普比率的均值方差优化(函数名保留为 minimize_volatility 以兼容原有代码)

    Parameters
    ----------
    returns : pd.DataFrame
        资产历史收益率数据,每列为一个资产,每行为一个时间点
    allow_short : bool, default=False
        是否允许卖空(负权重),仅在未提供 weight_bounds 时生效
    max_weight : float, optional
        单资产最大权重(统一上限),仅在未提供 weight_bounds 时生效
    min_weight : float, optional
        单资产最小权重(统一下限),仅在未提供 weight_bounds 时生效
    weight_bounds : List[Tuple[float, float]], optional
        每个资产的独立权重边界,形如 [(min1, max1), (min2, max2), ...]
        长度必须与资产数量一致。提供此参数时将忽略 allow_short/max_weight/min_weight
    risk_free_rate : float, default=0.03
        无风险利率(年化),用于计算夏普比率
    sample_length : int, optional
        使用最近多少期数据进行估计,None 表示全部数据
    returns_method : str, default='mean'
        预期收益率估计方法:
        'mean', 'median', 'exponential', 'half_life', 'simple_moving_average', 'geometric'
    covariance_method : str, default='sample'
        协方差矩阵估计方法:
        'sample', 'exponential', 'half_life', 'simple_moving_average', 'ledoit_wolf'
    **estimation_params : dict
        其他估计参数,如 span=60, halflife=60, window=60 等

    Returns
    -------
    dict
        包含以下键值:
        - weights : pd.Series,最优权重(索引为资产名)
        - expected_return : float,预期年化收益率
        - volatility : float,年化波动率(标准差)
        - sharpe_ratio : float,夏普比率
        - success : bool,优化是否成功
    """
    # ===================== 1. 数据采样 =====================
    if sample_length is not None:
        returns = returns.iloc[-sample_length:]

    # ===================== 2. 估计预期收益率 =====================
    if returns_method == 'mean':
        expected_returns = returns.mean()
    elif returns_method == 'median':
        expected_returns = returns.median()
    elif returns_method == 'exponential':
        span = estimation_params.get('span', 60)
        expected_returns = returns.ewm(span=span).mean().iloc[-1]
    elif returns_method == 'half_life':
        halflife = estimation_params.get('halflife', 60)
        expected_returns = returns.ewm(halflife=halflife).mean().iloc[-1]
    elif returns_method == 'simple_moving_average':
        window = estimation_params.get('window', 60)
        expected_returns = returns.rolling(window=window).mean().iloc[-1]
    elif returns_method == 'geometric':
        expected_returns = (1 + returns).prod() ** (1 / len(returns)) - 1
    else:
        raise ValueError(f"未知的收益率估计方法: {returns_method}")

    # ===================== 3. 估计协方差矩阵 =====================
    n_assets = len(returns.columns)
    if covariance_method == 'sample':
        cov_matrix = returns.cov()
    elif covariance_method == 'exponential':
        span = estimation_params.get('span', 60)
        ewm_cov = returns.ewm(span=span).cov()
        cov_matrix = ewm_cov.iloc[-n_assets:]
        if isinstance(cov_matrix.index, pd.MultiIndex):
            cov_matrix = cov_matrix.reset_index(level=0, drop=True)
    elif covariance_method == 'half_life':
        halflife = estimation_params.get('halflife', 60)
        ewm_cov = returns.ewm(halflife=halflife).cov()
        cov_matrix = ewm_cov.iloc[-n_assets:]
        if isinstance(cov_matrix.index, pd.MultiIndex):
            cov_matrix = cov_matrix.reset_index(level=0, drop=True)
    elif covariance_method == 'simple_moving_average':
        window = estimation_params.get('window', 60)
        rolling_cov = returns.rolling(window=window).cov()
        cov_matrix = rolling_cov.iloc[-n_assets:]
        if isinstance(cov_matrix.index, pd.MultiIndex):
            cov_matrix = cov_matrix.reset_index(level=0, drop=True)
    elif covariance_method == 'ledoit_wolf':
        sample_cov = returns.cov()
        n, p = len(returns), n_assets
        mu = np.trace(sample_cov) / p
        shrinkage = max(0, min(1, (1 - 2 / p) * np.sum(np.diag(sample_cov) ** 2) / np.sum(sample_cov ** 2)))
        shrunk_cov = shrinkage * mu * np.eye(p) + (1 - shrinkage) * sample_cov
        cov_matrix = pd.DataFrame(shrunk_cov, index=sample_cov.index, columns=sample_cov.columns)
    else:
        raise ValueError(f"未知的协方差估计方法: {covariance_method}")

    # ===================== 4. 年化处理 =====================
    n_periods = len(returns)
    if n_periods > 252:
        ann_factor = 252
    elif n_periods > 52:
        ann_factor = 52
    else:
        ann_factor = 12

    expected_returns_ann = expected_returns * ann_factor
    cov_matrix_ann = cov_matrix * ann_factor

    asset_names = returns.columns.tolist()
    n_assets = len(asset_names)

    # ===================== 5. 设置权重边界 =====================
    if weight_bounds is not None:
        if len(weight_bounds) != n_assets:
            raise ValueError(f"weight_bounds 长度 ({len(weight_bounds)}) 与资产数量 ({n_assets}) 不一致")
        bounds = list(weight_bounds)
    else:
        if allow_short:
            bounds = [(-1.0, 1.0) for _ in range(n_assets)]
        else:
            bounds = [(0.0, 1.0) for _ in range(n_assets)]

        if max_weight is not None:
            lower = -1.0 if allow_short else 0.0
            bounds = [(max(lower, b[0]), min(b[1], max_weight)) for b in bounds]
        if min_weight is not None:
            bounds = [(max(b[0], min_weight), b[1]) for b in bounds]

    # ===================== 6. 约束条件 =====================
    constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1.0}]

    # ===================== 7. 目标函数(最大化夏普) =====================
    def neg_sharpe_ratio(w):
        portfolio_return = np.dot(w, expected_returns_ann)
        portfolio_volatility = np.sqrt(np.dot(w.T, np.dot(cov_matrix_ann, w)))
        return -(portfolio_return - risk_free_rate) / portfolio_volatility

    # 回调函数(简洁版)
    def callback_func(xk):
        callback_func.iter_count += 1
        current_sharpe = -neg_sharpe_ratio(xk)
        print(f"  Iter {callback_func.iter_count:3d} | 夏普比率 = {current_sharpe:.6f}", end="\r")

    callback_func.iter_count = 0

    # ===================== 8. 执行优化 =====================
    initial_guess = np.array([1.0 / n_assets] * n_assets)
    result = minimize(
        neg_sharpe_ratio,
        initial_guess,
        method='SLSQP',
        bounds=bounds,
        constraints=constraints,
        tol=1e-6,
        callback=callback_func
    )

    if not result.success:
        raise ValueError(f"优化失败: {result.message}")

    # ===================== 9. 计算结果指标 =====================
    weights = result.x
    portfolio_return = np.dot(weights, expected_returns_ann)
    portfolio_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix_ann, weights)))
    sharpe_ratio = (portfolio_return - risk_free_rate) / portfolio_vol

    # 打印参数估计信息(可选,不影响结果)
    print("\n\n【参数估计】")
    print(f"  数据样本数: {n_periods} 个交易日")
    print(f"  年化因子: {ann_factor}")
    print(f"  预期年化收益率:\n{expected_returns_ann.to_string()}")
    print(f"  资产名称: {asset_names} (共 {n_assets} 个)")
    print(f"  权重边界: {bounds}")

    return {
        'weights': pd.Series(weights, index=asset_names),
        'expected_return': portfolio_return,
        'volatility': portfolio_vol,
        'sharpe_ratio': sharpe_ratio,
        'success': True
    }


# ----------------------------- 辅助打印函数 -----------------------------
def print_section(title: str, char: str = "=", width: int = 70):
    """打印带边框的分节标题"""
    print("\n" + char * width)
    print(f"{title.center(width)}")
    print(char * width)


def print_result(result: Dict[str, Any], title: str = "优化结果"):
    """统一打印优化结果"""
    print_section(title)
    # 权重显示为百分比
    weights_pct = result['weights'] * 100
    print("\n【最优权重配置】")
    # 使用 pandas 格式化输出,不显示索引名称
    print(weights_pct.to_string(float_format="%.1f%%", header=False))

    print("\n【组合绩效指标】")
    print(f"  预期年化收益:   {result['expected_return']:>8.2%}")
    print(f"  年化波动率:     {result['volatility']:>8.2%}")
    print(f"  夏普比率:       {result['sharpe_ratio']:>8.3f}")
    print("\n" + "=" * 70 + "\n")


# ----------------------------- 主程序 -----------------------------
if __name__ == "__main__":
    # 示例1 & 示例2 使用单文件数据
    returns = pd.read_excel("收益率数据.xlsx", index_col=0, parse_dates=True)

    # 示例1:独立边界
    indiv_bounds = [
        (0.05, 0.6),  # 指数A
        (0.10, 0.6),  # 指数B
        (0.00, 0.3),  # 指数C
        (0.00, 0.3)   # 指数D
    ]
    result1 = max_sharpe(returns=returns, weight_bounds=indiv_bounds)
    print_result(result1, "示例1:每个资产独立边界")

    # 示例2:统一边界
    result2 = max_sharpe(returns=returns, max_weight=0.7, min_weight=0.0)
    print_result(result2, "示例2:所有资产统一边界 (最大70%)")

    # 示例3:多指数合并与两种策略
    print_section("实操:五只指数组合优化", "=", 70)
    series_list = []
    index_names = ["931897.xlsx", "931573.xlsx", "H30269.xlsx", "000905.xlsx", "000688.xlsx"]
    # 对应中文名称
    asset_cn = ["绿色电力", "港股通科技", "红利低波", "小盘500", "科创50"]

    for idx_name, cn_name in zip(index_names, asset_cn):
        df = pd.read_excel(f"数据/{idx_name}")
        df_sub = df[['日期Date', '涨跌幅(%)Change(%)']].copy()
        df_sub.set_index('日期Date', inplace=True)
        df_sub.columns = [cn_name]  # 使用中文列名
        series_list.append(df_sub)

    df_index = pd.concat(series_list, axis=1, join='outer')
    print("\n合并后的数据形状:", df_index.shape)
    print("前5行数据预览:\n", df_index.head())

    # 稳定收益边界
    stable_bounds = [
        (0.10, 0.20),   # 绿色电力
        (0.05, 0.10),   # 港股通科技
        (0.30, 0.50),   # 红利低波
        (0.05, 0.15),   # 小盘500
        (0.05, 0.15)    # 科创50
    ]
    result3 = max_sharpe(returns=df_index, weight_bounds=stable_bounds)
    print_result(result3, "追求稳定收益(低风险、低波动)")

    # 高额收益边界
    high_return_bounds = [
        (0.00, 0.10),   # 绿色电力
        (0.15, 0.40),   # 港股通科技
        (0.00, 0.30),   # 红利低波
        (0.10, 0.40),   # 小盘500
        (0.10, 0.40)    # 科创50
    ]
    result4 = max_sharpe(returns=df_index, weight_bounds=high_return_bounds)
    print_result(result4, "追求高额收益(高风险、高回报)")

结果展示

D:\code\a-share-strategy\.venv\Scripts\python.exe D:\code\a-share-strategy\A股策略\9-金融模型\max_sharpe.py 
  Iter   5 | 夏普比率 = 1.169943

【参数估计】
  数据样本数: 3049 个交易日
  年化因子: 252
  预期年化收益率:
指数A    0.107674
指数B    0.048722
指数C    0.064697
指数D    0.027461
  资产名称: ['指数A', '指数B', '指数C', '指数D'] (共 4 个)
  权重边界: [(0.05, 0.6), (0.1, 0.6), (0.0, 0.3), (0.0, 0.3)]

======================================================================
                             示例1:每个资产独立边界                             
======================================================================

【最优权重配置】
指数A    5.0%
指数B   60.0%
指数C    5.0%
指数D   30.0%

【组合绩效指标】
  预期年化收益:      4.61%
  年化波动率:        1.38%
  夏普比率:          1.170

======================================================================

  Iter  10 | 夏普比率 = 1.486607

【参数估计】
  数据样本数: 3049 个交易日
  年化因子: 252
  预期年化收益率:
指数A    0.107674
指数B    0.048722
指数C    0.064697
指数D    0.027461
  资产名称: ['指数A', '指数B', '指数C', '指数D'] (共 4 个)
  权重边界: [(0.0, 0.7), (0.0, 0.7), (0.0, 0.7), (0.0, 0.7)]

======================================================================
                         示例2:所有资产统一边界 (最大70%)                         
======================================================================

【最优权重配置】
指数A    1.5%
指数B   70.0%
指数C    3.0%
指数D   25.5%

【组合绩效指标】
  预期年化收益:      4.47%
  年化波动率:        0.99%
  夏普比率:          1.487

======================================================================


======================================================================
                             实操:五只指数组合优化                              
======================================================================
合并后的数据形状: (1263, 5)
前5行数据预览:
           绿色电力  港股通科技  红利低波  小盘500  科创50
日期Date                                  
20210526  0.56   0.89  0.47   0.13 -0.44
20210527 -0.03   0.82  0.50   0.86  2.47
20210528 -0.77  -2.21  0.57  -0.26 -0.84
20210531  1.27   3.81  0.74   0.91  4.09
20210601 -1.07   2.09 -0.10   0.16  1.24
  Iter   2 | 夏普比率 = 0.409707

【参数估计】
  数据样本数: 1263 个交易日
  年化因子: 252
  预期年化收益率:
绿色电力      8.835633
港股通科技    -2.382843
红利低波      4.969132
小盘500     7.701737
科创50     10.050819
  资产名称: ['绿色电力', '港股通科技', '红利低波', '小盘500', '科创50'] (共 5 个)
  权重边界: [(0.1, 0.2), (0.05, 0.1), (0.3, 0.5), (0.05, 0.15), (0.05, 0.15)]

======================================================================
                           追求稳定收益(低风险、低波动)                            
======================================================================

【最优权重配置】
绿色电力    20.0%
港股通科技    5.0%
红利低波    45.0%
小盘500   15.0%
科创50    15.0%

【组合绩效指标】
  预期年化收益:    654.70%
  年化波动率:     1590.64%
  夏普比率:          0.410

======================================================================

  Iter   6 | 夏普比率 = 0.331688

【参数估计】
  数据样本数: 1263 个交易日
  年化因子: 252
  预期年化收益率:
绿色电力      8.835633
港股通科技    -2.382843
红利低波      4.969132
小盘500     7.701737
科创50     10.050819
  资产名称: ['绿色电力', '港股通科技', '红利低波', '小盘500', '科创50'] (共 5 个)
  权重边界: [(0.0, 0.1), (0.15, 0.4), (0.0, 0.3), (0.1, 0.4), (0.1, 0.4)]

======================================================================
                           追求高额收益(高风险、高回报)                            
======================================================================

【最优权重配置】
绿色电力    10.0%
港股通科技   15.0%
红利低波    30.0%
小盘500   10.0%
科创50    35.0%

【组合绩效指标】
  预期年化收益:    630.48%
  年化波动率:     1891.79%
  夏普比率:          0.332

======================================================================

小结

均值‑方差优化的核心不只是数学公式,更是你对市场风格的判断如何落到权重边界里。
本文通过最大夏普目标函数+灵活边界的方式,更自然地表达“稳定”或“进取”的主观判断。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容