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