Files
ProStock/tests/debug/fix_lookback_issue23/verify_root_causes.py
liaozhaorun 31b25074c3 test(debug): 添加因子回测一致性问题的调试测试套件
- 分析GTJA_alpha032等因子在不同LOOKBACK_DAYS下的差异来源
- 验证cs_rank嵌套和截面股票数量对结果的影响
- 测试ts_rank NaN处理和除法除零修复
2026-03-22 02:43:23 +08:00

289 lines
11 KiB
Python
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.
"""
验证问题2.3因子差异根本原因的测试
理论分析:
1. GTJA_alpha032: cs_rank(high)和cs_rank(vol)是截面排名
- 不同LOOKBACK_DAYS下载面股票数量可能不同3Y包含更多历史股票
- cs_rank = rank/countcount不同导致排名分母不同
2. GTJA_alpha077: ts_decay_linear + cs_rank
- ts_decay_linear使用np.convolve边界效应
- cs_rank嵌套导致排名基准变化
3. GTJA_alpha121: ts_rank嵌套ts_corr
- ts_rank使用滑动窗口边界敏感
- ts_corr的边界效应叠加
验证方法直接计算2Y和3Y数据下的截面股票数量差异
"""
import polars as pl
from datetime import datetime, timedelta
from src.factors import FactorEngine
import numpy as np
# =============================================================================
# 配置
# =============================================================================
PREDICT_START = "20250101"
PREDICT_END = "20250131"
LOOKBACK_2Y = 365 * 3
LOOKBACK_3Y = 365 * 4
def get_lookback_start_date(start_date: str, lookback_days: int) -> str:
start_dt = datetime.strptime(start_date, "%Y%m%d")
lookback_dt = start_dt - timedelta(days=lookback_days)
return lookback_dt.strftime("%Y%m%d")
def verify_cross_section_stock_count():
"""
验证不同LOOKBACK_DAYS下载面股票数量是否不同
关键问题如果3Y数据包含更多历史股票那么在相同日期D
2Y和3Y的截面股票数量可能不同导致cs_rank分母不同
"""
print("=" * 80)
print("验证截面股票数量差异")
print("=" * 80)
# 加载2Y数据
actual_start_2y = get_lookback_start_date(PREDICT_START, LOOKBACK_2Y)
engine_2y = FactorEngine()
data_2y = engine_2y.compute(
factor_names=["close"],
start_date=actual_start_2y,
end_date=PREDICT_END,
)
data_2y = data_2y.filter(data_2y["trade_date"] >= PREDICT_START)
# 加载3Y数据
actual_start_3y = get_lookback_start_date(PREDICT_START, LOOKBACK_3Y)
engine_3y = FactorEngine()
data_3y = engine_3y.compute(
factor_names=["close"],
start_date=actual_start_3y,
end_date=PREDICT_END,
)
data_3y = data_3y.filter(data_3y["trade_date"] >= PREDICT_START)
# 统计截面股票数量
stocks_per_date_2y = (
data_2y.group_by("trade_date")
.agg(pl.col("ts_code").count().alias("stock_count"))
.sort("trade_date")
)
stocks_per_date_3y = (
data_3y.group_by("trade_date")
.agg(pl.col("ts_code").count().alias("stock_count"))
.sort("trade_date")
)
print("\n2Y数据 - 每天截面股票数量:")
print(stocks_per_date_2y)
print("\n3Y数据 - 每天截面股票数量:")
print(stocks_per_date_3y)
# 比较差异
comparison = stocks_per_date_2y.join(
stocks_per_date_3y, on="trade_date", suffix="_3y"
).with_columns(
[(pl.col("stock_count_3y") - pl.col("stock_count")).alias("count_diff")]
)
print("\n股票数量差异 (3Y - 2Y):")
print(comparison)
diff_count = comparison.filter(pl.col("count_diff") != 0).height
print(f"\n有差异的日期数: {diff_count}")
if diff_count > 0:
print("结论2Y和3Y的截面股票数量确实不同")
else:
print("结论2Y和3Y的截面股票数量相同cs_rank分母应该一致")
def verify_cs_rank_formula_behavior():
"""
验证cs_rank在不同count下的行为
场景如果2Y截面有3000只股票3Y截面有3100只股票
假设股票X的rank都是1500中间位置
- 2Y: cs_rank = 1500/3000 = 0.5
- 3Y: cs_rank = 1500/3100 ≈ 0.484
"""
print("\n" + "=" * 80)
print("验证 cs_rank 在不同 count 下的行为")
print("=" * 80)
# 模拟相同rank但不同count的情况
df = pl.DataFrame(
{
"trade_date": ["20250101"] * 2,
"ts_code": ["001", "002"],
"rank": [1500.0, 1500.0], # 相同rank
"count_2y": [3000, 3000], # 2Y count
"count_3y": [3100, 3100], # 3Y count
}
)
result = df.with_columns(
[
(pl.col("rank") / pl.col("count_2y")).alias("cs_rank_2y"),
(pl.col("rank") / pl.col("count_3y")).alias("cs_rank_3y"),
]
)
print("模拟结果rank=1500的情况:")
print(
result.select(
["ts_code", "rank", "count_2y", "cs_rank_2y", "count_3y", "cs_rank_3y"]
)
)
print(f"\n差异: cs_rank_2y - cs_rank_3y = {1500 / 3000 - 1500 / 3100:.6f}")
def analyze_alpha032_deep():
"""
深入分析GTJA_alpha032 = -1 * ts_sum(cs_rank(ts_corr(cs_rank(high), cs_rank(vol), 3)), 3)
差异来源层级:
1. cs_rank(high) - 截面排名count不同导致差异
2. cs_rank(vol) - 截面排名count不同导致差异
3. ts_corr(..., 3) - 滚动相关,使用上述排名结果作为输入
4. cs_rank(ts_corr(...)) - 嵌套对ts_corr结果再做截面排名
5. ts_sum(..., 3) - 滚动求和
"""
print("\n" + "=" * 80)
print("GTJA_alpha032 深入分析")
print("=" * 80)
print("""
公式: (-1 * ts_sum(cs_rank(ts_corr(cs_rank(high), cs_rank(vol), 3)), 3))
差异传递链:
┌─────────────────────────────────────────────────────────────┐
│ L1: high, vol │
│ ↓ │
│ L2: cs_rank(high), cs_rank(vol) │
│ 问题: 2Y和3Y的截面股票数量可能不同 │
│ 例如: 股票X在2Y下cs_rank=1500/3000=0.5 │
│ 股票X在3Y下cs_rank=1500/3100=0.484 │
│ ↓ │
│ L3: ts_corr(cs_rank(high), cs_rank(vol), 3) │
│ 问题: 输入的cs_rank值已经不同ts_corr结果自然不同 │
│ ↓ │
│ L4: cs_rank(ts_corr(...)) │
│ 问题: 每天对ts_corr结果做截面排名count可能不同 │
│ ↓ │
│ L5: ts_sum(..., 3) │
│ 问题: ts_sum对L4的排名结果求和边界效应 │
│ ↓ │
│ L6: -1 * ... │
└─────────────────────────────────────────────────────────────┘
结论GTJA_alpha032的差异主要来自L2的cs_rank嵌套问题
""")
def analyze_alpha077_deep():
"""
深入分析GTJA_alpha077 = min_(cs_rank(ts_decay_linear(...)), cs_rank(ts_decay_linear(...)))
涉及ts_decay_linearnp.convolve边界效应+ cs_rank嵌套
"""
print("\n" + "=" * 80)
print("GTJA_alpha077 深入分析")
print("=" * 80)
print("""
公式: min_(cs_rank(ts_decay_linear(DECAY1)), cs_rank(ts_decay_linear(DECAY2)))
其中:
DECAY1 = ((((high + low) / 2) + high) - ((amount / vol) + high))
DECAY2 = ts_corr(((high + low) / 2), ts_mean(vol, 40), 3)
差异来源:
1. ts_decay_linear使用np.convolve
- mode='valid' 只返回完全重叠的结果
- 边界处(前window-1个)数据会是NaN
- 但2Y和3Y的边界位置相同结果应该一样
2. 真正的问题cs_rank(ts_decay_linear(...))
- 每天对ts_decay_linear结果做截面排名
- 2Y和3Y截面股票数量不同 → cs_rank分母不同 → 结果不同
结论差异主要来自cs_rank嵌套问题
""")
def analyze_alpha121_deep():
"""
深入分析GTJA_alpha121 = (cs_rank(...) ** ts_rank(ts_corr(...), 3)) * -1
涉及ts_rank滑动窗口边界敏感+ ts_corr + cs_rank嵌套
"""
print("\n" + "=" * 80)
print("GTJA_alpha121 深入分析")
print("=" * 80)
print("""
公式: ((cs_rank(((amount / vol) - min_((amount / vol), 12))) ** ts_rank(ts_corr(...), 3)) * -1)
差异来源:
1. ts_rank(ts_corr(...), 3)
- ts_rank使用sliding_window_view
- 对边界敏感:不同起始点导致边界处有效窗口数量不同
- ts_corr本身也有边界效应
2. cs_rank((amount / vol) - min_(..., 12))
- 截面排名问题
- 2Y和3Y截面股票数量不同
3. cs_rank(...) ** ts_rank(...)
- 嵌套:截面排名结果作为指数
- 两个差异来源叠加
结论差异来自ts_rank/ts_corr边界效应 + cs_rank嵌套问题
""")
def summarize_root_causes():
"""
总结问题2.7因子的根本原因
"""
print("\n" + "=" * 80)
print("问题2.3因子差异根本原因总结")
print("=" * 80)
print("""
┌─────────────────┬──────────────────────────────────────────────────────┐
│ 因子 │ 根本原因 │
├─────────────────┼──────────────────────────────────────────────────────┤
│ GTJA_alpha016 │ cs_rank嵌套ts_corr结果再做截面排名count不同 │
│ GTJA_alpha032 │ cs_rank嵌套cs_rank(high)和cs_rank(vol)截面count不同 │
│ GTJA_alpha077 │ ts_decay_linear边界效应 + cs_rank嵌套 │
│ GTJA_alpha091 │ cs_rank嵌套max_(close,5)结果再做截面排名 │
│ GTJA_alpha121 │ ts_rank/ts_corr边界效应 + cs_rank嵌套 │
│ GTJA_alpha130 │ ts_decay_linear边界效应 + cs_rank嵌套 │
│ GTJA_alpha141 │ cs_rank嵌套ts_corr结果再做截面排名 │
└─────────────────┴──────────────────────────────────────────────────────┘
核心问题cs_rank是截面排名函数当不同LOOKBACK_DAYS下截面股票数量
不同时cs_rank的分母(count)不同,导致归一化排名结果不同。
这是一个结构性问题,与具体实现无关。
""")
if __name__ == "__main__":
verify_cross_section_stock_count()
verify_cs_rank_formula_behavior()
analyze_alpha032_deep()
analyze_alpha077_deep()
analyze_alpha121_deep()
summarize_root_causes()