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 守恒误报回归:引擎跨交易日存活(进程不死)时,**昨日**的 终态足额订单不得进当日守恒对比——台账只记当日行,历史订单计入