Skip to content

Commit bc5351d

Browse files
authored
Merge pull request #111 from QuantStrategyLab/feat/global-audit-remediation
feat: safe dry-run defaults and shared execution marker store
2 parents aee8121 + 1274ae4 commit bc5351d

5 files changed

Lines changed: 495 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quant-platform-kit"
7-
version = "0.7.38"
7+
version = "0.7.39"
88
description = "Shared broker adapters, domain models, execution ports, and notification utilities for QuantStrategyLab strategies."
99
readme = "README.md"
1010
requires-python = ">=3.9"
Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
"""Execution marker storage for duplicate-run suppression across trading platforms."""
2+
3+
from __future__ import annotations
4+
5+
import json
6+
import re
7+
import tempfile
8+
from collections.abc import Callable, Mapping
9+
from dataclasses import dataclass
10+
from datetime import datetime, timezone
11+
from pathlib import Path
12+
from typing import Any
13+
14+
15+
DEFAULT_EXECUTION_STATE_DIR = "/tmp/quant_execution_state"
16+
DEFAULT_EXECUTION_STATE_NAMESPACE = "execution_markers"
17+
18+
19+
def _first_non_empty(*values: object) -> str:
20+
for value in values:
21+
text = str(value or "").strip()
22+
if text:
23+
return text
24+
return ""
25+
26+
27+
def _env_bool(value: object, *, default: bool) -> bool:
28+
text = str(value or "").strip().lower()
29+
if not text:
30+
return default
31+
if text in {"1", "true", "t", "yes", "y", "on"}:
32+
return True
33+
if text in {"0", "false", "f", "no", "n", "off"}:
34+
return False
35+
return default
36+
37+
38+
def _parse_gcs_uri(uri: str) -> tuple[str, str]:
39+
text = str(uri or "").strip()
40+
if not text.startswith("gs://"):
41+
raise ValueError(f"gcs uri must start with gs://, got: {uri!r}")
42+
remainder = text[5:]
43+
bucket, _, prefix = remainder.partition("/")
44+
if not bucket:
45+
raise ValueError(f"gcs uri must include a bucket, got: {uri!r}")
46+
return bucket, prefix.strip("/")
47+
48+
49+
def _clean_key_part(value: object, *, fallback: str) -> str:
50+
text = str(value or "").strip().lower()
51+
text = re.sub(r"[^a-z0-9._=-]+", "-", text)
52+
text = re.sub(r"-{2,}", "-", text).strip("-.")
53+
return text or fallback
54+
55+
56+
def _clean_relative_key(key: str) -> str:
57+
parts = [
58+
_clean_key_part(part, fallback="unknown")
59+
for part in str(key or "").replace("\\", "/").split("/")
60+
if str(part or "").strip()
61+
]
62+
return "/".join(parts) or "unknown"
63+
64+
65+
def build_execution_marker_key(
66+
*,
67+
platform: str,
68+
strategy_profile: str,
69+
account_scope: str,
70+
execution_mode: str,
71+
signal_date: object,
72+
effective_date: object,
73+
execution_timing_contract: object = None,
74+
) -> str:
75+
"""Build a stable marker key for one strategy signal execution."""
76+
signal = _first_non_empty(signal_date)
77+
effective = _first_non_empty(effective_date)
78+
if not signal and not effective:
79+
return ""
80+
return "/".join(
81+
(
82+
"v1",
83+
_clean_key_part(platform, fallback="platform"),
84+
_clean_key_part(account_scope, fallback="account"),
85+
_clean_key_part(strategy_profile, fallback="strategy"),
86+
_clean_key_part(execution_mode, fallback="mode"),
87+
_clean_key_part(signal or "no-signal-date", fallback="signal"),
88+
_clean_key_part(effective or "no-effective-date", fallback="effective"),
89+
_clean_key_part(execution_timing_contract or "no-contract", fallback="contract"),
90+
)
91+
)
92+
93+
94+
@dataclass(frozen=True)
95+
class ExecutionMarkerStore:
96+
local_dir: str | Path | None = DEFAULT_EXECUTION_STATE_DIR
97+
gcs_prefix_uri: str | None = None
98+
gcp_project_id: str | None = None
99+
namespace: str = DEFAULT_EXECUTION_STATE_NAMESPACE
100+
client_factory: Any = None
101+
prior_report_scan_limit: int = 100
102+
103+
def has_marker(self, marker_key: str) -> bool:
104+
if not str(marker_key or "").strip():
105+
return False
106+
if self.gcs_prefix_uri and self._gcs_blob(marker_key).exists():
107+
return True
108+
if self.local_dir and self._local_path(marker_key).exists():
109+
return True
110+
return False
111+
112+
def record_marker(
113+
self,
114+
marker_key: str,
115+
*,
116+
metadata: Mapping[str, Any] | None = None,
117+
) -> None:
118+
if not str(marker_key or "").strip():
119+
return
120+
payload = {
121+
"schema_version": "execution_marker.v1",
122+
"marker_key": str(marker_key),
123+
"recorded_at": datetime.now(timezone.utc).isoformat(),
124+
"metadata": dict(metadata or {}),
125+
}
126+
encoded = json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True)
127+
if self.gcs_prefix_uri:
128+
self._gcs_blob(marker_key).upload_from_string(
129+
encoded,
130+
content_type="application/json",
131+
)
132+
return
133+
if self.local_dir:
134+
path = self._local_path(marker_key)
135+
path.parent.mkdir(parents=True, exist_ok=True)
136+
path.write_text(encoded, encoding="utf-8")
137+
138+
def has_prior_execution_report(
139+
self,
140+
*,
141+
platform: str,
142+
strategy_profile: str,
143+
account_scope: str,
144+
signal_date: object,
145+
effective_date: object,
146+
dry_run_only: bool,
147+
) -> bool:
148+
if not self.gcs_prefix_uri:
149+
return False
150+
signal = _first_non_empty(signal_date)
151+
effective = _first_non_empty(effective_date)
152+
if not signal and not effective:
153+
return False
154+
month_segment = _month_segment(signal, effective)
155+
bucket_name, prefix = _parse_gcs_uri(str(self.gcs_prefix_uri or ""))
156+
object_prefix = "/".join(
157+
part.strip("/")
158+
for part in (
159+
prefix,
160+
_runtime_report_segment(platform),
161+
_runtime_report_segment(strategy_profile),
162+
_runtime_report_segment(account_scope),
163+
month_segment,
164+
)
165+
if part and part.strip("/")
166+
)
167+
client = self._gcs_client()
168+
scanned = 0
169+
for blob in client.list_blobs(bucket_name, prefix=object_prefix):
170+
name = str(getattr(blob, "name", "") or "")
171+
if not name.endswith(".json"):
172+
continue
173+
scanned += 1
174+
if scanned > max(1, int(self.prior_report_scan_limit or 1)):
175+
break
176+
try:
177+
payload = json.loads(blob.download_as_text())
178+
except Exception:
179+
continue
180+
if _report_matches_execution(
181+
payload,
182+
platform=platform,
183+
strategy_profile=strategy_profile,
184+
account_scope=account_scope,
185+
signal_date=signal,
186+
effective_date=effective,
187+
dry_run_only=dry_run_only,
188+
):
189+
return True
190+
return False
191+
192+
def _local_path(self, marker_key: str) -> Path:
193+
root = Path(self.local_dir or tempfile.gettempdir()).expanduser()
194+
return root / self.namespace / f"{_clean_relative_key(marker_key)}.json"
195+
196+
def _gcs_blob(self, marker_key: str):
197+
bucket_name, prefix = _parse_gcs_uri(str(self.gcs_prefix_uri or ""))
198+
object_name = "/".join(
199+
part.strip("/")
200+
for part in (
201+
prefix,
202+
self.namespace,
203+
f"{_clean_relative_key(marker_key)}.json",
204+
)
205+
if part and part.strip("/")
206+
)
207+
if self.client_factory is None:
208+
try:
209+
from google.cloud import storage # type: ignore
210+
except ImportError as exc:
211+
raise RuntimeError(
212+
"google-cloud-storage is required for GCS execution markers"
213+
) from exc
214+
client_factory = storage.Client
215+
else:
216+
client_factory = self.client_factory
217+
client = (
218+
client_factory(project=self.gcp_project_id)
219+
if self.gcp_project_id
220+
else client_factory()
221+
)
222+
return client.bucket(bucket_name).blob(object_name)
223+
224+
def _gcs_client(self):
225+
if self.client_factory is None:
226+
try:
227+
from google.cloud import storage # type: ignore
228+
except ImportError as exc:
229+
raise RuntimeError(
230+
"google-cloud-storage is required for GCS execution markers"
231+
) from exc
232+
client_factory = storage.Client
233+
else:
234+
client_factory = self.client_factory
235+
return (
236+
client_factory(project=self.gcp_project_id)
237+
if self.gcp_project_id
238+
else client_factory()
239+
)
240+
241+
242+
def build_execution_marker_store_from_env(
243+
*,
244+
platform_env_prefix: str,
245+
env_reader: Callable[[str, str | None], str | None],
246+
gcp_project_id: str | None = None,
247+
client_factory: Any = None,
248+
default_local_dir: str | Path | None = None,
249+
) -> ExecutionMarkerStore:
250+
prefix = str(platform_env_prefix or "").strip().upper()
251+
explicit_gcs_uri = env_reader(f"{prefix}_EXECUTION_STATE_GCS_URI", None)
252+
report_gcs_uri = env_reader("EXECUTION_REPORT_GCS_URI", None)
253+
local_dir = env_reader(f"{prefix}_EXECUTION_STATE_DIR", None)
254+
return ExecutionMarkerStore(
255+
local_dir=local_dir or default_local_dir or DEFAULT_EXECUTION_STATE_DIR,
256+
gcs_prefix_uri=explicit_gcs_uri or report_gcs_uri,
257+
gcp_project_id=gcp_project_id,
258+
client_factory=client_factory,
259+
)
260+
261+
262+
def resolve_execution_dedup_enabled(
263+
*,
264+
platform_env_prefix: str,
265+
env_reader: Callable[[str, str | None], str | None],
266+
dry_run_only: bool,
267+
account_scope: object = None,
268+
) -> bool:
269+
prefix = str(platform_env_prefix or "").strip().upper()
270+
raw_value = env_reader(f"{prefix}_EXECUTION_DEDUP_ENABLED", None)
271+
if raw_value is not None and str(raw_value).strip():
272+
return _env_bool(raw_value, default=bool(dry_run_only))
273+
return bool(dry_run_only) or _is_paper_account_scope(account_scope)
274+
275+
276+
def _is_paper_account_scope(value: object) -> bool:
277+
return str(value or "").strip().upper() == "PAPER"
278+
279+
280+
def _runtime_report_segment(value: object) -> str:
281+
text = str(value or "").strip()
282+
safe = "".join(ch if ch.isalnum() or ch in {"-", "_", "."} else "_" for ch in text)
283+
return safe or "unknown"
284+
285+
286+
def _month_segment(*values: object) -> str:
287+
for value in values:
288+
text = _optional_str(value)
289+
if len(text) >= 7 and text[4] == "-" and text[:4].isdigit() and text[5:7].isdigit():
290+
return text[:7]
291+
return ""
292+
293+
294+
def _optional_str(value: object) -> str:
295+
return str(value or "").strip()
296+
297+
298+
def _report_matches_execution(
299+
payload: Mapping[str, Any],
300+
*,
301+
platform: str,
302+
strategy_profile: str,
303+
account_scope: str,
304+
signal_date: str,
305+
effective_date: str,
306+
dry_run_only: bool,
307+
) -> bool:
308+
report = dict(payload or {})
309+
if _optional_str(report.get("platform")).lower() != _optional_str(platform).lower():
310+
return False
311+
if _optional_str(report.get("strategy_profile")).lower() != _optional_str(strategy_profile).lower():
312+
return False
313+
if _optional_str(report.get("account_scope")).lower() != _optional_str(account_scope).lower():
314+
return False
315+
if bool(report.get("dry_run")) != bool(dry_run_only):
316+
return False
317+
summary = dict(report.get("summary") or {})
318+
if signal_date and _date_key(signal_date) not in _report_signal_date_keys(report, summary):
319+
return False
320+
if effective_date and _date_key(effective_date) not in _report_effective_date_keys(report, summary):
321+
return False
322+
return (
323+
bool(summary.get("action_done"))
324+
or int(float(summary.get("orders_previewed_count") or 0)) > 0
325+
or int(float(summary.get("order_events_count") or 0)) > 0
326+
or _is_successful_no_action_report(report, summary)
327+
)
328+
329+
330+
def _is_successful_no_action_report(report: Mapping[str, Any], summary: Mapping[str, Any]) -> bool:
331+
if _optional_str(report.get("status")).lower() != "ok":
332+
return False
333+
if int(float(summary.get("orders_skipped_count") or 0)) > 0:
334+
return False
335+
return bool("action_done" in summary and not summary.get("action_done"))
336+
337+
338+
def _report_signal_date_keys(report: Mapping[str, Any], summary: Mapping[str, Any]) -> set[str]:
339+
signal_snapshot = _report_signal_snapshot(report)
340+
return _date_keys(
341+
summary.get("signal_date"),
342+
signal_snapshot.get("signal_as_of"),
343+
signal_snapshot.get("market_date"),
344+
signal_snapshot.get("price_as_of"),
345+
signal_snapshot.get("snapshot_as_of"),
346+
)
347+
348+
349+
def _report_effective_date_keys(report: Mapping[str, Any], summary: Mapping[str, Any]) -> set[str]:
350+
signal_snapshot = _report_signal_snapshot(report)
351+
return _date_keys(
352+
summary.get("effective_date"),
353+
signal_snapshot.get("effective_date"),
354+
)
355+
356+
357+
def _report_signal_snapshot(report: Mapping[str, Any]) -> dict[str, Any]:
358+
diagnostics = report.get("diagnostics")
359+
if not isinstance(diagnostics, Mapping):
360+
return {}
361+
signal_snapshot = diagnostics.get("signal_snapshot")
362+
return dict(signal_snapshot) if isinstance(signal_snapshot, Mapping) else {}
363+
364+
365+
def _date_keys(*values: object) -> set[str]:
366+
return {key for value in values if (key := _date_key(value))}
367+
368+
369+
def _date_key(value: object) -> str:
370+
text = _optional_str(value)
371+
if len(text) >= 10 and text[4] == "-" and text[7] == "-":
372+
return text[:10]
373+
return text

0 commit comments

Comments
 (0)