Files
sanguo_vnpy_v2/sanguo_trader/persistence.py
T

193 lines
7.7 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 表 + checkpoint + WALspec §8 / §9.1)。
表:paper_accounts / paper_trades / paper_positions / paper_daily_balance
WAL 模式支持 worker 进程写 + 主进程读(spec §9.1 共享 DB 进度)。
"""
import json
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
SCHEMA = """
CREATE TABLE IF NOT EXISTS paper_accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT, owner_id TEXT DEFAULT 'admin', name TEXT,
mode TEXT, interval TEXT,
symbols TEXT, strategies TEXT,
initial_capital REAL, rate REAL, slippage REAL, size REAL, pricetick REAL,
stamp_duty_rate REAL, transfer_fee_rate REAL, min_commission REAL,
status TEXT, start_date TEXT, end_date TEXT,
last_run_date TEXT, next_run_at TEXT, scheduler_job_id TEXT,
checkpoint_date TEXT, error_msg TEXT,
created_at TEXT, updated_at TEXT
);
CREATE TABLE IF NOT EXISTS paper_trades (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER, strategy_id TEXT, datetime TEXT, symbol TEXT,
direction TEXT, offset TEXT, match_session TEXT,
price REAL, volume INTEGER,
commission REAL, stamp_duty REAL, transfer_fee REAL,
rejected INTEGER, reject_reason TEXT, bar_date TEXT,
blocked_by_strategy_id TEXT
);
CREATE TABLE IF NOT EXISTS paper_positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER, scope TEXT, symbol TEXT, date TEXT,
volume INTEGER, frozen INTEGER, avg_price REAL, market_value REAL,
updated_at TEXT
);
CREATE TABLE IF NOT EXISTS paper_daily_balance (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER, date TEXT,
cash REAL, market_value REAL, total_equity REAL,
per_strategy_pnl TEXT, is_checkpoint INTEGER
);
"""
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()
def save_account(db_path: str, account: dict[str, Any]) -> int:
with sqlite3.connect(db_path) as conn:
cur = conn.execute(
"""INSERT INTO paper_accounts
(task_id, owner_id, name, mode, interval, symbols, strategies,
initial_capital, rate, slippage, size, pricetick,
stamp_duty_rate, transfer_fee_rate, min_commission,
status, start_date, end_date, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
account.get("task_id"), account.get("owner_id", "admin"),
account.get("name"), account.get("mode"), account.get("interval"),
json.dumps(account.get("symbols", [])),
json.dumps(account.get("strategies", [])),
account.get("initial_capital", 0), account.get("rate", 0.0003),
account.get("slippage", 0), account.get("size", 1),
account.get("pricetick", 0.01),
account.get("stamp_duty_rate", 0.0005),
account.get("transfer_fee_rate", 0.00001),
account.get("min_commission", 5.0),
account.get("status", "pending"), account.get("start_date"),
account.get("end_date"), _now(), _now(),
),
)
conn.commit()
return cur.lastrowid
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 paper_accounts SET status=?, error_msg=?, updated_at=? WHERE id=?",
(status, error_msg, _now(), account_id),
)
conn.commit()
def save_trade(db_path: str, account_id: int, trade: dict[str, Any],
rejected: bool = False, reject_reason: str = "") -> int:
with sqlite3.connect(db_path) as conn:
cur = conn.execute(
"""INSERT INTO paper_trades
(account_id, strategy_id, datetime, symbol, direction, offset,
match_session, price, volume, commission, stamp_duty, transfer_fee,
rejected, reject_reason, bar_date, blocked_by_strategy_id)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
account_id, trade.get("strategy_id", ""), trade.get("datetime", ""),
trade.get("symbol", ""), trade.get("direction", ""),
trade.get("offset", ""), trade.get("match_session", ""),
trade.get("price", 0), trade.get("volume", 0),
trade.get("commission", 0), trade.get("stamp_duty", 0),
trade.get("transfer_fee", 0),
1 if rejected else 0, reject_reason, trade.get("bar_date", ""),
trade.get("blocked_by_strategy_id"),
),
)
conn.commit()
return cur.lastrowid
def save_daily_balance(db_path: str, account_id: int, date: str, cash: float,
market_value: float, total_equity: float,
per_strategy_pnl: dict | None = None,
is_checkpoint: bool = False) -> None:
with sqlite3.connect(db_path) as conn:
conn.execute(
"""INSERT INTO paper_daily_balance
(account_id, date, cash, market_value, total_equity,
per_strategy_pnl, is_checkpoint)
VALUES (?,?,?,?,?,?,?)""",
(account_id, date, cash, market_value, total_equity,
json.dumps(per_strategy_pnl or {}), 1 if is_checkpoint else 0),
)
conn.commit()
def update_checkpoint(db_path: str, account_id: int, date: str) -> None:
with sqlite3.connect(db_path) as conn:
conn.execute(
"UPDATE paper_accounts SET checkpoint_date=?, updated_at=? WHERE id=?",
(date, _now(), account_id),
)
conn.commit()
def load_checkpoint(db_path: str, account_id: int) -> str | None:
with sqlite3.connect(db_path) as conn:
cur = conn.execute(
"SELECT checkpoint_date FROM paper_accounts WHERE id=?", (account_id,)
)
row = cur.fetchone()
return row[0] if row else None
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 paper_trades WHERE account_id=? ORDER BY id", (account_id,)
)
return [dict(r) for r in cur.fetchall()]
def list_daily_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 paper_daily_balance WHERE account_id=? ORDER BY date",
(account_id,),
)
return [dict(r) for r in cur.fetchall()]
def list_strategy_summary(db_path: str, account_id: int) -> list[dict]:
"""按 strategy_id 聚合成交/拒单/费用(分户归因,spec §7)。"""
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cur = conn.execute(
"""SELECT strategy_id,
COUNT(*) AS total_orders,
SUM(CASE WHEN rejected=0 THEN 1 ELSE 0 END) AS filled,
SUM(CASE WHEN rejected=1 THEN 1 ELSE 0 END) AS rejected,
SUM(commission) AS commission,
SUM(stamp_duty) AS stamp_duty,
SUM(transfer_fee) AS transfer_fee
FROM paper_trades WHERE account_id=?
GROUP BY strategy_id ORDER BY strategy_id""",
(account_id,),
)
return [dict(r) for r in cur.fetchall()]