From b695874b3cbf2ef31890c766885f724cd21495dc Mon Sep 17 00:00:00 2001 From: claude_dev Date: Mon, 31 Aug 2026 18:32:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(live):=20=E5=AE=88=E6=81=92/=E5=BA=9F?= =?UTF-8?q?=E5=8D=95=E8=B7=A8=E6=97=A5=E8=BF=87=E6=BB=A4=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E9=94=99=E4=BD=8D=E6=A0=B9=E6=B2=BB=E2=80=94=E2=80=94=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=E6=97=B6=E9=97=B4=E5=AD=97=E6=AE=B5=E8=AF=AF=E8=AF=BB?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=E7=9A=84datetime(bullet=5Ftrade=20O?= =?UTF-8?q?rder=E5=AE=9E=E4=B8=BAadd=5Ftime,=E5=85=A8=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E6=89=AB=E6=8F=8F=E5=AE=9E=E9=94=A4)=E6=81=92None=E2=86=92?= =?UTF-8?q?=E8=B7=A8=E6=97=A5=E8=BF=87=E6=BB=A4=E4=BB=8E=E4=B8=8D=E7=94=9F?= =?UTF-8?q?=E6=95=88,=E5=BC=95=E6=93=8E=E8=B7=A8=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=97=A5=E5=AD=98=E6=B4=BB=E6=97=B6=E5=8E=86=E5=8F=B2=E7=BB=88?= =?UTF-8?q?=E6=80=81=E8=AE=A2=E5=8D=95=E5=85=A8=E8=BF=9B=E5=BD=93=E6=97=A5?= =?UTF-8?q?=E5=AE=88=E6=81=92=E5=AF=B9=E6=AF=94(=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=E5=8F=AA=E8=AE=B0=E5=BD=93=E6=97=A5=E8=A1=8C)=E2=86=92?= =?UTF-8?q?=E6=AF=8F=E6=97=A5=E5=81=87=E7=BC=BA=E5=8F=A3=E5=91=8A=E8=AD=A6?= =?UTF-8?q?+=E9=87=8D=E8=AF=95=E5=88=B7=E5=88=B023:00;09-01=E8=BF=9B?= =?UTF-8?q?=E7=A8=8B=E5=B0=86=E9=A6=96=E6=AC=A1=E8=B7=A8=E6=97=A5=E5=AD=98?= =?UTF-8?q?=E6=B4=BB=E5=BF=85=E9=A6=96=E5=8F=91=E4=BD=9C;=E4=B8=A4?= =?UTF-8?q?=E5=A4=84=E5=90=8C=E4=BF=AE(=E5=AE=88=E6=81=92+=E5=BA=9F?= =?UTF-8?q?=E5=8D=95=E6=B8=85=E5=8D=95),=E6=B5=8B=E8=AF=95=E6=9B=BF?= =?UTF-8?q?=E8=BA=ABdatetime=5F=3D=E6=94=B9=E5=9B=9Eadd=5Ftime=5F=3D(08-29?= =?UTF-8?q?=E5=91=A8=E6=9C=AB=E6=8F=90=E4=BA=A4=E6=9B=BE=E5=96=82=E4=B8=8D?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E7=9A=84datetime=3D=E6=8A=8Abug=E9=94=81?= =?UTF-8?q?=E8=BF=9B=E6=96=AD=E8=A8=80)+=E6=96=B0=E5=A2=9E=E6=98=A8?= =?UTF-8?q?=E6=97=A5=E7=BB=88=E6=80=81=E8=AE=A2=E5=8D=95=E9=9B=B6=E7=BC=BA?= =?UTF-8?q?=E5=8F=A3=E5=9B=9E=E5=BD=92;531=E7=BB=BF=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_portfolio/live_reconcile.py | 8 ++++++-- tests/portfolio/test_live_reconcile.py | 25 ++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) 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