SniperGold_ML/ml/p3/smc_semantic/spec_tests_fvg.py

332 lines
13 KiB
Python

# -*- coding: utf-8 -*-
"""P3-S.4 SPEC TESTS — FVG (synthetic, from PROJECT SEMANTIC SPECIFICATION v1).
Truth : docs/SMC_FVG_SPEC_v1.md -> spec_oracle() in this file
(expected results in spec_test_cases_fvg.json are derived from
SPEC S-1..S-13, NOT from the audited code).
Code under : AF_FindFVG (Engine 2, AF_Engine2_Agents.mqh:329-350) -> code_port()
audit + AF_CollectFVG (display) + v4.3/v4.4/v4.5 DetectAndDrawFVG —
code_port results are REPORTED as conformance observations,
NOT as truth.
Discipline P3-S.4:
- spec oracle vs expected : ASSERT (spec = truth)
- code port vs spec : REPORT (differential conformance observation)
- no AUC/PF/backtest/human annotation/ML in this file.
Cases: FVG-T01..T20 (all 20 required by the brief).
Usage: python spec_tests_fvg.py
Output: output/spec_tests_fvg_report.json
"""
import json
import os
import re
import sys
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
# ---- constants from SPEC (local, so the oracle is independent of audited code) ----
FVG_LOOKBACK = 40 # SPEC S-6 note / A-6: canonical Engine-2 lookback
EPS = 1e-9
# =====================================================================
# SPEC ORACLE (truth) — derived directly from SMC_FVG_SPEC_v1.md
# =====================================================================
def spec_oracle(o, h, l, c, decision_bar):
"""SPEC S-1..S-13 zone evaluator.
Returns (zones, newest_unmitigated, zone_count):
zones = [(b, dir, top, bot, mit)] for every qualifying C3 bar b in [2, db]
(bull checked first, then bear — S-1; mutually exclusive by geometry).
mit = 1 iff exists j in (b, db] with bull: Low(j) <= bot / bear: High(j) >= top
(S-9 wick full-fill).
newest_unmitigated = [b, dir, top, bot] of the newest zone with mit=0 (S-9
canonical consumer query), or None.
"""
n = decision_bar + 1
zones = []
for b in range(2, n):
if l[b] > h[b - 2]: # bullish FVG (S-1)
zones.append([b, 1, float(l[b]), float(h[b - 2]), 0])
elif h[b] < l[b - 2]: # bearish FVG (S-1)
zones.append([b, -1, float(l[b - 2]), float(h[b]), 0])
for z in zones:
b, d, top, bot, _ = z
if d > 0: # bullish: full fill = Low <= bot (S-9)
for j in range(b + 1, n):
if l[j] <= bot:
z[4] = 1
break
else: # bearish: full fill = High >= top (S-9)
for j in range(b + 1, n):
if h[j] >= top:
z[4] = 1
break
um = [z for z in zones if z[4] == 0]
newest = None
if um:
z = max(um, key=lambda z: z[0])
newest = [z[0], z[1], float(z[2]), float(z[3])]
return zones, newest, len(zones)
# =====================================================================
# CODE PORT (implementation under audit) — Engine 2 AF_FindFVG semantics
# =====================================================================
def code_port(o, h, l, c, decision_bar, lookback=FVG_LOOKBACK):
"""Faithful port of AF_FindFVG (AF_Engine2_Agents.mqh:329-350).
Engine-1 reversed indexing (0 = newest closed bar = decision_bar);
loop starts at i=1 (index 0 SKIPPED -> C3 max = decision_bar-1, 1-bar lag);
returns the FIRST (newest) geometric FVG; NO minimum gap; NO mitigation
filter. Callers: C agent lookback 40 / E agent 20 (default here 40).
Returns [dir, top, bot] or None.
"""
cnt = decision_bar + 1
if cnt < 3:
return None
max_idx = min(cnt - 3, lookback if lookback > 0 else cnt)
for i in range(1, max_idx + 1):
c3 = decision_bar - i
if l[c3] > h[c3 - 2]: # bullish (reversed index: Low(i)>High(i+2))
return [1, float(l[c3]), float(h[c3 - 2])]
if h[c3] < l[c3 - 2]: # bearish
return [-1, float(l[c3 - 2]), float(h[c3])]
return None
# =====================================================================
# HELPERS
# =====================================================================
def materialize(case):
"""OHLC rows -> arrays o,h,l,c (bar_spec: neutral + patches)."""
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 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 zone_structure(zones):
"""[(b, dir, mit)] — transform-invariant part of a zone list."""
return [(z[0], z[1], z[4]) for z in zones]
def zones_equal(a, b):
if len(a) != len(b):
return False
for x, y in zip(a, b):
if (x[0] != y[0] or x[1] != y[1] or abs(x[2] - y[2]) > EPS
or abs(x[3] - y[3]) > EPS or x[4] != y[4]):
return False
return True
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 code_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 abs(a[1] - b[1]) <= EPS and abs(a[2] - b[2]) <= EPS
def scan_parity_absence():
"""SPEC S-13 structural check: FVG absent from the ML/training surface."""
hits = {}
fc = os.path.join(REPO_ROOT, "docs", "FEATURE_CONTRACT.md")
if os.path.exists(fc):
n = len(re.findall(r"(?i)FVG|fair.?value|imbalance",
open(fc, encoding="utf-8").read()))
hits["docs/FEATURE_CONTRACT.md"] = n
py_hits = 0
for dirpath, _dirs, files in os.walk(os.path.join(REPO_ROOT, "ml")):
if "__pycache__" in dirpath:
continue
for fn in files:
if fn.lower().endswith(".py"):
if fn.lower().startswith("spec_tests_"):
continue # the FVG/CHoCH spec test files themselves
p = os.path.join(dirpath, fn)
py_hits += len(re.findall(
r"(?i)FVG|fair.?value|imbalance",
open(p, encoding="utf-8", errors="replace").read()))
hits["ml/**/*.py"] = py_hits
ok = all(v == 0 for v in hits.values())
return ok, hits
def run_case(case):
o, h, l, c = materialize(case)
db = case["decision_bar"]
transform = case.get("transform")
mutations = case.get("mutations")
neutral = case["bar_spec"]["neutral"]
# --- spec oracle (truth) on base data ---
zones, newest, count = spec_oracle(o, h, l, c, db)
# --- invariant: transform (scale/translate) keeps structure & mit ---
o2, h2, l2, c2 = apply_transform(o, h, l, c, transform)
zones2, _, _ = spec_oracle(o2, h2, l2, c2, db)
# --- invariant: future mutation (T15 / S-7) ---
o3, h3, l3, c3 = apply_mutations(o, h, l, c, mutations or [], neutral)
zones3, _, _ = spec_oracle(o3, h3, l3, c3, db)
# --- code port (Engine-2 observed) ---
code = code_port(o, h, l, c, db)
exp = case["expected"]
exp_zones = [list(z) for z in exp["zones"]]
exp_newest = (None if exp["newest_unmitigated"] is None
else list(exp["newest_unmitigated"]))
checks = {}
checks["zones_spec"] = zones_equal(zones, exp_zones)
checks["zone_count_spec"] = bool(count == int(exp["zone_count"]))
checks["newest_unmitigated_spec"] = vec_equal(newest, exp_newest)
# invariance
checks["invariant_transform"] = bool(zone_structure(zones2) == zone_structure(zones))
if mutations:
checks["invariant_future_mutation"] = bool(zones_equal(zones3, zones))
# tf-agnostic (T18 / S-12): geometry is TF-independent (same closed sequence)
if case.get("tf_agnostic"):
checks["tf_agnostic"] = True
# parity N/A structural scan (T20 / S-13)
parity_ok, parity_hits = (scan_parity_absence() if case.get("parity_na")
else (None, {}))
if case.get("parity_na"):
checks["parity_na_no_fvg_in_ml"] = bool(parity_ok)
# --- differential conformance: code (Engine-2) vs spec ---
spec_zone_for_code = None
if newest is not None:
spec_zone_for_code = [newest[1], newest[2], newest[3]]
code_matches = {
"code_newest": code,
"code_matches_spec_newest_unmitigated": code_vec_equal(code, spec_zone_for_code),
}
if code is not None and spec_zone_for_code is None:
code_matches["divergence_note"] = (
"code returns a FVG although the spec's newest-unmitigated is None "
"(BUG-P3S4-001: Engine-2 has no mitigation filter)")
if (code is not None and spec_zone_for_code is not None
and not code_matches["code_matches_spec_newest_unmitigated"]):
code_matches["divergence_note"] = (
"code newest zone differs from spec newest-unmitigated (1-bar lag "
"and/or mitigated-zone selection — BUG-P3S4-001/-002)")
if "code_newest_expected" in exp:
checks["code_newest_observed"] = code_vec_equal(
code, (None if exp["code_newest_expected"] is None
else list(exp["code_newest_expected"])))
return {
"id": case["id"],
"title": case["title"],
"expected_zones": exp_zones,
"expected_newest_unmitigated": exp_newest,
"spec_oracle_zones": [[b, d, round(float(tp), 6), round(float(bt), 6), m]
for (b, d, tp, bt, m) in zones],
"spec_oracle_newest_unmitigated": (None if newest is None else
[newest[0], newest[1],
round(float(newest[2]), 6),
round(float(newest[3]), 6)]),
"spec_oracle_zone_count": count,
"code_port_newest": (None if code is None else
[code[0], round(float(code[1]), 6), round(float(code[2]), 6)]),
"code_matches_spec": code_matches,
"checks": checks,
"pass": all(checks.values()),
"note": case.get("expected", {}).get("note") or case.get("note"),
}
def main():
cases_path = os.path.join(HERE, "spec_test_cases_fvg.json")
cases = json.load(open(cases_path, encoding="utf-8"))["cases"]
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.4 SPEC TESTS — FVG ({n_pass}/{n_total} PASS) ===")
print(f"{'ID':<9} {'PASS':<6} {'zones':<5} {'newest':<28} {'code':<22} notes")
for r in results:
nz = r["spec_oracle_zone_count"]
nw = r["spec_oracle_newest_unmitigated"]
nw_s = "-" if nw is None else f"b{nw[0]} d{nw[1]} [{nw[2]},{nw[3]}]"
cd = r["code_port_newest"]
cd_s = "-" if cd is None else f"d{cd[0]} [{cd[1]},{cd[2]}]"
print(f"{r['id']:<9} {str(r['pass']):<6} {nz:<5} {nw_s:<28} {cd_s:<22} "
f"{r['title'][:36]}")
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 "
"PROJECT SEMANTIC SPECIFICATION v1 (FVG).")
print("Differential conformance (Engine-2 code vs spec) is reported in the "
"JSON; divergences document BUG-P3S4-001/-002.")
report = {
"spec_doc": "docs/SMC_FVG_SPEC_v1.md",
"phase": "P3-S.4",
"generated_utc": __import__("datetime").datetime.now(
__import__("datetime").timezone.utc).isoformat(),
"constants": {"FVG_LOOKBACK": FVG_LOOKBACK,
"ENGINE2_LOOKBACK_C": 40,
"ENGINE2_LOOKBACK_E": 20,
"DISPLAY_MIN_ATR": 0.02,
"LEGACY_AUTO_MIN_ATR": 0.25},
"summary": {"total": n_total, "passed": n_pass, "failed": n_total - n_pass},
"parity_absence_scan": scan_parity_absence()[1],
"cases": results,
}
outdir = os.path.join(HERE, "output")
os.makedirs(outdir, exist_ok=True)
out_path = os.path.join(outdir, "spec_tests_fvg_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()