Skip to content

Commit 4f98faf

Browse files
Pigbibicodex
andauthored
fix: require execution claim before IBKR submission (#477)
Co-authored-by: Codex <noreply@openai.com>
1 parent 072f01f commit 4f98faf

6 files changed

Lines changed: 267 additions & 19 deletions

File tree

application/execution_service.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,7 @@ def execute_rebalance(
13981398
translator,
13991399
strategy_symbols=None,
14001400
signal_metadata=None,
1401+
acquire_execution_claim=None,
14011402
strategy_profile=None,
14021403
account_group=None,
14031404
service_name=None,
@@ -1423,6 +1424,15 @@ def execute_rebalance(
14231424
):
14241425
"""Execute trades to reach target weights."""
14251426
del target_weights
1427+
if not dry_run_only:
1428+
delegate_submit = submit_order_intent
1429+
1430+
def submit_claimed_order(ib, order_intent):
1431+
if acquire_execution_claim is None or not acquire_execution_claim():
1432+
raise RuntimeError("IBKR execution claim required; refusing broker submission")
1433+
return delegate_submit(ib, order_intent)
1434+
1435+
submit_order_intent = submit_claimed_order
14261436
signal_metadata = signal_metadata or {}
14271437
allocation = _resolve_weight_allocation(signal_metadata)
14281438
target_weights = dict(allocation["targets"])

application/rebalance_service.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -965,7 +965,6 @@ def run_strategy_core(
965965
execution_marker_key = _build_execution_marker_key(config=config, signal_metadata=signal_metadata)
966966
execution_state_store = getattr(config, "execution_state_store", None)
967967
execution_already_recorded = False
968-
execution_claim_acquired = False
969968
if execution_marker_key and execution_state_store:
970969
try:
971970
execution_already_recorded = bool(execution_state_store.has_marker(execution_marker_key))
@@ -996,24 +995,6 @@ def run_strategy_core(
996995
flush=True,
997996
)
998997

999-
if (
1000-
not execution_already_recorded
1001-
and execution_marker_key
1002-
and execution_state_store
1003-
and bool(getattr(config, "execution_dedup_enabled", False))
1004-
and not bool(getattr(config, "dry_run_only", False))
1005-
):
1006-
try:
1007-
execution_claim_acquired = bool(execution_state_store.claim_marker(
1008-
execution_marker_key,
1009-
metadata={"platform": "ibkr", "strategy_profile": signal_metadata.get("strategy_profile")},
1010-
))
1011-
execution_already_recorded = not execution_claim_acquired
1012-
except Exception as exc:
1013-
raise RuntimeError(
1014-
f"IBKR execution claim unavailable; refusing broker submission: {type(exc).__name__}"
1015-
) from exc
1016-
1017998
if execution_already_recorded:
1018999
message = _execution_already_recorded_message(config=config, signal_metadata=signal_metadata)
10191000
print(message, flush=True)
@@ -1069,13 +1050,37 @@ def run_strategy_core(
10691050
reconciliation_record_path=str(record_path),
10701051
)
10711052

1053+
execution_claim_attempted = False
1054+
execution_claim_acquired = False
1055+
1056+
def acquire_execution_claim():
1057+
nonlocal execution_claim_attempted, execution_claim_acquired
1058+
# No-op cycles never claim; a failed attempt cannot retry on another intent.
1059+
if not execution_claim_attempted:
1060+
execution_claim_attempted = True
1061+
if (
1062+
not config.dry_run_only
1063+
and config.execution_dedup_enabled
1064+
and execution_marker_key
1065+
and execution_state_store is not None
1066+
):
1067+
try:
1068+
execution_claim_acquired = bool(execution_state_store.claim_marker(
1069+
execution_marker_key,
1070+
metadata={"platform": "ibkr", "strategy_profile": signal_metadata.get("strategy_profile")},
1071+
))
1072+
except Exception:
1073+
raise RuntimeError("IBKR execution claim unavailable; refusing broker submission") from None
1074+
return execution_claim_acquired
1075+
10721076
execution_result = runtime.execute_rebalance(
10731077
ib,
10741078
resolved_target_weights,
10751079
positions,
10761080
account_values,
10771081
strategy_symbols=allocation.get("strategy_symbols"),
10781082
signal_metadata=signal_metadata,
1083+
acquire_execution_claim=acquire_execution_claim,
10791084
)
10801085
if isinstance(execution_result, tuple) and len(execution_result) == 2:
10811086
trade_logs, execution_summary = execution_result

application/runtime_broker_adapters.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ def execute_rebalance(
307307
*,
308308
strategy_symbols=None,
309309
signal_metadata=None,
310+
acquire_execution_claim=None,
310311
):
311312
return self.application_execute_rebalance_fn(
312313
ib,
@@ -319,6 +320,7 @@ def execute_rebalance(
319320
translator=self.translator,
320321
strategy_symbols=strategy_symbols,
321322
signal_metadata=signal_metadata or {},
323+
acquire_execution_claim=acquire_execution_claim,
322324
strategy_profile=self.strategy_profile,
323325
account_group=self.account_group,
324326
service_name=self.service_name,

main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,6 +1346,7 @@ def execute_rebalance(
13461346
*,
13471347
strategy_symbols=None,
13481348
signal_metadata=None,
1349+
acquire_execution_claim=None,
13491350
dry_run_only_override: bool | None = None,
13501351
):
13511352
return build_broker_adapters(dry_run_only_override=dry_run_only_override).execute_rebalance(
@@ -1355,6 +1356,7 @@ def execute_rebalance(
13551356
account_values,
13561357
strategy_symbols=strategy_symbols,
13571358
signal_metadata=signal_metadata,
1359+
acquire_execution_claim=acquire_execution_claim,
13581360
)
13591361

13601362

tests/test_execution_service.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from types import SimpleNamespace
22

3+
import pytest
4+
35
from application.execution_service import check_order_submitted, execute_rebalance, get_available_buying_power
46
from notifications.telegram import build_translator
57
from quant_platform_kit.common.models import OrderIntent
@@ -159,6 +161,7 @@ def fake_fetch_quote_snapshots(_ib, symbols):
159161
fetch_quote_snapshots=fake_fetch_quote_snapshots,
160162
submit_order_intent=fake_submit_order_intent,
161163
order_intent_cls=OrderIntent,
164+
acquire_execution_claim=lambda: True,
162165
translator=translate,
163166
strategy_symbols=["VOO", "BIL"],
164167
strategy_profile="tech_communication_pullback_enhancement",
@@ -188,6 +191,30 @@ def fake_fetch_quote_snapshots(_ib, symbols):
188191
assert any(log.startswith("buy VOO") for log in trade_logs)
189192

190193

194+
@pytest.mark.parametrize("claim", [None, lambda: False])
195+
def test_execute_rebalance_cannot_submit_without_successful_claim_callback(tmp_path, claim):
196+
submitted = []
197+
ib = SimpleNamespace(
198+
openTrades=lambda: [],
199+
accountValues=lambda: [SimpleNamespace(tag="AvailableFunds", currency="USD", value="1000")],
200+
)
201+
with pytest.raises(RuntimeError, match="execution claim required"):
202+
execute_rebalance(
203+
ib, {"VOO": 0.5}, {}, {"equity": 1000.0, "buying_power": 1000.0},
204+
fetch_quote_snapshots=lambda _ib, symbols: {
205+
symbol: SimpleNamespace(last_price=100.0) for symbol in symbols
206+
},
207+
submit_order_intent=lambda _ib, intent: submitted.append(intent),
208+
order_intent_cls=OrderIntent, translator=translate,
209+
signal_metadata=_signal_metadata({"VOO": 0.5}, risk_symbols=("VOO",)),
210+
acquire_execution_claim=claim, dry_run_only=False,
211+
cash_reserve_ratio=0.0, rebalance_threshold_ratio=0.02,
212+
limit_buy_premium=1.0, sell_settle_delay_sec=0,
213+
execution_lock_dir=tmp_path,
214+
)
215+
assert submitted == []
216+
217+
191218
def test_execute_rebalance_paper_admission_blocks_before_calling_the_broker(tmp_path):
192219
class FakeIB:
193220
def openTrades(self):
@@ -273,6 +300,7 @@ def accountValues(self):
273300
status="Rejected",
274301
),
275302
order_intent_cls=OrderIntent,
303+
acquire_execution_claim=lambda: True,
276304
translator=translate,
277305
strategy_symbols=["VOO"],
278306
strategy_profile="tech_communication_pullback_enhancement",
@@ -324,6 +352,7 @@ def fake_submit_order_intent(_ib, intent):
324352
},
325353
submit_order_intent=fake_submit_order_intent,
326354
order_intent_cls=OrderIntent,
355+
acquire_execution_claim=lambda: True,
327356
translator=translate,
328357
strategy_symbols=["SOXL"],
329358
strategy_profile="soxl_soxx_trend_income",
@@ -649,6 +678,7 @@ def fake_submit_order_intent(_ib, intent):
649678
},
650679
submit_order_intent=fake_submit_order_intent,
651680
order_intent_cls=OrderIntent,
681+
acquire_execution_claim=lambda: True,
652682
translator=build_translator("zh"),
653683
strategy_symbols=["SOXL", "SOXX"],
654684
strategy_profile="soxl_soxx_trend_income",
@@ -708,6 +738,7 @@ def fake_submit_order_intent(_ib, intent):
708738
},
709739
submit_order_intent=fake_submit_order_intent,
710740
order_intent_cls=OrderIntent,
741+
acquire_execution_claim=lambda: True,
711742
translator=build_translator("zh"),
712743
strategy_symbols=["SOXL", "SOXX"],
713744
strategy_profile="soxl_soxx_trend_income",
@@ -770,6 +801,7 @@ def fake_submit_order_intent(_ib, intent):
770801
},
771802
submit_order_intent=fake_submit_order_intent,
772803
order_intent_cls=OrderIntent,
804+
acquire_execution_claim=lambda: True,
773805
translator=build_translator("zh"),
774806
strategy_symbols=["SOXL", "SOXX"],
775807
strategy_profile="soxl_soxx_trend_income",
@@ -1000,6 +1032,7 @@ def fake_submit_order_intent(_ib, intent):
10001032
},
10011033
submit_order_intent=fake_submit_order_intent,
10021034
order_intent_cls=OrderIntent,
1035+
acquire_execution_claim=lambda: True,
10031036
translator=translate,
10041037
strategy_symbols=["TQQQ"],
10051038
strategy_profile="tqqq_growth_income",
@@ -1078,6 +1111,7 @@ def fake_submit_order_intent(_ib, intent):
10781111
fetch_quote_snapshots=lambda *_args, **_kwargs: {"VOO": SimpleNamespace(last_price=165.85)},
10791112
submit_order_intent=fake_submit_order_intent,
10801113
order_intent_cls=OrderIntent,
1114+
acquire_execution_claim=lambda: True,
10811115
translator=translate,
10821116
strategy_symbols=["VOO"],
10831117
strategy_profile="global_etf_rotation",
@@ -1129,6 +1163,7 @@ def fake_submit_order_intent(_ib, intent):
11291163
},
11301164
submit_order_intent=fake_submit_order_intent,
11311165
order_intent_cls=OrderIntent,
1166+
acquire_execution_claim=lambda: True,
11321167
translator=translate,
11331168
strategy_symbols=["TQQQ", "QQQM"],
11341169
strategy_profile="tqqq_growth_income",
@@ -1457,6 +1492,7 @@ def fake_fetch_quote_snapshots(_ib, symbols):
14571492
fetch_quote_snapshots=fake_fetch_quote_snapshots,
14581493
submit_order_intent=lambda *_args, **_kwargs: SimpleNamespace(broker_order_id="1", status="Submitted"),
14591494
order_intent_cls=OrderIntent,
1495+
acquire_execution_claim=lambda: True,
14601496
translator=translate,
14611497
strategy_symbols=["VOO", "BOXX"],
14621498
strategy_profile="tech_communication_pullback_enhancement",
@@ -1576,6 +1612,7 @@ def fake_fetch_quote_snapshots(_ib, symbols):
15761612
fetch_quote_snapshots=fake_fetch_quote_snapshots,
15771613
submit_order_intent=lambda *_args, **_kwargs: SimpleNamespace(broker_order_id="1", status="Submitted"),
15781614
order_intent_cls=OrderIntent,
1615+
acquire_execution_claim=lambda: True,
15791616
translator=translate,
15801617
strategy_symbols=["VOO", "BOXX"],
15811618
strategy_profile="tech_communication_pullback_enhancement",
@@ -1705,6 +1742,7 @@ def fake_submit_order_intent(_ib, intent):
17051742
},
17061743
submit_order_intent=fake_submit_order_intent,
17071744
order_intent_cls=OrderIntent,
1745+
acquire_execution_claim=lambda: True,
17081746
translator=translate,
17091747
strategy_symbols=["VOO", "BOXX"],
17101748
strategy_profile="tech_communication_pullback_enhancement",
@@ -1901,6 +1939,7 @@ def fake_submit_order_intent(_ib, intent):
19011939
fetch_quote_snapshots=lambda *_args, **_kwargs: {},
19021940
submit_order_intent=fake_submit_order_intent,
19031941
order_intent_cls=OrderIntent,
1942+
acquire_execution_claim=lambda: True,
19041943
translator=translate,
19051944
strategy_symbols=["VOO"],
19061945
strategy_profile="tech_communication_pullback_enhancement",

0 commit comments

Comments
 (0)