Files
sanguo_vnpy_v2/sanguo_api/instance_store.py
T

174 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""策略实例 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,
meta: dict | None = None) -> bool:
"""事件型运行(回测/回放)完成/失败时回写档案。
kind: backtest/replaypaper_live/live 是持续运行,读时聚合不落盘)。
ret: 小数收益(0.1548=+15.48%),None 则保留旧值。
meta: 附加信息(如 {"code_hash": ...} 发起时代码版本)。
返回 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
if meta:
r.setdefault("run_meta", {})
r["run_meta"].setdefault(kind, {})
r["run_meta"][kind].update(meta)
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):账户参数 vs 档案当前参数。
account 兼容两种行:
- paper_accounts 行(strategies=[{params}]JSON 字符串或已解析)
- live_accounts 行(setting={params}JSON 字符串或已解析 dict
只比对参数键值。
"""
import json as _json
raw = account.get("strategies")
if raw is None and "setting" in account:
s = account["setting"]
if isinstance(s, str):
try:
s = _json.loads(s)
except (ValueError, TypeError):
return False
strats = [{"params": s}] if isinstance(s, dict) else []
elif 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 {})