Skip to content

Commit e8c339a

Browse files
Pigbibicodex
andcommitted
fix(execution): bridge cash-only sell-funded buys
Co-Authored-By: Codex <noreply@openai.com>
1 parent 3c0449d commit e8c339a

2 files changed

Lines changed: 113 additions & 0 deletions

File tree

application/execution_service.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,20 @@ def _should_bootstrap_whole_share_buy(symbol, *, target_value, limit_price) -> b
12251225
return max(0.0, float(target_value or 0.0)) >= effective_limit_price * float(min_target_share_ratio)
12261226

12271227

1228+
def _should_top_up_existing_whole_share_buy(symbol, *, target_gap_value, limit_price, quantity=0.0) -> bool:
1229+
normalized_symbol = str(symbol or "").strip().upper()
1230+
if normalized_symbol not in SMALL_ACCOUNT_WHOLE_SHARE_BOOTSTRAP_MIN_TARGET_SHARE_RATIO_BY_SYMBOL:
1231+
return False
1232+
if max(0.0, float(quantity or 0.0)) <= 0.0:
1233+
return False
1234+
effective_limit_price = max(0.0, float(limit_price or 0.0))
1235+
if effective_limit_price <= 0.0:
1236+
return False
1237+
return max(0.0, float(target_gap_value or 0.0)) >= (
1238+
effective_limit_price * float(_SMALL_ACCOUNT_RETENTION_MIN_TARGET_SHARE_RATIO_DEFAULT)
1239+
)
1240+
1241+
12281242
def _format_symbol_with_suffix(symbol, *, suffix=".US") -> str:
12291243
normalized = str(symbol or "").strip().upper()
12301244
if not normalized:
@@ -1370,6 +1384,7 @@ def record_quote_snapshot(symbol, snapshot) -> None:
13701384
"small_account_whole_share_cash_notes": [],
13711385
"small_account_allocation_drift_notes": [],
13721386
"residual_cash_estimate": float(account_values.get("buying_power", 0.0) or 0.0),
1387+
"projected_sell_release_value": 0.0,
13731388
"current_stock_weight": 0.0,
13741389
"current_safe_haven_weight": 0.0,
13751390
"price_source_mode": "market_quote",
@@ -1900,6 +1915,7 @@ def cash_sweep_sale_quantity_to_fund_buy(max_quantity: int, candidate_symbols: t
19001915
execution_summary["execution_status"] = "executing"
19011916

19021917
sell_executed = False
1918+
projected_sell_release_value = 0.0
19031919
pending_sell_release_symbols: list[str] = []
19041920
for symbol in all_symbols:
19051921
current = current_mv.get(symbol, 0)
@@ -1979,6 +1995,7 @@ def cash_sweep_sale_quantity_to_fund_buy(max_quantity: int, candidate_symbols: t
19791995
trade_logs.append(translator("market_sell", symbol=symbol, qty=format_quantity(qty)) + f" {status_msg}")
19801996
if ok:
19811997
sell_executed = True
1998+
projected_sell_release_value += float(qty) * float(price)
19821999

19832000
if dry_run_only:
19842001
buying_power = max(0.0, anticipated_buying_power + dry_run_sale_proceeds)
@@ -1992,9 +2009,15 @@ def cash_sweep_sale_quantity_to_fund_buy(max_quantity: int, candidate_symbols: t
19922009
currency=market_currency,
19932010
cash_only_execution=cash_only_execution,
19942011
)
2012+
if cash_only_execution and projected_sell_release_value > 0.0:
2013+
buying_power = max(
2014+
float(buying_power or 0.0),
2015+
float(anticipated_buying_power or 0.0) + float(projected_sell_release_value),
2016+
)
19952017
else:
19962018
buying_power = anticipated_buying_power
19972019
investable_buying_power = _investable_buying_power(buying_power, reserved)
2020+
execution_summary["projected_sell_release_value"] = float(projected_sell_release_value)
19982021
pending_sell_release_symbols = list(dict.fromkeys(pending_sell_release_symbols))
19992022
buy_needed_symbols = [
20002023
symbol
@@ -2068,6 +2091,34 @@ def cash_sweep_sale_quantity_to_fund_buy(max_quantity: int, candidate_symbols: t
20682091
buy_value / limit_price,
20692092
quantity_step=order_quantity_step,
20702093
)
2094+
held_quantity = max(0.0, float(positions.get(symbol, {}).get("quantity", 0.0) or 0.0))
2095+
if (
2096+
qty <= 0
2097+
and limit_price > 0.0
2098+
and investable_buying_power >= limit_price
2099+
and (
2100+
_should_top_up_existing_whole_share_buy(
2101+
symbol,
2102+
target_gap_value=buy_value,
2103+
limit_price=limit_price,
2104+
quantity=held_quantity,
2105+
)
2106+
or _should_bootstrap_whole_share_buy(
2107+
symbol,
2108+
target_value=buy_value,
2109+
limit_price=limit_price,
2110+
)
2111+
)
2112+
):
2113+
qty = _floor_order_quantity(1.0, quantity_step=order_quantity_step)
2114+
if qty > 0 and symbol not in execution_summary["small_account_whole_share_bootstrap_symbols"]:
2115+
execution_summary["small_account_whole_share_bootstrap_symbols"].append(symbol)
2116+
trade_logs.extend(
2117+
_format_small_account_whole_share_bootstrap_notes(
2118+
(symbol,),
2119+
translator=translator,
2120+
)
2121+
)
20712122
if qty <= 0:
20722123
execution_summary["orders_skipped"].append({"symbol": symbol, "side": "buy", "reason": "quantity_zero"})
20732124
continue

tests/test_execution_service.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,68 @@ def accountValues(self):
470470
assert buy_notional <= max_investable + 1.0
471471

472472

473+
def test_execute_rebalance_live_cash_only_reuses_projected_sell_proceeds_when_cash_snapshot_is_stale(
474+
tmp_path, monkeypatch
475+
):
476+
class FakeIB:
477+
def openTrades(self):
478+
return []
479+
480+
def fills(self):
481+
return []
482+
483+
def accountValues(self):
484+
return [SimpleNamespace(tag="CashBalance", currency="USD", value="112.38")]
485+
486+
prices = {"SOXL": 192.50, "SOXX": 582.25}
487+
submitted = []
488+
monkeypatch.setattr("application.execution_service.time.sleep", lambda _seconds: None)
489+
490+
def fake_submit_order_intent(_ib, intent):
491+
submitted.append(intent)
492+
return SimpleNamespace(broker_order_id=f"order-{len(submitted)}", status="Submitted")
493+
494+
_trade_logs, summary = execute_rebalance(
495+
FakeIB(),
496+
{},
497+
{
498+
"SOXL": {"quantity": 3},
499+
"SOXX": {"quantity": 2},
500+
},
501+
{"equity": 1920.36, "buying_power": 112.38},
502+
fetch_quote_snapshots=lambda _ib, symbols: {
503+
symbol: SimpleNamespace(last_price=prices[symbol]) for symbol in symbols
504+
},
505+
submit_order_intent=fake_submit_order_intent,
506+
order_intent_cls=OrderIntent,
507+
translator=build_translator("zh"),
508+
strategy_symbols=["SOXL", "SOXX"],
509+
strategy_profile="soxl_soxx_trend_income",
510+
signal_metadata=_signal_metadata(
511+
{"SOXL": 0.0, "SOXX": 0.90},
512+
risk_symbols=("SOXL", "SOXX"),
513+
trade_date="2026-07-09",
514+
),
515+
dry_run_only=False,
516+
cash_reserve_ratio=0.03,
517+
rebalance_threshold_ratio=0.01,
518+
limit_buy_premium=1.005,
519+
quantity_step=1.0,
520+
sell_settle_delay_sec=0,
521+
execution_lock_dir=tmp_path,
522+
return_summary=True,
523+
)
524+
525+
assert [(intent.symbol, intent.side, intent.quantity) for intent in submitted] == [
526+
("SOXL", "sell", 3),
527+
("SOXX", "buy", 1),
528+
]
529+
assert summary["orders_submitted"][0]["symbol"] == "SOXL"
530+
assert summary["orders_submitted"][1]["symbol"] == "SOXX"
531+
assert summary["projected_sell_release_value"] == 577.5
532+
assert summary["orders_skipped"] == []
533+
534+
473535
def test_execute_rebalance_projects_unbuyable_weight_target_to_zero(tmp_path, monkeypatch):
474536
class FakeIB:
475537
def openTrades(self):

0 commit comments

Comments
 (0)