Files
big-qmt/py-client/libs/orderbook.py
2026-09-07 00:27:33 +08:00

171 lines
7.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 状态簿:每个账户/策略使用独立数据库,单个策略串行读写。"""
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()