Files
sanguo_vnpy_v2/sanguo_api/instance_store.py
T

158 lines
5.2 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) -> 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 {})