chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"""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,
|
||||
)
|
||||
|
||||
# dati regime (DVOL/funding/feature) in data/regime/ — NON in data/raw/ (che e' solo OHLCV: i file
|
||||
# estranei in data/raw inquinano la discovery asset del backtest). Vedi diary 2026-06-02-fade-lossguard.
|
||||
RAW = ROOT / "data" / "regime"
|
||||
RAW.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- 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))
|
||||
Reference in New Issue
Block a user