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