7169614506
- regime_fetcher.py: fetch DVOL (2021+) + funding (2019+) BTC/ETH da Deribit mainnet public - regime_lab.py: API condivisa, allineamento regime<->prezzo CAUSALE no-look-ahead, feature regime (dvol_pct/vrp/funding_z/dvol_chg) + frattali (hurst/higuchi/vratio/williams), cache feature precalcolate, report()=netto-fee OOS via explore_lab - fractal_argo_workflow.js: workflow ~100 agenti (84 griglia + 8 wildcard + verifica + sintesi) Branch di ricerca: nessun impatto su main/live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
192 lines
8.6 KiB
Python
192 lines
8.6 KiB
Python
"""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))
|