
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 350 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
很多人学 Python 都想做量化交易,但一打开教程就被几十个指标、上百行参数劝退。其实,真正能稳定赚钱的策略,往往只用很少的逻辑。
最近看到一个非常有意思的案例:仅用 2 个指标,对 Zoetis(ZTS,辉瑞旗下分拆出来的动物保健龙头)做长线择时,从 2017 年到 2026 年,把 10 万美元变成了 30.96 万美元,总收益 209.60%,而同期买入持有只有 143.02%。更难得的是,它经过了 Walk-Forward Optimization(滚动前向优化)验证,避免了「回测好看、实盘翻车」的常见陷阱。
今天我们就用 Python 把这个策略的核心逻辑拆开讲清楚,并附上可直接运行的代码。
整个系统只用到 2 个指标:
ER 的计算非常简单:
反常识的用法:大多数人用 ER 作为趋势过滤器(高时做多,低时观望)。但本策略相反,只在 ER 处于 0.4~0.6 的中间带时入场——这是市场从震荡转向趋势的过渡区,恰恰是方向性突破的酝酿期。
HV 即对数收益率的年化滚动标准差,用来判断市场状态。当 HV 从高位向下穿越 0.30 时,意味着波动正在压缩,市场结构可能发生切换,触发离场。
一句话总结策略逻辑:
下面用 pandas 和 numpy 实现 ER 与 HV 的计算。
import numpy as np
import pandas as pd
import yfinance as yf
def kaufman_efficiency_ratio(close: pd.Series, period: int = 16) -> pd.Series:
"""
计算 Kaufman 效率比率(ER)
参数:
close: 收盘价序列
period: 计算周期,默认 16 天
返回:
ER 序列,取值范围 [0, 1]
"""
# 净价格变化(绝对值)
net_change = (close - close.shift(period)).abs()
# 累计每日波动绝对值之和
daily_change = close.diff().abs()
total_change = daily_change.rolling(window=period).sum()
# 防止分母为 0
er = net_change / total_change.replace(0, np.nan)
return er
def historical_volatility(close: pd.Series, period: int = 16) -> pd.Series:
"""
计算年化历史波动率(HV)
参数:
close: 收盘价序列
period: 计算周期,默认 16 天
返回:
年化波动率序列
"""
# 对数收益率
log_ret = np.log(close / close.shift(1))
# 滚动标准差并年化(一年约 252 个交易日)
hv = log_ret.rolling(window=period).std() * np.sqrt(252)
return hv
# 下载 Zoetis 历史数据
data = yf.download("ZTS", start="2017-01-01", end="2026-04-30", auto_adjust=True)
close = data["Close"].squeeze() # 转为一维 Series
# 计算两个指标
data["ER"] = kaufman_efficiency_ratio(close, period=16)
data["HV"] = historical_volatility(close, period=16)
print(data[["Close", "ER", "HV"]].tail())接下来根据策略规则生成持仓信号,并计算策略收益。
def generate_signals(df: pd.DataFrame,
er_low: float = 0.4,
er_high: float = 0.6,
hv_level: float = 0.3) -> pd.DataFrame:
"""
根据 ER 和 HV 生成持仓信号
入场条件:ER 落在 [er_low, er_high] 区间
离场条件:HV 由上向下穿越 hv_level
"""
df = df.copy()
# 入场信号:ER 处于中间带
entry = (df["ER"] >= er_low) & (df["ER"] <= er_high)
# 离场信号:HV 上一日 ≥ 阈值,今日 < 阈值
exit_ = (df["HV"].shift(1) >= hv_level) & (df["HV"] < hv_level)
# 按规则生成持仓状态(1 = 持有, 0 = 空仓)
position = np.zeros(len(df))
holding = 0
for i in range(len(df)):
if holding == 0 and entry.iloc[i]:
holding = 1
elif holding == 1 and exit_.iloc[i]:
holding = 0
position[i] = holding
df["position"] = position
return df
def backtest(df: pd.DataFrame) -> pd.DataFrame:
"""
简单回测:基于持仓信号计算策略累计收益
"""
df = df.copy()
# 每日收益率
df["ret"] = df["Close"].pct_change().fillna(0)
# 策略收益 = 上一期持仓 × 当期收益(避免未来函数)
df["strategy_ret"] = df["position"].shift(1).fillna(0) * df["ret"]
# 累计净值
df["strategy_nav"] = (1 + df["strategy_ret"]).cumprod()
df["buy_hold_nav"] = (1 + df["ret"]).cumprod()
return df
# 使用 2026 年最优参数(来自 Walk-Forward Optimization)
result = generate_signals(data, er_low=0.4, er_high=0.6, hv_level=0.3)
result = backtest(result)
# 打印最终业绩
final_strategy = result["strategy_nav"].iloc[-1]
final_buyhold = result["buy_hold_nav"].iloc[-1]
print(f"策略累计收益:{(final_strategy - 1) * 100:.2f}%")
print(f"买入持有累计收益:{(final_buyhold - 1) * 100:.2f}%")很多回测「漂亮的不真实」,问题就在于:用全部历史数据找到最优参数,再回到这段数据上验证——这是循环论证。
WFO 的做法是:
这样一来,每一笔交易都是在「优化器从未见过的数据」上完成的。原文做了 10 个滚动窗口,10 段样本外测试,结果显示:
更关键的是,10 个窗口里 ER 周期 16 出现了 6 次,HV 阈值 0.30 出现了 8 次——参数在不同训练区间里高度稳定,说明策略捕捉到的是真实信号,而不是过拟合噪声。
作者还跑了 1000 次 Block Bootstrap(分块自助法) 蒙特卡洛模拟(每 5 天为一个块,保留收益的自相关结构):
实测结果落在分布中位数附近,说明历史成绩不是「运气好」。下面是一个简化的 Block Bootstrap 实现示例:
def block_bootstrap(returns: np.ndarray,
block_size: int = 5,
n_simulations: int = 1000,
horizon: int = None) -> np.ndarray:
"""
分块自助法蒙特卡洛模拟
参数:
returns: 历史日收益率数组
block_size: 每个块的长度(保留自相关)
n_simulations: 模拟次数
horizon: 模拟序列长度,默认与原始相同
返回:
每次模拟的累计总收益数组
"""
if horizon is None:
horizon = len(returns)
n_blocks = horizon // block_size + 1
final_returns = np.zeros(n_simulations)
for sim in range(n_simulations):
# 随机选择若干起点,拼接出一条新路径
starts = np.random.randint(0, len(returns) - block_size, size=n_blocks)
sampled = np.concatenate([returns[s:s + block_size] for s in starts])
sampled = sampled[:horizon]
# 计算累计收益
final_returns[sim] = (1 + sampled).prod() - 1
return final_returns
# 用前面策略收益跑一次模拟
strat_ret = result["strategy_ret"].dropna().values
mc_results = block_bootstrap(strat_ret, block_size=5, n_simulations=1000)
print(f"中位数收益:{np.median(mc_results) * 100:.2f}%")
print(f"5% 分位收益:{np.percentile(mc_results, 5) * 100:.2f}%")
print(f"95% 分位收益:{np.percentile(mc_results, 95) * 100:.2f}%")抛开收益数字,这个案例真正值得学习的地方有 3 点:
风险提示也要说清楚:29 笔交易样本不算大,最大回撤 37% 实战中很难扛住,且策略只在 ZTS 单一标的上验证,迁移到其他股票需重新做 WFO。
「这个简单的 2 指标策略跑赢了 Zoetis 的买入持有」并不是魔法,而是把 ER 用作市场状态识别、把 HV 用作离场触发器的一个组合。它真正的价值不在于多赚了几十个百分点,而在于:
如果你正在学 Python 量化,不妨把这个例子作为一个小项目跑一遍,自己动手实现 ER、HV、WFO 和 Monte Carlo——相比于又看一遍教程,这才是更有效的进阶方式。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐