Files
Adriano 0e47956f7a 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>
2026-05-27 23:01:36 +02:00

321 lines
10 KiB
Python

"""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}%")