diff --git a/CLAUDE.md b/CLAUDE.md index 5bd69d5..ce579b8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,6 +118,14 @@ Leva non robusta scartate: vol-target sizing e skip-alta-volatilità (peggiorano × BTC/ETH, pos 0.15 ciascuno) abbatte il DD aggregato: ~14% full / ~10% OOS sul paniere di 8 sleeve, contro il 20-70% del singolo. È la vera leva anti-drawdown. +**Combinare le due famiglie (fade + honest).** Le fade (reversione intraday 1h) e le +honest (DIP/TR/ROT trend+rotazione multi-crypto) sono **quasi scorrelate** +(correlazione cross-famiglia ~0.05). Combinarle in un unico portafoglio migliora il +rischio/rendimento rispetto a ciascuna famiglia da sola: equal-weight degli 11 sleeve +→ DD 6.1% full / 4.6% OOS e Sharpe OOS 4.46 (vs honest-only 12% DD / 2.23 Sharpe e +fade-only 8.6% DD / 4.14 Sharpe), CAGR ~43% mantenuta. Studio in +`scripts/analysis/combine_portfolio.py`. + **Metodologia obbligatoria per ogni nuova strategia** (per non ripetere l'errore squeeze): 1. Ingresso eseguibile: direzione e prezzo decisi con dati **fino a `close[i]`**, mai `close[i-1]` con direzione da `i`. 2. Backtest **NETTO** dopo fee realistiche Deribit (**0.10% RT** taker, non 0.20%) + leva. diff --git a/docs/diary/2026-05-29.md b/docs/diary/2026-05-29.md new file mode 100644 index 0000000..0058a77 --- /dev/null +++ b/docs/diary/2026-05-29.md @@ -0,0 +1,49 @@ +# Diario di ricerca — 2026-05-29 + +## Combinare le strategie migliora i risultati? + +**Domanda:** usare insieme le due famiglie di strategie presenti sul repo migliora +il profilo rischio/rendimento rispetto a usarle separatamente? + +- **FADE** (mie): reversione intraday 1h, long/short, BTC/ETH — MR01 Bollinger, + MR02 Donchian, MR03 Keltner, MR07 Return-reversal (tutte col filtro trend 3.0 ATR). +- **HONEST** (altra sessione): long-only multi-regime multi-crypto — DIP01 (dip-buy + 1h BTC), TR01 (EMA-trend 4h basket), ROT02 (dual-momentum rotation 1d). + +**Metodo** (`scripts/analysis/combine_portfolio.py`): 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), 50/50 fra famiglie +e inverse-vol. Metriche FULL e OOS (ultimo 30% della finestra comune, da 2024-10-12): +ritorno, CAGR, max DD, Sharpe annualizzato. Le curve honest sono riusate da +`honest_improve2.py`; quelle fade da `risk_management.build_trades`. + +**Correlazione:** cross-famiglia **+0.05** (quasi indipendenti). Intra-fade +0.18, +intra-honest +0.05. L'unica coppia un po' correlata è MR01_BTC↔DIP01_BTC (+0.43), +entrambe mean-reversion su BTC. Famiglie scorrelate ⇒ diversificazione quasi ideale. + +**Risultati (FULL | OOS):** + +| Portafoglio | Ret% | CAGR | DD% | Sharpe | oDD% | oSharpe | +|---|---|---|---|---|---|---| +| FADE only (8) | +549 | 41 | 8.6 | 3.75 | 5.4 | 4.14 | +| HONEST only (3) | +642 | 45 | 12.0 | 1.90 | 6.5 | 2.23 | +| **ALL equal-weight (11)** | +589 | 43 | 6.1 | **3.95** | 4.6 | **4.46** | +| **ALL 50/50 famiglie** | +615 | 44 | **5.5** | 3.18 | **4.0** | 3.87 | +| ALL inverse-vol | +483 | 39 | 5.8 | 3.97 | 4.6 | 4.02 | + +**Conclusione: sì, combinare conviene.** +- DD crolla: combinato 5.5–6.1% full / 4.0–4.6% OOS, contro 8.6% (fade) e **12%** + (honest) da sole → drawdown ridotto del 35–50%. +- Sharpe sale: combinato OOS **4.46** vs honest 2.23 (raddoppia) e batte pure fade (4.14). +- CAGR resta ~43–44% (≈ media delle due famiglie) ma con metà del rischio: è il + "free lunch" della diversificazione fra sorgenti di edge scorrelate. +- Best Sharpe = equal-weight degli 11 sleeve; best DD = 50/50 fra le due famiglie. + +**Caveat onesti:** la finestra comune è 2021–2026 (5.4 anni), OOS ~2024-10→oggi +(1.6 anni) — pochi regimi. CAGR e Sharpe sono backtest a leva 3x; il 2024 cripto +favorevole pesa. Il target €50/giorno resta vincolato dal capitale: 43% CAGR su +€1000 non fa €50/giorno a breve, serve compounding pluriennale o più capitale. +Prossimo passo: confermare il portafoglio combinato nel paper trader live. + +**File:** `scripts/analysis/combine_portfolio.py` (nuovo). diff --git a/scripts/analysis/combine_portfolio.py b/scripts/analysis/combine_portfolio.py new file mode 100644 index 0000000..db39b6b --- /dev/null +++ b/scripts/analysis/combine_portfolio.py @@ -0,0 +1,150 @@ +"""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, MR03 keltner, + MR07 return-reversal — tutte col filtro trend 3.0 ATR. + 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 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()