
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
当大多数散户盯着热门科技股、meme 股追涨杀跌时,有一只叫 Zebra Technologies(代码 ZBRA)的中等市值公司,正在做着枯燥的条码扫描器和仓储自动化生意。谁能想到,就是这样一只"无聊"的股票,藏着一个 30 年的量化交易机会?
最近读到 Kryptera 发布的一篇策略文章,作者用 Python 构建了一套基于 TEMA + Regime RSI 的量化策略,在 1995 至 2026 年的 32 个滚动窗口中,实现了 5300% 的总收益,而同期买入持有只有 2658%。更难得的是,整套代码用的都是 pandas、numpy、yfinance、vectorbt 这些开源库,任何学 Python 的朋友都能复现。
今天这篇文章,我就带大家拆解这套策略的核心思路和 Python 实现,顺便讲讲量化回测里最容易翻车的几个坑。
这套策略只做两件事:
看起来简单,但背后的逻辑值得琢磨。
TEMA(Triple Exponential Moving Average)是在普通 EMA 基础上做了三层平滑,用来消除滞后。公式如下:
相比传统 EMA,TEMA 对价格反应更快,噪声更少。
Python 实现非常简洁:
import pandas as pd
import numpy as np
def calculate_tema(prices, period=20):
"""
计算三重指数移动平均线 TEMA
:param prices: 价格序列(pandas Series)
:param period: 平滑周期,默认 20
:return: TEMA 序列
"""
# 第一层 EMA 平滑
ema1 = prices.ewm(span=period, adjust=False).mean()
# 第二层 EMA:对 ema1 再做一次平滑
ema2 = ema1.ewm(span=period, adjust=False).mean()
# 第三层 EMA:对 ema2 再做一次平滑
ema3 = ema2.ewm(span=period, adjust=False).mean()
# TEMA 公式:减少滞后,放大趋势信号
tema = 3 * ema1 - 3 * ema2 + ema3
return tema
# 使用示例
# close 是收盘价序列
# tema = calculate_tema(close, period=20)
# 进场信号:当前 TEMA 小于 N 根 K 线之前的 TEMA
# entry_signal = tema < tema.shift(5)传统 RSI 用法是超买超卖(大于 70 卖出,小于 30 买入),但这种用法在实战中经常失效。
这套策略的创新在于:把 RSI 当作市场状态分类器,而且要求状态必须持续足够长时间才触发出场。
Python 实现如下:
def calculate_rsi(prices, period=120):
"""
计算 RSI 指标
:param prices: 价格序列
:param period: RSI 回看周期,这里用 120,属于长周期
:return: RSI 序列
"""
# 计算价格变动
delta = prices.diff()
# 上涨和下跌分别处理
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
# 指数加权平均
avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
# 计算相对强度与 RSI
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi
def regime_rsi_exit_signal(rsi, persistence=8):
"""
基于持续性的 Regime RSI 出场信号
:param rsi: RSI 序列
:param persistence: 看多状态需要连续持续的 K 线数
:return: 布尔序列,True 表示触发出场
"""
# 判断当前是否处于看多区间
bullish_now = (rsi >= 50) & (rsi <= 75)
# 前 3 根 K 线全部高于 46(确保不是短暂反弹)
prev_support = (rsi.shift(1) > 46) & (rsi.shift(2) > 46) & (rsi.shift(3) > 46)
bullish_zone = bullish_now & prev_support
# 要求看多状态连续持续 persistence 根 K 线
exit_signal = bullish_zone.rolling(persistence).sum() >= persistence
return exit_signal这里的核心思想是:进场抓短期弱势(低点),出场等确认强势(顶部前),吃掉中间那段肉最多的行情。
这是整篇文章最值得学习的部分。
很多新手写回测都会犯一个错:在全部历史数据上找"最好的参数",然后得到一份漂亮的业绩报告。结果一上实盘就亏钱,因为参数是对着过去拟合出来的,对未来没有泛化能力。
Walk-Forward Optimization(滚动窗口优化)的思路是:
本策略一共跑了 32 个滚动窗口,最终那 5300% 的收益,全部来自样本外测试,没有一根训练数据的 K 线混入其中。
Python 伪代码:
from itertools import product
def walk_forward_optimization(data, train_years=4, test_years=1):
"""
滚动窗口优化回测
:param data: 全部历史数据(含日期索引)
:param train_years: 训练窗口年数
:param test_years: 测试窗口年数
:return: 所有样本外测试结果的合集
"""
# 参数网格:RSI 回看、TEMA 周期、持续性阈值等
param_grid = {
'rsi_lookback': [80, 100, 120],
'rsi_persistence': [5, 8, 10],
'tema_period': [16, 20, 24],
'tema_shift': [3, 5, 7],
}
all_oos_results = [] # 存储所有样本外结果
years = data.index.year.unique()
# 滚动窗口
for i in range(len(years) - train_years - test_years + 1):
# 切分训练集和测试集
train_start = years[i]
train_end = years[i + train_years - 1]
test_year = years[i + train_years]
train_data = data[str(train_start):str(train_end)]
test_data = data[str(test_year):str(test_year)]
# 网格搜索最优参数
best_params = None
best_score = -np.inf
for params in product(*param_grid.values()):
param_dict = dict(zip(param_grid.keys(), params))
score = backtest(train_data, param_dict) # 在训练集评估
if score > best_score:
best_score = score
best_params = param_dict
# 用最优参数跑样本外测试
oos_result = backtest(test_data, best_params)
all_oos_results.append(oos_result)
# 拼接所有样本外结果作为最终业绩
return combine_results(all_oos_results)记住一句话:样本外表现才是真实表现,样本内漂亮的曲线都是幻觉。
作者给出了详细的业绩指标(1995 年 1 月至 2026 年 4 月):
注意:30 年只交易了 76 次,平均一年 2.5 笔。这不是高频策略,而是一个耐心游戏。
只跑一次回测不够,作者又做了 1000 次蒙特卡洛模拟(块自助法,block size=5),结果显示:
def monte_carlo_simulation(returns, n_simulations=1000, block_size=5):
"""
块自助法蒙特卡洛模拟
:param returns: 原始日收益率序列
:param n_simulations: 模拟次数
:param block_size: 块大小,保留短期自相关结构
:return: 所有模拟路径的最终收益
"""
results = []
n_blocks = len(returns) // block_size
for _ in range(n_simulations):
# 随机抽取块并拼接,保留短期相关性
sampled_blocks = []
for _ in range(n_blocks):
start = np.random.randint(0, len(returns) - block_size)
sampled_blocks.append(returns[start:start + block_size])
# 拼接成一条新的收益路径
simulated = np.concatenate(sampled_blocks)
# 累计收益
total_return = (1 + simulated).prod() - 1
results.append(total_return)
return np.array(results)
# 使用示例
# mc_results = monte_carlo_simulation(daily_returns, n_simulations=1000)
# print(f"中位数收益: {np.median(mc_results):.2%}")
# print(f"5% 分位数: {np.percentile(mc_results, 5):.2%}")这说明策略的风险调整后收益不是靠运气,而是结构性存在。
这篇策略文章给 Python 量化学习者的启发有三点:
如果你正在学 Python 量化,建议先从 pandas、vectorbt 入手,把 TEMA 和 RSI 这两个指标亲手实现一遍,再尝试搭建自己的 WFO 流程。技术不是复杂的,思路才是。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐