Files
sanguo_vnpy_v2/sanguo_live/persistence.py
T
claude_dev 96b1924fd5 feat: 实盘模拟(live) + 组合回测MVP(portfolio)
[live] 实盘模拟 vnpy+miniQMT 直连(supervisor 轮询, 前后端):
- sanguo_live: LiveTradingEngine + AShareCtaTemplate(定寸/禁做空) + runner_supervisor(DB驱动) + persistence(4表WAL)
- sanguo_api/routes_live: 9路由(create/start/stop/positions/trades/account/status)
- frontend live: New/List/Monitor + api/live.ts; config/live.yaml

[portfolio] 组合回测 MVP(BulletTrade, 链路代码完成待验证):
- runner_backtest 加 JSON 入口(--json, BacktestEngine 顶层 import)
- sanguo_api/routes_portfolio: POST /portfolio/backtest SSH 触发 VPS 跑
- frontend PortfolioBacktest.vue + api/portfolio.ts: 表单+结果+净值曲线
- 路由/菜单注册(/backtest/portfolio 组合回测)
- 已知: MVP 链路未端到端验证, agent 改至中途被停; 待 Mac 起服务联调
2026-07-18 20:04:16 +08:00

281 lines
9.3 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.
"""实盘模拟 SQLite 持久化(4 表,spec §live-api)。
表:live_accounts / live_trades / live_positions / live_balance
WAL 模式支持 supervisor 进程写 + API 进程读(DB 解耦,spec §live-api)。
设计参考 ``sanguo_trader/persistence.py``paper_* 表),但:
- account.setting 存 JSON 字符串(策略参数透传给 CtaTemplate.update_setting
- status: stopped | runningAPI 改字段,supervisor 轮询该字段决定起停)
- positions 为覆盖式快照(supervisor 定时把 OMS PositionData 落库,不做增量)
"""
from __future__ import annotations
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
SCHEMA = """
CREATE TABLE IF NOT EXISTS live_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
account TEXT,
vt_symbol TEXT,
strategy_class TEXT,
strategy_name TEXT,
setting TEXT,
status TEXT,
interval TEXT,
initial_capital REAL,
connect_wait_sec INTEGER,
init_wait_sec INTEGER,
mini_path TEXT,
error_msg TEXT,
created_at TEXT,
updated_at TEXT
);
CREATE TABLE IF NOT EXISTS live_trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
strategy_name TEXT,
symbol TEXT,
direction TEXT,
offset TEXT,
price REAL,
volume REAL,
traded_at TEXT,
vt_tradeid TEXT
);
CREATE TABLE IF NOT EXISTS live_positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
symbol TEXT,
volume REAL,
frozen REAL,
avg_price REAL,
updated_at TEXT,
UNIQUE(account_id, symbol)
);
CREATE TABLE IF NOT EXISTS live_balance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER,
date TEXT,
cash REAL,
market_value REAL,
total REAL
);
CREATE INDEX IF NOT EXISTS idx_live_trades_account ON live_trades(account_id);
CREATE INDEX IF NOT EXISTS idx_live_balance_account ON live_balance(account_id, date);
"""
def _now() -> str:
return datetime.now(timezone.utc).isoformat()
def init_db(db_path: str) -> None:
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(db_path) as conn:
conn.executescript(SCHEMA)
conn.execute("PRAGMA journal_mode=WAL")
conn.commit()
# ----------------- live_accounts CRUD -----------------
def save_account(db_path: str, account: dict[str, Any]) -> int:
with sqlite3.connect(db_path) as conn:
cur = conn.execute(
"""INSERT INTO live_accounts
(name, account, vt_symbol, strategy_class, strategy_name, setting,
status, interval, initial_capital, connect_wait_sec, init_wait_sec,
mini_path, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
account.get("name", "live"),
account.get("account", ""),
account.get("vt_symbol", ""),
account.get("strategy_class", "AShareDoubleMaStrategy"),
account.get("strategy_name", ""),
json.dumps(account.get("setting", {})),
account.get("status", "stopped"),
account.get("interval", "15m"),
account.get("initial_capital", 1_000_000),
int(account.get("connect_wait_sec", 10)),
int(account.get("init_wait_sec", 60)),
account.get("mini_path", ""),
_now(), _now(),
),
)
conn.commit()
return cur.lastrowid
def list_accounts(db_path: str) -> list[dict]:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute("SELECT * FROM live_accounts ORDER BY id DESC")
return [dict(r) for r in cur.fetchall()]
def get_account(db_path: str, account_id: int) -> dict | None:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"SELECT * FROM live_accounts WHERE id=?", (account_id,)
)
row = cur.fetchone()
return dict(row) if row else None
def update_account_status(
db_path: str, account_id: int, status: str, error_msg: str = ""
) -> None:
with sqlite3.connect(db_path) as conn:
conn.execute(
"UPDATE live_accounts SET status=?, error_msg=?, updated_at=? WHERE id=?",
(status, error_msg, _now(), account_id),
)
conn.commit()
def list_running_accounts(db_path: str) -> list[dict]:
"""supervisor 轮询:取所有 status=running 的实例。"""
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"SELECT * FROM live_accounts WHERE status=? ORDER BY id", ("running",)
)
return [dict(r) for r in cur.fetchall()]
# ----------------- live_trades -----------------
def save_trade(db_path: str, account_id: int, trade: dict[str, Any]) -> int:
with sqlite3.connect(db_path) as conn:
cur = conn.execute(
"""INSERT INTO live_trades
(account_id, strategy_name, symbol, direction, offset,
price, volume, traded_at, vt_tradeid)
VALUES (?,?,?,?,?,?,?,?,?)""",
(
account_id, trade.get("strategy_name", ""),
trade.get("symbol", ""), trade.get("direction", ""),
trade.get("offset", ""), trade.get("price", 0),
trade.get("volume", 0), trade.get("traded_at", ""),
trade.get("vt_tradeid", ""),
),
)
conn.commit()
return cur.lastrowid
def list_trades(db_path: str, account_id: int) -> list[dict]:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"SELECT * FROM live_trades WHERE account_id=? ORDER BY id",
(account_id,),
)
return [dict(r) for r in cur.fetchall()]
# ----------------- live_positions (覆盖式快照) -----------------
def save_positions(
db_path: str, account_id: int, positions: dict[str, dict]
) -> None:
"""覆盖式落库。positions = {symbol: {volume, frozen, avg_price}}。
supervisor 每 snapshot_interval_sec 调一次,把 OMS 最新 PositionData 覆盖落库。
只保留 volume>0 的持仓。
"""
now = _now()
with sqlite3.connect(db_path) as conn:
conn.execute("DELETE FROM live_positions WHERE account_id=?", (account_id,))
conn.executemany(
"""INSERT INTO live_positions
(account_id, symbol, volume, frozen, avg_price, updated_at)
VALUES (?,?,?,?,?,?)""",
[
(account_id, sym, p["volume"], p.get("frozen", 0),
p["avg_price"], now)
for sym, p in positions.items() if p.get("volume", 0) > 0
],
)
conn.commit()
def load_positions(db_path: str, account_id: int) -> list[dict]:
"""API 读持仓快照 → [{symbol, volume, frozen, avg_price, updated_at}]。"""
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"SELECT symbol, volume, frozen, avg_price, updated_at "
"FROM live_positions WHERE account_id=?",
(account_id,),
)
return [dict(r) for r in cur.fetchall()]
# ----------------- live_balance -----------------
def save_balance(
db_path: str, account_id: int, date: str, cash: float,
market_value: float, total: float
) -> None:
with sqlite3.connect(db_path) as conn:
conn.execute(
"""INSERT INTO live_balance
(account_id, date, cash, market_value, total)
VALUES (?,?,?,?,?)""",
(account_id, date, cash, market_value, total),
)
conn.commit()
def list_balance(db_path: str, account_id: int) -> list[dict]:
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"SELECT * FROM live_balance WHERE account_id=? ORDER BY date, id",
(account_id,),
)
return [dict(r) for r in cur.fetchall()]
def get_last_balance(db_path: str, account_id: int) -> dict | None:
"""最新一条账户快照(API /live/{aid}/account)。"""
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"SELECT account_id, date, cash, market_value, total "
"FROM live_balance WHERE account_id=? ORDER BY id DESC LIMIT 1",
(account_id,),
)
row = cur.fetchone()
return dict(row) if row else None
def get_first_balance(db_path: str, account_id: int) -> dict | None:
"""最早一条账户快照(收益率基线;首快照 total = baseline)。"""
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"SELECT account_id, date, cash, market_value, total "
"FROM live_balance WHERE account_id=? ORDER BY id ASC LIMIT 1",
(account_id,),
)
row = cur.fetchone()
return dict(row) if row else None
__all__ = [
"init_db", "save_account", "list_accounts", "get_account",
"update_account_status", "list_running_accounts",
"save_trade", "list_trades",
"save_positions", "load_positions",
"save_balance", "list_balance", "get_last_balance", "get_first_balance",
]