feat(api): 弹药速览读最新——icStats/icFullT数据源改根层∪daily/子目录取as_of新者(根层10-04旧schema无factor_stats不再挡住daily真值),判定层三端点仍只读根层一行不动;前端末次月度批标签改末次评估批(2026-10-11双层分工补丁B1,设计见4b4fc16) [vps] [no-doc]
This commit is contained in:
@@ -148,7 +148,7 @@ function goFactor(n: string): void {
|
||||
<span class="v">{{ detail.factor.icStats?.icAll != null ? detail.factor.icStats.icAll.toFixed(4) : '—' }}</span></div>
|
||||
<div class="stat"><span class="k">正IC占比<el-tooltip content="正 IC 天数占比(全期);论文口径 >60% 为强" placement="top"><span class="q">ⓘ</span></el-tooltip></span>
|
||||
<span class="v">{{ detail.factor.icStats?.positiveRatio != null ? `${(detail.factor.icStats.positiveRatio * 100).toFixed(0)}%` : '—' }}</span></div>
|
||||
<div class="stat"><span class="k">末次月度批</span><span class="v sm">{{ detail.factor.lastEvalDate || '—' }}</span></div>
|
||||
<div class="stat"><span class="k">末次评估批</span><span class="v sm">{{ detail.factor.lastEvalDate || '—' }}</span></div>
|
||||
<div class="stat"><span class="k">版本</span><span class="v sm">v{{ detail.factor.version }}</span></div>
|
||||
</div>
|
||||
<div v-if="yearLine" class="byyear mono">{{ yearLine }}</div>
|
||||
|
||||
@@ -543,8 +543,9 @@ def factors() -> dict:
|
||||
_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()
|
||||
# factors 端点升级;单次读取,勿循环内重读——codex review HIGH);
|
||||
# ∪daily/ 取新(2026-10-11 补丁)
|
||||
reports = _monthly_reports(include_daily=True)
|
||||
full_t: dict[str, Any] = (
|
||||
{n: (s or {}).get("tAll")
|
||||
for n, s in (reports[0][2].get("factor_stats") or {}).items()}
|
||||
@@ -599,26 +600,37 @@ def factors_graveyard() -> dict:
|
||||
_CROSS_DIFF_MAX = 0.05 # 双机对拍分歧阈(起步默认,首年校准;spec 对拍规则未定数)
|
||||
|
||||
|
||||
def _monthly_reports() -> list[tuple[str, str, dict]]:
|
||||
def _monthly_reports(include_daily: bool = False) -> list[tuple[str, str, dict]]:
|
||||
"""monthly dir 全部报告 [(host, as_of, doc)];按 as_of 降序,坏文件跳过.
|
||||
|
||||
文件名={host}_{as_of}.json(monthly_review.main 落盘原文)。
|
||||
include_daily=True 时并入 daily/ 子目录件(2026-10-11 双层分工补丁:
|
||||
速览读最新——根层∪daily 取 as_of 新者);判定层(月末快照语义)不传参,
|
||||
仍只读根层,一行不动。
|
||||
"""
|
||||
d = _monthly_dir()
|
||||
if not os.path.isdir(d):
|
||||
return []
|
||||
out: list[tuple[str, str, dict]] = []
|
||||
for fn in os.listdir(d):
|
||||
if not fn.endswith(".json"):
|
||||
continue
|
||||
host, _, as_of = fn[:-5].rpartition("_")
|
||||
if not host or len(as_of) != 10 or as_of[4] != "-":
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(d, fn), encoding="utf-8") as f:
|
||||
out.append((host, as_of, json.load(f)))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
|
||||
def _scan(folder: str) -> None:
|
||||
for fn in os.listdir(folder):
|
||||
if not fn.endswith(".json"):
|
||||
continue
|
||||
host, _, as_of = fn[:-5].rpartition("_")
|
||||
if not host or len(as_of) != 10 or as_of[4] != "-":
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(folder, fn), encoding="utf-8") as f:
|
||||
out.append((host, as_of, json.load(f)))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
|
||||
_scan(d)
|
||||
if include_daily:
|
||||
daily = os.path.join(d, "daily")
|
||||
if os.path.isdir(daily):
|
||||
_scan(daily)
|
||||
out.sort(key=lambda t: t[1], reverse=True)
|
||||
return out
|
||||
|
||||
@@ -719,7 +731,7 @@ def factor_detail(name: str) -> dict:
|
||||
"rounds": job["rounds"], "hypId": hyp_id}
|
||||
break
|
||||
stats = None
|
||||
reports = _monthly_reports()
|
||||
reports = _monthly_reports(include_daily=True)
|
||||
if reports:
|
||||
stats = (reports[0][2].get("factor_stats") or {}).get(name)
|
||||
return {"factor": {
|
||||
|
||||
@@ -186,3 +186,64 @@ def test_factors_list_ic_full_t_from_stats(client, tmp_path, monkeypatch):
|
||||
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
|
||||
|
||||
|
||||
def test_factor_detail_ic_stats_prefers_newer_daily(client, tmp_path, monkeypatch):
|
||||
"""2026-10-11 双层分工补丁:icStats 读根层∪daily/ 取 as_of 新者——
|
||||
根层 10-04 旧 schema 无 factor_stats,daily 件有真值→速览出真值."""
|
||||
import json as _json
|
||||
name, _ = _seed_factor_card(client, name="fa_x")
|
||||
rep_dir = tmp_path / "reports" / "factor_monthly"
|
||||
rep_dir.mkdir(parents=True)
|
||||
(rep_dir / "vps_2026-10-04.json").write_text(
|
||||
_json.dumps({"as_of": "2026-10-04", "monthly_points": {}}), "utf-8") # 旧 schema
|
||||
daily_dir = rep_dir / "daily"
|
||||
daily_dir.mkdir()
|
||||
(daily_dir / "vps_2026-10-09.json").write_text(_json.dumps({
|
||||
"as_of": "2026-10-09",
|
||||
"factor_stats": {"fa_x": {"icAll": 0.03, "tAll": 2.5,
|
||||
"positiveRatio": 0.8,
|
||||
"byYear": {"2026": 0.03}}}}), "utf-8")
|
||||
monkeypatch.setenv("SANGUO_FACTOR_MONTHLY_DIR", str(rep_dir))
|
||||
got = client.get(f"/api/v1/pipeline/factors/{name}/detail").json()["factor"]["icStats"]
|
||||
assert got and got["tAll"] == 2.5 # daily 件新者胜,不再被旧根层件挡住
|
||||
|
||||
|
||||
def test_factor_detail_ic_stats_root_newer_wins(client, tmp_path, monkeypatch):
|
||||
"""根层件更新时仍胜 daily(取 as_of 新者,不是 daily 优先)."""
|
||||
import json as _json
|
||||
name, _ = _seed_factor_card(client, name="fa_x")
|
||||
rep_dir = tmp_path / "reports" / "factor_monthly"
|
||||
rep_dir.mkdir(parents=True)
|
||||
(rep_dir / "vps_2026-11-04.json").write_text(_json.dumps({
|
||||
"as_of": "2026-11-04",
|
||||
"factor_stats": {"fa_x": {"icAll": 0.05, "tAll": 3.3,
|
||||
"positiveRatio": 0.9, "byYear": {}}}}), "utf-8")
|
||||
daily_dir = rep_dir / "daily"
|
||||
daily_dir.mkdir()
|
||||
(daily_dir / "vps_2026-10-20.json").write_text(_json.dumps({
|
||||
"as_of": "2026-10-20",
|
||||
"factor_stats": {"fa_x": {"icAll": 0.01, "tAll": 1.1,
|
||||
"positiveRatio": 0.5, "byYear": {}}}}), "utf-8")
|
||||
monkeypatch.setenv("SANGUO_FACTOR_MONTHLY_DIR", str(rep_dir))
|
||||
got = client.get(f"/api/v1/pipeline/factors/{name}/detail").json()["factor"]["icStats"]
|
||||
assert got and got["tAll"] == 3.3
|
||||
|
||||
|
||||
def test_judgement_layer_ignores_daily_dir(client, tmp_path, monkeypatch):
|
||||
"""判定层不动:monthly-review 仍只读根层,daily 件不得泄漏进速览卡/对拍."""
|
||||
import json as _json
|
||||
rep_dir = tmp_path / "reports" / "factor_monthly"
|
||||
rep_dir.mkdir(parents=True)
|
||||
(rep_dir / "vps_2026-10-04.json").write_text(_json.dumps({
|
||||
"as_of": "2026-10-04", "factors": {"fa_old": {"t": 1.0}},
|
||||
"collective": {"median": 2.1, "floor": 2.0}}), "utf-8")
|
||||
daily_dir = rep_dir / "daily"
|
||||
daily_dir.mkdir()
|
||||
(daily_dir / "vps_2026-10-09.json").write_text(_json.dumps({
|
||||
"as_of": "2026-10-09", "factors": {"fa_new": {"t": 9.9}},
|
||||
"collective": {"median": 9.9, "floor": 2.0}}), "utf-8")
|
||||
monkeypatch.setenv("SANGUO_FACTOR_MONTHLY_DIR", str(rep_dir))
|
||||
got = client.get("/api/v1/pipeline/monthly-review").json()
|
||||
assert got["runDate"] == "2026-10-04" # 仍是根层最新,不被 daily 顶掉
|
||||
assert got["factorsEvaluated"] == 1 # daily 的 fa_new 不算进来
|
||||
|
||||
Reference in New Issue
Block a user