112 lines
4.0 KiB
Python
112 lines
4.0 KiB
Python
# -*- 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()
|