@@ -1071,6 +1071,64 @@ def test_cash_sweep_symbol_can_fund_buy_when_investable_cash_is_positive_but_sho
10711071 self .assertIn ("限价买入" , sent_messages [0 ])
10721072 self .assertIn ("SOXL" , sent_messages [0 ])
10731073
1074+ def test_cash_sweep_symbol_sells_even_when_underweight_is_below_one_share (self ):
1075+ initial_plan = _build_plan (
1076+ strategy_symbols = ("SOXL" , "SOXX" , "BOXX" ),
1077+ risk_symbols = ("SOXL" , "SOXX" ),
1078+ safe_haven_symbols = ("BOXX" ,),
1079+ targets = {"SOXL" : 167.79 , "SOXX" : 0.0 , "BOXX" : 1000.0 },
1080+ market_values = {"SOXL" : 0.0 , "SOXX" : 0.0 , "BOXX" : 1000.0 },
1081+ sellable_quantities = {"SOXL" : 0 , "SOXX" : 0 , "BOXX" : 1 },
1082+ quantities = {"SOXL" : 0 , "SOXX" : 0 , "BOXX" : 1 },
1083+ current_min_trade = 100.0 ,
1084+ trade_threshold_value = 100.0 ,
1085+ investable_cash = 14.46 ,
1086+ market_status = "🧯 过热降档(SOXX)" ,
1087+ deploy_ratio_text = "15.0%" ,
1088+ income_ratio_text = "0.0%" ,
1089+ income_locked_ratio_text = "0.0%" ,
1090+ signal_message = "SOXX 仍在 140 日门槛线上方,但触发过热降档,目标仓位 SOXL 15.0%" ,
1091+ available_cash = 14.46 ,
1092+ total_strategy_equity = 1000.0 ,
1093+ portfolio_rows = (("SOXL" , "SOXX" ), ("BOXX" ,)),
1094+ )
1095+ refreshed_plan = _build_plan (
1096+ strategy_symbols = ("SOXL" , "SOXX" , "BOXX" ),
1097+ risk_symbols = ("SOXL" , "SOXX" ),
1098+ safe_haven_symbols = ("BOXX" ,),
1099+ targets = {"SOXL" : 167.79 , "SOXX" : 0.0 , "BOXX" : 1000.0 },
1100+ market_values = {"SOXL" : 0.0 , "SOXX" : 0.0 , "BOXX" : 900.0 },
1101+ sellable_quantities = {"SOXL" : 0 , "SOXX" : 0 , "BOXX" : 0 },
1102+ quantities = {"SOXL" : 0 , "SOXX" : 0 , "BOXX" : 0 },
1103+ current_min_trade = 100.0 ,
1104+ trade_threshold_value = 100.0 ,
1105+ investable_cash = 114.46 ,
1106+ market_status = "🧯 过热降档(SOXX)" ,
1107+ deploy_ratio_text = "15.0%" ,
1108+ income_ratio_text = "0.0%" ,
1109+ income_locked_ratio_text = "0.0%" ,
1110+ signal_message = "SOXX 仍在 140 日门槛线上方,但触发过热降档,目标仓位 SOXL 15.0%" ,
1111+ available_cash = 114.46 ,
1112+ total_strategy_equity = 1000.0 ,
1113+ portfolio_rows = (("SOXL" , "SOXX" ), ("BOXX" ,)),
1114+ )
1115+ before_sell_snapshot = _build_snapshot (initial_plan , phase = "before_cash_sweep_small_gap" )
1116+ after_sell_snapshot = _build_snapshot (refreshed_plan , phase = "after_cash_sweep_small_gap" )
1117+ sent_messages , observed_snapshots , observed_plan_inputs = self ._run_strategy (
1118+ initial_plan ,
1119+ refreshed_plan = refreshed_plan ,
1120+ portfolio_snapshots = [before_sell_snapshot , after_sell_snapshot ],
1121+ prices = {"SOXL.US" : 167.79 , "SOXX.US" : 200.0 , "BOXX.US" : 100.0 },
1122+ estimate_max_purchase_quantity_value = 10 ,
1123+ )
1124+
1125+ self .assertEqual (observed_snapshots , [before_sell_snapshot , after_sell_snapshot ])
1126+ self .assertEqual (len (observed_plan_inputs ), 2 )
1127+ self .assertEqual (len (sent_messages ), 1 )
1128+ self .assertIn ("BOXX" , sent_messages [0 ])
1129+ self .assertIn ("市价卖出" , sent_messages [0 ])
1130+ self .assertNotIn ("买入跳过" , sent_messages [0 ])
1131+
10741132 def test_dry_run_cash_sweep_can_simulate_buy_after_sell_settlement (self ):
10751133 initial_plan = _build_plan (
10761134 strategy_symbols = ("SOXL" , "SOXX" , "BOXX" ),
0 commit comments