Files
foxhunt/scripts/surfer/mft_horizon_sweep.py
jgrusewski f4bd1e2432 research(surfer): crypto edge-falsification probes — intraday/cascade dead, stablecoin peg-reversion validated
- crypto_mft_xsec / mft_*: intraday/MFT XS price edge FALSIFIED (fee-trap)
- crypto_cascade_reversion: liquidation-cascade reversion FALSIFIED (continuation)
- crypto_trend_sizing: TS-trend return-engine reconfirmed (Sharpe ~1.25)
- crypto_stablecoin_dislocation/harden/intraday: short-rich peg-reversion VALIDATED (bounded, uncorrelated); long-cheap = death-spiral trap

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 10:40:34 +02:00

155 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""MFT horizon sweep — does a SLOWER (lower-turnover) intraday trend edge clear costs?
Prong A (mft_quality_test) showed: at 15-min holds the net ≈ -1 tick = the cost itself (gross ≈ 0).
This sweeps the hold horizon UP (15m → full session) on clean ES OHLCV-1m. As the hold grows the
forward move (in ticks) grows ~√time while the 1-tick cost stays fixed, so cost/gross shrinks — this
isolates the question the user posed: keep costs low by trading SLOWER. If gross edge grows with
horizon (trend persistence), there's an MFT sweet spot; if gross stays ~0 everywhere, the price signal
is dead regardless of speed.
Pre-registered (NOT tuned): trend-follow, lookback = hold = H bars (5-min each); direction = sign(ret_H);
conviction = |t-stat| = |ret_H| / (per-bar vol · √H); entry close[t], exit close[t+H], intraday only
(same-day, flat-by-close); cost = 1.0 tick round-trip (crossing). Quality curve by conviction decile,
IS(first 70%) / OOS(last 30%). PASS bar: OOS top-decile net > 0 ticks & hit > 50% & |t| ≥ 2, at a
horizon with low trades/day (genuinely MFT).
"""
import glob
import math
import numpy as np
import torch
DEV = "cuda" if torch.cuda.is_available() else None
TICK = 0.25
COST_TICKS = 1.0
BAR_NS = 5 * 60 * 10**9
DAY_NS = 86_400 * 10**9
HOLDS = [3, 12, 24, 48, 78] # 15m, 1h, 2h, 4h, ~full session (5-min bars)
def load_es_5min():
import databento as db
es1m = sorted(glob.glob("data/surfer/es1m/*.dbn"))
ts_all, c_all = [], []
if es1m:
for p in es1m:
try:
arr = db.DBNStore.from_file(p).to_ndarray()
except Exception:
continue
if len(arr) == 0 or "close" not in arr.dtype.names:
continue
ts = arr["ts_event"].astype(np.int64)
c = arr["close"].astype(np.float64) / 1e9
m = c > 0
ts_all.append(ts[m]); c_all.append(c[m])
else:
for p in sorted(glob.glob("test_data/futures-baseline/ES.FUT/*.dbn.zst")):
try:
df = db.DBNStore.from_file(p).to_df().reset_index()
except Exception:
continue
if df.empty or "close" not in df.columns:
continue
df = df[df["close"] > 0]
if df.empty:
continue
dom = df["instrument_id"].value_counts().idxmax()
df = df[df["instrument_id"] == dom].sort_values("ts_event")
ts_all.append(df["ts_event"].astype("int64").to_numpy())
c_all.append(df["close"].to_numpy(np.float64))
ts = np.concatenate(ts_all); c = np.concatenate(c_all)
_u, _ui = np.unique(ts, return_index=True)
ts, c = ts[_ui], c[_ui]
o = np.argsort(ts, kind="stable"); ts, c = ts[o], c[o]
b5 = ts // BAR_NS
_, first = np.unique(b5, return_index=True)
last = np.append(first[1:] - 1, len(b5) - 1)
return ts[last], c[last]
def rolling_std(x, w):
z = torch.zeros(1, dtype=x.dtype, device=x.device)
cs = torch.cat([z, torch.cumsum(x, 0)])
cs2 = torch.cat([z, torch.cumsum(x * x, 0)])
s = cs[w:] - cs[:-w]
s2 = cs2[w:] - cs2[:-w]
mean = s / w
var = (s2 / w - mean * mean).clamp_min(0.0)
out = torch.full_like(x, float("nan"))
out[w - 1:] = var.sqrt()
return out
def run():
ts5, close5 = load_es_5min()
day = ts5 // DAY_NS
n_days = int(day.max() - day.min())
lc = torch.tensor(np.log(close5), device=DEV, dtype=torch.float64)
px = torch.tensor(close5, device=DEV, dtype=torch.float64)
dy = torch.tensor(day, device=DEV)
T = len(lc)
t = torch.arange(T, device=DEV)
split = int(0.7 * T)
def sameday(lag):
m = torch.zeros(T, dtype=torch.bool, device=DEV)
m[lag:] = dy[lag:] == dy[:-lag]
return m
r1 = torch.zeros_like(lc); r1[1:] = lc[1:] - lc[:-1]
r1 = torch.where(sameday(1), r1, torch.zeros_like(r1))
vol1 = rolling_std(r1, 12) # per-bar 5-min vol (fixed 1h window, pre-registered)
print("\n================ MFT HORIZON SWEEP (GPU, intraday trend, flat-by-close) ================")
print(f"device={DEV} 5-min bars={T} span days≈{n_days} cost={COST_TICKS} tick round-trip")
print("trend-follow: dir=sign(ret_H), conv=|ret_H|/(vol·√H). PASS: OOS top-decile net>0 & hit>50% & |t|≥2\n")
print(f"{'hold':>6} {'top-q':>6} {'n_OOS':>7} {'gross_t':>8} {'net_t':>8} {'cost/gr':>8} {'hit%':>6} {'t-stat':>7} {'trd/day':>8}")
print("-" * 78)
for H in HOLDS:
retH = torch.full_like(lc, float("nan")); retH[H:] = lc[H:] - lc[:-H]
tstat = retH / (vol1 * math.sqrt(H)).clamp_min(1e-9)
fwd_ticks = torch.full_like(px, float("nan"))
fwd_ticks[:-H] = (px[H:] - px[:-H]) / TICK
fwd_valid = torch.zeros(T, dtype=torch.bool, device=DEV)
fwd_valid[:-H] = dy[H:] == dy[:-H] # exit same day (flat-by-close)
valid = fwd_valid & sameday(H) & torch.isfinite(tstat) & torch.isfinite(vol1) & (vol1 > 0)
direction = torch.sign(retH)
net = direction * fwd_ticks - COST_TICKS
gross = direction * fwd_ticks
cand = valid & (direction != 0)
idx = torch.nonzero(cand, as_tuple=True)[0]
if len(idx) < 100:
print(f"{H:>6} (too few candidates: {len(idx)})"); continue
conv = tstat.abs()[idx]; tt = t[idx]
order = torch.argsort(conv, descending=True)
hold_hours = H * 5 / 60.0
for q in [0.05, 0.10, 1.00]:
k = max(int(q * len(idx)), 1)
sel = idx[order[:k]]
oos = sel[tt[order[:k]] >= split]
no = int(len(oos))
if no < 20:
print(f"{H:>6} {q:>6.2f} (OOS n<20)"); continue
g = gross[oos]; nt = net[oos]
gm = float(g.mean()); nm = float(nt.mean())
tval = nm / (float(nt.std()) / math.sqrt(no) + 1e-12)
hit = 100.0 * float((nt > 0).float().mean())
cost_gr = (COST_TICKS / gm * 100.0) if gm > 0 else float("nan")
trd_day = no / (0.3 * n_days) # OOS is last 30% of days
tag = f"{H}({hold_hours:.1f}h)" if q == 0.05 else ""
print(f"{tag:>6} {q:>6.2f} {no:>7} {gm:>+8.3f} {nm:>+8.3f} {cost_gr:>7.0f}% {hit:>6.1f} {tval:>+7.2f} {trd_day:>8.2f}")
print("-" * 78)
def main():
if DEV is None:
raise SystemExit("CUDA not available")
run()
if __name__ == "__main__":
main()