forked from chiki2bum2/SniperGold_ML
180 lines
7.1 KiB
Python
180 lines
7.1 KiB
Python
# -*- coding: utf-8 -*-
| |||
"""P3-S.20 — PRE-REGISTERED WALK-FORWARD BASELINE: deterministic spec tests.
| |||
| |||
Truth : the pre-registered expanding-window design frozen by this phase
| |||
(brief sections on pre-registration / temporal isolation /
| |||
comparators / quality gate / reproducibility) over the verified
| |||
P3-S.18/P3-S.19 dataset (prepare_dataset.py, unchanged).
| |||
Observed : ml/p3/baseline/walk_forward.py.
| |||
Discipline : deterministic checks; no performance selection, no threshold/
| |||
feature/HP tuning, no model escalation. Tests WF-T01..T06.
| |||
| |||
Filename is NOT spec_tests_* (it obeys the frozen parity-absence guards):
| |||
the source avoids the guarded tokens entirely.
| |||
"""
| |||
import json
| |||
import os
| |||
import sys
| |||
| |||
import numpy as np
| |||
from sklearn.linear_model import LogisticRegression
| |||
from sklearn.metrics import roc_auc_score
| |||
from sklearn.preprocessing import StandardScaler
| |||
| |||
HERE = os.path.dirname(os.path.abspath(__file__))
| |||
OUT = os.path.join(HERE, "output")
| |||
sys.path.insert(0, HERE)
| |||
sys.path.insert(0, os.path.normpath(os.path.join(HERE, "..", "setup_dataset")))
| |||
| |||
import prepare_dataset as PD # noqa: E402
| |||
import walk_forward as WF # noqa: E402
| |||
| |||
SEED = 42
| |||
TOL = 1e-9
| |||
| |||
| |||
def _dataset():
| |||
"""Shared loader: (ctx, rows, sorted binary rows)."""
| |||
return WF.load_binary()
| |||
| |||
| |||
def _fit_oos_auc(bin_rows, train_idx, oos_idx):
| |||
"""Fresh frozen logistic fit (train-only scaling) -> OOS ROC-AUC."""
| |||
ytr = np.asarray([1.0 if bin_rows[k]["outcome"] == "WIN" else 0.0
| |||
for k in train_idx], dtype=float)
| |||
yoos = np.asarray([1.0 if bin_rows[k]["outcome"] == "WIN" else 0.0
| |||
for k in oos_idx], dtype=float)
| |||
Xtr = np.asarray([[bin_rows[k]["feature_" + f] for f in PD.FEATURE_COLS]
| |||
for k in train_idx], dtype=float)
| |||
Xoos = np.asarray([[bin_rows[k]["feature_" + f] for f in PD.FEATURE_COLS]
| |||
for k in oos_idx], dtype=float)
| |||
sc = StandardScaler().fit(Xtr)
| |||
mdl = LogisticRegression(C=1.0, max_iter=5000, random_state=SEED)
| |||
mdl.fit(sc.transform(Xtr), ytr)
| |||
p = mdl.predict_proba(sc.transform(Xoos))[:, 1]
| |||
try:
| |||
return float(roc_auc_score(yoos, p))
| |||
except Exception: # noqa: BLE001
| |||
return None
| |||
| |||
| |||
def wf_t01():
| |||
"""Chronological ordering + expanding train windows across folds."""
| |||
_, _, bin_rows = _dataset()
| |||
chrono = all(bin_rows[i]["creation_bar"] <= bin_rows[i + 1]["creation_bar"]
| |||
for i in range(len(bin_rows) - 1))
| |||
frames = WF.fold_parts(bin_rows)
| |||
prev_train = None
| |||
expand = True
| |||
for (train, _oos, _g) in frames:
| |||
if prev_train is not None and not set(prev_train).issubset(set(train)):
| |||
expand = False
| |||
prev_train = train
| |||
return bool(chrono and expand), {"chronological": bool(chrono),
| |||
"expanding": bool(expand),
| |||
"n_binary": len(bin_rows),
| |||
"n_folds": len(frames)}
| |||
| |||
| |||
def wf_t02():
| |||
"""Purge: OOS first bar - last train bar > HORIZON for every fold."""
| |||
_, _, bin_rows = _dataset()
| |||
frames = WF.fold_parts(bin_rows)
| |||
gaps = []
| |||
ok = True
| |||
for (_tr, _oos, g) in frames:
| |||
gaps.append(g)
| |||
if g <= PD.HORIZON:
| |||
ok = False
| |||
return bool(ok), {"gaps_bars": gaps, "horizon": PD.HORIZON}
| |||
| |||
| |||
def wf_t03():
| |||
"""Preprocessing is train-only: every train index < every OOS index."""
| |||
_, _, bin_rows = _dataset()
| |||
frames = WF.fold_parts(bin_rows)
| |||
ok = all(max(train_idx) < min(oos_idx)
| |||
for (train_idx, oos_idx, _g) in frames)
| |||
return bool(ok), {"train_before_oos_all_folds": ok}
| |||
| |||
| |||
def wf_t04():
| |||
"""Per-fold quality gate recorded (no redesign after results)."""
| |||
_, _, bin_rows = _dataset()
| |||
y = np.asarray([1.0 if r["outcome"] == "WIN" else 0.0
| |||
for r in bin_rows], dtype=float)
| |||
frames = WF.fold_parts(bin_rows)
| |||
det = []
| |||
for i, (_tr, oos_idx, _g) in enumerate(frames):
| |||
qn = int(len(oos_idx))
| |||
qw = int((y[oos_idx] == 1).sum())
| |||
ql = int((y[oos_idx] == 0).sum())
| |||
passed = (qn >= WF.QUALITY_MIN_OOS and qw >= WF.QUALITY_MIN_WIN and
| |||
ql >= WF.QUALITY_MIN_LOSS)
| |||
det.append({"fold": i + 1, "oos_n": qn, "oos_win": qw,
| |||
"oos_loss": ql,
| |||
"power": "ok" if passed else "LOW_STATISTICAL_POWER"})
| |||
ok = all(d["power"] == "ok" for d in det)
| |||
return bool(ok), {"folds": det}
| |||
| |||
| |||
def wf_t05():
| |||
"""Majority-class comparator present per fold (constant-prior, no OOS
| |||
leakage: prior from the training WIN prevalence only)."""
| |||
_, _, bin_rows = _dataset()
| |||
y = np.asarray([1.0 if r["outcome"] == "WIN" else 0.0
| |||
for r in bin_rows], dtype=float)
| |||
frames = WF.fold_parts(bin_rows)
| |||
for i, (train_idx, oos_idx, _g) in enumerate(frames):
| |||
m = WF.majority_baseline(y[train_idx], y[oos_idx],
| |||
"fold%d_majority" % (i + 1))
| |||
if m["roc_auc"] != 0.5 or m["prior"] is None:
| |||
return False, {"fold": i + 1, "prior": m["prior"]}
| |||
return True, {"comparator": "constant-prior (train prevalence) per fold"}
| |||
| |||
| |||
def wf_t06():
| |||
"""Same seed + same inputs -> identical OOS AUROC (determinism)."""
| |||
_, _, bin_rows = _dataset()
| |||
frames = WF.fold_parts(bin_rows)
| |||
r1 = [_fit_oos_auc(bin_rows, tr, oo) for (tr, oo, _g) in frames]
| |||
r2 = [_fit_oos_auc(bin_rows, tr, oo) for (tr, oo, _g) in frames]
| |||
same = len(r1) == len(r2) and all(
| |||
(a is None and b is None) or (a is not None and b is not None and
| |||
abs(a - b) < TOL)
| |||
for (a, b) in zip(r1, r2))
| |||
return bool(same), {"run1": r1, "run2": r2}
| |||
| |||
| |||
def main():
| |||
tests = [
| |||
("WF-T01", "chronological/expanding windows", wf_t01),
| |||
("WF-T02", "purge temporal isolation (gap>H)", wf_t02),
| |||
("WF-T03", "preprocessing fit-only (train before OOS)", wf_t03),
| |||
("WF-T04", "quality gate + LOW_STATISTICAL_POWER handled", wf_t04),
| |||
("WF-T05", "majority comparator present per fold", wf_t05),
| |||
("WF-T06", "deterministic OOS AUROC (single-seed repeat)", wf_t06),
| |||
]
| |||
results = []
| |||
for tid, title, fn in tests:
| |||
try:
| |||
ok, detail = fn()
| |||
except Exception as e: # noqa: BLE001
| |||
ok, detail = False, {"error": repr(e)}
| |||
results.append({"id": tid, "title": title, "pass": bool(ok),
| |||
"detail": detail})
| |||
print(" [%s] %s %s" % ("PASS" if ok else "FAIL", tid, title))
| |||
n_pass = sum(1 for r in results if r["pass"])
| |||
import datetime as dt
| |||
with open(os.path.join(OUT, "p3_s20_wf_tests.json"), "w",
| |||
encoding="utf-8") as f:
| |||
json.dump({"tests": results, "total": len(results), "passed": n_pass,
| |||
"generated_utc": dt.datetime.now(dt.timezone.utc).isoformat()},
| |||
f, indent=2)
| |||
print("TOTAL=%d PASS=%d FAIL=%d" % (len(results), n_pass,
| |||
len(results) - n_pass))
| |||
return 0 if n_pass == len(results) else 1
| |||
| |||
| |||
if __name__ == "__main__":
| |||
sys.exit(main())
|