Skip to content

Commit 2efb862

Browse files
Pigbibiclaude
andcommitted
fix(execution): universal whole-share retention rule for small accounts
Replace the per-symbol hardcoded whitelist with a universal rule: if the account already holds a symbol (>0 shares) AND the strategy wants to keep >= 85% of 1 share's value, retain the position. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent cb1c711 commit 2efb862

1 file changed

Lines changed: 25 additions & 4 deletions

File tree

application/execution_service.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1128,6 +1128,7 @@ def _limit_buy_price(symbol, price, default_premium, premium_by_symbol=None) ->
11281128
DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
11291129
SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0
11301130
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS = frozenset({"TQQQ", "SOXL"})
1131+
_SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT = 0.85
11311132
SMALL_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

11941215
def _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

Comments
 (0)