#!/usr/bin/env python3
"""
═══════════════════════════════════════════════════════════════════════════════
 CRASH-TEST LSTM REVERSAL PREDICTOR — Backtest sur 2 semaines
═══════════════════════════════════════════════════════════════════════════════

Module INDÉPENDANT du bot de trading.
Télécharge les données historiques 5m des 14 derniers jours via l'API Binance,
exécute le LSTM Reversal Predictor sur chaque bougie, simule des trades et
produit un rapport de rentabilité détaillé.

Métriques:
  - Win rate, P&L total, P&L moyen par trade
  - Sharpe ratio, max drawdown, profit factor
  - Comparaison LSTM seul vs Buy & Hold
  - Analyse par symbole, par heure, par classe de signal
  - Courbe d'equity en ASCII

Usage:
    python backtest_lstm_reversal.py
    python backtest_lstm_reversal.py --symbols ETHUSDT BTCUSDT SOLUSDT
    python backtest_lstm_reversal.py --days 7

Auteur: IA Trading Bot — Crash Test Module
Date: 01/03/2026
"""

# ══ FORCE CPU AVANT TOUT IMPORT TORCH (évite conflits CUDA avec le bot) ══
import os
os.environ['CUDA_VISIBLE_DEVICES'] = ''

import sys
import json
import time
import io
import requests
import argparse
import numpy as np
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from collections import defaultdict

# Force UTF-8 output on Windows (avoid cp1252 encoding errors)
if sys.stdout.encoding != 'utf-8':
    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
if sys.stderr.encoding != 'utf-8':
    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

# ═══════════════════════════════════════════════════════════════════════════════
# CONFIGURATION
# ═══════════════════════════════════════════════════════════════════════════════

BASE_URL = "https://api.binance.com"
INTERVAL = "5m"  # Bougies 5 minutes (comme le bot)
BACKTEST_DAYS = 14  # 2 semaines

# Paramètres de trading simulé
INITIAL_CAPITAL = 5000.0  # USDT
POSITION_SIZE_PCT = 5.0   # 5% du capital par trade
MAX_OPEN_POSITIONS = 5
# 🔧 FIX 01/03 v2: SL 2.5→1.8%, TP 4.0→5.0% — ratio G/P était 0.57:1 (catastrophique)
# SL trop large = -601$ (91 trades, 0% WR). Avec SL=1.8% et TP=5%, R/R = 2.78:1
STOP_LOSS_PCT = 1.8       # %
TAKE_PROFIT_PCT = 5.0     # %
# 🔧 FIX 01/03: Trailing trop agressif = -284$ de pertes (WR 21.8%)
# Activation relevée à 2.5% et distance réduite à 1.5%
TRAILING_STOP_PCT = 1.5   # % (était 2.5 — trop large après activation précoce)
TRAILING_ACTIVATION_PCT = 2.5  # Activation trailing après +2.5% de profit (était 1.0%)

# Seuils LSTM pour signaux
# 🔧 FIX 01/03 v7: Confiance 55→65%, Prob 0.40→0.50 — filtrer faux signaux (modèle jeune)
REVERSAL_UP_MIN_CONFIDENCE = 65.0   # % min confiance pour acheter (était 55)
REVERSAL_UP_MIN_PROB = 0.50         # Probabilité min (était 0.40)
# 🔧 FIX 01/03 v2: LSTM_DANGER trop agressif → 440 exits à +0.33$/trade (gain ridicule)
# Confidence 50→70% et prob 0.35→0.50 pour éviter les fermetures prématurées
REVERSAL_DOWN_MIN_CONFIDENCE = 70.0
REVERSAL_DOWN_MIN_PROB = 0.50

# 🔧 FIX 01/03 v5: Max holding time — éviter les trades dormants qui dérivent vers SL
# Crash-test: trades SL souvent après 4-5h de holding → 4h max coupe plus tôt
MAX_HOLDING_CANDLES = 48  # 48 × 5min = 4h

# Frais Binance
TRADING_FEE_PCT = 0.1  # 0.1% par trade (maker/taker moyen)

# 🔧 FIX 01/03: Filtre horaire — bloquer achats pendant heures toxiques
# 🔧 FIX 02/03: Réduit de 14h bloquées → 3h seulement (3-5h UTC = dead zone Asia/Europe)
# Raison: 58% des heures bloquées = manque tous les pumps (SHIB +8%, PUMP +16%, ZRO +7%)
BLOCKED_HOURS_UTC = {3, 4, 5}  # Heures UTC à éviter (dead zone seulement)
ENABLE_HOUR_FILTER = True

# Symboles par défaut (top volume + diversité)
# 🔧 FIX 01/03 v3: Retiré FETUSDT, DOTUSDT, NEARUSDT (toxiques persistants)
# 🔧 FIX 01/03 v4: Retiré ATOMUSDT (-8.29$, 2 SL massifs)
DEFAULT_SYMBOLS = [
    "BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "ADAUSDT",
    "AVAXUSDT", "LINKUSDT",
    "XLMUSDT", "SEIUSDT", "JUPUSDT",
    "DASHUSDT", "APTUSDT", "ICPUSDT", "INJUSDT",
]

# Couleurs console
class C:
    G = '\033[92m'   # Green
    R = '\033[91m'   # Red
    Y = '\033[93m'   # Yellow
    B = '\033[94m'   # Blue
    CY = '\033[96m'  # Cyan
    M = '\033[95m'   # Magenta
    W = '\033[97m'   # White
    DIM = '\033[2m'  # Dim
    BOLD = '\033[1m' # Bold
    END = '\033[0m'  # Reset


# ═══════════════════════════════════════════════════════════════════════════════
# DATA FETCHING
# ═══════════════════════════════════════════════════════════════════════════════

def fetch_klines(symbol: str, interval: str, start_ms: int, end_ms: int) -> List[List]:
    """Télécharge les klines depuis Binance API publique (pas de clé nécessaire)."""
    url = f"{BASE_URL}/api/v3/klines"
    all_klines = []
    current = start_ms

    while current < end_ms:
        params = {
            "symbol": symbol,
            "interval": interval,
            "startTime": current,
            "endTime": end_ms,
            "limit": 1000,
        }
        try:
            resp = requests.get(url, params=params, timeout=30)
            resp.raise_for_status()
            klines = resp.json()
            if not klines:
                break
            all_klines.extend(klines)
            current = klines[-1][6] + 1  # close_time + 1ms
            time.sleep(0.08)  # Rate limit gentle
        except Exception as e:
            print(f"  {C.R}✗ Erreur API {symbol}: {e}{C.END}")
            break

    return all_klines


def parse_klines(raw: List[List]) -> Dict[str, np.ndarray]:
    """Parse klines brutes en arrays numpy (close, high, low, volume, timestamps)."""
    if not raw:
        return {}
    n = len(raw)
    data = {
        'open':      np.array([float(k[1]) for k in raw]),
        'high':      np.array([float(k[2]) for k in raw]),
        'low':       np.array([float(k[3]) for k in raw]),
        'close':     np.array([float(k[4]) for k in raw]),
        'volume':    np.array([float(k[5]) for k in raw]),
        'timestamp':  np.array([int(k[0]) for k in raw]),  # open_time ms
    }
    return data


def load_all_data(symbols: List[str], days: int) -> Dict[str, Dict[str, np.ndarray]]:
    """Télécharge toutes les données nécessaires."""
    end_ms = int(datetime.utcnow().timestamp() * 1000)
    start_ms = int((datetime.utcnow() - timedelta(days=days)).timestamp() * 1000)
    # 🔧 FIX 01/03 v7: Warmup 2→5 jours — plus de contexte pour indicateurs techniques
    # 5 jours = 1440 bougies 5m de contexte (EMA, RSI, BB, volume profile)
    warmup_ms = int(timedelta(days=5).total_seconds() * 1000)
    fetch_start = start_ms - warmup_ms

    total = len(symbols)
    all_data = {}

    print(f"\n{C.BOLD}═══ TÉLÉCHARGEMENT DES DONNÉES ═══{C.END}")
    print(f"  Période: {datetime.utcfromtimestamp(start_ms/1000):%Y-%m-%d %H:%M} → {datetime.utcfromtimestamp(end_ms/1000):%Y-%m-%d %H:%M}")
    print(f"  Intervalle: {INTERVAL} | Symboles: {total} | Warmup: +5 jours\n")

    for idx, sym in enumerate(symbols, 1):
        pct = int(idx / total * 100)
        bar = "█" * (pct // 3) + "░" * (33 - pct // 3)
        print(f"\r  [{bar}] {pct:3d}% {sym:12s}", end="", flush=True)

        raw = fetch_klines(sym, INTERVAL, fetch_start, end_ms)
        data = parse_klines(raw)
        if data and len(data['close']) > 200:
            all_data[sym] = data
            all_data[sym]['backtest_start_ms'] = start_ms
        else:
            print(f"  {C.Y}⚠ {sym}: pas assez de données ({len(raw)} bougies){C.END}")

    print(f"\r  {'█' * 33} 100% — {len(all_data)}/{total} symboles chargés{' ' * 20}")
    return all_data


# ═══════════════════════════════════════════════════════════════════════════════
# LSTM PREDICTOR LOADING
# ═══════════════════════════════════════════════════════════════════════════════

def load_lstm_predictor():
    """Charge le LSTM Reversal Predictor sur CPU (CUDA_VISIBLE_DEVICES='' déjà défini au top)."""
    try:
        import lstm_reversal_predictor as lrp
        # Force CPU explicitement (au cas où)
        lrp.DEVICE = "cpu"

        predictor = lrp.get_reversal_predictor()
        # S'assurer que le modèle est bien sur CPU
        if predictor.model is not None:
            predictor.model = predictor.model.cpu()

        print(f"\n  {C.G}✓ LSTM Reversal Predictor chargé sur CPU (trained={predictor.is_trained}){C.END}")
        return predictor
    except Exception as e:
        print(f"\n  {C.R}✗ Impossible de charger le LSTM Reversal Predictor: {e}{C.END}")
        import traceback
        traceback.print_exc()
        sys.exit(1)


# ═══════════════════════════════════════════════════════════════════════════════
# TRADE SIMULATION
# ═══════════════════════════════════════════════════════════════════════════════

class Position:
    __slots__ = ['symbol', 'entry_price', 'entry_time', 'size_usdt', 'qty',
                 'max_price', 'trailing_active', 'stop_loss', 'take_profit']

    def __init__(self, symbol, entry_price, entry_time, size_usdt):
        self.symbol = symbol
        self.entry_price = entry_price
        self.entry_time = entry_time
        self.size_usdt = size_usdt
        self.qty = size_usdt / entry_price
        self.max_price = entry_price
        self.trailing_active = False
        self.stop_loss = entry_price * (1 - STOP_LOSS_PCT / 100)
        self.take_profit = entry_price * (1 + TAKE_PROFIT_PCT / 100)


class Trade:
    __slots__ = ['symbol', 'entry_price', 'exit_price', 'entry_time', 'exit_time',
                 'size_usdt', 'pnl_usdt', 'pnl_pct', 'exit_reason', 'signal_confidence',
                 'signal_probability']

    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)


class BacktestEngine:
    """Moteur de backtest avec gestion des positions, trailing stop, frais."""

    def __init__(self, initial_capital: float):
        self.initial_capital = initial_capital
        self.capital = initial_capital
        self.positions: Dict[str, Position] = {}
        self.closed_trades: List[Trade] = []
        self.equity_curve: List[Tuple[int, float]] = []  # (timestamp_ms, equity)
        self.signals_log: List[Dict] = []

        # Compteurs signaux
        self.total_reversal_up = 0
        self.total_reversal_down = 0
        self.total_neutral = 0
        self.total_continuation = 0
        self.blocked_signals = 0  # Signaux ignorés (max positions, capital, etc.)
        # 🔧 FIX 01/03 v4: Cooldown après SL — {symbol: expiry_timestamp_ms}
        self.sl_cooldown: Dict[str, int] = {}

    @property
    def open_value(self) -> float:
        """Valeur des positions ouvertes au dernier prix connu."""
        return sum(p.size_usdt for p in self.positions.values())

    @property
    def equity(self) -> float:
        return self.capital + self.open_value

    def can_open_position(self, symbol: str, timestamp_ms: int = 0) -> bool:
        if symbol in self.positions:
            return False
        if len(self.positions) >= MAX_OPEN_POSITIONS:
            return False
        # 🔧 FIX 01/03 v4: Cooldown après SL — pas de ré-entrée pendant 1h
        if symbol in self.sl_cooldown:
            if timestamp_ms < self.sl_cooldown[symbol]:
                return False
            else:
                del self.sl_cooldown[symbol]  # Cooldown expiré
        size = self.capital * (POSITION_SIZE_PCT / 100)
        if size < 10 or size > self.capital * 0.95:
            return False
        return True

    def open_position(self, symbol: str, price: float, timestamp_ms: int,
                      confidence: float = 0, probability: float = 0):
        """Ouvre une position long."""
        size = self.capital * (POSITION_SIZE_PCT / 100)
        fee = size * (TRADING_FEE_PCT / 100)
        self.capital -= (size + fee)
        pos = Position(symbol, price, timestamp_ms, size)
        self.positions[symbol] = pos

        self.signals_log.append({
            'type': 'BUY',
            'symbol': symbol,
            'price': price,
            'time': timestamp_ms,
            'confidence': confidence,
            'probability': probability,
        })

    def update_position(self, symbol: str, current_price: float, timestamp_ms: int) -> Optional[Trade]:
        """Met à jour une position et vérifie SL/TP/trailing. Retourne Trade si clôturée."""
        if symbol not in self.positions:
            return None

        pos = self.positions[symbol]
        pnl_pct = (current_price - pos.entry_price) / pos.entry_price * 100

        # Mettre à jour max price
        if current_price > pos.max_price:
            pos.max_price = current_price

        # Activer trailing stop
        if pnl_pct >= TRAILING_ACTIVATION_PCT and not pos.trailing_active:
            pos.trailing_active = True

        exit_reason = None

        # 0. Max holding time (6h) — évite les trades qui stagnent et finissent en SL
        holding_ms = timestamp_ms - pos.entry_time
        holding_candles = holding_ms / (5 * 60 * 1000)  # nb de bougies 5min
        if holding_candles >= MAX_HOLDING_CANDLES:
            exit_reason = f"MAX_HOLD_6H ({pnl_pct:+.1f}%)"

        # 1. Stop Loss
        elif current_price <= pos.stop_loss:
            exit_reason = "STOP_LOSS"

        # 2. Take Profit
        elif current_price >= pos.take_profit:
            exit_reason = "TAKE_PROFIT"

        # 3. Trailing Stop
        elif pos.trailing_active:
            trailing_level = pos.max_price * (1 - TRAILING_STOP_PCT / 100)
            if current_price <= trailing_level:
                exit_reason = f"TRAILING_STOP ({pnl_pct:+.1f}%)"

        if exit_reason:
            return self._close_position(symbol, current_price, timestamp_ms, exit_reason)
        return None

    def close_on_danger(self, symbol: str, current_price: float, timestamp_ms: int,
                        confidence: float) -> Optional[Trade]:
        """Ferme une position sur signal REVERSAL_DOWN du LSTM."""
        if symbol not in self.positions:
            return None
        pos = self.positions[symbol]
        pnl_pct = (current_price - pos.entry_price) / pos.entry_price * 100
        # 🔧 FIX 01/03 v3: Seuil relevé 0.5→0.8% — laisser respirer les trades
        # Crash-test 3J: LSTM_DANGER ×24, WR 62.5% mais gains microscopiques (+0.1~0.4%)
        # Le seul TRAILING_STOP a fait +4.05$ vs LSTM_DANGER total -2.22$
        # On laisse le trade travailler jusqu'à 0.8% avant de le couper
        if pnl_pct < 0.8:
            return self._close_position(symbol, current_price, timestamp_ms,
                                         f"LSTM_DANGER (conf={confidence:.0f}%)")
        return None

    def _close_position(self, symbol: str, price: float, timestamp_ms: int,
                        reason: str) -> Trade:
        pos = self.positions.pop(symbol)
        gross_value = pos.qty * price
        fee = gross_value * (TRADING_FEE_PCT / 100)
        net_value = gross_value - fee
        pnl_usdt = net_value - pos.size_usdt
        pnl_pct = (price - pos.entry_price) / pos.entry_price * 100

        self.capital += net_value

        # 🔧 FIX 01/03 v4: Cooldown 1h après STOP_LOSS sur ce symbole
        if reason == "STOP_LOSS":
            # 12 bougies × 5min = 1h de cooldown par symbole
            self.sl_cooldown[symbol] = timestamp_ms + 12 * 5 * 60 * 1000

        trade = Trade(
            symbol=symbol,
            entry_price=pos.entry_price,
            exit_price=price,
            entry_time=pos.entry_time,
            exit_time=timestamp_ms,
            size_usdt=pos.size_usdt,
            pnl_usdt=pnl_usdt,
            pnl_pct=pnl_pct,
            exit_reason=reason,
            signal_confidence=0,
            signal_probability=0,
        )
        self.closed_trades.append(trade)
        return trade

    def force_close_all(self, prices: Dict[str, float], timestamp_ms: int):
        """Ferme toutes les positions ouvertes (fin de backtest)."""
        for sym in list(self.positions.keys()):
            if sym in prices:
                self._close_position(sym, prices[sym], timestamp_ms, "END_OF_BACKTEST")


# ═══════════════════════════════════════════════════════════════════════════════
# MAIN BACKTEST LOGIC
# ═══════════════════════════════════════════════════════════════════════════════

def run_backtest(symbols: List[str], days: int, use_ai: bool = False):
    """Exécute le crash-test complet. Si use_ai=True, valide chaque signal avec ai_predictor."""

    ai_mode_str = " + AI PREDICTOR" if use_ai else ""
    print(f"""
{C.BOLD}╔══════════════════════════════════════════════════════════════════════╗
║   🧠 CRASH-TEST LSTM REVERSAL PREDICTOR{ai_mode_str} — BACKTEST {days}J          ║
╚══════════════════════════════════════════════════════════════════════╝{C.END}
""")

    # Charger l'AI Predictor si mode --ai
    ai_pred = None
    ai_blocked = 0
    ai_validated = 0
    ai_pattern_stats = defaultdict(lambda: {'count': 0, 'win': 0, 'loss': 0, 'pnl': 0.0})
    
    if use_ai:
        try:
            # Forcer CPU pour TOUS les modules (éviter conflit CUDA)
            # CUDA_VISIBLE_DEVICES='' est déjà set au top, mais certains modules
            # testent torch.cuda.is_available() avant de vérifier le device id
            os.environ['CUDA_VISIBLE_DEVICES'] = '-1'  # Force no CUDA
            import torch as _torch
            # Monkey-patch pour empêcher tout accès CUDA
            _original_cuda_available = _torch.cuda.is_available
            _torch.cuda.is_available = lambda: False
            
            from ai_predictor import AIPredictor
            ai_pred = AIPredictor()
            
            # Restaurer après import
            _torch.cuda.is_available = _original_cuda_available
            os.environ['CUDA_VISIBLE_DEVICES'] = ''
            
            print(f"  {C.G}✓ AI Predictor chargé (smart_criteria + pattern validation){C.END}")
            
            # Définir les patterns achetables (même liste que le bot live)
            BUYABLE_PATTERNS = [
                'CREUX_REBOUND', 'PULLBACK', 'SQUEEZE_BREAKOUT', 'EARLY_BREAKOUT',
                'CONSOLIDATION_BREAKOUT', 'EMA_BULLISH', 'CROSSOVER_IMMINENT',
                'VOLUME_REVERSAL', 'RSI_REVERSAL', 'STRONG_UPTREND'
            ]
        except Exception as e:
            print(f"  {C.R}✗ AI Predictor non disponible: {e}{C.END}")
            ai_pred = None

    # 1. Charger les données
    all_data = load_all_data(symbols, days)
    if not all_data:
        print(f"{C.R}✗ Aucune donnée chargée. Abandon.{C.END}")
        return

    # 2. Charger le LSTM
    predictor = load_lstm_predictor()

    # 3. Préparer le moteur de backtest
    engine = BacktestEngine(INITIAL_CAPITAL)

    # 4. Déterminer le calendrier global des bougies (union de tous les timestamps)
    # On traite chaque symbole bougie par bougie, dans l'ordre chronologique
    print(f"\n{C.BOLD}═══ EXÉCUTION DU BACKTEST ═══{C.END}")
    print(f"  Capital: {INITIAL_CAPITAL:.0f} USDT | Position: {POSITION_SIZE_PCT}% | Max positions: {MAX_OPEN_POSITIONS}")
    print(f"  SL: {STOP_LOSS_PCT}% | TP: {TAKE_PROFIT_PCT}% | Trailing: {TRAILING_STOP_PCT}% (act. {TRAILING_ACTIVATION_PCT}%)")
    print(f"  Frais: {TRADING_FEE_PCT}% | LSTM seuils: conf≥{REVERSAL_UP_MIN_CONFIDENCE}% prob≥{REVERSAL_UP_MIN_PROB}")
    if use_ai:
        print(f"  {C.CY}🧠 MODE IA: Chaque signal LSTM validé par ai_predictor (smart_criteria + patterns){C.END}")
    if ENABLE_HOUR_FILTER:
        print(f"  🕐 Filtre horaire: bloqué {sorted(BLOCKED_HOURS_UTC)}h UTC")
    print(f"  ⏱️  Max holding: {MAX_HOLDING_CANDLES} bougies ({MAX_HOLDING_CANDLES * 5 // 60}h)")

    # Regrouper tous les timestamps uniques de la période de backtest
    backtest_start_ms = None
    for sym, data in all_data.items():
        s = data['backtest_start_ms']
        if backtest_start_ms is None or s < backtest_start_ms:
            backtest_start_ms = s

    # Créer un index pour chaque symbole: map timestamp → index dans data arrays
    sym_indices = {}
    for sym, data in all_data.items():
        ts_map = {}
        for i, ts in enumerate(data['timestamp']):
            ts_map[ts] = i
        sym_indices[sym] = ts_map

    # Collecter les timestamps (5m) couvrant la période de backtest
    all_ts = set()
    for sym, data in all_data.items():
        mask = data['timestamp'] >= backtest_start_ms
        all_ts.update(data['timestamp'][mask].tolist())
    all_ts = sorted(all_ts)

    total_candles = len(all_ts)
    # LSTM évalué toutes les EVAL_EVERY bougies (positions vérifiées à chaque bougie)
    EVAL_EVERY = 3  # = toutes les 15 min (3 × 5m) — réaliste, like le bot
    eval_count = total_candles * len(all_data) // EVAL_EVERY
    print(f"  Bougies à traiter: {total_candles:,} | LSTM évalué: ~{eval_count:,} fois (toutes les {EVAL_EVERY*5}min)")
    print()

    # Compteurs par heure
    signals_by_hour = defaultdict(lambda: {'buy': 0, 'win': 0, 'loss': 0})
    signals_by_symbol = defaultdict(lambda: {'buy': 0, 'win': 0, 'loss': 0, 'pnl': 0.0})

    # Minimum de bougies pour la prédiction (60 séquences + 20 warmup)
    MIN_HISTORY = 100

    progress_step = max(1, total_candles // 50)

    for step, ts in enumerate(all_ts):
        # Progress bar
        if step % progress_step == 0:
            pct = int(step / total_candles * 100)
            bar = "█" * (pct // 2) + "░" * (50 - pct // 2)
            eq = engine.equity
            n_trades = len(engine.closed_trades)
            print(f"\r  [{bar}] {pct:3d}% | Equity: {eq:,.0f}$ | Trades: {n_trades}", end="", flush=True)

        # Snapshot d'equity
        if step % (progress_step * 5) == 0:
            engine.equity_curve.append((ts, engine.equity))

        # Pour chaque symbole actif
        for sym, data in all_data.items():
            ts_map = sym_indices[sym]
            if ts not in ts_map:
                continue
            idx = ts_map[ts]

            current_price = data['close'][idx]
            current_high = data['high'][idx]
            current_low = data['low'][idx]

            # A. Mettre à jour les positions existantes (SL/TP/trailing)
            trade = engine.update_position(sym, current_low, ts)  # Check low first (SL)
            if trade is None:
                trade = engine.update_position(sym, current_high, ts)  # Check high (TP)
            if trade:
                is_win = trade.pnl_usdt > 0
                hour = datetime.utcfromtimestamp(trade.entry_time / 1000).hour
                signals_by_hour[hour]['win' if is_win else 'loss'] += 1
                signals_by_symbol[sym]['win' if is_win else 'loss'] += 1
                signals_by_symbol[sym]['pnl'] += trade.pnl_usdt
                continue  # Position fermée, on passe

            # B. Vérifier signal LSTM (seulement toutes les EVAL_EVERY bougies)
            if idx < MIN_HISTORY:
                continue
            if step % EVAL_EVERY != 0:
                continue

            # Extraire la fenêtre de données pour le LSTM (dernières ~200 bougies)
            window = min(idx + 1, 300)
            start_i = idx - window + 1
            prices_window = data['close'][start_i:idx + 1]
            volumes_window = data['volume'][start_i:idx + 1]
            highs_window = data['high'][start_i:idx + 1]
            lows_window = data['low'][start_i:idx + 1]

            # Prédiction LSTM
            result = predictor.predict(prices_window, volumes_window, highs_window, lows_window)

            rev_class = result['reversal_class']
            rev_label = result['reversal_label']
            confidence = result['confidence']
            probability = result['reversal_probability']
            is_reversal = result['is_reversal_signal']
            is_danger = result['is_danger_signal']

            # Compteurs
            if rev_class == 0:
                engine.total_neutral += 1
            elif rev_class == 1:
                engine.total_reversal_up += 1
            elif rev_class == 2:
                engine.total_reversal_down += 1
            elif rev_class == 3:
                engine.total_continuation += 1

            # C. Signal REVERSAL_UP → ouvrir une position
            if (rev_label == 'REVERSAL_UP' and
                confidence >= REVERSAL_UP_MIN_CONFIDENCE and
                probability >= REVERSAL_UP_MIN_PROB):

                # 🔧 FIX 01/03: Filtre horaire — éviter heures toxiques (WR < 40%)
                candle_hour = datetime.utcfromtimestamp(ts / 1000).hour
                if ENABLE_HOUR_FILTER and candle_hour in BLOCKED_HOURS_UTC:
                    engine.blocked_signals += 1
                    continue

                # 🧠 FIX 01/03 v3: Validation IA via smart_criteria (si mode --ai activé)
                # analyze_symbol() ne lance PAS smart_criteria — il faut passer par
                # update_watchlist() + get_watchlist() pour obtenir smart_signal/smart_eligible
                ai_pattern = None
                if ai_pred is not None:
                    try:
                        # Construire les listes prix/volumes pour l'IA
                        ai_prices = data['close'][start_i:idx + 1].tolist()
                        ai_volumes = data['volume'][start_i:idx + 1].tolist()
                        
                        # 1. Analyser le symbole (scoring, LSTM, blocages)
                        ai_item = ai_pred.analyze_symbol(sym, ai_prices, ai_volumes)
                        
                        # 2. Stocker dans watchlist pour que get_watchlist() lance smart_criteria
                        with ai_pred._lock:
                            ai_pred.watchlist = {sym: ai_item}  # Un seul symbole à la fois
                        
                        # 3. get_watchlist() exécute smart_criteria et retourne les données enrichies
                        enriched_list = ai_pred.get_watchlist()
                        
                        if enriched_list:
                            entry = enriched_list[0]
                            ai_pattern = entry.get('pattern', 'NEUTRAL')
                            ai_score = entry.get('smart_score', ai_item.score)
                            ai_smart_signal = entry.get('smart_signal', '')
                            ai_smart_eligible = entry.get('smart_eligible', False)
                        else:
                            ai_pattern = ai_item.pattern
                            ai_score = ai_item.score
                            ai_smart_signal = ''
                            ai_smart_eligible = False
                        
                        # Vérifier les critères du bot live:
                        # 1. smart_signal doit être 'ACHAT' et smart_eligible=True
                        # 2. Pattern doit être dans la liste achetable
                        # 3. Score >= 65 (MIN_SCORE_ABSOLUTE du bot)
                        is_ai_valid = (
                            ai_smart_signal == 'ACHAT' and
                            ai_smart_eligible and
                            ai_pattern in BUYABLE_PATTERNS and
                            ai_score >= 65
                        )
                        
                        if not is_ai_valid:
                            ai_blocked += 1
                            engine.blocked_signals += 1
                            continue
                        
                        ai_validated += 1
                        
                    except Exception as e:
                        # En cas d'erreur IA, laisser passer (ne pas bloquer le backtest)
                        pass

                if engine.can_open_position(sym, ts):
                    engine.open_position(sym, current_price, ts, confidence, probability)
                    hour = datetime.utcfromtimestamp(ts / 1000).hour
                    signals_by_hour[hour]['buy'] += 1
                    signals_by_symbol[sym]['buy'] += 1
                    # Tracker le pattern IA pour stats
                    if ai_pattern:
                        signals_by_symbol[sym]['ai_pattern'] = ai_pattern
                        ai_pattern_stats[ai_pattern]['count'] += 1
                else:
                    engine.blocked_signals += 1

            # D. Signal REVERSAL_DOWN → fermer position si existante
            elif (is_danger and
                  confidence >= REVERSAL_DOWN_MIN_CONFIDENCE and
                  probability >= REVERSAL_DOWN_MIN_PROB):

                trade = engine.close_on_danger(sym, current_price, ts, confidence)
                if trade:
                    is_win = trade.pnl_usdt > 0
                    hour = datetime.utcfromtimestamp(trade.entry_time / 1000).hour
                    signals_by_hour[hour]['win' if is_win else 'loss'] += 1
                    signals_by_symbol[sym]['win' if is_win else 'loss'] += 1
                    signals_by_symbol[sym]['pnl'] += trade.pnl_usdt

    # Fermer les positions ouvertes restantes
    last_prices = {}
    for sym, data in all_data.items():
        last_prices[sym] = data['close'][-1]
    last_ts = all_ts[-1] if all_ts else 0
    engine.force_close_all(last_prices, last_ts)
    for trade in engine.closed_trades:
        if trade.exit_reason == "END_OF_BACKTEST":
            signals_by_symbol[trade.symbol]['pnl'] += trade.pnl_usdt

    # Point final d'equity
    engine.equity_curve.append((last_ts, engine.equity))

    print(f"\r  {'█' * 50} 100% | Equity: {engine.equity:,.0f}$ | Trades: {len(engine.closed_trades)}{' ' * 20}")

    # 5. Rapport
    print_report(engine, all_data, signals_by_hour, signals_by_symbol, days,
                 use_ai=use_ai, ai_pred=ai_pred, ai_validated=ai_validated,
                 ai_blocked=ai_blocked, ai_pattern_stats=ai_pattern_stats)
    save_report_json(engine, signals_by_symbol, days)


# ═══════════════════════════════════════════════════════════════════════════════
# RAPPORT DE RÉSULTATS
# ═══════════════════════════════════════════════════════════════════════════════

def print_report(engine: BacktestEngine, all_data: Dict, signals_by_hour: Dict,
                 signals_by_symbol: Dict, days: int, use_ai: bool = False,
                 ai_pred=None, ai_validated: int = 0, ai_blocked: int = 0,
                 ai_pattern_stats=None):
    """Affiche le rapport complet du crash-test."""

    trades = engine.closed_trades
    n = len(trades)

    print(f"""
{C.BOLD}╔══════════════════════════════════════════════════════════════════════╗
║                  📊 RAPPORT CRASH-TEST LSTM REVERSAL               ║
╚══════════════════════════════════════════════════════════════════════╝{C.END}
""")

    # ─── RÉSUMÉ FINANCIER ─────────────────────────────────────────────
    initial = engine.initial_capital
    final = engine.equity
    total_pnl = final - initial
    total_pnl_pct = (total_pnl / initial) * 100

    color = C.G if total_pnl >= 0 else C.R
    print(f"  {C.BOLD}RÉSUMÉ FINANCIER{C.END}")
    print(f"  ├─ Capital initial:  {initial:>10,.2f} USDT")
    print(f"  ├─ Capital final:    {color}{final:>10,.2f} USDT{C.END}")
    print(f"  ├─ P&L total:        {color}{total_pnl:>+10,.2f} USDT ({total_pnl_pct:+.2f}%){C.END}")

    # Annualisé
    annual_pct = total_pnl_pct * (365 / max(days, 1))
    print(f"  └─ P&L annualisé:    {color}{annual_pct:>+10,.1f}%{C.END}")

    if n == 0:
        print(f"\n  {C.Y}⚠ Aucun trade exécuté. Le LSTM n'a généré aucun signal qualifiant.{C.END}")
        print(f"  Signaux REVERSAL_UP: {engine.total_reversal_up} | REVERSAL_DOWN: {engine.total_reversal_down}")
        print(f"  NEUTRAL: {engine.total_neutral} | CONTINUATION: {engine.total_continuation}")
        return

    # ─── STATISTIQUES DE TRADING ──────────────────────────────────────
    wins = [t for t in trades if t.pnl_usdt > 0]
    losses = [t for t in trades if t.pnl_usdt <= 0]
    n_wins = len(wins)
    n_losses = len(losses)
    win_rate = (n_wins / n * 100) if n > 0 else 0

    avg_win = np.mean([t.pnl_pct for t in wins]) if wins else 0
    avg_loss = np.mean([t.pnl_pct for t in losses]) if losses else 0
    avg_pnl = np.mean([t.pnl_pct for t in trades])
    median_pnl = np.median([t.pnl_pct for t in trades])

    best_trade = max(trades, key=lambda t: t.pnl_pct)
    worst_trade = min(trades, key=lambda t: t.pnl_pct)

    gross_profit = sum(t.pnl_usdt for t in wins) if wins else 0
    gross_loss = abs(sum(t.pnl_usdt for t in losses)) if losses else 0.01
    profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')

    # Durée moyenne des trades
    durations = [(t.exit_time - t.entry_time) / 60000 for t in trades]  # en minutes
    avg_duration = np.mean(durations)

    print(f"\n  {C.BOLD}STATISTIQUES DE TRADING{C.END}")
    print(f"  ├─ Trades total:     {n:>6d}")
    print(f"  ├─ Wins:             {C.G}{n_wins:>6d} ({win_rate:.1f}%){C.END}")
    print(f"  ├─ Losses:           {C.R}{n_losses:>6d} ({100 - win_rate:.1f}%){C.END}")
    print(f"  ├─ Gain moyen:       {C.G}{avg_win:>+6.2f}%{C.END}")
    print(f"  ├─ Perte moyenne:    {C.R}{avg_loss:>+6.2f}%{C.END}")
    print(f"  ├─ P&L moyen:        {avg_pnl:>+6.2f}%")
    print(f"  ├─ P&L médian:       {median_pnl:>+6.2f}%")
    print(f"  ├─ Profit Factor:    {profit_factor:>6.2f}")
    print(f"  ├─ Durée moy trade:  {avg_duration:>6.0f} min ({avg_duration/60:.1f}h)")
    print(f"  ├─ Meilleur trade:   {C.G}{best_trade.symbol} {best_trade.pnl_pct:+.2f}%{C.END}")
    print(f"  └─ Pire trade:       {C.R}{worst_trade.symbol} {worst_trade.pnl_pct:+.2f}%{C.END}")

    # ─── RATIOS DE RISQUE ─────────────────────────────────────────────
    returns = [t.pnl_pct for t in trades]
    sharpe = (np.mean(returns) / (np.std(returns) + 1e-8)) * np.sqrt(len(returns) / max(days, 1) * 252)

    # Max drawdown sur la courbe d'equity
    eq_values = [eq for _, eq in engine.equity_curve] if engine.equity_curve else [initial]
    peak = eq_values[0]
    max_dd = 0
    for eq in eq_values:
        if eq > peak:
            peak = eq
        dd = (peak - eq) / peak * 100
        if dd > max_dd:
            max_dd = dd

    # Calmar ratio
    calmar = (total_pnl_pct / max(max_dd, 0.01)) if max_dd > 0 else float('inf')

    print(f"\n  {C.BOLD}RATIOS DE RISQUE{C.END}")
    print(f"  ├─ Sharpe Ratio:     {sharpe:>6.2f}")
    print(f"  ├─ Max Drawdown:     {C.R}{max_dd:>6.2f}%{C.END}")
    print(f"  ├─ Calmar Ratio:     {calmar:>6.2f}")
    print(f"  └─ Trades/jour:      {n / max(days, 1):>6.1f}")

    # ─── SIGNAUX LSTM ─────────────────────────────────────────────────
    print(f"\n  {C.BOLD}SIGNAUX LSTM{C.END}")
    total_signals = engine.total_reversal_up + engine.total_reversal_down + engine.total_neutral + engine.total_continuation
    print(f"  ├─ Total évaluations: {total_signals:>10,d}")
    print(f"  ├─ REVERSAL_UP:       {engine.total_reversal_up:>10,d} ({engine.total_reversal_up/max(total_signals,1)*100:.2f}%)")
    print(f"  ├─ REVERSAL_DOWN:     {engine.total_reversal_down:>10,d} ({engine.total_reversal_down/max(total_signals,1)*100:.2f}%)")
    print(f"  ├─ NEUTRAL:           {engine.total_neutral:>10,d} ({engine.total_neutral/max(total_signals,1)*100:.2f}%)")
    print(f"  ├─ CONTINUATION:      {engine.total_continuation:>10,d} ({engine.total_continuation/max(total_signals,1)*100:.2f}%)")
    print(f"  └─ Signaux bloqués:   {engine.blocked_signals:>10,d} (max positions/capital)")

    # ─── STATS AI PREDICTOR (si mode --ai) ────────────────────────────
    if use_ai and ai_pred is not None:
        print(f"\n  {C.BOLD}🧠 VALIDATION AI PREDICTOR{C.END}")
        total_ai = ai_validated + ai_blocked
        print(f"  ├─ Signaux LSTM évalués par IA: {total_ai:>6,d}")
        print(f"  ├─ IA VALIDÉS (ACHAT):          {C.G}{ai_validated:>6,d}{C.END} ({ai_validated/max(total_ai,1)*100:.1f}%)")
        print(f"  ├─ IA BLOQUÉS (non-éligible):   {C.R}{ai_blocked:>6,d}{C.END} ({ai_blocked/max(total_ai,1)*100:.1f}%)")
        if ai_pattern_stats:
            print(f"  └─ Patterns IA autorisés:")
            for pat, stats in sorted(ai_pattern_stats.items(), key=lambda x: -x[1]['count']):
                print(f"     ├─ {pat:25s} × {stats['count']:3d}")

    # ─── ANALYSE PAR RAISON DE SORTIE ─────────────────────────────────
    exit_reasons = defaultdict(lambda: {'count': 0, 'pnl': 0.0, 'wins': 0})
    for t in trades:
        reason = t.exit_reason.split(" ")[0]  # Simplify trailing
        exit_reasons[reason]['count'] += 1
        exit_reasons[reason]['pnl'] += t.pnl_usdt
        if t.pnl_usdt > 0:
            exit_reasons[reason]['wins'] += 1

    print(f"\n  {C.BOLD}SORTIES PAR RAISON{C.END}")
    for reason, stats in sorted(exit_reasons.items(), key=lambda x: -x[1]['count']):
        wr = stats['wins'] / max(stats['count'], 1) * 100
        c_ = C.G if stats['pnl'] >= 0 else C.R
        print(f"  ├─ {reason:20s} × {stats['count']:3d} | WR: {wr:5.1f}% | P&L: {c_}{stats['pnl']:>+8.2f}${C.END}")

    # ─── ANALYSE PAR SYMBOLE (TOP 10) ────────────────────────────────
    sorted_symbols = sorted(signals_by_symbol.items(), key=lambda x: -x[1]['pnl'])
    print(f"\n  {C.BOLD}TOP 10 SYMBOLES{C.END}")
    for sym, stats in sorted_symbols[:10]:
        total_sym = stats['win'] + stats['loss']
        wr = stats['win'] / max(total_sym, 1) * 100
        c_ = C.G if stats['pnl'] >= 0 else C.R
        print(f"  ├─ {sym:12s} {stats['buy']:3d} trades | WR: {wr:5.1f}% | P&L: {c_}{stats['pnl']:>+8.2f}${C.END}")

    if len(sorted_symbols) > 10:
        print(f"\n  {C.BOLD}BOTTOM 5 SYMBOLES{C.END}")
        for sym, stats in sorted_symbols[-5:]:
            total_sym = stats['win'] + stats['loss']
            wr = stats['win'] / max(total_sym, 1) * 100
            c_ = C.G if stats['pnl'] >= 0 else C.R
            print(f"  ├─ {sym:12s} {stats['buy']:3d} trades | WR: {wr:5.1f}% | P&L: {c_}{stats['pnl']:>+8.2f}${C.END}")

    # ─── ANALYSE PAR HEURE ────────────────────────────────────────────
    print(f"\n  {C.BOLD}PERFORMANCE PAR HEURE (UTC){C.END}")
    for h in range(24):
        if h not in signals_by_hour:
            continue
        stats = signals_by_hour[h]
        total_h = stats['win'] + stats['loss']
        if total_h == 0:
            continue
        wr = stats['win'] / total_h * 100
        bar_len = int(total_h / max(1, max(s['win'] + s['loss'] for s in signals_by_hour.values())) * 20)
        bar = "█" * bar_len
        c_ = C.G if wr >= 50 else C.R
        print(f"  ├─ {h:02d}h: {bar:20s} {total_h:3d} trades | WR: {c_}{wr:5.1f}%{C.END}")

    # ─── BUY & HOLD COMPARISON ────────────────────────────────────────
    print(f"\n  {C.BOLD}COMPARAISON BUY & HOLD{C.END}")
    bh_returns = []
    for sym, data in all_data.items():
        bt_start = data['backtest_start_ms']
        mask = data['timestamp'] >= bt_start
        prices_bt = data['close'][mask]
        if len(prices_bt) > 1:
            bh_ret = (prices_bt[-1] - prices_bt[0]) / prices_bt[0] * 100
            bh_returns.append((sym, bh_ret))

    avg_bh = np.mean([r for _, r in bh_returns]) if bh_returns else 0
    bh_portfolio = initial * (1 + avg_bh / 100)

    c_bh = C.G if avg_bh >= 0 else C.R
    c_lstm = C.G if total_pnl_pct >= 0 else C.R
    print(f"  ├─ Buy & Hold (moy):  {c_bh}{avg_bh:>+.2f}% → {bh_portfolio:,.0f}${C.END}")
    print(f"  ├─ LSTM Strategy:     {c_lstm}{total_pnl_pct:>+.2f}% → {final:,.0f}${C.END}")
    diff = total_pnl_pct - avg_bh
    c_diff = C.G if diff >= 0 else C.R
    print(f"  └─ Alpha LSTM:        {c_diff}{diff:>+.2f}%{C.END}")

    # ─── EQUITY CURVE ASCII ───────────────────────────────────────────
    if engine.equity_curve and len(engine.equity_curve) > 2:
        print(f"\n  {C.BOLD}COURBE D'EQUITY{C.END}")
        eq_vals = [eq for _, eq in engine.equity_curve]
        min_eq = min(eq_vals)
        max_eq = max(eq_vals)
        range_eq = max_eq - min_eq if max_eq != min_eq else 1

        height = 12
        width = min(60, len(eq_vals))
        # Resample to width
        step_size = max(1, len(eq_vals) // width)
        sampled = eq_vals[::step_size][:width]

        for row in range(height, -1, -1):
            threshold = min_eq + (row / height) * range_eq
            line = "  │"
            for val in sampled:
                if val >= threshold:
                    line += "█"
                else:
                    line += " "
            if row == height:
                line += f"  {max_eq:,.0f}$"
            elif row == 0:
                line += f"  {min_eq:,.0f}$"
            elif row == height // 2:
                line += f"  {(min_eq + max_eq) / 2:,.0f}$"
            print(line)
        print(f"  └{'─' * len(sampled)}")

    # ─── VERDICT ──────────────────────────────────────────────────────
    print(f"\n{C.BOLD}{'═' * 70}{C.END}")
    if total_pnl_pct > 2 and win_rate > 50 and profit_factor > 1.2:
        verdict = f"{C.G}✅ RENTABLE — Le LSTM Reversal Predictor génère un alpha positif !{C.END}"
    elif total_pnl_pct > 0 and win_rate > 45:
        verdict = f"{C.Y}⚠️  MARGINALEMENT RENTABLE — Résultats positifs mais à optimiser.{C.END}"
    elif total_pnl_pct > -2:
        verdict = f"{C.Y}⚠️  NEUTRE — Pas de perte significative mais pas de gain non plus.{C.END}"
    else:
        verdict = f"{C.R}❌ NON RENTABLE — Le modèle nécessite plus d'entraînement ou d'ajustements.{C.END}"

    print(f"  VERDICT: {verdict}")

    if total_pnl_pct < 0:
        print(f"\n  {C.CY}💡 RECOMMANDATIONS:")
        print(f"     - Le modèle est encore jeune (peu de données d'entraînement)")
        print(f"     - L'entraînement online va progressivement améliorer les prédictions")
        print(f"     - Augmenter la confiance minimum (actuellement {REVERSAL_UP_MIN_CONFIDENCE}%) pourrait filtrer les faux signaux")
        print(f"     - Tester avec un lookback plus long (30 jours) pour plus de contexte{C.END}")

    print(f"\n{C.BOLD}{'═' * 70}{C.END}\n")


def save_report_json(engine: BacktestEngine, signals_by_symbol: Dict, days: int):
    """Sauvegarde le rapport JSON pour analyse ultérieure."""
    trades = engine.closed_trades
    n = len(trades)
    wins = [t for t in trades if t.pnl_usdt > 0]

    report = {
        'timestamp': datetime.utcnow().isoformat(),
        'backtest_days': days,
        'initial_capital': engine.initial_capital,
        'final_capital': round(engine.equity, 2),
        'total_pnl_usdt': round(engine.equity - engine.initial_capital, 2),
        'total_pnl_pct': round((engine.equity - engine.initial_capital) / engine.initial_capital * 100, 2),
        'total_trades': n,
        'win_rate': round(len(wins) / max(n, 1) * 100, 2),
        'profit_factor': round(sum(t.pnl_usdt for t in wins) / max(abs(sum(t.pnl_usdt for t in trades if t.pnl_usdt <= 0)), 0.01), 2) if n > 0 else 0,
        'avg_pnl_pct': round(np.mean([t.pnl_pct for t in trades]), 3) if n > 0 else 0,
        'max_drawdown_pct': 0,
        'signals': {
            'reversal_up': engine.total_reversal_up,
            'reversal_down': engine.total_reversal_down,
            'neutral': engine.total_neutral,
            'continuation': engine.total_continuation,
            'blocked': engine.blocked_signals,
        },
        'by_symbol': {sym: stats for sym, stats in signals_by_symbol.items()},
        'trades': [
            {
                'symbol': t.symbol,
                'entry_price': round(t.entry_price, 6),
                'exit_price': round(t.exit_price, 6),
                'entry_time': datetime.utcfromtimestamp(t.entry_time / 1000).isoformat(),
                'exit_time': datetime.utcfromtimestamp(t.exit_time / 1000).isoformat(),
                'pnl_usdt': round(t.pnl_usdt, 4),
                'pnl_pct': round(t.pnl_pct, 4),
                'exit_reason': t.exit_reason,
            }
            for t in trades
        ],
    }

    # Calcul max drawdown
    if engine.equity_curve:
        eq_vals = [eq for _, eq in engine.equity_curve]
        peak = eq_vals[0]
        max_dd = 0
        for eq in eq_vals:
            if eq > peak:
                peak = eq
            dd = (peak - eq) / peak * 100
            max_dd = max(max_dd, dd)
        report['max_drawdown_pct'] = round(max_dd, 2)

    out_path = Path(__file__).parent / "backtest_lstm_report.json"
    with open(out_path, 'w', encoding='utf-8') as f:
        json.dump(report, f, indent=2, ensure_ascii=False, default=str)

    print(f"  {C.CY}📄 Rapport JSON sauvegardé: {out_path.name}{C.END}")


# ═══════════════════════════════════════════════════════════════════════════════
# MAIN
# ═══════════════════════════════════════════════════════════════════════════════

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Crash-Test LSTM Reversal Predictor")
    parser.add_argument("--symbols", nargs="+", default=None,
                        help="Liste de symboles à tester (défaut: 20 top cryptos)")
    parser.add_argument("--days", type=int, default=BACKTEST_DAYS,
                        help=f"Nombre de jours de backtest (défaut: {BACKTEST_DAYS})")
    parser.add_argument("--capital", type=float, default=INITIAL_CAPITAL,
                        help=f"Capital initial en USDT (défaut: {INITIAL_CAPITAL})")
    parser.add_argument("--all", action="store_true",
                        help="Tester tous les symboles de la watchlist")
    parser.add_argument("--ai", action="store_true",
                        help="Activer la validation IA complète (smart_criteria + patterns)")

    args = parser.parse_args()

    if args.capital != INITIAL_CAPITAL:
        INITIAL_CAPITAL = args.capital

    symbols = args.symbols or DEFAULT_SYMBOLS

    if args.all:
        try:
            wl = Path(__file__).parent / "watchlist.json"
            with open(wl) as f:
                symbols = json.load(f).get('symbols', DEFAULT_SYMBOLS)
            print(f"  Mode --all: {len(symbols)} symboles de la watchlist")
        except Exception:
            symbols = DEFAULT_SYMBOLS

    run_backtest(symbols, args.days, use_ai=args.ai)
