均值回归策略实例
所属分类 quant
浏览量 8
均值回归 量化经典策略
假设资产价格会围绕其均值波动,当价格偏离均值超过一定阈值时,会向均值回归。
本案例以 A 股股票(沪深 300 成分股)为标的,
完整覆盖「数据采集→数据清洗→特征工程→策略生成→策略评估」全流程,
并补充模拟交易 / 实盘验证的关键要点。
一、环境准备
先安装所需依赖库:
pip install tushare pandas numpy talib backtrader pyfolio matplotlib
tushare:免费获取 A 股行情数据;
talib:计算技术指标(如均线、标准差);
backtrader:策略回测框架;
pyfolio:策略绩效分析。
二、Step1:数据采集
目标
获取沪深 300 成分股的日度行情数据(OHLCV、成交量),
时间范围 2020-01-01 至 2024-12-31。
import tushare as ts
import pandas as pd
import numpy as np
# 设置Tushare token(需注册Tushare获取:https://tushare.pro/)
ts.set_token("你的Tushare Token")
pro = ts.pro_api()
# 1. 获取沪深300成分股列表
hs300 = pro.index_weight(index_code='000300.SH', start_date='20200101', end_date='20241231')
stock_codes = hs300['con_code'].unique()[:10] # 取前10只股票(减少计算量)
# 2. 批量获取股票日度行情数据
def get_stock_data(code, start, end):
df = pro.daily(ts_code=code, start_date=start, end_date=end)
# 整理字段:时间升序、复权处理(前复权)
df['trade_date'] = pd.to_datetime(df['trade_date'])
df = df.sort_values('trade_date').reset_index(drop=True)
# 补充前复权因子(Tushare需单独获取)
adj = pro.adj_factor(ts_code=code, start_date=start, end_date=end)
adj['trade_date'] = pd.to_datetime(adj['trade_date'])
df = pd.merge(df, adj[['trade_date', 'adj_factor']], on='trade_date', how='left')
# 前复权计算
for col in ['open', 'high', 'low', 'close']:
df[col] = df[col] * df['adj_factor']
return df
# 采集数据并合并
data_list = []
for code in stock_codes:
try:
df = get_stock_data(code, '20200101', '20241231')
df['stock_code'] = code
data_list.append(df)
except Exception as e:
print(f"获取{code}数据失败:{e}")
raw_data = pd.concat(data_list, ignore_index=True)
# 保存原始数据(可选)
raw_data.to_csv('hs300_stock_data.csv', index=False)
print("数据采集完成,数据量:", len(raw_data))
三、Step2:数据清洗
目标
消除缺失值、异常值,统一数据格式,保证时序一致性。
# 加载数据(若已保存)
# raw_data = pd.read_csv('hs300_stock_data.csv', parse_dates=['trade_date'])
# 1. 缺失值处理
clean_data = raw_data.copy()
# 填充缺失的复权因子(线性插值)
clean_data['adj_factor'] = clean_data.groupby('stock_code')['adj_factor'].interpolate()
# 删除关键字段(收盘价、成交量)缺失的行
clean_data = clean_data.dropna(subset=['close', 'vol'])
# 2. 异常值处理
# 3σ原则剔除收盘价异常值
def remove_outliers(group):
mean = group['close'].mean()
std = group['close'].std()
return group[(group['close'] >= mean - 3*std) & (group['close'] <= mean + 3*std)]
clean_data = clean_data.groupby('stock_code').apply(remove_outliers).reset_index(drop=True)
# 3. 剔除停牌/涨跌停数据(业务规则)
# 涨跌停判断:A股涨跌幅限制±10%(ST股±5%,此处简化)
clean_data['pct_chg'] = clean_data.groupby('stock_code')['close'].pct_change()
clean_data = clean_data[
(clean_data['pct_chg'] > -0.1) & (clean_data['pct_chg'] < 0.1) # 剔除涨跌停
& (clean_data['vol'] > 0) # 剔除停牌(成交量为0)
]
# 4. 数据标准化:时间戳格式统一、字段筛选
clean_data = clean_data[['trade_date', 'stock_code', 'open', 'high', 'low', 'close', 'vol', 'pct_chg']]
clean_data = clean_data.sort_values(['stock_code', 'trade_date']).reset_index(drop=True)
print("数据清洗完成,清洗后数据量:", len(clean_data))
print("缺失值检查:\n", clean_data.isnull().sum())
四、Step3:特征工程
目标
提取均值回归核心特征:滚动均值(均值中枢)、滚动标准差(偏离度)、Z-score(标准化偏离程度)。
import talib
# 按股票分组计算特征
def add_features(group):
# 1. 滚动均值(20日均线,均值中枢)
group['ma20'] = talib.MA(group['close'], timeperiod=20)
# 2. 滚动标准差(20日,衡量波动)
group['std20'] = talib.STDDEV(group['close'], timeperiod=20)
# 3. Z-score(偏离均值的标准化程度)
group['z_score'] = (group['close'] - group['ma20']) / group['std20']
return group
# 特征工程
feature_data = clean_data.groupby('stock_code').apply(add_features).reset_index(drop=True)
# 删除特征计算导致的缺失值(前20行无均线)
feature_data = feature_data.dropna(subset=['ma20', 'std20', 'z_score'])
print("特征工程完成,特征字段:", feature_data.columns.tolist())
print("特征数据示例:\n", feature_data[['trade_date', 'stock_code', 'close', 'ma20', 'z_score']].head())
五、Step4:策略生成
策略逻辑(均值回归核心规则)
买入条件:Z-score < -1.5(价格低于均值 1.5 个标准差,超跌);
卖出条件:Z-score > 0(价格回归至均值附近);
止损条件:买入后跌幅超过 5%(避免极端下跌);
仓位管理:单只股票仓位不超过总资金的 10%,最大持仓 10 只股票。
Backtrader 策略代码
import backtrader as bt
import warnings
warnings.filterwarnings('ignore')
# 定义均值回归策略
class MeanReversionStrategy(bt.Strategy):
# 策略参数(可优化)
params = (
('z_buy', -1.5), # 买入Z-score阈值
('z_sell', 0), # 卖出Z-score阈值
('stop_loss', 0.05),# 止损比例
('max_pos', 10), # 最大持仓数
('single_pos', 0.1) # 单只股票最大仓位
)
def __init__(self):
# 存储每只股票的Z-score和买入价
self.z_score = {}
self.buy_price = {}
for data in self.datas:
self.z_score[data] = data.z_score
self.buy_price[data] = 0
def next(self):
# 遍历所有股票
for data in self.datas:
if not self.getposition(data): # 无持仓
# 买入条件:Z-score < 买入阈值,且有足够资金
if self.z_score[data][0] < self.p.z_buy:
# 计算可买入仓位
cash = self.broker.getcash()
position_size = cash * self.p.single_pos // (data.close[0] * 100) * 100 # A股100股为1手
if position_size > 0 and len(self.positions) < self.p.max_pos:
self.buy(data=data, size=position_size)
self.buy_price[data] = data.close[0] # 记录买入价
else: # 有持仓
# 卖出条件1:Z-score回归至0以上
if self.z_score[data][0] > self.p.z_sell:
self.sell(data=data, size=self.getposition(data).size)
# 卖出条件2:止损(跌幅超过5%)
elif (self.buy_price[data] - data.close[0]) / self.buy_price[data] > self.p.stop_loss:
self.sell(data=data, size=self.getposition(data).size)
print(f"止损:{data._name},买入价{self.buy_price[data]:.2f},当前价{data.close[0]:.2f}")
# 准备Backtrader数据馈送
def prepare_bt_data(df, stock_code):
# 筛选单只股票数据(Backtrader需按股票单独馈送)
stock_df = df[df['stock_code'] == stock_code].copy()
stock_df = stock_df.set_index('trade_date')
# 转换为Backtrader数据格式
data = bt.feeds.PandasData(
dataname=stock_df,
datetime=None,
open='open',
high='high',
low='low',
close='close',
volume='vol',
openinterest=-1,
# 新增Z-score字段
addplot=True,
plotcolumns=['z_score']
)
data._name = stock_code # 标记股票代码
return data
# 初始化回测引擎
cerebro = bt.Cerebro()
# 添加多只股票数据
for code in stock_codes[:5]: # 取前5只股票(减少计算量)
stock_df = feature_data[feature_data['stock_code'] == code]
if len(stock_df) > 0:
data = prepare_bt_data(stock_df, code)
cerebro.adddata(data)
# 添加策略
cerebro.addstrategy(MeanReversionStrategy)
# 设置初始资金
cerebro.broker.setcash(1000000.0) # 初始100万
# 设置交易成本(A股:佣金0.03% + 印花税0.1%)
cerebro.broker.setcommission(commission=0.0003, stampduty=0.001)
# 设置滑点(0.05%,模拟真实交易)
cerebro.broker.set_slippage_perc(perc=0.0005)
print("初始资金:", cerebro.broker.getvalue())
六、Step5:策略评估
目标
计算核心绩效指标,验证策略盈利能力和稳定性。
# 运行回测
results = cerebro.run()
print("回测完成,最终资金:", cerebro.broker.getvalue())
# 1. 提取回测结果,计算核心指标
strategy = results[0]
# 计算年化收益率
total_return = (cerebro.broker.getvalue() - 1000000) / 1000000
years = (feature_data['trade_date'].max() - feature_data['trade_date'].min()).days / 365
annual_return = (1 + total_return) ** (1/years) - 1
# 计算最大回撤(Backtrader内置分析器)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.02) # 无风险利率2%
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade_analyzer')
# 重新运行回测(添加分析器)
cerebro = bt.Cerebro()
for code in stock_codes[:5]:
stock_df = feature_data[feature_data['stock_code'] == code]
if len(stock_df) > 0:
data = prepare_bt_data(stock_df, code)
cerebro.adddata(data)
cerebro.addstrategy(MeanReversionStrategy)
cerebro.broker.setcash(1000000.0)
cerebro.broker.setcommission(commission=0.0003, stampduty=0.001)
cerebro.broker.set_slippage_perc(perc=0.0005)
results = cerebro.run()
# 提取分析结果
drawdown = results[0].analyzers.drawdown.get_analysis()
sharpe = results[0].analyzers.sharpe.get_analysis()
trade_analyzer = results[0].analyzers.trade_analyzer.get_analysis()
# 整理评估指标
eval_metrics = {
'累计收益率': f"{total_return*100:.2f}%",
'年化收益率': f"{annual_return*100:.2f}%",
'最大回撤': f"{drawdown['max']['drawdown']:.2f}%",
'夏普比率': f"{sharpe['sharperatio']:.2f}",
'总交易次数': trade_analyzer.total.closed if 'closed' in trade_analyzer.total else 0,
'胜率': f"{(trade_analyzer.won.total / trade_analyzer.total.closed)*100:.2f}%" if trade_analyzer.total.closed >0 else "0%",
'盈亏比': f"{trade_analyzer.won.pnl.total / abs(trade_analyzer.lost.pnl.total):.2f}" if 'lost' in trade_analyzer and trade_analyzer.lost.pnl.total !=0 else "0"
}
# 打印评估结果
print("\n=== 策略评估指标 ===")
for k, v in eval_metrics.items():
print(f"{k}: {v}")
# 2. 绘制回测曲线(净值+最大回撤)
cerebro.plot(style='candlestick', iplot=False, volume=False)
七、Step6:模拟交易
关键操作(无代码,核心步骤)
对接模拟交易平台:
选择券商模拟盘(如华泰涨乐财富通、中信证券信 e 投)或量化平台模拟盘(聚宽、米筐);
将策略代码适配平台 API(如聚宽使用set_universe设置股票池,order_target_percent下单)
模拟交易设置:
同步实盘行情(使用实时行情接口,如 Tushare pro 的实时行情);
严格模拟交易成本:佣金 0.03%、印花税 0.1%、滑点 0.05%;
监控订单成交率(避免模拟盘 “秒成交”,实盘可能因流动性无法成交)
模拟交易监控:
每日记录策略信号与成交结果,对比模拟收益与回测收益的差异;
重点关注:Z-score 阈值是否合理、止损是否触发过多、仓位是否超限制
八、Step7:实盘验证
核心步骤(风险控制优先)
小资金试盘:
初始资金:总资金的 10%-20%(如 100 万资金仅投入 10-20 万);
执行逻辑:严格按策略信号下单,禁止人工干预;
监控维度:订单成交时间、滑点实际值、冲击成本(大额订单对价格的影响)
实盘优化:
若实盘收益显著低于回测 / 模拟盘:
检查滑点设置是否过低(实盘滑点可能更高);
优化 Z-score 阈值(如将买入阈值从 - 1.5 调整为 - 1.8,减少频繁交易);
增加行业分散度(避免持仓集中在单一行业)
逐步加仓:
试盘周期:至少 1-3 个月,确保策略在不同市场环境(震荡 / 单边)下稳定;
加仓规则:每月收益率稳定在 1%-2%,最大回撤 < 5%,可加仓 10%-20%
九、策略优化与风险提示
1. 策略优化方向
参数优化:
用网格搜索优化 Z-score 买入 / 卖出阈值(如 - 1.2~-2.0)、均线周期(15/20/30 日);
因子增强:
加入成交量因子(超跌 + 缩量更可靠)、行业因子(剔除强趋势行业);
多周期验证:
分样本内(2020-2022)、样本外(2023-2024)验证,避免过拟合
2. 核心风险提示
均值回归失效:
若市场出现单边趋势(如 2021 年新能源单边上涨),策略会持续亏损;
流动性风险:
沪深 300 成分股流动性较好,但小市值股票可能出现订单无法成交
参数过拟合:
若过度优化 Z-score 阈值,策略在样本外会失效;
极端行情:
黑天鹅事件(如 2020 年疫情暴跌)会导致 Z-score 偏离极值,止损触发过多
总结
本案例完整实现了均值回归策略的研发流程,核心逻辑简单且可复现,但实际应用中需重点关注:
避免过拟合(样本外验证是关键);
严格控制交易成本(滑点和手续费会显著影响收益);
策略迭代(市场风格切换时,需调整 Z-score 阈值或加入新因子)
若需适配高频均值回归策略(如分钟级数据),
可将代码中的日度数据改为分钟级,
并用 C++/Cython 优化计算速度,减少回测耗时
上一篇
下一篇
Python量化投资实战
《战胜一切市场的人》笔记
量化策略研发全流程
python @classmethod 和 @staticmethod 区别
股票多因子模型实战