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,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}%")
|
||||
Reference in New Issue
Block a user