Skip to content

Commit 296da01

Browse files
Pigbibiclaude
andauthored
fix(execution): universal whole-share retention rule for small accounts (#209)
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 2d50452 commit 296da01

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
@@ -139,6 +139,7 @@ class ExecutionCycleResult:
139139
SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0
140140
MIN_NOTIONAL_BUY_USD = 1.0
141141
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS = frozenset({"TQQQ", "SOXL"})
142+
_SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT = 0.85
142143
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
143144
"SOXX": 0.90,
144145
}
@@ -272,20 +273,40 @@ def substitute_small_safe_haven_targets_with_cash(
272273
return adjusted_plan
273274

274275

275-
def _should_retain_existing_whole_share(symbol, *, target_value, price) -> bool:
276+
def _should_retain_existing_whole_share(symbol, *, target_value, price, quantity=0.0) -> bool:
277+
"""Decide whether an existing whole-share position should be retained.
278+
279+
Universal rule: if the account already holds this symbol (>0 shares) and the
280+
strategy wants to keep a meaningful fraction of a share (target >= 85% of
281+
1-share price), retain the position. This prevents the sell-then-fail-to-rebuy
282+
cycle for small accounts where target < 1 share but still close to it.
283+
284+
Genuine reductions (target << 1 share) are NOT blocked.
285+
The hardcoded lists act as overrides for symbols that need a different threshold.
286+
"""
276287
normalized_symbol = str(symbol or "").strip().upper()
288+
held = float(quantity or 0.0)
289+
target = float(target_value or 0.0)
290+
quote_price = max(0.0, float(price or 0.0))
291+
292+
# Universal: held + positive target + target close to 1-share price -> retain
293+
if held > 0.0 and target > 0.0 and quote_price > 0.0:
294+
if target >= quote_price * _SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT:
295+
return True
296+
297+
# Legacy whitelist -- unconditional retention (safety net)
277298
if normalized_symbol in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS:
278299
return True
279300

301+
# Legacy per-symbol ratio-based retention (override / tighter threshold)
280302
min_target_share_ratio = (
281303
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL.get(normalized_symbol)
282304
)
283305
if min_target_share_ratio is None:
284306
return False
285-
quote_price = max(0.0, float(price or 0.0))
286307
if quote_price <= 0.0:
287308
return False
288-
return max(0.0, float(target_value or 0.0)) >= quote_price * float(min_target_share_ratio)
309+
return target >= quote_price * float(min_target_share_ratio)
289310

290311

291312
def _should_bootstrap_whole_share_buy(symbol, *, target_value, limit_price) -> bool:
@@ -364,7 +385,7 @@ def _apply_small_account_whole_share_compatibility(
364385
)
365386
# Skip bootstrap if the account cannot afford even 1 share at limit price.
366387
_can_afford_one_share = limit_price > 0.0 and _estimated_buying_power >= limit_price
367-
if not _should_retain_existing_whole_share(symbol, target_value=target_value, price=price):
388+
if not _should_retain_existing_whole_share(symbol, target_value=target_value, price=price, quantity=quantities.get(symbol, 0.0)):
368389
if (
369390
quantities.get(symbol, 0.0) <= 0.0
370391
and 0.0 < target_value < limit_price

0 commit comments

Comments
 (0)