diff --git a/docs/diary/2026-06-02-fractal-argo-search.md b/docs/diary/2026-06-02-fractal-argo-search.md new file mode 100644 index 0000000..8e6d917 --- /dev/null +++ b/docs/diary/2026-06-02-fractal-argo-search.md @@ -0,0 +1,75 @@ +# 2026-06-02 — Ricerca a 100 agenti: Frattali del segnale × Regime ARGO + +> Workflow multi-agente (171 agenti, 8,4M token, ~7h) per cercare una strategia che combini +> un SEGNALE FRATTALE con un GATE/INTERAZIONE DI REGIME ARGO (DVOL/funding/VRP), validata OOS. +> Branch: `feat/fractal-argo-search`. Substrato e codice sul branch, niente impatto su main/live. + +## Substrato costruito +- `regime_fetcher.py`: DVOL (2021-03→oggi) + funding (2019→oggi) BTC/ETH da **Deribit mainnet + public** (no-auth, OI/IV reali — non il testnet farlocco di Cerbero). +- `regime_lab.py`: allineamento regime↔prezzo **causale no-look-ahead** (merge_asof backward), + feature regime (dvol_pct, **vrp=dvol−rv**, funding_z, dvol_chg) + frattali (rolling Hurst, + Higuchi FD, vol-ratio, Williams pivot), cache feature precalcolate, validazione netto-fee OOS + via `explore_lab`. Bug corretto in corsa: `vrp` annualizzava la realized-vol sempre come 1h → + rotta su 4h/1d (sempre negativa); fix per timeframe. +- `fractal_argo_workflow.js`: 84 agenti griglia (7 famiglie frattali × 6 angoli regime × BTC/ETH) + + 8 wildcard + verifica avversariale dei survivor + sintesi. + +## Verdetto + +**Esistono edge frattale×regime reali, causali, robusti** (15 confermati dalla batteria +avversariale: audit look-ahead bit-esatto, cross-asset, split alternativo, fee 0.2% RT, plateau). +**MA nessuno batte PORT06 standalone** (OOS Sharpe 8,19 / DD 2,3%): sono **diversificatori a +bassa esposizione** (1,5-8%, ~100-460 trade), profilo SH01/pairs. + +### Top candidati confermati +| Strategia | Asset/TF | OOS Sharpe | OOS DD | trade | +|-----------|----------|:--:|:--:|:--:| +| FRAC-VRP multiscala (chop × VRP<0) | ETH 1h | 5,55 | 11% | 184 | +| HigVRP-Fade (Higuchi alto × VRP<0) | BTC 1h | 4,55 | 7,2% | 286 | +| **HurstCalmFade (hurst<0,55 × dvol<0,4)** | BTC 1h | **3,73** | 5,1% | 198 | +| WILD8 Pivot-Hurst | BTC 4h | 3,87 | 10% | 482 | +| AnalogFundingFade (kNN forma × funding) | ETH 4h | 2,15 | 9,3% | 229 | + +## Il finding chiave (controintuitivo): il prior ARGO è SMENTITO + +La tesi naïve **"VRP>0 = GEX+ = range = fade" FALLISCE** sistematicamente. L'edge robusto e +ripetuto è su **VRP<0** (vol implicita *sottoprezzata* vs realizzata → mean-reversion whippy) e su +**DVOL bassa**, l'opposto del brief. Gate invertito VRP>0 → Sharpe −2,08/−1,30 su entrambi gli +asset. È il risultato più solido di tutta la ricerca, look-ahead-clean (lag-1/3/6 robusto). + +## Cosa aggiunge valore vs cosa è decorativo +- **Load-bearing (confermato per ablazione):** VRP<0; hurst-low × dvol-low (HurstCalmFade taglia + DD OOS 17%→5%); funding_z estremo |fz|≥1,8 (analog ETH 4h). +- **Decorativo (DD-reducer, non interazione):** dvol_chg, dvol_high/low e funding come gate spesso + riducono solo esposizione senza migliorare il segno. + +## Cosa è RUMORE (conferma i priori del progetto) +- Frattali da soli (angolo=none): hurst/Higuchi/vratio/chop/candle/analog intraday → non robusti + (DD 30-90%, muoiono di fee). Conferma `shape_lab`. +- Momentum/breakout gateato (hurst>0,6, dvol-rising): catastrofico (Sharpe −2…−7) → riconferma + dominanza mean-reversion, i breakout rientrano. +- ARGO-GEX nella direzione attesa (VRP>0): perde. Coerente con W18/19/21 scartate. +- Pivot-fade non-laggato (frac_up[i]): artefatto squeeze-like, va sempre laggato a i−2. + +## Vincitore selezionato + test decisivo + +**FR01 HurstCalmFade BTC 1h** (`scripts/strategies/FR01_hurst_calm_fade.py`): il più verificato, +DD più basso (5,1% OOS), generalizza a ETH. **Test di correlazione decisivo** (la domanda che +conta: aggiungerlo migliora il PORT06 o è ridondante?): correlazione daily-returns coi fade +esistenti **MR01 +0,17 / MR02 +0,08 / MR07 −0,03** → **BASSA, quasi-ortogonale**, NON ridondante. +Passa il gate → vale l'inserimento come diversificatore. + +## Onestà finale +L'edge frattale×regime è **reale, causale, robusto** ma è sempre **mean-reversion già nota +condizionata dal regime (VRP<0 / hurst-low / dvol-low)**, non un motore ortogonale nuovo. Valore = +**riduzione DD aggregato come sleeve a bassa esposizione**. La correlazione bassa lo qualifica come +diversificatore reale. + +## Resta da fare prima del deploy (NON ancora superato) +1. Verifica di correlazione formale sull'**intero MASTER** (`combine_v2.py`), non solo vs le 3 + fade BTC — confermare che migliora Sharpe/DD OOS del PORT06. +2. **Wiring DVOL live**: il gate dipende da dvol_pct; il backtest usa la cache `regime_lab`, ma il + runner live non ha un feed DVOL. Va costruito (regime_fetcher in produzione + allineamento + causale) prima di tradare FR01 live. +3. Walk-forward per-finestra + replay worker == backtest + exit intrabar via StrategyWorker. diff --git a/scripts/analysis/fractal_argo_peryear.py b/scripts/analysis/fractal_argo_peryear.py new file mode 100644 index 0000000..ea44e46 --- /dev/null +++ b/scripts/analysis/fractal_argo_peryear.py @@ -0,0 +1,86 @@ +import sys; sys.path.insert(0,".") +import numpy as np, pandas as pd +from scripts.analysis.regime_lab import load_features +from scripts.analysis.explore_lab import atr + +FEE=0.001; LEV=3 + +def build(df, gate, k=2.5, sl_atr=2.0, mb=24, bb=50): + c=df['close'].values; a=atr(df,14) + ma=pd.Series(c).rolling(bb).mean().values; sd=pd.Series(c).rolling(bb).std().values + ent=[] + for i in range(bb+14,len(c)-1): + if np.isnan(sd[i]) or sd[i]==0 or np.isnan(a[i]): continue + if not gate(df,i): continue + if c[i]ma[i]+k*sd[i]: d,sl=-1,c[i]+sl_atr*a[i] + else: continue + ent.append({'i':i,'d':d,'tp':ma[i],'sl':sl,'mb':mb}) + return ent + +def per_year(df, ent): + """replay intrabar fedele (sl-first, tp, poi max_bars@close) -> per anno {n,ret%,win%}.""" + h=df['high'].values; l=df['low'].values; c=df['close'].values + ts=pd.to_datetime(df['timestamp'],unit='ms',utc=True) + Y={} + last=-1 + for e in ent: + i=e['i'] + if i<=last: continue + d=e['d']; tp=e['tp']; sl=e['sl']; j=min(i+e['mb'],len(c)-1) + exit_p=c[j] + for t in range(i+1,j+1): + if d==1: + if l[t]<=sl: exit_p=sl; j=t; break + if h[t]>=tp: exit_p=tp; j=t; break + else: + if h[t]>=sl: exit_p=sl; j=t; break + if l[t]<=tp: exit_p=tp; j=t; break + ret=(exit_p-c[i])/c[i]*d*LEV - FEE*LEV + last=j; yr=ts.iloc[i].year + if yr not in Y: Y[yr]=[0,0.0,0] + Y[yr][0]+=1; Y[yr][1]+=ret*100; Y[yr][2]+= (ret>0) + return Y + +# gate functions +def g_hurst_calm(df,i): return df['hurst'].iloc[i]<0.55 and not np.isnan(df['dvol_pct'].iloc[i]) and df['dvol_pct'].iloc[i]<0.40 +def g_vrp_neg(df,i): return not np.isnan(df['vrp'].iloc[i]) and df['vrp'].iloc[i]<0 +def g_hig_vrp(df,i): + hi=df['higuchi'].iloc[i]; return (not np.isnan(hi)) and hi>1.5 and (not np.isnan(df['vrp'].iloc[i])) and df['vrp'].iloc[i]<0 +def g_none(df,i): return True + +STRATS=[("HurstCalmFade (hurst<.55 & DVOL1.5 & VRP<0)",g_hig_vrp), + ("Fade NUDA (no gate, baseline)",g_none)] + +# regime mercato BTC per anno (da BTC close annuale) +btc=load_features("BTC","1d") +bts=pd.to_datetime(btc['timestamp'],unit='ms',utc=True); bc=btc['close'].values +mkt={} +for yr in range(2021,2027): + m=(bts.dt.year==yr).values + if m.sum()>5: + r=(bc[m][-1]/bc[m][0]-1)*100 + mkt[yr]=("BULL" if r>40 else "BEAR" if r<-30 else "RANGE", r) + +for asset in ("BTC","ETH"): + df=load_features(asset,"1h") + print(f"\n{'='*78}\n {asset} 1h — performance per anno (somma ret% per-trade, netto leva3x+fee0.10%)\n{'='*78}") + print(f" {'Strategia':<40} " + " ".join(f"{y}" for y in range(2021,2027))) + for name,g in STRATS: + ent=build(df,g); Y=per_year(df,ent) + cells=[] + for y in range(2021,2027): + if y in Y and Y[y][0]>0: + cells.append(f"{Y[y][1]:+5.0f}") + else: cells.append(" . ") + print(f" {name:<40} " + " ".join(cells)) + # riga trades/anno per la strategia principale + ent=build(df,g_hurst_calm); Y=per_year(df,ent) + tr=" ".join(f"{Y.get(y,[0])[0]:>5}" for y in range(2021,2027)) + print(f" {'(HurstCalmFade trades/anno)':<40} {tr}") + +print(f"\n REGIME MERCATO BTC per anno (ret% annuale prezzo):") +for y in range(2021,2027): + if y in mkt: print(f" {y}: {mkt[y][0]:6} ({mkt[y][1]:+.0f}%)") diff --git a/scripts/strategies/FR01_hurst_calm_fade.py b/scripts/strategies/FR01_hurst_calm_fade.py new file mode 100644 index 0000000..5d27faa --- /dev/null +++ b/scripts/strategies/FR01_hurst_calm_fade.py @@ -0,0 +1,123 @@ +"""FR01 — Hurst-Calm Fade (FRATTALE x REGIME). Esito della ricerca a 100 agenti (2026-06-02). + +Fade della banda di Bollinger (k=2.5 su SMA50, TP=media, SL=2*ATR, max_bars=24) ATTIVATO +SOLO quando coincidono due condizioni di regime: + - FRATTALE: rolling Hurst < 0.55 (regime anti-persistente -> la mean-reversion ha senso + fratalmente; con Hurst>0.55 il fade peggiora, il momentum perde comunque). + - VOLATILITA: dvol_pct < 0.40 (DVOL nel terzile basso del suo storico -> regime calmo/range). + +Doppio gate frattale x regime: l'INTERAZIONE e' l'ingrediente attivo, non il fade di per se' +(ablation: senza gate Sharpe ~0.8 e muore a fee 0.2% RT; col doppio gate OOS Sharpe ~3.7). + +VALIDAZIONE (netto fee 0.10% RT, leva 3x, OOS ultimo 30%, ricerca fractal_argo_workflow): + BTC 1h: 198 trade, FULL +100% / OOS +54% / Sharpe OOS 3.73 / DD OOS 5.1% / 6/6 anni positivi, + regge fee 0.2% RT. Confermato avversarialmente (no look-ahead, split alternativo). + Generalizza a ETH 1h (Sharpe ~2.6, secondario). 4h/1d = rumore (pochi trade). + Correlazione coi fade esistenti BASSA: MR01 +0.17, MR02 +0.08, MR07 -0.03 -> DIVERSIFICATORE + quasi-ortogonale (profilo SH01/pairs), NON ridondante. Esposizione ~1-9% -> low-frequency. + +RUOLO: diversificatore a basso DD per il MASTER/PORT06, NON motore standalone (non batte il +portafoglio da solo). Coerente coi priori: i frattali da soli sono rumore; il valore e' nel +gating del regime. NB il prior ARGO "VRP>0=range=fade" e' SMENTITO: l'edge robusto e' su VRP<0 +e su DVOL bassa (questo gate dvol_pct<0.4), non su vol alta. + +DIPENDENZA REGIME (caveat deploy): il gate usa DVOL/dvol_pct. Per il BACKTEST le feature +arrivano da regime_lab (cache da Deribit mainnet). Per il LIVE serve un feed DVOL in produzione +(regime_fetcher + allineamento causale nel runner) -> wiring NON ancora fatto. Finche' manca, +FR01 e' validata-in-ricerca ma non deployabile live. + +Run backtest: uv run python scripts/strategies/FR01_hurst_calm_fade.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from src.strategies.base import Strategy, Signal # noqa: E402 +from src.fractal.indicators import rolling_hurst # noqa: E402 + + +def _atr(df: pd.DataFrame, n: int = 14) -> np.ndarray: + h, l, c = df["high"].values, df["low"].values, df["close"].values + pc = np.roll(c, 1); pc[0] = c[0] + tr = np.maximum(h - l, np.maximum(np.abs(h - pc), np.abs(l - pc))) + return pd.Series(tr).rolling(n).mean().values + + +class HurstCalmFade(Strategy): + name = "FR01_hurst_calm_fade" + description = "Fade Bollinger gateato da Hurst<0.55 (anti-persistente) + DVOL bassa (calm)" + default_assets = ["BTC", "ETH"] + default_timeframes = ["1h"] + fee_rt = 0.001 + leverage = 3.0 + position_size = 0.15 + initial_capital = 1000.0 + + def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex, **params) -> list[Signal]: + bb_w = params.get("bb_window", 50) + k = params.get("k", 2.5) + sl_atr = params.get("sl_atr", 2.0) + max_bars = params.get("max_bars", 24) + hurst_thr = params.get("hurst_thr", 0.55) + hurst_win = params.get("hurst_win", 100) + dvol_pct_thr = params.get("dvol_pct_thr", 0.40) + + c = df["close"].values.astype(float) + n = len(c) + ma = pd.Series(c).rolling(bb_w).mean().values + sd = pd.Series(c).rolling(bb_w).std().values + a = _atr(df, 14) + hurst = rolling_hurst(c, window=hurst_win) # causale (returns[i-win:i]) + # dvol_pct: dalla colonna se presente (regime_lab.load_features), altrimenti gate OFF + dvol_pct = df["dvol_pct"].values if "dvol_pct" in df.columns else np.full(n, np.nan) + + signals: list[Signal] = [] + for i in range(bb_w + 14, n): + if np.isnan(sd[i]) or np.isnan(a[i]) or sd[i] == 0: + continue + # GATE FRATTALE x REGIME (tutto noto a i) + if hurst[i] >= hurst_thr: + continue + if "dvol_pct" in df.columns: + if np.isnan(dvol_pct[i]) or dvol_pct[i] >= dvol_pct_thr: + continue + up, lo = ma[i] + k * sd[i], ma[i] - k * sd[i] + if c[i] < lo: + d, sl = 1, c[i] - sl_atr * a[i] + elif c[i] > up: + d, sl = -1, c[i] + sl_atr * a[i] + else: + continue + signals.append(Signal( + idx=i, direction=d, entry_price=float(c[i]), + metadata={"tp": float(ma[i]), "sl": float(sl), "max_bars": int(max_bars)}, + )) + return signals + + +if __name__ == "__main__": + # backtest via l'harness onesto + feature di regime_lab (DVOL reale) + from scripts.analysis.regime_lab import load_features, report + from scripts.analysis.explore_lab import robust + + strat = HurstCalmFade() + print(f"{'=' * 100}") + print(f" FR01 HURST-CALM FADE — netto fee {strat.fee_rt*100:.2f}% RT, leva {strat.leverage:.0f}x") + print(f" gate: hurst<0.55 (anti-persistente) + dvol_pct<0.40 (DVOL bassa)") + print(f"{'=' * 100}") + for asset in ("BTC", "ETH"): + df = load_features(asset, "1h") + ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) + sigs = strat.generate_signals(df, ts) + ent = [{"i": s.idx, "d": s.direction, "tp": s.metadata["tp"], + "sl": s.metadata["sl"], "max_bars": s.metadata["max_bars"]} for s in sigs] + res = report(f"FR01_{asset}_1h", ent, df) + print(f" -> {asset}: robust={robust(res)} OOS Sharpe={res['oos']['sharpe']:.2f} " + f"OOS ret={res['oos']['ret']:+.0f}% DD={res['full']['dd']:.0f}%")