research: fade TF sweep 1m/2m/5m/10m/30m — 15m confermato, 10m in watchlist, 1m/2m chiusi
Frontiera Sharpe monotona al scendere del tf ma margine fee si assottiglia: MR02_BTC muore a fee2x a 5m (-1.70); MR02 sotto i 15m e' fee-death nel regime corrente (1m -64%). 1m: flat share ETH 25.6% + niente storia full -> chiuso. Corr col 15m live: 5m 0.46 / 10m 0.53. Fix resample unit-safe (pandas 2.x ms). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
|||||||
|
# 2026-06-12 — FADE TF SWEEP: 1m / 2m / 5m / 10m / 30m (post-swap 15m)
|
||||||
|
|
||||||
|
Richiesta utente: estendere l'analisi timeframe dei fade oltre il 15m appena
|
||||||
|
deployato (v1.1.30). Script: `scripts/analysis/fade_tf_sweep.py`.
|
||||||
|
Dati: parquet locale (5m/15m/30m full-history; 10m = resample dal 5m, unit-safe);
|
||||||
|
1m/2m da Cerbero (120 giorni recenti — la storia 1m locale non esiste: esclusa
|
||||||
|
dal refresh notturno per costo, 2m/10m non sono intervalli nativi del v2).
|
||||||
|
|
||||||
|
## A. Storia completa (engine canonico, OOS da 2024-10, fee 0.10% RT)
|
||||||
|
|
||||||
|
OOS Sharpe per timeframe (e OOS Sharpe a fee 2x del peggiore):
|
||||||
|
|
||||||
|
| tf | MR01_BTC | MR02_BTC | MR07_BTC | MR01_ETH | MR02_ETH | MR07_ETH | worst f2x |
|
||||||
|
|----|---------|---------|---------|---------|---------|---------|-----------|
|
||||||
|
| 5m | 3.66 | 1.90 | 4.12 | 5.31 | 6.54 | 5.52 | **MR02_BTC −1.70** |
|
||||||
|
| 10m | 2.62 | 2.69 | 3.31 | 5.32 | 6.49 | 5.59 | MR02_BTC 0.32 |
|
||||||
|
| 15m (live) | 1.94 | 2.30 | 2.37 | 4.94 | 6.40 | 4.44 | MR02_BTC 0.60 |
|
||||||
|
| 30m | 1.35 | 2.32 | 1.56 | 3.25 | 5.23 | 2.81 | MR02_BTC 1.40 |
|
||||||
|
|
||||||
|
**La frontiera è monotona**: più il tf scende, più Sharpe sale (MR01/MR07)… e più
|
||||||
|
il margine fee si assottiglia. A fee 2x MR02_BTC muore a 5m e resta fragile a 10m.
|
||||||
|
MR02 (donchian) fa 3-6x i trade degli altri: è la strategia più esposta al churn.
|
||||||
|
|
||||||
|
## B. Finestra comune recente (2026-02-12 → 06-12, il regime CORRENTE)
|
||||||
|
|
||||||
|
- **MR02 sotto i 15m è un disastro**: 1m −64%, 2m −44%, 5m −22% (fee-death).
|
||||||
|
- **MR01 a 1m brilla** (ETH +60.6%, Sh 5.7; BTC +33.5%) ma **muore a fee 2x**
|
||||||
|
(unico sopravvissuto MR01_ETH +16.5%): margine troppo sottile per fidarsi.
|
||||||
|
- Flat share a 1m: ETH 25.6%, BTC 13.3% → rischio stale-print alto (la lezione
|
||||||
|
del giorno: pairs-alt/XEX/PAXG).
|
||||||
|
- Il regime recente è CALMO: anche il 5m vi è fiacco (+4.8/−22.9/+3.7 BTC).
|
||||||
|
I tf veloci pagano nella volatilità, non nella calma — il loro vantaggio
|
||||||
|
full-history viene dai regimi mossi (2021-22, 2024).
|
||||||
|
|
||||||
|
## C. Correlazione col 15m live (daily, storia completa)
|
||||||
|
|
||||||
|
5m↔15m media **0.46**, 10m↔15m media **0.53** (range 0.28-0.81). Diversificazione
|
||||||
|
parziale: un eventuale ADD del 10m avrebbe senso ma è meno pulito del salto
|
||||||
|
1h→15m (che era a 0.26).
|
||||||
|
|
||||||
|
## Verdetto
|
||||||
|
|
||||||
|
- **1m / 2m: CHIUSI.** Fee-margin nullo a stress, microstruttura flat pesante,
|
||||||
|
validazione full-history impraticabile. Non deployare mai MR02 sotto i 15m.
|
||||||
|
- **5m: no-swap.** L'edge c'è ma MR02_BTC muore a fee 2x — viola il criterio di
|
||||||
|
robustezza fee che tutte le strategie deployate rispettano.
|
||||||
|
- **10m: in WATCHLIST.** Quasi l'edge del 5m con più margine (f2x 0.32 resta
|
||||||
|
sotto la soglia di comfort per MR02_BTC; MR01/MR07 reggono bene). Possibile
|
||||||
|
ADD selettivo (solo MR01/MR07?) da gateare su PORT06 più avanti — NON ora:
|
||||||
|
il 15m è live da poche ore, un cambio alla volta e si lascia parlare il
|
||||||
|
ledger reale.
|
||||||
|
- **15m: confermato** come ginocchio della frontiera margine-fee/rendimento.
|
||||||
|
|
||||||
|
Collaterale tecnico: bug di resample scoperto e fixato nello sweep — pandas 2.x
|
||||||
|
conserva `datetime64[ms]` da `to_datetime(unit="ms")`, quindi `.view(int64)//10**6`
|
||||||
|
divide due volte e manda i timestamp nel 1970 (equity piatta silenziosa). Usare
|
||||||
|
`(index - EPOCH) // pd.Timedelta(milliseconds=1)`.
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
"""FADE TF SWEEP — i fade MR01/02/07 su 1m/2m/5m/10m/30m (oltre a 15m live e 1h).
|
||||||
|
|
||||||
|
Ricerca 2026-06-12 (post-swap 15m). Diario: docs/diary/2026-06-12-fade-tf-sweep.md.
|
||||||
|
|
||||||
|
Due banchi di prova:
|
||||||
|
A. STORIA COMPLETA (parquet locale; 10m = resample dal 5m): engine canonico
|
||||||
|
build_trades, daily equity su IDX comune, OOS da 2024-10, fee 0.10% e 2x.
|
||||||
|
B. FINESTRA COMUNE RECENTE (2026-02-12 -> 06-12): 1m/2m (fetch Cerbero, non
|
||||||
|
esiste storia locale 1m: il refresh la esclude per costo) vs 5m/15m sugli
|
||||||
|
STESSI 120 giorni — confronto apples-to-apples sul regime corrente.
|
||||||
|
|
||||||
|
Esiti:
|
||||||
|
- La frontiera Sharpe e' MONOTONA al scendere del tf per MR01/MR07 (full
|
||||||
|
history OOS: 5m > 10m > 15m > 30m > 1h)... ma il margine fee si assottiglia
|
||||||
|
insieme: a fee 2x MR02_BTC muore a 5m (-1.70) e resta fragile a 10m (0.32).
|
||||||
|
- MR02 (donchian, 3-6x i trade degli altri) sotto i 15m muore di fee nel
|
||||||
|
regime corrente: 1m -64%, 2m -44%, 5m -22% sulla finestra recente.
|
||||||
|
- 1m/2m: SCARTATI. MR01 a 1m brilla sulla finestra recente (ETH +60%, Sh 5.7)
|
||||||
|
ma muore a fee 2x, il flat-share 1m e' alto (ETH 25.6%, BTC 13.3% -> rischio
|
||||||
|
stale-print) e la validazione full-history e' impraticabile (storia 1m non
|
||||||
|
mantenuta). Il regime recente e' CALMO: anche il 5m vi e' fiacco — i tf
|
||||||
|
veloci pagano nella volatilita', non nella calma.
|
||||||
|
- 10m: il miglior candidato OLTRE il 15m (quasi l'edge del 5m con piu' margine
|
||||||
|
fee; corr daily col 15m live 0.53 media). Eventuale ADD da gateare in
|
||||||
|
futuro, NON ora: il 15m e' appena andato live (v1.1.30), un cambio alla volta.
|
||||||
|
- VERDETTO: tenere il 15m (ginocchio della frontiera margine-fee/rendimento);
|
||||||
|
10m in watchlist; 1m/2m chiusi; 5m no-swap (fee-fragile su MR02_BTC).
|
||||||
|
|
||||||
|
uv run python scripts/analysis/fade_tf_sweep.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
from scripts.analysis.risk_management import strats_for, build_trades, INIT, POS
|
||||||
|
from scripts.analysis.combine_portfolio import IDX, SPLIT, _norm, metrics
|
||||||
|
|
||||||
|
EPOCH = pd.Timestamp(0, tz="UTC")
|
||||||
|
WINDOW_START = "2026-02-12" # finestra comune del banco B
|
||||||
|
RECENT_1M = {a: Path(f"/tmp/{a.lower()}_1m_recent.parquet") for a in ("BTC", "ETH")}
|
||||||
|
|
||||||
|
|
||||||
|
def resample_ohlcv(df: pd.DataFrame, minutes: int) -> pd.DataFrame:
|
||||||
|
"""Resample OHLCV unit-safe (pandas 2.x conserva datetime64[ms]: niente
|
||||||
|
aritmetica diretta su .view int64 — il //10**6 doppio manda i ts nel 1970)."""
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
g = df.set_index(ts).resample(f"{minutes}min")
|
||||||
|
out = pd.DataFrame({"open": g["open"].first(), "high": g["high"].max(),
|
||||||
|
"low": g["low"].min(), "close": g["close"].last(),
|
||||||
|
"volume": g["volume"].sum()}).dropna()
|
||||||
|
out["timestamp"] = (out.index - EPOCH) // pd.Timedelta(milliseconds=1)
|
||||||
|
return out.reset_index(drop=True)
|
||||||
|
|
||||||
|
|
||||||
|
def daily_eq(df: pd.DataFrame, fn, params, fee_rt: float = 0.001) -> tuple[pd.Series, int]:
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
trades = build_trades(fn(df, **params), df, fee_rt=fee_rt, trend_max=3.0)
|
||||||
|
n = len(df)
|
||||||
|
eq = np.full(n, INIT)
|
||||||
|
cap = INIT
|
||||||
|
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
||||||
|
cap = max(cap + cap * POS * ret, 10.0)
|
||||||
|
eq[j:] = cap
|
||||||
|
s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
|
||||||
|
return _norm(s), len(trades)
|
||||||
|
|
||||||
|
|
||||||
|
def full_history() -> None:
|
||||||
|
print("=== A. STORIA COMPLETA (OOS da 2024-10, fee 0.10% RT; f2x = OOS Sharpe a fee 2x) ===")
|
||||||
|
print(f"{'tf':<5} {'sleeve':<10} {'FULL%':>10} {'DD%':>6} {'Sh':>6} | {'OOS%':>8} {'oDD%':>6} {'oSh':>6} | {'f2x_oSh':>7} {'n':>6}")
|
||||||
|
for tf in ("5m", "10m", "15m", "30m"):
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
df = resample_ohlcv(load_data(asset, "5m"), 10) if tf == "10m" else load_data(asset, tf)
|
||||||
|
for nm, (fn, params) in strats_for(asset).items():
|
||||||
|
eq, n = daily_eq(df, fn, params)
|
||||||
|
r = eq.pct_change().fillna(0.0)
|
||||||
|
f, o = metrics(r), metrics(r, lo=SPLIT)
|
||||||
|
eq2, _ = daily_eq(df, fn, params, fee_rt=0.002)
|
||||||
|
o2 = metrics(eq2.pct_change().fillna(0.0), lo=SPLIT)
|
||||||
|
print(f"{tf:<5} {nm + '_' + asset:<10} {f['ret']:>10.0f} {f['dd']:>6.1f} {f['sharpe']:>6.2f}"
|
||||||
|
f" | {o['ret']:>8.0f} {o['dd']:>6.1f} {o['sharpe']:>6.2f} | {o2['sharpe']:>7.2f} {n:>6}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def trade_stats(df: pd.DataFrame, fn, params, fee_rt: float = 0.001) -> dict:
|
||||||
|
trades = build_trades(fn(df, **params), df, fee_rt=fee_rt, trend_max=3.0)
|
||||||
|
cap = peak = INIT
|
||||||
|
dd = 0.0
|
||||||
|
rets = []
|
||||||
|
wins = 0
|
||||||
|
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
||||||
|
cap = max(cap + cap * POS * ret, 10.0)
|
||||||
|
peak = max(peak, cap)
|
||||||
|
dd = max(dd, (peak - cap) / peak)
|
||||||
|
rets.append(ret * POS)
|
||||||
|
wins += ret > 0
|
||||||
|
n = len(trades)
|
||||||
|
sh = float(np.mean(rets) / np.std(rets) * np.sqrt(n)) if n > 1 and np.std(rets) > 0 else 0.0
|
||||||
|
return dict(ret=(cap / INIT - 1) * 100, dd=dd * 100, n=n,
|
||||||
|
wr=wins / n * 100 if n else 0.0, sh=sh)
|
||||||
|
|
||||||
|
|
||||||
|
def recent_window() -> None:
|
||||||
|
if not all(p.exists() for p in RECENT_1M.values()):
|
||||||
|
print("\n=== B. saltato: manca il parquet 1m recente (fetch Cerbero, vedi diario) ===")
|
||||||
|
return
|
||||||
|
start_ms = int(pd.Timestamp(WINDOW_START, tz="UTC").timestamp() * 1000)
|
||||||
|
data: dict[tuple[str, str], pd.DataFrame] = {}
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
m1 = pd.read_parquet(RECENT_1M[asset]).sort_values("timestamp").reset_index(drop=True)
|
||||||
|
data[(asset, "1m")] = m1
|
||||||
|
data[(asset, "2m")] = resample_ohlcv(m1, 2)
|
||||||
|
for tf in ("5m", "15m"):
|
||||||
|
df = load_data(asset, tf)
|
||||||
|
data[(asset, tf)] = df[df["timestamp"] >= start_ms].reset_index(drop=True)
|
||||||
|
|
||||||
|
print(f"\n=== B. FINESTRA COMUNE {WINDOW_START} -> oggi (regime corrente) ===")
|
||||||
|
print("flat share (O=H=L=C):")
|
||||||
|
for (asset, tf), df in sorted(data.items()):
|
||||||
|
fl = ((df["open"] == df["high"]) & (df["high"] == df["low"]) & (df["low"] == df["close"])).mean() * 100
|
||||||
|
print(f" {asset} {tf:>3}: {fl:5.1f}%")
|
||||||
|
print(f"\n{'tf':<4} {'sleeve':<10} {'ret%':>8} {'DD%':>6} {'n':>5} {'WR%':>5} {'Sh_tr':>6} | {'fee2x_ret%':>10}")
|
||||||
|
for tf in ("1m", "2m", "5m", "15m"):
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
for nm, (fn, params) in strats_for(asset).items():
|
||||||
|
r1 = trade_stats(data[(asset, tf)], fn, params)
|
||||||
|
r2 = trade_stats(data[(asset, tf)], fn, params, fee_rt=0.002)
|
||||||
|
print(f"{tf:<4} {nm + '_' + asset:<10} {r1['ret']:>8.1f} {r1['dd']:>6.1f} {r1['n']:>5}"
|
||||||
|
f" {r1['wr']:>5.1f} {r1['sh']:>6.2f} | {r2['ret']:>10.1f}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def correlations() -> None:
|
||||||
|
print("=== C. corr daily (storia completa) vs twin 15m LIVE ===")
|
||||||
|
c5s, c10s = [], []
|
||||||
|
for asset in ("BTC", "ETH"):
|
||||||
|
d15, d5 = load_data(asset, "15m"), load_data(asset, "5m")
|
||||||
|
d10 = resample_ohlcv(d5, 10)
|
||||||
|
for nm, (fn, params) in strats_for(asset).items():
|
||||||
|
e15 = daily_eq(d15, fn, params)[0].pct_change()
|
||||||
|
c5 = daily_eq(d5, fn, params)[0].pct_change().corr(e15)
|
||||||
|
c10 = daily_eq(d10, fn, params)[0].pct_change().corr(e15)
|
||||||
|
c5s.append(c5)
|
||||||
|
c10s.append(c10)
|
||||||
|
print(f" {nm}_{asset:<4} 5m-15m {c5:.2f} 10m-15m {c10:.2f}")
|
||||||
|
print(f" media: 5m-15m {np.mean(c5s):.2f} | 10m-15m {np.mean(c10s):.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
full_history()
|
||||||
|
recent_window()
|
||||||
|
correlations()
|
||||||
Reference in New Issue
Block a user