feat(analysis): studio combinazione strategie fade + honest (diversificazione)
combine_portfolio.py: costruisce l'equity giornaliera di tutte le sleeve (8 fade + 3 honest) su indice comune 2021-2026, misura la correlazione cross-famiglia e confronta i portafogli FULL/OOS (ret, CAGR, DD, Sharpe). Risultato: le due famiglie sono quasi scorrelate (corr ~0.05). Combinarle migliora il rischio/rendimento: equal-weight 11 sleeve -> DD 6.1% full / 4.6% OOS, Sharpe OOS 4.46 (vs honest-only 12% DD / 2.23 e fade-only 8.6% DD / 4.14), CAGR ~43% mantenuta. Il 50/50 fra famiglie da' il DD piu' basso (5.5% full / 4.0% OOS). Diario 2026-05-29 e nota CLAUDE.md aggiornati. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user