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:
Adriano Dal Pastro
2026-06-19 15:16:03 +00:00
parent 8401a280b9
commit 14522262e6
383 changed files with 1971 additions and 779 deletions
+231
View File
@@ -0,0 +1,231 @@
"""Strategia 4: Regime-aware fractal ML.
Combina:
1. Hurst exponent per regime detection (trend vs mean-revert vs random)
2. Feature engineering da indicatori frattali
3. RandomForest per predizione direzione
4. Trade filtering aggressivo (solo alta confidenza)
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, classification_report
from src.data.downloader import load_data
from src.fractal.indicators import (
hurst_exponent,
fractal_dimension_higuchi,
self_similarity_score,
volatility_ratio,
)
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
from src.backtest.engine import run_backtest
print("=" * 60)
print(" STRATEGIA 4: REGIME-AWARE FRACTAL ML — BTC 1H")
print("=" * 60)
df = load_data("BTC", "1h")
close = df["close"].values
n = len(close)
LOOKBACK = 48
LOOKAHEAD = 6
MIN_CONFIDENCE = 0.60
print(f"\nDati: {n} candele")
print(f"Lookback: {LOOKBACK}, Lookahead: {LOOKAHEAD}")
# --- Feature engineering ---
print("\nCalcolo features...")
features_list = []
labels = []
indices = []
returns = np.diff(np.log(np.where(close == 0, 1e-10, close)))
candle_types = encode_candles(df)
body_ratios = extract_body_ratios(df)
shadow_ratios = extract_shadow_ratios(df)
for i in range(LOOKBACK, n - LOOKAHEAD, 3):
if i % 5000 == 0:
print(f" Feature extraction: {i}/{n}")
window = close[i - LOOKBACK : i]
ret_window = returns[i - LOOKBACK : i - 1]
if len(ret_window) < 10:
continue
h = hurst_exponent(ret_window, max_lag=min(len(ret_window) // 4, 20))
fd = fractal_dimension_higuchi(ret_window, k_max=min(8, len(ret_window) // 4))
larger_window = close[max(0, i - LOOKBACK * 6) : i]
ss = self_similarity_score(larger_window, min(LOOKBACK, len(larger_window)))
vr = volatility_ratio(window, fast=12, slow=LOOKBACK)
# Candle pattern features
ct = candle_types[i - 6 : i]
br = body_ratios[i - 6 : i]
sr = shadow_ratios[i - 6 : i]
recent_returns = ret_window[-12:]
momentum_short = np.sum(recent_returns[-3:])
momentum_mid = np.sum(recent_returns[-6:])
momentum_long = np.sum(recent_returns)
vol_short = np.std(recent_returns[-6:]) if len(recent_returns) >= 6 else 0
vol_long = np.std(ret_window) if len(ret_window) > 0 else 0
volume_window = df["volume"].values[i - 12 : i]
vol_avg = np.mean(volume_window) if len(volume_window) > 0 else 0
vol_last = df["volume"].values[i - 1] if i > 0 else 0
vol_ratio = vol_last / vol_avg if vol_avg > 0 else 1.0
up_count_6 = np.sum(ct[-6:] == 1) / 6
down_count_6 = np.sum(ct[-6:] == -1) / 6
features = [
h, # Hurst exponent
fd, # Fractal dimension
ss, # Self-similarity
vr, # Volatility ratio
momentum_short, # 3-candle momentum
momentum_mid, # 6-candle momentum
momentum_long, # Full window momentum
vol_short, # Short-term volatility
vol_long, # Long-term volatility
vol_ratio, # Volume spike ratio
up_count_6, # Bullish ratio (last 6)
down_count_6, # Bearish ratio (last 6)
np.mean(br[-6:]), # Avg body ratio
np.mean(sr[-6:]), # Avg shadow ratio
np.mean(br[-3:]), # Avg body ratio (last 3)
np.std(br[-6:]), # Body ratio std
close[i - 1] / np.mean(window), # Price vs MA
]
# Label: 1 if price goes up in next LOOKAHEAD candles, 0 otherwise
future_ret = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
label = 1 if future_ret > 0.002 else (0 if future_ret > -0.002 else -1)
features_list.append(features)
labels.append(label)
indices.append(i)
X = np.array(features_list)
y = np.array(labels)
idx_arr = np.array(indices)
print(f"\nDataset: {len(X)} samples")
print(f"Label distribution: up={np.sum(y==1)}, flat={np.sum(y==0)}, down={np.sum(y==-1)}")
# Train/test split cronologico
split_point = int(len(X) * 0.7)
X_train, X_test = X[:split_point], X[split_point:]
y_train, y_test = y[:split_point], y[split_point:]
idx_train, idx_test = idx_arr[:split_point], idx_arr[split_point:]
# Handle NaN/Inf
X_train = np.nan_to_num(X_train, nan=0.0, posinf=1e6, neginf=-1e6)
X_test = np.nan_to_num(X_test, nan=0.0, posinf=1e6, neginf=-1e6)
# --- Modelli ---
print("\n--- TRAINING ---")
models = {
"RandomForest": RandomForestClassifier(
n_estimators=200, max_depth=8, min_samples_leaf=20,
class_weight="balanced", random_state=42, n_jobs=-1,
),
"GradientBoosting": GradientBoostingClassifier(
n_estimators=200, max_depth=5, min_samples_leaf=20,
learning_rate=0.05, random_state=42,
),
}
for name, model in models.items():
print(f"\n{'='*40}")
print(f" {name}")
print(f"{'='*40}")
model.fit(X_train, y_train)
# Feature importance
if hasattr(model, "feature_importances_"):
feat_names = [
"hurst", "fractal_dim", "self_sim", "vol_ratio",
"mom_3", "mom_6", "mom_full", "vol_short", "vol_long",
"vol_spike", "up_ratio", "down_ratio", "body_avg",
"shadow_avg", "body_3", "body_std", "price_vs_ma"
]
imp = model.feature_importances_
sorted_idx = np.argsort(imp)[::-1]
print("\nFeature importance (top 10):")
for j in sorted_idx[:10]:
print(f" {feat_names[j]:15s}: {imp[j]:.4f}")
# Prediction con probabilità
y_pred = model.predict(X_test)
proba = model.predict_proba(X_test)
print(f"\nAccuracy: {accuracy_score(y_test, y_pred)*100:.1f}%")
print(classification_report(y_test, y_pred, target_names=["down", "flat", "up"], zero_division=0))
# Genera segnali filtrati per confidenza
signals = pd.Series(0, index=df.index)
accuracies_filtered = []
classes = model.classes_
up_class_idx = list(classes).index(1) if 1 in classes else -1
down_class_idx = list(classes).index(-1) if -1 in classes else -1
for k, i in enumerate(idx_test):
p = proba[k]
if up_class_idx >= 0 and p[up_class_idx] >= MIN_CONFIDENCE:
signals.iloc[i] = 1
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
accuracies_filtered.append(1 if actual > 0 else 0)
elif down_class_idx >= 0 and p[down_class_idx] >= MIN_CONFIDENCE:
signals.iloc[i] = -1
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
accuracies_filtered.append(1 if actual < 0 else 0)
n_signals = (signals != 0).sum()
print(f"\nSegnali filtrati (conf>={MIN_CONFIDENCE}): {n_signals}")
if accuracies_filtered:
print(f"Accuratezza filtrata: {np.mean(accuracies_filtered)*100:.1f}%")
# Backtest
split_idx = int(len(df) * 0.7)
test_df = df.iloc[split_idx:].reset_index(drop=True)
test_signals = signals.iloc[split_idx:].reset_index(drop=True)
result = run_backtest(test_df, test_signals, initial_capital=1000, fee_pct=0.001, max_hold_candles=LOOKAHEAD)
print(f"\nBACKTEST:")
for kk, v in result.summary().items():
print(f" {kk}: {v}")
# Prova con soglie diverse
print(f"\n Varianti soglia:")
for threshold in [0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
sigs = pd.Series(0, index=df.index)
accs = []
for k, i in enumerate(idx_test):
p = proba[k]
if up_class_idx >= 0 and p[up_class_idx] >= threshold:
sigs.iloc[i] = 1
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
accs.append(1 if actual > 0 else 0)
elif down_class_idx >= 0 and p[down_class_idx] >= threshold:
sigs.iloc[i] = -1
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
accs.append(1 if actual < 0 else 0)
t_sigs = sigs.iloc[split_idx:].reset_index(drop=True)
res = run_backtest(test_df, t_sigs, initial_capital=1000, fee_pct=0.001, max_hold_candles=LOOKAHEAD)
acc = np.mean(accs) * 100 if accs else 0
print(f" thr={threshold:.2f}: signals={len(accs):4d} acc={acc:.1f}% ret={res.total_return*100:+.1f}% wr={res.win_rate*100:.0f}% sharpe={res.sharpe_ratio:.2f}")