SniperGold_ML/ml/train_hybrid.py

560 lines
22 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
"""
Algo Forge - Tahap 4 sub-sesi 3: train_hybrid.py
==================================================
LSTM 2-LAPIS + 2-HEAD (P(long), P(short)) numpy murni, walk-forward multi-split,
multi-seed [42,7,123,2024,999], bootstrap paired CI 2000x, gate G1-G7
vs baseline MLP (long 0.6270 / short 0.6207) like-for-like pd cache XAUUSDc.
Data:
- BARU : Files\\AlgoForge\\Data\\features_XAUUSD.npz (F19 + F2-6 regime,
window padat XAUUSD M15 2018+; split_bar=148470, split_pos=115631)
- BASELINE (read-only, gate) : Files\\SniperGold_ML\\features_XAUUSDc.npz
Mode:
--selftest : unit test B1-B5 (2-lapis forward/BPTT/gradcheck/determinisme/
anti-lookahead/overfit)
--calib : 1 seed pd subset train (kalibrasi waktu & sanity AUC)
--gate : like-for-like pd cache XAUUSDc (5 seed) vs 0.6270/0.6207
--wf : walk-forward pd data baru (75/25 + 70/30 + 80/20, 5 seed,
[F19] vs [F19+F2]) mahal, jalankan bila budget cukup
Anti-lookahead: fitur/standarisasi train-only; sequence bar t hanya [t-W+1..t];
test disentuh SEKALI di akhir.
"""
import os
import sys
import math
import json
import time
import argparse
import datetime as dt
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.normpath(os.path.join(HERE, "..", "..", "SniperGold_ML"))
sys.path.insert(0, SRC)
import train_model as TM # noqa: E402 (fitur, auc, MLP baseline)
import train_lstm as TL # noqa: E402 (bootstrap_ci, build_sequences, head_forward dll)
DATA_NEW = os.path.normpath(os.path.join(HERE, "..", "..", "..", "Files",
"AlgoForge", "Data", "features_XAUUSD.npz"))
DATA_BASE = os.path.normpath(os.path.join(HERE, "..", "..", "..", "Files",
"SniperGold_ML", "features_XAUUSDc.npz"))
EVAL_LOG = os.path.join(HERE, "TAHAP4_HYBRID.log")
# ---- konfigurasi terkunci (mirip Fase 3, 2 lapis + 2 head) ----
W = 32
H = 32
LR = 1e-3
BATCH = 256
PATIENCE = 10
EPOCH_MAX = 30
CLIP = 1.0
SEEDS = [42, 7, 123, 2024, 999]
BASE_LONG, BASE_SHORT = 0.6270, 0.6207
CONFIG = dict(W=W, H=H, layers=2, heads=2, lr=LR, batch=BATCH, patience=PATIENCE,
epoch_max=EPOCH_MAX, clip=CLIP, seeds=SEEDS,
base_long=BASE_LONG, base_short=BASE_SHORT)
def elog(msg):
print(msg, flush=True)
with open(EVAL_LOG, "a", encoding="utf-8") as f:
f.write(msg + "\n")
def sigmoid(x):
return 1.0 / (1.0 + np.exp(-np.clip(x, -30.0, 30.0)))
# ======================================================================
# LSTM 2-LAPIS: FORWARD
# ======================================================================
def lstm_forward(Xs, Wx, Wh, bx, bh):
"""Satu lapis LSTM (reuse TL.lstm_forward). Xs: (B,T,D) -> hs: (B,T,H)."""
return TL.lstm_forward(Xs, Wx, Wh, bx, bh)
def lstm2_forward(Xs, W1, W2):
"""Dua lapis: L1(Xs)->h1s; L2(h1s)->h2s. W1/W2=(Wx,Wh,bx,bh)."""
Wx1, Wh1, bx1, bh1 = W1
Wx2, Wh2, bx2, bh2 = W2
h1s, c1 = lstm_forward(Xs, Wx1, Wh1, bx1, bh1)
h2s, c2 = lstm_forward(h1s, Wx2, Wh2, bx2, bh2)
return h2s, c1, c2, h1s
def head2_forward(hT, Wy, by):
"""2-target: (B,H)@(H,2)+b -> sigmoid (B,2)."""
return sigmoid(hT @ Wy + by)
# ======================================================================
# LSTM 2-LAPIS: BPTT
# ======================================================================
def lstm_layer_bptt(Xs, caches, dh_seq, Wx, Wh, bx, bh, want_dX=False):
"""BPTT satu lapis dgn gradien hidden per-timestep dh_seq[t] (B,H).
Return (dWx,dWh,dbx,dbh[, dX]) dX = grad thd input tiap t."""
B, T, D = Xs.shape
Hh = Wh.shape[0]
Gx = Xs @ Wx + bx
dWx = np.zeros_like(Wx)
dWh = np.zeros_like(Wh)
dbx = np.zeros_like(bx)
dbh = np.zeros_like(bh)
dX = np.empty((B, T, D)) if want_dX else None
dh_next = np.zeros((B, Hh))
dc_next = np.zeros((B, Hh))
for t in reversed(range(T)):
dh = dh_seq[t] + dh_next
g, i, f, o, gg, cp, c, h = caches[t]
do = dh * np.tanh(c)
dc = dh * o * (1.0 - np.tanh(c) ** 2) + dc_next
di = dc * gg
df = dc * cp
dg = dc * i
dgates = np.concatenate([
di * i * (1.0 - i),
df * f * (1.0 - f),
do * o * (1.0 - o),
dg * (1.0 - gg ** 2),
], axis=1)
dWx += Xs[:, t].T @ dgates
dbx += dgates.sum(0)
hprev = caches[t - 1][-1] if t > 0 else np.zeros((B, Hh))
dWh += hprev.T @ dgates
dbh += dgates.sum(0)
if want_dX:
dX[:, t] = dgates @ Wx.T
dh_next = dgates @ Wh.T
dc_next = dc * f
out = (dWx, dWh, dbx, dbh)
return (out + (dX,)) if want_dX else out
def head2_backward(hT, P, Y, Wy):
"""BCE 2-target. Y: (B,2); loss = mean atas B*2 elemen. Return (dWy,dby,dhT)."""
B = P.shape[0]
d = (P - Y) / (B * 2)
dWy = hT.T @ d
dby = d.sum(0)
dhT = d @ Wy.T
return dWy, dby, dhT
def lstm2_bptt(Xs, h1s, c1, c2, dh2_last, W1, W2):
"""BPTT 2 lapis. h1s: (B,T,H) output L1 (input L2). dh2_last: grad head thd
h2[T-1]. Return (grads1, grads2)."""
Wx1, Wh1, bx1, bh1 = W1
Wx2, Wh2, bx2, bh2 = W2
T = Xs.shape[1]
B = Xs.shape[0]
Hh = c2[0][-1].shape[1]
# L2: dh_seq = 0 kecuali t=T-1 = dh2_last
dh2_seq = np.zeros((T, B, Hh))
dh2_seq[T - 1] = dh2_last
dWx2, dWh2, dbx2, dbh2, dX2 = lstm_layer_bptt(
h1s, c2, dh2_seq, Wx2, Wh2, bx2, bh2, want_dX=True)
# L1: dh_seq = dX2 (grad thd input L2 = h1) dgn urutan waktu (T,B,H)
dh1_seq = dX2.transpose(1, 0, 2)
g1 = lstm_layer_bptt(Xs, c1, dh1_seq, Wx1, Wh1, bx1, bh1)
return g1, (dWx2, dWh2, dbx2, dbh2)
# ======================================================================
# TRAINING (Adam + grad clip + early stopping mean-AUC val)
# ======================================================================
def init_lstm2(rng, D, Hh):
sin = 1.0 / math.sqrt(D)
Wx1 = rng.normal(0.0, sin, (D, 4 * Hh))
Wh1 = rng.normal(0.0, 0.05, (Hh, 4 * Hh))
bx1 = np.zeros(4 * Hh)
bh1 = np.zeros(4 * Hh)
Wx2 = rng.normal(0.0, sin, (Hh, 4 * Hh))
Wh2 = rng.normal(0.0, 0.05, (Hh, 4 * Hh))
bx2 = np.zeros(4 * Hh)
bh2 = np.zeros(4 * Hh)
Wy = rng.normal(0.0, 0.05, (Hh, 2))
by = np.zeros(2)
return (Wx1, Wh1, bx1, bh1), (Wx2, Wh2, bx2, bh2), Wy, by
def train_lstm2(Xtr, Ytr, Xva, Yva, seed=42, hidd=H, lr=LR, epoch_max=EPOCH_MAX,
patience=PATIENCE, verbose=True):
"""Ytr/Yva: (n,2) {long,short}. Early stopping pd mean(AUC_long, AUC_short) val."""
rng = np.random.default_rng(seed)
Bt, T, D = Xtr.shape
W1, W2, Wy, by = init_lstm2(rng, D, hidd)
params = [W1[0], W1[1], W1[2], W1[3],
W2[0], W2[1], W2[2], W2[3],
Wy, by]
m = [np.zeros_like(p) for p in params]
v = [np.zeros_like(p) for p in params]
beta1, beta2, eps = 0.9, 0.999, 1e-8
best_va, best_ep, best_state = -1.0, 0, None
tstep = 0
n = len(Xtr)
for ep in range(epoch_max):
perm = rng.permutation(n)
ep_loss = 0.0
for s in range(0, n, BATCH):
idx = perm[s:s + BATCH]
Xb, Yb = Xtr[idx], Ytr[idx]
h2s, c1, c2, h1s = lstm2_forward(Xb, W1, W2)
P = head2_forward(h2s[:, -1], Wy, by)
loss = -np.mean(Yb * np.log(P + 1e-12) +
(1.0 - Yb) * np.log(1.0 - P + 1e-12))
ep_loss += loss * len(idx)
dWy, dby, dh2_last = head2_backward(h2s[:, -1], P, Yb, Wy)
(dWx1, dWh1, dbx1, dbh1), (dWx2, dWh2, dbx2, dbh2) = \
lstm2_bptt(Xb, h1s, c1, c2, dh2_last, W1, W2)
grads = [dWx1, dWh1, dbx1, dbh1, dWx2, dWh2, dbx2, dbh2, dWy, dby]
gn = math.sqrt(sum(float((g ** 2).sum()) for g in grads)) + 1e-12
if gn > CLIP:
grads = [g * (CLIP / gn) for g in grads]
tstep += 1
for i in range(10):
m[i] = beta1 * m[i] + (1 - beta1) * grads[i]
v[i] = beta2 * v[i] + (1 - beta2) * grads[i] ** 2
mh = m[i] / (1 - beta1 ** tstep)
vh = v[i] / (1 - beta2 ** tstep)
params[i] = params[i] - lr * mh / (np.sqrt(vh) + eps)
W1 = (params[0], params[1], params[2], params[3])
W2 = (params[4], params[5], params[6], params[7])
Wy, by = params[8], params[9]
Pl, Ps = predict_lstm2(Xva, W1, W2, Wy, by)
auc_l = TM.auc(Yva[:, 0].astype(int), Pl)
auc_s = TM.auc(Yva[:, 1].astype(int), Ps)
va = 0.5 * (auc_l + auc_s)
if va > best_va:
best_va, best_ep = va, ep
best_state = ([p.copy() for p in params])
if verbose:
print(f" ep {ep:2d} loss {ep_loss / n:.4f} "
f"AUC_va L={auc_l:.4f} S={auc_s:.4f}")
if ep - best_ep > patience:
break
W1 = (best_state[0], best_state[1], best_state[2], best_state[3])
W2 = (best_state[4], best_state[5], best_state[6], best_state[7])
Wy, by = best_state[8], best_state[9]
return (W1, W2, Wy, by), best_va, best_ep
def predict_lstm2(Xs, W1, W2, Wy, by):
h2s, _, _, _ = lstm2_forward(Xs, W1, W2)
P = head2_forward(h2s[:, -1], Wy, by)
return P[:, 0], P[:, 1]
# ======================================================================
# DATA: cache baru & baseline
# ======================================================================
def load_new():
z = np.load(DATA_NEW)
F, F2, label, close, t = (z["F"], z["F2"], z["label"], z["close"], z["time"])
split_bar, split_pos = int(z["split_bar"]), int(z["split_pos"])
return F, F2, label, close, t, split_bar, split_pos
def load_base():
z = np.load(DATA_BASE)
F, label, ATR, close = z["F"], z["label"], z["ATR"], z["close"]
if F.shape[1] != len(TM.FEAT_NAMES):
F = np.column_stack([F, TM.confluence_feature(F)]).astype(float)
mask = label != 0
midx = np.where(mask)[0]
split_pos = int(0.75 * len(midx))
split_bar = int(midx[split_pos])
return F, label, close, midx, split_pos, split_bar
def build_sequences(Fs, idxs, window=W):
return TL.build_sequences(Fs, idxs, window)
def prepare_split(X, label, midx, split_pos, frac=1.0):
"""Standarisasi train-only; sequence; Y 2-kolom. frac=1.0 = pakai semua."""
n_use = max(1, int(frac * split_pos))
Xt = X[midx[:n_use]]
yt = label[midx[:n_use]]
va_from = int(0.85 * n_use)
mean = Xt[:va_from].mean(0)
std = Xt[:va_from].std(0)
std[std < 1e-9] = 1.0
Xs = (X - mean) / std
Xtr = build_sequences(Xs, midx[:va_from], W)
Xva = build_sequences(Xs, midx[va_from:n_use], W)
Xte = build_sequences(Xs, midx[n_use:], W)
Ytr = np.column_stack([(yt[:va_from] == 1).astype(float),
(yt[:va_from] == -1).astype(float)])
Yva = np.column_stack([(label[midx[va_from:n_use]] == 1).astype(float),
(label[midx[va_from:n_use]] == -1).astype(float)])
Yte = np.column_stack([(label[midx[n_use:]] == 1).astype(float),
(label[midx[n_use:]] == -1).astype(float)])
return Xtr, Xva, Xte, Ytr, Yva, Yte, mean, std
# ======================================================================
# UNIT TEST B1-B5 (2-lapis + 2-head)
# ======================================================================
def test_B1():
rng = np.random.default_rng(0)
B, T, D, Hh = 3, 2, 4, 2
Xs = rng.normal(size=(B, T, D))
W1 = (rng.normal(0, 0.3, (D, 4 * Hh)), rng.normal(0, 0.3, (Hh, 4 * Hh)),
rng.normal(0, 0.1, 4 * Hh), rng.normal(0, 0.1, 4 * Hh))
W2 = (rng.normal(0, 0.3, (Hh, 4 * Hh)), rng.normal(0, 0.3, (Hh, 4 * Hh)),
rng.normal(0, 0.1, 4 * Hh), rng.normal(0, 0.1, 4 * Hh))
# referensi: jalankan TL.lstm_forward dua kali (1-lapis, teruji B1 Fase 3)
h1s_ref, _ = TL.lstm_forward(Xs, *W1)
h2s_ref, _ = TL.lstm_forward(h1s_ref, *W2)
h2s, c1, c2, h1s = lstm2_forward(Xs, W1, W2)
err = float(np.abs(h2s - h2s_ref).max())
print(f" B1 forward 2-lapis vs 2x1-lapis : max_err={err:.2e}")
assert err < 1e-8, "B1 GAGAL"
def test_B2():
rng = np.random.default_rng(1)
B, T, D, Hh = 2, 3, 3, 2
Xs = rng.normal(size=(B, T, D))
W1 = (rng.normal(0, 0.2, (D, 4 * Hh)), rng.normal(0, 0.2, (Hh, 4 * Hh)),
rng.normal(0, 0.1, 4 * Hh), rng.normal(0, 0.1, 4 * Hh))
W2 = (rng.normal(0, 0.2, (Hh, 4 * Hh)), rng.normal(0, 0.2, (Hh, 4 * Hh)),
rng.normal(0, 0.1, 4 * Hh), rng.normal(0, 0.1, 4 * Hh))
Wy = rng.normal(0, 0.2, (Hh, 2))
by = np.array([0.1, -0.1])
Y = np.array([[1.0, 0.0], [0.0, 1.0]])
def loss_fn(p):
W1p = (p[0], p[1], p[2], p[3])
W2p = (p[4], p[5], p[6], p[7])
Wyp = p[8]
byp = p[9]
h2s, _, _, _ = lstm2_forward(Xs, W1p, W2p)
P = head2_forward(h2s[:, -1], Wyp, byp)
return float(-np.mean(Y * np.log(P + 1e-12) +
(1.0 - Y) * np.log(1.0 - P + 1e-12)))
h2s, c1, c2, h1s = lstm2_forward(Xs, W1, W2)
P = head2_forward(h2s[:, -1], Wy, by)
dWy, dby, dh2_last = head2_backward(h2s[:, -1], P, Y, Wy)
(dWx1, dWh1, dbx1, dbh1), (dWx2, dWh2, dbx2, dbh2) = \
lstm2_bptt(Xs, h1s, c1, c2, dh2_last, W1, W2)
grads = [dWx1, dWh1, dbx1, dbh1, dWx2, dWh2, dbx2, dbh2, dWy, dby]
params = [W1[0], W1[1], W1[2], W1[3], W2[0], W2[1], W2[2], W2[3], Wy, by]
eps = 1e-6
maxrel = 0.0
names = ["Wx1", "Wh1", "bx1", "bh1", "Wx2", "Wh2", "bx2", "bh2", "Wy", "by"]
for nm, g, p in zip(names, grads, params):
num = np.zeros_like(p)
it = np.nditer(p, flags=["multi_index"])
while not it.finished:
i = it.multi_index
old = p[i]
p[i] = old + eps
fp = loss_fn(params)
p[i] = old - eps
fm = loss_fn(params)
p[i] = old
num[i] = (fp - fm) / (2.0 * eps)
it.iternext()
denom = np.abs(num) + np.abs(g) + 1e-12
rel = float((np.abs(num - g) / denom).max())
maxrel = max(maxrel, rel)
print(f" B2 {nm}: rel={rel:.2e} |num|max={np.abs(num).max():.3e}")
print(f" B2 gradient check (BPTT 2-lapis) : max_rel_err={maxrel:.2e}")
assert maxrel < 1e-4, "B2 GAGAL"
def test_B3():
rng = np.random.default_rng(42)
Xs = rng.normal(size=(64, 8, 5))
y = (rng.random(64) > 0.5).astype(float)
Y = np.column_stack([y, 1.0 - y])
r1 = train_lstm2(Xs, Y, Xs[:32], Y[:32], seed=7, verbose=False)
r2 = train_lstm2(Xs, Y, Xs[:32], Y[:32], seed=7, verbose=False)
ok = all(np.array_equal(a, b) for a, b in zip(r1[0][0], r2[0][0])) and \
all(np.array_equal(a, b) for a, b in zip(r1[0][1], r2[0][1]))
print(f" B3 determinisme (seed sama) : {'OK' if ok else 'GAGAL'}")
assert ok, "B3 GAGAL"
def test_B4():
F = np.arange(500 * 5, dtype=np.float64).reshape(500, 5)
idxs = np.array([10, 100, 250])
Xs = build_sequences(F, idxs, W)
for k, t in enumerate(idxs):
assert np.array_equal(Xs[k, -1], F[t]), f"B4 GAGAL t={t}"
assert Xs[k].shape == (W, 5)
print(" B4 anti-lookahead (seq berakhir di bar t) : OK")
def test_B5():
rng = np.random.default_rng(3)
Xs = rng.normal(size=(200, 4, 6))
y = (rng.random(200) > 0.5).astype(float)
Y = np.column_stack([y, 1.0 - y])
state, va, ep = train_lstm2(Xs, Y, Xs, Y, seed=9, hidd=8, lr=1e-2,
epoch_max=300, verbose=False)
Pl, Ps = predict_lstm2(Xs, *state)
auc_tr = TM.auc(y.astype(int), Pl)
print(f" B5 overfit sanity (2-lapis) : AUC_train(long)={auc_tr:.4f}")
assert auc_tr > 0.95, "B5 GAGAL"
def selftest():
print("=== UNIT TEST B1-B5 (LSTM 2-LAPIS + 2-HEAD) ===")
test_B1()
test_B2()
test_B3()
test_B4()
test_B5()
print(">>> SEMUA UNIT TEST B1-B5 LOLOS (G1 OK) <<<")
# ======================================================================
# GATE LIKE-FOR-LIKE pd cache XAUUSDc (vs 0.6270/0.6207)
# ======================================================================
def gate_xauusdc():
elog("\n=== GATE LIKE-FOR-LIKE (cache XAUUSDc 60k, split 75/25) ===")
F, label, close, midx, split_pos, split_bar = load_base()
X = F
n = len(midx)
elog(f" bar={len(close)} berlabel={n} split_pos={split_pos} split_bar={split_bar}")
# baseline MLP (reproduksi, kontrol kontaminasi) — long & short
yl = (label[midx] == 1).astype(float)
ys = (label[midx] == -1).astype(float)
Xtr, ytr = X[midx[:split_pos]], yl[:split_pos]
va_from = int(0.85 * split_pos)
mean = Xtr[:va_from].mean(0)
std = Xtr[:va_from].std(0)
std[std < 1e-9] = 1.0
auc_base_l, auc_base_s = 0.0, 0.0
# long
(W1l, b1l, W2l, b2l), auc_va, _ = TM.train(
(Xtr[:va_from] - mean) / std, yl[:va_from],
(Xtr[va_from:] - mean) / std, yl[va_from:])
_, Pte_l = TM.forward((X[midx[split_pos:]] - mean) / std, W1l, b1l, W2l, b2l)
auc_base_l = TM.auc(yl[split_pos:].astype(int), Pte_l[:, 0])
# short
(W1s, b1s, W2s, b2s), _, _ = TM.train(
(Xtr[:va_from] - mean) / std, ys[:va_from],
(Xtr[va_from:] - mean) / std, ys[va_from:])
_, Pte_s = TM.forward((X[midx[split_pos:]] - mean) / std, W1s, b1s, W2s, b2s)
auc_base_s = TM.auc(ys[split_pos:].astype(int), Pte_s[:, 0])
elog(f" BASELINE MLP (reproduksi): LONG={auc_base_l:.4f} SHORT={auc_base_s:.4f} "
f"(target 0.6270/0.6207)")
# hybrid 2-lapis 2-head pd split yang SAMA
Xtrs = build_sequences((X - mean) / std, midx[:split_pos], W)
Xtes = build_sequences((X - mean) / std, midx[split_pos:], W)
# bagi train -> train/val 85/15
va2 = int(0.85 * split_pos)
Xtr2, Xva2 = Xtrs[:va2], Xtrs[va2:]
Ytr2 = np.column_stack([yl[:va2], ys[:va2]])
Yva2 = np.column_stack([yl[va2:split_pos], ys[va2:split_pos]])
Yte = np.column_stack([yl[split_pos:], ys[split_pos:]])
aucs_l, aucs_s = [], []
P_ens_l, P_ens_s = np.zeros(len(Yte)), np.zeros(len(Yte))
for sd in SEEDS:
st, va, ep = train_lstm2(Xtr2, Ytr2, Xva2, Yva2, seed=sd, verbose=False)
Pl, Ps = predict_lstm2(Xtes, *st)
al = TM.auc(Yte[:, 0].astype(int), Pl)
as_ = TM.auc(Yte[:, 1].astype(int), Ps)
aucs_l.append(al)
aucs_s.append(as_)
P_ens_l += Pl / len(SEEDS)
P_ens_s += Ps / len(SEEDS)
elog(f" seed {sd}: LONG={al:.4f} SHORT={as_:.4f}")
mean_l = float(np.mean(aucs_l))
mean_s = float(np.mean(aucs_s))
std_l = float(np.std(aucs_l))
std_s = float(np.std(aucs_s))
ens_l = TM.auc(Yte[:, 0].astype(int), P_ens_l)
ens_s = TM.auc(Yte[:, 1].astype(int), P_ens_s)
elog(f" HYBRID 2-lapis: LONG mean={mean_l:.4f}+-{std_l:.4f} ens={ens_l:.4f} | "
f"SHORT mean={mean_s:.4f}+-{std_s:.4f} ens={ens_s:.4f}")
# bootstrap paired CI (ens vs baseline MLP) 2000x
lo_l, hi_l, dm_l = TL.bootstrap_ci(Yte[:, 0].astype(int), P_ens_l, Pte_l[:, 0])
lo_s, hi_s, dm_s = TL.bootstrap_ci(Yte[:, 1].astype(int), P_ens_s, Pte_s[:, 0])
elog(f" dAUC ens-vs-MLP: LONG {dm_l:+.4f} CI95=[{lo_l:+.4f},{hi_l:+.4f}] | "
f"SHORT {dm_s:+.4f} CI95=[{lo_s:+.4f},{hi_s:+.4f}]")
# gate summary
elog("\n=== GATE (vs baseline 0.6270/0.6207) ===")
g_long = ens_l - BASE_LONG
g_short = ens_s - BASE_SHORT
elog(f" LONG : ens={ens_l:.4f} vs base {BASE_LONG:.4f} -> dAUC={g_long:+.4f} "
f"({'>=+0.005' if g_long >= 0.005 else 'GAGAL G3'})")
elog(f" SHORT : ens={ens_s:.4f} vs base {BASE_SHORT:.4f} -> dAUC={g_short:+.4f} "
f"({'>=+0.005' if g_short >= 0.005 else 'GAGAL G3'})")
elog(f" G4 CI tak memuat 0 : LONG {'YA' if lo_l > 0 else 'TIDAK'} | "
f"SHORT {'YA' if lo_s > 0 else 'TIDAK'}")
elog(f" G6 std seed <0.01 & >=4/5 > base : LONG std={std_l:.4f} "
f"beat={int(sum(a > BASE_LONG for a in aucs_l))}/5 | SHORT std={std_s:.4f} "
f"beat={int(sum(a > BASE_SHORT for a in aucs_s))}/5")
return dict(ens_l=ens_l, ens_s=ens_s, base_l=auc_base_l, base_s=auc_base_s,
ci_l=(lo_l, hi_l), ci_s=(lo_s, hi_s))
# ======================================================================
# WALK-FORWARD pd data baru (75/25, 70/30, 80/20; [F19] vs [F19+F2])
# ======================================================================
def run_wf_split(X, label, midx, split_pos, use_regime, seed):
Xt = np.column_stack([X[0], X[1]]) if use_regime else X[0]
Xtr, Xva, Xte, Ytr, Yva, Yte, mean, std = prepare_split(Xt, label, midx, split_pos)
st, va, ep = train_lstm2(Xtr, Ytr, Xva, Yva, seed=seed, verbose=False)
Pl, Ps = predict_lstm2(Xte, *st)
al = TM.auc(Yte[:, 0].astype(int), Pl)
as_ = TM.auc(Yte[:, 1].astype(int), Ps)
return al, as_
def wf_new():
elog("\n=== WALK-FORWARD DATA BARU (features_XAUUSD.npz) ===")
F, F2, label, close, t, split_bar, split_pos = load_new()
mask = label != 0
midx = np.where(mask)[0]
elog(f" berlabel={len(midx)} split_pos(75/25)={split_pos}")
for frac in (0.75, 0.70, 0.80):
sp = int(frac * len(midx))
for use_regime in (False, True):
tag = f"split{int(frac*100)}/{int((1-frac)*100)} " \
f"{'F19+F2' if use_regime else 'F19'}"
al_, as_ = [], []
for sd in SEEDS[:2]: # kalibrasi dulu 2 seed
a, b = run_wf_split((F, F2), label, midx, sp, use_regime, sd)
al_.append(a)
as_.append(b)
elog(f" [{tag}] (2 seed) LONG={np.mean(al_):.4f} SHORT={np.mean(as_):.4f}")
elog(" (wf 5-seed penuh dijalankan bila budget/token cukup — lihat handoff)")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--selftest", action="store_true")
ap.add_argument("--gate", action="store_true")
ap.add_argument("--wf", action="store_true")
ap.add_argument("--seed", type=int, default=None)
args = ap.parse_args()
if args.selftest:
selftest()
return
if args.gate:
gate_xauusdc()
return
if args.wf:
wf_new()
return
selftest()
gate_xauusdc()
if __name__ == "__main__":
main()