@@ -235,7 +235,7 @@ def fake_client_factory(*args, **kwargs):
235235 assert store .writes == []
236236
237237
238- def test_run_strategy_cycle_persists_live_no_action_without_duplicate_terminal_stage (monkeypatch ):
238+ def test_run_strategy_cycle_persists_live_execution_blocked_without_terminal_stage (monkeypatch ):
239239 store = FakeStateStore ()
240240 settings = _runtime_settings_with_persistence (
241241 dry_run_only = False ,
@@ -260,7 +260,63 @@ def test_run_strategy_cycle_persists_live_no_action_without_duplicate_terminal_s
260260
261261 latest_payload = store .writes [- 2 ][1 ]
262262 assert result ["action_done" ] is False
263- assert latest_payload ["stage" ] == "NO_ACTION"
263+ assert result ["ok" ] is False
264+ assert result ["execution_blocked" ] is True
265+ assert latest_payload ["stage" ] == "EXECUTION_BLOCKED"
266+
267+
268+ def test_run_strategy_cycle_persists_live_partial_submission_as_non_terminal (monkeypatch ):
269+ store = FakeStateStore ()
270+ settings = _runtime_settings_with_persistence (
271+ dry_run_only = False ,
272+ live_trading_enabled = True ,
273+ live_order_ack = True ,
274+ persist_strategy_runs = True ,
275+ max_order_notional_usd = 1000.0 ,
276+ )
277+
278+ class PartialRuntime (FakeStrategyRuntime ):
279+ managed_symbols = ("AAA" , "BBB" )
280+
281+ def evaluate (self , ** inputs ):
282+ assert "portfolio_snapshot" in inputs
283+ return SimpleNamespace (
284+ decision = StrategyDecision (
285+ positions = (
286+ PositionTarget (symbol = "AAA" , target_value = 50.0 , role = "risk" ),
287+ PositionTarget (symbol = "BBB" , target_value = 150.0 , role = "risk" ),
288+ ),
289+ diagnostics = {"execution_annotations" : {"trade_threshold_value" : 1.0 }},
290+ ),
291+ metadata = {"strategy_profile" : self .profile },
292+ )
293+
294+ class PartialClient (FakeFirstradeClient ):
295+ def get_balances (self , _account ):
296+ return {"total_value" : "100.00" , "cash" : "60.00" , "buying_power" : "60.00" }
297+
298+ def get_quote (self , _account , symbol ):
299+ prices = {"AAA" : "10.00" , "BBB" : "100.00" }
300+ return {"symbol" : symbol , "last" : prices [symbol ], "bid" : "9.90" , "ask" : "10.10" }
301+
302+ monkeypatch .setattr (
303+ "application.rebalance_service.load_strategy_runtime" ,
304+ lambda * _args , ** _kwargs : PartialRuntime (),
305+ )
306+
307+ result = run_strategy_cycle (
308+ runtime_settings = settings ,
309+ credentials = FirstradeCredentials (username = "user" , password = "pass" ),
310+ client_factory = PartialClient ,
311+ state_store = store ,
312+ env_reader = lambda _name , default = None : default ,
313+ )
314+
315+ latest_payload = store .writes [- 2 ][1 ]
316+ assert result ["action_done" ] is True
317+ assert result ["ok" ] is False
318+ assert result ["execution_blocked" ] is True
319+ assert latest_payload ["stage" ] == "PARTIAL_SUBMITTED"
264320
265321
266322def test_render_cycle_summary_formats_skipped_orders_in_unified_chinese_template ():
@@ -400,3 +456,34 @@ def test_render_cycle_summary_formats_skipped_orders_in_unified_english_template
400456 assert "信号" not in message
401457 assert "profile:" not in message
402458 assert "targets:" not in message
459+
460+
461+ def test_render_cycle_summary_shows_execution_blocked_banner ():
462+ message = render_cycle_summary (
463+ {
464+ "account" : "****1234" ,
465+ "strategy_profile" : "mega_cap_leader_rotation_top50_balanced" ,
466+ "strategy_display_name" : "Mega Cap Leader Rotation Top50 Balanced" ,
467+ "dry_run_only" : False ,
468+ "execution_blocked" : True ,
469+ "execution_blocking_skips" : [
470+ {"symbol" : "NVDA" , "reason" : "insufficient_cash_for_whole_share" }
471+ ],
472+ "portfolio" : {
473+ "total_equity" : 50.0 ,
474+ "liquid_cash" : 50.0 ,
475+ "portfolio_rows" : (("NVDA" ,),),
476+ "market_values" : {"NVDA" : 0.0 },
477+ "quantities" : {"NVDA" : 0 },
478+ },
479+ "allocation" : {"targets" : {"NVDA" : 500.0 }},
480+ "execution" : {},
481+ "submitted_orders" : [],
482+ "skipped_orders" : [
483+ {"symbol" : "NVDA" , "reason" : "insufficient_cash_for_whole_share" }
484+ ],
485+ },
486+ lang = "zh" ,
487+ )
488+
489+ assert "⚠️ 执行阻塞: 现金不足以买入一整股:NVDA" in message
0 commit comments