|
| 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