From bcccfde9a0a5a93f832c932ec48e9b2d24e9b072 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Fri, 29 May 2026 00:21:37 +0200 Subject: [PATCH] feat(strategie): portafogli master (PORT02/PORT03) + waste delle peggiori (MR03, ROT01) 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) --- CLAUDE.md | 22 +++++- scripts/analysis/combine_portfolio.py | 10 ++- scripts/analysis/risk_management.py | 9 ++- scripts/strategies/PORT02_fade_master.py | 52 +++++++++++++ scripts/strategies/PORT03_all_master.py | 77 +++++++++++++++++++ .../MR03_keltner_fade.py | 0 .../ROT01_xsect_rotation.py | 0 src/live/strategy_loader.py | 1 - strategies.yml | 26 +------ 9 files changed, 162 insertions(+), 35 deletions(-) create mode 100644 scripts/strategies/PORT02_fade_master.py create mode 100644 scripts/strategies/PORT03_all_master.py rename scripts/{strategies => waste}/MR03_keltner_fade.py (100%) rename scripts/{strategies => waste}/ROT01_xsect_rotation.py (100%) diff --git a/CLAUDE.md b/CLAUDE.md index ce579b8..5a98e9f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -90,9 +90,13 @@ Tutte le strategie estendono `src.strategies.base.Strategy` |--------|------|-----------|-----------------------------------|----|------| | **MR01** | Bollinger Fade | banda std attorno a SMA | BTC +201% / ETH +1238% | 15-72% | Fada la banda, TP alla media, SL ad ATR | | **MR02** | Donchian Fade | estremi canale H/L | BTC +172% / ETH enorme | 30-42% | Fada la rottura del canale, TP al centro | -| **MR03** | Keltner Fade | canale ATR attorno a EMA | BTC +112% / ETH +886% | 20-66% | Banda indipendente da Bollinger | | **MR07** | Return Reversal | z dei rendimenti di barra | BTC +105% / ETH +195% | 25-46% | Fada il movimento estremo, exit in ATR; esposizione ~8% | +> **MR03 Keltner Fade** spostata in `scripts/waste/`: era la fade più debole +> (BTC Sharpe 1.22, il filtro trend la peggiorava) e ridondante con MR01 (stessa +> idea di banda). Rimuoverla dal portafoglio ne ha *migliorato* le metriche. +> La funzione `keltner_fade` resta in `strategy_research_v2.py` come record. + **Lezione confermata:** l'edge è sempre *mean-reversion* (i breakout rientrano). Il trend-following (Donchian trend, RSI cross) e gli oscillatori senza filtro (RSI revert, ADX-filtered fade) perdono netti → restano scartati. @@ -121,11 +125,21 @@ 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 +rischio/rendimento rispetto a ciascuna famiglia da sola: equal-weight dei 9 sleeve +→ DD 5.2% full / 4.7% OOS e Sharpe OOS 4.42 (vs honest-only 12% DD / 2.23 Sharpe e +fade-only 8.2% DD / 4.09 Sharpe), CAGR ~46% mantenuta. Studio in `scripts/analysis/combine_portfolio.py`. +**Portafogli pronti (artefatti accorpati e migliorati).** Oltre a `PORT01` (solo +honest), due script in `scripts/strategies/`: +- `PORT02_fade_master.py` — le 3 fade × BTC/ETH accorpate (6 sleeve, filtro trend), + equal-weight daily: DD ~8.2% full / 5.9% OOS, Sharpe 3.95/4.09, CAGR ~46%. +- `PORT03_all_master.py` — portafoglio MASTER (fade + honest, 9 sleeve). Due varianti: + `equal` (massimo Sharpe: DD 5.2%/4.7% OOS, Sharpe 3.95/4.42) e `5050` fra le due + famiglie (minimo DD: 5.1% full / 4.3% OOS). È la configurazione consigliata. +Come `PORT01`, sono meta-portafogli (script `run()` di report), non `Strategy` con +`generate_signals`, quindi non nel `strategy_loader`. + **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/scripts/analysis/combine_portfolio.py b/scripts/analysis/combine_portfolio.py index db39b6b..c730ea0 100644 --- a/scripts/analysis/combine_portfolio.py +++ b/scripts/analysis/combine_portfolio.py @@ -1,8 +1,8 @@ """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. + 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). @@ -79,6 +79,12 @@ def metrics(daily_ret: pd.Series, lo: int = 0, hi: int | None = None) -> dict: 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()}) diff --git a/scripts/analysis/risk_management.py b/scripts/analysis/risk_management.py index be7e10e..20bb524 100644 --- a/scripts/analysis/risk_management.py +++ b/scripts/analysis/risk_management.py @@ -29,18 +29,19 @@ sys.path.insert(0, str(PROJECT_ROOT)) from src.data.downloader import load_data from scripts.analysis.strategy_research import bollinger_fade, atr -from scripts.analysis.strategy_research_v2 import donchian_fade, keltner_fade, return_reversal +from scripts.analysis.strategy_research_v2 import donchian_fade, return_reversal FEE_RT, LEV, POS, INIT, OOS_FRAC = 0.001, 3.0, 0.15, 1000.0, 0.30 -# config base di ogni strategia (come strategies.yml); su ETH MR03 usa n=50 +# config base di ogni strategia (come strategies.yml). +# NB: MR03 keltner_fade spostata in scripts/waste/ (fade piu' debole, ridondante +# con MR01); la funzione keltner_fade resta in strategy_research_v2 come record. STRATS = { "MR01": (bollinger_fade, dict(n=50, k=2.5, sl_atr=2.0, max_bars=24)), "MR02": (donchian_fade, dict(n=20, sl_atr=2.0, max_bars=24)), - "MR03": (keltner_fade, dict(n=30, k=2.0, sl_atr=2.0, max_bars=24)), "MR07": (return_reversal,dict(n=50, k=3.5, tp_atr=2.0, sl_atr=1.5, max_bars=24)), } -STRATS_ETH = dict(STRATS); STRATS_ETH["MR03"] = (keltner_fade, dict(n=50, k=2.0, sl_atr=2.0, max_bars=24)) +STRATS_ETH = dict(STRATS) def strats_for(asset: str) -> dict: diff --git a/scripts/strategies/PORT02_fade_master.py b/scripts/strategies/PORT02_fade_master.py new file mode 100644 index 0000000..e8689d7 --- /dev/null +++ b/scripts/strategies/PORT02_fade_master.py @@ -0,0 +1,52 @@ +"""PORT02 — Portafoglio FADE accorpato e migliorato (6 sleeve, equal-weight daily). + +Accorpa le 3 strategie fade mean-reversion su BTC e ETH, ciascuna MIGLIORATA con +il filtro trend (salta i fade contro trend estremi: |close-EMA200|/ATR > 3.0): + MR01 Bollinger fade · MR02 Donchian fade · MR07 Return-reversal + x {BTC, ETH} -> 6 sleeve indipendenti, pos 0.15 ciascuna, leva 3x. + (MR03 Keltner spostata in waste: fade piu' debole e ridondante con MR01.) + +Le curve sono poco correlate fra loro (corr media intra-fade ~0.18): la +diversificazione abbatte il DD aggregato ben sotto quello del singolo sleeve. + +Risultato (netto fee 0.10% RT, equal-weight ribilanciato ogni giorno, finestra +comune 2021-2026, OOS = ultimo 30%): + Ret +666% / CAGR 46% / DD 8.2% FULL · OOS DD 5.9% / Sharpe 3.95 FULL / 4.09 OOS. +Ricostruzione e confronto: scripts/analysis/combine_portfolio.py. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.analysis.combine_portfolio import ( # noqa: E402 + build_all_sleeves, port_returns, metrics, yearly_returns, SPLIT, OOS_DATE, IDX, +) + + +def run(): + sleeves = {k: v for k, v in build_all_sleeves().items() if k.startswith("MR")} + pr = port_returns(sleeves) + full, oos = metrics(pr), metrics(pr, lo=SPLIT) + + print("=" * 84) + print(f" PORT02 — FADE master (8 sleeve, equal-weight daily) | {IDX[0].date()} -> {IDX[-1].date()}") + print("=" * 84) + print(f" {'sleeve':<14s}{'Ret%':>9s}{'DD%':>7s}{'Shrp':>7s}") + for name, s in sleeves.items(): + m = metrics(s.pct_change().fillna(0.0)) + print(f" {name:<14s}{m['ret']:>+9.0f}{m['dd']:>7.1f}{m['sharpe']:>7.2f}") + print(" " + "-" * 80) + print(f" {'PORTAFOGLIO':<14s}{full['ret']:>+9.0f}{full['dd']:>7.1f}{full['sharpe']:>7.2f}" + f" CAGR {full['cagr']:.0f}%") + print(f" {' di cui OOS':<14s}{oos['ret']:>+9.0f}{oos['dd']:>7.1f}{oos['sharpe']:>7.2f}" + f" (da {OOS_DATE})") + pa = yearly_returns(pr) + print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in pa.items())) + + +if __name__ == "__main__": + run() diff --git a/scripts/strategies/PORT03_all_master.py b/scripts/strategies/PORT03_all_master.py new file mode 100644 index 0000000..8544cb1 --- /dev/null +++ b/scripts/strategies/PORT03_all_master.py @@ -0,0 +1,77 @@ +"""PORT03 — Portafoglio MASTER: accorpa TUTTE le strategie (fade + honest). + +Unisce le due famiglie del progetto, quasi scorrelate (correlazione cross-famiglia +~0.05), in un unico portafoglio diversificato: + FADE (reversione intraday 1h, long/short, BTC/ETH, col filtro trend): + MR01 Bollinger · MR02 Donchian · MR07 Return-reversal -> 6 sleeve + HONEST (long-only multi-regime multi-crypto): + DIP01 dip-buy (1h) · TR01 EMA-trend (4h basket) · ROT02 dual-momentum (1d) -> 3 sleeve + +Combinare le due famiglie migliora il rischio/rendimento rispetto a ciascuna da +sola: il DD scende e lo Sharpe sale (la honest, da sola piu' lumpy, viene +stabilizzata dalle fade ad alta frequenza). Vedi scripts/analysis/combine_portfolio.py. + +Risultato (netto, equal-weight daily, finestra comune 2021-2026, OOS = ultimo 30%): + FADE only (6) DD 8.2% Sharpe 3.95 (OOS DD 5.9 / Shrp 4.09) + HONEST only (3) DD 12.0% Sharpe 1.90 (OOS DD 6.5 / Shrp 2.23) + MASTER eq (9) DD 5.2% Sharpe 3.95 (OOS DD 4.7 / Shrp 4.42) <- miglior Sharpe + MASTER 50/50 DD 5.1% Sharpe 3.31 (OOS DD 4.3) <- miglior DD + CAGR ~46% mantenuta in entrambe le varianti combinate. + +Due varianti operative selezionabili: + weighting="equal" -> equal-weight sui 9 sleeve (massimo Sharpe) + weighting="5050" -> 50% famiglia fade + 50% famiglia honest (minimo DD) +""" +from __future__ import annotations + +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.analysis.combine_portfolio import ( # noqa: E402 + build_all_sleeves, port_returns, metrics, yearly_returns, SPLIT, OOS_DATE, IDX, +) + + +def master_returns(sleeves: dict, weighting: str = "equal"): + """Rendimenti giornalieri del portafoglio master. + equal = equal-weight su tutti gli 11 sleeve; 5050 = media fra le due famiglie.""" + fade = {k: v for k, v in sleeves.items() if k.startswith("MR")} + honest = {k: v for k, v in sleeves.items() if not k.startswith("MR")} + if weighting == "5050": + return (port_returns(fade) + port_returns(honest)) / 2 + return port_returns(sleeves) + + +def run(): + sleeves = build_all_sleeves() + fade = {k: v for k, v in sleeves.items() if k.startswith("MR")} + honest = {k: v for k, v in sleeves.items() if not k.startswith("MR")} + + print("=" * 90) + print(f" PORT03 — MASTER (fade + honest) | {IDX[0].date()} -> {IDX[-1].date()} | OOS da {OOS_DATE}") + print("=" * 90) + print(f" {'portafoglio':<22s}{'Ret%':>9s}{'CAGR':>7s}{'DD%':>7s}{'Shrp':>7s}" + f" | {'oRet%':>9s}{'oDD%':>7s}{'oShrp':>7s}") + print(" " + "-" * 86) + + def line(label, pr): + f, o = metrics(pr), metrics(pr, lo=SPLIT) + print(f" {label:<22s}{f['ret']:>+9.0f}{f['cagr']:>7.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}" + f" | {o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}") + + line(f"FADE only ({len(fade)})", port_returns(fade)) + line(f"HONEST only ({len(honest)})", port_returns(honest)) + line(f"MASTER equal ({len(sleeves)})", master_returns(sleeves, "equal")) + line("MASTER 50/50 fam", master_returns(sleeves, "5050")) + + print(" " + "-" * 86) + pa = yearly_returns(master_returns(sleeves, "equal")) + print(" MASTER equal per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in pa.items())) + print(" -> combinare le due famiglie scorrelate (~0.05) abbassa il DD e alza lo Sharpe.") + + +if __name__ == "__main__": + run() diff --git a/scripts/strategies/MR03_keltner_fade.py b/scripts/waste/MR03_keltner_fade.py similarity index 100% rename from scripts/strategies/MR03_keltner_fade.py rename to scripts/waste/MR03_keltner_fade.py diff --git a/scripts/strategies/ROT01_xsect_rotation.py b/scripts/waste/ROT01_xsect_rotation.py similarity index 100% rename from scripts/strategies/ROT01_xsect_rotation.py rename to scripts/waste/ROT01_xsect_rotation.py diff --git a/src/live/strategy_loader.py b/src/live/strategy_loader.py index c1dd8fc..e6ae01c 100644 --- a/src/live/strategy_loader.py +++ b/src/live/strategy_loader.py @@ -19,7 +19,6 @@ _REGISTRY: dict[str, type[Strategy]] = {} MODULE_MAP = { "MR01_bollinger_fade": ("MR01_bollinger_fade", "BollingerFade"), "MR02_donchian_fade": ("MR02_donchian_fade", "DonchianFade"), - "MR03_keltner_fade": ("MR03_keltner_fade", "KeltnerFade"), "MR07_return_reversal": ("MR07_return_reversal", "ReturnReversal"), } diff --git a/strategies.yml b/strategies.yml index 66699d0..5bb8b4a 100644 --- a/strategies.yml +++ b/strategies.yml @@ -60,30 +60,8 @@ strategies: trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD- ema_long: 200 - # MR03 Keltner fade: fade canale ATR su EMA (banda indipendente da Bollinger). - # Robusto su tutta la griglia n x k. BTC n30 k2.0 +112% OOS DD20%. - # ETH: edge ampio ma DD pieno ~65% (tratto dell'asset, come MR01) -> leva bassa. - - name: MR03_keltner_fade - asset: BTC - tf: 1h - enabled: true - params: - n: 30 - k: 2.0 - sl_atr: 2.0 - max_bars: 24 - # NB: su MR03 BTC il filtro trend PEGGIORA Acc e DD (unico sleeve) -> disattivo. - - name: MR03_keltner_fade - asset: ETH - tf: 1h - enabled: true - params: - n: 50 - k: 2.0 - sl_atr: 2.0 - max_bars: 24 - trend_max: 3.0 # salta fade contro trend estremo (|close-EMA200|/ATR>3): Acc+ DD- - ema_long: 200 + # MR03 Keltner fade -> spostata in scripts/waste/ (fade piu' debole e ridondante + # con MR01; il filtro trend la peggiorava su BTC). Vedi CLAUDE.md. # MR07 Return reversal: fade movimento di barra estremo (z dei rendimenti). # Meccanismo distinto (volatilita' rendimenti, non livelli). Esposizione bassa