313 lines
14 KiB
Python
313 lines
14 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""AUDIT LIQUIDITY SWEEP — uji semantik kode existing (TANPA modifikasi production).
|
||
|
|
|
||
|
|
Menguji 3 primitif sweep FEATURE_CONTRACT v1.0:
|
||
|
|
f7 (DetectLiquidityGrabs), f10/f11 (DetectEQ)
|
||
|
|
|
||
|
|
A. DEFINISI AKTUAL KODE (ekstraksi otomatis, bukan asumsi)
|
||
|
|
B. EVENT VS STATE TEST (section 17):
|
||
|
|
- onset transition vs total active bars (satu onset = SATU event)
|
||
|
|
- run-length distribusi state f7/f10/f11
|
||
|
|
- state age f7 (berapa lama state bertahan setelah grab)
|
||
|
|
C. REPEATED-EVENT CHECK utk stream f9-confirm (E_BUY/E_SELL P3.2.2)
|
||
|
|
D. WINDOWED vs FULL-FEED divergence f7 (EA 700-bar window vs training full feed)
|
||
|
|
E. SEMANTIC CHECKLIST (reference, rejection, ATR, threshold, timeframe, emission)
|
||
|
|
F. REGRESSION TEST SPECS (Given/When/Expected/Current) — spek SAJA, TIDAK fix
|
||
|
|
|
||
|
|
Usage: python audit_liquidity_sweep.py
|
||
|
|
"""
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import json
|
||
|
|
import datetime as dt
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||
|
|
sys.path.insert(0, HERE)
|
||
|
|
import smc_semantic_common as SC
|
||
|
|
|
||
|
|
|
||
|
|
def run_stats(mask):
|
||
|
|
rr = []
|
||
|
|
cur = mask[0]
|
||
|
|
ln = 1
|
||
|
|
for i in range(1, len(mask)):
|
||
|
|
if mask[i] == cur:
|
||
|
|
ln += 1
|
||
|
|
else:
|
||
|
|
if cur:
|
||
|
|
rr.append(ln)
|
||
|
|
cur = mask[i]
|
||
|
|
ln = 1
|
||
|
|
if cur:
|
||
|
|
rr.append(ln)
|
||
|
|
return np.array(rr)
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
t, o, h, l, c, v, htf = SC.load_data()
|
||
|
|
F = SC.load_F()
|
||
|
|
A = SC.atr_series(h, l, c)
|
||
|
|
n = len(c)
|
||
|
|
|
||
|
|
sw_full, inn_full = SC.build_structures(o, h, l, c)
|
||
|
|
pairs_h, pairs_l = SC.eq_pairs(h, l, sw_full["pivots"])
|
||
|
|
sweep_dir, sweep_bar = SC.sweep_state(h, l, c, inn_full["pivots"])
|
||
|
|
|
||
|
|
f7 = F[:, 7].astype(int)
|
||
|
|
f10 = F[:, 10].astype(int)
|
||
|
|
f11 = F[:, 11].astype(int)
|
||
|
|
f9 = F[:, 9].astype(int)
|
||
|
|
|
||
|
|
report = {}
|
||
|
|
|
||
|
|
# ================= A. DEFINISI AKTUAL KODE =================
|
||
|
|
report["definition_f7_grab"] = {
|
||
|
|
"reference": "internal pivot (fractal len=5) — ProcessStructure(INTERNAL_LEN=5, internal=true)",
|
||
|
|
"condition_bearish": "isHigh && high[b] > lvl && close[b] < lvl (buy-side swept -> dir=-1)",
|
||
|
|
"condition_bullish": "!isHigh && low[b] < lvl && close[b] > lvl (sell-side swept -> dir=+1)",
|
||
|
|
"lookback_window": f"{SC.GRAB_WINDOW} bar setelah pivot (b in (p, p+8])",
|
||
|
|
"atr_usage": "TIDAK dipakai",
|
||
|
|
"minimum_excess": "0 (semua wick > lvl sudah dihitung, tanpa ambang)",
|
||
|
|
"close_requirement": "YA (close kembali di bawah/atas level)",
|
||
|
|
"reference_liquidity": "single internal swing high/low (bukan EQ pair)",
|
||
|
|
"timeframe": "M15 (hanya chart TF; fitur f7 dihitung pada bar M15 tertutup)",
|
||
|
|
"event_emission": "STATE: g_swpDir/g_swpBar disimpan dan persist sampai grab lebih baru "
|
||
|
|
"(state age tidak dibatasi)",
|
||
|
|
"multiple_per_bar": "v4.4/v4.5 menggambar 1 arrow per (pivot, bar pertama match); "
|
||
|
|
"beberapa pivot dpt menghasilkan beberapa arrow pd bar sama",
|
||
|
|
}
|
||
|
|
report["definition_f10_f11_eq"] = {
|
||
|
|
"reference": "pasangan SWING pivot berurutan (fractal len=50), same-type (HH atau LL)",
|
||
|
|
"pair_requirement": f"|bar(p2)-bar(p1)| >= {SC.EQ_BARS} ; |price(p2)-price(p1)| <= "
|
||
|
|
f"{SC.EQ_TOL_ATR}*ATR(row)",
|
||
|
|
"swept_condition": "EQH: ada bar b in (p2, r] dgn high[b] > pr2 ; "
|
||
|
|
"EQL: ada bar b in (p2, r] dgn low[b] < pr2",
|
||
|
|
"lookback_window": f"p1 >= r-{SC.W_LO} DAN p2 <= r-{SC.W_HI} (pivot valid absolut)",
|
||
|
|
"atr_usage": f"YA — tolerance = {SC.EQ_TOL_ATR} * ATR(bar row r) (bukan ATR pivot)",
|
||
|
|
"minimum_excess": "0 (tembus wick berapa pun dihitung)",
|
||
|
|
"close_requirement": "TIDAK ADA (close-back DITARIK karena uji AUC — DESAIN_MTF_v45.md)",
|
||
|
|
"reference_liquidity": "equal highs (EQH) / equal lows (EQL)",
|
||
|
|
"timeframe": "M15",
|
||
|
|
"event_emission": "STATE monotonic: eqh_swept/eql_swept = 1 utk semua r >= first_cross "
|
||
|
|
"sampai pair keluar window [r-649, r-50] (~649 bar)",
|
||
|
|
}
|
||
|
|
|
||
|
|
# ================= B. EVENT VS STATE TEST =================
|
||
|
|
sb_events = np.unique(sweep_bar[sweep_bar >= 0])
|
||
|
|
f7_active_bars = int((f7 != 0).sum())
|
||
|
|
f7_onset = len(sb_events)
|
||
|
|
# run "aktif" (f7 != 0): apakah state pernah kembali ke 0 setelah grab pertama?
|
||
|
|
runs_any = run_stats((f7 != 0).astype(int))
|
||
|
|
# run arah (f7 == nilai yg sama): berapa lama SATU grab mendominasi state
|
||
|
|
runs_dir = []
|
||
|
|
cur, ln = f7[0], 1
|
||
|
|
for i in range(1, n):
|
||
|
|
if f7[i] == cur:
|
||
|
|
ln += 1
|
||
|
|
else:
|
||
|
|
if cur != 0:
|
||
|
|
runs_dir.append(ln)
|
||
|
|
cur, ln = f7[i], 1
|
||
|
|
if cur != 0:
|
||
|
|
runs_dir.append(ln)
|
||
|
|
runs_dir = np.array(runs_dir)
|
||
|
|
report["event_state_f7"] = {
|
||
|
|
"active_bars": f7_active_bars,
|
||
|
|
"onset_events": f7_onset,
|
||
|
|
"repetition_ratio": round(f7_active_bars / max(1, f7_onset), 1),
|
||
|
|
"any_active_runs_n": int(len(runs_any)),
|
||
|
|
"any_active_run_len": int(runs_any.sum()) if len(runs_any) else None,
|
||
|
|
"never_returns_to_zero": bool(len(runs_any) == 1 and runs_any[0] >= n - 200),
|
||
|
|
"dir_run_median": int(np.median(runs_dir)) if len(runs_dir) else None,
|
||
|
|
"dir_run_max": int(runs_dir.max()) if len(runs_dir) else None,
|
||
|
|
"dir_run_n": int(len(runs_dir)),
|
||
|
|
"note": "f7 TIDAK pernah reset ke 0 setelah grab pertama (state permanen); "
|
||
|
|
"satu grab mendominasi state selama median dir_run bar",
|
||
|
|
"verdict": "STATE PERMANEN (bukan event) — hipotesis D terkonfirmasi di level populasi",
|
||
|
|
}
|
||
|
|
|
||
|
|
def onset_offset(x):
|
||
|
|
tr = np.diff(np.concatenate(([0], x.astype(int))))
|
||
|
|
return int((tr == 1).sum()), int((tr == -1).sum())
|
||
|
|
|
||
|
|
for name, arr in (("f10_eqh", f10), ("f11_eql", f11)):
|
||
|
|
on, off = onset_offset(arr)
|
||
|
|
true_bars = int((arr == 1).sum())
|
||
|
|
rr = run_stats(arr.astype(int))
|
||
|
|
report[f"event_state_{name}"] = {
|
||
|
|
"true_bars": true_bars,
|
||
|
|
"onsets": on,
|
||
|
|
"offsets": off,
|
||
|
|
"repetition_ratio": round(true_bars / max(1, on), 1),
|
||
|
|
"state_run_median": int(np.median(rr)) if len(rr) else None,
|
||
|
|
"state_run_max": int(rr.max()) if len(rr) else None,
|
||
|
|
"state_run_n": int(len(rr)),
|
||
|
|
"note": "onset 1x -> state bertahan ~r sampai pair keluar window (max ~649 bar)",
|
||
|
|
"verdict": "STATE MONOTONIC (event hanya pada onset)",
|
||
|
|
}
|
||
|
|
|
||
|
|
age = np.full(n, -1)
|
||
|
|
for i in range(n):
|
||
|
|
if sweep_bar[i] >= 0:
|
||
|
|
age[i] = i - sweep_bar[i]
|
||
|
|
age_pos = age[age >= 0]
|
||
|
|
report["f7_state_age_bars"] = {
|
||
|
|
"median": int(np.median(age_pos)) if len(age_pos) else None,
|
||
|
|
"p90": int(np.percentile(age_pos, 90)) if len(age_pos) else None,
|
||
|
|
"max": int(age_pos.max()) if len(age_pos) else None,
|
||
|
|
"frac_age_lt_8": round(float((age_pos < 8).mean()), 4),
|
||
|
|
"frac_age_ge_16": round(float((age_pos >= 16).mean()), 4),
|
||
|
|
"frac_age_ge_40": round(float((age_pos >= 40).mean()), 4),
|
||
|
|
}
|
||
|
|
|
||
|
|
# ================= C. REPEATED-EVENT f9-confirm =================
|
||
|
|
e_buy = (f9 == 1) & (f7 > 0)
|
||
|
|
e_sell = (f9 == 1) & (f7 < 0)
|
||
|
|
report["repeated_event_f9confirm"] = {}
|
||
|
|
for name, mask in (("E_BUY", e_buy), ("E_SELL", e_sell)):
|
||
|
|
rr = run_stats(mask.astype(int))
|
||
|
|
tot = int(mask.sum())
|
||
|
|
report["repeated_event_f9confirm"][name] = {
|
||
|
|
"event_bars": tot,
|
||
|
|
"runs": int(len(rr)),
|
||
|
|
"run_median": int(np.median(rr)) if len(rr) else None,
|
||
|
|
"run_max": int(rr.max()) if len(rr) else None,
|
||
|
|
"repetition_ratio": round(tot / max(1, len(rr)), 1),
|
||
|
|
}
|
||
|
|
|
||
|
|
# ================= D. WINDOWED vs FULL-FEED (f7) =================
|
||
|
|
rnd = np.random.RandomState(7)
|
||
|
|
probe = np.sort(rnd.choice(np.arange(700, n), size=min(1500, n - 700), replace=False))
|
||
|
|
div = 0
|
||
|
|
n_probe = len(probe)
|
||
|
|
for r in probe:
|
||
|
|
s = r - 699
|
||
|
|
o_w, h_w, l_w, c_w = o[s:r + 1], h[s:r + 1], l[s:r + 1], c[s:r + 1]
|
||
|
|
sw_at_w = np.zeros(len(c_w), dtype=int)
|
||
|
|
sw_w = SC.TM.build_structure(o_w, h_w, l_w, c_w, SC.SWING_LEN, False,
|
||
|
|
sw_at_w, begin=100)
|
||
|
|
inn_w = SC.TM.build_structure(o_w, h_w, l_w, c_w, SC.INTERNAL_LEN, True,
|
||
|
|
sw_w["trend"].copy(), begin=100)
|
||
|
|
cur_sd, cur_sb = 0, -1
|
||
|
|
for (p, lvl, is_high) in inn_w["pivots"]:
|
||
|
|
pa = p + s
|
||
|
|
last = min(r, pa + SC.GRAB_WINDOW)
|
||
|
|
for b in range(pa + 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 and cur_sd != f7[r]:
|
||
|
|
div += 1
|
||
|
|
report["f7_windowed_vs_fullfeed"] = {
|
||
|
|
"probed_bars": n_probe,
|
||
|
|
"divergent_bars": div,
|
||
|
|
"divergence_rate": round(div / max(1, n_probe), 6),
|
||
|
|
"note": "EA memakai window 700 bar; training (parity-verified) memakai full feed. "
|
||
|
|
"Divergensi teoretis bila grab terakhir berasal dr pivot < r-699.",
|
||
|
|
}
|
||
|
|
|
||
|
|
# ================= E. SEMANTIC CHECKLIST =================
|
||
|
|
report["semantic_checklist"] = {
|
||
|
|
"reference": {
|
||
|
|
"f7": "internal swing (len=5) single level",
|
||
|
|
"f10/f11": "EQ pair swing (len=50)",
|
||
|
|
},
|
||
|
|
"rejection": {
|
||
|
|
"f7": "ADA (close-back)",
|
||
|
|
"f10/f11": "TIDAK ADA (ditarik via uji AUC — definisi dipilih berdasarkan backtest, "
|
||
|
|
"bukan semantik SMC)",
|
||
|
|
},
|
||
|
|
"atr": {"f7": "TIDAK", "f10/f11": "YA (tol 0.10*ATR row)"},
|
||
|
|
"threshold": {"f7": "TIDAK (excess>=0)", "f10/f11": "hanya utk |dp| EQ, bukan excess sweep"},
|
||
|
|
"timeframe": {"f7": "M15", "f10/f11": "M15"},
|
||
|
|
"emission": {"f7": "state persist tanpa batas umur", "f10/f11": "state monotonic ~649 bar"},
|
||
|
|
"per_bar_multiple": "f7 dapat mengaktifkan beberapa arrow (beberapa pivot) pd bar yg sama; "
|
||
|
|
"state hanya menyimpan yg paling baru",
|
||
|
|
}
|
||
|
|
|
||
|
|
# ================= F. REGRESSION TEST SPECS (spek SAJA) =================
|
||
|
|
report["regression_test_specs"] = [
|
||
|
|
{
|
||
|
|
"id": "R1_EVENT_VS_STATE_F10",
|
||
|
|
"given": "EQH pair valid (p1,p2,fc) dgn fc <= r. Kondisi terpenuhi mulai r=fc.",
|
||
|
|
"when": "r berjalan fc, fc+1, ..., fc+649",
|
||
|
|
"expected": "SATU event di onset (r=fc); state boleh bertahan utk fitur kontinu",
|
||
|
|
"current": "f10==1 utk SEMUA r in [fc, fc+~649] — stream event menghitung ~649 event "
|
||
|
|
"utk satu sweep (lihat P3.2.2 E_EQH retention 2.05%)",
|
||
|
|
"severity": "SEMANTIC STATE/EVENT BUG (hipotesis D)",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "R2_NO_REJECTION_EQ",
|
||
|
|
"given": "EQH pair valid; bar fc dgn high[fc] > pr2 namun close[fc] < pr2 "
|
||
|
|
"(wick sweeps, close kembali)",
|
||
|
|
"when": "machine mengevaluasi f10 di bar fc",
|
||
|
|
"expected": "Sweep memerlukan rejection/close-back utk konsisten dgn definisi SMC "
|
||
|
|
"(sweep = grab + rejection)",
|
||
|
|
"current": "f10=1 tanpa syarat close — wick break saja sudah 'swept' (definisi "
|
||
|
|
"dipilih via AUC, DESAIN_MTF_v45.md)",
|
||
|
|
"severity": "DEFINITION AMBIGUITY (hipotesis B) — wajib adjudikasi dgn human",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "R3_F7_STATE_AGE",
|
||
|
|
"given": "Grab f7 terjadi di bar b (sweep_dir=-1)",
|
||
|
|
"when": "r = b+40 (melewati InpSeqWindow=40 dan umur setup P3.2.2 8-16 bar)",
|
||
|
|
"expected": "Event sweep dianggap basi; fitur tidak lagi menyatakan 'sweep aktif' "
|
||
|
|
"tanpa konteks waktu",
|
||
|
|
"current": "f7 tetap -1 tanpa batas umur sampai ada grab baru — state basi tetap "
|
||
|
|
"dihitung sbg sinyal (f18 +15, f9-confirm tetap 1)",
|
||
|
|
"severity": "EVENT/STATE SEMANTIC ISSUE (hipotesis D) — umur setup tidak dimodelkan",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "R4_F7_WINDOWED_VS_FULLFEED",
|
||
|
|
"given": "Grab terakhir terjadi di bar b dari pivot p < r-699",
|
||
|
|
"when": "EA (window 700) vs training (full feed) mengevaluasi f7 di bar r",
|
||
|
|
"expected": "f7 runtime == f7 training (parity)",
|
||
|
|
"current": "divergence diukur (lihat f7_windowed_vs_fullfeed) — kecil secara empiris, "
|
||
|
|
"tapi secara semantik ada window mismatch",
|
||
|
|
"severity": "LOW (parity empiris hampir sempurna); dokumentasikan sbg risiko",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "R5_NO_EXCESS_THRESHOLD",
|
||
|
|
"given": "Bar dgn wick menembus level 0.001 ATR di atas reference",
|
||
|
|
"when": "f10/f11 dievaluasi",
|
||
|
|
"expected": "Sweep bermakna memerlukan excess signifikan (>= ambang) utk mencegah noise",
|
||
|
|
"current": "excess minimum = 0 — tembus wick sekecil apa pun = swept",
|
||
|
|
"severity": "DEFINITION SIMPLIFICATION (hipotesis B)",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"id": "R6_REFERENCE_TIMEFRAME_M15_ONLY",
|
||
|
|
"given": "Human melihat sweep pada M30/H4 (level likuiditas HTF)",
|
||
|
|
"when": "machine mengevaluasi sweep pd bar M15",
|
||
|
|
"expected": "Deteksi sweep pada timeframe yg sama dgn referensi likuiditas",
|
||
|
|
"current": "f7/f10/f11 hanya dihitung pada M15; HTF hanya dipakai sbg bias arah "
|
||
|
|
"(f0-f2) — referensi likuiditas HTF TIDAK ada di fitur sweep",
|
||
|
|
"severity": "TIMEFRAME SEMANTIC GAP (hipotesis C) — wajib diuji dgn golden cases",
|
||
|
|
},
|
||
|
|
]
|
||
|
|
|
||
|
|
report["verdict_draft"] = {
|
||
|
|
"event_vs_state": "KONFIRMASI BUG STATE/EVENT (D): f7 persist tanpa batas umur "
|
||
|
|
"(99.95% bar aktif); f10/f11 state monotonic ~649 bar; stream "
|
||
|
|
"f9-confirm berulang dgn run median > 1",
|
||
|
|
"rejection_semantics": "f10/f11 TANPA rejection (definisi dipilih via AUC) — "
|
||
|
|
"konflik dgn definisi SMC manusia; wajib golden-test",
|
||
|
|
"timeframe": "Hanya M15 — referensi likuiditas HTF tidak direpresentasikan",
|
||
|
|
}
|
||
|
|
|
||
|
|
SC.save_json("audit_liquidity_sweep.json", report)
|
||
|
|
|
||
|
|
print(json.dumps({k: report[k] for k in
|
||
|
|
("event_state_f7", "event_state_f10_eqh", "event_state_f11_eql",
|
||
|
|
"repeated_event_f9confirm", "f7_windowed_vs_fullfeed")},
|
||
|
|
indent=2, default=str))
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|