Skip to content

Commit f4c7ac2

Browse files
Pigbibiclaude
andauthored
refactor(notifications): migrate to shared renderer_base (#308)
* fix(execution): retain existing QQQM positions in small account whole-share compatibility QQQM (TQQQ's unlevered sleeve in tqqq_growth_income) was not in the whole-share retention whitelist. When the strategy's target for QQQM was below 1 share price, the small account compatibility layer zeroed the target, creating an artificial sell signal. After selling, the system couldn't buy back because the target (below 1 share) couldn't be fulfilled, leaving the account in cash. Add QQQM to: - SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS (unconditional) - SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL (0.85) - SMALL_ACCOUNT_WHOLE_SHARE_BOOTSTRAP_MIN_TARGET_SHARE_RATIO_BY_SYMBOL (0.85) Co-Authored-By: Claude <noreply@anthropic.com> * 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. This prevents the sell-then-fail-to-rebuy cycle for ALL symbols, not just those in the whitelist. Genuine reductions (target << 85% of 1 share) are NOT blocked — the sell proceeds normally. The 0.85 default threshold means: - QQQM $262.74 target vs $292.42 price → 90% → retain - SOXX $155 target vs $537 price → 29% → sell (genuine reduction) Legacy whitelist (TQQQ, SOXL) and per-symbol ratio overrides (e.g. SOXX:0.90) are kept as safety nets. Co-Authored-By: Claude <noreply@anthropic.com> * chore: bump QPK pin to 8378e93 for notification renderer_base Co-Authored-By: Claude <noreply@anthropic.com> * ci: trigger re-run after QPK_PIN update Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7466ccd commit f4c7ac2

4 files changed

Lines changed: 66 additions & 298 deletions

File tree

application/execution_service.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,15 @@ class ExecutionCycleResult:
265265
DEFAULT_BUY_QUANTITY_STEP = 1.0
266266
FRACTIONAL_BUY_QUANTITY_STEP = 0.0001
267267
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS = frozenset({"TQQQ", "SOXL"})
268+
_SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT = 0.85
268269
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
269270
"SOXX": 0.90,
270271
}
271272
SMALL_ACCOUNT_WHOLE_SHARE_BOOTSTRAP_MIN_TARGET_SHARE_RATIO_BY_SYMBOL = {
272273
"TQQQ": 0.90,
273274
"SOXL": 0.90,
274275
"SOXX": 0.90,
276+
"QQQM": 0.85,
275277
}
276278

277279

@@ -392,20 +394,40 @@ def _apply_safe_haven_cash_substitution(
392394
return adjusted_plan, adjusted_allocation
393395

394396

395-
def _should_retain_existing_whole_share(symbol, *, target_value, price) -> bool:
397+
def _should_retain_existing_whole_share(symbol, *, target_value, price, quantity=0.0) -> bool:
398+
"""Decide whether an existing whole-share position should be retained.
399+
400+
Universal rule: if the account already holds this symbol (>0 shares) and the
401+
strategy wants to keep a meaningful fraction of a share (target >= 85% of 1-share
402+
price), retain the position. This prevents the sell-then-fail-to-rebuy cycle for
403+
small accounts where target < 1 share but still close to it.
404+
405+
Genuine reductions (target << 1 share) are NOT blocked — the sell proceeds.
406+
The hardcoded lists act as overrides for symbols that need a different threshold.
407+
"""
396408
normalized_symbol = str(symbol or "").strip().upper()
409+
held = float(quantity or 0.0)
410+
target = float(target_value or 0.0)
411+
quote_price = max(0.0, float(price or 0.0))
412+
413+
# Universal: held + positive target + target close to 1-share price → retain
414+
if held > 0.0 and target > 0.0 and quote_price > 0.0:
415+
if target >= quote_price * _SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT:
416+
return True
417+
418+
# Legacy whitelist — unconditional retention (safety net)
397419
if normalized_symbol in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS:
398420
return True
399421

422+
# Legacy per-symbol ratio-based retention (override / tighter threshold)
400423
min_target_share_ratio = (
401424
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_MIN_TARGET_SHARE_RATIO_BY_SYMBOL.get(normalized_symbol)
402425
)
403426
if min_target_share_ratio is None:
404427
return False
405-
quote_price = max(0.0, float(price or 0.0))
406428
if quote_price <= 0.0:
407429
return False
408-
return max(0.0, float(target_value or 0.0)) >= quote_price * float(min_target_share_ratio)
430+
return target >= quote_price * float(min_target_share_ratio)
409431

410432

411433
def _should_bootstrap_whole_share_buy(symbol, *, target_value, limit_price) -> bool:
@@ -721,7 +743,7 @@ def _apply_small_account_whole_share_compatibility(
721743
)
722744
# Skip bootstrap if the account cannot afford even 1 share at limit price.
723745
_can_afford_one_share = limit_price > 0.0 and _estimated_buying_power >= limit_price
724-
if not _should_retain_existing_whole_share(symbol, target_value=target_value, price=price):
746+
if not _should_retain_existing_whole_share(symbol, target_value=target_value, price=price, quantity=quantities.get(symbol, 0.0)):
725747
if (
726748
quantities.get(symbol, 0.0) <= 0.0
727749
and 0.0 < target_value < limit_price

0 commit comments

Comments
 (0)