From 1d9265b922fe247aa7744ccc1b9a697ea5425734 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Tue, 1 Sep 2026 12:17:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(live):=20=E5=BA=9F=E5=8D=95=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=E8=A1=A5=E7=AC=AC=E4=BA=8C=E8=82=A2=E6=B2=BB=E6=9F=9C?= =?UTF-8?q?=E5=8F=B0=E7=BA=A7=E6=8B=92=E5=8D=95=E7=9B=B2=E5=8C=BA=E2=80=94?= =?UTF-8?q?=E2=80=9408-31=20T+1=E6=8E=A2=E9=92=88=E5=AE=9E=E9=94=A4:?= =?UTF-8?q?=E6=9F=9C=E5=8F=B0=E6=8B=92=E5=8D=95=E4=B8=8D=E8=BF=9BQMT?= =?UTF-8?q?=E5=BD=93=E6=97=A5=E8=AE=A2=E5=8D=95API(query=5Fstock=5Forders?= =?UTF-8?q?=2015=E7=AC=94=E5=85=A8SUCCEEDED=E4=B8=8D=E5=90=AB=E8=A2=AB?= =?UTF-8?q?=E6=8B=92=E5=8D=96=E5=8D=95),=E8=AE=A2=E5=8D=95=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=B0=B8=E5=81=9C=E9=9D=9E=E7=BB=88=E6=80=81,?= =?UTF-8?q?=E5=8E=9F=E5=91=8A=E8=AD=A6=E7=AD=89=E6=9F=9C=E5=8F=B0=E6=8A=A5?= =?UTF-8?q?rejected=E7=BB=93=E6=9E=84=E6=80=A7=E7=AD=89=E4=B8=8D=E5=88=B0;?= =?UTF-8?q?=E6=96=B0=E8=82=A2=3D=E5=BD=93=E6=97=A5=E8=87=AA=E6=8F=90?= =?UTF-8?q?=E4=BA=A4=E8=AE=A2=E5=8D=95EOD=E4=BB=8D=E9=9D=9E=E7=BB=88?= =?UTF-8?q?=E6=80=81=E4=B8=94=E9=9B=B6=E6=88=90=E4=BA=A4=E2=86=92=E7=96=91?= =?UTF-8?q?=E4=BC=BC=E5=BA=9F=E5=8D=95(=E6=8F=90=E4=BA=A4=E4=BE=A7?= =?UTF-8?q?=E8=87=AA=E8=AF=81,=E4=B8=8D=E4=BE=9D=E8=B5=96QMT=E6=8A=A5?= =?UTF-8?q?=E7=8A=B6=E6=80=81;=E6=AD=A3=E5=B8=B8=E6=92=A4=E5=8D=95?= =?UTF-8?q?=E5=B7=B2=E5=88=B0=E7=BB=88=E6=80=81=E4=B8=8D=E8=AE=A1,?= =?UTF-8?q?=E9=83=A8=E5=88=86=E6=88=90=E4=BA=A4=E6=AE=8B=E9=87=8F=E4=BB=8D?= =?UTF-8?q?=E5=BD=92=E5=AE=88=E6=81=92=E7=AE=A1);=E4=B8=A4=E8=82=A2?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=85=A5rejected=5Forders=E5=B8=A6why?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=8C=BA=E5=88=86;+2=E6=B5=8B=E8=AF=95(?= =?UTF-8?q?=E7=96=91=E4=BC=BC=E5=8D=95=E5=91=BD=E4=B8=AD/=E6=92=A4?= =?UTF-8?q?=E5=8D=95=E4=B8=8E=E8=B7=A8=E6=97=A5=E4=B8=8D=E8=AF=AF=E6=8A=A5?= =?UTF-8?q?);=E5=85=A8=E9=87=8F1368=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 | 28 +++++++++++++++------- tests/portfolio/test_live_reconcile.py | 33 +++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/sanguo_portfolio/live_reconcile.py b/sanguo_portfolio/live_reconcile.py index bddf56b..58f352c 100644 --- a/sanguo_portfolio/live_reconcile.py +++ b/sanguo_portfolio/live_reconcile.py @@ -310,22 +310,34 @@ def eod_reconcile(engine: Any, ledger: Any, db: str, account_id: int, "order_volume": ov, "ledger_volume": lv, "gap": ov - lv, }) - # 当日废单告警(filled=0 的 rejected/failed):08-28 600818 实锤——盘中 - # 废单只有 ERROR 日志躺在文件里,复盘才发现当日目标缺仓 ~8 万;EOD 汇总 - # 让它当天可见。部分成交残量不进此清单(守恒/补插路径已覆盖)。 + # 当日废单告警:08-28 600818 实锤——盘中废单只有 ERROR 日志躺在文件里, + # 复盘才发现当日目标缺仓 ~8 万;EOD 汇总让它当天可见。部分成交残量不进 + # 此清单(守恒/补插路径已覆盖)。两肢: + # ① 柜台报 rejected/failed/error(交易所级拒单,QMT 订单视图可见); + # ② EOD 仍非终态且零成交的当日自提交订单 = 疑似废单——柜台级拒单 + # 不进 QMT 当日订单 API(09-01 前实测 08-31 T+1 探针:query_stock_orders + # 15 笔全 SUCCEEDED,被拒的卖单根本不在列表),状态永停非终态,等 + # 柜台报状态等不到,只能提交侧自证:下过的单收盘零成交又没到终态, + # 就是废单/挂死(当日目标缺仓同形态;正常撤单已到终态不计)。 rejected_orders = [] for o in own_by_broker.values(): - if _status_str(getattr(o, "status", None)) not in ("rejected", "failed", "error"): - continue + st = _status_str(getattr(o, "status", 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: continue + if st in ("rejected", "failed", "error"): + why = "废单(柜台报rejected)" + elif st not in _TERMINAL: + why = "疑似废单(EOD非终态零成交)" + else: + continue rejected_orders.append({ "symbol": str(getattr(o, "security", "") or "")[:6], "side": "buy" if bool(getattr(o, "is_buy", True)) else "sell", - "amount": int(getattr(o, "amount", 0) or 0)}) + "amount": int(getattr(o, "amount", 0) or 0), + "why": why}) summary = {"qmt_trades": len(trades_all), "ours": ours, "backfilled": backfilled, @@ -363,8 +375,8 @@ def eod_reconcile(engine: Any, ledger: Any, db: str, account_id: int, rsig = "%s|%s" % (today, rejected_orders) if rsig != _LAST_REJ_LOG_SIG: logger.warning( - "[live-reconcile] 当日废单 %d 笔 (account=%s): %s——当日目标缺仓," - "查柜台废单原因(600818 形态)", + "[live-reconcile] 当日废单/疑似废单 %d 笔 (account=%s): %s——当日" + "目标缺仓,查柜台废单原因(600818/柜台拒单不进API 两形态)", len(rejected_orders), account_id, rejected_orders) _LAST_REJ_LOG_SIG = rsig return summary diff --git a/tests/portfolio/test_live_reconcile.py b/tests/portfolio/test_live_reconcile.py index ae33479..fad516f 100644 --- a/tests/portfolio/test_live_reconcile.py +++ b/tests/portfolio/test_live_reconcile.py @@ -354,7 +354,8 @@ class TestEodReconcile: with caplog.at_level("WARNING"): summary = eod_reconcile(eng, led, db, 19, "momentum") assert summary["rejected_orders"] == [ - {"symbol": "600818", "side": "buy", "amount": 10900}] + {"symbol": "600818", "side": "buy", "amount": 10900, + "why": "废单(柜台报rejected)"}] assert any("废单" in r.message for r in caplog.records) def test_rejected_partial_or_crossday_not_in_alarm(self, db, caplog): @@ -370,6 +371,36 @@ class TestEodReconcile: assert summary["rejected_orders"] == [] assert not any("废单" in r.message for r in caplog.records) + def test_stale_order_suspected_alarm(self, db, caplog): + """09-01 盲区补第二肢:当日订单 EOD 非终态+零成交 = 疑似废单。 + + 08-31 T+1 探针实锤:柜台级拒单不进 QMT 当日订单 API,订单状态 + 永停非终态——等柜台报 rejected 等不到,只能提交侧自证。 + """ + led = LiveInstanceLedger() + eng = _engine( + [_order(status="submitted", filled=0, amount=100, is_buy=False, + security="300059.XSHE", broker_oid="p1")], []) + with caplog.at_level("WARNING"): + summary = eod_reconcile(eng, led, db, 18, "channel_test") + assert summary["rejected_orders"] == [ + {"symbol": "300059", "side": "sell", "amount": 100, + "why": "疑似废单(EOD非终态零成交)"}] + assert any("废单" in r.message for r in caplog.records) + + def test_cancelled_or_crossday_stale_not_suspected(self, db, caplog): + """正常撤单(零成交但已到终态)与昨日挂死单都不进疑似清单。""" + led = LiveInstanceLedger() + eng = _engine( + [_order(status="cancelled", filled=0, broker_oid="c1"), + _order(status="submitted", filled=0, broker_oid="c2", + add_time_="2020-01-01 10:00:00")], + []) + with caplog.at_level("WARNING"): + summary = eod_reconcile(eng, led, db, 18, "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 守恒误报回归:引擎跨交易日存活(进程不死)时,**昨日**的 终态足额订单不得进当日守恒对比——台账只记当日行,历史订单计入