# -*- coding: utf-8 -*- """P3-S22.2 COMMON — shared infrastructure for the discrepancy impact assessment (ADJ-1 / ADJ-2). Disposable / research-only. NO production modification. Reuses only the FROZEN committed research inputs (caches, npz) and the frozen F3 oracle (canonical_oracle / F3SetupEngine) as the semantic reference. The independent oracles for the discrepancies live in this namespace and do NOT call the MQL5 runtime. Usage: imported by s222_partA_*.py / s222_partB_*.py / s222_*.py """ 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")) for p in (SETUP, SMC, os.path.normpath(os.path.join(HERE, ".."))): if p not in sys.path: sys.path.insert(0, p) M15 = 900 # seconds M30 = 1800 H4 = 14400 # F2 zone-state enumeration (frozen AF_Defines / AF_Engine2_Agents.mqh) UNMITIGATED = 0 PARTIALLY = 1 FULLY = 2 # Frozen F3 windows (P3-S.17R.2) 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) # ------------------------------------------------------------------ # Independent UTC-clock M30 aggregation from M15 bars (ADJ-2 oracle) # ------------------------------------------------------------------ def utc_clock_m30(t15, o15, h15, l15, c15): """Aggregate M15 bars into fixed UTC 30-minute buckets purely by wall-clock timestamp. An M30 bar covers [open, open+1800) with open a multiple of 1800 s from the Unix epoch. Every M15 bar belongs to the bucket whose interval contains its OPEN time. Returns (t30, o30, h30, l30, c30) chronological.""" 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 // M30) * M30 # UTC bucket open (int64) # group starts where the bucket changes 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() # drop any trailing bucket with no closed bar (should not happen for a # complete feed; a bucket is always closed once its M15 bar is present) return t30, o30, h30, l30, c30 def as_of_m30_utc_gate(t15, m30_dir_per_bucket): """Map per-bucket context dir to a per-M15-bar gate series using the frozen as-of rule (newest CLOSED M30 bar with close_time <= t+900).""" n = len(t15) def bucket_id(t): return int(((t + M15 - M30) // M30)) # newest closed M30 id at t+900 ids = np.array([bucket_id(int(x)) for x in t15], dtype=np.int64) base = int(ids[0]) if n else 0 rel = ids - base gate = np.zeros(n, dtype=int) for i, ridx in enumerate(rel): if 0 <= ridx < len(m30_dir_per_bucket): gate[i] = int(m30_dir_per_bucket[ridx]) elif ridx < 0: gate[i] = 0 else: gate[i] = int(m30_dir_per_bucket[-1]) return gate