Merge feat/fractal-argo-search: ricerca 100 agenti frattali x ARGO (record)
Esito: 15 strategie frattale x regime robuste e causali, ma NESSUNA migliora PORT06 (FR01 lo diluisce: OOS Sharpe 8.89->8.72). Prior ARGO 'VRP>0=range' smentito, edge su VRP<0 + DVOL bassa. RECORD DI RICERCA: non deployato, infrastruttura regime riusabile. Artefatti: regime_fetcher/regime_lab (dati DVOL+funding Deribit mainnet, feature regime+frattali causali), fractal_argo_workflow.js (171 agenti), FR01_hurst_calm_fade.py (NON in MODULE_MAP/yml), analisi per-anno + test MASTER, diario 2026-06-02. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## TEST DECISIVO SUL MASTER — VERDETTO FINALE: NON deployare
|
||||||
|
|
||||||
|
Misurato il contributo marginale di FR01 al PORT06 intero (equal-weight, `master_corr`):
|
||||||
|
|
||||||
|
| Portafoglio | FULL Sharpe | OOS Sharpe | OOS DD | OOS ret |
|
||||||
|
|-------------|:--:|:--:|:--:|:--:|
|
||||||
|
| PORT06 (17 sleeve) | 6,62 | **8,89** | 1,2% | +175% |
|
||||||
|
| PORT06 + FR01 (19) | 6,55 | **8,72** | 1,1% | +156% |
|
||||||
|
|
||||||
|
**FR01 NON migliora il PORT06: lo DILUISCE** (OOS Sharpe 8,89→8,72, OOS ret +175%→+156%; DD
|
||||||
|
marginalmente meglio 1,2→1,1% ma a costo di Sharpe). Corr FR01 vs MASTER +0,18 (BTC)/+0,23 (ETH).
|
||||||
|
|
||||||
|
**Causa + nota di onestà metrica:** lo Sharpe "3,73" dei report del workflow è **per-trade/annuale**
|
||||||
|
(`explore_lab`); quello rilevante per il portafoglio è lo **Sharpe daily-return** (`combine`), che per
|
||||||
|
FR01 è solo **~1,85/1,53** — troppo basso per muovere un PORT06 a 8,89. È "ridondanza robusta":
|
||||||
|
mean-reversion regime-gated che si sovrappone a ciò che il MASTER già fa.
|
||||||
|
|
||||||
|
**ESITO: il search a 100 agenti ha trovato strategie robuste e causali, ma NESSUNA migliora il
|
||||||
|
PORT06.** Non deployare FR01 né i candidati gemelli. Valore del progetto resta nell'estendere
|
||||||
|
fade/pairs validati. Tutto resta come RECORD DI RICERCA sul branch (non si merge in produzione).
|
||||||
|
Wiring DVOL live e walk-forward: non necessari, deploy abbandonato.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import sys; sys.path.insert(0,".")
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
from scripts.analysis.combine_portfolio import IDX, SPLIT, INIT, _norm, metrics, port_returns
|
||||||
|
from src.portfolio.sleeves import all_sleeve_equities
|
||||||
|
from scripts.analysis.regime_lab import load_features
|
||||||
|
from scripts.analysis.explore_lab import atr
|
||||||
|
FEE=0.001; LEV=3; POS=0.15
|
||||||
|
|
||||||
|
def fr01_daily_equity(asset):
|
||||||
|
df=load_features(asset,"1h")
|
||||||
|
c=df['close'].values; h=df['high'].values; l=df['low'].values; a=atr(df,14)
|
||||||
|
ma=pd.Series(c).rolling(50).mean().values; sd=pd.Series(c).rolling(50).std().values
|
||||||
|
ts=pd.to_datetime(df['timestamp'],unit='ms',utc=True)
|
||||||
|
eq=np.full(len(c),INIT,float); cap=INIT; last=-1
|
||||||
|
for i in range(64,len(c)-1):
|
||||||
|
if i<=last or np.isnan(sd[i]) or sd[i]==0 or np.isnan(a[i]): continue
|
||||||
|
if df['hurst'].iloc[i]>=0.55: continue
|
||||||
|
if np.isnan(df['dvol_pct'].iloc[i]) or df['dvol_pct'].iloc[i]>=0.40: continue
|
||||||
|
if c[i]<ma[i]-2.5*sd[i]: d,sl,tp=1,c[i]-2*a[i],ma[i]
|
||||||
|
elif c[i]>ma[i]+2.5*sd[i]: d,sl,tp=-1,c[i]+2*a[i],ma[i]
|
||||||
|
else: continue
|
||||||
|
j=min(i+24,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
|
||||||
|
cap=max(cap+cap*POS*ret,10.0); eq[j:]=cap; last=j
|
||||||
|
s=pd.Series(eq,index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
|
||||||
|
return _norm(s)
|
||||||
|
|
||||||
|
members=all_sleeve_equities()
|
||||||
|
print(f"PORT06 sleeve: {len(members)} | finestra {IDX[0].date()}..{IDX[-1].date()} | OOS da idx {SPLIT} ({IDX[SPLIT].date()})")
|
||||||
|
fr={"FR01_BTC":fr01_daily_equity("BTC"), "FR01_ETH":fr01_daily_equity("ETH")}
|
||||||
|
|
||||||
|
base=port_returns(members) # equal-weight 17 sleeve (metro combine)
|
||||||
|
aug =port_returns({**members,**fr}) # + FR01x2 (19 sleeve)
|
||||||
|
|
||||||
|
def show(tag, dr):
|
||||||
|
f=metrics(dr); o=metrics(dr,lo=SPLIT)
|
||||||
|
print(f" {tag:<22} FULL: Sharpe {f['sharpe']:.2f} DD {f['dd']:.1f}% ret {f['ret']:+.0f}% | OOS: Sharpe {o['sharpe']:.2f} DD {o['dd']:.1f}% ret {o['ret']:+.0f}%")
|
||||||
|
|
||||||
|
print("\n=== MASTER equal-weight: con/senza FR01 ===")
|
||||||
|
show("PORT06 (17 sleeve)", base)
|
||||||
|
show("PORT06 + FR01 (19)", aug)
|
||||||
|
|
||||||
|
# correlazione FR01 vs portafoglio MASTER aggregato + standalone
|
||||||
|
for k,e in fr.items():
|
||||||
|
r=e.pct_change().fillna(0.0); corr=np.corrcoef(r, base)[0,1]
|
||||||
|
f=metrics(r); o=metrics(r,lo=SPLIT)
|
||||||
|
print(f"\n {k}: corr vs MASTER = {corr:+.3f} | standalone OOS Sharpe {o['sharpe']:.2f} DD {o['dd']:.1f}% ret {o['ret']:+.0f}%")
|
||||||
@@ -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]
|
||||||
|
elif 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 & DVOL<p40)",g_hurst_calm),
|
||||||
|
("VRP<0 Fade (core driver)",g_vrp_neg),
|
||||||
|
("HigVRP Fade (Higuchi>1.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}%)")
|
||||||
@@ -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/<tuofile>.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 }
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
|
||||||
|
_BARS_PER_YEAR = {"1h": 24 * 365, "4h": 6 * 365, "1d": 365}
|
||||||
|
|
||||||
|
|
||||||
|
def regime_features(df: pd.DataFrame, tf: str = "1h", 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.
|
||||||
|
tf serve ad annualizzare correttamente la realized vol (sqrt barre/anno per timeframe)."""
|
||||||
|
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 (punti %, scala come DVOL): std rolling * sqrt(barre/anno del tf)
|
||||||
|
bpy = _BARS_PER_YEAR.get(tf, 24 * 365)
|
||||||
|
rv = pd.Series(ret).rolling(rv_win).std().values * np.sqrt(bpy) * 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, tf=tf)
|
||||||
|
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:
|
||||||
|
# struttura compatibile con robust() (tutti zero) -> robust()=False pulito, niente crash
|
||||||
|
z = {"ret": 0.0, "sharpe": 0.0, "dd": 0.0, "trades": 0, "win": 0.0, "exposure": 0.0, "yearly": {}}
|
||||||
|
print(f" {name:<24s} NO ENTRIES")
|
||||||
|
return {"full": dict(z), "oos": dict(z), "sweep": {0.0: 0.0, 0.0005: 0.0, 0.001: 0.0, 0.002: 0.0},
|
||||||
|
"sweep_oos": {0.0: 0.0, 0.0005: 0.0, 0.001: 0.0, 0.002: 0.0}, "pos_yrs": 0, "n_yrs": 0}
|
||||||
|
return evaluate(name, entries, df) # full + oos + fee sweep
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
@@ -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}%")
|
||||||
Reference in New Issue
Block a user