""" THE LOAD-BEARING TEST for the volatility meta-label pivot. Question: is 48-72h volatility EXPANSION predictable well enough that a "block unless P(expansion) >= 0.70" gate is a real filter rather than a coin flip with extra steps? This is the one thing worth knowing before any of the pipeline work. If the answer is no, the architecture is moot regardless of how clean the code is. Method, deliberately conservative: * label = unsigned 48-72h expansion vs K*ATR, Friday-clipped (vol_target.py) * features = volatility-clustering only (no direction, no price level) * walk-forward, expanding train window, PURGED by the label span so no training row's 72h path overlaps a test row * report OOS AUC, and the precision/coverage actually achieved AT the 0.70 gate -- an AUC alone does not tell you whether the gate is usable Deliberately NOT done here: any per-symbol threshold tuning, any feature search. Both would manufacture the result this test exists to check. """ from __future__ import annotations import sys import numpy as np sys.path.insert(0, __file__.rsplit("\\", 1)[0] if "\\" in __file__ else ".") from vol_target import garman_klass_var, expansion_label # noqa: E402 COMMON = r"C:\Users\admin\AppData\Roaming\MetaQuotes\Terminal\Common\Files" # ------------------------------------------------------------------ loading def load(symbol: str, period: str = "PERIOD_H4"): path = rf"{COMMON}\bars_{symbol}_{period}.csv" raw = np.genfromtxt(path, delimiter=",", skip_header=1, dtype=str, encoding="ansi") ts = np.array( [f"{r[0][:10].replace('.', '-')}T{r[0][11:]}" for r in raw], dtype="datetime64[s]", ) o, h, l, c = (raw[:, i].astype(float) for i in (1, 2, 3, 4)) v = raw[:, 5].astype(float) return ts, o, h, l, c, v def atr(h, l, c, n=14): """Wilder ATR, causal. out[i] uses bars <= i only.""" tr = np.maximum(h[1:] - l[1:], np.maximum(np.abs(h[1:] - c[:-1]), np.abs(l[1:] - c[:-1]))) tr = np.concatenate([[h[0] - l[0]], tr]) out = np.full(len(tr), np.nan) if len(tr) <= n: return out out[n - 1] = tr[:n].mean() for i in range(n, len(tr)): out[i] = (out[i - 1] * (n - 1) + tr[i]) / n return out def friday_flat(ts_scalar, flat_hour_utc=20): """Friday 20:00 UTC (~16:00 NY) liquidation instant for that week.""" d = ts_scalar.astype("datetime64[D]") dow = (d.astype(int) + 4) % 7 # 1970-01-01 was a Thursday -> 0=Mon return (d + np.timedelta64(int((4 - dow) % 7), "D")).astype("datetime64[s]") + np.timedelta64( flat_hour_utc * 3600, "s" ) # ----------------------------------------------------------------- features def build_features(ts, o, h, l, c, v, a): """Volatility-state features ONLY. No direction, no level, no future.""" gk = garman_klass_var(o, h, l, c) gk = np.where(np.isfinite(gk), gk, np.nan) def roll_mean(x, n): out = np.full(len(x), np.nan) cs = np.concatenate([[0.0], np.nancumsum(x)]) out[n - 1:] = (cs[n:] - cs[:-n]) / n return out s6, s30, s120 = (np.sqrt(np.maximum(roll_mean(gk, n), 0)) for n in (6, 30, 120)) rng = (h - l) / np.where(a > 0, a, np.nan) rng6 = roll_mean(rng, 6) volr = v / np.where(roll_mean(v, 30) > 0, roll_mean(v, 30), np.nan) hour = ts.astype("datetime64[h]").astype(int) % 24 dow = (ts.astype("datetime64[D]").astype(int) + 4) % 7 feats = { "gk_ratio_6_30": s6 / np.where(s30 > 0, s30, np.nan), # short vs medium vol "gk_ratio_30_120": s30 / np.where(s120 > 0, s120, np.nan), "gk_level_30": s30, # absolute vol regime "range_atr_6": rng6, "vol_ratio": volr, "hour_sin": np.sin(2 * np.pi * hour / 24), "hour_cos": np.cos(2 * np.pi * hour / 24), "dow": dow.astype(float), } names = list(feats) X = np.column_stack([feats[k] for k in names]) return X, names # ------------------------------------------------------- logistic regression def fit_logit(X, y, iters=300, lr=0.1, l2=1e-3): """Plain gradient descent. No sklearn on this box.""" Xb = np.column_stack([np.ones(len(X)), X]) w = np.zeros(Xb.shape[1]) for _ in range(iters): p = 1.0 / (1.0 + np.exp(-np.clip(Xb @ w, -30, 30))) g = Xb.T @ (p - y) / len(y) + l2 * np.r_[0.0, w[1:]] w -= lr * g return w def predict(w, X): Xb = np.column_stack([np.ones(len(X)), X]) return 1.0 / (1.0 + np.exp(-np.clip(Xb @ w, -30, 30))) def auc(y, s): """Rank AUC with tie handling.""" y = np.asarray(y) if len(np.unique(y)) < 2: return np.nan order = np.argsort(s) r = np.empty(len(s), float) sv = np.asarray(s)[order] ranks = np.arange(1, len(s) + 1, dtype=float) i = 0 while i < len(sv): j = i while j + 1 < len(sv) and sv[j + 1] == sv[i]: j += 1 ranks[i:j + 1] = (i + j + 2) / 2.0 i = j + 1 r[order] = ranks n1 = float((y == 1).sum()) n0 = float((y == 0).sum()) return (r[y == 1].sum() - n1 * (n1 + 1) / 2) / (n1 * n0) # ------------------------------------------------------------------- driver def run(symbol, k_atr=2.0, n_folds=6, gate=0.70): ts, o, h, l, c, v = load(symbol) a = atr(h, l, c, 14) X, names = build_features(ts, o, h, l, c, v, a) trig = np.arange(140, len(ts) - 1) y_all, meta = expansion_label(h, l, c, a, ts, trig, k_atr=k_atr, h_min_hours=48, h_max_hours=72, flat_by=friday_flat) keep = (y_all >= 0) & np.isfinite(X[trig]).all(axis=1) idx = trig[keep] y = y_all[keep].astype(float) Xk = X[idx] span = int(np.median([m["path_bars"] for m in meta if "path_bars" in m])) # standardise on train only, per fold n = len(y) fold = n // (n_folds + 1) rows = [] for f in range(1, n_folds + 1): tr_end = f * fold te_beg, te_end = tr_end + span, min(tr_end + span + fold, n) # PURGE = label span if te_end - te_beg < 50: continue Xtr, ytr = Xk[:tr_end], y[:tr_end] Xte, yte = Xk[te_beg:te_end], y[te_beg:te_end] mu, sd = Xtr.mean(0), Xtr.std(0) sd[sd == 0] = 1.0 w = fit_logit((Xtr - mu) / sd, ytr) p = predict(w, (Xte - mu) / sd) sel = p >= gate rows.append({ "fold": f, "n_test": len(yte), "base": yte.mean(), "auc": auc(yte, p), "cov": sel.mean(), "prec": yte[sel].mean() if sel.any() else np.nan, }) return rows, y, span, names if __name__ == "__main__": syms = sys.argv[1:] or ["SP500", "NAS100", "US30", "DAX40", "EURUSD", "USDJPY", "XAUUSD"] print(f"{'symbol':<8}{'n':>7}{'base':>8}{'span':>6}{'OOS AUC':>10}{'cov@.70':>9}{'prec@.70':>10}{'lift':>8}") print("-" * 66) for s in syms: try: rows, y, span, names = run(s) except Exception as e: # noqa: BLE001 print(f"{s:<8} FAILED: {e}") continue if not rows: print(f"{s:<8} no usable folds") continue aucs = np.array([r["auc"] for r in rows], float) base = np.average([r["base"] for r in rows], weights=[r["n_test"] for r in rows]) cov = np.average([r["cov"] for r in rows], weights=[r["n_test"] for r in rows]) precs = np.array([r["prec"] for r in rows], float) wts = np.array([r["cov"] * r["n_test"] for r in rows], float) prec = np.nansum(precs * wts) / wts.sum() if wts.sum() > 0 else np.nan print(f"{s:<8}{len(y):>7}{base:>8.3f}{span:>6}{np.nanmean(aucs):>10.3f}" f"{cov:>9.3f}{prec:>10.3f}{(prec - base):>+8.3f}")