@@ -134,6 +134,39 @@ class ExecutionCycleResult:
134134DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
135135SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0
136136SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS = frozenset ({"TQQQ" , "SOXL" })
137+ SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
138+ "SOXX" : 0.90 ,
139+ }
140+ SMALL_ACCOUNT_WHOLE_SHARE_BOOTSTRAP_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
141+ "TQQQ" : 0.90 ,
142+ "SOXL" : 0.90 ,
143+ "SOXX" : 0.90 ,
144+ }
145+
146+
147+ def _limit_buy_premium_for_symbol (symbol , default_premium , premium_by_symbol = None ) -> float :
148+ normalized_symbol = str (symbol or "" ).strip ().upper ()
149+ try :
150+ fallback = float (default_premium )
151+ except (TypeError , ValueError ):
152+ fallback = 1.005
153+ if not isinstance (premium_by_symbol , dict ):
154+ return fallback
155+ raw_value = premium_by_symbol .get (normalized_symbol )
156+ if raw_value is None :
157+ return fallback
158+ try :
159+ premium = float (raw_value )
160+ except (TypeError , ValueError ):
161+ return fallback
162+ return premium if premium > 0.0 else fallback
163+
164+
165+ def _limit_buy_price (symbol , price , default_premium , premium_by_symbol = None ) -> float :
166+ return round (
167+ float (price ) * _limit_buy_premium_for_symbol (symbol , default_premium , premium_by_symbol ),
168+ 2 ,
169+ )
137170
138171
139172def _floor_quantity (quantity : float ) -> int :
@@ -210,6 +243,35 @@ def substitute_small_safe_haven_targets_with_cash(
210243 return adjusted_plan
211244
212245
246+ def _should_retain_existing_whole_share (symbol , * , target_value , price ) -> bool :
247+ normalized_symbol = str (symbol or "" ).strip ().upper ()
248+ if normalized_symbol in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS :
249+ return True
250+
251+ min_target_share_ratio = (
252+ SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL .get (normalized_symbol )
253+ )
254+ if min_target_share_ratio is None :
255+ return False
256+ quote_price = max (0.0 , float (price or 0.0 ))
257+ if quote_price <= 0.0 :
258+ return False
259+ return max (0.0 , float (target_value or 0.0 )) >= quote_price * float (min_target_share_ratio )
260+
261+
262+ def _should_bootstrap_whole_share_buy (symbol , * , target_value , limit_price ) -> bool :
263+ normalized_symbol = str (symbol or "" ).strip ().upper ()
264+ min_target_share_ratio = (
265+ SMALL_ACCOUNT_WHOLE_SHARE_BOOTSTRAP_MIN_TARGET_SHARE_RATIO_BY_SYMBOL .get (normalized_symbol )
266+ )
267+ if min_target_share_ratio is None :
268+ return False
269+ effective_limit_price = max (0.0 , float (limit_price or 0.0 ))
270+ if effective_limit_price <= 0.0 :
271+ return False
272+ return max (0.0 , float (target_value or 0.0 )) >= effective_limit_price * float (min_target_share_ratio )
273+
274+
213275def _quote_price (market_data_port : MarketDataPort , symbol : str ) -> float | None :
214276 try :
215277 price = float (market_data_port .get_quote (symbol ).last_price )
@@ -222,6 +284,8 @@ def _apply_small_account_whole_share_compatibility(
222284 plan : dict [str , Any ],
223285 * ,
224286 market_data_port : MarketDataPort ,
287+ limit_buy_premium : float = 1.005 ,
288+ limit_buy_premium_by_symbol : dict [str , float ] | None = None ,
225289) -> dict [str , Any ]:
226290 adjusted_plan = dict (plan or {})
227291 allocation = dict (adjusted_plan .get ("allocation" ) or {})
@@ -248,6 +312,7 @@ def _apply_small_account_whole_share_compatibility(
248312 if price is not None :
249313 prices [str (symbol ).strip ().upper ()] = price
250314 retained_symbols = []
315+ bootstrap_symbols = []
251316 quantities = {
252317 str (symbol or "" ).strip ().upper (): float (quantity or 0.0 )
253318 for symbol , quantity in dict (portfolio .get ("quantities" ) or {}).items ()
@@ -257,13 +322,33 @@ def _apply_small_account_whole_share_compatibility(
257322 for symbol , value in targets .items ()
258323 }
259324 for symbol in candidate_symbols :
260- if symbol not in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS :
261- continue
262325 target_value = max (0.0 , float (compatibility_targets .get (symbol , 0.0 ) or 0.0 ))
263326 price = max (0.0 , float (prices .get (symbol , 0.0 ) or 0.0 ))
327+ limit_price = (
328+ _limit_buy_price (symbol , price , limit_buy_premium , limit_buy_premium_by_symbol )
329+ if price > 0.0
330+ else 0.0
331+ )
332+ if not _should_retain_existing_whole_share (symbol , target_value = target_value , price = price ):
333+ if (
334+ quantities .get (symbol , 0.0 ) <= 0.0
335+ and 0.0 < target_value < limit_price
336+ and _should_bootstrap_whole_share_buy (symbol , target_value = target_value , limit_price = limit_price )
337+ ):
338+ compatibility_targets [symbol ] = limit_price
339+ bootstrap_symbols .append (symbol )
340+ continue
264341 if price > 0.0 and 0.0 < target_value < price and quantities .get (symbol , 0.0 ) >= 1.0 :
265342 compatibility_targets [symbol ] = price
266343 retained_symbols .append (symbol )
344+ continue
345+ if (
346+ quantities .get (symbol , 0.0 ) <= 0.0
347+ and 0.0 < target_value < limit_price
348+ and _should_bootstrap_whole_share_buy (symbol , target_value = target_value , limit_price = limit_price )
349+ ):
350+ compatibility_targets [symbol ] = limit_price
351+ bootstrap_symbols .append (symbol )
267352 safe_haven_symbols = _safe_haven_cash_symbols (portfolio = portfolio , allocation = allocation )
268353 compatibility = apply_small_account_cash_compatibility (
269354 compatibility_targets ,
@@ -285,6 +370,10 @@ def _apply_small_account_whole_share_compatibility(
285370 allocation ["small_account_existing_whole_share_retained_symbols" ] = tuple (
286371 dict .fromkeys (retained_symbols )
287372 )
373+ if bootstrap_symbols :
374+ allocation ["small_account_whole_share_bootstrap_symbols" ] = tuple (
375+ dict .fromkeys (bootstrap_symbols )
376+ )
288377 if compatibility .cash_substitution_notes :
289378 allocation ["small_account_whole_share_cash_notes" ] = tuple (compatibility .cash_substitution_notes )
290379 adjusted_plan ["allocation" ] = allocation
@@ -334,6 +423,7 @@ def execute_value_target_plan(
334423 dry_run_only : bool ,
335424 limit_sell_discount : float = 0.995 ,
336425 limit_buy_premium : float = 1.005 ,
426+ limit_buy_premium_by_symbol : dict [str , float ] | None = None ,
337427 max_order_notional_usd : float | None = None ,
338428 safe_haven_cash_substitute_threshold_usd : float = DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD ,
339429) -> ExecutionCycleResult :
@@ -345,6 +435,8 @@ def execute_value_target_plan(
345435 plan = _apply_small_account_whole_share_compatibility (
346436 plan ,
347437 market_data_port = market_data_port ,
438+ limit_buy_premium = limit_buy_premium ,
439+ limit_buy_premium_by_symbol = limit_buy_premium_by_symbol ,
348440 )
349441 allocation = dict (plan .get ("allocation" ) or {})
350442 portfolio = dict (plan .get ("portfolio" ) or {})
@@ -437,16 +529,17 @@ def execute_value_target_plan(
437529 buy_budget = min (float (delta_value ), investable_cash )
438530 if order_notional_cap is not None :
439531 buy_budget = min (buy_budget , order_notional_cap )
440- quantity = _floor_quantity (buy_budget / price )
532+ limit_price = _limit_buy_price (symbol , price , limit_buy_premium , limit_buy_premium_by_symbol )
533+ quantity = _floor_quantity (buy_budget / limit_price ) if limit_price > 0 else 0
441534 if quantity <= 0 :
442- if order_notional_cap is None and investable_cash < price :
535+ if order_notional_cap is None and investable_cash < limit_price :
443536 skipped .append (
444537 {
445538 "symbol" : symbol ,
446539 "reason" : "insufficient_cash_for_whole_share" ,
447- "price" : round (price , 2 ),
540+ "price" : round (limit_price , 2 ),
448541 "investable_cash" : round (investable_cash , 2 ),
449- "required_cash_for_one_share" : round (price , 2 ),
542+ "required_cash_for_one_share" : round (limit_price , 2 ),
450543 }
451544 )
452545 else :
@@ -468,11 +561,11 @@ def execute_value_target_plan(
468561 symbol = symbol ,
469562 side = "buy" ,
470563 quantity = quantity ,
471- limit_price = price * float ( limit_buy_premium ) ,
564+ limit_price = limit_price ,
472565 max_notional_usd = max_order_notional_usd ,
473566 )
474567 )
475- investable_cash = max (0.0 , investable_cash - (quantity * price ))
568+ investable_cash = max (0.0 , investable_cash - (quantity * limit_price ))
476569
477570 return ExecutionCycleResult (
478571 submitted_orders = tuple (submitted ),
0 commit comments