fix bug
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
):
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user