SniperGold_ML/ml/p3/label_audit.py

239 lines
11 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
"""P3.1b LABEL FORENSIC AUDIT — SniperGold_ML.
Diagnostic ONLY. Answers the second research question:
"Apakah label 24-bar / 0.75 x ATR sesuai dengan tujuan SMC?"
Contents:
* base label (24-bar, 0.75 ATR): class balance, entropy, outcome rate
* label decomposition: future return, MFE, MAE, time-to-hit,
TP-before-SL, SL-before-TP, no-hit all without look-ahead
* horizon sensitivity (descriptive): 12 / 24 / 36 / 48 bars
-> does SMC information persist across horizons?
* label temporal persistence: autocorrelation, transition matrix,
P(label_t | label_{t-1})
* label x SMC state table (diagnostic link between label and setup concept)
NOT a parameter optimization; no horizon/threshold is promoted to production.
"""
import os
import sys
import json
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
if HERE not in sys.path:
sys.path.insert(0, HERE)
import p3_common as P3
import train_model as TM
HORIZONS = [12, 24, 36, 48]
BASE_H = TM.H_LABEL # 24
BASE_T = TM.LABEL_ATR # 0.75
def entropy2(p1, p0):
p = [x for x in (p1, p0) if x > 0]
if not p:
return 0.0
return float(-sum(x * np.log(x) for x in p))
def entropy3(p1, p0, pz):
p = [x for x in (p1, p0, pz) if x > 0]
return float(-sum(x * np.log(x) for x in p))
def main():
t, o, h, l, c, v, htf = P3.load_data()
F = P3.load_F()
A = P3.atr_series(h, l, c)
n = len(c)
report = {"provenance": P3.provenance(),
"dataset_hash": P3.dataset_hash(F, P3.make_label(c, A), A, t),
"base_label": {"horizon": BASE_H, "thr_mult": BASE_T}}
# =====================================================================
# 1) BASE LABEL decomposition
# =====================================================================
print("=== BASE LABEL (24-bar, 0.75 x ATR) — class balance & entropy ===")
lab = P3.make_label(c, A)
p1 = float((lab == 1).mean())
pm1 = float((lab == -1).mean())
pz = float((lab == 0).mean())
labeled = lab != 0
n_lab = int(labeled.sum())
n_pos = int((lab == 1).sum())
n_neg = int((lab == -1).sum())
e2 = entropy2(p1 / (p1 + pm1), pm1 / (p1 + pm1))
e3 = entropy3(p1, pm1, pz)
print(f" P(+1)={p1:.4f} P(-1)={pm1:.4f} P(0)={pz:.4f} "
f"| labeled={n_lab} ({n_lab/n*100:.1f}%) pos={n_pos} neg={n_neg}")
print(f" 2-class entropy (labeled only)={e2:.4f} | 3-class entropy={e3:.4f}")
report["base_class"] = {"p_pos": p1, "p_neg": pm1, "p_zero": pz,
"n_labeled": n_lab, "n_pos": n_pos, "n_neg": n_neg,
"entropy_labeled": e2, "entropy_3class": e3}
print("\n=== LABEL DECOMPOSITION (H=24, thr=0.75 ATR) — all bars with future ===")
dec = P3.label_decompose(c, h, l, A, np.arange(n - BASE_H), horizon=BASE_H,
thr_mult=BASE_T)
# summary on ALL decision bars (includes flat outcomes)
fwd_atr = dec["fwd"] / A[:n - BASE_H]
mfe = dec["mfe"]
mae = dec["mae"]
tp = dec["tp_before_sl"]
sl = dec["sl_before_tp"]
nh = dec["no_hit"]
t_hit = dec["t_hit"]
stat = lambda x: dict(mean=float(np.nanmean(x)), median=float(np.nanmedian(x)),
p25=float(np.nanpercentile(x, 25)),
p75=float(np.nanpercentile(x, 75)),
std=float(np.nanstd(x)))
all_sum = {"n": int(len(fwd_atr)),
"fwd_atr": stat(fwd_atr),
"mfe_atr": stat(mfe),
"mae_atr": stat(mae),
"P_tp_before_sl": float(tp.mean()),
"P_sl_before_tp": float(sl.mean()),
"P_no_hit": float(nh.mean()),
"time_to_hit_median": float(np.nanmedian(t_hit)) if np.isfinite(t_hit).any() else None}
print(f" n={all_sum['n']}")
print(f" fwd/ATR: mean={all_sum['fwd_atr']['mean']:.3f} med={all_sum['fwd_atr']['median']:.3f}")
print(f" MFE/ATR: mean={all_sum['mfe_atr']['mean']:.3f} med={all_sum['mfe_atr']['median']:.3f}")
print(f" MAE/ATR: mean={all_sum['mae_atr']['mean']:.3f} med={all_sum['mae_atr']['median']:.3f}")
print(f" TP-before-SL={all_sum['P_tp_before_sl']:.4f} SL-before-TP={all_sum['P_sl_before_tp']:.4f} "
f"no-hit={all_sum['P_no_hit']:.4f} | median time-to-hit={all_sum['time_to_hit_median']}")
# direction-conditioned
dir_sum = {}
for dname, mask in (("long", lab[:n - BASE_H] == 1),
("short", lab[:n - BASE_H] == -1),
("flat", lab[:n - BASE_H] == 0)):
if not mask.any():
dir_sum[dname] = None
continue
sub = dict(n=int(mask.sum()),
fwd_atr=stat(fwd_atr[mask]),
mfe_atr=stat(mfe[mask]),
mae_atr=stat(mae[mask]),
P_tp=float(tp[mask].mean()),
P_sl=float(sl[mask].mean()),
P_no_hit=float(nh[mask].mean()),
time_to_hit_median=float(np.nanmedian(t_hit[mask])) if np.isfinite(t_hit[mask]).any() else None)
dir_sum[dname] = sub
print(f" [{dname}] n={sub['n']} MFE med={sub['mfe_atr']['median']:.2f} "
f"MAE med={sub['mae_atr']['median']:.2f} "
f"TP={sub['P_tp']:.3f} SL={sub['P_sl']:.3f} nohit={sub['P_no_hit']:.3f} "
f"t_hit_med={sub['time_to_hit_median']}")
report["decomposition_H24"] = {"all": all_sum, "by_direction": dir_sum}
# =====================================================================
# 2) HORIZON SENSITIVITY (descriptive, no promotion)
# =====================================================================
print("\n=== HORIZON SENSITIVITY (thr fixed = 0.75 x ATR) ===")
horiz = {}
for H in HORIZONS:
labH = P3.make_label(c, A, horizon=H, thr_mult=BASE_T)
pH1 = float((labH == 1).mean())
pHm1 = float((labH == -1).mean())
pHz = float((labH == 0).mean())
decH = P3.label_decompose(c, h, l, A, np.arange(n - H), horizon=H,
thr_mult=BASE_T)
# label persistence: autocorrelation of 3-state label (lag 1)
s3 = labH.astype(float)
ac1 = float(np.corrcoef(s3[:-1], s3[1:])[0, 1]) if np.ptp(s3) > 0 else 0.0
ent_lab = entropy2(pH1 / (pH1 + pHm1), pHm1 / (pH1 + pHm1))
ent3 = entropy3(pH1, pHm1, pHz)
e = {"horizon": H, "p_pos": float(pH1), "p_neg": float(pHm1),
"p_zero": float(pHz), "n_labeled": int((labH != 0).sum()),
"outcome_rate": float((labH != 0).mean()),
"entropy_labeled": ent_lab, "entropy_3class": ent3,
"autocorr3state_lag1": ac1,
"fwd_atr_med": float(np.nanmedian(decH["fwd"] / A[:n - H])),
"mfe_med": float(np.nanmedian(decH["mfe"])),
"mae_med": float(np.nanmedian(decH["mae"])),
"P_tp_before_sl": float(decH["tp_before_sl"].mean()),
"P_sl_before_tp": float(decH["sl_before_tp"].mean()),
"P_no_hit": float(decH["no_hit"].mean()),
"t_hit_median": float(np.nanmedian(decH["t_hit"])) if np.isfinite(decH["t_hit"]).any() else None}
horiz[str(H)] = e
print(f" H={H:>2}: P+={pH1:.4f} P-={pHm1:.4f} P0={pHz:.4f} "
f"| ent3={ent3:.4f} ac1={ac1:.4f} | MFE med={e['mfe_med']:.2f} "
f"MAE med={e['mae_med']:.2f} | TP={e['P_tp_before_sl']:.3f} "
f"SL={e['P_sl_before_tp']:.3f} nohit={e['P_no_hit']:.3f} "
f"t_hit_med={e['t_hit_median']}")
report["horizon_sensitivity"] = horiz
# =====================================================================
# 3) LABEL TEMPORAL PERSISTENCE (full 3-state series)
# =====================================================================
print("\n=== LABEL TEMPORAL PERSISTENCE (3-state, lag-1 transition) ===")
lab = P3.make_label(c, A) # base 24
s3 = lab.astype(int)
tr = {}
for a in (-1, 0, 1):
row = {}
for b in (-1, 0, 1):
row[str(b)] = int(((s3[:-1] == a) & (s3[1:] == b)).sum())
tr[str(a)] = row
# conditional probabilities from transition counts
trp = {}
for a in (-1, 0, 1):
tot = sum(tr[str(a)].values())
trp[str(a)] = {k: (v / tot if tot else 0.0) for k, v in tr[str(a)].items()}
# labeled-only persistence (ignore flats)
lm = lab != 0
li = np.where(lm)[0]
same_dir = float((lab[li[1:]] == lab[li[:-1]]).mean()) if len(li) > 1 else 0.0
p1_given1 = float((lab[li[1:]][lab[li[:-1]] == 1] == 1).mean()) if (lab[li[:-1]] == 1).any() else float("nan")
p_neg_given_neg = float((lab[li[1:]][lab[li[:-1]] == -1] == -1).mean()) if (lab[li[:-1]] == -1).any() else float("nan")
print(f" transition P(state_t | state_{'{t-1}'}):")
for a in (-1, 0, 1):
print(f" from {a:+d}: " + " ".join(f"->{b:+d}:{trp[str(a)][str(b)]:.3f}" for b in (-1, 0, 1)))
print(f" labeled-only same-direction persistence={same_dir:.4f} "
f"(P(+1|+1)={p1_given1:.4f} P(-1|-1)={p_neg_given_neg:.4f})")
report["label_persistence"] = {"transition_counts": tr,
"transition_prob": trp,
"labeled_same_dir": same_dir,
"P_pos_given_pos": p1_given1,
"P_neg_given_neg": p_neg_given_neg}
# =====================================================================
# 4) LABEL x SMC STATE (diagnostic link label <-> setup concept)
# =====================================================================
print("\n=== LABEL x SMC STATE (H=24, thr=0.75) ===")
lab = P3.make_label(c, A)
y = lab
p1_uncond = float((y == 1).mean())
rows = []
# f5 chart bias direction
for nm, col, conds in (
("f5_chart_bias", 5, {"bull": (F[:, 5] > 0), "bear": (F[:, 5] < 0)}),
("f7_sweep", 7, {"bull_grab": (F[:, 7] > 0), "bear_grab": (F[:, 7] < 0)}),
("f8_choch", 8, {"bull_choch": (F[:, 8] > 0), "bear_choch": (F[:, 8] < 0)}),
("f9_confirm", 9, {"confirm_on": (F[:, 9] == 1)}),
("f18_conf", 18, {"conf>=60": (F[:, 18] >= 60),
"conf>=70": (F[:, 18] >= 70),
"conf>=80": (F[:, 18] >= 80)}),
("f10_eqh", 10, {"eqh_on": (F[:, 10] == 1)}),
("f11_eql", 11, {"eql_on": (F[:, 11] == 1)}),
):
for cname, mask in conds.items():
if not mask.any():
continue
r = dict(feature=nm, condition=cname, n=int(mask.sum()),
p_pos=float((y[mask] == 1).mean()),
p_neg=float((y[mask] == -1).mean()),
lift_pos=float((y[mask] == 1).mean() / max(p1_uncond, 1e-12)))
rows.append(r)
print(f" {nm:11s} {cname:12s} n={r['n']:>6d} P(+1)={r['p_pos']:.4f} "
f"P(-1)={r['p_neg']:.4f} lift={r['lift_pos']:.3f}")
report["label_x_smc_state"] = {"unconditional_p_pos": float(p1_uncond), "rows": rows}
P3.save_json("label_audit.json", report)
print("\nLabel audit selesai. Output: ml/p3/output/label_audit.json")
if __name__ == "__main__":
main()