
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
做量化策略时,很多人只盯着买入和卖出信号,却忽略了一个细节:当策略「空仓」的时候,那笔现金在干什么?
对于择时类策略(比如均值回归、趋势跟随),实际持有标的的天数往往只占一半左右,剩下大量时间资金都在「躺平」,收益率为 0。如果能让这部分闲置资金也「打工」,整体表现会不会更好?
本文用 26 年的 SPY 历史数据做了一次回测:把空仓时的闲置资金按趋势信号分配到「黄金 (GLD) + 中期国债 (IEF) + 短债 (SHV)」上,看看结果如何。全程基于 Python,涉及 pandas、yfinance、vectorbt 等常用库,非常适合正在学习 Python 量化的同学参考。
先说清楚一个前提:这套逻辑只作用于策略空仓的那些天。当策略持有 SPY 时,一切照旧,买卖点完全不变;只有在 SPY 不持仓时,才把这部分现金拿去做配置。
早期的思路是把闲置资金 75% 投黄金、25% 投短债,并用黄金自身的「黄金交叉」(50 日均线上穿 200 日均线) 作为开关。这次做了一个更分散的版本:
根据 GLD 和 IEF 各自是否金叉,一共有 4 种情况:
思路很直观:两个都在涨,就平分那 75%;只有一个在涨,它就拿满 75%;两个都不涨,就全躲进短债里避险。
import vectorbt as vbt
# 计算 2 日 RSI(超短周期,对回调非常敏感)
rsi = vbt.RSI.run(df["Close"], window=2).rsi
sma_exit = vbt.MA.run(df["Close"], window=5).ma # 5 日短均线,用于离场
# 超卖(RSI < 10)时买入
mr_entries = rsi < 10
# 超买(RSI > 70)或价格重新站上短均线时卖出
mr_exits = (rsi > 70) | (df["Close"] > sma_exit)sma_fast = vbt.MA.run(df["Close"], window=50).ma # 快线
sma_slow = vbt.MA.run(df["Close"], window=200).ma # 慢线
tf_entries = sma_fast.vbt.crossed_above(sma_slow) # 金叉买入
tf_exits = sma_fast.vbt.crossed_below(sma_slow) # 死叉卖出import pandas as pd
# 分别用各自的 50/200 金叉判断是否处于上升趋势
both = gld_golden & ief_golden # 两者都金叉
gld_only = gld_golden & ~ief_golden # 只有黄金金叉
ief_only = ~gld_golden & ief_golden # 只有债券金叉
neither = ~gld_golden & ~ief_golden # 都没金叉
# 按四种情况分别计算闲置资金的每日收益
idle_ret = pd.Series(0.0, index=common_index)
idle_ret[both] = 0.375 * gld_ret[both] + 0.375 * ief_ret[both] + 0.25 * shv_ret[both]
idle_ret[gld_only] = 0.75 * gld_ret[gld_only] + 0.25 * shv_ret[gld_only]
idle_ret[ief_only] = 0.75 * ief_ret[ief_only] + 0.25 * shv_ret[ief_only]
idle_ret[neither] = shv_ret[neither] # 最差情况:全部躲进短债坑一:脏数据会「毁掉」整条净值曲线。
行情接口偶尔会返回一根坏 K 线(收盘价为 0、负值或 NaN)。一个 -100% 的假跌,会让后续净值直接乘以 0 且再也回不来,哪怕真实市场什么都没发生。
# 把非正数 / 缺失的收盘价标记出来,用前一个有效价格前向填充
bad = px.isna() | (px <= 0)
px = px.mask(bad).ffill() # ffill:forward fill,前向填充同理,对单日涨跌超过 20% 的「异常波动」也做了裁剪——GLD、SHV、IEF 这类品种基本不可能单日暴动 20% 以上,遇到就当作数据毛刺清零处理。
坑二:不要盲目相信封装好的持仓掩码。
回测中发现,某些框架自带的「持仓掩码」虽然统计出的总仓位比例是对的,但逐日对齐时会把「哪几天在场内」标错,从而悄悄毁掉任何按天结合收益率的计算。更稳妥的做法是直接从成交记录表 (trades) 里,按每笔交易的入场、出场时间戳还原逐日持仓状态:
in_market = pd.Series(False, index=strat_ret_index)
for _, row in trades.iterrows():
entry = row["Entry Timestamp"]
# 未平仓的交易,出场时间用序列的最后一天兜底
exit_ts = row["Exit Timestamp"] if row["Status"] == "Closed" else strat_ret_index[-1]
in_market.loc[entry:exit_ts] = True # 这段区间标记为「在场内」这是一个很好的提醒:回测框架给的便捷方法不一定和你的收益序列逐日对齐,关键结果一定要用「数据源真相」交叉验证。
先看 26 年里四种情况各自出现的天数占比:
「最差情况全短债」大约只占 17.9% 的时间,而黄金和债券单独金叉的天数分布也比较均衡,没有一头倒向某个资产。
这类策略空仓时间高达 59.2%,闲置资金规模大,redeploy 的空间也最大:
年化收益几乎翻倍,回撤还略有下降,夏普明显抬升。空仓那些天里,这套闲置资金组合累计贡献了 275.41% 的收益。「收益更高 + 回撤更低」同时出现,对一个完全不改变原策略买卖点的叠加层来说,是相当漂亮的结果。
这类策略只有 31.8% 的时间空仓,可动用的闲置资金少,效果自然打折:
年化从 6.15% 提升到 8.09%,回撤基本持平,夏普小幅上升。方向是对的,只是「原材料」不够多。
核心逻辑是站得住脚的:本来收益为 0 的闲置资金,被投进了能产生真实收益的资产;同时用趋势信号做闸门,避免在黄金或债券的下跌趋势里硬扛。把 75% 拆给两个资产、而不是押注单一标的,是更保守的构造——44.8% 的时间两者同涨,此时也只各配 37.5%,用「放弃一部分单一赢家的上行空间」换「更低的集中度风险」。
但要诚实:
对正在学 Python 量化的你,这篇最值得抄走的不只是那几行配置代码,更是「对框架保持怀疑、用数据源真相交叉验证」的工程习惯。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐