fix(api): codex review 修批——factors 列表 icFullT 从月报 factor_stats.tAll 接真(单次读取)+decompose similarity 仅建条落盘(已存在幂等重注册不重写) [vps]

This commit is contained in:
2026-10-10 11:14:15 +08:00
parent eaac48a1db
commit 28e648b4fe
3 changed files with 91 additions and 9 deletions
+20 -9
View File
@@ -437,6 +437,13 @@ def factors() -> dict:
reg = vr.load_registry(ensure_runtime_registry(
_factor_registry_path(), os.path.join("config", "factor_registry.yaml")))
points, last_eval = _eval_latest_points()
# 全史 t(=全期合成 t)从最新月报 factor_stats 一次性接真(spec §4.2 承诺
# factors 端点升级;单次读取,勿循环内重读——codex review HIGH)
reports = _monthly_reports()
full_t: dict[str, Any] = (
{n: (s or {}).get("tAll")
for n, s in (reports[0][2].get("factor_stats") or {}).items()}
if reports else {})
items: list[dict[str, Any]] = []
for name, e in reg["factors"].items():
if e.get("status") == "graveyard":
@@ -453,7 +460,7 @@ def factors() -> dict:
"hypothesis": e.get("hypothesis"),
"description": e.get("description") or "",
"icRecentT": t, # 最近批(12M 滚动窗)即近窗 t
"icFullT": None, # eval_store 每批单窗 t,无全史批数据源,如实 null
"icFullT": full_t.get(name), # 全期合成 t(月报 factor_stats.tAll)
"promotedAtT": e.get("promotion_t") or None,
"similarity": e.get("similarity"), # 疑似换皮提示(注册时落)
"decayMonths": 0,
@@ -1169,6 +1176,9 @@ async def _decompose_worker(hyp_id: str, config, job_id: str | None = None) -> d
today = date.today().isoformat()
for cand in result["passed"]:
src = derive_source(cand["expression"]) # 已过门,必为 str
# 仅建条纪律(codex review):已存在条目(同名同假设幂等重注册)
# 不重写 similarity——与 description/origin 落盘纪律同款
is_new = cand["name"] not in reg["factors"]
vr.upsert_factor(reg, cand["name"], hypothesis=hyp_id,
status="incubating", origin="decomposer",
description=cand.get("description"))
@@ -1180,14 +1190,15 @@ async def _decompose_worker(hyp_id: str, config, job_id: str | None = None) -> d
# AST 防换皮提示(spec §4.2 2026-10-10):对既有池比对,≥0.6 存
# entry.similarity——硬门(dup_subtree)外的部分同构仍可注册,
# 提示非硬拒,判定人做.池=注册表条目当前表达式(versions[-1]).
from sanguo_factor import expression_match as em
_cands = {n: str(((v.get("versions") or [{}])[-1]
.get("params") or {}).get("expression") or "")
for n, v in reg["factors"].items()
if n != cand["name"]}
_sim = em.top_similar(str(cand["expression"]), _cands)
if _sim:
reg["factors"][cand["name"]]["similarity"] = _sim
if is_new:
from sanguo_factor import expression_match as em
_cands = {n: str(((v.get("versions") or [{}])[-1]
.get("params") or {}).get("expression") or "")
for n, v in reg["factors"].items()
if n != cand["name"]}
_sim = em.top_similar(str(cand["expression"]), _cands)
if _sim:
reg["factors"][cand["name"]]["similarity"] = _sim
registered.append({"name": cand["name"], "source": src,
"expression": cand["expression"],
"description": cand.get("description", ""),
+31
View File
@@ -155,3 +155,34 @@ def test_factor_detail_ic_stats(client, tmp_path, monkeypatch):
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
def test_factors_list_ic_full_t_from_stats(client, tmp_path, monkeypatch):
"""codex review:列表端点 icFullT 从最新报告 factor_stats.tAll 接真
(spec 承诺 factors 端点全史 t 升级);无报告仍如实 None."""
import json as _json
name, _ = _seed_factor_card(client, name="fa_x")
rep_dir = tmp_path / "reports" / "factor_monthly"
rep_dir.mkdir(parents=True)
doc = {"as_of": "2026-09-30",
"factor_stats": {"fa_x": {"icAll": 0.04, "tAll": 1.7321,
"positiveRatio": 1.0,
"byYear": {"2025": 0.05}}}}
(rep_dir / "nas_2026-09-30.json").write_text(_json.dumps(doc), "utf-8")
monkeypatch.setenv("SANGUO_FACTOR_MONTHLY_DIR", str(rep_dir))
items = client.get("/api/v1/pipeline/factors").json()["items"]
row = next(i for i in items if i["id"] == name)
assert row["icFullT"] == 1.7321
# tAll=None(样本不足)如实 null,不造数
doc["factor_stats"]["fa_x"]["tAll"] = None
(rep_dir / "nas_2026-09-30.json").write_text(_json.dumps(doc), "utf-8")
items = client.get("/api/v1/pipeline/factors").json()["items"]
row = next(i for i in items if i["id"] == name)
assert row["icFullT"] is None
# 无报告:零读不崩,如实 null
empty = tmp_path / "reports_empty"
empty.mkdir()
monkeypatch.setenv("SANGUO_FACTOR_MONTHLY_DIR", str(empty))
items = client.get("/api/v1/pipeline/factors").json()["items"]
row = next(i for i in items if i["id"] == name)
assert row["icFullT"] is None
@@ -295,6 +295,46 @@ class TestDecompose:
from sanguo_api import routes_pipeline as rp
assert isinstance(rp._DECOMPOSE_LOCK, type(threading.Lock()))
assert isinstance(rp._GRADUATE_LOCK, type(threading.Lock()))
def test_decompose_similarity_only_on_new_entry(self, client, fake_llm,
tmp_path, monkeypatch):
"""codex review 仅建条纪律:条目已存在时 upsert 不重写 similarity
(强化形态——同名异假设 upsert 会拒,同名同假设幂等返回,此测后者)."""
import yaml
# 先建卡拿 hyp id,再预置 registry:fa_dup 绑同一假设(幂等重注册路径)
hyp = _seed_card(client)
reg_path = tmp_path / "r_sim2.yaml"
monkeypatch.setenv("SANGUO_FACTOR_REGISTRY", str(reg_path))
reg = {"factors": {
"fa_old": {"name": "fa_old", "hypothesis": "H-1",
"status": "assessable",
"versions": [{"v": 1, "commit": "x",
"params": {"expression":
"ts_mean(close, 5) * turnover"},
"effective_from": "2026-09-01"}]},
"fa_dup": {"name": "fa_dup", "hypothesis": hyp,
"status": "incubating",
"versions": [{"v": 1, "commit": "x",
"params": {"expression": "close + open"},
"effective_from": "2026-09-01"}]}}}
with open(reg_path, "w", encoding="utf-8") as f:
yaml.safe_dump(reg, f, allow_unicode=True)
# 绕开 LLM 与 name_taken 硬门,直接喂「已存在名」的 passed
import sanguo_api.routes_pipeline as rp
async def fake_run(client_, card, **k):
return {"passed": [{"name": "fa_dup",
"expression": "ts_mean(close, 5) / volume",
"description": "同窗均值除量",
"justification": "j"}],
"failed": [], "rounds": 1}
monkeypatch.setattr(rp, "_run_decompose", fake_run)
assert _wait_job(client, hyp)["status"] == "completed"
with open(reg_path, encoding="utf-8") as f:
reg2 = yaml.safe_load(f)
# 已存在条目:similarity 不写入(旧值 None 保持 None)
assert "similarity" not in reg2["factors"]["fa_dup"]
def test_decompose_register_similarity_hint(self, client, fake_llm,
tmp_path, monkeypatch):
"""AST 防换皮提示(spec §4.2 2026-10-10):新因子注册时对既有池比对,