bcccfde9a0
Crea gli artefatti accorpati e migliorati: - PORT02_fade_master: 3 fade (MR01/MR02/MR07) x BTC/ETH = 6 sleeve, filtro trend, equal-weight daily. DD 8.2% full / 5.9% OOS, Sharpe 3.95/4.09, CAGR ~46%. - PORT03_all_master: portafoglio MASTER fade+honest (9 sleeve), varianti equal (max Sharpe: DD 5.2%/4.7% OOS, Sharpe 3.95/4.42) e 50/50 (min DD 5.1%/4.3%). Sposta in scripts/waste/ le due peggiori: - MR03 keltner_fade: fade piu' debole (BTC Sharpe 1.22), ridondante con MR01, il filtro trend la peggiorava; rimuoverla MIGLIORA il portafoglio fade. - ROT01 xsect_rotation: strettamente dominata da ROT02 (stesso meccanismo, ROT02 meglio su tutto), non usata da alcun portafoglio. Sganciata MR03 da strategy_loader, strategies.yml e dal motore portafogli (risk_management.STRATS). La funzione keltner_fade resta in strategy_research_v2 come record. CLAUDE.md aggiornato. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
157 lines
7.0 KiB
Python
157 lines
7.0 KiB
Python
"""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()
|