This commit is contained in:
2026-09-12 16:25:34 +08:00
parent 7a7049ce44
commit 554dd0f4cb
13 changed files with 646 additions and 93 deletions

View File

@@ -2,6 +2,9 @@
from os import PathLike
from pathlib import Path
import json
import os
from tempfile import NamedTemporaryFile
def is_lock(file_path: str | PathLike[str]) -> bool:
@@ -14,3 +17,32 @@ def write_lockfile(file_path: str | PathLike[str]) -> None:
path = Path(file_path)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("LOCK", encoding="utf-8")
def claim_json(path: Path, record: dict) -> bool:
"""原子占位并落盘,成功后才允许提交;异常留下记录等待核对。"""
path.parent.mkdir(parents=True, exist_ok=True)
try:
stream = path.open('x', encoding='utf-8')
except FileExistsError:
return False
with stream:
json.dump(record, stream, ensure_ascii=False)
stream.flush()
os.fsync(stream.fileno())
return True
def replace_json(path: Path, record: dict) -> None:
"""原子替换核对结果,避免其他任务读取到半条记录。"""
temporary = None
try:
with NamedTemporaryFile(mode='w', encoding='utf-8', dir=path.parent, delete=False) as stream:
temporary = Path(stream.name)
json.dump(record, stream, ensure_ascii=False)
stream.flush()
os.fsync(stream.fileno())
os.replace(temporary, path)
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)

View File

@@ -33,9 +33,8 @@ class OrderBook:
"""线程安全的活动委托缓存。"""
def __init__(
self, order_prefix: str, lock_timeout_sec: int = 180, cancel_timeout_sec: float = 30
self, lock_timeout_sec: int = 180, cancel_timeout_sec: float = 30
) -> None:
self.order_prefix = order_prefix
self.lock_timeout_sec = max(1, lock_timeout_sec)
self.cancel_timeout_sec = timedelta(seconds=cancel_timeout_sec)
self.data: list[OrderItem] = []
@@ -59,7 +58,7 @@ class OrderBook:
def _busy_key(side: str, code: str) -> str:
return f"{side}-{code}"
def refresh(self, client: Client, orders: list[OrderItem]) -> None:
def refresh(self, client: Client, orders: list[OrderItem], *, cancel_prefix: str | None = None) -> None:
"""用账户快照刷新委托,并撤销超时的活动委托。"""
current = datetime.now()
data: list[OrderItem] = []
@@ -77,7 +76,8 @@ class OrderBook:
created_at = item.created_at
if (
created_at is not None
and item.local_order_id.startswith(f"{self.order_prefix}-")
and not item.local_order_id.startswith('IPO-')
and (cancel_prefix is None or item.local_order_id.startswith(cancel_prefix))
and status in CANCELABLE_STATUSES
and current - created_at > self.cancel_timeout_sec
):

View File

@@ -1,6 +1,7 @@
"""SQLite 策略状态与成交存储;每个数据库仅使用一个写入者,不做数据迁移。"""
import math
import json
import logging as log
import sqlite3
from contextlib import closing
@@ -144,9 +145,11 @@ class State:
self.deals, self.deals_sys_ids = deals, set(deals)
@staticmethod
def _insert_deals(db: sqlite3.Connection, deals: list[DealItem]) -> None:
def _insert_deals(db: sqlite3.Connection, deals: list[DealItem],
existing_ids: set[str] | None = None) -> None:
"""共享事务内保存成交;空本地编号使用明确的待核对标记。"""
existing = {row[0] for row in db.execute('SELECT order_sys_id FROM deals')}
existing = existing_ids if existing_ids is not None else {
row[0] for row in db.execute('SELECT order_sys_id FROM deals')}
new_deals: dict[str, DealItem] = {}
for deal in deals:
if deal.order_sys_id not in existing:
@@ -280,13 +283,16 @@ class State:
'AND is_arch = 0', (code,),
)
def _archive_pending(self, db: sqlite3.Connection, snapshot_dedup: bool = True) -> None:
def _archive_pending(self, db: sqlite3.Connection, snapshot_dedup: bool = True,
blocked_codes: set[str] | None = None) -> None:
"""保留现有逐证券保存点ZT 增量路径禁用数量相等推断。"""
codes = db.execute(
'SELECT DISTINCT stock_code FROM deals WHERE is_arch = 0'
).fetchall()
for row in codes:
code = row['stock_code']
if blocked_codes and code in blocked_codes:
continue
db.execute('SAVEPOINT archive_stock')
try:
self._archive_stock(db, code, snapshot_dedup)
@@ -314,6 +320,11 @@ class State:
holdings = {p.stock_code: p for p in positions if p.volume > 0}
with closing(self._connect()) as db, db:
db.execute('BEGIN IMMEDIATE')
db.execute('''CREATE TABLE IF NOT EXISTS zt_rejected_deals (
stock_code TEXT NOT NULL, order_sys_id TEXT NOT NULL,
payload TEXT NOT NULL, error TEXT NOT NULL,
PRIMARY KEY (stock_code, order_sys_id)
)''')
if initialize:
if (db.execute('SELECT 1 FROM state LIMIT 1').fetchone()
or db.execute('SELECT 1 FROM deals LIMIT 1').fetchone()):
@@ -330,11 +341,13 @@ class State:
# 这些成交已包含在已核对的基准中,不再增减快照库存。
db.execute('UPDATE deals SET is_arch = 1')
else:
self._insert_deals(db, deals)
self._archive_pending(db, snapshot_dedup=False)
self._insert_account_deals(db, deals)
rejected = {row[0] for row in db.execute('SELECT stock_code FROM zt_rejected_deals')}
self._archive_pending(db, snapshot_dedup=False, blocked_codes=rejected)
state = self._read_state(db)
cached_deals = self._read_deals(db)
blocked = {d['stock_code'] for d in cached_deals.values() if d['is_arch'] != 1}
blocked.update(row[0] for row in db.execute('SELECT stock_code FROM zt_rejected_deals'))
for code in set(state) | set(holdings):
row = state.get(code, {})
recorded = row.get('base_qty', 0) + row.get('added_qty', 0)
@@ -345,3 +358,35 @@ class State:
self.blocked_codes = blocked
if blocked:
log.warning('[ZT 同步] 以下证券状态待核对,暂停交易:%s', ', '.join(sorted(blocked)))
def _insert_account_deals(self, db: sqlite3.Connection, deals: list[DealItem]) -> None:
"""仅 ZT 增量路径隔离坏成交;保留原始字段,缺席回报不会解除隔离。"""
existing = {row[0] for row in db.execute('SELECT order_sys_id FROM deals')}
for deal in deals:
if not deal.stock_code:
raise ValueError('Trade without stock code cannot be isolated')
db.execute('SAVEPOINT insert_zt_deal')
try:
if not deal.stock_code or not deal.order_sys_id:
raise ValueError('Trade code and order ID are required')
if (deal.offset_flag not in (FLAG_BUY, FLAG_SELL)
or not math.isfinite(deal.volume) or deal.volume <= 0
or int(deal.volume) != deal.volume
or not math.isfinite(deal.price) or deal.price < 0
or not math.isfinite(deal.trade_amount)
or not math.isfinite(deal.close_profit)):
raise ValueError('Invalid trade direction, volume or amount')
self._insert_deals(db, [deal], existing)
except (ValueError, TypeError, OverflowError, sqlite3.IntegrityError) as exc:
db.execute('ROLLBACK TO insert_zt_deal')
db.execute('''INSERT OR REPLACE INTO zt_rejected_deals
(stock_code, order_sys_id, payload, error) VALUES (?, ?, ?, ?)''',
(deal.stock_code, deal.order_sys_id, json.dumps(asdict(deal), ensure_ascii=False), str(exc)))
log.warning('[ZT 成交] 隔离证券=%s,委托=%s,原因=%s',
deal.stock_code, deal.order_sys_id, exc)
else:
existing.add(deal.order_sys_id)
db.execute('DELETE FROM zt_rejected_deals WHERE stock_code = ? AND order_sys_id = ?',
(deal.stock_code, deal.order_sys_id))
finally:
db.execute('RELEASE insert_zt_deal')