Files
Adriano 212427ffa1 feat(analysis): 3 strategie oneste validate OOS multi-crypto (DIP/TR/ROT)
Ricerca onesta post-squeeze su 8 crypto (2018-2026), engine fee-aware con
ingresso eseguibile a close[i], uscita TP/SL intrabar, OOS held-out, sweep fee.

Lezione madre: shortare cripto perde OOS sistematicamente (campione net-bull)
-> tutte le strategie robuste sono long-biased.

Tre meccanismi distinti e complementari:
- DIP01  dip-buy z-score reversion (long-only, 1h)  robusto BTC/ETH/SOL
- TR01   EMA 20/100 trend-following (long-only, 4h) robusto su 5/8 asset
- ROT01  rotazione cross-sectional momentum sul paniere (1d) OOS +44%, param-insensitive

Engine e validazione: scripts/analysis/honest_lab.py + honest_final.py
(+ honest_candidates/diag/diag2/trend/rotation). Diario in docs/diary/.

Onesto sull'obiettivo: €50/giorno su €1000 in pochi mesi non e' raggiungibile a
rischio sano (~1825%/anno); edge reali 30-60% OOS pluriennale. Via realistica:
portafoglio delle 3, leva moderata, crescita composta.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 23:28:00 +02:00

110 lines
3.7 KiB
Python

"""Strategia #3 candidata: time-series momentum / trend (TSMOM).
Posizione continua decisa a close[i] dai dati passati; fee SOLO sui cambi di
posizione (poche operazioni su TF alto = fee non letali). Niente look-ahead:
il rendimento del bar i->i+1 usa la direzione decisa a close[i].
"""
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 scripts.analysis.honest_lab import ema, get_df, available_assets, FEE_RT # noqa: E402
LEV = 3.0
POS = 0.15
def simulate_position(sig: np.ndarray, df: pd.DataFrame, fee_rt: float = FEE_RT,
lev: float = LEV, pos: float = POS) -> dict:
"""sig[i] in {-1,0,1} = direzione tenuta nel bar i->i+1, decisa a close[i].
Fee one-way = fee_rt/2 su ogni unita' di variazione posizione."""
c = df["close"].values
n = len(c)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
cap = peak = 1000.0
max_dd = 0.0
cur = 0.0
flips = 0
bars_in = 0
yearly: dict[int, float] = {}
for i in range(n - 1):
s = sig[i]
if not np.isfinite(s):
s = 0.0
if s != cur:
cap -= cap * pos * (fee_rt / 2) * lev * abs(s - cur)
flips += abs(s - cur) > 0
cur = s
pr = (c[i + 1] - c[i]) / c[i]
bar_ret = pos * lev * pr * cur
cap = max(cap * (1 + bar_ret), 10.0)
peak = max(peak, cap); max_dd = max(max_dd, (peak - cap) / peak)
if cur != 0:
bars_in += 1
yr = ts.iloc[i].year
yearly[yr] = yearly.get(yr, 0.0) + bar_ret * 100
return {
"ret": (cap / 1000 - 1) * 100,
"dd": max_dd * 100,
"flips": flips,
"exposure": bars_in / n * 100,
"yearly": yearly,
"pos_years": sum(1 for v in yearly.values() if v > 0),
"n_years": len(yearly),
}
def tsmom_signal(df, lookback=30, long_only=False):
"""+1 se close>close[-lookback], -1 (o 0 se long_only) altrimenti."""
c = df["close"].values
sig = np.zeros(len(c))
for i in range(lookback, len(c)):
up = c[i] > c[i - lookback]
sig[i] = 1.0 if up else (0.0 if long_only else -1.0)
return sig
def ema_dual_signal(df, fast=20, slow=100, long_only=False):
"""+1 se EMA_fast>EMA_slow."""
c = df["close"].values
ef, es = ema(c, fast), ema(c, slow)
sig = np.where(ef > es, 1.0, 0.0 if long_only else -1.0)
sig[:slow] = 0.0
return sig
def oos(sig, df, frac=0.30):
split = int(len(df) * (1 - frac))
s2 = sig.copy(); s2[:split] = 0.0
return simulate_position(s2, df)
def show(label, df, sig):
full = simulate_position(sig, df)
o = oos(sig, df)
anni = f"{full['pos_years']}/{full['n_years']}"
print(f" {label:<26s}{full['flips']:>6d}{full['ret']:>+9.0f}{o['ret']:>+9.0f}"
f"{full['dd']:>6.0f}{full['exposure']:>6.0f}{anni:>8s}")
return full, o
if __name__ == "__main__":
assets = available_assets()
print(f"TSMOM / trend — fee {FEE_RT*100:.2f}% RT, leva3x pos15% | OOS30%")
for tf in ["1d", "4h"]:
print(f"\n ###### TF {tf} ######")
for a in assets:
df = get_df(a, tf)
print(f"\n === {a} {tf} === {'Flip':>5s}{'FULL%':>8s}{'OOS%':>8s}{'DD%':>6s}{'Exp%':>6s}{'AnniP':>8s}")
show("TSMOM lb30 long/short", df, tsmom_signal(df, 30))
show("TSMOM lb30 long-only", df, tsmom_signal(df, 30, long_only=True))
show("TSMOM lb90 long/short", df, tsmom_signal(df, 90))
show("EMA 20/100 long/short", df, ema_dual_signal(df, 20, 100))
show("EMA 20/100 long-only", df, ema_dual_signal(df, 20, 100, long_only=True))