# -*- coding: utf-8 -*- """P3-S.11 F1 SPEC TESTS — EVENT CONTRACT (sweep + CHoCH validity / supersession). Truth : docs/SNIPERGOLD_CANONICAL_SETUP_CONTRACT_v1.md (frozen, P3-S.10) + docs/P3_S10_OWNER_ADJUDICATION.md OD-3/OD-4 + SMC_LIQUIDITY_SWEEP_SPEC_v1.md R-H (valid b..b+40, expired b+41) + SMC_CHOCH_MSS_SPEC_v1.md S-9/S-10/S-12 (chain validity). Code under : EVENT CONSUMERS of the ML-path EA AlgoForge_Backtest_Baseline.mq5 audit (f9 choch_confirms, f18 confluence) — before (legacy stale state) vs after (canonical event validity) — plus the DETECTORS (DetectLiquidityGrabs / ProcessStructure internal) which must remain UNCHANGED (F1-T10 regression). Canonical EVENT (frozen): EVENT { ts, dir, valid_until, superseded, source } - W_sweep = 40, W_choch = 40 (bars, M15 chain frame). - active iff onset <= r <= onset + W (r = decision bar; valid at age W, EXPIRED at age W+1). - superseded : a newer onset of the same type replaced it (permanent; the older event never reactivates). - chain : sweep onset <= CHoCH onset <= sweep onset + W_sweep; both fresh at r; directions equal. Disiplin P3-S.11: - F1-T01..T09 : ASSERT the canonical contract (oracle = contract). - legacy vs repaired : REPORT (differential before/after observation). - F1-T10 : ASSERT existing P3-S.2/P3-S.3 detector suites still PASS (detection semantics unchanged). - no AUC/PF/backtest/human annotation/ML in this file. Usage: python spec_tests_event_contract.py Output: output/p3_s11_event_contract_report.json """ import json import os import subprocess import sys import numpy as np HERE = os.path.dirname(os.path.abspath(__file__)) # ---- canonical constants (frozen contract P3-S.10 OD-3) ---- W_SWEEP = 40 # Liquidity EVENT validity (M15 bars) W_CHOCH = 40 # CHoCH/MSS EVENT validity (M15 bars) GRAB_WINDOW = 8 # sweep detection window (UNCHANGED, P3-S.2 R-C) INTERNAL_LEN = 5 # fractal confirmation (UNCHANGED, P3-S.3 S-2) # ===================================================================== # CANONICAL EVENT CONTRACT (reference implementation — the contract) # ===================================================================== class AFEvent: """Canonical EVENT {ts=bar, dir, valid_until, superseded, source}.""" __slots__ = ("bar", "dir", "valid_until", "superseded", "source") def __init__(self, bar, direction, valid_until, superseded, source): self.bar = int(bar) self.dir = int(direction) self.valid_until = int(valid_until) self.superseded = bool(superseded) self.source = source def active_at(self, r): """Contract: active iff onset <= r <= valid_until AND not superseded.""" return (not self.superseded) and (self.bar <= r <= self.valid_until) def as_dict(self): return {"ts": self.bar, "dir": self.dir, "valid_until": self.valid_until, "superseded": self.superseded, "source": self.source} def _supersede(events): """A newer onset of the same type permanently supersedes an older event.""" for e in events: if any(x.bar > e.bar for x in events): e.superseded = True return events def detect_sweep_events(h, l, c, refs, decision_bar, grab_window=GRAB_WINDOW, w=W_SWEEP): """Sweep DETECTION (UNCHANGED, mirror DetectLiquidityGrabs / P3-S.2 R-C) + canonical EVENT construction. refs: [(p, lvl, is_high)]. Onset order asc.""" events = [] for (p, lvl, ih) in refs: last = min(decision_bar, p + grab_window) for b in range(p + 1, last + 1): if ih and h[b] > lvl and c[b] < lvl: # buy-side swept -> bearish -1 events.append(AFEvent(b, -1, b + w, False, "sweep")) break if (not ih) and l[b] < lvl and c[b] > lvl: # sell-side swept -> bullish +1 events.append(AFEvent(b, 1, b + w, False, "sweep")) break return _supersede(events) def detect_choch_events(o, h, l, c, refs, swing_breaks, decision_bar, w=W_CHOCH): """CHoCH DETECTION (UNCHANGED, mirror P3-S.3 spec_oracle S-3..S-10) + canonical EVENT construction. refs: [(p, level, is_high)]. swing_breaks: [(bar, trend)]. Returns list[AFEvent] onset asc.""" n = decision_bar + 1 events = [] up_target, up_bar = float("inf"), -1 dn_target, dn_bar = -float("inf"), -1 trend = 0 confirm = {} for (p, lvl, ih) in refs: confirm.setdefault(p + INTERNAL_LEN, []).append((p, lvl, ih)) for i in range(n): for (p, lvl, ih) in confirm.get(i, []): if ih: up_target, up_bar = lvl, p else: dn_target, dn_bar = lvl, p if up_bar >= 0 and c[i] > up_target: choch = (trend < 0) allow = swing_at_of(swing_breaks, i) >= 0 if allow and choch: events.append(AFEvent(i, 1, i + w, False, "choch")) trend = 1 up_target, up_bar = float("inf"), -1 if dn_bar >= 0 and c[i] < dn_target: choch = (trend > 0) allow = swing_at_of(swing_breaks, i) <= 0 if allow and choch: events.append(AFEvent(i, -1, i + w, False, "choch")) trend = -1 dn_target, dn_bar = -float("inf"), -1 return _supersede(events) def swing_at_of(swing_breaks, i): v = 0 for (b, val) in swing_breaks: if b <= i: v = val else: break return v def latest_active(events, r): """Latest (newest onset) event <= r; active iff within its validity window. Supersession is permanent: an older event never reactivates (matches the single-latest-state of the EA).""" cand = [e for e in events if e.bar <= r] if not cand: return None e = max(cand, key=lambda x: x.bar) return e if e.active_at(r) else None def chain_valid(sweep_events, choch_events, r): """Canonical setup chain (frozen §D/§M + OD-4): latest active sweep AND latest active CHoCH, sweep onset <= choch onset <= sweep onset + W_sweep, directions equal.""" swp = latest_active(sweep_events, r) chc = latest_active(choch_events, r) if swp is None or chc is None: return False if chc.bar < swp.bar: return False if (chc.bar - swp.bar) > W_SWEEP: return False if chc.dir != swp.dir: return False return True def f7_contract(sweep_events, r): """f7 sweep_dir under the contract: latest active sweep -> dir else 0.""" swp = latest_active(sweep_events, r) return swp.dir if swp is not None else 0 def f9_contract(sweep_events, choch_events, r): """f9 choch_confirms under the contract: 1 iff the canonical chain holds.""" return 1 if chain_valid(sweep_events, choch_events, r) else 0 def f18_event_legs_contract(sweep_events, choch_events, r, bias): """f18 confluence — the three event-sensitive +15 legs under the contract. (HTF/premium/EQ legs are untouched by F1 and not re-tested here.)""" cb = 0 swp = latest_active(sweep_events, r) swp_valid = swp is not None chain = chain_valid(sweep_events, choch_events, r) if swp_valid and swp.dir != 0: cb += 15 if chain: cb += 15 if swp_valid and ((swp.dir == 1 and bias > 0) or (swp.dir == -1 and bias < 0)): cb += 15 return cb # ===================================================================== # LEGACY PORT (BEFORE) — old EA consumers: raw persistent state, NO validity # ===================================================================== def legacy_f9(sweep_state, choch_state): """OLD EA chochOK (BUG-P3S3-001): cd!=0 && cb>=sb && cd==sd — no expiry.""" sb, sd = sweep_state cb, cd = choch_state return 1 if (cd != 0 and cb >= sb and cd == sd) else 0 def legacy_f18_event_legs(sweep_state, choch_state, bias): """OLD EA f18 event legs (BUG-P3S2-001/BUG-P3S3-001): raw state, no expiry.""" sb, sd = sweep_state cb, cd = choch_state cbx = 0 if sd != 0: cbx += 15 if cd != 0 and cd == sd: cbx += 15 if (sd == 1 and bias > 0) or (sd == -1 and bias < 0): cbx += 15 return cbx # ===================================================================== # HELPERS (synthetic case materialization — same shape as P3-S.2/S.3) # ===================================================================== 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 sweep_state_of(events): """Latest (newest onset) sweep event state (bar, dir), (-1,0) if none.""" if not events: return (-1, 0) e = max(events, key=lambda x: x.bar) return (e.bar, e.dir) def choch_state_of(events): if not events: return (-1, 0) e = max(events, key=lambda x: x.bar) return (e.bar, e.dir) def mutate_future(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 # ===================================================================== # TEST CASES — F1-T01..T09 (canonical contract assertions) # ===================================================================== NEUTRAL = [100.0, 101.5, 99.0, 100.5] # o,h,l,c (small body) # CHoCH scaffolding: bearish internal break at bar 10 (LOW ref 101.0 confirmed # at 9) sets prior internal trend = -1, then a bullish break above HIGH ref # 102.0 produces a bullish CHoCH. Swing timeline flat (permissive gate). CHOCH_REFS = [(4, 101.0, 0), (8, 102.0, 1)] SWING_BREAKS = [(5, 0)] CHOCH_BREAK_BARS = {30: [100.0, 104.0, 100.0, 103.0], 14: [100.0, 104.0, 100.0, 103.0], 55: [100.0, 104.0, 100.0, 103.0]} def case_single_sweep(): """F1-T01 single sweep event + F1-T02 expiration boundary.""" patches = {12: [100.0, 101.0, 97.0, 99.0]} # low<98, close>98 -> +1 at 12 return { "id": "F1-T01", "title": "Single sweep event + boundary (valid at +40, expired at +41)", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0)], "assert": [ ("events_count", lambda evs, cevs, r: len(evs) == 1), ("onset_bar", lambda evs, cevs, r: evs[0].bar == 12), ("onset_dir", lambda evs, cevs, r: evs[0].dir == 1), ("valid_until", lambda evs, cevs, r: evs[0].valid_until == 12 + W_SWEEP), ("superseded_false", lambda evs, cevs, r: not evs[0].superseded), ("active_at_40", lambda evs, cevs, r: f7_contract(evs, 12 + W_SWEEP) == 1), ("expired_at_41", lambda evs, cevs, r: f7_contract(evs, 12 + W_SWEEP + 1) == 0), ("source_sweep", lambda evs, cevs, r: evs[0].source == "sweep"), ], } def case_consumer_expiration(): """F1-T03 sweep consumer expiration: age 41+ -> NO VALID SWEEP EVENT.""" patches = {10: [100.0, 101.0, 100.0, 100.4], # bearish internal break (trend -> -1) 12: [100.0, 101.0, 97.0, 99.0], # sweep +1 at 12 14: CHOCH_BREAK_BARS[14]} # bullish CHoCH +1 at 14 return { "id": "F1-T03", "title": "Sweep consumer expiration (age 43 -> NO VALID SWEEP; legacy f9 still 1)", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0)], "given_choch_references": CHOCH_REFS, "given_swing_breaks": SWING_BREAKS, "assert": [ ("no_active_sweep_at_55", lambda evs, cevs, r: latest_active(evs, 55) is None), ("f7_zero_at_55", lambda evs, cevs, r: f7_contract(evs, 55) == 0), ("f9_zero_at_55", lambda evs, cevs, r: f9_contract(evs, cevs, 55) == 0), ], } def case_supersession(): """F1-T04 new sweep supersedes old (deterministic supersession).""" patches = {12: [100.0, 101.0, 97.0, 99.0], # sweep A +1 at 12 (LOW ref 98) 20: [100.0, 102.0, 98.0, 100.0], # HIGH ref bar 102 22: [100.0, 103.0, 101.0, 101.5]} # sweep B -1 at 22 (high>102, close<102) return { "id": "F1-T04", "title": "New sweep supersedes old (deterministic supersession)", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0), (20, 102.0, 1)], "assert": [ ("two_events", lambda evs, cevs, r: len(evs) == 2), ("old_superseded", lambda evs, cevs, r: evs[0].superseded), ("new_active", lambda evs, cevs, r: not evs[1].superseded), ("latest_is_new", lambda evs, cevs, r: latest_active(evs, 30).bar == 22), ("f7_uses_new", lambda evs, cevs, r: f7_contract(evs, 30) == -1), ("no_fallback_after_expiry", lambda evs, cevs, r: f7_contract(evs, 70) == 0), ], } def case_choch_inside_chain(): """F1-T05 CHoCH inside valid sweep window -> VALID (f9=1).""" patches = {10: [100.0, 101.0, 100.0, 100.4], # bearish internal break at 10 12: [100.0, 101.0, 97.0, 99.0], # sweep +1 at 12 30: CHOCH_BREAK_BARS[30]} # bullish CHoCH +1 at 30 return { "id": "F1-T05", "title": "CHoCH inside valid sweep window -> chain VALID (f9=1)", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0)], "given_choch_references": CHOCH_REFS, "given_swing_breaks": SWING_BREAKS, "assert": [ ("choch_onset_30", lambda evs, cevs, r: latest_active(cevs, 40).bar == 30), ("chain_valid_at_40", lambda evs, cevs, r: f9_contract(evs, cevs, 40) == 1), ("dir_match", lambda evs, cevs, r: latest_active(evs, 40).dir == latest_active(cevs, 40).dir), ], } def case_choch_outside_chain(): """F1-T06 CHoCH outside the valid sweep chain -> INVALID (f9=0; legacy 1).""" patches = {10: [100.0, 101.0, 100.0, 100.4], 12: [100.0, 101.0, 97.0, 99.0], # sweep +1 at 12 55: CHOCH_BREAK_BARS[55]} # bullish CHoCH +1 at 55 (43 bars after) return { "id": "F1-T06", "title": "CHoCH outside sweep validity chain -> INVALID for canonical chain", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0)], "given_choch_references": CHOCH_REFS, "given_swing_breaks": SWING_BREAKS, "assert": [ ("sweep_expired_at_55", lambda evs, cevs, r: f7_contract(evs, 55) == 0), ("chain_invalid", lambda evs, cevs, r: f9_contract(evs, cevs, 55) == 0), ("chain_bound_violated", lambda evs, cevs, r: (55 - 12) > W_SWEEP), ], } def case_choch_expiry(): """F1-T07 CHoCH consumer expiration: stale CHoCH must not confirm.""" patches = {10: [100.0, 101.0, 100.0, 100.4], 12: [100.0, 101.0, 97.0, 99.0], # sweep +1 at 12 30: CHOCH_BREAK_BARS[30]} # bullish CHoCH +1 at 30 return { "id": "F1-T07", "title": "CHoCH consumer expiration (age > W_choch -> f9=0)", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0)], "given_choch_references": CHOCH_REFS, "given_swing_breaks": SWING_BREAKS, "assert": [ ("choch_expired_at_75", lambda evs, cevs, r: latest_active(cevs, 75) is None), ("f9_zero_at_75", lambda evs, cevs, r: f9_contract(evs, cevs, 75) == 0), ], } def case_no_lookahead(): """F1-T08 future-bar mutation must not change past event state.""" patches = {12: [100.0, 101.0, 97.0, 99.0]} # sweep +1 at 12 return { "id": "F1-T08", "title": "No look-ahead: future mutation does not alter past event state", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0)], "decision_bar": 50, "mutations": [(60, [100.0, 110.0, 99.0, 109.0])], # future bearish sweep bar "assert": [ ("state_unchanged_at_50", lambda evs, cevs, r: f7_contract(evs, r) == 1), ("no_lookahead_invariant", lambda evs, cevs, r: len(evs) == 1 and evs[0].bar == 12), ], } def case_persistent_condition(): """F1-T09 persistent condition -> ONE event, not repeated emissions.""" patches = {10: [100.0, 100.0, 98.0, 100.0]} for b in range(12, 52): # condition persists 40 bars patches[b] = [100.0, 101.0, 97.0, 99.0] return { "id": "F1-T09", "title": "Persistent condition -> ONE event (no duplicate emission)", "bar_spec": {"neutral": NEUTRAL, "count": 120, "patches": sorted(patches.items())}, "given_references": [(10, 98.0, 0)], "assert": [ ("one_event", lambda evs, cevs, r: len(evs) == 1), ("active_window_bounded", lambda evs, cevs, r: f7_contract(evs, 51) == 1 and f7_contract(evs, 53) == 0), ], } # ===================================================================== # RUNNER # ===================================================================== def run_case(case): o, h, l, c = materialize(case) refs = [tuple(r) for r in case["given_references"]] db = case.get("decision_bar", case["bar_spec"]["count"] - 1) sweep_events = detect_sweep_events(h, l, c, refs, db) choch_events = [] if "given_choch_references" in case: crefs = [tuple(r) for r in case["given_choch_references"]] sbreaks = [tuple(s) for s in case.get("given_swing_breaks", [])] choch_events = detect_choch_events(o, h, l, c, crefs, sbreaks, db) # F1-T08: future mutation invariance (mutations at bars > db must not change # the event state known at db) if "mutations" in case: o3, h3, l3, c3 = mutate_future(o, h, l, c, case["mutations"], NEUTRAL) evs3 = detect_sweep_events(h3, l3, c3, refs, db) else: evs3 = None checks = {} for (name, fn) in case["assert"]: try: checks[name] = bool(fn(sweep_events, choch_events, db)) except Exception as ex: checks[name] = False checks[name + "_error"] = str(ex) if evs3 is not None: checks["no_lookahead_future_mutation"] = bool( len(evs3) == len(sweep_events) and all(a.bar == b.bar and a.dir == b.dir for a, b in zip(evs3, sweep_events))) # legacy vs contract differential (BEFORE/AFTER) for f9/f18 swp_state = sweep_state_of(sweep_events) chc_state = choch_state_of(choch_events) legacy_f9_v = legacy_f9(swp_state, chc_state) contract_f9_v = f9_contract(sweep_events, choch_events, db) legacy_f18_v = legacy_f18_event_legs(swp_state, chc_state, 1) contract_f18_v = f18_event_legs_contract(sweep_events, choch_events, db, 1) return { "id": case["id"], "title": case["title"], "sweep_events": [e.as_dict() for e in sweep_events], "choch_events": [e.as_dict() for e in choch_events], "latest_active_sweep": latest_active(sweep_events, db).as_dict() if latest_active(sweep_events, db) else None, "f7_contract": f7_contract(sweep_events, db), "f9_contract": contract_f9_v, "f9_legacy": legacy_f9_v, "f18_event_legs_contract": contract_f18_v, "f18_event_legs_legacy": legacy_f18_v, "checks": checks, "pass": all(v for k, v in checks.items() if not k.endswith("_error")), } def regression_p3s2_s3(): """F1-T10: existing detector synthetic suites remain unchanged and PASS.""" ok = True detail = {} for (name, script) in (("P3-S.2 liquidity sweep", "spec_tests_liquidity_sweep.py"), ("P3-S.3 choch/mss", "spec_tests_choch_mss.py")): r = subprocess.run([sys.executable, os.path.join(HERE, script)], capture_output=True, text=True) ok = ok and (r.returncode == 0) detail[name] = {"returncode": r.returncode, "pass": r.returncode == 0} return ok, detail def main(): cases = [case_single_sweep(), case_consumer_expiration(), case_supersession(), case_choch_inside_chain(), case_choch_outside_chain(), case_choch_expiry(), case_no_lookahead(), case_persistent_condition()] 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.11 F1 SPEC TESTS — EVENT CONTRACT ({n_pass}/{n_total} PASS) ===") print(f"{'ID':<8} {'PASS':<6} {'f7':<4} {'f9 c/l':<8} notes") for r in results: print(f"{r['id']:<8} {str(r['pass']):<6} {r['f7_contract']:<4} " f"{str(r['f9_contract'])+'/'+str(r['f9_legacy']):<8} {r['title'][:48]}") for r in results: if not r["pass"]: print(" FAILED:", r["id"], r["checks"]) # F1-T10 regression reg_ok, reg_detail = regression_p3s2_s3() if n_pass != n_total or not reg_ok: print("\nF1 TESTS FAILED") if not reg_ok: print(" F1-T10 regression failed:", reg_detail) sys.exit(1) print("\nAll F1 contract assertions PASS; P3-S.2/P3-S.3 detection suites PASS " "(detection semantics unchanged).") stale_legacy = sum(1 for r in results if r["f9_legacy"] == 1 and r["f9_contract"] == 0) total_f9_on = sum(1 for r in results if r["f9_legacy"] == 1) report = { "doc": "docs/P3_S11_EVENT_CONTRACT_REPAIR.md", "phase": "P3-S.11", "generated_utc": __import__("datetime").datetime.now( __import__("datetime").timezone.utc).isoformat(), "constants": {"W_SWEEP": W_SWEEP, "W_CHOCH": W_CHOCH, "GRAB_WINDOW": GRAB_WINDOW, "INTERNAL_LEN": INTERNAL_LEN}, "summary": {"total": n_total, "passed": n_pass, "failed": n_total - n_pass, "regression": reg_detail, "before_after": { "legacy_f9_active_cases": total_f9_on, "stale_usage_eliminated_cases": stale_legacy, }}, "cases": results, } outdir = os.path.join(HERE, "output") os.makedirs(outdir, exist_ok=True) out_path = os.path.join(outdir, "p3_s11_event_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()