2026-08-22 07:23:37 +07:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
"""SMC SEMANTIC COMMON — infra bersama utk P3 SMC Semantic Golden Dataset (Fase: Liquidity Sweep).
|
|
|
|
|
|
|
|
|
|
Reuse p3_common (load data 2017+, F cache P2.6, ATR, provenance) + train_model
|
|
|
|
|
(build_structure) — TIDAK membuat framework duplikat.
|
|
|
|
|
|
|
|
|
|
Mengimplementasikan SEMANTIK KODE SAAT INI secara eksak (FEATURE_CONTRACT v1.0,
|
|
|
|
|
parity-verified thd EA AlgoForge_Backtest_Baseline.mq5 / v4.4/v4.5):
|
|
|
|
|
|
|
|
|
|
f7 sweep_dir : DetectLiquidityGrabs — pivot INTERNAL (len=5), GRAB_WINDOW=8,
|
|
|
|
|
wick menembus level + close kembali (REJECTION ADA), state persist.
|
|
|
|
|
f10 eqh_swept : DetectEQ — pasangan swing high berurutan (len=50),
|
|
|
|
|
|dp| <= 0.10*ATR(row), swept bila ada bar setelah p2 dgn high>pr2
|
|
|
|
|
(REJECTION TIDAK ADA — dipilih via AUC, lihat DESAIN_MTF_v45.md).
|
|
|
|
|
f11 eql_swept : DetectEQ — sama utk lows (REJECTION TIDAK ADA).
|
|
|
|
|
|
|
|
|
|
Disiplin P3: OBSERVE -> HYPOTHESIS -> TEST -> RECORD -> CLASSIFY.
|
|
|
|
|
Tidak ada definisi Sweep baru; hanya mengukur apa yang dilakukan kode sekarang.
|
|
|
|
|
"""
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import json
|
|
|
|
|
import hashlib
|
|
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
sys.path.insert(0, os.path.normpath(os.path.join(HERE, ".."))) # ml/p3
|
|
|
|
|
sys.path.insert(0, os.path.normpath(os.path.join(HERE, "..", "..", "..", "..",
|
|
|
|
|
"SniperGold_ML"))) # Shared Projects\SniperGold_ML
|
|
|
|
|
|
|
|
|
|
import p3_common as P3 # data + F cache + ATR + provenance
|
|
|
|
|
import train_model as TM # build_structure + constants
|
|
|
|
|
|
|
|
|
|
# ---- konstanta FEATURE_CONTRACT v1.0 (sumber: train_model.py / EA #define) ----
|
|
|
|
|
SWING_LEN = TM.SWING_LEN # 50 (InpSwingLen, v4.4)
|
|
|
|
|
INTERNAL_LEN = TM.INTERNAL_LEN # 5 (InpInternalLen, v4.4)
|
|
|
|
|
EQ_TOL_ATR = TM.EQ_TOL_ATR # 0.10 (InpEQThreshold, v4.4)
|
|
|
|
|
EQ_BARS = TM.EQ_BARS # 3 (InpEQBarsConfirm, v4.4)
|
|
|
|
|
GRAB_WINDOW = TM.GRAB_WINDOW # 8 (InpGrabWindow, v4.4)
|
2026-08-22 07:46:00 +07:00
|
|
|
SEQ_WINDOW = 40 # InpSeqWindow (v4.4/v4.5): sweep -> CHoCH -> entry chain
|
|
|
|
|
# = event validity/expiry utk f7 (P3-S.0 f7 lifecycle fix)
|
2026-08-22 07:23:37 +07:00
|
|
|
ATR_PERIOD = TM.ATR_PERIOD # 14
|
|
|
|
|
# window pivot valid absolut utk f10/f11 & sw_high/sw_low (FEATURE_CONTRACT §0)
|
|
|
|
|
W_LO = 649
|
|
|
|
|
W_HI = 50
|
|
|
|
|
|
|
|
|
|
DECISION_TF = "M15"
|
|
|
|
|
SYMBOL = "XAUUSD"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_data():
|
|
|
|
|
"""t,o,h,l,c,v (M15, 2017+) + htf dict (D1/H4/H1) — sama dgn P2.6/P3."""
|
|
|
|
|
return P3.load_data()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_F():
|
|
|
|
|
"""F (n,19) corrected P2.6 cache — utk cross-check & stratifikasi sampling."""
|
|
|
|
|
return P3.load_F()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def atr_series(h, l, c):
|
|
|
|
|
return P3.atr_series(h, l, c)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_structures(o, h, l, c):
|
|
|
|
|
"""Swing (len=50) + internal (len=5) pivot lists, full-feed, begin=100.
|
|
|
|
|
Semantik identik build_features_p2 (parity-verified 1e-9 thd EA)."""
|
|
|
|
|
n = len(c)
|
|
|
|
|
sw_at = np.zeros(n, dtype=int)
|
|
|
|
|
sw_full = TM.build_structure(o, h, l, c, SWING_LEN, False, sw_at, begin=100)
|
|
|
|
|
inn_full = TM.build_structure(o, h, l, c, INTERNAL_LEN, True,
|
|
|
|
|
sw_full["trend"].copy(), begin=100)
|
|
|
|
|
return sw_full, inn_full
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sweep_state(h, l, c, inn_pivots):
|
|
|
|
|
"""f7 sweep_dir + sweep_bar per bar (eksak build_features_p2 / EA DetectLiquidityGrabs).
|
2026-08-22 07:46:00 +07:00
|
|
|
Returns (sweep_dir int[n], sweep_bar int[n]).
|
|
|
|
|
NOTE: ini state LEGACY (persist tanpa batas umur) — bukan output f7 final."""
|
2026-08-22 07:23:37 +07:00
|
|
|
n = len(c)
|
|
|
|
|
sweep_dir = np.zeros(n, dtype=int)
|
|
|
|
|
sweep_bar = np.full(n, -1, dtype=int)
|
|
|
|
|
cur_sd, cur_sb = 0, -1
|
|
|
|
|
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 = -1, b
|
|
|
|
|
break
|
|
|
|
|
if (not is_high) and l[b] < lvl and c[b] > lvl:
|
|
|
|
|
if b > cur_sb:
|
|
|
|
|
cur_sd, cur_sb = 1, b
|
|
|
|
|
break
|
|
|
|
|
if cur_sb >= 0:
|
|
|
|
|
sweep_dir[cur_sb:] = cur_sd
|
|
|
|
|
sweep_bar[cur_sb:] = cur_sb
|
|
|
|
|
return sweep_dir, sweep_bar
|
|
|
|
|
|
|
|
|
|
|
2026-08-22 07:46:00 +07:00
|
|
|
def f7_lifecycle(sweep_dir, sweep_bar):
|
|
|
|
|
"""P3-S.0 corrected f7: event lifecycle dgn expiration SEQ_WINDOW (40 bar).
|
|
|
|
|
NO_SWEEP -> SWEEP_ONSET -> EXPIRED -> NO_SWEEP.
|
|
|
|
|
f7[r] = sweep_dir[r] jika grab onset di sweep_bar[r] dan (r - sweep_bar[r]) <= SEQ_WINDOW,
|
|
|
|
|
selain itu 0. Trigger condition TIDAK diubah — hanya membatasi umur state sbg event.
|
|
|
|
|
Return int[n]."""
|
|
|
|
|
n = len(sweep_dir)
|
|
|
|
|
out = np.zeros(n, dtype=int)
|
|
|
|
|
for r in range(n):
|
|
|
|
|
sb = int(sweep_bar[r])
|
|
|
|
|
if sb >= 0 and (r - sb) <= SEQ_WINDOW:
|
|
|
|
|
out[r] = int(sweep_dir[r])
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
2026-08-22 07:23:37 +07:00
|
|
|
def eq_pairs(h, l, sp):
|
|
|
|
|
"""EQH/EQL pasangan berurutan (p1,p2,first_cross,|dp|) — eksak build_features_p2.
|
|
|
|
|
sp = sw_full['pivots'] : list (p, price, is_high_int)."""
|
|
|
|
|
n = len(h)
|
|
|
|
|
pairs_h = [] # (p1, p2, first_cross, |dp|)
|
|
|
|
|
pairs_l = []
|
|
|
|
|
for k in range(1, len(sp)):
|
|
|
|
|
p1, pr1, ih1 = sp[k - 1]
|
|
|
|
|
p2, pr2, ih2 = sp[k]
|
|
|
|
|
if abs(p2 - p1) < EQ_BARS:
|
|
|
|
|
continue
|
|
|
|
|
if ih1 and ih2:
|
|
|
|
|
q = np.where(h[p2 + 1:] > pr2)[0]
|
|
|
|
|
fc = p2 + 1 + q[0] if len(q) else n + 1
|
|
|
|
|
pairs_h.append((p1, p2, fc, abs(pr2 - pr1)))
|
|
|
|
|
if (not ih1) and (not ih2):
|
|
|
|
|
q = np.where(l[p2 + 1:] < pr2)[0]
|
|
|
|
|
fc = p2 + 1 + q[0] if len(q) else n + 1
|
|
|
|
|
pairs_l.append((p1, p2, fc, abs(pr2 - pr1)))
|
|
|
|
|
pairs_h = np.array(pairs_h, dtype=np.float64) if pairs_h else np.zeros((0, 4))
|
|
|
|
|
pairs_l = np.array(pairs_l, dtype=np.float64) if pairs_l else np.zeros((0, 4))
|
|
|
|
|
return pairs_h, pairs_l
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def eq_state_at(r, pairs_h, pairs_l, A):
|
|
|
|
|
"""f10/f11 state + detail pair paling baru yg memenuhi window [r-649, r-50],
|
|
|
|
|
first_cross<=r, |dp|<=tol (tol=0.10*ATR(r)). Returns dict."""
|
|
|
|
|
lo_r, hi_r = r - W_LO, r - W_HI
|
|
|
|
|
tol = EQ_TOL_ATR * A[r]
|
|
|
|
|
out = {"eqh": 0, "eql": 0, "pair_h": None, "pair_l": None,
|
|
|
|
|
"tol_atr": float(tol), "lo_r": int(lo_r), "hi_r": int(hi_r)}
|
|
|
|
|
if len(pairs_h):
|
|
|
|
|
mh = ((pairs_h[:, 0] >= lo_r) & (pairs_h[:, 1] <= hi_r) &
|
|
|
|
|
(pairs_h[:, 2] <= r) & (pairs_h[:, 3] <= tol))
|
|
|
|
|
if mh.any():
|
|
|
|
|
sel = pairs_h[mh]
|
|
|
|
|
k = int(np.argmax(sel[:, 1])) # pair dgn p2 paling baru
|
|
|
|
|
out["eqh"] = 1
|
|
|
|
|
out["pair_h"] = [int(sel[k, 0]), int(sel[k, 1]), int(sel[k, 2]),
|
|
|
|
|
float(sel[k, 3])]
|
|
|
|
|
if len(pairs_l):
|
|
|
|
|
ml = ((pairs_l[:, 0] >= lo_r) & (pairs_l[:, 1] <= hi_r) &
|
|
|
|
|
(pairs_l[:, 2] <= r) & (pairs_l[:, 3] <= tol))
|
|
|
|
|
if ml.any():
|
|
|
|
|
sel = pairs_l[ml]
|
|
|
|
|
k = int(np.argmax(sel[:, 1]))
|
|
|
|
|
out["eql"] = 1
|
|
|
|
|
out["pair_l"] = [int(sel[k, 0]), int(sel[k, 1]), int(sel[k, 2]),
|
|
|
|
|
float(sel[k, 3])]
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def active_grab_at(r, sweep_dir, sweep_bar, h, l, c):
|
|
|
|
|
"""Detail grab internal paling baru yg aktif di bar r (f7).
|
|
|
|
|
Returns dict atau None bila tidak ada grab aktif."""
|
|
|
|
|
sb = int(sweep_bar[r])
|
|
|
|
|
if sb < 0:
|
|
|
|
|
return None
|
|
|
|
|
d = int(sweep_dir[r])
|
|
|
|
|
return {"sweep_bar": sb, "dir": d,
|
|
|
|
|
"sweep_dir_state": d}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def provenance():
|
|
|
|
|
p = P3.provenance()
|
|
|
|
|
p["SMC_SEMANTIC_PHASE"] = "LIQUIDITY_SWEEP"
|
|
|
|
|
p["contract"] = {
|
|
|
|
|
"SWING_LEN": SWING_LEN, "INTERNAL_LEN": INTERNAL_LEN,
|
|
|
|
|
"EQ_TOL_ATR": EQ_TOL_ATR, "EQ_BARS": EQ_BARS,
|
|
|
|
|
"GRAB_WINDOW": GRAB_WINDOW, "ATR_PERIOD": ATR_PERIOD,
|
|
|
|
|
"window": f"[r-{W_LO}, r-{W_HI}]",
|
|
|
|
|
}
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_json(name, obj):
|
|
|
|
|
outdir = os.path.join(HERE, "output")
|
|
|
|
|
os.makedirs(outdir, exist_ok=True)
|
|
|
|
|
path = os.path.join(outdir, name)
|
|
|
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
|
|
|
json.dump(obj, f, indent=2, default=float)
|
|
|
|
|
print(f" [saved] {path}")
|
|
|
|
|
return path
|