295 lines
12 KiB
Python
295 lines
12 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
P2.1 HTF bias forensic — hypothesis test vs EA runtime dump.
|
||
|
|
|
||
|
|
Tujuan: mengidentifikasi SEMANTIK WINDOW BTTFBias yang benar-benar dipakai EA
|
||
|
|
runtime (AlgoForge_Backtest_Baseline.mq5 mode 2) pada tiap row M15, dan
|
||
|
|
membandingkannya dengan semantik training (train_model.build_features).
|
||
|
|
|
||
|
|
Fakta runtime yang sudah diverifikasi dari kode EA:
|
||
|
|
- Row ditulis pada t = open time bar M15 tertutup terbaru (e1.Time(sM15,0)).
|
||
|
|
- Fitur dihitung pada tick pertama bar M15 baru -> waktu komputasi tc = t + 900s.
|
||
|
|
- Cache Engine 1 = HANYA bar tertutup (closed-bar lock), kapasitas 250 (D1/H4/H1).
|
||
|
|
- BTTFBias: need = min(200, e1.Count(slot)); need < 120 -> "Neutral" (0).
|
||
|
|
- Tester model=0 (every tick), FromDate=2026.01.01 00:00 UTC -> cache TF tinggi
|
||
|
|
KOSONG di awal test (tidak ada history sebelum FromDate di tester).
|
||
|
|
|
||
|
|
Hipotesis semantik window (per HTF, per row t):
|
||
|
|
E_py = containing_htf(t) - 1 (semantik training: lag-1, as-of open t)
|
||
|
|
E_ea = last closed HTF bar at tc (semantik runtime: as-of close tc)
|
||
|
|
H_PY : tf_bias_asof(E_py) # training as-is
|
||
|
|
H_CLOSED : tf_bias_asof(E_ea) # full history, window ends E_ea
|
||
|
|
H_ANCHOR : window [anchor .. E_ea], need=min(200,count), need<120 -> 0
|
||
|
|
H_ANCHOR250: sama + cap cache 250 (need=min(200,min(250,count)))
|
||
|
|
|
||
|
|
Output:
|
||
|
|
- mismatch rate tiap hipotesis vs f0/f1/f2 EA (atas semua row parity)
|
||
|
|
- detail deterministik utk beberapa timestamp terpilih
|
||
|
|
"""
|
||
|
|
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")
|
||
|
|
|
||
|
|
ANCHOR = int(dt.datetime(2026, 1, 1, tzinfo=dt.timezone.utc).timestamp())
|
||
|
|
TRIM = int(dt.datetime(2025, 1, 1, tzinfo=dt.timezone.utc).timestamp()) # cukup utk 200-bar window D1
|
||
|
|
PERIOD = {"D1": 86400, "H4": 14400, "H1": 3600}
|
||
|
|
NEED_MAX = 200
|
||
|
|
NEED_MIN = 120
|
||
|
|
|
||
|
|
|
||
|
|
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 bttf_window(h, l, c, need):
|
||
|
|
"""BTTFBias inti (s=3 fractal + break), window h/l/c (0=tertua), need=len."""
|
||
|
|
if need < NEED_MIN:
|
||
|
|
return 0
|
||
|
|
s = 3
|
||
|
|
m = need - 1
|
||
|
|
up, dn = float("inf"), -float("inf")
|
||
|
|
upb = dnb = -1
|
||
|
|
trend = 0
|
||
|
|
for i in range(s + 1, m):
|
||
|
|
p = i - s
|
||
|
|
if p >= s:
|
||
|
|
isH = isL = True
|
||
|
|
for kk in range(1, s + 1):
|
||
|
|
if h[p] <= h[p + kk] or h[p] <= h[p - kk]:
|
||
|
|
isH = False
|
||
|
|
if l[p] >= l[p + kk] or l[p] >= l[p - kk]:
|
||
|
|
isL = False
|
||
|
|
if isH:
|
||
|
|
up, upb = h[p], p
|
||
|
|
if isL:
|
||
|
|
dn, dnb = l[p], p
|
||
|
|
if upb >= 0 and c[i] > up:
|
||
|
|
trend = 1
|
||
|
|
up, upb = float("inf"), -1
|
||
|
|
if dnb >= 0 and c[i] < dn:
|
||
|
|
trend = -1
|
||
|
|
dn, dnb = -float("inf"), -1
|
||
|
|
return trend
|
||
|
|
|
||
|
|
|
||
|
|
def bias_full(hh, hl, hc, lag):
|
||
|
|
"""bias[k] = bttf window ending at k-lag (full history, need=200)."""
|
||
|
|
n = len(hc)
|
||
|
|
out = np.zeros(n, dtype=int)
|
||
|
|
for k in range(n):
|
||
|
|
end = k - lag
|
||
|
|
if end < 0:
|
||
|
|
continue
|
||
|
|
start = max(0, end - NEED_MAX + 1)
|
||
|
|
out[k] = bttf_window(hh[start:end + 1], hl[start:end + 1],
|
||
|
|
hc[start:end + 1], end - start + 1)
|
||
|
|
return out
|
||
|
|
|
||
|
|
|
||
|
|
def parse_ea_time(s):
|
||
|
|
return int(dt.datetime.strptime(s, "%Y.%m.%d %H:%M")
|
||
|
|
.replace(tzinfo=dt.timezone.utc).timestamp())
|
||
|
|
|
||
|
|
|
||
|
|
def parse_pick_time(s):
|
||
|
|
return int(dt.datetime.strptime(s, "%Y-%m-%d %H:%M")
|
||
|
|
.replace(tzinfo=dt.timezone.utc).timestamp())
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
log("Load data + trim (>= 2025-01-01)...")
|
||
|
|
htf = {}
|
||
|
|
for key in ("D1", "H4", "H1"):
|
||
|
|
ht, ho, hh, hl, hc, hv = load_npz("XAUUSD_" + key)
|
||
|
|
m = ht >= TRIM
|
||
|
|
htf[key] = (ht[m], ho[m], hh[m], hl[m], hc[m])
|
||
|
|
log(f" {key}: n={int(m.sum())} first={dt.datetime.fromtimestamp(int(ht[m][0]), dt.timezone.utc)}")
|
||
|
|
|
||
|
|
log("Load M15 (full, 2017+) + EA CSV...")
|
||
|
|
mt, mo, mh, ml, mc, mv = load_npz("XAUUSD_M15")
|
||
|
|
keep = mt >= int(dt.datetime(2017, 1, 1, tzinfo=dt.timezone.utc).timestamp())
|
||
|
|
mt, mo, mh, ml, mc, mv = mt[keep], mo[keep], mh[keep], ml[keep], mc[keep], mv[keep]
|
||
|
|
|
||
|
|
rows = []
|
||
|
|
with open(EA_CSV, 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())
|
||
|
|
feats = [float(x) for x in r[3:22]]
|
||
|
|
except ValueError:
|
||
|
|
continue
|
||
|
|
rows.append((t, feats))
|
||
|
|
log(f" EA rows={len(rows)} M15(2017+) n={len(mc)}")
|
||
|
|
|
||
|
|
# ---- precompute bias arrays per HTF ----
|
||
|
|
log("Precompute bias arrays...")
|
||
|
|
B = {} # B[key][hyp] = array bias per bar index
|
||
|
|
HYP = ["PY", "CLOSED", "ANCHOR", "ANCHOR250"]
|
||
|
|
for key in ("D1", "H4", "H1"):
|
||
|
|
ht, ho, hh, hl, hc = htf[key]
|
||
|
|
B[key] = {}
|
||
|
|
B[key]["PY"] = bias_full(hh, hl, hc, lag=1)
|
||
|
|
log(f" {key} PY done")
|
||
|
|
B[key]["CLOSED"] = bias_full(hh, hl, hc, lag=0)
|
||
|
|
log(f" {key} CLOSED done")
|
||
|
|
a_idx = int(np.searchsorted(ht, ANCHOR, side="left"))
|
||
|
|
n = len(hc)
|
||
|
|
for hyp, cap in (("ANCHOR", None), ("ANCHOR250", 250)):
|
||
|
|
out = np.zeros(n, dtype=int)
|
||
|
|
for k in range(a_idx, n):
|
||
|
|
cnt = k - a_idx + 1
|
||
|
|
if cap is not None:
|
||
|
|
cnt = min(cnt, cap)
|
||
|
|
need = min(NEED_MAX, cnt)
|
||
|
|
if need < NEED_MIN:
|
||
|
|
continue
|
||
|
|
start = max(a_idx, k - NEED_MAX + 1)
|
||
|
|
out[k] = bttf_window(hh[start:k + 1], hl[start:k + 1],
|
||
|
|
hc[start:k + 1], need)
|
||
|
|
B[key][hyp] = out
|
||
|
|
log(f" {key} {hyp} done")
|
||
|
|
|
||
|
|
# ---- verifikasi pipeline: hitung py_full persis spt harness (TM.build_features) ----
|
||
|
|
log("Verifikasi vs TM.build_features (harness py_full)...")
|
||
|
|
htf_full = {}
|
||
|
|
for key in ("D1", "H4", "H1"):
|
||
|
|
z = np.load(os.path.join(DATA, "XAUUSD_" + key + ".npz"))
|
||
|
|
ht_, ho_, hh_, hl_, hc_ = (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))
|
||
|
|
htf_full[key] = (hh_, hl_, hc_, ht_)
|
||
|
|
F, label, A, _ = TM.build_features((mt, mo, mh, ml, mc, mv), htf_full)
|
||
|
|
py_idx = {int(tt): i for i, tt in enumerate(mt)}
|
||
|
|
diff_py = {k: 0 for k in ("D1", "H4", "H1")}
|
||
|
|
nchk = 0
|
||
|
|
for (t, feats) in rows:
|
||
|
|
bi = py_idx.get(t)
|
||
|
|
if bi is None:
|
||
|
|
continue
|
||
|
|
nchk += 1
|
||
|
|
for fi, key in enumerate(("D1", "H4", "H1")):
|
||
|
|
if int(F[bi][fi]) != int(B[key]["PY"][int(np.searchsorted(htf[key][0], t, side="right")) - 1]):
|
||
|
|
diff_py[key] += 1
|
||
|
|
log(f" [check] PY-mapping vs harness py_full: n={nchk} diff D1={diff_py['D1']} H4={diff_py['H4']} H1={diff_py['H1']}")
|
||
|
|
|
||
|
|
# ---- uji agregasi: HTF dari M15 (harus == npz HTF bila feed tester konsisten) ----
|
||
|
|
log("Uji agregasi HTF dari M15 vs npz HTF...")
|
||
|
|
for key, per in (("H1", 3600), ("H4", 14400), ("D1", 86400)):
|
||
|
|
bucket = (mt // per) * per
|
||
|
|
idx_sort = np.argsort(bucket, kind="stable")
|
||
|
|
bs, a_o = bucket[idx_sort], mo[idx_sort]
|
||
|
|
a_h = mh[idx_sort]
|
||
|
|
a_l = ml[idx_sort]
|
||
|
|
a_c = mc[idx_sort]
|
||
|
|
uniq, starts = np.unique(bs, return_index=True)
|
||
|
|
ends = np.append(starts[1:], len(bs))
|
||
|
|
g_o = np.array([a_o[s] for s in starts])
|
||
|
|
g_h = np.array([a_h[s:e].max() for s, e in zip(starts, ends)])
|
||
|
|
g_l = np.array([a_l[s:e].min() for s, e in zip(starts, ends)])
|
||
|
|
g_c = np.array([a_c[e - 1] for e in ends])
|
||
|
|
ht_, ho_, hh_, hl_, hc_ = htf[key][0], htf[key][1], htf[key][2], htf[key][3], htf[key][4]
|
||
|
|
common = np.intersect1d(uniq, ht_)
|
||
|
|
jj = np.searchsorted(uniq, common)
|
||
|
|
jn = np.searchsorted(ht_, common)
|
||
|
|
d_o = float(np.max(np.abs(g_o[jj] - ho_[jn])))
|
||
|
|
d_h = float(np.max(np.abs(g_h[jj] - hh_[jn])))
|
||
|
|
d_l = float(np.max(np.abs(g_l[jj] - hl_[jn])))
|
||
|
|
d_c = float(np.max(np.abs(g_c[jj] - hc_[jn])))
|
||
|
|
log(f" {key}: M15-agg vs npz max|d| open={d_o:.5f} high={d_h:.5f} low={d_l:.5f} "
|
||
|
|
f"close={d_c:.5f} (n={len(jn)})")
|
||
|
|
|
||
|
|
# ---- per row: nilai EA vs tiap hipotesis ----
|
||
|
|
log("Evaluate hypotheses per row...")
|
||
|
|
mis = {h: {k: 0 for k in ("D1", "H4", "H1")} for h in HYP}
|
||
|
|
tot = {k: 0 for k in ("D1", "H4", "H1")}
|
||
|
|
best = {k: {h: 0 for h in HYP + ["NONE"]} for k in ("D1", "H4", "H1")}
|
||
|
|
for (t, feats) in rows:
|
||
|
|
tc = t + 900
|
||
|
|
for fi, key in enumerate(("D1", "H4", "H1")):
|
||
|
|
ht, ho, hh, hl, hc = htf[key]
|
||
|
|
ea_val = int(feats[fi])
|
||
|
|
per = PERIOD[key]
|
||
|
|
cidx = int(np.searchsorted(ht, t, side="right")) - 1
|
||
|
|
e_ea = int(np.searchsorted(ht, tc - per, side="right")) - 1
|
||
|
|
tot[key] += 1
|
||
|
|
vals = {}
|
||
|
|
if 0 <= cidx < len(hc):
|
||
|
|
vals["PY"] = int(B[key]["PY"][cidx])
|
||
|
|
else:
|
||
|
|
vals["PY"] = 0
|
||
|
|
if 0 <= e_ea < len(hc):
|
||
|
|
vals["CLOSED"] = int(B[key]["CLOSED"][e_ea])
|
||
|
|
vals["ANCHOR"] = int(B[key]["ANCHOR"][e_ea])
|
||
|
|
vals["ANCHOR250"] = int(B[key]["ANCHOR250"][e_ea])
|
||
|
|
else:
|
||
|
|
vals["CLOSED"] = vals["ANCHOR"] = vals["ANCHOR250"] = 0
|
||
|
|
for h in HYP:
|
||
|
|
if vals[h] != ea_val:
|
||
|
|
mis[h][key] += 1
|
||
|
|
hit = [h for h in HYP if vals[h] == ea_val]
|
||
|
|
best[key][hit[0] if hit else "NONE"] += 1
|
||
|
|
|
||
|
|
log("")
|
||
|
|
log("=== MISMATCH RATE vs EA (f0/f1/f2) ===")
|
||
|
|
log(f"{'hypothesis':10s} {'D1(f0)':>10s} {'H4(f1)':>10s} {'H1(f2)':>10s}")
|
||
|
|
for h in HYP:
|
||
|
|
log(f"{h:10s} {mis[h]['D1']/tot['D1']:10.4f} {mis[h]['H4']/tot['H4']:10.4f} {mis[h]['H1']/tot['H1']:10.4f}")
|
||
|
|
|
||
|
|
log("")
|
||
|
|
log("=== BEST MATCH per row (fraksi row yg cocok dgn tiap hipotesis) ===")
|
||
|
|
log(f"{'hypothesis':10s} {'D1(f0)':>10s} {'H4(f1)':>10s} {'H1(f2)':>10s}")
|
||
|
|
for h in HYP + ["NONE"]:
|
||
|
|
log(f"{h:10s} {best['D1'][h]/tot['D1']:10.4f} {best['H4'][h]/tot['H4']:10.4f} {best['H1'][h]/tot['H1']:10.4f}")
|
||
|
|
|
||
|
|
# ---- detail deterministik ----
|
||
|
|
picks = [
|
||
|
|
"2026-01-05 12:00",
|
||
|
|
"2026-03-18 01:00",
|
||
|
|
"2026-04-01 00:00",
|
||
|
|
"2026-06-16 13:30",
|
||
|
|
"2026-07-20 15:45",
|
||
|
|
]
|
||
|
|
pick_ts = [parse_pick_time(p) for p in picks]
|
||
|
|
log("")
|
||
|
|
log("=== DETAIL TIMESTAMP TERPILIH (PY vs CLOSED vs ANCHOR vs EA) ===")
|
||
|
|
for t in pick_ts:
|
||
|
|
tc = t + 900
|
||
|
|
log(f"row t={dt.datetime.fromtimestamp(t, dt.timezone.utc)} tc={dt.datetime.fromtimestamp(tc, dt.timezone.utc)}")
|
||
|
|
for fi, key in enumerate(("D1", "H4", "H1")):
|
||
|
|
ht, ho, hh, hl, hc = htf[key]
|
||
|
|
per = PERIOD[key]
|
||
|
|
cidx = int(np.searchsorted(ht, t, side="right")) - 1
|
||
|
|
e_ea = int(np.searchsorted(ht, tc - per, side="right")) - 1
|
||
|
|
log(f" {key:3s} contain={cidx}({dt.datetime.fromtimestamp(int(ht[cidx]), dt.timezone.utc).strftime('%m-%d')}) "
|
||
|
|
f"E_ea={e_ea}({dt.datetime.fromtimestamp(int(ht[e_ea]), dt.timezone.utc).strftime('%m-%d')}) "
|
||
|
|
f"PY={int(B[key]['PY'][cidx])} CLOSED={int(B[key]['CLOSED'][e_ea])} "
|
||
|
|
f"ANCHOR={int(B[key]['ANCHOR'][e_ea])} ANCHOR250={int(B[key]['ANCHOR250'][e_ea])} "
|
||
|
|
f"EA={int(feats[fi])}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|