diff --git a/sanguo_portfolio/live_reconcile.py b/sanguo_portfolio/live_reconcile.py index 66dccdf..bddf56b 100644 --- a/sanguo_portfolio/live_reconcile.py +++ b/sanguo_portfolio/live_reconcile.py @@ -284,11 +284,15 @@ def eod_reconcile(engine: Any, ledger: Any, db: str, account_id: int, ledger_vol[vkey] = ledger_vol.get(vkey, 0) + int(r.get("amount") or 0) # 守恒校验:订单成交合计 vs 台账记录合计(前缀 6 位码归一,跨日订单不计) + # ⚠️ 订单时间字段=bullet_trade Order.add_time(08-31 修:原误读不存在的 + # ``datetime`` 属性恒 None → 跨日过滤从不生效,引擎跨交易日存活时历史 + # 订单全进对比 → 每日假缺口告警;测试替身当时被喂了 datetime= 字段, + # 把 bug 行为锁进了断言——替身必须贴真实字段名) order_vol: Dict[tuple, int] = {} for o in own_by_broker.values(): if _status_str(getattr(o, "status", None)) not in _TERMINAL: continue - o_dt = getattr(o, "datetime", None) + o_dt = getattr(o, "add_time", None) if o_dt is not None and not str(o_dt).startswith(today): continue filled = int(getattr(o, "filled", 0) or 0) @@ -313,7 +317,7 @@ def eod_reconcile(engine: Any, ledger: Any, db: str, account_id: int, for o in own_by_broker.values(): if _status_str(getattr(o, "status", None)) not in ("rejected", "failed", "error"): continue - o_dt = getattr(o, "datetime", None) + o_dt = getattr(o, "add_time", None) if o_dt is not None and not str(o_dt).startswith(today): continue if int(getattr(o, "filled", 0) or 0) > 0: diff --git a/tests/portfolio/test_live_reconcile.py b/tests/portfolio/test_live_reconcile.py index 5c97acd..ae33479 100644 --- a/tests/portfolio/test_live_reconcile.py +++ b/tests/portfolio/test_live_reconcile.py @@ -26,11 +26,13 @@ from sanguo_portfolio.live_reconcile import ( # ------------------ 公共替身 ------------------ def _order(oid="o1", broker_oid="1001", security="000049.XSHE", is_buy=True, - amount=500, filled=0, status="open", datetime_=None): + amount=500, filled=0, status="open", add_time_=None): + """替身字段名必须贴真实 bullet_trade Order(08-31 教训:曾被喂不存在的 + ``datetime``=,把守恒跨日过滤的属性错位 bug 锁进了断言)。""" return SimpleNamespace( order_id=oid, _broker_order_id=broker_oid, security=security, is_buy=is_buy, amount=amount, filled=filled, status=status, - datetime=datetime_, + add_time=add_time_, ) @@ -361,13 +363,30 @@ class TestEodReconcile: eng = _engine( [_order(status="rejected", filled=300, amount=500, broker_oid="a"), _order(status="rejected", filled=0, amount=200, broker_oid="b", - datetime_="2020-01-01 10:00:00")], + add_time_="2020-01-01 10:00:00")], []) with caplog.at_level("WARNING"): summary = eod_reconcile(eng, led, db, 19, "s") assert summary["rejected_orders"] == [] assert not any("废单" in r.message for r in caplog.records) + def test_yesterday_order_no_conservation_gap(self, db, caplog): + """08-31 守恒误报回归:引擎跨交易日存活(进程不死)时,**昨日**的 + 终态足额订单不得进当日守恒对比——台账只记当日行,历史订单计入 + 必然产生假缺口(原 ``datetime`` 属性误读恒 None,过滤从不生效)。""" + led = LiveInstanceLedger(initial_cash=1_000_000) + from datetime import timedelta as _td + yday = (datetime.now() - _td(days=1)).strftime("%Y-%m-%d 09:30:00") + eng = _engine( + [_order(status="filled", filled=500, broker_oid="old", + add_time_=yday), + _order(status="filled", filled=500, broker_oid="new")], + [_qmt_trade(order_id="new", trade_id="t-new")]) + with caplog.at_level("WARNING"): + summary = eod_reconcile(eng, led, db, 19, "s") + assert summary["conservation_gaps"] == [] + assert summary["rejected_orders"] == [] + def test_gap_warning_dedup_on_identical_retry(self, db, caplog): """缺口重试轮:内容不变只首轮回 WARNING,重复轮降级 INFO 防刷屏。""" from sanguo_live.persistence import save_trade