ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python实现四种权重模型:市值加权、等权重、均值方差与最小方差

Python实现四种权重模型:市值加权、等权重、均值方差与最小方差 简介面向Python量化投资初学者与金融工程学习者压缩包围绕股票组合构建系统覆盖市值加权、等权重、均值方差与最小方差四种经典策略。其中均值方差策略侧重收益与风险的平衡最小方差策略专注风险极小化两种加权策略则为组合配置提供直观基准。压缩包内共9个文件包含4个xlsx数据与结果表、3个ipynb策略实现笔记、1个py指标脚本及1个pyc缓存文件整体仅1.18MB便于快速下载与展开学习。配套提供股票数据、收益汇总和性能对比表格以及用于指标计算的Python脚本读者可对照Notebook逐步运行掌握从数据读取、权重计算、组合构建到回测评价的完整流程复现四种策略的构建、回测和比较适合入门策略研究或课程作业参考。目前已有530人学习下载资源紧凑且实用。1. 为什么把四个权重模型放在同一套 Python 代码里做量化投资的人迟早会撞到一个问题同样一批股票用市值加权、等权重、均值方差、最小方差四种方式配出来收益和回撤完全像四个不同的产品。很多新手一上来就只盯着“哪种最赚钱”结果用均值方差优化出一组极端权重实盘两个月就被回撤打懵。我的建议是先别急着选边站把这四个模型当成同一套 Python 量化投资框架里的四个可替换模块先跑通、再对比、最后才谈偏好。市值加权是市场的默认答案等权重是学术实证里经常“吊打”复杂模型的朴素基准均值方差是经典理论而最小方差是前者的稳健补丁。这篇文章不聊理论课直接讲数据怎么准备、优化器怎么配参数、协方差矩阵怎么估计才不翻车以及回测里那些让结果虚高的坑具体长什么样。2. 数据底座收益率口径、协方差矩阵与滚动窗口2.1 用 pandas 整理行情数据复权、对齐与收益率计算四个模型共用的输入只有两个每只股票的收益率序列和协方差矩阵市值加权还需要市值快照。先解决收益率。常见做法是从本地数据库或量化数据服务商导出日线数据我一般把数据洗成一张宽表行是日期列是股票代码值是复权收盘价。注意必须用前复权或后复权否则除权除息日会出现人为跳水收益率序列里会多出几个异常尖峰。import pandas as pd import numpy as np # 假设 close 是宽表index 为交易日columns 为股票代码 close pd.read_csv(daily_close.csv, index_col0, parse_datesTrue) close close.dropna(howall).ffill() # 停牌日向前填充避免收益率变成 NaN # 简单收益率今天的价格相对昨天涨跌了多少 simple_ret close.pct_change().dropna(howall) # 对数收益率对量化建模更友好多期收益可以直接相加 log_ret np.log(close / close.shift(1)).dropna(howall) # 只保留至少有 250 个交易日收益率的股票避免新上市股票污染协方差估计 valid simple_ret.count() 250 simple_ret simple_ret.loc[:, valid] log_ret log_ret.loc[:, valid]这个预处理有两个关键决策。第一停牌日我选择用 ffill 而不是直接删除行因为删除行会把时间轴打散协方差矩阵估计时不同股票的时间点对不齐。第二组合收益的计算我用简单收益率而协方差矩阵的估计我用对数收益率。原因很实际组合净值是按资金权重累乘的简单收益率在组合层面更贴近真实资金曲线而对数收益率在统计上接近正态分布协方差估计的偏差更小。2.2 协方差矩阵与预期收益样本估计是起点也是硬伤拿到收益率序列后最直接的做法就是算样本协方差矩阵和样本均值向量。这两样东西是后面均值方差和最小方差模型的直接输入但它们的质量天差地别样本均值向量几乎不可用样本协方差矩阵则在小样本下非常脆弱。# 用最近 252 个交易日约一年的对数收益率做估计 window 252 hist_log_ret log_ret.iloc[-window:] mu hist_log_ret.mean() * window # 年化预期收益日均值 × 252 sigma hist_log_ret.cov() * window # 年化协方差矩阵日协方差 × 252 # 检查矩阵是否正定这决定了优化器能不能收敛 eigenvalues np.linalg.eigvalsh(sigma) print(f最小特征值: {eigenvalues.min():.6f})先解释年化日频收益率的标准差大约是年化的 1/√252所以协方差矩阵要乘 252预期收益要乘 252后面优化器算出的夏普比率才是年化口径。再说正定性检查这一步很多人跳过实际上一旦股票数量接近交易日数量样本协方差矩阵就会出现接近零甚至为负的特征值后面 scipy 的优化器会直接报错或者给出一组荒谬的权重。这就是我常说的“黑匣子”模型没毛病毛病出在喂进去的矩阵上。这个阶段先建立两条经验第一mu 的历史均值估计噪声极大均值方差模型后续翻车主要就翻在这里第二sigma 需要做收缩估计后面会讲 Ledoit-Wolf不能直接用原始样本矩阵。数据底座到此为止还差一个滚动窗口的框架但那个放在最后一章验证阶段再讲否则现在展开容易绕晕。3. 市值加权与等权重两个免优化基准的代码实现3.1 市值加权权重 市值占比代码与再平衡逻辑市值加权是所有指数基金默认的做法逻辑最简单市值大的公司权重高组合自动跟随市场整体。它的隐含假设是你放弃主动判断接受“市场是有效的”这个说法。实现上就两行但真正容易搞混的是市值数据的时点对齐。# 假设 mcap 是 DataFrameindex 为日期columns 为股票代码值是总市值或流通市值 # 注意权重计算必须使用调仓日当天可获得的市值不能用未来数据 mcap pd.read_csv(market_cap.csv, index_col0, parse_datesTrue) mcap mcap.reindex(columnssimple_ret.columns).ffill() # 调仓日这里用每月最后一个交易日 rebalance_dates simple_ret.resample(ME).last().index weights_cap pd.DataFrame(indexsimple_ret.index, columnssimple_ret.columns, dtypefloat) for dt in rebalance_dates: if dt not in mcap.index: continue w mcap.loc[dt].fillna(0) total w.sum() if total 0: w w / total # 从调仓日到下一个调仓日前一天权重保持不变 end_idx simple_ret.index.searchsorted(dt, sideright) start_idx simple_ret.index.searchsorted(dt, sideleft) weights_cap.iloc[start_idx:end_idx] w.values # 组合日收益 当日权重 × 当日简单收益率 port_ret_cap (weights_cap.shift(1) * simple_ret).sum(axis1).dropna()市值加权的一个隐藏细节是再平衡频率。理论上市值每天都在变但实际没人天天调仓常见做法是月度或季度再平衡。上面的代码用的是“买入并持有到下一次调仓”的逻辑权重矩阵在两次调仓日之间保持不变。另一个细节是市值数据本身也要复权处理有些数据源的市值不包含解禁股变化导致权重突变这是我踩过的坑。3.2 等权重1/n 为什么是强基准等权重的代码比市值加权还简单每只股票权重相同1 除以股票数量。但它的地位一点不低。学术上有篇被引很多的实证研究比较了 14 种优化模型和 1/n 等权重组合的样本外表现结果是等权重在夏普比率上经常不输给复杂的均值方差优化。直觉解释是等权重天然做了分散化不依赖任何参数估计也就不会有估计误差放大问题。n len(simple_ret.columns) w_equal np.ones(n) / n weights_eq pd.DataFrame( np.tile(w_equal, (len(simple_ret.index), 1)), indexsimple_ret.index, columnssimple_ret.columns, ) # 月度再平衡每月的第一天重置为等权月度内保持不变 rebalanced_ret [] for dt in rebalance_dates: seg simple_ret.loc[dt:].iloc[:21] # 近似一个月交易窗口 if seg.empty: continue port (w_equal * seg).sum(axis1) rebalanced_ret.append(port) port_ret_eq pd.concat(rebalanced_ret)这里要提醒一个容易搞错的地方组合日收益要用的权重是“上一日收盘后确定的权重”也就是代码里 shift(1) 的作用。如果不 shift你等于在用当天的收盘权重计算当天的收益而权重是收盘后才算出来的这就是典型的未来函数。等权重虽然不需要估计参数但它对单只股票的暴雷没有防护某只股票退市归零会让组合直接损失 1/n。3.3 和基准对齐口径超额收益才是有意义的指标无论后续做均值方差还是最小方差都要拿结果和这两个基准比。比起绝对收益我更建议比的是超额收益和超额夏普。对新手来说最容易犯的错误是拿优化模型的收益率曲线和市值加权比但没有对齐调仓频率和交易成本。一个公平的对比框架是所有模型都按同一个调仓频率比如月度、同一个股票池、同一段时间回测并统一扣除手续费和滑点。市值加权因为换手低天然占便宜而均值方差模型如果月度调仓换手率可能高到把 alpha 全吃光。这一章的实用产出是三个可复用函数市值加权权重生成、等权重权重生成、以及组合收益合成。后面的均值方差和最小方差模型也输出同样的格式——权重矩阵加组合收益序列。这样四个模型可以在完全相同的口径下对比谁优谁劣才有意义。4. 均值方差与最小方差用 scipy 和 sklearn 落地两个优化模型4.1 均值方差最大化夏普的目标函数与约束条件均值方差模型的经典表述是在给定收益目标下最小化方差或者在给定风险预算下最大化收益。实操中我更喜欢直接最大化夏普比率因为它把收益和风险放在一个目标函数里不需要额外猜目标收益。目标函数是负的夏普比率因为 scipy 的 minimize 只能做最小化。from scipy.optimize import minimize def neg_sharpe(w, mu, sigma, rf0.0): 负夏普比率组合收益减去无风险利率除以组合波动率 port_ret w mu - rf port_vol np.sqrt(w sigma w) return -port_ret / port_vol n_assets len(mu) init_w np.ones(n_assets) / n_assets # 用等权重做初始解 constraints [{type: eq, fun: lambda w: np.sum(w) - 1}] # 权重之和必须等于 1 bounds [(0.0, 0.3) for _ in range(n_assets)] # 单只股票权重上限 30%防止过度集中 result minimize( neg_sharpe, init_w, args(mu, sigma), methodSLSQP, boundsbounds, constraintsconstraints, options{maxiter: 1000, ftol: 1e-9}, ) if result.success: w_mv result.x else: print(f优化失败: {result.message})这段代码有几个参数值得逐一说。bounds 里的 0.3 是单票权重上限如果不加限制优化器经常给出单票 90% 的极端解这种组合回测时夏普高得吓人实盘一次踩雷就清零。ftol 是收敛容差默认值经常导致提前收敛我一般调到 1e-9让优化器多迭代几步。SLSQP 是 scipy 里最常用的带约束优化方法但它是局部优化器对初始值敏感后面避坑章节会展开讲。4.2 最小方差不赌收益只赌协方差最小方差模型和均值方差的核心区别是它彻底不碰预期收益 mu只做一件事——最小化组合方差。这在预期收益估计不可靠的场景下非常讨巧。你不需要预测哪只股票会涨只需要估计哪些股票之间相关性高、哪些能对冲然后配出一个波动率最低的组合。def portfolio_var(w, sigma): 组合方差w sigma w return w sigma w result minimize( portfolio_var, init_w, args(sigma,), methodSLSQP, bounds[(0.0, 0.2) for _ in range(n_assets)], constraints[{type: eq, fun: lambda w: np.sum(w) - 1}], options{maxiter: 1000, ftol: 1e-12}, ) w_minvar result.x最小方差组合有一个解析解w Σ⁻¹·1 / (1ᵀ·Σ⁻¹·1)。如果股票数量不大比如 50 只以内直接用 numpy 求逆算解析解更快也更稳不需要跑迭代优化器。inv_sigma np.linalg.pinv(sigma) # 用伪逆避免奇异矩阵报错 ones np.ones(n_assets) w_minvar_analytic inv_sigma ones / (ones inv_sigma ones)这里特意用 pinv 而不是 inv是有血泪教训的。样本协方差矩阵在股票数接近样本天数时几乎必然是病态的直接求逆会放大微小误差伪逆至少保证能算出一个结果。但伪逆不等于解决了问题它只是把“报错”变成了“看似合理的错误答案”真正的修正在下一章讲。4.3 均值方差 vs 最小方差一个赌预测一个赌估计这两个模型的本质差异决定了它们的适用场景完全不同。均值方差需要 mu 和 sigma 两个输入其中 mu 的估计误差比 sigma 大一个数量级。历史收益率的均值并不能预测未来这是金融计量里最稳定的结论之一。所以均值方差模型做出来常常是回测时完美、实盘时平庸。最小方差只需要 sigma。虽然协方差矩阵也不是常数但相关结构的稳定性远好于收益均值。尤其在市场风格剧烈切换的年份最小方差组合的波动率控制表现往往比均值方差稳定得多。这并不意味着最小方差“更好”而是它输错了代价更小。如果你对收益预测没有独立于历史数据的判断我一般建议优先走最小方差路线。4.4 用 sklearn 的 Ledoit-Wolf 收缩估计替代样本协方差样本协方差矩阵在股票数超过样本天数时协方差矩阵就是奇异的。即使股票数没那么多矩阵也会“病态”最小方差模型会把大量权重压在被估计误差污染的方向上。这时就该用收缩估计了。常见做法是用 sklearn 里的 Ledoit-Wolf 估计器它会把样本协方差朝一个结构化矩阵方向收缩从而降低均方误差。from sklearn.covariance import LedoitWolf lw LedoitWolf() lw.fit(log_ret.iloc[-window:]) # 同样只用最近一年数据 sigma_shrunk lw.covariance_ * window # 记得年化Ledoit-Wolf 的收缩强度由 sklearn 自动从数据里估计不需要手调参数。它会输出一个“收缩系数”存储在 lw.shrinkage_ 里我一般会打印出来看如果收缩系数大于 0.5说明样本协方差噪声非常大原始矩阵基本不可信。替代方案还有手动设置固定收缩系数进行收缩例如 sigma_shrunk 0.5 * sigma 0.5 * np.eye(n) * np.trace(sigma) / n但 Ledoit-Wolf 自动选择收缩强度在绝大多数情况下更省心。5. 回测避坑清单协方差奇异、未来函数与优化器玄学5.1 现象优化器直接报错“Singular matrix C”原因股票数量比样本天数还多或者部分股票高度相关比如同行业的两只龙头导致协方差矩阵近似奇异。scipy 在计算约束雅可比矩阵时会去求逆遇到奇异矩阵就抛异常。解决先降维把股票池从全部标的砍到流动性最好且行业分散的 50 只以内再用 Ledoit-Wolf 收缩估计替换原始样本协方差最后在代码里给矩阵求逆的位置加上 np.linalg.pinv 兜底保证流程不中断。5.2 现象权重极端集中一只票配了 90%原因优化器在无边界约束下会追逐“尖点解”尤其当样本协方差把低波动误判为零风险资产时权重会像黑洞一样吸到某只票上。这不是模型错了是输入数据的估计误差被优化器的非线性放大了。解决给 bounds 加上限单票权重上限按股票池大小设置20 只票设 0.250 只票设 0.1同时用 Ledoit-Wolf 收缩后的协方差矩阵重新优化。两招一起上权重分布会从极端走向合理。5.3 现象回测曲线完美上涨实盘完全对不上原因十有八九是用了未来数据。最常见的是用全样本区间计算 mu 和 sigma再在整个历史区间上回测或者权重计算用的市值是当月末数据但回测从月初就开始生效。这类未来函数在回测里不会报错只会让结果异常好看是最难排查的一类坑。解决所有参数估计严格只用截至调仓日的数据。用 expanding 窗口每到一个调仓日用“过去 252 天”的数据重新估计 mu 和 sigma再计算权重、应用到未来一个月。我在 5.4 里给出滚动回测的代码骨架直接照着套。设计回测时我通常还会做一个 sanity check把估计窗口随机平移 5 个交易日结果若出现显著漂移基本就是数据对齐出了问题。5.4 现象同一份数据跑两次权重结果不同原因scipy 的 SLSQP 是局部优化器目标函数又是非凸的初始值不同会收敛到不同的局部最优。你以为代码没变实际上初始值可能因为 DataFrame 列顺序变化或随机种子变化而变了。解决对优化器的初始值做敏感性测试初始化一个数组包含等权重、各只股票单独满仓、随机权重等多种起点取所有起点里目标函数值最优的解或者干脆换成解析解最小方差或用 cvxpy 这类凸优化库。这类数值稳定性问题有时很玄学我通常会把优化结果连同初始值、迭代次数一起落盘作为排错依据。5.5 现象换手率高到惊人收益全被手续费吃掉原因均值方差和最小方差模型对参数变化极敏感月度调仓时权重可能在 30% 和 0% 之间来回横跳。回测里扣不扣交易成本结论可能完全反转。解决权重约束加一条“单期换手率上限”例如组合单边换手不超过 20%或者在目标函数里加惩罚项把权重与上一期权重的绝对差乘以成本系数一并最小化。更简单的是用“调仓阈值”机制权重偏离目标权重超过 5 个百分点才实际调仓否则保持不动。这个思路来自指数调仓的实操惯例能显著压低换手却不伤太多收益。6. 滚动回测验证与四个模型的实战对比6.1 expanding 窗口回测代码骨架验证模型好不好最可靠的方法是滚动回测每个调仓日只用当时可得的数据做参数估计然后把权重应用于未来一段时间的收益记录组合净值。直接上骨架代码def backtest_sigma_only(sigma_estimator, ret, rebalance_dates, window252): port_ret [] for dt in rebalance_dates: idx ret.index.get_loc(dt) if idx window: continue hist ret.iloc[idx - window:idx] # 只用调仓日之前的数据 sigma sigma_estimator(hist) * window # 估计协方差 w min_variance_weight(sigma, bound0.15) # 复用第 4 章的最小方差优化 future ret.iloc[idx:idx 21] # 持有至下一次调仓 port_ret.append(future w) return pd.concat(port_ret)这个骨架里有两个值得注意的设计估值窗口固定为 252 个交易日我一般还会跑一个 60 天的短窗口做稳健性对比权重计算里 bound0.15 意味着股票池 30 只以内单票上限 15%这个参数要按股票池大小调整不要照抄。6.2 四模型对比表看什么指标才不会被回测骗把四个模型跑完输出一张对比表这是我每次复盘必做的动作。模型参数依赖典型年化波动最大回撤适用场景市值加权市值快照高深跟踪市场基准低换手等权重无中高中深朴素分散不依赖任何预测均值方差预期收益 协方差目标可控依赖收益预测质量有独立收益预测信号时最小方差协方差低浅风险预算优先收益预测不可靠时对比时重点看三件事最大回撤是否在你能承受的范围换手率折算成本后还剩多少超额收益以及极端年份比如风格切换剧烈的年份哪个模型最先失控。不要只看年化收益那是最容易被未来函数和美化的数字。6.3 一个提高可信度的小技巧协方差估计窗口的敏感性测试协方差矩阵估计窗口的选择本身就是一个可以无限调下去的参数。窗口短比如 60 天对近期风格切换反应快但噪声大窗口长比如 504 天更稳定但反应迟钝。我的习惯是主回测用 252 天再补跑两个稳健性测试60 天和 504 天三个结果如果趋势一致说明这个策略不是靠某个窗口参数撑起来的如果结果差异巨大那就是模型本质上不稳定参数怎么调都是赌。这种敏感性分析本身也是说服自己投入这个方向的最后一道检验。最后说一个我自己的教训曾经花了很长时间调均值方差的预期收益参数回测夏普做到 2.0上实盘三个月就原形毕露。后来把精力转向最小方差和协方差收缩收益没更高但曲线稳了手上的钱也拿得住。做量化投资很多时候先活下来比赚得多更值钱。希望这些经验帮你在 Python 里把这四个模型一次跑通少走我走过的弯路。本文还有配套的精品资源点击获取
返回列表