"""实盘模拟 API 路由(spec §live-api)。 create 建 live_account(持久化配置,status=stopped);start/stop 改 status 字段; GET 查询持仓/成交/账户/状态。runner(supervisor) 是独立常驻进程,轮询 status 字段 决定起停 LiveTradingEngine;两者只通过 DB 通信,本模块不实例化 engine。 风格参考 ``sanguo_api/routes_paper.py``。 """ from __future__ import annotations import os from fastapi import APIRouter, Depends, Header, HTTPException from pydantic import BaseModel from .auth import verify_token as verify_token_impl router = APIRouter() _db_path = {"path": None} # miniQMT 默认 userdata_mini 路径(国金QMT交易端模拟); # req.mini_path 空 → env SANGUO_QMT_PATH → 此默认(双保险,避免 connect=-1) _DEFAULT_MINI_PATH = r"C:\国金QMT交易端模拟\userdata_mini" def set_db_path(p): _db_path["path"] = p if p: from sanguo_live.persistence import init_db init_db(p) # app 启动建表(幂等) async def verify_token(authorization: str | None = Header(None)): if authorization is None or not authorization.startswith("Bearer "): raise HTTPException(401, "Missing/invalid authorization") return verify_token_impl(authorization.split(" ", 1)[1]) class LiveCreateRequest(BaseModel): name: str = "live" account: str vt_symbol: str = "600000.SSE" strategy_class: str = "AShareDoubleMaStrategy" strategy_name: str setting: dict = {} interval: str = "15m" initial_capital: float = 1_000_000 connect_wait_sec: int = 10 init_wait_sec: int = 60 mini_path: str = "" @router.post("/live/create", dependencies=[Depends(verify_token)]) def create_live(req: LiveCreateRequest): """创建实盘实例(写 live_accounts,status=stopped)。需调 start 才会启动。""" from sanguo_live.persistence import init_db, save_account db = _db_path["path"] or ":memory:" init_db(db) payload = req.model_dump() # mini_path 兜底:req → env SANGUO_QMT_PATH → 内置默认(空值会导致 connect=-1) if not payload.get("mini_path"): payload["mini_path"] = ( os.environ.get("SANGUO_QMT_PATH") or _DEFAULT_MINI_PATH ) aid = save_account(db, {**payload, "status": "stopped"}) return {"account_id": aid, "status": "stopped"} @router.get("/live", dependencies=[Depends(verify_token)]) def list_lives(): """实盘实例列表。每行带最新账户快照摘要(total/收益率)。 收益率用首快照基线:(last_total - first_total) / first_total, 避免用 initial_capital 兜底导致入金/出金瞬间收益率失真。 无快照时 total_return=None(不兜底 initial_capital)。 """ from sanguo_live.persistence import ( list_accounts, get_last_balance, get_first_balance, load_positions, ) db = _db_path["path"] if not db: return {"accounts": []} items = list_accounts(db) for item in items: last = get_last_balance(db, item["id"]) first = get_first_balance(db, item["id"]) if last: item["latest_equity"] = last.get("total") item["latest_date"] = last.get("date") else: item["latest_equity"] = None item["latest_date"] = None # 收益率:首快照 total 为 baseline;last/first 同条时为 0 baseline = (first or {}).get("total") if first else None if last and baseline: item["total_return"] = (last.get("total", 0) - baseline) / baseline else: item["total_return"] = None item["position_count"] = len(load_positions(db, item["id"])) return {"accounts": items} @router.get("/live/{aid}", dependencies=[Depends(verify_token)]) def get_live(aid: int): from sanguo_live.persistence import get_account acc = get_account(_db_path["path"], aid) if not acc: raise HTTPException(404, "account not found") return acc @router.post("/live/{aid}/start", dependencies=[Depends(verify_token)]) def start_live(aid: int): """启动实例(status=running)。supervisor 轮询发现后起 engine。""" from sanguo_live.persistence import get_account, update_account_status acc = get_account(_db_path["path"], aid) if not acc: raise HTTPException(404, "account not found") if not acc["account"]: raise HTTPException(400, "account 字段(交易账号)不能为空") update_account_status(_db_path["path"], aid, "running") return {"account_id": aid, "status": "running"} @router.post("/live/{aid}/stop", dependencies=[Depends(verify_token)]) def stop_live(aid: int): """停止实例(status=stopped)。supervisor 轮询发现后停 engine。""" from sanguo_live.persistence import get_account, update_account_status if not get_account(_db_path["path"], aid): raise HTTPException(404, "account not found") update_account_status(_db_path["path"], aid, "stopped") return {"account_id": aid, "status": "stopped"} @router.get("/live/{aid}/positions", dependencies=[Depends(verify_token)]) def get_positions(aid: int): """持仓快照(读 live_positions,supervisor 定时落库)。""" from sanguo_live.persistence import load_positions return load_positions(_db_path["path"], aid) @router.get("/live/{aid}/trades", dependencies=[Depends(verify_token)]) def get_trades(aid: int): """成交明细(读 live_trades,supervisor 事件回调落库)。""" from sanguo_live.persistence import list_trades return list_trades(_db_path["path"], aid) @router.get("/live/{aid}/account", dependencies=[Depends(verify_token)]) def get_account_balance(aid: int): """账户最新快照(读 live_balance 最新一条)。""" from sanguo_live.persistence import get_last_balance last = get_last_balance(_db_path["path"], aid) return last or {} @router.get("/live/{aid}/status", dependencies=[Depends(verify_token)]) def get_status(aid: int): """运行状态(读 live_accounts.status)。""" from sanguo_live.persistence import get_account acc = get_account(_db_path["path"], aid) if not acc: raise HTTPException(404, "account not found") return { "account_id": aid, "status": acc["status"], "name": acc["name"], "account": acc["account"], "vt_symbol": acc["vt_symbol"], "strategy_name": acc["strategy_name"], "updated_at": acc["updated_at"], "error_msg": acc.get("error_msg", ""), }