# -*- 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)