From 41495b0c0831dc14da3c19b87adb3a719dce1189 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Tue, 25 Aug 2026 21:30:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(live):=20=E5=8D=96=E5=87=BA=E9=97=B8?= =?UTF-8?q?=E9=97=A8=E6=97=A0=E4=BB=B7=E9=80=8F=E4=BC=A0=E6=94=B9fail-clos?= =?UTF-8?q?ed=E6=8B=92=E5=8D=95=E2=80=94=E2=80=94issue#38=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=B8=80A=E6=94=B6=E5=B0=BE=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 08-24 300059 跨实例互卖定罪(issue#38):闸门虽有台账硬顶,但两处「无有效价格」 路径透传引擎原函数=数据源退化回全账户视图(引擎按账户算target,账户有量会替 别家卖到目标值/按账户卖出)——正是「账户可卖视图放行18归属4400股」的变体洞。 改动(_instance_order_wrappers): - otv value>0 无价(市价拿不到且账本无此票无成本可回退)→ 拒单fail-closed, 不再透传bt_otv引擎target语义(宁可少买不可互卖,策略session定夺=拒单语义) - 负order_value无价 → 拒单,不再透传bt_ov按全账户卖出 - 事故主洞(同窗双卖吃到60s归因间隙)已由191270c即时归因关闭:新增钉死测试 「双卖窗口阻断」——第一笔4400即时归因(on_order_done钩子)→第二笔闸门读 账本可卖0拒单;闸门每调用现读positions_view的B链路行为从此有回归锚 测试:test_live_instance_orders +3(双卖窗口阻断/otv无价拒单/负ov无价拒单), 17/17绿;portfolio+data_platform+api全量819绿。B2 closeable口径已走账本 (InstancePosition直读账本info,无改动);账本bootstrap/重归=15:10恒等式 reconcile既有runbook。issue#38验收(恒等式连续3日PASS)按issue标准观察。 --- sanguo_portfolio/live_strategy.py | 19 ++++++--- tests/portfolio/test_live_instance_orders.py | 41 ++++++++++++++++++++ 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/sanguo_portfolio/live_strategy.py b/sanguo_portfolio/live_strategy.py index 9c28c66..2a064ed 100644 --- a/sanguo_portfolio/live_strategy.py +++ b/sanguo_portfolio/live_strategy.py @@ -158,8 +158,14 @@ def _instance_order_wrappers(ledger, bt_otv, bt_ov): return _done(bt_order(security, -sell, *args, **kwargs)) price = _price(security, own) if price <= 0: - logger.warning("[instance-order] %s 无有效价格 → 透传引擎 target 语义", security) - return _done(bt_otv(security, value, *args, **kwargs)) + # issue#38 fail-closed:绝不透传引擎 target 语义——引擎按全账户 + # 视图算 target,账户有量会替别家卖到目标值(300059 账户级放行的 + # 变体)。无价=市价拿不到且账本无此票无成本可回退 → 拒单, + # 宁可少买不可互卖。 + logger.warning( + "[instance-order] %s 无有效价格(账本无成本可回退) → 拒单 " + "fail-closed,不透传引擎账户级 target 语义", security) + return None target = int(value / price) // lot * lot diff = target - int(own["amount"]) if diff >= lot: @@ -181,9 +187,12 @@ def _instance_order_wrappers(ledger, bt_otv, bt_ov): own = _own(security) price = _price(security, own) if price <= 0: - logger.warning("[instance-order] %s 无有效价格 → 负 order_value 透传引擎", - security) - return _done(bt_ov(security, value, *args, **kwargs)) + # issue#38 fail-closed:负 order_value 无价折算不出股数,透传引擎 + # =按全账户卖出,可卖别家归属——拒单。 + logger.warning( + "[instance-order] %s 无有效价格(账本无成本可回退) → 负 order_value " + "拒单 fail-closed,不透传引擎账户级卖出", security) + return None want = int(abs(value) / price + 0.999) # 向上取整再由可卖量硬顶 sell = _sell_shares(security, want, own, "按价值卖出") if sell <= 0: diff --git a/tests/portfolio/test_live_instance_orders.py b/tests/portfolio/test_live_instance_orders.py index 68fb2bb..89a3199 100644 --- a/tests/portfolio/test_live_instance_orders.py +++ b/tests/portfolio/test_live_instance_orders.py @@ -200,3 +200,44 @@ def test_order_done_hook_silent_when_no_order(fakes): ret = otv("513030.XSHG", 0) assert ret is None and orders == [] assert fired == [] + + +# ------------------ 修复一A(issue#38):闸门认账本,账户级语义绝不放行 ------------------ +def test_double_sell_within_attribution_window_blocked(fakes): + """08-24 300059 事故形态:同窗两次清仓,第一笔 4400 已即时归因(191270c)→ + 第二笔闸门读到账本可卖 0 → 拒单,绝不碰 18 归属的另 4400(B 修法链路闸门侧钉死)。""" + orders, _, _ = fakes + led = _ledger_with("300059.XSHE", 4400, price=2.0, trade_date="2026-08-20") + # 模拟 191270c 即时归因:下单返回 → 同步把本笔卖出落账本 + led.on_order_done = lambda: led.apply_trade( + False, "300059.XSHE", 2.0, 4400, "t-sync", "2026-08-25") + otv, _ov = _instance_order_wrappers(led, None, None) + first = otv("300059.XSHE", 0) + second = otv("300059.XSHE", 0) + assert orders == [("300059.XSHE", -4400)] # 只有一笔,第二笔不再下 + assert first is not None and second is None + + +def test_otv_no_price_no_ledger_holding_fail_closed(fakes, monkeypatch): + """issue#38:无价且账本无此票(无成本可回退)→ 拒单 fail-closed。旧版透传 + 引擎 target 语义按全账户视图算 target,账户有量会替别家卖到目标值—— + 闸门数据源退化到账户级=300059 账户级放行的变体,必须堵死。""" + orders, _, otv_calls = fakes + import bullet_trade.data.api as bt_data + monkeypatch.setattr(bt_data, "get_current_data", lambda: _FakeCurrentData(0.0)) + led = _ledger_with("ANY.XSHG", 0) # 账本无 NEW.XSHG + otv, _ov = _instance_order_wrappers(led, None, None) + out = otv("NEW.XSHG", 60000) + assert out is None and orders == [] and otv_calls == [] + + +def test_negative_order_value_no_price_fail_closed(fakes, monkeypatch): + """issue#38:负 order_value 无价(账本无成本可折算)→ 拒单,不透传引擎 + 按全账户卖出。""" + orders, _, ov_calls = fakes + import bullet_trade.data.api as bt_data + monkeypatch.setattr(bt_data, "get_current_data", lambda: _FakeCurrentData(0.0)) + led = _ledger_with("ANY.XSHG", 0) + _otv, ov = _instance_order_wrappers(led, None, None) + out = ov("OTHER.XSHG", -30000) + assert out is None and orders == [] and ov_calls == []