#!/usr/bin/env python3
"""
AI Predictor - Analyse prédictive des cryptomonnaies avec Deep Learning
Utilise PyTorch avec GPU (RTX 5060 Ti) pour anticiper les mouvements de marché
"""

# 🔵 FIX 25/03: Limiter les threads CPU pour éviter saturation serveur
# PyTorch utilise par défaut TOUS les cœurs via OpenMP → explosion avec 20 workers
import os
os.environ.setdefault('OMP_NUM_THREADS', '2')
os.environ.setdefault('MKL_NUM_THREADS', '2')
os.environ.setdefault('OPENBLAS_NUM_THREADS', '2')
os.environ.setdefault('VECLIB_MAXIMUM_THREADS', '2')
os.environ.setdefault('NUMEXPR_NUM_THREADS', '2')

import numpy as np
from datetime import datetime, timedelta
from collections import deque
from typing import Dict, List, Tuple, Optional
import json
import os
import threading
import time
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed

# Configuration du logging avec timestamps
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
    datefmt='%H:%M:%S'
)
logger = logging.getLogger("AIPredictor")

# === IMPORT DU SCORER DE COMPATIBILITÉ IA ===
# DÉSACTIVÉ: Remplacé par Volatility Scorer (évite les doublons d'analyse volatilité/BB)
AI_COMPATIBILITY_AVAILABLE = False
# try:
#     from ai_compatibility_scorer import get_compatibility_scorer, prioritize_watchlist
#     AI_COMPATIBILITY_AVAILABLE = True
#     logger.info("✅ AI Compatibility Scorer disponible")
# except ImportError as e:
#     logger.warning(f"⚠️ AI Compatibility Scorer non disponible: {e}")

# === IMPORT DU SYSTÈME DE ROTATION INTELLIGENTE ===
SMART_ROTATION_AVAILABLE = False
try:
    from smart_rotation import get_smart_rotation, analyze_rotation_opportunity
    SMART_ROTATION_AVAILABLE = True
    logger.info("✅ Smart Rotation disponible")
except ImportError as e:
    logger.warning(f"⚠️ Smart Rotation non disponible: {e}")

# === IMPORT DU SCORER AVANCÉ GPU ===
ADVANCED_SCORER_AVAILABLE = False
try:
    from ai_advanced_scorer import get_advanced_scorer, CryptoProfile
    ADVANCED_SCORER_AVAILABLE = True
    logger.info("✅ AI Advanced Scorer (GPU) disponible")
except ImportError as e:
    logger.warning(f"⚠️ AI Advanced Scorer non disponible: {e}")

# === IMPORT DE L'ANALYSEUR DE PERFORMANCE ===
PERFORMANCE_ANALYZER_AVAILABLE = False
try:
    from performance_analyzer import get_performance_analyzer
    PERFORMANCE_ANALYZER_AVAILABLE = True
    logger.info("✅ Performance Analyzer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Performance Analyzer non disponible: {e}")

# === IMPORT DU VOLATILITY SCORER ===
VOLATILITY_SCORER_AVAILABLE = False
try:
    from volatility_scorer import get_volatility_scorer
    VOLATILITY_SCORER_AVAILABLE = True
    logger.info("✅ Volatility Scorer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Volatility Scorer non disponible: {e}")

# === IMPORT DU AI OPPORTUNITY SELECTOR ===
AI_OPPORTUNITY_SELECTOR_AVAILABLE = False
try:
    from ai_opportunity_selector import get_opportunity_selector
    AI_OPPORTUNITY_SELECTOR_AVAILABLE = True
    logger.info("✅ AI Opportunity Selector disponible (PyTorch + GPU)")
except ImportError as e:
    logger.warning(f"⚠️ AI Opportunity Selector non disponible: {e}")

# === IMPORT DU DYNAMIC SL/TP CALCULATOR ===
DYNAMIC_SLTP_AVAILABLE = False
try:
    from dynamic_sltp import calculate_optimal_sltp, get_dynamic_sltp_calculator
    DYNAMIC_SLTP_AVAILABLE = True
    logger.info("✅ Dynamic SL/TP Calculator disponible")
except ImportError as e:
    logger.warning(f"⚠️ Dynamic SL/TP Calculator non disponible: {e}")

# === IMPORT DES NOUVEAUX MODULES D'AMÉLIORATION IA ===
MULTI_TIMEFRAME_AVAILABLE = False
try:
    from multi_timeframe_analyzer import get_multi_tf_analyzer
    MULTI_TIMEFRAME_AVAILABLE = True
    logger.info("✅ Multi-Timeframe Analyzer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Multi-Timeframe Analyzer non disponible: {e}")

VOLUME_PROFILE_AVAILABLE = False
try:
    from volume_profile_analyzer import get_volume_analyzer
    VOLUME_PROFILE_AVAILABLE = True
    logger.info("✅ Volume Profile Analyzer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Volume Profile Analyzer non disponible: {e}")

REGIME_DETECTOR_AVAILABLE = False
try:
    from market_regime_detector import get_regime_detector
    REGIME_DETECTOR_AVAILABLE = True
    logger.info("✅ Market Regime Detector disponible")
except ImportError as e:
    logger.warning(f"⚠️ Market Regime Detector non disponible: {e}")

FEATURE_EXTRACTOR_AVAILABLE = False
try:
    from advanced_feature_engineering import get_feature_extractor
    FEATURE_EXTRACTOR_AVAILABLE = True
    logger.info("✅ Advanced Feature Extractor disponible")
except ImportError as e:
    logger.warning(f"⚠️ Advanced Feature Extractor non disponible: {e}")

TIME_PATTERN_AVAILABLE = False
try:
    from time_pattern_analyzer import get_time_pattern_analyzer
    TIME_PATTERN_AVAILABLE = True
    logger.info("✅ Time Pattern Analyzer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Time Pattern Analyzer non disponible: {e}")

CORRELATION_ANALYZER_AVAILABLE = False
try:
    from correlation_analyzer import get_correlation_analyzer
    CORRELATION_ANALYZER_AVAILABLE = True
    logger.info("✅ Correlation Analyzer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Correlation Analyzer non disponible: {e}")

RISK_ADJUSTED_AVAILABLE = False
try:
    from risk_adjusted_scorer import get_risk_adjusted_scorer
    RISK_ADJUSTED_AVAILABLE = True
    logger.info("✅ Risk-Adjusted Scorer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Risk-Adjusted Scorer non disponible: {e}")

MONTE_CARLO_AVAILABLE = False
try:
    from monte_carlo_simulator import get_monte_carlo_simulator
    MONTE_CARLO_AVAILABLE = True
    logger.info("✅ Monte Carlo Simulator disponible")
except ImportError as e:
    logger.warning(f"⚠️ Monte Carlo Simulator non disponible: {e}")

ENSEMBLE_PREDICTOR_AVAILABLE = False
try:
    from ensemble_predictor import get_ensemble_predictor
    ENSEMBLE_PREDICTOR_AVAILABLE = True
    logger.info("✅ Ensemble Predictor disponible")
except ImportError as e:
    logger.warning(f"⚠️ Ensemble Predictor non disponible: {e}")

LONG_TERM_TREND_AVAILABLE = False
try:
    from long_term_trend_analyzer import get_long_term_analyzer
    LONG_TERM_TREND_AVAILABLE = True
    logger.info("✅ Long-Term Trend Analyzer disponible")
except ImportError as e:
    logger.warning(f"⚠️ Long-Term Trend Analyzer non disponible: {e}")

# === IMPORT DU LSTM REVERSAL PREDICTOR (GPU) ===
LSTM_REVERSAL_AVAILABLE = False
try:
    from lstm_reversal_predictor import get_reversal_predictor
    LSTM_REVERSAL_AVAILABLE = True
    logger.info("✅ LSTM Reversal Predictor (GPU) disponible")
except ImportError as e:
    logger.warning(f"⚠️ LSTM Reversal Predictor non disponible: {e}")

# === IMPORT DES CONSTANTES DE CONFIGURATION ===
from config import (
    MOMENTUM_REVERSAL_MIN, MOMENTUM_REVERSAL_MAX, MOMENTUM_MODERATE,
    MOMENTUM_THRESHOLD, MOMENTUM_STRONG, MOMENTUM_DROP_LIGHT,
    MOMENTUM_DROP_MODERATE, MOMENTUM_DROP_SIGNIFICANT, MOMENTUM_DROP_SEVERE,
    MOMENTUM_CRASH, CREUX_MIN_GAP, CREUX_OPTIMAL_START, CREUX_OPTIMAL_END,
    CREUX_TOO_DEEP, SQUEEZE_FAVORABLE, SQUEEZE_CROSS_ZONE, PULLBACK_MIN_GAP,
    DEATH_CROSS_LIGHT, DEATH_CROSS_MODERATE, DEATH_CROSS_STRONG,
    EMA_SHORT, EMA_LONG
)

# ═══════════════════════════════════════════════════════════════════════════════
# TOP 20 CRYPTOS PAR MARKET CAP - PRIORISÉES EN PÉRIODE DE RESTRICTION
# Ces cryptos reçoivent un bonus de score car plus stables et prévisibles
# ═══════════════════════════════════════════════════════════════════════════════
TOP_20_CRYPTOS = [
    "BTCUSDT", "ETHUSDT", "BNBUSDT", "XRPUSDT", "ADAUSDT", 
    "DOGEUSDT", "SOLUSDT", "DOTUSDT", "POLUSDT", "LTCUSDT",
    "AVAXUSDT", "LINKUSDT", "ATOMUSDT", "UNIUSDT", "XLMUSDT",
    "ETCUSDT", "FILUSDT", "APTUSDT", "NEARUSDT", "ARBUSDT"
]

# Bonus de score pour les TOP 20 (en période de restriction ou toujours)
TOP_20_SCORE_BONUS = 15  # +15 points de score pour les TOP 20

# Vérifier si PyTorch est disponible avec GPU
TORCH_AVAILABLE = False
DEVICE = "cpu"

try:
    import torch
    import torch.nn as nn
    # 🔵 FIX 25/03: Limiter les threads intra/inter-op pour CPU-only server
    torch.set_num_threads(2)
    try:
        torch.set_num_interop_threads(1)
    except RuntimeError:
        pass  # Peut être déjà fixé par un autre module importé avant

    if torch.cuda.is_available():
        DEVICE = "cuda"
        gpu_name = torch.cuda.get_device_name(0)
        logger.info(f"✅ PyTorch GPU activé: {gpu_name}")
        logger.info(f"   - CUDA version: {torch.version.cuda}")
        logger.info(f"   - PyTorch version: {torch.__version__}")
        TORCH_AVAILABLE = True
    else:
        logger.info("ℹ️  Aucun GPU CUDA détecté, utilisation CPU")
        TORCH_AVAILABLE = True
except ImportError:
    logger.info("ℹ️  PyTorch non installé, mode règles utilisé")
    TORCH_AVAILABLE = False

# === MODÈLE LSTM GLOBAL POUR PYTORCH 2.6+ ===
# Défini au niveau global pour permettre torch.serialization.add_safe_globals
if TORCH_AVAILABLE:
    class PredictorLSTM(nn.Module):
        """Modèle LSTM pour la prédiction de prix crypto"""
        def __init__(self, input_size=20, hidden_size=128, num_layers=2, dropout=0.2):
            super().__init__()
            self.lstm = nn.LSTM(
                input_size=input_size,
                hidden_size=hidden_size,
                num_layers=num_layers,
                batch_first=True,
                dropout=dropout
            )
            self.dropout = nn.Dropout(dropout)
            self.fc1 = nn.Linear(hidden_size, 64)
            self.fc2 = nn.Linear(64, 32)
            self.fc3 = nn.Linear(32, 3)  # 3 classes: baisse, neutre, hausse
            
        def forward(self, x):
            lstm_out, _ = self.lstm(x)
            # Prendre le dernier timestep
            last_output = lstm_out[:, -1, :]
            x = self.dropout(last_output)
            x = torch.relu(self.fc1(x))
            x = self.dropout(x)
            x = torch.relu(self.fc2(x))
            x = self.fc3(x)
            return x
    
    # Enregistrer la classe pour le chargement sécurisé (PyTorch 2.6+)
    try:
        torch.serialization.add_safe_globals([PredictorLSTM])
        logger.info("✅ PredictorLSTM enregistré pour chargement sécurisé")
    except AttributeError:
        # Anciennes versions de PyTorch n'ont pas cette fonction
        pass


class PatternFeatures:
    """Extraction de features pour l'analyse de patterns"""
    
    @staticmethod
    def extract_features(prices: List[float], volumes: List[float] = None) -> Dict:
        """Extrait toutes les features pertinentes pour la prédiction"""
        if len(prices) < 50:
            return None
        
        prices = np.array(prices)
        
        features = {}
        
        # === PRIX ET TENDANCE ===
        features['price_current'] = prices[-1]
        features['price_change_5'] = (prices[-1] - prices[-5]) / prices[-5] * 100 if len(prices) >= 5 else 0
        features['price_change_10'] = (prices[-1] - prices[-10]) / prices[-10] * 100 if len(prices) >= 10 else 0
        features['price_change_20'] = (prices[-1] - prices[-20]) / prices[-20] * 100 if len(prices) >= 20 else 0
        
        # === EMAs (🔧 OPTIM 25/03: EMA7/25/99 vs anciens 9/21/50) ===
        ema9 = PatternFeatures._ema(prices, EMA_SHORT)   # EMA7 (nommé ema9 pour compat. features)
        ema21 = PatternFeatures._ema(prices, EMA_LONG)   # EMA25 (nommé ema21 pour compat. features)
        ema50 = PatternFeatures._ema(prices, 99) if len(prices) >= 99 else ema21  # EMA99 (filtre long terme)
        
        features['ema9'] = ema9
        features['ema21'] = ema21
        features['ema_diff'] = (ema9 - ema21) / ema21 * 100  # Écart EMA en %
        features['ema_slope'] = PatternFeatures._ema_slope(prices, EMA_SHORT)  # Pente EMA court terme (5 périodes)
        features['ema_slope_long'] = PatternFeatures._ema_slope(prices, EMA_SHORT, lookback=15)  # Pente EMA moyen terme
        features['ema21_slope'] = PatternFeatures._ema_slope(prices, EMA_LONG, lookback=10)  # Pente EMA25
        features['ema_cross_distance'] = abs(ema9 - ema21) / ema21 * 100  # Distance au croisement
        
        # === TENDANCE EMA GLOBALE ===
        # Pour acheter: EMA9 moyen terme doit être positive + momentum positif
        ema_slope = features['ema_slope']
        ema_slope_long = features['ema_slope_long']
        ema21_slope = features['ema21_slope']
        
        # Bullish: Tendance moyen terme positive ET court terme pas fortement négatif
        ema9_trend_ok = ema_slope_long >= 0 and ema_slope > -0.15
        ema21_trend_ok = ema21_slope >= -0.1
        features['ema_trend_bullish'] = 1 if (ema9_trend_ok and ema21_trend_ok) else 0
        
        # Bearish: Tendance moyen terme négative OU court terme fortement négatif
        # Pour éviter les achats quand l'EMA descend
        features['ema_trend_bearish'] = 1 if (ema_slope_long < 0 or (ema_slope < 0 and ema21_slope < 0)) else 0
        
        # === DÉTECTION CORRECTION APRÈS PIC ===
        # Si le prix DESCEND vers les EMAs alors que EMA9 > EMA21, c'est une CORRECTION
        # On ne doit PAS acheter pendant une correction!
        price_above_ema9 = (prices[-1] - ema9) / ema9 * 100  # % au-dessus de EMA9
        price_above_ema21 = (prices[-1] - ema21) / ema21 * 100  # % au-dessus de EMA21
        
        # Correction = prix qui descend vers les EMAs depuis un récent pic
        recent_high = max(prices[-10:])  # Plus haut des 10 dernières bougies
        price_drop_from_high = (prices[-1] - recent_high) / recent_high * 100  # Chute depuis le pic
        
        # 🔧 FIX AUDIT 28/02: Pré-calculer momentum_3 ICI (avant utilisation)
        # Bug: features['momentum_3'] n'existe pas encore → .get() retournait toujours 0
        # → is_correction ne se déclenchait jamais (détection correction morte)
        momentum_3_precalc = (prices[-1] - prices[-3]) / prices[-3] * 100 if len(prices) >= 3 else 0
        
        # Marquer comme correction si:
        # 1. EMA9 > EMA21 (configuration haussière héritée)
        # 2. MAIS le prix a chuté de plus de 0.5% depuis le récent pic
        # 3. ET momentum négatif (prix en baisse)
        momentum_3 = momentum_3_precalc
        is_correction = (
            ema9 > ema21 and                    # EMAs encore en mode haussier
            price_drop_from_high < -0.3 and     # Chute de 0.3%+ depuis le pic
            momentum_3 < 0                       # Momentum actuel négatif
        )
        features['price_correcting'] = 1 if is_correction else 0
        features['price_drop_from_high'] = price_drop_from_high
        features['price_above_ema9'] = price_above_ema9
        features['price_above_ema21'] = price_above_ema21
        
        # === DÉTECTION CROISEMENT EMA (CRUCIAL pour timing d'achat!) ===
        # Détecter quand EMA_SHORT VIENT DE croiser EMA_LONG par le haut
        if len(prices) >= EMA_LONG + 2:
            # Calculer EMA short/long pour les 3 dernières bougies
            ema9_prev2 = PatternFeatures._ema(prices[:-2], EMA_SHORT)
            ema21_prev2 = PatternFeatures._ema(prices[:-2], EMA_LONG)
            ema9_prev1 = PatternFeatures._ema(prices[:-1], EMA_SHORT)
            ema21_prev1 = PatternFeatures._ema(prices[:-1], EMA_LONG)
            
            # Croisement haussier récent : EMA9 était sous EMA21, maintenant au-dessus
            was_below_2 = ema9_prev2 < ema21_prev2
            was_below_1 = ema9_prev1 < ema21_prev1
            is_above_now = ema9 > ema21
            
            # Croisement dans les 1-2 dernières bougies
            features['ema_cross_bullish'] = 1 if ((was_below_2 or was_below_1) and is_above_now) else 0
            # Croisement très récent (1 bougie)
            features['ema_cross_fresh'] = 1 if (was_below_1 and is_above_now) else 0
            # Croisement baissier (EMA9 passe sous EMA21)
            features['ema_cross_bearish'] = 1 if (not was_below_1 and not is_above_now) else 0
        else:
            features['ema_cross_bullish'] = 0
            features['ema_cross_fresh'] = 0
            features['ema_cross_bearish'] = 0
        
        # === BOLLINGER BANDS ===
        bb_upper, bb_mid, bb_lower = PatternFeatures._bollinger(prices)
        bb_bandwidth = (bb_upper - bb_lower) / bb_mid * 100 if bb_mid > 0 else 0
        
        features['bb_bandwidth'] = bb_bandwidth
        features['bb_upper'] = bb_upper
        features['bb_lower'] = bb_lower
        features['bb_mid'] = bb_mid
        features['bb_position'] = (prices[-1] - bb_lower) / (bb_upper - bb_lower) if (bb_upper - bb_lower) > 0 else 0.5
        # 🔧 FIX: Squeeze = UNIQUEMENT contraction de bandwidth (pas near_lower qui est un signal différent)
        # near_bb_lower indique un prix bas (potentiel rebond), PAS un squeeze
        near_bb_lower = features['bb_position'] < 0.20
        features['bb_squeeze'] = 1 if bb_bandwidth < 4.0 else 0  # Squeeze = bandwidth contraction ONLY
        features['near_bb_lower'] = 1 if near_bb_lower else 0
        
        # === KELTNER CHANNELS (pour confirmation breakout) ===
        kc_upper, kc_mid, kc_lower = PatternFeatures._keltner(prices)
        features['kc_upper'] = kc_upper
        features['kc_mid'] = kc_mid
        features['kc_lower'] = kc_lower
        features['price_above_kc_mid'] = 1 if prices[-1] > kc_mid else 0
        features['price_above_kc_upper'] = 1 if prices[-1] > kc_upper else 0
        # KC upper > BB upper = expansion de volatilité = mouvement fort
        features['kc_above_bb'] = 1 if kc_upper > bb_upper else 0
        
        # === DÉTECTION SQUEEZE AVANCÉE (Stratégie Bollinger Squeeze Breakout) ===
        # Calculer le bandwidth historique pour détecter une contraction
        historical_bandwidths = []
        for i in range(10, min(50, len(prices))):
            bbu, bbm, bbl = PatternFeatures._bollinger(prices[:i+1])
            if bbm > 0:
                historical_bandwidths.append((bbu - bbl) / bbm * 100)
        
        if historical_bandwidths:
            avg_bandwidth = np.mean(historical_bandwidths)
            features['bb_squeeze_ratio'] = bb_bandwidth / avg_bandwidth if avg_bandwidth > 0 else 1
            # Squeeze confirmé si bandwidth actuel < 60% de la moyenne
            features['bb_squeeze_confirmed'] = 1 if features['bb_squeeze_ratio'] < 0.6 else 0
        else:
            features['bb_squeeze_ratio'] = 1
            features['bb_squeeze_confirmed'] = 0
        
        # Durée du squeeze (combien de bougies consécutives en squeeze)
        squeeze_duration = 0
        for i in range(1, min(20, len(prices))):
            if len(prices) >= 20 + i:
                bbu, bbm, bbl = PatternFeatures._bollinger(prices[:-i])
                bw = (bbu - bbl) / bbm * 100 if bbm > 0 else 100
                if bw < 3:
                    squeeze_duration += 1
                else:
                    break
        features['squeeze_duration'] = squeeze_duration
        
        # Prix en expansion après squeeze (breakout)
        features['breakout_strength'] = 0
        if squeeze_duration >= 3 and bb_bandwidth > 3:
            # Le squeeze vient de se terminer = breakout potentiel
            features['breakout_strength'] = (bb_bandwidth / 3) * 100  # Force du breakout
        
        # === RSI ===
        rsi = PatternFeatures._rsi(prices)
        features['rsi'] = rsi
        features['rsi_oversold'] = 1 if rsi < 30 else 0
        features['rsi_overbought'] = 1 if rsi > 70 else 0
        
        # === MOMENTUM ===
        features['momentum_5'] = (prices[-1] - prices[-5]) / prices[-5] * 100 if len(prices) >= 5 else 0
        features['momentum_10'] = (prices[-1] - prices[-10]) / prices[-10] * 100 if len(prices) >= 10 else 0
        features['momentum_3'] = (prices[-1] - prices[-3]) / prices[-3] * 100 if len(prices) >= 3 else 0
        
        # === SIGNAUX DE RETOURNEMENT (pour vente anticipée) ===
        # Détecter les premiers signes de faiblesse après une hausse
        if len(prices) >= 5:
            # Le prix a atteint un pic et commence à baisser
            recent_high = max(prices[-5:])
            features['distance_from_high'] = (prices[-1] - recent_high) / recent_high * 100
            
            # Le prix baisse sur les 2 dernières bougies
            features['price_declining'] = 1 if prices[-1] < prices[-2] < prices[-3] else 0
            
            # EMA court terme commence à s'aplatir ou baisser après une hausse
            ema9_prev = PatternFeatures._ema(prices[:-1], EMA_SHORT)
            features['ema9_turning'] = 1 if ema9 < ema9_prev and features['momentum_10'] > 2 else 0
            
            # RSI en zone de surachat et commence à baisser
            rsi_prev = PatternFeatures._rsi(prices[:-1]) if len(prices) > 15 else rsi
            features['rsi_turning_down'] = 1 if rsi > 65 and rsi < rsi_prev else 0
        else:
            features['distance_from_high'] = 0
            features['price_declining'] = 0
            features['ema9_turning'] = 0
            features['rsi_turning_down'] = 0
        
        # Score de signal de vente (0-100)
        sell_signals = 0
        if features['rsi_overbought']: sell_signals += 25
        if features['rsi_turning_down']: sell_signals += 25
        if features['ema9_turning']: sell_signals += 25
        if features['price_declining']: sell_signals += 15
        if features['distance_from_high'] < -1: sell_signals += 10
        features['sell_signal_score'] = min(100, sell_signals)
        
        # === VOLATILITÉ ===
        if len(prices) >= 21:
            price_slice = prices[-21:]
            returns = np.diff(price_slice) / price_slice[:-1]
            features['volatility'] = np.std(returns) * 100
        else:
            features['volatility'] = 0

        # === DÉTECTION COIN PLAT STRUCTUREL (ex: ANKR) ===
        # Range sur 48h+ : si le coin n'a jamais bougé de plus de 5%, inutile de le trader
        # Distinction avec squeeze (temporaire): bb_squeeze_confirmed=1 si compression DEPUIS état volatil
        _slice_48h = prices[-48:] if len(prices) >= 48 else prices
        _range_48h = (max(_slice_48h) - min(_slice_48h)) / max(min(_slice_48h), 1e-10) * 100 if len(_slice_48h) >= 3 else 99.0
        features['range_48h'] = round(_range_48h, 2)
        # is_flat_market: range < 5% + faible volatilité intrinsèque + PAS un squeeze réel
        features['is_flat_market'] = bool(
            _range_48h < 5.0 and
            features.get('volatility', 99) < 0.15 and
            not features.get('bb_squeeze_confirmed', 0)  # Squeeze confirmé = compression temporaire OK
        )
        
        # === PATTERNS DE PRIX ===
        # Higher highs / Lower lows
        highs_5 = [max(prices[i-5:i]) for i in range(5, len(prices), 5)][-3:] if len(prices) >= 15 else [prices[-1]]
        features['higher_highs'] = 1 if len(highs_5) >= 2 and all(highs_5[i] > highs_5[i-1] for i in range(1, len(highs_5))) else 0
        
        # Support/Resistance proximity
        recent_high = max(prices[-20:])
        recent_low = min(prices[-20:])
        features['near_resistance'] = 1 if prices[-1] > recent_high * 0.98 else 0
        features['near_support'] = 1 if prices[-1] < recent_low * 1.02 else 0
        
        # === VOLUME (si disponible) ===
        if volumes is not None and len(volumes) >= 20:
            volumes = np.array(volumes)
            avg_volume = np.mean(volumes[-20:])
            features['volume_ratio'] = volumes[-1] / avg_volume if avg_volume > 0 else 1
            features['volume_trend'] = (np.mean(volumes[-5:]) - np.mean(volumes[-10:-5])) / np.mean(volumes[-10:-5]) * 100 if np.mean(volumes[-10:-5]) > 0 else 0
        else:
            features['volume_ratio'] = 1
            features['volume_trend'] = 0
        
        # ═══════════════════════════════════════════════════════════════
        # 🆕 EARLY CYCLE DETECTION FEATURES
        # Détection PRÉCOCE des cycles haussiers pour capturer les gains tôt
        # ═══════════════════════════════════════════════════════════════
        
        # 1. RSI DIVERGENCE HAUSSIÈRE: Prix baisse mais RSI monte = retournement imminent
        # C'est l'un des signaux les plus fiables de retournement
        features['rsi_divergence_bullish'] = 0
        if len(prices) >= 15:
            rsi_current = features['rsi']
            rsi_prev = PatternFeatures._rsi(prices[:-5]) if len(prices) > 20 else rsi_current
            price_change_5 = features.get('price_change_5', 0)
            # Prix baisse mais RSI monte = divergence haussière
            if price_change_5 < -0.1 and rsi_current > rsi_prev + 2:
                features['rsi_divergence_bullish'] = 1
            # RSI en zone basse qui commence à monter (signal early)
            elif rsi_current < 45 and rsi_current > rsi_prev + 1:
                features['rsi_divergence_bullish'] = 0.5  # Signal partiel
        
        # 2. EMA CONVERGENCE SPEED: Vitesse à laquelle EMA_SHORT se rapproche d'EMA_LONG
        # Plus ça converge vite, plus le croisement est imminent
        features['ema_convergence_speed'] = 0
        if len(prices) >= EMA_LONG + 4:
            ema_diff_current = features['ema_diff']  # en %
            ema9_prev3 = PatternFeatures._ema(prices[:-3], EMA_SHORT)
            ema21_prev3 = PatternFeatures._ema(prices[:-3], EMA_LONG)
            ema_diff_prev3 = (ema9_prev3 - ema21_prev3) / ema21_prev3 * 100 if ema21_prev3 > 0 else 0
            # Convergence = ema_diff se rapproche de 0 (écart diminue)
            if ema_diff_current < 0:  # EMA9 sous EMA21
                convergence = ema_diff_prev3 - ema_diff_current  # Positif si ça converge
                features['ema_convergence_speed'] = max(0, convergence)
            elif ema_diff_current > 0 and ema_diff_current < 0.3:  # Just crossed
                features['ema_convergence_speed'] = 0.5  # Already converged
        
        # 3. MOMENTUM ACCELERATION: Dérivée du momentum (change de direction)
        # momentum_3 qui augmente = accélération haussière
        features['momentum_acceleration'] = 0
        features['momentum_jerk'] = 0  # 2nd derivative
        if len(prices) >= 8:
            mom_3_current = features['momentum_3']
            mom_3_prev = (prices[-4] - prices[-7]) / prices[-7] * 100 if prices[-7] > 0 else 0
            features['momentum_acceleration'] = mom_3_current - mom_3_prev
            # Jerk = acceleration change (positive = momentum is strengthening its upward push)
            if len(prices) >= 11:
                mom_3_prev2 = (prices[-7] - prices[-10]) / prices[-10] * 100 if prices[-10] > 0 else 0
                accel_prev = mom_3_prev - mom_3_prev2
                features['momentum_jerk'] = features['momentum_acceleration'] - accel_prev
        
        # 4. VOLUME PRECURSOR: Volume qui augmente AVANT le mouvement de prix
        # Volume surge + prix stable = accumulation (smart money entre)
        features['volume_precursor'] = 0
        if volumes is not None and len(volumes) >= 10:
            vol_ratio = features.get('volume_ratio', 1)
            price_stable = abs(features.get('momentum_3', 0)) < 0.3  # Prix stable (<0.3%)
            vol_increasing = vol_ratio > 1.3  # Volume +30% vs moyenne
            if vol_increasing and price_stable:
                features['volume_precursor'] = vol_ratio  # Plus le ratio est haut, plus fort le signal
        
        # 5. EARLY CYCLE COMPOSITE SCORE (0-100)
        # Combine tous les signaux précoces en un score unique
        early_score = 0
        # RSI divergence contribution (0-25)
        early_score += features['rsi_divergence_bullish'] * 25
        # EMA convergence contribution (0-25) 
        conv_speed = min(1.0, features['ema_convergence_speed'] / 0.15)  # Normalize to 0-1
        early_score += conv_speed * 25
        # Momentum acceleration contribution (0-25)
        if features['momentum_acceleration'] > 0:
            accel_normalized = min(1.0, features['momentum_acceleration'] / 0.5)
            early_score += accel_normalized * 25
        # Volume precursor contribution (0-25)
        if features['volume_precursor'] > 0:
            vol_normalized = min(1.0, (features['volume_precursor'] - 1.0) / 1.0)
            early_score += vol_normalized * 25
        features['early_cycle_score'] = min(100, early_score)
        
        # 6. EMA CROSS LOOKBACK: Combien de bougies depuis le dernier croisement haussier
        # Permet de détecter les crossovers plus anciens (pas seulement 1-2 bougies)
        features['candles_since_bullish_cross'] = 99  # Défaut: pas de croisement détecté
        if len(prices) >= EMA_LONG + 2:
            for lookback in range(1, min(15, len(prices) - EMA_LONG)):
                ema9_lb = PatternFeatures._ema(prices[:-lookback], EMA_SHORT) if lookback > 0 else ema9
                ema21_lb = PatternFeatures._ema(prices[:-lookback], EMA_LONG) if lookback > 0 else ema21
                ema9_lb_prev = PatternFeatures._ema(prices[:-(lookback+1)], EMA_SHORT) if lookback + 1 < len(prices) - EMA_SHORT else ema9_lb
                ema21_lb_prev = PatternFeatures._ema(prices[:-(lookback+1)], EMA_LONG) if lookback + 1 < len(prices) - EMA_LONG else ema21_lb
                
                if ema9_lb_prev < ema21_lb_prev and ema9_lb >= ema21_lb:
                    features['candles_since_bullish_cross'] = lookback
                    break
        
        return features
    
    @staticmethod
    def _ema(prices: np.ndarray, period: int) -> float:
        """Calcule l'EMA"""
        if len(prices) < period:
            return prices[-1]
        mult = 2 / (period + 1)
        ema = np.mean(prices[:period])
        for price in prices[period:]:
            ema = (price * mult) + (ema * (1 - mult))
        return ema
    
    @staticmethod
    def _ema_slope(prices: np.ndarray, period: int, lookback: int = 5) -> float:
        """Calcule la pente de l'EMA"""
        if len(prices) < period + lookback:
            return 0
        ema_current = PatternFeatures._ema(prices, period)
        ema_prev = PatternFeatures._ema(prices[:-lookback], period)
        return (ema_current - ema_prev) / ema_prev * 100 if ema_prev > 0 else 0
    
    @staticmethod
    def _bollinger(prices: np.ndarray, period: int = 20, std_dev: int = 2) -> Tuple[float, float, float]:
        """Calcule les bandes de Bollinger"""
        if len(prices) < period:
            return prices[-1], prices[-1], prices[-1]
        sma = np.mean(prices[-period:])
        std = np.std(prices[-period:])
        return sma + std_dev * std, sma, sma - std_dev * std
    
    @staticmethod
    def _keltner(prices: np.ndarray, ema_period: int = 20, atr_period: int = 10, atr_mult: float = 2.0) -> Tuple[float, float, float]:
        """Calcule les Keltner Channels basés sur l'ATR
        
        Returns:
            (kc_upper, kc_mid, kc_lower)
        """
        if len(prices) < max(ema_period, atr_period + 1):
            return prices[-1], prices[-1], prices[-1]
        
        # EMA centrale (base du Keltner)
        kc_mid = PatternFeatures._ema(prices, ema_period)
        
        # ATR (Average True Range) - simplifié sans high/low
        # Utilise la variation absolue du prix comme proxy
        true_ranges = []
        for i in range(1, min(atr_period + 1, len(prices))):
            tr = abs(prices[-i] - prices[-i-1])
            true_ranges.append(tr)
        
        atr = np.mean(true_ranges) if true_ranges else 0
        
        kc_upper = kc_mid + atr_mult * atr
        kc_lower = kc_mid - atr_mult * atr
        
        return kc_upper, kc_mid, kc_lower
    
    @staticmethod
    def _rsi(prices: np.ndarray, period: int = 14) -> float:
        """Calcule le RSI"""
        if len(prices) < period + 1:
            return 50
        deltas = np.diff(prices)
        gains = np.where(deltas > 0, deltas, 0)
        losses = np.where(deltas < 0, -deltas, 0)
        avg_gain = np.mean(gains[-period:])
        avg_loss = np.mean(losses[-period:])
        if avg_loss == 0:
            return 100
        rs = avg_gain / avg_loss
        return 100 - (100 / (1 + rs))


class WatchlistItem:
    """Item de surveillance avec scoring IA"""
    
    def __init__(self, symbol: str):
        self.symbol = symbol
        self.score = 0  # Score de probabilité de hausse (0-100)
        self.pattern = None  # Pattern détecté
        self.trigger_price = None  # Prix de déclenchement
        self.predicted_gain = 0  # Gain prédit en %
        self.confidence = 0  # Confiance de la prédiction (0-100)
        self.features = {}  # Features extraites
        self.last_update = None
        self.status = "watching"  # watching, ready, triggered, expired
        self.reason = ""
        self.countdown = 0  # Minutes restantes estimées avant signal


class AIPredictor:
    """
    Système de prédiction IA pour anticiper les mouvements de marché
    Utilise le GPU pour l'analyse massive de patterns
    """
    
    # Chemin absolu vers le dossier du script
    SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
    
    # Flag pour logger le warning PyTorch qu'une seule fois
    _pytorch_warning_logged = False
    
    def __init__(self, model_path: str = None):
        # Utiliser un chemin absolu par défaut
        if model_path is None:
            model_path = os.path.join(self.SCRIPT_DIR, "models", "predictor.h5")
        self.model_path = model_path
        self.model = None
        self.klines_fetcher = None  # Fonction pour récupérer les klines (injectée)
        self.binance_client = None  # Client Binance pour long_term_trend_analyzer
        self.watchlist: Dict[str, WatchlistItem] = {}
        self.historical_patterns = []  # Patterns passés pour apprentissage
        self.prediction_history = []  # Historique des prédictions
        self.is_running = False
        self.update_interval = 5  # Secondes entre les mises à jour (réactivité maximale - détection rapide)
        self._lock = threading.Lock()
        
        # ═══ RESET MODE CRASH AU DÉMARRAGE ═══
        # Éviter que le mode crash reste bloqué entre redémarrages
        AIPredictor._market_crash_mode = False
        AIPredictor._btc_momentum = 0.0
        AIPredictor._last_market_check = None
        logger.info("🔄 Mode crash réinitialisé au démarrage")
        
        # Seuils de déclenchement
        self.SCORE_THRESHOLD = 60  # Score minimum pour déclencher un achat (réduit de 70)
        self.CONFIDENCE_THRESHOLD = 55  # Confiance minimum (réduit de 60)
        
        # === STATISTIQUES D'ENTRAÎNEMENT ===
        self.training_stats = {
            'status': 'untrained',  # untrained, training, trained
            'samples_count': 0,      # Nombre d'échantillons utilisés
            'epochs_completed': 0,   # Nombre d'epochs effectués
            'last_loss': None,       # Dernière loss
            'last_accuracy': None,   # Dernière accuracy
            'last_training': None,   # Date du dernier entraînement
            'predictions_made': 0,   # Nombre de prédictions effectuées
            'correct_predictions': 0, # Prédictions correctes (après validation)
            'validation_accuracy': 0, # Précision sur validation
            'gpu_name': None,        # Nom du GPU
            'gpu_available': TORCH_AVAILABLE and DEVICE == 'cuda'
        }
        
        # Charger les stats sauvegardées
        self._load_training_stats()
        
        # === INITIALISER LES NOUVEAUX ANALYSEURS ===
        self.multi_tf_analyzer = get_multi_tf_analyzer() if MULTI_TIMEFRAME_AVAILABLE else None
        self.volume_analyzer = get_volume_analyzer() if VOLUME_PROFILE_AVAILABLE else None
        self.regime_detector = get_regime_detector() if REGIME_DETECTOR_AVAILABLE else None
        self.feature_extractor = get_feature_extractor() if FEATURE_EXTRACTOR_AVAILABLE else None
        self.time_analyzer = get_time_pattern_analyzer() if TIME_PATTERN_AVAILABLE else None
        self.correlation_analyzer = get_correlation_analyzer() if CORRELATION_ANALYZER_AVAILABLE else None
        self.risk_scorer = get_risk_adjusted_scorer() if RISK_ADJUSTED_AVAILABLE else None
        self.monte_carlo = get_monte_carlo_simulator(num_simulations=1000) if MONTE_CARLO_AVAILABLE else None
        self.ensemble = get_ensemble_predictor() if ENSEMBLE_PREDICTOR_AVAILABLE else None
        
        # === LSTM REVERSAL PREDICTOR (GPU BiLSTM + Attention) ===
        self.reversal_predictor = None
        if LSTM_REVERSAL_AVAILABLE:
            try:
                self.reversal_predictor = get_reversal_predictor()
                logger.info(f"🧠 LSTM Reversal Predictor initialisé (trained={self.reversal_predictor.is_trained})")
            except Exception as e:
                logger.warning(f"⚠️ Erreur init LSTM Reversal: {e}")
        
        if MULTI_TIMEFRAME_AVAILABLE or VOLUME_PROFILE_AVAILABLE or REGIME_DETECTOR_AVAILABLE:
            logger.info("🚀 Modules d'amélioration IA activés avec succès")
        
        # 🔧 FIX 07/02: Cache pour le régime marché (évite appels répétés)
        self._cached_market_regime = 'NEUTRAL'
        self._regime_cache_time = 0
        
        # Charger ou créer le modèle
        self._init_model()
    
    def _load_training_stats(self):
        """Charge les statistiques d'entraînement sauvegardées"""
        stats_file = os.path.join(self.SCRIPT_DIR, "ai_training_stats.json")
        try:
            if os.path.exists(stats_file):
                with open(stats_file, 'r') as f:
                    saved_stats = json.load(f)
                    self.training_stats.update(saved_stats)
                    logger.info(f"📊 Stats d'entraînement chargées: {saved_stats.get('status', 'unknown')}")
        except Exception as e:
            logger.warning(f"Impossible de charger les stats: {e}")
        
        # Mettre à jour les infos GPU
        if TORCH_AVAILABLE and DEVICE == 'cuda':
            try:
                self.training_stats['gpu_name'] = torch.cuda.get_device_name(0)
                self.training_stats['gpu_available'] = True
            except:
                pass
    
    def _save_training_stats(self):
        """Sauvegarde les statistiques d'entraînement"""
        stats_file = os.path.join(self.SCRIPT_DIR, "ai_training_stats.json")
        try:
            with open(stats_file, 'w') as f:
                json.dump(self.training_stats, f, indent=2, default=str)
        except Exception as e:
            logger.warning(f"Impossible de sauvegarder les stats: {e}")
    
    def _get_market_regime(self) -> str:
        """
        🔧 FIX 07/02: Récupère le régime marché de façon fiable.
        Lit le singleton partagé avec trading_bot.py (synchronisé via get_market_regime_detector).
        Cache le résultat 30s pour éviter les appels répétés.
        """
        import time as _time
        now = _time.time()
        # Cache 30s
        if now - self._regime_cache_time < 30 and self._cached_market_regime != 'NEUTRAL':
            return self._cached_market_regime
        
        regime = 'NEUTRAL'  # Fallback
        
        # Méthode 1: Singleton market_regime.py (synchronisé par trading_bot)
        try:
            from market_regime import get_market_regime_detector
            _singleton = get_market_regime_detector()
            if _singleton and hasattr(_singleton, 'current_regime'):
                _r = _singleton.current_regime
                if _r and _r != 'NEUTRAL':
                    regime = _r
                    self._cached_market_regime = regime
                    self._regime_cache_time = now
                    return regime
        except Exception:
            pass
        
        # Méthode 2: regime_detector (market_regime_detector.py)
        try:
            if self.regime_detector and hasattr(self.regime_detector, 'current_regime'):
                _r = self.regime_detector.current_regime
                if _r and _r not in ('NEUTRAL', 'SIDEWAYS'):
                    regime = _r
        except Exception:
            pass
        
        self._cached_market_regime = regime
        self._regime_cache_time = now
        return regime
    
    
    def get_training_info(self) -> Dict:
        """Retourne les informations d'entraînement pour le dashboard"""
        # Calculer le niveau d'entraînement (0-100%)
        level = 0
        if self.training_stats['status'] == 'trained':
            # Base: 50% si entraîné
            level = 50
            # +20% si accuracy > 60%
            if self.training_stats.get('last_accuracy', 0) and self.training_stats['last_accuracy'] > 0.6:
                level += 20
            # +15% si validation_accuracy > 55%
            if self.training_stats.get('validation_accuracy', 0) > 55:
                level += 15
            # +15% si > 1000 échantillons
            samples = self.training_stats.get('samples_count', 0)
            if samples > 1000:
                level += 15
            elif samples > 500:
                level += 10
            elif samples > 100:
                level += 5
        elif self.training_stats['status'] == 'training':
            level = 25
        
        return {
            'level': min(level, 100),
            'status': self.training_stats['status'],
            'status_label': {
                'untrained': '🔴 Non entraîné',
                'training': '🟡 En cours...',
                'trained': '🟢 Entraîné'
            }.get(self.training_stats['status'], 'Inconnu'),
            'samples_count': self.training_stats['samples_count'],
            'epochs': self.training_stats['epochs_completed'],
            'accuracy': self.training_stats.get('last_accuracy', 0) or 0,
            'loss': self.training_stats.get('last_loss', 0) or 0,
            'validation_accuracy': self.training_stats.get('validation_accuracy', 0),
            'predictions_made': self.training_stats['predictions_made'],
            'correct_predictions': self.training_stats['correct_predictions'],
            'last_training': self.training_stats.get('last_training'),
            'gpu_name': self.training_stats.get('gpu_name', 'CPU'),
            'gpu_available': self.training_stats['gpu_available'],
            'model_loaded': self.model is not None
        }
    
    def _init_model(self):
        """Initialise le modèle de prédiction PyTorch"""
        if not TORCH_AVAILABLE:
            # Logger le warning qu'une seule fois pour éviter la pollution des logs
            if not AIPredictor._pytorch_warning_logged:
                logger.info("ℹ️  PyTorch non disponible, utilisation du mode règles (performance réduite)")
                AIPredictor._pytorch_warning_logged = True
            return
        
        try:
            model_path_pt = self.model_path.replace('.h5', '.pt')
            logger.info(f"🔍 Recherche modèle: {model_path_pt}")
            logger.info(f"   Fichier existe: {os.path.exists(model_path_pt)}")
            
            if os.path.exists(model_path_pt):
                # Autoriser explicitement la classe PredictorLSTM pour PyTorch 2.6+
                try:
                    torch.serialization.add_safe_globals([PredictorLSTM])
                except:
                    pass
                
                # Charger directement avec weights_only=False car le modèle utilise PredictorLSTM
                try:
                    loaded = torch.load(model_path_pt, map_location=DEVICE, weights_only=False)
                    
                    if isinstance(loaded, dict) and 'model_state_dict' in loaded:
                        # Format state_dict: reconstruire le modèle avec les paramètres sauvegardés
                        sd = loaded['model_state_dict']
                        # Déduire l'architecture depuis le state_dict (évite les incompatibilités)
                        # lstm.weight_ih_l0 shape = [4*hidden, input] → hidden = shape[0] // 4
                        ih_l0 = sd.get('lstm.weight_ih_l0', sd.get('lstm.weight_ih_l0_reverse'))
                        saved_hidden  = ih_l0.shape[0] // 4 if ih_l0 is not None else 128
                        saved_input   = ih_l0.shape[1]        if ih_l0 is not None else 20
                        saved_layers  = sum(1 for k in sd if k.startswith('lstm.weight_ih_l') and '_reverse' not in k)
                        has_fc3       = 'fc3.weight' in sd
                        
                        if has_fc3:
                            # Nouvelle architecture : fc1(hidden→64) fc2(64→32) fc3(32→3)
                            compat_model = PredictorLSTM(
                                input_size=saved_input,
                                hidden_size=saved_hidden,
                                num_layers=max(saved_layers, 1)
                            ).to(DEVICE)
                        else:
                            # Ancienne architecture : fc1(hidden→32) fc2(32→3) — 2 couches seulement
                            class _LegacyLSTM(torch.nn.Module):
                                def __init__(self, inp, hid, nl):
                                    super().__init__()
                                    self.lstm = torch.nn.LSTM(inp, hid, nl, batch_first=True)
                                    self.dropout = torch.nn.Dropout(0.2)
                                    self.fc1 = torch.nn.Linear(hid, 32)
                                    self.fc2 = torch.nn.Linear(32, 3)
                                def forward(self, x):
                                    out, _ = self.lstm(x)
                                    x = self.dropout(out[:, -1, :])
                                    return self.fc2(torch.relu(self.fc1(x)))
                            compat_model = _LegacyLSTM(saved_input, saved_hidden, max(saved_layers, 1)).to(DEVICE)
                        
                        compat_model.load_state_dict(sd)
                        self.model = compat_model
                        logger.info(f"✅ Modèle PyTorch chargé (state_dict, hidden={saved_hidden}, layers={saved_layers}): {model_path_pt}")
                    else:
                        # Format modèle complet
                        self.model = loaded
                        logger.info(f"✅ Modèle PyTorch chargé (complet): {model_path_pt}")
                        
                except Exception as e:
                    # Créer un nouveau modèle si le chargement échoue
                    logger.warning(f"   Impossible de charger le modèle: {e}")
                    self.model = self._create_model()
                    logger.info("✅ Nouveau modèle PyTorch créé (non entraîné)")
                
                if self.model:
                    self.model.eval()
            else:
                # Créer un nouveau modèle LSTM pour la prédiction
                self.model = self._create_model()
                logger.info("✅ Nouveau modèle PyTorch créé (non entraîné)")
        except Exception as e:
            logger.error(f"Erreur chargement modèle: {e}")
            self.model = None
    
    def _create_model(self):
        """Crée un modèle LSTM PyTorch pour la prédiction de prix"""
        if not TORCH_AVAILABLE:
            return None
        
        # Utilise la classe PredictorLSTM définie globalement (compatible PyTorch 2.6+)
        model = PredictorLSTM().to(DEVICE)
        return model
    
    # Variable de classe pour tracker l'état du marché global
    _market_crash_mode = False
    _last_market_check = None
    _btc_momentum = 0.0
    
    def analyze_symbol(self, symbol: str, prices: List[float], volumes: List[float] = None) -> WatchlistItem:
        """
        Analyse complète d'un symbole pour détecter les opportunités
        Retourne un WatchlistItem avec le score et les prédictions
        Utilise le scorer GPU avancé si disponible
        
        L'ANALYSE CONTINUE TOUJOURS - Seul l'ACHAT est bloqué en mode crash
        """
        item = WatchlistItem(symbol)
        item.last_update = datetime.now()
        
        # Mémoriser si on est en mode crash (pour bloquer l'achat plus tard)
        is_market_crash = AIPredictor._market_crash_mode and symbol != 'BTCUSDT'
        
        # ═══════════════════════════════════════════════════════════════════════
        # FILTRE CRITIQUE: REJETER LES STABLECOINS ET ACTIFS À FAIBLE VOLATILITÉ
        # Les stablecoins sont conçus pour rester à ~1$, ils ne peuvent JAMAIS
        # atteindre un Take-Profit de 0.5% car leur volatilité est quasi nulle!
        # PAXG (or tokenisé) a aussi une volatilité trop faible pour le trading!
        # ═══════════════════════════════════════════════════════════════════════
        STABLECOINS = [
            # Stablecoins
            'USDCUSDT', 'BUSDUSDT', 'DAIUSDT', 'TUSDUSDT', 'FDUSDUSDT',
            'USDPUSDT', 'FRAXUSDT', 'LUSDUSDT', 'USTCUSDT', 'PYUSDUSDT',
            # Devises fiat
            'EURUSDT', 'GBPUSDT',
            # Or tokenisé
            'PAXGUSDT', 'XAUTUSDT',
            # Wrapped tokens
            'WBTCUSDT', 'STETHUSDT', 'RETHUSDT', 'BETHUSDT', 'WBETHUSDT', 'CBETHUSDT',
            # Tokens problématiques
            'LUNCUSDT'
        ]
        base_asset = symbol.replace('USDT', '')
        is_stablecoin = symbol in STABLECOINS or 'USD' in base_asset or 'EUR' in base_asset or 'PAXG' in symbol
        
        if is_stablecoin:
            item.score = 0
            item.pattern = 'STABLECOIN_REJECTED'
            item.status = 'rejected'
            item.confidence = 0
            logger.debug(f"🚫 {symbol}: Stablecoin rejeté - ne peut jamais atteindre TP!")
            return item
        
        # === UTILISER LE SCORER AVANCÉ GPU SI DISPONIBLE ===
        advanced_profile = None
        if ADVANCED_SCORER_AVAILABLE and len(prices) >= 50:
            try:
                scorer = get_advanced_scorer()
                advanced_profile = scorer.analyze_crypto(symbol, prices, volumes)
                
                # Stocker le profil avancé dans les features
                item.features = PatternFeatures.extract_features(prices, volumes) or {}
                item.features['advanced_profile'] = advanced_profile.to_dict()
                item.features['advanced_score'] = advanced_profile.final_score
                item.features['profit_potential'] = advanced_profile.profit_potential
                item.features['entry_quality'] = advanced_profile.entry_quality
                item.features['reversal_score'] = advanced_profile.reversal_score
                
                # Utiliser les scores du scorer avancé
                item.score = int(advanced_profile.final_score)
                # � FIX AUDIT 28/02: Sauvegarder le score GPU original pour caper les bonuses totaux
                gpu_original_score = item.score
                # �🔴 FIX 26/01 20h40: Ne plus assigner advanced_profile.signal au pattern
                # L'ancien système (ACHAT/POSSIBLE/NO_BUY) entre en conflit avec smart_criteria
                # Le pattern sera assigné plus tard par smart_criteria (ligne 2950+)
                item.pattern = 'NEUTRAL'  # Était: advanced_profile.signal
                item.confidence = advanced_profile.confidence
                
                # ══════════════════════════════════════════════════════════════════════
                # BONUS TOP 20: Appliquer IMMÉDIATEMENT pour que tous les tests utilisent le score correct
                # ══════════════════════════════════════════════════════════════════════
                if symbol in TOP_20_CRYPTOS:
                    old_score = item.score
                    item.score = min(100, item.score + TOP_20_SCORE_BONUS)
                    item.features['is_top20'] = True
                    item.features['top20_bonus'] = TOP_20_SCORE_BONUS
                    if item.score >= 60:
                        logger.info(f"⭐ {symbol}: TOP 20 BONUS +{TOP_20_SCORE_BONUS} ({old_score}→{item.score})")
                else:
                    item.features['is_top20'] = False
                
                # ══════════════════════════════════════════════════════════════════════
                # PERFORMANCE-BASED SCORING: Whitelist dynamique UNIQUEMENT
                # 🔴 FIX 28/01 18h: BLACKLIST DÉSACTIVÉE - Ne plus pénaliser les cryptos
                # L'IA doit analyser TOUTES les cryptos sans préjugés historiques
                # Seuls les stablecoins restent exclus (ligne 817)
                # ══════════════════════════════════════════════════════════════════════
                if PERFORMANCE_ANALYZER_AVAILABLE:
                    analyzer = get_performance_analyzer()
                    
                    # Vérifier si mise à jour nécessaire (auto-actualisation)
                    if analyzer.should_update():
                        analyzer.update()
                    
                    # 🚫 BLACKLIST DÉSACTIVÉE - Ne plus réduire les scores automatiquement
                    # Anciennement: perf_blacklist réduisait score de 50% pour SOL, ETH, etc.
                    # Problème: Cryptos légitimes (SOL, ETH) bloquées injustement
                    # Solution: Laisser l'IA décider selon conditions actuelles du marché
                    
                    # ✅ WHITELIST ACTIVE - Bonus pour cryptos performantes uniquement
                    
                    # WHITELIST: Bonus pour les cryptos performantes
                    perf_whitelist, whitelist_scores = analyzer.get_whitelist()
                    if symbol in perf_whitelist:
                        bonus_data = whitelist_scores.get(symbol, {})
                        bonus = bonus_data.get('bonus_score', 15)
                        old_score = item.score
                        item.score = min(100, item.score + bonus)
                        item.features['is_whitelisted'] = True
                        item.features['whitelist_bonus'] = bonus
                        logger.info(f"⭐ {symbol}: PERFORMANCE WHITELIST +{bonus} ({old_score}→{item.score}) - Win rate {bonus_data.get('win_rate', 'N/A')}")
                    else:
                        item.features['is_whitelisted'] = False
                
                # ══════════════════════════════════════════════════════════════════════
                # VOLATILITY-BASED SCORING: Prioriser les cryptos avec cycles réguliers
                # Identifie les cryptos qui ont des patterns exploitables (calme → breakout)
                # Note: L'update est fait en arrière-plan par auto_updater_service, pas ici!
                # ══════════════════════════════════════════════════════════════════════
                if VOLATILITY_SCORER_AVAILABLE:
                    try:
                        vol_scorer = get_volatility_scorer()
                        
                        # NE PAS faire l'update ici (trop lent: 29s pour 64 cryptos!)
                        # L'update est géré par auto_updater_service toutes les 6h
                        
                        vol_score = vol_scorer.get_score(symbol)
                        vol_category = vol_scorer.get_category(symbol)
                        
                        # Bloquer les cryptos avec mauvaise volatilité (< 40/100)
                        # 🔧 FIX 06/02: Ne plus faire return prématuré - laisser continuer l'analyse
                        # 🔧 FIX 28/02: Réduction 70%→40% pour ne pas tuer les scores
                        if vol_scorer.is_poor(symbol):
                            item.status = 'volatility_poor'
                            item.score = int(item.score * 0.6)  # Réduction 40% (était 70%)
                            logger.info(f"⚠️ {symbol}: VOLATILITÉ MAUVAISE (score={vol_score}) - Réduction 40%")
                            # 🔴 NE PLUS RETURN ICI - continuer l'analyse pour time_pattern, correlation, etc.
                        
                        # Bonus pour les cryptos excellentes (≥ 80/100) - SOL-like
                        if vol_scorer.is_excellent(symbol):
                            bonus = 20
                            old_score = item.score
                            item.score = min(100, item.score + bonus)
                            item.features['volatility_excellent'] = True
                            item.features['volatility_bonus'] = bonus
                            logger.info(f"🌟 {symbol}: VOLATILITÉ EXCELLENTE +{bonus} ({old_score}→{item.score}) - Score vol={vol_score}")
                        
                        # Bonus modéré pour les bonnes cryptos (≥ 60/100)
                        elif vol_category == 'good':
                            bonus = 10
                            old_score = item.score
                            item.score = min(100, item.score + bonus)
                            item.features['volatility_good'] = True
                            item.features['volatility_bonus'] = bonus
                            logger.info(f"✅ {symbol}: VOLATILITÉ BONNE +{bonus} ({old_score}→{item.score}) - Score vol={vol_score}")
                        
                        item.features['volatility_score'] = vol_score
                        item.features['volatility_category'] = vol_category
                    except Exception as e:
                        logger.warning(f"⚠️ Erreur volatility scorer pour {symbol}: {e} - Ignoré, analyse continue")
                        item.features['volatility_score'] = 50
                        item.features['volatility_category'] = 'unknown'
                
                # ══════════════════════════════════════════════════════════════════════
                # 🔴 FIX 01/04: CAPER LES BONUSES — Le score GPU reflète le marché réel.
                # Les bonuses (TOP20+whitelist+volatilité) ne doivent PAS masquer un signal
                # bearish. Max +25 points de bonus au-dessus du score GPU original.
                # Exemple: GPU=73 (bearish/oversold) → max autorisé = 98, pas 100
                # ══════════════════════════════════════════════════════════════════════
                max_bonus = 25
                if item.score > gpu_original_score + max_bonus:
                    capped_score = gpu_original_score + max_bonus
                    logger.info(f"🔒 {symbol}: BONUS CAPPÉ — GPU={gpu_original_score}, brut={item.score}, cappé={capped_score} (max +{max_bonus})")
                    item.score = capped_score
                item.features['gpu_original_score'] = gpu_original_score
                item.features['total_bonus'] = item.score - gpu_original_score
                
                # ══════════════════════════════════════════════════════════════════════
                # ANALYSE TENDANCE LONG TERME (4h-12h) - NOUVEAU 12/01/2026
                # Détecte les "Dead Cat Bounce" et tendances baissières prolongées
                # Cas FET: Tendance baissière 6h+ → rebond temporaire → rechute
                # ══════════════════════════════════════════════════════════════════════
                if LONG_TERM_TREND_AVAILABLE:
                    try:
                        # Utiliser binance_client seulement s'il est configuré
                        binance_client = getattr(self, 'binance_client', None)
                        lt_analyzer = get_long_term_analyzer(binance_client)
                        lt_analysis = lt_analyzer.analyze_long_term_trend(symbol, prices)
                        
                        # Stocker l'analyse
                        item.features['long_term_analysis'] = lt_analysis
                        item.features['trend_4h'] = lt_analysis['trend_4h']
                        item.features['trend_8h'] = lt_analysis.get('trend_8h', 0)
                        item.features['is_dead_cat_bounce'] = lt_analysis['is_dead_cat_bounce']
                        item.features['is_prolonged_downtrend'] = lt_analysis['is_prolonged_downtrend']
                        item.features['long_term_risk_score'] = lt_analysis['risk_score']
                        
                        # BLOQUER si dead cat bounce ou risque trop élevé
                        should_block, block_reason = lt_analyzer.should_block_buy(lt_analysis)
                        
                        if should_block:
                            item.status = 'long_term_blocked'
                            item.score = 0
                            item.pattern = 'DEAD_CAT_BOUNCE' if lt_analysis['is_dead_cat_bounce'] else 'PROLONGED_DOWNTREND'
                            item.confidence = 0
                            logger.warning(f"⛔ {symbol}: BLOQUÉ - {block_reason}")
                            logger.warning(f"   └─ Trend 4h: {lt_analysis['trend_4h']:.1f}%, Risk: {lt_analysis['risk_score']}")
                            return item
                        
                        # PÉNALISER le score si tendance baissière modérée
                        # NOUVEAU: Ne pénaliser QUE si tendance ACTUELLE aussi baissière
                        # Si tendance court terme HAUSSIÈRE, ignorer pénalité long terme (reprise!)
                        score_penalty = lt_analyzer.get_score_penalty(lt_analysis)
                        
                        # Protection contre fausses pénalités pendant reprises
                        current_momentum = item.features.get('momentum_3', 0)
                        ema_diff = item.features.get('ema_diff', 0)
                        lstm_rev_dir = item.features.get('lstm_reversal_direction', '')
                        
                        # 🔧 FIX 02/03: AND → OR pour la protection reprise
                        # KAVA avait momentum=+1.67% mais ema_diff=-1.31% → AND empêchait la réduction
                        # En reversal naissant, ema_diff est TOUJOURS négatif (EMA21 réagit lentement)
                        # Nouveau: momentum>0 OU ema_diff en amélioration (>-0.5%) = reprise probable
                        if current_momentum > 0 or ema_diff > -0.5:
                            # Reprise en cours! Pénalité long terme non pertinente
                            score_penalty = int(score_penalty * 0.2)  # Réduction 80%
                            if score_penalty > 0:
                                logger.info(f"📈 {symbol}: Pénalité long terme RÉDUITE (reprise: mom={current_momentum:.2f}% ema_diff={ema_diff:.2f}%)")
                        
                        # 🧠 FIX 02/03: LSTM REVERSAL_UP override → cap pénalité à 10 max
                        # Le BiLSTM VOIT le retournement avant les indicateurs !
                        if lstm_rev_dir == 'REVERSAL_UP' and score_penalty > 10:
                            score_penalty = 10
                            logger.info(f"🧠 {symbol}: LSTM REVERSAL_UP → pénalité long terme CAPPÉE à 10")
                        
                        if score_penalty > 0:
                            old_score = item.score
                            item.score = max(0, item.score - score_penalty)
                            item.features['long_term_penalty'] = score_penalty
                            
                            if score_penalty >= 20:
                                logger.info(f"⚠️ {symbol}: PÉNALITÉ TENDANCE LONG TERME -{score_penalty} ({old_score}→{item.score})")
                                logger.info(f"   └─ {lt_analysis['recommendation']}: {', '.join(lt_analysis['reasons'][:2])}")
                        
                    except Exception as e:
                        logger.error(f"Erreur analyse long terme {symbol}: {e}")
                
                # ══════════════════════════════════════════════════════════════════════
                # 🧠 LSTM REVERSAL PREDICTOR - Pré-analyse GPU (BiLSTM + Attention)
                # Détection anticipée des retournements AVANT les indicateurs techniques
                # Architecture: 3-layer BiLSTM × 256 + 4-head Attention sur 60 bougies
                # ══════════════════════════════════════════════════════════════════════
                lstm_reversal_result = None
                if self.reversal_predictor and len(prices) >= 60:
                    try:
                        prices_arr = np.array(prices, dtype=np.float64)
                        volumes_arr = np.array(volumes, dtype=np.float64) if volumes else None
                        
                        lstm_reversal_result = self.reversal_predictor.predict(prices_arr, volumes_arr)
                        
                        if lstm_reversal_result:
                            # Stocker les résultats dans les features pour utilisation en aval
                            item.features['lstm_reversal_class'] = lstm_reversal_result.get('reversal_class', 0)
                            item.features['lstm_reversal_label'] = lstm_reversal_result.get('reversal_label', 'NEUTRAL')
                            item.features['lstm_reversal_prob'] = lstm_reversal_result.get('reversal_probability', 0)
                            item.features['lstm_reversal_confidence'] = lstm_reversal_result.get('confidence', 0)
                            item.features['lstm_is_reversal_signal'] = lstm_reversal_result.get('is_reversal_signal', False)
                            item.features['lstm_is_danger_signal'] = lstm_reversal_result.get('is_danger_signal', False)
                            
                            # BONUS/MALUS basé sur la prédiction LSTM
                            if lstm_reversal_result.get('is_reversal_signal', False):
                                # REVERSAL_UP détecté par le LSTM → boost le score
                                rev_confidence = lstm_reversal_result.get('confidence', 0)
                                rev_prob = lstm_reversal_result.get('reversal_probability', 0)
                                lstm_bonus = min(20, int(rev_confidence * rev_prob * 0.3))
                                if lstm_bonus > 0:
                                    old_score = item.score
                                    item.score = min(100, item.score + lstm_bonus)
                                    item.features['lstm_reversal_bonus'] = lstm_bonus
                                    logger.info(f"🧠 {symbol}: LSTM REVERSAL_UP détecté! +{lstm_bonus} ({old_score}→{item.score}) conf={rev_confidence:.0f}% prob={rev_prob:.2f}")
                            
                            elif lstm_reversal_result.get('is_danger_signal', False):
                                # REVERSAL_DOWN détecté → pénaliser
                                rev_confidence = lstm_reversal_result.get('confidence', 0)
                                lstm_penalty = min(15, int(rev_confidence * 0.15))
                                if lstm_penalty > 0:
                                    old_score = item.score
                                    item.score = max(0, item.score - lstm_penalty)
                                    item.features['lstm_reversal_penalty'] = lstm_penalty
                                    logger.info(f"🧠 {symbol}: LSTM REVERSAL_DOWN détecté! -{lstm_penalty} ({old_score}→{item.score})")
                            
                            # Alimenter le buffer d'entraînement online
                            self.reversal_predictor.feed_data(prices_arr, volumes_arr)
                    except Exception as e:
                        logger.debug(f"Erreur LSTM reversal {symbol}: {e}")
                
                # ══════════════════════════════════════════════════════════════════════
                # ══════════════════════════════════════════════════════════════════════
                # 🔴 REFONTE CREUX_REBOUND 01/04 — STRATEGIE STRICTE
                # Principe fondamental en 2 phases:
                #   PHASE 1 — CREUX: prix en zone basse BB, RSI oversold
                #   PHASE 2 — REBOUND CONFIRMÉ: volume d'achat > moyenne,
                #     momentum positif, prix qui monte réellement.
                #
                # 4 conditions OBLIGATOIRES (toutes simultanées):
                #   1. bb_position < seuil → prix dans la zone basse des BB
                #   2. Volume >= 1.3x moy → pression d'achat CONFIRMÉE (non négociable)
                #   3. Momentum > 0.15%   → le prix MONTE (pas flat, pas baissier)
                #   4. RSI < 50           → oversold/recovery (pas en surachat)
                # ══════════════════════════════════════════════════════════════════════

                bb_position       = item.features.get('bb_position', 0.5)
                ema_diff          = item.features.get('ema_diff', 0)
                ema_slope         = item.features.get('ema_slope', 0)
                ema_trend_bearish = item.features.get('ema_trend_bearish', 0)
                momentum_3        = item.features.get('momentum_3', 0)
                momentum_5        = item.features.get('momentum_5', 0)
                rsi               = item.features.get('rsi', 50)
                ema_cross_bullish = item.features.get('ema_cross_bullish', 0)
                ema_cross_fresh   = item.features.get('ema_cross_fresh', 0)
                consec_green      = item.features.get('consec_green_candles', 0)

                # Volume ratio — pression d'achat réelle
                vol_ratio = (
                    volumes[-1] / (np.mean(volumes[-20:]) + 1e-10)
                    if (volumes is not None and len(volumes) >= 20) else 1.0
                )
                item.features['vol_ratio'] = vol_ratio
                item.features['volume_ratio'] = vol_ratio  # alias pour get_watchlist

                # ── Surveillance BTC pour détecter les crashs marché
                if symbol == 'BTCUSDT':
                    AIPredictor._btc_momentum = momentum_5
                    AIPredictor._last_market_check = datetime.now()
                    _crash_regime = self._get_market_regime()
                    # 🔧 FIX 01/04: momentum_5 est en % (ex: -5.0 = -5%), seuils corrigés
                    _crash_m5 = {'BULL_STRONG': -5.0, 'BULL_WEAK': -4.0, 'NEUTRAL': -3.0, 'BEAR': -2.0}.get(_crash_regime, -3.0)
                    _crash_m3 = {'BULL_STRONG': -3.0, 'BULL_WEAK': -2.5, 'NEUTRAL': -2.0, 'BEAR': -1.5}.get(_crash_regime, -2.0)
                    is_btc_crashing   = (momentum_5 < _crash_m5 and momentum_3 < _crash_m3)
                    is_btc_recovering = (momentum_3 > 0 or momentum_5 > 0)
                    if is_btc_crashing:
                        if not AIPredictor._market_crash_mode:
                            logger.warning(f"🚨 CRASH MARCHÉ! BTC Mom5={momentum_5:.2f}% Mom3={momentum_3:.2f}% - Achats BLOQUÉS!")
                        AIPredictor._market_crash_mode = True
                    elif is_btc_recovering:
                        if AIPredictor._market_crash_mode:
                            logger.warning(f"✅ FIN CRASH - BTC en reprise: Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}% - Achats DÉBLOQUÉS!")
                        AIPredictor._market_crash_mode = False

                # ══════════════════════════════════════════════════════════════════════
                # 🔴 REFONTE 01/04: Seuils STRICTS — UN CREUX_REBOUND SANS VOLUME
                # N'EST PAS UN CREUX_REBOUND. Le volume est la CONFIRMATION obligatoire.
                # Le régime ne fait que légèrement adapter bb_position.
                # ══════════════════════════════════════════════════════════════════════
                _regime_now = self._get_market_regime()
                _is_top20 = symbol in TOP_20_CRYPTOS

                # ── Condition 1: Prix dans la zone basse des BB
                if _regime_now == 'BULL_STRONG':
                    _bb_threshold = 0.45   # Creux en bull = sous 45% de la BB
                elif _regime_now == 'BULL_WEAK':
                    _bb_threshold = 0.42
                else:
                    _bb_threshold = 0.35   # NEUTRAL/BEAR: vrai creux = tiers inférieur
                signal_bb_low = bb_position < _bb_threshold

                # ── Condition 2: VOLUME AU-DESSUS DE LA MOYENNE — NON NÉGOCIABLE
                # Pas de volume = pas de rebound confirmé. Jamais d'exception.
                if _is_top20:
                    _vol_threshold = 1.0   # TOP20: volume au moins à la moyenne
                else:
                    _vol_threshold = 1.3   # Altcoins: volume 30% au-dessus minimum
                signal_volume = vol_ratio >= _vol_threshold

                # ── Condition 3: Momentum POSITIF — le prix DOIT monter
                _mom_min = 0.10 if _is_top20 else 0.15
                signal_momentum = momentum_3 > _mom_min

                # ── Condition 4: RSI en zone basse (recovery, pas surachat)
                _rsi_max = 50
                signal_rsi = rsi < _rsi_max

                # ── Bonus EMA: confirme le retournement (pas bloquant)
                signal_ema_turning = (
                    ema_cross_bullish == 1 or
                    ema_slope > 0 or
                    (ema_diff > -0.15 and momentum_3 > 0.2)
                )

                # ── Condition 5: TENDANCE — Si tendance baissière, exiger retournement EMA
                # 🔧 FIX: Le bot achetait en pleine baisse (ADA, STG) car ema_trend_bearish
                # n'était JAMAIS vérifié. Un creux-rebond DOIT montrer l'EMA court-terme
                # qui remonte (ema_slope > 0), sinon c'est un dead-cat bounce.
                signal_trend_ok = (ema_trend_bearish == 0 or ema_slope > 0)

                # ── Condition 6: DOWNTREND STRUCTUREL — pas d'achat si EMA bearish depuis trop longtemps
                # 🔧 FIX 06/04: AIXBT acheté avec 99 bougies sans golden cross (8h+ en downtrend)
                # Un micro-rebond dans un downtrend de 60+ bougies n'est PAS un creux-rebond,
                # c'est un dead cat bounce. Bloquer même si les 5 conditions techniques passent.
                _candles_since_cross = item.features.get('candles_since_bullish_cross', 99)
                signal_not_structural_downtrend = _candles_since_cross <= 60

                # ── Signal global: 6/6 conditions OBLIGATOIRES
                is_buy_signal = (signal_bb_low and signal_volume and
                                 signal_momentum and signal_rsi and signal_trend_ok
                                 and signal_not_structural_downtrend)

                # ── Signal fort: volume puissant + EMA cross + bougies vertes
                is_strong_signal = is_buy_signal and (
                    signal_ema_turning and
                    vol_ratio >= 1.8 and
                    consec_green >= 2
                )

                # ── Attribution pattern et statut
                if is_strong_signal:
                    item.pattern = 'CREUX_REBOUND'
                    item.status  = 'ready'
                    strength_bonus = min(20, int((vol_ratio - 1.3) * 15))
                    item.score = min(100, item.score + strength_bonus)
                    logger.info(
                        f"🔥 {symbol}: SIGNAL FORT — BB={bb_position:.2f} "
                        f"EMA_diff={ema_diff:.2f}% VOL={vol_ratio:.1f}x "
                        f"RSI={rsi:.0f} Mom={momentum_3:.2f}% Score={item.score}"
                    )
                elif is_buy_signal:
                    item.pattern = 'CREUX_REBOUND'
                    item.status  = 'ready'
                    logger.info(
                        f"✅ {symbol}: SIGNAL ACHAT — BB={bb_position:.2f} "
                        f"EMA_diff={ema_diff:.2f}% VOL={vol_ratio:.1f}x "
                        f"RSI={rsi:.0f} Mom={momentum_3:.2f}% Score={item.score}"
                    )
                else:
                    item.pattern = 'NEUTRAL'
                    item.status  = 'watching'
                    if item.score >= 50:
                        missing = []
                        if not signal_bb_low:    missing.append(f"BB={bb_position:.2f}>={_bb_threshold}")
                        if not signal_volume:    missing.append(f"VOL={vol_ratio:.1f}x<{_vol_threshold}")
                        if not signal_momentum:  missing.append(f"Mom={momentum_3:.2f}%<{_mom_min}")
                        if not signal_rsi:       missing.append(f"RSI={rsi:.0f}>={_rsi_max}")
                        if not signal_trend_ok:  missing.append(f"BEARISH(slope={ema_slope:.3f})")
                        if not signal_not_structural_downtrend: missing.append(f"DOWNTREND({_candles_since_cross}bougies)")
                        # INFO pour TOP20 (visibilité), DEBUG pour les autres
                        if _is_top20:
                            logger.info(f"⏳ {symbol}: WATCHING — manque: {', '.join(missing)}")
                        else:
                            logger.debug(f"⏳ {symbol}: WATCHING — manque: {', '.join(missing)}")
                return item
                
            except Exception as e:
                import traceback
                logger.warning(f"Erreur scorer avancé pour {symbol}: {e}")
                logger.debug(f"Traceback: {traceback.format_exc()}")
                # Continuer avec l'analyse classique
        
        # === ANALYSE CLASSIQUE (fallback) ===
        # Extraire les features
        features = PatternFeatures.extract_features(prices, volumes)
        if not features:
            item.status = "insufficient_data"
            return item
        
        item.features = features
        
        # === SCORING BASÉ SUR LES PATTERNS — VERSION SIMPLIFIÉE ===
        # Même logique 4-conditions que le chemin advanced scorer

        bb_position_c       = features.get('bb_position', 0.5)
        ema_diff_c          = features.get('ema_diff', 0)
        ema_slope_c         = features.get('ema_slope', 0)
        ema_trend_bearish_c = features.get('ema_trend_bearish', 0)
        momentum_3_c        = features.get('momentum_3', 0)
        momentum_5_c        = features.get('momentum_5', 0)
        rsi_c               = features.get('rsi', 50)
        ema_cross_bullish_c = features.get('ema_cross_bullish', 0)
        ema_cross_fresh_c   = features.get('ema_cross_fresh', 0)
        consec_green_c      = features.get('consec_green_candles', 0)

        vol_ratio_c = (
            volumes[-1] / (np.mean(volumes[-20:]) + 1e-10)
            if (volumes is not None and len(volumes) >= 20) else 1.0
        )
        features['vol_ratio'] = vol_ratio_c

        signal_bb_low_c     = bb_position_c < 0.40
        signal_volume_c     = vol_ratio_c >= 1.3
        signal_momentum_c   = momentum_3_c > 0.15
        signal_rsi_c        = rsi_c < 50
        # 🔧 FIX: Même garde tendance baissière que le chemin avancé
        signal_trend_ok_c   = (ema_trend_bearish_c == 0 or ema_slope_c > 0)
        is_buy_signal_c     = signal_bb_low_c and signal_volume_c and signal_momentum_c and signal_rsi_c and signal_trend_ok_c
        signal_ema_c        = (ema_cross_bullish_c == 1 or ema_slope_c > 0)
        is_strong_signal_c  = is_buy_signal_c and (signal_ema_c and vol_ratio_c >= 1.8 and consec_green_c >= 2)

        if is_strong_signal_c:
            item.pattern = 'CREUX_REBOUND'
            item.score   = min(100, item.score + min(20, int((vol_ratio_c - 1.3) * 15)))
            item.status  = 'ready'
        elif is_buy_signal_c:
            item.pattern = 'CREUX_REBOUND'
            item.status  = 'ready'
        else:
            item.pattern = 'NEUTRAL'
            item.status  = 'watching' if item.score >= 50 else 'low_potential'

        # BONUS TOP 20: Prioriser les cryptos à forte capitalisation
        # Ces cryptos sont plus stables et prévisibles (analyse classique)
        # ══════════════════════════════════════════════════════════════════════
        if symbol in TOP_20_CRYPTOS:
            old_score = item.score
            item.score = min(100, item.score + TOP_20_SCORE_BONUS)
            features['is_top20'] = True
            features['top20_bonus'] = TOP_20_SCORE_BONUS
            if item.score >= 60:
                logger.info(f"⭐ {symbol}: TOP 20 BONUS +{TOP_20_SCORE_BONUS} ({old_score}→{item.score})")
        else:
            features['is_top20'] = False
        
        # ═══════════════════════════════════════════════════════════════════════
        # PROTECTION CRASH: Bloquer UNIQUEMENT l'ACHAT en mode crash
        # L'ANALYSE CONTINUE pour avoir des opportunités prêtes après le crash
        # ═══════════════════════════════════════════════════════════════════════
        if is_market_crash:
            item.pattern = 'MARKET_CRASH_BLOCKED'
            item.status = 'market_crash_blocked'
            # Réduire le score pour que le signal ne soit pas éligible à l'achat
            # MAIS garder le vrai score dans les features pour analyse
            features['real_score_before_crash_block'] = item.score
            item.score = 10  # Score trop bas pour acheter
            if len(self.watchlist) < 3:  # Log les premières seulement
                logger.warning(f"🚨 {symbol}: ACHAT BLOQUÉ (mode crash) - Score réel={features['real_score_before_crash_block']} conservé pour analyse")
        
        return item
    
    def _predict_with_model(self, prices: List[float]) -> Optional[Dict]:
        """Utilise le modèle LSTM PyTorch pour prédire la direction"""
        if not self.model or len(prices) < 50 or not TORCH_AVAILABLE:
            return None
        
        # Préparer les données pour le modèle
        # Normaliser les prix
        prices_norm = np.array(prices[-50:])
        prices_norm = (prices_norm - np.mean(prices_norm)) / (np.std(prices_norm) + 1e-8)
        
        # Créer les features temporelles (simplifié)
        X = np.zeros((1, 50, 20), dtype=np.float32)
        X[0, :, 0] = prices_norm
        
        # Ajouter d'autres features
        for i in range(50):
            if i >= 5:
                X[0, i, 1] = (prices_norm[i] - prices_norm[i-5]) / 5  # Momentum 5
            if i >= 10:
                X[0, i, 2] = (prices_norm[i] - prices_norm[i-10]) / 10  # Momentum 10
        
        # Convertir en tensor PyTorch et envoyer sur GPU
        X_tensor = torch.tensor(X).to(DEVICE)
        
        # Prédiction (mode évaluation, pas de gradients)
        self.model.eval()
        with torch.no_grad():
            prediction = self.model(X_tensor)
            prediction = prediction.cpu().numpy()
        
        # Interpréter
        direction = ['down', 'neutral', 'up'][np.argmax(prediction[0])]
        confidence = float(np.max(prediction[0]) * 100)
        
        # Compter les prédictions
        self.training_stats['predictions_made'] += 1
        
        return {
            'direction': direction,
            'confidence': confidence,
            'score': float(prediction[0][2] * 100)  # Score hausse
        }
    
    def _identify_pattern(self, features: Dict) -> str:
        """Identifie le pattern principal"""
        # Priorité 1: Squeeze Breakout confirmé
        if features.get('bb_squeeze_confirmed', 0) and features['ema_diff'] < 0:
            return "SQUEEZE_BREAKOUT"
        elif features['bb_squeeze'] and features['ema_diff'] < 0:
            return "SQUEEZE_SETUP"
        elif features['bb_squeeze'] and features['ema_slope'] > 0:
            return "SQUEEZE_BREAKOUT"
        elif features['ema_diff'] < -0.5 and features['ema_slope'] > 0:
            return "DIP_RECOVERY"
        elif features['rsi_oversold']:
            return "RSI_OVERSOLD"
        elif features['near_support'] and features['momentum_5'] > 0:
            return "SUPPORT_BOUNCE"
        elif features['ema_diff'] > 0 and features['ema_cross_distance'] < 0.5:
            return "GOLDEN_CROSS"
        elif features['volatility'] < 0.5 and features['ema_slope'] > 0:
            return "LOW_VOL_TREND"
        else:
            return "NEUTRAL"
    
    def should_sell_early(self, symbol: str, prices: List[float], entry_price: float, current_profit_pct: float) -> Tuple[bool, str]:
        """
        Détecte si une position doit être vendue aux premiers signes de faiblesse
        Retourne (should_sell, reason)
        
        AMÉLIORATIONS v2.0:
        - Détection RSI TRAP (chute soudaine du RSI)
        - Détection ventes massives avec momentum
        - Seuils abaissés pour réactivité
        - Vente urgente en cas de danger
        
        Stratégie: Vendre dès les premiers signaux de retournement - RÉACTION RAPIDE
        L'IA prend le contrôle de la décision de vente
        """
        if len(prices) < 20:
            return False, ""
        
        features = PatternFeatures.extract_features(prices)
        if not features:
            return False, ""
        
        # Calculer le score de vente
        sell_score = features.get('sell_signal_score', 0)
        
        reasons = []
        urgent_sell = False  # Flag pour vente URGENTE immédiate
        
        # ════════════════════════════════════════════════════════════════════
        # DONNÉES DE BASE
        # ════════════════════════════════════════════════════════════════════
        momentum_3 = features.get('momentum_3', 0)
        momentum_5 = features.get('momentum_5', 0)
        ema_diff = features.get('ema_diff', 0)
        rsi = features.get('rsi', 50)
        bb_position = features.get('bb_position', 0.5)
        
        # PROTECTION : Ne PAS vendre si tendance haussière forte active
        strong_uptrend = (ema_diff > 0.2 and momentum_3 > 0.3 and momentum_5 > 0.5)
        if strong_uptrend:
            # Tendance haussière forte = ne pas vendre même avec RSI élevé
            return False, "🚀 Tendance haussière forte - HOLD"
        
        # ════════════════════════════════════════════════════════════════════
        # 🚨 DÉTECTION URGENTE #1: RSI TRAP - Chute RSI soudaine
        # 🔴 FIX 24/01 18h45: RSI <45 au lieu de <40 pour ULTRA précocité
        # ════════════════════════════════════════════════════════════════════
        if rsi < 30 and momentum_3 < -0.5:
            # RSI très bas + momentum négatif = danger immédiat
            urgent_sell = True
            sell_score += 50
            reasons.append(f"🚨 RSI TRAP: RSI={rsi:.0f} + chute")
        elif rsi < 45 and ema_diff < -0.10 and momentum_5 < -0.3:  # 🔴 RSI<45, EMA<-0.10, Mom<-0.3
            # RSI<45 + tendance baissière = signal fort (détection TRÈS précoce)
            sell_score += 40
            reasons.append(f"⚠️ RSI danger précoce: {rsi:.0f} + EMA baissière")
        elif rsi < 40 and momentum_3 < -0.2:  # 🔴 RSI<40, Mom<-0.2
            # RSI<40 + chute modérée = alerte
            sell_score += 30
            reasons.append(f"⚠️ RSI faible: {rsi:.0f} + momentum négatif")
        
        # ════════════════════════════════════════════════════════════════════
        # 🚨 DÉTECTION URGENTE #2: CRASH / VENTE MASSIVE
        # Momentum très négatif = vente massive en cours
        # ════════════════════════════════════════════════════════════════════
        if momentum_3 < -1.0:
            # Chute de plus de 1% sur 3 bougies = CRASH
            urgent_sell = True
            sell_score += 60
            reasons.append(f"🚨 CRASH: {momentum_3:.2f}% sur 3 bougies")
        elif momentum_3 < -0.5 and momentum_5 < -0.8:
            sell_score += 35
            reasons.append(f"Chute rapide: Mom3={momentum_3:.2f}%")
        elif momentum_5 < -1.5:
            # Chute de plus de 1.5% sur 5 bougies
            sell_score += 40
            reasons.append(f"Chute forte: {momentum_5:.2f}% sur 5 bougies")
        
        # ════════════════════════════════════════════════════════════════════
        # 🚨 DÉTECTION URGENTE #3: FIN DE CYCLE
        # Détection précoce: momentum qui ralentit en zone haute = sommet proche
        # ════════════════════════════════════════════════════════════════════
        
        # Détection précoce: Momentum ralentit en zone haute (sommet imminent)
        if bb_position > 0.75 and momentum_3 < 0.1 and rsi > 60:
            # Prix haut + momentum ralentit + RSI élevé = pic proche
            sell_score += 35
            reasons.append(f"⚠️ SOMMET PROCHE: BB={bb_position:.2f} Mom ralentit RSI={rsi:.0f}")
        
        # Death cross confirmé
        if ema_diff < -0.2 and rsi < 40 and momentum_3 < 0:
            urgent_sell = True
            sell_score += 45
            reasons.append(f"🚨 FIN CYCLE: EMA-{abs(ema_diff):.2f}% RSI={rsi:.0f}")
        
        # ════════════════════════════════════════════════════════════════════
        # SIGNAUX CLASSIQUES (pondération augmentée)
        # ════════════════════════════════════════════════════════════════════
        
        # 1. RSI en surachat et commence à baisser - SIGNAL FORT
        if features.get('rsi_turning_down', 0):
            sell_score += 35
            reasons.append("RSI retourne en zone haute")
        
        # 2. EMA9 commence à fléchir après une hausse - SIGNAL FORT
        if features.get('ema9_turning', 0):
            sell_score += 30
            reasons.append("EMA9 fléchit")
        
        # 3. Prix baisse depuis le sommet récent
        distance_from_high = features.get('distance_from_high', 0)
        if distance_from_high < -0.3:
            sell_score += 25
            reasons.append(f"Prix -{abs(distance_from_high):.1f}% du sommet")
        
        # 4. Momentum court terme négatif
        if momentum_3 < -0.3:
            sell_score += 25
            reasons.append(f"Momentum 3 bougies: {momentum_3:.2f}%")
        
        # 5. Prix sort de la bande supérieure puis revient
        if bb_position < 0.5 and current_profit_pct > 0.5:
            sell_score += 20
            reasons.append("Sortie zone BB haute")
        
        # 6. Prix décline sur 3 bougies consécutives - SIGNAL TRÈS FORT
        if features.get('price_declining', 0):
            sell_score += 35
            reasons.append("3 bougies baissières")
        
        # 7. EMA9 passe sous EMA21 (death cross)
        if ema_diff < -0.1:
            sell_score += 30
            reasons.append("Death cross EMA")
        
        # ════════════════════════════════════════════════════════════════════
        # DÉCISION DE VENTE - SEUILS ABAISSÉS pour réactivité
        # ════════════════════════════════════════════════════════════════════
        
        # 🚨 VENTE URGENTE IMMÉDIATE (RSI TRAP, CRASH, FIN CYCLE)
        if urgent_sell and sell_score >= 80:
            reason = f"🚨 IA VENTE URGENTE ({sell_score}): " + ", ".join(reasons[:3])
            logger.warning(f"🚨 {symbol}: VENTE URGENTE IA - Score={sell_score} P&L={current_profit_pct:.2f}%")
            return True, reason
        
        # Signal très fort avec profit OU perte
        if sell_score >= 100 and (current_profit_pct >= 0.2 or current_profit_pct <= -0.3):
            reason = f"⚠️ IA VENTE FORTE ({sell_score}): " + ", ".join(reasons[:3])
            logger.warning(f"🚨 {symbol}: VENTE FORTE IA - Score={sell_score} P&L={current_profit_pct:.2f}%")
            return True, reason
        
        # ════════════════════════════════════════════════════════════════════
        # NOUVEAU: Vente en cas de perte avec conditions dégradées
        # Si on est en perte ET conditions techniques mauvaises = couper les pertes
        # ════════════════════════════════════════════════════════════════════
        if current_profit_pct < -0.3 and sell_score >= 60:
            # En perte modérée avec signaux de vente = couper
            reason = f"⚠️ CUT LOSS ({sell_score}): " + ", ".join(reasons[:3])
            logger.warning(f"✂️ {symbol}: CUT LOSS IA - Score={sell_score} P&L={current_profit_pct:.2f}%")
            return True, reason
        
        # ════════════════════════════════════════════════════════════════════
        # SEUILS DE VENTE IA - ULTRA RÉACTIFS pour vendre au sommet
        # ════════════════════════════════════════════════════════════════════
        if current_profit_pct >= 8:
            threshold = 25  # Très gros profit: sécuriser facilement
        elif current_profit_pct >= 5:
            threshold = 35  # Gros profit: vendre facilement
        elif current_profit_pct >= 3.5:
            threshold = 45  # Bon profit: modéré (réactivité++)
        elif current_profit_pct >= 2.5:
            threshold = 55  # Profit correct: (réactivité++)
        elif current_profit_pct >= 1.5:
            threshold = 65  # Petit profit: (réactivité++)
        elif current_profit_pct >= 0.8:
            threshold = 75  # Petit profit: (nouveau palier)
        elif current_profit_pct >= 0:
            threshold = 85  # Break-even: plus facile
        else:
            threshold = 50  # En perte: vendre TRÈS FACILEMENT
        
        should_sell = sell_score >= threshold
        
        if should_sell:
            reason = f"🔔 IA VENTE ({sell_score}): " + ", ".join(reasons[:3])
            return True, reason
        
        return False, ""
    
    def analyze_symbol_for_interval(self, symbol: str, interval: str = '5m') -> Optional[Dict]:
        """
        Analyse un symbole pour un intervalle spécifique.
        Cette méthode est utilisée par l'API pour analyser à la demande.
        
        Args:
            symbol: Le symbole à analyser (ex: BTCUSDT)
            interval: L'intervalle Binance (1m, 5m, 15m, 1h, 4h, 1d)
        
        Returns:
            Dict avec les données d'analyse ou None si erreur
        """
        if not self.klines_fetcher:
            logger.warning("klines_fetcher non défini pour analyze_symbol_for_interval")
            return None
        
        try:
            # Récupérer les données pour l'intervalle demandé
            klines = self.klines_fetcher(symbol, interval, 100)
            if not klines or len(klines) < 50:
                return None
            
            # 🔧 FIX SYNCHRO: Exclure la bougie EN COURS (ouverte) de l'analyse
            import time as _now_tf
            if len(klines) >= 2 and float(klines[-1][6]) / 1000 > _now_tf.time():
                closed_klines = klines[:-1]
            else:
                closed_klines = klines
            
            prices = [float(k[4]) for k in closed_klines]  # Close prices (bougies clôturées)
            volumes = [float(k[5]) for k in closed_klines]  # Volumes
            
            # Analyser
            item = self.analyze_symbol(symbol, prices, volumes)
            
            # Retourner les données formatées
            return {
                'symbol': symbol,
                'interval': interval,
                'score': item.score,
                'pattern': item.pattern,
                'predicted_gain': item.predicted_gain,
                'confidence': item.confidence,
                'status': item.status,
                'reason': item.reason,
                'features': {
                    'ema_diff': item.features.get('ema_diff', 0) if item.features else 0,
                    'ema_slope': item.features.get('ema_slope', 0) if item.features else 0,
                    'ema_slope_long': item.features.get('ema_slope_long', 0) if item.features else 0,
                    'ema21_slope': item.features.get('ema21_slope', 0) if item.features else 0,
                    'ema_trend_bullish': item.features.get('ema_trend_bullish', 0) if item.features else 0,
                    'ema_trend_bearish': item.features.get('ema_trend_bearish', 0) if item.features else 0,
                    'bb_bandwidth': item.features.get('bb_bandwidth', 0) if item.features else 0,
                    'bb_squeeze': item.features.get('bb_squeeze', 0) if item.features else 0,
                    'rsi': item.features.get('rsi', 50) if item.features else 50,
                    'momentum_5': item.features.get('momentum_5', 0) if item.features else 0
                },
                'timestamp': datetime.now().isoformat()
            }
            
        except Exception as e:
            logger.error(f"Erreur analyze_symbol_for_interval {symbol} {interval}: {e}")
            return None
    
    def set_klines_fetcher(self, fetcher):
        """Définit la fonction pour récupérer les klines"""
        self.klines_fetcher = fetcher
        # Configurer également le fetcher pour multi-timeframe analyzer
        if self.multi_tf_analyzer:
            self.multi_tf_analyzer.set_klines_fetcher(fetcher)
    
    def set_binance_client(self, client):
        """Définit le client Binance pour l'analyse long terme"""
        self.binance_client = client
    
    def update_watchlist(self, symbol: str, item: WatchlistItem):
        """Met à jour un item dans la watchlist"""
        with self._lock:
            self.watchlist[symbol] = item
    
    def get_watchlist(self) -> List[Dict]:
        """Retourne la watchlist triée par score avec Smart Criteria v3.0"""
        # Import Smart Entry Criteria pour enrichir les données
        try:
            from smart_entry_criteria import quick_analyze, SignalType
            smart_available = True
        except ImportError:
            smart_available = False
        
        with self._lock:
            items = []
            for symbol, item in self.watchlist.items():
                # Données de base
                item_data = {
                    'symbol': symbol,
                    'score': item.score,
                    # 🔥 FIX 26/01 22h30: Ne PAS initialiser pattern ici, sera fait après smart_criteria
                    # 'pattern': item.pattern,
                    'predicted_gain': item.predicted_gain,
                    'confidence': item.confidence,
                    'status': item.status,
                    'reason': item.reason,
                    'countdown': item.countdown,
                    'last_update': item.last_update.isoformat() if item.last_update else None,
                    'features': {
                        'price_current': item.features.get('price_current', 0),  # Prix actuel pour rotation
                        'ema_diff': item.features.get('ema_diff', 0),
                        'ema_slope': item.features.get('ema_slope', 0),  # Pente EMA9 court terme
                        'ema_slope_long': item.features.get('ema_slope_long', 0),  # Pente EMA9 moyen terme
                        'ema21_slope': item.features.get('ema21_slope', 0),  # Pente EMA21
                        'ema_trend_bullish': item.features.get('ema_trend_bullish', 0),  # 1 si haussier
                        'ema_trend_bearish': item.features.get('ema_trend_bearish', 0),  # 1 si baissier
                        'bb_bandwidth': item.features.get('bb_bandwidth', 0),
                        'rsi': item.features.get('rsi', 50),
                        'momentum_3': item.features.get('momentum_3', 0),  # Momentum court terme (3 bougies)
                        'momentum_5': item.features.get('momentum_5', 0),
                        'near_bb_lower': item.features.get('near_bb_lower', 0),
                        'bb_position': item.features.get('bb_position', 0.5),
                        'price_change_recent': item.features.get('price_change_recent', 0),
                        # 🆕 Early cycle detection features
                        'early_cycle_score': item.features.get('early_cycle_score', 0),
                        'rsi_divergence_bullish': item.features.get('rsi_divergence_bullish', 0),
                        'ema_convergence_speed': item.features.get('ema_convergence_speed', 0),
                        'momentum_acceleration': item.features.get('momentum_acceleration', 0),
                        'volume_precursor': item.features.get('volume_precursor', 0),
                        'candles_since_bullish_cross': item.features.get('candles_since_bullish_cross', 99),
                    } if item.features else {},
                    # Smart Criteria v3.0
                    'smart_criteria': {},
                    'smart_signal': 'HOLD',
                    'smart_score': 0,
                    'smart_eligible': False,
                    'smart_status': 'En attente'  # En surveillance, Achat, Vente, Abandonnée
                }
                
                # Enrichir avec Smart Criteria si disponible
                if smart_available and item.features:
                    try:
                        # Extraire les données Smart depuis les features existantes
                        # Convertir en types Python natifs pour JSON
                        bb_squeeze_val = bool(item.features.get('bb_squeeze', 0) == 1)
                        ema_diff_val = float(item.features.get('ema_diff', 0))
                        momentum_3_val = float(item.features.get('momentum_3', 0))  # Momentum court terme (3 bougies)
                        momentum_5_val = float(item.features.get('momentum_5', 0))
                        ema_slope_val = float(item.features.get('ema_slope', 0))
                        ema_slope_long_val = float(item.features.get('ema_slope_long', ema_slope_val))  # Pente moyen terme
                        ema21_slope_val = float(item.features.get('ema21_slope', 0))  # Pente EMA21
                        ema_trend_bearish = bool(item.features.get('ema_trend_bearish', 0) == 1)  # Tendance baissière globale
                        ema_trend_bullish = bool(item.features.get('ema_trend_bullish', 0) == 1)  # Tendance haussière globale
                        bb_bandwidth_val = float(item.features.get('bb_bandwidth', 100))
                        
                        # 🆕 FIX 01/03: Variables pour détection ventre mou et baisse persistante
                        mom_accel_val = float(item.features.get('momentum_acceleration', 0))
                        is_ventre_mou_val = bool(item.features.get('is_ventre_mou', False))
                        is_downtrend_persistent_val = bool(item.features.get('is_downtrend_persistent', False))
                        # 🆕 FIX 10/03: Coin plat structurel (ex: ANKR — mois de stagnation)
                        is_flat_market_val = bool(item.features.get('is_flat_market', False))
                        _range_48h_val = float(item.features.get('range_48h', 99.0))
                        vol_ratio_val = float(item.features.get('volume_ratio', 1.0))
                        # 🔧 FIX 21/03: exceptions coin plat = compression avant breakout (ex ATOM 6h)
                        _rsi_flat_check = float(item.features.get('rsi', 50) if item.features else 50)
                        _mom_accel_flat = float(item.features.get('momentum_acceleration', 0) if item.features else 0)
                        _flat_override = (
                            (item.score >= 80 and _rsi_flat_check <= 42) or          # Creux haute confiance IA
                            (item.score >= 85 and _rsi_flat_check <= 50 and _mom_accel_flat > 0.2) or  # Accélération momentum confirmée
                            item.pattern in ('CREUX_REBOUND', 'PULLBACK')            # Pattern rebond déjà assigné
                        )
                        
                        # 🧠 LSTM Reversal Predictor variables
                        lstm_is_reversal = bool(item.features.get('lstm_is_reversal_signal', False))
                        lstm_is_danger = bool(item.features.get('lstm_is_danger_signal', False))
                        lstm_rev_prob = float(item.features.get('lstm_reversal_prob', 0))
                        lstm_rev_confidence = float(item.features.get('lstm_reversal_confidence', 0))
                        lstm_rev_label = str(item.features.get('lstm_reversal_label', 'NEUTRAL'))
                        
                        bb_position_val = float(item.features.get('bb_position', 0.5))
                        near_bb_lower_val = bool(bb_position_val < 0.25)  # Prix proche de la BB basse
                        
                        # 🆕 31/03: RSI 1m complémentaire — détection rapide de retournement
                        rsi_1m_val = float(item.features.get('rsi_1m', rsi_val))  # Fallback sur RSI 5m
                        mom_1m_3_val = float(item.features.get('momentum_1m_3', momentum_3_val))
                        has_rsi_1m = 'rsi_1m' in (item.features or {})
                        # RSI 1m remonte au-dessus du RSI 5m = début de retournement rapide
                        rsi_1m_rebounding = has_rsi_1m and rsi_1m_val > rsi_val + 3 and mom_1m_3_val > 0
                        
                        smart_data = {
                            'signal': 'HOLD',
                            'score': int(item.score),
                            'eligible': bool(item.status == 'ready'),
                            'criteria': {
                                'bb_squeeze': bb_squeeze_val,
                                # EMA9 doit être SIGNIFICATIVEMENT sous EMA21 (au moins -0.1%)
                                'ema9_under_ema21': bool(ema_diff_val < -0.1),
                                # RÈGLE AMÉLIORÉE: Hausse = stabilisation OU proche BB basse (zone rebond)
                                # momentum_3 > -0.5 = pas en chute libre (-0.5%)
                                # OU proche BB basse = zone de rebond naturelle
                                'hausse_confirmee': bool(
                                    (momentum_3_val > -0.5 and (momentum_3_val > -0.2 or ema_slope_val > -0.1)) or
                                    near_bb_lower_val  # Proche BB basse = autoriser achat
                                ),
                                'baisse_confirmee': bool(momentum_5_val < -1.0 or (ema_trend_bearish and momentum_3_val < -0.8))
                            },
                            'exclusions': {
                                # RÈGLE: Tendance baissière CONFIRMÉE = court ET moyen terme négatifs
                                'ema_baisse_permanente': bool(ema_trend_bearish and momentum_5_val < -1.0),  # -1.0%
                                'peu_variations': bool(bb_bandwidth_val < 0.5),
                                # EMA9 doit être VRAIMENT au-dessus ou égal à EMA21 (>= -0.1%)
                                'ema9_above_ema21': bool(ema_diff_val >= -0.1)  # EXCLUSION CRITIQUE
                            }
                        }
                        
                        # Déterminer le signal Smart
                        criteria = smart_data['criteria']
                        exclusions = smart_data['exclusions']
                        
                        # Vérifier tendance baissière AVANT tout
                        features = item.features if item.features else {}
                        ema_trend_bearish = features.get('ema_trend_bearish', 0)
                        momentum_3 = features.get('momentum_3', 0)
                        momentum_5 = features.get('momentum_5', 0)
                        ema_slope = features.get('ema_slope', 0)
                        
                        # Détecter chute active (momentum déjà en %, -0.5 = -0.5%)
                        is_falling = (
                            (momentum_3 < -0.5 and momentum_5 < -MOMENTUM_DROP_SIGNIFICANT) or  # -0.5% et -0.8%
                            (ema_slope < -0.1 and momentum_3 < -MOMENTUM_DROP_MODERATE)  # EMA baisse + mom < -0.3%
                        )
                        
                        # Détecter TENDANCE HAUSSIÈRE CONFIRMÉE (conditions assouplies)
                        rsi_val = features.get('rsi', 50)
                        # TOUTES les conditions doivent être vraies:
                        # 1. EMA9 > EMA21 (tendance établie) - seuil réduit pour grandes cryptos
                        # 2. Momentum positif (pas de baisse)
                        # 3. EMA montante (pas de retournement)
                        # 4. RSI < 75 (pas en surachat)
                        # 5. PAS de tendance baissière détectée par l'IA
                        is_bullish_trend = (
                            ema_diff_val > 0.01 and  # EMA9 > EMA21 (0.01%)
                            momentum_3 > -MOMENTUM_REVERSAL_MIN and  # Momentum pas trop négatif (> -0.1%)
                            momentum_5 > -MOMENTUM_DROP_LIGHT and    # Momentum 5 pas en chute (> -0.2%)
                            ema_slope > -0.05 and    # EMA pas en chute
                            rsi_val < 65 and         # RSI max 65
                            ema_trend_bearish == 0   # PAS de signal bearish de l'IA
                        )
                        
                        # ══════════════════════════════════════════════════════════
                        # 3 STRATÉGIES D'ACHAT ALIGNÉES AVEC LE BOT
                        # 🔴 NOTE: Pattern CREUX_REBOUND a priorité absolue (voir DÉCISION DU SIGNAL)
                        # ══════════════════════════════════════════════════════════
                        
                        # ══════════════════════════════════════════════════════════
                        # 🔧 FIX 04/02 v2: CREUX_REBOUND - DÉTECTER DÉBUT DE RETOURNEMENT
                        # Problème v1: Conditions trop strictes → achat tardif (10h44 au lieu de 10h30)
                        # Solution: Détecter le début du rebond, pas attendre confirmation complète
                        # ══════════════════════════════════════════════════════════
                        
                        # Détecter zone de creux (EMA9 sous EMA21 ou très proche)
                        is_creux = ema_diff_val < 0.1  # EMA9 <= EMA21 (inclut crossover imminent)

                        # 🆕 FIX 11/03: Lire l'analyse de cycle calculée plus tôt (features)
                        # cycle_is_ongoing = baisse > 2.5% sur 5h ET s'accélère = PAS de fond
                        # cycle_exhausting = baisse ralentit nettement = fin de cycle proche
                        _sc_cycle_ongoing = bool(features.get('cycle_is_ongoing', False))
                        _sc_cycle_exhausting = bool(features.get('cycle_exhausting', False))
                        _sc_vel_h1 = float(features.get('cycle_vel_h1', 0))
                        _sc_vel_h2 = float(features.get('cycle_vel_h2', 0))
                        _sc_drop_5h = float(features.get('cycle_drop_5h', 0))

                        # 🔧 FIX 04/02 v2 + FIX 01/03: CREUX_REBOUND = début de retournement détecté
                        # Conditions ÉQUILIBRÉES + accélération momentum pour vrai retournement
                        # 🧠 FIX 01/03 v2: LSTM Reversal Predictor peut CONFIRMER un creux-rebond
                        #    même si les indicateurs techniques sont encore ambigus
                        # 🔴 FIX 02/03: Seuils assouplis pour capter le DÉBUT du retournement
                        # Problème: bot achète APRÈS le pompe car conditions trop strictes
                        # ema_slope>0.2 est trop tard (EMA est LENTE), momentum>0.05 aussi
                        # 🆕 FIX 11/03: Ajouter guard "cycle non terminé"
                        # Si le cycle 5h s'accélère encore, le fond n'est PAS atteint
                        # 🆕 FIX RSI 1m: Si RSI 1m confirme rebond, on tolère des seuils 5m plus bas
                        #    car le 5m est naturellement EN RETARD sur le retournement
                        _mom3_min = 0.005 if rsi_1m_rebounding else 0.01
                        _ema_slope_alt = 0.02 if rsi_1m_rebounding else 0.05
                        is_creux_rebound_technical = is_creux and (
                            momentum_3_val > _mom3_min and   # 🔴 0.01 (ou 0.005 si RSI 1m confirme)
                            (mom_accel_val > 0 or momentum_3_val > 0.10 or rsi_1m_rebounding) and  # Accélération OU momentum OU RSI 1m
                            (ema_slope_val > _ema_slope_alt or momentum_3_val > 0.08) and  # EMA lente → RSI 1m assouplit
                            rsi_val >= 20 and rsi_val <= 55 and  # 🔴 RSI 25→20 (vrais creux)
                            momentum_5_val > -0.5 and       # 🔴 -0.3→-0.5 (tolérer chute récente)
                            vol_ratio_val >= 0.7 and         # 🔴 0.8→0.7 (volume peut être faible au creux)
                            # 🆕 FIX 11/03: GUARD CYCLE — bloquer si cycle baissier 5h encore actif
                            # 3 cas autorisés:
                            #   a) Cycle pas majeur (baisse < 2.5% sur 5h) → conditions normales
                            #   b) Cycle présent mais ESSOUFFLÉ (baisse ralentit) → achat possible
                            #   c) Cycle actif MAIS RSI extrême + rebond très fort → exception
                            (not _sc_cycle_ongoing                                           # a) Pas de grand cycle baissier actif
                             or _sc_cycle_exhausting                                          # b) Essoufflement vendeur détecté
                             or (rsi_val <= 30 and momentum_3_val > 0.25 and vol_ratio_val >= 1.2))  # c) RSI extrême + rebond fort
                        )
                        
                        # 🧠 LSTM peut détecter des retournements AVANT les indicateurs techniques
                        # Si le LSTM prédit REVERSAL_UP avec forte confiance, assouplir les conditions
                        # 🔧 FIX 01/03 v7: LSTM confiance 55→65%, prob 0.35→0.45
                        is_creux_rebound_lstm = is_creux and lstm_is_reversal and (
                            lstm_rev_confidence >= 65 and    # Confiance LSTM >= 65% (était 55)
                            lstm_rev_prob >= 0.45 and        # Probabilité retournement >= 45% (était 0.35)
                            rsi_val >= 20 and rsi_val <= 60 and  # Zone RSI élargie pour LSTM
                            momentum_5_val > -0.5 and        # Pas en crash violent
                            momentum_3_val > -0.15 and       # Momentum pas trop négatif
                            # 🆕 FIX 11/03: LSTM doit aussi respecter le cycle — sauf très haute confiance
                            # LSTM confiance ≥ 85% = détection précoce autorisée même si cycle actif
                            (not _sc_cycle_ongoing or _sc_cycle_exhausting or lstm_rev_confidence >= 85)
                        )
                        
                        is_creux_rebound = is_creux_rebound_technical or is_creux_rebound_lstm
                        
                        # Creux en chute = ne PAS acheter (HOLD)
                        # 🧠 LSTM danger signal RENFORCE le blocage
                        is_creux_falling = is_creux and (
                            momentum_3_val < -0.05 or       # Momentum clairement négatif < -0.05%
                            (ema_slope_val < -0.1 and momentum_3_val < 0.05) or  # EMA baisse ET mom faible
                            (mom_accel_val < -0.1 and momentum_3_val < 0.10) or  # Momentum décélère rapidement
                            (lstm_is_danger and lstm_rev_confidence >= 50 and momentum_3_val < 0.10)  # 🧠 LSTM dit DANGER
                        )
                        
                        # STRATÉGIE 2: PULLBACK (EMA9 > EMA21 avec momentum positif)
                        # 🔧 FIX 27/02: Exiger que la tendance NE SOIT PAS bearish!
                        # PULLBACK = repli dans UNE TENDANCE HAUSSIÈRE, pas achat en baisse
                        # 🔧 FIX 12/03: Conditions RENFORCÉES — ACE acheté à tort (RSI=37, mom négatif, EMA plate)
                        # Problème v1: momentum >= -0.2% + ema_slope > -0.05 = trop permissif
                        # Un PULLBACK valide = rebond ACTIF dans uptrend, pas continuation de baisse
                        is_pullback = (ema_diff_val > PULLBACK_MIN_GAP and ema_diff_val < 2)
                        is_pullback_valid = is_pullback and (
                            ema_slope_val > 0.0 and          # 🔧 FIX 12/03: EMA9 doit MONTER (était > -0.05 = trop laxiste)
                            ema21_slope_val >= -0.02 and      # 🔧 FIX 12/03: EMA21 stable/haussière (uptrend réel)
                            momentum_3_val >= 0.0 and         # 🔧 FIX 12/03: Momentum POSITIF requis (était -0.2%: achat en baisse!)
                            momentum_3_val < 2.0 and         # Momentum max 2%
                            momentum_5_val > -0.10 and        # 🔧 FIX 12/03: Tendance 5 bougies quasi-neutre (pas en chute)
                            rsi_val < 65 and                 # RSI max 65
                            ema_trend_bearish == 0 and       # 🔧 FIX 27/02: INTERDIT en tendance bearish!
                            rsi_val > 38                      # 🔧 FIX 12/03: RSI min 38 (était 20: piège baissier; RSI<38 = déclin, pas pullback)
                        )
                        
                        # STRATÉGIE 3: SQUEEZE BREAKOUT (BB resserrées + EMA proches + tendance haussière)
                        # C'est le pattern LUNA idéal: BB stables → EMA croise → momentum faible positif → ACHAT!
                        # 🔴 FIX 28/01 12h35: PROTECTION BREAKOUT TARDIF
                        # Problème ASTERUSDT: Acheté RSI=52 Mom=0.00% → Breakout déjà terminé
                        # Solution: Exiger momentum ACTIF (>0.15%) pour confirmer breakout en cours
                        is_squeeze = bb_squeeze_val or bb_bandwidth_val < 3.5
                        is_ema_cross_zone = abs(ema_diff_val) < 0.5  # EMA9 et EMA21 très proches (zone de croisement)
                        
                        # 🆕 MOMENTUM STRICT: Breakout = mouvement ACTIF, pas stagnation
                        # Si momentum = 0 → breakout terminé, ne PAS acheter
                        # 🔧 FIX 08/02: Seuils relevés — 0.0015 = 0.0015% = essentiellement zéro!
                        # Valeurs réelles: momentum_3 en % (0.091 = 0.091%), exiger vrai mouvement
                        is_breakout_active = (
                            momentum_3_val > 0.05 or  # Momentum > 0.05% (breakout actif réel)
                            (momentum_3_val > 0.02 and ema_slope_val > 0.01)  # OU Mom > 0.02% + EMA montante
                        )
                        
                        # Protection RSI: Si RSI > 55, exiger momentum encore plus fort
                        is_not_too_late = (
                            rsi_val < 55 or  # RSI < 55 = OK
                            (rsi_val < 60 and momentum_3_val > 0.3)  # OU RSI 55-60 mais Mom > 0.3%
                        )
                        
                        is_squeeze_breakout = (
                            is_squeeze and 
                            is_ema_cross_zone and 
                            is_breakout_active and  # 🆕 Exiger breakout ACTIF
                            is_not_too_late and     # 🆕 Bloquer si trop tard
                            rsi_val > 30 and rsi_val < 65 and  # 🔴 FIX 10/03: 40→30 (squeeze déprime RSI, ex: XLM 38.7)
                            not ema_trend_bearish and
                            # 🔧 FIX 08/02: INTERDIRE achats quand EMA9 chute ou tendance baissière!
                            # APT: ema_slope=-0.282 → BLOQUÉ, UNI: ema_slope=-0.304 → BLOQUÉ
                            ema_slope_val > -0.1 and             # EMA9 ne doit PAS chuter fort (> -0.1%)
                            momentum_5_val > -0.2 and            # Pas de baisse 5-bougies (> -0.2%)
                            (ema_trend_bullish or                # Soit tendance bullish confirmée
                             ema_slope_val > 0 or               # Soit EMA9 monte activement
                             (ema_diff_val > 0.1 and momentum_3_val > 0.05))  # Soit gap EMA + momentum actif
                        )
                        
                        # Conditions de blocage absolu (momentum déjà en %, -2.0 = -2%)
                        is_crash = (momentum_3_val < -2.0 and momentum_5_val < -3.0)  # -2% et -3%
                        
                        # ══════════════════════════════════════════════════════════
                        # 🛑 BLOCAGE: TENDANCE BAISSIÈRE FORTE
                        # Seuil: EMA diff < -0.5% ET momentum négatif ET ema_trend_bearish
                        # ══════════════════════════════════════════════════════════
                        is_absolute_bearish = (
                            ema_diff_val < -0.5 and      # EMA diff < -0.5%
                            momentum_3_val < -0.3 and    # ET momentum < -0.3%
                            ema_trend_bearish            # ET tendance baissière IA
                        )
                        
                        # ══════════════════════════════════════════════════════════
                        # DÉTECTION CROISEMENT EMA IMMINENT (opportunité d'achat LUNA!)
                        # Si EMA proche de croiser avec momentum positif
                        # 🔧 FIX 09/02: Fenêtre élargie -0.2% (was -0.1%) + ema_slope assoupli
                        # Cas XRP 18:32: EMA convergentes + momentum haussier raté
                        # ══════════════════════════════════════════════════════════
                        is_crossover_imminent = (
                            ema_diff_val < 0 and           # EMA9 encore sous EMA21
                            ema_diff_val > -0.3 and        # Fenêtre de détection
                            ema_slope_val > 0.0 and        # EMA au moins stable/montante
                            momentum_3_val > -0.2          # ET momentum pas en chute
                        )
                        
                        # 🔴 FIX 26/01 22h: DÉTECTION POST-CROISEMENT (phase initiale de tendance)
                        # Détecter quand EMA vient JUSTE de croiser (0-0.3%) = meilleure opportunité!
                        # C'est cette phase que BTC a eu à 18h38 et que nous avons raté
                        # 🔥 FIX 26/01 22h20: Assouplir conditions pour attraper ICP-like (Mom3=0.7%, EMA_diff=0.17%)
                        is_fresh_crossover = (
                            ema_diff_val > 0 and           # EMA9 au-dessus EMA21 (croisé)
                            ema_diff_val < 0.5 and         # Fenêtre fraîche
                            ema_slope_val > 0.0 and        # EMA monte
                            momentum_3_val > 0.01 and      # Momentum positif
                            momentum_5_val > -0.5          # Pas de chute forte
                        )
                        
                        # 🔥 FIX 26/01 22h20: FRESH CROSSOVER ULTRA-FORT
                        # Si momentum très fort (>0.5%), ignorer ema_slope strict
                        is_fresh_crossover_strong = (
                            ema_diff_val > 0 and           # EMA9 au-dessus EMA21
                            ema_diff_val < 0.3 and         # < 0.3% (fresh)
                            momentum_3_val > 0.5 and       # Momentum TRÈS fort (> 0.5%)
                            momentum_5_val > -0.5          # Pas de chute
                        )
                        
                        # 🔥 FIX 27/01 13h: POST_CROSSOVER - Détection 1-2 min APRÈS croisement
                        # Capture les cas où bot démarre juste après un croisement EMA
                        # Exemple: BTC 12h27 (croisement) → bot démarré 12h33 → EMA_diff=-0.02%
                        # 🔧 FIX: POST_CROSSOVER fenêtre élargie (-0.3% à +0.1%) au lieu de (-0.1% à 0%)
                        # Permet de capturer les crossovers qui viennent d'arriver
                        is_post_crossover = (
                            ema_diff_val > -0.4 and        # Autour du croisement
                            ema_diff_val <= 0.2 and
                            ema_slope_val > 0.0 and        # EMA au moins stable
                            momentum_3_val > 0.02 and      # Momentum positif
                            rsi_val > 30 and
                            momentum_5_val > -0.3
                        )
                        
                        # Si crossover imminent OU fresh crossover OU post crossover, on annule le blocage bearish
                        if is_crossover_imminent or is_fresh_crossover or is_fresh_crossover_strong or is_post_crossover:
                            is_absolute_bearish = False
                        
                        # ══════════════════════════════════════════════════════════
                        # BLOCAGE: Tendance baissière SANS retournement
                        # On bloque SEULEMENT si en vraie chute forte (momentum < -1%)
                        # ══════════════════════════════════════════════════════════
                        is_bearish_trend = (
                            ema_trend_bearish and 
                            momentum_3_val < -1.0 and      # 🔧 FIX AUDIT 28/02: -0.01 → -1.0 (en %, -1.0 = -1%)
                            not is_crossover_imminent and  # PAS si croisement imminent
                            not (momentum_3_val > MOMENTUM_REVERSAL_MIN and ema_slope_val > 0) and  # PAS si remontée (0.1%)
                            not (momentum_3_val > 0.2 and rsi_val < 35)  # PAS si retournement fort (0.2%)
                        )
                        
                        # ══════════════════════════════════════════════════════════
                        # DÉCISION DU SIGNAL
                        # 🔴 FIX 24/01: Priorité CREUX_REBOUND AVANT toute logique Smart Criteria
                        # Priorité: CREUX_REBOUND > BLOCAGE ABSOLU > CROSSOVER > CRASH > BEARISH > etc.
                        # ══════════════════════════════════════════════════════════
                        
                        # � FIX 08/02: Pré-calcul des seuils adaptatifs AVANT la chaîne elif
                        # Évite le bug où un elif trop large "avalait" tous les signaux
                        _regime = self._get_market_regime()
                        # 🔧 FIX: Seuils NEUTRAL trop agressifs → bloquaient 100% des signaux
                        # NEUTRAL RSI 65→72 (65 est normal en crypto), momentum 0.5%→0.2%
                        _rsi_tardif = {'BULL_STRONG': 75, 'BULL_WEAK': 68, 'NEUTRAL': 72, 'BEAR': 60, 'CORRECTION': 60}.get(_regime, 68)
                        _mom_tardif = {'BULL_STRONG': 0.1, 'BULL_WEAK': 0.3, 'NEUTRAL': 0.2, 'BEAR': 0.5}.get(_regime, 0.3)
                        _rsi_cycle = {'BULL_STRONG': 72, 'BULL_WEAK': 62, 'NEUTRAL': 63, 'BEAR': 50}.get(_regime, 58)
                        # 🔧 FIX: _mom_cycle négatif = ne bloquer que si momentum activement baissier
                        # En consolidation (mom ~0%), le signal NEUTRAL est légitime
                        _mom_cycle = {'BULL_STRONG': 0.1, 'BULL_WEAK': 0.1}.get(_regime, -0.1)
                        _rsi_dist_min = {'BULL_STRONG': 70, 'BULL_WEAK': 62, 'NEUTRAL': 63, 'BEAR': 50}.get(_regime, 58)
                        _rsi_dist_max = {'BULL_STRONG': 82, 'BULL_WEAK': 75, 'NEUTRAL': 78, 'BEAR': 65}.get(_regime, 75)
                        _bb_position_val = item.features.get('bb_position', 0.5) if hasattr(item, 'features') and item.features else 0.5
                        # 🔴 FIX 04/03: RSI ADAPTATIF pour CREUX_REBOUND selon régime
                        # Problème: RSI 65 autorisé même en NEUTRAL/BEAR → faux rebonds (ENSO RSI=62, ZEC RSI=58)
                        # Solution: Plafonner RSI selon le régime. Seul BULL_STRONG permet RSI=65
                        _rsi_creux_max = {'BULL_STRONG': 65, 'BULL_WEAK': 58, 'NEUTRAL': 48, 'BEAR': 40, 'CORRECTION': 43}.get(_regime, 48)
                        # 🆕 FIX 24/03 BTC: TOP20 rebondissent depuis RSI plus élevé que les altcoins
                        # BTC/ETH: dip est souvent RSI 50-65 en bull, pas RSI < 40
                        # Garde: EMA slope > 0 + momentum positif confirmé
                        # 🔧 FIX: min(55,...) était un bug — réduisait le seuil BULL_STRONG de 65→55
                        if symbol in TOP_20_CRYPTOS and ema_slope_val > 0 and momentum_3_val > 0.10:
                            _rsi_creux_cap = {'BULL_STRONG': 72, 'BULL_WEAK': 65, 'NEUTRAL': 55, 'BEAR': 50, 'CORRECTION': 50}.get(_regime, 55)
                            _rsi_creux_max = min(_rsi_creux_cap, _rsi_creux_max + 10)
                            logger.debug(f"⭐ {symbol}: TOP20 rebond — _rsi_creux_max assoupli à {_rsi_creux_max} (slope={ema_slope_val:+.2f}%)")
                        
                        # PRIORITÉ #0: Pattern CREUX_REBOUND — résultat du signal propre.
                        # Les 4 conditions (BB<0.40 + EMA cross + VOL>=1.3 + Mom>0.15 + RSI<55)
                        # ont été validées dans analyze_symbol.
                        # Garde minimale: vérifier que les valeurs actuelles sont toujours cohérentes
                        # (protège contre les données périmées lors du redémarrage du bot)
                        if item.pattern == 'CREUX_REBOUND':
                            # 🆕 FIX RSI 1m: RSI 1m rebounding confirme le retournement → seuil momentum assoupli
                            _mom3_valid_min = 0.05 if rsi_1m_rebounding else 0.10
                            _signal_still_valid = (
                                momentum_3_val > _mom3_valid_min and  # Momentum confirmé (assoupli si RSI 1m OK)
                                rsi_val <= _rsi_creux_max and      # 🔧 FIX 24/03: Appliquer RSI adaptatif régime
                                                                   #   (BEAR=40, NEUTRAL=48, BULL_STRONG=65)
                                                                   #   au lieu de <60 fixe (trop permissif)
                                not is_crash and                   # Pas de crash marché
                                # 🔧 FIX: BLOQUER achat en tendance baissière sans retournement EMA
                                # ema_trend_bearish=1 ET ema_slope<=0 → on est en pleine baisse, pas un rebond
                                (not ema_trend_bearish or ema_slope_val > 0)
                            )
                            if _signal_still_valid:
                                smart_data['signal']   = 'ACHAT'
                                smart_data['eligible'] = True
                                item_data['smart_status'] = 'CREUX_REBOUND'
                                _rsi1m_tag = f" RSI1m={rsi_1m_val:.0f}↑" if rsi_1m_rebounding else (f" RSI1m={rsi_1m_val:.0f}" if has_rsi_1m else "")
                                logger.info(
                                    f"✅ {symbol}: CREUX_REBOUND → ACHAT "
                                    f"(RSI={rsi_val:.0f} Mom={momentum_3_val:.2%} "
                                    f"VOL={vol_ratio_val:.1f}x{_rsi1m_tag})"
                                )
                            else:
                                smart_data['signal']   = 'NO_BUY'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'CREUX_REBOUND invalide (conditions dégradées)'
                                logger.debug(
                                    f"🚫 {symbol}: CREUX_REBOUND rejeté "
                                    f"(RSI={rsi_val:.0f} Mom={momentum_3_val:.2%} crash={is_crash}"
                                    f" bearish={ema_trend_bearish} slope={ema_slope_val:+.3f})"
                                )
                        # SIGNAL NEUTRE: analyze_symbol n'a pas détecté de signal d'achat → HOLD
                        # Tous les anciens chemins (is_creux_rebound, is_pullback, crossover standalone, etc.)
                        # sont court-circuités. Le seul achat est via analyze_symbol → CREUX_REBOUND.
                        elif item.pattern == 'NEUTRAL':
                            # 🔧 FIX 23/03: En BULL_STRONG/BULL_WEAK, un coin NEUTRAL avec crossover
                            # actif doit quand même passer en ACHAT (sinon les elif crossover plus bas
                            # sont inatteignables, tout finit en HOLD, 0 signaux en bull).
                            if _regime in ('BULL_STRONG', 'BULL_WEAK') and not is_crash and is_crossover_imminent:
                                smart_data['signal']   = 'ACHAT'
                                smart_data['eligible'] = True
                                smart_data['score']    = min(100, item.score + 20)
                                item_data['smart_status'] = f'CROSSOVER IMMINENT ({_regime})'
                                logger.info(f"🔥 {symbol}: NEUTRAL→CROSSOVER IMMINENT en {_regime}")
                            elif _regime in ('BULL_STRONG', 'BULL_WEAK') and not is_crash and (is_fresh_crossover or is_fresh_crossover_strong):
                                bonus = 30 if is_fresh_crossover_strong else 25
                                smart_data['signal']   = 'ACHAT'
                                smart_data['eligible'] = True
                                smart_data['score']    = min(100, item.score + bonus)
                                lbl = 'FRESH CROSSOVER STRONG' if is_fresh_crossover_strong else 'FRESH CROSSOVER'
                                item_data['smart_status'] = f'{lbl} ({_regime})'
                                logger.info(f"🔥 {symbol}: NEUTRAL→{lbl} en {_regime}")
                            elif _regime in ('BULL_STRONG', 'BULL_WEAK') and not is_crash and is_post_crossover:
                                smart_data['signal']   = 'ACHAT'
                                smart_data['eligible'] = True
                                smart_data['score']    = min(100, item.score + 15)
                                item_data['smart_status'] = f'POST CROSSOVER ({_regime})'
                                logger.info(f"🔥 {symbol}: NEUTRAL→POST CROSSOVER en {_regime}")
                            else:
                                smart_data['signal']   = 'HOLD'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'En surveillance'
                        # 🔧 FIX 07/02: Seuils adaptatifs par RÉGIME MARCHÉ
                        # 🔧 FIX 08/02: Condition COMPLÈTE dans le elif (évite d'avaler les signaux)
                        # BULL_STRONG: RSI > 75, BULL_WEAK: RSI > 68, NEUTRAL: RSI > 65, BEAR: RSI > 60
                        elif (not is_fresh_crossover_strong and 
                              rsi_val > _rsi_tardif and momentum_3_val < _mom_tardif):
                            logger.warning(f"🚫 {symbol}: ACHAT TARDIF bloqué - RSI={rsi_val:.0f}>{_rsi_tardif} Mom={momentum_3_val:.2%} regime={_regime}")
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = f'Achat tardif (RSI>{_rsi_tardif} {_regime})'
                        # 🆕 FIX 28/01 17h50: BLOCAGE PATTERNS NEUTRAL APRÈS CYCLE HAUSSIER
                        # � FIX 08/02: Condition COMPLÈTE dans le elif (évite d'avaler les signaux)
                        # Si pattern NEUTRAL + RSI > seuil + momentum faible + pas de crossover → BLOQUÉ
                        elif (item.pattern in ['NEUTRAL', 'None', None] and 
                              not is_crossover_imminent and not is_fresh_crossover and not is_fresh_crossover_strong and not is_post_crossover and
                              rsi_val > _rsi_cycle and momentum_3_val < _mom_cycle):
                            logger.warning(f"🚫 {symbol}: Pattern NEUTRAL APRÈS CYCLE bloqué - RSI={rsi_val:.0f}>{_rsi_cycle} Mom={momentum_3_val:.2%} regime={_regime}")
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = f'NEUTRAL après cycle ({_regime})'
                        # 🆕 FIX 28/01 19h: PHASE DISTRIBUTION - Consolidation haute après hausse
                        # 🔧 FIX 08/02: Condition COMPLÈTE dans le elif (évite d'avaler les signaux)
                        # RSI dans zone distribution + BB 0.60-0.85 + momentum < 0.2%
                        elif (momentum_3_val < 0.2 and 
                              rsi_val >= _rsi_dist_min and rsi_val < _rsi_dist_max and
                              _bb_position_val > 0.60 and _bb_position_val < 0.85):
                            logger.warning(f"🚫 {symbol}: PHASE DISTRIBUTION bloqué - RSI={rsi_val:.0f} BB={_bb_position_val:.2f} Mom={momentum_3_val:.2%} regime={_regime}")
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = f'Phase distribution ({_regime})'
                        # 🆕 FIX 28/01 14h50: BLOCAGE PATTERNS SANS MOMENTUM
                        # SQUEEZE_BREAKOUT nécessite momentum actif
                        # 🔴 FIX 08/02: TREND_CONTINUATION retiré (désactivé)
                        elif item.pattern in ['SQUEEZE_BREAKOUT'] and momentum_3_val < 0.1:
                            logger.warning(f"🚫 {symbol}: Pattern {item.pattern} SANS momentum (Mom={momentum_3_val:.2%}) → BLOQUÉ")
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = f'{item.pattern} sans momentum'
                        # 🛑 BLOCAGE ABSOLU #1: EMA bearish = NO BUY (sauf si CREUX_REBOUND valide)
                        elif is_absolute_bearish:
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = 'EMA BEARISH'
                        # 🆕 FIX 28/01 19h: BLOCAGE BAISSE ACTIVE - EMA9 < EMA21 + momentum négatif
                        # Cas AAVE 16h30: EMA_diff=-0.19%, Mom5=-0.15% = baisse établie → NO_BUY absolu
                        elif ema_diff_val < -5.0 and momentum_5 < -10.0:
                            logger.warning(f"🚫 {symbol}: BAISSE ACTIVE bloqué - EMA_diff={ema_diff_val:.2%} Mom5={momentum_5:.2%}")
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = 'Baisse active (NO_BUY absolu)'
                        # PRIORITÉ #1: CROISEMENT IMMINENT = ACHAT FORT! (momentum > 0 requis)
                        elif is_crossover_imminent and not is_crash:
                            smart_data['signal'] = 'ACHAT'
                            smart_data['eligible'] = True
                            smart_data['score'] = min(100, item.score + 20)  # Gros bonus score!
                            item_data['smart_status'] = 'CROSSOVER IMMINENT'
                        # 🔴 FIX 26/01 22h: PRIORITÉ #1B: FRESH CROSSOVER (vient de croiser)
                        # C'est la phase la plus rentable: croisement juste effectué, tendance démarre
                        # Exemple: BTC 18h38 (raté car système ne détectait pas cette phase)
                        # 🔥 FIX 26/01 22h20: Détection STRONG si momentum > 0.5% (ICP-like)
                        elif (is_fresh_crossover or is_fresh_crossover_strong) and not is_crash:
                            smart_data['signal'] = 'ACHAT'
                            smart_data['eligible'] = True
                            # Bonus +30 si STRONG (momentum très fort), +25 sinon
                            bonus = 30 if is_fresh_crossover_strong else 25
                            smart_data['score'] = min(100, item.score + bonus)
                            item_data['smart_status'] = 'FRESH CROSSOVER STRONG' if is_fresh_crossover_strong else 'FRESH CROSSOVER'
                            logger.info(f"🔥 {symbol}: {'STRONG ' if is_fresh_crossover_strong else ''}FRESH CROSSOVER - EMA_diff={ema_diff_val:.3f}% Mom3={momentum_3_val:.3f}%")
                        # 🔥 FIX 27/01 13h: PRIORITÉ #1C: POST_CROSSOVER (1-2 min après croisement)
                        # Capture les croisements qui ont eu lieu juste avant le démarrage du bot
                        # Exemple: BTC 12h27 croisement → bot démarré 12h33 → EMA_diff=-0.02%
                        elif is_post_crossover and not is_crash:
                            smart_data['signal'] = 'ACHAT'
                            smart_data['eligible'] = True
                            smart_data['score'] = min(100, item.score + 20)  # Bonus +20 (moins optimal que FRESH)
                            item_data['smart_status'] = 'POST CROSSOVER'
                            logger.info(f"⚡ {symbol}: POST CROSSOVER - EMA_diff={ema_diff_val:.3f}% Mom3={momentum_3_val:.3f}% RSI={rsi_val:.0f}")
                        # BLOCAGE #2: CRASH ACTIF = NO BUY absolu
                        elif is_crash:
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = 'Crash actif'
                        # BLOCAGE #3: TENDANCE BAISSIÈRE SANS RETOURNEMENT
                        elif is_bearish_trend:
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = 'BEARISH TREND'
                        # 🔴 DÉSACTIVÉ 21/03: LSTM FORCED REVERSAL override
                        # Rétro PHA: conf=93% prob=0.51 → achat raté (-1.23%)
                        # prob=0.51 = pile ou face. Le LSTM ne remplace pas is_bearish_trend.
                        # Ce bloc ne fait plus rien — is_bearish_trend → NO_BUY reste appliqué.
                        # elif (lstm_is_reversal and not is_crash and ...):
                        #     DÉSACTIVÉ — ne plus overrider les blocages tendance baissière
                        # 🆕 FIX 01/03: BLOCAGE #3B - TENDANCE BAISSIÈRE PERSISTANTE
                        # Momentum négatif constant + accélération baissière
                        # Évite les achats pendant les baisses lentes mais continues
                        elif is_downtrend_persistent_val:
                            smart_data['signal'] = 'HOLD'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = 'Baisse persistante (HOLD)'
                            logger.info(f"📉 {symbol}: Baisse persistante smart - Mom3={momentum_3_val:.2f}% Mom5={momentum_5:.2f}% accel={mom_accel_val:.3f}")
                        # 🆕 FIX 01/03: BLOCAGE #3C - VENTRE MOU (marché sans direction)
                        # Pas de momentum, pas de tendance, volatilité faible = pertes
                        elif is_ventre_mou_val:
                            smart_data['signal'] = 'HOLD'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = 'Ventre mou (sans direction)'
                            logger.info(f"😴 {symbol}: Ventre mou smart - Mom3={momentum_3_val:.2f}% EMA_slope={ema_slope_val:.3f}")
                        # 🔧 FIX 16/03: BLOCAGE COIN PLAT STRUCTUREL
                        # Range < 5% sur 48h+ et faible volatilité = aucun potentiel de gain
                        # EXCEPTION grandes caps: BTC/ETH/BNB/SOL en accumulation (range faible = breakout imminent)
                        # Note: SQUEEZE_BREAKOUT exempté naturellement (is_flat_market=False si bb_squeeze_confirmed)
                        # 🔧 FIX 21/03: EXCEPTION si score ≥ 80 + RSI oversold, ou momentum accélère, ou pattern rebond
                        elif is_flat_market_val and not _flat_override and symbol not in {'BTCUSDT','ETHUSDT','BNBUSDT','SOLUSDT','XRPUSDT','LTCUSDT','ADAUSDT','DOGEUSDT','DOTUSDT','AVAXUSDT'}:
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = f'Coin plat structurel (range48h={_range_48h_val:.1f}%)'
                            logger.info(f"📊 {symbol}: COIN PLAT — range_48h={_range_48h_val:.1f}% < 5% → NO_BUY (pas de potentiel)")
                        elif is_flat_market_val and _flat_override:
                            _reason = 'RSI creux' if _rsi_flat_check <= 42 else 'momentum acceleration' if _mom_accel_flat > 0.2 else 'pattern rebond'
                            logger.info(f"🔓 {symbol}: COIN PLAT override ({_reason}) — range48h={_range_48h_val:.1f}% mais score={item.score} RSI={_rsi_flat_check:.0f} accel={_mom_accel_flat:.2f} → analyse continue")
                        # STRATÉGIE #1: SQUEEZE BREAKOUT
                        # 🔴 FIX 04/03: Bloquer SQUEEZE_BREAKOUT en BEAR/CORRECTION
                        # Problème: DENT(-20), ALICE(-11), USUAL(-10) achetés en SQUEEZE_BREAKOUT
                        # pendant un marché qui tournait au bear → cascade de stop-losses
                        # Solution: Regime BEAR/CORRECTION = SQUEEZE_BREAKOUT interdit (résistances trop fortes)
                        elif is_squeeze_breakout and _regime not in ('BEAR', 'CORRECTION'):
                            smart_data['signal'] = 'ACHAT'
                            smart_data['eligible'] = True
                            # 🆕 FIX 10/03: Bonus adaptatif selon durée squeeze + volume explosion (modèle XLM)
                            # Squeeze long + explosion de volume = signal haute qualité → bonus max +10
                            _sq_duration = item.features.get('squeeze_duration', 0) if item.features else 0
                            _sq_vol = item.features.get('volume_ratio', 1.0) if item.features else 1.0
                            _sq_bonus = 10 if (_sq_duration >= 10 and _sq_vol >= 2.0) else 5 if (_sq_duration >= 5 and _sq_vol >= 1.5) else 0
                            _sq_score = min(100, item.score + 15 + _sq_bonus)
                            smart_data['score'] = _sq_score
                            _sq_tag = ' [LONG+VOL🔥]' if _sq_bonus >= 10 else ' [LONG📊]' if _sq_bonus >= 5 else ''
                            item_data['smart_status'] = f'SQUEEZE BREAKOUT{_sq_tag}'
                            logger.info(f"📊 {symbol}: SQUEEZE BREAKOUT{_sq_tag} → ACHAT (regime={_regime}, dur={_sq_duration}b, vol={_sq_vol:.1f}x, score={_sq_score})")
                        elif is_squeeze_breakout:
                            # SQUEEZE_BREAKOUT présent mais régime défavorable
                            smart_data['signal'] = 'HOLD'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = f'SQUEEZE BREAKOUT bloqué (regime={_regime})'
                            logger.info(f"🚫 {symbol}: SQUEEZE BREAKOUT bloqué — regime={_regime} (BEAR/CORRECTION)")
                        # ══════════════════════════════════════════════════════════
                        # 🧠 STRATÉGIE #1B: LSTM REVERSAL_UP - Retournement prédit par le LSTM
                        # Le modèle BiLSTM+Attention détecte les retournements AVANT les indicateurs
                        # Conditions: LSTM dit REVERSAL_UP + pas en crash + confirmations minimales
                        # ══════════════════════════════════════════════════════════
                        # 🔧 FIX 01/03 v7: LSTM confiance 55→65%, prob 0.40→0.50, EMA bearish 65→75%
                        elif (lstm_is_reversal and not is_crash and not is_bearish_trend and
                              lstm_rev_confidence >= 65 and lstm_rev_prob >= 0.50 and
                              rsi_val < 65 and rsi_val > 20 and
                              momentum_3_val > -0.5 and
                              (not ema_trend_bearish or lstm_rev_confidence >= 75)):  # 🧠 FIX: EMA bearish OK si LSTM confiant
                            lstm_bonus = min(20, int(lstm_rev_confidence * lstm_rev_prob * 0.25))
                            smart_data['signal'] = 'ACHAT'
                            smart_data['eligible'] = True
                            smart_data['score'] = min(100, item.score + lstm_bonus)
                            item_data['smart_status'] = f'LSTM REVERSAL (conf={lstm_rev_confidence:.0f}%)'
                            logger.info(f"🧠 {symbol}: LSTM REVERSAL_UP → ACHAT! conf={lstm_rev_confidence:.0f}% prob={lstm_rev_prob:.2f} +{lstm_bonus}")
                        # ══════════════════════════════════════════════════════════
                        # 🆕 STRATÉGIE EARLY UPTREND CYCLE: Détection précoce de cycle haussier
                        # Combine: RSI divergence + EMA convergence + momentum acceleration + volume
                        # C'est LA stratégie clé pour "repérer rapidement les valeurs en hausse"
                        # ══════════════════════════════════════════════════════════
                        elif not is_crash and not is_bearish_trend and item.features:
                            early_cycle = item.features.get('early_cycle_score', 0)
                            rsi_div = item.features.get('rsi_divergence_bullish', 0)
                            ema_conv = item.features.get('ema_convergence_speed', 0)
                            mom_accel = item.features.get('momentum_acceleration', 0)
                            vol_precursor = item.features.get('volume_precursor', 0)
                            candles_cross = item.features.get('candles_since_bullish_cross', 99)
                            
                            # EARLY CYCLE FORT: Score composite >= 40 + au moins 2 signaux
                            signals_count = sum([
                                1 if rsi_div >= 0.5 else 0,          # RSI divergence
                                1 if ema_conv > 0.05 else 0,         # EMA converge
                                1 if mom_accel > 0.1 else 0,         # Momentum accélère
                                1 if vol_precursor > 1.2 else 0,     # Volume précurseur
                                1 if candles_cross <= 5 else 0       # Croisement récent (< 5 bougies)
                            ])
                            
                            if early_cycle >= 40 and signals_count >= 2 and rsi_val < 65 and momentum_3_val > -0.3:
                                early_bonus = min(20, int(early_cycle / 5))
                                smart_data['signal'] = 'ACHAT'
                                smart_data['eligible'] = True
                                smart_data['score'] = min(100, item.score + early_bonus)
                                item_data['smart_status'] = 'EARLY UPTREND CYCLE'
                                logger.info(f"🌅 {symbol}: EARLY UPTREND CYCLE détecté! early_score={early_cycle:.0f} "
                                           f"signals={signals_count} (div={rsi_div:.1f} conv={ema_conv:.2f} "
                                           f"accel={mom_accel:.3f} vol={vol_precursor:.1f} cross={candles_cross})")
                            # EARLY CYCLE PARTIEL: Score >= 30 + EMA converge + momentum positif
                            elif early_cycle >= 30 and ema_conv > 0.03 and momentum_3_val > 0 and rsi_val < 60:
                                smart_data['signal'] = 'ACHAT'
                                smart_data['eligible'] = True
                                smart_data['score'] = min(100, item.score + 10)
                                item_data['smart_status'] = 'EARLY CYCLE PARTIAL'
                                logger.info(f"🌄 {symbol}: EARLY CYCLE PARTIAL - early_score={early_cycle:.0f} conv={ema_conv:.2f}")
                            # Si pas early cycle, continuer avec les stratégies existantes
                            elif is_bullish_trend and momentum_5 > 0 and momentum_3_val > 0.05:
                                # 🔴 FIX 08/02: TREND CONTINUATION → HOLD (0% WR récent, 31% all-time)
                                # Ce pattern catch-all n'est PAS fiable pour acheter
                                smart_data['signal'] = 'HOLD'
                                smart_data['eligible'] = False
                                smart_data['score'] = item.score
                                item_data['smart_status'] = 'Tendance haussière (observation)'
                            elif is_bullish_trend and momentum_5 > 0 and momentum_3_val <= 0.05:
                                smart_data['signal'] = 'HOLD'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'Correction après pic (HOLD)'
                                logger.info(f"⚠️ {symbol}: TREND CONTINUATION bloqué - momentum négatif {momentum_3_val:.4f}% (correction après pic)")
                            # Creux avec rebond
                            elif is_creux_rebound and not is_creux_falling:
                                # 🔧 FIX 06/04: Ce chemin fallback Phase 2 BYPASS les conditions
                                # strictes de Phase 1 (volume ≥1.3x, momentum >0.15%, trend OK).
                                # Bug: `is_creux_rebound_technical` accepte volume ≥0.7x et momentum >0.01%
                                # et `is_creux_rebound_lstm` ne vérifie MÊME PAS le volume.
                                # Résultat: achats illégitimes (AIXBT -1.29%) que Phase 1 aurait bloqué.
                                # Désactivé → HOLD. Le seul achat CREUX est via Phase 1 → CREUX_REBOUND.
                                smart_data['signal'] = 'HOLD'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'Creux + Rebond Phase2 (HOLD — Phase1 non validé)'
                                logger.debug(f"⏸️ {symbol}: CREUX_REBOUND Phase2 → HOLD (Phase1 non confirmé)")
                            # PULLBACK valide
                            elif is_pullback_valid:
                                smart_data['signal'] = 'ACHAT'
                                smart_data['eligible'] = True
                                item_data['smart_status'] = 'Pullback haussier'
                            # Creux en chute
                            elif is_creux_falling:
                                smart_data['signal'] = 'HOLD'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'Creux - Momentum négatif (HOLD)'
                            # Creux sans confirmation
                            elif is_creux:
                                smart_data['signal'] = 'HOLD'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'Creux - Attendre rebond (HOLD)'
                            # Squeeze sans conditions
                            elif is_squeeze and is_ema_cross_zone:
                                smart_data['signal'] = 'HOLD'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'Squeeze - Surveiller (HOLD)'
                            else:
                                smart_data['signal'] = 'HOLD'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'En attente'
                        
                        item_data['smart_criteria'] = smart_data
                        item_data['smart_signal'] = smart_data['signal']
                        item_data['smart_score'] = smart_data['score']
                        item_data['smart_eligible'] = smart_data['eligible']
                        
                        # ══════════════════════════════════════════════════════════════
                        # 🔴 NOUVEAU SYSTÈME 26/01: ASSIGNATION PATTERNS DEPUIS smart_criteria
                        # Mapper les smart_status vers les 6 patterns autorisés
                        # FIX 20h40: Mapper TOUS les signaux, pas seulement ACHAT
                        # ══════════════════════════════════════════════════════════════
                        
                        # Assignation pattern selon smart_status et signal
                        # Pattern basé sur smart_status (indépendant du signal ACHAT/HOLD)
                        status = item_data.get('smart_status', '')
                        
                        # Pattern 1: CREUX_REBOUND
                        if 'Creux + Rebond' in status or 'CREUX_REBOUND' in status:
                            item.pattern = 'CREUX_REBOUND'
                        
                        # Pattern 2: PULLBACK  
                        elif 'Pullback haussier' in status or 'PULLBACK' in status:
                            item.pattern = 'PULLBACK'
                        
                        # Pattern 3: TREND_CONTINUATION
                        elif 'TREND CONTINUATION' in status:
                            item.pattern = 'TREND_CONTINUATION'
                        
                        # Pattern 4: SQUEEZE_BREAKOUT
                        elif 'SQUEEZE BREAKOUT' in status:
                            item.pattern = 'SQUEEZE_BREAKOUT'
                        
                        # 🆕 Pattern 4B: EARLY UPTREND CYCLE → CREUX_REBOUND
                        # 🔴 FIX 08/02: Était TREND_CONTINUATION (0% WR) → CREUX_REBOUND (meilleur pattern)
                        elif 'EARLY UPTREND CYCLE' in status or 'EARLY CYCLE PARTIAL' in status:
                            item.pattern = 'CREUX_REBOUND'
                        
                        # Pattern 5: CROSSOVER_IMMINENT (assigné comme CREUX_REBOUND)
                        elif 'CROSSOVER IMMINENT' in status:
                            item.pattern = 'CREUX_REBOUND'
                        
                        # Pattern 6: FRESH CROSSOVER → EMA_BULLISH
                        # 🔴 FIX 08/02: Était TREND_CONTINUATION (0% WR) → PULLBACK (pattern approprié)
                        # 🔵 FIX 25/03: PULLBACK blacklisté (perf 13/03) → EMA_BULLISH (enabled, non blacklisté)
                        # EMA_BULLISH: croisement EMA9/EMA21 haussier = signal de rebond légitime
                        elif 'FRESH CROSSOVER' in status:
                            item.pattern = 'EMA_BULLISH'
                        
                        # Pattern 7: POST CROSSOVER → EMA_BULLISH
                        elif 'POST CROSSOVER' in status:
                            item.pattern = 'EMA_BULLISH'
                        
                        # 🧠 Pattern: LSTM REVERSAL → CREUX_REBOUND (retournement = rebond de creux)
                        elif 'LSTM REVERSAL' in status or 'LSTM FORCED REVERSAL' in status:
                            item.pattern = 'CREUX_REBOUND'
                        
                        # 🔴 FIX 08/02: Si signal ACHAT mais pattern non spécifique → NEUTRAL (pas d'achat catch-all)
                        # Était TREND_CONTINUATION (0% WR) - les patterns non classifiés ne doivent PAS acheter
                        elif smart_data['signal'] == 'ACHAT' and smart_data['eligible']:
                            if item.pattern not in ['CREUX_REBOUND', 'PULLBACK', 'SQUEEZE_BREAKOUT', 'EMA_BULLISH', 'CROSSOVER_IMMINENT']:
                                item.pattern = 'NEUTRAL'
                                smart_data['eligible'] = False
                                smart_data['signal'] = 'HOLD'
                        
                        # Si signal HOLD → garder pattern NEUTRAL (pas d'achat)
                        elif smart_data['signal'] == 'HOLD':
                            item.pattern = 'NEUTRAL'
                        
                        # 🔴 FIX 26/01 21h: Mettre à jour item_data['pattern'] pour que le bot le voie
                        # 🔥 FIX 26/01 22h30: S'assurer que pattern n'est jamais None
                        item_data['pattern'] = item.pattern if item.pattern else 'NEUTRAL'
                        
                        # 🔴 FIX 26/01: FORCER smart_eligible=False pour POSSIBLE_BLOCKED
                        # Pattern toxique qui cause achats à la baisse (PEOPLE 18:30)
                        if item.pattern == 'POSSIBLE_BLOCKED':
                            item_data['smart_eligible'] = False
                            item_data['smart_signal'] = 'NO_BUY'
                            logger.info(f"🚫 {symbol}: POSSIBLE_BLOCKED → smart_eligible=False, signal=NO_BUY")
                        
                        # ══════════════════════════════════════════════════════════════
                        # 🔴 OVERRIDE CONDITIONNEL DÉSACTIVÉ - HIGH_SCORE_OVERRIDE
                        # FIX 24/01: Pattern bloqué (0% win rate, -1.03% avg, 6 trades)
                        # Ne JAMAIS forcer l'éligibilité même avec score élevé
                        # ══════════════════════════════════════════════════════════════
                        if False and item.pattern == 'HIGH_SCORE_OVERRIDE' and item.score >= 70:  # 🔴 DÉSACTIVÉ
                            # Vérifier qu'il n'y a pas d'exclusions critiques
                            exclusions = smart_data.get('exclusions', {})
                            critical_exclusions = ['ema_baisse_permanente', 'peu_variations']
                            has_critical = any(exclusions.get(excl, False) for excl in critical_exclusions)
                            
                            # 🚨 VÉRIFICATION RENFORCÉE: EMA Bearish = INTERDIT
                            # SAUF si on est en CREUX (bb_position < 0.40) avec momentum positif = rebond imminent
                            ema_trend_bearish_check = item.features.get('ema_trend_bearish', 0) if item.features else 0
                            bb_position_check = item.features.get('bb_position', 0.5) if item.features else 0.5
                            momentum_3_check = item.features.get('momentum_3', 0) if item.features else 0
                            is_bearish = (ema_trend_bearish_check == 1 or exclusions.get('ema_baisse_permanente', False))
                            
                            # Exception: creux avec rebond = OK même si EMA bearish
                            is_creux_rebond = (bb_position_check < 0.40 and momentum_3_check > 0)
                            
                            if is_bearish and not is_creux_rebond:
                                # 🚨 BLOCAGE: Tendance baissière = INTERDIT (sauf creux+rebond)
                                item_data['smart_signal'] = 'NO_BUY'
                                item_data['smart_eligible'] = False
                                item_data['status'] = 'bearish_blocked'
                                logger.warning(f"🚫 {symbol}: HIGH_SCORE_OVERRIDE BLOQUÉ - EMA BEARISH (EMA_diff={item.features.get('ema_diff', 0):.2f}%)")
                            elif is_creux_rebond and is_bearish:
                                # ✅ EXCEPTION: Creux + rebond = stratégie de crossover anticipé
                                item_data['smart_signal'] = 'ACHAT'
                                item_data['smart_eligible'] = True
                                logger.info(f"✅ {symbol}: EXCEPTION creux+rebond - bb_pos={bb_position_check:.2f} mom3={momentum_3_check:.2f}% - AUTORISÉ malgré EMA bearish")
                            elif not has_critical and smart_data['signal'] in ['ACHAT', 'POSSIBLE']:
                                # Safe override: pas d'exclusions critiques
                                item_data['smart_signal'] = 'ACHAT'
                                item_data['smart_eligible'] = True
                                logger.info(f"🔓 {symbol}: HIGH_SCORE_OVERRIDE → smart_eligible FORCÉ (score={item.score}, safe)")
                            else:
                                # Respecter les smart_criteria à cause d'exclusions critiques
                                # NE PAS override, garder les valeurs des smart_criteria
                                item_data['smart_signal'] = smart_data['signal']
                                item_data['smart_eligible'] = smart_data['eligible']
                                reason = "exclusions critiques" if has_critical else f"signal={smart_data['signal']}"
                                logger.warning(f"⚠️ {symbol}: HIGH_SCORE_OVERRIDE BLOQUÉ par smart_criteria ({reason})")
                                # DEBUG
                                if symbol == 'MATICUSDT':
                                    logger.info(f"🐛 {symbol}: item_data après blocage: smart_signal={item_data['smart_signal']}, eligible={item_data['smart_eligible']}")
                        
                    except Exception as e:
                        logger.debug(f"Smart Criteria error for {symbol}: {e}")
                
                # 🔥 FIX 26/01 22h30: Assurer pattern initialisé même si smart_criteria non exécuté
                if 'pattern' not in item_data or item_data['pattern'] is None:
                    item_data['pattern'] = item.pattern if item.pattern else 'NEUTRAL'
                
                items.append(item_data)
            
            # Trier par: éligibles d'abord, puis par score décroissant
            def sort_key(x):
                # Priorité: ACHAT > POSSIBLE > En surveillance > VENTE > ABANDONNEE
                signal_priority = {
                    'ACHAT': 0,
                    'POSSIBLE': 1,
                    'HOLD': 2,
                    'VENTE': 3,
                    'ABANDONNEE': 4
                }
                priority = signal_priority.get(x.get('smart_signal', 'HOLD'), 2)
                return (priority, -x['score'])
            
            items.sort(key=sort_key)
            
            # ══════════════════════════════════════════════════════════════════
            # ANALYSE DES POSITIONS OUVERTES POUR SIGNAUX DE VENTE
            # ══════════════════════════════════════════════════════════════════
            try:
                import json
                import os
                positions_file = os.path.join(os.path.dirname(__file__), 'positions.json')
                if os.path.exists(positions_file):
                    with open(positions_file, 'r') as f:
                        positions = json.load(f)
                    
                    for symbol, pos in positions.items():
                        entry_price = pos.get('entry_price', 0)
                        if entry_price <= 0:
                            continue
                        
                        # Chercher si le symbole existe déjà dans items
                        existing_item = None
                        for item in items:
                            if item.get('symbol') == symbol:
                                existing_item = item
                                break
                        
                        # Récupérer les features du symbole
                        features = {}
                        if symbol in self.watchlist:
                            wi = self.watchlist[symbol]
                            features = wi.features or {}
                        elif existing_item:
                            features = existing_item.get('features', {})
                        
                        # Calculer le profit actuel
                        current_price = features.get('price_current', 0)
                        if current_price <= 0 and existing_item:
                            # Essayer de récupérer le prix depuis d'autres sources
                            current_price = entry_price  # Fallback
                        
                        profit_pct = ((current_price / entry_price) - 1) * 100 if entry_price > 0 and current_price > 0 else 0
                        
                        # Calculer le score de vente basé sur les critères IA
                        sell_score = 0
                        sell_reasons = []
                        should_sell = False
                        
                        # Critères de vente IA
                        ema_diff = features.get('ema_diff', 0)
                        ema_slope = features.get('ema_slope', 0)
                        momentum_3 = features.get('momentum_3', 0)
                        momentum_5 = features.get('momentum_5', 0)
                        rsi = features.get('rsi', 50)
                        bb_position = features.get('bb_position', 0.5)
                        
                        # ══════════════════════════════════════════════════════════
                        # 🛡️ PROTECTION DU CAPITAL - Priorité haute
                        # ══════════════════════════════════════════════════════════
                        
                        # STOP LOSS IA : Couper les pertes rapidement
                        if profit_pct <= -1.5:
                            sell_score += 80  # Vendre immédiatement
                            sell_reasons.append(f"🚨 STOP LOSS {profit_pct:.2f}%")
                        elif profit_pct <= -0.8:
                            sell_score += 50
                            sell_reasons.append(f"⚠️ Perte {profit_pct:.2f}%")
                        elif profit_pct <= -0.3:
                            sell_score += 25
                            sell_reasons.append(f"Perte légère {profit_pct:.2f}%")
                        
                        # ══════════════════════════════════════════════════════════
                        # 💰 PROTECTION DES GAINS - Trailing mental
                        # ══════════════════════════════════════════════════════════
                        
                        # Si profit important mais momentum négatif = sécuriser
                        if profit_pct >= 2.0 and momentum_3 < -0.1:
                            sell_score += 45
                            sell_reasons.append(f"💰 Sécuriser +{profit_pct:.1f}%")
                        elif profit_pct >= 1.0 and momentum_3 < -0.2:
                            sell_score += 35
                            sell_reasons.append(f"📈 Protéger +{profit_pct:.1f}%")
                        
                        # ══════════════════════════════════════════════════════════
                        # 📊 SIGNAUX TECHNIQUES - Confirmation de retournement
                        # ══════════════════════════════════════════════════════════
                        
                        # Signal 1: EMA9 passe sous EMA21 (death cross)
                        # Unifié avec config.py: DEATH_CROSS_MODERATE=-0.15, LIGHT=-0.05
                        if ema_diff < -0.15:  # DEATH_CROSS_MODERATE
                            sell_score += 35
                            sell_reasons.append("Death cross fort")
                        elif ema_diff < -0.05:  # DEATH_CROSS_LIGHT
                            sell_score += 20
                            sell_reasons.append("EMA9 < EMA21")
                        
                        # Signal 2: Momentum négatif - Réactif
                        if momentum_3 < -0.5:
                            sell_score += 30
                            sell_reasons.append(f"Chute rapide {momentum_3:.1f}%")
                        elif momentum_3 < -0.2:
                            sell_score += 15
                            sell_reasons.append(f"Mom↓ {momentum_3:.1f}%")
                        
                        # Signal 3: EMA en baisse confirmée
                        if ema_slope < -0.2:
                            sell_score += 25
                            sell_reasons.append("Tendance baissière")
                        elif ema_slope < -0.08:
                            sell_score += 12
                            sell_reasons.append("EMA fléchit")
                        
                        # Signal 4: RSI surachat (vendre au sommet)
                        if rsi > 75:
                            sell_score += 25
                            sell_reasons.append(f"RSI surachat {rsi:.0f}")
                        elif rsi > 65 and momentum_3 < 0:
                            sell_score += 12
                            sell_reasons.append(f"RSI élevé + baisse")
                        
                        # Signal 5: Sortie bande Bollinger haute
                        if bb_position > 0.9 and momentum_3 < 0:
                            sell_score += 20
                            sell_reasons.append("Sortie BB haute")
                        
                        # ══════════════════════════════════════════════════════════
                        # 🎯 SEUILS ADAPTATIFS - Équilibre gains/risque
                        # ══════════════════════════════════════════════════════════
                        
                        # Logique: Plus on a de profit, plus on protège agressivement
                        # Plus on est en perte, plus on coupe vite
                        
                        if profit_pct >= 4:
                            threshold = 30  # Très facile de vendre (sécuriser gros gain)
                        elif profit_pct >= 2.5:
                            threshold = 40  # Facile (bon profit)
                        elif profit_pct >= 1.5:
                            threshold = 50  # Modéré
                        elif profit_pct >= 0.8:
                            threshold = 60  # Prudent
                        elif profit_pct >= 0.3:
                            threshold = 70  # Conservateur (petit profit)
                        elif profit_pct >= -0.5:
                            threshold = 75  # Neutre (break-even zone)
                        else:
                            threshold = 50  # En perte = vendre plus facilement
                        
                        should_sell = sell_score >= threshold
                        
                        # TOUJOURS créer un item pour les positions ouvertes
                        # (pour les afficher dans la section VENTE du dashboard)
                        sell_item = {
                            'symbol': symbol,
                            'score': sell_score,
                            'pattern': 'POSITION_OUVERTE',
                            'predicted_gain': profit_pct,
                            'confidence': min(100, sell_score) if sell_score > 0 else 50,
                            'status': 'sell_signal' if should_sell else 'position_open',
                            'reason': ', '.join(sell_reasons) if sell_reasons else f'P&L: {profit_pct:+.2f}%',
                            'countdown': 0,
                            'last_update': datetime.now().isoformat(),
                            'features': features,
                            'smart_signal': 'VENTE' if should_sell else 'EN_POSITION',
                            'smart_score': sell_score,
                            'smart_eligible': should_sell,
                            'smart_status': 'Signal vente IA' if should_sell else f'En position ({profit_pct:+.2f}%)',
                            'smart_criteria': {
                                'signal': 'VENTE' if should_sell else 'SURVEILLER',
                                'eligible': should_sell,
                                'sell_score': sell_score,
                                'threshold': threshold,
                                'profit_pct': round(profit_pct, 2),
                                'entry_price': entry_price,
                                'reasons': sell_reasons
                            },
                            'position_info': {
                                'entry_price': entry_price,
                                'quantity': pos.get('quantity', 0),
                                'stop_loss': pos.get('stop_loss', 0),
                                'take_profit': pos.get('take_profit', 0),
                                'profit_pct': round(profit_pct, 2),
                                'entry_pattern': pos.get('pattern', '')
                            }
                        }
                        
                        # Remplacer l'item existant ou ajouter
                        if existing_item:
                            # Mettre à jour avec les infos de position
                            existing_item['smart_signal'] = sell_item['smart_signal']
                            existing_item['smart_score'] = sell_score
                            existing_item['smart_eligible'] = should_sell
                            existing_item['smart_status'] = sell_item['smart_status']
                            existing_item['smart_criteria'] = sell_item['smart_criteria']
                            existing_item['position_info'] = sell_item['position_info']
                            existing_item['pattern'] = pos.get('pattern', 'POSITION_OUVERTE')
                            existing_item['predicted_gain'] = profit_pct
                        else:
                            items.append(sell_item)
                        
            except Exception as e:
                logger.debug(f"Erreur analyse positions pour vente: {e}")
            
            # ══════════════════════════════════════════════════════════════════════
            # 🧠 TRI PAR COMPATIBILITÉ IA - Prioriser les valeurs les plus efficaces
            # ══════════════════════════════════════════════════════════════════════
            if AI_COMPATIBILITY_AVAILABLE and len(items) > 0:
                try:
                    scorer = get_compatibility_scorer()
                    
                    # Analyser toutes les valeurs
                    scored_items = []
                    for item in items:
                        features = item.get('features', {})
                        if features:
                            compat = scorer.calculate_compatibility_score(
                                item['symbol'], 
                                features
                            )
                            item['ai_compatibility'] = {
                                'score': compat['total_score'],
                                'percentage': compat['percentage'],
                                'grade': compat['grade'],
                                'compatibility': compat['compatibility'],
                                'reasons': compat['reasons'][:3]  # Top 3 raisons
                            }
                            scored_items.append((compat['total_score'], item))
                        else:
                            item['ai_compatibility'] = {
                                'score': 0,
                                'percentage': 0,
                                'grade': 'F',
                                'compatibility': 'NON_ANALYSÉ',
                                'reasons': []
                            }
                            scored_items.append((0, item))
                    
                    # Trier: d'abord par signal (ACHAT > VENTE > autres), puis par compatibilité
                    signal_priority = {
                        'ACHAT': 100,
                        'VENTE': 90,
                        'EN_POSITION': 85,
                        'POSSIBLE': 50,
                        'ABANDONNEE': 10,
                        'HOLD': 5
                    }
                    
                    scored_items.sort(
                        key=lambda x: (
                            signal_priority.get(x[1].get('smart_signal', 'HOLD'), 0),
                            x[0]  # Score compatibilité
                        ),
                        reverse=True
                    )
                    
                    items = [item for _, item in scored_items]
                    
                    logger.debug(f"✅ Watchlist triée par compatibilité IA ({len(items)} items)")
                    
                except Exception as e:
                    logger.warning(f"⚠️ Erreur tri compatibilité: {e}")
            
            return items
    
    def get_ready_symbols(self) -> List[str]:
        """Retourne les symboles prêts à acheter (score >= seuil)"""
        with self._lock:
            return [
                symbol for symbol, item in self.watchlist.items()
                if item.status == "ready" and item.score >= self.SCORE_THRESHOLD
            ]
    
    def record_prediction(self, symbol: str, prediction: Dict, actual_result: float):
        """Enregistre le résultat d'une prédiction pour l'apprentissage"""
        self.prediction_history.append({
            'symbol': symbol,
            'timestamp': datetime.now().isoformat(),
            'prediction': prediction,
            'actual_result': actual_result,
            'success': actual_result > 0
        })
        
        # 🔧 FIX 05/04: Limiter en mémoire aussi (était unbounded → OOM)
        if len(self.prediction_history) > 1000:
            self.prediction_history = self.prediction_history[-1000:]
        
        # Sauvegarder périodiquement
        if len(self.prediction_history) % 100 == 0:
            self._save_history()
    
    def _save_history(self):
        """Sauvegarde l'historique des prédictions"""
        try:
            with open('ai_prediction_history.json', 'w') as f:
                json.dump(self.prediction_history[-1000:], f)  # Garder les 1000 dernières
        except Exception as e:
            logger.error(f"Erreur sauvegarde historique: {e}")
    
    def get_market_regime_parameters(self) -> Dict:
        """
        Obtient le régime de marché actuel et les paramètres adaptés
        
        Returns:
            Dict avec regime, min_score, max_positions, risk_multiplier
        """
        if not REGIME_DETECTOR_AVAILABLE or not self.regime_detector or not self.klines_fetcher:
            return {
                'regime': 'UNKNOWN',
                'min_score': self.SCORE_THRESHOLD,
                'max_positions': 20,
                'risk_multiplier': 1.0
            }
        
        try:
            # Récupérer les données BTC
            btc_klines = self.klines_fetcher.fetch_klines('BTC/USDT', '1d', limit=30)
            if not btc_klines or len(btc_klines) < 14:
                return {
                    'regime': 'UNKNOWN',
                    'min_score': self.SCORE_THRESHOLD,
                    'max_positions': 20,
                    'risk_multiplier': 1.0
                }
            
            # Détecter le régime
            regime_result = self.regime_detector.detect_regime(btc_klines)
            
            return {
                'regime': regime_result.get('regime', 'UNKNOWN'),
                'regime_name': regime_result.get('regime_name', 'Unknown'),
                'min_score': regime_result.get('recommended_min_score', self.SCORE_THRESHOLD),
                'max_positions': regime_result.get('recommended_max_positions', 20),
                'risk_multiplier': regime_result.get('recommended_risk_multiplier', 1.0),
                'btc_trend_24h': regime_result.get('btc_trend_24h', 0),
                'btc_trend_7d': regime_result.get('btc_trend_7d', 0)
            }
        except Exception as e:
            logger.error(f"Erreur détection régime: {e}")
            return {
                'regime': 'UNKNOWN',
                'min_score': self.SCORE_THRESHOLD,
                'max_positions': 20,
                'risk_multiplier': 1.0
            }
    
    def train_model(self, training_data: List[Dict]):
        """
        Entraîne le modèle sur les données historiques
        training_data: Liste de {prices: [], result: 'up'/'down'/'neutral'}
        """
        if not self.model or not TORCH_AVAILABLE:
            logger.warning("Modèle non disponible pour l'entraînement")
            return
        
        logger.info(f"🎓 Entraînement du modèle sur {len(training_data)} échantillons...")
        
        # Mettre à jour le statut
        self.training_stats['status'] = 'training'
        self._save_training_stats()
        
        # Préparer les données
        X = []
        y = []
        
        for sample in training_data:
            prices = sample['prices']
            if len(prices) < 50:
                continue
            
            # Features
            prices_arr = np.array(prices[-50:])
            std = np.std(prices_arr)
            if std == 0:
                continue
            prices_norm = (prices_arr - np.mean(prices_arr)) / std
            
            features = np.zeros((50, 20), dtype=np.float32)
            features[:, 0] = prices_norm
            
            # Ajouter momentum
            for i in range(50):
                if i >= 5:
                    features[i, 1] = (prices_norm[i] - prices_norm[i-5]) / 5
                if i >= 10:
                    features[i, 2] = (prices_norm[i] - prices_norm[i-10]) / 10
            
            X.append(features)
            
            # Label
            if sample['result'] == 'up':
                y.append(2)
            elif sample['result'] == 'down':
                y.append(0)
            else:
                y.append(1)
        
        if len(X) < 100:
            logger.warning("Pas assez de données pour l'entraînement")
            self.training_stats['status'] = 'untrained'
            return
        
        # Convertir en tensors PyTorch
        X = torch.tensor(np.array(X), dtype=torch.float32).to(DEVICE)
        y = torch.tensor(np.array(y), dtype=torch.long).to(DEVICE)
        
        # Split train/validation
        split_idx = int(len(X) * 0.8)
        X_train, X_val = X[:split_idx], X[split_idx:]
        y_train, y_val = y[:split_idx], y[split_idx:]
        
        # Optimiseur et loss
        optimizer = torch.optim.Adam(self.model.parameters(), lr=0.001)
        criterion = nn.CrossEntropyLoss()
        
        # Entraînement
        self.model.train()
        epochs = 50
        batch_size = 32
        
        for epoch in range(epochs):
            total_loss = 0
            correct = 0
            total = 0
            
            # Mini-batches
            for i in range(0, len(X_train), batch_size):
                batch_X = X_train[i:i+batch_size]
                batch_y = y_train[i:i+batch_size]
                
                optimizer.zero_grad()
                outputs = self.model(batch_X)
                loss = criterion(outputs, batch_y)
                loss.backward()
                optimizer.step()
                
                total_loss += loss.item()
                _, predicted = torch.max(outputs.data, 1)
                total += batch_y.size(0)
                correct += (predicted == batch_y).sum().item()
            
            train_acc = correct / total if total > 0 else 0
            
            # Validation
            self.model.eval()
            with torch.no_grad():
                val_outputs = self.model(X_val)
                _, val_predicted = torch.max(val_outputs.data, 1)
                val_acc = (val_predicted == y_val).sum().item() / len(y_val) if len(y_val) > 0 else 0
            self.model.train()
            
            if epoch % 10 == 0:
                logger.info(f"  Epoch {epoch+1}/{epochs} - Loss: {total_loss:.4f} - Train Acc: {train_acc:.2%} - Val Acc: {val_acc:.2%}")
        
        # Mettre à jour les stats
        self.training_stats['status'] = 'trained'
        self.training_stats['samples_count'] = len(X)
        self.training_stats['epochs_completed'] = epochs
        self.training_stats['last_loss'] = total_loss
        self.training_stats['last_accuracy'] = train_acc
        self.training_stats['validation_accuracy'] = val_acc * 100
        self.training_stats['last_training'] = datetime.now().isoformat()
        self._save_training_stats()
        
        # Sauvegarder le modèle
        model_path_pt = self.model_path.replace('.h5', '.pt')
        os.makedirs(os.path.dirname(model_path_pt) if os.path.dirname(model_path_pt) else '.', exist_ok=True)
        torch.save(self.model, model_path_pt)
        logger.info(f"✅ Modèle entraîné et sauvegardé: {model_path_pt}")


class SurveillanceService:
    """
    Service de surveillance en temps réel
    Analyse toutes les cryptos et maintient une watchlist IA
    """
    
    def __init__(self, predictor: AIPredictor):
        self.predictor = predictor
        self.is_running = False
        self._thread = None
        self.symbols_to_watch = []
        self.klines_fetcher = None  # Sera injecté
        self.on_signal_callback = None  # Callback quand un signal est prêt
        self.update_interval = 5  # Secondes (ultra-réactif pour achat immédiat)
        
        # 🔥 Timestamp du dernier cycle pour affichage dashboard
        self.last_cycle_time = None
        
        # === CACHE PERSISTANTE DES SIGNAUX VALIDÉS ===
        self._cached_ready_signals = []  # Persiste les signaux entre les cycles
        self._cache_expiration = {}  # Timestamp d'expiration par signal
        self._cache_ttl = 120  # 🔧 FIX TEMPORAL: 180s→120s — aligne avec le cycle bot (~115s) pour signaux frais
        # AVANT: 30s trop court - signaux expiraient pendant le cycle d'analyse!
        # MAINTENANT: 300s = laisse le temps au bot de traiter les signaux
        
        # === LOGS RÉCENTS POUR DASHBOARD ===
        from collections import deque
        self._recent_analysis_logs = deque(maxlen=50)  # Garder les 50 derniers logs
        
        # === TRACKING TEMPS RÉEL POUR WIDGET DASHBOARD ===
        self.ia_start_time = None
        self.current_phase = 'NOT_STARTED'
        self.errors_count = 0
        self.last_cycle_duration = 0
        self.cycle_durations = deque(maxlen=60)
        self.signals_found_total = 0
        self.last_smart_summary = {}
    
    def set_klines_fetcher(self, fetcher):
        """Définit la fonction pour récupérer les klines"""
        self.klines_fetcher = fetcher
    
    def set_on_signal(self, callback):
        """Définit le callback pour les signaux d'achat"""
        self.on_signal_callback = callback
    
    def set_symbols(self, symbols: List[str]):
        """Définit les symboles à surveiller"""
        self.symbols_to_watch = symbols
        logger.info(f"📋 Surveillance IA: {len(symbols)} symboles configurés")
    
    def start(self):
        """Démarre le service de surveillance"""
        logger.info(f"🔍 [START] Début start() - is_running={self.is_running}")
        
        if self.is_running:
            logger.warning("⚠️ Service déjà en cours d'exécution")
            return
        
        if not self.symbols_to_watch:
            logger.error("⚠️ Impossible de démarrer: aucun symbole configuré!")
            return
        
        logger.info(f"🔍 [START] symbols_to_watch: {len(self.symbols_to_watch)} symboles")
        logger.info(f"🔍 [START] klines_fetcher: {self.klines_fetcher is not None}")
        logger.info(f"🔍 [START] predictor.klines_fetcher: {self.predictor.klines_fetcher is not None if self.predictor else 'No predictor'}")
        
        if not self.klines_fetcher:
            logger.error("⚠️ Impossible de démarrer: klines_fetcher non configuré!")
            return
        
        self.is_running = True
        self.ia_start_time = time.time()
        self.current_phase = 'INITIALIZING'
        logger.info(f"🔍 [START] Création du thread...")
        self._thread = threading.Thread(target=self._surveillance_loop, daemon=True)
        logger.info(f"🔍 [START] Démarrage du thread...")
        self._thread.start()
        logger.info(f"✅ Service de surveillance IA démarré ({len(self.symbols_to_watch)} symboles) - Thread: {self._thread.is_alive()}")
    
    def stop(self):
        """Arrête le service"""
        self.is_running = False
        if self._thread:
            self._thread.join(timeout=5)
        logger.info("⏹️ Service de surveillance IA arrêté")
    
    def _surveillance_loop(self):
        """Boucle principale de surveillance"""
        try:
            logger.info("🔍 [THREAD] Entrée dans _surveillance_loop - Thread démarré")
            logger.info(f"🔍 [THREAD] is_running={self.is_running}, symbols={len(self.symbols_to_watch) if self.symbols_to_watch else 0}")
            
            while self.is_running:
                try:
                    cycle_num = getattr(self, '_cycle_count', 0) + 1
                    cycle_start = time.time()
                    
                    # 🔥 Enregistrer le timestamp pour l'affichage dashboard
                    self.last_cycle_time = datetime.now()
                    self.current_phase = 'ANALYZING'
                    
                    logger.info(f"\n{'='*80}")
                    logger.info(f"🔄 CYCLE IA #{cycle_num} - {datetime.now().strftime('%H:%M:%S')} - Analyse {len(self.symbols_to_watch)} symboles...")
                    logger.info(f"{'='*80}")
                    
                    self._analyze_all_symbols()
                    
                    # 🧠 LSTM REVERSAL: Entraînement online périodique (toutes les 30 min)
                    # 🔴 FIX 23/03: Exécution dans un thread séparé avec timeout 60s
                    # BUG: train_online() bloquait le thread surveillance indéfiniment (cycle #122 bloqué 13min+)
                    if LSTM_REVERSAL_AVAILABLE and hasattr(self, 'predictor') and self.predictor.reversal_predictor:
                        try:
                            rev_pred = self.predictor.reversal_predictor
                            if rev_pred.should_retrain():
                                logger.info("🧠 LSTM Reversal: Lancement entraînement online (thread non-bloquant)...")
                                _lstm_result = {}
                                def _run_lstm_train():
                                    try:
                                        _lstm_result['metrics'] = rev_pred.train_online()
                                    except Exception as _e:
                                        _lstm_result['error'] = str(_e)
                                _lstm_thread = threading.Thread(target=_run_lstm_train, daemon=True)
                                _lstm_thread.start()
                                _lstm_thread.join(timeout=60)  # 60s max — si bloqué, on abandonne
                                if _lstm_thread.is_alive():
                                    logger.warning("🧠 LSTM Reversal: Entraînement timeout (>60s) — cycle IA non bloqué, thread abandonné")
                                elif 'metrics' in _lstm_result:
                                    metrics = _lstm_result['metrics']
                                    if metrics.get('success'):
                                        logger.info(f"🧠 LSTM Reversal: Entraînement OK - loss={metrics.get('final_loss', 0):.4f} acc={metrics.get('accuracy', 0):.1f}%")
                                    else:
                                        logger.debug(f"🧠 LSTM Reversal: {metrics.get('reason', 'pas assez de données')}")
                                elif 'error' in _lstm_result:
                                    logger.debug(f"🧠 LSTM Reversal: erreur entraînement: {_lstm_result['error']}")
                        except Exception as e:
                            logger.debug(f"Erreur train LSTM reversal: {e}")
                    
                    cycle_duration = time.time() - cycle_start
                    self.last_cycle_duration = cycle_duration
                    self.cycle_durations.append(cycle_duration)
                    self.current_phase = 'WAITING'
                    self._write_ia_status()
                    
                    logger.info(f"✅ CYCLE #{cycle_num} TERMINÉ en {cycle_duration:.1f}s - Prochain dans {self.update_interval}s\n")
                    time.sleep(self.update_interval)
                except Exception as e:
                    self.errors_count += 1
                    self.current_phase = 'ERROR'
                    logger.error(f"❌ Erreur cycle surveillance: {e}")
                    import traceback
                    traceback.print_exc()
                    self._write_ia_status()
                    time.sleep(5)
        except Exception as e:
            logger.error(f"💥 CRASH FATAL _surveillance_loop: {e}")
            import traceback
            traceback.print_exc()
            self.is_running = False
    
    def _write_ia_status(self):
        """Écrit le statut temps réel de la surveillance IA pour le widget dashboard"""
        try:
            import os as _os
            import json as _json
            from datetime import datetime as _dt
            
            uptime = time.time() - self.ia_start_time if self.ia_start_time else 0
            avg_duration = sum(self.cycle_durations) / len(self.cycle_durations) if self.cycle_durations else 0
            cycle_count = getattr(self, '_cycle_count', 0)
            cycles_per_min = (cycle_count / uptime * 60) if uptime > 0 and cycle_count > 0 else 0
            
            smart = self.last_smart_summary or {}
            
            status = {
                'running': self.is_running,
                'phase': self.current_phase,
                'mode': 'POLLING',
                'uptime_seconds': round(uptime, 1),
                'cycle_count': cycle_count,
                'update_interval': self.update_interval,
                'last_cycle_time': time.time(),
                'last_cycle_duration': round(self.last_cycle_duration, 2),
                'avg_cycle_duration': round(avg_duration, 2),
                'cycles_per_minute': round(cycles_per_min, 1),
                'symbols_total': len(self.symbols_to_watch),
                'symbols_analyzed': smart.get('analyzed', len(self.symbols_to_watch)),
                'signals_achat': smart.get('achat', 0),
                'signals_possible': smart.get('possible', 0),
                'signals_vente': smart.get('vente', 0),
                'signals_abandonnee': smart.get('abandonnee', 0),
                'eligible_count': smart.get('eligible', 0),
                'errors_count': self.errors_count,
                'cached_signals': len(self._cached_ready_signals),
                'cache_ttl': self._cache_ttl,
                'timestamp': _dt.now().isoformat(),
                'pid': _os.getpid()
            }
            
            status_file = _os.path.join(_os.path.dirname(__file__), 'ia_status.json')
            
            # 🔧 FIX 01/04: Seul le bot principal écrit ia_status.json
            # 🔧 FIX 22/04: Vérifier que le bot PID est réellement vivant
            _should_write_status = True
            try:
                _bot_pid_file = _os.path.join(_os.path.dirname(__file__), 'bot.pid')
                if _os.path.exists(_bot_pid_file):
                    with open(_bot_pid_file, 'r') as _f:
                        _bot_pid = int(_f.read().strip())
                    _bot_alive = False
                    try:
                        _os.kill(_bot_pid, 0)
                        _bot_alive = True
                    except (ProcessLookupError, PermissionError):
                        _bot_alive = False
                    if _os.getpid() != _bot_pid and _bot_alive:
                        _should_write_status = False
                    elif not _bot_alive:
                        try:
                            _os.remove(_bot_pid_file)
                        except Exception:
                            pass
            except Exception:
                pass
            
            if _should_write_status:
                status_tmp = status_file + '.tmp'
                with open(status_tmp, 'w', encoding='utf-8') as f:
                    _json.dump(status, f, ensure_ascii=False, indent=2)
                _os.replace(status_tmp, status_file)
        except Exception as e:
            logger.debug(f"Erreur écriture ia_status.json: {e}")
    
    def _analyze_all_symbols(self):
        """Analyse tous les symboles - cycle rapide pour réactivité maximale"""
        if not self.klines_fetcher or not self.symbols_to_watch:
            logger.error(f"❌ _analyze_all_symbols BLOQUÉ: klines_fetcher={self.klines_fetcher is not None}, symbols={len(self.symbols_to_watch) if self.symbols_to_watch else 0}")
            return
        
        logger.debug(f"🔄 _analyze_all_symbols: Début analyse de {len(self.symbols_to_watch)} symboles")
        
        # S'assurer que le predictor a aussi le klines_fetcher
        if not self.predictor.klines_fetcher:
            self.predictor.set_klines_fetcher(self.klines_fetcher)
        
        start_time = time.time()
        analyzed = 0
        signals_found = 0
        
        # 🚀 ANALYSE PARALLÈLE RAPIDE - 12 symboles simultanés
        # 🔧 FIX 27/02: 8→12 workers pour réduire le temps de cycle (115s→~80s)
        def analyze_one_symbol(symbol):
            """Analyse un symbole (pour exécution parallèle)"""
            try:
                klines = self.klines_fetcher(symbol, "5m", 100)
                if not klines or len(klines) < 50:
                    return None
                
                # 🔧 FIX SYNCHRO: Exclure la bougie EN COURS (ouverte) de l'analyse.
                # Binance inclut toujours la bougie active en dernière position.
                # Son close_time (k[6]) est dans le futur → données OHLCV incomplètes.
                # Inclure une bougie partielle fausse RSI/EMA/BB et génère de faux signaux.
                import time as _now
                if len(klines) >= 2 and float(klines[-1][6]) / 1000 > _now.time():
                    closed_klines = klines[:-1]  # Uniquement bougies clôturées
                else:
                    closed_klines = klines
                
                prices = [float(k[4]) for k in closed_klines]
                volumes = [float(k[5]) for k in closed_klines]
                
                item = self.predictor.analyze_symbol(symbol, prices, volumes)
                
                # 🆕 31/03: RSI 1m complémentaire — confirmation rapide pour CREUX_REBOUND
                # Le RSI 5m (70min) est trop lent pour capter les micro-retournements.
                # RSI 1m (14min) détecte le début du rebond bien plus tôt.
                if item and item.features:
                    try:
                        klines_1m = self.klines_fetcher(symbol, "1m", 20)
                        if klines_1m and len(klines_1m) >= 15:
                            if len(klines_1m) >= 2 and float(klines_1m[-1][6]) / 1000 > _now.time():
                                klines_1m = klines_1m[:-1]
                            prices_1m = np.array([float(k[4]) for k in klines_1m])
                            rsi_1m = float(PatternFeatures._rsi(prices_1m))
                            item.features['rsi_1m'] = rsi_1m
                            # Momentum 1m (3 bougies = 3 min)
                            if len(prices_1m) >= 3:
                                item.features['momentum_1m_3'] = (prices_1m[-1] - prices_1m[-3]) / prices_1m[-3] * 100
                            if len(prices_1m) >= 5:
                                item.features['momentum_1m_5'] = (prices_1m[-1] - prices_1m[-5]) / prices_1m[-5] * 100
                    except Exception:
                        pass  # RSI 1m en complément, pas critique si indisponible
                
                return (symbol, item)
            except Exception as e:
                logger.debug(f"Erreur {symbol}: {e}")
                return None
        
        self.current_phase = 'SCORING'
        
        # � FIX 30/03: 8→12 workers — display_status throttlé libère du CPU
        # 12 workers × 2 threads torch = ~24 threads (acceptable, display ne sature plus)
        # Gain: cycle IA ~17s → ~12s, réduit signal age de 5s
        with ThreadPoolExecutor(max_workers=12) as executor:
            futures = {executor.submit(analyze_one_symbol, sym): sym for sym in self.symbols_to_watch}
            
            for future in as_completed(futures):
                result = future.result()
                if result:
                    symbol, item = result
                    self.predictor.update_watchlist(symbol, item)
                    analyzed += 1
                    
                    if item.status == "ready":
                        signals_found += 1
                        if self.on_signal_callback:
                            self.on_signal_callback(symbol, item)
        
        # Log du cycle (toutes les 4 analyses seulement pour pas spammer)
        elapsed = time.time() - start_time
        if analyzed > 0 and hasattr(self, '_cycle_count'):
            self._cycle_count += 1
        else:
            self._cycle_count = 1
        
        self.current_phase = 'VALIDATING'
        
        # Sauvegarder le statut pour le dashboard à CHAQUE cycle
        try:
            import json
            import os
            status = self._build_surveillance_status()
            
            # Sauver le smart_summary pour le widget IA live
            self.last_smart_summary = status.get('smart_summary', {})
            self.last_smart_summary['analyzed'] = analyzed
            
            # Ajouter les logs récents pour affichage dans le dashboard
            if hasattr(self, '_recent_analysis_logs'):
                status['recent_logs'] = list(self._recent_analysis_logs)
            
            # 🔥 NOUVEAU: Ajouter le temps de cycle pour affichage dans le dashboard
            status['cycle_duration_seconds'] = round(elapsed, 1)
            status['analyzed_symbols'] = analyzed
            
            cache_file = os.path.join(os.path.dirname(__file__), "ia_surveillance_cache.json")
            
            # 🔧 FIX 01/04: Éviter race condition bot vs dashboard
            # Seul le processus principal (trading_bot) écrit le cache
            # Le dashboard_api_server ne doit PAS écraser les données du bot
            should_write = True
            try:
                bot_pid_file = os.path.join(os.path.dirname(__file__), 'bot.pid')
                if os.path.exists(bot_pid_file):
                    with open(bot_pid_file, 'r') as f:
                        bot_pid = int(f.read().strip())
                    my_pid = os.getpid()
                    # Vérifier si le bot PID est réellement vivant
                    bot_alive = False
                    try:
                        os.kill(bot_pid, 0)
                        bot_alive = True
                    except (ProcessLookupError, PermissionError):
                        bot_alive = False
                    if my_pid != bot_pid and bot_alive:
                        # Le bot est en cours ET c'est un autre processus → ne pas écraser
                        # Mais mettre à jour last_smart_summary depuis le cache existant
                        if os.path.exists(cache_file):
                            with open(cache_file, 'r', encoding='utf-8') as f_check:
                                existing = json.load(f_check)
                            if existing.get('analyzed', 0) > 0:
                                self.last_smart_summary = existing.get('smart_summary', self.last_smart_summary)
                        should_write = False
                        logger.debug(f"📊 Cache IA non écrit (process secondaire PID {my_pid} ≠ bot PID {bot_pid})")
                    elif not bot_alive and os.path.exists(bot_pid_file):
                        # bot.pid périmé → le supprimer
                        try:
                            os.remove(bot_pid_file)
                            logger.info(f"🧹 bot.pid périmé supprimé (PID {bot_pid} mort)")
                        except Exception:
                            pass
            except Exception:
                pass  # En cas d'erreur, écrire quand même

            if should_write:
                cache_tmp = cache_file + '.tmp'
                with open(cache_tmp, 'w', encoding='utf-8') as f:
                    json.dump(status, f, ensure_ascii=False, indent=2, default=str)
                os.replace(cache_tmp, cache_file)
        except Exception as e:
            logger.debug(f"Erreur sauvegarde cache IA: {e}")
        
        # Stats détaillées du cycle
        logger.info(f"📊 Résultats: {analyzed}/{len(self.symbols_to_watch)} symboles analysés | ⚡ {signals_found} signaux prêts | ⏱️ {elapsed:.1f}s")
        if signals_found > 0:
            logger.info(f"🎯 {signals_found} opportunités d'achat détectées!")
    
    def get_surveillance_status(self) -> Dict:
        """Retourne le statut de la surveillance - LECTURE DIRECTE DU CACHE"""
        # OPTIMISATION: Lire directement le cache déjà calculé au lieu de tout retraiter
        # Le cache est mis à jour toutes les 5 secondes par _analyze_all_symbols()
        try:
            import os
            import json
            cache_path = os.path.join(os.path.dirname(__file__), 'ia_surveillance_cache.json')
            if os.path.exists(cache_path):
                with open(cache_path, 'r', encoding='utf-8') as f:
                    cache_data = json.load(f)
                    logger.debug(f"📊 Cache IA retourné directement ({cache_data.get('analyzed', 0)} symboles)")
                    return cache_data
        except Exception as e:
            logger.warning(f"⚠️ Erreur lecture cache: {e}")
        
        # Fallback: retourner structure vide si cache indisponible
        return {
            'is_running': self.is_running,
            'ai_available': True,
            'total_symbols': 58,
            'analyzed': 0,
            'ready_to_buy': 0,
            'watching': 0,
            'top_opportunities': [],
            'ready_signals': [],
            'rotation_opportunities': [],
            'rotation_status': {},
            'ai_opportunities': [],
            'ai_selected_symbols': [],
            'ai_opportunity_count': 0,
            'dynamic_sltp_enabled': False,
            'smart_summary': {'achat': 0, 'possible': 0, 'vente': 0, 'abandonnee': 0, 'eligible': 0},
            'by_status': {'achat': [], 'en_surveillance': [], 'vente': [], 'abandonnee': []}
        }
    
    def _build_surveillance_status(self) -> Dict:
        """Construit le statut complet depuis la watchlist du predictor (pour générer le cache)"""
        # Charger la watchlist DIRECTEMENT du predictor (contient les 58 symboles analysés)
        watchlist = self.predictor.get_watchlist()
        
        logger.debug(f"📊 Construction cache depuis watchlist predictor: {len(watchlist)} symboles")
        
        # ══════════════════════════════════════════════════════════════
        # RÈGLE: Validation finale avant achat (blocages absolus seulement)
        # Les patterns ont déjà été validés par analyze_symbol()
        # Ici on vérifie juste les conditions critiques de sécurité
        # ══════════════════════════════════════════════════════════════
        def is_valid_buy_signal(w):
            symbol = w.get('symbol', '')
            
            pattern = w.get('pattern', '')
            score = w.get('score', 0)
            smart_signal = w.get('smart_signal', '')
            smart_eligible = w.get('smart_eligible', False)
            
            # DEBUG pour MATICUSDT
            if symbol == 'MATICUSDT':
                logger.info(f"🔍 VALIDATION {symbol}: smart_signal={smart_signal}, eligible={smart_eligible}, pattern={pattern}, score={score}")
            
            # NOUVEAU: Utiliser smart_criteria au lieu de status
            # Accepter si smart_signal='ACHAT' et smart_eligible=True
            if smart_signal != 'ACHAT' or not smart_eligible:
                if symbol == 'MATICUSDT':
                    logger.info(f"❌ VALIDATION {symbol}: REJETÉ (smart_signal={smart_signal}, eligible={smart_eligible})")
                return False
            
            # Score minimum absolu = 15 (assoupli car smart_criteria valide déjà)
            if score < 15:
                return False
            
            # 🔴🔴🔴 BLOCAGE ABSOLU si pattern dangereux (inclut END_OF_CYCLE) 🔴🔴🔴
            if pattern in ['ACTIVE_CRASH', 'PRICE_CORRECTION', 'RSI_TRAP', 'STRONG_DOWNTREND', 'BEARISH_TREND', 'CREUX_TOO_DEEP', 'END_OF_CYCLE', 'DEAD_CAT_BOUNCE', 'PROLONGED_DOWNTREND']:
                return False
            
            # 🔴 FIX 09/02: BLOCAGE si pattern NON-ACHETABLE (NEUTRAL, UNKNOWN, etc.)
            # Même si smart_criteria dit ACHAT, si le pattern est NEUTRAL = pas d'achat
            # Évite les signaux READY en surachat/marché baissier
            BUYABLE_PATTERNS_VALIDATION = [
                'CREUX_REBOUND', 'SQUEEZE_BREAKOUT', 'EARLY_BREAKOUT',
                'CONSOLIDATION_BREAKOUT', 'EMA_BULLISH', 'CROSSOVER_IMMINENT',
                'VOLUME_REVERSAL', 'RSI_REVERSAL', 'STRONG_UPTREND', 'HIGH_SCORE_OVERRIDE'
                # 🔧 FIX 13/03: PULLBACK retiré — blacklisté (perf insuffisante) donc inutile ici
            ]
            if pattern not in BUYABLE_PATTERNS_VALIDATION:
                logger.debug(f"🚫 {symbol}: Pattern '{pattern}' non-achetable - REJETÉ par is_valid_buy_signal")
                return False
            
            # 🆕 FIX 28/01: Protection BAISSE MODÉRÉE en BULL_WEAK
            # Si marché baisse doucement (-0.3% à -1%), éviter nouveaux achats
            features = w.get('features', {})
            momentum_3_val = features.get('momentum_3', 0)
            momentum_5_val = features.get('momentum_5', 0)
            
            # Vérifier le régime (importé depuis market_regime_detector)
            # En BULL_WEAK avec baisse persistante, hausser les exigences
            if hasattr(AIPredictor, '_current_regime'):
                regime = AIPredictor._current_regime
                # BULL_WEAK + momentum négatif PERSISTANT = trop risqué
                if regime == 'BULL_WEAK':
                    # Si les 2 momentum sont négatifs ET altcoins baissent plus que -0.6%
                    if momentum_3_val < -0.5 and momentum_5_val < -0.5:  # -0.5% sur les 2
                        # Exception: Score TRÈS élevé (≥ 90) ou pattern rebond confirmé avec RSI < 35
                        rsi_temp = features.get('rsi', 50)
                        is_strong_rebound = (pattern in ['CREUX_REBOUND', 'SQUEEZE_BREAKOUT'] and rsi_temp < 35 and score >= 85)
                        if not (score >= 90 or is_strong_rebound):
                            logger.info(f"🚫 {symbol}: BULL_WEAK + baisse persistante (Mom3={momentum_3_val:.2%} Mom5={momentum_5_val:.2%}) - ACHAT BLOQUÉ")
                            return False
            
            features = w.get('features', {})
            momentum_3 = features.get('momentum_3', 0)
            momentum_5 = features.get('momentum_5', 0)
            ema_diff = features.get('ema_diff', 0)
            ema_slope = features.get('ema_slope', 0)
            rsi = features.get('rsi', 50)
            bb_position = features.get('bb_position', 0.5)
            ema9 = features.get('ema9', 0)
            ema21 = features.get('ema21', 0)
            
            # ═══════════════════════════════════════════════════════════════
            # 🔴 RÈGLE CRITIQUE #1: NE PAS ACHETER AU SOMMET DE LA BB!
            # bb_position > 0.65 = prix dans la zone très haute = DANGEREUX
            # Objectif: Acheter en zone basse/médiane-basse
            # Exception: si score >= 80 (très confiant) on accepte jusqu'à 0.70
            # ASSOUPLI: score >= 75 → 0.70, score >= 70 → 0.68, sinon 0.65
            # 🆕 NOUVEAU: Si bb > 0.60 SANS momentum fort → BLOQUER!
            # ═══════════════════════════════════════════════════════════════
            
            # VÉRIFICATION MOMENTUM FORT REQUIS en zone haute
            # Si bb_position > 0.60 sans momentum fort (+0.25%) = DANGER!
            # Exemple: HBAR bb=0.64 mom=+0.12% → BLOQUÉ vs OPUSDT bb=0.57 mom=+0.26% → OK
            # 🆕 EXCEPTION: Si score >= 80 (très confiant) ou pattern fort, on autorise
            # 🆕 PULLBACK ajouté 20/01: Pullback dans tendance = légitime en zone haute
            is_strong_pattern = pattern in ['EARLY_BREAKOUT', 'CONSOLIDATION_BREAKOUT', 'STRONG_UPTREND', 'PULLBACK']
            # 🔧 FIX: BB 0.60 bloquait trop de signaux légitimes en tendance normale
            # Relevé à 0.72 (zone vraiment haute), momentum 0.25%→0.15%, score 80→75
            if bb_position > 0.72 and momentum_3 < 0.15 and score < 75 and not is_strong_pattern:
                logger.info(f"🚫 {symbol}: BB_position={bb_position:.2f} en zone haute SANS momentum fort (mom={momentum_3:.2%}) - ACHAT BLOQUÉ")
                return False
            
            # 🔧 FIX: Seuils BB progressifs assouplis pour laisser passer les signaux normaux
            # Zone haute (bb>0.72) = Plus exigeant sur le score IA
            # Zone médiane-haute (bb>0.65) = Exigence modérée
            if bb_position > 0.72 and score < 70 and not is_strong_pattern:
                logger.info(f"🚫 {symbol}: BB_position={bb_position:.2f} > 0.72 avec score={score} < 70 - ACHAT BLOQUÉ (score insuffisant pour zone haute)")
                return False
            elif bb_position > 0.65 and score < 65 and not is_strong_pattern:
                logger.info(f"🚫 {symbol}: BB_position={bb_position:.2f} > 0.65 avec score={score} < 65 - ACHAT BLOQUÉ (score insuffisant pour zone médiane-haute)")
                return False
            
            # SEUIL MINIMUM 17/01: Éviter falling knife en zone très basse
            # ETH 07h36: BB=0.33 → chute -2.76%. Zone <0.20 = danger extrême
            # 🔧 FIX: CREUX_REBOUND EXEMPTÉ - c'est justement un achat en zone basse!
            # RSI_REVERSAL aussi car c'est un signal de retournement en zone basse
            is_low_zone_pattern = pattern in ['CREUX_REBOUND', 'RSI_REVERSAL']
            if bb_position < 0.20 and not is_low_zone_pattern:
                logger.info(f"🚫 {symbol}: BB_position={bb_position:.2f} < 0.20 - ACHAT BLOQUÉ (zone trop basse, risque falling knife)")
                return False
            elif bb_position < 0.10:  # Ultra-bas = danger même pour CREUX_REBOUND
                # 🔴 DÉSACTIVÉ 21/03: exception LSTM FORCED REVERSAL retirée (override désactivé globalement)
                logger.info(f"🚫 {symbol}: BB_position={bb_position:.2f} < 0.10 - ACHAT BLOQUÉ (zone EXTRÊME)")
                return False
            
            # Seuils bb_position progressifs selon score
            # ASSOUPLI 20/01: Patterns forts (EARLY_BREAKOUT, CONSOLIDATION_BREAKOUT) peuvent aller jusqu'à BB=1.00
            # Car ce sont des cassures légitimes qui méritent d'être tradées même en zone haute
            # 🔥 FIX 27/01: CREUX_REBOUND EXEMPT de bb_threshold (rebond légitime même si prix a remonté)
            # ⚠️ PULLBACK: Seuil plus conservateur (0.88) car risque de fin de cycle
            if pattern == 'CREUX_REBOUND':
                # CREUX_REBOUND = rebond depuis creux, même si bb_position a remonté c'est OK
                logger.info(f"✅ {symbol}: CREUX_REBOUND EXEMPT de bb_threshold (rebond légitime)")
                bb_threshold = 1.00  # Pas de limite pour CREUX_REBOUND
            elif pattern == 'PULLBACK' and score >= 80:
                bb_threshold = 0.88  # PULLBACK autorisé en zone haute mais pas extrême
                logger.info(f"✅ {symbol}: PULLBACK score {score} → bb_threshold={bb_threshold:.2f} (conservateur)")
            elif is_strong_pattern and score >= 80:
                bb_threshold = 1.00  # 🆕 BREAKOUT confirmé = accepter zone très haute!
                logger.info(f"✅ {symbol}: Pattern {pattern} + score {score} → bb_threshold relevé à {bb_threshold:.2f}")
            elif score >= 80:
                bb_threshold = 0.85  # Zone très haute OK si score excellent
            elif score >= 75:
                bb_threshold = 0.82
            elif score >= 70:
                bb_threshold = 0.78
            else:
                bb_threshold = 0.75  # Relevé de 0.65 pour éviter achats trop bas
            
            if bb_position > bb_threshold:
                logger.info(f"🚫 {symbol}: BB_position={bb_position:.2f} > {bb_threshold:.2f} - ACHAT BLOQUÉ (zone haute)")
                return False
            
            # ⚠️ PULLBACK EN ZONE HAUTE: Vérification END_OF_CYCLE stricte
            # Si BB > 0.85 + RSI > 65 = risque élevé de fin de cycle
            if pattern == 'PULLBACK' and bb_position > 0.85:
                rsi_pullback_check = features.get('rsi', 50)  # 🔧 FIX: was data.get('rsi') - 'data' undefined
                if rsi_pullback_check > 65:
                    logger.info(f"🚫 {symbol}: PULLBACK en zone haute BLOQUÉ - BB={bb_position:.2f} RSI={rsi_pullback_check:.0f} (fin de cycle probable)")
                    return False
                logger.info(f"⚠️ {symbol}: PULLBACK en zone haute ACCEPTÉ avec vigilance - BB={bb_position:.2f} RSI={rsi_pullback_check:.0f}")
            
            # ═══════════════════════════════════════════════════════════════
            # 🔴 RÈGLE CRITIQUE #2: NE PAS ACHETER QUAND EMA9 >> EMA21!
            # ema_diff > 0.15 = EMA9 au-dessus d'EMA21 = cycle déjà en hausse
            # On doit acheter quand EMA9 < EMA21 (creux, avant le rebond)
            # Exception: si RSI < 35 (survente extrême = rebond probable)
            # 🆕 EXCEPTION: Patterns EMA_BULLISH, PULLBACK validés jusqu'à 0.25
            # 🆕 EXCEPTION: STRONG_UPTREND peut avoir ema_diff très élevé (tendance longue)
            # ═══════════════════════════════════════════════════════════════
            # Patterns qui autorisent ema_diff élevé (tendance haussière confirmée)
            is_ema_pattern = pattern in ['EMA_BULLISH', 'PULLBACK', 'EARLY_BREAKOUT', 'CONSOLIDATION_BREAKOUT']
            ema_threshold = 0.30 if is_ema_pattern else 0.15
            
            # Patterns exempts du filtre ema_diff (achats valides en tendance haussière)
            # 🔴 FIX 08/02: TREND_CONTINUATION retiré (0% WR)
            # 🔧 FIX 13/03: CREUX_REBOUND exempt en BULL_STRONG — en uptrend, EMA9 > EMA21 est NORMAL
            # BUG: EMA_diff 0.15-0.5% bloquait tous les signaux après la reprise du marché
            _regime_now = features.get('market_regime', self._get_market_regime() if hasattr(self, '_get_market_regime') else 'NEUTRAL')
            _creux_in_bull = (pattern == 'CREUX_REBOUND' and _regime_now in ('BULL_STRONG', 'BULL_WEAK'))
            allowed_uptrend_patterns = ['STRONG_UPTREND', 'PULLBACK', 'SQUEEZE_BREAKOUT', 'HIGH_SCORE_OVERRIDE']
            if pattern in allowed_uptrend_patterns or _creux_in_bull:
                pass  # Ces patterns sont validés pour acheter en tendance haussière (ema_diff positif)
            elif ema_diff > ema_threshold and rsi > 40:
                logger.info(f"🚫 {symbol}: EMA_diff={ema_diff:.2f}% > {ema_threshold:.2f}% + RSI={rsi:.0f} - ACHAT BLOQUÉ (pas un creux)")
                return False
            
            # ═══════════════════════════════════════════════════════════════
            # 🔴 RÈGLE CRITIQUE #3: NE PAS ACHETER AVEC RSI ÉLEVÉ!
            # RSI > 70 = surachat, risque de correction imminente
            # ASSOUPLI 27/01: Seuils relevés pour bull market (65→70, 60→65, 80→85)
            # En marché haussier, RSI 65-70 est normal et ne signale pas forcément surachat
            # ═══════════════════════════════════════════════════════════════
            # 🔧 FIX: RSI 65 en NEUTRAL bloquait tout signal en légère hausse
            # RSI 70 est le vrai seuil de surachat, 65 est normal pour un actif haussier
            if is_strong_pattern and score >= 80:
                rsi_threshold = 85  # 🆕 BREAKOUT confirmé = RSI peut être très élevé!
            elif score >= 75:
                rsi_threshold = 75  # Signaux forts: RSI 75 acceptable
            else:
                rsi_threshold = 70  # RSI 70 = seuil standard de surachat
            
            if rsi > rsi_threshold:
                logger.info(f"🚫 {symbol}: RSI={rsi:.0f} > {rsi_threshold} - ACHAT BLOQUÉ (surachat)")
                return False
            
            # 🆕 PROTECTION CREUX_REBOUND: Ne pas acheter si ema_diff LÉGÈREMENT positif
            # Un vrai creux doit avoir ema_diff < 0 (prix sous EMA9)
            # Si ema_diff > 0 + bb_position > 0.60 = creux déjà rebondi, trop tard!
            # Exception: Score très élevé (>85) + RSI bas (<40) = survente confirmée
            if pattern == 'CREUX_REBOUND':
                if ema_diff > 0 and bb_position > 0.60 and not (score >= 85 and rsi < 40):
                    logger.info(f"🚫 {symbol}: CREUX_REBOUND trop tard - EMA_diff={ema_diff:.3f}% positif + BB={bb_position:.2f} > 0.60 (creux déjà rebondi)")
                    return False
                # 🔴 FIX: CREUX_REBOUND sans indicateurs de creux réel
                # Un vrai creux doit avoir AU MOINS UN signe de zone basse:
                #   - EMA_diff < -0.15% (prix clairement sous EMA9), OU
                #   - BB < 0.55 (zone inférieure bandes de Bollinger), OU
                #   - RSI ≤ 40 (survendu confirmé — seuil durci 24/03: 44→40)
                # Sinon, c'est une position mid-range = pas un creux, achat à mi-chemin risqué
                # BCH 18h01: EMA=-0.09%, BB=0.63, RSI=46 → aucun signe de creux → bloqué ✓
                # LTC 24/03: EMA=-0.03%, BB=0.61, RSI=43 → achat fictif à mi-mont → BLOQUÉ désormais ✓
                # 🔧 FIX 30/03: Exception momentum fort — si Mom5 > +3% le mouvement est réel
                # Cas HEMI 28/03: EMA=-0.007%, BB=0.74, RSI=58 mais Mom5=+8% → pump réel bloqué
                # Un momentum 5min > 3% avec score ≥ 70 = cassure de consolidation, pas un faux creux
                _mom5_val = features.get('momentum_5', 0) if isinstance(features, dict) else 0
                _strong_momentum_bypass = (_mom5_val > 3.0 and score >= 70)
                if ema_diff > -0.15 and bb_position > 0.55 and rsi > 40 and not (score >= 85 and rsi < 35) and not _strong_momentum_bypass:
                    logger.info(f"🚫 {symbol}: CREUX_REBOUND fictif — aucun signe de creux réel (EMA={ema_diff:.3f}%, BB={bb_position:.2f}, RSI={rsi:.0f}) → position mid-range, achat bloqué")
                    return False
                if _strong_momentum_bypass:
                    logger.info(f"✅ {symbol}: CREUX_REBOUND mid-range ACCEPTÉ — momentum fort (Mom5={_mom5_val:.1f}%, Score={score})")
            
            # ═══════════════════════════════════════════════════════════════
            # 🔧 FIX 08/02: BLOCAGE EMA_SLOPE NÉGATIF = ACHETER EN BAISSE INTERDIT!
            # ema_slope = direction de EMA9. Si négatif = EMA9 descend = prix chute
            # APT: ema_slope=-0.282 mais ema_diff=+0.109 → ema_diff positif mais prix CHUTE
            # ═══════════════════════════════════════════════════════════════
            if ema_slope < -0.15 and pattern not in ['CREUX_REBOUND', 'RSI_REVERSAL']:
                # EMA9 chute fortement — seuls les patterns de retournement sont autorisés
                logger.info(f"🚫 {symbol}: EMA_slope={ema_slope:.3f}% CHUTE FORTE — pattern {pattern} BLOQUÉ")
                return False
            
            # BLOCAGE RENFORCÉ: EMA diff négatif = tendance baissière court terme
            # Un score élevé ne justifie PAS d'acheter en tendance baissière
            # SEULS les patterns de retournement confirmés peuvent passer
            # 🔧 FIX: smart_criteria a DÉJÀ validé ces signaux — faire confiance
            # Ajout CROSSOVER_IMMINENT qui est assigné comme CREUX_REBOUND mais peut avoir ema_diff < 0
            if ema_diff < 0:
                # Exceptions pour retournements confirmés par smart_criteria:
                is_reversal_pattern = pattern in ['SQUEEZE_BREAKOUT', 'VOLUME_REVERSAL', 'RSI_REVERSAL', 'CREUX_REBOUND']
                # 🆕 Si smart_criteria a validé ACHAT et ema_diff > -0.5% → faire confiance
                smart_validated_early = (w.get('smart_signal') == 'ACHAT' and ema_diff > -0.5)
                
                if rsi < 25 or is_reversal_pattern or smart_validated_early:
                    if is_reversal_pattern:
                        logger.info(f"✅ {symbol}: EMA négatif AUTORISÉ - Pattern retournement {pattern}")
                    elif smart_validated_early:
                        logger.info(f"✅ {symbol}: EMA négatif AUTORISÉ - Smart criteria validé (ema_diff={ema_diff:.3f}%)")
                else:
                    logger.info(f"🚫 {symbol}: EMA_diff={ema_diff:.3f}% NÉGATIF (bearish) + RSI={rsi:.0f} - ACHAT BLOQUÉ")
                    return False
            
            # BLOCAGE: RSI survente + momentum négatif = CHUTE EN COURS
            # 🔧 FIX 09/02: Exception CREUX_REBOUND — un creux en zone RSI<30 avec momentum
            # à peine négatif est un EXCELLENT point d'entrée (survente), pas une chute
            # Seuls les crashs violents (mom3 < -0.5%) restent bloqués
            if rsi < 30 and momentum_3 < 0:
                is_oversold_creux = (pattern in ['CREUX_REBOUND', 'RSI_REVERSAL'] and momentum_3 > -0.5)
                if not is_oversold_creux:
                    return False
            
            # CRASH ACTIF: Momentum TRÈS négatif (crash violent uniquement)
            # 🔧 FIX AUDIT 28/02: Valeurs en % → -3 = -3%, -4 = -4% (était -0.03/-0.04 = 100x trop sensible)
            if momentum_3 < -3.0 and momentum_5 < -4.0:
                return False
            
            # CHUTE FORTE: EMA très négative + momentum en chute
            # 🔧 FIX AUDIT 28/02: -3% ema_diff + -1% momentum (était -0.03/-0.01 = 100x trop sensible)
            if ema_diff < -3.0 and momentum_3 < -1.0:
                return False
            
            # ══════════════════════════════════════════════════════════════
            # 🔴 FIX 09/02: SYNCHRONISATION avec trading_bot.py
            # Le bot vérifie: momentum_3 > -0.01 pour SMART_VALIDATED
            # Si momentum en chute majeure (< -1%), AUCUNE stratégie ne passera
            # Sauf CREUX avec RSI très bas (< 35) = rebond légitime de survente
            # ══════════════════════════════════════════════════════════════
            # 🔧 FIX AUDIT 28/02: -0.01 → -1.0 (momentum en %, -1.0 = -1%, pas -0.01 = -0.01%)
            if momentum_3_val < -1.0:  # Mom3 < -1% = chute majeure
                # Exception: CREUX_REBOUND avec RSI très bas (survente → rebond)
                is_creux_oversold = (pattern in ['CREUX_REBOUND', 'RSI_REVERSAL'] and rsi < 35)
                # Exception: Score exceptionnel (>= 90) avec momentum pas catastrophique
                is_exceptional = (score >= 90 and momentum_3_val > -3.0)
                if not (is_creux_oversold or is_exceptional):
                    logger.info(f"🚫 {symbol}: Momentum3={momentum_3_val:.2f}% CHUTE MAJEURE (< -1%) - non-tradable")
                    return False
            
            # 🔴 FIX 09/02: REGIME MIN_SCORE — Vérifier que le score respecte le min du régime
            # Identique au filtre de trading_bot.py ligne ~4860
            try:
                from market_regime import get_market_regime_detector, REGIME_CONFIG
                _regime_detector = get_market_regime_detector()
                if _regime_detector and hasattr(_regime_detector, 'current_regime'):
                    _regime_name = _regime_detector.current_regime or 'NEUTRAL'
                    _regime_min = REGIME_CONFIG.get(_regime_name, {}).get('min_score', 40)
                    if score < _regime_min:
                        logger.info(f"🛡️ {symbol}: Score {score} < {_regime_min} (régime {_regime_name}) - non-tradable")
                        return False
            except Exception:
                pass  # Pas de régime disponible, laisser passer
            
            # Sinon, accepter le signal (smart_criteria a déjà validé)
            return True
        
        # ══════════════════════════════════════════════════════════════
        # UTILISER smart_signal='ACHAT' du nouveau système Smart Criteria
        # 🔴 FIX 24/01: Filtrer aussi par score minimum (60) pour éviter signaux invalides
        # Le status='ready' est l'ancien système, smart_signal est le nouveau
        # 🔴 FIX 04/02: Accepter aussi POSSIBLE avec score >= 80 (signaux solides)
        # Le problème était que les signaux passent ACHAT → POSSIBLE rapidement
        # et le bot n'avait pas le temps de les acheter
        # ══════════════════════════════════════════════════════════════
        # 🔧 FIX 31/03b: Score minimum adaptatif selon le régime (aligné avec bot)
        # BULL_STRONG: 55, Autres: 65
        _ready_regime = getattr(AIPredictor, '_current_regime', 'NEUTRAL')
        MIN_SCORE_READY = 55 if _ready_regime == 'BULL_STRONG' else 65  # 🔴 FIX v6b COHÉRENCE: adaptatif
        MIN_SCORE_POSSIBLE = 55 if _ready_regime == 'BULL_STRONG' else 65  # POSSIBLE — adaptatif 31/03b
        
        # 🔴 FIX 09/02: Liste des patterns achetables — NEUTRAL, UNKNOWN etc. sont exclus
        BUYABLE_PATTERNS_LIST = [
            'CREUX_REBOUND', 'PULLBACK', 'SQUEEZE_BREAKOUT', 'EARLY_BREAKOUT',
            'CONSOLIDATION_BREAKOUT', 'EMA_BULLISH', 'CROSSOVER_IMMINENT',
            'VOLUME_REVERSAL', 'RSI_REVERSAL', 'STRONG_UPTREND', 'HIGH_SCORE_OVERRIDE'
        ]
        
        # 1. Signaux ACHAT classiques — 🔴 FIX 09/02: Filtrer aussi par pattern achetable
        ready_from_smart = [w for w in watchlist 
                           if w.get('smart_signal') == 'ACHAT' 
                           and w.get('smart_eligible') == True
                           and w.get('score', 0) >= MIN_SCORE_READY
                           and w.get('pattern', '') in BUYABLE_PATTERNS_LIST]  # 🔴 Pattern achetable requis!
        
        # 2. 🆕 Signaux POSSIBLE avec score élevé (solides mais temporairement en POSSIBLE)
        ready_from_possible = [w for w in watchlist
                               if w.get('smart_signal') == 'POSSIBLE'
                               and w.get('smart_eligible') == True
                               and w.get('score', 0) >= MIN_SCORE_POSSIBLE  # Score élevé requis
                               and w.get('pattern', '') in BUYABLE_PATTERNS_LIST]  # 🔴 FIX 09/02: Pattern achetable requis!
        
        if ready_from_possible:
            logger.info(f"🆕 {len(ready_from_possible)} signaux POSSIBLE avec score >= {MIN_SCORE_POSSIBLE} acceptés")
        ready_from_status = [w for w in watchlist if w.get('status') == 'ready']
        
        # 🔴 FIX COHERENCE: Exclure les patterns blacklistés AVANT de construire les ready_signals
        # Avant: l'IA envoyait des signaux PULLBACK que le bot rejetait systématiquement
        _blacklisted_patterns = set()
        _pattern_manager = None
        try:
            from pattern_manager import PatternManager
            _pattern_manager = PatternManager()
            import json as _json_bl
            _pc_path = os.path.join(os.path.dirname(__file__), 'pattern_config.json')
            if os.path.exists(_pc_path):
                with open(_pc_path, 'r', encoding='utf-8') as _f_bl:
                    _pc_data = _json_bl.load(_f_bl)
                    _blacklisted_patterns = set(_pc_data.get('blacklisted', []))
        except Exception:
            pass
        
        # 🔴 FIX 02/03: Utiliser PatternManager.is_pattern_allowed() pour cohérence complète
        # Vérifie blacklist + needs_review + enabled (même logique que le bot)
        def _is_pattern_allowed_by_bot(pattern_name, ai_score=60):
            """Vérifie si le bot accepterait ce pattern (cohérence IA↔Bot)"""
            if _pattern_manager:
                allowed, reason = _pattern_manager.is_pattern_allowed(pattern_name, ai_score=ai_score)
                return allowed, reason
            # Fallback: vérifier seulement la blacklist
            if pattern_name in _blacklisted_patterns:
                return False, f"Pattern '{pattern_name}' blacklisté"
            return True, "OK"
        
        # 🔴 FIX 02/03: Marquer les signaux ACHAT bloqués par PatternManager
        # Ces signaux restent visibles dans le dashboard (surveillance) avec explication
        _pattern_blocked_signals = []
        
        if _blacklisted_patterns or _pattern_manager:
            _before_smart = len(ready_from_smart)
            _before_possible = len(ready_from_possible)
            
            # Séparer les signaux autorisés et bloqués
            _smart_allowed = []
            _smart_blocked = []
            for w in ready_from_smart:
                allowed, reason = _is_pattern_allowed_by_bot(w.get('pattern', ''), ai_score=w.get('score', 60))
                if allowed:
                    _smart_allowed.append(w)
                else:
                    w['pattern_blocked'] = True
                    w['pattern_blocked_reason'] = reason
                    _smart_blocked.append(w)
            ready_from_smart = _smart_allowed
            _pattern_blocked_signals.extend(_smart_blocked)
            
            _possible_allowed = []
            _possible_blocked = []
            for w in ready_from_possible:
                allowed, reason = _is_pattern_allowed_by_bot(w.get('pattern', ''), ai_score=w.get('score', 60))
                if allowed:
                    _possible_allowed.append(w)
                else:
                    w['pattern_blocked'] = True
                    w['pattern_blocked_reason'] = reason
                    _possible_blocked.append(w)
            ready_from_possible = _possible_allowed
            _pattern_blocked_signals.extend(_possible_blocked)
            
            ready_from_status = [w for w in ready_from_status if _is_pattern_allowed_by_bot(w.get('pattern', ''), ai_score=w.get('score', 60))[0]]
            _filtered = (_before_smart - len(ready_from_smart)) + (_before_possible - len(ready_from_possible))
            if _filtered > 0:
                blocked_details = ', '.join([f"{w.get('symbol','?')}({w.get('pattern','')})" for w in _pattern_blocked_signals[:5]])
                logger.info(f"🚫 PATTERN COHERENCE: {_filtered} signaux exclus → {blocked_details}")
        
        # Fusionner les trois sources (éviter les doublons)
        ready_symbols = set()
        ready_from_analyzer = []
        
        # Priorité 1: Smart Criteria ACHAT
        for w in ready_from_smart:
            symbol = w.get('symbol', '')
            if symbol not in ready_symbols:
                ready_symbols.add(symbol)
                ready_from_analyzer.append(w)
        
        # Priorité 2: POSSIBLE — NE PAS intégrer dans ready_from_analyzer
        # is_valid_buy_signal() les rejette (exige smart_signal=='ACHAT')
        # Fix 3 (cache revalidation) gère la suppression si un signal cache passe ACHAT→POSSIBLE
        # POSSIBLE reste listable en surveillance (watching), jamais en ready
        if ready_from_possible:
            logger.info(f"👁️ {len(ready_from_possible)} signaux POSSIBLE (surveillance uniquement, pas d'achat)")
        
        # Priorité 3: Ancien système status='ready' (backup)
        # IMPORTANT: Vérifier aussi smart_signal pour éviter les incohérences
        for w in ready_from_status:
            symbol = w.get('symbol', '')
            # Si le symbole a déjà smart_criteria, vérifier qu'il est cohérent
            if w.get('smart_signal') and w.get('smart_signal') != 'ACHAT':
                logger.debug(f"⚠️ {symbol}: status='ready' mais smart_signal={w.get('smart_signal')} - IGNORÉ")
                continue
            if symbol not in ready_symbols:
                ready_symbols.add(symbol)
                ready_from_analyzer.append(w)
        
        if len(ready_from_analyzer) > 0:
            logger.info(f"🔍 SURVEILLANCE: {len(ready_from_analyzer)} signaux ACHAT détectés (smart={len(ready_from_smart)}, status={len(ready_from_status)})")
        
        ready_validated = []
        
        # ═══════════════════════════════════════════════════════════════
        # VALIDATION STRICTE UNIFIÉE - Seuil minimum absolu
        # ═══════════════════════════════════════════════════════════════
        MIN_SCORE_ABSOLUTE = 65  # 🔴 FIX v6b COHÉRENCE: 60→65 (aligné avec bot MIN_SCORE_ABSOLUTE=65)
        
        # 🔴 PATTERNS DANGEREUX - Liste centralisée pour cohérence
        # 🔴 FIX 04/02: SQUEEZE_BREAKOUT ajouté - 0% win rate sur 9 trades (toutes pertes)
        # 🔧 FIX: SQUEEZE_BREAKOUT retiré - il a sa propre validation dans smart_criteria
        DANGEROUS_PATTERNS = ['ACTIVE_CRASH', 'PRICE_CORRECTION', 'RSI_TRAP', 'STRONG_DOWNTREND', 'BEARISH_TREND', 'CREUX_TOO_DEEP', 'END_OF_CYCLE']
        
        # Valider chaque signal
        for w in ready_from_analyzer:
            symbol = w.get('symbol', '')
            score = w.get('score', 0)
            pattern = w.get('pattern', '')
            features = w.get('features', {})
            
            # 🔴 BLOCAGE 0: Pattern dangereux EN PREMIER (avant tout autre check)
            if pattern in DANGEROUS_PATTERNS:
                logger.info(f"⛔ {symbol}: Pattern DANGEREUX {pattern} - REJETÉ IMMÉDIATEMENT")
                continue
            
            # BLOCAGE 1: Score minimum absolu
            if score < MIN_SCORE_ABSOLUTE:
                logger.debug(f"⛔ {symbol}: Score {score} < {MIN_SCORE_ABSOLUTE} (seuil absolu) - REJETÉ")
                continue
            
            # BLOCAGE 2: Validation smart_signal
            is_valid = is_valid_buy_signal(w)
            if is_valid:
                ready_validated.append(w)
                if len(ready_validated) <= 10:  # Logger seulement les 10 premiers
                    logger.info(f"✅ {symbol}: Validé pour achat - Score={score} Pattern={pattern}")
            else:
                # DEBUG: Logger pourquoi le signal est rejeté (logger.info pour visibilité)
                if score < 15:
                    logger.info(f"🚫 {symbol}: Rejeté (score={score} < 15)")
                elif pattern in ['ACTIVE_CRASH', 'PRICE_CORRECTION', 'RSI_TRAP', 'STRONG_DOWNTREND', 'BEARISH_TREND', 'CREUX_TOO_DEEP', 'END_OF_CYCLE']:
                    logger.info(f"🚫 {symbol}: Rejeté (pattern dangereux={pattern} score={score})")
                elif features.get('momentum_3', 0) < -0.03 and features.get('momentum_5', 0) < -0.04:
                    logger.info(f"🚫 {symbol}: Rejeté (momentum_3={features.get('momentum_3', 0):.2%} momentum_5={features.get('momentum_5', 0):.2%})")
                elif features.get('ema_diff', 0) < -0.03 and features.get('momentum_3', 0) < -0.01:
                    logger.info(f"🚫 {symbol}: Rejeté (ema_diff={features.get('ema_diff', 0):.2%} momentum_3={features.get('momentum_3', 0):.2%})")
        
        # ══════════════════════════════════════════════════════════════
        # PERSISTANCE: Mettre à jour la cache avec les nouveaux signaux
        # ══════════════════════════════════════════════════════════════
        import time
        current_time = time.time()
        
        # Nettoyer les signaux expirés de la cache
        self._cached_ready_signals = [
            s for s in self._cached_ready_signals 
            if self._cache_expiration.get(s['symbol'], 0) > current_time
        ]
        
        # Ajouter/mettre à jour les nouveaux signaux validés
        cached_symbols = {s['symbol'] for s in self._cached_ready_signals}
        for signal in ready_validated:
            symbol = signal['symbol']
            if symbol not in cached_symbols:
                # NOUVEAU: Ajouter timestamp de détection pour vérifier l'âge du signal
                signal['detected_at'] = current_time
                self._cached_ready_signals.append(signal)
                self._cache_expiration[symbol] = current_time + self._cache_ttl
                logger.info(f"💾 CACHE: {symbol} ajouté (expire dans {self._cache_ttl//60}min {self._cache_ttl%60}s)")
            else:
                # Mettre à jour le signal existant avec les nouvelles données
                for i, s in enumerate(self._cached_ready_signals):
                    if s['symbol'] == symbol:
                        # Garder le timestamp original de première détection
                        original_detection = s.get('detected_at', current_time)
                        signal['detected_at'] = original_detection
                        self._cached_ready_signals[i] = signal
                        self._cache_expiration[symbol] = current_time + self._cache_ttl
        
        # ══════════════════════════════════════════════════════════════
        # IMPORTANT: Revalider les signaux du cache avec la watchlist actuelle
        # Supprimer du cache les symboles qui n'ont plus smart_signal='ACHAT'
        # ══════════════════════════════════════════════════════════════
        watchlist_map = {w.get('symbol'): w for w in watchlist}
        cache_to_remove = []
        
        # Patterns dangereux qui doivent invalider le cache
        # � FIX: SQUEEZE_BREAKOUT retiré - validé par smart_criteria quand momentum > 0.1%
        DANGEROUS_PATTERNS = ['ACTIVE_CRASH', 'PRICE_CORRECTION', 'RSI_TRAP', 'STRONG_DOWNTREND', 'BEARISH_TREND', 'CREUX_TOO_DEEP', 'END_OF_CYCLE']
        
        logger.debug(f"🔄 Revalidation cache: {len(self._cached_ready_signals)} signaux à vérifier")
        
        for cached_signal in self._cached_ready_signals:
            symbol = cached_signal.get('symbol', '')
            # Vérifier si le symbole existe toujours dans la watchlist
            current_state = watchlist_map.get(symbol)
            if current_state:
                # Mettre à jour le smart_signal avec l'état actuel
                current_smart_signal = current_state.get('smart_signal', '')
                current_smart_eligible = current_state.get('smart_eligible', False)
                current_pattern = current_state.get('pattern', '')
                
                # DEBUG pour MATICUSDT
                if symbol == 'MATICUSDT':
                    logger.info(f"🔍 CACHE DEBUG {symbol}: watchlist smart_signal={current_smart_signal}, eligible={current_smart_eligible}, pattern={current_pattern}")
                
                # RÉCUPÉRER LE SCORE ACTUEL de la watchlist
                current_score = current_state.get('score', 0)
                current_features = current_state.get('features', {})
                
                # 🔴 FIX 09/03 v5 → POSSIBLE est TOUJOURS retiré du cache (bloc ci-dessous)
                # is_valid_signal ne couvre donc QUE ACHAT+eligible
                # (l'ancienne clause POSSIBLE+score>=80 est maintenant du code mort)
                is_valid_signal = (
                    current_smart_signal == 'ACHAT' and current_smart_eligible
                )
                
                # 🔴 FIX 09/03 v5: Si le signal est POSSIBLE (pas ACHAT), supprimer du cache TOUJOURS
                # BUG SUI: signal POSSIBLE gardé en cache car score=70+, puis bot l'achète avec pattern cache
                # Un signal POSSIBLE = IA incertaine = PAS d'achat, quelle que soit le score
                if current_smart_signal == 'POSSIBLE':
                    cache_to_remove.append(symbol)
                    logger.info(f"🗑️ CACHE: {symbol} retiré — signal est POSSIBLE (pas ACHAT confirmé, score={current_score})")
                # Si le signal n'est plus valide (non ACHAT ou non-eligible), retirer du cache
                # 🔴 FIX 09/03 v6b: Retirer INCONDITIONNELLEMENT — la loophole score>=70
                # permettait à des signaux NO_BUY avec score élevé de rester en cache.
                elif not is_valid_signal:
                    cache_to_remove.append(symbol)
                    logger.info(f"🗑️ CACHE: {symbol} retiré — signal n'est plus ACHAT (={current_smart_signal}, score={current_score})")
                elif current_pattern in DANGEROUS_PATTERNS:
                    cache_to_remove.append(symbol)
                    logger.info(f"🗑️ CACHE: {symbol} retiré (pattern dangereux={current_pattern})")
                # 🔴 FIX 09/02: Retirer du cache si pattern non-achetable (NEUTRAL, UNKNOWN, etc.)
                elif current_pattern not in BUYABLE_PATTERNS_LIST:
                    cache_to_remove.append(symbol)
                    logger.info(f"🗑️ CACHE: {symbol} retiré (pattern non-achetable={current_pattern})")
                elif current_score < MIN_SCORE_ABSOLUTE:
                    # 🔴 NOUVEAU: Retirer si le score tombe sous le seuil
                    cache_to_remove.append(symbol)
                    logger.info(f"🗑️ CACHE: {symbol} retiré (score={current_score} < {MIN_SCORE_ABSOLUTE})")
                else:
                    # Mettre à jour TOUTES les données du cache avec l'état actuel
                    cached_signal['smart_signal'] = current_smart_signal
                    cached_signal['smart_eligible'] = current_smart_eligible
                    cached_signal['pattern'] = current_pattern
                    cached_signal['score'] = current_score  # 🔴 CRUCIAL: Mettre à jour le score!
                    cached_signal['features'] = current_features  # Mettre à jour les features aussi
        
        # Retirer les symboles invalidés
        self._cached_ready_signals = [
            s for s in self._cached_ready_signals 
            if s.get('symbol', '') not in cache_to_remove
        ]
        
        # Utiliser la cache comme source de vérité
        ready = sorted(self._cached_ready_signals, key=lambda x: x.get('score', 0), reverse=True)
        
        if len(ready) > 0:
            top_signals = ', '.join([f"{w['symbol']}={w['score']}" for w in ready[:5]])
            logger.info(f"📊 SURVEILLANCE: {len(ready)} signaux en cache prêts pour achat (top: {top_signals})")
        watching = [w for w in watchlist if w.get('smart_signal') == 'POSSIBLE']
        
        # Filtrer les top_opportunities - inclure creux ET pullbacks
        def has_valid_opportunity(w):
            features = w.get('features', {})
            ema_diff = features.get('ema_diff', 0)
            near_bb_lower = features.get('near_bb_lower', 0)
            # Creux EMA OU pullback léger OU proche BB basse
            return (ema_diff < 0) or (0 < ema_diff < 2) or near_bb_lower
        
        valid_opportunities = [w for w in watchlist if has_valid_opportunity(w)]
        
        # ══════════════════════════════════════════════════════════════
        # 🔄 ANALYSE DES ROTATIONS INTELLIGENTES
        # ══════════════════════════════════════════════════════════════
        rotation_opportunities = []
        rotation_status = None
        
        if SMART_ROTATION_AVAILABLE:
            try:
                rotation_mgr = get_smart_rotation()
                rotation_status = rotation_mgr.get_status()
                
                # Charger les positions actuelles
                positions_file = os.path.join(os.path.dirname(__file__), 'positions.json')
                if os.path.exists(positions_file):
                    with open(positions_file, 'r') as f:
                        positions = json.load(f)
                    
                    # Récupérer les VRAIS prix actuels via l'API Binance
                    current_prices = rotation_mgr.get_current_prices()
                    logger.debug(f"🔄 Prix actuels récupérés: {len(current_prices)} symboles")
                    
                    # Créer le features_map avec le prix courant RÉEL
                    features_map = {}
                    watchlist_symbols = {w.get('symbol', '') for w in watchlist}
                    
                    for w in watchlist:
                        symbol = w.get('symbol', '')
                        features = w.get('features', {}).copy()
                        
                        # Ajouter le VRAI prix courant depuis l'API
                        real_price = current_prices.get(symbol, 0)
                        if real_price > 0:
                            features['price_current'] = real_price
                        elif features and 'price_current' not in features:
                            features['price_current'] = w.get('price', 0)
                        
                        if features:
                            features_map[symbol] = features
                    
                    # Pour les positions, toujours utiliser le VRAI prix actuel
                    for symbol, pos in positions.items():
                        entry_price = pos.get('entry_price', 0)
                        real_price = current_prices.get(symbol, 0)
                        
                        if entry_price > 0 and real_price > 0:
                            profit_pct = ((real_price / entry_price) - 1) * 100
                            
                            # Mettre à jour ou créer les features avec le vrai prix
                            if symbol not in features_map:
                                features_map[symbol] = {}
                            
                            features_map[symbol].update({
                                'price_current': real_price,
                                'ema_diff': 0.02 if profit_pct > 0 else -0.02,
                                'ema_slope': 0.05 if profit_pct > 0.2 else (-0.1 if profit_pct < -0.3 else 0),
                                'ema_slope_long': 0,
                                'momentum_3': profit_pct / 5,  # Approximation
                                'momentum_5': profit_pct / 8,
                                'rsi': 50 + (profit_pct * 5),  # Approximation RSI
                                'bb_position': 0.5 + (profit_pct / 10),
                                'bb_bandwidth': 1.5
                            })
                            logger.debug(f"🔄 Features avec vrai prix pour {symbol} (P&L: {profit_pct:.2f}%)")
                    
                    logger.debug(f"🔄 Features map: {len(features_map)} symboles avec features")
                    
                    # Analyser les opportunités de rotation
                    rotation_opportunities = rotation_mgr.find_rotation_opportunities(
                        positions, 
                        watchlist, 
                        features_map
                    )
                    
                    if rotation_opportunities:
                        logger.info(f"🔄 {len(rotation_opportunities)} rotation(s) potentielle(s) détectée(s)")
                        
            except Exception as e:
                logger.debug(f"Erreur analyse rotation: {e}")
        
        # ══════════════════════════════════════════════════════════════
        # 🧠 AI OPPORTUNITY SELECTOR - Sélection intelligente via PyTorch
        # Analyse les 58 cryptos et sélectionne TOP 20 opportunités
        # ══════════════════════════════════════════════════════════════
        ai_opportunities = []
        ai_selected_symbols = []
        
        if AI_OPPORTUNITY_SELECTOR_AVAILABLE:
            try:
                opportunity_selector = get_opportunity_selector()
                
                # Vérifier si mise à jour nécessaire (toutes les 30min)
                if opportunity_selector.should_update():
                    logger.info("🧠 AI OPPORTUNITY SELECTOR: Analyse des 58 cryptos...")
                    
                    # Préparer les données pour l'analyse
                    crypto_data_list = []
                    for w in watchlist:
                        crypto_data = {
                            'symbol': w.get('symbol', ''),
                            'current_price': w.get('price', 0),
                            'ai_score': w.get('score', 0),
                            'momentum_3': w.get('features', {}).get('momentum_3', 0),
                            'momentum_5': w.get('features', {}).get('momentum_5', 0),
                            'momentum_10': w.get('features', {}).get('momentum_10', 0),
                            'ema_9': w.get('features', {}).get('ema_9', 0),
                            'ema_21': w.get('features', {}).get('ema_21', 0),
                            'ema_slope': w.get('features', {}).get('ema_slope', 0),
                            'rsi': w.get('features', {}).get('rsi', 50),
                            'bb_position': w.get('features', {}).get('bb_position', 0.5),
                            'bb_bandwidth': w.get('features', {}).get('bb_bandwidth', 0),
                            'squeeze_active': w.get('features', {}).get('bb_squeeze', 0) == 1,
                            'breakout_detected': w.get('pattern', '') in ['SQUEEZE_BREAKOUT', 'VOLUME_BREAKOUT'],
                            'technical_score': w.get('technical_score', 0),
                            'pattern_score': w.get('pattern_score', 0),
                            'confidence': w.get('confidence', 0),
                            # 🆕 FIX 10/03: Contexte de tendance structurelle (cas PHA — 20h de baisse)
                            'ema_trend_bearish': w.get('features', {}).get('ema_trend_bearish', 0),
                            'price_change_20': w.get('features', {}).get('price_change_20', 0),
                            'momentum_20': w.get('features', {}).get('momentum_20', 0),
                        }
                        crypto_data_list.append(crypto_data)
                    
                    # Analyse et sélection
                    profiles = opportunity_selector.select_opportunities(crypto_data_list)
                    
                    # Extraire TOP opportunités sélectionnées
                    ai_opportunities = [p.to_dict() for p in profiles if p.selected]
                    ai_selected_symbols = [p.symbol for p in profiles if p.selected]
                    
                    logger.info(f"✅ AI OPPORTUNITY SELECTOR: {len(ai_selected_symbols)} cryptos sélectionnées")
                    if ai_selected_symbols:
                        top_5 = ', '.join(ai_selected_symbols[:5])
                        logger.info(f"   TOP 5: {top_5}")
                else:
                    # Charger les opportunités sauvegardées
                    profiles = opportunity_selector.load_opportunities()
                    ai_opportunities = [p.to_dict() for p in profiles if p.selected]
                    ai_selected_symbols = [p.symbol for p in profiles if p.selected]
                    logger.debug(f"📂 AI Opportunities chargées: {len(ai_selected_symbols)} sélectionnées")
            
            except Exception as e:
                logger.warning(f"⚠️ Erreur AI Opportunity Selector: {e}")
        
        # DEBUG: Logger ce qui est retourné
        logger.info(f"📤 RETURN get_surveillance_status: ready_signals={len(ready)} ready_to_buy={len(ready)} ai_selected={len(ai_selected_symbols)}")
        if len(ready) > 0:
            symbols_str = ', '.join([f"{w['symbol']}={w['score']}" for w in ready[:5]])
            logger.info(f"   📤 Signaux retournés: {symbols_str}")

        
        # Classer les signaux par statut Smart pour le dashboard
        # 🔴 FIX 09/02: smart_summary.achat doit refléter les signaux RÉELLEMENT tradables
        # Avant: comptait tous les ACHAT avec pattern achetable (même si score < regime_min ou momentum en chute)
        # Maintenant: utilise `ready` (signaux validés par is_valid_buy_signal + cache + regime_min_score)
        # Cela synchronise le dashboard avec ce que le trading_bot achètera VRAIMENT
        smart_achat_validated = ready  # 🔴 Signaux RÉELLEMENT tradables (passent TOUS les filtres)
        smart_possible = [w for w in watchlist if w.get('smart_signal') == 'POSSIBLE']
        # 🔴 FIX 02/03: Inclure les signaux ACHAT bloqués par pattern dans la surveillance
        # Pour qu'ils apparaissent dans le dashboard avec explication au lieu de disparaître
        smart_possible = _pattern_blocked_signals + smart_possible
        smart_vente = [w for w in watchlist if w.get('smart_signal') in ['VENTE', 'EN_POSITION']]
        # 🔧 FIX 01/04 v2: Compléter EN ATTENTE avec des coins qui ont une chance RÉELLE de trigger
        # Avant: tous les coins score >= 55 + status=watching → bearish coins toujours affichés
        # Maintenant: exiger momentum positif OU ema_slope positive (preuve de rapprochement)
        # Un coin 100% bearish (EMA↓, momentum↓) ne doit PAS apparaître "en attente"
        vente_syms = {w.get('symbol') for w in smart_vente}
        achat_syms = {w.get('symbol') for w in smart_achat_validated}
        possible_syms = {w.get('symbol') for w in smart_possible}
        watching_candidates = sorted(
            [w for w in watchlist
             if w.get('status') == 'watching'
             and w.get('score', 0) >= 55
             and w.get('symbol') not in vente_syms
             and w.get('symbol') not in achat_syms
             and w.get('symbol') not in possible_syms
             and (  # Preuve CONCRÈTE de rapprochement : momentum ET ema_slope positifs ensemble
                 # ET l'EMA ne doit pas être fortement baissière (ema_diff >= -0.05 ou pattern explicite)
                 (w.get('features', {}).get('momentum_3', 0) > 0
                  and w.get('features', {}).get('ema_slope', 0) > 0
                  and w.get('features', {}).get('ema_diff', -1) >= -0.05)
                 or w.get('pattern', 'NEUTRAL') not in ('NEUTRAL', 'HOLD', '')
             )],
            key=lambda x: x.get('score', 0), reverse=True
        )
        # Compléter smart_possible jusqu'à 8 items
        remaining_slots = 8 - len(smart_possible)
        if remaining_slots > 0:
            smart_possible = smart_possible + watching_candidates[:remaining_slots]
        smart_abandonnee = [w for w in watchlist if w.get('smart_signal') == 'ABANDONNEE']
        eligible_values = [w for w in watchlist if w.get('smart_eligible') == True]
        
        # === CALCULER SL/TP DYNAMIQUES POUR LES SIGNAUX ===
        if DYNAMIC_SLTP_AVAILABLE:
            try:
                market_regime = "NEUTRAL"  # TODO: Intégrer MarketRegimeDetector
                for signal in ready:
                    symbol = signal['symbol']
                    
                    # Enrichir le signal avec les données nécessaires pour le calcul SL/TP
                    # 1. Récupérer volatility_score depuis le cache du predictor
                    volatility_score = 50.0  # Défaut
                    if symbol in self.predictor.watchlist:
                        cached_item = self.predictor.watchlist[symbol]
                        # WatchlistItem a l'attribut .features (dict)
                        volatility_score = cached_item.features.get('volatility_score', 50.0)
                        signal['volatility_score'] = volatility_score
                        
                        # 2. Ajouter ATR percent si disponible
                        if 'atr_percent' in cached_item.features:
                            signal['atr_percent'] = cached_item.features['atr_percent']
                        
                        # 3. Ajouter prédiction volatilité IA si disponible
                        if 'ai_opportunity' in cached_item.features:
                            ai_opp = cached_item.features['ai_opportunity']
                            if 'predicted_volatility_6h' in ai_opp:
                                signal['predicted_volatility_6h'] = ai_opp['predicted_volatility_6h']
                    
                    # Calculer SL/TP optimaux basés sur volatilité et IA
                    sl_pct, tp_pct = calculate_optimal_sltp(
                        symbol=symbol,
                        crypto_data=signal,
                        market_regime=market_regime
                    )
                    signal['dynamic_sl'] = sl_pct
                    signal['dynamic_tp'] = tp_pct
                    signal['dynamic_rr'] = round(tp_pct / sl_pct, 2)
                logger.debug(f"✅ SL/TP dynamiques calculés pour {len(ready)} signaux")
            except Exception as e:
                logger.warning(f"⚠️ Erreur calcul SL/TP dynamiques: {e}")
        
        # Calculer total_symbols: utiliser symbols_to_watch (univers réel de surveillance)
        # Si dynamique (top marché), symbols_to_watch > watchlist.json → toujours plus précis
        total_symbols = len(self.symbols_to_watch) if self.symbols_to_watch else 0
        if total_symbols == 0:
            try:
                import os
                watchlist_path = os.path.join(os.path.dirname(__file__), 'watchlist.json')
                if os.path.exists(watchlist_path):
                    with open(watchlist_path, 'r', encoding='utf-8') as f:
                        import json
                        watchlist_data = json.load(f)
                        manual = watchlist_data.get('symbols', [])
                        auto_added = list(watchlist_data.get('auto_added', {}).keys())
                        spy_inj = list(watchlist_data.get('spy_injected', {}).keys())
                        total_symbols = len(set(manual + auto_added + spy_inj))
            except Exception:
                total_symbols = 0
        
        # Déterminer is_running: vérifier bot.pid OU données récentes (< 10 min)
        is_running_status = self.is_running
        ai_available = False
        try:
            import os
            from datetime import datetime
            script_dir = os.path.dirname(__file__)
            
            # Vérifier bot.pid
            bot_pid_file = os.path.join(script_dir, 'bot.pid')
            if os.path.exists(bot_pid_file):
                is_running_status = True
            
            # Vérifier fraîcheur des données - UTILISER ia_surveillance_cache.json au lieu de bot_analysis.json
            cache_file = os.path.join(script_dir, 'ia_surveillance_cache.json')
            if os.path.exists(cache_file):
                mtime = os.path.getmtime(cache_file)
                age_seconds = (datetime.now().timestamp() - mtime)
                if age_seconds < 600:  # Données < 10 minutes
                    ai_available = True
                    if age_seconds < 120:  # Données < 2 minutes = très actif
                        is_running_status = True
                        
            # Fallback: si des signaux ready existent, l'IA est disponible
            if len(ready) > 0 or len(watchlist) > 0:
                ai_available = True
        except Exception as e:
            logger.debug(f"Erreur vérification is_running: {e}")
        
        return {
            'is_running': is_running_status,
            'ai_available': ai_available,
            'total_symbols': total_symbols,
            # analyzed = nombre réel de symboles traités lors du dernier cycle
            'analyzed': self.last_smart_summary.get('analyzed', total_symbols) if hasattr(self, 'last_smart_summary') and self.last_smart_summary else total_symbols,
            'ready_to_buy': len(ready),
            'watching': len(watching),
            'top_opportunities': valid_opportunities[:10],
            'ready_signals': ready,
            'rotation_opportunities': rotation_opportunities[:5],
            'rotation_status': rotation_status,
            # AI Opportunity Selector (PyTorch)
            'ai_opportunities': ai_opportunities,
            'ai_selected_symbols': ai_selected_symbols,
            'ai_opportunity_count': len(ai_selected_symbols),
            # Dynamic SL/TP
            'dynamic_sltp_enabled': DYNAMIC_SLTP_AVAILABLE,
            # Dashboard summary
            'smart_summary': {
                'achat': len(smart_achat_validated),
                'possible': len(smart_possible),
                'vente': len(smart_vente),
                'abandonnee': len(smart_abandonnee),
                # 🔧 FIX: 'eligible' = ready_signals (tradables), pas eligible_values (smart_eligible=True)
                'eligible': len(ready),
                'analyzed': len(self.symbols_to_watch)
            },
            'by_status': {
                'achat': smart_achat_validated[:5],
                # Limiter la surveillance aux 5 meilleurs en marché BEAR
                'en_surveillance': smart_possible[:5] if len(smart_possible) > 5 else smart_possible,
                'vente': smart_vente[:25],
                'abandonnee': smart_abandonnee[:5]
            }
        }


# Instance globale
ai_predictor = AIPredictor()
surveillance_service = SurveillanceService(ai_predictor)


def get_ai_predictor() -> AIPredictor:
    """Retourne l'instance globale du prédicteur"""
    return ai_predictor


def get_surveillance_service() -> SurveillanceService:
    """Retourne l'instance globale du service de surveillance"""
    return surveillance_service


def setup_klines_fetcher(fetcher):
    """Configure le fetcher de klines sur l'instance globale"""
    ai_predictor.set_klines_fetcher(fetcher)
    surveillance_service.set_klines_fetcher(fetcher)


if __name__ == "__main__":
    # Test
    import requests
    
    print("🧪 Test du module AI Predictor")
    
    # Récupérer des données de test
    url = "https://api.binance.com/api/v3/klines"
    params = {"symbol": "BTCUSDT", "interval": "5m", "limit": 100}
    r = requests.get(url, params=params)
    klines = r.json()
    
    prices = [float(k[4]) for k in klines]
    volumes = [float(k[5]) for k in klines]
    
    # Analyser
    predictor = AIPredictor()
    item = predictor.analyze_symbol("BTCUSDT", prices, volumes)
    
    print(f"\n📊 Analyse BTCUSDT:")
    print(f"   Score: {item.score}/100")
    print(f"   Pattern: {item.pattern}")
    print(f"   Status: {item.status}")
    print(f"   Gain prédit: {item.predicted_gain:.2f}%")
    print(f"   Countdown: ~{item.countdown} min")
    print(f"   Raison: {item.reason}")
