"""ML01 — Squeeze + GBM (Gradient Boosting Machine) Walk-Forward.

Strategia ibrida: squeeze breakout come pre-filtro (QUANDO tradare),
GradientBoosting su features strutturali come conferma (QUALE direzione).

Pipeline:
    1. Rileva squeeze release (Bollinger esce da Keltner)
    2. Estrai 44 features dalla finestra (structural multi-window + squeeze
       metadata + price position + ATR + momentum breakout)
    3. GBM walk-forward: train su 50% rolling, step 10%, predice direzione
    4. Trade solo se ML ha confidenza ≥ ml_threshold

IN:
    - OHLCV DataFrame
    - Parametri: bb_window (14), sq_threshold (0.8), brk_bars (3),
                 ml_threshold (0.70), leverage (3), position_pct (0.15)

OUT:
    - BacktestResult con metriche walk-forward (no data leakage)
    - Solo periodo di test (seconda metà dati)

Risultati tipici:
    ETH 15m bb14 ml=0.70: 76.9% acc, 1213 trades, DD 4.2%, €13.78/day
    BTC 15m bb14 ml=0.70: 78.8% acc, 1964 trades, DD 7.0%, €5.51/day
    BTC 1h  bb14 ml=0.70: 77.3% acc,  617 trades, DD 6.7%, €3.85/day

Note:
    - GBM = GradientBoostingClassifier di scikit-learn
    - Walk-forward: nessun look-ahead, train sempre prima di test
    - Il baseline squeeze puro ha accuracy più alta (~79.5%) ma DD peggiore
    - Il valore del ML è filtrare breakout deboli → DD ridotto
"""
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.strategies.base import Strategy, Signal, BacktestResult, YearlyStats, TF_MINUTES
from src.strategies.indicators import keltner_ratio, detect_squeezes
from src.data.downloader import load_data


def _build_features(df: pd.DataFrame, i: int, squeeze_info: dict) -> np.ndarray | None:
    """44 features per il punto di squeeze release."""
    if i < 100:
        return None
    o, h, l, c, v = (df["open"].values, df["high"].values, df["low"].values,
                      df["close"].values, df["volume"].values)
    feats = []
    for w in [12, 24, 48]:
        wc, wo = c[i-w:i], o[i-w:i]
        wh, wl, wv = h[i-w:i], l[i-w:i], v[i-w:i]
        mn, mx = wl.min(), max(wh.max(), wc.max())
        rng = mx - mn if mx - mn > 0 else 1e-10
        total = np.where(wh - wl == 0, 1e-10, wh - wl)
        body = np.abs(wc - wo) / total
        direction = np.sign(wc - wo)
        log_c = np.log(np.where(wc == 0, 1e-10, wc))
        rets = np.diff(log_c)
        v_mean = np.mean(wv)
        feats.extend([
            np.mean(rets) if len(rets) > 0 else 0,
            np.std(rets) if len(rets) > 0 else 0,
            np.sum(rets) if len(rets) > 0 else 0,
            float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
            float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
            np.mean(body), np.std(body),
            np.mean(direction), np.mean(direction[-min(3, w):]),
            (wc[-1] - mn) / rng,
            wv[-1] / v_mean if v_mean > 0 else 1,
            np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
        ])
    sq = squeeze_info
    feats.extend([
        sq["dur"], sq["dur"] / 24, sq["kcr_at_release"],
        v[i-1] / sq.get("avg_vol", 1) if sq.get("avg_vol", 0) > 0 else 1,
        np.mean(v[i:min(i+3, len(v))]) / sq.get("avg_vol", 1) if sq.get("avg_vol", 0) > 0 else 1,
    ])
    h48, l48 = np.max(h[max(0, i-48):i]), np.min(l[max(0, i-48):i])
    r48 = h48 - l48
    feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
    tr = np.maximum(h[i-14:i] - l[i-14:i],
                    np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
                               np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
    feats.append(np.mean(tr[1:]) / c[i-1] if c[i-1] > 0 else 0)
    feats.append((c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0)
    return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)


class SqueezeGBM(Strategy):
    name = "ML01_squeeze_gbm"
    description = "Squeeze + GBM walk-forward — ML filtra breakout deboli"
    default_assets = ["BTC", "ETH"]
    default_timeframes = ["15m", "1h"]
    fee_ml = 0.001

    def generate_signals(self, df, ts, **params):
        raise NotImplementedError("ML01 usa backtest custom con walk-forward")

    def backtest(self, asset: str, tf: str, hold: int = 3, **params) -> BacktestResult | None:
        bb_w = params.get("bb_window", 14)
        sq_thr = params.get("sq_threshold", 0.8 if tf == "1h" else 0.9)
        brk = params.get("brk_bars", hold)
        ml_thr = params.get("ml_threshold", 0.70)
        lev = params.get("leverage", self.leverage)
        pos = params.get("position_pct", self.position_size)

        df = load_data(asset, tf)
        close = df["close"].values
        high = df["high"].values
        low = df["low"].values
        volume = df["volume"].values
        n = len(df)
        ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)

        kcr = keltner_ratio(close, high, low, bb_w)
        raw_events = detect_squeezes(close, high, low, kcr, sq_thr)

        # Aggiungi avg_vol a ogni evento
        events = []
        for ev in raw_events:
            ev["avg_vol"] = float(np.mean(volume[ev["sq_start"]:ev["idx"]]))
            events.append(ev)

        X_all, y_all, ev_all = [], [], []
        for ev in events:
            i = ev["idx"]
            if i + brk >= n or i < 100:
                continue
            feats = _build_features(df, i, ev)
            if feats is None:
                continue
            actual_ret = (close[i + brk - 1] - close[i - 1]) / close[i - 1]
            X_all.append(feats)
            y_all.append(1 if actual_ret > 0 else 0)
            ev_all.append(ev)

        if len(X_all) < 50:
            return None

        X, y = np.array(X_all), np.array(y_all)
        TRAIN_SIZE = max(int(len(X) * 0.5), 50)
        STEP_SIZE = max(int(len(X) * 0.1), 10)

        yearly: dict[int, dict] = {}
        capital = float(self.initial_capital)
        peak = capital
        max_dd = 0.0
        total_bars = 0
        all_t = all_w = 0

        start = 0
        while start + TRAIN_SIZE + STEP_SIZE <= len(X):
            train_end = start + TRAIN_SIZE
            test_end = min(train_end + STEP_SIZE, len(X))
            X_tr, y_tr = X[start:train_end], y[start:train_end]
            X_te = X[train_end:test_end]

            if len(np.unique(y_tr)) < 2:
                start += STEP_SIZE
                continue

            scaler = StandardScaler()
            X_tr_s = scaler.fit_transform(X_tr)
            X_te_s = scaler.transform(X_te)

            model = GradientBoostingClassifier(
                n_estimators=150, max_depth=4, min_samples_leaf=10,
                learning_rate=0.05, subsample=0.8, random_state=42,
            )
            model.fit(X_tr_s, y_tr)
            up_idx = list(model.classes_).index(1) if 1 in model.classes_ else -1
            if up_idx < 0:
                start += STEP_SIZE
                continue

            for j in range(len(X_te)):
                proba = model.predict_proba(X_te_s[j:j+1])[0]
                p_up = proba[up_idx]
                ev = ev_all[train_end + j]
                i = ev["idx"]
                actual_ret = (close[i + brk - 1] - close[i - 1]) / close[i - 1]

                if p_up >= ml_thr:
                    direction = 1
                elif p_up <= (1 - ml_thr):
                    direction = -1
                else:
                    continue

                is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0)
                trade_ret = actual_ret * direction
                net = trade_ret * lev - self.fee_ml * 2 * lev
                capital += capital * pos * net
                capital = max(capital, 10)
                if capital > peak:
                    peak = capital
                dd = (peak - capital) / peak
                max_dd = max(max_dd, dd)
                total_bars += brk

                all_t += 1
                if is_correct:
                    all_w += 1

                year = ts.iloc[i].year
                if year not in yearly:
                    yearly[year] = {"w": 0, "t": 0, "pnl": 0.0}
                yearly[year]["t"] += 1
                if is_correct:
                    yearly[year]["w"] += 1
                yearly[year]["pnl"] += net * self.initial_capital

            start += STEP_SIZE

        if all_t == 0:
            return None

        yearly_stats = [
            YearlyStats(year=y, trades=d["t"], wins=d["w"], pnl=d["pnl"])
            for y, d in sorted(yearly.items())
        ]

        return BacktestResult(
            strategy_name=self.name,
            asset=asset,
            timeframe=tf,
            params={"bb_w": bb_w, "sq_thr": sq_thr, "ml_thr": ml_thr,
                    "brk": brk, "lev": lev, "pos": pos},
            trades=all_t,
            wins=all_w,
            pnl=sum(d["pnl"] for d in yearly.values()),
            capital=capital,
            initial_capital=self.initial_capital,
            max_dd=max_dd * 100,
            time_in_market_pct=total_bars / n * 100,
            avg_trade_duration_h=brk * TF_MINUTES.get(tf, 60) / 60,
            years_active=len(yearly),
            yearly=yearly_stats,
        )


if __name__ == "__main__":
    strategy = SqueezeGBM()
    print("Training ML models...\n")
    results = []
    for asset in ["ETH", "BTC"]:
        for tf in ["15m", "1h"]:
            for ml_thr in [0.65, 0.70]:
                r = strategy.backtest(asset, tf, ml_threshold=ml_thr)
                if r and r.trades >= 20:
                    r.strategy_name = f"ML01 {asset} {tf} ml={ml_thr}"
                    results.append(r)
    results.sort(key=lambda r: r.accuracy, reverse=True)

    print(f"{'=' * 120}")
    print(f"  ML01 SQUEEZE+GBM — RISULTATI")
    print(f"{'=' * 120}")
    for r in results:
        r.print_summary()
    if results:
        results[0].print_yearly()
