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) <noreply@anthropic.com>
This commit is contained in:
@@ -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()})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user