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
49 changes: 47 additions & 2 deletions application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,17 @@ def _sell_order_quantity(


DEFAULT_SAFE_HAVEN_CASH_SUBSTITUTE_THRESHOLD_USD = 1000.0
SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD = 2000.0


def _positive_target_total(targets: dict[str, Any]) -> float:
total = 0.0
for value in dict(targets or {}).values():
try:
total += max(0.0, float(value or 0.0))
except (TypeError, ValueError):
continue
return total


def _apply_safe_haven_cash_substitution_to_weights(
Expand Down Expand Up @@ -651,6 +662,7 @@ def execute_rebalance(
),
"safe_haven_cash_substituted_symbols": [],
"small_account_whole_share_substituted_symbols": [],
"small_account_safe_haven_cash_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 @@ -726,11 +738,17 @@ def execute_rebalance(

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"]))
dict.fromkeys(
str(symbol or "").strip().upper()
for symbol in tuple(allocation["risk_symbols"]) + tuple(allocation["income_symbols"])
if str(symbol or "").strip()
)
)
if not small_account_candidate_symbols:
small_account_candidate_symbols = tuple(
symbol for symbol in target_mv if symbol not in safe_haven_symbols
str(symbol or "").strip().upper()
for symbol in target_mv
if str(symbol or "").strip().upper() not in safe_haven_symbols
)
target_mv, small_account_substituted_symbols = project_unbuyable_value_targets_to_cash(
target_mv,
Expand All @@ -740,9 +758,36 @@ def execute_rebalance(
)
for symbol in small_account_substituted_symbols:
target_weights[symbol] = 0.0
remaining_non_safe_targets = [
symbol
for symbol in small_account_candidate_symbols
if float(target_mv.get(str(symbol or "").strip().upper(), 0.0) or 0.0) > 0.0
]
small_account_safe_haven_cash_substituted_symbols: list[str] = []
if (
small_account_substituted_symbols
and not remaining_non_safe_targets
and _positive_target_total(target_mv) <= SMALL_ACCOUNT_SAFE_HAVEN_CASH_SUBSTITUTE_LIMIT_USD
):
for symbol in safe_haven_symbols:
normalized_symbol = str(symbol or "").strip().upper()
if float(target_mv.get(normalized_symbol, 0.0) or 0.0) > 0.0:
target_mv[normalized_symbol] = 0.0
target_weights[normalized_symbol] = 0.0
Comment on lines +775 to +776

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recompute realized safe-haven weight after cash substitution

When a small account's only positive risk target is projected to cash, this branch zeroes the safe-haven target but leaves execution_summary["realized_safe_haven_weight"] at the value computed earlier from the pre-substitution weights. In the new SOXX/BOXX scenario this means reconciliation/reporting can show an 85% realized safe-haven weight while target_vs_current and order generation treat BOXX as 0%, so the summary is internally inconsistent; update the realized safe-haven weight after this substitution.

Useful? React with 👍 / 👎.

small_account_safe_haven_cash_substituted_symbols.append(normalized_symbol)
if safe_haven_symbols:
execution_summary["realized_safe_haven_weight"] = float(
sum(
float(target_weights.get(str(symbol or "").strip().upper(), 0.0) or 0.0)
for symbol in safe_haven_symbols
)
)
execution_summary["small_account_whole_share_substituted_symbols"] = list(
small_account_substituted_symbols
)
execution_summary["small_account_safe_haven_cash_substituted_symbols"] = (
small_account_safe_haven_cash_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
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
flask
gunicorn
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@6f71766356beb0b1517a539dcacfe1c6d2719ef8
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@2918e1485561a50ad14abcd0bb05a09bd755b8c4
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@190edb21c5fe0c8e0efba66bfd8bacfdd00b3f77
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ead0c075f5b47ec1d05d3a45cfcdda58217fe086
pandas
numpy
requests
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 @@ -223,6 +223,61 @@ def accountValues(self):
assert summary["orders_submitted"][1]["quantity"] == 2


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

def fills(self):
return []

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

prices = {"SOXL": 175.0, "SOXX": 525.0, "BOXX": 116.83}
submitted = []
monkeypatch.setattr("application.execution_service.time.sleep", lambda _seconds: None)

_trade_logs, summary = execute_rebalance(
FakeIB(),
{},
{},
{"equity": 1294.0, "buying_power": 1294.0},
fetch_quote_snapshots=lambda _ib, symbols: {
symbol: SimpleNamespace(last_price=prices[symbol]) for symbol in symbols
},
submit_order_intent=lambda _ib, intent: submitted.append(intent) or 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.0, "SOXX": 0.15, "BOXX": 0.85},
risk_symbols=("SOXL", "SOXX"),
safe_haven_symbols=("BOXX",),
trade_date="2026-05-26",
),
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 submitted == []
assert summary["small_account_whole_share_substituted_symbols"] == ["SOXX"]
assert summary["small_account_safe_haven_cash_substituted_symbols"] == ["BOXX"]
assert summary["realized_safe_haven_weight"] == 0.0
boxx_row = next(row for row in summary["target_vs_current"] if row["symbol"] == "BOXX")
assert boxx_row["target_weight"] == 0.0


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