Skip to content

Commit b8ca54a

Browse files
authored
Merge pull request #285 from QuantStrategyLab/codex/execution-receipt-adapter
feat: publish bounded Firstrade execution receipts
2 parents 2d8b219 + 6bdcf6b commit b8ca54a

7 files changed

Lines changed: 169 additions & 7 deletions

File tree

.github/workflows/execution-report-heartbeat.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ jobs:
9898
run: python scripts/execution_report_heartbeat.py
9999

100100
- name: Publish read-only runtime execution evidence
101-
uses: QuantStrategyLab/QuantRuntimeSettings/actions/publish-runtime-execution-evidence@f28bee6a4e28e2aead5fb37bd3213c12dd419e29
101+
uses: QuantStrategyLab/QuantRuntimeSettings/actions/publish-runtime-execution-evidence@813ba38c4adb759e143503c8e6b70715090391cc
102102
with:
103103
source-id: runtime-reports-firstrade
104104
report-platform: firstrade
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""Bounded execution-receipt facts derived from Firstrade cycle results."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Mapping, Sequence
6+
from typing import Any
7+
8+
from quant_platform_kit.common.execution_receipts import (
9+
attach_runtime_execution_receipt,
10+
resolve_execution_receipt_fact,
11+
)
12+
13+
14+
_RECONCILIATION_STAGES = frozenset({"PENDING_RECONCILIATION", "PENDING_SUBMISSION"})
15+
_RISK_BLOCKED_STAGES = frozenset({"EXECUTION_BLOCKED", "FUNDING_BLOCKED"})
16+
17+
18+
def attach_strategy_result_execution_receipt(
19+
report: dict[str, Any],
20+
result: Mapping[str, Any],
21+
*,
22+
dry_run: bool,
23+
) -> dict[str, Any]:
24+
"""Attach only the highest broker fact already present in a cycle result.
25+
26+
A locally recorded submission is deliberately reported as ``submitted``;
27+
Firstrade's cycle result has no fill-confirmation field, so this adapter
28+
never infers an acknowledgement or fill from it.
29+
"""
30+
31+
stage = str(result.get("strategy_run_stage") or "").strip().upper()
32+
submitted_orders = _as_sequence(result.get("submitted_orders"))
33+
submission_attempted = bool(
34+
result.get("action_done")
35+
or result.get("broker_submission_done")
36+
or submitted_orders
37+
)
38+
reconciliation_required = stage in _RECONCILIATION_STAGES or bool(
39+
result.get("execution_status") == "pending_reconciliation"
40+
or result.get("orders_pending_count")
41+
)
42+
risk_blocked = not submission_attempted and (
43+
stage in _RISK_BLOCKED_STAGES
44+
or bool(result.get("execution_blocked"))
45+
or bool(result.get("funding_blocked"))
46+
)
47+
failed = (
48+
result.get("ok", True) is False
49+
and not submission_attempted
50+
and not reconciliation_required
51+
and not risk_blocked
52+
)
53+
outcome, confirmation = resolve_execution_receipt_fact(
54+
dry_run=dry_run,
55+
submission_attempted=submission_attempted,
56+
reconciliation_required=reconciliation_required,
57+
risk_blocked=risk_blocked,
58+
failed=failed,
59+
)
60+
return attach_runtime_execution_receipt(
61+
report,
62+
outcome=outcome,
63+
broker_confirmation=confirmation,
64+
)
65+
66+
67+
def attach_unknown_failure_execution_receipt(report: dict[str, Any]) -> dict[str, Any]:
68+
"""Preserve uncertainty when an exception escapes the strategy cycle."""
69+
70+
outcome, confirmation = resolve_execution_receipt_fact(
71+
dry_run=bool(report.get("dry_run")),
72+
submission_attempted=True,
73+
failed=True,
74+
)
75+
return attach_runtime_execution_receipt(
76+
report,
77+
outcome=outcome,
78+
broker_confirmation=confirmation,
79+
)
80+
81+
82+
def _as_sequence(value: object) -> Sequence[object]:
83+
return value if isinstance(value, (list, tuple)) else ()

main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
resolve_paper_execution_command_consumer_enabled,
3333
)
3434
from application.rebalance_service import run_strategy_cycle
35+
from application.execution_receipt_adapter import (
36+
attach_strategy_result_execution_receipt,
37+
attach_unknown_failure_execution_receipt,
38+
)
3539
from application.runtime_broker_adapters import build_runtime_broker_adapters
3640
from application.session_check_service import run_session_check
3741
from notifications.telegram import build_sender
@@ -367,6 +371,7 @@ def _run_strategy_cycle_with_report(
367371
send_cycle_notification=send_cycle_notification,
368372
dispatch_plugin_alerts=dispatch_plugin_alerts,
369373
)
374+
attach_strategy_result_execution_receipt(report, result, dry_run=dry_run)
370375
finalize_runtime_report(
371376
report,
372377
status="ok" if result.get("ok", True) else "error",
@@ -387,6 +392,7 @@ def _run_strategy_cycle_with_report(
387392
message=str(exc),
388393
error_type=type(exc).__name__,
389394
)
395+
attach_unknown_failure_execution_receipt(report)
390396
finalize_runtime_report(report, status="error")
391397
try:
392398
report_path = _persist_runtime_report(report)

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies = [
1818
"pytest",
1919
"pytz",
2020
"requests",
21-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ee8d996392f96e8bdf40988bd68ae30bf5911d2d",
21+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b567acd52a6afa6caca3e35648d4347a1ce13ab1",
2222
"us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4584de176ca27955270471284663a13a2ce7f828",
2323
]
2424
license = "MIT"
@@ -82,5 +82,5 @@ show_missing = true
8282

8383
[tool.uv]
8484
override-dependencies = [
85-
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ee8d996392f96e8bdf40988bd68ae30bf5911d2d",
85+
"quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b567acd52a6afa6caca3e35648d4347a1ce13ab1",
8686
]

qsl.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ upgrade_ring = "ring_d"
55
allow_legacy = false
66

77
[qsl.requires]
8-
quant_platform_kit = "ee8d996392f96e8bdf40988bd68ae30bf5911d2d"
8+
quant_platform_kit = "b567acd52a6afa6caca3e35648d4347a1ce13ab1"
99
us_equity_strategies = "4584de176ca27955270471284663a13a2ce7f828"
1010

1111
[qsl.compat]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from __future__ import annotations
2+
3+
import unittest
4+
5+
from application.execution_receipt_adapter import attach_strategy_result_execution_receipt
6+
7+
8+
REVISION = "a" * 40
9+
10+
11+
def _report() -> dict[str, object]:
12+
return {
13+
"platform": "firstrade",
14+
"strategy_profile": "ibit_smart_dca",
15+
"dry_run": False,
16+
"runtime_target": {"execution_mode": "live"},
17+
"runtime_release_receipt": {
18+
"attestation_state": "self_attested",
19+
"strategy_release": {"strategy_revision": REVISION},
20+
},
21+
}
22+
23+
24+
class ExecutionReceiptAdapterTest(unittest.TestCase):
25+
def test_submission_is_not_reported_as_a_fill(self) -> None:
26+
report = _report()
27+
28+
attach_strategy_result_execution_receipt(
29+
report,
30+
{
31+
"strategy_run_stage": "SUBMITTED",
32+
"action_done": True,
33+
"submitted_orders": [{"symbol": "IBIT"}],
34+
},
35+
dry_run=False,
36+
)
37+
38+
self.assertEqual(report["execution_receipt"]["outcome"], "submitted")
39+
self.assertEqual(report["execution_receipt"]["broker_confirmation"], "not_observed")
40+
41+
def test_pending_prior_submission_requires_reconciliation(self) -> None:
42+
report = _report()
43+
44+
attach_strategy_result_execution_receipt(
45+
report,
46+
{"strategy_run_stage": "PENDING_RECONCILIATION"},
47+
dry_run=False,
48+
)
49+
50+
self.assertEqual(report["execution_receipt"]["outcome"], "reconciliation_required")
51+
52+
def test_funding_block_is_not_a_broker_failure(self) -> None:
53+
report = _report()
54+
55+
attach_strategy_result_execution_receipt(
56+
report,
57+
{"strategy_run_stage": "FUNDING_BLOCKED", "funding_blocked": True},
58+
dry_run=False,
59+
)
60+
61+
self.assertEqual(report["execution_receipt"]["outcome"], "risk_blocked")
62+
63+
def test_dry_run_never_claims_submission(self) -> None:
64+
report = _report()
65+
report["dry_run"] = True
66+
67+
attach_strategy_result_execution_receipt(
68+
report,
69+
{"strategy_run_stage": "SUBMITTED", "action_done": True},
70+
dry_run=True,
71+
)
72+
73+
self.assertEqual(report["execution_receipt"]["outcome"], "no_action")

uv.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)