97 lines
3.8 KiB
Python
97 lines
3.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""成交回报回执日志(①,2026-09-04 守恒缺口观测件)。
|
|
|
|
挂点=包装 QmtBroker.get_trades:bullet_trade 是轮询制(无成交推送回调,
|
|
qmt._Callback 仅桥接断连),``broker.get_trades()`` 是柜台成交行进入本进程
|
|
的唯一咽喉——引擎同步(_sync_trades_from_broker)、a窄修(reconcile_pending)、
|
|
b宽修(eod_reconcile) 全部经它取行。回执在**任何处理/匹配/去重之前**打。
|
|
|
|
行格式(dev 2026-09-06 契约,② 解析——字段名与顺序不得改):
|
|
[trade-receipt] tid={柜台成交号} order={订单号} acc={账号} sym={代码}
|
|
vol={量} px={价} t_raw={回报自带时间,无则NONE} recv={本地时刻}
|
|
|
|
铁律:
|
|
- t_raw 原样照记,None/退化也记——09-04 事故根因即时间字段中途退化,
|
|
补插五元组无从对齐,回执即为此留证;
|
|
- 日志永不抛、永不影响交易路径(逐行 try/except 包死;get_trades 本身的
|
|
异常原样上抛,不改变消费方行为);
|
|
- 量控=观测层变化检测,非处理层去重:同 tid 状态签名(t_raw+量+价+单号)
|
|
不变不重打,退化/漂移即重打;行照常流向所有消费方。签名表进程内
|
|
数百行量级(当日成交数),引擎轮换即清,无需淘汰。
|
|
|
|
③ 注(同批观测契约):live_trades.vt_tradeid 三条归因路径已承载柜台成交号
|
|
(即时归因=引擎 trades 键、a窄修=trade_id、b宽修=eod: 前缀),测试已钉死,
|
|
无 schema 改动;② 用本回执与 live_trades 行按 tid 比对即闭环。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# tid -> 状态签名(观测层变化检测;线程安全=py dict 原子操作,竞争最坏多打一行)
|
|
_RECEIPT_SEEN: Dict[str, str] = {}
|
|
|
|
|
|
def _first_present(row: Dict[str, Any], *names: str) -> Any:
|
|
for n in names:
|
|
v = row.get(n)
|
|
if v is not None:
|
|
return v
|
|
return None
|
|
|
|
|
|
def _t_raw_str(v: Any) -> str:
|
|
return "NONE" if v is None else str(v)
|
|
|
|
|
|
def log_trade_receipts(rows: Optional[List[Any]], account: str) -> None:
|
|
"""逐行打回执(纯日志副作用,永不抛)。"""
|
|
if not rows:
|
|
return
|
|
recv = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
for r in rows:
|
|
try:
|
|
if not isinstance(r, dict):
|
|
continue
|
|
tid = str(_first_present(r, "trade_id") or "")
|
|
if not tid:
|
|
continue
|
|
t_raw = _first_present(r, "time", "trade_time")
|
|
vol = _first_present(r, "amount", "volume", "trade_volume")
|
|
px = _first_present(r, "price", "traded_price")
|
|
order = _first_present(r, "order_id", "entrust_id")
|
|
sig = "|".join((_t_raw_str(t_raw), str(vol), str(px), str(order)))
|
|
if _RECEIPT_SEEN.get(tid) == sig:
|
|
continue
|
|
_RECEIPT_SEEN[tid] = sig
|
|
logger.info(
|
|
"[trade-receipt] tid=%s order=%s acc=%s sym=%s vol=%s px=%s "
|
|
"t_raw=%s recv=%s",
|
|
tid, order, account, _first_present(r, "security"),
|
|
vol, px, _t_raw_str(t_raw), recv)
|
|
except Exception: # noqa: BLE001 - 回执绝不影响交易路径
|
|
pass
|
|
|
|
|
|
def with_trade_receipt(broker: Any, account: str) -> Any:
|
|
"""包一层 broker.get_trades:返回行交给消费方之前先打回执。
|
|
|
|
与 ``with_connect_retry`` 同款实例级包装(bullet_trade 源码零改动);
|
|
get_trades 原异常照抛,回执打点失败吞掉不声张。
|
|
"""
|
|
orig = broker.get_trades
|
|
|
|
def _get_trades() -> Any:
|
|
rows = orig()
|
|
try:
|
|
log_trade_receipts(rows, account)
|
|
except Exception: # noqa: BLE001 - 双保险,契约=永不影响交易路径
|
|
pass
|
|
return rows
|
|
|
|
broker.get_trades = _get_trades # type: ignore[method-assign]
|
|
return broker
|