From 31596f58f9a5d5cd4ea5bbf29b718b8a50fb87a9 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Mon, 24 Aug 2026 23:07:12 +0800 Subject: [PATCH] =?UTF-8?q?fix(shadow):=20=E5=8F=8C=E8=BD=A8=E5=AF=B9?= =?UTF-8?q?=E8=B4=A6count=5Fmatch=E6=94=B9=E6=8C=89=E7=A5=A8+=E6=96=B9?= =?UTF-8?q?=E5=90=91=E8=81=9A=E5=90=88=E6=80=BB=E9=87=8F=E2=80=94=E2=80=94?= =?UTF-8?q?QMT=E9=83=A8=E5=88=86=E6=88=90=E4=BA=A4=E6=8B=86=E8=A1=8C?= =?UTF-8?q?=E8=87=B427vs5=E6=81=92False=E7=BA=AF=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E5=99=AA=E9=9F=B3=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 08-24实录: momentum一笔3800股被QMT拆19笔fill,对账27vs5恒count_match=False,总量却分毫不差。旧口径按原始行数比(注释自称'差异如实呈现待归因',实则部分成交属成交粒度非分歧)。修=按聚合桶live_volume==shadow_volume判;原始行数保留在live_count/shadow_count供归因。真量差仍判False(新增测试钉死)。trader 254绿。 --- sanguo_trader/shadow/reconcile_report.py | 7 +++++-- tests/trader/test_shadow_reconcile_report.py | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/sanguo_trader/shadow/reconcile_report.py b/sanguo_trader/shadow/reconcile_report.py index 1fd228b..8bea2c4 100644 --- a/sanguo_trader/shadow/reconcile_report.py +++ b/sanguo_trader/shadow/reconcile_report.py @@ -104,13 +104,16 @@ def _reconcile_trades(conn: sqlite3.Connection, live_id: int, shadow_id: int, "price_diff_bps": round(bps, 2) if bps is not None else None, }) avg_bps = sum(diffs) / len(diffs) if diffs else 0.0 - # 笔数=原始成交行数(非聚合桶):实盘部分成交会多行,差异如实呈现待归因 + # 笔数口径(2026-08-24 修订):count_match 按 票+方向 聚合桶总量比较——实盘 + # QMT 部分成交会把一笔 3800 股拆 19 行(08-24 momentum 27 vs 5 恒 False, + # 纯计数噪音);原始行数仍如实呈现(live_count/shadow_count)供归因。 + volume_match = all(r["live_volume"] == r["shadow_volume"] for r in rows) live_count = len(live_rows) shadow_count = len(shadow_rows) return { "live_count": live_count, "shadow_count": shadow_count, - "count_match": live_count == shadow_count, + "count_match": volume_match, "rows": rows, "avg_price_diff_bps": round(avg_bps, 2), "pass_price": avg_bps <= PRICE_DIFF_BPS_MAX, diff --git a/tests/trader/test_shadow_reconcile_report.py b/tests/trader/test_shadow_reconcile_report.py index cb62b61..43b31c3 100644 --- a/tests/trader/test_shadow_reconcile_report.py +++ b/tests/trader/test_shadow_reconcile_report.py @@ -148,7 +148,11 @@ class TestBuildReconcileReport: assert r["nav"]["mtd_deviation_pct"] == pytest.approx(0, abs=1e-9) assert r["passed"] is True - def test_count_mismatch_fails(self, db): + def test_partial_fill_split_same_total_volume_matches(self, db): + """QMT 部分成交拆多行 vs 影子一行,总量一致 → count_match=True。 + + 2026-08-24 修订:旧口径按原始行数比(27 vs 5 恒 False,000887 一笔 + 3800 股被拆 19 行)纯计数噪音;改按 票+方向 聚合总量比。""" _add_live_account(db) _add_shadow_account(db) _add_live_trade(db, 5, "510300.SH", "buy", 4.00, 1000, f"{D} 09:35:00") @@ -156,7 +160,18 @@ class TestBuildReconcileReport: _add_paper_trade(db, 39, "510300.XSHG", "long", 4.00, 1500, f"{D} 09:35:00", D) r = build_reconcile_report(db, 5, 39, D) - assert r["trades"]["count_match"] is False # 2 vs 1 + assert r["trades"]["count_match"] is True # 聚合总量 1500 == 1500 + assert r["trades"]["live_count"] == 2 and r["trades"]["shadow_count"] == 1 # 原始行数仍如实呈现 + + def test_aggregated_volume_mismatch_fails(self, db): + """聚合总量不一致(真缺一笔/量差)→ count_match=False。""" + _add_live_account(db) + _add_shadow_account(db) + _add_live_trade(db, 5, "510300.SH", "buy", 4.00, 1500, f"{D} 09:35:00") + _add_paper_trade(db, 39, "510300.XSHG", "long", 4.00, 1200, + f"{D} 09:35:00", D) + r = build_reconcile_report(db, 5, 39, D) + assert r["trades"]["count_match"] is False # 1500 != 1200 def test_price_diff_over_10bps_fails(self, db): _add_live_account(db)