From 28b6fbe24313732b757925b77e03356f4cd3aa52 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:17:59 +0800 Subject: [PATCH 1/2] fix: keep Firstrade submissions pending reconciliation Co-Authored-By: Codex --- application/execution_service.py | 15 ++++++++++- application/rebalance_service.py | 27 +++++++++++++++---- application/strategy_run_persistence.py | 6 +++++ tests/test_execution_service.py | 36 +++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/application/execution_service.py b/application/execution_service.py index 839c913..e691c7e 100644 --- a/application/execution_service.py +++ b/application/execution_service.py @@ -134,6 +134,8 @@ class ExecutionCycleResult: submitted_orders: tuple[dict[str, Any], ...] skipped_orders: tuple[dict[str, Any], ...] action_done: bool + broker_submission_done: bool = False + pending_reconciliation: bool = False execution_notes: tuple[dict[str, Any], ...] = () @@ -939,9 +941,20 @@ def execute_value_target_plan( ) execution_notes = tuple(execution_notes) + tuple(drift_notes) + pending_statuses = {"accepted", "submitted", "partiallyfilled"} + completed_statuses = {"previewed", "filled"} + order_statuses = { + "".join(ch for ch in str(order.get("status") or "").strip().lower() if ch.isalnum()) + for order in submitted + } + pending_reconciliation = bool(order_statuses & pending_statuses) + action_done = bool(submitted) and not pending_reconciliation and order_statuses <= completed_statuses + return ExecutionCycleResult( submitted_orders=tuple(submitted), skipped_orders=tuple(skipped), - action_done=bool(submitted), + action_done=action_done, + broker_submission_done=pending_reconciliation, + pending_reconciliation=pending_reconciliation, execution_notes=execution_notes, ) diff --git a/application/rebalance_service.py b/application/rebalance_service.py index bcd5fc6..dc2f9e2 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -660,11 +660,15 @@ def log_message(message: str) -> None: execution_blocked = bool(blocking_skips) funding_blocked = is_terminal_funding_block(blocking_skips) terminal_funding_block = funding_blocked and not execution_result.action_done - strategy_run_stage = resolve_strategy_run_stage( - dry_run_only=settings.dry_run_only, - execution_blocked=execution_blocked, - terminal_funding_block=terminal_funding_block, - action_done=execution_result.action_done, + strategy_run_stage = ( + "PENDING_RECONCILIATION" + if execution_result.pending_reconciliation + else resolve_strategy_run_stage( + dry_run_only=settings.dry_run_only, + execution_blocked=execution_blocked, + terminal_funding_block=terminal_funding_block, + action_done=execution_result.action_done, + ) ) signal_snapshot = build_signal_snapshot( platform="firstrade", @@ -698,6 +702,13 @@ def log_message(message: str) -> None: "skipped_orders": skipped_orders, "execution_notes": execution_notes, "action_done": execution_result.action_done, + "broker_submission_done": execution_result.broker_submission_done, + "execution_status": ( + "pending_reconciliation" if execution_result.pending_reconciliation else "" + ), + "orders_pending_count": ( + len(submitted_orders) if execution_result.pending_reconciliation else 0 + ), } if execution_blocked: result["execution_blocked"] = True @@ -737,6 +748,9 @@ def log_message(message: str) -> None: skipped_orders=list(execution_result.skipped_orders), execution_notes=list(execution_result.execution_notes), action_done=execution_result.action_done, + broker_submission_done=execution_result.broker_submission_done, + execution_status=result["execution_status"], + orders_pending_count=result["orders_pending_count"], now=now, ) try: @@ -772,6 +786,9 @@ def log_message(message: str) -> None: { "platform": "firstrade", "action_done": result.get("action_done"), + "broker_submission_done": result.get("broker_submission_done"), + "execution_status": result.get("execution_status"), + "orders_pending_count": result.get("orders_pending_count"), "strategy_run_stage": result.get("strategy_run_stage"), "dry_run_only": settings.dry_run_only, "submitted_orders": result.get("submitted_orders"), diff --git a/application/strategy_run_persistence.py b/application/strategy_run_persistence.py index 8066d69..5bbbf91 100644 --- a/application/strategy_run_persistence.py +++ b/application/strategy_run_persistence.py @@ -191,6 +191,9 @@ def build_strategy_run_state( skipped_orders: list[dict[str, Any]] | tuple[dict[str, Any], ...] = (), execution_notes: list[dict[str, Any]] | tuple[dict[str, Any], ...] = (), action_done: bool = False, + broker_submission_done: bool = False, + execution_status: str = "", + orders_pending_count: int = 0, error: str | None = None, now: datetime | None = None, ) -> dict[str, Any]: @@ -212,6 +215,9 @@ def build_strategy_run_state( "skipped_orders": list(skipped_orders), "execution_notes": list(execution_notes), "action_done": action_done, + "broker_submission_done": broker_submission_done, + "execution_status": str(execution_status or ""), + "orders_pending_count": max(0, int(orders_pending_count or 0)), } if error: payload["error"] = error diff --git a/tests/test_execution_service.py b/tests/test_execution_service.py index 975e0b8..51869fd 100644 --- a/tests/test_execution_service.py +++ b/tests/test_execution_service.py @@ -43,6 +43,19 @@ def submit_order(self, order_intent) -> ExecutionReport: ) +class SubmittedExecutionPort(FakeExecutionPort): + def submit_order(self, order_intent) -> ExecutionReport: + self.orders.append(order_intent) + return ExecutionReport( + symbol=order_intent.symbol, + side=order_intent.side, + quantity=order_intent.quantity, + status="submitted", + broker_order_id=f"OID-{len(self.orders)}", + raw_payload={}, + ) + + def test_execute_value_target_plan_sells_before_buys_and_caps_order_notional(): execution_port = FakeExecutionPort() result = execute_value_target_plan( @@ -71,6 +84,29 @@ def test_execute_value_target_plan_sells_before_buys_and_caps_order_notional(): assert all(order.metadata["max_notional_usd"] == 25.0 for order in execution_port.orders) +def test_execute_value_target_plan_marks_live_submissions_pending_reconciliation(): + execution_port = SubmittedExecutionPort() + result = execute_value_target_plan( + plan={ + "allocation": {"targets": {"AAA": 20.0}}, + "portfolio": { + "market_values": {"AAA": 0.0}, + "sellable_quantities": {"AAA": 0.0}, + "liquid_cash": 100.0, + }, + "execution": {"current_min_trade": 5.0, "investable_cash": 100.0}, + }, + market_data_port=FakeMarketDataPort({"AAA": 10.0}), + execution_port=execution_port, + dry_run_only=False, + ) + + assert result.action_done is False + assert result.broker_submission_done is True + assert result.pending_reconciliation is True + assert len(result.submitted_orders) == 1 + + def test_execute_value_target_plan_uses_sellable_quantity_when_market_value_is_stale_below_quote(): execution_port = FakeExecutionPort() result = execute_value_target_plan( From c2ef809da461a8a3930886f56ed8e14e7871f26a Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Mon, 24 Aug 2026 15:19:49 +0800 Subject: [PATCH 2/2] test: cover Firstrade pending reconciliation state Co-Authored-By: Codex --- tests/test_rebalance_service.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index 18b3182..550a2e9 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -903,11 +903,15 @@ def get_quote(self, _account, symbol): ) latest_payload = store.writes[-2][1] - assert result["action_done"] is True + assert result["action_done"] is False + assert result["broker_submission_done"] is True + assert result["execution_status"] == "pending_reconciliation" + assert result["orders_pending_count"] == 1 assert result["ok"] is False assert result["execution_blocked"] is True - assert result["strategy_run_stage"] == "PARTIAL_SUBMITTED" - assert latest_payload["stage"] == "PARTIAL_SUBMITTED" + assert result["strategy_run_stage"] == "PENDING_RECONCILIATION" + assert latest_payload["stage"] == "PENDING_RECONCILIATION" + assert latest_payload["broker_submission_done"] is True def test_render_cycle_summary_formats_skipped_orders_in_unified_chinese_template():