在投资组合管理中,均值‑方差优化是最经典的工具之一。
上一讲讲了「最小化波动率」,得到了一个相对保守的组合。
今天这篇往前走一步:追求「风险调整后收益最大化」,也就是从“让组合更稳”变成“让组合更划算”。 本文依然以中证港股通科技(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
======================================================================
小结
均值‑方差优化的核心不只是数学公式,更是你对市场风格的判断如何落到权重边界里。
本文通过最大夏普目标函数+灵活边界的方式,更自然地表达“稳定”或“进取”的主观判断。