Skip to content

Commit 78ab532

Browse files
committed
Keep safe haven cash for unbuyable small accounts
1 parent 8009033 commit 78ab532

3 files changed

Lines changed: 73 additions & 2 deletions

File tree

application/execution_service.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ def execute_rebalance(
651651
),
652652
"safe_haven_cash_substituted_symbols": [],
653653
"small_account_whole_share_substituted_symbols": [],
654+
"small_account_safe_haven_cash_substituted_symbols": [],
654655
"residual_cash_estimate": float(account_values.get("buying_power", 0.0) or 0.0),
655656
"current_stock_weight": 0.0,
656657
"current_safe_haven_weight": 0.0,
@@ -740,9 +741,25 @@ def execute_rebalance(
740741
)
741742
for symbol in small_account_substituted_symbols:
742743
target_weights[symbol] = 0.0
744+
remaining_non_safe_targets = [
745+
symbol
746+
for symbol in small_account_candidate_symbols
747+
if float(target_mv.get(symbol, 0.0) or 0.0) > 0.0
748+
]
749+
small_account_safe_haven_cash_substituted_symbols: list[str] = []
750+
if small_account_substituted_symbols and not remaining_non_safe_targets:
751+
for symbol in safe_haven_symbols:
752+
normalized_symbol = str(symbol or "").strip().upper()
753+
if float(target_mv.get(normalized_symbol, 0.0) or 0.0) > 0.0:
754+
target_mv[normalized_symbol] = 0.0
755+
target_weights[normalized_symbol] = 0.0
756+
small_account_safe_haven_cash_substituted_symbols.append(normalized_symbol)
743757
execution_summary["small_account_whole_share_substituted_symbols"] = list(
744758
small_account_substituted_symbols
745759
)
760+
execution_summary["small_account_safe_haven_cash_substituted_symbols"] = (
761+
small_account_safe_haven_cash_substituted_symbols
762+
)
746763
trade_logs = []
747764
target_hash = _build_target_hash(target_weights)
748765
execution_summary["target_vs_current"] = _build_target_diff_rows(target_weights, current_mv, equity)

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
3-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@6f71766356beb0b1517a539dcacfe1c6d2719ef8
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@2918e1485561a50ad14abcd0bb05a09bd755b8c4
3+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@190edb21c5fe0c8e0efba66bfd8bacfdd00b3f77
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@ead0c075f5b47ec1d05d3a45cfcdda58217fe086
55
pandas
66
numpy
77
requests

tests/test_execution_service.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,60 @@ def accountValues(self):
223223
assert summary["orders_submitted"][1]["quantity"] == 2
224224

225225

226+
def test_execute_rebalance_keeps_safe_haven_cash_when_only_risk_target_is_unbuyable(tmp_path, monkeypatch):
227+
class FakeIB:
228+
def openTrades(self):
229+
return []
230+
231+
def fills(self):
232+
return []
233+
234+
def accountValues(self):
235+
return [SimpleNamespace(tag="AvailableFunds", currency="USD", value="1294.00")]
236+
237+
prices = {"SOXL": 175.0, "SOXX": 525.0, "BOXX": 116.83}
238+
submitted = []
239+
monkeypatch.setattr("application.execution_service.time.sleep", lambda _seconds: None)
240+
241+
_trade_logs, summary = execute_rebalance(
242+
FakeIB(),
243+
{},
244+
{},
245+
{"equity": 1294.0, "buying_power": 1294.0},
246+
fetch_quote_snapshots=lambda _ib, symbols: {
247+
symbol: SimpleNamespace(last_price=prices[symbol]) for symbol in symbols
248+
},
249+
submit_order_intent=lambda _ib, intent: submitted.append(intent) or SimpleNamespace(
250+
broker_order_id="dry-run",
251+
status="Submitted",
252+
),
253+
order_intent_cls=OrderIntent,
254+
translator=translate,
255+
strategy_symbols=["SOXL", "SOXX", "BOXX"],
256+
strategy_profile="soxl_soxx_trend_income",
257+
signal_metadata=_signal_metadata(
258+
{"SOXL": 0.0, "SOXX": 0.15, "BOXX": 0.85},
259+
risk_symbols=("SOXL", "SOXX"),
260+
safe_haven_symbols=("BOXX",),
261+
trade_date="2026-05-26",
262+
),
263+
dry_run_only=True,
264+
cash_reserve_ratio=0.03,
265+
rebalance_threshold_ratio=0.01,
266+
limit_buy_premium=1.0,
267+
quantity_step=1.0,
268+
sell_settle_delay_sec=0,
269+
execution_lock_dir=tmp_path,
270+
return_summary=True,
271+
)
272+
273+
assert submitted == []
274+
assert summary["small_account_whole_share_substituted_symbols"] == ["SOXX"]
275+
assert summary["small_account_safe_haven_cash_substituted_symbols"] == ["BOXX"]
276+
boxx_row = next(row for row in summary["target_vs_current"] if row["symbol"] == "BOXX")
277+
assert boxx_row["target_weight"] == 0.0
278+
279+
226280
def test_execute_rebalance_routes_order_to_single_account_id(monkeypatch, tmp_path):
227281
class FakeIB:
228282
def openTrades(self):

0 commit comments

Comments
 (0)