SniperGold_ML/ml/parity/parity_harness.py

266 lines
10 KiB
Python

# -*- 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())