212427ffa1
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>
110 lines
3.7 KiB
Python
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))
|