feat(strategy): 策略实例做实P0+P1+策略库A+B混合布局(spec§12.6定稿)——实例=策略档案:①绑定:paper/live账户+回测任务加instance_id(paper_accounts/live_accounts ALTER迁移),发起即建档(无档案自动建),绑已有档案时D1发起快照(账户用档案参数复印件)②回写:事件型(回测_on_done/回放线程)落盘update_instance_run;持续型(实走/影子/实盘)读时聚合_instance_runtime(四格覆盖+在跑账户+漂移检测)③D2/D3同步:POST /paper/sync/{id}批量刷运行中模拟账户(实走+影子锁死一致),实盘不在线改参④D5删除保护409⑤P1全景:GET instances/{id}/overview(全部运行账户+净值尾部+合并持仓归因)收编挂起项「按实例归因持仓」⑥前端:策略库重做A+B混合(统计条+在跑巡检模式+左栏代码树中文主显/文件副行+档案区漂移角标/同步/全景;STRATEGY_LABELS抽共享常量),InstanceOverview抽屉(echarts净值对比+归因持仓表),模拟盘/实盘表单加实例档案下拉(选中预填+绑定,路由?instance=直进);mock层enriched/sync/overview(路由序enriched先于{id});+7绑定测试,921绿,build绿,dev浏览器验收过 [vps]
CI/CD / test (push) Successful in 14s
CI/CD / nas-deploy (push) Successful in 52s
CI/CD / nas-verify (push) Successful in 17s

This commit is contained in:
2026-08-15 22:39:05 +08:00
parent 5615b11640
commit 256820d36f
24 changed files with 2048 additions and 204 deletions
+70 -2
View File
@@ -1,7 +1,8 @@
"""策略实例 JSON 存储(配置数据,几行;无需 DB 迁移)。
实例 = 代码文件的参数变体(spec §12 三层模型中层)。
运行态字段 status/last_return 给默认占位,由后续运行关联回填。
实例 = 代码文件的参数变体(spec §12 三层模型中层)= 策略档案(§12.6 做实设计)
status/last_return:事件型运行(回测/回放)完成时落盘回写;持续型运行
(实走/影子/实盘)由 API 读时聚合覆盖(见 routes_strategy.enrich_instances)。
"""
from __future__ import annotations
import json
@@ -87,3 +88,70 @@ def delete_instance(inst_id: int) -> bool:
return False
_save(new_rows)
return True
# ===== §12.6 做实:事件型运行回写 + 参数漂移(§12.6 P0=====
_KINDS = ("backtest", "replay")
def update_instance_run(inst_id: int, kind: str, status: str,
ret: float | None = None) -> bool:
"""事件型运行(回测/回放)完成/失败时回写档案。
kind: backtest/replaypaper_live/live 是持续运行,读时聚合不落盘)。
ret: 小数收益(0.1548=+15.48%),None 则保留旧值。
返回 False = 实例不存在(如已删,静默丢弃)。
"""
if kind not in _KINDS:
return False
rows = _load()
for r in rows:
if r.get("id") == inst_id:
r.setdefault("status", dict(_DEFAULT_STATUS))
r["status"][kind] = status
if ret is not None:
r["last_return"] = ret
r.setdefault("run_returns", {})
r["run_returns"][kind] = ret
r["updated_at"] = time.strftime("%Y-%m-%d")
_save(rows)
return True
return False
def get_instance_params_snapshot(inst_id: int) -> dict | None:
"""档案当前参数(发起时快照用,D1)。不存在返回 None。"""
for r in _load():
if r.get("id") == inst_id:
return {
"params": dict(r.get("params") or {}),
"symbol_or_pool": r.get("symbol_or_pool", ""),
"interval": r.get("interval", "d"),
"match_session": r.get("match_session", "next_open"),
"code_file": r.get("code_file", ""),
"type": r.get("type", "cta"),
"name": r.get("name", ""),
}
return None
def account_params_drifted(snapshot_params: dict, account: dict) -> bool:
"""参数漂移检测(D2):账户 strategies 参数 vs 档案当前参数。
account 为 paper_accounts 行(strategies 是 JSON 字符串或已解析 list)。
只比对参数键值(标的/周期改了也算漂移——都影响行为)。
"""
import json as _json
raw = account.get("strategies")
if isinstance(raw, str):
try:
strats = _json.loads(raw)
except (ValueError, TypeError):
return False
else:
strats = raw or []
if not strats:
return False
live_params = dict(strats[0].get("params") or {})
return live_params != dict(snapshot_params.get("params") or {})