forked from chiki2bum2/SniperGold_ML
113 lines
5 KiB
Python
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}
|