"""Studio: combinare TUTTE le strategie (fade + honest) migliora i risultati? Due famiglie con meccanismi e orizzonti diversi: FADE (intraday 1h, long/short, BTC/ETH): MR01 boll, MR02 donchian, MR07 return-reversal — tutte col filtro trend 3.0 ATR. (MR03 keltner -> waste.) HONEST (long-only, multi-regime, multi-crypto): DIP01 (dip-buy 1h BTC), TR01 (EMA trend 4h basket), ROT02 (dual-momentum rotation 1d). Metodo: per ogni sleeve si costruisce l'equity GIORNALIERA normalizzata su un indice comune (2021-01-01 -> 2026-05-26), si passa ai rendimenti giornalieri, si misura la correlazione cross-famiglia e si confrontano i portafogli equal-weight (ribilanciati ogni giorno) e inverse-vol. Metriche FULL e OOS (ultimo 30% della finestra comune): ritorno, CAGR, max DD, Sharpe annualizzato. Tutto NETTO (fee gia' incluse nelle sleeve), leva 3x, pos 15% per sleeve. """ 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 # curve daily honest gia' pronte nell'altra famiglia from scripts.analysis.honest_improve2 import ( _daily_equity, _norm, dip_market_gated, _tr_basket_daily, _rot_daily_equity, ) IDX = pd.date_range("2021-01-01", "2026-05-26", freq="1D", tz="UTC") OOS_FRAC = 0.30 SPLIT = int(len(IDX) * (1 - OOS_FRAC)) # confine OOS sulla finestra comune OOS_DATE = IDX[SPLIT].date() ANN = 365.0 # giorni/anno per annualizzare # ---------------- equity giornaliere ---------------- def fade_daily_equity(asset: str, fn, params) -> pd.Series: """Equity giornaliera di uno sleeve fade: trade 1h (filtro trend 3.0) -> equity -> daily.""" df = load_data(asset, "1h") ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) trades = build_trades(fn(df, **params), df, trend_max=3.0) n = len(df); eq = np.full(n, INIT, dtype=float); cap = INIT for i, j, ret in sorted(trades, key=lambda t: t[1]): cap = max(cap + cap * 0.15 * ret, 10.0) eq[j:] = cap s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill() return _norm(s) def build_all_sleeves() -> dict[str, pd.Series]: sleeves: dict[str, pd.Series] = {} # --- FADE: 8 sleeve --- for asset in ["BTC", "ETH"]: for nm, (fn, params) in strats_for(asset).items(): sleeves[f"{nm}_{asset}"] = fade_daily_equity(asset, fn, params) # --- HONEST: 3 sleeve (riuso le funzioni dell'altra famiglia) --- d = dip_market_gated("BTC", market_n=0, return_equity=True) sleeves["DIP01_BTC"] = _norm(_daily_equity(d["eq_ts"], d["eq_v"], IDX)) sleeves["TR01_basket"] = _norm(_tr_basket_daily(["BNB", "BTC", "DOGE", "SOL", "XRP"], IDX)) sleeves["ROT02_rot"] = _norm(_rot_daily_equity(IDX)) return sleeves # ---------------- metriche ---------------- def metrics(daily_ret: pd.Series, lo: int = 0, hi: int | None = None) -> dict: r = daily_ret.iloc[lo:hi] eq = (1 + r).cumprod() peak = eq.cummax(); dd = float(((peak - eq) / peak).max() * 100) yrs = len(r) / ANN tot = (eq.iloc[-1] - 1) * 100 cagr = ((eq.iloc[-1]) ** (1 / yrs) - 1) * 100 if yrs > 0 else 0.0 sharpe = float(r.mean() / r.std() * np.sqrt(ANN)) if r.std() > 0 else 0.0 return dict(ret=tot, cagr=cagr, dd=dd, sharpe=sharpe) def yearly_returns(daily_ret: pd.Series) -> dict[int, float]: """Rendimento % netto per anno solare dai rendimenti giornalieri composti.""" g = daily_ret.groupby(daily_ret.index.year).apply(lambda x: ((1 + x).prod() - 1) * 100) return {int(y): float(v) for y, v in g.items()} def port_returns(members: dict[str, pd.Series], weights: dict[str, float] | None = None) -> pd.Series: """Rendimenti giornalieri di un portafoglio ribilanciato ogni giorno ai pesi dati.""" dr = pd.DataFrame({k: v.pct_change().fillna(0.0) for k, v in members.items()}) if weights is None: return dr.mean(axis=1) w = pd.Series(weights); w = w / w.sum() return (dr * w).sum(axis=1) def inv_vol_weights(members: dict[str, pd.Series], lo=0, hi=None) -> dict[str, float]: """Pesi inversamente proporzionali alla volatilita' (stimata sulla finestra train).""" vol = {k: v.pct_change().iloc[lo:hi].std() for k, v in members.items()} inv = {k: (1.0 / s if s and s > 0 else 0.0) for k, s in vol.items()} tot = sum(inv.values()) return {k: x / tot for k, x in inv.items()} # ---------------- report ---------------- def row(label, dr): f = metrics(dr); o = metrics(dr, lo=SPLIT) print(f" {label:<26s}{f['ret']:>+9.0f}{f['cagr']:>7.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" f" | {o['ret']:>+9.0f}{o['cagr']:>7.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}") def main(): print("Costruzione equity giornaliere (puo' richiedere ~1 min)...") S = build_all_sleeves() fade = {k: v for k, v in S.items() if k.startswith("MR")} honest = {k: v for k, v in S.items() if not k.startswith("MR")} # --- correlazione cross-famiglia --- dr = pd.DataFrame({k: v.pct_change().fillna(0.0) for k, v in S.items()}) corr = dr.corr() fade_k, hon_k = list(fade), list(honest) cross = corr.loc[fade_k, hon_k] print("\n" + "=" * 92) print(f" CORRELAZIONE rendimenti giornalieri — FADE (righe) vs HONEST (colonne) | {IDX[0].date()}->{IDX[-1].date()}") print("=" * 92) print(f" {'':<12s}" + "".join(f"{c:>13s}" for c in hon_k)) for f in fade_k: print(f" {f:<12s}" + "".join(f"{cross.loc[f,c]:>13.2f}" for c in hon_k)) intra_fade = corr.loc[fade_k, fade_k].values[np.triu_indices(len(fade_k), 1)].mean() intra_hon = corr.loc[hon_k, hon_k].values[np.triu_indices(len(hon_k), 1)].mean() print(f"\n Corr media intra-FADE {intra_fade:+.2f} | intra-HONEST {intra_hon:+.2f} | " f"cross-famiglia {cross.values.mean():+.2f} (piu' bassa = piu' diversificazione)") # --- confronto portafogli --- print("\n" + "=" * 92) print(f" PORTAFOGLI equal-weight (ribil. giornaliero) | OOS da {OOS_DATE} | leva3x pos15%/sleeve") print("=" * 92) print(f" {'portafoglio':<26s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}" f" | {'oRet%':>9s}{'oCAGR':>7s}{'oDD%':>7s}{'oShrp':>7s}") print(" " + "-" * 88) row("FADE only (8 sleeve)", port_returns(fade)) row("HONEST only (3 sleeve)", port_returns(honest)) row("ALL equal-weight (11)", port_returns(S)) # 50/50 fra le due famiglie (ogni famiglia equipesata al suo interno) fr, hr = port_returns(fade), port_returns(honest) row("ALL 50/50 famiglie", (fr + hr) / 2) # inverse-vol sul train, applicato a tutti gli 11 sleeve w = inv_vol_weights(S, lo=0, hi=SPLIT) row("ALL inverse-vol", port_returns(S, w)) print(" " + "-" * 88) print(" Sharpe annualizzato sui rendimenti giornalieri. Confronta DD e Sharpe:") print(" se il combinato ha DD piu' basso e Sharpe piu' alto delle singole famiglie, combinare conviene.") if __name__ == "__main__": main()