diff --git a/scripts/analysis/fractal_argo_workflow.js b/scripts/analysis/fractal_argo_workflow.js new file mode 100644 index 0000000..8e0ed6c --- /dev/null +++ b/scripts/analysis/fractal_argo_workflow.js @@ -0,0 +1,175 @@ +export const meta = { + name: 'fractal-argo-search', + description: 'Ricerca a ~100 agenti: strategia FRATTALI del segnale x REGIME ARGO (DVOL/funding/VRP) validata OOS', + phases: [ + { title: 'Search', detail: '92 agenti: griglia frattale x regime x asset + wildcard' }, + { title: 'Verify', detail: 'verifica avversariale dei survivor (look-ahead, fee 0.2%, altro asset/split)' }, + { title: 'Synth', detail: 'classifica, sceglie vincitori, propone implementazione' }, + ], +} + +const API = ` +=== regime_lab API (gia pronta, dati FRESCHI in cache) === +from scripts.analysis.regime_lab import load_features, report +from scripts.analysis.explore_lab import robust, atr, ema, rsi + +df = load_features(ASSET, TF) # ASSET in {BTC,ETH}, TF in {1h,4h,1d} +# Colonne (tutte CAUSALI, valore a barra i usa solo dati <= i): +# OHLCV: open high low close volume timestamp +# REGIME (ARGO-proxy backtestabile): dvol, dvol_pct (percentile rolling 0..1), +# rv (realized vol ann.), vrp = dvol-rv (>0 = vol sopravvalutata ~ ARGO GEX+ range), +# funding, funding_z (z-score rolling), dvol_chg (DVOL salita/discesa, proxy term-structure) +# FRATTALI: hurst (>0.5 persistente/trend, <0.5 anti-persistente/mean-rev), higuchi (FD: alta=frastagliato), +# vratio (vol breve/lunga), frac_up/frac_dn (Williams pivot bool: swing high/low confermati, CAUSALI) +# NB: dvol e' NaN prima del 2021-03 (storico DVOL) -> salta le barre con dvol NaN se usi il regime. + +# Costruisci 'entries': lista dict {i, d(+1/-1), tp, sl, max_bars}. INGRESSO ESEGUIBILE: +# i, d, tp, sl decisi con dati <= close[i]. tp/sl in PREZZO (o None). Esempio fade: +ent=[] +c=df['close'].values; a=atr(df,14); ma=df['close'].rolling(50).mean().values; sd=df['close'].rolling(50).std().values +for i in range(300, len(c)-1): + if np.isnan(sd[i]) or np.isnan(df['dvol_pct'].iloc[i]): continue + if df['vrp'].iloc[i] > 0 and c[i] < ma[i]-2.5*sd[i]: # GATE regime + SEGNALE frattale/tecnico + ent.append({'i':i,'d':1,'tp':ma[i],'sl':c[i]-2*a[i],'max_bars':24}) +res = report('NOME', ent, df) # -> {full:{ret,sharpe,dd,trades,win,exposure}, oos:{...}, sweep, sweep_oos, pos_yrs, n_yrs} +ok = robust(res) # True = full+oos>0 E regge fee 0.2% RT E anni ~tutti positivi +print('ROBUST', ok, 'trd', res['full']['trades'], 'OOSsharpe', round(res['oos']['sharpe'],2), + 'OOSret', round(res['oos']['ret']), 'fee02OOS', round(res['sweep_oos'][0.002])) +` + +const CONTEXT = ` +PROGETTO PythagorasGoal: trading crypto BTC/ETH. Edge dimostrato = SOLO mean-reversion (fade) + pairs. +ASTICELLA ALTA: il portafoglio PORT06 e' gia a Sharpe OOS 8.19 / DD 2.3%. Una strategia nuova vale solo +se ha edge NETTO validato OOS e robusto. + +PRIORI ONESTI (non ignorarli): i FRATTALI sono stati gia esplorati e quasi tutti RUMORE (shape_lab: +analog kNN solo BTC-overfit; PIP/pivot 0/48 robuste; DTW peggiora). Le OPZIONI sono state SCARTATE +(W18/19/21 VRP). L'unico edge frattale validato e SH01 (shape-ML logit, diversificatore). MA: la +combinazione FRATTALE-del-segnale x REGIME-ARGO (gating su DVOL/funding/VRP) e' NUOVA e non testata -> +e' qui che potrebbe esserci valore: il regime puo dire QUANDO il segnale frattale funziona. + +OBIETTIVO: trovare una strategia che combini un SEGNALE FRATTALE con un GATE/INTERAZIONE DI REGIME +(ARGO-proxy: DVOL percentile, VRP, funding) e che superi la validazione onesta (robust()=True). + +METODOLOGIA OBBLIGATORIA: ingresso ESEGUIBILE senza look-ahead (le colonne regime_lab sono gia causali; +le TUE entries devono usare solo dati <= i). Backtest NETTO fee (report() fa gia sweep 0.0-0.2% RT + OOS +ultimo 30%). robust()=True e' il gate minimo. Diffida dell'overfit: poche entries o edge solo full e +non-oos = rumore. Riporta ONESTAMENTE anche i fallimenti. + +` + API + +const SCHEMA = { + type: 'object', + properties: { + strategy: { type: 'string', description: 'nome + 1 frase: segnale frattale + gate regime' }, + family: { type: 'string' }, angle: { type: 'string' }, asset: { type: 'string' }, tf: { type: 'string' }, + trades: { type: 'integer' }, + full_ret: { type: 'number' }, oos_ret: { type: 'number' }, + full_sharpe: { type: 'number' }, oos_sharpe: { type: 'number' }, oos_dd: { type: 'number' }, + fee02_oos_ret: { type: 'number', description: 'OOS ret a fee 0.2% RT' }, + robust: { type: 'boolean', description: 'robust()=True' }, + promising: { type: 'boolean', description: 'vale una verifica avversariale (robust o quasi, non overfit)' }, + edge_desc: { type: 'string', description: 'perche funziona / perche e rumore, con i numeri' }, + }, + required: ['strategy', 'asset', 'tf', 'trades', 'oos_sharpe', 'robust', 'promising', 'edge_desc'], +} + +const FAMILIES = [ + ['hurst', 'Hurst regime: fade quando hurst<0.5 (anti-persistente), o trend quando hurst>0.5. Soglia hurst come segnale o gate.'], + ['higuchi', 'Fractal dimension Higuchi: FD alta = frastagliato/range (fade), FD bassa = liscio/trend (momentum).'], + ['williams', 'Williams pivot (frac_up/frac_dn, causali): fade del pivot (reversione allo swing) o breakout del pivot.'], + ['vratio', 'volatility_ratio: >1 espansione vol (breakout/fade del breakout), <1 compressione (range/squeeze).'], + ['analog', 'analog kNN sulla FORMA (puoi usare scripts.analysis.shape_lab.analog_signals(df,...)): forecast causale segno a H barre, gatealo col regime.'], + ['multiscale', 'multi-scala: combina hurst+higuchi+vratio in un indice di "regime frattale" (trend vs chop) come segnale.'], + ['candle', 'pattern candele frattali (src.fractal.patterns: extract_body_ratios/shadow, find_patterns): sequenze multi-barra come segnale.'], +] +const ANGLES = [ + ['none', 'NESSUN gate regime: segnale frattale puro (baseline per misurare il valore marginale del regime).'], + ['dvol_high', 'agisci solo con dvol_pct alto (>0.6..0.8): vol elevata (spesso mean-reversion piu forte).'], + ['dvol_low', 'agisci solo con dvol_pct basso (<0.3..0.4): calma/range.'], + ['vrp', 'VRP=vrp colonna: VRP>0 (vol sopravvalutata, analogo ARGO GEX+ -> range/fade); confronta con VRP<0. Gate o peso.'], + ['funding', 'funding_z estremo: troppi long (funding_z alto) -> fade ribassista; troppi short -> fade rialzista (flusso ARGO via perp).'], + ['dvol_chg', 'dvol_chg: DVOL in salita (espansione vol/stress -> trend) vs discesa (ritorno calma -> range).'], +] +const ASSETS = ['BTC', 'ETH'] + +phase('Search') +// 7 famiglie x 6 angoli x 2 asset = 84 agenti griglia +const gridSpecs = [] +for (const [fam, fdesc] of FAMILIES) + for (const [ang, adesc] of ANGLES) + for (const asset of ASSETS) + gridSpecs.push({ fam, fdesc, ang, adesc, asset }) + +const gridTasks = gridSpecs.map((s) => () => agent( + CONTEXT + + `\n\nIL TUO CELLA:\n- FAMIGLIA FRATTALE: ${s.fam} -> ${s.fdesc}\n- ANGOLO REGIME: ${s.ang} -> ${s.adesc}\n- ASSET: ${s.asset}\n\n` + + `Progetta la MIGLIORE strategia in questa cella: un SEGNALE basato sulla famiglia frattale ${s.fam}, ` + + `condizionato/interagito col regime ${s.ang}. Scrivi uno script in /tmp (cd /opt/docker/PythagorasGoal && ` + + `uv run python /tmp/.py), prova SIA TF=1h SIA TF=1d (e se vuoi 4h), itera 2-4 varianti di soglia/` + + `direzione/exit, e RIPORTA la migliore (quella con oos_sharpe piu alto e robust se possibile). Usa report()+robust(). ` + + `Privilegia mean-reversion (l'edge del progetto) ma testa anche momentum dove il regime lo motiva. ` + + `Mai look-ahead. Se tutto e rumore, dillo onestamente (promising=false). Ritorna lo schema.`, + { label: `srch:${s.fam}/${s.ang}/${s.asset}`, phase: 'Search', schema: SCHEMA })) + +// 8 wildcard: mandato aperto +const wildTasks = Array.from({ length: 8 }, (_, k) => () => agent( + CONTEXT + + `\n\nSEI UN AGENTE WILDCARD #${k + 1}. Mandato APERTO: inventa una combinazione FRATTALE-del-segnale x ` + + `REGIME-ARGO NON banale e non nella griglia ovvia. Idee: interazione hurst*vrp (mean-rev solo se ` + + `anti-persistente E vol sopravvalutata); Williams pivot come TP/SL adattivo gateato da dvol; analog kNN ` + + `pesato per funding; size/exit modulati dal regime; combinare 2 segnali frattali con conferma di regime. ` + + `Asset e TF a tua scelta (prova entrambi gli asset). Costruisci, testa onesto (report()+robust()), riporta ` + + `la migliore. Diversifica dagli altri: varia idea in base a #${k + 1}. Schema in output.`, + { label: `wild:${k + 1}`, phase: 'Search', schema: SCHEMA })) + +const searchResults = (await parallel([...gridTasks, ...wildTasks])).filter(Boolean) + +// survivor = robust, oppure promising con oos_sharpe alto e abbastanza trade +const survivors = searchResults.filter(r => + (r.robust || (r.promising && (r.oos_sharpe || 0) >= 1.0)) && (r.trades || 0) >= 30) +log(`Search: ${searchResults.length} testati, ${survivors.length} survivor da verificare`) + +phase('Verify') +const VSCHEMA = { + type: 'object', + properties: { + strategy: { type: 'string' }, confirmed: { type: 'boolean' }, + reason: { type: 'string', description: 'esito audit look-ahead + fee0.2% + altro asset + split alternativo' }, + oos_sharpe_recheck: { type: 'number' }, killed_by: { type: 'string' }, + }, + required: ['strategy', 'confirmed', 'reason'], +} +let verified = [] +if (survivors.length) { + verified = (await parallel(survivors.map(s => () => agent( + CONTEXT + + `\n\nVERIFICA AVVERSARIALE di un candidato survivor:\n${JSON.stringify(s, null, 1)}\n\n` + + `Tuo compito: PROVARE A FALSIFICARLO. (1) Ricostruisci la strategia (chiedi i dettagli dal suo edge_desc; ` + + `riusa regime_lab). (2) AUDIT look-ahead: ogni colonna/calcolo usa solo dati <= i? Il gate regime e' noto a i? ` + + `(3) Regge fee 0.2% RT in OOS? (4) Regge sull'ALTRO asset (se BTC prova ETH e viceversa)? (5) Regge a uno SPLIT ` + + `OOS alternativo (es. train<=2024, test 2025-26)? (6) Numero trade sufficiente e non concentrato in 1 anno? ` + + `Default a confirmed=FALSE se incerto o se sopravvive solo per overfit. Sii spietato. Schema in output.`, + { label: `verify:${(s.strategy || '').slice(0, 24)}`, phase: 'Verify', schema: VSCHEMA })))).filter(Boolean) +} +const confirmed = verified.filter(v => v.confirmed) + +phase('Synth') +const synthesis = await agent( + CONTEXT + + `\n\nHai i risultati di ${searchResults.length} agenti di ricerca e ${verified.length} verifiche avversariali.\n\n` + + `SURVIVOR CONFERMATI:\n${JSON.stringify(confirmed, null, 1)}\n\n` + + `TUTTI I SURVIVOR (anche non confermati):\n${JSON.stringify(survivors, null, 1)}\n\n` + + `TOP 15 per oos_sharpe fra tutti i testati:\n${JSON.stringify( + searchResults.slice().sort((a, b) => (b.oos_sharpe || 0) - (a.oos_sharpe || 0)).slice(0, 15), null, 1)}\n\n` + + `Produci la SINTESI FINALE (italiano) per il decisore: + 1) VERDETTO: esiste una strategia frattale x ARGO con edge validato OOS? quale/i (confermate)? + 2) Tabella dei top candidati: strategia, asset/tf, OOS Sharpe, OOS ret, DD, robust, confermato? + 3) Il regime ARGO (DVOL/VRP/funding) AGGIUNGE valore al segnale frattale (vs angolo 'none')? In quali celle? + 4) Cosa e' rumore e perche (coerente coi priori: frattali deboli, opzioni scartate). + 5) Se c'e un vincitore: piano di implementazione (file in scripts/strategies/, MODULE_MAP, validazione finale). + Se NON c'e: dillo chiaro, niente forzature. + Cita NUMERI reali (OOS Sharpe, ret, trades). Onesta brutale: deve battere PORT06, non solo essere >0.`, + { label: 'synthesis', phase: 'Synth' }) + +return { searchResults, survivors, confirmed, synthesis } diff --git a/scripts/analysis/regime_fetcher.py b/scripts/analysis/regime_fetcher.py new file mode 100644 index 0000000..5268f7e --- /dev/null +++ b/scripts/analysis/regime_fetcher.py @@ -0,0 +1,112 @@ +"""Fetch dati REGIME backtestabili da Deribit MAINNET (public, no-auth) -> parquet. + +Abilita la ricerca strategie frattali x regime (ARGO-proxy). Salva in data/raw/: + {btc,eth}_dvol.parquet : DVOL index 1h (IV 30d "VIX crypto"), storico ~2021->oggi + {btc,eth}_funding.parquet : funding rate perp 1h, storico ~2019->oggi + +Solo componenti ARGO con STORICO GRATUITO (DVOL, funding) -> validabili OOS. Il GEX +per-strike resta snapshot-only (vedi analisi 2026-06-01). Run: + uv run python scripts/analysis/regime_fetcher.py +""" +from __future__ import annotations + +import time +import urllib.request +import urllib.parse +import json +from pathlib import Path + +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +BASE = "https://www.deribit.com/api/v2/public/" + + +def _get(method: str, params: dict) -> dict: + url = BASE + method + "?" + urllib.parse.urlencode(params) + for _ in range(4): + try: + with urllib.request.urlopen(url, timeout=30) as r: + return json.loads(r.read()) + except Exception: + time.sleep(1.0) + return {} + + +def fetch_dvol(currency: str, start_ms: int, end_ms: int, res: int = 3600) -> pd.DataFrame: + """DVOL index (OHLC). Cap 1000 righe/chiamata -> chaining all'indietro.""" + rows = [] + cur_end = end_ms + span = 1000 * res * 1000 + while cur_end > start_ms: + cur_start = max(start_ms, cur_end - span) + d = _get("get_volatility_index_data", { + "currency": currency, "start_timestamp": cur_start, + "end_timestamp": cur_end, "resolution": res}) + data = (d.get("result") or {}).get("data") or [] + if not data: + break + rows.extend(data) + oldest = min(x[0] for x in data) + if oldest >= cur_end: + break + cur_end = oldest - 1 + time.sleep(0.15) + if not rows: + return pd.DataFrame() + df = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close"]) + df = df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True) + df["dvol"] = df["close"] + return df + + +def fetch_funding(instrument: str, start_ms: int, end_ms: int) -> pd.DataFrame: + """funding rate history perp (1h). Paginazione ~30g/chiamata.""" + rows = [] + cur_start = start_ms + step = 30 * 24 * 3600 * 1000 + while cur_start < end_ms: + cur_end = min(end_ms, cur_start + step) + d = _get("get_funding_rate_history", { + "instrument_name": instrument, + "start_timestamp": cur_start, "end_timestamp": cur_end}) + data = d.get("result") or [] + if data: + rows.extend(data) + cur_start = cur_end + 1 + time.sleep(0.12) + if not rows: + return pd.DataFrame() + df = pd.DataFrame(rows) + ts_col = "timestamp" if "timestamp" in df.columns else df.columns[0] + df = df.rename(columns={ts_col: "timestamp"}) + keep = [c for c in ("timestamp", "interest_1h", "interest_8h", "index_price", "prev_index_price") if c in df.columns] + df = df[keep].drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True) + return df + + +def main(): + RAW.mkdir(parents=True, exist_ok=True) + now = _get("get_time", {}) + end_ms = int(now.get("result", 0)) or int(time.time() * 1000) + start_ms = end_ms - int(6.5 * 365 * 24 * 3600 * 1000) # ~6.5 anni + for cur, inst in (("BTC", "BTC-PERPETUAL"), ("ETH", "ETH-PERPETUAL")): + dv = fetch_dvol(cur, start_ms, end_ms) + if not dv.empty: + p = RAW / f"{cur.lower()}_dvol.parquet" + dv.to_parquet(p) + rng = (pd.to_datetime(dv['timestamp'].min(), unit='ms').date(), + pd.to_datetime(dv['timestamp'].max(), unit='ms').date()) + print(f" {cur} DVOL: {len(dv)} righe {rng[0]}->{rng[1]} (ora={dv['dvol'].iloc[-1]:.1f}) -> {p.name}") + fr = fetch_funding(inst, start_ms, end_ms) + if not fr.empty: + p = RAW / f"{cur.lower()}_funding.parquet" + fr.to_parquet(p) + rng = (pd.to_datetime(fr['timestamp'].min(), unit='ms').date(), + pd.to_datetime(fr['timestamp'].max(), unit='ms').date()) + print(f" {cur} FUNDING: {len(fr)} righe {rng[0]}->{rng[1]} -> {p.name}") + + +if __name__ == "__main__": + main() diff --git a/scripts/analysis/regime_lab.py b/scripts/analysis/regime_lab.py new file mode 100644 index 0000000..40a2257 --- /dev/null +++ b/scripts/analysis/regime_lab.py @@ -0,0 +1,191 @@ +"""regime_lab — API condivisa per la ricerca strategie FRATTALI x REGIME (ARGO-proxy). + +Allinea prezzo (OHLCV) + DVOL + funding in modo CAUSALE (no look-ahead: il valore di +regime alla barra i usa solo dati <= timestamp[i]) ed espone: + - feature REGIME (ARGO-proxy backtestabili): dvol, dvol_pct (percentile rolling), + rv (realized vol), vrp = dvol - rv, funding, funding_z, dvol_chg (proxy term-structure). + - feature FRATTALI (src/fractal): rolling_hurst, higuchi, self_similarity, volatility_ratio, + williams fractals (pivot), candle encoding. + - validazione: report(name, entries, df) -> full/oos netto-fee + robustezza griglia/fee, + riusando l'engine onesto di explore_lab (simulate/evaluate). + +Convenzione entries (come explore_lab): lista di dict {i, d (+1/-1), tp, sl, max_bars}. +Ingresso ESEGUIBILE: i, d, tp, sl decisi con dati <= close[i]. + +Uso tipico in un agente: + from scripts.analysis.regime_lab import load, report, regime_features, frac_features + df = load("BTC", "1h") # OHLCV + colonne regime allineate + R = regime_features(df); F = frac_features(df) + entries = [...] # la tua logica + print(report("MIA_STRATEGIA", entries, df)) +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)) + +from scripts.analysis.explore_lab import get_df, simulate, evaluate, atr, ema, rsi # noqa: E402 +from src.fractal.indicators import ( # noqa: E402 + rolling_hurst, fractal_dimension_higuchi, self_similarity_score, volatility_ratio, +) + +RAW = ROOT / "data" / "raw" + + +# --------------------------------------------------------------------------- dati +def _load_regime_series(asset: str) -> tuple[pd.DataFrame, pd.DataFrame]: + a = asset.lower() + dvol = pd.read_parquet(RAW / f"{a}_dvol.parquet") if (RAW / f"{a}_dvol.parquet").exists() else pd.DataFrame() + fund = pd.read_parquet(RAW / f"{a}_funding.parquet") if (RAW / f"{a}_funding.parquet").exists() else pd.DataFrame() + return dvol, fund + + +def load(asset: str, tf: str) -> pd.DataFrame: + """OHLCV (explore_lab.get_df) + colonne regime allineate CAUSALMENTE (merge_asof backward). + Ogni barra prezzo riceve l'ultimo DVOL/funding con timestamp <= timestamp barra.""" + df = get_df(asset, tf).copy() + df["timestamp"] = df["timestamp"].astype("int64") + dvol, fund = _load_regime_series(asset) + if not dvol.empty: + d = dvol[["timestamp", "dvol"]].astype({"timestamp": "int64"}).sort_values("timestamp") + df = pd.merge_asof(df.sort_values("timestamp"), d, on="timestamp", direction="backward") + else: + df["dvol"] = np.nan + if not fund.empty: + col = "interest_1h" if "interest_1h" in fund.columns else fund.columns[1] + f = fund[["timestamp", col]].astype({"timestamp": "int64"}).rename(columns={col: "funding"}).sort_values("timestamp") + df = pd.merge_asof(df.sort_values("timestamp"), f, on="timestamp", direction="backward") + else: + df["funding"] = np.nan + return df.reset_index(drop=True) + + +# ---------------------------------------------------------------- feature REGIME +def _rolling_pct(x: np.ndarray, win: int) -> np.ndarray: + """Percentile rolling CAUSALE: rank di x[i] nella finestra [i-win, i] (solo passato).""" + s = pd.Series(x) + return s.rolling(win, min_periods=max(20, win // 4)).apply( + lambda w: (w.iloc[-1] >= w).mean(), raw=False).values + + +def regime_features(df: pd.DataFrame, pct_win: int = 252, rv_win: int = 24, fund_win: int = 168) -> dict: + """Tutte causali. dvol_pct/funding_z usano solo finestra passata. vrp = dvol - rv annualizz.""" + c = df["close"].values.astype(float) + dvol = df["dvol"].values.astype(float) + fund = df["funding"].values.astype(float) + ret = np.zeros_like(c); ret[1:] = np.diff(np.log(c)) + # realized vol annualizzata (in punti %, scala come DVOL): std rolling * sqrt(barre/anno) + bars_per_year = {1: 24 * 365}.get(1, 24 * 365) # default 1h; per tf diversi e' un proxy + rv = pd.Series(ret).rolling(rv_win).std().values * np.sqrt(24 * 365) * 100 + dvol_pct = _rolling_pct(dvol, pct_win) + fmean = pd.Series(fund).rolling(fund_win).mean().values + fstd = pd.Series(fund).rolling(fund_win).std().values + funding_z = (fund - fmean) / np.where(fstd == 0, np.nan, fstd) + dvol_chg = pd.Series(dvol).diff(rv_win).values # proxy term-structure (DVOL in salita/discesa) + return { + "dvol": dvol, "dvol_pct": dvol_pct, "rv": rv, "vrp": dvol - rv, + "funding": fund, "funding_z": funding_z, "dvol_chg": dvol_chg, + } + + +# --------------------------------------------------------------- feature FRATTALI +def williams_fractals(df: pd.DataFrame, k: int = 2) -> tuple[np.ndarray, np.ndarray]: + """Pivot di Bill Williams: frac_up[i]=high[i] e' il max delle 2k+1 barre centrate (causale a i+k). + Ritorna due array bool (up=swing high confermato, dn=swing low). Confermati con ritardo k.""" + h, l = df["high"].values, df["low"].values + n = len(h) + up = np.zeros(n, bool); dn = np.zeros(n, bool) + for i in range(k, n - k): + if h[i] == max(h[i - k:i + k + 1]): + up[i] = True + if l[i] == min(l[i - k:i + k + 1]): + dn[i] = True + return up, dn + + +def frac_features(df: pd.DataFrame, hurst_win: int = 100, higuchi_win: int = 64, + step: int = 1) -> dict: + """Feature frattali rolling, CAUSALI (finestra passata che termina a i). step>1: calcola + ogni `step` barre e fa forward-fill (i frattali variano lentamente) -> molto piu' veloce.""" + c = df["close"].values.astype(float) + n = len(c) + hurst = rolling_hurst(c, window=hurst_win, step=step) # gia' causale + stepped (src/fractal) + vratio = np.full(n, np.nan) + higuchi = np.full(n, np.nan) + last_hi = last_vr = np.nan + for i in range(higuchi_win, n): + if (i - higuchi_win) % step == 0: + last_hi = fractal_dimension_higuchi(c[i - higuchi_win:i]) + last_vr = volatility_ratio(c[max(0, i - 60):i]) + higuchi[i] = last_hi + vratio[i] = last_vr + up, dn = williams_fractals(df) + return {"hurst": hurst, "higuchi": higuchi, "vratio": vratio, + "frac_up": up, "frac_dn": dn} + + +# ------------------------------------------------------------------------- cache +_FEATCOLS_R = ("dvol", "dvol_pct", "rv", "vrp", "funding", "funding_z", "dvol_chg") +_FEATCOLS_F = ("hurst", "higuchi", "vratio", "frac_up", "frac_dn") + + +def _cache_path(asset: str, tf: str) -> Path: + return RAW / f"features_{asset.lower()}_{tf}.parquet" + + +def build_cache(asset: str, tf: str, frac_step: int = 6) -> pd.DataFrame: + """Precompute OHLCV + regime + frattali -> parquet condiviso (per i 100 agenti).""" + df = load(asset, tf) + R = regime_features(df) + F = frac_features(df, step=frac_step) + for k in _FEATCOLS_R: + df[k] = R[k] + for k in _FEATCOLS_F: + df[k] = F[k] + p = _cache_path(asset, tf) + df.to_parquet(p) + return df + + +def load_features(asset: str, tf: str) -> pd.DataFrame: + """Carica la cache feature (la costruisce se manca). OHLCV + tutte le colonne regime+frattali.""" + p = _cache_path(asset, tf) + if p.exists(): + return pd.read_parquet(p) + return build_cache(asset, tf) + + +# ------------------------------------------------------------------- validazione +def report(name: str, entries: list[dict], df: pd.DataFrame, asset: str = "", tf: str = "") -> dict: + """Netto-fee full + OOS (ultimo 30%) + sweep fee, via engine onesto di explore_lab. + Ritorna dict compatto: trades, full/oos (ret%, sharpe, dd, acc), robust (OK su tutte le fee).""" + if not entries: + return {"name": name, "trades": 0, "verdict": False, "note": "no entries"} + ev = evaluate(name, entries, df) # full + oos + fee sweep + return ev + + +if __name__ == "__main__": + # smoke: una fade Bollinger gateata dal regime (DVOL alto) come esempio d'uso + df = load("BTC", "1h") + R = regime_features(df); F = frac_features(df) + c = df["close"].values + ma = pd.Series(c).rolling(50).mean().values + sd = pd.Series(c).rolling(50).std().values + a = atr(df, 14) + ent = [] + for i in range(300, len(c) - 1): + if np.isnan(sd[i]) or np.isnan(R["dvol_pct"][i]): + continue + if R["dvol_pct"][i] < 0.6: # gate: solo regime DVOL alto + continue + if c[i] < ma[i] - 2.5 * sd[i]: # fade banda bassa + ent.append({"i": i, "d": 1, "tp": ma[i], "sl": c[i] - 2 * a[i], "max_bars": 24}) + print(f"smoke BTC 1h fade|DVOL>p60: {len(ent)} entries") + print(report("SMOKE", ent, df))