refactor: riorganizzazione script — Strategy ABC, folder strategies/waste/analysis
- src/strategies/base.py: Strategy ABC con Signal, BacktestResult, YearlyStats - src/strategies/indicators.py: keltner_ratio, detect_squeezes, ema, atr, rv, corr - scripts/strategies/: SQ01-SQ04 (squeeze puro/filtri), ML01 (squeeze+GBM) - scripts/waste/: W01-W22 script scartati + REF originali - scripts/analysis/: compare, best_yearly, final_report, paper_status - CLAUDE.md aggiornato con nuova struttura e tabella strategie Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}")
|
||||
Reference in New Issue
Block a user