返回首页

时序因子回测

2026-07-21 量化实操 未标签
1137 字 6 分钟
目录

单品种期货按信号分位数分层回测方法,滚动分位数分组与绩效评估

核心思路#

与多因子选股横截面分组不同,时序因子回测针对单个品种,按信号值的时间序列分位数分组,构建多空组合。

基本流程#

信号值 → 滚动分位数 → 分组标签 → 组内等权 → 组间绩效 → 多空组合

分组方法#

1. 固定阈值分组#

组别阈值含义
组1(多头)信号 > 70%分位数强信号
组2(中性)30%~70%分位数弱信号/无信号
组3(空头)信号 < 30%分位数负信号

缺点:市场结构变化会导致各组样本量不均

2. 滚动分位数分组(推荐)#

使用滚动窗口计算分位数阈值,适应非平稳市场:

def rolling_quantile_groups(signal, window=252, n_groups=5):
"""
滚动分位数分组
- signal: 时间序列信号
- window: 滚动窗口
- n_groups: 分组数 (默认5组)
"""
quantiles = signal.rolling(window).quantile(
[i/n_groups for i in range(1, n_groups)]
)
# 每组等量 -> 按滚动分位数分配标签
groups = pd.DataFrame(index=signal.index)
for i in range(n_groups):
lower = quantiles.iloc[:, i-1] if i > 0 else -np.inf
upper = quantiles.iloc[:, i]
groups[f'G{i+1}'] = (signal >= lower) & (signal < upper)
return groups
参数选择#
参数推荐值说明
滚动窗口252天(1年)足够长来稳定分位数,又足够短适应市场变化
分组数3~53组(多/中/空)或 5组(分层更细)
最小样本观察期≥60天信号进入回测前需要预热期

绩效评估#

组内收益计算#

组收益_t = mean(品种收益_t × 信号组标签_{t-1})
  • t-1 信号 预测 t 收益
  • 组内等权(时间序列上所有信号点均值)

多空组合#

多空收益 = Group1_收益 - GroupN_收益

其中 Group1 是信号最强的组,GroupN 是信号最弱的组。

统计指标#

指标公式阈值参考
年化收益率mean × 252> 10% 有实际意义
年化波动率std × √252< 20% 可接受
夏普比率年化收益/年化波动> 1.0 优秀,> 2.0 极好
最大回撤peak-to-trough< 15% 良好
胜率正收益天数/总天数> 55% 较稳定
盈亏比平均盈利/平均亏损> 1.5

分组单调性检验#

评估信号质量的关键指标——各组绩效应呈单调排列:

期望: G1 > G2 > G3 > G4 > G5
若 G3 > G2 或 G1 < G5 → 信号存在非线性问题

单调性度量

  • Spearman相关系数:组序号与组收益的秩相关
  • Monotonic Ratio:相邻组方向一致的占比

完整回测框架示例#

import numpy as np
import pandas as pd
def timing_factor_backtest(price, signal, window=252, n_groups=5):
"""
单品种时序因子回测
Parameters
----------
price : pd.Series
期货价格序列(主连)
signal : pd.Series
信号序列(与price对齐)
window : int
滚动窗口
n_groups : int
分组数
Returns
-------
results : dict
各组的绩效统计
"""
ret = price.pct_change()
# 滚动分位数
roll_quantiles = signal.rolling(window).quantile(
[i/n_groups for i in range(1, n_groups)]
)
# 分组
group_rets = pd.DataFrame(index=ret.index, columns=range(n_groups))
for i in range(n_groups):
lower = roll_quantiles.iloc[:, i-1] if i > 0 else -np.inf
upper = roll_quantiles.iloc[:, i]
mask = (signal >= lower) & (signal < upper)
group_rets[i] = ret * mask.shift(1)
# 绩效统计
stats = {}
for g in range(n_groups):
g_ret = group_rets[g].dropna()
stats[f'G{g+1}'] = {
'ann_return': g_ret.mean() * 252,
'ann_vol': g_ret.std() * np.sqrt(252),
'sharpe': g_ret.mean() / g_ret.std() * np.sqrt(252),
'win_rate': (g_ret > 0).mean(),
'max_dd': (g_ret.cumsum() - g_ret.cumsum().cummax()).min()
}
# 多空组合
ls_ret = group_rets[0] - group_rets[n_groups-1]
stats['Long-Short'] = {
'ann_return': ls_ret.mean() * 252,
'ann_vol': ls_ret.std() * np.sqrt(252),
'sharpe': ls_ret.mean() / ls_ret.std() * np.sqrt(252),
'win_rate': (ls_ret > 0).mean(),
'max_dd': (ls_ret.cumsum() - ls_ret.cumsum().cummax()).min()
}
return stats, group_rets

与截面因子回测的区别#

维度时序因子回测截面因子回测
标的单一品种多个品种/股票
分组依据信号历史分位数信号截面排序
收益来源时间序列预测能力横截面区分能力
统计检验t检验(时序)IC/IR(截面)
样本量N天N天 × M标的

注意事项#

过拟合防范#

  • 信号构造参数不要在回测中反复调整
  • 使用 walk-forward 验证(滚动优化+固定样本外)
  • 区分样本内/样本外绩效

数据偏差#

  • 前视偏差:确保信号只用历史数据
  • 幸存者偏差:回测品种不应有退市选择(期货影响小)
  • 流动性偏差:信号产生时对应合约应可交易

常见问题#

  • 信号自相关高 → 实际交易次数少,t统计量失真 → 用Newey-West调整
  • 收益非正态 → 夏普比率可能高估 → 补充最大回撤、Calmar比率
  • 参数敏感性 → 对滚动窗口、分组数做敏感性分析