@@ -1128,6 +1128,7 @@ def _limit_buy_price(symbol, price, default_premium, premium_by_symbol=None) ->
11281128DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
11291129SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0
11301130SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS = frozenset ({"TQQQ" , "SOXL" })
1131+ _SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT = 0.85
11311132SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
11321133 "SOXX" : 0.90 ,
11331134}
@@ -1175,20 +1176,40 @@ def _apply_safe_haven_cash_substitution_to_weights(
11751176 return adjusted , tuple (dict .fromkeys (substituted ))
11761177
11771178
1178- def _should_retain_existing_whole_share (symbol , * , target_value , price ) -> bool :
1179+ def _should_retain_existing_whole_share (symbol , * , target_value , price , quantity = 0.0 ) -> bool :
1180+ """Decide whether an existing whole-share position should be retained.
1181+
1182+ Universal rule: if the account already holds this symbol (>0 shares) and the
1183+ strategy wants to keep a meaningful fraction of a share (target >= 85% of 1-share
1184+ price), retain the position. This prevents the sell-then-fail-to-rebuy cycle for
1185+ small accounts where target < 1 share but still close to it.
1186+
1187+ Genuine reductions (target << 1 share) are NOT blocked — the sell proceeds.
1188+ The hardcoded lists act as overrides for symbols that need a different threshold.
1189+ """
11791190 normalized_symbol = str (symbol or "" ).strip ().upper ()
1191+ held = float (quantity or 0.0 )
1192+ target = float (target_value or 0.0 )
1193+ quote_price = max (0.0 , float (price or 0.0 ))
1194+
1195+ # Universal: held + positive target + target close to 1-share price → retain
1196+ if held > 0.0 and target > 0.0 and quote_price > 0.0 :
1197+ if target >= quote_price * _SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT :
1198+ return True
1199+
1200+ # Legacy whitelist — unconditional retention (safety net)
11801201 if normalized_symbol in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS :
11811202 return True
11821203
1204+ # Legacy per-symbol ratio-based retention (override / tighter threshold)
11831205 min_target_share_ratio = (
11841206 SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL .get (normalized_symbol )
11851207 )
11861208 if min_target_share_ratio is None :
11871209 return False
1188- quote_price = max (0.0 , float (price or 0.0 ))
11891210 if quote_price <= 0.0 :
11901211 return False
1191- return max ( 0.0 , float ( target_value or 0.0 )) >= quote_price * float (min_target_share_ratio )
1212+ return target >= quote_price * float (min_target_share_ratio )
11921213
11931214
11941215def _should_bootstrap_whole_share_buy (symbol , * , target_value , limit_price ) -> bool :
@@ -1484,7 +1505,7 @@ def record_quote_snapshot(symbol, snapshot) -> None:
14841505 _can_afford_one_share = limit_price > 0.0 and investable >= limit_price
14851506 held_quantity = max (0.0 , float (positions .get (symbol , {}).get ("quantity" , 0.0 ) or 0.0 ))
14861507 if (
1487- _should_retain_existing_whole_share (symbol , target_value = target_value , price = price )
1508+ _should_retain_existing_whole_share (symbol , target_value = target_value , price = price , quantity = held_quantity )
14881509 and price > 0.0
14891510 and 0.0 < target_value < price
14901511 and held_quantity >= 1.0
0 commit comments