Files
sanguo_vnpy_v2/tests/api/test_routes_pipeline_composite.py
T
claude_dev 55bcde798b
CI/CD / test (push) Successful in 37s
CI/CD / nas-deploy (push) Successful in 7s
CI/CD / nas-verify (push) Successful in 8s
feat(api): 控制台可读化波——合成层族切换器+归因页整页重构(用户令:多策略就绪+人类可读) [vps]
①合成层多族就绪: /pipeline/composite 加 ?family= 选族+families 清单
  (前端下拉切换器)——以策略为锚,新策略钉住新合成层上位时登记新树,
  无策略消费的单代族不进树; 未知族 fail-soft items=[]+清单照常。
  前端: 族下拉+族说明+「新族自动出现」提示+空态+版本角色徽标
  (在位者/挑战者)+TE 未算人话文案; sub 明示「页面上没有也不该有一键
  换因子(决议 A)」。
②归因页整页人类可读重构(用户令: 按策略归因颗粒度讲清楚):
  - ?variant=incumbent|challenger 切在位者/挑战者归因件(challenger=
    生效位 challenger/ 子目录)+attributionMeta(asOf/windowEnd/months)
  - 前端: variant 切换条+口径条(数值含义+数据截至+覆盖月数+更新节律);
    右块分组=「收益结构·风格四桶」(market/size/industry/specific 带
    tooltip 人话解释)与「选股因子贡献·12 源」(组级说明=暴露×因子收益
    =贡献,长期负贡献=降权/退役候选); 合计行(恒等式加总≈同窗组合总
    收益); 空态=「归因批未产出(首次 10-04)非故障」; 左块加偏差含义
    说明+IS 金额行脚注; 条宽 min(…,100)% 防溢出。
- 测试: tests/api 202→208(families/未知族/challenger 子目录/meta/
  variant 422); 前端 pipeline+mock 34 过(分组+合计+空态+variant 重拉+
  族切换/空态); vue-tsc 过; spec §4.7 端点节同步(档随码走)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-27 18:43:21 +08:00

135 lines
4.9 KiB
Python

"""GET /pipeline/composite 真端点单测(P1 接真, 09-27; 前端告别 mock).
契约=前端 CompositeVersion {version, releasedAt, note,
members[{factorId,weight}], shadowCompare?{versus,week,totalWeeks,
trackingError,verdict}}; 影子对照读挑战者 szneul_eq_reb63 周报
(weeks_counted/te_annual), 09-28 首跑前无周报 → 如是 0/4 周+TE null
+「进行中」(不造数).
"""
import pytest
from fastapi.testclient import TestClient
from sanguo_api.app import create_app
from sanguo_api.auth import set_jwt_config, create_token
@pytest.fixture()
def client(tmp_path):
# jwt secret 每测试现设现签(全量套件中其它测试会改全局 secret)
set_jwt_config(secret="test", expire_minutes=60)
app = create_app(db_path=str(tmp_path / "t.db"), file_dir=None)
c = TestClient(app)
c.headers.update({"Authorization": f"Bearer {create_token('admin')}"})
return c
@pytest.fixture()
def no_shadow_weekly(monkeypatch):
"""无周报(09-28 首跑前形态)."""
from sanguo_portfolio import pipeline_store
monkeypatch.setattr(pipeline_store, "latest_weekly", lambda *a, **kw: None)
@pytest.fixture()
def env(no_shadow_weekly):
return None # 真档案真源表, 无需打桩
def _get(client):
r = client.get("/api/v1/pipeline/composite")
assert r.status_code == 200
return r.json()
def test_family_and_two_quant12_nodes(client, env):
doc = _get(client)
assert doc["family"] == "composite_quant12"
assert [v["version"] for v in doc["items"]] == ["quant12_v1", "quant12_v2a"]
def test_v1_equal_weight_12_sources(client, env):
v1 = _get(client)["items"][0]
assert len(v1["members"]) == 12
assert {m["factorId"] for m in v1["members"]} >= {"alpha16", "klow", "vma_60"}
for m in v1["members"]:
assert m["weight"] == pytest.approx(1 / 12, abs=1e-3)
assert isinstance(v1["releasedAt"], str) and v1["releasedAt"]
assert isinstance(v1["note"], str) and v1["note"]
def test_v2a_profile_weights_not_equal(client, env):
"""v2a=L1 档案 resolved 权重(ICIR 加权, 与 v1 等权可区分)且和=1."""
v2a = _get(client)["items"][1]
ws = {m["factorId"]: m["weight"] for m in v2a["members"]}
assert len(ws) == 12
assert sum(ws.values()) == pytest.approx(1.0, abs=1e-3)
assert len(set(round(w, 4) for w in ws.values())) > 3 # 非等权
assert ws["alpha16"] == pytest.approx(0.1435, abs=5e-4) # 档案真值锚
def test_shadow_compare_before_first_run(client, env):
"""09-28 首跑前: 无周报 → 0/4 周 + TE null + 进行中(挂挑战者节点)."""
doc = _get(client)
assert doc["items"][0]["shadowCompare"] == {
"versus": "quant12_v2a", "week": 0, "totalWeeks": 4,
"trackingError": None, "verdict": "进行中"}
assert "shadowCompare" not in doc["items"][1] # 在位者无对照
def test_shadow_compare_progressing_and_verdicts(client, no_shadow_weekly,
monkeypatch):
from types import SimpleNamespace
from sanguo_portfolio import pipeline_store
def fake_latest(path, sid, **kw):
assert sid == "szneul_eq_reb63"
return fake_latest.row
monkeypatch.setattr(pipeline_store, "get_effective_thresholds",
lambda *a, **kw: SimpleNamespace(
shadow_te_annual_max=0.02))
for weeks, te, want in ((2, 0.012, "进行中"), (4, 0.008, "达标"),
(4, 0.05, "超阈")):
fake_latest.row = {"weeks_counted": weeks, "te_annual": te}
monkeypatch.setattr(pipeline_store, "latest_weekly", fake_latest)
sc = _get(client)["items"][0]["shadowCompare"]
assert sc["week"] == weeks
assert sc["trackingError"] == te
assert sc["verdict"] == want
def test_fail_soft_when_factor_side_broken(client, no_shadow_weekly,
monkeypatch):
"""因子侧 import/档案坏 → fail-soft items=[](看板不 500, warning 留痕)."""
import builtins
real_import = builtins.__import__
def broken_import(name, *a, **kw):
if name.startswith("sanguo_factor"):
raise ImportError("factor side broken")
return real_import(name, *a, **kw)
monkeypatch.setattr(builtins, "__import__", broken_import)
doc = _get(client)
assert doc["items"] == []
assert doc["family"] == "composite_quant12"
def test_families_listed_for_switcher(client, env):
"""多族就绪: families 清单(前端切换器)+缺省返回首棵活树."""
doc = _get(client)
fams = doc["families"]
assert fams and fams[0]["key"] == "composite_quant12"
assert fams[0]["label"] and fams[0]["note"]
def test_unknown_family_fail_soft(client, env):
r = client.get("/api/v1/pipeline/composite?family=composite_nope")
assert r.status_code == 200
doc = r.json()
assert doc["family"] == "composite_nope"
assert doc["items"] == []
assert doc["families"] # 清单照常(切换器不空)