SniperGold_ML/ml/p3/smc_semantic/sample_cases.py

233 lines
8.9 KiB
Python
Raw Permalink Normal View History

# -*- coding: utf-8 -*-
"""SAMPLE CASES — stratifikasi 50-100 golden cases utk SMC Semantic Golden Dataset (Liquidity Sweep).
Sumber keputusan stratifikasi : state fitur machine SAAT INI (F cache P2.6, f7/f10/f11)
+ state age grab + struktur swing + ATR BUKAN profit.
Prinsip blinding : tag stratifikasi ditulis ke cases_meta.json (BLINDED),
cases.csv utk manusia TIDAK memuat keputusan machine.
CATATAN EMPIRIS (diverifikasi, lihat audit_liquidity_sweep.json):
f7!=0 pd 99.95% bar (state grab persist; median run 94 bar, max 996 bar)
-> stratum "NEG (machine)" TIDAK ADA di populasi; yg ada hanyalah usia state.
Stratifikasi memakai STATE AGE utk mengekspos semantik event-vs-state.
Stratum:
FRESH_GRAB : f7 aktif & age<8 (machine YES grab baru, close-back ADA)
AGED_GRAB : f7 aktif & 16<=age<40 (machine YES state lewat umur setup 8-16 bar)
STALE_GRAB : f7 aktif & age>=40 (machine YES state basi > SeqWindow 40)
EQH_SWEPT : f10==1 (machine YES EQH, rejection TIDAK ADA)
EQL_SWEPT : f11==1 (machine YES EQL, rejection TIDAK ADA)
EQ_NEARCROSS: pair EQ valid, |dp|<=tol, belum cross (fc>r) near-sweep
EQ_MARGINAL : pair EQ crossed dgn |dp| in [0.7*tol, tol] marginal tolerance
Distribusi wajib: trend (swing_trend -1/0/+1), volatilitas (tercil ATR), tahun 2017-2026.
Constraint: min spacing 96 bar (24 jam) antar kasus; r >= 1500; r <= n-97.
Usage: python sample_cases.py [N] [seed]
"""
import os
import sys
import json
import random
import csv as _csv
import datetime as dt
from collections import Counter
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import smc_semantic_common as SC
MIN_SPACING = 96
WARMUP = 1500
TAIL_MARGIN = 97
QUOTA_FRAC = {
"FRESH_GRAB": 0.25,
"AGED_GRAB": 0.17,
"STALE_GRAB": 0.25,
"EQH_SWEPT": 0.10,
"EQL_SWEPT": 0.10,
"EQ_NEARCROSS": 0.10,
"EQ_MARGINAL": 0.03,
}
def iso(ts):
return dt.datetime.fromtimestamp(int(ts), tz=dt.timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
def main(n_cases=60, seed=42):
t, o, h, l, c, v, htf = SC.load_data()
F = SC.load_F()
A = SC.atr_series(h, l, c)
n = len(c)
print(f"bars={n} F={F.shape}")
if len(F) != n:
raise RuntimeError("F cache rows != bars")
sw_full, inn_full = SC.build_structures(o, h, l, c)
pairs_h, pairs_l = SC.eq_pairs(h, l, sw_full["pivots"])
tol_arr = SC.EQ_TOL_ATR * A
sweep_dir, sweep_bar = SC.sweep_state(h, l, c, inn_full["pivots"])
f7, f10, f11 = F[:, 7].astype(int), F[:, 10].astype(int), F[:, 11].astype(int)
age = np.where(sweep_bar >= 0, np.arange(n) - sweep_bar, -1)
# ---- flags per bar (window [r-649, r-50], |dp|<=tol, fc) ----
b1 = np.zeros(n, dtype=bool) # EQ_MARGINAL: crossed & |dp| in [0.7tol, tol]
b2 = np.zeros(n, dtype=bool) # EQ_NEARCROSS: |dp|<=tol & belum cross (fc>r)
for r in range(WARMUP, n - TAIL_MARGIN):
lo_r, hi_r = r - SC.W_LO, r - SC.W_HI
tol = tol_arr[r]
for arr in (pairs_h, pairs_l):
if len(arr) == 0:
continue
m = (arr[:, 0] >= lo_r) & (arr[:, 1] <= hi_r) & (arr[:, 3] <= tol)
if not m.any():
continue
sel = arr[m]
crossed = sel[:, 2] <= r
if crossed.any():
if (sel[crossed][:, 3] >= 0.7 * tol).any():
b1[r] = True
elif (sel[:, 3] <= tol).all():
b2[r] = True
# ---- stratum (urutan: assignment TERAKHIR menang; spesifik > umum) ----
# base: age grab (f7 aktif ~99.95% bar); lalu EQ menimpa grab; marginal > nearcross
stratum = np.full(n, "NONE", dtype=object)
stratum[(f7 != 0) & (age >= 0) & (age < 8)] = "FRESH_GRAB"
stratum[(f7 != 0) & (age >= 16) & (age < 40)] = "AGED_GRAB"
stratum[(f7 != 0) & (age >= 40)] = "STALE_GRAB"
stratum[f10 == 1] = "EQH_SWEPT"
stratum[f11 == 1] = "EQL_SWEPT"
stratum[b2] = "EQ_NEARCROSS"
stratum[b1] = "EQ_MARGINAL"
# ---- quota ----
target = {k: max(1, int(round(n_cases * v))) for k, v in QUOTA_FRAC.items()}
while sum(target.values()) > n_cases:
k = max(target, key=lambda x: target[x])
if target[k] > 1:
target[k] -= 1
else:
break
while sum(target.values()) < n_cases:
k = min(target, key=lambda x: target[x])
target[k] += 1
# ---- pool ----
valid = (np.arange(n) >= WARMUP) & (np.arange(n) <= n - TAIL_MARGIN)
pools = {}
for s in target:
pools[s] = np.where(valid & (stratum == s))[0].tolist()
print(f" pool {s:14s}: {len(pools[s]):6d}")
# ---- tag konteks (vol/regime/years) — dipakai utk quota regime & output ----
q1, q2 = np.quantile(A[WARMUP:n - TAIL_MARGIN], [1 / 3, 2 / 3])
vol_bucket = np.where(A < q1, "LOW", np.where(A < q2, "MID", "HIGH"))
# regime: swing_trend jarang 0 -> tambah proxy sideways (momentum rendah + konfluensi rendah)
sideways = (np.abs(F[:, 16]) <= 0.5) & (F[:, 18] < 30)
regime = np.where(F[:, 3] > 0, "TREND_UP",
np.where(F[:, 3] < 0, "TREND_DN",
np.where(sideways, "RANGE", "SIDEWAYS")))
years = np.array([dt.datetime.fromtimestamp(int(x), tz=dt.timezone.utc).year for x in t])
# ---- greedy stratified pick dgn min spacing + quota regime ----
rnd = random.Random(seed)
used = np.zeros(n, dtype=bool)
selected = []
regime_count = Counter()
def pick(pool):
# prioritaskan regime yg paling sedikit terwakili (masih ada kandidat)
by_regime = {}
for idx in pool:
rg = str(regime[idx])
by_regime.setdefault(rg, []).append(idx)
regs = sorted(by_regime, key=lambda rg: (regime_count[rg], rnd.random()))
for rg in regs:
cands = by_regime[rg]
rnd.shuffle(cands)
for idx in cands:
if not used[idx]:
return idx
return None
order = []
for s, k in target.items():
order += [s] * k
rnd.shuffle(order)
for s in order:
idx = pick(pools[s])
if idx is None:
print(f" [warn] stratum {s} habis, dilewati")
continue
selected.append((int(idx), s))
regime_count[str(regime[idx])] += 1
lo = max(0, idx - MIN_SPACING)
hi = min(n, idx + MIN_SPACING + 1)
used[lo:hi] = True
selected.sort(key=lambda x: x[0])
print(f"selected={len(selected)}")
# ---- output ----
rows_csv = []
meta = {"n_cases": len(selected), "seed": seed, "symbol": SC.SYMBOL,
"decision_tf": SC.DECISION_TF, "min_spacing_bars": MIN_SPACING,
"quota": target, "generated_utc": dt.datetime.now(dt.timezone.utc).isoformat(),
"note": "Stratifikasi memakai state machine (bukan profit). File ini BLINDED: "
"jangan ditampilkan ke annotator manusia.",
"cases": []}
for k, (idx, s) in enumerate(selected, start=1):
cid = f"SGML_SWEEP_{k:03d}"
rows_csv.append({
"case_id": cid,
"symbol": SC.SYMBOL,
"decision_timestamp": iso(t[idx]),
"decision_tf": SC.DECISION_TF,
"available_data_end": iso(t[idx]),
"note": "REVIEW HANYA SAMPAI decision_timestamp. Jangan lihat future.",
})
meta["cases"].append({
"case_id": cid,
"bar_idx": int(idx),
"decision_timestamp": iso(t[idx]),
"sampling_stratum": s,
"machine_state": {
"f7_sweep_dir": int(f7[idx]),
"f10_eqh": int(f10[idx]),
"f11_eql": int(f11[idx]),
"sweep_bar": int(sweep_bar[idx]),
"state_age_bars": int(idx - sweep_bar[idx]) if sweep_bar[idx] >= 0 else None,
},
"context": {
"year": int(years[idx]),
"vol_bucket": str(vol_bucket[idx]),
"atr": float(A[idx]),
"regime": str(regime[idx]),
"swing_trend": int(F[idx, 3]),
"close": float(c[idx]),
},
})
outdir = os.path.join(HERE, "output")
os.makedirs(outdir, exist_ok=True)
with open(os.path.join(outdir, "cases.csv"), "w", newline="", encoding="utf-8") as f:
w = _csv.DictWriter(f, fieldnames=list(rows_csv[0].keys()))
w.writeheader()
w.writerows(rows_csv)
SC.save_json("cases_meta.json", meta)
print("stratum:", dict(Counter(s for _, s in selected)))
print("vol :", dict(Counter(vol_bucket[i] for i, _ in selected)))
print("regime :", dict(Counter(regime[i] for i, _ in selected)))
print("tahun :", dict(sorted(Counter(int(years[i]) for i, _ in selected).items())))
if __name__ == "__main__":
n_cases = int(sys.argv[1]) if len(sys.argv) > 1 else 60
seed = int(sys.argv[2]) if len(sys.argv) > 2 else 42
main(n_cases, seed)