From 73ba705bca01a8ef42692c579c164e2760b4c7a7 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sat, 10 Oct 2026 10:20:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E5=9B=A0=E5=AD=90=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E7=AB=AF=E7=82=B9=E9=80=8F=E4=BC=A0=20icStats=20?= =?UTF-8?q?=E5=85=A8=E6=9C=9F=E5=BC=B9=E8=8D=AF(=E6=97=A0=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E5=A6=82=E5=AE=9E=20None)+monthly=5Fdir=20env=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_api/routes_pipeline.py | 5 ++++ tests/api/test_routes_pipeline_factors.py | 29 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/sanguo_api/routes_pipeline.py b/sanguo_api/routes_pipeline.py index ec98d886..81d23da6 100644 --- a/sanguo_api/routes_pipeline.py +++ b/sanguo_api/routes_pipeline.py @@ -605,6 +605,10 @@ def factor_detail(name: str) -> dict: birth = {"jobId": job["jobId"], "startedAt": job["startedAt"], "rounds": job["rounds"], "hypId": hyp_id} break + stats = None + reports = _monthly_reports() + if reports: + stats = (reports[0][2].get("factor_stats") or {}).get(name) return {"factor": { "name": name, "status": e["status"], "origin": e.get("origin") or "manual", "hypothesis": hyp_id, @@ -615,6 +619,7 @@ def factor_detail(name: str) -> dict: "createdAt": first.get("effective_from"), "version": str(first.get("v")) if first else "1", "icRecentT": (points.get(name) or {}).get("t"), + "icStats": stats, "lastEvalDate": last_eval}, "birth": birth} diff --git a/tests/api/test_routes_pipeline_factors.py b/tests/api/test_routes_pipeline_factors.py index f6b2ed5a..84d31bd7 100644 --- a/tests/api/test_routes_pipeline_factors.py +++ b/tests/api/test_routes_pipeline_factors.py @@ -126,3 +126,32 @@ def test_graveyard_factor_list_endpoint(client, tmp_path): assert got["fa_old"]["cause"] == "IC 两窗反号(09-22 终判)" # 活跃因子不出现在墓园列表 assert "fa_gross_margin" not in got + + +def test_factor_detail_ic_stats(client, tmp_path, monkeypatch): + """detail 端点带出 factor_stats(全期弹药),无报告时如实 None.""" + import json as _json + name, _ = _seed_factor_card(client, name="fa_x") # 注册活因子(assessable) + rep_dir = tmp_path / "reports" / "factor_monthly" + rep_dir.mkdir(parents=True) + doc = {"as_of": "2026-09-30", + "monthly_points": {"fa_x": [ + {"month": "2025-09", "ic_mean": 0.04, "t": 2.1, "count": 1}, + {"month": "2025-10", "ic_mean": 0.06, "t": 2.4, "count": 1}, + {"month": "2026-09", "ic_mean": 0.02, "t": 1.1, "count": 1}]}, + "factor_stats": {"fa_x": {"icAll": 0.04, "tAll": 1.7321, + "positiveRatio": 1.0, + "byYear": {"2025": 0.05, "2026": 0.02}}}} + (rep_dir / "nas_2026-09-30.json").write_text(_json.dumps(doc), "utf-8") + monkeypatch.setenv("SANGUO_FACTOR_MONTHLY_DIR", str(rep_dir)) + r = client.get(f"/api/v1/pipeline/factors/{name}/detail") + assert r.status_code == 200 + got = r.json()["factor"]["icStats"] + assert got["icAll"] == 0.04 and got["tAll"] == 1.7321 + assert got["byYear"]["2025"] == 0.05 + # 无报告时的反向用例:指到空目录,icStats 如实 None(不造数) + empty = tmp_path / "reports_empty" + empty.mkdir() + monkeypatch.setenv("SANGUO_FACTOR_MONTHLY_DIR", str(empty)) + r2 = client.get(f"/api/v1/pipeline/factors/{name}/detail") + assert r2.status_code == 200 and r2.json()["factor"]["icStats"] is None