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>
|
<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>
|
<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>
|
<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 class="stat"><span class="k">版本</span><span class="v sm">v{{ detail.factor.version }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="yearLine" class="byyear mono">{{ yearLine }}</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")))
|
_factor_registry_path(), os.path.join("config", "factor_registry.yaml")))
|
||||||
points, last_eval = _eval_latest_points()
|
points, last_eval = _eval_latest_points()
|
||||||
# 全史 t(=全期合成 t)从最新月报 factor_stats 一次性接真(spec §4.2 承诺
|
# 全史 t(=全期合成 t)从最新月报 factor_stats 一次性接真(spec §4.2 承诺
|
||||||
# factors 端点升级;单次读取,勿循环内重读——codex review HIGH)
|
# factors 端点升级;单次读取,勿循环内重读——codex review HIGH);
|
||||||
reports = _monthly_reports()
|
# ∪daily/ 取新(2026-10-11 补丁)
|
||||||
|
reports = _monthly_reports(include_daily=True)
|
||||||
full_t: dict[str, Any] = (
|
full_t: dict[str, Any] = (
|
||||||
{n: (s or {}).get("tAll")
|
{n: (s or {}).get("tAll")
|
||||||
for n, s in (reports[0][2].get("factor_stats") or {}).items()}
|
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 对拍规则未定数)
|
_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 降序,坏文件跳过.
|
"""monthly dir 全部报告 [(host, as_of, doc)];按 as_of 降序,坏文件跳过.
|
||||||
|
|
||||||
文件名={host}_{as_of}.json(monthly_review.main 落盘原文)。
|
文件名={host}_{as_of}.json(monthly_review.main 落盘原文)。
|
||||||
|
include_daily=True 时并入 daily/ 子目录件(2026-10-11 双层分工补丁:
|
||||||
|
速览读最新——根层∪daily 取 as_of 新者);判定层(月末快照语义)不传参,
|
||||||
|
仍只读根层,一行不动。
|
||||||
"""
|
"""
|
||||||
d = _monthly_dir()
|
d = _monthly_dir()
|
||||||
if not os.path.isdir(d):
|
if not os.path.isdir(d):
|
||||||
return []
|
return []
|
||||||
out: list[tuple[str, str, dict]] = []
|
out: list[tuple[str, str, dict]] = []
|
||||||
for fn in os.listdir(d):
|
|
||||||
if not fn.endswith(".json"):
|
def _scan(folder: str) -> None:
|
||||||
continue
|
for fn in os.listdir(folder):
|
||||||
host, _, as_of = fn[:-5].rpartition("_")
|
if not fn.endswith(".json"):
|
||||||
if not host or len(as_of) != 10 or as_of[4] != "-":
|
continue
|
||||||
continue
|
host, _, as_of = fn[:-5].rpartition("_")
|
||||||
try:
|
if not host or len(as_of) != 10 or as_of[4] != "-":
|
||||||
with open(os.path.join(d, fn), encoding="utf-8") as f:
|
continue
|
||||||
out.append((host, as_of, json.load(f)))
|
try:
|
||||||
except (OSError, json.JSONDecodeError):
|
with open(os.path.join(folder, fn), encoding="utf-8") as f:
|
||||||
continue
|
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)
|
out.sort(key=lambda t: t[1], reverse=True)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -719,7 +731,7 @@ def factor_detail(name: str) -> dict:
|
|||||||
"rounds": job["rounds"], "hypId": hyp_id}
|
"rounds": job["rounds"], "hypId": hyp_id}
|
||||||
break
|
break
|
||||||
stats = None
|
stats = None
|
||||||
reports = _monthly_reports()
|
reports = _monthly_reports(include_daily=True)
|
||||||
if reports:
|
if reports:
|
||||||
stats = (reports[0][2].get("factor_stats") or {}).get(name)
|
stats = (reports[0][2].get("factor_stats") or {}).get(name)
|
||||||
return {"factor": {
|
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"]
|
items = client.get("/api/v1/pipeline/factors").json()["items"]
|
||||||
row = next(i for i in items if i["id"] == name)
|
row = next(i for i in items if i["id"] == name)
|
||||||
assert row["icFullT"] is None
|
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