feat(factor): challenger 对拍评估——三路样外 IC/ICIR/分层多空+特征贡献+池冗余注记件 [nas]

This commit is contained in:
2026-10-10 12:38:24 +08:00
parent cf451466a6
commit 066a2d3b87
2 changed files with 148 additions and 0 deletions
+49
View File
@@ -1,5 +1,6 @@
"""LGBM challenger 三路对拍(spec §4.4 2026-10-10):数据层."""
import json
from pathlib import Path
import numpy as np
import pandas as pd
@@ -12,6 +13,8 @@ from sanguo_factor.challenger_lgbm import (
icir_signal,
load_pool,
lgbm_walk_forward,
run_challenge,
score_signal,
)
@@ -96,3 +99,49 @@ def test_lgbm_walk_forward_holdout_isolated():
pred, imp = lgbm_walk_forward(values, label, last_month="2025-03", params=light_params)
assert list(pred.index) == [d for d in oos if d in label.index and label.loc[d].notna().any()]
assert set(imp.keys()) == {"s0"} and imp["s0"] > 0
def test_score_signal_perfect_and_flat():
# 5 列:实现有最小截面阈值(len(pair)<5 skip),plan 原稿 3 列进不了评分
idx = pd.date_range("2025-03-03", periods=10, freq="B")
cols = ["a", "b", "c", "d", "e"]
sig = pd.DataFrame(np.linspace(-1, 1, 50).reshape(10, 5), index=idx, columns=cols)
lab = sig * 1.0 # 完美信号
s = score_signal(sig, lab)
assert s["ic_mean"] == pytest.approx(1.0, abs=1e-6)
assert s["q5q1"] > 0
flat = pd.DataFrame(0.0, index=idx, columns=cols)
s0 = score_signal(flat, lab)
assert s0["days"] == 10 # 进评分的天数(IC=NaN 但天在)
def test_run_challenge_end_to_end(tmp_path, monkeypatch):
"""端到端:合成 2 因子×40 日数据落宽表 parquet+合成 close db 太重——
本用例走 values_dir 真文件+vnpy_db 用 monkeypatch 替换拉取函数."""
from sanguo_factor import challenger_lgbm as cl
idx = pd.date_range("2025-01-01", "2025-03-31", freq="B")
cols = ["a", "b"]
rng = np.random.default_rng(3)
values = {n: pd.DataFrame(rng.normal(size=(len(idx), 2)), index=idx, columns=cols)
for n in ("s0", "s1")}
vdir = tmp_path / "factor_values"
vdir.mkdir()
for n, v in values.items():
v.to_parquet(vdir / f"{n}.parquet")
close = pd.DataFrame(100 + np.cumsum(rng.normal(scale=0.4, size=(len(idx), 2)), axis=0),
index=idx, columns=cols)
# s0/s1 不在 quant12 档案内,plan 原稿缺此 patch 必 FileNotFoundError
monkeypatch.setattr(cl, "load_pool", lambda: {
"s0": {"direction": "+", "weight": 0.6},
"s1": {"direction": "-", "weight": 0.4}})
# 真实 loader(universe.load_universe_bars)必传 start/end,签名 4 参
monkeypatch.setattr(cl, "_load_close_wide", lambda db, c, s, e: close)
out = cl.run_challenge(str(vdir), "fake.db", "2025-03-31", str(tmp_path))
doc = json.loads(Path(out).read_text())
assert doc["as_of"] == "2025-03-31"
assert set(doc["signals"]) == {"equal", "icir", "lgbm"}
for k, v in doc["signals"].items():
assert {"ic_mean", "icir", "q5q1", "days"} <= set(v)
assert doc["pool"]["names"] == ["s0", "s1"]
assert len(doc["feature_importance"]) == 2
assert doc["split"]["oos_month"] == "2025-03"