560 lines
22 KiB
Python
560 lines
22 KiB
Python
|
|
# -*- 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()
|