forked from chiki2bum2/SniperGold_ML
838 lines
32 KiB
Python
838 lines
32 KiB
Python
# -*- coding: utf-8 -*-
| |||
"""
| |||
SniperGold ML - Training pipeline (numpy murni)
| |||
-----------------------------------------------
| |||
1. Baca CSV OHLCV (M15 + HTF D1/H4/H1) dari Files\\SniperGold_ML
| |||
2. Hitung 18 fitur per bar (meniru logika indikator SniperGold SMC Pro+)
| |||
3. Buat label hasil forward (H bar ke depan vs ambang x ATR)
| |||
4. Latih MLP (1 hidden layer) dengan numpy murni (tanpa sklearn)
| |||
5. Evaluasi walk-forward (train 75% lama, test 25% terbaru)
| |||
6. Ekspor model -> MQL5\\Include\\SniperGold_ML.mqh
| |||
| |||
Cara pakai:
| |||
python train_model.py [symbol]
| |||
"""
| |||
import os
| |||
import sys
| |||
import math
| |||
import datetime as dt
| |||
| |||
import numpy as np
| |||
| |||
# ----------------------------------------------------------------------
| |||
# KONFIGURASI (harus konsisten dengan indikator)
| |||
# ----------------------------------------------------------------------
| |||
SWING_LEN = 50 # InpSwingLen
| |||
INTERNAL_LEN = 5 # InpInternalLen
| |||
LOOKBACK = 600 # InpLookbackBars
| |||
ATR_PERIOD = 14
| |||
EQ_TOL_ATR = 0.10 # InpEQThreshold
| |||
EQ_BARS = 3 # InpEQBarsConfirm
| |||
GRAB_WINDOW = 8 # InpGrabWindow
| |||
DELTA_BARS = 10 # InpDeltaBars
| |||
CONFLUENCE = True # InpConfluenceFilter
| |||
| |||
# label / training
| |||
H_LABEL = 24 # horizon label (bar M15) = 6 jam
| |||
LABEL_ATR = 0.75 # ambang hasil (x ATR)
| |||
HIDDEN = 12 # neuron hidden layer
| |||
EPOCHS = 300
| |||
BATCH = 256
| |||
LR = 5e-3
| |||
PATIENCE = 25
| |||
| |||
BASE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "Files", "SniperGold_ML")
| |||
OUT_MQH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "Include", "SniperGold_ML.mqh")
| |||
| |||
FEAT_NAMES = [
| |||
"htf1_bias", "htf2_bias", "htf3_bias",
| |||
"swing_trend", "internal_trend", "chart_bias",
| |||
"eq_pos_norm", "sweep_dir", "choch_dir", "choch_confirms",
| |||
"eqh_swept", "eql_swept", "delta_sign", "delta_mag",
| |||
"dist_high_atr", "dist_low_atr", "mom20_atr", "range_atr",
| |||
"confluence",
| |||
]
| |||
NF = len(FEAT_NAMES)
| |||
| |||
| |||
def confluence_feature(F):
| |||
"""Skor konfluensi heuristik (replikasi ComputeAIScore v4.3) dari fitur dasar."""
| |||
b1, b2, b3 = F[:, 0], F[:, 1], F[:, 2]
| |||
chart = F[:, 5]
| |||
eq, swp, cho = F[:, 6], F[:, 7], F[:, 8]
| |||
bull = (b1 > 0).astype(int) + (b2 > 0).astype(int) + (b3 > 0).astype(int)
| |||
bear = (b1 < 0).astype(int) + (b2 < 0).astype(int) + (b3 < 0).astype(int)
| |||
c = np.zeros(len(F))
| |||
c += (chart != 0) * 10.0
| |||
c += np.where((bull == 3) | (bear == 3), 25.0,
| |||
np.where(((bull >= 2) & (bear == 0)) | ((bear >= 2) & (bull == 0)), 12.0, 0.0))
| |||
c += ((eq < 0) & (chart > 0)) * 10.0 # discount + bullish
| |||
c += ((eq > 0) & (chart < 0)) * 10.0 # premium + bearish
| |||
c += (swp != 0) * 15.0
| |||
c += ((cho != 0) & (cho == swp)) * 15.0
| |||
c += (((swp == 1) & (chart > 0)) | ((swp == -1) & (chart < 0))) * 15.0
| |||
return np.minimum(c, 100.0)
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# BACA CSV
| |||
# ----------------------------------------------------------------------
| |||
def load_csv(path):
| |||
t, o, h, l, c, v = [], [], [], [], [], []
| |||
with open(path, "r", encoding="utf-8") as f:
| |||
f.readline()
| |||
for line in f:
| |||
p = line.strip().split(",")
| |||
if len(p) < 7:
| |||
continue
| |||
t.append(dt.datetime.strptime(p[0], "%Y-%m-%d %H:%M:%S"))
| |||
o.append(float(p[1])); h.append(float(p[2])); l.append(float(p[3]))
| |||
c.append(float(p[4])); v.append(float(p[5]))
| |||
return (np.array(t), np.array(o), np.array(h), np.array(l), np.array(c), np.array(v))
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# UTILITAS DASAR
| |||
# ----------------------------------------------------------------------
| |||
def atr_series(h, l, c):
| |||
n = len(c)
| |||
out = np.zeros(n)
| |||
tr = np.zeros(n)
| |||
tr[0] = h[0] - l[0]
| |||
for i in range(1, n):
| |||
tr[i] = max(h[i] - l[i], abs(h[i] - c[i - 1]), abs(l[i] - c[i - 1]))
| |||
s = 0.0
| |||
for i in range(n):
| |||
s += tr[i]
| |||
if i >= ATR_PERIOD:
| |||
s -= tr[i - ATR_PERIOD]
| |||
out[i] = s / ATR_PERIOD if i >= ATR_PERIOD - 1 else (h[i] - l[i])
| |||
return out
| |||
| |||
| |||
def tf_bias_asof(hh, ll, cc, k):
| |||
"""Replikasi TFBias() indikator utk HTF bar tertutup k (window 200, pivot len=3)."""
| |||
need = 200
| |||
s = 3
| |||
start = max(0, k - need + 1)
| |||
h = hh[start:k + 1]
| |||
l = ll[start:k + 1]
| |||
c = cc[start:k + 1]
| |||
m = len(h) - 1
| |||
if m < s + 2:
| |||
return 0
| |||
up, dn = float("inf"), -float("inf")
| |||
upb, dnb = -1, -1
| |||
trend = 0
| |||
for i in range(s + 1, m):
| |||
p = i - s
| |||
if p >= s:
| |||
is_h = is_l = True
| |||
for kk in range(1, s + 1):
| |||
if h[p] <= h[p + kk] or h[p] <= h[p - kk]:
| |||
is_h = False
| |||
if l[p] >= l[p + kk] or l[p] >= l[p - kk]:
| |||
is_l = False
| |||
if is_h:
| |||
up, upb = h[p], p
| |||
if is_l:
| |||
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 htf_bias_series(hh, ll, cc):
| |||
"""bias per HTF bar tertutup k -> bias yg berlaku utk bar k-1 (lag 1, spt indikator)."""
| |||
n = len(cc)
| |||
out = np.zeros(n, dtype=int)
| |||
for k in range(1, n):
| |||
out[k] = tf_bias_asof(hh, ll, cc, k - 1)
| |||
return out
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# MESIN STRUKTUR (streaming, replika ProcessStructure)
| |||
# ----------------------------------------------------------------------
| |||
def build_structure(o, h, l, c, length, internal, swing_at, begin=None):
| |||
"""
| |||
Streaming ProcessStructure(). swing_at: array tren swing per bar (utk confluence internal).
| |||
Mengembalikan trend, last_break, choch_dir/bar, dan pivot list per bar.
| |||
"""
| |||
n = len(c)
| |||
trend = np.zeros(n, dtype=int)
| |||
last_break = np.full(n, -1, dtype=int)
| |||
choch_dir = np.zeros(n, dtype=int)
| |||
choch_bar = np.full(n, -1, dtype=int)
| |||
pivots = []
| |||
| |||
up_target, dn_target = float("inf"), -float("inf")
| |||
up_bar, dn_bar = -1, -1
| |||
cur_trend = 0
| |||
cur_choch_dir, cur_choch_bar = 0, -1
| |||
cur_last_break = -1
| |||
| |||
if begin is None:
| |||
begin = 100
| |||
begin = max(begin, 2 * length)
| |||
for i in range(begin, n):
| |||
p = i - length
| |||
if p >= length:
| |||
is_ph = is_pl = True
| |||
for k in range(1, length + 1):
| |||
if h[p] <= h[p - k] or h[p] <= h[p + k]:
| |||
is_ph = False
| |||
if l[p] >= l[p - k] or l[p] >= l[p + k]:
| |||
is_pl = False
| |||
if is_ph:
| |||
up_target, up_bar = h[p], p
| |||
pivots.append((p, h[p], True))
| |||
if is_pl:
| |||
dn_target, dn_bar = l[p], p
| |||
pivots.append((p, l[p], False))
| |||
| |||
broke = False
| |||
if up_bar >= 0 and c[i] > up_target:
| |||
choch = (cur_trend < 0)
| |||
allow = (not internal) or swing_at[i] >= 0
| |||
if internal and allow and choch:
| |||
cur_choch_dir, cur_choch_bar = 1, i
| |||
cur_trend = 1
| |||
up_target, up_bar = float("inf"), -1
| |||
cur_last_break = i
| |||
broke = True
| |||
if dn_bar >= 0 and c[i] < dn_target:
| |||
choch = (cur_trend > 0)
| |||
allow = (not internal) or swing_at[i] <= 0
| |||
if internal and allow and choch:
| |||
cur_choch_dir, cur_choch_bar = -1, i
| |||
cur_trend = -1
| |||
dn_target, dn_bar = -float("inf"), -1
| |||
cur_last_break = i
| |||
broke = True
| |||
| |||
trend[i] = cur_trend
| |||
if broke:
| |||
last_break[i] = i
| |||
else:
| |||
last_break[i] = cur_last_break
| |||
choch_dir[i] = cur_choch_dir
| |||
choch_bar[i] = cur_choch_bar
| |||
| |||
return dict(trend=trend, last_break=last_break, choch_dir=choch_dir,
| |||
choch_bar=choch_bar, pivots=pivots)
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# FITUR + LABEL
| |||
# ----------------------------------------------------------------------
| |||
def build_features(m15, htf_data, begin=None):
| |||
t, o, h, l, c, v = m15
| |||
n = len(c)
| |||
A = np.maximum(atr_series(h, l, c), 1e-9)
| |||
| |||
# ---- HTF bias (precompute per bar HTF, lalu map ke M15) ----
| |||
htf_bias = np.zeros((3, n), dtype=int)
| |||
for fi, key in enumerate(("D1", "H4", "H1")):
| |||
hh, hl, hc, ht = htf_data[key]
| |||
bs = htf_bias_series(hh, hl, hc)
| |||
idx = np.searchsorted(ht, t, side="right") - 1
| |||
idx = np.clip(idx, 0, len(bs) - 1)
| |||
htf_bias[fi] = bs[idx]
| |||
| |||
# ---- struktur swing ----
| |||
swing_at = np.zeros(n, dtype=int)
| |||
sw = build_structure(o, h, l, c, SWING_LEN, False, swing_at, begin=begin)
| |||
# timeline swing -> array tren per bar
| |||
# (bangun ulang dari pivot/break: pakai trend array hasil streaming)
| |||
swing_at = sw["trend"].copy()
| |||
inn = build_structure(o, h, l, c, INTERNAL_LEN, True, swing_at, begin=begin)
| |||
| |||
sw_trend = sw["trend"]
| |||
in_trend = inn["trend"]
| |||
chart_bias = np.zeros(n, dtype=int)
| |||
for i in range(n):
| |||
b = in_trend[i] if (inn["last_break"][i] >= sw["last_break"][i] and in_trend[i] != 0) else sw_trend[i]
| |||
if b == 0:
| |||
b = sw_trend[i] if sw_trend[i] != 0 else in_trend[i]
| |||
chart_bias[i] = 1 if b > 0 else (-1 if b < 0 else 0)
| |||
| |||
# ---- swing high/low terakhir per bar ----
| |||
sw_high = np.zeros(n)
| |||
sw_low = np.zeros(n)
| |||
cur_h = cur_l = 0.0
| |||
sp_idx = 0
| |||
sp = sw["pivots"]
| |||
for i in range(n):
| |||
while sp_idx < len(sp) and sp[sp_idx][0] <= i:
| |||
pb, pr, ph = sp[sp_idx]
| |||
if ph:
| |||
cur_h = pr
| |||
else:
| |||
cur_l = pr
| |||
sp_idx += 1
| |||
sw_high[i] = cur_h
| |||
sw_low[i] = cur_l
| |||
| |||
rng = sw_high - sw_low
| |||
eq_pos = np.zeros(n)
| |||
ok = (sw_high > 0) & (sw_low > 0) & (rng > 0)
| |||
eq_pos[ok] = 2.0 * (c[ok] - sw_low[ok]) / rng[ok] - 1.0
| |||
| |||
# ---- liquidity grabs (pivot internal) ----
| |||
sweep_dir = np.zeros(n, dtype=int)
| |||
sweep_bar = np.full(n, -1)
| |||
cur_sd, cur_sb, cur_sl = 0, -1, 0.0
| |||
for (p, lvl, is_high) in inn["pivots"]:
| |||
last = min(n - 1, p + GRAB_WINDOW)
| |||
for b in range(p + 1, last + 1):
| |||
if is_high and h[b] > lvl and c[b] < lvl:
| |||
if b > cur_sb:
| |||
cur_sd, cur_sb, cur_sl = -1, b, lvl
| |||
break
| |||
if (not is_high) and l[b] < lvl and c[b] > lvl:
| |||
if b > cur_sb:
| |||
cur_sd, cur_sb, cur_sl = 1, b, lvl
| |||
break
| |||
if cur_sb >= 0:
| |||
sweep_dir[cur_sb:] = cur_sd
| |||
sweep_bar[cur_sb:] = cur_sb
| |||
| |||
choch_conf = np.zeros(n, dtype=int)
| |||
for i in range(n):
| |||
cd = inn["choch_dir"][i]
| |||
cb = inn["choch_bar"][i]
| |||
if cd != 0 and cb >= sweep_bar[i] and cd == sweep_dir[i]:
| |||
choch_conf[i] = 1
| |||
| |||
# ---- EQH/EQL swept (vectorized; flag mulai dari bar crossing yg sebenarnya) ----
| |||
eqh_swept = np.zeros(n, dtype=int)
| |||
eql_swept = np.zeros(n, dtype=int)
| |||
lastH = lastL = None
| |||
for (p, pr, is_high) in sp:
| |||
tol = EQ_TOL_ATR * A[p]
| |||
if is_high:
| |||
if lastH is not None and p - lastH[0] >= EQ_BARS and abs(pr - lastH[1]) <= tol:
| |||
if p + 1 < n:
| |||
q = np.where(h[p + 1:] > pr)[0]
| |||
if len(q):
| |||
eqh_swept[p + 1 + q[0]:] = 1
| |||
lastH = (p, pr)
| |||
else:
| |||
if lastL is not None and p - lastL[0] >= EQ_BARS and abs(pr - lastL[1]) <= tol:
| |||
if p + 1 < n:
| |||
q = np.where(l[p + 1:] < pr)[0]
| |||
if len(q):
| |||
eql_swept[p + 1 + q[0]:] = 1
| |||
lastL = (p, pr)
| |||
| |||
# ---- delta proxy ----
| |||
delta_dir = np.zeros(n, dtype=int)
| |||
delta_mag = np.zeros(n)
| |||
for i in range(n):
| |||
kk = min(DELTA_BARS, i - 1)
| |||
if kk <= 0:
| |||
continue
| |||
s = 0.0
| |||
tv = 0.0
| |||
for j in range(i - kk, i):
| |||
rj = h[j] - l[j]
| |||
if rj <= 0:
| |||
rj = 1e-9
| |||
body = abs(c[j] - o[j])
| |||
uw = h[j] - max(o[j], c[j])
| |||
lw = min(o[j], c[j]) - l[j]
| |||
bb, ss = (body + lw, uw) if c[j] >= o[j] else (lw, body + uw)
| |||
tt = bb + ss
| |||
if tt <= 0:
| |||
tt = 1e-9
| |||
s += (bb - ss) / tt * v[j]
| |||
tv += v[j]
| |||
if tv > 0:
| |||
delta_mag[i] = max(-1.0, min(1.0, s / tv))
| |||
delta_dir[i] = 1 if s > 0 else (-1 if s < 0 else 0)
| |||
| |||
# ---- jarak, momentum, range ----
| |||
dist_high = np.zeros(n)
| |||
dist_low = np.zeros(n)
| |||
for i in range(n):
| |||
if sw_high[i] > 0:
| |||
dist_high[i] = max(-10.0, min(10.0, (sw_high[i] - c[i]) / A[i]))
| |||
if sw_low[i] > 0:
| |||
dist_low[i] = max(-10.0, min(10.0, (c[i] - sw_low[i]) / A[i]))
| |||
mom20 = np.zeros(n)
| |||
for i in range(21, n):
| |||
mom20[i] = (c[i] - c[i - 20]) / A[i]
| |||
range_atr = np.zeros(n)
| |||
okr = rng > 0
| |||
range_atr[okr] = rng[okr] / A[okr]
| |||
| |||
F = np.column_stack([
| |||
htf_bias[0], htf_bias[1], htf_bias[2],
| |||
sw_trend, in_trend, chart_bias,
| |||
eq_pos, sweep_dir, inn["choch_dir"], choch_conf,
| |||
eqh_swept, eql_swept, delta_dir, delta_mag,
| |||
dist_high, dist_low, mom20, range_atr,
| |||
]).astype(float)
| |||
F = np.column_stack([F, confluence_feature(F)]).astype(float) # fitur 19: konfluensi
| |||
| |||
# ---- label forward ----
| |||
label = np.zeros(n, dtype=int)
| |||
for i in range(n - H_LABEL):
| |||
fwd = c[i + H_LABEL] - c[i]
| |||
thr = LABEL_ATR * A[i]
| |||
if fwd >= thr:
| |||
label[i] = 1
| |||
elif fwd <= -thr:
| |||
label[i] = -1
| |||
| |||
return F, label, A, c
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# MLP (numpy murni)
| |||
# ----------------------------------------------------------------------
| |||
def init_mlp(d_in, d_h, d_out=1, seed=42):
| |||
rng = np.random.default_rng(seed)
| |||
W1 = rng.normal(0, math.sqrt(2.0 / (d_in + d_h)), (d_in, d_h))
| |||
b1 = np.zeros(d_h)
| |||
W2 = rng.normal(0, math.sqrt(2.0 / (d_h + d_out)), (d_h, d_out))
| |||
b2 = np.zeros(d_out)
| |||
return W1, b1, W2, b2
| |||
| |||
| |||
def forward(X, W1, b1, W2, b2):
| |||
Z = np.maximum(0.0, X @ W1 + b1)
| |||
return Z, 1.0 / (1.0 + np.exp(-(Z @ W2 + b2)))
| |||
| |||
| |||
def train(Xtr, ytr, Xva, yva, d_h=HIDDEN, epochs=EPOCHS, batch=BATCH, lr=LR, patience=PATIENCE, seed=42, d_out=1):
| |||
n, d = Xtr.shape
| |||
d_out = ytr.shape[1] if ytr.ndim == 2 else d_out
| |||
W1, b1, W2, b2 = init_mlp(d, d_h, d_out=d_out, seed=seed)
| |||
m1 = [np.zeros_like(W1), np.zeros_like(b1), np.zeros_like(W2), np.zeros_like(b2)]
| |||
v1 = [np.zeros_like(W1), np.zeros_like(b1), np.zeros_like(W2), np.zeros_like(b2)]
| |||
beta1, beta2, eps = 0.9, 0.999, 1e-8
| |||
best_va, best_epoch, best_state = -1.0, 0, None
| |||
t_step = 0
| |||
yv = yva.reshape(-1, 1) if yva.ndim == 1 else yva
| |||
for ep in range(epochs):
| |||
perm = np.random.default_rng(ep).permutation(n)
| |||
for s in range(0, n, batch):
| |||
idx = perm[s:s + batch]
| |||
Xb, yb = Xtr[idx], ytr[idx]
| |||
yb = yb.reshape(-1, 1) if yb.ndim == 1 else yb
| |||
Z, P = forward(Xb, W1, b1, W2, b2)
| |||
dL = (P - yb) / len(idx)
| |||
gW2 = Z.T @ dL
| |||
gb2 = dL.sum(0, keepdims=True)
| |||
dZ = dL @ W2.T
| |||
dZ[Z <= 0] = 0.0
| |||
gW1 = Xb.T @ dZ
| |||
gb1 = dZ.sum(0)
| |||
grads = [gW1, gb1, gW2, gb2.reshape(-1)]
| |||
params = [W1, b1, W2, b2.reshape(-1)]
| |||
t_step += 1
| |||
for i in range(4):
| |||
m1[i] = beta1 * m1[i] + (1 - beta1) * grads[i]
| |||
v1[i] = beta2 * v1[i] + (1 - beta2) * grads[i] ** 2
| |||
mh = m1[i] / (1 - beta1 ** t_step)
| |||
vh = v1[i] / (1 - beta2 ** t_step)
| |||
params[i] -= lr * mh / (np.sqrt(vh) + eps)
| |||
W1, b1 = params[0], params[1]
| |||
W2, b2 = params[2], params[3].reshape(d_out)
| |||
_, Pv = forward(Xva, W1, b1, W2, b2)
| |||
_, Pv = forward(Xva, W1, b1, W2, b2)
| |||
if yv.shape[1] > 1:
| |||
a_l, a_s = val_auc_2head(yv, Pv)
| |||
va = 0.5 * (a_l + a_s)
| |||
else:
| |||
va = auc(yv[:, 0].astype(int), Pv[:, 0])
| |||
if va > best_va:
| |||
best_va = va
| |||
best_epoch = ep
| |||
best_state = (W1.copy(), b1.copy(), W2.copy(), b2.copy())
| |||
if ep - best_epoch > patience:
| |||
break
| |||
return best_state, best_va, best_epoch
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# METRIK (numpy murni)
| |||
# ----------------------------------------------------------------------
| |||
def auc(y, p):
| |||
"""AUC-ROC sederhana (ranking)."""
| |||
order = np.argsort(p)
| |||
y_sorted = y[order]
| |||
n_pos = int(y_sorted.sum())
| |||
n_neg = len(y_sorted) - n_pos
| |||
if n_pos == 0 or n_neg == 0:
| |||
return 0.5
| |||
rank_sum = 0.0
| |||
for i in range(len(y_sorted)):
| |||
if y_sorted[i] == 1:
| |||
rank_sum += i + 1
| |||
return (rank_sum - n_pos * (n_pos + 1) / 2.0) / (n_pos * n_neg)
| |||
| |||
| |||
def val_auc_2head(yv, Pv):
| |||
"""AUC validasi per-head untuk model 2-target (SB-01).
| |||
yv: (n,2) label {long,short}; Pv: (n,2) probabilitas.
| |||
Perbaikan: TIDAK lagi memakai yv.ravel() (bug: mencampur label 2 head
| |||
terhadap 1 head prediksi -> AUC validasi ~0.5 & early stopping rusak)."""
| |||
a_l = auc(yv[:, 0].astype(int), Pv[:, 0])
| |||
a_s = auc(yv[:, 1].astype(int), Pv[:, 1])
| |||
return a_l, a_s
| |||
| |||
| |||
def purged_split(midx, split, gap):
| |||
"""Split temporal dengan PURGE GAP (SB-02).
| |||
midx : bar index sampel berlabel (ascending).
| |||
split: jumlah sampel train yang diinginkan (batas lama).
| |||
gap : purge minimum (H_LABEL).
| |||
Return (tr_idx, te_idx, pg_idx, split_bar)."""
| |||
split_bar = int(midx[split])
| |||
tr = midx[midx < split_bar]
| |||
te = midx[midx >= split_bar + gap]
| |||
pg = midx[(midx >= split_bar) & (midx < split_bar + gap)]
| |||
return tr, te, pg, split_bar
| |||
| |||
| |||
def purge_verify(tr_midx, te_midx, gap):
| |||
"""Verifikasi tidak ada overlap outcome train/test (SB-02):
| |||
max(train_outcome_end) < min(test_outcome_start).
| |||
outcome_end(sampel i) = i + gap ; outcome_start(sampel j) = j + 1."""
| |||
tr_end = int(tr_midx.max()) + gap
| |||
te_start = int(te_midx.min()) + 1
| |||
return tr_end < te_start, tr_end, te_start
| |||
| |||
| |||
def metrics(y, p):
| |||
pred = (p >= 0.5).astype(int)
| |||
acc = float((pred == y).mean())
| |||
tp = float(((pred == 1) & (y == 1)).sum())
| |||
fp = float(((pred == 1) & (y == 0)).sum())
| |||
fn = float(((pred == 0) & (y == 1)).sum())
| |||
prec = tp / (tp + fp) if tp + fp > 0 else 0.0
| |||
rec = tp / (tp + fn) if tp + fn > 0 else 0.0
| |||
return dict(auc=auc(y, p), acc=acc, prec=prec, rec=rec,
| |||
n_pos=int(y.sum()), n=len(y))
| |||
| |||
| |||
def precision_at_threshold(y, p, thr, side='long'):
| |||
"""Presisi pada ambang untuk satu sisi (long: P>=thr; short: P>=thr pada P(short))."""
| |||
lg = p >= thr
| |||
n_lg = int(lg.sum())
| |||
prec_lg = float(y[lg].mean()) if n_lg else 0.0
| |||
return dict(n_long=n_lg, prec_long=prec_lg)
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# KALIBRASI (Platt scaling) - praktik terbaik decision-threshold
| |||
# ----------------------------------------------------------------------
| |||
def fit_platt(o, y, iters=200, lr=0.1):
| |||
"""Fit Platt scaling P(y=1)=sigmoid(A*o+B) memakai IRLS cepat.
| |||
o = logit mentah dari model pada data kalibrasi (validation)."""
| |||
o = np.asarray(o, dtype=float).reshape(-1)
| |||
y = np.asarray(y, dtype=float)
| |||
# inisialisasi stabil
| |||
A = np.array([1.0], dtype=float)
| |||
B = np.array([0.0], dtype=float)
| |||
for _ in range(iters):
| |||
z = A[0] * o + B[0]
| |||
zc = np.clip(z, -30, 30)
| |||
p = 1.0 / (1.0 + np.exp(-zc))
| |||
# robust: clip probabilitas
| |||
p = np.clip(p, 1e-6, 1 - 1e-6)
| |||
err = p - y
| |||
gA = (err * o).mean()
| |||
gB = err.mean()
| |||
# hessian diagonal (IRLS) untuk step
| |||
w = p * (1.0 - p)
| |||
hA = (w * o * o).mean() + 1e-8
| |||
hB = w.mean() + 1e-8
| |||
A[0] -= lr * gA / hA
| |||
B[0] -= lr * gB / hB
| |||
return float(A[0]), float(B[0])
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# EKSPOR KE MQL5
| |||
# ----------------------------------------------------------------------
| |||
def export_mqh(mean, std, W1, b1, W2, b2, meta, path, cal_Al=None, cal_Bl=None, cal_As=None, cal_Bs=None):
| |||
def fmt(v):
| |||
return f"{v:.9g}"
| |||
| |||
L = []
| |||
L.append("//+------------------------------------------------------------------+")
| |||
L.append("//| SniperGold_ML.mqh - model AI/ML tertanam (dihasilkan otomatis) |")
| |||
L.append("//+------------------------------------------------------------------+")
| |||
L.append(f"//| fitur : {NF}")
| |||
L.append(f"//| hidden : {HIDDEN}")
| |||
L.append(f"//| horizon label : {H_LABEL} bar | ambang {LABEL_ATR} x ATR")
| |||
L.append(f"//| AUC validasi : {meta['auc_va']:.4f} | AUC long: {meta.get('auc_l', meta.get('auc_te',0)):.4f} | AUC short: {meta.get('auc_s',0):.4f}")
| |||
L.append(f"//| akurasi test : {meta.get('acc_te','-'):}")
| |||
L.append(f"//| sampel : train {meta['n_tr']} / test {meta['n_te']}")
| |||
L.append(f"//| dibuat : {meta['date']} | {meta['symbol']} | {meta['tf']}")
| |||
if "tag" in meta and meta["tag"]:
| |||
L.append(f"//| versi model : {meta['tag']}")
| |||
L.append("//+------------------------------------------------------------------+")
| |||
L.append("#ifndef SNIPERGOLD_ML_MQH")
| |||
L.append("#define SNIPERGOLD_ML_MQH")
| |||
L.append(f"#define SGML_NF {NF}")
| |||
L.append(f"#define SGML_NH {HIDDEN}")
| |||
ver = meta.get("tag") or meta["date"]
| |||
L.append(f'#define SGML_VER "{ver}"')
| |||
L.append("")
| |||
L.append("// fitur: " + ", ".join(FEAT_NAMES))
| |||
L.append("double SGML_MEAN[" + str(NF) + "]={" + ",".join(fmt(x) for x in mean) + "};")
| |||
L.append("double SGML_STD[" + str(NF) + "]={" + ",".join(fmt(x) for x in std) + "};")
| |||
L.append("")
| |||
L.append("// W1[NF][NH] (baris-mayor)")
| |||
L.append("double SGML_W1[" + str(NF) + "][" + str(HIDDEN) + "]={")
| |||
for j in range(NF):
| |||
L.append(" {" + ",".join(fmt(W1[j, i]) for i in range(HIDDEN)) + "}" +
| |||
("," if j < NF - 1 else ""))
| |||
L.append("};")
| |||
L.append("double SGML_B1[" + str(HIDDEN) + "]={" + ",".join(fmt(x) for x in b1) + "};")
| |||
L.append("double SGML_W2L[" + str(HIDDEN) + "]={" + ",".join(fmt(x) for x in W2[:, 0]) + "};")
| |||
L.append(f"double SGML_B2L={fmt(b2[0])};")
| |||
if W2.shape[1] > 1:
| |||
L.append("double SGML_W2S[" + str(HIDDEN) + "]={" + ",".join(fmt(x) for x in W2[:, 1]) + "};")
| |||
L.append(f"double SGML_B2S={fmt(b2[1])};")
| |||
L.append("")
| |||
# forward helper (hidden) -> logit(o)
| |||
L.append("double SGML_Logit(const double &f[],int out){")
| |||
L.append(" double z[SGML_NF],h[SGML_NH];")
| |||
L.append(" for(int j=0;j<SGML_NF;j++)")
| |||
L.append(" z[j]=(SGML_STD[j]>0.0)?(f[j]-SGML_MEAN[j])/SGML_STD[j]:0.0;")
| |||
L.append(" for(int i=0;i<SGML_NH;i++){")
| |||
L.append(" double s=SGML_B1[i];")
| |||
L.append(" for(int j=0;j<SGML_NF;j++) s+=z[j]*SGML_W1[j][i];")
| |||
L.append(" h[i]=(s>0.0)?s:0.0;")
| |||
L.append(" }")
| |||
L.append(" double o=(out==0?SGML_B2L:SGML_B2S);")
| |||
L.append(" for(int i=0;i<SGML_NH;i++) o+=h[i]*(out==0?SGML_W2L[i]:SGML_W2S[i]);")
| |||
L.append(" return o;")
| |||
L.append("}")
| |||
# P(long) terkalibrasi
| |||
L.append("double SGMLProbLong(const double &f[]){")
| |||
L.append(" double o=SGML_Logit(f,0);")
| |||
if cal_Al is not None:
| |||
L.append(f" return 1.0/(1.0+MathExp(-(o*{fmt(cal_Al)}+{fmt(cal_Bl)})));")
| |||
else:
| |||
L.append(" return 1.0/(1.0+MathExp(-o));")
| |||
L.append("}")
| |||
# P(short) terkalibrasi
| |||
L.append("double SGMLProbShort(const double &f[]){")
| |||
L.append(" double o=SGML_Logit(f,1);")
| |||
if cal_As is not None:
| |||
L.append(f" return 1.0/(1.0+MathExp(-(o*{fmt(cal_As)}+{fmt(cal_Bs)})));")
| |||
else:
| |||
L.append(" return 1.0/(1.0+MathExp(-o));")
| |||
L.append("}")
| |||
# alias backward-compat: SGMLProb = P(long)
| |||
L.append("double SGMLProb(const double &f[]){ return SGMLProbLong(f); }")
| |||
L.append("#endif")
| |||
with open(path, "w", encoding="utf-8") as f:
| |||
f.write("\n".join(L) + "\n")
| |||
print(f" -> {path}")
| |||
| |||
| |||
# ----------------------------------------------------------------------
| |||
# MAIN
| |||
# ----------------------------------------------------------------------
| |||
def main():
| |||
import argparse
| |||
ap = argparse.ArgumentParser()
| |||
ap.add_argument("symbol", nargs="?", default="XAUUSDc")
| |||
ap.add_argument("--out", default=OUT_MQH, help="path output .mqh")
| |||
ap.add_argument("--tag", default="", help="versi/identitas model")
| |||
args = ap.parse_args()
| |||
symbol = args.symbol
| |||
out_mqh = args.out
| |||
cache_path = os.path.join(BASE, f"features_{symbol}.npz")
| |||
| |||
if os.path.exists(cache_path):
| |||
print("Memuat fitur dari cache...")
| |||
z = np.load(cache_path)
| |||
F, label, ATR, close = z["F"], z["label"], z["ATR"], z["close"]
| |||
if F.shape[1] != NF:
| |||
print(f" cache lama ({F.shape[1]} fitur) -> upgrade ke {NF} fitur")
| |||
F = np.column_stack([F, confluence_feature(F)]).astype(float)
| |||
np.savez(cache_path, F=F, label=label, ATR=ATR, close=close, ver=NF)
| |||
else:
| |||
print("Membaca data...")
| |||
m15 = load_csv(os.path.join(BASE, f"{symbol}_M15.csv"))
| |||
htf = {k: load_csv(os.path.join(BASE, f"{symbol}_{k}.csv")) for k in ("D1", "H4", "H1")}
| |||
htf = {k: (v[2], v[3], v[4], v[0]) for k, v in htf.items()}
| |||
| |||
print("Menghitung fitur & label...")
| |||
F, label, ATR, close = build_features(m15, htf)
| |||
np.savez(cache_path, F=F, label=label, ATR=ATR, close=close)
| |||
print(f" cache -> {cache_path}")
| |||
| |||
mask = label != 0
| |||
midx = np.where(mask)[0] # bar index sampel berlabel (ascending)
| |||
X = F[midx]
| |||
y = label[midx]
| |||
print(f" total bar: {len(close)} | sampel berlabel: {len(y)} "
| |||
f"(bull {int((y == 1).sum())}, bear {int((y == -1).sum())})")
| |||
| |||
split = int(0.75 * len(y)) # batas keinginan (jumlah sampel, sama dgn lama)
| |||
# ---- SB-02: PURGED TEMPORAL SPLIT (train | purge gap=H_LABEL | test) ----
| |||
tr_idx, te_idx, pg_idx, split_bar = purged_split(midx, split, H_LABEL)
| |||
tr_m = np.isin(midx, tr_idx)
| |||
te_m = np.isin(midx, te_idx)
| |||
ok_purge, tr_end, te_start = purge_verify(tr_idx, te_idx, H_LABEL)
| |||
if not ok_purge:
| |||
raise RuntimeError("SB-02 purge gap rusak: max_train_end=%d min_test_start=%d"
| |||
% (tr_end, te_start))
| |||
Xtr, ytr = X[tr_m], y[tr_m]
| |||
Xte, yte = X[te_m], y[te_m]
| |||
ytr = (ytr == 1).astype(float)
| |||
yte = (yte == 1).astype(float)
| |||
print(" SB-02 purged split: split_bar=%d train=%d purge=%d test=%d | "
| |||
"max_train_outcome_end=%d < min_test_outcome_start=%d OK"
| |||
% (split_bar, len(tr_idx), len(pg_idx), len(te_idx), tr_end, te_start))
| |||
# (SB-02) Xtr/ytr/Xte/yte dibangun dari purged_split di atas
| |||
# (SB-02) lihat blok purged_split
| |||
va_from = int(0.85 * len(Xtr))
| |||
Xva, yva = Xtr[va_from:], ytr[va_from:]
| |||
Xtr2, ytr2 = Xtr[:va_from], ytr[:va_from]
| |||
| |||
mean = Xtr2.mean(0)
| |||
std = Xtr2.std(0)
| |||
std[std < 1e-9] = 1.0
| |||
Xtr2s = (Xtr2 - mean) / std
| |||
Xvas = (Xva - mean) / std
| |||
Xtes = (Xte - mean) / std
| |||
| |||
print(f"Melatih MLP ({NF}->{HIDDEN}->2)...")
| |||
# label 2-output: kolom0 = P(long), kolom1 = P(short)
| |||
ytwo = np.column_stack([(y == 1).astype(float), (y == -1).astype(float)])
| |||
y2tr = ytwo[tr_m]
| |||
y2te = ytwo[te_m]
| |||
y2va = y2tr[va_from:]
| |||
y2tr2 = y2tr[:va_from]
| |||
| |||
(W1, b1, W2, b2), auc_va, best_ep = train(Xtr2s, y2tr2, Xvas, y2va, d_out=2)
| |||
# (SB-01) print metrik validasi terkoreksi ada di bawah (setelah forward)
| |||
| |||
_, Ptr = forward(Xtr2s, W1, b1, W2, b2)
| |||
_, Pva = forward(Xvas, W1, b1, W2, b2)
| |||
_, Pte = forward(Xtes, W1, b1, W2, b2)
| |||
# P[:,0]=long, P[:,1]=short
| |||
yv_long = y2va[:, 0]; yv_short = y2va[:, 1]
| |||
yte_long = y2te[:, 0]; yte_short = y2te[:, 1]
| |||
ytr_long = y2tr2[:, 0]; ytr_short = y2tr2[:, 1]
| |||
| |||
mtr_l = metrics(ytr_long.astype(int), Ptr[:, 0])
| |||
mva_l = metrics(yv_long.astype(int), Pva[:, 0])
| |||
mte_l = metrics(yte_long.astype(int), Pte[:, 0])
| |||
mte_s = metrics(yte_short.astype(int), Pte[:, 1])
| |||
mva_s = metrics(yv_short.astype(int), Pva[:, 1])
| |||
auc_va_l = mva_l["auc"]
| |||
auc_va_s = mva_s["auc"]
| |||
auc_va = 0.5 * (auc_va_l + auc_va_s) # SB-01: mean AUC validasi 2-head (bukan ravel ~0.5)
| |||
print(f" AUC validasi (SB-01 corrected): LONG={auc_va_l:.4f} SHORT={auc_va_s:.4f} "
| |||
f"mean={auc_va:.4f} (epoch terbaik {best_ep})")
| |||
| |||
# ---- KALIBRASI Platt (fit pada VALIDASI; test tak disentuh) per sisi ----
| |||
lo_l = np.log(np.clip(Pva[:, 0], 1e-6, 1 - 1e-6) / (1 - np.clip(Pva[:, 0], 1e-6, 1 - 1e-6)))
| |||
cal_Al, cal_Bl = fit_platt(lo_l, yv_long)
| |||
lo_s = np.log(np.clip(Pva[:, 1], 1e-6, 1 - 1e-6) / (1 - np.clip(Pva[:, 1], 1e-6, 1 - 1e-6)))
| |||
cal_As, cal_Bs = fit_platt(lo_s, yv_short)
| |||
| |||
print(f" LONG : TRAIN AUC {mtr_l['auc']:.4f} | VAL {mva_l['auc']:.4f} | TEST {mte_l['auc']:.4f}")
| |||
print(f" SHORT: TEST AUC {mte_s['auc']:.4f}")
| |||
print(f" CALIBRASI LONG A={cal_Al:.4f} B={cal_Bl:.4f} | SHORT A={cal_As:.4f} B={cal_Bs:.4f}")
| |||
| |||
print(" Presisi per ambang (test):")
| |||
for thr in (0.55, 0.60, 0.65, 0.70, 0.75):
| |||
rl = precision_at_threshold(yte_long.astype(int), Pte[:, 0], thr, side='long')
| |||
rs = precision_at_threshold(yte_short.astype(int), Pte[:, 1], thr, side='short')
| |||
print(f" p>={thr:.2f}: LONG n={rl['n_long']} prec={rl['prec_long']:.3f}"
| |||
f" | SHORT n={rs['n_long']} prec={rs['prec_long']:.3f}")
| |||
| |||
# korelasi fitur vs label bullish (informasi tambahan)
| |||
yb = (y == 1).astype(float)
| |||
C = np.corrcoef(X.T, yb)
| |||
corr = [(FEAT_NAMES[j], C[j, -1]) for j in range(NF)]
| |||
corr.sort(key=lambda x: -abs(x[1]))
| |||
print(" Korelasi fitur vs label bullish (terkuat):")
| |||
for name, cj in corr[:8]:
| |||
print(f" {name:16s} {cj:+.4f}")
| |||
| |||
meta = dict(date=dt.date.today().isoformat(), symbol=symbol, tf="M15",
| |||
auc_va=auc_va, auc_l=mte_l["auc"], auc_s=mte_s["auc"],
| |||
n_tr=len(y2tr2), n_te=len(yte), n_purge=len(pg_idx),
| |||
split_bar=int(split_bar), purge_gap=int(H_LABEL))
| |||
meta["cal_Al"] = cal_Al; meta["cal_Bl"] = cal_Bl
| |||
meta["cal_As"] = cal_As; meta["cal_Bs"] = cal_Bs
| |||
if args.tag:
| |||
meta["tag"] = args.tag
| |||
export_mqh(mean, std, W1, b1, W2, b2, meta, out_mqh,
| |||
cal_Al=cal_Al, cal_Bl=cal_Bl, cal_As=cal_As, cal_Bs=cal_Bs)
| |||
print(f"METRICS auc_va={auc_va:.4f} auc_long={mte_l['auc']:.4f} auc_short={mte_s['auc']:.4f} "
| |||
f"n_tr={len(y2tr2)} n_purge={len(pg_idx)} n_te={len(yte)} "
| |||
f"split_bar={split_bar} tag={args.tag}")
| |||
print("Selesai.")
| |||
| |||
| |||
def selftest():
| |||
"""Regression test P1 (SB-01 & SB-02). Fail pada implementasi lama, PASS pada baru."""
| |||
ok = True
| |||
print("=== SELFTEST P1 (SB-01 / SB-02) ===")
| |||
| |||
# ---- Test A: AUC validasi per-head (SB-01) ----
| |||
print("--- Test A: per-head validation AUC (bukan ravel) ---")
| |||
yv = np.array([[1, 0], [1, 0], [1, 0], [1, 0],
| |||
[0, 1], [0, 1], [0, 1], [0, 1]], dtype=float)
| |||
Pv = np.array([[0.9, 0.1], [0.8, 0.2], [0.7, 0.3], [0.6, 0.4],
| |||
[0.4, 0.6], [0.3, 0.7], [0.2, 0.8], [0.1, 0.9]])
| |||
a_l, a_s = val_auc_2head(yv, Pv)
| |||
legacy = auc(yv.ravel(), Pv[:, 0]) # implementasi LAMA (bug: campur 2 head)
| |||
exp_l, exp_s = 1.0, 1.0
| |||
ok_a = abs(a_l - exp_l) < 1e-9 and abs(a_s - exp_s) < 1e-9
| |||
ok_legacy_diff = abs(legacy - exp_l) > 1e-6 # buktikan formula lama salah
| |||
print(f" AUC_LONG={a_l:.4f} (exp {exp_l:.4f}) | AUC_SHORT={a_s:.4f} (exp {exp_s:.4f}) "
| |||
f"| legacy_ravel={legacy:.4f} (harus != {exp_l:.4f})")
| |||
print(f" -> per-head PASS={ok_a} | legacy-differs PASS={ok_legacy_diff}")
| |||
ok &= ok_a and ok_legacy_diff
| |||
| |||
# ---- Test B: purged temporal split (SB-02) ----
| |||
print("--- Test B: purge gap (train | gap=H_LABEL | test) ---")
| |||
midx = np.arange(200)
| |||
H = 24
| |||
split = 150
| |||
tr_old = midx[:split]
| |||
te_old = midx[split:]
| |||
tr_new, te_new, pg_new, split_bar = purged_split(midx, split, H)
| |||
ok_new, tr_end, te_start = purge_verify(tr_new, te_new, H)
| |||
ok_old, tr_end_old, te_start_old = purge_verify(tr_old, te_old, H)
| |||
print(f" OLD split: max_train_outcome_end={tr_end_old} "
| |||
f"min_test_outcome_start={te_start_old} -> overlap={not ok_old} (harus True=rusak)")
| |||
print(f" NEW split: max_train_outcome_end={tr_end} "
| |||
f"min_test_outcome_start={te_start} -> clean={ok_new} (harus True) "
| |||
f"| purge={len(pg_new)} bar | split_bar={split_bar}")
| |||
print(f" -> old-rusak PASS={not ok_old} | new-clean PASS={ok_new}")
| |||
ok &= ok_new and (not ok_old)
| |||
| |||
print("\n=== SELFTEST P1:", "PASS" if ok else "FAIL", "===")
| |||
return ok
| |||
| |||
| |||
if __name__ == "__main__":
| |||
import argparse as _ap
| |||
_ap = _ap.ArgumentParser(add_help=False)
| |||
_ap.add_argument("--selftest", action="store_true")
| |||
_args_selftest, _ = _ap.parse_known_args()
| |||
if _args_selftest.selftest:
| |||
sys.exit(0 if selftest() else 1)
| |||
main()
| |||
main()
|