Skip to content

Commit 15ba8c3

Browse files
Pigbibicodex
andcommitted
Add offline E3 receipt validation
Co-Authored-By: Codex <noreply@openai.com>
1 parent 23444b9 commit 15ba8c3

3 files changed

Lines changed: 235 additions & 4 deletions

File tree

runtime_preflight.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,28 @@
44
import os
55
import re
66
from dataclasses import dataclass
7+
from datetime import datetime, timezone
78
from pathlib import Path
89
from typing import Any
910

1011
from runtime_config_support import PlatformRuntimeSettings, load_platform_runtime_settings
1112
from strategy_loader import load_strategy_entrypoint_for_profile
1213

1314

15+
E3_RECEIPT_SCHEMA_VERSION = "qmt.e3.receipt.v1"
16+
E3_RECEIPT_REQUIRED_SUMMARY_COUNTS = ("accounts", "positions", "orders", "fills", "cash", "ledger")
17+
_E3_RECEIPT_FIELDS = frozenset(
18+
{
19+
"schema_version",
20+
"as_of",
21+
"freshness_seconds",
22+
"no_order",
23+
"verify_only",
24+
"summary_counts",
25+
}
26+
)
27+
28+
1429
@dataclass(frozen=True)
1530
class PreflightIssue:
1631
code: str
@@ -42,6 +57,109 @@ def to_payload(self) -> dict[str, Any]:
4257
}
4358

4459

60+
@dataclass(frozen=True)
61+
class E3ReceiptValidationReport:
62+
status: str
63+
schema_version: str | None
64+
issues: tuple[PreflightIssue, ...]
65+
66+
def to_payload(self) -> dict[str, Any]:
67+
return {
68+
"status": self.status,
69+
"schema_version": self.schema_version,
70+
"required_summary_counts": list(E3_RECEIPT_REQUIRED_SUMMARY_COUNTS),
71+
"issues": [
72+
{"code": issue.code, "message": issue.message}
73+
for issue in self.issues
74+
],
75+
}
76+
77+
78+
def validate_e3_receipt(
79+
receipt: object,
80+
*,
81+
now: datetime | None = None,
82+
) -> E3ReceiptValidationReport:
83+
"""Validate a local E3 receipt without connecting to miniQMT or a provider."""
84+
if not isinstance(receipt, dict):
85+
return E3ReceiptValidationReport(
86+
status="error",
87+
schema_version=None,
88+
issues=(PreflightIssue("invalid_receipt", "Receipt must be a JSON object."),),
89+
)
90+
91+
issues: list[PreflightIssue] = []
92+
schema_version = receipt.get("schema_version")
93+
reported_schema_version = schema_version if schema_version == E3_RECEIPT_SCHEMA_VERSION else None
94+
95+
receipt_fields = frozenset(receipt)
96+
if _E3_RECEIPT_FIELDS - receipt_fields:
97+
issues.append(PreflightIssue("missing_receipt_fields", "Receipt has required fields missing."))
98+
if receipt_fields - _E3_RECEIPT_FIELDS:
99+
issues.append(PreflightIssue("unexpected_receipt_fields", "Receipt must not contain detail-bearing fields."))
100+
if schema_version != E3_RECEIPT_SCHEMA_VERSION:
101+
issues.append(PreflightIssue("invalid_schema_version", "Receipt schema version is not accepted."))
102+
103+
_validate_e3_summary_counts(receipt.get("summary_counts"), issues)
104+
_validate_e3_declarations(receipt, issues)
105+
_validate_e3_freshness(receipt, now=now, issues=issues)
106+
107+
return E3ReceiptValidationReport(
108+
status="ok" if not issues else "error",
109+
schema_version=reported_schema_version,
110+
issues=tuple(issues),
111+
)
112+
113+
114+
def _validate_e3_summary_counts(value: object, issues: list[PreflightIssue]) -> None:
115+
if not isinstance(value, dict) or frozenset(value) != frozenset(E3_RECEIPT_REQUIRED_SUMMARY_COUNTS):
116+
issues.append(PreflightIssue("invalid_summary_counts", "Receipt must contain only the required summary counts."))
117+
return
118+
if any(not isinstance(count, int) or isinstance(count, bool) or count < 0 for count in value.values()):
119+
issues.append(PreflightIssue("invalid_summary_counts", "Receipt summary counts must be non-negative integers."))
120+
121+
122+
def _validate_e3_declarations(receipt: dict[object, object], issues: list[PreflightIssue]) -> None:
123+
if receipt.get("no_order") is not True:
124+
issues.append(PreflightIssue("no_order_required", "Receipt must declare no_order=true."))
125+
if receipt.get("verify_only") is not True:
126+
issues.append(PreflightIssue("verify_only_required", "Receipt must declare verify_only=true."))
127+
128+
129+
def _validate_e3_freshness(
130+
receipt: dict[object, object],
131+
*,
132+
now: datetime | None,
133+
issues: list[PreflightIssue],
134+
) -> None:
135+
freshness_seconds = receipt.get("freshness_seconds")
136+
if not isinstance(freshness_seconds, int) or isinstance(freshness_seconds, bool) or freshness_seconds < 0:
137+
issues.append(PreflightIssue("invalid_freshness", "Receipt freshness must be a non-negative integer."))
138+
return
139+
140+
as_of = receipt.get("as_of")
141+
if not isinstance(as_of, str):
142+
issues.append(PreflightIssue("invalid_as_of", "Receipt as_of must be a timezone-aware timestamp."))
143+
return
144+
try:
145+
as_of_time = datetime.fromisoformat(as_of.replace("Z", "+00:00"))
146+
except ValueError:
147+
issues.append(PreflightIssue("invalid_as_of", "Receipt as_of must be a timezone-aware timestamp."))
148+
return
149+
if as_of_time.tzinfo is None:
150+
issues.append(PreflightIssue("invalid_as_of", "Receipt as_of must be a timezone-aware timestamp."))
151+
return
152+
153+
reference_time = now or datetime.now(timezone.utc)
154+
if reference_time.tzinfo is None:
155+
reference_time = reference_time.replace(tzinfo=timezone.utc)
156+
age_seconds = (reference_time - as_of_time).total_seconds()
157+
if age_seconds < 0:
158+
issues.append(PreflightIssue("invalid_as_of", "Receipt as_of cannot be in the future."))
159+
elif age_seconds > freshness_seconds:
160+
issues.append(PreflightIssue("stale_receipt", "Receipt is older than its declared freshness window."))
161+
162+
45163
def run_preflight(*, paper_admission: bool = False) -> QmtPreflightReport:
46164
issues: list[PreflightIssue] = []
47165
try:

scripts/preflight_qmt_runtime.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
if str(ROOT) not in sys.path:
1111
sys.path.insert(0, str(ROOT))
1212

13-
from runtime_preflight import run_preflight # noqa: E402
13+
from runtime_preflight import run_preflight, validate_e3_receipt # noqa: E402
1414

1515

1616
def main(argv: list[str] | None = None) -> int:
@@ -20,11 +20,28 @@ def main(argv: list[str] | None = None) -> int:
2020
action="store_true",
2121
help="Validate the offline paper-admission contract without calling QMT or creating orders.",
2222
)
23+
parser.add_argument(
24+
"--e3-receipt",
25+
type=Path,
26+
help="Validate a local, sanitized E3 receipt without connecting to QMT or creating orders.",
27+
)
2328
args = parser.parse_args(argv)
2429

2530
report = run_preflight(paper_admission=args.paper_admission)
26-
print(json.dumps(report.to_payload(), ensure_ascii=False, indent=2))
27-
return 0 if report.status == "ok" else 2
31+
payload = report.to_payload()
32+
receipt_status = "ok"
33+
if args.e3_receipt:
34+
receipt_status = "error"
35+
try:
36+
receipt = json.loads(args.e3_receipt.read_text(encoding="utf-8"))
37+
except (OSError, json.JSONDecodeError):
38+
receipt = None
39+
receipt_report = validate_e3_receipt(receipt)
40+
payload["e3_receipt"] = receipt_report.to_payload()
41+
receipt_status = receipt_report.status
42+
43+
print(json.dumps(payload, ensure_ascii=False, indent=2))
44+
return 0 if report.status == "ok" and receipt_status == "ok" else 2
2845

2946

3047
if __name__ == "__main__":

tests/test_runtime_preflight.py

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import annotations
22

33
import hashlib
4+
import json
5+
from datetime import datetime, timezone
46
from pathlib import Path
57

68
import pytest
79

8-
from runtime_preflight import run_preflight
10+
from runtime_preflight import E3_RECEIPT_SCHEMA_VERSION, run_preflight, validate_e3_receipt
911
from scripts.preflight_qmt_runtime import main
1012

1113

@@ -17,6 +19,24 @@ def _sha256(path: str) -> str:
1719
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
1820

1921

22+
def _valid_e3_receipt() -> dict[str, object]:
23+
return {
24+
"schema_version": E3_RECEIPT_SCHEMA_VERSION,
25+
"as_of": "2026-09-05T00:00:00+00:00",
26+
"freshness_seconds": 60,
27+
"no_order": True,
28+
"verify_only": True,
29+
"summary_counts": {
30+
"accounts": 1,
31+
"positions": 0,
32+
"orders": 0,
33+
"fills": 0,
34+
"cash": 1,
35+
"ledger": 1,
36+
},
37+
}
38+
39+
2040
def test_preflight_accepts_primary_dry_run_config(monkeypatch, market_history_csv: str):
2141
monkeypatch.setenv("STRATEGY_PROFILE", "cn_industry_etf_rotation")
2242
monkeypatch.setenv("QMT_DRY_RUN_ONLY", "true")
@@ -136,3 +156,79 @@ def test_preflight_rejects_research_only_dividend_profile(monkeypatch):
136156

137157
assert report.status == "error"
138158
assert _issue_codes(report) == {"runtime_config_error"}
159+
160+
161+
def test_e3_receipt_validator_accepts_complete_sanitized_receipt():
162+
report = validate_e3_receipt(
163+
_valid_e3_receipt(),
164+
now=datetime(2026, 9, 5, 0, 0, 30, tzinfo=timezone.utc),
165+
)
166+
167+
assert report.status == "ok"
168+
assert report.schema_version == E3_RECEIPT_SCHEMA_VERSION
169+
assert report.issues == ()
170+
assert report.to_payload() == {
171+
"status": "ok",
172+
"schema_version": E3_RECEIPT_SCHEMA_VERSION,
173+
"required_summary_counts": ["accounts", "positions", "orders", "fills", "cash", "ledger"],
174+
"issues": [],
175+
}
176+
177+
178+
@pytest.mark.parametrize(
179+
("field", "value", "expected_code"),
180+
[
181+
("summary_counts", {"accounts": 1}, "invalid_summary_counts"),
182+
("as_of", "not-a-timestamp", "invalid_as_of"),
183+
("freshness_seconds", -1, "invalid_freshness"),
184+
("no_order", False, "no_order_required"),
185+
("verify_only", False, "verify_only_required"),
186+
],
187+
)
188+
def test_e3_receipt_validator_fails_closed_for_missing_or_unsafe_fields(field, value, expected_code):
189+
receipt = _valid_e3_receipt()
190+
receipt[field] = value
191+
192+
report = validate_e3_receipt(receipt, now=datetime(2026, 9, 5, tzinfo=timezone.utc))
193+
194+
assert report.status == "error"
195+
assert expected_code in _issue_codes(report)
196+
197+
198+
def test_e3_receipt_validator_fails_closed_for_stale_or_detail_bearing_receipts():
199+
stale_report = validate_e3_receipt(
200+
_valid_e3_receipt(),
201+
now=datetime(2026, 9, 5, 0, 6, tzinfo=timezone.utc),
202+
)
203+
detail_receipt = _valid_e3_receipt()
204+
detail_receipt["account"] = {"id": "must-not-be-accepted"}
205+
detail_report = validate_e3_receipt(detail_receipt, now=datetime(2026, 9, 5, tzinfo=timezone.utc))
206+
207+
assert _issue_codes(stale_report) == {"stale_receipt"}
208+
assert _issue_codes(detail_report) == {"unexpected_receipt_fields"}
209+
assert "must-not-be-accepted" not in str(detail_report.to_payload())
210+
211+
212+
def test_e3_receipt_validator_fails_closed_for_missing_required_fields():
213+
receipt = _valid_e3_receipt()
214+
receipt.pop("verify_only")
215+
216+
report = validate_e3_receipt(receipt, now=datetime(2026, 9, 5, tzinfo=timezone.utc))
217+
218+
assert _issue_codes(report) == {"missing_receipt_fields", "verify_only_required"}
219+
220+
221+
def test_preflight_cli_validates_e3_receipt_offline(monkeypatch, market_history_csv: str, tmp_path, capsys):
222+
monkeypatch.setenv("STRATEGY_PROFILE", "cn_industry_etf_rotation")
223+
monkeypatch.setenv("QMT_DRY_RUN_ONLY", "true")
224+
monkeypatch.setenv("QMT_MARKET_HISTORY_PATH", market_history_csv)
225+
monkeypatch.delenv("RUNTIME_TARGET_JSON", raising=False)
226+
receipt_path = tmp_path / "e3-receipt.json"
227+
receipt = _valid_e3_receipt()
228+
receipt["as_of"] = datetime.now(timezone.utc).isoformat()
229+
receipt_path.write_text(json.dumps(receipt), encoding="utf-8")
230+
231+
assert main(["--e3-receipt", str(receipt_path)]) == 0
232+
233+
payload = json.loads(capsys.readouterr().out)
234+
assert payload["e3_receipt"]["status"] == "ok"

0 commit comments

Comments
 (0)