Files
sanguo_vnpy_v2/sanguo_portfolio/live_trade_receipt.py
T
claude_dev 90b37ce27f
CI/CD / test (push) Successful in 13s
CI/CD / nas-deploy (push) Successful in 46s
CI/CD / nas-verify (push) Successful in 9s
feat(live): 成交回报回执日志①——09-04守恒缺口观测件 [vps]——根因件:002633/002836缺口复盘发现引擎会话当日成交视图行缺+时间字段中途退化None,QMT行dump全打查询时刻即铁证,事后无从对「哪笔什么时候以什么形态被看到」;用户钦点观测机制「再出现时根因可查」,dev契约:任何处理/匹配/去重之前打[trade-receipt] tid/order/acc/sym/vol/px/t_raw/recv八字段(②解析此格式,拼写冻结);实现=with_trade_receipt包装QmtBroker.get_trades(bullet_trade轮询制无成交推送回调,_Callback仅桥接断连,broker.get_trades是柜台行进本进程唯一咽喉,引擎同步/a窄修/b宽修全经它),与with_connect_retry同款实例级包装源码零改动;铁律=t_raw原样照记None也记/日志永不抛逐行try-except包死/get_trades原异常照抛行为不变;量控=观测层变化检测非处理层去组:同tid签名(t_raw+量+价+单号)不变不重打,退化即重打(捕获09-04形态),行照常流向消费方;③结论免改:vt_tradeid三条归因路径已承载柜台成交号(即时=引擎trades键/a窄修=trade_id/b宽修=eod:前缀),既有测试已钉(test_live_reconcile.py:123,206+test_live_instance_ledger.py:236),②按tid比回执↔live_trades即闭环;TDD 7新测先红后绿(caplog须set_level(INFO)坑)+598全绿
2026-09-06 11:14:29 +08:00

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