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