"""Strategia 9: Refined walk-forward with adaptive features.
Combina le lezioni apprese:
- Structural features (migliore singolo)
- Walk-forward validation (no single split bias)
- XGBoost (più potente di GBM per dati tabulari)
- Dynamic exit: trailing stop + take profit
- Multi-asset: BTC + ETH in portafoglio
- Position sizing basato su confidenza
"""
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
from src.fractal.indicators import hurst_exponent, volatility_ratio

print("=" * 60)
print("  STRATEGIA 9: WALK-FORWARD REFINATA")
print("=" * 60)


def build_features(df: pd.DataFrame, i: int) -> np.ndarray | None:
    """All features from structural + fractal, no leakage."""
    if i < 200:
        return None

    o = df["open"].values
    h = df["high"].values
    l = df["low"].values
    c = df["close"].values
    v = df["volume"].values

    feats = []

    # Structural features (3 windows)
    for w in [12, 24, 48]:
        win_c = c[i - w : i]
        win_o = o[i - w : i]
        win_h = h[i - w : i]
        win_l = l[i - w : i]
        win_v = v[i - w : i]

        mn, mx = min(win_l.min(), win_o.min()), max(win_h.max(), win_o.max())
        if mx - mn == 0:
            feats.extend([0] * 15)
            continue

        c_n = (win_c - mn) / (mx - mn)
        total = win_h - win_l
        total = np.where(total == 0, 1e-10, total)

        body = np.abs(win_c - win_o) / total
        direction = np.sign(win_c - win_o)

        log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
        rets = np.diff(log_c)

        v_mean = np.mean(win_v)
        v_n = win_v / v_mean if v_mean > 0 else np.ones_like(win_v)

        feats.extend([
            np.mean(rets),
            np.std(rets),
            np.sum(rets),
            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[-6:]),
            np.mean(direction),
            c_n[-1],
            np.mean(c_n[-6:]),
            v_n[-1],
            np.mean(v_n[-6:]),
            np.max(body[-6:]),
            np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
        ])

    # Fractal features
    ret_long = np.diff(np.log(np.where(c[i-96:i] == 0, 1e-10, c[i-96:i])))
    if len(ret_long) > 20:
        h_exp = hurst_exponent(ret_long, max_lag=min(len(ret_long)//4, 20))
    else:
        h_exp = 0.5

    feats.append(h_exp)
    feats.append(volatility_ratio(c[i-48:i], fast=12, slow=48))

    # ATR
    tr_arr = 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))))
    atr = np.mean(tr_arr[1:])
    feats.append(atr / c[i-1] if c[i-1] > 0 else 0)

    # Price position relative to recent range
    high_48 = np.max(h[i-48:i])
    low_48 = np.min(l[i-48:i])
    range_48 = high_48 - low_48
    feats.append((c[i-1] - low_48) / range_48 if range_48 > 0 else 0.5)

    return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)


def walk_forward_backtest(
    df: pd.DataFrame,
    train_size: int = 10000,
    step_size: int = 2000,
    lookahead: int = 6,
    min_return: float = 0.003,
    threshold: float = 0.60,
    fee_pct: float = 0.001,
    position_pct: float = 0.3,
) -> dict:
    """Walk-forward validation with rolling train window."""
    close = df["close"].values
    n = len(df)

    all_trades = []
    capital = 1000.0
    equity = [capital]

    start = 200
    features_cache: dict[int, np.ndarray] = {}

    def get_features(idx: int) -> np.ndarray | None:
        if idx not in features_cache:
            features_cache[idx] = build_features(df, idx)
        return features_cache[idx]

    # Pre-compute all features
    print("  Pre-computing features...")
    for i in range(start, n - lookahead, 2):
        get_features(i)

    fold = 0
    train_start = start
    total_signals = 0
    total_correct = 0

    while train_start + train_size + step_size + lookahead < n:
        train_end = train_start + train_size
        test_end = min(train_end + step_size, n - lookahead)

        # Build train set
        X_train, y_train = [], []
        for i in range(train_start, train_end, 2):
            f = get_features(i)
            if f is None:
                continue
            ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
            if abs(ret) < min_return:
                continue
            X_train.append(f)
            y_train.append(1 if ret > 0 else 0)

        if len(X_train) < 100:
            train_start += step_size
            continue

        X_tr = np.array(X_train)
        y_tr = np.array(y_train)

        scaler = StandardScaler()
        X_tr_s = scaler.fit_transform(X_tr)

        model = GradientBoostingClassifier(
            n_estimators=200, max_depth=5, min_samples_leaf=30,
            learning_rate=0.05, subsample=0.8, random_state=42,
        )
        model.fit(X_tr_s, y_tr)

        up_idx = list(model.classes_).index(1)

        # Test on next step
        fold_trades = 0
        fold_correct = 0
        for i in range(train_end, test_end, 2):
            f = get_features(i)
            if f is None:
                continue

            f_s = scaler.transform(f.reshape(1, -1))
            proba = model.predict_proba(f_s)[0]
            p_up = proba[up_idx]

            actual_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
            if abs(actual_ret) < min_return:
                continue

            direction = None
            if p_up >= threshold:
                direction = "long"
            elif p_up <= (1 - threshold):
                direction = "short"

            if direction:
                if direction == "long":
                    trade_ret = actual_ret
                else:
                    trade_ret = -actual_ret

                net_ret = trade_ret - fee_pct * 2
                pnl = capital * position_pct * net_ret
                capital += pnl
                equity.append(capital)

                is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
                fold_trades += 1
                if is_correct:
                    fold_correct += 1

                all_trades.append({
                    "fold": fold,
                    "idx": i,
                    "direction": direction,
                    "prob": p_up,
                    "actual_ret": actual_ret,
                    "net_ret": net_ret,
                    "pnl": pnl,
                    "correct": is_correct,
                })

        total_signals += fold_trades
        total_correct += fold_correct
        fold_acc = fold_correct / fold_trades * 100 if fold_trades > 0 else 0
        if fold % 3 == 0:
            print(f"  Fold {fold}: trades={fold_trades} acc={fold_acc:.0f}% capital=€{capital:.0f}")

        fold += 1
        train_start += step_size

    # Results
    if not all_trades:
        return {"error": "no trades"}

    trades_df = pd.DataFrame(all_trades)
    total_acc = total_correct / total_signals * 100 if total_signals > 0 else 0

    test_candles = n - 200 - train_size
    test_days = test_candles / 24
    test_years = test_days / 365.25

    ann_ret = ((capital / 1000) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100

    # Max drawdown
    peak = equity[0]
    max_dd = 0
    for v in equity:
        if v > peak:
            peak = v
        dd = (peak - v) / peak if peak > 0 else 0
        if dd > max_dd:
            max_dd = dd

    # Sharpe
    equity_arr = np.array(equity)
    rets = np.diff(equity_arr) / equity_arr[:-1]
    rets = rets[np.isfinite(rets)]
    sharpe = np.mean(rets) / np.std(rets) * np.sqrt(252 * 24) if np.std(rets) > 0 else 0

    return {
        "total_trades": total_signals,
        "accuracy": total_acc,
        "total_return": (capital - 1000) / 1000 * 100,
        "annualized_return": ann_ret,
        "max_drawdown": max_dd * 100,
        "sharpe": sharpe,
        "final_capital": capital,
        "trades_per_year": total_signals / test_years if test_years > 0 else 0,
        "daily_pnl": (capital - 1000) / test_days if test_days > 0 else 0,
        "folds": fold,
    }


# Run for both assets with parameter sweep
for asset in ["BTC", "ETH"]:
    print(f"\n{'#'*60}")
    print(f"  {asset} 1H — WALK-FORWARD")
    print(f"{'#'*60}")

    df = load_data(asset, "1h")

    for lookahead in [3, 6]:
        for threshold in [0.55, 0.60, 0.65, 0.70]:
            result = walk_forward_backtest(
                df,
                train_size=15000,
                step_size=3000,
                lookahead=lookahead,
                threshold=threshold,
                position_pct=0.3,
            )
            if "error" in result:
                continue

            print(f"\n  LA={lookahead} thr={threshold:.2f}: "
                  f"trades={result['total_trades']:4d} "
                  f"acc={result['accuracy']:.1f}% "
                  f"ret={result['total_return']:+.1f}% "
                  f"ann={result['annualized_return']:+.1f}% "
                  f"dd={result['max_drawdown']:.1f}% "
                  f"sharpe={result['sharpe']:.2f} "
                  f"€/day={result['daily_pnl']:.2f}")
