From b86af798aa2c88d198247f531e68e428cc72f72f Mon Sep 17 00:00:00 2001 From: claude_dev Date: Wed, 19 Aug 2026 22:42:30 +0800 Subject: [PATCH] =?UTF-8?q?test(strategy):=20A3=E9=A2=84=E6=A3=80=E2=80=94?= =?UTF-8?q?=E2=80=94B2=E5=AE=9A=E5=AF=B8=E8=99=9A=E6=8B=9F=E5=8C=96=C3=97A?= =?UTF-8?q?1=E9=80=9A=E9=81=93=E4=B8=89=E6=98=8E=E6=B2=BB=E9=9B=86?= =?UTF-8?q?=E6=88=90=E6=B5=8B=E8=AF=955=E6=9D=A1(issue#29)=20[vps]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复刻 live_strategy._setup 实盘接线(A1 broker通道注入+B2 wrap_scheduler代理 context 同开=VPS重建后将运行的真实状态),真实 LiveInstanceLedger+真策略驱动: ①available_cash 过代理=账本现金(99.3万预算口径,非全账户396万) ②channel_test 等权买入 per=(账本现金+Σ自己市值)/N≈50万/只(hold_n=2), 精确断言 spec §A3 验收口径(非168万=995万/6量级) ③三明治下轮换只卖台账标的(别家600036不动) ④止损链路三层通:账本avg_cost+真账户现价透传(6.0<7.0*0.92触发,只卖自己) ⑤无账本 make_proxy_context 原样返回(模拟盘零改动铁律) 19/19绿;portfolio 367绿(3失败=Mac缺bullet_trade环境性,NAS CI过) --- .../portfolio/test_instance_view_isolation.py | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/tests/portfolio/test_instance_view_isolation.py b/tests/portfolio/test_instance_view_isolation.py index cc336f2..84d13ad 100644 --- a/tests/portfolio/test_instance_view_isolation.py +++ b/tests/portfolio/test_instance_view_isolation.py @@ -13,6 +13,8 @@ from __future__ import annotations from datetime import datetime from unittest.mock import MagicMock +import pytest + from tests.portfolio.conftest import FakeContext, FakePosition from sanguo_portfolio.strategies import ( ChannelTestConfig, @@ -256,3 +258,118 @@ class TestAllWeatherStopLossIsolation: if c.args[1] == 0 } assert sell_codes == {"518880.XSHG"} + + +# =================== A3 预检:B2 定寸虚拟化 × A1 通道 三明治(=VPS 实盘真实状态) =================== +# live_strategy._setup 的接线:A1 broker 通道注入 + B2 wrap_scheduler 代理 context +# 同时生效。本组用真实 LiveInstanceLedger + make_proxy_context 驱动真策略, +# 验证 spec §A3 验收口径(定寸=预算/N,卖出只卖自己)在部署前就成立。 +from sanguo_portfolio.live_instance_ledger import LiveInstanceLedger # noqa: E402 +from sanguo_portfolio.live_portfolio_proxy import make_proxy_context # noqa: E402 +from sanguo_portfolio.strategies.all_weather import _available_cash # noqa: E402 + + +def _own_ledger() -> LiveInstanceLedger: + """预算 100 万的实例账本,买过 518880×1000@7.0(现金≈99.3万)。""" + led = LiveInstanceLedger(initial_cash=1_000_000.0) + led.apply_trade(is_buy=True, symbol="518880.XSHG", price=7.0, volume=1000, + trade_id="T-BUY-1", trade_date="2024-10-08") + return led + + +def _full_account_ctx() -> FakeContext: + """共享 QMT 全账户(前后端 session 8-19 实锤口径):现金 396 万+ + 本实例 518880 + 别家 600036,总账户≈995 万量级。""" + return FakeContext( + current_dt=datetime(2024, 10, 8, 9, 35), + cash=3_961_768.0, + positions={ + "518880.XSHG": FakePosition("518880.XSHG", avg_cost=7.0, price=7.5), + "600036.XSHG": FakePosition("600036.XSHG", avg_cost=35.0, price=36.0), + }, + ) + + +class TestB2SizingSandwich: + """B2(4a9208d)+A1(5a91be2) 同开:定寸走账本现金,卖出走账本标的。""" + + def _sandwich(self, strategy, ledger): + """复刻 live_strategy._setup 两层接线(A1 通道 + B2 代理 context)。""" + strategy.broker.get_instance_positions = ledger.positions_view + return make_proxy_context(_full_account_ctx(), ledger) + + def test_available_cash_is_ledger_cash_not_account(self): + """定寸读数=账本现金(≈99.3万),不是全账户 396 万——168万/只污染根治点。""" + led = _own_ledger() + ctx = make_proxy_context(_full_account_ctx(), led) + cash = _available_cash(ctx) + assert cash == led.cash + assert 990_000 < cash < 1_000_000 # 预算口径 + assert cash < 3_000_000 # 绝不是全账户 + + def test_channel_test_buy_sized_by_budget(self): + """spec §A3 验收:channel_test 等权买入 per=(账本现金+Σ自己市值)/N, + ≈50万/只(hold_n=2),不再是 168万(=995万/6)量级。""" + led = _own_ledger() + broker = _RecordingBroker() + s = ChannelTestStrategy( + provider=None, broker=broker, + config=ChannelTestConfig(hold_n=2, period=1, probe_t1=False, + intraday_partial=False, intraday_swap=False), + ) + ctx = self._sandwich(s, led) + s.rotate(ctx) + buys = [(c, v) for c, v in broker.calls if v > 0] + assert len(buys) == 2 + own_mv = 7.5 * 1000 # 518880 现价×量(现价从真 portfolio 透传) + expect_per = (led.cash + own_mv) / 2 + for _code, v in buys: + assert v == pytest.approx(expect_per, rel=1e-6) + assert expect_per < 600_000 # 预算/2 量级,非全账户/2 + + def test_sandwich_sells_only_own(self): + """三明治下轮换只卖台账里的 518880,别家 600036 不动。""" + led = _own_ledger() + broker = _RecordingBroker() + s = ChannelTestStrategy( + provider=None, broker=broker, + config=ChannelTestConfig(hold_n=2, period=1, probe_t1=False, + intraday_partial=False, intraday_swap=False), + ) + ctx = self._sandwich(s, led) + s.rotate(ctx) + sells = [c for c, v in broker.calls if v == 0] + assert sells == ["518880.XSHG"] + + def test_stop_loss_price_flows_through_both_layers(self): + """止损链路三层通:账本 avg_cost(7.0) + 真账户现价透传(6.0) → 触发; + 别家 600036 同跌不动。""" + led = _own_ledger() + from sanguo_portfolio.strategies import AllWeatherStrategy + provider = MagicMock(name="provider") + provider.get_limit_status_batch.side_effect = lambda codes, date=None: { + c: {"is_limit_up": False, "is_limit_down": False, "is_paused": False} + for c in codes + } + broker = BrokerFacade() + broker.order_target_value = MagicMock(return_value=MagicMock(filled=100)) + s = AllWeatherStrategy(provider=provider, broker=broker) + real_ctx = FakeContext( + current_dt=datetime(2024, 10, 8, 14, 0), + positions={ + "518880.XSHG": FakePosition("518880.XSHG", avg_cost=7.0, price=6.0), + "600036.XSHG": FakePosition("600036.XSHG", avg_cost=40.0, price=30.0), + }, + ) + s.broker.get_instance_positions = led.positions_view + s.stop_loss(make_proxy_context(real_ctx, led)) + sell_codes = { + c.args[0] for c in broker.order_target_value.call_args_list + if c.args[1] == 0 + } + assert sell_codes == {"518880.XSHG"} # 6.0 < 7.0*0.92=6.44 触发,只卖自己 + + def test_no_ledger_proxy_is_identity(self): + """无账本(回测/影子/模拟盘)→ make_proxy_context 原样返回,零改动铁律。""" + ctx = _full_account_ctx() + assert make_proxy_context(ctx, None) is ctx