Skip to content

Commit 28b6fbe

Browse files
Pigbibicodex
andcommitted
fix: keep Firstrade submissions pending reconciliation
Co-Authored-By: Codex <noreply@openai.com>
1 parent a2851b4 commit 28b6fbe

4 files changed

Lines changed: 78 additions & 6 deletions

File tree

application/execution_service.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ class ExecutionCycleResult:
134134
submitted_orders: tuple[dict[str, Any], ...]
135135
skipped_orders: tuple[dict[str, Any], ...]
136136
action_done: bool
137+
broker_submission_done: bool = False
138+
pending_reconciliation: bool = False
137139
execution_notes: tuple[dict[str, Any], ...] = ()
138140

139141

@@ -939,9 +941,20 @@ def execute_value_target_plan(
939941
)
940942
execution_notes = tuple(execution_notes) + tuple(drift_notes)
941943

944+
pending_statuses = {"accepted", "submitted", "partiallyfilled"}
945+
completed_statuses = {"previewed", "filled"}
946+
order_statuses = {
947+
"".join(ch for ch in str(order.get("status") or "").strip().lower() if ch.isalnum())
948+
for order in submitted
949+
}
950+
pending_reconciliation = bool(order_statuses & pending_statuses)
951+
action_done = bool(submitted) and not pending_reconciliation and order_statuses <= completed_statuses
952+
942953
return ExecutionCycleResult(
943954
submitted_orders=tuple(submitted),
944955
skipped_orders=tuple(skipped),
945-
action_done=bool(submitted),
956+
action_done=action_done,
957+
broker_submission_done=pending_reconciliation,
958+
pending_reconciliation=pending_reconciliation,
946959
execution_notes=execution_notes,
947960
)

application/rebalance_service.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -660,11 +660,15 @@ def log_message(message: str) -> None:
660660
execution_blocked = bool(blocking_skips)
661661
funding_blocked = is_terminal_funding_block(blocking_skips)
662662
terminal_funding_block = funding_blocked and not execution_result.action_done
663-
strategy_run_stage = resolve_strategy_run_stage(
664-
dry_run_only=settings.dry_run_only,
665-
execution_blocked=execution_blocked,
666-
terminal_funding_block=terminal_funding_block,
667-
action_done=execution_result.action_done,
663+
strategy_run_stage = (
664+
"PENDING_RECONCILIATION"
665+
if execution_result.pending_reconciliation
666+
else resolve_strategy_run_stage(
667+
dry_run_only=settings.dry_run_only,
668+
execution_blocked=execution_blocked,
669+
terminal_funding_block=terminal_funding_block,
670+
action_done=execution_result.action_done,
671+
)
668672
)
669673
signal_snapshot = build_signal_snapshot(
670674
platform="firstrade",
@@ -698,6 +702,13 @@ def log_message(message: str) -> None:
698702
"skipped_orders": skipped_orders,
699703
"execution_notes": execution_notes,
700704
"action_done": execution_result.action_done,
705+
"broker_submission_done": execution_result.broker_submission_done,
706+
"execution_status": (
707+
"pending_reconciliation" if execution_result.pending_reconciliation else ""
708+
),
709+
"orders_pending_count": (
710+
len(submitted_orders) if execution_result.pending_reconciliation else 0
711+
),
701712
}
702713
if execution_blocked:
703714
result["execution_blocked"] = True
@@ -737,6 +748,9 @@ def log_message(message: str) -> None:
737748
skipped_orders=list(execution_result.skipped_orders),
738749
execution_notes=list(execution_result.execution_notes),
739750
action_done=execution_result.action_done,
751+
broker_submission_done=execution_result.broker_submission_done,
752+
execution_status=result["execution_status"],
753+
orders_pending_count=result["orders_pending_count"],
740754
now=now,
741755
)
742756
try:
@@ -772,6 +786,9 @@ def log_message(message: str) -> None:
772786
{
773787
"platform": "firstrade",
774788
"action_done": result.get("action_done"),
789+
"broker_submission_done": result.get("broker_submission_done"),
790+
"execution_status": result.get("execution_status"),
791+
"orders_pending_count": result.get("orders_pending_count"),
775792
"strategy_run_stage": result.get("strategy_run_stage"),
776793
"dry_run_only": settings.dry_run_only,
777794
"submitted_orders": result.get("submitted_orders"),

application/strategy_run_persistence.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ def build_strategy_run_state(
191191
skipped_orders: list[dict[str, Any]] | tuple[dict[str, Any], ...] = (),
192192
execution_notes: list[dict[str, Any]] | tuple[dict[str, Any], ...] = (),
193193
action_done: bool = False,
194+
broker_submission_done: bool = False,
195+
execution_status: str = "",
196+
orders_pending_count: int = 0,
194197
error: str | None = None,
195198
now: datetime | None = None,
196199
) -> dict[str, Any]:
@@ -212,6 +215,9 @@ def build_strategy_run_state(
212215
"skipped_orders": list(skipped_orders),
213216
"execution_notes": list(execution_notes),
214217
"action_done": action_done,
218+
"broker_submission_done": broker_submission_done,
219+
"execution_status": str(execution_status or ""),
220+
"orders_pending_count": max(0, int(orders_pending_count or 0)),
215221
}
216222
if error:
217223
payload["error"] = error

tests/test_execution_service.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,19 @@ def submit_order(self, order_intent) -> ExecutionReport:
4343
)
4444

4545

46+
class SubmittedExecutionPort(FakeExecutionPort):
47+
def submit_order(self, order_intent) -> ExecutionReport:
48+
self.orders.append(order_intent)
49+
return ExecutionReport(
50+
symbol=order_intent.symbol,
51+
side=order_intent.side,
52+
quantity=order_intent.quantity,
53+
status="submitted",
54+
broker_order_id=f"OID-{len(self.orders)}",
55+
raw_payload={},
56+
)
57+
58+
4659
def test_execute_value_target_plan_sells_before_buys_and_caps_order_notional():
4760
execution_port = FakeExecutionPort()
4861
result = execute_value_target_plan(
@@ -71,6 +84,29 @@ def test_execute_value_target_plan_sells_before_buys_and_caps_order_notional():
7184
assert all(order.metadata["max_notional_usd"] == 25.0 for order in execution_port.orders)
7285

7386

87+
def test_execute_value_target_plan_marks_live_submissions_pending_reconciliation():
88+
execution_port = SubmittedExecutionPort()
89+
result = execute_value_target_plan(
90+
plan={
91+
"allocation": {"targets": {"AAA": 20.0}},
92+
"portfolio": {
93+
"market_values": {"AAA": 0.0},
94+
"sellable_quantities": {"AAA": 0.0},
95+
"liquid_cash": 100.0,
96+
},
97+
"execution": {"current_min_trade": 5.0, "investable_cash": 100.0},
98+
},
99+
market_data_port=FakeMarketDataPort({"AAA": 10.0}),
100+
execution_port=execution_port,
101+
dry_run_only=False,
102+
)
103+
104+
assert result.action_done is False
105+
assert result.broker_submission_done is True
106+
assert result.pending_reconciliation is True
107+
assert len(result.submitted_orders) == 1
108+
109+
74110
def test_execute_value_target_plan_uses_sellable_quantity_when_market_value_is_stale_below_quote():
75111
execution_port = FakeExecutionPort()
76112
result = execute_value_target_plan(

0 commit comments

Comments
 (0)