Files
PythagorasGoal/tests/portfolio/test_hurst_lossguard.py
T
Adriano Dal Pastro ac6f3766b0 feat(fade): loss-guard Hurst (skip regime persistente) — dimezza il DD del PORT06
GOAL: limitare le perdite delle fade in regime sfavorevole. Diagnosi (3022 trade): le perdite/stop
si concentrano nel regime PERSISTENTE (hurst>0.55: stop-rate 43% vs 21% anti-persistente), NON in
bassa vol (low-vol e' net positivo). Ricerca web + workflow 11 agenti: l'UNICO meccanismo che riduce
DD senza uccidere l'edge e' il filtro Hurst (ADX, vol-expansion, time-stop, ER, vol-target falliscono
il gate FR01). Test esterni ADX/vol-expansion NON si replicano su queste fade crypto.

TEST DECISIVO PORT06 (gate FR01) SUPERATO: Hurst-skip h<0.55 sulle 6 fade ->
FULL Sharpe 6.62->6.76, FULL DD 4.10%->2.39% (quasi dimezzato), OOS Sharpe 8.89->9.15.
Migliora il portafoglio (a differenza di FR01 che diluiva).

Implementazione: hurst_skip_mask in fade_base.py (rolling-Hurst causale dalle SOLE close -> nessun
feed dati esterno, deployabile inline dal worker) + param hurst_max (default None=off) in
MR01/MR02/MR07. Test: test_hurst_lossguard.py. Default off -> zero impatto su backtest/parita'/live
finche' non attivato.

FIX collaterale: regime_fetcher/regime_lab scrivevano DVOL/funding/feature in data/raw/ ->
inquinavano la discovery asset del backtest (rompeva il regression-lock PORT06). Spostati in
data/regime/ (gitignored). Suite: 54 passed (lock incluso).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:08:15 +00:00

52 lines
2.3 KiB
Python

"""Loss-guard Hurst: le fade saltano i segnali in regime persistente/trending (rolling-Hurst >=
soglia), dove si concentrano stop-loss e perdite. Validato 2026-06-02: filtrare hurst>=0.55
DIMEZZA il DD del PORT06 alzando lo Sharpe. Filtro CAUSALE (close<=i), default off (None)."""
import numpy as np
import pandas as pd
from src.strategies.fade_base import hurst_skip_mask
def _df(close):
n = len(close)
return pd.DataFrame({"timestamp": range(n), "open": close, "high": close,
"low": close, "close": close, "volume": [1.0] * n})
def test_mask_off_when_none():
df = _df(np.cumsum(np.random.default_rng(0).normal(size=400)) + 100)
m = hurst_skip_mask(df, None)
assert m.dtype == bool and not m.any() # None -> nessuno skip
def test_mask_flags_persistent_regime():
# serie fortemente TRENDING (persistente, Hurst alto) -> deve essere mascherata (skip) molto
trend = np.linspace(100, 300, 600)
df = _df(trend)
m = hurst_skip_mask(df, hurst_max=0.55, window=100)
# dopo il warmup, una rampa pulita e' persistente -> gran parte mascherata
assert m[150:].mean() > 0.5
def test_fade_strategy_filters_signals():
"""Una fade con hurst_max produce <= segnali del baseline, e tutti i superstiti sono in
regime non-persistente (la maschera e' False alla loro barra)."""
import importlib
rng = np.random.default_rng(1)
# serie mean-reverting (anti-persistente) con qualche estensione -> genera fade
n = 1200
c = 100 + np.cumsum(rng.normal(scale=0.5, size=n))
c = 100 + (c - c.mean()) * 0.3 # comprimi verso la media (mean-revert)
df = _df(c)
ts = pd.to_datetime(df["timestamp"], unit="s", utc=True)
m = importlib.import_module("scripts.strategies.MR01_bollinger_fade")
Strat = next(v for k, v in vars(m).items()
if isinstance(v, type) and getattr(v, "__module__", "") == m.__name__
and hasattr(v, "generate_signals"))
s = Strat()
base = s.generate_signals(df, ts, bb_window=50, k=2.0, sl_atr=2.0)
filt = s.generate_signals(df, ts, bb_window=50, k=2.0, sl_atr=2.0, hurst_max=0.55)
assert len(filt) <= len(base) # il filtro non aggiunge mai segnali
skip = hurst_skip_mask(df, 0.55, 100)
assert all(not skip[sig.idx] for sig in filt) # nessun superstite in regime persistente