chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (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