Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ def _sell_order_quantity(

DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0
SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS = frozenset({"TQQQ", "SOXL"})


def _positive_target_total(targets: dict[str, Any]) -> float:
Expand Down Expand Up @@ -1155,6 +1156,18 @@ def record_quote_snapshot(symbol, snapshot) -> None:
for symbol in target_mv
if str(symbol or "").strip().upper() not in safe_haven_symbols
)
small_account_retained_symbols = []
for symbol in small_account_candidate_symbols:
if symbol not in SMALL_ACCOUNT_EXISTING_WHOLE_SHARE_RETENTION_SYMBOLS:
continue
target_value = max(0.0, float(target_mv.get(symbol, 0.0) or 0.0))
price = max(0.0, float(prices.get(symbol, 0.0) or 0.0))
held_quantity = max(0.0, float(positions.get(symbol, {}).get("quantity", 0.0) or 0.0))
if price > 0.0 and 0.0 < target_value < price and held_quantity >= 1.0:
target_mv[symbol] = price
if investable > 0.0:
target_weights[symbol] = price / investable
Comment on lines +1167 to +1169

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve total target notional when retaining a share

When a fully allocated small account already holds one TQQQ/SOXL share and the desired sleeve is below one share, this raises only that sleeve's target notional but leaves the other targets unchanged. For example, with $100 equity, one $80 TQQQ share, 60% TQQQ / 40% QQQM targets, and margin buying power, the effective targets become $80 + $40, so the buy loop can submit QQQM buys that take the account to 120% gross exposure instead of treating the retained extra $20 as already consuming allocation. Please offset the retained-share notional from cash/other sleeves or keep this adjustment limited to the sell floor rather than increasing total targets.

Useful? React with 👍 / 👎.

small_account_retained_symbols.append(symbol)
small_account_compatibility = apply_small_account_cash_compatibility(
target_mv,
prices,
Expand Down Expand Up @@ -1185,6 +1198,9 @@ def record_quote_snapshot(symbol, snapshot) -> None:
execution_summary["small_account_safe_haven_cash_substituted_symbols"] = (
small_account_safe_haven_cash_substituted_symbols
)
execution_summary["small_account_existing_whole_share_retained_symbols"] = list(
dict.fromkeys(small_account_retained_symbols)
)
execution_summary["small_account_whole_share_cash_notes"] = list(
small_account_compatibility.cash_substitution_notes
)
Expand Down
55 changes: 55 additions & 0 deletions tests/test_execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,61 @@ def fake_submit_order_intent(_ib, intent):
assert submitted[0].quantity == 2


def test_execute_rebalance_keeps_existing_whole_share_when_positive_target_is_unbuyable(monkeypatch, tmp_path):
class FakeIB:
def openTrades(self):
return []

def fills(self):
return []

def accountValues(self):
return [SimpleNamespace(tag="AvailableFunds", currency="USD", value="500")]

submitted = []

def fake_submit_order_intent(_ib, intent):
submitted.append(intent)
return SimpleNamespace(broker_order_id="1", status="Submitted")

monkeypatch.setattr("application.execution_service.time.sleep", lambda _seconds: None)

_trade_logs, summary = execute_rebalance(
FakeIB(),
{"TQQQ": 0.1, "QQQM": 0.6},
{"TQQQ": {"quantity": 7}, "QQQM": {"quantity": 0}},
{"equity": 540.0, "buying_power": 540.0},
fetch_quote_snapshots=lambda *_args, **_kwargs: {
"TQQQ": SimpleNamespace(last_price=77.33),
"QQQM": SimpleNamespace(last_price=297.19),
},
submit_order_intent=fake_submit_order_intent,
order_intent_cls=OrderIntent,
translator=translate,
strategy_symbols=["TQQQ", "QQQM"],
strategy_profile="tqqq_growth_income",
signal_metadata=_signal_metadata(
{"TQQQ": 60.94 / 540.0, "QQQM": 320.0 / 540.0},
risk_symbols=("TQQQ", "QQQM"),
trade_date="2026-06-17",
),
dry_run_only=False,
cash_reserve_ratio=0.0,
rebalance_threshold_ratio=0.02,
limit_buy_premium=1.005,
quantity_step=1.0,
sell_settle_delay_sec=0,
execution_lock_dir=tmp_path,
return_summary=True,
)

assert summary["small_account_existing_whole_share_retained_symbols"] == ["TQQQ"]
sell_orders = [intent for intent in submitted if intent.side == "sell"]
assert len(sell_orders) == 1
assert sell_orders[0].symbol == "TQQQ"
assert sell_orders[0].quantity == 6


def test_execute_rebalance_skips_when_pending_orders_exist():
class FakeIB:
def openTrades(self):
Expand Down