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

254 lines
7.9 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.
"""
深入分析GTJA_alpha032差异来源
GTJA_alpha032 DSL: (-1 * ts_sum(cs_rank(ts_corr(cs_rank(high), cs_rank(vol), 3)), 3))
问题该因子在不同LOOKBACK_DAYS下有8341个差异数据点max_diff=0.605
目标:分析差异来源
"""
import numpy as np
import polars as pl
from datetime import datetime, timedelta
# =============================================================================
# 配置
# =============================================================================
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 analyze_data_boundary_effect():
"""
分析数据边界效应
问题不同LOOKBACK_DAYS下边界处的ts_corr/ts_sum计算结果可能不同
"""
print("=" * 80)
print("分析GTJA_alpha032的数据边界效应")
print("=" * 80)
actual_start_2y = get_lookback_start_date(PREDICT_START, LOOKBACK_2Y)
actual_start_3y = get_lookback_start_date(PREDICT_START, LOOKBACK_3Y)
print(f"\n2Y数据起始点: {actual_start_2y}")
print(f"3Y数据起始点: {actual_start_3y}")
print(f"差异: {LOOKBACK_3Y - LOOKBACK_2Y} 天 = {1460 - 1095}")
print("\n关键发现:")
print("-" * 60)
print("ts_corr(window=3) 计算时:")
print(" - 需要当前日期 + 前2天共3天数据")
print(" - 对于预测日期20250101:")
print(" * 2Y模式下需要: 20241230, 20241231, 20250101")
print(" * 3Y模式下需要: 20241230, 20241231, 20250101")
print(" * 两者从同一预测日期向前看,需要的历史数据相同")
print(" - 但如果2Y在20221230有NA而3Y在20211230有数据结果会不同")
def analyze_cs_rank_nesting_issue():
"""
分析 cs_rank 嵌套问题
GTJA_alpha032 = -1 * ts_sum(cs_rank(ts_corr(...)), 3)
嵌套结构: cs_rank(ts_corr(...))
- ts_corr输出可能有NA值边界、数据不足等
- cs_rank(ts_corr(...)) 对截面进行排名
- 问题不同日期截面内可能有不同数量的NA值
"""
print("\n" + "=" * 80)
print("分析 cs_rank 嵌套问题")
print("=" * 80)
# 模拟截面数据:同一日期内不同股票有/无NA
print("\n场景1同一截面内有NA值")
df_with_na = pl.DataFrame(
{
"trade_date": ["20250101"] * 5,
"ts_code": ["001", "002", "003", "004", "005"],
"corr_val": [0.5, 0.6, None, 0.8, 0.9], # 003是NA
}
)
print("数据:")
print(df_with_na)
result = (
df_with_na.lazy()
.with_columns(
[
pl.col("corr_val").rank().alias("rank"),
pl.col("corr_val").count().over("trade_date").alias("count"),
]
)
.with_columns([(pl.col("rank") / pl.col("count")).alias("cs_rank")])
.collect()
)
print("\ncs_rank结果 (有NA):")
print(result.select(["ts_code", "corr_val", "rank", "count", "cs_rank"]))
# 验证004的值0.8在有NA时排名是3/4=0.75
print("\n验证: 004的0.8是第3名count=4所以cs_rank=3/4=0.75")
def analyze_ts_sum_boundary():
"""
分析 ts_sum 边界效应
GTJA_alpha032 = -1 * ts_sum(cs_rank(ts_corr(...)), 3)
ts_sum(window=3) 是时间序列求和,不是截面求和
问题不同起始点导致ts_sum在边界处的有效窗口数量不同
"""
print("\n" + "=" * 80)
print("分析 ts_sum 边界效应")
print("=" * 80)
# 创建不同起始点的数据
df1 = pl.DataFrame(
{
"trade_date": ["20250101", "20250102", "20250103", "20250104", "20250105"],
"ts_code": ["001"] * 5,
"value": [1.0, 2.0, 3.0, 4.0, 5.0],
}
)
df2 = pl.DataFrame(
{
"trade_date": [
"20241229",
"20241230",
"20241231",
"20250101",
"20250102",
"20250103",
"20250104",
"20250105",
],
"ts_code": ["001"] * 8,
"value": [-1.0, 0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
}
)
print("\n数据集1从20250101开始:")
print(df1)
print("\n数据集2从20241229开始:")
print(df2.filter(pl.col("trade_date") >= "20250101"))
# 模拟ts_sum(window=3)的结果
def rolling_sum(values, window):
result = np.full(len(values), np.nan)
for i in range(window - 1, len(values)):
result[i] = np.nansum(values[i - window + 1 : i + 1])
return result
vals1 = df1["value"].to_numpy()
vals2 = df2["value"].to_numpy()[3:] # 取相同日期部分
sum1 = rolling_sum(vals1, 3)
sum2 = rolling_sum(vals2, 3)
print("\nts_sum(window=3) 结果:")
print(f" 数据集1: {sum1}")
print(f" 数据集2: {sum2}")
print("\n分析: 对于同一日期20250105:")
print(f" 数据集1使用: [3,4,5] -> sum=12")
print(
f" 数据集2使用: [2,3,4] -> sum=9 (因为20241229=-1,20241230=0,20241231=1,20250101=2,...)"
)
print(
" 但这里使用的是相同日期的数据[1,2,3,4,5] vs [2,3,4,5,6],所以不同日期的数据本身不同"
)
def identify_root_cause():
"""
识别GTJA_alpha032差异的根本原因
结论:
1. GTJA_alpha032 = -1 * ts_sum(cs_rank(ts_corr(cs_rank(high), cs_rank(vol), 3)), 3)
2. cs_rank(high) 和 cs_rank(vol) 是截面排名
3. 如果2Y和3Y在某个日期的截面组成不同股票池差异cs_rank结果会不同
4. ts_corr(window=3) 对嵌套的cs_rank值计算相关系数
5. ts_sum(window=3) 对ts_corr结果进行滚动求和
"""
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) - 截面排名(每天独立计算)
L3: ts_corr(..., 3) - 滚动相关3日窗口
L4: cs_rank(ts_corr(...)) - 对ts_corr结果再做截面排名
L5: ts_sum(..., 3) - 滚动求和3日窗口
L6: -1 * ... - 取反
差异来源:
1. 【主要】cs_rank(high) 和 cs_rank(vol) 是截面排名
- 2Y和3Y的股票池可能在边界处有差异
- 新上市/退市股票导致截面组成不同
- 导致排名结果不同
2. 【次要】ts_corr(window=3) 的边界效应
- 不同起始点导致有效数据点不同
- 但由于window=3较小影响有限
3. 【主要】cs_rank(ts_corr(...)) 嵌套排名
- 每天对ts_corr结果再做截面排名
- 如果2Y和3Y的ts_corr值不同排名结果也不同
""")
def analyze_cross_section_composition():
"""
分析截面组成的差异
如果2Y和3Y的股票池在边界处有差异cs_rank结果会不同
"""
print("\n" + "=" * 80)
print("分析截面组成差异")
print("=" * 80)
print("""
关键问题cs_rank 是截面排名
例如对于日期20250101
- 2Y模式股票池A假设3000只
- 3Y模式股票池B假设3200只
如果股票池B包含一些股票A没有的早期股票但这些股票在20250101也存在于A
那么在20250101的截面排名中
- 2Y: 对3000只股票排名
- 3Y: 对3200只股票排名
假设股票X在2Y模式下排名第1500/3000 = 0.5
但在3Y模式下排名第1500/3200 = 0.47(因为分母变大了)
这就是 cs_rank 嵌套导致差异的根本原因!
""")
if __name__ == "__main__":
analyze_data_boundary_effect()
analyze_cs_rank_nesting_issue()
analyze_ts_sum_boundary()
identify_root_cause()
analyze_cross_section_composition()