本地量化系列教程 · 第三篇。配套笔记本:
均值回归.ipynb;数据:stock_data/000063_中兴通讯.csv(本地 A 股日线)。
双均线这类趋势策略假设"涨的会继续涨";均值回归策略赌的是相反的方向——价格偏离均值太远后,会自己回来。这一篇实现一个最简洁的均值回归系统:SMA ± N 倍标准差构成通道,突破上轨做空、跌破下轨做多、回到均值附近平仓离场。同时你会学到与趋势策略完全不同的持仓管理方式。
1. 策略原理
1.1 统计基础
把价格看作围绕其均线波动的随机过程,用滑动窗口估计波动幅度:
$$ \text{SMA}_t = \frac{1}{N}\sum_{i=0}^{N-1} P_{t-i}, \qquad \sigma_t = \text{std}(P_{t-N+1}, \dots, P_t) $$
通道上下轨即 SMA ± k·σ(k 取 1~2,N 取 15~20 是常用起点)。这与布林带(Bollinger Bands)同构,只是布林带默认 N=20、k=2。
1.2 三段式交易规则
本篇策略(N=15,k=1,中兴通讯日线,2020-01 起):
| 状态 | 触发条件 | 动作 |
|---|---|---|
| 开空 | 收盘价 上穿 上轨(超涨) | position = -1 |
| 开多 | 收盘价 下穿 下轨(超跌) | position = +1 |
| 回归中性 | 价格回到均线附近(distance 变号) |
position = 0 |
| 维持 | 其余情况 | 保持前一状态(ffill) |
与双均线最大的区别:双均线的仓位永远是 ±1(永远在场内),均值回归有"离场"这个动作——价格回归均值、赌注兑现后主动清仓,等待下一次极端偏离。
2. 实现代码
import pandas as pd
import numpy as np
# 读入本地日线数据并截取 2020 年以后
z = pd.read_csv('./stock_data/000063_中兴通讯.csv')
z['日期'] = pd.to_datetime(z['日期'])
z = z.set_index('日期').sort_index()
z = pd.DataFrame({'close': z.loc['2020-01-01':, '收盘']})
z['returns'] = np.log(z['close'] / z['close'].shift(1))
SMA, TH = 15, 1.0
z['SMA'] = z['close'].rolling(SMA).mean()
z['sigma'] = z['close'].rolling(SMA).std()
z['upper'] = z['SMA'] + TH * z['sigma']
z['lower'] = z['SMA'] - TH * z['sigma']
z['distance'] = z['close'] - z['SMA'] # 偏离度
# --- 信号逻辑(自上而下覆盖)---
z['position'] = np.where(z['close'] > z['upper'], -1, np.nan) # 超涨开空
z['position'] = np.where(z['close'] < z['lower'], 1, z['position']) # 超跌开多
# 价格与均线的偏离变号 → 已回归 → 平仓归零
z['position'] = np.where(z['distance'] * z['distance'].shift(1) < 0, 0, z['position'])
z['position'] = z['position'].ffill().fillna(0) # 维持,初始为 0
z['strategy'] = z['position'].shift(1) * z['returns']
注意信号逻辑的写法顺序:先赋极端信号,再赋反向信号,再处理回归平仓,最后 ffill() 把中间的 NaN 填成"维持现状"。np.where 链 + ffill 是向量化实现状态机的标准范式——虽然看起来像几条独立规则,实际执行是有优先级顺序的。
distance * distance.shift(1) < 0 是"偏离度变号"的向量化表达:今天在均线下方、昨天在上方(或反之),说明价格完成了从一侧到另一侧的穿越,即回归完成。
3. 可视化
3.1 价格通道

灰色线是收盘价,±1σ 通道随波动率自动伸缩。2020-07 那波急拉、2023 年中的深跌、2025 年初的冲高,都出现过明显的通道外运行——正是策略的入场窗口。
3.2 持仓状态

三档持仓清晰可见。统计一下时间占比(实际输出):
| 持仓 | 时间占比 |
|---|---|
| +1(做多) | 41.1% |
| -1(做空) | 33.5% |
| 0(中性空仓) | 25.3% |
策略有四分之一的时间空仓——这是与双均线"永远在场"的本质区别,也是这类策略常被忽视的优点:离场本身就是风控。
4. 回测结果

实际数字:
| 指标 | 买入持有 | 均值回归策略 |
|---|---|---|
| 累计收益 | -3.5% | +8.6% |
| 年化夏普 | — | 0.04 |
和第二篇刚好反过来:2020 年后的中兴通讯是个阴跌样本,买入持有亏钱,均值回归小幅正收益但夏普只有 0.04——赚得很吃力。这个结果同样要打个问号:
- k=1 太激进,价格稍出通道就开仓,交易频繁、每次利润薄,成本敏感性极高;
- A 股做空实际不可行(融券券源有限、成本高),
position=-1的收益在实盘中拿不到,只做多版本才是可执行基准; - 单票单区间,偶然性大(对比第六篇批量回测会更清楚)。
一个重要的对照实验
把同样的代码跑在 2020 年之前的中兴通讯数据上,或者在强趋势标的(如 2024-2025 的 BTC)上,均值回归会被反复"逆势开仓、越亏越扛"——均值回归策略在趋势行情中天然亏钱,趋势策略在震荡行情中天然亏钱,两者互为镜像。所以成熟的组合通常同时配置两类策略,让它们互相对冲行情风格的切换。
5. 参数讨论
| 参数 | 作用 | 太小 | 太大 |
|---|---|---|---|
| N(窗口) | 定义"均值"与"波动"的参考系 | 通道跟价格贴太近,噪声即触发 | 反应迟钝,错过回归 |
| k(倍数) | 定义"极端"的门槛 | 频繁开仓、利润被成本吃光 | 几乎不开仓,策略失效 |
第二篇的网格搜索方法在这里同样适用(扫 N × k 二维网格),不再重复。唯一提醒:均值回归策略的绩效对交易成本的敏感度通常比趋势策略高一个数量级,网格搜索时请把成本加进去再排名。
6. 小结与练习
- 均值回归 = 极端偏离入场 + 回归均值离场,持仓三态(+1 / 0 / -1)是与趋势策略的核心差异;
np.where链 +ffill()是向量化状态机的标准写法;- 通道外运行 = 入场信号,
distance变号 = 平仓信号; - 策略风格决定行情适配:趋势策略怕震荡,均值回归怕单边;
- 做空假设在 A 股语境下几乎不可执行,回测结论要打折。
练习:
- (基础)把 k 改成 2,观察开仓频率和收益结构的变化;
- (进阶)实现"只做多"版本:
position = np.where(close < lower, 1, 回归时清零),与完整版对比; - (进阶)给策略加 0.1% 单边成本后重新计算收益(提示:
position.diff().abs() > 0的日期各扣一次); - (挑战)在
stock_data/里另选 3 只股票(一只大蓝筹、一只 ST、一只次新股)重跑本策略,总结"什么样的股票适合均值回归"。
上一篇:02 · 向量化回测:双均线策略 · 下一篇:04 · 动量效应:过去涨的还会涨吗