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
55 changes: 55 additions & 0 deletions application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,42 @@ def should_sell_cash_sweep_to_fund_whole_share_buy(
if current_buying_power + sweep_capacity >= quote_price:
return True
return False
try:
from quant_platform_kit.common.small_account_compatibility import (
project_unbuyable_value_targets_to_cash,
)
except ImportError: # pragma: no cover - compatibility with older pinned shared wheels
def project_unbuyable_value_targets_to_cash(
target_values,
prices,
*,
symbols=None,
quantity_step=1.0,
):
adjusted = {
str(symbol or "").strip().upper(): float(value or 0.0)
for symbol, value in dict(target_values or {}).items()
}
step = max(0.0, float(quantity_step or 0.0))
if step <= 0.0:
return adjusted, ()
candidate_symbols = (
tuple(adjusted)
if symbols is None
else tuple(dict.fromkeys(str(symbol or "").strip().upper() for symbol in symbols))
)
normalized_prices = {
str(symbol or "").strip().upper(): float(price or 0.0)
for symbol, price in dict(prices or {}).items()
}
substituted = []
for symbol in candidate_symbols:
target_value = max(0.0, float(adjusted.get(symbol, 0.0) or 0.0))
price = max(0.0, float(normalized_prices.get(symbol, 0.0) or 0.0))
if price > 0.0 and 0.0 < target_value < (price * step):
adjusted[symbol] = 0.0
substituted.append(symbol)
return adjusted, tuple(dict.fromkeys(substituted))
from quant_platform_kit.common.quantity import (
floor_to_quantity_step,
format_quantity,
Expand Down Expand Up @@ -613,6 +649,7 @@ def execute_rebalance(
max(0.0, float(safe_haven_cash_substitute_threshold_usd or 0.0))
),
"safe_haven_cash_substituted_symbols": [],
"small_account_whole_share_substituted_symbols": [],
"residual_cash_estimate": float(account_values.get("buying_power", 0.0) or 0.0),
"current_stock_weight": 0.0,
"current_safe_haven_weight": 0.0,
Expand Down Expand Up @@ -684,6 +721,24 @@ def execute_rebalance(
current_mv[symbol] = qty * price

target_mv = {symbol: investable * weight for symbol, weight in target_weights.items()}
small_account_candidate_symbols = tuple(
dict.fromkeys(tuple(allocation["risk_symbols"]) + tuple(allocation["income_symbols"]))
)
if not small_account_candidate_symbols:
small_account_candidate_symbols = tuple(
symbol for symbol in target_mv if symbol not in safe_haven_symbols
)
target_mv, small_account_substituted_symbols = project_unbuyable_value_targets_to_cash(
target_mv,
prices,
symbols=small_account_candidate_symbols,
quantity_step=order_quantity_step,
)
for symbol in small_account_substituted_symbols:
target_weights[symbol] = 0.0
execution_summary["small_account_whole_share_substituted_symbols"] = list(
small_account_substituted_symbols
)
trade_logs = []
target_hash = _build_target_hash(target_weights)
execution_summary["target_vs_current"] = _build_target_diff_rows(target_weights, current_mv, equity)
Expand Down
58 changes: 58 additions & 0 deletions tests/test_execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,64 @@ def fake_fetch_quote_snapshots(_ib, symbols):
assert any(log.startswith("buy VOO") for log in trade_logs)


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

def fills(self):
return []

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

prices = {"SOXL": 191.15, "SOXX": 536.88, "BOXX": 100.0}
monkeypatch.setattr("application.execution_service.time.sleep", lambda _seconds: None)

_trade_logs, summary = execute_rebalance(
FakeIB(),
{},
{"SOXX": {"quantity": 1}},
{"equity": 773.69, "buying_power": 236.81},
fetch_quote_snapshots=lambda _ib, symbols: {
symbol: SimpleNamespace(last_price=prices[symbol]) for symbol in symbols
},
submit_order_intent=lambda *_args, **_kwargs: SimpleNamespace(
broker_order_id="dry-run",
status="Submitted",
),
order_intent_cls=OrderIntent,
translator=translate,
strategy_symbols=["SOXL", "SOXX", "BOXX"],
strategy_profile="soxl_soxx_trend_income",
signal_metadata=_signal_metadata(
{"SOXL": 0.70, "SOXX": 0.20, "BOXX": 0.10},
risk_symbols=("SOXL", "SOXX"),
safe_haven_symbols=("BOXX",),
trade_date="2026-05-22",
),
dry_run_only=True,
cash_reserve_ratio=0.03,
rebalance_threshold_ratio=0.01,
limit_buy_premium=1.0,
quantity_step=1.0,
sell_settle_delay_sec=0,
execution_lock_dir=tmp_path,
return_summary=True,
)

assert summary["small_account_whole_share_substituted_symbols"] == ["SOXX"]
assert summary["orders_submitted"][0] == {
"symbol": "SOXX",
"side": "sell",
"quantity": 1,
"status": "dry_run",
}
assert summary["orders_submitted"][1]["symbol"] == "SOXL"
assert summary["orders_submitted"][1]["side"] == "buy"
assert summary["orders_submitted"][1]["quantity"] == 2


def test_execute_rebalance_routes_order_to_single_account_id(monkeypatch, tmp_path):
class FakeIB:
def openTrades(self):
Expand Down