SniperGold_ML/ml/p3/smc_semantic/audit_choch_state.py

154 lines
6.2 KiB
Python

# -*- coding: utf-8 -*-
"""P3-S.3 AUDIT — CHOCH STATE/EVENT FORENSIC (diagnostic, mirror f7 P3-S.0).
Mengukur perilaku POPULASI state CHoCH (f8/f9/f18) pada XAUUSD M15 2017-2026
dengan semantik RUNTIME (equal-allowed pivot, build_structure_fast):
- onsets : bar di mana CHoCH event BARU tercatat (choch_bar == r)
- active bars : bar dengan choch_dir != 0 (state f8 "last CHoCH")
- repetition : active/onsets
- run stats : panjang run state (dir konstan) & age (r - choch_bar)
- f9 staleness: chochOK (chochBar >= swpBar & searah) tanpa expiry -> berapa
banyak bar dengan CHoCH age > SEQ_WINDOW masih "confirm"
- f18 staleness: kontribusi +15 (chochDir==swpDir) pada CHoCH basi
Output: output/audit_choch_state.json
Disiplin: OBSERVE -> RECORD. Bukan oracle; bukan definisi. Spec = truth.
"""
import os
import sys
import json
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
REPO_ML = os.path.normpath(os.path.join(HERE, "..", "..", "..", "ml")) # publish/AlgoForge/ml
REPO_P3 = os.path.normpath(os.path.join(REPO_ML, "p3"))
REPO_PARITY = os.path.normpath(os.path.join(REPO_ML, "parity"))
SRC_LEGACY = os.path.normpath(os.path.join(
HERE, "..", "..", "..", "..", "..", "SniperGold_ML")) # Shared Projects\SniperGold_ML
sys.path.insert(0, HERE)
sys.path.insert(0, REPO_ML)
sys.path.insert(0, REPO_P3)
sys.path.insert(0, REPO_PARITY)
sys.path.insert(0, SRC_LEGACY)
import p3_common as P3
import smc_semantic_common as SC
import build_features_p2 as BFP
# Checkout ini nested (publish\AlgoForge) -> path relatif p3_common meleset;
# data riil ada di MQL5\Files\AlgoForge\Data. Patch konstanta data root.
P3.DATA = r"D:\TradingTerminal\HFM Metatrader 5\MQL5\Files\AlgoForge\Data"
SEQ_WINDOW = SC.SEQ_WINDOW # 40
def main():
t, o, h, l, c, v, htf = SC.load_data()
n = len(c)
# ---- struktur runtime-semantics (equal allowed), full-feed, begin=100 ----
sw_at = np.zeros(n, dtype=int)
sw = BFP.build_structure_fast(o, h, l, c, SC.SWING_LEN, False, sw_at, begin=100)
inn = BFP.build_structure_fast(o, h, l, c, SC.INTERNAL_LEN, True,
sw["trend"].copy(), begin=100)
cd = inn["choch_dir"] # state f8 (last CHoCH direction)
cb = inn["choch_bar"] # bar event CHoCH terakhir
new_event = (cb == np.arange(n)) # bar di mana event BARU tercatat
onsets = int(new_event.sum())
active = int((cd != 0).sum())
rep = active / onsets if onsets else 0.0
# run stat: panjang run arah konstan (cd != 0)
runs = []
run_len = 0
prev = 0
for x in cd:
if x != 0 and x == prev:
run_len += 1
else:
if prev != 0:
runs.append(run_len)
run_len = 1 if x != 0 else 0
prev = x
if prev != 0:
runs.append(run_len)
runs = np.array(runs, dtype=int) if runs else np.zeros(0, dtype=int)
# age state (bar setelah event)
mask = cb >= 0
age = np.where(mask, np.arange(n) - cb, -1)
age_active = age[(cd != 0) & mask]
# ---- sweep state legacy (utk f9/f18); pivot list = feed internal build_structure_fast ----
is_ph, is_pl = BFP._pivots_vec(h, l, SC.INTERNAL_LEN)
piv = []
for p in range(SC.INTERNAL_LEN, n - SC.INTERNAL_LEN):
if p >= 95 and (is_ph[p] or is_pl[p]):
piv.append((p, float(h[p]) if is_ph[p] else float(l[p]), bool(is_ph[p])))
sweep_dir, sweep_bar = SC.sweep_state(h, l, c, piv)
# f9 chochOK (kode EA, tanpa expiry): chochDir!=0 & chochBar>=swpBar & dir sama
chochOK = np.zeros(n, dtype=bool)
for r in range(n):
if cd[r] != 0 and cb[r] >= sweep_bar[r] and cd[r] == sweep_dir[r]:
chochOK[r] = True
# f9 pada CHoCH basi (age > SEQ_WINDOW)
ok_basi = chochOK & (age > SEQ_WINDOW)
ok_fresh = chochOK & (age >= 0) & (age <= SEQ_WINDOW)
# f18 +15 leg: chochDir==swpDir (tanpa expiry)
f18_leg = (cd != 0) & (cd == sweep_dir)
f18_leg_basi = f18_leg & (age > SEQ_WINDOW)
# kapan state f8 pertama kali menjadi 0 setelah pernah aktif (post-first-event)
first_active = int(np.argmax(cd != 0)) if (cd != 0).any() else -1
zero_after_first = bool((cd[first_active:] == 0).any()) if first_active >= 0 else None
result = {
"scope": "XAUUSD M15 2017-2026 full-feed (runtime-semantics equal-allowed pivots)",
"n_bars": int(n),
"choch_state": {
"onsets": onsets,
"active_bars": int(active),
"repetition_ratio": round(float(rep), 3),
"zero_after_first_event": zero_after_first,
"run_median": float(np.median(runs)) if len(runs) else None,
"run_max": int(runs.max()) if len(runs) else None,
"run_n": int(len(runs)),
"age_active_median": float(np.median(age_active)) if len(age_active) else None,
"age_active_p99": float(np.percentile(age_active, 99)) if len(age_active) else None,
"age_active_max": int(age_active.max()) if len(age_active) else None,
"note": "state f8 = last CHoCH direction (persisten, per FEATURE_CONTRACT); "
"bukan duplicate emission detector."
},
"f9_chochOK": {
"true_bars": int(chochOK.sum()),
"fresh_bars_le40": int(ok_fresh.sum()),
"stale_bars_gt40": int(ok_basi.sum()),
"stale_fraction": round(float(ok_basi.sum() / max(1, int(chochOK.sum()))), 4),
"note": "f9 memakai CHoCH tanpa expiry -> event basi tetap 'confirm'"
},
"f18_leg_choch_swp": {
"true_bars": int(f18_leg.sum()),
"stale_bars_gt40": int(f18_leg_basi.sum()),
"stale_fraction": round(float(f18_leg_basi.sum() / max(1, int(f18_leg.sum()))), 4),
"note": "f18 +15 (chochDir==swpDir) tanpa expiry"
},
"seq_window": SEQ_WINDOW,
}
out_path = os.path.join(HERE, "output", "audit_choch_state.json")
os.makedirs(os.path.dirname(out_path), exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, default=str)
print(json.dumps(result, indent=2, default=str))
print("[saved]", out_path)
if __name__ == "__main__":
main()