feat libs,sdk,trend

This commit is contained in:
2026-09-07 14:04:26 +08:00
parent d37f9edefc
commit e320de3241
30 changed files with 515 additions and 507 deletions

View File

@@ -1,62 +1,79 @@
"""SQLite 状态簿:每个账户/策略使用独立数据库,单个策略串行读写。"""
"""SQLite positions and deals, aligned with SDK models; one writer per database."""
from __future__ import annotations
import math
import sqlite3
from contextlib import closing
from dataclasses import asdict
from dataclasses import asdict, fields
from datetime import datetime
from pathlib import Path
from sdk import DealItem
from sdk import DealItem, PositionItem
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
stock_code TEXT NOT NULL,
stock_name TEXT NOT NULL DEFAULT '',
direction INTEGER,
volume INTEGER NOT NULL DEFAULT 0 CHECK (volume >= 0),
open_price REAL NOT NULL DEFAULT 0,
open_cost REAL NOT NULL DEFAULT 0,
float_profit REAL NOT NULL DEFAULT 0,
market_value REAL NOT NULL DEFAULT 0,
stock_holder TEXT NOT NULL DEFAULT '',
frozen_volume INTEGER NOT NULL DEFAULT 0,
can_use_volume INTEGER NOT NULL DEFAULT 0,
on_road_volume INTEGER NOT NULL DEFAULT 0,
yesterday_volume INTEGER NOT NULL DEFAULT 0,
last_price REAL NOT NULL DEFAULT 0,
profit_rate REAL NOT NULL DEFAULT 0,
future_trade_type INTEGER,
expire_date TEXT NOT NULL DEFAULT ''
);
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 UNIQUE INDEX IF NOT EXISTS idx_positions_stock_code ON positions (stock_code);
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)
id INTEGER PRIMARY KEY AUTOINCREMENT,
stock_code TEXT NOT NULL,
order_sys_id TEXT NOT NULL CHECK (order_sys_id <> ''),
order_local_id TEXT NOT NULL CHECK (order_local_id <> ''),
ref INTEGER NOT NULL DEFAULT 0,
order_ref TEXT NOT NULL DEFAULT '',
direction INTEGER NOT NULL DEFAULT 0,
offset_flag INTEGER NOT NULL CHECK (offset_flag IN (23, 24, 48, 49)),
price REAL NOT NULL CHECK (price >= 0),
volume INTEGER NOT NULL CHECK (volume > 0),
trade_amount REAL NOT NULL CHECK (trade_amount > 0),
trade_date TEXT NOT NULL,
trade_time TEXT NOT NULL,
remark TEXT NOT NULL DEFAULT '',
close_profit REAL NOT NULL DEFAULT 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);
CREATE UNIQUE INDEX IF NOT EXISTS idx_deals_order_sys_id ON deals (order_sys_id);
CREATE INDEX IF NOT EXISTS idx_deals_order_ref ON deals (order_local_id);
CREATE INDEX IF NOT EXISTS idx_deals_stock_code_date ON deals (stock_code);
CREATE INDEX IF NOT EXISTS idx_deals_date_time ON deals (trade_date);
"""
# SQL is assembled once; order_local_id is derived from the SDK remark property.
POSITION_FIELDS = tuple(field.name for field in fields(PositionItem))
DEAL_FIELDS = tuple(field.name for field in fields(DealItem)) + ('order_local_id',)
POSITION_UPSERT = (
f"INSERT INTO positions ({', '.join(POSITION_FIELDS)}) "
f"VALUES ({', '.join(':' + key for key in POSITION_FIELDS)}) "
"ON CONFLICT(stock_code) DO UPDATE SET "
+ ', '.join(f'{key} = excluded.{key}' for key in POSITION_FIELDS if key != 'stock_code')
)
DEAL_INSERT = (
f"INSERT INTO deals ({', '.join(DEAL_FIELDS)}) "
f"VALUES ({', '.join(':' + key for key in DEAL_FIELDS)})"
)
class OrderBook:
"""Persist position snapshots and append-only executions; one writer per database."""
"""Position snapshots and append-only deals. No schema migration."""
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
@@ -76,8 +93,8 @@ class OrderBook:
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')}
positions = {row['stock_code']: dict(row) for row in db.execute('SELECT * FROM positions')}
deals = {row['order_sys_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)
@@ -85,86 +102,58 @@ class OrderBook:
@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,
)
db.executemany(DEAL_INSERT, 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')
if not deal.order_sys_id or deal.volume <= 0:
raise ValueError('System order ID and positive volume are required')
if not deal.local_order_id:
raise ValueError('Local order ID is required')
row = asdict(deal)
row['order_local_id'] = deal.local_order_id
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
raise ValueError('Numeric values must be finite')
amount = deal.trade_amount if deal.trade_amount > 0 else deal.price * deal.volume
if not math.isfinite(amount) or amount <= 0:
raise ValueError('Execution amount must be positive and finite')
raise ValueError('Trade amount must be positive and finite')
row['trade_amount'] = amount
date = deal.insert_date or datetime.now().date().isoformat()
date = deal.trade_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
row['trade_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:
if deal.order_sys_id in seen:
continue
new_deals.append(self.deal_record(deal))
seen.add(deal.sys_order_id)
seen.add(deal.order_sys_id)
if not new_deals:
return
with closing(self._connect()) as db, db:
self._insert_deals(db, new_deals)
self.load()
def sync_positions(self, positions: list[PositionItem]) -> None:
"""Replace the complete position snapshot, retaining IDs for existing stocks."""
self.save({item.stock_code: asdict(item) for item in positions}, list(self.deals.values()))
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]:
positions = [{**asdict(PositionItem()), **item} for item in items.values()]
for row in [*positions, *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')
raise ValueError('Numeric values 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,
)
for row in db.execute('SELECT stock_code FROM positions').fetchall():
if row['stock_code'] not in items:
db.execute('DELETE FROM positions WHERE stock_code = ?', (row['stock_code'],))
db.executemany(POSITION_UPSERT, positions)
self._insert_deals(db, new_deals)
self.load()