From 6adb40e7a055a0faa3924fa96ff1df67e6bd3fde Mon Sep 17 00:00:00 2001 From: claude_dev Date: Fri, 4 Sep 2026 18:29:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(live):=20=E6=81=92=E7=AD=89=E5=BC=8F?= =?UTF-8?q?=E9=80=90=E7=A5=A8=E5=AF=B9=E8=B4=A6=E8=A1=A5=E8=B4=9F=E5=90=91?= =?UTF-8?q?=E5=B9=BB=E5=BD=B1=E2=80=94=E2=80=9409-04=20=E6=94=B6=E7=9B=98?= =?UTF-8?q?=E8=81=94=E6=A3=80=E5=AE=9E=E8=AF=81:002633=E4=B9=B0=E7=BC=BA10?= =?UTF-8?q?0(=E6=AD=A3=E5=90=91=E5=AD=A4=E5=84=BF,=E5=B7=B2=E5=88=97)+0028?= =?UTF-8?q?36=E5=8D=96=E7=BC=BA300(=E8=B4=9F=E5=90=91=E5=B9=BB=E5=BD=B1,?= =?UTF-8?q?=E6=BC=8F=E5=88=97),=E6=97=A7=E4=BB=A3=E7=A0=81unattr=5Fpositio?= =?UTF-8?q?ns=E5=8F=AA=E9=81=8D=E5=8E=86snap=5Fpos.items(),=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E6=B2=A1=E6=9C=89=E7=9A=84=E7=A5=A8=E6=B0=B8=E4=B8=8D?= =?UTF-8?q?=E8=A2=AB=E9=81=8D=E5=8E=86=E5=88=B0,=E5=8D=95=E7=9C=8B?= =?UTF-8?q?=E7=A5=A8=E5=8D=95=E4=BC=9A=E6=8A=8A=E4=BF=AE=E8=B4=A6=E5=81=9A?= =?UTF-8?q?=E6=88=90=E5=8D=8A=E6=8B=89=E5=AD=90(=E5=8F=AA=E8=A1=A5?= =?UTF-8?q?=E5=AD=A4=E5=84=BF=E4=B8=8D=E8=A1=A5=E5=B9=BB=E5=BD=B1);?= =?UTF-8?q?=E4=BF=AE=3D=E5=8F=8C=E7=AB=AF=E9=94=AE=E5=B9=B6=E9=9B=86sorted?= =?UTF-8?q?(set(snap=5Fpos)|set(inst=5Fpos)),=E6=AD=A3=E5=90=91=E5=AD=A4?= =?UTF-8?q?=E5=84=BF/=E8=B4=9F=E5=90=91=E5=B9=BB=E5=BD=B1=E9=83=BD?= =?UTF-8?q?=E5=85=A5=E5=88=97(002836=E5=BD=93=E6=97=A5=E5=9C=A8=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E9=87=8C=E5=8F=AA=E6=9C=89MV=E5=B7=AE=E5=AF=B9?= =?UTF-8?q?=E4=B8=8D=E4=B8=8A=E6=95=B0=E6=89=8D=E7=8E=B0=E5=BD=A2);?= =?UTF-8?q?=E6=AE=8B=E5=B7=AE=E6=8B=86=E8=85=BF=E5=90=8C=E6=AD=A5=E5=AE=9A?= =?UTF-8?q?=E6=80=A7:-1535=3D100=C3=97002633=E6=94=B6=E7=9B=9815.35-300?= =?UTF-8?q?=C3=97002836=E6=94=B6=E7=9B=9810.23,100%=E6=9D=A5=E8=87=AA?= =?UTF-8?q?=E4=B8=A4=E7=AC=94=E5=BC=95=E6=93=8E=E6=9C=AA=E6=8A=A5=E6=88=90?= =?UTF-8?q?=E4=BA=A4,=E4=BF=AE=E8=B4=A6=E5=90=8E=E5=BA=94=E5=BD=92?= =?UTF-8?q?=E9=9B=B6;TDD=201=E6=96=B0=E6=B5=8B=E5=85=88=E7=BA=A2=E5=90=8E?= =?UTF-8?q?=E7=BB=BF+21=E5=85=A8=E8=BF=87=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_trader/shadow/reconcile_report.py | 10 ++++++---- tests/trader/test_shadow_reconcile_report.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/sanguo_trader/shadow/reconcile_report.py b/sanguo_trader/shadow/reconcile_report.py index 8bea2c4..a5840d7 100644 --- a/sanguo_trader/shadow/reconcile_report.py +++ b/sanguo_trader/shadow/reconcile_report.py @@ -264,12 +264,14 @@ def build_identity_report(db: str, date: Optional[str] = None) -> Dict[str, Any] "WHERE account_id=?", (inst["id"],)): inst_pos[_norm_symbol(r[0])] = inst_pos.get( _norm_symbol(r[0]), 0.0) + float(r[1] or 0) + # 双端键并集:快照独有=正向孤儿(遗留/手动仓),实例独有=负向幻影 + # (09-04 002836 实证:只遍历快照会漏幻影,导致修账只补一半) unattr_positions = [ - {"symbol": s, "snapshot_volume": v, + {"symbol": s, "snapshot_volume": snap_pos.get(s, 0.0), "instance_volume": inst_pos.get(s, 0.0), - "diff": v - inst_pos.get(s, 0.0)} - for s, v in sorted(snap_pos.items()) - if abs(v - inst_pos.get(s, 0.0)) > 0.5 + "diff": snap_pos.get(s, 0.0) - inst_pos.get(s, 0.0)} + for s in sorted(set(snap_pos) | set(inst_pos)) + if abs(snap_pos.get(s, 0.0) - inst_pos.get(s, 0.0)) > 0.5 ] if snap is None: status = "snapshot_missing" diff --git a/tests/trader/test_shadow_reconcile_report.py b/tests/trader/test_shadow_reconcile_report.py index 43b31c3..ebbd762 100644 --- a/tests/trader/test_shadow_reconcile_report.py +++ b/tests/trader/test_shadow_reconcile_report.py @@ -380,6 +380,23 @@ def test_identity_unattributed_position_listed(db): assert r["identity_passed"] is False +def test_identity_phantom_position_listed(db): + """实例账本有快照没有的票(幻影仓,如卖出迟到未记账)→ 负差票单列(09-04 002836 实证)。""" + _add_live_account(db, aid=5) + _seed_snapshot(db, mv=1_000_000, positions=[ + {"symbol": "600036.SH", "volume": 1000, "can_use": 1000, + "avg_price": 38, "mv": 38000}]) + _seed_inst_mv(db, 5, mv=997_000, + positions={"600036.XSHG": 1000, "002836.XSHE": 300}) # 幻影 300 股 + r = build_identity_report(db) + row = r["rows"][0] + unattr = row["unattributed_positions"] + assert [p["symbol"] for p in unattr] == ["002836"] + assert unattr[0]["snapshot_volume"] == 0 + assert unattr[0]["instance_volume"] == 300 + assert unattr[0]["diff"] == -300 + + def test_identity_instance_over_snapshot_negative(db): """Σ实例>快照(旧全账户行叠加期)→ 未归因为负,如实呈现 FAIL。""" _add_live_account(db, aid=5)