forked from chiki2bum2/SniperGold_ML
233 lines
8.9 KiB
Python
233 lines
8.9 KiB
Python
# -*- 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)
|