@@ -211,6 +211,101 @@ def test_safe_haven_target_below_cash_substitute_threshold_stays_cash(self):
211211 self .assertEqual (submitted_orders , [])
212212 self .assertEqual (result .allocation ["targets" ]["BOXX" ], 0.0 )
213213
214+ def test_small_account_whole_share_layer_sells_unbuyable_soxx_sleeve (self ):
215+ submitted_orders = []
216+ prices = {"SOXL" : 191.15 , "SOXX" : 536.88 , "BOXX" : 100.0 }
217+ plan = _build_plan (
218+ strategy_symbols = ("SOXL" , "SOXX" , "BOXX" ),
219+ risk_symbols = ("SOXL" , "SOXX" ),
220+ safe_haven_symbols = ("BOXX" ,),
221+ targets = {"SOXL" : 541.58 , "SOXX" : 154.74 , "BOXX" : 77.37 },
222+ market_values = {"SOXL" : 0.0 , "SOXX" : 536.88 , "BOXX" : 0.0 },
223+ sellable_quantities = {"SOXL" : 0 , "SOXX" : 1 , "BOXX" : 0 },
224+ quantities = {"SOXL" : 0 , "SOXX" : 1 , "BOXX" : 0 },
225+ current_min_trade = 7.74 ,
226+ trade_threshold_value = 7.74 ,
227+ investable_cash = 213.60 ,
228+ market_status = "Risk on" ,
229+ deploy_ratio_text = "90.0%" ,
230+ income_ratio_text = "0.0%" ,
231+ income_locked_ratio_text = "0.0%" ,
232+ signal_message = "SOXX above gate" ,
233+ available_cash = 236.81 ,
234+ total_strategy_equity = 773.69 ,
235+ portfolio_rows = (("SOXL" , "SOXX" ), ("BOXX" ,)),
236+ )
237+ refreshed_plan = _build_plan (
238+ strategy_symbols = ("SOXL" , "SOXX" , "BOXX" ),
239+ risk_symbols = ("SOXL" , "SOXX" ),
240+ safe_haven_symbols = ("BOXX" ,),
241+ targets = {"SOXL" : 541.58 , "SOXX" : 154.74 , "BOXX" : 77.37 },
242+ market_values = {"SOXL" : 0.0 , "SOXX" : 0.0 , "BOXX" : 0.0 },
243+ sellable_quantities = {"SOXL" : 0 , "SOXX" : 0 , "BOXX" : 0 },
244+ quantities = {"SOXL" : 0 , "SOXX" : 0 , "BOXX" : 0 },
245+ current_min_trade = 7.74 ,
246+ trade_threshold_value = 7.74 ,
247+ investable_cash = 750.48 ,
248+ market_status = "Risk on" ,
249+ deploy_ratio_text = "90.0%" ,
250+ income_ratio_text = "0.0%" ,
251+ income_locked_ratio_text = "0.0%" ,
252+ signal_message = "SOXX above gate" ,
253+ available_cash = 773.69 ,
254+ total_strategy_equity = 773.69 ,
255+ portfolio_rows = (("SOXL" , "SOXX" ), ("BOXX" ,)),
256+ )
257+
258+ result = execute_rebalance_cycle (
259+ trade_context = object (),
260+ plan = plan ,
261+ portfolio = plan ["portfolio" ],
262+ execution = plan ["execution" ],
263+ allocation = plan ["allocation" ],
264+ fetch_replanned_state = lambda : (
265+ refreshed_plan ,
266+ refreshed_plan ["portfolio" ],
267+ refreshed_plan ["execution" ],
268+ refreshed_plan ["allocation" ],
269+ ),
270+ market_data_port = CallableMarketDataPort (
271+ quote_loader = lambda symbol : QuoteSnapshot (
272+ symbol = symbol ,
273+ as_of = "2026-05-22" ,
274+ last_price = prices [str (symbol ).replace (".US" , "" )],
275+ )
276+ ),
277+ estimate_max_purchase_quantity = lambda * _args , ** _kwargs : 10 ,
278+ execution_port = CallableExecutionPort (
279+ lambda order_intent : (
280+ submitted_orders .append (order_intent ),
281+ ExecutionReport (
282+ symbol = order_intent .symbol ,
283+ side = order_intent .side ,
284+ quantity = order_intent .quantity ,
285+ status = "accepted" ,
286+ broker_order_id = f"order-{ len (submitted_orders )} " ,
287+ ),
288+ )[- 1 ]
289+ ),
290+ notify_issue = lambda _title , _detail : None ,
291+ translator = build_translator ("zh" ),
292+ with_prefix = lambda message : message ,
293+ limit_sell_discount = 1.0 ,
294+ limit_buy_premium = 1.0 ,
295+ safe_haven_cash_substitute_threshold_usd = 1000.0 ,
296+ )
297+
298+ self .assertTrue (result .action_done )
299+ self .assertEqual (result .allocation ["targets" ]["SOXX" ], 0.0 )
300+ self .assertEqual (
301+ result .allocation ["small_account_whole_share_substituted_symbols" ],
302+ ("SOXX" ,),
303+ )
304+ self .assertEqual ([(order .symbol , order .side , order .quantity ) for order in submitted_orders ], [
305+ ("SOXX.US" , "sell" , 1 ),
306+ ("SOXL.US" , "buy" , 2 ),
307+ ])
308+
214309 def test_run_strategy_prefers_portfolio_port_runtime_path (self ):
215310 sent_messages = []
216311 observed = {}
@@ -629,13 +724,12 @@ def test_strategy_target_rebuys_cash_sweep_symbol_after_buy_skip(self):
629724
630725 self .assertEqual (len (sent_messages ), 1 )
631726 self .assertIn ("🔔 【调仓指令】" , sent_messages [0 ])
632- self .assertIn ("SOXX.US 目标差额 $163.14" , sent_messages [0 ])
633- self .assertIn ("SOXX.US 目标差额 $163.14 未超过 1 股价格 $504.60" , sent_messages [0 ])
727+ self .assertNotIn ("SOXX.US 目标差额 $163.14" , sent_messages [0 ])
634728 self .assertNotIn ("可投资现金 $1191.03 不足买入 1 股" , sent_messages [0 ])
635729 self .assertNotIn ("市价卖出] BOXX" , sent_messages [0 ])
636730 self .assertNotIn ("市价买入] SOXX" , sent_messages [0 ])
637731 self .assertIn ("市价买入] BOXX: 10股" , sent_messages [0 ])
638- self .assertIn ("买入说明 " , sent_messages [0 ])
732+ self .assertIn ("BOXX.US 目标差额 $524.92 " , sent_messages [0 ])
639733 self .assertNotIn ("限价买入] SOXX" , sent_messages [0 ])
640734
641735 def test_target_gap_below_one_share_does_not_report_cash_shortage (self ):
0 commit comments