research: porta artefatti da strategy-research-calendar (tracks F-I + eval crypto_backtest + lead OPZIONI/VRP)
Dal branch parallelo strategy-research-calendar (continuazione della linea TP01). Porta su main il record di ricerca + la fondazione del lead opzioni (NIENTE blob dati, niente codice in conflitto): - Tracks F/G/H/I (seasonality/calendar, prior-levels, volume-vol, momentum-reversal): tutti NEGATIVI/spurii -> confermano il soffitto Sharpe ~1.3 su BTC/ETH direzionale (calendar = buy&hold travestito; mean-reversion morta anche a fee 0). Diari + script. - trackD_lookahead_audit.py: audit anti-look-ahead (stesso esito del nostro fix >=12h). - eval-crypto-backtest-options.md: valutazione strategia esterna crypto_backtest. Cross-valida TP01 (il loro sleeve spot 12h ~ TP01: due ricerche indipendenti, stessa conclusione). Identifica il LEAD: sleeve income OPZIONI (vendita put settimanali delta-0.28, VRP IV>RV), scorrelato ~0.22 al trend -> via per superare il soffitto ~1.3. - options_real_quote_check.py + cerbero-bite-mainnet-verified.md: VERIFICATO su QUOTE REALI Deribit mainnet (cerbero-bite/MCP = mainnet, bit-identico a ccxt.deribit). Premio reale (BID, con skew) = 1.29x il modellato -> il backtest SOTTOSTIMA il premio; il rischio vero e' la CODA (short-vol) + liquidita' di roll in stress, non la magnitudine. NB: lo sleeve opzioni e' un LEAD, NON deployato: prezzato da modello (BS su DVOL) + 1 snapshot in regime calmo. Serve validazione real-chain multi-regime + stress crash + paper su testnet prima di aggiungerlo al portafoglio. Portafoglio attivo invariato: TP01 70% + XS01 30%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""ADVERSARIAL LOOK-AHEAD / EXECUTION-LAG AUDIT of the trend portfolio across timeframes.
|
||||
|
||||
Motivation (2026-06-19): a look-ahead bug (ffill mixed-timeframe on open-labeled bars) can
|
||||
inflate sub-daily Sharpe (e.g. 4h to ~1.6 vs a real ~1.1). This audit stress-tests OUR pipeline:
|
||||
|
||||
1. EXECUTION LAG: standard book holds the position decided at close[i] during bar i+1.
|
||||
We re-run with an EXTRA bar of delay (held during i+2) — i.e. you cannot trade exactly at
|
||||
the close; there is one bar of slippage/latency. A genuine slow-trend edge barely moves; a
|
||||
timing artifact collapses. We sweep lag = 1 (standard) and 2 (conservative).
|
||||
2. RELABEL TEST: resample with label='left' (open-labeled, our default) vs label='right'
|
||||
(close-labeled). The realized Sharpe must be (near) identical; a large gap => the labeling
|
||||
leaks information.
|
||||
|
||||
Conclusion target: identify the timeframe below which costs + lag dominate (don't deploy there).
|
||||
|
||||
Run: uv run python scripts/research/trackD_lookahead_audit.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
|
||||
from src.backtest.harness import load
|
||||
from src.strategies.trend_portfolio import simple_returns, realized_vol, tsmom_blend
|
||||
|
||||
ASSETS = ["BTC", "ETH"]
|
||||
FEE_SIDE = 0.0005
|
||||
TARGET_VOL = 0.20
|
||||
LEVERAGE = 2.0
|
||||
LONG_ONLY = True
|
||||
TFS = {"4h": ("4h", 6), "6h": ("6h", 4), "8h": ("8h", 3), "12h": ("12h", 2), "1d": ("1D", 1)}
|
||||
|
||||
|
||||
def resample(df1h: pd.DataFrame, rule: str, label: str) -> pd.DataFrame:
|
||||
g = df1h.copy()
|
||||
idx = pd.to_datetime(g["timestamp"], unit="ms", utc=True)
|
||||
idx.name = "dt"
|
||||
g.index = idx
|
||||
out = g.resample(rule, label=label, closed="left").agg(
|
||||
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"})
|
||||
out = out.dropna(subset=["open"])
|
||||
out["datetime"] = out.index
|
||||
return out.reset_index(drop=True)
|
||||
|
||||
|
||||
def target_series(c, bpd):
|
||||
bpy = bpd * 365.25
|
||||
r = simple_returns(c)
|
||||
vol = realized_vol(r, 30 * bpd, bpy)
|
||||
direction = np.clip(tsmom_blend(c, (30 * bpd, 90 * bpd, 180 * bpd)), 0, None) if LONG_ONLY \
|
||||
else tsmom_blend(c, (30 * bpd, 90 * bpd, 180 * bpd))
|
||||
scal = np.where((vol > 0) & np.isfinite(vol), TARGET_VOL / vol, 0.0)
|
||||
tgt = np.clip(direction * scal, -LEVERAGE, LEVERAGE)
|
||||
tgt[~np.isfinite(tgt)] = 0.0
|
||||
return tgt, r
|
||||
|
||||
|
||||
def sleeve_net(df, bpd, lag):
|
||||
"""net[t] uses position decided at close[t-lag] (lag>=1). lag=1 = standard, lag=2 = +1 delay."""
|
||||
c = df["close"].values.astype(float)
|
||||
tgt, r = target_series(c, bpd)
|
||||
pos = np.zeros(len(tgt))
|
||||
pos[lag:] = tgt[:-lag]
|
||||
gross = pos * r
|
||||
turn = np.abs(np.diff(pos, prepend=0.0))
|
||||
net = gross - FEE_SIDE * turn
|
||||
net[:lag] = 0.0
|
||||
return np.clip(net, -0.99, None), pd.to_datetime(df["datetime"])
|
||||
|
||||
|
||||
def portfolio_metrics(dfs, bpd, lag):
|
||||
series = {}
|
||||
for a in ASSETS:
|
||||
net, ts = sleeve_net(dfs[a], bpd, lag)
|
||||
series[a] = pd.Series(net, index=pd.to_datetime(ts.values))
|
||||
J = pd.concat(series, axis=1, join="inner").dropna()
|
||||
combo = 0.5 * J["BTC"].values + 0.5 * J["ETH"].values
|
||||
bpy = bpd * 365.25
|
||||
sh = float(np.mean(combo) / np.std(combo) * np.sqrt(bpy)) if np.std(combo) > 0 else 0.0
|
||||
eq = np.cumprod(1.0 + np.clip(combo, -0.99, None))
|
||||
dd = float(np.max((np.maximum.accumulate(eq) - eq) / np.maximum.accumulate(eq)))
|
||||
yrs = (J.index[-1] - J.index[0]).days / 365.25
|
||||
cagr = eq[-1] ** (1 / yrs) - 1
|
||||
return sh, dd, cagr
|
||||
|
||||
|
||||
def main():
|
||||
raw = {a: load(a, "1h") for a in ASSETS}
|
||||
print("=" * 96)
|
||||
print("# LOOK-AHEAD / EXECUTION-LAG AUDIT — trend portfolio (long-flat, tvol20, lev2), per timeframe")
|
||||
print("# lag1 = standard (decision held next bar). lag2 = +1 bar execution delay (conservative).")
|
||||
print("# left/right = resample label (open vs close). Big gap => labeling leak.")
|
||||
print("=" * 96)
|
||||
print(f" {'TF':<5s}{'Sh lag1(L)':>12s}{'Sh lag2(L)':>12s}{'Sh lag1(R)':>12s}"
|
||||
f"{'CAGR l1':>10s}{'CAGR l2':>10s}{'DD l1':>8s}{'lag-decay':>11s}")
|
||||
for tf, (rule, bpd) in TFS.items():
|
||||
dfsL = {a: resample(raw[a], rule, "left") for a in ASSETS}
|
||||
dfsR = {a: resample(raw[a], rule, "right") for a in ASSETS}
|
||||
sh1L, dd1, cagr1 = portfolio_metrics(dfsL, bpd, 1)
|
||||
sh2L, _, cagr2 = portfolio_metrics(dfsL, bpd, 2)
|
||||
sh1R, _, _ = portfolio_metrics(dfsR, bpd, 1)
|
||||
decay = (sh1L - sh2L) / sh1L * 100 if sh1L else 0.0
|
||||
flag = " <-- robust" if sh2L >= 0.9 * sh1L and abs(sh1L - sh1R) < 0.1 else ""
|
||||
print(f" {tf:<5s}{sh1L:>12.2f}{sh2L:>12.2f}{sh1R:>12.2f}"
|
||||
f"{cagr1*100:>+9.1f}%{cagr2*100:>+9.1f}%{dd1*100:>7.1f}%{decay:>+10.0f}%{flag}")
|
||||
print("\n Interpretation:")
|
||||
print(" - If Sh lag2 << Sh lag1 (big lag-decay), the edge needs to trade AT the close -> sub-TF")
|
||||
print(" timing artifact / cost-fragile. Robust slow-trend should barely move with +1 bar.")
|
||||
print(" - If Sh lag1(left) != Sh lag1(right), the bar LABELING leaks -> look-ahead. Should match.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user