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
2 changes: 1 addition & 1 deletion .github/workflows/execution-report-heartbeat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
run: python scripts/execution_report_heartbeat.py

- name: Publish read-only runtime execution evidence
uses: QuantStrategyLab/QuantRuntimeSettings/actions/publish-runtime-execution-evidence@361341c23dd5cd638651d1766411efd98e1885fc
uses: QuantStrategyLab/QuantRuntimeSettings/actions/publish-runtime-execution-evidence@813ba38c4adb759e143503c8e6b70715090391cc
with:
source-id: runtime-reports-ibkr
report-platform: interactive_brokers
Expand Down
78 changes: 78 additions & 0 deletions application/execution_receipt_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Bounded execution-receipt facts derived from IBKR reconciliation results."""

from __future__ import annotations

from collections.abc import Mapping
from typing import Any

from quant_platform_kit.common.execution_receipts import (
attach_runtime_execution_receipt,
resolve_execution_receipt_fact,
)


_SUBMITTED_KEYS = ("orders_submitted", "option_orders_submitted")
_PENDING_KEYS = ("orders_pending", "option_orders_pending")
_PARTIAL_FILL_KEYS = ("orders_partially_filled", "option_orders_partially_filled")
_FILLED_KEYS = ("orders_filled", "option_orders_filled")
_FAILURE_STATUSES = frozenset({"error", "failed", "failure"})


def attach_cycle_execution_receipt(
report: dict[str, Any],
execution_summary: Mapping[str, Any],
reconciliation_record: Mapping[str, Any],
*,
execution_failed: bool,
) -> dict[str, Any]:
"""Attach the strongest fact supplied by IBKR's own reconciliation data.

Submission arrays are intentionally only submission evidence. A fill is
emitted solely when the reconciliation result contains its explicit filled
array; no status label or local marker is promoted to a fill.
"""

summary = _combined_summary(execution_summary, reconciliation_record)
status = str(summary.get("execution_status") or "").strip().lower()
reconciliation_required = status == "pending_reconciliation" or _has_any(summary, _PENDING_KEYS)
outcome, confirmation = resolve_execution_receipt_fact(
dry_run=bool(report.get("dry_run")),
submission_attempted=_has_any(summary, _SUBMITTED_KEYS),
partially_filled=_has_any(summary, _PARTIAL_FILL_KEYS),
filled=_has_any(summary, _FILLED_KEYS),
reconciliation_required=reconciliation_required,
risk_blocked=status == "blocked" and not execution_failed,
failed=execution_failed or status in _FAILURE_STATUSES,
)
return attach_runtime_execution_receipt(
report,
outcome=outcome,
broker_confirmation=confirmation,
)


def attach_terminal_fallback_execution_receipt(report: dict[str, Any]) -> dict[str, Any]:
"""Attach a no-action or uncertainty fact for a report that exited early."""

failed = str(report.get("status") or "").strip().lower() == "error"
outcome, confirmation = resolve_execution_receipt_fact(
dry_run=bool(report.get("dry_run")),
submission_attempted=failed,
failed=failed,
)
return attach_runtime_execution_receipt(
report,
outcome=outcome,
broker_confirmation=confirmation,
)


def _combined_summary(
execution_summary: Mapping[str, Any],
reconciliation_record: Mapping[str, Any],
) -> dict[str, Any]:
return {**dict(reconciliation_record or {}), **dict(execution_summary or {})}


def _has_any(summary: Mapping[str, Any], keys: tuple[str, ...]) -> bool:
return any(bool(tuple(summary.get(key) or ())) for key in keys)
17 changes: 17 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
get_compute_discovery = None

from application.cycle_result import coerce_strategy_cycle_result
from application.execution_receipt_adapter import (
attach_cycle_execution_receipt,
attach_terminal_fallback_execution_receipt,
)
from application.broker_reconciliation import (
IBKRReconciliationReadError,
build_reconciliation_candidate,
Expand Down Expand Up @@ -1594,6 +1598,12 @@ def _handle_request(
}
if execution_failed:
report_diagnostics["failure_category"] = "strategy_execution_blocked"
attach_cycle_execution_receipt(
report,
execution_summary,
reconciliation_record,
execution_failed=execution_failed,
)
finalize_runtime_report(
report,
status="error" if execution_failed else "ok",
Expand Down Expand Up @@ -1726,6 +1736,13 @@ def _handle_request(
finally:
if lock_acquired:
STRATEGY_RUN_LOCK.release()
if "execution_receipt" not in report:
try:
attach_terminal_fallback_execution_receipt(report)
except ValueError:
# A legacy runtime target remains visible as evidence-missing;
# reporting metadata must never change trading behavior.
pass
if not deadline_exceeded and persist_report:
try:
if dry_run_only_override is None:
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies = [
"google-cloud-secret-manager",
"google-cloud-storage",
"yfinance",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@25bc80681f09e5ae1316ebafc3adb31307ed1399",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b567acd52a6afa6caca3e35648d4347a1ce13ab1",
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4584de176ca27955270471284663a13a2ce7f828",
"hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@c526f2ed869c59f37a494d5fd3a64be299855ec8",
]
Expand Down Expand Up @@ -64,5 +64,5 @@ include = [

[tool.uv]
override-dependencies = [
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@25bc80681f09e5ae1316ebafc3adb31307ed1399",
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b567acd52a6afa6caca3e35648d4347a1ce13ab1",
]
2 changes: 1 addition & 1 deletion qsl.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ upgrade_ring = "ring_d"
allow_legacy = false

[qsl.requires]
quant_platform_kit = "ee8d996392f96e8bdf40988bd68ae30bf5911d2d"
quant_platform_kit = "b567acd52a6afa6caca3e35648d4347a1ce13ab1"
us_equity_strategies = "4584de176ca27955270471284663a13a2ce7f828"
hk_equity_strategies = "c526f2ed869c59f37a494d5fd3a64be299855ec8"

Expand Down
72 changes: 72 additions & 0 deletions tests/test_execution_receipt_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from __future__ import annotations

import unittest

from application.execution_receipt_adapter import attach_cycle_execution_receipt


REVISION = "a" * 40


def _report() -> dict[str, object]:
return {
"platform": "interactive_brokers",
"strategy_profile": "soxl_soxx_trend_income",
"dry_run": False,
"runtime_target": {"execution_mode": "live"},
"runtime_release_receipt": {
"attestation_state": "self_attested",
"strategy_release": {"strategy_revision": REVISION},
},
}


class ExecutionReceiptAdapterTest(unittest.TestCase):
def test_explicit_fill_is_the_only_fill_claim(self) -> None:
report = _report()

attach_cycle_execution_receipt(
report,
{"orders_filled": [{"symbol": "SOXL"}]},
{},
execution_failed=False,
)

self.assertEqual(report["execution_receipt"]["outcome"], "filled")

def test_submission_is_not_reported_as_acknowledged_or_filled(self) -> None:
report = _report()

attach_cycle_execution_receipt(
report,
{"orders_submitted": [{"symbol": "SOXL"}]},
{},
execution_failed=False,
)

self.assertEqual(report["execution_receipt"]["outcome"], "submitted")
self.assertEqual(report["execution_receipt"]["broker_confirmation"], "not_observed")

def test_pending_order_requires_reconciliation(self) -> None:
report = _report()

attach_cycle_execution_receipt(
report,
{"orders_pending": [{"symbol": "SOXL"}]},
{},
execution_failed=False,
)

self.assertEqual(report["execution_receipt"]["outcome"], "reconciliation_required")

def test_expected_block_is_risk_blocked(self) -> None:
report = _report()

attach_cycle_execution_receipt(
report,
{"execution_status": "blocked"},
{},
execution_failed=False,
)

self.assertEqual(report["execution_receipt"]["outcome"], "risk_blocked")
6 changes: 3 additions & 3 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.