# -*- coding: utf-8 -*- """成交回报回执日志单测(①,2026-09-04 守恒缺口观测件)。 契约(dev 2026-09-06 钦定,② 解析本格式——字段名与顺序不得改): [trade-receipt] tid={柜台成交号} order={订单号} acc={账号} sym={代码} vol={量} px={价} t_raw={回报自带时间,无则NONE} recv={本地时刻} 铁律: - 任何处理/匹配/去重**之前**打(挂点=broker.get_trades 包装,bullet_trade 轮询制下它是柜台行进本进程的唯一咽喉); - t_raw 原样照记,None/退化也记(09-04 事故根因=时间字段中途退化); - 日志永不抛、永不影响交易路径(get_trades 原异常照抛,行为不变); - 观测层变化检测:同 tid 状态签名不变不重打,退化/漂移即重打——行照常 流向消费方,这不是处理层去重。 """ import logging import pytest from sanguo_portfolio.live_trade_receipt import ( log_trade_receipts, with_trade_receipt, ) _ROW = { "trade_id": "ecf0fbda55b9765f", "order_id": "1098910841", "security": "002633.SZ", "amount": 100, "price": 14.84, "time": "20260904133738", } @pytest.fixture(autouse=True) def _clear_seen(caplog): from sanguo_portfolio import live_trade_receipt live_trade_receipt._RECEIPT_SEEN.clear() caplog.set_level(logging.INFO, logger="sanguo_portfolio.live_trade_receipt") yield live_trade_receipt._RECEIPT_SEEN.clear() def test_receipt_line_format_pins_contract(caplog): """格式铁契:tag+八字段顺序,recv 为本地时刻。""" log_trade_receipts([_ROW], "66639661") lines = [r.getMessage() for r in caplog.records if r.getMessage().startswith("[trade-receipt]")] assert len(lines) == 1 assert lines[0].startswith( "[trade-receipt] tid=ecf0fbda55b9765f order=1098910841 " "acc=66639661 sym=002633.SZ vol=100 px=14.84 " "t_raw=20260904133738 recv=") recv = lines[0].rsplit("recv=", 1)[1] assert len(recv) == 19 and recv[4] == "-" and recv[13] == ":" # YYYY-MM-DD HH:MM:SS def test_receipt_t_raw_none_logged_as_none(caplog): """t_raw=None → 字面 NONE(09-04 退化形态照记)。""" log_trade_receipts([dict(_ROW, time=None)], "a1") assert "t_raw=NONE" in caplog.records[-1].getMessage() def test_repeat_same_state_suppressed(caplog): """同 tid 状态签名不变 → 只打一次(观测层量控)。""" for _ in range(3): log_trade_receipts([_ROW], "a1") lines = [r.getMessage() for r in caplog.records if r.getMessage().startswith("[trade-receipt]")] assert len(lines) == 1 def test_degraded_t_raw_relogged(caplog): """t_raw 中途退化(有值→None)→ 签名变化即重打——捕获 09-04 形态。""" log_trade_receipts([_ROW], "a1") log_trade_receipts([dict(_ROW, time=None)], "a1") lines = [r.getMessage() for r in caplog.records if r.getMessage().startswith("[trade-receipt]")] assert len(lines) == 2 assert "t_raw=NONE" in lines[1] def test_bad_rows_skipped_never_raise(caplog): """非 dict 行 / 缺 tid 行:静默跳过,绝不抛。""" log_trade_receipts([None, "junk", {"order_id": "x"}, 42], "a1") assert not [r for r in caplog.records if r.getMessage().startswith("[trade-receipt]")] class _Broker: def __init__(self, rows=None, exc=None): self._rows, self._exc = rows or [], exc def get_trades(self): if self._exc: raise self._exc return self._rows def test_wrapper_returns_rows_unchanged(caplog): """包装层透传:同一列表原样返回,回执即打。""" rows = [dict(_ROW)] b = with_trade_receipt(_Broker(rows), "66639661") out = b.get_trades() assert out is rows assert any(r.getMessage().startswith("[trade-receipt]") for r in caplog.records) def test_wrapper_preserves_original_exception(): """get_trades 原异常照抛(不吞不改消费方行为)。""" b = with_trade_receipt(_Broker(exc=RuntimeError("qmt down")), "a1") with pytest.raises(RuntimeError, match="qmt down"): b.get_trades()