均值回归策略实战:从统计检验到Python回测 这次我们来看系列第四讲均值回归。如果前面几讲已经讲完了趋势跟踪和资金管理那么“均值回归”就是量化交易里另一条完全不同的路。它的核心逻辑很简单价格偏离均值太远就会像弹簧一样被拉回来。这个想法听起来很朴素但难点从来不在“知道这个概念”而在“怎么判断它当前是否有效”“什么时候该进场”“什么时候该止损”。这篇文章不会停在概念层面。我会从均值回归的数学基础、统计检验、单标的策略实现、配对交易、参数风控一直写到回测陷阱和排查方法。你会拿到一套可以直接在本地验证的 Python 代码思路以及判断策略是否真正有效的检查清单。如果你正在做量化回测或者准备把某个震荡品种做成策略这篇可以保存下来对着操作。先说明本文所有代码和数据逻辑均为教学示例不构成任何投资建议。回测结果不代表实盘收益实盘前需要自己完成数据核验和风险评估。1. 均值回归核心定义与策略速览均值回归Mean Reversion指的是资产价格在偏离长期均值后会向均值靠拢的现象。这种统计特性在震荡行情里最明显价格被推到区间上沿随后大概率会回到中枢价格打到区间下沿也容易出现反弹。在量化框架里均值回归通常被归为“非趋势策略”它和趋势跟踪的核心假设正好相反。对比维度趋势跟踪均值回归核心假设趋势会延续价格会回归均值盈利区间单边大行情震荡区间典型标的强趋势品种、事件驱动板块股指ETF、外汇对、部分商品止损含义止损是趋势判断错误止损是“均值”可能已经失效风险来源震荡市反复磨损单边趋势中连续逆势交易回归交易系统最常用的是 z-score 指标。它的计算方式并不复杂z (current_price - mean_price) / std_price当 z 大于某个阈值时说明价格高于均值可以考虑做空当 z 小于负阈值时说明价格低于均值可以考虑做多。这里的“均值”和“标准差”通常由滚动窗口计算典型窗口在 20 到 60 根 K 线之间。均值回归策略之所以长期存在并不是因为市场“应该”回归而是因为市场参与者的行为会形成短期过度反应。消息落地时情绪推动价格冲过头大宗交易带来的临时抛压也会让价格短时间偏离基本价值。这些偏离结束后价格会回到更接近真实供需水平的位置。策略要赚的就是这部分“偏差修复”的钱。2. 适用场景与使用边界均值回归不是万能的。它最适合的品种通常具有几个特征价格长期在一个区间内波动、成交活跃、没有频繁的跳空缺口、基本面稳定。股指 ETF、主流外汇对、某些流动性好的商品期货都具备这些特征。下表给出一个粗略的适配判断。实际使用的时候建议你对自己关注的品种做分年度的统计检验而不是凭感觉选择。标的状态更适合的策略原因长期横盘整理均值回归价格中枢稳定偏离后修复概率高单边上涨趋势跟踪做空回归会被不断打脸事件驱动的暴涨暴跌短线动量消息面主导回归时点不确定流动性差的冷门品种都不适合滑点大数据噪声高不适用场景要特别警惕。如果某只股票长期处于退市边缘或者某个板块正在被政策持续驱动价格可能长期偏离“均值”而不是回归。这种局面下均值回归策略会表现为净值曲线反复回撤止损被连续触发。另一个边界是周期选择。均值回归在分钟级别、小时级别和日线级别都可以做但不同周期的“均值”含义完全不同。5 分钟线的均值可能只是短期资金博弈的结果日线均值更接近基本面定价。周期越短手续费和滑点占比越高周期越长验证回归有效性所需要的数据量也就越大。3. 均值回归的统计检验先验证再上车很多人拿到一个品种就直接写 z-score 策略这是错误顺序。正确做法是先做统计检验确认这个品种在历史数据上确实存在均值回归特性。三个最常用的检验ADF 检验、Hurst 指数、波动半衰期。3.1 ADF 检验ADFAugmented Dickey-Fuller检验用来判断价格序列是否平稳。如果序列是平稳的说明价格没有随机游走那么“飘”均值回归的概率更高。import pandas as pd from statsmodels.tsa.stattools import adfuller def check_stationarity(series, nameprice): result adfuller(series.dropna(), autolagAIC) print(f{name} ADF stat: {result[0]:.4f}) print(f{name} p-value: {result[1]:.4f}) if result[1] 0.05: print(结论拒绝单位根假设序列平稳具备均值回归条件) else: print(结论不能拒绝单位根假设序列非平稳回归可靠性存疑)使用场景里有一点要注意原始价格序列大多数非平稳平稳的通常是“价差”或者“对数价格的一阶差分”。所以在做单标的策略时我会先看收益率的平稳性再对价格偏离做回归约束在做配对交易时则直接检验两个品种价差的平稳性。3.2 Hurst 指数Hurst 指数用来判断时间序列的类型。H 0.5 表示均值回归特征明显H 0.5 表示随机游走H 0.5 表示存在趋势性。import numpy as np def hurst_ts(ts): ts np.asarray(ts, dtypefloat) lags range(2, min(len(ts) // 4, 100)) tau [np.sqrt(np.std(ts[lag:] - ts[:-lag])) for lag in lags] poly np.polyfit(np.log(lags), np.log(tau), 1) return poly[0] * 2.0 # 示例随机游走序列应接近 0.5 np.random.seed(42) random_walk np.cumsum(np.random.randn(1000)) print(Hurst:, hurst_ts(random_walk))注意 Hurst 指数的估计结果对数据长度和滞后区间很敏感。实际判断时我会分别计算不同时间窗的 Hurst看它是否稳定。如果最终结果在 0.45 到 0.55 之间反复跳动就不要强行下结论配合 ADF 检验一起看。3.3 波动半衰期半衰期衡量“价格偏离均值后大约需要多少根 K 线才能修复一半”。这个数值直接决定了你的持仓周期应该设多长。半衰期太短策略交易频率过高手续费吃掉利润半衰期太长资金占用成本高策略会经历很长的浮亏期。可以通过一阶自回归来估算半衰期。先对价格序列做滞后一期的线性回归得到回归系数 lambda再通过 -ln(2) / lambda 计算半衰期。import numpy as np from sklearn.linear_model import LinearRegression def estimate_half_life(price_series): data price_series.dropna().values y np.diff(data) x data[:-1] x x.reshape(-1, 1) model LinearRegression().fit(x, y) lam model.coef_[0] if lam 0: return np.inf half_life -np.log(2) / lam return half_life half_life_value estimate_half_life(df[close]) print(估计半衰期根K线:, half_life_value)这个数值用来指导策略参数很有效。比如半衰期是 15 根日 K那么你的持有周期预期就在 15 到 30 根 K 线附近如果 10 天都等不到回归就要检查是不是价格结构已经变了。3.4 检验结果怎么看上面三个指标组合起来可以给出一张比较完整的判断结论。检验指标倾向均值回归倾向趋势 / 随机游走ADF p 值p 0.05p ≥ 0.05Hurst 指数 0.5≥ 0.5半衰期落在策略可接受持仓周期内过长或为无穷大价差稳定性均值附近波动持续漂移或突变不要把单个指标当唯一依据。三个指标都指向均值回归再进入策略开发阶段会省掉后面很多无效回测时间。4. 单标的均值回归策略实现通过统计检验之后可以先用一个单标的策略测试回归逻辑。这里选择“价格相对布林带的 z-score”作为信号源。4.1 策略逻辑先定义一个基础函数计算序列的滚动均值和标准差。def zscore_series(series, window20): mean series.rolling(windowwindow).mean() std series.rolling(windowwindow).std() z (series - mean) / std return z然后定义信号逻辑。常见做法是设置两个阈值入场阈值和出场阈值。例如 z-score 低于 -2 时做多高于 2 时做空回归到 0 附近时平仓。这样设计的好处是逻辑清晰回测时容易定位每一次交易的开仓和平仓原因。4.2 完整回测示例下面给出一个最小可运行的回测框架。这里的思路是根据持仓状态和当前 z-score 生成交易信号按收盘价成交计算每日收益。数据用本地的 CSV 文件读取字段至少包含date、close。import pandas as pd import numpy as np def backtest_mean_reversion(file_path, window20, entry_threshold2.0, exit_threshold0.5): df pd.read_csv(file_path, parse_dates[date]).sort_values(date) df[z] zscore_series(df[close], windowwindow) df[position] 0 for i in range(window, len(df)): z df.loc[i, z] if np.isnan(z): continue if df.loc[i - 1, position] 0: if z -entry_threshold: df.loc[i, position] 1 elif z entry_threshold: df.loc[i, position] -1 else: prev_pos df.loc[i - 1, position] if prev_pos 1 and z -exit_threshold: df.loc[i, position] 0 elif prev_pos -1 and z exit_threshold: df.loc[i, position] 0 else: df.loc[i, position] prev_pos df[position] df[position].shift(1) df[ret] df[close].pct_change() df[strategy_ret] df[ret] * df[position] df[cum_ret] (1 df[strategy_ret]).cumprod() return df df_test backtest_mean_reversion(sample_data.csv, window20, entry_threshold2.0, exit_threshold0.5) print(df_test[[date, close, z, position, strategy_ret, cum_ret]].tail())这里要注意position的 shift 处理。信号实际发生在收盘后第二天才能成交所以把当日信号后移一天避免未来函数。4.3 判断运行结果是否合理回测跑完后不要只盯着总收益率。先看几个关键指标交易次数是否足够多样本太少无法判断有效性单次交易的最大浮亏多大会不会超过心理承受范围曲线是否存在长时间不创新高的情况最大回撤出现在哪段行情对应的是不是单边趋势。一套策略至少要跑过三种行情上涨、下跌、横盘。如果策略只在横盘区间赚钱却在一轮单边行情里亏损回撤超过收入的几倍那么整体仓位就值得重新设计。5. 配对交易与协整经典均值回归玩法单标的均值回归的问题是“均值”不稳定。一只股票可能因为基本面变化价格中枢整体上移或下移。这时再看历史波动均值就容易不停抄底亏损。配对交易Pairs Trading通过构造两个品种的价差来规避这个问题。它的逻辑是两只相关性较高的资产价差会围绕一个均衡水平波动。价差拉大时做空强势品种、做多弱势品种价差回归时平仓。5.1 选择合适的交易对基本面相近的股票、同行业指数 ETF、关联商品期货都可以作为候选。实际操作中要先做相关性分析再检验价差的平稳性。import pandas as pd df_assets pd.read_csv(pair_prices.csv, index_coldate, parse_datesTrue) corr df_assets.corr() print(corr) # 以 A、B 两列为例画价差序列 spread df_assets[A] - df_assets[B]相关性不是唯一条件。两只股票相关性高但价差可能出现长期漂移这时做多空组合依然有风险。真正能用来交易的是协整关系。可以用 statsmodels 的coint函数来检验。from statsmodels.tsa.stattools import coint coint_t, p_value, crit_value coint(df_assets[A], df_assets[B]) print(f协整检验 p 值: {p_value:.4f}) if p_value 0.05: print(存在协整关系价差具备均值回归条件) else: print(协整关系不明显不建议直接做配对交易)5.2 配对交易信号得到价差序列后对价差计算 z-score并用和单标的类似的方式开平仓。做多价差表示做多 A、做空 B做空价差表示做空 A、做多 B。这个策略最难的部分不是信号生成而是头寸配比。简单做法是等市值配比严谨做法是用回归或协整系数计算对冲比例。对冲比例不准确整体组合就不是中性组合大盘方向性风险会重新引入。5.3 配对交易的风险边界配对交易也不是无风险套利。最典型的失效场景是价差本应回归却因为某个品种出现基本面变化而彻底脱离原有区间。比如两只同行业股票其中一只是重组标的另一只被机构连续做空价差可能持续扩大。此时如果还坚持“越跌越买”可能直接造成严重亏损。所以配对交易同样需要止损。止损的对象不是单一品种而是“价差本身”。当价差偏离达到某一阈值且超过预设的最大容忍时间后应该主动平仓而不是继续等待回归。6. 参数选择与风险管理均值回归策略比趋势策略更依赖参数。窗口期、开仓阈值、出场阈值、止损线、最长持仓时间每一个参数都会明显影响最终净值和回撤水平。参数影响方式建议调整方向窗口期决定“均值”和“标准差”的计算范围用半衰期估算作为起点再小幅调整开仓阈值决定交易频率和信号质量阈值越大交易越少单笔质量要求越高出场阈值决定盈利兑现时机过小容易提前下车过大容易回吐利润止损线决定单次最大亏损按品种波动率动态设置最长持仓时间防止资金被长期占用结合半衰期和回测持仓分布设定策略调参时很容易陷入过度优化。我的建议是先固定一个参数组合做完整回测再逐个参数增加一档或减少一档观察结果是否变化剧烈。如果参数从 18 改成 22策略就从大幅盈利变成大幅亏损说明策略存在严重的过拟合风险。仓位上均值回归策略最忌讳的是“越跌越加仓”。技术指标的 z-score 可以到 -3、-4 甚至更低说明价格偏离程度很大但也可能代表基本面已经发生剧变。每次开仓使用固定仓位回撤达到阀值就停止交易都比盲目翻倍加仓更安全。7. 常见回测陷阱与排查方法写均值回归策略的代码并不难难的是回测结果能不能反映真实的市场情况。下面几个问题在回测里最容易出现。问题现象可能原因排查方式解决方案回测收益很高实盘完全跑不出来使用未来函数检查是否有信号当日成交、数据是否前视信号次日成交禁止用到未来数据策略在一轮大行情里连续亏损品种不适合均值回归查看该阶段行情是否单边结合趋势过滤单边行情暂停策略参数改动一点结果差异巨大过拟合做参数敏感性分析使用固定参数增加样本外测试交易次数过少开仓阈值过高统计历史信号数量降低阈值或扩大测试周期价差长期不回归协整关系失效重新做协整检验更换交易对或直接止损手续费滑点后不赚钱交易频率过高增加交易成本参数提高阈值减少交易次数或改用更长周期净值曲线稳步上涨但只在交易所交易时间内有效数据时间戳错位对齐时间周期使用统一时区和复权数据幸存者偏差也要特别留意。如果用现在的股票列表回测十年前的数据其中很多退市和长期下跌的股票会被排除在外结果会虚高。回测前应该尽量使用“当下时点实际存在的标的池”或者加入退市样本保证回测结果贴近真实可交易范围。另一个容易被忽略的点是交易成本。均值回归策略的单笔盈亏通常不大有时候单次盈利 1%手续费加滑点就吃掉 0.2%累积下来对净值影响非常大。回测时不要只算佣金要把印花税、滑点、冲击成本都估算进去。稳妥的做法是设置保守的成本模型看策略在“高成本”场景下还有没有正期望。8. 最佳实践与后续扩展如果你是从零开始搭建均值回归体系建议按下面这个顺序推进。第一步先确定测试周期和数据集。用至少三年以上的日线数据覆盖牛熊震荡三种行情。数据来源尽量和实盘行情源一致避免数据源切换带来的偏差。第二步对候选品种做统计检验。ADF、Hurst、半衰期都满足均值回归特征再进入下一步。检验结果不好的品种直接放弃不要用参数硬凑。第三步用小参数最小化策略跑通代码。先看能不能正确生成信号、正确平仓、正确记录每笔交易再调整参数和风控逻辑。建议维护一套最小可运行回测脚本后续所有策略迭代都在它的基础上进行。第四步加入交易成本和滑点。把手续费、冲击成本设置到偏高位置重新跑结果。如果扣除成本后依然为正才有继续研究的价值。第五步多做样本外验证。把数据切成两段前半段用来开发参数后半段用来做样本外测试。样本外表现大幅缩水的策略要找到缩水原因而不是直接忽略。后续还可以向两个方向扩展。一个方向是从单标的走向配对组合用多组低相关交易对构建均值回归组合降低单一价差失效的风险。另一个方向是叠加市场环境过滤比如用长期均线判断市场处于趋势状态时暂停策略只在震荡状态开启交易。这样做的目的不是把策略变复杂而是让策略只在自己有优势的环境里运行。均值回归是一个值得花时间打磨的策略模块但它对执行细节的要求很高。最优先的验证动作是选一个流动性好的品种计算它的 z-score 和半衰期跑一遍完整回测并记录每次交易的时间和盈亏。把这条路走通之后再考虑配对和组合层面的扩展。