
用 Python 揭秘均值回归策略:你的收益从何而来?
2026年重磅升级已全面落地!欢迎加入专注财经数据与量化投研的【数据科学实战】知识星球!您将获取持续更新的《财经数据宝典》与《量化投研宝典》,双典协同提供系统化指引;星球内含 500 篇以上独有高质量文章,深度覆盖策略开发、因子分析、风险管理等核心领域,内容基本每日更新;同步推出的「量化因子专题教程」系列(含完整可运行代码与实战案例),系统详解因子构建、回测与优化全流程,并实现日更迭代。我们持续扩充独家内容资源,全方位赋能您的投研效率与专业成长。无论您是量化新手还是资深研究者,这里都是助您少走弯路、事半功倍的理想伙伴,携手共探数据驱动的投资未来!
很多刚接触量化交易的朋友,第一个学会的指标往往是 SMA(简单移动平均)或 EMA(指数移动平均)。它们简单好用,但有一个共同的「硬伤」:平滑周期是固定的。市场在快速趋势和横盘震荡之间反复切换时,固定周期的均线要么反应太慢错过行情,要么太敏感产生大量假信号(俗称「打脸」或 whipsaw)。
有没有一种均线能「自己看情况」调整快慢?答案是肯定的——它就是 John Ehlers 提出的 MESA 自适应移动平均线(MAMA,MESA Adaptive Moving Average)。
本文带你完整走一遍:用 Python 构建 MAMA/FAMA 交叉策略,跨股票、债券、商品、加密货币五类资产做回测,再用贝叶斯优化自动调参,最后客观评估它到底能不能跑赢「买入并持有」。结论可能和你想的不太一样。
普通均线假设价格是「随机但可平滑」的;而 Ehlers 的思路不同——他把价格看作「带有可变周期的部分周期信号」。
MAMA 的核心做法是:先估计市场当前的主导周期,再据此动态调整平滑速度——快市场里反应更快,慢市场里更平滑。
在量化实践中,MAMA 通常会搭配一条更慢的伴随线 FAMA(Following Adaptive Moving Average)一起使用。最经典的信号规则是:
需要提醒的是:MAMA/FAMA 的设计初衷就是「比传统均线更少打脸」,但没有任何交叉系统能完全消除假信号。所以选对标的同样关键。
文中精心挑选了 5 个标的,各有用意(这也是新手容易忽略的一步):
MAMA 之所以能感知周期,靠的是希尔伯特变换(Hilbert Transform)——一种数字信号处理(DSP)方法,它能把信号做 90 度相位偏移。
简单理解流程是这样的:
价格 → 希尔伯特变换 → 同相分量(I) + 正交分量(Q) → 瞬时相位 → 自适应 Alpha → MAMA/FAMA有了 I 和 Q 两个分量,就能算出瞬时相位:
而对 MAMA 真正重要的,不是相位本身,而是相位变化率:
MAMA 再把相位变化转换成自适应平滑系数 ,并限制在 (通常为 )区间内。 越大,均线反应越快;越小则越平滑。
一句话总结:希尔伯特变换负责「测出市场处于哪个周期」,MAMA 据此动态切换快慢。
下面按照「数据 → 信号 → 回测 → 优化 → 评估」五步走,并附带可运行的核心代码。
import requests
import pandas as pd
API_KEY = "你的 API KEY"
START_DATE = "2015-01-02"
END_DATE = "2026-05-27"
# 数据加载函数:拉取日线 OHLCV 数据
def get_data(symbol):
url = f"https://example-data-api.com/api/eod/{symbol}"
params = {
"api_token": API_KEY,
"period": "d", # 日线
"from": START_DATE,
"to": END_DATE,
"fmt": "json"
}
r = requests.get(url, params=params)
r.raise_for_status() # 出错时抛异常,方便调试
df = pd.DataFrame(r.json())
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date").sort_index() # 以日期为索引并排序
return df
spy = get_data("SPY") # 拉取标普 500 ETF 数据这里直接用 TA-Lib 计算 MAMA 与 FAMA,并衍生出价差、斜率、波动率等特征。
import numpy as np
import pandas as pd
import talib
# 计算 MAMA 与 FAMA 两条自适应均线
def mama(series, fast=0.5, slow=0.05):
mama_line, fama_line = talib.MAMA(
series.values,
fastlimit=fast, # 快速上限
slowlimit=slow # 慢速下限
)
return pd.DataFrame(
{"mama": mama_line, "fama": fama_line},
index=series.index
)
def build_strategy(df, fast, slow, slope_window, vol_filter):
df = df.copy()
ma = mama(df["price"], fast=fast, slow=slow)
df["mama"] = ma["mama"]
df["fama"] = ma["fama"]
# 价差:MAMA 与 FAMA 的相对差距(衡量趋势强度)
df["spread"] = (df["mama"] - df["fama"]) / df["price"]
# 斜率:MAMA 的变化(衡量动量方向)
df["slope"] = df["mama"].diff(slope_window) / df["price"]
# 波动率:20 日收益率滚动标准差(用于过滤高波动环境)
df["vol"] = df["price"].pct_change().rolling(20).std()
# 趋势强度评分:价差与斜率加权
strength = 5 * df["spread"] + 20 * df["slope"]
# 仓位引擎:趋势向上且波动可控时满仓,否则降低仓位
position = np.where((strength > 0) & (df["vol"] < vol_filter), 1.0, 0.25)
# 强下跌趋势中清空仓位以规避风险
position = np.where(strength < 0, 0.0, position)
df["position"] = position
return dfdef backtest(df, initial=10000):
df = df.copy()
df["ret"] = df["price"].pct_change().fillna(0) # 标的日收益
# 用前一日仓位乘以当日收益(避免未来函数)
df["strategy_ret"] = df["position"].shift(1).fillna(0) * df["ret"]
# 交易成本:以换手率 × 万分之五计费
turnover = df["position"].diff().abs().fillna(0)
df["strategy_ret"] -= turnover * 0.0005
# 构建权益曲线
df["equity"] = (1 + df["strategy_ret"]).cumprod() * initial
df["bh_equity"] = (1 + df["ret"]).cumprod() * initial # 买入并持有基准
# 夏普比率(年化)
sharpe = (df["strategy_ret"].mean() / df["strategy_ret"].std()) * np.sqrt(252)
strategy_return = df["equity"].iloc[-1] / initial - 1
bh_return = df["bh_equity"].iloc[-1] / initial - 1
alpha = strategy_return - bh_return # 相对基准的超额收益
# 最大回撤
peak = df["equity"].cummax()
max_dd = ((df["equity"] - peak) / peak).min()
return {
"sharpe": sharpe, "return": strategy_return,
"alpha": alpha, "drawdown": max_dd
}, df小贴士:
position.shift(1)这一步非常关键。用「前一日仓位」结算「今日收益」,可以避免「今天才知道信号、却假装昨天就建好仓」的未来函数陷阱,这是新手最常犯的回测错误之一。
为什么不用网格搜索(Grid Search)?因为交易系统的参数效应往往是非线性的、存在突变和不稳定区域。贝叶斯优化能「从历史试验中学习」,把更多精力放在表现好的参数区域,收敛更快。
import optuna
def objective(trial):
# 让优化器在给定范围内自动取值
fast = trial.suggest_float("fast", 0.05, 0.95)
slow = trial.suggest_float("slow", 0.01, 0.5)
slope_window = trial.suggest_int("slope_window", 2, 10)
vol_filter = trial.suggest_float("vol_filter", 0.005, 0.05)
if slow >= fast: # 约束:慢线必须慢于快线
return -999
df = build_strategy(df_base, fast, slow, slope_window, vol_filter)
metrics, _ = backtest(df)
sharpe = metrics["sharpe"]
alpha = metrics["alpha"]
dd = abs(metrics["drawdown"])
# 目标函数:重奖夏普,奖励超额收益,惩罚回撤
score = sharpe * 4 + alpha * 2 - dd
if sharpe < 1: # 夏普低于 1 的策略强力扣分
score -= 100
return score
# 运行优化,最大化得分
study = optuna.create_study(direction="maximize")
study.optimize(objective, n_trials=300)
print(study.best_params) # 输出最优参数组合用最优参数重新回测,对比策略与买入并持有的权益曲线、回撤、滚动夏普、月度收益热力图等。
这是全文最有价值的部分。我们看几个代表性资产的「策略 vs 买入并持有」对比。
SPY(标普 500)
可以看到:策略总收益略输大盘,但夏普更高、回撤几乎砍半、波动率大幅下降。换句话说——它用更小的风险,拿到了接近的收益。
TLT(长期美债)
测试期内恰逢历史级别的债券熊市,买入并持有总收益约 -33%。而策略通过大量时间持币观望(平均仓位仅 26.5%),把回撤从 -51.8% 压到 -11.5%。但代价是年化仅 0.31%,几乎等同于「现金替代品」,夏普接近 0。这也暴露了一个隐患:贝叶斯优化可能「学会了躲避风险」而非「捕捉收益」,存在过拟合嫌疑。
WTI(原油)与 BTC(比特币)
几乎所有资产上,策略的胜率都偏低(25%35%),盈利因子(Profit Factor)多在 1.021.18 之间。这说明它是典型的「趋势型系统」——靠少数几段大行情赚钱,大部分时间在小亏或持平。盈利因子越接近 1,统计优势越弱,这一点务必警惕。
读完整个研究,最重要的认知是:
对学习 Python 量化的朋友来说,这个项目最大的价值,不在于「找到一个赚钱策略」,而在于完整跑通了一条工程化流水线:数据获取 → 信号构建 → 严谨回测 → 自动调参 → 客观评估。把这条链路吃透,比记住任何一个指标都更重要。
如果你想继续深入,可以尝试把 MAMA 与传统均线(SMA、EMA)、快速响应均线(HMA、DEMA、ZLEMA)、其他自适应均线(KAMA、FRAMA)做横向对比,在滞后性、抗噪性、周期适应性三个维度上评估高下。
2026年全面升级已落地!【数据科学实战】知识星球核心权益如下:
星球已沉淀丰富内容生态——涵盖量化文章专题教程库、因子日更系列、高频数据集、PyBroker实战课程、专家深度分享与实时答疑服务。无论您是初探量化的学习者,还是深耕领域的从业者,这里都是助您少走弯路、高效成长的理想平台。诚邀加入,共探数据驱动的投资未来!
好文推荐
1. 用 Python 打造股票预测系统:Transformer 模型教程(一)
2. 用 Python 打造股票预测系统:Transformer 模型教程(二)
3. 用 Python 打造股票预测系统:Transformer 模型教程(三)
4. 用 Python 打造股票预测系统:Transformer 模型教程(完结)
6. YOLO 也能预测股市涨跌?计算机视觉在股票市场预测中的应用
9. Python 量化投资利器:Ridge、Lasso 和 Elastic Net 回归详解
好书推荐