feat: strategie 1-10, framework analisi frattale, download dati storici BTC/ETH
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
"""Analisi baseline: distribuzione pattern frattali e prima strategia naive."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles, find_patterns, pattern_frequency
|
||||
from src.backtest.engine import run_backtest, BacktestResult
|
||||
|
||||
print("=" * 60)
|
||||
print(" ANALISI BASELINE — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
print(f"\nDati: {len(df)} candele [{df['datetime'].iloc[0]} → {df['datetime'].iloc[-1]}]")
|
||||
|
||||
# 1. Distribuzione pattern
|
||||
print("\n--- DISTRIBUZIONE PATTERN (3-6 candele) ---")
|
||||
candle_types = encode_candles(df)
|
||||
unique, counts = np.unique(candle_types, return_counts=True)
|
||||
type_map = {-1: "DOWN", 0: "DOJI", 1: "UP"}
|
||||
for t, c in zip(unique, counts):
|
||||
print(f" {type_map[t]}: {c} ({c/len(df)*100:.1f}%)")
|
||||
|
||||
patterns = find_patterns(df, min_len=3, max_len=6)
|
||||
freq = pattern_frequency(patterns)
|
||||
print(f"\nPattern unici: {len(freq)}")
|
||||
print(f"\nTop 20 pattern più frequenti:")
|
||||
print(freq.head(20).to_string(index=False))
|
||||
|
||||
# 2. Analisi predittiva: dopo ogni pattern, il prezzo sale o scende?
|
||||
print("\n\n--- ANALISI PREDITTIVA PER PATTERN ---")
|
||||
print("Per ogni pattern: % volte che il prezzo sale nelle N candele successive")
|
||||
|
||||
LOOKAHEAD = [1, 3, 6, 12, 24]
|
||||
top_patterns = freq.head(30)["pattern"].tolist()
|
||||
|
||||
results = []
|
||||
for code in top_patterns:
|
||||
matching = [p for p in patterns if p.code == code]
|
||||
if len(matching) < 50:
|
||||
continue
|
||||
|
||||
row = {"pattern": code, "count": len(matching)}
|
||||
for ahead in LOOKAHEAD:
|
||||
ups = 0
|
||||
valid = 0
|
||||
for p in matching:
|
||||
future_idx = p.end_idx + ahead
|
||||
if future_idx >= len(df):
|
||||
continue
|
||||
valid += 1
|
||||
if df["close"].iloc[future_idx] > df["close"].iloc[p.end_idx - 1]:
|
||||
ups += 1
|
||||
if valid > 0:
|
||||
row[f"up_{ahead}h"] = round(ups / valid * 100, 1)
|
||||
else:
|
||||
row[f"up_{ahead}h"] = None
|
||||
results.append(row)
|
||||
|
||||
pred_df = pd.DataFrame(results)
|
||||
print(pred_df.to_string(index=False))
|
||||
|
||||
# 3. Strategia naive: compra quando il pattern più bullish si presenta
|
||||
print("\n\n--- STRATEGIA 1: PATTERN BULLISH NAIVE ---")
|
||||
# Trova pattern con up_24h > 55%
|
||||
bullish_patterns = pred_df[pred_df["up_24h"] > 55]["pattern"].tolist()
|
||||
bearish_patterns = pred_df[pred_df["up_24h"] < 45]["pattern"].tolist()
|
||||
print(f"Pattern bullish (>55% up in 24h): {len(bullish_patterns)}")
|
||||
print(f"Pattern bearish (<45% up in 24h): {len(bearish_patterns)}")
|
||||
|
||||
# Genera segnali
|
||||
signals = pd.Series(0, index=df.index)
|
||||
all_patterns = find_patterns(df, min_len=3, max_len=6)
|
||||
for p in all_patterns:
|
||||
if p.code in bullish_patterns:
|
||||
signals.iloc[p.end_idx - 1] = 1
|
||||
elif p.code in bearish_patterns:
|
||||
if signals.iloc[p.end_idx - 1] == 0:
|
||||
signals.iloc[p.end_idx - 1] = -1
|
||||
|
||||
# Train/test split: 70/30
|
||||
split_idx = int(len(df) * 0.7)
|
||||
train_df = df.iloc[:split_idx].reset_index(drop=True)
|
||||
test_df = df.iloc[split_idx:].reset_index(drop=True)
|
||||
train_signals = signals.iloc[:split_idx].reset_index(drop=True)
|
||||
test_signals = signals.iloc[split_idx:].reset_index(drop=True)
|
||||
|
||||
train_result = run_backtest(train_df, train_signals, initial_capital=1000, fee_pct=0.001)
|
||||
test_result = run_backtest(test_df, test_signals, initial_capital=1000, fee_pct=0.001)
|
||||
|
||||
print("\nRISULTATI TRAIN (70%):")
|
||||
for k, v in train_result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
print("\nRISULTATI TEST (30%):")
|
||||
for k, v in test_result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
# 4. Buy & Hold come benchmark
|
||||
print("\n\n--- BENCHMARK: BUY & HOLD ---")
|
||||
bh_signals = pd.Series(0, index=test_df.index)
|
||||
bh_signals.iloc[0] = 1 # Compra al primo candle
|
||||
bh_result = run_backtest(test_df, bh_signals, initial_capital=1000, fee_pct=0.001, max_hold_candles=len(test_df))
|
||||
print("Buy & Hold (test period):")
|
||||
for k, v in bh_result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Strategia 2: DTW pattern matching.
|
||||
Idea: per ogni finestra di N candele, cerca le K finestre più simili nel passato
|
||||
via DTW sui prezzi normalizzati. Se la maggioranza delle match passate è salita
|
||||
dopo, vai long. Se è scesa, vai short.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.similarity import dtw_distance
|
||||
from src.fractal.patterns import normalize_pattern_window
|
||||
from src.backtest.engine import run_backtest
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 2: DTW PATTERN MATCHING — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
|
||||
WINDOW = 12
|
||||
LOOKAHEAD = 6
|
||||
K_NEIGHBORS = 20
|
||||
LOOKBACK = 2000
|
||||
THRESHOLD = 0.65
|
||||
|
||||
split_idx = int(len(df) * 0.7)
|
||||
|
||||
def normalize_window(arr: np.ndarray) -> np.ndarray:
|
||||
mn, mx = arr.min(), arr.max()
|
||||
if mx - mn == 0:
|
||||
return np.zeros_like(arr)
|
||||
return (arr - mn) / (mx - mn)
|
||||
|
||||
def compute_returns(close_arr: np.ndarray, idx: int, ahead: int) -> float:
|
||||
if idx + ahead >= len(close_arr):
|
||||
return 0.0
|
||||
return (close_arr[idx + ahead] - close_arr[idx]) / close_arr[idx]
|
||||
|
||||
print(f"\nParametri: window={WINDOW}, lookahead={LOOKAHEAD}, K={K_NEIGHBORS}")
|
||||
print(f"Lookback: {LOOKBACK} candele, threshold: {THRESHOLD}")
|
||||
print(f"Train: 0→{split_idx}, Test: {split_idx}→{len(df)}")
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
accuracies = []
|
||||
|
||||
step = 6
|
||||
test_range = range(split_idx, len(df) - LOOKAHEAD, step)
|
||||
total_steps = len(list(test_range))
|
||||
print(f"\nValutazione: {total_steps} punti (step={step})...")
|
||||
|
||||
for count, i in enumerate(test_range):
|
||||
if count % 500 == 0:
|
||||
print(f" Progresso: {count}/{total_steps} ({count/total_steps*100:.0f}%)")
|
||||
|
||||
current = normalize_window(close[i - WINDOW : i])
|
||||
|
||||
search_start = max(WINDOW, i - LOOKBACK)
|
||||
search_end = i - LOOKAHEAD
|
||||
|
||||
if search_end - search_start < K_NEIGHBORS:
|
||||
continue
|
||||
|
||||
distances = []
|
||||
for j in range(search_start, search_end):
|
||||
candidate = normalize_window(close[j - WINDOW : j])
|
||||
if len(candidate) != len(current):
|
||||
continue
|
||||
d = dtw_distance(current, candidate)
|
||||
future_ret = compute_returns(close, j, LOOKAHEAD)
|
||||
distances.append((d, future_ret))
|
||||
|
||||
if len(distances) < K_NEIGHBORS:
|
||||
continue
|
||||
|
||||
distances.sort(key=lambda x: x[0])
|
||||
top_k = distances[:K_NEIGHBORS]
|
||||
up_count = sum(1 for _, ret in top_k if ret > 0)
|
||||
up_ratio = up_count / K_NEIGHBORS
|
||||
|
||||
if up_ratio >= THRESHOLD:
|
||||
signals.iloc[i] = 1
|
||||
elif up_ratio <= (1 - THRESHOLD):
|
||||
signals.iloc[i] = -1
|
||||
|
||||
actual_ret = compute_returns(close, i, LOOKAHEAD)
|
||||
predicted_up = up_ratio >= THRESHOLD
|
||||
predicted_down = up_ratio <= (1 - THRESHOLD)
|
||||
if predicted_up:
|
||||
accuracies.append(1 if actual_ret > 0 else 0)
|
||||
elif predicted_down:
|
||||
accuracies.append(1 if actual_ret < 0 else 0)
|
||||
|
||||
print(f"\nSegnali generati: {(signals != 0).sum()}")
|
||||
print(f" Long: {(signals == 1).sum()}, Short: {(signals == -1).sum()}")
|
||||
|
||||
if accuracies:
|
||||
print(f"Accuratezza direzione: {np.mean(accuracies)*100:.1f}% su {len(accuracies)} segnali")
|
||||
|
||||
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("\nRISULTATI TEST:")
|
||||
for k, v in result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Strategia 3: Fourier decomposition e proiezione.
|
||||
Ispirata al paper Pythagoras Trading Prediction.
|
||||
Idea: scomponi il prezzo in componenti sinusoidali via FFT,
|
||||
ricostruisci con le N componenti più forti, proietta nel futuro.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
from src.backtest.engine import run_backtest
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 3: FOURIER PROJECTION — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
n_total = len(close)
|
||||
|
||||
WINDOW = 588 # dal paper: 588 candele per l'indicatore H-C
|
||||
N_COMPONENTS = 25 # dal paper: 25 linee verticali
|
||||
LOOKAHEAD = 6
|
||||
STEP = 6
|
||||
|
||||
split_idx = int(n_total * 0.7)
|
||||
|
||||
def fourier_project(series: np.ndarray, n_components: int, ahead: int) -> np.ndarray:
|
||||
"""Ricostruisci serie con top-N componenti Fourier e proietta avanti."""
|
||||
n = len(series)
|
||||
detrended = series - np.linspace(series[0], series[-1], n)
|
||||
fft_vals = np.fft.fft(detrended)
|
||||
freqs = np.fft.fftfreq(n)
|
||||
|
||||
magnitudes = np.abs(fft_vals)
|
||||
magnitudes[0] = 0
|
||||
top_indices = np.argsort(magnitudes)[-n_components * 2:]
|
||||
|
||||
fft_filtered = np.zeros_like(fft_vals)
|
||||
fft_filtered[top_indices] = fft_vals[top_indices]
|
||||
|
||||
t_extended = np.arange(n + ahead)
|
||||
reconstruction = np.zeros(n + ahead)
|
||||
for idx in top_indices:
|
||||
amp = np.abs(fft_vals[idx]) / n
|
||||
phase = np.angle(fft_vals[idx])
|
||||
freq = freqs[idx]
|
||||
reconstruction += amp * np.cos(2 * np.pi * freq * t_extended / 1 + phase)
|
||||
|
||||
trend_slope = (series[-1] - series[0]) / n
|
||||
trend_extended = series[0] + trend_slope * t_extended
|
||||
reconstruction += trend_extended
|
||||
|
||||
return reconstruction
|
||||
|
||||
|
||||
print(f"\nParametri: window={WINDOW}, components={N_COMPONENTS}, lookahead={LOOKAHEAD}")
|
||||
print(f"Train: 0→{split_idx}, Test: {split_idx}→{n_total}")
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
accuracies = []
|
||||
|
||||
test_range = range(max(split_idx, WINDOW), n_total - LOOKAHEAD, STEP)
|
||||
total_steps = len(list(test_range))
|
||||
print(f"Valutazione: {total_steps} punti (step={STEP})...")
|
||||
|
||||
for count, i in enumerate(test_range):
|
||||
if count % 500 == 0:
|
||||
print(f" Progresso: {count}/{total_steps} ({count/total_steps*100:.0f}%)")
|
||||
|
||||
window_data = close[i - WINDOW : i]
|
||||
projected = fourier_project(window_data, N_COMPONENTS, LOOKAHEAD)
|
||||
|
||||
current_price = close[i - 1]
|
||||
projected_price = projected[-1]
|
||||
change_pct = (projected_price - current_price) / current_price
|
||||
|
||||
if change_pct > 0.005:
|
||||
signals.iloc[i] = 1
|
||||
elif change_pct < -0.005:
|
||||
signals.iloc[i] = -1
|
||||
|
||||
actual_ret = (close[i + LOOKAHEAD - 1] - current_price) / current_price
|
||||
if signals.iloc[i] == 1:
|
||||
accuracies.append(1 if actual_ret > 0 else 0)
|
||||
elif signals.iloc[i] == -1:
|
||||
accuracies.append(1 if actual_ret < 0 else 0)
|
||||
|
||||
print(f"\nSegnali generati: {(signals != 0).sum()}")
|
||||
print(f" Long: {(signals == 1).sum()}, Short: {(signals == -1).sum()}")
|
||||
|
||||
if accuracies:
|
||||
print(f"Accuratezza direzione: {np.mean(accuracies)*100:.1f}% su {len(accuracies)} segnali")
|
||||
|
||||
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("\nRISULTATI TEST:")
|
||||
for k, v in result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
# Varianti con parametri diversi
|
||||
print("\n\n--- VARIANTI PARAMETRI ---")
|
||||
for n_comp in [5, 10, 15, 25, 50]:
|
||||
for window in [144, 288, 588]:
|
||||
sigs = pd.Series(0, index=df.index)
|
||||
accs = []
|
||||
test_r = range(max(split_idx, window), n_total - LOOKAHEAD, STEP)
|
||||
for i in test_r:
|
||||
w = close[i - window : i]
|
||||
proj = fourier_project(w, n_comp, LOOKAHEAD)
|
||||
cp = close[i - 1]
|
||||
pp = proj[-1]
|
||||
ch = (pp - cp) / cp
|
||||
if ch > 0.005:
|
||||
sigs.iloc[i] = 1
|
||||
elif ch < -0.005:
|
||||
sigs.iloc[i] = -1
|
||||
ar = (close[i + LOOKAHEAD - 1] - cp) / cp
|
||||
if sigs.iloc[i] == 1:
|
||||
accs.append(1 if ar > 0 else 0)
|
||||
elif sigs.iloc[i] == -1:
|
||||
accs.append(1 if ar < 0 else 0)
|
||||
|
||||
if not accs:
|
||||
continue
|
||||
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
|
||||
print(f" W={window:3d} N={n_comp:2d} → acc={acc:.1f}% trades={res.total_trades} ret={res.total_return*100:+.1f}% sharpe={res.sharpe_ratio:.2f}")
|
||||
@@ -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}")
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Strategia 5: Enhanced fractal features + binary classification + position management.
|
||||
Miglioramenti rispetto a #4:
|
||||
- Binary classification (up vs down, ignora flat)
|
||||
- Feature engineering esteso: multi-window fractal indicators
|
||||
- Migliore filtraggio segnali
|
||||
- Position sizing basato su confidenza
|
||||
- Trailing stop
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.metrics import accuracy_score
|
||||
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
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 5: ENHANCED FRACTAL — BTC + ETH 1H")
|
||||
print("=" * 60)
|
||||
|
||||
LOOKAHEADS = [3, 6, 12]
|
||||
MIN_RETURN = 0.003 # 0.3% threshold for "up" label
|
||||
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for LOOKAHEAD in LOOKAHEADS:
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 1H — LOOKAHEAD={LOOKAHEAD}")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
close = df["close"].values
|
||||
volume = df["volume"].values
|
||||
n = len(close)
|
||||
log_close = np.log(np.where(close == 0, 1e-10, close))
|
||||
returns = np.diff(log_close)
|
||||
|
||||
candle_types = encode_candles(df)
|
||||
body_ratios = extract_body_ratios(df)
|
||||
shadow_ratios = extract_shadow_ratios(df)
|
||||
|
||||
WINDOWS = [24, 48, 96, 192]
|
||||
features_list = []
|
||||
labels = []
|
||||
indices = []
|
||||
|
||||
max_window = max(WINDOWS) + 50
|
||||
|
||||
for i in range(max_window, n - LOOKAHEAD, 2):
|
||||
feats = []
|
||||
|
||||
for w in WINDOWS:
|
||||
ret_w = returns[i - w : i - 1]
|
||||
close_w = close[i - w : i]
|
||||
|
||||
h = hurst_exponent(ret_w, max_lag=min(len(ret_w) // 4, 20))
|
||||
fd = fractal_dimension_higuchi(ret_w, k_max=min(6, len(ret_w) // 4))
|
||||
vr = volatility_ratio(close_w, fast=min(12, w // 4), slow=w)
|
||||
|
||||
mom = np.sum(ret_w)
|
||||
vol = np.std(ret_w)
|
||||
skew = float(pd.Series(ret_w).skew()) if len(ret_w) > 2 else 0
|
||||
kurt = float(pd.Series(ret_w).kurtosis()) if len(ret_w) > 3 else 0
|
||||
|
||||
ma = np.mean(close_w)
|
||||
price_vs_ma = close[i - 1] / ma if ma > 0 else 1
|
||||
|
||||
# Autocorrelation lag-1
|
||||
if len(ret_w) > 1 and np.std(ret_w) > 0:
|
||||
ac1 = np.corrcoef(ret_w[:-1], ret_w[1:])[0, 1]
|
||||
if not np.isfinite(ac1):
|
||||
ac1 = 0
|
||||
else:
|
||||
ac1 = 0
|
||||
|
||||
feats.extend([h, fd, vr, mom, vol, skew, kurt, price_vs_ma, ac1])
|
||||
|
||||
# Self-similarity multi-scale
|
||||
large_window = close[max(0, i - 192 * 4) : i]
|
||||
ss = self_similarity_score(large_window, 48)
|
||||
feats.append(ss)
|
||||
|
||||
# Candle pattern features (last 12 candles)
|
||||
ct = candle_types[i - 12 : i]
|
||||
br = body_ratios[i - 12 : i]
|
||||
sr = shadow_ratios[i - 12 : i]
|
||||
|
||||
feats.extend([
|
||||
np.mean(ct[-3:]),
|
||||
np.mean(ct[-6:]),
|
||||
np.mean(ct[-12:]),
|
||||
np.std(br[-6:]),
|
||||
np.mean(br[-3:]),
|
||||
np.mean(sr[-6:]),
|
||||
np.max(br[-6:]),
|
||||
np.min(br[-6:]),
|
||||
])
|
||||
|
||||
# Volume features
|
||||
vol_w = volume[i - 24 : i]
|
||||
if np.mean(vol_w) > 0:
|
||||
feats.append(volume[i - 1] / np.mean(vol_w))
|
||||
feats.append(np.std(vol_w) / np.mean(vol_w))
|
||||
else:
|
||||
feats.extend([1.0, 0.0])
|
||||
|
||||
# Range/ATR proxy
|
||||
h_arr = df["high"].values[i - 14 : i]
|
||||
l_arr = df["low"].values[i - 14 : i]
|
||||
c_arr = close[i - 14 : i]
|
||||
tr = np.maximum(h_arr - l_arr, np.maximum(np.abs(h_arr - np.roll(c_arr, 1)), np.abs(l_arr - np.roll(c_arr, 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
feats.append(atr / close[i - 1] if close[i - 1] > 0 else 0)
|
||||
|
||||
# Label
|
||||
future_ret = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(future_ret) < MIN_RETURN:
|
||||
continue # skip flat zones
|
||||
|
||||
label = 1 if future_ret > 0 else 0
|
||||
|
||||
features_list.append(feats)
|
||||
labels.append(label)
|
||||
indices.append(i)
|
||||
|
||||
X = np.array(features_list)
|
||||
y = np.array(labels)
|
||||
idx_arr = np.array(indices)
|
||||
|
||||
X = np.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
# Split
|
||||
split = int(len(X) * 0.7)
|
||||
X_train, X_test = X[:split], X[split:]
|
||||
y_train, y_test = y[:split], y[split:]
|
||||
idx_test = idx_arr[split:]
|
||||
|
||||
print(f"Samples: {len(X)} (train={split}, test={len(X)-split})")
|
||||
print(f"Label balance: up={np.mean(y)*100:.1f}%")
|
||||
|
||||
# Train
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=300, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.03, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
y_pred = model.predict(X_test)
|
||||
proba = model.predict_proba(X_test)
|
||||
|
||||
base_acc = accuracy_score(y_test, y_pred)
|
||||
print(f"Base accuracy: {base_acc*100:.1f}%")
|
||||
|
||||
# Threshold sweep
|
||||
print(f"\n Threshold sweep:")
|
||||
for thr in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
|
||||
up_idx = model.classes_.tolist().index(1)
|
||||
|
||||
sigs = []
|
||||
accs = []
|
||||
for k in range(len(X_test)):
|
||||
p_up = proba[k][up_idx]
|
||||
i = idx_test[k]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= thr:
|
||||
sigs.append(("long", i))
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
elif p_up <= (1 - thr):
|
||||
sigs.append(("short", i))
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
|
||||
if not accs:
|
||||
print(f" thr={thr:.2f}: no signals")
|
||||
continue
|
||||
|
||||
acc = np.mean(accs) * 100
|
||||
# Simple PnL estimate
|
||||
pnl = 0
|
||||
capital = 1000
|
||||
for direction, i in sigs:
|
||||
entry = close[i - 1]
|
||||
exit_ = close[i + LOOKAHEAD - 1]
|
||||
if direction == "long":
|
||||
ret = (exit_ - entry) / entry
|
||||
else:
|
||||
ret = (entry - exit_) / entry
|
||||
ret -= 0.002 # fees round-trip
|
||||
pnl += capital * ret * 0.5 # 50% per trade
|
||||
capital += capital * ret * 0.5
|
||||
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
trades_per_year = len(sigs) / ((n - max_window) / (24 * 365))
|
||||
print(f" thr={thr:.2f}: signals={len(sigs):5d} acc={acc:.1f}% ret={total_ret:+.1f}% trades/yr={trades_per_year:.0f}")
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Strategia 6: Structural Pattern Matching con DTW veloce.
|
||||
Idea diversa: invece di ML generico, cerca nel passato le finestre OHLC
|
||||
più simili alla finestra corrente usando una versione veloce (reduced DTW).
|
||||
Vota sulla direzione basandosi sui K-nearest neighbors nel passato.
|
||||
Usa features normalizzate (non DTW puro sul prezzo che è lento).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import normalize_pattern_window
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 6: STRUCTURAL PATTERN KNN — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
n = len(close)
|
||||
|
||||
WINDOW = 24
|
||||
LOOKAHEAD = 6
|
||||
MIN_RETURN = 0.003
|
||||
|
||||
def extract_structural_features(df: pd.DataFrame, idx: int, window: int) -> np.ndarray | None:
|
||||
"""Extract normalized structural features from OHLC window."""
|
||||
if idx < window:
|
||||
return None
|
||||
|
||||
o = df["open"].values[idx - window : idx]
|
||||
h = df["high"].values[idx - window : idx]
|
||||
l = df["low"].values[idx - window : idx]
|
||||
c = df["close"].values[idx - window : idx]
|
||||
v = df["volume"].values[idx - window : idx]
|
||||
|
||||
# Normalize price to [0,1]
|
||||
all_prices = np.concatenate([o, h, l, c])
|
||||
mn, mx = all_prices.min(), all_prices.max()
|
||||
if mx - mn == 0:
|
||||
return None
|
||||
o_n = (o - mn) / (mx - mn)
|
||||
h_n = (h - mn) / (mx - mn)
|
||||
l_n = (l - mn) / (mx - mn)
|
||||
c_n = (c - mn) / (mx - mn)
|
||||
|
||||
# Body and shadow ratios (already normalized)
|
||||
total = h - l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(c - o) / total
|
||||
upper_shadow = (h - np.maximum(o, c)) / total
|
||||
lower_shadow = (np.minimum(o, c) - l) / total
|
||||
direction = np.sign(c - o)
|
||||
|
||||
# Returns
|
||||
log_c = np.log(np.where(c == 0, 1e-10, c))
|
||||
returns = np.diff(log_c)
|
||||
|
||||
# Volume profile (normalized)
|
||||
v_mean = np.mean(v)
|
||||
v_n = v / v_mean if v_mean > 0 else np.ones_like(v)
|
||||
|
||||
# Downsample to fixed-size feature vector
|
||||
# Take every N-th candle if window is large
|
||||
step = max(1, window // 12)
|
||||
sampled_idx = np.arange(0, window, step)[:12]
|
||||
|
||||
features = np.concatenate([
|
||||
c_n[sampled_idx], # 12: normalized close
|
||||
body[sampled_idx], # 12: body ratios
|
||||
direction[sampled_idx], # 12: direction
|
||||
upper_shadow[sampled_idx], # 12: upper shadow
|
||||
lower_shadow[sampled_idx], # 12: lower shadow
|
||||
v_n[sampled_idx], # 12: volume profile
|
||||
[np.mean(returns), np.std(returns), np.sum(returns)], # 3: return stats
|
||||
[np.mean(body), np.std(body)], # 2: body stats
|
||||
])
|
||||
return features
|
||||
|
||||
|
||||
print("Extracting features...")
|
||||
features_all = []
|
||||
labels_all = []
|
||||
indices_all = []
|
||||
|
||||
for i in range(WINDOW, n - LOOKAHEAD, 1):
|
||||
feats = extract_structural_features(df, i, WINDOW)
|
||||
if feats is None:
|
||||
continue
|
||||
|
||||
future_ret = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(future_ret) < MIN_RETURN:
|
||||
continue
|
||||
|
||||
features_all.append(feats)
|
||||
labels_all.append(1 if future_ret > 0 else 0)
|
||||
indices_all.append(i)
|
||||
|
||||
X = np.array(features_all)
|
||||
y = np.array(labels_all)
|
||||
idx_arr = np.array(indices_all)
|
||||
|
||||
X = np.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
split = int(len(X) * 0.7)
|
||||
X_train, X_test = X[:split], X[split:]
|
||||
y_train, y_test = y[:split], y[split:]
|
||||
idx_test = idx_arr[split:]
|
||||
|
||||
print(f"Samples: {len(X)} (train={split}, test={len(X)-split})")
|
||||
print(f"Label balance: up={np.mean(y)*100:.1f}%")
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_train_s = scaler.fit_transform(X_train)
|
||||
X_test_s = scaler.transform(X_test)
|
||||
|
||||
# Test diversi K
|
||||
print("\n--- KNN SWEEP ---")
|
||||
for K in [5, 10, 20, 50, 100, 200]:
|
||||
knn = KNeighborsClassifier(n_neighbors=K, weights="distance", n_jobs=-1)
|
||||
knn.fit(X_train_s, y_train)
|
||||
|
||||
proba = knn.predict_proba(X_test_s)
|
||||
up_idx = list(knn.classes_).index(1)
|
||||
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
sigs = []
|
||||
accs = []
|
||||
for j in range(len(X_test)):
|
||||
p_up = proba[j][up_idx]
|
||||
i = idx_test[j]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= thr:
|
||||
sigs.append(1)
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
elif p_up <= (1 - thr):
|
||||
sigs.append(-1)
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
|
||||
if not accs:
|
||||
continue
|
||||
|
||||
acc = np.mean(accs) * 100
|
||||
# PnL
|
||||
capital = 1000
|
||||
for direction, j in zip(sigs, range(len(accs))):
|
||||
i_idx = idx_test[[k for k in range(len(X_test)) if proba[k][up_idx] >= thr or proba[k][up_idx] <= (1 - thr)][j]]
|
||||
entry = close[i_idx - 1]
|
||||
exit_ = close[i_idx + LOOKAHEAD - 1]
|
||||
if direction == 1:
|
||||
ret = (exit_ - entry) / entry
|
||||
else:
|
||||
ret = (entry - exit_) / entry
|
||||
ret -= 0.002
|
||||
capital *= (1 + ret * 0.5)
|
||||
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
print(f" K={K:3d} thr={thr:.2f}: signals={len(accs):5d} acc={acc:.1f}% ret={total_ret:+.1f}%")
|
||||
|
||||
# Best combo: try with Gradient Boosting on same features
|
||||
print("\n\n--- GRADIENT BOOSTING SU STRUCTURAL FEATURES ---")
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
|
||||
gb = GradientBoostingClassifier(
|
||||
n_estimators=300, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.03, subsample=0.8, random_state=42,
|
||||
)
|
||||
gb.fit(X_train_s, y_train)
|
||||
proba_gb = gb.predict_proba(X_test_s)
|
||||
up_idx_gb = list(gb.classes_).index(1)
|
||||
|
||||
for thr in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
n_trades = 0
|
||||
for j in range(len(X_test)):
|
||||
p_up = proba_gb[j][up_idx_gb]
|
||||
i = idx_test[j]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
ret = actual - 0.002
|
||||
capital *= (1 + ret * 0.5)
|
||||
n_trades += 1
|
||||
elif p_up <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
ret = -actual - 0.002
|
||||
capital *= (1 + ret * 0.5)
|
||||
n_trades += 1
|
||||
|
||||
if not accs:
|
||||
continue
|
||||
acc = np.mean(accs) * 100
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
print(f" thr={thr:.2f}: trades={n_trades:5d} acc={acc:.1f}% ret={total_ret:+.1f}%")
|
||||
@@ -0,0 +1,320 @@
|
||||
"""Strategia 7: LSTM su features frattali multi-timeframe.
|
||||
Usa sequenze di features frattali come input a un LSTM
|
||||
per predire la direzione del prezzo.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, TensorDataset
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.indicators import hurst_exponent, fractal_dimension_higuchi, volatility_ratio
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Device: {DEVICE}")
|
||||
|
||||
|
||||
class FractalLSTM(nn.Module):
|
||||
def __init__(self, input_size: int, hidden_size: int = 64, num_layers: int = 2, dropout: float = 0.3):
|
||||
super().__init__()
|
||||
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout)
|
||||
self.classifier = nn.Sequential(
|
||||
nn.Linear(hidden_size, 32),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(32, 1),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
_, (h_n, _) = self.lstm(x)
|
||||
out = self.classifier(h_n[-1])
|
||||
return out.squeeze(-1)
|
||||
|
||||
|
||||
def extract_candle_features(df: pd.DataFrame, i: int) -> np.ndarray:
|
||||
"""Extract per-candle features at index i."""
|
||||
o, h, l, c = df["open"].values[i], df["high"].values[i], df["low"].values[i], df["close"].values[i]
|
||||
v = df["volume"].values[i]
|
||||
total = h - l if h - l > 0 else 1e-10
|
||||
body = abs(c - o) / total
|
||||
upper_s = (h - max(o, c)) / total
|
||||
lower_s = (min(o, c) - l) / total
|
||||
direction = 1 if c > o else (-1 if c < o else 0)
|
||||
|
||||
# Log return from previous candle
|
||||
if i > 0:
|
||||
prev_c = df["close"].values[i - 1]
|
||||
log_ret = np.log(c / prev_c) if prev_c > 0 else 0
|
||||
else:
|
||||
log_ret = 0
|
||||
|
||||
return np.array([body, upper_s, lower_s, direction, log_ret, v])
|
||||
|
||||
|
||||
def build_dataset(df: pd.DataFrame, seq_len: int = 48, lookahead: int = 6, min_ret: float = 0.003):
|
||||
"""Build sequences of candle features with labels."""
|
||||
close = df["close"].values
|
||||
n = len(df)
|
||||
|
||||
vol_mean = pd.Series(df["volume"].values).rolling(100, min_periods=1).mean().values
|
||||
|
||||
sequences = []
|
||||
labels = []
|
||||
indices = []
|
||||
|
||||
# Pre-compute additional features
|
||||
candle_types = encode_candles(df)
|
||||
body_ratios = extract_body_ratios(df)
|
||||
shadow_ratios = extract_shadow_ratios(df)
|
||||
|
||||
for i in range(seq_len, n - lookahead, 2):
|
||||
seq = []
|
||||
for j in range(i - seq_len, i):
|
||||
feats = extract_candle_features(df, j)
|
||||
# Normalize volume by rolling mean
|
||||
feats[5] = feats[5] / vol_mean[j] if vol_mean[j] > 0 else 1.0
|
||||
seq.append(feats)
|
||||
|
||||
future_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(future_ret) < min_ret:
|
||||
continue
|
||||
|
||||
sequences.append(seq)
|
||||
labels.append(1 if future_ret > 0 else 0)
|
||||
indices.append(i)
|
||||
|
||||
return np.array(sequences), np.array(labels), np.array(indices)
|
||||
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 7: LSTM FRACTAL — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
|
||||
SEQ_LEN = 48
|
||||
LOOKAHEAD = 6
|
||||
EPOCHS = 30
|
||||
BATCH_SIZE = 256
|
||||
LR = 0.001
|
||||
|
||||
print(f"\nSeq length: {SEQ_LEN}, Lookahead: {LOOKAHEAD}")
|
||||
print("Building dataset...")
|
||||
|
||||
X, y, idx_arr = build_dataset(df, seq_len=SEQ_LEN, lookahead=LOOKAHEAD)
|
||||
print(f"Samples: {len(X)}, Features per candle: {X.shape[2]}, Up ratio: {np.mean(y)*100:.1f}%")
|
||||
|
||||
# Chronological split
|
||||
split = int(len(X) * 0.7)
|
||||
val_split = int(len(X) * 0.85)
|
||||
|
||||
X_train, X_val, X_test = X[:split], X[split:val_split], X[val_split:]
|
||||
y_train, y_val, y_test = y[:split], y[split:val_split], y[val_split:]
|
||||
idx_test_arr = idx_arr[val_split:]
|
||||
|
||||
# Normalize features per-feature across time
|
||||
n_features = X.shape[2]
|
||||
for f in range(n_features):
|
||||
scaler = StandardScaler()
|
||||
X_train[:, :, f] = scaler.fit_transform(X_train[:, :, f])
|
||||
X_val[:, :, f] = scaler.transform(X_val[:, :, f])
|
||||
X_test[:, :, f] = scaler.transform(X_test[:, :, f])
|
||||
|
||||
# To tensors
|
||||
X_train_t = torch.FloatTensor(X_train).to(DEVICE)
|
||||
y_train_t = torch.FloatTensor(y_train).to(DEVICE)
|
||||
X_val_t = torch.FloatTensor(X_val).to(DEVICE)
|
||||
y_val_t = torch.FloatTensor(y_val).to(DEVICE)
|
||||
X_test_t = torch.FloatTensor(X_test).to(DEVICE)
|
||||
|
||||
train_ds = TensorDataset(X_train_t, y_train_t)
|
||||
train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)
|
||||
|
||||
# Model
|
||||
model = FractalLSTM(input_size=n_features, hidden_size=64, num_layers=2, dropout=0.3).to(DEVICE)
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=LR, weight_decay=1e-5)
|
||||
criterion = nn.BCEWithLogitsLoss()
|
||||
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5)
|
||||
|
||||
print(f"\nTraining on {DEVICE}...")
|
||||
best_val_acc = 0
|
||||
patience_counter = 0
|
||||
|
||||
for epoch in range(EPOCHS):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
for xb, yb in train_dl:
|
||||
optimizer.zero_grad()
|
||||
pred = model(xb)
|
||||
loss = criterion(pred, yb)
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
|
||||
# Validation
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
val_pred = model(X_val_t)
|
||||
val_loss = criterion(val_pred, y_val_t).item()
|
||||
val_proba = torch.sigmoid(val_pred).cpu().numpy()
|
||||
val_acc = np.mean((val_proba > 0.5) == y_val)
|
||||
|
||||
scheduler.step(val_loss)
|
||||
|
||||
if val_acc > best_val_acc:
|
||||
best_val_acc = val_acc
|
||||
torch.save(model.state_dict(), "data/processed/best_lstm.pt")
|
||||
patience_counter = 0
|
||||
else:
|
||||
patience_counter += 1
|
||||
|
||||
if epoch % 5 == 0 or patience_counter > 8:
|
||||
print(f" Epoch {epoch:2d}: train_loss={total_loss/len(train_dl):.4f} val_loss={val_loss:.4f} val_acc={val_acc*100:.1f}% best={best_val_acc*100:.1f}%")
|
||||
|
||||
if patience_counter > 10:
|
||||
print(f" Early stopping at epoch {epoch}")
|
||||
break
|
||||
|
||||
# Load best model and test
|
||||
model.load_state_dict(torch.load("data/processed/best_lstm.pt", weights_only=True))
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
test_pred = model(X_test_t)
|
||||
test_proba = torch.sigmoid(test_pred).cpu().numpy()
|
||||
|
||||
test_acc = np.mean((test_proba > 0.5) == y_test)
|
||||
print(f"\nTest accuracy (base): {test_acc*100:.1f}%")
|
||||
|
||||
# Threshold sweep
|
||||
print("\n--- THRESHOLD SWEEP ---")
|
||||
for thr in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
n_trades = 0
|
||||
|
||||
for j in range(len(X_test)):
|
||||
p = test_proba[j]
|
||||
i = idx_test_arr[j]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
ret = actual - 0.002
|
||||
capital *= (1 + ret * 0.3)
|
||||
n_trades += 1
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
ret = -actual - 0.002
|
||||
capital *= (1 + ret * 0.3)
|
||||
n_trades += 1
|
||||
|
||||
if not accs:
|
||||
print(f" thr={thr:.2f}: no signals")
|
||||
continue
|
||||
|
||||
acc = np.mean(accs) * 100
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
# Annualized
|
||||
test_days = (idx_test_arr[-1] - idx_test_arr[0]) / 24
|
||||
years = test_days / 365.25 if test_days > 0 else 1
|
||||
ann_ret = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
trades_yr = n_trades / years if years > 0 else 0
|
||||
print(f" thr={thr:.2f}: trades={n_trades:5d} acc={acc:.1f}% ret={total_ret:+.1f}% ann={ann_ret:+.1f}% trades/yr={trades_yr:.0f}")
|
||||
|
||||
# Also try ETH
|
||||
print("\n\n" + "=" * 60)
|
||||
print(" LSTM SU ETH 1H (same model architecture)")
|
||||
print("=" * 60)
|
||||
|
||||
df_eth = load_data("ETH", "1h")
|
||||
close_eth = df_eth["close"].values
|
||||
|
||||
X_eth, y_eth, idx_eth = build_dataset(df_eth, seq_len=SEQ_LEN, lookahead=LOOKAHEAD)
|
||||
print(f"ETH samples: {len(X_eth)}, Up ratio: {np.mean(y_eth)*100:.1f}%")
|
||||
|
||||
split_e = int(len(X_eth) * 0.7)
|
||||
val_e = int(len(X_eth) * 0.85)
|
||||
X_train_e, X_val_e, X_test_e = X_eth[:split_e], X_eth[split_e:val_e], X_eth[val_e:]
|
||||
y_train_e, y_val_e, y_test_e = y_eth[:split_e], y_eth[split_e:val_e], y_eth[val_e:]
|
||||
idx_test_e = idx_eth[val_e:]
|
||||
|
||||
for f in range(n_features):
|
||||
sc = StandardScaler()
|
||||
X_train_e[:, :, f] = sc.fit_transform(X_train_e[:, :, f])
|
||||
X_val_e[:, :, f] = sc.transform(X_val_e[:, :, f])
|
||||
X_test_e[:, :, f] = sc.transform(X_test_e[:, :, f])
|
||||
|
||||
X_tr_e = torch.FloatTensor(X_train_e).to(DEVICE)
|
||||
y_tr_e = torch.FloatTensor(y_train_e).to(DEVICE)
|
||||
X_va_e = torch.FloatTensor(X_val_e).to(DEVICE)
|
||||
y_va_e = torch.FloatTensor(y_val_e).to(DEVICE)
|
||||
X_te_e = torch.FloatTensor(X_test_e).to(DEVICE)
|
||||
|
||||
model_eth = FractalLSTM(input_size=n_features, hidden_size=64, num_layers=2, dropout=0.3).to(DEVICE)
|
||||
opt_e = torch.optim.Adam(model_eth.parameters(), lr=LR, weight_decay=1e-5)
|
||||
ds_e = TensorDataset(X_tr_e, y_tr_e)
|
||||
dl_e = DataLoader(ds_e, batch_size=BATCH_SIZE, shuffle=True)
|
||||
sch_e = torch.optim.lr_scheduler.ReduceLROnPlateau(opt_e, patience=5, factor=0.5)
|
||||
|
||||
best_e = 0
|
||||
pc = 0
|
||||
for epoch in range(EPOCHS):
|
||||
model_eth.train()
|
||||
tl = 0
|
||||
for xb, yb in dl_e:
|
||||
opt_e.zero_grad()
|
||||
p = model_eth(xb)
|
||||
loss = criterion(p, yb)
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model_eth.parameters(), 1.0)
|
||||
opt_e.step()
|
||||
tl += loss.item()
|
||||
|
||||
model_eth.eval()
|
||||
with torch.no_grad():
|
||||
vp = model_eth(X_va_e)
|
||||
vl = criterion(vp, y_va_e).item()
|
||||
va = np.mean((torch.sigmoid(vp).cpu().numpy() > 0.5) == y_val_e)
|
||||
|
||||
sch_e.step(vl)
|
||||
if va > best_e:
|
||||
best_e = va
|
||||
torch.save(model_eth.state_dict(), "data/processed/best_lstm_eth.pt")
|
||||
pc = 0
|
||||
else:
|
||||
pc += 1
|
||||
if epoch % 5 == 0:
|
||||
print(f" Epoch {epoch:2d}: val_acc={va*100:.1f}% best={best_e*100:.1f}%")
|
||||
if pc > 10:
|
||||
break
|
||||
|
||||
model_eth.load_state_dict(torch.load("data/processed/best_lstm_eth.pt", weights_only=True))
|
||||
model_eth.eval()
|
||||
with torch.no_grad():
|
||||
tp_e = torch.sigmoid(model_eth(X_te_e)).cpu().numpy()
|
||||
|
||||
print(f"\nETH Test accuracy: {np.mean((tp_e > 0.5) == y_test_e)*100:.1f}%")
|
||||
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(X_test_e)):
|
||||
p = tp_e[j]
|
||||
i = idx_test_e[j]
|
||||
actual = (close_eth[i + LOOKAHEAD - 1] - close_eth[i - 1]) / close_eth[i - 1]
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.3)
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.3)
|
||||
if accs:
|
||||
print(f" thr={thr:.2f}: trades={len(accs):5d} acc={np.mean(accs)*100:.1f}% ret={(capital-1000)/10:+.1f}%")
|
||||
@@ -0,0 +1,290 @@
|
||||
"""Strategia 8: Ensemble multi-timeframe.
|
||||
Combina i migliori approcci:
|
||||
1. GBM su structural features (miglior singolo finora: 58.6%/+57.5%)
|
||||
2. GBM su fractal indicators
|
||||
3. Multi-timeframe: 1h features + 15m aggregati
|
||||
Vota con consensus: trade solo quando almeno 2/3 modelli concordano.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 8: ENSEMBLE MULTI-TF — BTC")
|
||||
print("=" * 60)
|
||||
|
||||
# Load both timeframes
|
||||
df_1h = load_data("BTC", "1h")
|
||||
df_15m = load_data("BTC", "15m")
|
||||
|
||||
close_1h = df_1h["close"].values
|
||||
ts_1h = df_1h["timestamp"].values
|
||||
|
||||
WINDOW_1H = 24
|
||||
LOOKAHEAD = 6
|
||||
MIN_RETURN = 0.003
|
||||
|
||||
def structural_features_1h(df: pd.DataFrame, i: int, window: int = 24) -> np.ndarray | None:
|
||||
if i < window:
|
||||
return None
|
||||
|
||||
o = df["open"].values[i - window : i]
|
||||
h = df["high"].values[i - window : i]
|
||||
l = df["low"].values[i - window : i]
|
||||
c = df["close"].values[i - window : i]
|
||||
v = df["volume"].values[i - window : i]
|
||||
|
||||
all_p = np.concatenate([o, h, l, c])
|
||||
mn, mx = all_p.min(), all_p.max()
|
||||
if mx - mn == 0:
|
||||
return None
|
||||
o_n = (o - mn) / (mx - mn)
|
||||
h_n = (h - mn) / (mx - mn)
|
||||
l_n = (l - mn) / (mx - mn)
|
||||
c_n = (c - mn) / (mx - mn)
|
||||
|
||||
total = h - l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(c - o) / total
|
||||
u_shadow = (h - np.maximum(o, c)) / total
|
||||
l_shadow = (np.minimum(o, c) - l) / total
|
||||
direction = np.sign(c - o)
|
||||
|
||||
log_c = np.log(np.where(c == 0, 1e-10, c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
v_mean = np.mean(v)
|
||||
v_n = v / v_mean if v_mean > 0 else np.ones_like(v)
|
||||
|
||||
step = max(1, window // 12)
|
||||
idx = np.arange(0, window, step)[:12]
|
||||
|
||||
features = np.concatenate([
|
||||
c_n[idx], body[idx], direction[idx],
|
||||
u_shadow[idx], l_shadow[idx], v_n[idx],
|
||||
[np.mean(rets), np.std(rets), np.sum(rets),
|
||||
np.mean(body), np.std(body),
|
||||
np.max(body[-6:]) - np.min(body[-6:])],
|
||||
])
|
||||
return features
|
||||
|
||||
|
||||
def multi_tf_features(ts_current: int, df_15m: pd.DataFrame, n_bars: int = 48) -> np.ndarray | None:
|
||||
"""Extract aggregated features from 15m data aligned to current 1h candle."""
|
||||
ts_15m = df_15m["timestamp"].values
|
||||
mask = ts_15m <= ts_current
|
||||
end_idx = np.sum(mask)
|
||||
|
||||
if end_idx < n_bars:
|
||||
return None
|
||||
|
||||
start = end_idx - n_bars
|
||||
chunk = df_15m.iloc[start:end_idx]
|
||||
|
||||
c = chunk["close"].values
|
||||
h = chunk["high"].values
|
||||
l = chunk["low"].values
|
||||
v = chunk["volume"].values
|
||||
|
||||
if len(c) < n_bars:
|
||||
return None
|
||||
|
||||
log_c = np.log(np.where(c == 0, 1e-10, c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
# Micro-structure features
|
||||
mom_12 = np.sum(rets[-12:])
|
||||
mom_24 = np.sum(rets[-24:])
|
||||
vol_12 = np.std(rets[-12:])
|
||||
vol_48 = np.std(rets)
|
||||
|
||||
# Candle pattern stats
|
||||
ct = encode_candles(chunk)
|
||||
up_ratio_12 = np.mean(ct[-12:] == 1)
|
||||
up_ratio_24 = np.mean(ct[-24:] == 1)
|
||||
|
||||
# Intra-bar volatility (high-low range)
|
||||
ranges = (h - l) / np.where(c == 0, 1e-10, c)
|
||||
avg_range_12 = np.mean(ranges[-12:])
|
||||
avg_range_48 = np.mean(ranges)
|
||||
|
||||
# Volume profile
|
||||
v_mean = np.mean(v)
|
||||
v_recent = np.mean(v[-12:])
|
||||
vol_surge = v_recent / v_mean if v_mean > 0 else 1.0
|
||||
|
||||
# Autocorrelation
|
||||
if np.std(rets) > 0 and len(rets) > 1:
|
||||
ac1 = np.corrcoef(rets[:-1], rets[1:])[0, 1]
|
||||
ac1 = 0 if not np.isfinite(ac1) else ac1
|
||||
else:
|
||||
ac1 = 0
|
||||
|
||||
return np.array([
|
||||
mom_12, mom_24, vol_12, vol_48,
|
||||
up_ratio_12, up_ratio_24,
|
||||
avg_range_12, avg_range_48,
|
||||
vol_surge, ac1,
|
||||
vol_12 / vol_48 if vol_48 > 0 else 1.0,
|
||||
])
|
||||
|
||||
|
||||
print("Extracting features...")
|
||||
n_1h = len(df_1h)
|
||||
|
||||
X_struct = []
|
||||
X_multi = []
|
||||
y_all = []
|
||||
indices = []
|
||||
|
||||
for i in range(WINDOW_1H, n_1h - LOOKAHEAD, 1):
|
||||
if i % 5000 == 0:
|
||||
print(f" {i}/{n_1h}")
|
||||
|
||||
sf = structural_features_1h(df_1h, i, WINDOW_1H)
|
||||
if sf is None:
|
||||
continue
|
||||
|
||||
mf = multi_tf_features(ts_1h[i - 1], df_15m)
|
||||
if mf is None:
|
||||
continue
|
||||
|
||||
future_ret = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
if abs(future_ret) < MIN_RETURN:
|
||||
continue
|
||||
|
||||
X_struct.append(sf)
|
||||
X_multi.append(mf)
|
||||
y_all.append(1 if future_ret > 0 else 0)
|
||||
indices.append(i)
|
||||
|
||||
X_s = np.nan_to_num(np.array(X_struct), nan=0, posinf=1e6, neginf=-1e6)
|
||||
X_m = np.nan_to_num(np.array(X_multi), nan=0, posinf=1e6, neginf=-1e6)
|
||||
X_combined = np.hstack([X_s, X_m])
|
||||
y = np.array(y_all)
|
||||
idx_arr = np.array(indices)
|
||||
|
||||
print(f"\nSamples: {len(y)}, struct_feats: {X_s.shape[1]}, multi_feats: {X_m.shape[1]}, combined: {X_combined.shape[1]}")
|
||||
print(f"Up ratio: {np.mean(y)*100:.1f}%")
|
||||
|
||||
split = int(len(y) * 0.7)
|
||||
|
||||
# 3 models
|
||||
configs = {
|
||||
"M1_structural": X_s,
|
||||
"M2_multi_tf": X_m,
|
||||
"M3_combined": X_combined,
|
||||
}
|
||||
|
||||
probas = {}
|
||||
for name, X_data in configs.items():
|
||||
X_tr, X_te = X_data[:split], X_data[split:]
|
||||
y_tr, y_te = y[:split], y[split:]
|
||||
|
||||
sc = StandardScaler()
|
||||
X_tr_s = sc.fit_transform(X_tr)
|
||||
X_te_s = sc.transform(X_te)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=300, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.03, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
proba = model.predict_proba(X_te_s)
|
||||
up_idx = list(model.classes_).index(1)
|
||||
|
||||
probas[name] = proba[:, up_idx]
|
||||
|
||||
# Individual results
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(X_te)):
|
||||
p = proba[j][up_idx]
|
||||
i = idx_arr[split + j]
|
||||
actual = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.5)
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.5)
|
||||
|
||||
if accs:
|
||||
acc = np.mean(accs) * 100
|
||||
ret = (capital - 1000) / 1000 * 100
|
||||
test_days = (idx_arr[-1] - idx_arr[split]) / 24
|
||||
years = test_days / 365.25
|
||||
ann = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
print(f" {name:15s} thr={thr:.2f}: trades={len(accs):5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}%")
|
||||
|
||||
|
||||
# Ensemble voting
|
||||
print("\n\n--- ENSEMBLE VOTING ---")
|
||||
y_test = y[split:]
|
||||
idx_test = idx_arr[split:]
|
||||
|
||||
for min_agree in [2, 3]:
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(y_test)):
|
||||
votes_up = sum(1 for p in probas.values() if p[j] >= thr)
|
||||
votes_down = sum(1 for p in probas.values() if p[j] <= (1 - thr))
|
||||
|
||||
i = idx_test[j]
|
||||
actual = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
|
||||
if votes_up >= min_agree:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.5)
|
||||
elif votes_down >= min_agree:
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.5)
|
||||
|
||||
if accs:
|
||||
acc = np.mean(accs) * 100
|
||||
ret = (capital - 1000) / 1000 * 100
|
||||
test_days = (idx_test[-1] - idx_test[0]) / 24
|
||||
years = test_days / 365.25
|
||||
ann = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
trades_yr = len(accs) / years if years > 0 else 0
|
||||
print(f" agree>={min_agree} thr={thr:.2f}: trades={len(accs):5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% trades/yr={trades_yr:.0f}")
|
||||
|
||||
|
||||
# Average probability ensemble
|
||||
print("\n--- ENSEMBLE AVERAGE PROBABILITY ---")
|
||||
avg_proba = np.mean([p for p in probas.values()], axis=0)
|
||||
|
||||
for thr in [0.55, 0.60, 0.65, 0.70, 0.75]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(y_test)):
|
||||
p = avg_proba[j]
|
||||
i = idx_test[j]
|
||||
actual = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.5)
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.5)
|
||||
|
||||
if accs:
|
||||
acc = np.mean(accs) * 100
|
||||
ret = (capital - 1000) / 1000 * 100
|
||||
test_days = (idx_test[-1] - idx_test[0]) / 24
|
||||
years = test_days / 365.25
|
||||
ann = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
trades_yr = len(accs) / years if years > 0 else 0
|
||||
daily_ret = capital ** (1 / (test_days)) - 1 if test_days > 0 and capital > 0 else 0
|
||||
daily_pnl_on_1k = 1000 * daily_ret
|
||||
print(f" thr={thr:.2f}: trades={len(accs):5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% trades/yr={trades_yr:.0f} daily_pnl=€{daily_pnl_on_1k:.2f}")
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Strategia 9: Refined walk-forward with adaptive features.
|
||||
Combina le lezioni apprese:
|
||||
- Structural features (migliore singolo)
|
||||
- Walk-forward validation (no single split bias)
|
||||
- XGBoost (più potente di GBM per dati tabulari)
|
||||
- Dynamic exit: trailing stop + take profit
|
||||
- Multi-asset: BTC + ETH in portafoglio
|
||||
- Position sizing basato su confidenza
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
from src.fractal.indicators import hurst_exponent, volatility_ratio
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 9: WALK-FORWARD REFINATA")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def build_features(df: pd.DataFrame, i: int) -> np.ndarray | None:
|
||||
"""All features from structural + fractal, no leakage."""
|
||||
if i < 200:
|
||||
return None
|
||||
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
c = df["close"].values
|
||||
v = df["volume"].values
|
||||
|
||||
feats = []
|
||||
|
||||
# Structural features (3 windows)
|
||||
for w in [12, 24, 48]:
|
||||
win_c = c[i - w : i]
|
||||
win_o = o[i - w : i]
|
||||
win_h = h[i - w : i]
|
||||
win_l = l[i - w : i]
|
||||
win_v = v[i - w : i]
|
||||
|
||||
mn, mx = min(win_l.min(), win_o.min()), max(win_h.max(), win_o.max())
|
||||
if mx - mn == 0:
|
||||
feats.extend([0] * 15)
|
||||
continue
|
||||
|
||||
c_n = (win_c - mn) / (mx - mn)
|
||||
total = win_h - win_l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
|
||||
body = np.abs(win_c - win_o) / total
|
||||
direction = np.sign(win_c - win_o)
|
||||
|
||||
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
v_mean = np.mean(win_v)
|
||||
v_n = win_v / v_mean if v_mean > 0 else np.ones_like(win_v)
|
||||
|
||||
feats.extend([
|
||||
np.mean(rets),
|
||||
np.std(rets),
|
||||
np.sum(rets),
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body),
|
||||
np.std(body),
|
||||
np.mean(direction[-6:]),
|
||||
np.mean(direction),
|
||||
c_n[-1],
|
||||
np.mean(c_n[-6:]),
|
||||
v_n[-1],
|
||||
np.mean(v_n[-6:]),
|
||||
np.max(body[-6:]),
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
])
|
||||
|
||||
# Fractal features
|
||||
ret_long = np.diff(np.log(np.where(c[i-96:i] == 0, 1e-10, c[i-96:i])))
|
||||
if len(ret_long) > 20:
|
||||
h_exp = hurst_exponent(ret_long, max_lag=min(len(ret_long)//4, 20))
|
||||
else:
|
||||
h_exp = 0.5
|
||||
|
||||
feats.append(h_exp)
|
||||
feats.append(volatility_ratio(c[i-48:i], fast=12, slow=48))
|
||||
|
||||
# ATR
|
||||
tr_arr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
atr = np.mean(tr_arr[1:])
|
||||
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
|
||||
|
||||
# Price position relative to recent range
|
||||
high_48 = np.max(h[i-48:i])
|
||||
low_48 = np.min(l[i-48:i])
|
||||
range_48 = high_48 - low_48
|
||||
feats.append((c[i-1] - low_48) / range_48 if range_48 > 0 else 0.5)
|
||||
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
def walk_forward_backtest(
|
||||
df: pd.DataFrame,
|
||||
train_size: int = 10000,
|
||||
step_size: int = 2000,
|
||||
lookahead: int = 6,
|
||||
min_return: float = 0.003,
|
||||
threshold: float = 0.60,
|
||||
fee_pct: float = 0.001,
|
||||
position_pct: float = 0.3,
|
||||
) -> dict:
|
||||
"""Walk-forward validation with rolling train window."""
|
||||
close = df["close"].values
|
||||
n = len(df)
|
||||
|
||||
all_trades = []
|
||||
capital = 1000.0
|
||||
equity = [capital]
|
||||
|
||||
start = 200
|
||||
features_cache: dict[int, np.ndarray] = {}
|
||||
|
||||
def get_features(idx: int) -> np.ndarray | None:
|
||||
if idx not in features_cache:
|
||||
features_cache[idx] = build_features(df, idx)
|
||||
return features_cache[idx]
|
||||
|
||||
# Pre-compute all features
|
||||
print(" Pre-computing features...")
|
||||
for i in range(start, n - lookahead, 2):
|
||||
get_features(i)
|
||||
|
||||
fold = 0
|
||||
train_start = start
|
||||
total_signals = 0
|
||||
total_correct = 0
|
||||
|
||||
while train_start + train_size + step_size + lookahead < n:
|
||||
train_end = train_start + train_size
|
||||
test_end = min(train_end + step_size, n - lookahead)
|
||||
|
||||
# Build train set
|
||||
X_train, y_train = [], []
|
||||
for i in range(train_start, train_end, 2):
|
||||
f = get_features(i)
|
||||
if f is None:
|
||||
continue
|
||||
ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(ret) < min_return:
|
||||
continue
|
||||
X_train.append(f)
|
||||
y_train.append(1 if ret > 0 else 0)
|
||||
|
||||
if len(X_train) < 100:
|
||||
train_start += step_size
|
||||
continue
|
||||
|
||||
X_tr = np.array(X_train)
|
||||
y_tr = np.array(y_train)
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=200, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.05, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
|
||||
up_idx = list(model.classes_).index(1)
|
||||
|
||||
# Test on next step
|
||||
fold_trades = 0
|
||||
fold_correct = 0
|
||||
for i in range(train_end, test_end, 2):
|
||||
f = get_features(i)
|
||||
if f is None:
|
||||
continue
|
||||
|
||||
f_s = scaler.transform(f.reshape(1, -1))
|
||||
proba = model.predict_proba(f_s)[0]
|
||||
p_up = proba[up_idx]
|
||||
|
||||
actual_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(actual_ret) < min_return:
|
||||
continue
|
||||
|
||||
direction = None
|
||||
if p_up >= threshold:
|
||||
direction = "long"
|
||||
elif p_up <= (1 - threshold):
|
||||
direction = "short"
|
||||
|
||||
if direction:
|
||||
if direction == "long":
|
||||
trade_ret = actual_ret
|
||||
else:
|
||||
trade_ret = -actual_ret
|
||||
|
||||
net_ret = trade_ret - fee_pct * 2
|
||||
pnl = capital * position_pct * net_ret
|
||||
capital += pnl
|
||||
equity.append(capital)
|
||||
|
||||
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
|
||||
fold_trades += 1
|
||||
if is_correct:
|
||||
fold_correct += 1
|
||||
|
||||
all_trades.append({
|
||||
"fold": fold,
|
||||
"idx": i,
|
||||
"direction": direction,
|
||||
"prob": p_up,
|
||||
"actual_ret": actual_ret,
|
||||
"net_ret": net_ret,
|
||||
"pnl": pnl,
|
||||
"correct": is_correct,
|
||||
})
|
||||
|
||||
total_signals += fold_trades
|
||||
total_correct += fold_correct
|
||||
fold_acc = fold_correct / fold_trades * 100 if fold_trades > 0 else 0
|
||||
if fold % 3 == 0:
|
||||
print(f" Fold {fold}: trades={fold_trades} acc={fold_acc:.0f}% capital=€{capital:.0f}")
|
||||
|
||||
fold += 1
|
||||
train_start += step_size
|
||||
|
||||
# Results
|
||||
if not all_trades:
|
||||
return {"error": "no trades"}
|
||||
|
||||
trades_df = pd.DataFrame(all_trades)
|
||||
total_acc = total_correct / total_signals * 100 if total_signals > 0 else 0
|
||||
|
||||
test_candles = n - 200 - train_size
|
||||
test_days = test_candles / 24
|
||||
test_years = test_days / 365.25
|
||||
|
||||
ann_ret = ((capital / 1000) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
|
||||
# Max drawdown
|
||||
peak = equity[0]
|
||||
max_dd = 0
|
||||
for v in equity:
|
||||
if v > peak:
|
||||
peak = v
|
||||
dd = (peak - v) / peak if peak > 0 else 0
|
||||
if dd > max_dd:
|
||||
max_dd = dd
|
||||
|
||||
# Sharpe
|
||||
equity_arr = np.array(equity)
|
||||
rets = np.diff(equity_arr) / equity_arr[:-1]
|
||||
rets = rets[np.isfinite(rets)]
|
||||
sharpe = np.mean(rets) / np.std(rets) * np.sqrt(252 * 24) if np.std(rets) > 0 else 0
|
||||
|
||||
return {
|
||||
"total_trades": total_signals,
|
||||
"accuracy": total_acc,
|
||||
"total_return": (capital - 1000) / 1000 * 100,
|
||||
"annualized_return": ann_ret,
|
||||
"max_drawdown": max_dd * 100,
|
||||
"sharpe": sharpe,
|
||||
"final_capital": capital,
|
||||
"trades_per_year": total_signals / test_years if test_years > 0 else 0,
|
||||
"daily_pnl": (capital - 1000) / test_days if test_days > 0 else 0,
|
||||
"folds": fold,
|
||||
}
|
||||
|
||||
|
||||
# Run for both assets with parameter sweep
|
||||
for asset in ["BTC", "ETH"]:
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 1H — WALK-FORWARD")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
|
||||
for lookahead in [3, 6]:
|
||||
for threshold in [0.55, 0.60, 0.65, 0.70]:
|
||||
result = walk_forward_backtest(
|
||||
df,
|
||||
train_size=15000,
|
||||
step_size=3000,
|
||||
lookahead=lookahead,
|
||||
threshold=threshold,
|
||||
position_pct=0.3,
|
||||
)
|
||||
if "error" in result:
|
||||
continue
|
||||
|
||||
print(f"\n LA={lookahead} thr={threshold:.2f}: "
|
||||
f"trades={result['total_trades']:4d} "
|
||||
f"acc={result['accuracy']:.1f}% "
|
||||
f"ret={result['total_return']:+.1f}% "
|
||||
f"ann={result['annualized_return']:+.1f}% "
|
||||
f"dd={result['max_drawdown']:.1f}% "
|
||||
f"sharpe={result['sharpe']:.2f} "
|
||||
f"€/day={result['daily_pnl']:.2f}")
|
||||
@@ -0,0 +1,340 @@
|
||||
"""Strategia 10: High Precision (target >80% accuracy).
|
||||
Approccio: combina TUTTI gli indicatori disponibili, usa ensemble di 5 modelli,
|
||||
trade SOLO quando tutti concordano. Pochi trade ma molto precisi.
|
||||
Usa leva 3x per compensare bassa frequenza.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier, ExtraTreesClassifier
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.svm import SVC
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
from src.fractal.indicators import hurst_exponent, volatility_ratio
|
||||
|
||||
LEVERAGE = 3
|
||||
FEE_PCT = 0.001
|
||||
INITIAL_CAPITAL = 1000
|
||||
|
||||
|
||||
def build_rich_features(df: pd.DataFrame, i: int) -> np.ndarray | None:
|
||||
if i < 200:
|
||||
return None
|
||||
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
c = df["close"].values
|
||||
v = df["volume"].values
|
||||
|
||||
feats = []
|
||||
|
||||
for w in [6, 12, 24, 48, 96]:
|
||||
if i < w:
|
||||
feats.extend([0] * 18)
|
||||
continue
|
||||
|
||||
win_c = c[i - w : i]
|
||||
win_o = o[i - w : i]
|
||||
win_h = h[i - w : i]
|
||||
win_l = l[i - w : i]
|
||||
win_v = v[i - w : i]
|
||||
|
||||
mn, mx = win_l.min(), max(win_h.max(), win_c.max())
|
||||
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||
|
||||
total = win_h - win_l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(win_c - win_o) / total
|
||||
direction = np.sign(win_c - win_o)
|
||||
|
||||
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
v_mean = np.mean(win_v)
|
||||
|
||||
feats.extend([
|
||||
np.mean(rets) if len(rets) > 0 else 0,
|
||||
np.std(rets) if len(rets) > 0 else 0,
|
||||
np.sum(rets) if len(rets) > 0 else 0,
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body),
|
||||
np.std(body),
|
||||
np.mean(direction),
|
||||
np.mean(direction[-min(3, w):]),
|
||||
(win_c[-1] - mn) / rng,
|
||||
win_v[-1] / v_mean if v_mean > 0 else 1,
|
||||
np.max(body) - np.min(body),
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
np.max(rets) if len(rets) > 0 else 0,
|
||||
np.min(rets) if len(rets) > 0 else 0,
|
||||
np.mean(np.abs(rets)) if len(rets) > 0 else 0,
|
||||
np.sum(direction == 1) / w,
|
||||
np.sum(direction == -1) / w,
|
||||
])
|
||||
|
||||
# Hurst on different windows
|
||||
for w in [48, 96]:
|
||||
ret_w = np.diff(np.log(np.where(c[max(0,i-w):i] == 0, 1e-10, c[max(0,i-w):i])))
|
||||
if len(ret_w) > 20:
|
||||
feats.append(hurst_exponent(ret_w, max_lag=min(len(ret_w)//4, 15)))
|
||||
else:
|
||||
feats.append(0.5)
|
||||
|
||||
# Volatility ratios
|
||||
feats.append(volatility_ratio(c[max(0,i-48):i], fast=6, slow=48))
|
||||
feats.append(volatility_ratio(c[max(0,i-96):i], fast=12, slow=96))
|
||||
|
||||
# ATR normalized
|
||||
tr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
|
||||
|
||||
# Position in range
|
||||
h48 = np.max(h[i-48:i])
|
||||
l48 = np.min(l[i-48:i])
|
||||
r48 = h48 - l48
|
||||
feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
|
||||
|
||||
h96 = np.max(h[i-96:i])
|
||||
l96 = np.min(l[i-96:i])
|
||||
r96 = h96 - l96
|
||||
feats.append((c[i-1] - l96) / r96 if r96 > 0 else 0.5)
|
||||
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
def run_high_precision(asset: str, lookahead: int = 3):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 1H — HIGH PRECISION (LA={lookahead})")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
close = df["close"].values
|
||||
n = len(df)
|
||||
|
||||
MIN_RETURN = 0.003
|
||||
|
||||
# Build dataset
|
||||
print(" Building features...")
|
||||
X_all, y_all, idx_all = [], [], []
|
||||
for i in range(200, n - lookahead, 1):
|
||||
f = build_rich_features(df, i)
|
||||
if f is None:
|
||||
continue
|
||||
ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(ret) < MIN_RETURN:
|
||||
continue
|
||||
X_all.append(f)
|
||||
y_all.append(1 if ret > 0 else 0)
|
||||
idx_all.append(i)
|
||||
|
||||
X = np.array(X_all)
|
||||
y = np.array(y_all)
|
||||
idx_arr = np.array(idx_all)
|
||||
|
||||
print(f" Samples: {len(X)}, Features: {X.shape[1]}, Up: {np.mean(y)*100:.1f}%")
|
||||
|
||||
# Walk-forward with 5-model ensemble
|
||||
TRAIN_SIZE = 15000
|
||||
STEP_SIZE = 3000
|
||||
|
||||
models_config = [
|
||||
("GB1", GradientBoostingClassifier(n_estimators=200, max_depth=4, min_samples_leaf=30, learning_rate=0.05, subsample=0.8, random_state=42)),
|
||||
("GB2", GradientBoostingClassifier(n_estimators=300, max_depth=5, min_samples_leaf=50, learning_rate=0.03, subsample=0.7, random_state=123)),
|
||||
("RF", RandomForestClassifier(n_estimators=300, max_depth=8, min_samples_leaf=30, class_weight="balanced", random_state=42, n_jobs=-1)),
|
||||
("ET", ExtraTreesClassifier(n_estimators=300, max_depth=8, min_samples_leaf=30, class_weight="balanced", random_state=42, n_jobs=-1)),
|
||||
("LR", LogisticRegression(max_iter=1000, C=0.1, random_state=42)),
|
||||
]
|
||||
|
||||
capital = float(INITIAL_CAPITAL)
|
||||
all_trades = []
|
||||
equity = [capital]
|
||||
|
||||
fold = 0
|
||||
start = 0
|
||||
|
||||
while start + TRAIN_SIZE + STEP_SIZE < len(X):
|
||||
train_end = start + TRAIN_SIZE
|
||||
test_end = min(train_end + STEP_SIZE, len(X))
|
||||
|
||||
X_tr, y_tr = X[start:train_end], y[start:train_end]
|
||||
X_te, y_te = X[train_end:test_end], y[train_end:test_end]
|
||||
idx_te = idx_arr[train_end:test_end]
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
X_te_s = scaler.transform(X_te)
|
||||
|
||||
# Train all models
|
||||
trained = []
|
||||
for name, model in models_config:
|
||||
m = type(model)(**model.get_params())
|
||||
m.fit(X_tr_s, y_tr)
|
||||
trained.append((name, m))
|
||||
|
||||
# Test with consensus voting
|
||||
for j in range(len(X_te)):
|
||||
votes_up = 0
|
||||
votes_down = 0
|
||||
max_conf = 0
|
||||
|
||||
for name, m in trained:
|
||||
proba = m.predict_proba(X_te_s[j:j+1])[0]
|
||||
up_idx = list(m.classes_).index(1)
|
||||
p_up = proba[up_idx]
|
||||
|
||||
if p_up >= 0.60:
|
||||
votes_up += 1
|
||||
max_conf = max(max_conf, p_up)
|
||||
elif p_up <= 0.40:
|
||||
votes_down += 1
|
||||
max_conf = max(max_conf, 1 - p_up)
|
||||
|
||||
i = idx_te[j]
|
||||
actual_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
# Trade only with strong consensus
|
||||
min_votes = 4 # at least 4 out of 5 models agree
|
||||
direction = None
|
||||
if votes_up >= min_votes:
|
||||
direction = "long"
|
||||
elif votes_down >= min_votes:
|
||||
direction = "short"
|
||||
|
||||
if direction:
|
||||
if direction == "long":
|
||||
trade_ret = actual_ret
|
||||
else:
|
||||
trade_ret = -actual_ret
|
||||
|
||||
net_ret = trade_ret * LEVERAGE - FEE_PCT * 2 * LEVERAGE
|
||||
pos_size = 0.2 # 20% of capital per trade
|
||||
pnl = capital * pos_size * net_ret
|
||||
capital += pnl
|
||||
capital = max(capital, 0)
|
||||
equity.append(capital)
|
||||
|
||||
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
|
||||
all_trades.append({
|
||||
"fold": fold,
|
||||
"idx": i,
|
||||
"direction": direction,
|
||||
"votes_up": votes_up,
|
||||
"votes_down": votes_down,
|
||||
"actual_ret": actual_ret,
|
||||
"net_ret": net_ret,
|
||||
"pnl": pnl,
|
||||
"correct": is_correct,
|
||||
})
|
||||
|
||||
fold += 1
|
||||
start += STEP_SIZE
|
||||
|
||||
if not all_trades:
|
||||
print(" No trades generated!")
|
||||
return
|
||||
|
||||
trades_df = pd.DataFrame(all_trades)
|
||||
n_correct = trades_df["correct"].sum()
|
||||
n_total = len(trades_df)
|
||||
accuracy = n_correct / n_total * 100
|
||||
|
||||
test_candles = idx_arr[-1] - idx_arr[TRAIN_SIZE]
|
||||
test_days = test_candles / 24
|
||||
test_years = test_days / 365.25
|
||||
|
||||
ann_ret = ((capital / INITIAL_CAPITAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
daily_pnl = (capital - INITIAL_CAPITAL) / test_days if test_days > 0 else 0
|
||||
|
||||
# Max DD
|
||||
peak = equity[0]
|
||||
max_dd = 0
|
||||
for v in equity:
|
||||
if v > peak:
|
||||
peak = v
|
||||
dd = (peak - v) / peak if peak > 0 else 0
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
print(f"\n RISULTATI:")
|
||||
print(f" Trades: {n_total}")
|
||||
print(f" Accuracy: {accuracy:.1f}%")
|
||||
print(f" Return: {(capital-INITIAL_CAPITAL)/INITIAL_CAPITAL*100:+.1f}%")
|
||||
print(f" Annualized: {ann_ret:+.1f}%")
|
||||
print(f" Max Drawdown: {max_dd*100:.1f}%")
|
||||
print(f" Capital: €{capital:.0f}")
|
||||
print(f" Trades/year: {n_total/test_years:.0f}")
|
||||
print(f" €/day avg: €{daily_pnl:.2f}")
|
||||
|
||||
# Consensus threshold sweep
|
||||
print(f"\n --- CONSENSUS SWEEP ---")
|
||||
for min_v in [3, 4, 5]:
|
||||
for ind_thr in [0.55, 0.60, 0.65]:
|
||||
cap = float(INITIAL_CAPITAL)
|
||||
trades_count = 0
|
||||
correct_count = 0
|
||||
eq = [cap]
|
||||
|
||||
fold_s = 0
|
||||
start_s = 0
|
||||
while start_s + TRAIN_SIZE + STEP_SIZE < len(X):
|
||||
train_end_s = start_s + TRAIN_SIZE
|
||||
test_end_s = min(train_end_s + STEP_SIZE, len(X))
|
||||
|
||||
X_tr_s2 = scaler.fit_transform(X[start_s:train_end_s])
|
||||
X_te_s2 = scaler.transform(X[train_end_s:test_end_s])
|
||||
y_tr_s2 = y[start_s:train_end_s]
|
||||
idx_te_s2 = idx_arr[train_end_s:test_end_s]
|
||||
|
||||
trained_s = []
|
||||
for name, model in models_config:
|
||||
m2 = type(model)(**model.get_params())
|
||||
m2.fit(X_tr_s2, y_tr_s2)
|
||||
trained_s.append(m2)
|
||||
|
||||
for j in range(len(X_te_s2)):
|
||||
vu = sum(1 for m2 in trained_s
|
||||
if m2.predict_proba(X_te_s2[j:j+1])[0][list(m2.classes_).index(1)] >= ind_thr)
|
||||
vd = sum(1 for m2 in trained_s
|
||||
if m2.predict_proba(X_te_s2[j:j+1])[0][list(m2.classes_).index(1)] <= (1-ind_thr))
|
||||
|
||||
i_s = idx_te_s2[j]
|
||||
ar = (close[i_s + lookahead - 1] - close[i_s - 1]) / close[i_s - 1]
|
||||
|
||||
d = None
|
||||
if vu >= min_v:
|
||||
d = "long"
|
||||
elif vd >= min_v:
|
||||
d = "short"
|
||||
|
||||
if d:
|
||||
tr = ar if d == "long" else -ar
|
||||
nr = tr * LEVERAGE - FEE_PCT * 2 * LEVERAGE
|
||||
cap += cap * 0.2 * nr
|
||||
cap = max(cap, 0)
|
||||
eq.append(cap)
|
||||
trades_count += 1
|
||||
if (d == "long" and ar > 0) or (d == "short" and ar < 0):
|
||||
correct_count += 1
|
||||
|
||||
start_s += STEP_SIZE
|
||||
|
||||
if trades_count > 0:
|
||||
acc_s = correct_count / trades_count * 100
|
||||
ret_s = (cap - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
ann_s = ((cap / INITIAL_CAPITAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and cap > 0 else -100
|
||||
dpnl = (cap - INITIAL_CAPITAL) / test_days if test_days > 0 else 0
|
||||
print(f" min_votes={min_v} ind_thr={ind_thr:.2f}: trades={trades_count:4d} acc={acc_s:.1f}% ret={ret_s:+.1f}% ann={ann_s:+.1f}% €/day={dpnl:.2f}")
|
||||
|
||||
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for la in [3, 6]:
|
||||
run_high_precision(asset, la)
|
||||
Reference in New Issue
Block a user