forked from chiki2bum2/SniperGold_ML
155 lines
No EOL
5.4 KiB
Python
155 lines
No EOL
5.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""P3-S.18 — SETUP-LEVEL BASELINE ML: deterministic spec tests (ML-T01..T08).
|
|
|
|
Truth : docs/P3_S16_SETUP_DATASET_CONTRACT_v1.md (observation unit,
|
|
identity/feature/label separation, temporal purged split) +
|
|
docs/P3_S16_LABEL_CONTRACT.md (primary TP-before-SL).
|
|
Observed : ml/p3/baseline/prepare_dataset.py (dataset reconstruction).
|
|
Discipline : deterministic tests, no training, no AUC/PF, no tuning.
|
|
|
|
Filename is spec_tests_* (guard-exempt) per the frozen P3-S.4/S.5 parity-
|
|
absence policy.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import numpy as np
|
|
|
|
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
|
|
|
|
SEED = 42
|
|
|
|
|
|
def load_ctx():
|
|
return PD.load_verified_population()
|
|
|
|
|
|
def ml_t01():
|
|
"""ONE Candidate Setup = ONE row; no per-bar duplication."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
ids = [r["setup_id"] for r in rows]
|
|
ok = len(ids) == len(set(ids))
|
|
return ok, {"rows": len(rows), "unique": len(set(ids))}
|
|
|
|
|
|
def ml_t02():
|
|
"""No duplicate setup_id across the whole dataset."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
dup = len(rows) - len({r["setup_id"] for r in rows})
|
|
return dup == 0, {"duplicates": dup}
|
|
|
|
|
|
def ml_t03():
|
|
"""No target leakage: label fields are not features; feature snapshot
|
|
holds only causal values (no post-entry / outcome-derived fields)."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
ok = PD.namespace_verify(rows)
|
|
return ok, {"namespace_ok": ok}
|
|
|
|
|
|
def ml_t04():
|
|
"""Preprocessing (split + feature table) is deterministic; no random
|
|
shuffle across time: chronological creation_bar ordering in each split."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
tr, va, te, ok = PD.temporal_split(rows)
|
|
chrono = all(tr[i]["creation_bar"] <= tr[i + 1]["creation_bar"]
|
|
for i in range(len(tr) - 1))
|
|
ok2 = chrono and ok
|
|
return ok2, {"chronological_train": chrono, "purge_ok": ok}
|
|
|
|
|
|
def ml_t05():
|
|
"""Temporal ordering preserved across train/val/test (sequential)."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
tr, va, te, _ = PD.temporal_split(rows)
|
|
if not (tr and va and te):
|
|
return False, {"error": "empty split"}
|
|
a = tr[-1]["creation_bar"]
|
|
b = va[0]["creation_bar"]
|
|
c = va[-1]["creation_bar"]
|
|
d = te[0]["creation_bar"]
|
|
ok = a < b and c < d
|
|
return ok, {"boundaries": (a, b, c, d)}
|
|
|
|
|
|
def ml_t06():
|
|
"""Purge gap enforced: train|val and val|test gap > HORIZON."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
tr, va, te, ok = PD.temporal_split(rows)
|
|
g1 = va[0]["creation_bar"] - tr[-1]["creation_bar"] if tr and va else None
|
|
g2 = te[0]["creation_bar"] - va[-1]["creation_bar"] if va and te else None
|
|
ok2 = ok and (g1 is None or g1 > PD.HORIZON) and \
|
|
(g2 is None or g2 > PD.HORIZON)
|
|
return bool(ok2), {"gap1": g1, "gap2": g2, "horizon": PD.HORIZON}
|
|
|
|
|
|
def ml_t07():
|
|
"""Same seed / same inputs -> identical manifest and hashes."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
tr1, va1, te1, ok1 = PD.temporal_split(rows)
|
|
tr2, va2, te2, ok2 = PD.temporal_split(rows)
|
|
m1 = PD.make_manifest(rows, tr1, va1, te1, ok1)
|
|
m2 = PD.make_manifest(rows, tr2, va2, te2, ok2)
|
|
same = (m1["feature_sha16"] == m2["feature_sha16"] and
|
|
m1["label_sha16"] == m2["label_sha16"] and
|
|
m1["counts"] == m2["counts"])
|
|
return bool(same), {"sha": m1["feature_sha16"]}
|
|
|
|
|
|
def ml_t08():
|
|
"""Provenance preserved per row (identity kept, not a feature)."""
|
|
ctx = load_ctx()
|
|
rows = PD.build_rows(ctx)
|
|
keep = all(r.get("setup_id") is not None and r.get("entry_timestamp")
|
|
is not None and r.get("symbol") == "XAUUSD" for r in rows)
|
|
no_leak = all("feature_setup_id" not in r and
|
|
"feature_entry_timestamp" not in r for r in rows)
|
|
return bool(keep and no_leak), {"provenance_ok": keep,
|
|
"feature_leak": not no_leak}
|
|
|
|
|
|
def main():
|
|
tests = [
|
|
("ML-T01", "one setup = one row", ml_t01),
|
|
("ML-T02", "no duplicate setup ID", ml_t02),
|
|
("ML-T03", "no target leakage", ml_t03),
|
|
("ML-T04", "preprocessing deterministic / train only", ml_t04),
|
|
("ML-T05", "temporal ordering preserved", ml_t05),
|
|
("ML-T06", "purge gap enforced", ml_t06),
|
|
("ML-T07", "same seed = same result", ml_t07),
|
|
("ML-T08", "provenance preserved", ml_t08),
|
|
]
|
|
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"])
|
|
print("TOTAL=%d PASS=%d FAIL=%d" % (len(results), n_pass,
|
|
len(results) - n_pass))
|
|
with open(os.path.join(OUT, "p3_s18_ml_tests.json"), "w",
|
|
encoding="utf-8") as f:
|
|
json.dump({"tests": results, "total": len(results),
|
|
"passed": n_pass}, f, indent=2)
|
|
return 0 if n_pass == len(results) else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |