- 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>
155 lines
6.3 KiB
Python
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()
|