# -*- coding: utf-8 -*- """P3-S22.3 M30 REPAIR - COMMON shared infrastructure. Disposable / research-only. This namespace is ADDITIVE: it does NOT modify the committed research M30 construction, frozen gate caches, P3-S16/P3-S17/P3-S18/ P3-S20 artifacts, FEATURE_CONTRACT, labels, TP/SL/horizon, or production MQL5. It imports ONLY frozen commits + frozen chain-parity machinery. The corrected UTC-clock M30 construction and its independent oracle live in this namespace. Usage: imported by s223_part*.py modules. """ import datetime as dt import hashlib import json import os import sys import numpy as np HERE = os.path.dirname(os.path.abspath(__file__)) OUT = os.path.join(HERE, "output") REPO = os.path.normpath(os.path.join(HERE, "..", "..", "..")) SETUP = os.path.normpath(os.path.join(HERE, "..", "setup_dataset")) SMC = os.path.normpath(os.path.join(HERE, "..", "smc_semantic")) P3 = os.path.normpath(os.path.join(HERE, "..")) for p in (P3, SETUP, SMC): if p not in sys.path: sys.path.insert(0, p) M15 = 900 # seconds M30 = 1800 H4 = 14400 E2_MIN_BARS = 80 # frozen Context-agent minimum cache (AF_Defines.mqh) # Frozen F3 windows (P3-S.17R.2 contracts) W_SWEEP = 40 W_CHOCH = 40 W_SETUP = 40 W_M3 = 2 SCOPE_START = dt.datetime(2017, 1, 3, tzinfo=dt.timezone.utc) SCOPE_END = dt.datetime(2026, 7, 21, 23, 45, tzinfo=dt.timezone.utc) def _sha16(obj): return hashlib.sha256( json.dumps(obj, sort_keys=True, default=str).encode()).hexdigest()[:16] def sha256_of(obj): return hashlib.sha256( json.dumps(obj, sort_keys=True, default=str).encode()).hexdigest() def now_utc(): return dt.datetime.now(dt.timezone.utc).isoformat() def load_all(): import spec_tests_vectorized_primitives as VPR return VPR.load_all() def load_gates(): import spec_tests_vectorized_primitives as VPR return VPR.load_gates() def scope_mask(t): import spec_tests_vectorized_primitives as VPR return VPR.scope_mask(t) def committed_m30_gate(): g = load_gates() return g[1] # index-pair resample research M30 gate (committed) # -------------------------------------------------------------------------- # CORRECTED RESEARCH M30 CONSTRUCTION (UTC-clock buckets) # -------------------------------------------------------------------------- def corrected_m30_from_m15(t15, o15, h15, l15, c15): """Build corrected research M30 bars from M15 by FIXED UTC 30-minute buckets: t30_open = floor(t15 / 1800) * 1800 (UTC epoch, seconds) Aggregate open / high / low / close within [open, open+1800). Deterministic chronological output. A bucket is emitted only when it contains >= 1 M15 bar. No forward fill, no look-ahead. Returns arrays. """ n = len(t15) if n == 0: return (np.array([], dtype=np.int64), np.array([], dtype=np.float64), np.array([], dtype=np.float64), np.array([], dtype=np.float64), np.array([], dtype=np.float64)) opens = (t15.astype(np.int64) // M30) * M30 change = np.empty(n, dtype=bool) change[0] = True change[1:] = opens[1:] != opens[:-1] start = np.flatnonzero(change) end = np.append(start[1:], n) t30 = opens[start] o30 = o15[start] c30 = c15[end - 1] h30 = np.empty(len(start), dtype=np.float64) l30 = np.empty(len(start), dtype=np.float64) for k, (a, b) in enumerate(zip(start, end)): seg = slice(a, b) h30[k] = h15[seg].max() l30[k] = l15[seg].min() return t30, o30, h30, l30, c30 def as_of_m30_gate(t15, m30_dir_per_bucket, t30=None): """Map per-bucket Context-agent direction to a per-M15-bar gate series using the FROZEN as-of rule: newest CLOSED M30 bar with close_time <= t+900. close_time = bucket_open + 1800 ; decision close = t + 900. Implemented with np.searchsorted over the ACTUAL closed closes (gap-aware): for every gap, each M15 bar still as-ofs the most-recently closed M30 bar that precedes its decision close. This is the exact rule used by the validated P3-S22.2 oracle (which reproduced the committed dirs 0-mismatch). Requires t15 and t30 (bucket opens) for the closes. """ t15 = np.asarray(t15, dtype=np.int64) if t30 is None: # derive bucket opens from t15 (must be contiguous gap-free input; # for gapped feeds the caller MUST supply the real t30) t30 = (t15 // M30) * M30 m30_close = np.asarray(t30, dtype=np.int64) + M30 dc = t15 + M15 # decision close idx = np.searchsorted(m30_close, dc, side="right") - 1 n = len(t15) gate = np.zeros(n, dtype=int) valid = (idx >= 0) & (idx < len(m30_dir_per_bucket)) gate[valid] = np.asarray(m30_dir_per_bucket, dtype=int)[ np.clip(idx[valid], 0, len(m30_dir_per_bucket) - 1)] return gate def invariant_checkpoint(t15, t30): """Assert the explicit M30 invariant: M30_open == floor(M15_open / 1800) * 1800 (UTC) verified for every research M15 bar that begins a complete UTC M30 bucket. """ opens = (t15.astype(np.int64) // M30) * M30 change = np.empty(len(t15), dtype=bool) change[0] = True change[1:] = opens[1:] != opens[:-1] start = np.flatnonzero(change) mismatch = int((t30 != opens[start]).sum()) return {"n_buckets": int(len(t30)), "n_start_m15_checked": int(len(start)), "mismatch": mismatch, "invariant_holds": bool(mismatch == 0), "formula": "M30_open = floor(M15_open / 1800) * 1800 (UTC clock)"}