forked from chiki2bum2/SniperGold_ML
266 lines
10 KiB
Python
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())
|