Files
ProStock/docs/factor_lookback_consistency_20260319.md
liaozhaorun ccd42082c2 refactor(experiment): 重构模型保存机制,支持 processors 持久化
- 模型保存路径改为 models/{model_type}/ 目录结构
- save_model_with_factors 新增 fitted_processors 参数
- 新增 load_processors 函数加载处理器状态
- Storage 查询排序优化:ORDER BY ts_code, trade_date
2026-03-19 21:06:11 +08:00

442 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 因子回看一致性测试问题报告
**测试日期:** 2026-03-19
**测试文件:** `tests/debug/test_lookback_consistency.py::test_simple_factor_consistency`
**测试结果:** FAILED - 33个因子不一致
---
## 1. 测试概述
### 1.1 测试目的
验证不同 LOOKBACK_DAYS回看窗口设置下同一预测日期范围的因子值是否一致。如果结果不一致可能存在以下问题
1. **数据泄露**:因子计算使用了超出合理回看期的历史数据
2. **设计缺陷**:某些因子天然依赖更长的历史数据(如累积和因子)
3. **边界效应**:滚动窗口在数据边界处的处理差异
### 1.2 测试配置
| 参数 | 值 | 说明 |
|-----|-----|-----|
| LOOKBACK_2Y | 1095天 (3年) | 较短回看窗口 |
| LOOKBACK_3Y | 1460天 (4年) | 较长回看窗口 |
| PREDICT_START | 20250101 | 预测起始日期 |
| PREDICT_END | 20250131 | 预测结束日期 |
| 2Y实际数据范围 | 20220102 - 20250131 | 3年数据 |
| 3Y实际数据范围 | 20210102 - 20250131 | 4年数据 |
### 1.3 测试结果摘要
```
数据集形状:
2Y 回看: (96761, 241)
3Y 回看: (96761, 241)
总因子数: 191
一致因子数: 158
不一致因子数: 33 (17.3%)
```
---
## 2. 不一致因子详细分类
### 2.1 分类标准
| 类别 | 标准 | 风险等级 |
|-----|-----|---------|
| 浮点精度差异 | max_diff < 1e-6 | 低 - 可忽略 |
| 边界效应差异 | 1e-6 <= max_diff < 0.01 | 中 - 需关注 |
| 数值显著差异 | 0.01 <= max_diff < 0.1 | 高 - 需修复 |
| 严重不一致 | max_diff >= 0.1 或 inf/nan | 极高 - 必须修复 |
### 2.2 严重不一致因子(必须修复)
| 因子名称 | 最大差异 | 平均差异 | 差异数据点 | 问题类型 |
|---------|---------|---------|-----------|---------|
| GTJA_alpha005 | inf | inf | 2170 | -inf值产生 |
| GTJA_alpha113 | 0.803 | 0.108 | 95337 | 累积和历史依赖 |
| GTJA_alpha115 | 0.989 | 0.0014 | 83182 | ts_rank差异传播 |
| GTJA_alpha138 | 0.857 | 0.108 | 21689 | ts_decay_linear+ts_rank |
| GTJA_alpha140 | 0.999 | 0.029 | 7535 | min_/max_边界 |
| GTJA_alpha146 | 3.719 | 0.0058 | 81526 | 复杂嵌套公式 |
| GTJA_alpha148 | 1.000 | 1e-5 | 1 | cs_rank+ts_min边界 |
| GTJA_alpha176 | inf | inf | 74 | 除零/无穷大 |
#### 2.2.1 GTJA_alpha005-inf值问题
```
差异数据点示例:
idx=56: 2Y=-0.0000000262, 3Y=-inf, diff=inf
idx=57: 2Y=-0.0000000262, 3Y=-inf, diff=inf
idx=58: 2Y=-0.4564354646, 3Y=-inf, diff=inf
```
**问题分析:**
- 3Y模式下产生-inf值说明存在除零或对负数取对数
- DSL中可能包含 `log``sqrt` 操作
#### 2.2.2 GTJA_alpha113累积和历史依赖
```
差异数据点示例:
idx=0: 2Y=0.1848, 3Y=0.9883, diff=0.8035
idx=1: 2Y=-0.1146, 3Y=-0.9890, diff=0.8744
idx=2: 2Y=0.1098, 3Y=0.9783, diff=0.8684
```
**问题分析:**
- 包含 `ts_delay(close, 5)` 导致数据偏移
- `ts_corr(close, vol, 2)` 只有2日窗口对边界敏感
#### 2.2.3 GTJA_alpha138复杂嵌套
```
差异数据点示例:
idx=3: 2Y=0.9808, 3Y=0.8380, diff=0.1429
idx=4: 2Y=0.9782, 3Y=0.8354, diff=0.1429
idx=5: 2Y=0.9738, 3Y=0.6880, diff=0.2857
idx=6: 2Y=0.9780, 3Y=0.4066, diff=0.5714
idx=7: 2Y=0.5586, 3Y=0.1300, diff=0.4286
```
**问题分析:**
- 5层嵌套`cs_rank → ts_decay_linear → ts_delta → ts_rank → ts_corr → ts_rank`
- `ts_decay_linear` 使用 `numpy.convolve`,结果依赖输入序列长度
- `ts_rank` 使用 `sliding_window_view`,对数据起始点敏感
#### 2.2.4 GTJA_alpha176无穷大值
```
差异数据点示例:
idx=948: 2Y=-0.9146, 3Y=-0.9146, diff=0.0000000001
idx=949: 2Y=-0.8809, 3Y=-0.8809, diff=0.0000000002
(差异本身很小但3Y模式下产生了inf值)
```
**问题分析:**
- 存在除零操作导致inf值
- 需要添加epsilon保护
---
### 2.3 数值显著差异因子(需修复)
| 因子名称 | 最大差异 | 平均差异 | 差异数据点 | 可能原因 |
|---------|---------|---------|-----------|---------|
| GTJA_alpha016 | 0.021 | 2.3e-6 | 336 | cs_rank嵌套 |
| GTJA_alpha032 | 0.605 | 3.9e-5 | 8341 | ts_sum嵌套cs_rank |
| GTJA_alpha077 | 0.580 | 0.00013 | 36577 | cs_rank+ts_decay_linear |
| GTJA_alpha091 | 0.204 | 4.2e-6 | 2147 | cs_rank嵌套max_ |
| GTJA_alpha121 | 0.296 | 0.00457 | 2412 | ts_rank嵌套ts_corr |
| GTJA_alpha130 | 0.613 | 2.9e-5 | 358 | ts_rank+ts_decay_linear |
| GTJA_alpha141 | 0.629 | 0.00011 | 28256 | cs_rank+ts_corr |
---
### 2.4 边界效应差异因子(需关注)
| 因子名称 | 最大差异 | 平均差异 | 差异数据点 | 问题类型 |
|---------|---------|---------|-----------|---------|
| GTJA_alpha042 | 0.00027 | 1.8e-7 | 214 | ts_std嵌套 |
| GTJA_alpha062 | 1.0e-6 | 0.0 | 579 | ts_corr嵌套 |
| GTJA_alpha064 | 0.501 | 0.00022 | 74838 | ts_decay_linear嵌套 |
| GTJA_alpha070 | 2.3e-6 | 8.1e-9 | 58440 | ts_std(amount) |
| GTJA_alpha074 | 0.00837 | 8.5e-7 | 241 | cs_rank+ts_corr |
| GTJA_alpha104 | 0.00028 | 2.7e-8 | 381 | ts_delta+ts_std |
| GTJA_alpha119 | 0.160 | 1.5e-5 | 4051 | cs_rank+ts_decay_linear |
---
### 2.5 浮点精度差异因子(可忽略)
| 因子名称 | 最大差异 | 平均差异 | 差异数据点 |
|---------|---------|---------|-----------|
| volatility_5 | 4.3e-9 | 0.0 | 906 |
| volatility_ratio | 6.0e-10 | 0.0 | 96 |
| volatility_squeeze_5_60 | 2.0e-10 | 0.0 | 16 |
| turnover_deviation | 2.0e-9 | 0.0 | 37 |
| GTJA_alpha083 | 9.3e-5 | 1.9e-9 | 2 |
| GTJA_alpha139 | 2.0e-10 | 0.0 | 12 |
| GTJA_alpha179 | 1.9e-4 | 5.9e-9 | 4 |
| GTJA_alpha191 | 5.6e-9 | 0.0 | 1010 |
---
### 2.6 NaN模式不一致因子需关注
| 因子名称 | 2Y NaN数 | 3Y NaN数 | 差异 |
|---------|---------|---------|------|
| GTJA_alpha005 | 704 | 600 | -104 |
| GTJA_alpha028 | 87678 | 89155 | +1477 |
| GTJA_alpha111 | 29410 | 35516 | +6106 |
| GTJA_alpha113 | 294 | 282 | -12 |
| GTJA_alpha164 | 29410 | 35516 | +6106 |
---
## 3. 根因分析
### 3.1 问题分类
#### 3.1.1 设计缺陷型(无法修复,只能排除)
以下因子天然依赖从数据起始点开始的累积计算,**不应该**在不同回看期下产生相同结果:
| 因子 | DSL公式 | 问题 |
|-----|--------|-----|
| GTJA_alpha165 | `ts_sumac(close-ts_mean(close,48))` | 累积和从数据起点开始 |
| GTJA_alpha183 | `ts_sumac(close-ts_mean(close,24))` | 累积和从数据起点开始 |
> 本次测试中这两个因子未出现但之前测试中差异巨大14万级别
#### 3.1.2 数值稳定性问题(需要修复)
| 问题类型 | 相关因子 | 修复方案 |
|---------|---------|---------|
| 除零导致inf | GTJA_alpha005, GTJA_alpha176 | 添加epsilon保护 |
| 负数取对数 | GTJA_alpha005 | 检查log输入是否为正 |
| 负数取平方根 | 待查 | 检查sqrt输入 |
#### 3.1.3 滚动窗口边界效应(需要优化)
以下模式会导致不同回看期下边界数据点不同:
```python
# ts_rank 实现
def rank_calc(s: pl.Series) -> pl.Series:
values = s.to_numpy()
n = len(values)
windows = np.lib.stride_tricks.sliding_window_view(values, window)
# 从第 window 个元素开始产生有效值
# 不同起始点导致滑动窗口内容不同
```
```python
# ts_decay_linear 实现
# 使用 numpy.convolve输出依赖输入序列长度
```
#### 3.1.4 cs_rank截面排名差异需要优化
`cs_rank` 对截面数据进行排名,不同回看期下:
- 有效数据点数量不同
- 排名时的百分位数分母不同
---
## 4. 修复建议
### 4.1 立即修复(高优先级)
#### 4.1.1 排除问题因子
将以下因子加入排除列表:
```python
EXCLUDED_FACTORS = [
# 设计缺陷 - 累积和因子
"GTJA_alpha165", # ts_sumac 累积和历史依赖
"GTJA_alpha183", # ts_sumac 累积和历史依赖
# 数值稳定性问题
"GTJA_alpha005", # 产生-inf值
"GTJA_alpha176", # 产生inf值
]
```
#### 4.1.2 修复数值稳定性
**修复位置:** `src/factors/translator.py`
```python
# 为除法操作添加 epsilon 保护
def _safe_divide(numerator, denominator, epsilon=1e-10):
return numerator / (denominator + epsilon)
# 为 log 函数添加输入检查
def _safe_log(x, epsilon=1e-10):
return log(x + epsilon) # 确保输入为正
# 为 sqrt 函数添加输入检查
def _safe_sqrt(x, epsilon=1e-10):
return sqrt(abs(x)) # 确保输入非负
```
### 4.2 中期优化(中优先级)
#### 4.2.1 优化 ts_rank 边界处理
**当前问题:**
- `sliding_window_view` 从第 `window` 个元素开始产生有效值
- 不同起始点导致初始NaN数量不同
**优化方案:**
```python
# 使用动态起始点对齐
def ts_rank_aligned(expr, window):
# 确保不同回看期产生相同起始点
pass
```
#### 4.2.2 优化 cs_rank 排名基准
**当前问题:**
- 排名时使用滑动窗口内的元素
- 不同回看期下窗口内元素不同
**优化方案:**
```python
def cs_rank_aligned(expr):
# 使用固定的排名基准
# 例如:固定使用当日全部股票进行排名
pass
```
### 4.3 长期改进(低优先级)
#### 4.3.1 建立因子分类体系
```python
class FactorCategory(Enum):
"""因子分类"""
TIME_SERIES_ROLLING = "ts_rolling" # 滚动窗口型(对回看期不敏感)
TIME_SERIES_CUMULATIVE = "ts_cumulative" # 累积型(对回看期敏感)
CROSS_SECTIONAL = "cs_rank" # 截面型(对回看期敏感)
HYBRID = "hybrid" # 混合型
```
#### 4.3.2 增强一致性测试
```python
def test_factor_consistency_threshold():
"""测试因子一致性阈值"""
THRESHOLDS = {
"float_precision": 1e-6, # 浮点精度差异可接受
"boundary_effect": 0.01, # 边界效应差异需关注
"significant": 0.1, # 显著差异需修复
}
```
---
## 5. 影响评估
### 5.1 对模型训练的影响
| 风险等级 | 因子数量 | 影响 |
|---------|---------|-----|
| 低 | 8 | 浮点精度差异,不影响训练 |
| 中 | 16 | 边界效应,可能轻微影响 |
| 高 | 8 | 显著不一致,会影响模型 |
| 极高 | 1 | inf/nan值必须修复 |
### 5.2 对回测的影响
**关键问题:**
- 训练时使用3年回看预测时用4年回看 → 因子值不一致
- 可能导致回测结果与实盘表现不符
**建议:**
- 训练和预测使用相同的LOOKBACK_DAYS配置
- 或在模型中记录回看期设置
---
## 6. 测试代码分析
### 6.1 测试逻辑
```python
def compute_simple_factors(lookback_days: int) -> pl.DataFrame:
actual_start = get_lookback_start_date(PREDICT_START, lookback_days)
# 从 actual_start 开始加载数据
# 计算因子
# 过滤到 PREDICT_START 之后的日期
return data
```
### 6.2 比较逻辑
```python
def compare_factor_values(data_2y, data_3y, feature_cols):
# 使用 np.allclose(valid_2y, valid_3y, rtol=1e-10, atol=1e-10)
# rtol=1e-10 相对容差
# atol=1e-10 绝对容差
```
### 6.3 断言
```python
assert results["inconsistent_factors"] == 0, (
f"发现 {results['inconsistent_factors']} 个简单因子在不同 LOOKBACK_DAYS 下结果不一致"
)
```
---
## 7. 结论
### 7.1 关键发现
1. **33个因子17.3%)在不同回看期下结果不一致**
2. **其中8个因子存在严重差异max_diff >= 0.1 或 inf/nan**
3. **问题根源包括:设计缺陷、数值稳定性、边界效应**
### 7.2 修复优先级
| 优先级 | 因子 | 行动 |
|-------|-----|-----|
| P0 | GTJA_alpha005, GTJA_alpha176 | 排除产生inf值 |
| P1 | GTJA_alpha113, GTJA_alpha138, GTJA_alpha140, GTJA_alpha146 | 修复数值稳定性 |
| P2 | 其他16个显著差异因子 | 优化滚动窗口处理 |
| P3 | 8个浮点精度因子 | 可忽略 |
### 7.3 后续步骤
1. **立即**:更新 `SELECTED_FACTORS` 排除问题因子
2. **本周**:修复 `translator.py` 中的数值稳定性问题
3. **本月**:建立因子分类和一致性测试体系
---
## 附录:完整不一致因子列表
| 序号 | 因子名称 | 最大差异 | 平均差异 | 差异数据点 | 风险等级 |
|-----|---------|---------|---------|-----------|---------|
| 1 | volatility_5 | 4.3e-09 | 0.0 | 906 | 低 |
| 2 | volatility_ratio | 6.0e-10 | 0.0 | 96 | 低 |
| 3 | volatility_squeeze_5_60 | 2.0e-10 | 0.0 | 16 | 低 |
| 4 | turnover_deviation | 2.0e-09 | 0.0 | 37 | 低 |
| 5 | GTJA_alpha005 | inf | inf | 2170 | 极高 |
| 6 | GTJA_alpha016 | 0.021 | 2.3e-06 | 336 | 高 |
| 7 | GTJA_alpha032 | 0.605 | 3.9e-05 | 8341 | 高 |
| 8 | GTJA_alpha042 | 0.00027 | 1.8e-07 | 214 | 中 |
| 9 | GTJA_alpha062 | 1.0e-06 | 0.0 | 579 | 中 |
| 10 | GTJA_alpha064 | 0.501 | 0.00022 | 74838 | 高 |
| 11 | GTJA_alpha070 | 2.3e-06 | 8.1e-09 | 58440 | 中 |
| 12 | GTJA_alpha074 | 0.00837 | 8.5e-07 | 241 | 中 |
| 13 | GTJA_alpha077 | 0.580 | 0.00013 | 36577 | 高 |
| 14 | GTJA_alpha083 | 9.3e-05 | 1.9e-09 | 2 | 低 |
| 15 | GTJA_alpha090 | 0.021 | 1.5e-06 | 416 | 高 |
| 16 | GTJA_alpha091 | 0.204 | 4.2e-06 | 2147 | 高 |
| 17 | GTJA_alpha104 | 0.00028 | 2.7e-08 | 381 | 中 |
| 18 | GTJA_alpha105 | nan | nan | 591 | 极高 |
| 19 | GTJA_alpha113 | 0.803 | 0.108 | 95337 | 极高 |
| 20 | GTJA_alpha114 | nan | nan | 11 | 极高 |
| 21 | GTJA_alpha115 | 0.989 | 0.0014 | 83182 | 极高 |
| 22 | GTJA_alpha119 | 0.160 | 1.5e-05 | 4051 | 高 |
| 23 | GTJA_alpha121 | 0.296 | 0.00457 | 2412 | 高 |
| 24 | GTJA_alpha130 | 0.613 | 2.9e-05 | 358 | 高 |
| 25 | GTJA_alpha138 | 0.857 | 0.108 | 21689 | 极高 |
| 26 | GTJA_alpha139 | 2.0e-10 | 0.0 | 12 | 低 |
| 27 | GTJA_alpha140 | 0.999 | 0.029 | 7535 | 极高 |
| 28 | GTJA_alpha141 | 0.629 | 0.00011 | 28256 | 高 |
| 29 | GTJA_alpha146 | 3.719 | 0.0058 | 81526 | 极高 |
| 30 | GTJA_alpha148 | 1.000 | 1.0e-05 | 1 | 极高 |
| 31 | GTJA_alpha176 | inf | inf | 74 | 极高 |
| 32 | GTJA_alpha179 | 0.00019 | 5.9e-09 | 4 | 低 |
| 33 | GTJA_alpha191 | 5.6e-09 | 0.0 | 1010 | 低 |