Python量化组合四策略:市值加权、等权重、均值方差与最小方差
简介面向使用Python进行量化投资与股票组合管理的开发者系统讲解市值加权、等权重、均值方差与最小方差四种经典组合构建方法解决如何科学分配资产权重、平衡收益与风险的核心问题。内容围绕真实股票数据展开先加载股票数据再利用各策略构建组合最后生成收益汇总与性能对比结果。资源共9个文件压缩包约1.18MB包含3个Jupyter Notebook分别对应四种策略的实现与对比、4个Excel工作簿股票原始数据、市值加权与等权重收益、性能比较等以及metric.py性能评估脚本另有pyc编译缓存文件代码结构清晰便于查看和复用。目前已有526人学习下载。通过学习读者能够对照Notebook逐步理解均值方差中收益与风险的权衡、最小方差优化目标以及市值加权和等权重配置差异直接替换股票数据即可将方法迁移到其他投资场景也可借助metric.py快速评估组合效果。资源非常适合量化投资初学者、金融工程学生以及需要快速验证组合策略的从业者。1. 从市值加权到最优化这个 Python 量化包里藏着的四个策略如果只用一个词概括这份资源的价值那就是「可对照复现」。我打开市值加权等权重均值方差最小方差模型.rar之后发现作者用同一份股票数据把四种组合构建策略全部跑了一遍并把所有结果汇总到了收益汇总.xlsx和性能比较.xlsx里。四个策略的适用场景差异极大市值加权是基准等权重是低风险增强均值方差是收益优先最小方差是风险优先。我拆完代码后一个很直观的感受是等权重策略在很多历史回测窗口里打败了市值加权而这恰恰是多数人直觉上不太相信的结论。如果你在用 Python 做量化投资研究或者正在给学生、同事讲组合构建与资产配置这份材料可以直接当作一套能跑的练习框架指标口径和边界也基本完整。2. 数据口径与协方差矩阵四个策略共同的地基2.1 拆开 stock data先确认你拿到的收益率是哪一种股票数据.xlsx是全部策略的输入。我习惯第一步先看 index 是日期还是序号再看价格列是否已经做过复权。常见做法是使用前复权收盘价因为后复权价格序列数值会很大且不同股票间不可直接比较而前复权价格能保证最近一段时间的价格是真实成交价计算收益率和协方差时更符合实际交易状态。多数情况下表格里存放的是每只股票的日收盘价列名是股票代码或简称。要验证有没有停牌或缺失值可以跑下面这段代码import pandas as pd df pd.read_excel(股票数据.xlsx, index_col0) print(df.head()) print(df.isna().sum()) print(df.index) pct df.pct_change().dropna(howall) print(pct.describe())这段代码做了三件事打印前 5 行确认表头结构统计每列缺失值数量定位停牌或未上市导致的 NaN用pct_change()构建日收益率矩阵并查看基本统计量判断数据是否干净。停牌股票如果直接删除会造成幸存者偏差我一般会先保留 NaN用前向填充df.ffill()处理回测的交易逻辑里再通过valid_assets过滤当日可交易标的。注意dropna(howall)只删除全部为空的日期单个股票的缺失不会影响整体对齐。2.2 收益率矩阵日频还是月频决定后续所有参数这套资源里四个策略的输入统一是收益率矩阵但频率不同结果会差很多。日频数据样本量大协方差矩阵估计更稳定但会引入高频噪声月频数据更贴近资产配置周期可是样本点太少协方差矩阵接近于奇异矩阵。作者在 ipynb 里大概率用的是日频数据然后通过252做年化。业界做类似组合优化时会把日收益率按「交易日历对齐」后统一成 DataFrame行索引是日期列索引是资产代码。构建收益率矩阵时还有一个容易忽略的细节pct_change()默认是简单收益率而多期组合收益的累乘需要用(1 r).cumprod()不能直接对收益序列求和。均值方差模型里如果用到对数收益率优化求解的目标函数形式也要相应调整否则结果会有微小偏差。returns pct.dropna(howall) mean_ret returns.mean() * 252 cov_matrix returns.cov() * 252 print(年化收益率:\n, mean_ret.head()) print(年化协方差矩阵:\n, cov_matrix.head())把日收益率的均值和协方差同时乘以 252是因为日频波动累加后方差随时间线性增长年化后不同频率数据得到的风险指标才能互相比较。returns.cov()用的是样本协方差默认分母是N-1当股票数量接近样本天数时这个估计会失真后面讲 Ledoit-Wolf 收缩时再展开。2.3 协方差矩阵的敏感性最小方差策略成败的关键四个策略里最小方差和均值方差都依赖协方差矩阵。协方差矩阵估计不准优化器会重点配置在那些「看起来」低风险的资产上而这些低风险大概率是历史噪声。一个常见的稳健化处理是给协方差矩阵加上一个小的对角扰动cov np.eye(n) * 1e-6目的只是保证矩阵可逆避免求逆时数值溢出。但如果噪声大到影响权重分配方向光加扰动是不够的。实践中我一般会先看协方差矩阵的条件数如果大于1e6说明某些资产高度共线要继续做去重或降维处理。metric.py里如果出现np.linalg.inv调用排查矩阵病态问题是调通这段逻辑的前提。3. 四大权重策略的建模与实现从最简单到最复杂3.1 期望收益与权重向量数学符号先约定好后续所有代码都遵循同一套记号n是资产数量w是权重向量mu是年化期望收益向量Sigma是年化协方差矩阵。市值加权和等权重不需要用到它们但均值方差和最小方差必须处理这两个输入。权重向量的约束条件有两类等式约束要求权重之和等于 1不等式约束要求每个权重不小于 0即不允许做空。国内做股票多头的组合优化默认不允许做空这个约束要写进优化器里。scipy.optimize.minimize是这个过程的主力函数方法选SLSQP即可处理带边界和等式约束的非线性规划问题。import numpy as np import pandas as pd from scipy.optimize import minimize def min_variance_weight(returns): mu returns.mean() * 252 cov returns.cov() * 252 n len(mu) def objective(w): return w cov w constraints ({type: eq, fun: lambda w: np.sum(w) - 1}) bounds [(0, 1)] * n w0 np.ones(n) / n res minimize( objective, w0, methodSLSQP, boundsbounds, constraintsconstraints, options{ftol: 1e-8} ) return res.x这段代码实现了最小方差策略。目标函数w cov w就是组合方差不包含期望收益项。初始点w0统一设为等权重让优化器从一个合理位置出发。bounds为每个资产加上了 0 到 1 的限制等价于禁止做空和加杠杆。ftol的值调小一点是为了收敛精度更高默认1e-6在矩阵条件数偏大时可能提前停止迭代。均值方差策略只需要在目标函数里加入收益项def mean_variance_weight(returns, risk_aversion1.0): mu returns.mean() * 252 cov returns.cov() * 252 n len(mu) def objective(w): return -(mu w) risk_aversion * (w cov w) constraints ({type: eq, fun: lambda w: np.sum(w) - 1}) bounds [(0, 1)] * n res minimize( objective, np.ones(n) / n, methodSLSQP, boundsbounds, constraintsconstraints, options{ftol: 1e-8} ) return res.xrisk_aversion是风险厌恶系数。这个系数越大组合越偏向低波动资产越小越追逐高收益资产。实际操作中很多人不知道的是均值方差优化对mu的估计误差极其敏感历史上mu的估计只要偏差 1%最优权重的配置方向可能完全不同。所以这个策略在四个里最难做稳定需要配合 2.3 节讲的协方差收缩和后续的滚动窗口回测。模型参数里risk_aversion1.0是常见起点也可以按0.5到5的范围做敏感性测试。3.2 市值加权与等权重不需要求解器的对照组市值加权策略的数学表达式为w_i market_cap_i / sum(market_cap_j)。实现时只需要拿到每只股票的总市值权重就是市值占比。难点在于市值数据的获取与对齐回测时要用当时点的市值不能用最新市值否则会产生前视偏差。这份资源里的股票数据.xlsx大概率只包含收盘价市值数据可能由价格乘以总股本得到也可能作者直接内置了一个市值序列。等权重就简单得多每个资产的权重恒为1/n不需要任何优化逻辑。def market_cap_weight(market_cap): cap np.array(market_cap, dtypefloat) return cap / cap.sum() def equal_weight(n): return np.ones(n) / n两个策略对比最大的区别在行业暴露市值加权策略天然偏向大市值行业比如金融、白酒等权重策略在行业上更均匀隐含了对小市值股票的增配。回测结果中等权重长期跑赢市值加权很大程度上不是等权重本身多厉害而是它规避了市值加权「单一行业权重过高」的结构性问题。beta 相对基准更低但换手率更高因为每次调仓都要卖出一部分涨上去的股票、买入跌下来的股票相当于做了系统性的低买高卖。3.3 四种策略的约束对比与适用边界策略是否依赖收益预测是否依赖协方差矩阵组合波动特征换手率特征市值加权否否跟随大盘低于其他组合等权重否否分散度高偏向小盘调仓时偏高均值方差是是对估计误差敏感可能很高最小方差否是历史波动最低中等均值方差理论听上去最完备但实际上线最难最小方差策略不需要预测收益只需要协方差矩阵所以在机构里反而更常用。市值加权是基准等权重是强基准。作者在metric.py里大概率对这四个策略统一计算了年化收益率、年化波动率、夏普比率和最大回撤用这四个指标横向比较才有意义。如果只给出收益曲线没有对应指标一定要补上否则没法判断策略好坏。3.4 数值边界权重全部为零或集中在一个资产上即使禁止做空优化器仍可能给出极端解比如所有权重压在一只股票上。手写代码时踩过的一个坑是SLSQP的迭代步长过大导致权重出现-1e-12这样的负值加总后恰好满足约束但看起来很奇怪。解决方法是优化完成后对权重做一次小值截断w np.clip(w, 0, 1)然后再标准化。如果优化结果连续出现集中在单一资产的解首先要怀疑协方差矩阵是否有效其次看边界约束是否真的传进了优化器。可以打印res.success和res.message判断是否收敛。这部分调试细节原作者放在 ipynb 里不一定写在代码块中但跑数时几乎必然会遇到。4. 回测框架与收益汇总metric.py 背后的评估口径4.1 从每日权重到组合净值曲线构建完成四个策略的权重序列后下一步是把每日权重映射到收益上生成组合净值曲线。这里的核心代码是基于每日持仓计算下一日的组合收益port_ret (weights.shift(1) * returns).sum(axis1)。为什么要shift(1)因为今日的权重是依据昨日及之前的信息计算得出的最早只能用于今日收盘价到明日收盘价之间的收益。这个处理能彻底杜绝未来函数让回测结果接近实盘逻辑。def backtest(weights, returns): weights weights.fillna(0).clip(lower0) weights weights.div(weights.sum(axis1), axis0) port_ret (weights.shift(1) * returns).sum(axis1) nav (1 port_ret).cumprod() return nav, port_ret回测函数里先对权重做归一化防止因股票退市或停牌导致权重加起来不等于 1再用shift(1)避免前视偏差最后用cumprod()把日收益序列累乘为净值。nav是策略净值port_ret是每日组合收益序列。这里有一点容易被忽视如果某只股票停牌导致当日收益率为 NaNsum(axis1)会默认跳过缺失值但权重归一化时这只停牌股票仍然占着一个权重会导致实际仓位不满。处理方法是把停牌股票的权重先置 0再重新归一化。停牌股在 A 股环境下并不罕见算收益时务必把这条逻辑写进去。return之后的指标计算常见做法是把日收益序列输入metric.py里的函数输出总收益率、年化收益率、年化波动率、夏普比率、最大回撤、卡玛比率这六个指标。写一个简洁的版本def compute_metrics(nav, rf0.02): total_return nav.iloc[-1] / nav.iloc[0] - 1 years len(nav) / 252 annual_return (1 total_return) ** (1 / years) - 1 daily_ret nav.pct_change().dropna() annual_vol daily_ret.std() * np.sqrt(252) sharpe (annual_return - rf) / annual_vol drawdown nav / nav.cummax() - 1 max_drawdown drawdown.min() calmar annual_return / abs(max_drawdown) return { 总收益率: total_return, 年化收益率: annual_return, 年化波动率: annual_vol, 夏普比率: sharpe, 最大回撤: max_drawdown, 卡玛比率: calmar }rf0.02代表无风险利率 2%也可以按具体年份改成一年期国债收益率。nav / nav.cummax() - 1是向量化的回撤算法比 for 循环效率高很多。该函数输出的字典可以直接转成 DataFrame横向对比四个策略的指标。收益汇总.xlsx大概率就是这个字典或类似结构的导出结果如果你发现里面的指标和这份代码的计算结果对不上重点检查年化方式有的是252日年化有的是244交易日年化还有的用几何年化而不是算数年化。4.2 性能比较文件最重要的不是收益而是稳定性打开性能比较.xlsx时我建议先看最大回撤和夏普比率两列而不是总收益率。四个策略里均值方差在理论上拥有最高的收益风险比但实际操作中由于协方差矩阵不可精确估计夏普比率往往并不出色。最小方差策略在熊市里优势明显2015 年后的 A 股环境中尤其如此。等权重策略的夏普通常介于市值加权和最小方差之间收益却经常跑赢市值加权这就是它在资产配置领域被广泛使用的原因。如果性能比较表里没有换手率或双边换手率列建议自己补上否则无法判断高收益是不是靠高换手硬堆出来的。高换手意味着更高的交易成本和冲击成本策略实盘后的表现大概率不如回测。4.3 未来函数自查一眼检查权重时效性回测最容易犯的错误就是无意中引入未来函数。除了前面说的shift(1)还有一个隐蔽点是协方差矩阵计算窗口的使用。如果回测中某一日计算权重时用到了「未来 60 日」的收益率数据业绩会极度失真。自查方法很简单手动挑一个回测日期打印出该日期前后 5 天的原始数据核对权重是用哪段数据算出来的。另外观察权重序列的第一行如果第一天权重就非零并且第一天的收益就被记入了净值那么相当于用了当天收盘信息去预测当天收益这在盘中是无法做到的一定要把第一天收益置 0 或空。5. 把模型推向下一个阶段滚动窗口重估、收缩估计与换手率控制5.1 滚动窗口重估破解均值方差对参数的高度敏感前文的优化代码用了全样本均值方差来求权重相当于「上帝视角」回测样本外往往达不到回测效果。成熟的量化研究中更常用的做法是滚动窗口每次调仓只用过去lookback天的数据计算协方差和期望收益得到下一次调仓的权重。这样做既避免了前视偏差又更接近实际投资中的决策频率。def rolling_optimize(returns, rebalance_freq21, lookback252): dates returns.index weights pd.DataFrame(indexdates, columnsreturns.columns, dtypefloat) for start in range(lookback, len(dates), rebalance_freq): window returns.iloc[start - lookback:start] w min_variance_weight(window) end min(start rebalance_freq, len(dates)) for i in range(start, end): weights.iloc[i] w return weights.ffill().fillna(0)这段代码每 21 个交易日约一个月调仓一次每次用过去 252 个交易日的收益率数据重新计算最小方差权重。rebalance_freq控制调仓频率太短会导致较高的换手和交易成本太长又会让组合偏离目标风险状态。我一般会在lookback上做敏感性测试252、504、756 分别回测一遍观察结果是否稳健。如果lookback252和lookback756的净值曲线形状差异巨大说明协方差估计不够稳定组合大概率过拟合。5.2 Ledoit-Wolf 收缩优化协方差矩阵估计均值方差和最小方差对协方差矩阵的估计精度要求很高。当资产数量较多、样本数量有限时样本协方差矩阵的特征值会整体高估导致优化器「误以为」某些资产存在低风险套利机会。业界常用的改进是 Ledoit-Wolf 收缩估计它把样本协方差矩阵向单位矩阵收缩减小极端特征值的影响。from sklearn.covariance import LedoitWolf ledoit LedoitWolf().fit(returns[window_index]) cov_shrunk ledoit.covariance_LedoitWolf内部会自动计算最优收缩强度不需要手工调参。用cov_shrunk替换前文的returns.cov() * 252通常能让最小方差策略的样本外波动率下降 10% 到 20%。如果无法使用 sklearn也可以自己实现简单的收缩cov_shrunk (1 - alpha) * sample_cov alpha * np.diag(np.diag(sample_cov))alpha设定为 0.2 到 0.5 之间达到类似效果。在四个策略的横向对比表中加上「Ledoit-Wolf 最小方差」这一列能明显看到风险指标改善。5.3 验证改进方向样本外区间的选择在拿改进后的模型上线前把整个数据集切成训练期和样本外测试期两个部分训练期占七成样本外占三成。必须保证所有调参过程都只用训练期数据只有最终确认权重表达式后才允许在样本外测试期跑一次。这样拿到的收益、回撤、夏普比率才是可信度的底线。如果样本外测试的净值曲线在训练期结束后瞬间跳水说明参数过拟合或数据分割时股票池发生了漂移。建议在训练期与样本外测试期之间留出 20 个交易日的缓冲期用于验证权重转换是否平稳。5.4 降低换手率的工程技巧连续调仓会产生巨额换手。一个常用的工程技巧是把新权重向量加上旧权重向量乘一个权重衰减系数实现分步调仓。new_w 0.5 * target_w 0.5 * current_w即为半调仓模式适合交易成本较高的情况。如果要精确控制换手上限把换手率约束直接写进优化器内部np.abs(w - current_w).sum() turnover_limit但这会让minimize的约束条件变成非凸求解难度增加SLSQP 容易陷入不可行解。我实际跑下来的经验是多数场景下 0.4 的目标换手率已经能兼顾跟踪误差与交易成本没必要追求每次完全换到最优。这部分的调试记录建议回写到 ipynb 里作为策略迭代日志方便后续复盘时对应参数变化。本文还有配套的精品资源点击获取