fix(factor): tears对秩类离散因子必空根治(qcut duplicates+quantiles=10) [vps]
CI/CD / test (push) Successful in 1s
CI/CD / nas-deploy (push) Successful in 6s
CI/CD / nas-verify (push) Successful in 8s

alpha16实锤(08-30用户验收factor_b7f3a2e8无图表):cs_rank(ts_cov(cs_rank,cs_rank,5))
截面值高度并列(中位日25值仅6唯一,-19.5×12/-5×9)→alphalens quantize_factor的
pd.qcut(x,q)不传duplicates(默认raise)→Bin edges must be unique→no_raise吞成全NaN
→binning丢100%(303/303日期全抛)→merged空→IC全'No valid IC values'+tears
IndexError+报告空=页面无图表。该形态下此类因子tears永远不可能成功。

修复三件:
①tears_data.py加pd.qcut duplicates='drop'幂等patch(与demean pandas2 patch同位,
_SANGUO flag防重入)——并列值并入少数分位,quantile_keys动态短化前端已容错
②analyzer get_clean调用前import tears_data激活模块级补丁(原import在IC后=补丁
晚到)+quantiles默认5→10显式传参(对齐tears页D1..D10设计与排行榜decile口径)
③build_tears_data qkeys防御:<2组多空退化零序列/0组空Series,不再IndexError

+回归测试:离散并列因子走真实alphalens binning(原测试自合成factor_quantile
绕过该路径=漏掉此bug的原因);RED→GREEN双向验证(无patch merged空(0,3)97.5%
bin丢/有patch绿)。factor+api 123绿;NAS容器30只股端到端:binning丢0.0%、
merged 7732行、tears三周期出数(ic_mean 5D=-0.0014)
This commit is contained in:
2026-08-30 09:36:21 +08:00
parent 32d17676b3
commit 8eba3e1cf1
3 changed files with 73 additions and 3 deletions
+42
View File
@@ -127,3 +127,45 @@ def test_build_tears_data_json_serializable(built):
"""端点要 FileResponse 这个 dict → 必须整棵 json 可序列化."""
import json
json.dumps(built)
# —— 离散并列因子走真实 alphalens binning(NAS 2026-08-30 alpha16 实锤回归) ——
def test_discrete_factor_binning_survives():
"""alpha16 实锤形态:cs_rank 族因子截面值高度并列(中位日 25 值仅 6 唯一,
-19.5×12/-5×9)→ alphalens quantize_factor 的 pd.qcut 不传 duplicates,
边界必重合 → ValueError 被 no_raise 吞成全 NaN → binning 丢 100%
merged 空 → IC 全空 + tears IndexError。tears_data 模块级 qcut
duplicates='drop' 补丁后:merged 非空、tears 出数、keys 动态短化不崩。"""
pytest.importorskip("alphalens")
import sanguo_factor.tears_data # noqa: F401 — 触发 pd.qcut duplicates='drop' 补丁
from alphalens.utils import get_clean_factor_and_forward_returns
from sanguo_factor.tears_data import build_tears_data
rng = np.random.default_rng(11)
n_days, n_assets = 40, 25
dates = pd.date_range("2024-01-02", periods=n_days, freq="B", name="date")
assets = [f"S{i:03d}" for i in range(n_assets)]
idx = pd.MultiIndex.from_product([dates, assets], names=["date", "asset"])
levels = np.array([-19.5, -13.0, -12.0, -11.0, -10.0, -5.0])
picks = rng.integers(0, len(levels), size=len(idx))
factor = pd.Series(levels[picks], index=idx, name="factor")
px = pd.DataFrame(
100 + np.cumsum(rng.normal(0, 1, size=(n_days, n_assets)), axis=0),
index=dates,
columns=assets,
)
merged = get_clean_factor_and_forward_returns(
factor=factor, prices=px, periods=(1,), quantiles=10, max_loss=1.0,
)
assert not merged.empty, "离散并列因子不应被 binning 丢光(修复回归)"
tears = build_tears_data(merged, periods=(1,))
d = tears["periods"]["1D"]
assert d["count"] > 0
assert d["quantile_keys"], "至少应有一组"
assert len(d["ls_nav"]) == len(d["nav_dates"])
import json
json.dumps(tears)