feat(risk): filtro trend per alzare Acc e ridurre DD + modello portafoglio

Filtro opzionale trend_max/ema_long su tutte le fade (MR01/MR02/MR03/MR07):
salta i segnali quando |close-EMA200|/ATR supera la soglia (non fadare un trend
o crollo estremo). Con trend_max=3.0 (default in strategies.yml): accuratezza su
e DD giu' su 7/8 sleeve, drastico su ETH (MR01 71->26%, MR02 42->25%,
MR03 66->34%, MR07 46->21%); edge OOS confermato. MR03 BTC: filtro disattivo
(unico sleeve dove peggiora entrambe).

Scartate come non robuste: vol-target sizing e skip-alta-volatilita' (peggiorano
sia Acc che DD). Aggiunto modello di portafoglio equipesato su sotto-conti
indipendenti: DD aggregato ~14% full / ~10% OOS sul paniere di 8 sleeve, contro
20-70% del singolo -> vera leva anti-drawdown.

Banco di prova: scripts/analysis/risk_improvements.py, risk_portfolio.py.
Helper trend_distance() in fade_base. CLAUDE.md e diario aggiornati.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 23:47:52 +02:00
parent 21d3ba609d
commit cff0d08fca
10 changed files with 415 additions and 3 deletions
@@ -57,16 +57,21 @@ class BollingerFade(Strategy):
k = params.get("k", 2.5)
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)
ma = pd.Series(c).rolling(bb_w).mean().values
sd = pd.Series(c).rolling(bb_w).std().values
a = _atr(df, 14)
up, lo = ma + k * sd, ma - k * sd
el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values if trend_max is not None else None
signals: list[Signal] = []
for i in range(bb_w + 14, n_len):
if np.isnan(up[i]) or np.isnan(a[i]):
continue
if el is not None and (a[i] == 0 or np.isnan(el[i]) or abs(c[i] - el[i]) / a[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]:
+6 -1
View File
@@ -26,7 +26,7 @@ import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class DonchianFade(FadeStrategy):
@@ -40,16 +40,21 @@ class DonchianFade(FadeStrategy):
n = params.get("n", 20)
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)
h, l, c = df["high"].values, df["low"].values, df["close"].values
hh = pd.Series(h).rolling(n).max().shift(1).values
ll = pd.Series(l).rolling(n).min().shift(1).values
a = atr(df, 14)
td = trend_distance(df, ema_long) if trend_max is not None else None
signals: list[Signal] = []
for i in range(n + 14, len(c)):
if np.isnan(hh[i]) or np.isnan(a[i]):
continue
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
continue
mid = (hh[i] + ll[i]) / 2.0
if c[i] > hh[i] and c[i - 1] <= hh[i - 1]: # rottura rialzista -> fade short
d, sl = -1, c[i] + sl_atr * a[i]
+6 -1
View File
@@ -26,7 +26,7 @@ import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class KeltnerFade(FadeStrategy):
@@ -41,16 +41,21 @@ class KeltnerFade(FadeStrategy):
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]:
+6 -1
View File
@@ -29,7 +29,7 @@ import numpy as np
import pandas as pd
from src.strategies.base import Signal
from src.strategies.fade_base import FadeStrategy, atr
from src.strategies.fade_base import FadeStrategy, atr, trend_distance
class ReturnReversal(FadeStrategy):
@@ -45,17 +45,22 @@ class ReturnReversal(FadeStrategy):
tp_atr = params.get("tp_atr", 2.0)
sl_atr = params.get("sl_atr", 1.5)
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
ret = np.zeros_like(c)
ret[1:] = np.diff(c) / c[:-1]
sig = pd.Series(ret).rolling(n).std().values
a = atr(df, 14)
td = trend_distance(df, ema_long) if trend_max is not None else None
signals: list[Signal] = []
for i in range(n + 14, len(c)):
if np.isnan(sig[i]) or sig[i] == 0 or np.isnan(a[i]):
continue
if td is not None and (np.isnan(td[i]) or td[i] > trend_max):
continue
z = ret[i] / sig[i]
if z <= -k: # crollo di barra -> fade long
d, tp, sl = 1, c[i] + tp_atr * a[i], c[i] - sl_atr * a[i]