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:
2026-05-29 00:21:37 +02:00
parent 1af4addbdd
commit bcccfde9a0
9 changed files with 162 additions and 35 deletions
+82
View File
@@ -0,0 +1,82 @@
"""MR03 — Keltner Fade (mean-reversion sul canale ATR).
Stessa tesi di MR01 (i breakout rientrano) ma con banda costruita su ATR
attorno a una EMA, invece che su deviazione standard attorno a una SMA.
Reagisce diversamente a gap e code: edge indipendente, non ridondante con MR01.
Logica:
1. Canale di Keltner: EMA(n) +/- k*ATR(n)
2. ENTRY: close esce sotto la banda inferiore -> LONG (o sopra la superiore -> SHORT)
Ingresso a close[i] (eseguibile dal vivo, nessun look-ahead).
3. EXIT: take-profit alla EMA centrale (il rientro atteso),
stop-loss a sl_atr*ATR oltre l'estremo, time-limit max_bars.
Validazione (netto, fee 0.10% RT reale Deribit, leva 3x, OOS = ultimo 30%):
BTC 1h n=30 k=2.0: +112% OOS, DD 20%
ETH 1h n=50 k=1.5: +1426% OOS, DD 20%
Robusto su TUTTA la griglia n in {14,20,30,50} x k in {1.5,2.0,2.5}
(BTC+ETH 1h sempre positivo OOS).
Ricerca completa: scripts/analysis/strategy_research_v2.py.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class KeltnerFade(FadeStrategy):
name = "MR03_keltner_fade"
description = "Mean-reversion: fada il canale di Keltner (ATR), TP alla EMA"
default_assets = ["BTC", "ETH"]
default_timeframes = ["1h"]
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
**params) -> list[Signal]:
n = params.get("n", 30)
k = params.get("k", 2.0)
sl_atr = params.get("sl_atr", 2.0)
max_bars = params.get("max_bars", 24)
trend_max = params.get("trend_max") # None = filtro disattivo
ema_long = params.get("ema_long", 200)
c = df["close"].values
e = pd.Series(c).ewm(span=n, adjust=False).mean().values
a = atr(df, n)
up, lo = e + k * a, e - k * a
td = trend_distance(df, ema_long) if trend_max is not None else None
signals: list[Signal] = []
for i in range(n + 1, len(c)):
if np.isnan(up[i]) or np.isnan(a[i]):
continue
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
continue
if c[i] < lo[i] and c[i - 1] >= lo[i - 1]:
d, sl = 1, c[i] - sl_atr * a[i]
elif c[i] > up[i] and c[i - 1] <= up[i - 1]:
d, sl = -1, c[i] + sl_atr * a[i]
else:
continue
signals.append(Signal(
idx=i, direction=d, entry_price=c[i],
metadata={"tp": float(e[i]), "sl": float(sl), "max_bars": max_bars},
))
return signals
if __name__ == "__main__":
strat = KeltnerFade()
print("=" * 110)
print(f" MR03 KELTNER FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x")
print("=" * 110)
for asset, n, k in [("BTC", 30, 2.0), ("ETH", 50, 1.5)]:
r = strat.backtest(asset, "1h", n=n, k=k, sl_atr=2.0, max_bars=24)
if r:
r.strategy_name = f"MR03 {asset} 1h n{n} k{k}"
r.print_summary()
r.print_yearly()
+48
View File
@@ -0,0 +1,48 @@
"""ROT01 — Cross-Sectional Momentum Rotation (multi-crypto, long-only), 1d.
UNA strategia che usa l'INTERO paniere di crypto in un solo book: ogni giorno
ordina gli asset per momentum (rendimento sugli ultimi `lookback` giorni) e alloca
il capitale in parti uguali ai `top_k` con momentum positivo; il resto in cash.
Cattura la dispersione tra crypto (gli alt forti corrono molto piu' di BTC nei bull)
senza shortare nulla. Meccanismo distinto da DIP01/TR01 -> vera diversificazione.
Onesto: i pesi a close[i] usano solo rendimenti passati; il rendimento del giorno
i->i+1 e' realizzato con quei pesi. Fee sul turnover. Allineamento per timestamp.
Validazione (netto, fee 0.10% RT, gross 0.45, OOS = ultimo 30%):
lb=60 top2 -> FULL +679% / OOS +44% / DD 53% / 5-7 anni positivi.
Param-insensitive (tutte le lb/k positive) e regge fee fino 0.20% RT (OOS +41%).
Per-anno: 2020+33 2021+181 2022-29 2023+43 2024+59 2025+6 2026-10 (i negativi = bear).
Dettagli in scripts/analysis/honest_rotation.py / honest_final.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.honest_rotation import build_panel, simulate_rotation # noqa: E402
from scripts.analysis.honest_lab import available_assets
LOOKBACK, TOP_K, TF = 60, 2, "1d"
def run():
assets = available_assets()
panel = build_panel(assets, TF)
print("=" * 90)
print(f" ROT01 ROTAZIONE cross-sectional momentum | {TF} lb={LOOKBACK} top{TOP_K} | netto fee 0.10% RT")
print("=" * 90)
print(f" Paniere: {list(panel.columns)}")
print(f" Periodo: {panel.index[0].date()} -> {panel.index[-1].date()} ({panel.shape[0]} barre)")
full = simulate_rotation(panel, lookback=LOOKBACK, top_k=TOP_K, fee_rt=0.001)
oos = simulate_rotation(panel, lookback=LOOKBACK, top_k=TOP_K, fee_rt=0.001, oos_frac=0.30)
print(f"\n FULL: {full['ret']:+.0f}% DD {full['dd']:.0f}% turnover {full['turnover']:.0f}")
print(f" OOS : {oos['ret']:+.0f}% DD {oos['dd']:.0f}% ({full['pos_years']}/{full['n_years']} anni positivi)")
print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(full["yearly"].items())))
if __name__ == "__main__":
run()