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