Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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], ...] = ()


Expand Down Expand Up @@ -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,
)
27 changes: 22 additions & 5 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"),
Expand Down
6 changes: 6 additions & 0 deletions application/strategy_run_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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
Expand Down
36 changes: 36 additions & 0 deletions tests/test_execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
10 changes: 7 additions & 3 deletions tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down