diff --git a/sanguo_factor/challenger_lgbm.py b/sanguo_factor/challenger_lgbm.py index 9204c60d..cea2b57f 100644 --- a/sanguo_factor/challenger_lgbm.py +++ b/sanguo_factor/challenger_lgbm.py @@ -186,12 +186,18 @@ def _read_value_parquet(path: str) -> pd.DataFrame | None: """读因子宽表 parquet;坏件/空件/索引不可转 datetime → None(codex 复审 M-5). 结构化剔除走缺源 skip 语义,不裸抛 AttributeError/ArrowInvalid. + 索引须严格升序且唯一(codex 尾批):embargo 的 shift(-2) 以位置序为地基, + 乱序/重复日期件拒收. """ try: df = pd.read_parquet(path) if df.empty or df.columns.empty: return None idx = pd.to_datetime(df.index) + if not (idx.is_monotonic_increasing and idx.is_unique): + print(f"[challenger] ⚠️ parquet 索引乱序/重复: {path} " + f"(monotonic={idx.is_monotonic_increasing}, unique={idx.is_unique})") + return None except Exception as exc: print(f"[challenger] ⚠️ parquet 不可读: {path} ({exc!r})") return None diff --git a/tests/factor/test_challenger_lgbm.py b/tests/factor/test_challenger_lgbm.py index d57e3be6..56a185f9 100644 --- a/tests/factor/test_challenger_lgbm.py +++ b/tests/factor/test_challenger_lgbm.py @@ -336,3 +336,26 @@ def test_redundancy_flags_per_day_unit(): assert ("x", "z") not in pairs and ("y", "z") not in pairs # 独立源不标 xy = next(f for f in flagged if (f["a"], f["b"]) == ("x", "y")) assert xy["corr"] == pytest.approx(1.0, abs=1e-6) + + +def test_unsorted_or_dupe_index_parquet_rejected(tmp_path, monkeypatch, capsys): + """codex 复审尾批:乱序/重复日期件 structured reject 并入缺源 skip. + + embargo 的 shift(-2) 数学以位置序为地基,乱序/重复日期件必须拒.""" + from sanguo_factor import challenger_lgbm as cl + idx = pd.date_range("2025-01-01", "2025-03-31", freq="B") + vdir = tmp_path / "vals"; vdir.mkdir() + good = pd.DataFrame(np.zeros((len(idx), 2)), index=idx, columns=["a", "b"]) + good.to_parquet(vdir / "s0.parquet") + good.iloc[::-1].to_parquet(vdir / "s1.parquet") # 乱序:日期倒排 + pd.concat([good.iloc[:1], good]).to_parquet(vdir / "s2.parquet") # 重复首日 + monkeypatch.setattr(cl, "load_pool", lambda: { + n: {"direction": "+", "weight": 1.0 / 3} for n in ("s0", "s1", "s2")}) + rc = cl.main(["--values-dir", str(vdir), "--as-of", "2025-03-31", + "--out-dir", str(tmp_path), "--vnpy-db", "fake.db"]) + assert rc == 0 + assert not (tmp_path / "challenger_lgbm").exists() + out = capsys.readouterr().out + assert "expected=3" in out and "loaded=1" in out + assert "s1" in out and "s2" in out + assert "乱序" in out