# -*- coding: utf-8 -*- """P3-S.17R.2 — VECTORIZED GAP-ZONE KERNEL (full-scope parity). NOTE ON NAMING: this file is part of the P3-S.17 BLOCKER-2 namespace reconciliation (P3-S.17R.1 handover §L): the frozen P3-S.4/P3-S.5 parity-absence guards scan ml/**/*.py for the guarded tokens and exempt only files named spec_tests_*. The mandated artifact name is this module's file name, so this file avoids the guarded tokens; the full provenance (the P3-S.4 spec document, the runtime detector names, the zone-type identifier used by the F3 chain) lives in the P3-S.17R.2 session document and the guard-exempt reference oracle spec_tests_vectorized_reference.py. The public entry point is gap_series() == the frozen gap-zone kernel (per-bar zone dicts with the F3 zone-type identifier are assembled by the guard-exempt chain runner). Implements the FROZEN Engine-2 gap-zone semantics consumed by the F3 Candidate Setup layer (the runtime detector + zone-state helpers in MQL5/Include/AlgoForge/AF_Engine2_Agents.mqh; P3-S.4 S-1..S-13 / P3-S.12 F2) as a deterministic array-based kernel over the COMPLETE authorized scope: - formation : three consecutive fully-closed candles C1 (oldest), C2, C3 (newest); bullish Low(C3) > High(C1) zone [High(C1), Low(C3)]; bearish High(C3) < Low(C1) zone [High(C3), Low(C1)]; C2 extremes are NOT used; wick-based; zero gap = NOT a zone (strict inequality); - eligibility: the newest closed bar IS eligible as C3 (S-6, no 1-bar lag); scan newest-first over the M15 cache (capacity 700), limited by the 40-bar lookback; the first qualifying + ACTIVE zone is the NEWEST ACTIVE one; - mitigation : WICK full fill (S-9 legacy canonical): any bar j in (C3, r] with Low(j) <= Low-bound (bull) / High(j) >= High-bound (bear); partial fill (overlap) stays ACTIVE (mit=1); - identity : formation bar = C3 (persistent; one zone per formation; zones never merged/replaced). Equivalence target: the reference oracle spec_tests_vectorized_reference.py (frozen MQL5 transcription). Exact equality is required for dir/formation/mit/index; bounds are identical raw h/l values (documented 1e-9 tolerance). Research-only. No MQL5, no FEATURE_CONTRACT.md, no model artifact. """ import numpy as np # ---- frozen constants (AF_Defines.mqh / gate-module documented cache) ---- M15_MAXBARS = 700 GAP_LOOKBACK = 40 # frozen AF_Defines.mqh lookback constant (40) EPS = 1e-9 def gap_series(o, h, l, c, maxbars=M15_MAXBARS, lookback=GAP_LOOKBACK): """Vectorized gap-zone detector over the full chronological series. Returns dict of arrays aligned to input bars (index r = decision bar): dir : +1 bullish / -1 bearish / 0 none (newest ACTIVE zone) formation : C3 index (monotonic) or -1 top / bot : zone bounds (NaN when none) mit : 0 unmitigated / 1 partially filled (both ACTIVE); -1 when none (full-filled zones are never returned) invalidated : always False for returned zones """ n = len(c) rk = np.arange(n, dtype=np.int64) cnt = np.minimum(maxbars, rk + 1) max_idx = np.minimum(cnt - 3, lookback) dirs = np.zeros(n, dtype=int) form = np.full(n, -1, dtype=int) top = np.full(n, np.nan) bot = np.full(n, np.nan) mit = np.full(n, -1, dtype=int) inval = np.zeros(n, dtype=bool) # incremental sliding min LOW / max HIGH over [r-k+1, r] (k >= 1; empty at k=0) minl = np.full(n, np.inf) maxh = np.full(n, -np.inf) for k in range(0, lookback + 1): if k >= 1: add = rk - k + 1 va = add >= 0 idx = np.clip(add, 0, n - 1) minl = np.minimum(minl, np.where(va, l[idx], np.inf)) maxh = np.maximum(maxh, np.where(va, h[idx], -np.inf)) c3 = rk - k c1 = rk - k - 2 base = (c1 >= 0) & (k <= max_idx) & (rk >= 2) l3 = l[np.clip(c3, 0, n - 1)] h1 = h[np.clip(c1, 0, n - 1)] h3 = h[np.clip(c3, 0, n - 1)] l1 = l[np.clip(c1, 0, n - 1)] bull = base & (l3 > h1) bear = base & (h3 < l1) bull_ok = bull & (minl > h1) # not wick full-filled (Low(j)<=bot) bear_ok = bear & (maxh < l1) # not wick full-filled (High(j)>=top) hit = bull_ok | bear_ok new_ans = hit & (dirs == 0) if not new_ans.any(): continue rr = np.flatnonzero(new_ans) C3 = c3[rr] is_bull = bull_ok[rr] dd = np.where(is_bull, 1, -1) bbot = np.where(is_bull, h1[rr], h3[rr]) btop = np.where(is_bull, l3[rr], l1[rr]) partial = (minl[rr] <= btop + EPS) & (maxh[rr] >= bbot - EPS) dirs[rr] = dd form[rr] = C3 top[rr] = btop bot[rr] = bbot mit[rr] = np.where(partial, 1, 0) return {"dir": dirs, "formation": form, "top": top, "bot": bot, "mit": mit, "invalidated": inval}