故事引入:弹簧与橡皮筋
想象你手里拿着一根橡皮筋,一端系着一个乒乓球。你把球拉得很远,然后松手——球会怎样?它会弹回来,回到平衡点。拉得越远,弹回来的力量越大。
金融市场中的很多价格行为就像这根橡皮筋。当某只股票因为短期恐慌被过度抛售时,价格会偏离其内在价值。虽然短期内恐慌可能继续,但从统计上看,极端偏离最终会被修正。这就是均值回归的直觉。
当然,市场不总是橡皮筋。有时候它是瀑布——一旦开始下跌,根本停不下来。所以均值回归策略最危险的时刻,就是把瀑布当成橡皮筋的时候。2015年A股股灾中,无数"抄底"的均值回归者被市场碾压,因为他们没有意识到:均值回归只在"有均值可回归"的市场中有效。
趋势跟踪和均值回归就像白天和黑夜。趋势跟踪在趋势市中赚钱,在震荡市中亏钱;均值回归在震荡市中赚钱,在趋势市中亏钱。没有哪种策略永远有效,真正成熟的量化交易者会同时拥有这两种武器,并根据市场环境切换。
核心知识:当价格想"回家"
2.1 均值回归的统计学基础
均值回归(Mean Reversion)是统计学中的一个核心概念。它的理论基础是:如果价格序列是平稳的(stationary),那么偏离均值的部分最终会收敛回均值。
判断一个价格序列是否适合均值回归,最常用的方法是ADF检验(Augmented Dickey-Fuller Test)。如果检验结果的p值小于0.05,我们就可以认为该序列是平稳的,适合均值回归策略。
但股市价格本身通常不是平稳的——它更像随机游走。所以均值回归策略通常不直接作用于价格,而是作用于价格的某种变换:
- 布林带:价格相对于移动平均线的偏离程度(Z-Score)
- RSI:价格动量的标准化指标
- 配对交易:两只相关股票的价格差(价差序列通常更平稳)
2.2 布林带均值回归策略
布林带(Bollinger Bands)是均值回归策略中最经典的工具。它由三条线组成:
中轨 = N日移动平均线
上轨 = 中轨 + K倍标准差
下轨 = 中轨 - K倍标准差
布林带的核心逻辑是:价格有95%的概率落在2倍标准差的轨道内(假设价格服从正态分布)。当价格触及上轨时,视为"超买",可能回落;当价格触及下轨时,视为"超卖",可能反弹。
flowchart LR
A[计算20日均线] --> B[计算2倍标准差轨道]
B --> C{价格上穿上轨?}
C -->|是| D[卖出/做空]
C -->|否| E{价格跌破下轨?}
E -->|是| F[买入/做多]
E -->|否| G[等待]
D --> H[价格回归中轨时平仓]
F --> H
style D fill:#e74c3c,color:#fff
style F fill:#10b981,color:#fff
style H fill:#f97316,color:#fff
2.3 布林带策略的Python实现
"""
布林带均值回归策略
逻辑:价格触及下轨时买入,触及上轨时卖出
"""
import backtrader as bt
import akshare as ak
import pandas as pd
class BollingerMeanReversion(bt.Strategy):
params = (
('period', 20), # 布林带周期
('devfactor', 2.0), # 标准差倍数
('stop_loss', 0.05), # 5%止损(防止趋势延续)
)
def __init__(self):
self.boll = bt.indicators.BollingerBands(
period=self.p.period,
devfactor=self.p.devfactor
)
self.order = None
self.entry_price = None
def next(self):
if self.order:
return
current_price = self.data.close[0]
# 止损检查
if self.position and self.entry_price:
if current_price < self.entry_price * (1 - self.p.stop_loss):
self.order = self.sell()
print(f'止损卖出 @ {current_price:.2f}')
return
# 买入信号: 价格跌破下轨
if not self.position and current_price < self.boll.lines.bot[0]:
self.order = self.buy()
print(f'买入 @ {current_price:.2f} (下轨: {self.boll.lines.bot[0]:.2f})')
# 卖出信号: 价格突破上轨 或 回归中轨上方
elif self.position and current_price > self.boll.lines.mid[0]:
self.order = self.sell()
pnl = (current_price - self.entry_price) / self.entry_price * 100
print(f'卖出 @ {current_price:.2f}, 盈亏: {pnl:.2f}%')
def notify_order(self, order):
if order.status in [order.Completed]:
if order.isbuy():
self.entry_price = order.executed.price
else:
self.entry_price = None
self.order = None
# ========== 回测设置 ==========
if __name__ == "__main__":
cerebro = bt.Cerebro()
cerebro.addstrategy(BollingerMeanReversion)
cerebro.broker.setcash(100000.0)
cerebro.broker.setcommission(commission=0.0003)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='dd')
# 请在此处添加数据加载代码
# df = ak.stock_zh_a_hist(...)
# data = bt.feeds.PandasData(dataname=df, ...)
# cerebro.adddata(data)
results = cerebro.run()
strat = results[0]
print(f'最终资金: {cerebro.broker.getvalue():.2f}')
print(f"夏普: {strat.analyzers.sharpe.get_analysis().get('sharperatio','N/A')}")
print(f"最大回撤: {strat.analyzers.dd.get_analysis().max.drawdown:.2f}%")
2.4 RSI超卖反弹策略
RSI(相对强弱指标)是另一个经典的均值回归工具。它的取值范围是0-100:
- RSI < 30:超卖区,价格可能反弹
- RSI > 70:超买区,价格可能回落
- RSI 30-70:中性区,不操作
纯粹的RSI策略有一个问题:在强趋势市中,RSI可以长期停留在超买或超卖区。比如一只妖股连续涨停,RSI可能连续20天大于80。如果你在RSI>70时就卖出,会错过后续的巨大涨幅。
改进方法是结合趋势过滤:只在价格高于200日均线时用RSI>70做空,只在价格低于200日均线时用RSI<30做多。这样可以避免在强趋势中逆势操作。
2.5 配对交易:寻找"连体婴儿"
配对交易(Pairs Trading)是均值回归策略的"高级形态"。它的核心思想是:找到两只历史上价格走势高度相关的股票,当它们的价差异常扩大时,做空贵的、买入便宜的,等价差回归时平仓。
配对交易的经典案例是可口可乐和百事可乐。两家公司处于同一行业,受相似的基本面因素影响,股价长期走势高度相关。当某一天可口可乐因为某个临时性利空暴跌,而百事没有跌时,两者的价差异常扩大——这就是配对交易者的入场信号。
"""
配对交易简化版 - 协整性检验 + 阈值交易
需要安装: pip install statsmodels
"""
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import coint
class PairsTrading:
def __init__(self, stock_a, stock_b, lookback=60, entry_z=2.0, exit_z=0.5):
"""
stock_a, stock_b: 价格序列 (pandas Series)
lookback: 计算价差的回看窗口
entry_z: 开仓阈值 (Z-Score)
exit_z: 平仓阈值 (Z-Score)
"""
self.stock_a = stock_a
self.stock_b = stock_b
self.lookback = lookback
self.entry_z = entry_z
self.exit_z = exit_z
self.hedge_ratio = None
def test_cointegration(self):
"""检验两只股票是否协整"""
score, pvalue, _ = coint(self.stock_a, self.stock_b)
return {
'coint_score': score,
'pvalue': pvalue,
'is_cointegrated': pvalue < 0.05
}
def calculate_spread(self):
"""计算价差序列 (A - hedge_ratio * B)"""
# 用线性回归计算对冲比率
self.hedge_ratio = np.polyfit(self.stock_b, self.stock_a, 1)[0]
spread = self.stock_a - self.hedge_ratio * self.stock_b
return spread
def generate_signals(self):
"""生成交易信号"""
spread = self.calculate_spread()
# 计算滚动Z-Score
mean = spread.rolling(window=self.lookback).mean()
std = spread.rolling(window=self.lookback).std()
zscore = (spread - mean) / std
signals = pd.DataFrame(index=spread.index)
signals['zscore'] = zscore
signals['spread'] = spread
# 信号逻辑
signals['long_a'] = zscore < -self.entry_z # A被低估,做多A做空B
signals['short_a'] = zscore > self.entry_z # A被高估,做空A做多B
signals['exit'] = abs(zscore) < self.exit_z # 平仓
return signals
# ========== 使用示例 ==========
if __name__ == "__main__":
# 模拟两只相关股票的价格
np.random.seed(42)
n = 500
# 创建协整的价格序列
trend = np.cumsum(np.random.normal(0, 1, n))
stock_a = 100 + trend + np.random.normal(0, 2, n)
stock_b = 80 + 0.8 * trend + np.random.normal(0, 2, n)
df = pd.DataFrame({
'A': stock_a,
'B': stock_b
})
# 运行配对交易分析
pt = PairsTrading(df['A'], df['B'])
# 检验协整性
coint_result = pt.test_cointegration()
print("=== 协整性检验 ===")
print(f"P值: {coint_result['pvalue']:.4f}")
print(f"是否协整: {coint_result['is_cointegrated']}")
# 生成信号
if coint_result['is_cointegrated']:
signals = pt.generate_signals()
print("\n=== 最近交易信号 ===")
recent_signals = signals[signals['long_a'] | signals['short_a']].tail(10)
print(recent_signals[['zscore', 'long_a', 'short_a']].to_string())
2.6 均值回归的致命陷阱
均值回归策略最大的敌人是"假回归"——你以为价格会回归,但它只是短暂停顿后继续狂奔。以下几种情况尤其危险:
- 基本面恶化:公司突然爆出财务造假,之前的"均值"已经不复存在
- 行业结构性变化:传统行业被新技术颠覆,旧的估值中枢被彻底打破
- 流动性危机:市场恐慌时,所有相关性都失效,即使是协整的配对也会 diverge
- 过度杠杆:均值回归策略通常使用高杠杆,一次"不归"就能致命
科学争议:均值回归在A股有效吗?
正方:A股散户占比高,情绪波动大,导致价格经常偏离基本面。这种"非理性波动"为均值回归策略提供了丰富的机会。特别是在蓝筹股中,均值回归效果显著。
反方:A股的"涨停跌停"制度限制了价格发现效率。一只股票跌停后可能连续跌停,根本没有"回归"的机会。而且A股的"炒题材"文化让价格可以长期偏离基本面。
量化交易者的务实态度:均值回归在A股确实有效,但需要严格的止损和仓位控制。不要试图"抄底"连续跌停的股票,只在波动率正常、流动性充足的环境中使用均值回归策略。
2.7 布林带交互式模拟器
调整参数,观察不同标准差倍数对交易频率和潜在收益的影响:
布林带参数探索器
预计触及上轨频率: 约2.3%
预计触及下轨频率: 约2.3%
适合市场环境: 震荡市
风险提示: 趋势较强,假信号风险中等
动手实践:构建你的第一个均值回归策略
下面是一个完整的、可直接运行的布林带+RSI组合策略。它同时要求价格触及布林带下轨和RSI<30才入场,双重过滤提高了信号质量:
"""
布林带+RSI双重过滤均值回归策略
"""
import backtrader as bt
class MeanReversionStrategy(bt.Strategy):
params = (
('bb_period', 20),
('bb_dev', 2.0),
('rsi_period', 14),
('rsi_oversold', 30),
('rsi_overbought', 70),
('stop_loss', 0.05),
)
def __init__(self):
self.boll = bt.indicators.BollingerBands(
period=self.p.bb_period,
devfactor=self.p.bb_dev
)
self.rsi = bt.indicators.RSI(period=self.p.rsi_period)
self.order = None
self.entry_price = None
def next(self):
if self.order:
return
price = self.data.close[0]
# 止损
if self.position and self.entry_price:
if price < self.entry_price * (1 - self.p.stop_loss):
self.order = self.sell()
return
# 双重过滤买入: 价格跌破下轨 AND RSI超卖
if not self.position:
if price < self.boll.lines.bot[0] and self.rsi[0] < self.p.rsi_oversold:
self.order = self.buy()
print(f'双重过滤买入: 价格={price:.2f}, RSI={self.rsi[0]:.1f}')
# 卖出: 价格回归中轨上方 OR RSI脱离超卖
elif self.position:
if price > self.boll.lines.mid[0] or self.rsi[0] > 50:
self.order = self.sell()
pnl = (price - self.entry_price) / self.entry_price * 100
print(f'卖出: 价格={price:.2f}, 盈亏={pnl:.2f}%')
def notify_order(self, order):
if order.status in [order.Completed]:
if order.isbuy():
self.entry_price = order.executed.price
else:
self.entry_price = None
self.order = None
if __name__ == "__main__":
cerebro = bt.Cerebro()
cerebro.addstrategy(MeanReversionStrategy)
cerebro.broker.setcash(100000.0)
cerebro.broker.setcommission(commission=0.0003)
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='dd')
print('初始资金: %.2f' % cerebro.broker.getvalue())
results = cerebro.run()
strat = results[0]
print('最终资金: %.2f' % cerebro.broker.getvalue())
print(f"夏普: {strat.analyzers.sharpe.get_analysis().get('sharperatio','N/A')}")
print(f"最大回撤: {strat.analyzers.dd.get_analysis().max.drawdown:.2f}%")