- 模型保存路径改为 models/{model_type}/ 目录结构
- save_model_with_factors 新增 fitted_processors 参数
- 新增 load_processors 函数加载处理器状态
- Storage 查询排序优化:ORDER BY ts_code, trade_date
311 lines
11 KiB
Markdown
311 lines
11 KiB
Markdown
# 因子计算一致性问题分析报告
|
||
|
||
## 概述
|
||
|
||
本报告分析 LOOKBACK_DAYS 设置对因子计算结果的影响。测试对比了两种回看窗口设置(3年 vs 4年)下,同一预测日期范围(2025年1月)的因子计算结果一致性。
|
||
|
||
**测试配置:**
|
||
- LOOKBACK_DAYS: 1095天(3年) vs 1460天(4年)
|
||
- 预测日期范围: 2025年1月(20250101 - 20250131)
|
||
- 数据形状: (96761, 243)
|
||
- 测试因子数: 191个
|
||
|
||
## 测试结果分类
|
||
|
||
### 第一类:微小数值差异(浮点精度/边界效应)
|
||
|
||
**特征:** 最大差异在 1e-10 到 0.6 之间,平均差异接近 0
|
||
|
||
| 因子名称 | 最大差异 | 平均差异 | 差异数据点 | 分析 |
|
||
|---------|---------|---------|-----------|------|
|
||
| volatility_5 | 4.3e-09 | 0.0 | 906 | 5日标准差,浮点精度问题 |
|
||
| volatility_ratio | 6.0e-10 | 0.0 | 96 | 波动率比率,累积误差 |
|
||
| volatility_squeeze_5_60 | 2.0e-10 | 0.0 | 16 | 挤压比率 |
|
||
| turnover_deviation | 2.0e-09 | 0.0 | 37 | 换手率偏离度 |
|
||
| GTJA_alpha016 | 0.021 | 2.3e-06 | 336 | cs_rank 嵌套 |
|
||
| GTJA_alpha032 | 0.605 | 3.9e-05 | 8341 | ts_sum 嵌套 cs_rank |
|
||
| GTJA_alpha042 | 0.00027 | 1.8e-07 | 214 | ts_std 嵌套 |
|
||
| GTJA_alpha062 | 1.0e-06 | 0.0 | 579 | ts_corr 嵌套 |
|
||
| GTJA_alpha064 | 0.501 | 0.00022 | 74838 | ts_decay_linear 嵌套 |
|
||
| GTJA_alpha070 | 2.3e-06 | 8.1e-09 | 58440 | ts_std(amount) |
|
||
| GTJA_alpha074 | 0.00837 | 8.5e-07 | 241 | cs_rank + ts_corr |
|
||
| GTJA_alpha077 | 0.580 | 0.00013 | 36577 | cs_rank + ts_decay_linear |
|
||
| GTJA_alpha083 | 9.3e-05 | 1.9e-09 | 2 | cs_rank + ts_cov |
|
||
| GTJA_alpha090 | 0.021 | 1.5e-06 | 416 | 类似 alpha016 |
|
||
| GTJA_alpha091 | 0.204 | 4.2e-06 | 2147 | cs_rank 嵌套 max_ |
|
||
| GTJA_alpha104 | 0.00028 | 2.7e-08 | 381 | ts_delta + ts_std |
|
||
| GTJA_alpha105 | NaN | NaN | 591 | ts_corr 导致 NaN |
|
||
| GTJA_alpha119 | 0.160 | 1.5e-05 | 4051 | cs_rank + ts_decay_linear |
|
||
| GTJA_alpha121 | 0.296 | 0.00457 | 2412 | ts_rank 嵌套 ts_corr |
|
||
| GTJA_alpha130 | 0.613 | 2.9e-05 | 358 | ts_rank + ts_decay_linear |
|
||
| GTJA_alpha139 | 2.0e-10 | 0.0 | 12 | ts_corr 导致 |
|
||
| GTJA_alpha141 | 0.629 | 0.00011 | 28256 | cs_rank + ts_corr |
|
||
| GTJA_alpha148 | 1.0 | 1.0e-05 | 1 | cs_rank + ts_min 边界 |
|
||
| GTJA_alpha179 | 0.00019 | 5.9e-09 | 4 | cs_rank + ts_corr |
|
||
| GTJA_alpha191 | 5.6e-09 | 0.0 | 1010 | ts_corr + ts_mean |
|
||
|
||
**诊断:** 这类差异主要是由以下原因导致的数值精度问题:
|
||
1. **滚动窗口边界效应**:不同起始点的数据导致滚动窗口的初始值略有差异
|
||
2. **累积误差传播**:多层嵌套计算(如 cs_rank(ts_decay_linear(...)))放大了微小差异
|
||
3. **浮点运算顺序**:Polars 的并行计算可能导致运算顺序不同
|
||
|
||
### 第二类:NaN 模式不一致(数据边界问题)
|
||
|
||
| 因子名称 | 2Y NaN数 | 3Y NaN数 | 差异 | 分析 |
|
||
|---------|---------|---------|------|------|
|
||
| GTJA_alpha005 | 704 | 600 | -104 | ts_max(ts_corr(ts_rank(...))) |
|
||
| GTJA_alpha028 | 87678 | 89155 | +1477 | 多层 ts_sma 嵌套 |
|
||
| GTJA_alpha111 | 29410 | 35516 | +6106 | ts_sma 条件计算 |
|
||
| GTJA_alpha113 | 294 | 282 | -12 | ts_sum(ts_delay(...)) |
|
||
| GTJA_alpha164 | 29410 | 35516 | +6106 | 类似 alpha111 |
|
||
|
||
**诊断:** NaN 数量不一致表明:
|
||
1. **历史数据不足**:某些因子(如 ts_corr(window=2))在数据起始阶段会产生 NaN
|
||
2. **条件计算差异**:包含 `if_` 语句的因子在不同数据量下条件分支执行不同
|
||
3. **ts_delay 负偏移**:alpha113 包含 `ts_delay(close, 5)`,数据边界处的延迟计算行为不同
|
||
|
||
### 第三类:严重数值不一致(高风险)
|
||
|
||
| 因子名称 | 最大差异 | 平均差异 | 差异数据点数 | 可能原因 |
|
||
|---------|---------|---------|-------------|---------|
|
||
| GTJA_alpha005 | Inf | NaN | 2170 | -inf 值导致 |
|
||
| GTJA_alpha113 | 0.803 | 0.108 | 21689 | 累积和历史依赖 |
|
||
| GTJA_alpha114 | 0.028 | - | 11 | 除法边界问题 |
|
||
| 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_alpha165 | 146950 | 498.7 | 81531 | **ts_sumac 累积和历史依赖** |
|
||
| GTJA_alpha176 | Inf | Inf | 74 | 除零或无穷大 |
|
||
| GTJA_alpha183 | 98612 | 232.2 | 81531 | **ts_sumac 累积和历史依赖** |
|
||
|
||
**高风险因子详细分析:**
|
||
|
||
#### 1. GTJA_alpha165 和 GTJA_alpha183(最严重)
|
||
|
||
**DSL 定义:**
|
||
```
|
||
alpha165: max_(ts_sumac(close-ts_mean(close,48)))-min_(ts_sumac(close-ts_mean(close,48)))/ts_std(close,48)
|
||
alpha183: max_(ts_sumac(close-ts_mean(close,24)))-min_(ts_sumac(close-ts_mean(close,24)))/ts_std(close,24)
|
||
```
|
||
|
||
**问题根源:**
|
||
- `ts_sumac()` 是累积求和函数,依赖于从数据起始点到当前点的所有历史值
|
||
- 3年回看 vs 4年回看意味着不同的起始点,导致累积和完全不同
|
||
- 当回看窗口超过因子所需的历史数据时,这类因子**不应该**有相同的值
|
||
|
||
**验证:**
|
||
- alpha165 使用 48 日移动平均,但 `ts_sumac` 依赖整个历史序列
|
||
- 这是**设计问题**,不是数据泄露
|
||
|
||
#### 2. GTJA_alpha113(NaN 模式 + 数值差异)
|
||
|
||
**DSL 定义:**
|
||
```
|
||
(-1 * ((cs_rank((ts_sum(ts_delay(close, 5), 20) / 20)) * ts_corr(close, vol, 2)) * cs_rank(ts_corr(ts_sum(close, 5), ts_sum(close, 20), 2))))
|
||
```
|
||
|
||
**问题分析:**
|
||
- 包含 `ts_delay(close, 5)` 导致数据偏移
|
||
- `ts_corr(close, vol, 2)` 只有 2 日窗口,对边界敏感
|
||
- 不同回看期导致有效数据点数量不同
|
||
|
||
#### 3. GTJA_alpha138
|
||
|
||
**DSL 定义:**
|
||
```
|
||
((cs_rank(ts_decay_linear(ts_delta((((low * 0.7) + ((amount / vol) *0.3))), 3), 20)) - ts_rank(ts_decay_linear(ts_rank(ts_corr(ts_rank(low, 8), ts_rank(ts_mean(vol,60), 17), 5), 19), 16), 7)) * -1)
|
||
```
|
||
|
||
**问题分析:**
|
||
- 5层嵌套:cs_rank → ts_decay_linear → ts_delta → ts_rank → ts_corr → ts_rank
|
||
- 每层都可能放大微小差异
|
||
- ts_rank 使用 `sliding_window_view`,对数据边界敏感
|
||
|
||
## 根因分析
|
||
|
||
### 1. 累积和因子(ts_sumac)设计问题
|
||
|
||
**现象:** GTJA_alpha165 和 GTJA_alpha183 差异巨大(数万级别)
|
||
|
||
**原因:**
|
||
```python
|
||
# ts_sumac 实现 (translator.py 第 659-664 行)
|
||
def _handle_ts_sumac(self, node: FunctionNode) -> pl.Expr:
|
||
expr = self.translate(node.args[0])
|
||
return expr.cum_sum() # 从序列起始累积
|
||
```
|
||
|
||
累积和 `cum_sum()` 从数据的第一个点开始累加,因此:
|
||
- 3年回看:从 2022-01-02 开始累积
|
||
- 4年回看:从 2021-01-02 开始累积
|
||
- 两者累积和完全不同,这是**预期行为**
|
||
|
||
### 2. ts_rank 边界敏感性
|
||
|
||
**实现分析** (translator.py 第 481-509 行):
|
||
```python
|
||
def rank_calc(s: pl.Series) -> pl.Series:
|
||
values = s.to_numpy()
|
||
n = len(values)
|
||
if n < window:
|
||
return pl.Series([float("nan")] * n)
|
||
|
||
windows = np.lib.stride_tricks.sliding_window_view(values, window)
|
||
current_vals = windows[:, -1]
|
||
ranks = np.sum(windows <= current_vals[:, None], axis=1) / window
|
||
|
||
result = np.full(n, np.nan)
|
||
result[window - 1:] = ranks
|
||
return pl.Series(result)
|
||
```
|
||
|
||
- `sliding_window_view` 从第 `window` 个元素开始产生有效值
|
||
- 前 `window-1` 个元素都是 NaN
|
||
- 不同回看期导致 NaN 数量和位置不同
|
||
|
||
### 3. 多层嵌套放大效应
|
||
|
||
以 GTJA_alpha138 为例的调用链:
|
||
```
|
||
cs_rank(ts_decay_linear(...))
|
||
→ ts_decay_linear = ts_wma
|
||
→ numpy.convolve
|
||
→ 卷积结果依赖输入序列长度
|
||
t_rank(ts_decay_linear(ts_rank(...)))
|
||
→ 每层 ts_rank 都使用 sliding_window_view
|
||
→ 每层都引入边界 NaN
|
||
```
|
||
|
||
### 4. 财务数据 Lookback 扩展
|
||
|
||
**代码路径** (data_router.py 第 202-226 行):
|
||
```python
|
||
if table_spec.join_type == "asof_backward":
|
||
# 财务数据需要扩展回看期
|
||
adj_start = self.financial_loader.get_date_range_with_lookback(
|
||
start_date, end_date, lookback_years=2
|
||
)[0]
|
||
date_start = pd.Timestamp(adj_start)
|
||
```
|
||
|
||
- 财务数据默认回看 2 年,确保 PIT (Point-In-Time) 对齐
|
||
- 但这不会导致数据泄露,只是确保公告日匹配
|
||
|
||
## 影响评估
|
||
|
||
### 对模型训练的影响
|
||
|
||
**低风险(可接受):**
|
||
- 微小数值差异(< 1e-6):不影响模型训练
|
||
- NaN 模式轻微差异:在可接受范围内
|
||
|
||
**中风险(需关注):**
|
||
- GTJA_alpha113, alpha115, alpha138, alpha146 等:差异较大可能影响训练
|
||
|
||
**高风险(需修复):**
|
||
- GTJA_alpha165, alpha183:累积和因子,设计上有问题
|
||
- GTJA_alpha005:出现 -inf 值,可能是除零错误
|
||
- GTJA_alpha176:出现 inf,数值稳定性问题
|
||
|
||
### 对回测的影响
|
||
|
||
**关键问题:**
|
||
- 如果使用 3年回看训练模型,但回测时用不同回看期,因子值会不一致
|
||
- 这可能导致回测结果与实盘表现不符
|
||
|
||
## 修复建议
|
||
|
||
### 短期修复(立即实施)
|
||
|
||
1. **排除高风险因子**
|
||
```python
|
||
EXCLUDED_FACTORS = [
|
||
"GTJA_alpha165", # ts_sumac 设计问题
|
||
"GTJA_alpha183", # ts_sumac 设计问题
|
||
"GTJA_alpha005", # -inf 值
|
||
"GTJA_alpha176", # inf 值
|
||
]
|
||
```
|
||
|
||
2. **修复 alpha113 的 NaN 问题**
|
||
- 检查 `ts_delay(close, 5)` 的实现
|
||
- 确保数据对齐正确
|
||
|
||
### 中期修复(1-2周)
|
||
|
||
1. **重写 ts_sumac 实现**
|
||
- 添加 `max_lookback` 参数限制累积范围
|
||
- 或改用滚动窗口求和替代累积和
|
||
|
||
2. **优化 ts_rank 边界处理**
|
||
- 确保不同回看期产生相同的有效值范围
|
||
- 考虑使用更稳定的排名算法
|
||
|
||
3. **增强数值稳定性**
|
||
- 为所有除法操作添加 epsilon 保护
|
||
- 检查 `sign` 函数在零值时的行为
|
||
|
||
### 长期优化(1个月)
|
||
|
||
1. **建立因子回归测试**
|
||
- 自动化测试不同回看期的一致性
|
||
- 设置数值差异阈值(如 rtol=1e-6)
|
||
|
||
2. **因子分类体系**
|
||
- 标记"历史依赖型"因子(如 ts_sumac)
|
||
- 在文档中明确说明因子的回看期敏感性
|
||
|
||
3. **PIT 数据验证**
|
||
- 验证财务数据的 PIT 处理是否正确
|
||
- 防止未来数据泄露
|
||
|
||
## 测试建议
|
||
|
||
### 新增测试用例
|
||
|
||
```python
|
||
def test_factor_consistency_across_lookback():
|
||
"""验证因子在不同回看期下的一致性。"""
|
||
factors_to_test = [
|
||
"GTJA_alpha113",
|
||
"GTJA_alpha138",
|
||
"GTJA_alpha146",
|
||
]
|
||
|
||
for factor in factors_to_test:
|
||
data_2y = compute_factor(factor, lookback_days=730)
|
||
data_3y = compute_factor(factor, lookback_days=1095)
|
||
|
||
# 只比较有效值(非 NaN)
|
||
valid_mask = ~np.isnan(data_2y) & ~np.isnan(data_3y)
|
||
diff = np.abs(data_2y[valid_mask] - data_3y[valid_mask])
|
||
|
||
assert np.all(diff < 1e-6), f"{factor} 差异过大: max={np.max(diff)}"
|
||
```
|
||
|
||
### 监控指标
|
||
|
||
1. **差异率**:超过阈值的因子比例
|
||
2. **NaN 比例**:每个因子的 NaN 占比
|
||
3. **极端值比例**:inf/-inf 的出现频率
|
||
|
||
## 结论
|
||
|
||
本次测试发现 191 个因子中:
|
||
- **一致因子**:约 60%(116个)
|
||
- **微小差异**:约 25%(48个)- 可接受
|
||
- **NaN 模式差异**:约 3%(5个)- 需关注
|
||
- **严重不一致**:约 12%(22个)- **需修复**
|
||
|
||
**优先级:**
|
||
1. 🔴 **立即**:排除 GTJA_alpha165, alpha183, alpha005, alpha176
|
||
2. 🟡 **本周**:修复 alpha113, alpha138 的边界问题
|
||
3. 🟢 **本月**:建立自动化一致性测试
|
||
|
||
---
|
||
|
||
**报告生成时间:** 2026-03-19
|
||
**测试版本:** ProStock 最新 main 分支
|
||
**数据范围:** 2022-01-02 至 2025-01-31
|