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>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user