【维克】统计套利建模:从因子到信号的完整流程

上期我们聊了统计套利的基本概念------市场存在短期定价偏差,偏差会均值回归,你从中获利。

很多读者看完留言说:"概念我懂了,但到底怎么建一个能跑的模型?"

今天就来拆解这个问题。

先讲个故事。

2007年,量化圈发生了一件大事。

当时世界上最牛的几个量化对冲基金------Renaissance、D.E. Shaw、Citadel------在同一年都遇到了同一个问题:它们的统计套利模型,突然不灵了。

市场出现了一种前所未有的状态------所有的相关性都趋向1,所有的"定价偏差"都不回归了。

为什么?因为2007年爆发了次贷危机。

在危机中,市场不是按照统计规律运行的,是按照恐惧运行的。价格偏离了合理水平,但没有回归。因为它们会偏离得更远。

Renaissance的中转基金亏了大约30%,被迫清仓。而D.E. Shaw因为风控做得好,只亏了不到8%。

这个故事的教训是什么?

统计套利模型不是建好就完了。你得知道它的边界在哪,知道什么情况下它会失效,知道怎么监控它的健康状态。

今天这篇,我们就来走一遍统计套利建模的完整流程------从定义偏离指标到实盘监控,每一步怎么操作、哪里有坑、怎么避坑。

Why:为什么建模流程这么重要?

一个残酷的现实

统计套利的建模,90%的人都会犯同一个错误------在回测里表现很好,实盘一塌糊涂。

为什么?

因为你掉进了过拟合的陷阱。

举个例子:

你跑了一组配对交易,发现Z-Score=1.8的时候开仓、0.3的时候平仓,年化收益25%,夏普比率2.5。

你觉得发现了金矿。

然后你把策略投入实盘。

三个月后,年化收益5%,夏普比率0.3。

为什么差这么多?

因为你在回测里"调"出了一个在历史数据上表现最好的参数组合。但这个参数组合,只适合那段特定的历史。到了新的市场环境中,它就不灵了。

这就是过拟合------你把噪声当成了规律。

过拟合的三种表现形式

第一种:参数过拟合

你试了100组Z-Score阈值,发现2.1开仓、0.2平仓效果最好。但你没有问自己:为什么是2.1不是2.0?这个"最优"是不是碰巧的?

第二种:样本选择过拟合

你在2015-2020年的A股数据上回测,效果很好。但你没有问自己:这段时间恰好是A股波动率比较高的时期,如果换一段低波动的时期呢?

第三种:前视偏差

你在建模的时候,用了"未来"的信息。比如你用了当前的成分股列表来构建过去的配对,但你忘了------过去的成分股和现在不一样,有些股票是后来才加进来的。

这三种错误,每一个都能让你的策略从"回测金矿"变成"实盘坟墓"。

What:建模的五个核心步骤

统计套利建模,从因子到信号,完整的流程分五步:

  1. 定义偏离指标 ------你用什么来衡量"价格偏离了合理水平"

  2. 检验均值回归 ------你衡量的这个偏离,会不会回归

  3. 确定阈值 ------偏离到什么程度开仓,回归到什么程度平仓

  4. 回测验证 ------用历史数据验证策略是否有效

  5. 实盘监控 ------投入真金白银后,持续监控策略健康度

每一步都有坑,每一步都决定了最终成败。

步骤一:定义偏离指标

你首先需要一个指标,来衡量"价格偏离了合理水平多少"。

常用的有三种方法:

方法1:Z-Score

最直觉的方法。

Z-Score = (当前价差 - 历史均值) / 历史标准差

Z-Score > 2,说明价差已经偏离历史均值两个标准差,大概率要回归。

Z-Score < -2,同理。

// python
def calculate_zscore(spread, window=60):
"""计算滚动Z-Score"""
mean = spread.rolling(window=window).mean()
std = spread.rolling(window=window).std()
zscore = (spread - mean) / std
return zscore

优点 :简单直觉,易于理解。

缺点 :假设价差服从正态分布,但实际金融数据经常有厚尾。

方法2:协整残差

更严格的方法。

先对两只股票做协整回归,得到一个长期均衡关系。然后用当前价格与均衡关系的偏离程度作为信号。

// python
import statsmodels.api as sm
from statsmodels.tsa.stattools import coint

def calculate_cointegration_residual(stock1, stock2):
"""计算协整残差"""

协整检验

score, pvalue, _ = coint(stock1, stock2)
if pvalue > 0.05:
raise ValueError("两只股票不存在协整关系")

OLS回归:stock1 = beta * stock2 + epsilon

X = sm.add_constant(stock2)
model = sm.OLS(stock1, X).fit()

残差就是偏离

residual = model.resid

return residual, model.params

优点 :统计基础更扎实,直接检验了均值回归的前提。

缺点 :计算更复杂,且假设线性关系。

方法3:相对强弱(Relative Strength)

更灵活的方法。

计算两只股票的相对强弱指标,当RS偏离历史区间时,产生信号。

// python
def calculate_relative_strength(stock1_prices, stock2_prices, window=20):
"""计算相对强弱"""

收益率比

returns1 = stock1_prices.pct_change()
returns2 = stock2_prices.pct_change()

滚动相对强弱

rs = returns1.rolling(window).mean() / returns2.rolling(window).mean()

标准化

rs_mean = rs.rolling(window=60).mean()
rs_std = rs.rolling(window=60).std()
rs_zscore = (rs - rs_mean) / rs_std

return rs, rs_zscore

优点 :不要求协整关系,适用范围更广。

缺点 :统计基础不如协整方法强。

步骤二:检验均值回归

有了偏离指标后,下一步是检验------这个偏离真的会回归吗?

如果不回归,你整个策略的基础就不存在了。

检验方法1:ADF检验(Augmented Dickey-Fuller)

ADF检验是检验时间序列是否平稳的标准方法。平稳意味着均值回归。

// python
from statsmodels.tsa.stattools import adfuller

def test_mean_reversion(spread):
"""ADF检验均值回归特性"""

ADF检验

result = adfuller(spread.dropna())

adf_stat = result0
p_value = result1
critical_values = result4

print(f"ADF统计量: {adf_stat:.4f}")
print(f"p值: {p_value:.4f}")
print(f"临界值: {critical_values}")

if p_value < 0.05:
print("✅ 序列平稳,具有均值回归特性")
return True
else:
print("❌ 序列非平稳,不具有均值回归特性")
return False

检验方法2:半衰期估计

不仅要知道"会不会回归",还要知道"多久能回归"。

// python
def estimate_half_life(spread):
"""估计均值回归的半衰期"""
spread_lag = spread.shift(1)
spread_delta = spread - spread_lag

回归:delta_spread = lambda * lag_spread + error

X = sm.add_constant(spread_lag)
model = sm.OLS(spread_delta, X).fit()
lambda_coef = model.params.iloc1

if lambda_coef >= 0:
return float('inf') # 不回归

half_life = -np.log(2) / lambda_coef
return half_life # 单位:天

半衰期告诉你:价差偏离后,平均需要多少天回归一半。

• 半衰期 < 10天:回归很快,适合短线策略

• 半衰期 10-30天:适合中短线策略

• 半衰期 > 60天:回归太慢,资金效率低,不建议

步骤三:确定开平仓阈值

知道"会回归"之后,下一个问题是------什么时候进场?什么时候出场?

// python
def optimize_thresholds(spread, zscore, transaction_cost=0.001):
"""优化开平仓阈值"""
best_sharpe = -999
best_params = None

搜索空间

entry_range = np.arange(1.5, 3.5, 0.1)
exit_range = np.arange(0.0, 1.0, 0.1)

for entry_threshold in entry_range:
for exit_threshold in exit_range:
if exit_threshold >= entry_threshold:
continue # 平仓阈值必须小于开仓阈值

模拟交易

trades = simulate_trades(zscore, entry_threshold, exit_threshold, transaction_cost)

if len(trades) < 10:
continue # 交易次数太少,统计意义不足

计算夏普比率

returns = t\['pnl' for t in trades]
sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252)

if sharpe > best_sharpe:
best_sharpe = sharpe
best_params = {
'entry_threshold': entry_threshold,
'exit_threshold': exit_threshold,
'num_trades': len(trades),
'sharpe': sharpe
}

return best_params

阈值设定的核心原则:

  1. 开仓阈值不能太低 :太低意味着频繁开仓,交易成本吃掉利润。一般Z-Score在1.5-2.5之间。

  2. 平仓阈值不能太高 :太高意味着你等回归回到"完美"才平仓,但回归可能在中途就停止了。一般Z-Score在0-0.5之间。

  3. 必须覆盖交易成本 :单次利润至少要是交易成本的3-5倍。

步骤四:回测验证

回测是验证策略是有效的最后关卡。

// python
def backtest_stat_arb(stock1_prices, stock2_prices, params,
initial_capital=1000000):
"""完整的统计套利回测"""

计算价差和Z-Score

spread = np.log(stock1_prices) - np.log(stock2_prices)
zscore = calculate_zscore(spread, window=params'window')

交易模拟

position = 0
cash = initial_capital
portfolio_values = initial_capital
trade_log = \[\]

for i in range(params'window', len(zscore)):
z = zscore.iloci
price1 = stock1_prices.iloci
price2 = stock2_prices.iloci

开仓逻辑

if position == 0:
if z > params'entry_threshold':

价差偏高,做空stock1做多stock2

position = -1
entry_spread = z
elif z < -params'entry_threshold':

价差偏低,做多stock1做空stock2

position = 1
entry_spread = z

平仓逻辑

elif position != 0:
if abs(z) < params'exit_threshold':

回归,平仓

pnl = position * (z - entry_spread) * params'position_size'
cash += pnl
trade_log.append({
'entry_z': entry_spread,
'exit_z': z,
'pnl': pnl
})
position = 0

计算组合价值

portfolio_value = cash + position * params'position_size' * (price1 - price2) / price2
portfolio_values.append(portfolio_value)

计算指标

portfolio_values = np.array(portfolio_values)
returns = np.diff(portfolio_values) / portfolio_values:-1

sharpe = np.mean(returns) / np.std(returns) * np.sqrt(252) if len(returns) > 0 else 0
max_drawdown = np.max(np.maximum.accumulate(portfolio_values) - portfolio_values) / np.max(portfolio_values)
total_return = (portfolio_values-1 / initial_capital - 1) * 100

return {
'total_return': total_return,
'sharpe_ratio': sharpe,
'max_drawdown': max_drawdown * 100,
'num_trades': len(trade_log),
'avg_pnl_per_trade': np.mean(t\['pnl' for t in trade_log]) if trade_log else 0,
'win_rate': len(t for t in trade_log if t\['pnl' > 0]) / len(trade_log) if trade_log else 0
}

回测中必须检查的指标:

|--------|----------|-------------|
| 指标 | 合格线 | 说明 |
| 年化收益率 | > 8% | 低于此不值得做 |
| 夏普比率 | > 1.5 | 低于1说明风险补偿不够 |
| 最大回撤 | < 10% | 高于15%心理承受不住 |
| 交易次数 | > 50次/年 | 太少没有统计意义 |
| 胜率 | > 55% | 低于50%策略有问题 |
| 盈亏比 | > 1.5 | 太低说明开仓阈值不对 |

步骤五:实盘监控

策略上实盘之后,不是结束,是另一个开始。

你需要持续监控以下指标:

// python
class LiveMonitor:
"""实盘监控系统"""

def init(self, expected_sharpe, expected_half_life, max_drawdown_limit=0.10):
self.expected_sharpe = expected_sharpe
self.expected_half_life = expected_half_life
self.max_drawdown_limit = max_drawdown_limit
self.rolling_window = 60 # 60天滚动窗口

def daily_check(self, portfolio_values, trade_log, zscore_current):
"""每日健康检查"""

alerts = \[\]

1. 检查实时回撤

peak = np.max(portfolio_values)
current = portfolio_values-1
current_drawdown = (peak - current) / peak

if current_drawdown > self.max_drawdown_limit:
alerts.append({
'level': 'CRITICAL',
'message': f'回撤{current_drawdown:.1%}超过限制{self.max_drawdown_limit:.1%}',
'action': '立即停止策略,全面平仓'
})
elif current_drawdown > self.max_drawdown_limit * 0.7:
alerts.append({
'level': 'WARNING',
'message': f'回撤{current_drawdown:.1%}接近预警线',
'action': '减仓50%,密切监控'
})

2. 检查滚动夏普比率

recent_returns = np.diff(portfolio_values-self.rolling_window:) / portfolio_values-self.rolling_window:-1
recent_sharpe = np.mean(recent_returns) / np.std(recent_returns) * np.sqrt(252)

if recent_sharpe < self.expected_sharpe * 0.3:
alerts.append({
'level': 'WARNING',
'message': f'滚动夏普{recent_sharpe:.2f}远低于预期{self.expected_sharpe:.2f}',
'action': '检查模型是否失效,考虑暂停策略'
})

3. 检查交易频率

recent_trades = len(t for t in trade_log if t\['date' > len(portfolio_values) - self.rolling_window])
expected_trades = self.rolling_window / self.expected_half_life * 2

if recent_trades < expected_trades * 0.3:
alerts.append({
'level': 'INFO',
'message': f'交易频率过低:{recent_trades}次 vs 预期{expected_trades:.0f}次',
'action': '检查市场状态,可能不适合当前策略'
})

4. 检查当前Z-Score

if abs(zscore_current) > 4.0:
alerts.append({
'level': 'CRITICAL',
'message': f'Z-Score={zscore_current:.2f}极端偏离,可能结构断裂',
'action': '暂停开新仓,等待Z-Score回归正常区间'
})

return alerts

How:避坑指南------建模中最常见的陷阱

陷阱一:过拟合

表现: 回测年化收益50%,实盘年化收益5%。

原因: 你在历史数据上"雕花",把每一个参数都调到了最优。但这些参数只对那段历史有效。

解决方法:

  1. 样本外测试 :把数据分成两段,前70%用来训练(调参),后30%用来测试(验证)。只有在测试段也表现好的参数,才是可靠的。

  2. 参数稳健性检验 :把参数微调±10%,如果收益变化超过20%,说明参数太"尖"了,不够稳健。

  3. 交叉验证 :把数据分成多段,轮流训练和测试。

// python
def walk_forward_validation(prices, n_splits=5):
"""滚动前推验证"""
split_size = len(prices) // (n_splits + 1)
results = \[\]

for i in range(n_splits):

训练段

train_start = i * split_size
train_end = (i + 1) * split_size

测试段

test_start = train_end
test_end = min((i + 2) * split_size, len(prices))

在训练段上优化参数

train_data = pricestrain_start:train_end
optimal_params = optimize_on_train(train_data)

在测试段上验证

test_data = pricestest_start:test_end
test_result = backtest_with_params(test_data, optimal_params)

results.append(test_result)

所有测试段的平均表现

avg_return = np.mean(r\['return' for r in results])
avg_sharpe = np.mean(r\['sharpe' for r in results])

return {
'avg_return': avg_return,
'avg_sharpe': avg_sharpe,
'consistency': len(r for r in results if r\['return' > 0]) / n_splits
}

陷阱二:前视偏差

表现: 回测用了"未来"的信息,但你自己不知道。

常见场景:

  1. 用当前成分股列表构建过去的配对(有些股票是后来才加入的)

  2. 用未来的价格计算过去的技术指标(指标公式写错了)

  3. 用了经过除权除息调整后的数据,但调整方式引入了未来信息

解决方法:

  1. Point-in-Time数据 :确保你用的每一天的数据,都是那一天能获取到的数据。

  2. 严格的时间分割 :训练和测试严格分开,不能有任何信息泄漏。

  3. 代码审查 :让另一个人检查你的代码,看有没有用到未来信息。

陷阱三:幸存者偏差

表现: 你只在"活到现在"的股票上回测,忽略了已经退市的股票。

后果: 回测结果系统性偏乐观。

解决方法: 使用包含退市股票的"全样本"数据。

陷阱四:忽略交易成本

表现: 回测不考虑手续费、滑点、冲击成本。

后果: 回测年化20%,实盘年化5%。

解决方法:

// python
def realistic_transaction_cost(trade_value, commission_rate=0.001,
slippage_rate=0.0005,
market_impact=0.0002):
"""真实交易成本计算"""
commission = trade_value * commission_rate
slippage = trade_value * slippage_rate
impact = trade_value * market_impact * (trade_value / 1e6) ** 0.5 # 大单冲击更大

total_cost = commission + slippage + impact
return total_cost

总结

今天走完了统计套利建模的完整流程。

回顾一下五个核心步骤:

  1. 定义偏离指标 :Z-Score、协整残差、相对强弱------选一个合适的"尺子"来衡量价格偏离

  2. 检验均值回归 :ADF检验 + 半衰期估计------确认偏离真的会回归,而且回归速度够快

  3. 确定阈值 :开仓阈值(Z-Score 1.5-2.5)、平仓阈值(Z-Score 0-0.5)------必须覆盖交易成本

  4. 回测验证 :年化>8%、夏普>1.5、回撤<10%、交易次数>50------用滚动前推验证稳健性

  5. 实盘监控 :回撤预警、滚动夏普、交易频率、极端Z-Score------持续监控策略健康度

三大避坑要点:

• 过拟合 :用样本外测试、参数稳健性检验、交叉验证来应对

• 前视偏差 :严格使用Point-in-Time数据,让第三方审查代码

• 交易成本 :用真实的手续费+滑点+冲击成本回测

最后送一句话:

好的统计套利模型,不是回测里最漂亮的那个,而是实盘中最稳健的那个。

下一期,我们进入波动率溢价------做多波动率也能赚钱?这是一个大多数人都不知道的策略领域。

相关推荐
全栈练习生1 小时前
RAG 准确率提升体系化方案
python·ai
Pin_Qing1 小时前
同一天,谷歌半价卖旗舰,阶跃翻倍收缓存
大数据·开发语言·人工智能·程序人生·缓存
Zentceh1 小时前
PixelClean 这类 AI 降噪算法是怎么工作的?从 RAW 域到 GAN 色彩重建的技术拆解
图像处理·人工智能·科技·算法·计算机视觉·车载系统·智能硬件
用户7624117567051 小时前
第十一章 GUI界面编程
python
天天被压力1 小时前
【Python量化实战宝典 #03】指数与板块行情接口全拆解:大盘·行业·概念一键取(Python 可跑)
java·人工智能·python
2501_930707781 小时前
使用C#代码在 Word 文档中添加、插入或删除页面
开发语言·c#·word
小马不起床1 小时前
第七阶段:数据存储与部署
数据库·python
hunterandroid1 小时前
Room 数据库迁移与多表关联查询实战
android
用户0390614707191 小时前
无感观测器理论模型(NTSM-SMO-PLL 与 SMO-PLL)
算法