SniperGold_ML/ml/p3/setup_dataset/vectorized_fvg.py

113 lines
5 KiB
Python

# -*- 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}