# -*- coding: utf-8 -*- """P3-S.12 SPEC TESTS — F2 ZONE CONTRACT (FVG + Order Block). Truth : docs/SNIPERGOLD_CANONICAL_SETUP_CONTRACT_v1.md (frozen §K/§M/§O) + docs/SMC_FVG_SPEC_v1.md (S-1..S-13) + docs/SMC_ORDER_BLOCK_SPEC_v1.md (S-1..S-15) -> spec_oracle_*() in this file (canonical ZONE contract). Code under : Engine 2 AF_FindFVG / AF_FindOrderBlock (AF_Engine2_Agents.mqh) audit and display AF_CollectFVG / AF_CollectOBs (AF_Engine2_Display.mqh) -> repaired_port_*() (the F2 target). Legacy port : legacy_port_*() = the PRE-F2 Engine-2 behavior (no mitigation, FVG 1-bar lag) — kept as historical before/after evidence. Discipline P3-S.12 (F2): - spec oracle vs expected : ASSERT (canonical zone contract = truth) - repaired port vs spec : ASSERT (the MQL5 fix must satisfy the contract) - legacy port vs repaired : REPORT (before/after differential evidence) - invariance (transform/future-mutation/symmetry) : ASSERT - no AUC/PF/backtest/optimization/ML/human annotation in this file. Cases: F2-T01..T20 (all 20 required by the brief §10). Zone contract (frozen §K refined by the F2 brief §5/§11): ZONE { ts(formation bar), dir, upper, lower, mit_state, invalidated } mit_state : UNMITIGATED(0) | PARTIALLY_FILLED(1) | FULLY_MITIGATED(2) PARTIALLY_FILLED is ACTIVE (S-9: partial fill != mitigation). invalidated: terminal flag; zone-level = set when FULLY_MITIGATED (frozen §M "invalidation = mitigation is the terminal usable-state"); setup-level (F3) may set it independently. consumer : available iff mit_state in {UNMITIGATED, PARTIALLY_FILLED} and NOT invalidated. A zone is a persistent entity: one formation = one zone, never re-emitted per bar (S-8). FVG mit : wick full-fill — bull Low(j)<=bot / bear High(j)>=top (S-9). OB mit : close-through full fill — bull Close(j)top (S-9); scan includes the move candle (j>B), display-consistent. no age expiry (W_zone_age = OPEN NUMERIC PARAMETER, default none). Usage: python spec_tests_zone_contract.py Output: output/p3_s12_zone_contract_report.json """ import json import os import sys import numpy as np HERE = os.path.dirname(os.path.abspath(__file__)) # ---- zone contract constants (SPEC local) ---- FVG_LOOKBACK = 40 # Engine-2 FVG consumer lookback (C agent; AF_E2_FVG_LOOKBACK) AVG_N = 20 # OB avg-body window (AF_E2_LOOKBACK_AVG) MIN_BARS_OB = 22 # OB detector requires avgN+2 closed bars MOVE_MULT = 1.5 # OB strong-move body multiplier (AF_E3_MOVE_BODY) EPS = 1e-9 # mitigation states (canonical) UNMITIGATED = 0 PARTIALLY_FILLED = 1 FULLY_MITIGATED = 2 STATE_NAMES = {0: "UNMITIGATED", 1: "PARTIALLY_FILLED", 2: "FULLY_MITIGATED"} # ===================================================================== # SPEC ORACLES (truth) — canonical ZONE contract # ===================================================================== def _overlap(h_j, l_j, bot, top): """A bar's range [l_j,h_j] overlaps the zone [bot,top].""" return h_j >= bot - EPS and l_j <= top + EPS def spec_oracle_fvg(o, h, l, c, db): """Canonical FVG zone evaluator (S-1..S-13 + frozen §K). Returns list of zone dicts: {b, d, top, bot, mit, partial, invalidated} for every qualifying C3 bar b in [2, db]; state computed from bars (b, db]. """ zones = [] for b in range(2, db + 1): if l[b] > h[b - 2]: zones.append(dict(b=b, d=1, top=float(l[b]), bot=float(h[b - 2]))) elif h[b] < l[b - 2]: zones.append(dict(b=b, d=-1, top=float(l[b - 2]), bot=float(h[b]))) for z in zones: full = False partial = False for j in range(z["b"] + 1, db + 1): if z["d"] > 0 and l[j] <= z["bot"]: # wick full-fill (S-9) full = True if z["d"] < 0 and h[j] >= z["top"]: # wick full-fill (S-9) full = True if _overlap(h[j], l[j], z["bot"], z["top"]): partial = True z["mit"] = FULLY_MITIGATED if full else (PARTIALLY_FILLED if partial else UNMITIGATED) z["partial"] = partial z["invalidated"] = full # zone-level terminal (frozen §M) return zones def avg_body(o, c, db, avg_n=AVG_N): m = min(avg_n, db + 1) if m <= 0: return 0.0 s = 0.0 for i in range(db - m + 1, db + 1): s += abs(c[i] - o[i]) return s / m def spec_oracle_ob(o, h, l, c, db, avg_n=AVG_N, min_bars=MIN_BARS_OB, move_mult=MOVE_MULT): """Canonical OB zone evaluator (S-1..S-15 + frozen §O/§K). Zone for every qualifying pair (B=b, M=b+1) with b+1 <= db; state computed from bars (b, db]. Mitigation scan includes M (display-consistent); partial-fill scan is strictly AFTER M (j >= b+2) — the move candle is part of formation, not a re-entry. """ n = db + 1 if n < min_bars: return [] avg = avg_body(o, c, db, avg_n) if avg <= 0.0: return [] zones = [] for b in range(0, n - 1): mb = abs(c[b + 1] - o[b + 1]) if mb < move_mult * avg: continue up = c[b + 1] > o[b + 1] d = 0 if up and c[b] < o[b]: d = 1 elif (not up) and c[b] > o[b]: d = -1 if d == 0: continue zones.append(dict(b=b, d=d, top=float(h[b]), bot=float(l[b]))) for z in zones: full = False partial = False for j in range(z["b"] + 1, db + 1): # includes M (display-consistent) if z["d"] > 0 and c[j] < z["bot"]: # close-through (S-9) full = True if z["d"] < 0 and c[j] > z["top"]: full = True for j in range(z["b"] + 2, db + 1): # strictly after M (re-entry) if _overlap(h[j], l[j], z["bot"], z["top"]): partial = True z["mit"] = FULLY_MITIGATED if full else (PARTIALLY_FILLED if partial else UNMITIGATED) z["partial"] = partial z["invalidated"] = full return zones def newest_active(zones): """Canonical consumer query: newest zone that is ACTIVE (unmitigated and not invalidated). Returns [b, d, top, bot] or None.""" act = [z for z in zones if z["mit"] in (UNMITIGATED, PARTIALLY_FILLED) and not z["invalidated"]] if not act: return None z = max(act, key=lambda z: z["b"]) return [z["b"], z["d"], float(z["top"]), float(z["bot"])] # ===================================================================== # LEGACY PORTS (PRE-F2 Engine-2 behavior — historical before-evidence) # ===================================================================== def legacy_fvg_port(o, h, l, c, db, lookback=FVG_LOOKBACK): """PRE-F2 AF_FindFVG: loop starts at i=1 (1-bar lag), NO mitigation filter, returns the FIRST (newest) geometric FVG.""" cnt = db + 1 if cnt < 3: return None max_i = min(cnt - 3, lookback if lookback > 0 else cnt) for i in range(1, max_i + 1): c3 = db - i if l[c3] > h[c3 - 2]: return [1, float(l[c3]), float(h[c3 - 2])] if h[c3] < l[c3 - 2]: return [-1, float(l[c3 - 2]), float(h[c3])] return None def legacy_ob_port(o, h, l, c, db, avg_n=AVG_N, min_bars=MIN_BARS_OB, move_mult=MOVE_MULT): """PRE-F2 AF_FindOrderBlock: newest pair first, NO mitigation filter.""" cnt = db + 1 if cnt < min_bars: return None avg = avg_body(o, c, db, avg_n) if avg <= 0.0: return None for i in range(1, cnt - 1): b = db - i move = b + 1 if abs(c[move] - o[move]) < move_mult * avg: continue if c[move] > o[move] and c[b] < o[b]: return [1, float(h[b]), float(l[b])] if c[move] < o[move] and c[b] > o[b]: return [-1, float(h[b]), float(l[b])] return None # ===================================================================== # REPAIRED PORTS (POST-F2 Engine-2 target — the MQL5 fix must match) # ===================================================================== def repaired_fvg_port(o, h, l, c, db, lookback=FVG_LOOKBACK): """POST-F2 AF_FindFVG: i starts at 0 (newest closed bar eligible as C3, S-6 — no 1-bar lag, no future candle) and skips zones already wick full-filled (S-9); returns the FIRST (newest) UNMITIGATED zone.""" cnt = db + 1 if cnt < 3: return None max_i = min(cnt - 3, lookback if lookback > 0 else cnt) for i in range(0, max_i + 1): c3 = db - i if l[c3] > h[c3 - 2]: bot, top = float(h[c3 - 2]), float(l[c3]) if not any(l[j] <= bot for j in range(c3 + 1, cnt)): return [1, top, bot] if h[c3] < l[c3 - 2]: bot, top = float(h[c3]), float(l[c3 - 2]) if not any(h[j] >= top for j in range(c3 + 1, cnt)): return [-1, top, bot] return None def repaired_ob_port(o, h, l, c, db, avg_n=AVG_N, min_bars=MIN_BARS_OB, move_mult=MOVE_MULT): """POST-F2 AF_FindOrderBlock: newest pair first, skips zones already close-through full-filled (S-9).""" cnt = db + 1 if cnt < min_bars: return None avg = avg_body(o, c, db, avg_n) if avg <= 0.0: return None for i in range(1, cnt - 1): b = db - i move = b + 1 if abs(c[move] - o[move]) < move_mult * avg: continue d = 0 if c[move] > o[move] and c[b] < o[b]: d = 1 elif c[move] < o[move] and c[b] > o[b]: d = -1 if d == 0: continue top, bot = float(h[b]), float(l[b]) if d > 0 and any(c[j] < bot for j in range(b + 1, cnt)): continue if d < 0 and any(c[j] > top for j in range(b + 1, cnt)): continue return [d, top, bot] return None # ===================================================================== # HELPERS # ===================================================================== def materialize(case): bs = case["bar_spec"] bars = [list(bs["neutral"]) for _ in range(bs["count"])] for (idx, ohlc) in bs["patches"]: if idx < len(bars): bars[idx] = list(ohlc) arr = np.asarray(bars, dtype=float) return arr[:, 0], arr[:, 1], arr[:, 2], arr[:, 3] def mirror(o, h, l, c, pivot): return (2.0 * pivot - o, 2.0 * pivot - l, 2.0 * pivot - h, 2.0 * pivot - c) def apply_transform(o, h, l, c, transform): if transform is None: return o, h, l, c kind = transform["kind"] if kind == "scale": k = transform["factor"] return o * k, h * k, l * k, c * k if kind == "translate": v = transform["value"] return o + v, h + v, l + v, c + v raise ValueError("unknown transform " + kind) def apply_mutations(o, h, l, c, mutations, neutral): o = o.copy(); h = h.copy(); l = l.copy(); c = c.copy() for (idx, ohlc) in mutations: while idx >= len(o): o = np.append(o, neutral[0]) h = np.append(h, neutral[1]) l = np.append(l, neutral[2]) c = np.append(c, neutral[3]) o[idx], h[idx], l[idx], c[idx] = ohlc return o, h, l, c def zones_sig(zones): """[(b, d, top, bot, mit, invalidated)] — comparison key.""" return [(z["b"], z["d"], z["top"], z["bot"], z["mit"], z["invalidated"]) for z in zones] def vec_equal(a, b): if a is None or b is None: return a is None and b is None return (a[0] == b[0] and a[1] == b[1] and abs(a[2] - b[2]) <= EPS and abs(a[3] - b[3]) <= EPS) def port_equal(a, b): if a is None or b is None: return a is None and b is None return a[0] == b[0] and abs(a[1] - b[1]) <= EPS and abs(a[2] - b[2]) <= EPS # ===================================================================== # F2 CASE TABLE (F2-T01..T20) — expected values derive from the canonical # zone contract, NOT from the buggy implementation. # mit encoding: 0=UNMITIGATED, 1=PARTIALLY_FILLED, 2=FULLY_MITIGATED # ===================================================================== NEUT_HI = [100.6, 100.9, 100.51, 100.8] # above-zone neutral (no overlap) NEUT_LO = [100.0, 100.3, 99.9, 100.1] # near-zone neutral NEUT_FVG = [100.0, 100.5, 99.8, 100.3] # FVG base neutral CASES = [ # ---- FVG ---- dict(id="F2-T01", kind="fvg", title="FVG formation (bullish, S-1/S-4)", bar_spec=dict(neutral=NEUT_FVG, count=6, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]], [5, [100.6, 100.8, 100.45, 100.7]]]), decision_bar=5, expected=dict(zones=[[4, 1, 100.4, 100.2, 0, False, False]], zone_count=1, newest_active=[4, 1, 100.4, 100.2], legacy=[1, 100.4, 100.2], repaired=[1, 100.4, 100.2])), dict(id="F2-T02", kind="fvg", title="FVG remains ONE zone after many bars (S-8)", bar_spec=dict(neutral=[100.6, 100.9, 100.5, 100.8], count=25, patches=[[0, [100.6, 100.9, 100.2, 100.8]], [1, [100.6, 100.9, 100.2, 100.8]], [2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]]]), decision_bar=24, extra_dbs=[dict(db=14, zone_ids=[4], newest_active=[4, 1, 100.4, 100.2])], expected=dict(zones=[[4, 1, 100.4, 100.2, 0, False, False]], zone_count=1, newest_active=[4, 1, 100.4, 100.2], legacy=[1, 100.4, 100.2], repaired=[1, 100.4, 100.2])), dict(id="F2-T03", kind="fvg", title="FVG partial fill -> still ACTIVE (S-9)", bar_spec=dict(neutral=NEUT_FVG, count=6, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]], [5, [100.25, 100.45, 100.22, 100.3]]]), decision_bar=5, expected=dict(zones=[[4, 1, 100.4, 100.2, 1, True, False]], zone_count=1, newest_active=[4, 1, 100.4, 100.2], legacy=[1, 100.4, 100.2], repaired=[1, 100.4, 100.2])), dict(id="F2-T04", kind="fvg", title="FVG full mitigation (wick full-fill, S-9)", bar_spec=dict(neutral=NEUT_FVG, count=7, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]], [5, [100.6, 100.8, 100.45, 100.7]], [6, [100.1, 100.45, 100.1, 100.2]]]), decision_bar=6, expected=dict(zones=[[4, 1, 100.4, 100.2, 2, True, True]], zone_count=1, newest_active=None, legacy=[1, 100.4, 100.2], repaired=None, note="FVG-T10 equivalent: legacy returns mitigated zone (BUG-P3S4-001); repaired None.")), dict(id="F2-T05", kind="fvg", title="FVG mitigated zone NOT consumed; older active zone returned", bar_spec=dict(neutral=NEUT_FVG, count=12, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]], [5, [100.6, 100.8, 100.45, 100.7]], [6, [100.7, 100.9, 100.5, 100.8]], [7, [100.7, 100.9, 100.6, 100.8]], [8, [100.8, 101.2, 100.7, 101.1]], [9, [101.0, 101.3, 100.95, 101.2]], [10, [101.0, 101.3, 100.96, 101.2]], [11, [100.5, 100.95, 100.45, 100.6]]]), decision_bar=11, expected=dict(zones=[[4, 1, 100.4, 100.2, 0, False, False], [9, 1, 100.95, 100.9, 2, True, True]], zone_count=2, newest_active=[4, 1, 100.4, 100.2], legacy=[1, 100.95, 100.9], repaired=[1, 100.4, 100.2], note="newest zone (b=9) fully mitigated -> legacy consumes it (BUG); repaired returns older active b=4.")), dict(id="F2-T06", kind="fvg", title="FVG invalidation is terminal (S-10/§K)", bar_spec=dict(neutral=NEUT_FVG, count=10, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]], [5, [100.6, 100.8, 100.45, 100.7]], [6, [100.1, 100.45, 100.1, 100.2]], [7, [100.1, 100.6, 100.05, 100.2]], [8, [100.1, 100.6, 100.05, 100.2]], [9, [100.1, 100.6, 100.05, 100.2]]]), decision_bar=9, extra_dbs=[dict(db=6, zone_ids=[4], newest_active=None)], expected=dict(zones=[[4, 1, 100.4, 100.2, 2, True, True]], zone_count=1, newest_active=None, legacy=[1, 100.4, 100.2], repaired=None, note="price re-enters the zone (bars 7-9) but the zone stays invalidated (irreversible); no reactivation.")), dict(id="F2-T07", kind="fvg", title="FVG formation timing boundary: C3 = newest closed bar (S-6)", bar_spec=dict(neutral=NEUT_FVG, count=5, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]]]), decision_bar=4, expected=dict(zones=[[4, 1, 100.4, 100.2, 0, False, False]], zone_count=1, newest_active=[4, 1, 100.4, 100.2], legacy=None, repaired=[1, 100.4, 100.2], note="BUG-P3S4-002: legacy skips C3=index0 (1-bar lag); repaired detects at C3 close.")), dict(id="F2-T08", kind="fvg", title="FVG one-bar timing regression: no future candle, no needless delay", bar_spec=dict(neutral=NEUT_FVG, count=5, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]]]), decision_bar=4, extra_dbs=[dict(db=3, zone_ids=[], newest_active=None)], expected=dict(zones=[[4, 1, 100.4, 100.2, 0, False, False]], zone_count=1, newest_active=[4, 1, 100.4, 100.2], legacy=None, repaired=[1, 100.4, 100.2], note="db=3 (before C3 closes): NO zone (no future candle); db=4 (C3 closed): zone present (no needless 1-bar delay).")), # ---- OB ---- dict(id="F2-T09", kind="ob", title="OB formation with move = newest bar (S-1/S-6)", bar_spec=dict(neutral=NEUT_HI, count=25, patches=[[23, [100.2, 100.5, 99.8, 100.1]], [24, [100.3, 101.0, 100.2, 100.9]]]), decision_bar=24, expected=dict(zones=[[23, 1, 100.5, 99.8, 0, False, False]], zone_count=1, newest_active=[23, 1, 100.5, 99.8], legacy=[1, 100.5, 99.8], repaired=[1, 100.5, 99.8])), dict(id="F2-T10", kind="ob", title="OB remains ONE zone after many bars (S-8)", bar_spec=dict(neutral=NEUT_HI, count=25, patches=[[10, [100.2, 100.5, 99.8, 100.1]], [11, [100.3, 101.0, 100.2, 100.9]]]), decision_bar=24, extra_dbs=[dict(db=22, zone_ids=[10], newest_active=[10, 1, 100.5, 99.8])], expected=dict(zones=[[10, 1, 100.5, 99.8, 0, False, False]], zone_count=1, newest_active=[10, 1, 100.5, 99.8], legacy=[1, 100.5, 99.8], repaired=[1, 100.5, 99.8])), dict(id="F2-T11", kind="ob", title="OB partial fill -> still ACTIVE (S-9)", bar_spec=dict(neutral=NEUT_LO, count=25, patches=[[20, [100.2, 100.5, 99.8, 100.1]], [21, [100.3, 101.0, 100.2, 100.9]], [22, [100.3, 100.4, 99.9, 100.2]]]), decision_bar=24, expected=dict(zones=[[20, 1, 100.5, 99.8, 1, True, False]], zone_count=1, newest_active=[20, 1, 100.5, 99.8], legacy=[1, 100.5, 99.8], repaired=[1, 100.5, 99.8])), dict(id="F2-T12", kind="ob", title="OB full mitigation (close-through, S-9)", bar_spec=dict(neutral=NEUT_LO, count=25, patches=[[20, [100.2, 100.5, 99.8, 100.1]], [21, [100.3, 101.0, 100.2, 100.9]], [22, [99.89, 100.0, 99.6, 99.7]]]), decision_bar=24, expected=dict(zones=[[20, 1, 100.5, 99.8, 2, True, True]], zone_count=1, newest_active=None, legacy=[1, 100.5, 99.8], repaired=None, note="OB-T11 equivalent: legacy returns mitigated zone (BUG-P3S5-001); repaired None.")), dict(id="F2-T13", kind="ob", title="OB mitigated zone NOT consumed; older active zone returned", bar_spec=dict(neutral=NEUT_HI, count=25, patches=[[9, [100.7, 100.9, 100.55, 100.6]], [10, [100.2, 100.5, 99.6, 99.9]], [11, [100.0, 101.0, 99.9, 100.9]], [20, [100.1, 100.6, 99.7, 100.0]], [21, [100.2, 101.0, 100.1, 100.9]], [22, [99.5, 100.0, 99.3, 99.6]]]), decision_bar=24, expected=dict(zones=[[10, 1, 100.5, 99.6, 1, True, False], [20, 1, 100.6, 99.7, 2, True, True]], zone_count=2, newest_active=[10, 1, 100.5, 99.6], legacy=[1, 100.6, 99.7], repaired=[1, 100.5, 99.6], note="newest zone (b=20) close-through mitigated -> legacy consumes it (BUG); repaired returns older active b=10.")), dict(id="F2-T14", kind="ob", title="OB invalidation is terminal (S-10/S-12/§K)", bar_spec=dict(neutral=NEUT_LO, count=25, patches=[[20, [100.2, 100.5, 99.8, 100.1]], [21, [100.3, 101.0, 100.2, 100.9]], [22, [99.89, 100.0, 99.6, 99.7]], [24, [100.0, 100.8, 99.9, 100.7]]]), decision_bar=24, extra_dbs=[dict(db=22, zone_ids=[20], newest_active=None)], expected=dict(zones=[[20, 1, 100.5, 99.8, 2, True, True]], zone_count=1, newest_active=None, legacy=[1, 100.5, 99.8], repaired=None, note="OB-T12/T16 equivalents: bar-24 opposite move does NOT reactivate the zone (no breaker, no invalidation-reversal).")), dict(id="F2-T15", kind="fvg", title="Two independent FVG zones (S-11)", bar_spec=dict(neutral=[100.0, 100.3, 99.9, 100.2], count=7, patches=[[1, [100.1, 100.5, 100.0, 100.4]], [2, [100.3, 100.6, 100.2, 100.5]], [3, [100.6, 100.9, 100.55, 100.8]], [4, [100.7, 100.9, 100.6, 100.8]], [5, [100.7, 101.0, 100.65, 100.9]], [6, [101.0, 101.3, 100.95, 101.2]]]), decision_bar=6, expected=dict(zones=[[3, 1, 100.55, 100.5, 0, False, False], [6, 1, 100.95, 100.9, 0, False, False]], zone_count=2, newest_active=[6, 1, 100.95, 100.9], legacy=[1, 100.55, 100.5], repaired=[1, 100.95, 100.9], note="FVG-T13 equivalent: legacy returns older b=3 (1-bar lag); repaired returns newest b=6.")), dict(id="F2-T16", kind="ob", title="Two independent OB zones (S-11)", bar_spec=dict(neutral=NEUT_LO, count=25, patches=[[10, [100.2, 100.5, 99.8, 100.1]], [11, [100.3, 101.0, 100.2, 100.9]], [20, [100.4, 100.7, 100.1, 100.3]], [21, [100.6, 101.3, 100.5, 101.2]]]), decision_bar=24, expected=dict(zones=[[10, 1, 100.5, 99.8, 1, True, False], [20, 1, 100.7, 100.1, 1, True, False]], zone_count=2, newest_active=[20, 1, 100.7, 100.1], legacy=[1, 100.7, 100.1], repaired=[1, 100.7, 100.1])), dict(id="F2-T17", kind="fvg", title="Overlapping FVG zones, no merge (S-11)", bar_spec=dict(neutral=[100.0, 100.3, 99.9, 100.2], count=6, patches=[[1, [100.1, 100.5, 100.0, 100.4]], [2, [100.3, 100.6, 100.2, 100.5]], [3, [100.6, 100.9, 100.55, 100.8]], [4, [100.7, 101.0, 100.65, 100.9]], [5, [100.9, 101.2, 100.95, 101.1]]]), decision_bar=5, expected=dict(zones=[[3, 1, 100.55, 100.5, 0, False, False], [4, 1, 100.65, 100.6, 0, False, False], [5, 1, 100.95, 100.9, 0, False, False]], zone_count=3, newest_active=[5, 1, 100.95, 100.9], legacy=[1, 100.65, 100.6], repaired=[1, 100.95, 100.9], note="FVG-T12 equivalent: legacy returns b=4 (1-bar lag); repaired returns newest b=5.")), dict(id="F2-T18", kind="ob", title="Overlapping OB zones, no merge (S-11)", bar_spec=dict(neutral=NEUT_LO, count=25, patches=[[18, [100.2, 100.5, 99.8, 100.1]], [19, [100.3, 101.0, 100.2, 100.9]], [20, [100.4, 100.7, 100.1, 100.3]], [21, [100.6, 101.3, 100.5, 101.2]]]), decision_bar=24, expected=dict(zones=[[18, 1, 100.5, 99.8, 1, True, False], [20, 1, 100.7, 100.1, 1, True, False]], zone_count=2, newest_active=[20, 1, 100.7, 100.1], legacy=[1, 100.7, 100.1], repaired=[1, 100.7, 100.1])), dict(id="F2-T19", kind="fvg", title="Future-bar mutation cannot alter past zone state (S-7)", bar_spec=dict(neutral=NEUT_FVG, count=6, patches=[[2, [100.0, 100.2, 99.9, 100.1]], [3, [100.3, 100.9, 100.25, 100.8]], [4, [100.5, 101.0, 100.4, 100.9]], [5, [100.6, 100.8, 100.45, 100.7]]]), decision_bar=5, mutations=[[20, [200.0, 210.0, 199.0, 205.0]]], expected=dict(zones=[[4, 1, 100.4, 100.2, 0, False, False]], zone_count=1, newest_active=[4, 1, 100.4, 100.2], legacy=[1, 100.4, 100.2], repaired=[1, 100.4, 100.2])), dict(id="F2-T20", kind="fvg", title="Bullish/bearish symmetry (S-4; runner mirrors every case)", bar_spec=dict(neutral=[100.0, 100.2, 99.5, 99.7], count=6, patches=[[2, [100.0, 100.1, 99.8, 99.9]], [3, [99.7, 99.8, 99.2, 99.4]], [4, [99.4, 99.6, 99.0, 99.3]], [5, [99.3, 99.5, 99.0, 99.4]]]), decision_bar=5, expected=dict(zones=[[4, -1, 99.8, 99.6, 0, False, False]], zone_count=1, newest_active=[4, -1, 99.8, 99.6], legacy=[-1, 99.8, 99.6], repaired=[-1, 99.8, 99.6])), ] # ===================================================================== # RUNNER # ===================================================================== def evaluate(kind, o, h, l, c, db): if kind == "fvg": zones = spec_oracle_fvg(o, h, l, c, db) legacy = legacy_fvg_port(o, h, l, c, db) repaired = repaired_fvg_port(o, h, l, c, db) else: zones = spec_oracle_ob(o, h, l, c, db) legacy = legacy_ob_port(o, h, l, c, db) repaired = repaired_ob_port(o, h, l, c, db) return zones, newest_active(zones), legacy, repaired def run_case(case): o, h, l, c = materialize(case) db = case["decision_bar"] kind = case["kind"] transform = case.get("transform") mutations = case.get("mutations") neutral = case["bar_spec"]["neutral"] zones, na, legacy, repaired = evaluate(kind, o, h, l, c, db) exp = case["expected"] exp_zones = [list(z) for z in exp["zones"]] exp_na = (None if exp["newest_active"] is None else list(exp["newest_active"])) checks = {} # spec oracle == expected (canonical zone contract = truth) got_sig = [[z["b"], z["d"], z["top"], z["bot"], z["mit"], z["partial"], z["invalidated"]] for z in zones] checks["zones_spec"] = bool(len(got_sig) == len(exp_zones)) if checks["zones_spec"]: for g, e in zip(got_sig, exp_zones): if (g[0] != e[0] or g[1] != e[1] or abs(g[2] - e[2]) > EPS or abs(g[3] - e[3]) > EPS or g[4] != e[4] or g[5] != e[5] or g[6] != e[6]): checks["zones_spec"] = False break checks["zone_count_spec"] = bool(len(zones) == int(exp["zone_count"])) checks["newest_active_spec"] = vec_equal(na, exp_na) # extra decision bars (timing / persistence / terminal checks) extra_ok = True extra_info = [] for ex in case.get("extra_dbs", []): edb = ex["db"] z2, na2, _lg2, _rp2 = evaluate(kind, o, h, l, c, edb) ids2 = [z["b"] for z in z2] exp_ids = ex["zone_ids"] exp_na2 = (None if ex["newest_active"] is None else list(ex["newest_active"])) same = (ids2 == exp_ids) and vec_equal(na2, exp_na2) extra_ok = extra_ok and same extra_info.append({"db": edb, "zone_ids": ids2, "newest_active": na2, "expected_zone_ids": exp_ids, "expected_newest_active": exp_na2, "ok": same}) checks["extra_dbs"] = bool(extra_ok) # invariance: transform (scale/translate) keeps structure & state o2, h2, l2, c2 = apply_transform(o, h, l, c, transform) z2, _, _, _ = evaluate(kind, o2, h2, l2, c2, db) sig_t = [[z["b"], z["d"], z["top"], z["bot"], z["mit"], z["partial"], z["invalidated"]] for z in z2] checks["invariant_transform"] = bool(sig_t == got_sig) # invariance: future mutation (S-7 / T19) if mutations: o3, h3, l3, c3 = apply_mutations(o, h, l, c, mutations, neutral) z3, _, _, _ = evaluate(kind, o3, h3, l3, c3, db) sig_m = [[z["b"], z["d"], z["top"], z["bot"], z["mit"], z["partial"], z["invalidated"]] for z in z3] checks["invariant_future_mutation"] = bool(sig_m == got_sig) # invariance: bull/bear symmetry (S-4) — mirror every case pivot = (float(np.min(l)) + float(np.max(h))) / 2.0 om, hm, lm, cm = mirror(o, h, l, c, pivot) zm, _, _, _ = evaluate(kind, om, hm, lm, cm, db) sym_ok = (len(zm) == len(zones)) if sym_ok: for z, zz in zip(zones, zm): # mirrored: same b, dir flips, bounds mirror, state identical if (z["b"] != zz["b"] or z["d"] != -zz["d"] or z["mit"] != zz["mit"] or z["invalidated"] != zz["invalidated"] or abs(zz["top"] - (2 * pivot - z["bot"])) > EPS or abs(zz["bot"] - (2 * pivot - z["top"])) > EPS): sym_ok = False break checks["symmetry"] = bool(sym_ok) # repaired port == spec newest_active (the F2 contract the MQL5 fix satisfies) spec_for_port = None if na is None else [na[1], na[2], na[3]] checks["repaired_matches_spec"] = port_equal(repaired, spec_for_port) checks["repaired_expected"] = port_equal( repaired, (None if exp["repaired"] is None else list(exp["repaired"]))) # legacy port == expected legacy (before/after evidence) checks["legacy_expected"] = port_equal( legacy, (None if exp["legacy"] is None else list(exp["legacy"]))) return { "id": case["id"], "kind": kind, "title": case["title"], "zones": got_sig, "newest_active": na, "legacy_port": legacy, "repaired_port": repaired, "checks": checks, "extra_dbs": extra_info, "pass": all(checks.values()), "note": exp.get("note") or case.get("note"), } def main(): results = [run_case(c) for c in CASES] n_pass = sum(1 for r in results if r["pass"]) n_total = len(results) print(f"=== P3-S.12 SPEC TESTS — F2 ZONE CONTRACT ({n_pass}/{n_total} PASS) ===") print(f"{'ID':<9} {'PASS':<6} {'zones':<5} {'newest':<26} {'legacy':<18} {'repaired':<18}") for r in results: na = r["newest_active"] na_s = "-" if na is None else f"b{na[0]} d{na[1]} [{na[2]},{na[3]}]" lg = r["legacy_port"] lg_s = "-" if lg is None else f"d{lg[0]} [{lg[1]},{lg[2]}]" rp = r["repaired_port"] rp_s = "-" if rp is None else f"d{rp[0]} [{rp[1]},{rp[2]}]" print(f"{r['id']:<9} {str(r['pass']):<6} {len(r['zones']):<5} {na_s:<26} " f"{lg_s:<18} {rp_s:<18} {r['title'][:30]}") if n_pass != n_total: print("\nFAILED:") for r in results: if not r["pass"]: print(" ", r["id"], r["checks"]) sys.exit(1) print("\nAll spec assertions PASS — expected results are consistent with the " "frozen canonical ZONE contract (P3-S.10 §K/§M/§O + P3-S.4 S-1..S-13 + " "P3-S.5 S-1..S-15).") print("Legacy ports document the PRE-F2 Engine-2 behavior (BUG-P3S4-001/-002, " "BUG-P3S5-001); repaired ports are the F2 contract the MQL5 fix must match.") # before/after evidence mapping (brief §27) evidence = { "FVG-T10 (full mitigation)": "F2-T04", "FVG-T11 (invalidation not modeled)": "F2-T06", "FVG-T12 (overlapping)": "F2-T17", "FVG-T13 (multiple consecutive)": "F2-T15", "OB-T11 (full mitigation)": "F2-T12", "OB-T12 (invalidation not modeled)": "F2-T14", "OB-T16 (breaker not implemented)": "F2-T14", } before_after = [] for r in results: before_after.append({ "id": r["id"], "kind": r["kind"], "zones": r["zones"], "newest_active": r["newest_active"], "legacy_port (before)": r["legacy_port"], "repaired_port (after)": r["repaired_port"], }) report = { "spec_docs": ["docs/SNIPERGOLD_CANONICAL_SETUP_CONTRACT_v1.md", "docs/SMC_FVG_SPEC_v1.md", "docs/SMC_ORDER_BLOCK_SPEC_v1.md"], "phase": "P3-S.12", "generated_utc": __import__("datetime").datetime.now( __import__("datetime").timezone.utc).isoformat(), "constants": {"FVG_LOOKBACK": FVG_LOOKBACK, "AVG_N": AVG_N, "MIN_BARS_OB": MIN_BARS_OB, "MOVE_MULT": MOVE_MULT, "W_zone_age": "OPEN NUMERIC PARAMETER (default none)"}, "summary": {"total": n_total, "passed": n_pass, "failed": n_total - n_pass}, "evidence_mapping": evidence, "before_after": before_after, "cases": results, } outdir = os.path.join(HERE, "output") os.makedirs(outdir, exist_ok=True) out_path = os.path.join(outdir, "p3_s12_zone_contract_report.json") with open(out_path, "w", encoding="utf-8") as f: json.dump(report, f, indent=2, default=str) print(f"[saved] {out_path}") if __name__ == "__main__": main()