158 lines
5.2 KiB
Python
158 lines
5.2 KiB
Python
"""策略实例 JSON 存储(配置数据,几行;无需 DB 迁移)。
|
||
|
||
实例 = 代码文件的参数变体(spec §12 三层模型中层)= 策略档案(§12.6 做实设计)。
|
||
status/last_return:事件型运行(回测/回放)完成时落盘回写;持续型运行
|
||
(实走/影子/实盘)由 API 读时聚合覆盖(见 routes_strategy.enrich_instances)。
|
||
"""
|
||
from __future__ import annotations
|
||
import json
|
||
import os
|
||
import time
|
||
|
||
_DEFAULT_STATUS = {"backtest": "-", "replay": "-", "paper_live": "-", "live": "-"}
|
||
|
||
# 路径:env 覆盖 > 仓库根 data/strategy_instances.json。测试用 monkeypatch 改 _STORE_PATH。
|
||
_STORE_PATH: str = os.environ.get(
|
||
"SANGUO_INSTANCE_STORE",
|
||
os.path.join(
|
||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||
"data", "strategy_instances.json",
|
||
),
|
||
)
|
||
|
||
|
||
def _load() -> list[dict]:
|
||
if not os.path.exists(_STORE_PATH):
|
||
return []
|
||
with open(_STORE_PATH, encoding="utf-8") as f:
|
||
try:
|
||
return json.load(f)
|
||
except json.JSONDecodeError:
|
||
return []
|
||
|
||
|
||
def _save(rows: list[dict]) -> None:
|
||
os.makedirs(os.path.dirname(_STORE_PATH), exist_ok=True)
|
||
with open(_STORE_PATH, "w", encoding="utf-8") as f:
|
||
json.dump(rows, f, ensure_ascii=False, indent=2)
|
||
|
||
|
||
def list_instances() -> dict:
|
||
return {"instances": _load()}
|
||
|
||
|
||
def get_instance(inst_id: int) -> dict:
|
||
for r in _load():
|
||
if r.get("id") == inst_id:
|
||
return {"instance": r}
|
||
return {"instance": None}
|
||
|
||
|
||
def create_instance(payload: dict) -> int:
|
||
rows = _load()
|
||
new_id = (max((r.get("id", 0) for r in rows), default=0) + 1) if rows else 1
|
||
row = {
|
||
"id": new_id,
|
||
"code_file": payload.get("code_file", ""),
|
||
"name": payload.get("name", ""),
|
||
"type": payload.get("type", "cta"),
|
||
"params": payload.get("params", {}),
|
||
"symbol_or_pool": payload.get("symbol_or_pool", ""),
|
||
"interval": payload.get("interval", "d"),
|
||
"match_session": payload.get("match_session", "next_open"),
|
||
"status": dict(_DEFAULT_STATUS),
|
||
"last_return": None,
|
||
"updated_at": time.strftime("%Y-%m-%d"),
|
||
}
|
||
rows.append(row)
|
||
_save(rows)
|
||
return new_id
|
||
|
||
|
||
def update_instance(inst_id: int, payload: dict) -> None:
|
||
rows = _load()
|
||
for r in rows:
|
||
if r.get("id") == inst_id:
|
||
for k in ("code_file", "name", "type", "params", "symbol_or_pool", "interval", "match_session"):
|
||
if k in payload:
|
||
r[k] = payload[k]
|
||
r["updated_at"] = time.strftime("%Y-%m-%d")
|
||
_save(rows)
|
||
return
|
||
|
||
|
||
def delete_instance(inst_id: int) -> bool:
|
||
rows = _load()
|
||
new_rows = [r for r in rows if r.get("id") != inst_id]
|
||
if len(new_rows) == len(rows):
|
||
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/replay(paper_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 {})
|