""" 验证问题2.3因子差异根本原因的测试 理论分析: 1. GTJA_alpha032: cs_rank(high)和cs_rank(vol)是截面排名 - 不同LOOKBACK_DAYS下载面股票数量可能不同(3Y包含更多历史股票) - cs_rank = rank/count,count不同导致排名分母不同 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_linear(np.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()