fix(portfolio): 运行注册表种子缺失条目自动 merge——治挑战者双机漏登记缺口 [vps]
2026-09-27 实证: 挑战者 szneul_eq_reb63 登记 config 种子后, 双机运行副本 (ensure_runtime_registry 首触才拷贝)永不知晓——周报 job 按运行副本点名 stage+paper_account_id, 挑战者被跳过=影子 A/B 评测腿(周报/孵化梯/毕业 gate)全盲。 治本: 首触整拷语义不变, 另加 _merge_seed_entries——种子有而运行副本缺的 条目(strategies/factors 双主顾)每次 touch 自动补入, 同名条目运行态为准 (防部署回滚转移结果), 无缺条目不写盘(幂等)。 config 种子挑战者 paper_account_id null→63(周报点名非空才算, 与双机手动补行一致)。 runbook 注册表行同步更语义(档随码走)。a 方案(双机手动补行)已先行落地救急。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -29,4 +29,4 @@ strategies:
|
||||
capital: {mode: 固定金额, value: 50000, at: '2026-09-28'}
|
||||
retired_cause: null
|
||||
backtest_run: null
|
||||
paper_account_id: null
|
||||
paper_account_id: 63
|
||||
|
||||
@@ -278,7 +278,7 @@ ssh 49.232.102.198 "powershell -Command \"Get-Content C:\sanguo_bigqmt\qmt_relog
|
||||
| 研判卡回放 | CLI `PYTHONPATH=. python -m sanguo_factor.verdict_replay --as-of <同月度批评>`(月度批评链后跟一条,同机同 as-of;10-04 首班双机首跑) | 产物 `<monthly_dir>/verdict_replay.json`(派生件可重建);触发点普查+七档事后成绩(近似估算,spec §4.7 P3 第一刀);`GET /pipeline/research-card` 读月报+此件出卡 |
|
||||
| 研判裁决留痕 | env `SANGUO_VERDICT_RECORDS`(缺省 `data/verdict_records.jsonl`) | `POST /pipeline/research-card/verdict` 落档(月键一条,重复 409);七档全人工触点零自动执行(决议 C) |
|
||||
| 偏差日报(IS四分解) | CLI `PYTHONPATH=. python -m sanguo_portfolio.ishortfall_daily --as-of <今日>`(每日 20:45 后、周报 job 之后跟跑,避开 20:30 paper 回放与 21:00 xt_eod;价格源=`scripts/pipeline/is_price_source_vps.py` 桥 xtdata shim 分钟 bar+dbbardata 日线;首跑/部署后必跑其 `probe(<day>)` fail-loud 自检) | 产物 `data/ishortfall/<day>.json`+pipeline_store `is_daily` 表(`GET /pipeline/attribution` IS 四行+价格移动参考行数据源=`latest_is()`);事件表 `order_events` 主库幂等自建(FIX 七态 append-only,老 backlog 委托入库就此闭环);回放=`--from 2026-09-14 --to <日>` 出首份真日报(幂等可重跑);⚠️回执缺供的'filled'终态单 cum 按快照兜不住→整腿按未全额计机会成本(identity_resid 非零可见,缺数非错算);spec §4.7 P3 第二刀 |
|
||||
| 策略注册表 | **运行副本=`data/strategy_registry.yaml`+`data/strategy_events.jsonl`**(env `SANGUO_STRATEGY_REGISTRY`/`SANGUO_STRATEGY_EVENTS` 名不变);git 种子=`config/strategy_registry.yaml` **首触自举**(运行副本缺失时从种子拷贝) | promote.sh 每次 push 都 rsync `config/`——运行期转移(毕业/退回/延长)若写 config/ 会被下次部署静默回滚,故运行态一律落 `data/`(部署不覆盖);因子 verdict 端点同款:`data/factor_registry.yaml`+`data/registry_events.jsonl`(种子 `config/factor_registry.yaml`);**跨机同步=人工 git 收编**(运行副本改动抄回 config/ 种子后 commit;转移事件同时落 `reports/registry_events/` 毕业模板,防丢) |
|
||||
| 策略注册表 | **运行副本=`data/strategy_registry.yaml`+`data/strategy_events.jsonl`**(env `SANGUO_STRATEGY_REGISTRY`/`SANGUO_STRATEGY_EVENTS` 名不变);git 种子=`config/strategy_registry.yaml` **首触自举+缺失条目自动 merge**(运行副本缺失时从种子拷贝;已存在时种子新登记条目由 `ensure_runtime_registry` 每次 touch 自动补入,**同名条目运行态为准**——2026-09-27 挑战者双机漏登记、周报点名跳过实证后治本) | promote.sh 每次 push 都 rsync `config/`——运行期转移(毕业/退回/延长)若写 config/ 会被下次部署静默回滚,故运行态一律落 `data/`(部署不覆盖;merge 只补缺、绝不覆盖同名运行态);因子 verdict 端点同款:`data/factor_registry.yaml`+`data/registry_events.jsonl`(种子 `config/factor_registry.yaml`,`factors` 容器同走 merge);**跨机同步=人工 git 收编**(运行副本改动抄回 config/ 种子后 commit;转移事件同时落 `reports/registry_events/` 毕业模板,防丢) |
|
||||
| 存量补录 | `PYTHONPATH=. python -m sanguo_portfolio.strategy_registry register <name> --hypothesis <H-x> --composite-version <v> --backfilled [--paper-account-id <id>] [--stage <s>]` | 逐条补录在跑实例(gate 快照留空);**现在写 data/ 运行副本**(缺则从 config/ 种子自举;`--stage` 直落初档,如 `--stage live`);其余 CLI 子命令=`transition`(--stage/--gate-json/--capital-json/--cause)与 `extend-shadow`(--extra-weeks --note) |
|
||||
| 回测 gate 判定 | `PYTHONPATH=. python -m sanguo_portfolio.promotion_gate backtest --task-id <run> --db <主库>`(或 `--sharpe X --max-dd Y`) | 打印 gate JSON,喂 `strategy_registry transition --gate-json` |
|
||||
| VPS 控制台 | schtask `sanguo-api`(开机自启,`run_api.ps1`→`run_web.py`→uvicorn 8000,vnpy.mysanguo.top 外网绑定) | B' 生产控制台;**09-25 勘误:本就常驻跑纯 API**(旧文「VPS 无常驻 web」过时);09-25 起挂前端 dist(`frontend/dist`,CI 只推 NAS 故 VPS 前端需手动 scp)+PASSPHRASE/GITEA_BASE_URL env 进 run_api.ps1;**改代码后须 `schtasks /end /tn sanguo-api && schtasks /run /tn sanguo-api` 重启加载**;env 优先级=SANGUO_DB_PATH env > config/backtest.yaml(yaml 里 db_path 是 NAS 容器路径,promote 种子直推,勿被误导) |
|
||||
|
||||
@@ -54,8 +54,43 @@ def ensure_runtime_registry(path: str, seed_path: str = os.path.join(
|
||||
|
||||
promote.sh 每次 push 都 rsync config/;运行期转移若写 config/ 会被下次部署
|
||||
静默回滚——运行态一律落 data/ 首触从种子拷贝(M1 终审裁定).
|
||||
首触之外:种子新登记条目自动 merge 进运行副本,同名条目运行态为准(防部署
|
||||
回滚转移结果)——治「config 登记了但运行副本永不知晓」缺口(2026-09-27
|
||||
挑战者 szneul_eq_reb63 双机漏登记、周报点名跳过实证).
|
||||
"""
|
||||
return ensure_runtime_file(path, seed_path)
|
||||
ensure_runtime_file(path, seed_path)
|
||||
_merge_seed_entries(path, seed_path)
|
||||
return path
|
||||
|
||||
|
||||
def _merge_seed_entries(path: str, seed_path: str) -> list[str]:
|
||||
"""种子有而运行副本缺的条目补入(strategies/factors 双主顾);同名不覆盖.
|
||||
|
||||
幂等:无缺条目不写盘。返回补入的条目名(空=无动作)。
|
||||
"""
|
||||
if not os.path.exists(seed_path) or not os.path.exists(path):
|
||||
return []
|
||||
with open(seed_path, encoding="utf-8") as f:
|
||||
seed = yaml.safe_load(f) or {}
|
||||
with open(path, encoding="utf-8") as f:
|
||||
runtime = yaml.safe_load(f) or {}
|
||||
if not isinstance(seed, dict) or not isinstance(runtime, dict):
|
||||
return []
|
||||
added: list[str] = []
|
||||
for section in ("strategies", "factors"):
|
||||
seed_sec = seed.get(section)
|
||||
if not isinstance(seed_sec, dict):
|
||||
continue
|
||||
run_sec = runtime.setdefault(section, {})
|
||||
if not isinstance(run_sec, dict):
|
||||
continue
|
||||
for name, entry in seed_sec.items():
|
||||
if name not in run_sec and isinstance(entry, dict):
|
||||
run_sec[name] = entry
|
||||
added.append(name)
|
||||
if added:
|
||||
save_registry(path, runtime)
|
||||
return added
|
||||
|
||||
|
||||
def load_registry(path: str) -> dict[str, Any]:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
- 血统硬闸(J2 同构):hypothesis+composite_version 缺一不可
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
@@ -192,3 +193,67 @@ def test_cli_register_stage(tmp_path, monkeypatch):
|
||||
"v1", "--stage", "live", "--registry", tmp_yaml])
|
||||
assert rc == 0
|
||||
assert load_registry(tmp_yaml)["strategies"]["x9"]["stage"] == "live"
|
||||
|
||||
|
||||
# —— 种子缺失条目 merge(2026-09-27 挑战者双机漏登记实证,治「config 登记
|
||||
# 了但运行副本永不知晓」;首触整拷语义不变,同名条目运行态为准防部署回滚) ——
|
||||
|
||||
|
||||
def _write_seed(tmp_path, strategies=None, factors=None):
|
||||
seed = tmp_path / "seed.yaml"
|
||||
doc = {}
|
||||
if strategies is not None:
|
||||
doc["strategies"] = strategies
|
||||
if factors is not None:
|
||||
doc["factors"] = factors
|
||||
seed.write_text(yaml.safe_dump(doc, allow_unicode=True), encoding="utf-8")
|
||||
return str(seed)
|
||||
|
||||
|
||||
def test_ensure_runtime_first_touch_still_copies_seed(tmp_path):
|
||||
seed = _write_seed(tmp_path, strategies={"a1": {"stage": "shadow"}})
|
||||
rt = str(tmp_path / "rt.yaml")
|
||||
ensure_runtime_registry(rt, seed)
|
||||
assert load_registry(rt)["strategies"]["a1"]["stage"] == "shadow"
|
||||
|
||||
|
||||
def test_ensure_runtime_merges_seed_new_entry(tmp_path):
|
||||
seed = _write_seed(tmp_path, strategies={
|
||||
"a1": {"stage": "shadow"}, "b2": {"stage": "shadow"}})
|
||||
rt = str(tmp_path / "rt.yaml")
|
||||
save_registry(rt, {"strategies": {"a1": {"stage": "live"}}})
|
||||
ensure_runtime_registry(rt, seed)
|
||||
got = load_registry(rt)["strategies"]
|
||||
assert set(got) == {"a1", "b2"} # b2 补入,a1 保留
|
||||
|
||||
|
||||
def test_ensure_runtime_runtime_wins_on_same_name(tmp_path):
|
||||
# 运行副本转移结果(live)不被种子旧档(shadow)回滚
|
||||
seed = _write_seed(tmp_path, strategies={"a1": {"stage": "shadow"}})
|
||||
rt = str(tmp_path / "rt.yaml")
|
||||
save_registry(rt, {"strategies": {"a1": {"stage": "live",
|
||||
"capital": {"value": 50000}}}})
|
||||
ensure_runtime_registry(rt, seed)
|
||||
entry = load_registry(rt)["strategies"]["a1"]
|
||||
assert entry["stage"] == "live" and entry["capital"]["value"] == 50000
|
||||
|
||||
|
||||
def test_ensure_runtime_merges_factor_section(tmp_path):
|
||||
# factors 容器同款(version_registry 借用本件做自举)
|
||||
seed = _write_seed(tmp_path, factors={"fa_x": {"status": "incubating"}})
|
||||
rt = str(tmp_path / "rt.yaml")
|
||||
save_registry(rt, {"factors": {}})
|
||||
ensure_runtime_registry(rt, seed)
|
||||
assert load_registry(rt)["factors"]["fa_x"]["status"] == "incubating"
|
||||
|
||||
|
||||
def test_ensure_runtime_idempotent_no_rewrite(tmp_path):
|
||||
seed = _write_seed(tmp_path, strategies={"a1": {"stage": "shadow"}})
|
||||
rt = str(tmp_path / "rt.yaml")
|
||||
save_registry(rt, {"strategies": {"a1": {"stage": "live"}}})
|
||||
ensure_runtime_registry(rt, seed)
|
||||
before = Path(rt).read_text(encoding="utf-8")
|
||||
mtime_before = os.stat(rt).st_mtime_ns
|
||||
ensure_runtime_registry(rt, seed) # 二跑无缺条目
|
||||
assert Path(rt).read_text(encoding="utf-8") == before
|
||||
assert os.stat(rt).st_mtime_ns == mtime_before # 未触发写盘
|
||||
|
||||
Reference in New Issue
Block a user