14522262e6
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>
232 lines
8.4 KiB
Python
232 lines
8.4 KiB
Python
"""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}")
|