feat(factor): challenger 对拍评估——三路样外 IC/ICIR/分层多空+特征贡献+池冗余注记件 [nas]
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user