"""SQLite 状态簿:每个账户/策略使用独立数据库,单个策略串行读写。""" from __future__ import annotations import math import sqlite3 from contextlib import closing from dataclasses import asdict from datetime import datetime from pathlib import Path from sdk import DealItem SCHEMA = """ CREATE TABLE IF NOT EXISTS positions ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL, base_order_id TEXT NOT NULL DEFAULT '', base_qty INTEGER NOT NULL DEFAULT 0 CHECK (base_qty >= 0), base_cost REAL NOT NULL DEFAULT 0.0 CHECK (base_cost >= 0), added_order_id TEXT NOT NULL DEFAULT '', added_num INTEGER NOT NULL DEFAULT 0 CHECK (added_num >= 0), added_qty INTEGER NOT NULL DEFAULT 0 CHECK (added_qty >= 0), added_cost REAL NOT NULL DEFAULT 0.0 CHECK (added_cost >= 0), status TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS idx_positions_code ON positions (code); CREATE INDEX IF NOT EXISTS idx_positions_base_order_id ON positions (base_order_id); CREATE INDEX IF NOT EXISTS idx_positions_added_order_id ON positions (added_order_id); CREATE TABLE IF NOT EXISTS deals ( id INTEGER PRIMARY KEY, sys_order_id TEXT NOT NULL UNIQUE CHECK (sys_order_id <> ''), local_order_id TEXT NOT NULL, code TEXT NOT NULL, instrument_id TEXT NOT NULL, exchange_id TEXT NOT NULL, name TEXT NOT NULL, offset_flag TEXT NOT NULL, side TEXT NOT NULL CHECK (side IN ('BUY', 'SELL')), status TEXT NOT NULL, remaining_volume INTEGER NOT NULL CHECK (remaining_volume >= 0), traded_volume INTEGER NOT NULL CHECK (traded_volume > 0), order_time INTEGER NOT NULL, insert_date TEXT NOT NULL, insert_time TEXT NOT NULL, remark TEXT NOT NULL, price REAL NOT NULL, trade_price REAL NOT NULL CHECK (trade_price >= 0), trade_amount REAL NOT NULL CHECK (trade_amount > 0) ); CREATE INDEX IF NOT EXISTS idx_deals_local_order_id ON deals (local_order_id); CREATE INDEX IF NOT EXISTS idx_deals_code_date ON deals (code, insert_date); CREATE INDEX IF NOT EXISTS idx_deals_date_time ON deals (insert_date, insert_time); """ class OrderBook: """Persist position snapshots and append-only executions; one writer per database.""" def __init__(self, path: str | Path) -> None: self.path = Path(path) self.positions: dict[str, dict] = {} self.deals: dict[str, dict] = {} self.deals_sys_ids: set[str] = set() self.path.parent.mkdir(parents=True, exist_ok=True) with closing(self._connect()) as db: db.executescript(SCHEMA) self.load() def _connect(self) -> sqlite3.Connection: db = sqlite3.connect(self.path, timeout=30) db.row_factory = sqlite3.Row return db def load(self) -> None: with closing(self._connect()) as db, db: db.execute('BEGIN') positions = {row['code']: dict(row) for row in db.execute('SELECT * FROM positions')} deals = {row['sys_order_id']: dict(row) for row in db.execute('SELECT * FROM deals ORDER BY id')} self.positions = positions self.deals = deals self.deals_sys_ids = set(deals) self._deal_count = len(deals) @staticmethod def _insert_deals(db: sqlite3.Connection, deals: list[dict]) -> None: db.executemany( """INSERT INTO deals (sys_order_id, local_order_id, code, instrument_id, exchange_id, name, offset_flag, side, status, remaining_volume, traded_volume, order_time, insert_date, insert_time, remark, price, trade_price, trade_amount) VALUES (:sys_order_id, :local_order_id, :code, :instrument_id, :exchange_id, :name, :offset_flag, :side, :status, :remaining_volume, :traded_volume, :order_time, :insert_date, :insert_time, :remark, :price, :trade_price, :trade_amount)""", deals, ) @staticmethod def deal_record(deal: DealItem) -> dict: """API 字段转入库记录;委托价格不用于推算成交金额。""" if not deal.sys_order_id or deal.traded_volume <= 0: raise ValueError('System order ID and positive traded volume are required') row = asdict(deal) if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()): raise ValueError('Prices and amounts must be finite') amount = deal.trade_amount if deal.trade_amount > 0 else deal.trade_price * deal.traded_volume if not math.isfinite(amount) or amount <= 0: raise ValueError('Execution amount must be positive and finite') row['trade_amount'] = amount date = deal.insert_date or datetime.now().date().isoformat() if len(date) == 8 and date.isdigit(): date = f'{date[:4]}-{date[4:6]}-{date[6:]}' row['insert_date'] = date return row def sync_deals(self, deals: list[DealItem]) -> None: """按系统订单号去重,批量写入已成交数据;失败时不更新缓存。""" new_deals = [] seen = self.deals_sys_ids.copy() for deal in deals: if deal.sys_order_id in seen: continue new_deals.append(self.deal_record(deal)) seen.add(deal.sys_order_id) if not new_deals: return with closing(self._connect()) as db, db: self._insert_deals(db, new_deals) self.load() def save(self, items: dict, deals: list[dict]) -> None: if len(deals) < self._deal_count: raise ValueError('Execution history is append-only') new_deals = deals[self._deal_count:] positions = [ { 'base_order_id': '', 'base_qty': 0, 'base_cost': 0.0, 'added_order_id': '', 'added_num': 0, 'added_qty': 0, 'added_cost': 0.0, **item, } for item in items.values() ] for row in [*items.values(), *new_deals]: if any(isinstance(value, float) and not math.isfinite(value) for value in row.values()): raise ValueError('Quantities, prices and amounts must be finite') with closing(self._connect()) as db, db: # 更新已有证券时保留其自增 ID;仅删除快照中已移除的证券。 for row in db.execute('SELECT code FROM positions').fetchall(): if row['code'] not in items: db.execute('DELETE FROM positions WHERE code = ?', (row['code'],)) db.executemany( """INSERT INTO positions (code, base_order_id, base_qty, base_cost, added_order_id, added_num, added_qty, added_cost, status) VALUES (:code, :base_order_id, :base_qty, :base_cost, :added_order_id, :added_num, :added_qty, :added_cost, :status) ON CONFLICT(code) DO UPDATE SET base_order_id = excluded.base_order_id, base_qty = excluded.base_qty, base_cost = excluded.base_cost, added_order_id = excluded.added_order_id, added_num = excluded.added_num, added_qty = excluded.added_qty, added_cost = excluded.added_cost, status = excluded.status""", positions, ) self._insert_deals(db, new_deals) self.load()