# -*- coding: utf-8 -*- """ SB-06 parity harness: RUNTIME (EA) vs TRAINING (Python) feature parity. Membandingkan vektor fitur 19 SMC pada timestamp yang sama: - Python : train_model.build_features (definisi training; warmup 2017+) - EA : AlgoForge_Backtest_Baseline.mq5 mode 2 (dump per bar M15 tertutup, cache 700 bar -> analisis indeks 100..699 = 600 bar terakhir) Membedakan dua sumber perbedaan: A. Historical window difference : py_full (dari 2017) vs py_windowed (700 bar) B. Feature formula difference : py_windowed vs EA (semantik window sama) py_windowed mereplikasi cache EA secara PERSIS: slice 700 bar berakhir di t (baris t-699..t), begin=100 default -> identik dgn array EA (700 bar, begin 100). Toleransi numerik per fitur (eps representasional float64): f0-f5, f7-f12, f18 : 1e-9 (fitur diskrit) f6, f13-f17 : 1e-6 (fitur kontinu) Output: RUNTIME_TRAINING_PARITY_REPORT.md (folder ini) + ringkasan console. """ import os import sys import csv import datetime as dt import numpy as np HERE = os.path.dirname(os.path.abspath(__file__)) SRC_TM = os.path.normpath(os.path.join(HERE, "..", "..", "..", "SniperGold_ML")) sys.path.insert(0, SRC_TM) import train_model as TM # noqa: E402 DATA = os.path.normpath(os.path.join(HERE, "..", "..", "..", "..", "Files", "AlgoForge", "Data")) EA_CSV = os.path.join(HERE, "AlgoForge_bt_features_XAUUSD_M15.csv") REPORT = os.path.join(HERE, "RUNTIME_TRAINING_PARITY_REPORT.md") WARMUP = int(dt.datetime(2017, 1, 1).replace(tzinfo=dt.timezone.utc).timestamp()) EA_CACHE_BARS = 700 # cache EA (InpMaxBars) -> analisis indeks 100..699 FEAT_NAMES = ["f0_htf1", "f1_htf2", "f2_htf3", "f3_swing", "f4_internal", "f5_bias", "f6_eqpos", "f7_sweep", "f8_choch", "f9_chochok", "f10_eqh", "f11_eql", "f12_dsign", "f13_dmag", "f14_dhigh", "f15_dlow", "f16_mom20", "f17_range", "f18_conf"] EPS = [1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-6, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-9, 1e-6, 1e-6, 1e-6, 1e-6, 1e-6, 1e-6] WINDOW_CLASSIFY_MAX = 40 def log(msg): print(msg, flush=True) def load_npz(name): z = np.load(os.path.join(DATA, name + ".npz")) return (z["time"].astype(np.int64), z["open"].astype(np.float64), z["high"].astype(np.float64), z["low"].astype(np.float64), z["close"].astype(np.float64), z["tick_volume"].astype(np.float64)) def parse_ea_time(s): return int(dt.datetime.strptime(s, "%Y.%m.%d %H:%M") .replace(tzinfo=dt.timezone.utc).timestamp()) def load_ea(path): rows = [] with open(path, "r", encoding="utf-8-sig") as f: rdr = csv.reader(f, delimiter="\t") next(rdr, None) for r in rdr: if len(r) < 22: continue try: t = parse_ea_time(r[0].strip()) close = float(r[1]) atr = float(r[2]) feats = [float(x) for x in r[3:22]] except ValueError: continue rows.append((t, close, atr, feats)) return rows def trunc_htf(htf, t_now): out = {} for key, (hh, hl, hc, ht) in htf.items(): j = np.searchsorted(ht, t_now, side="right") out[key] = (hh[:j], hl[:j], hc[:j], ht[:j]) return out def py_windowed_at(t_idx, t, o, h, l, c, v, htf): """Replikasi cache EA: slice 700 bar berakhir di t_idx, begin=100 (default).""" s = max(0, t_idx - (EA_CACHE_BARS - 1)) e = t_idx + 1 m15w = (t[s:e], o[s:e], h[s:e], l[s:e], c[s:e], v[s:e]) htf_w = trunc_htf(htf, t[t_idx]) Fw, _, _, _ = TM.build_features(m15w, htf_w) return Fw[e - s - 1] def main(): log("Memuat data AlgoForge (npz)...") t, o, h, l, c, v = load_npz("XAUUSD_M15") htf = {} for key in ("D1", "H4", "H1"): ht, ho, hh, hl, hc, hv = load_npz("XAUUSD_" + key) htf[key] = (hh, hl, hc, ht) keep = t >= WARMUP t, o, h, l, c, v = t[keep], o[keep], h[keep], l[keep], c[keep], v[keep] n = len(c) log(f" M15 window (2017+): n={n}") log("Menghitung fitur Python full (1 stream, begin=100)...") F, label, A, _ = TM.build_features((t, o, h, l, c, v), htf) py_time = t py_idx = {int(tt): i for i, tt in enumerate(py_time)} log("Memuat dump EA...") ea = load_ea(EA_CSV) log(f" rows EA={len(ea)}") joined = [] tmiss = [] for r in ea: tt = r[0] if tt in py_idx: joined.append((py_idx[tt], r)) else: tmiss.append(tt) log(f" timestamp intersection={len(joined)} missing={len(tmiss)}") if len(joined) == 0: log("FATAL: tidak ada timestamp yang cocok (cek timezone/format).") return 1 feats_py = np.empty((len(joined), 19)) feats_ea = np.empty((len(joined), 19)) close_py = np.empty(len(joined)) close_ea = np.empty(len(joined)) atr_py = np.empty(len(joined)) atr_ea = np.empty(len(joined)) bar_idx = np.empty(len(joined), dtype=np.int64) for k, (bi, r) in enumerate(joined): feats_py[k] = F[bi] feats_ea[k] = r[3] close_py[k] = c[bi] close_ea[k] = r[1] atr_py[k] = A[bi] atr_ea[k] = r[2] bar_idx[k] = bi lines = [] lines.append("# RUNTIME_TRAINING_PARITY_REPORT (SB-06)") lines.append("") lines.append("- Source Python: `train_model.build_features` (train_model.py, hash 1B967C76...), warmup 2017+, data Files\\\\AlgoForge\\\\Data") lines.append("- Source EA : `AlgoForge_Backtest_Baseline.mq5` mode 2 (cache 700 bar, analisis 600 bar), XAUUSD M15, 2026.01.01-08.20") lines.append(f"- Rows Python (2017+, n) : {n}") lines.append(f"- Rows EA (dump) : {len(ea)}") lines.append(f"- Rows timestamp intersection : {len(joined)}") lines.append(f"- Exact timestamp match : {len(joined)}") lines.append(f"- Timestamp mismatch : {len(tmiss)}") lines.append("") lines.append("## Per-feature (py_full vs EA)") lines.append("") lines.append("| Feature | MeanAbsDiff | MaxAbsDiff | MismatchRate | eps |") lines.append("|---|---|---|---|---|") mism_by_feat = [] for j in range(19): d = np.abs(feats_py[:, j] - feats_ea[:, j]) rate = float((d > EPS[j]).mean()) lines.append(f"| {FEAT_NAMES[j]} | {d.mean():.6e} | {d.max():.6e} | {rate:.4f} | {EPS[j]:.0e} |") mism_by_feat.append((j, rate, d.max())) lines.append("") d_close = np.abs(close_py - close_ea) d_atr = np.abs(atr_py - atr_ea) lines.append(f"- Feed sanity close: max|d|={d_close.max():.4f} mean|d|={d_close.mean():.4f}") lines.append(f"- Feed sanity atr : max|d|={d_atr.max():.4f} mean|d|={d_atr.mean():.4f}") lines.append("") # ---- klasifikasi mismatch (A=window vs B=formula) ---- lines.append("## Klasifikasi sumber mismatch (A=window / B=formula)") lines.append("") flagged = [(j, rate) for j, rate, _ in mism_by_feat if rate > 0.001] if flagged: lines.append("Fitur dgn mismatch_rate>0.001 (py_full vs EA): " + ", ".join(f"{FEAT_NAMES[j]} ({rate:.3f})" for j, rate in flagged)) else: lines.append("Tidak ada fitur dgn mismatch_rate>0.001 (py_full vs EA).") lines.append("") cand = [] for j, rate, _ in mism_by_feat: if rate > 0.001: d = np.abs(feats_py[:, j] - feats_ea[:, j]) order = np.argsort(-d)[:8] for k in order: if d[k] > EPS[j]: cand.append((int(bar_idx[k]), j, float(feats_py[k, j]), float(feats_ea[k, j]), float(d[k]))) cand = sorted(cand, key=lambda x: -x[4])[:WINDOW_CLASSIFY_MAX] if cand: lines.append(f"### Uji windowed (py_windowed vs EA) pd {len(cand)} mismatch terbesar") lines.append("") lines.append("| bar_idx | time | feature | py_full | EA | abs | py_windowed | win-vs-EA | klasifikasi |") lines.append("|---|---|---|---|---|---|---|---|---|") a_explained = 0 b_formula = 0 for bi, j, pf, pe, dd in cand: try: Fw = py_windowed_at(bi, t, o, h, l, c, v, htf) pw = float(Fw[j]) except Exception: pw = float("nan") de_w = abs(pw - pe) if not np.isnan(pw) else float("nan") if not np.isnan(de_w) and de_w <= EPS[j]: cls = "A-window" a_explained += 1 else: cls = "B-formula" b_formula += 1 tm_str = dt.datetime.fromtimestamp(int(py_time[bi]), dt.timezone.utc).strftime("%Y-%m-%d %H:%M") lines.append(f"| {bi} | {tm_str} | {FEAT_NAMES[j]} | {pf:.6g} | {pe:.6g} | {dd:.2e} | " f"{pw:.6g} | {de_w:.2e} | {cls} |") lines.append("") lines.append(f"Klasifikasi: {a_explained} x A-window, {b_formula} x B-formula (bukan window).") lines.append("") # ---- top-20 mismatch terbesar (seluruh fitur, py_full vs EA) ---- lines.append("## Top-20 mismatch terbesar (py_full vs EA)") lines.append("") all_d = [] for j in range(19): d = np.abs(feats_py[:, j] - feats_ea[:, j]) order = np.argsort(-d)[:20] for k in order: all_d.append((float(d[k]), int(bar_idx[k]), j, float(feats_py[k, j]), float(feats_ea[k, j]))) all_d = sorted(all_d, key=lambda x: -x[0])[:20] lines.append("| timestamp | feature | python_value | ea_value | abs_diff |") lines.append("|---|---|---|---|---|") for dd, bi, j, pf, pe in all_d: tm_str = dt.datetime.fromtimestamp(int(py_time[bi]), dt.timezone.utc).strftime("%Y-%m-%d %H:%M") lines.append(f"| {tm_str} | {FEAT_NAMES[j]} | {pf:.6g} | {pe:.6g} | {dd:.2e} |") lines.append("") with open(REPORT, "w", encoding="utf-8") as f: f.write("\n".join(lines)) log(f"\nReport tersimpan: {REPORT}") log("Ringkasan per fitur (py_full vs EA):") for j in range(19): d = np.abs(feats_py[:, j] - feats_ea[:, j]) rate = float((d > EPS[j]).mean()) log(f" {FEAT_NAMES[j]:12s} mean|d|={d.mean():.3e} max|d|={d.max():.3e} rate={rate:.4f}") log(f"Feed close max|d|={d_close.max():.4f} | atr max|d|={d_atr.max():.4f}") if cand: log(f"Klasifikasi windowed: A-window={a_explained} B-formula={b_formula}") return 0 if __name__ == "__main__": sys.exit(main())