#!/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é
"""

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 LA BLACKLIST DYNAMIQUE ===
DYNAMIC_BLACKLIST_AVAILABLE = False
try:
    from dynamic_blacklist import get_blacklist_manager, is_symbol_blacklisted, update_dynamic_blacklist
    DYNAMIC_BLACKLIST_AVAILABLE = True
    logger.info("✅ Dynamic Blacklist disponible")
except ImportError as e:
    logger.warning(f"⚠️ Dynamic Blacklist 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
    
    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 ===
        ema9 = PatternFeatures._ema(prices, 9)
        ema21 = PatternFeatures._ema(prices, 21)
        ema50 = PatternFeatures._ema(prices, 50) if len(prices) >= 50 else ema21
        
        features['ema9'] = ema9
        features['ema21'] = ema21
        features['ema_diff'] = (ema9 - ema21) / ema21 * 100  # Écart EMA en %
        features['ema_slope'] = PatternFeatures._ema_slope(prices, 9)  # Pente EMA9 court terme (5 périodes)
        features['ema_slope_long'] = PatternFeatures._ema_slope(prices, 9, lookback=15)  # Pente EMA9 moyen terme
        features['ema21_slope'] = PatternFeatures._ema_slope(prices, 21, lookback=10)  # Pente EMA21
        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 EMA9 VIENT DE croiser EMA21 par le haut
        if len(prices) >= 22:
            # Calculer EMA9 et EMA21 pour les 3 dernières bougies
            ema9_prev2 = PatternFeatures._ema(prices[:-2], 9)
            ema21_prev2 = PatternFeatures._ema(prices[:-2], 21)
            ema9_prev1 = PatternFeatures._ema(prices[:-1], 9)
            ema21_prev1 = PatternFeatures._ema(prices[:-1], 21)
            
            # 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
            
            # EMA9 commence à s'aplatir ou baisser après une hausse
            ema9_prev = PatternFeatures._ema(prices[:-1], 9)
            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 EMA9 se rapproche d'EMA21
        # Plus ça converge vite, plus le croisement est imminent
        features['ema_convergence_speed'] = 0
        if len(prices) >= 25:
            ema_diff_current = features['ema_diff']  # en %
            ema9_prev3 = PatternFeatures._ema(prices[:-3], 9)
            ema21_prev3 = PatternFeatures._ema(prices[:-3], 21)
            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) >= 25:
            for lookback in range(1, min(15, len(prices) - 21)):
                ema9_lb = PatternFeatures._ema(prices[:-lookback], 9) if lookback > 0 else ema9
                ema21_lb = PatternFeatures._ema(prices[:-lookback], 21) if lookback > 0 else ema21
                ema9_lb_prev = PatternFeatures._ema(prices[:-(lookback+1)], 9) if lookback + 1 < len(prices) - 9 else ema9_lb
                ema21_lb_prev = PatternFeatures._ema(prices[:-(lookback+1)], 21) if lookback + 1 < len(prices) - 21 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: créer le modèle puis charger les poids
                        self.model = self._create_model()
                        self.model.load_state_dict(loaded['model_state_dict'])
                        logger.info(f"✅ Modèle PyTorch chargé (state_dict): {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
                
                # ══════════════════════════════════════════════════════════════════════
                # 🤖 AJUSTEMENT AUTOMATIQUE SELON PERFORMANCES RÉCENTES
                # FIX 29/01: L'IA ajuste les scores selon win rate récent
                # Les cryptos avec mauvaises perfs sont pénalisées mais réintégrables
                # ══════════════════════════════════════════════════════════════════════
                try:
                    from ai_score_adjuster import get_score_adjuster
                    adjuster = get_score_adjuster()
                    base_score = item.score
                    item.score = adjuster.adjust_score(symbol, item.score)
                    if item.score != base_score:
                        item.features['score_adjusted'] = True
                        item.features['score_penalty'] = item.score - base_score
                except Exception as e:
                    logger.debug(f"Ajusteur score désactivé: {e}")
                
                # ══════════════════════════════════════════════════════════════════════
                # 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'
                
                # ══════════════════════════════════════════════════════════════════════
                # 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}")
                
                # ══════════════════════════════════════════════════════════════════════
                # ANALYSE DES STRATÉGIES D'ACHAT - VERSION AMÉLIORÉE
                # ══════════════════════════════════════════════════════════════════════
                ema_trend_bearish = item.features.get('ema_trend_bearish', 0)
                ema_trend_bullish = item.features.get('ema_trend_bullish', 0)
                momentum_3 = item.features.get('momentum_3', 0)
                momentum_5 = item.features.get('momentum_5', 0)
                ema_slope = item.features.get('ema_slope', 0)
                ema_slope_long = item.features.get('ema_slope_long', 0)
                ema21_slope = item.features.get('ema21_slope', 0)
                price_correcting = item.features.get('price_correcting', 0)
                ema_diff = item.features.get('ema_diff', 0)
                rsi = item.features.get('rsi', 50)
                near_bb_lower = item.features.get('bb_position', 0.5) < 0.25
                
                # ══════════════════════════════════════════════════════════════════════
                # DÉTECTION CRASH GLOBAL (BTC uniquement) - ASSOUPLI 02/01
                # Mode crash seulement pour les GROS crashs (-3% sur 5 bougies)
                # Petites fluctuations normales (-0.5% à -1%) = ne pas bloquer tout!
                # ══════════════════════════════════════════════════════════════════════
                if symbol == 'BTCUSDT':
                    AIPredictor._btc_momentum = momentum_5
                    AIPredictor._last_market_check = datetime.now()
                    
                    # CRASH = baisse brutale COHÉRENTE (les deux momentum doivent être négatifs)
                    # 🔧 FIX 07/02: Seuils adaptatifs - en BULL_STRONG, tolérer plus de volatilité
                    _crash_regime = self._get_market_regime()
                    _crash_m5 = {'BULL_STRONG': -0.05, 'BULL_WEAK': -0.04, 'NEUTRAL': -0.03, 'BEAR': -0.02}.get(_crash_regime, -0.03)
                    _crash_m3 = {'BULL_STRONG': -0.03, 'BULL_WEAK': -0.025, 'NEUTRAL': -0.02, 'BEAR': -0.015}.get(_crash_regime, -0.02)
                    is_btc_crashing = (momentum_5 < _crash_m5 and momentum_3 < _crash_m3)
                    # REPRISE = momentum positif ou stabilisation
                    is_btc_recovering = (momentum_3 > 0 or momentum_5 > 0 or (momentum_3 > -0.5 and momentum_5 > -1.0))
                    
                    if is_btc_crashing:
                        if not AIPredictor._market_crash_mode:
                            # CORRIGÉ: momentum déjà en %
                            logger.warning(f"🚨 ALERTE 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 MARCHÉ - BTC en reprise: Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}% - Achats DÉBLOQUÉS!")
                        AIPredictor._market_crash_mode = False
                    else:
                        # Zone grise : ni crash ni reprise claire
                        logger.info(f"📊 BTC zone neutre: Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}% - Mode crash={AIPredictor._market_crash_mode}")
                
                # Features Bollinger Squeeze
                bb_squeeze = item.features.get('bb_squeeze', 0)
                bb_squeeze_confirmed = item.features.get('bb_squeeze_confirmed', 0)
                squeeze_duration = item.features.get('squeeze_duration', 0)
                bb_bandwidth = item.features.get('bb_bandwidth', 10)
                bb_position = item.features.get('bb_position', 0.5)
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE PRIORITAIRE: BOLLINGER SQUEEZE + CROISEMENT EMA
                # CONDITIONS:
                # 1. BB resserrées (squeeze) = volatilité faible, explosion imminente
                # 2. EMA9 proche ou au-dessus d'EMA21 (pas de tendance baissière forte)
                # 3. Prix au-dessus de la BB moyenne (breakout VERS LE HAUT!)
                # 4. Momentum positif confirmé
                # ══════════════════════════════════════════════════════════════════════
                
                # EMA9 doit être proche d'EMA21 ou au-dessus (pas de baisse forte)
                # Seuil: EMA_diff > DEATH_CROSS_MODERATE (assoupli pour capter les croisements)
                is_ema_favorable = (ema_diff >= -SQUEEZE_FAVORABLE)
                
                # Zone de croisement EMA (pour compatibilité)
                is_ema_cross_zone = (ema_diff >= -SQUEEZE_CROSS_ZONE and ema_diff <= SQUEEZE_CROSS_ZONE)
                
                # Squeeze Bollinger actif
                is_squeeze_active = (
                    bb_squeeze == 1 or 
                    bb_squeeze_confirmed == 1 or 
                    bb_bandwidth < 4.0 or
                    squeeze_duration >= 2
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # ★★★ STRATÉGIE PRIORITAIRE #0: EARLY_BREAKOUT ★★★
                # ACHETER AU DÉBUT DU MOUVEMENT, PAS À LA FIN!
                # 
                # Conditions (basées sur exemple utilisateur 15h04):
                # 1. EMA9 VIENT DE croiser EMA21 par le haut (croisement récent!)
                # 2. Prix passe au-dessus de KC mid (Keltner Channel)
                # 3. KC upper > BB upper (expansion de volatilité = mouvement fort)
                # 4. Momentum positif (le mouvement démarre)
                # 5. RSI pas en surachat (< 65)
                # ══════════════════════════════════════════════════════════════════════
                ema_cross_bullish = item.features.get('ema_cross_bullish', 0)
                ema_cross_fresh = item.features.get('ema_cross_fresh', 0)
                price_above_kc_mid = item.features.get('price_above_kc_mid', 0)
                kc_above_bb = item.features.get('kc_above_bb', 0)
                
                # � FIX AUDIT 28/02: Pré-calculer vol_ratio ici (était utilisé à L1278 avant d'être défini à L1417)
                vol_ratio = volumes[-1] / (np.mean(volumes[-20:]) + 1e-10) if (volumes is not None and len(volumes) >= 20) else 1.0
                
                # �🔴 FIX 23/01: EARLY_BREAKOUT QUASI-BLOQUÉ - Win rate 0%, -1.36% avg P&L
                # Analyse 31 trades: 6 trades, 0 wins, 6 losses, 50% stop-loss
                # Problème: Achète APRÈS remontée (trop tard) vs CREUX_REBOUND (achète AU creux)
                # Solution NOUVELLE: Momentum 0.3% au lieu de 1.0% + RSI 45-72 au lieu de 60-68
                is_early_breakout = (
                    ema_cross_bullish == 1 and          # EMA9 vient de croiser EMA21 par le haut
                    price_above_kc_mid == 1 and         # Prix au-dessus de KC mid
                    momentum_3 > 0.3 and              # 🔴 Momentum > 0.3% (assoupli pour meilleur timing)
                    momentum_5 > -0.1 and             # Mom5 quasi-positif (-0.1% toléré)
                    rsi > 45 and rsi < 72 and           # 🔴 RSI 45-72 élargi
                    ema_slope > 0.5 and               # EMA9 monte (assoupli >0.5%)
                    ema_diff > 0.2                    # 🔴 EMA diff > 0.2% (assoupli détection précoce)
                )
                
                # Bonus si croisement très frais (1 bougie) + KC > BB + volume fort
                is_early_breakout_strong = is_early_breakout and (
                    ema_cross_fresh == 1 and            # Croisement très récent ET
                    kc_above_bb == 1 and                # KC > BB = forte expansion ET
                    vol_ratio > 1.5                     # Volume > 150% (confirmation)
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # ZONE OPTIMALE D'ACHAT: Entre la moyenne BB et 75% de la BB
                # bb_position > 0.45 = au-dessus de la moyenne (force)
                # bb_position < 0.75 = pas trop haut
                is_price_in_buy_zone = (bb_position > 0.45 and bb_position < 0.75)
                
                # CONDITION CRITIQUE: Le momentum doit être STRICTEMENT POSITIF
                momentum_shows_reversal = (momentum_3 > MOMENTUM_REVERSAL_MIN and momentum_3 < MOMENTUM_REVERSAL_MAX)  # Entre 0.10% et 0.80%
                not_crashing = (momentum_5 > -MOMENTUM_DROP_MODERATE)  # Plus strict: pas en chute lente (-0.3%)
                
                # SQUEEZE BREAKOUT VALIDE - Zone optimale d'achat
                is_squeeze_breakout = (
                    is_squeeze_active and
                    is_ema_favorable and          # EMA pas bearish
                    is_price_in_buy_zone and      # Prix dans la zone optimale
                    momentum_shows_reversal and   # Momentum positif confirmé (0.10-0.50%)
                    not_crashing and
                    rsi > 30 and rsi < 65         # 🔧 RSI max 65 (était 60 - trop restrictif en bull)
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE CREUX IMMÉDIAT: Prix chute rapidement vers BB inférieure
                # Opportunité d'achat sur repli brutal en tendance haussière globale
                # ══════════════════════════════════════════════════════════════════════
                
                # Chute rapide avec approche de la BB inférieure
                is_approaching_bb_lower = (bb_position < 0.40)  # Élargi de 0.35 à 0.40
                is_fast_drop = (momentum_3 < -MOMENTUM_REVERSAL_MIN or momentum_5 < -MOMENTUM_DROP_LIGHT)  # -0.10% ou -0.20%
                ema_not_bearish = (ema_diff >= -DEATH_CROSS_LIGHT)  # Accepter EMA neutre (pas strictement bullish)
                rsi_not_extreme = (rsi > 20 and rsi < 75)
                
                # CREUX IMMÉDIAT: Prix bas + momentum commence à remonter
                # Accepter EMA neutre car c'est justement le point de retournement
                momentum_rebounding = (momentum_3 >= MOMENTUM_REVERSAL_MIN or (momentum_3 > -0.03 and ema_slope > 0))  # 0.02% OU momentum quasi-nul + EMA↑
                is_immediate_dip = (
                    is_approaching_bb_lower and
                    ema_not_bearish and  # Pas bearish (>= -0.05%)
                    rsi_not_extreme and
                    bb_bandwidth < 5 and
                    momentum_rebounding  # CRITIQUE: le prix doit remonter!
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE CORRECTION_BUY: Prix descend depuis un récent pic
                # Acheter sur la correction dans une tendance globalement haussière
                # ══════════════════════════════════════════════════════════════════════
                
                # Correction récente (momentum négatif court terme mais tendance haussière long terme)
                is_short_correction = (momentum_3 < 0 and momentum_5 > -MOMENTUM_DROP_MODERATE)  # -0.3%
                still_bullish_trend = (ema21_slope >= 0 or ema_slope_long > 0 or ema_trend_bullish == 1)  # Assoupli
                price_not_extreme = (bb_position > 0.25 and bb_position < 0.75)  # Milieu des BB
                rsi_pullback = (rsi > 40 and rsi < 70)  # RSI en zone achetable
                
                # CORRECTION_BUY = DÉSACTIVÉ - trop risqué d'acheter pendant une correction
                # Cette stratégie achète pendant que le prix baisse, ce qui mène à des pertes
                is_correction_buy = False  # DÉSACTIVÉ - momentum négatif = danger!
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE 2: CREUX EMA (EMA9 légèrement < EMA21 avec retournement)
                # CONDITIONS STRICTES pour éviter les faux signaux:
                # - EMA9 doit être légèrement sous EMA21 (-0.3% à -1.5% max)
                # - Si EMA9 est TROP loin de EMA21 (< -1.5%), on attend le croisement
                # - Le momentum doit montrer un RETOURNEMENT réel (momentum positif)
                # - L'EMA9 doit commencer à remonter (ema_slope > 0)
                # - RSI survendu avec divergence haussière
                # ══════════════════════════════════════════════════════════════════════
                
                # Zone de creux valide: entre -0.3% et -1.5% d'écart EMA
                is_creux_zone = (ema_diff >= -1.5 and ema_diff < -0.3)
                
                # Creux trop profond: EMA9 trop loin d'EMA21, risque élevé
                is_creux_too_deep = ema_diff < -1.5
                
                # Confirmation de retournement: momentum doit être légèrement positif OU EMA remonte
                has_rebound_confirmation = (
                    momentum_3 > 0.01 or             # Momentum > 0.01% (TRÈS précoce) OU
                    (momentum_3 > -0.05 and ema_slope > 0)  # Momentum quasi-nul + EMA remonte
                )
                
                # Confirmation supplémentaire avec RSI
                rsi_rebound = (rsi > 25 and rsi < 47)  # 🔴 FIX 04/03: RSI sort de survente (was 55 — trop large)
                
                # CREUX VALIDE: zone correcte + retournement confirmé
                is_creux_rebound = is_creux_zone and has_rebound_confirmation and rsi_rebound
                
                # CREUX EN CHUTE: toujours en baisse, ne pas acheter
                is_creux_falling = (ema_diff < -0.3) and (
                    momentum_3 < 0 or                  # Prix descend encore
                    (momentum_3 < MOMENTUM_REVERSAL_MIN and ema_slope < 0)  # Faible rebond et EMA baisse
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE 3: PULLBACK (EMA9 > EMA21 avec momentum haussier)
                # � FIX 13/03: ALIGNÉ avec smart_criteria (was: thresholds laxistes)
                # PULLBACK = repli dans UPTREND ACTIF — EMA doit monter, momentum positif requis
                # ══════════════════════════════════════════════════════════════════════
                is_pullback = (ema_diff >= PULLBACK_MIN_GAP and ema_diff < 1.5)  # EMA9 au-dessus
                is_pullback_valid = is_pullback and (
                    ema_slope > 0.0 and               # 🔧 FIX 13/03: EMA9 doit MONTER (était >0.5, puis -0.05)
                    ema21_slope >= -0.02 and           # 🔧 FIX 13/03: EMA21 stable/haussière
                    momentum_3 >= 0.0 and             # 🔧 FIX 13/03: Momentum POSITIF requis (était -0.01%)
                    momentum_3 < MOMENTUM_REVERSAL_MAX and   # Momentum max 0.80%
                    momentum_5 > -0.10 and            # 🔧 FIX 13/03: Tendance 5 bougies quasi-neutre (était -0.02%)
                    rsi > 38 and rsi < 70 and          # RSI 38-70
                    ema_trend_bearish == 0             # INTERDIT en tendance bearish!
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE 4: CROSSOVER IMMINENT - Détecter le croisement EMA AVANT qu'il soit trop tard!
                # Accepter momentum encore légèrement négatif car c'est le point de retournement
                # 🔧 FIX 09/02: Fenêtre élargie -0.35% (was -0.25%) + conditions assouplies (cas XRP 18:32)
                # ══════════════════════════════════════════════════════════════════════
                is_crossover_imminent = (
                    ema_diff > -0.35 and               # 🔧 FIX: -0.35% (was -0.25%) = détecter plus tôt
                    ema_diff <= 0.15 and               # 🔧 FIX: +0.15% (was +0.10%) = capter juste APRÈS
                    ema_slope > 0.2 and              # 🔧 FIX: Assoupli 0.2% (was 0.3%) = détecter pentes douces
                    momentum_3 > -0.8 and            # 🔧 FIX: Assoupli -0.80% (was -0.50%)
                    momentum_5 > -MOMENTUM_DROP_SIGNIFICANT and  # Momentum 5 pas en crash (-0.80%)
                    bb_position > 0.30 and             # 🔧 FIX: Assoupli 30% (was 35%)
                    bb_position < 0.85 and             # Prix pas trop haut (assoupli)
                    rsi > 28 and rsi < 72              # 🔧 FIX: Élargi 28-72 (was 30-70)
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE 5: VOLUME REVERSAL - DÉSACTIVÉ (trop risqué)
                # Acheter près de BB basse est souvent un piège (RSI trap)
                # ══════════════════════════════════════════════════════════════════════
                vol_ratio = volumes[-1] / (np.mean(volumes[-20:]) + 1e-10) if (volumes is not None and len(volumes) >= 20) else 1.0
                # VOLUME REVERSAL: Exiger que le prix soit DÉJÀ remonté au-dessus de la moyenne BB
                is_volume_reversal = (
                    bb_position > 0.45 and             # Prix AU-DESSUS de la moyenne BB
                    bb_position < 0.75 and             # Mais pas trop haut
                    vol_ratio > 1.5 and                # Volume +50%
                    momentum_3 > MOMENTUM_REVERSAL_MIN and  # Momentum min 0.10%
                    momentum_3 < MOMENTUM_REVERSAL_MAX and  # Momentum max 0.80%
                    ema_diff > -DEATH_CROSS_LIGHT and  # EMA pas bearish
                    rsi > 40 and rsi < 60              # RSI max 60
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE 5B: RSI REVERSAL - Détection PRÉCOCE de retournement
                # CREUX + RSI SURVENTE + DÉBUT DE REBOND = Opportunité AVANT le croisement EMA
                # ══════════════════════════════════════════════════════════════════════
                is_rsi_reversal = (
                    rsi < 35 and                       # RSI en survente (< 35)
                    bb_position < 0.35 and             # Prix très bas (< 35% BB)
                    momentum_3 > -0.5 and            # Momentum pas en crash (-0.50% max)
                    momentum_5 > -0.8 and            # Mom5 pas en crash fort (-0.80% max)
                    (momentum_3 > momentum_5 or        # Momentum s'améliore OU
                     ema_slope > -0.01) and            # EMA arrête de baisser
                    vol_ratio > 0.8                    # Volume pas mort (> 80% moy)
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # 🔴 FIX 21/01 - STRATÉGIE 5C: CREUX_REBOUND_EARLY (ACHAT AU CREUX)
                # Problème: EARLY_BREAKOUT achète APRÈS remontée (trop tard)
                # Solution: Détecter le CREUX + début rebond AVANT la remontée complète
                # Exemple: SOPH 11h06 RSI=20 → +1.7% en 37min, WIFUSDT 17h36
                # 🔴 FIX 24/01: RSI 15-35 (zone rebond), Momentum >0.05% (BCH 14h30)
                # ══════════════════════════════════════════════════════════════════════
                # 🔴 SUPPRIMÉ: Calcul price_change_recent (nécessite dataframe non disponible ici)
                # La validation du rebond se fait uniquement avec RSI + momentum
                
                # 🔴 AMÉLIORATION 25/01: Détecter période CALME avant le rebond (comme exemple LTC)
                # Phase 1 (rectangle noir): Consolidation, faible volume, BB étroites
                # Phase 2 (rectangle rouge): Rebond avec volume, momentum positif → ACHAT!
                # Phase 3 (rectangle blanc): Suivi montée puis vente sur retournement
                
                # Détecter CONSOLIDATION (période calme propice au rebond)
                is_consolidating = (
                    bb_bandwidth < 5.0 or              # BB resserrées (consolidation)
                    (bb_squeeze == 1 or bb_squeeze_confirmed == 1)  # Squeeze actif
                )
                
                # Volume CALME puis REPRISE — doit monter significativement pour confirmer un vrai retournement
                # 🔧 FIX 21/03: vol_ratio > 0.7 était trop permissif (dead cat bounce sans volume)
                # Rétro: le vrai creux à 11:20 avait vol >> moyen. Exiger vol_ratio >= 1.3 (était > 0.7)
                volume_awakening = (
                    vol_ratio >= 1.3                   # Volume doit vraiment revenir (rebond réel)
                )
                
                # 🆕 FIX 01/03: Extraire momentum_acceleration pour validation des retournements
                # Si mom_accel > 0 = momentum s'améliore (passage de négatif vers positif)
                mom_accel = item.features.get('momentum_acceleration', 0) if hasattr(item, 'features') and item.features else 0
                
                # 🔧 FIX 04/02 v2 + FIX 01/03 + 🧠 FIX LSTM: CREUX_REBOUND_EARLY
                # Problème v1: conditions trop strictes → achat tardif
                # FIX 01/03: Exiger accélération positive pour confirmer VRAI retournement
                # 🧠 FIX LSTM: Le BiLSTM+Attention détecte les retournements AVANT que le
                # momentum ne devienne positif. Quand LSTM dit REVERSAL_UP avec haute confiance,
                # accepter momentum légèrement négatif (le rebond vient de commencer)
                _lstm_ft = item.features if hasattr(item, 'features') and item.features else {}
                _lstm_confirms_reversal = (
                    _lstm_ft.get('lstm_is_reversal_signal', False) and
                    _lstm_ft.get('lstm_reversal_confidence', 0) >= 70 and
                    _lstm_ft.get('lstm_reversal_prob', 0) >= 0.45
                )
                
                # 🔴 FIX URGENT 09/03: CRITÈRES DURCIS — empêcher les achats sur micro-rebonds
                # PROBLÈME: momentum_3 > 0.01 = 0.01% = quasiment zéro → achat sur 1 candle haussière
                # durant une tendance baissière. Ex: FIL 07:52 bought at -2% from peak.

                # 🆕 FIX 09/03: Direction BTC — ne pas acheter en CREUX si BTC baisse > 0.3%
                # _btc_momentum est en DECIMAL: -0.003 = -0.3%
                _btc_dir_creux = getattr(AIPredictor, '_btc_momentum', 0)
                _btc_allows_creux = (symbol == 'BTCUSDT' or _btc_dir_creux >= -0.003)

                # 🆕 FIX 09/03 v2: POST-PUMP EXHAUSTION sur données 5min (prices = bougies 5min)
                # Cas KNC: pic à 11:20 dans la bougie 1h → invisible dans 1h close → check échoue
                # Solution: vérifier directement si le prix a chuté depuis son pic des 90 dernières min (18 bougies)
                _p90m = prices[-24:] if len(prices) >= 24 else prices  # 🔴 DURCI 10/03: 90→120min (18→24 bougies 5min, cas COS: pic à 11:25 invisible en 90min)
                _peak_90m = max(_p90m)
                _drop_from_peak_90m = (prices[-1] - _peak_90m) / _peak_90m * 100 if _peak_90m > 0 else 0
                # Confirmer presence d'un pump préalable (pas juste une chute sans hausse)
                _p_prior = prices[-30:-6] if len(prices) >= 30 else prices[:max(1, len(prices) - 6)]
                _prior_low_90m = min(_p_prior) if len(_p_prior) > 0 else prices[0]
                _pump_before_peak = (_peak_90m - _prior_low_90m) / _prior_low_90m * 100 if _prior_low_90m > 0 else 0
                # 🆕 FIX 09/03 v4: DISCRIMINATEUR "AU FOND" — si prix proche du minimum des 90 dernières min,
                # c'est un VRAI creux (retournement), PAS un essoufflement post-pump.
                # Ex BTC 14h30: prix ≈ _bottom_90m → _is_at_bottom=True → pas bloqué ✓
                # Ex AUDIO 13h38: prix ≠ bottom (descend depuis pic récent) → still blocked ✓
                _bottom_90m = min(_p90m)
                _is_at_bottom = (prices[-1] - _bottom_90m) / max(_bottom_90m, 1e-10) * 100 < 0.3
                _is_post_pump_5m = (
                    _drop_from_peak_90m < -0.7 and   # 🔴 DURCI 10/03: -0.9→-0.7% depuis pic 120min (cas COS: -0.6% non détecté)
                    _pump_before_peak > 0.7 and      # Vrai pump > 0.7% avant pic (pas une chute simple)
                    momentum_5 < 0 and               # Toujours en descente (5 bougies = 25min)
                    not _is_at_bottom                # 🆕 v4: PAS si au fond du creux (vrai retournement)
                )

                # ══════════════════════════════════════════════════════════════
                # 🆕 FIX 11/03: ANALYSE CYCLE REPRÉSENTATIF (5h = 60 bougies 5-min)
                # Un cycle baissier sur 15-min/1h dure en moyenne 2-8 heures
                # Les indicateurs 1-min (mom3, mom5) voient le MICRO-rebond,
                # mais pas si la TENDANCE CYCLE est encore active.
                # Solution: analyser la VELOCITY de baisse sur 2 sub-périodes d'1h
                # → Si la baisse s'accélère encore = cycle NON terminé, bloquer l'achat
                # → Si la baisse RALENTIT nettement = essoufflement vendeur = achat possible
                # ══════════════════════════════════════════════════════════════
                _cp1h_a = prices[-24:-12] if len(prices) >= 24 else prices[:max(1, len(prices)//2)]  # De -2h à -1h
                _cp1h_b = prices[-12:] if len(prices) >= 12 else prices                               # Dernière heure
                _cp5h = prices[-60:] if len(prices) >= 60 else prices                                  # 5 dernières heures
                _vel_h1 = (_cp1h_a[-1] - _cp1h_a[0]) / max(abs(_cp1h_a[0]), 1e-10) * 100  # Tendance 1ère heure
                _vel_h2 = (_cp1h_b[-1] - _cp1h_b[0]) / max(abs(_cp1h_b[0]), 1e-10) * 100  # Tendance dernière heure
                _drop_cycle = (_cp5h[-1] - max(_cp5h)) / max(abs(max(_cp5h)), 1e-10) * 100  # Baisse totale 5h
                # Cycle ACTIF: baisse > 2% sur 5h ET (accélérant OU encore actif)
                _cycle_is_accelerating = _vel_h2 < _vel_h1 - 0.5   # Dernière heure > 0.5% pire
                _cycle_is_active = _vel_h2 < -0.3                    # Dernière heure encore en baisse active (>0.3%/h)
                # 🔧 FIX 13/03: Un cycle CONSTANT (non accélérant) est AUSSI un cycle en cours!
                # Ex SOL: -3.2% sur 1.5h, rythme constant → _cycle_is_accelerating=False mais cycle actif!
                # Avant: _is_cycle_ongoing=False → coin sélectionné malgré downtrend clair → perte.
                _is_cycle_ongoing = _drop_cycle < -2.0 and (_cycle_is_accelerating or _cycle_is_active)
                # Essoufflement RÉEL: cycle baissier qui RALENTIT clairement (fin de cycle proche)
                # La baisse était forte (-0.5%/h) mais la dernière heure est nettement moins forte
                _cycle_exhausting = (
                    _vel_h1 < -0.5 and              # 1ère heure était en baisse active
                    _vel_h2 > _vel_h1 + 0.4 and    # Dernière heure = baisse nettement moins forte (+0.4%)
                    momentum_3 >= 0                  # Et momentum court terme commence à se redresser
                )
                # 🆕 FIX 13/03: Compter les bougies 5min CONSÉCUTIVES en hausse depuis le fond
                # Un VRAI rebond → au moins 2 bougies consécutives en hausse (= 10 min minimum)
                # Un micro-rebond d'1 bougie pendant une chute = bruit, pas un retournement réel
                # Ex SOL: 1 bougie verte isolated dans 90 min de chute = pas un rebond confirmé
                _consec_green = 0
                for _ci in range(len(prices) - 1, max(len(prices) - 10, 0), -1):
                    if _ci > 0 and prices[_ci] > prices[_ci - 1]:
                        _consec_green += 1
                    else:
                        break

                # Stocker pour réutilisation dans smart_criteria
                item.features['cycle_is_ongoing'] = _is_cycle_ongoing
                item.features['cycle_exhausting'] = _cycle_exhausting
                item.features['cycle_vel_h1'] = _vel_h1
                item.features['cycle_vel_h2'] = _vel_h2
                item.features['cycle_drop_5h'] = _drop_cycle
                item.features['consec_green_candles'] = _consec_green

                is_creux_rebound_early = (
                    # PATH A: CREUX CLASSIQUE (prix BAS dans Bollinger)
                    # 1. CREUX confirmé (prix BAS dans Bollinger — pas juste un pullback normal)
                    bb_position < 0.35 and             # 🔴 DURCI 0.45→0.35: doit être VRAIMENT dans la zone basse BB
                    rsi >= 20 and rsi <= 42 and        # 🔴 DURCI 47→42: RSI 42-47 = début de pullback, PAS un creux

                    # 2. VRAI DÉBUT DE REBOND (pas juste un micro-rebond d'1 candle)
                    (momentum_3 > 0.10 or              # 🔴 DURCI 0.01→0.10%: momentum réel requis
                     (_lstm_confirms_reversal and momentum_3 > 0.05)) and  # 🧠 OU LSTM + mom >= 0.05%
                    momentum_5 > -0.30 and             # 🔴 DURCI -0.5→-0.30%: pas en chute active sur 5 bougies

                    # 3. Momentum accélère ET positif (les deux requis, pas juste l'un)
                    mom_accel > 0 and                  # 🔴 DURCI: accélération OBLIGATOIRE (était OR)
                    momentum_3 > 0.05 and              # 🔴 AJOUTÉ: mom3 > 0.05% obligatoire

                    # 4. EMA remonte OU LSTM confirme (l'EMA est lente, Ok si LSTM confirme)
                    (ema_slope > 0.05 or _lstm_confirms_reversal) and

                    # 5. VOLUME pas mort
                    volume_awakening and               # Volume réveillé

                    # 6. 🆕 BTC ne baisse pas franchement (indexation BTC)
                    _btc_allows_creux and

                    # 7. 🆕 v2 09/03: PAS un essoufflement post-pump sur 5min
                    # Cas KNC: pic intraday non visible dans 1h closes → check 5min direct
                    not _is_post_pump_5m and

                    # 8. 🆕 FIX 11/03: PAS si cycle baissier encore actif
                    # Exception: si essoufflement réel détecté (baisse ralentit nettement)
                    (not _is_cycle_ongoing or _cycle_exhausting) and

# 9. 🆕 FIX 13/03: REBOND SOUTENU — au moins 3 bougies 5min consécutives en hausse
                    # 0 ou 1 bougie verte = micro-pause dans la chute, pas un retournement
                    # 2 bougies = dead cat bounce possible (10 min insuffisant)
                    # 🔧 FIX 21/03: DURCI 2→3 bougies (15 min minimum de hausse continue)
                    # Analyse: BANANAS31 acheté sur 2 bougies → dead cat, vrai creux à +17min
                    # Exception LSTM forte confiance: 2 bougies suffisantes si LSTM confirme
                    (_consec_green >= 3 or (_lstm_confirms_reversal and _consec_green >= 2))
                )

                # 🆕 FIX 10/03: PATH B — SQUEEZE REBOUND (modèle XLM 10/03 15h40)
                # Longue consolidation (BB/KC très serrées pendant des heures) puis volume + momentum → breakout
                # DIFFÉRENCE vs PATH A: bb_position peut être jusqu'à 0.65 (milieu de BB étroites)
                # car pendant un squeeze, le prix oscille dans un range serré sans toucher les BB basses
                # Exemple parfait: XLM calme plat 09h→15h30, RSI=38.7, BB serrées,
                # puis breakout +4.7% avec volume explosif × 3 → achat pile au démarrage → bravo!
                _is_squeeze_rebound_early = (
                    (bb_squeeze_confirmed == 1 or bb_squeeze == 1) and  # BB resserrées (consolidation longue)
                    bb_bandwidth < 4.0 and              # Squeeze confirmé (bandes très étroites)
                    bb_position >= 0.20 and bb_position < 0.65 and  # Prix dans moitié basse/centrale (pas en surachat)
                    rsi >= 25 and rsi <= 52 and         # RSI zone élargie (squeeze déprime le RSI, ex: XLM=38.7)
                    momentum_3 > 0.10 and               # Momentum réel positif (breakout commence)
                    mom_accel > 0 and                   # Accélération positive (pas un faux départ)
                    vol_ratio >= 1.3 and                # Volume commence à monter (confirmation début breakout)
                    ema_slope >= -0.05 and              # EMA ne plonge pas (pas en tendance baissière active)
                    _btc_allows_creux and               # BTC ne se crashe pas
                    not _is_post_pump_5m                # Pas un essoufflement post-pump récent
                )
                is_creux_rebound_early = is_creux_rebound_early or _is_squeeze_rebound_early

                # Bonus si rebond TRÈS fort (momentum violent)
                # 🔴 FIX 24/01: Mom >0.3% ou >0.2%+vol 1.5x
                is_creux_rebound_strong = is_creux_rebound_early and (
                    momentum_3 > 0.3 or              # Momentum >0.3% = rebond violent
                    (momentum_3 > 0.2 and vol_ratio > 1.5)  # Momentum >0.2% + volume 1.5x
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE 6: EMA BULLISH CONFIRMÉ - ULTRA STRICT!
                # EXIGE EMA9 > EMA21 ET momentum POSITIF - PAS DE TOLÉRANCE!
                # ══════════════════════════════════════════════════════════════════════
                is_ema_bullish_confirmed = (
                    ema_diff >= PULLBACK_MIN_GAP and   # EMA9 NETTEMENT au-dessus
                    ema_diff < 0.25 and                # Mais pas trop écarté
                    bb_position > 0.35 and             # Prix au-dessus de BB basse
                    bb_position < 0.75 and             # Prix pas en surachat
                    momentum_3 >= MOMENTUM_REVERSAL_MIN and  # Momentum min 0.10%
                    momentum_3 < MOMENTUM_REVERSAL_MAX and   # Momentum max 0.80%
                    momentum_5 >= 0.05 and           # Mom5 aussi positif (0.05%!)
                    rsi > 40 and rsi < 60              # RSI max 60
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # STRATÉGIE 7: CONSOLIDATION BREAKOUT - NOUVEAU!
                # Détecte les sorties de consolidation (BB étroites) avec reprise haussière
                # Inspiré du trade TAO parfait: BB stable → EMA reprend → achat → gains
                # ══════════════════════════════════════════════════════════════════════
                # BB étroites = consolidation (bandwidth < 2.5%)
                bb_consolidation = bb_bandwidth < 2.5
                
                # EMA bullish avec momentum qui redémarre (DOIT ÊTRE SIGNIFICATIF!)
                is_consolidation_breakout = (
                    bb_consolidation and               # BB étroites = phase de consolidation
                    ema_diff >= PULLBACK_MIN_GAP and   # EMA bullish confirmé
                    ema_diff < 0.30 and                # Pas trop écarté
                    momentum_3 >= MOMENTUM_REVERSAL_MIN and  # Momentum min 0.10%
                    momentum_3 < 0.6 and             # Momentum max 0.60% (plus strict - meilleur pattern)
                    bb_position > 0.40 and             # Prix au-dessus de la moyenne
                    bb_position < 0.70 and             # Pas en surachat
                    rsi > 45 and rsi < 58              # RSI max 58 (strict - meilleur moment)
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # 🆕 DÉFINIR is_strong_persistent_uptrend AVANT son utilisation
                # ══════════════════════════════════════════════════════════════════════
                is_strong_persistent_uptrend = (
                    ema_diff > 0.5 and                 # Tendance haussière forte (EMA9 >> EMA21)
                    momentum_5 > 0.15 and              # Momentum moyen positif (tendance continue)
                    momentum_3 > 0 and                 # Momentum court terme toujours positif
                    ema_slope > 0 and                  # EMA9 monte toujours
                    vol_ratio > 1.0 and                # Volume au-dessus de la moyenne (tendance confirmée)
                    bb_position > 0.70                 # Prix en zone haute (tendance forte)
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # BLOCAGES ABSOLUS - Ne jamais acheter dans ces conditions
                # ══════════════════════════════════════════════════════════════════════
                
                # ★★★ NOUVEAU: BLOCAGE FIN DE CYCLE - NE PAS ACHETER EN HAUT! ★★★
                # Conditions de fin de cycle (quand on devrait VENDRE pas acheter):
                # 1. Prix proche de BB supérieure (en surachat)
                # 2. RSI élevé (> 70)
                # 3. Momentum qui ralentit ou devient négatif
                # 4. EMA9 commence à s'aplatir après une hausse
                # CORRECTION TRX: Si LSTM/IA prédit fortement ACHAT, NE PAS BLOQUER!
                # 🆕 20/01: Si pattern fort détecté (EARLY_BREAKOUT, CONSOLIDATION_BREAKOUT), NE PAS BLOQUER!
                # Le score IA LSTM est plus fiable que les indicateurs techniques bruts
                ema_cross_bearish = item.features.get('ema_cross_bearish', 0)
                lstm_confidence = getattr(item, 'confidence', 0)  # Score LSTM (0-1) - sécurisé
                lstm_prediction = getattr(item, 'prediction', 1)  # 0=baisse, 1=neutre, 2=hausse - sécurisé
                
                # Si LSTM prédit fortement ACHAT (prediction=2 + confidence>0.7), autoriser achat
                # OU si Score final élevé (>= 70), l'IA a déjà validé la qualité du signal
                # OU si pattern fort détecté (breakout légitime basé sur vraies features)
                lstm_says_buy = (lstm_prediction == 2 and lstm_confidence > 0.7)
                high_ai_score = (item.score >= 70)  # Score élevé = IA valide l'opportunité
                
                # 🆕 Patterns forts qui méritent exemption - UTILISE LES VRAIES VARIABLES
                # is_early_breakout et is_consolidation_breakout sont déjà définis plus haut
                # Ces patterns utilisent des features calculées (ema_cross_bullish, etc.)
                is_strong_pattern_detected = (
                    is_early_breakout or               # EMA cross bullish confirmé
                    is_early_breakout_strong or        # EMA cross + KC > BB
                    is_consolidation_breakout or       # Sortie de consolidation
                    is_strong_persistent_uptrend       # Tendance haussière longue
                )
                
                # 🔴 FIN DE CYCLE - DÉTECTION ASSOUPLIE POUR PATTERNS AUTORISÉS
                # Les patterns CREUX_REBOUND, PULLBACK, TREND_CONTINUATION, SQUEEZE_BREAKOUT
                # ont leurs propres conditions de sécurité, pas besoin de double blocage
                # On bloque uniquement les cas EXTRÊMES (RSI > 75, BB > 1.5)
                is_strong_rebound = (momentum_3 > 0.5 and vol_ratio > 1.5)  # >0.5% + volume 1.5x
                
                is_end_of_cycle = (
                    (rsi > 75 and not is_strong_rebound) or  # RSI > 75 seulement (était 70)
                    (rsi > 70 and bb_position > 1.0 and momentum_3 < 0.1) or  # RSI > 70 + très haut + momentum faible
                    (bb_position > 1.5) or  # Prix EXTRÊMEMENT haut (était 0.93 - trop restrictif)
                    (ema_cross_bearish == 1 and bb_position > 0.85) or  # Death cross en zone très haute
                    (momentum_3 < -0.01 and bb_position > 0.8)  # Retournement violent
                )
                
                # CRASH ACTIF = momentum FORTEMENT négatif (aligné avec seuils BTC crash)
                # Précédemment: -0.5% bloquait trop de signaux valides
                # ASSOUPLI: -3% sur 3 bougies OU -4% sur 5 bougies (plus permissif pour corrections normales)
                # momentum stocké en % (ex: -0.42 pour -0.42%), donc seuils en % aussi
                is_crash = (momentum_3 < -MOMENTUM_CRASH or momentum_5 < -4.0)  # -3% ou -4%
                
                # TENDANCE BAISSIÈRE FORTE = EMA9 descend ET loin de EMA21
                is_strong_downtrend = (
                    ema_diff < -1.0 and                # EMA9 bien en dessous d'EMA21
                    ema_slope < -0.05 and              # EMA9 descend
                    momentum_3 < MOMENTUM_REVERSAL_MIN  # Pas de vrai rebond (< 0.10%)
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # 🆕 FIX 01/03: TENDANCE BAISSIÈRE PERSISTANTE (moins forte que crash)
                # Momentum négatif constant sur 3 ET 5 bougies + EMA en baisse + accélération baissière
                # Piège classique: RSI survendu tente les acheteurs mais prix continue de baisser
                # Exemple FIL: baisse 3h30-9h20, momentum constamment négatif
                # ══════════════════════════════════════════════════════════════════════
                is_downtrend_persistent = (
                    momentum_3 < -0.10 and           # Mom3 négatif (<-0.10%)
                    momentum_5 < -0.15 and           # Mom5 aussi négatif (<-0.15%)
                    ema_slope < 0 and                # EMA9 en baisse
                    mom_accel < 0 and                # Momentum qui empire (accélère à la baisse)
                    not is_creux_rebound_early        # Sauf si vrai rebond détecté
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # 🆕 FIX 01/03: DÉTECTION "VENTRE MOU" - Marché sans direction
                # Pas de momentum clair, pas de tendance EMA, volatilité faible
                # Ces périodes génèrent des pertes car pas de mouvement exploitable
                # Exemple FIL: phases de dérive latérale sans direction claire
                # ══════════════════════════════════════════════════════════════════════
                is_ventre_mou = (
                    abs(momentum_3) < 0.15 and       # Momentum quasi-nul (<0.15%)
                    abs(momentum_5) < 0.20 and       # Mom5 aussi quasi-nul
                    abs(ema_slope) < 0.10 and        # EMA sans direction
                    abs(ema_diff) < 0.20 and         # EMA9 et EMA21 très proches (pas de tendance)
                    bb_bandwidth < 4.0 and           # BB pas trop larges
                    bb_bandwidth > 0.3 and           # BB pas en squeeze extrême
                    not is_consolidating             # Pas en squeeze (squeeze = début mouvement possible)
                )
                
                # Stocker dans features pour utilisation dans smart_criteria
                if hasattr(item, 'features') and item.features:
                    item.features['is_ventre_mou'] = is_ventre_mou
                    item.features['is_downtrend_persistent'] = is_downtrend_persistent
                
                # RSI EXTRÊME EN TENDANCE BAISSIÈRE = piège à achat
                # 🔴 FIX 27/02: RENFORCÉ — RSI < 20 en bearish = TOUJOURS TRAP (pas d'exception!)
                # RSI 20-30 en bearish = TRAP sauf rebond fort (momentum > 0.3%)
                # Cas LTC: RSI=12.8 + EMA Bearish + PULLBACK pattern → acheté à tort!
                is_rsi_trap_extreme = (
                    rsi < 20 and                       # RSI EXTRÊMEMENT bas
                    ema_trend_bearish == 1 and         # Tendance baissière
                    momentum_3 < 0.5                 # Pas de retournement violent (< 0.50%)
                    # PAS d'exception creux_rebound — RSI < 20 en bearish = TRAP ABSOLU
                )
                
                is_rsi_trap = (
                    rsi < 30 and                       # RSI survendu
                    ema_trend_bearish == 1 and         # Tendance baissière
                    momentum_3 < 0.3 and             # Pas de rebond fort (< 0.30%)
                    not is_creux_rebound_early         # Exception CREUX_REBOUND si momentum positif
                ) or is_rsi_trap_extreme               # RSI < 20 = TOUJOURS trap
                
                # ══════════════════════════════════════════════════════════════════════
                # BLOCAGE TENDANCE BAISSIÈRE GÉNÉRALE - RENFORCÉ!
                # Si ema_trend_bearish=1 SANS momentum de retournement fort → PAS D'ACHAT
                # ══════════════════════════════════════════════════════════════════════
                # DÉTECTION CROISEMENT EMA IMMINENT (pour déblocage seulement)
                # CONDITIONS TRÈS STRICTES: momentum positif + prix au-dessus de BB moyenne
                is_crossover_imminent_for_block = (
                    ema_diff < 0 and           # EMA9 encore sous EMA21
                    ema_diff > -0.08 and       # TRÈS proche du croisement (< 0.08%)
                    ema_slope > 0.02 and       # EMA9 remonte FORTEMENT
                    momentum_3 > MOMENTUM_REVERSAL_MIN and  # Momentum 3 bougies POSITIF (> 0.10%)
                    momentum_5 > -0.05 and   # Momentum 5 bougies pas négatif (> -0.05%)
                    bb_position > 0.45         # Prix au-dessus de la moyenne BB
                )
                
                # ══════════════════════════════════════════════════════════════════════
                # SUPPRIMÉ: Blocage absolu bearish car empêche la détection des patterns
                # CROSSOVER_IMMINENT, RSI_REVERSAL qui sont justement faits pour
                # détecter les retournements AVANT le croisement EMA !
                # Les patterns ont leurs propres conditions de sécurité.
                # ══════════════════════════════════════════════════════════════════════
                
                # ══════════════════════════════════════════════════════════════════════
                # 🔴🔴🔴 FILTRE PRÉALABLE: BLOQUER FIN DE CYCLE AVANT TOUT PATTERN! 🔴🔴🔴
                # CRITIQUE: Doit être évalué AVANT les patterns pour éviter contournement
                # 🔴 FIX 22/01: RSI > 70 = FIN DE CYCLE, bloqué pour TOUS les patterns!
                # 🆕 FIX 26/01: Patterns autorisés (PULLBACK, TREND_CONTINUATION, SQUEEZE_BREAKOUT)
                # exempts du blocage END_OF_CYCLE - ils ont leurs propres sécurités
                # ══════════════════════════════════════════════════════════════════════
                # NOTE: Le pattern n'est pas encore déterminé ici, on doit l'évaluer plus tard
                # On stocke juste l'état END_OF_CYCLE pour logging
                if is_end_of_cycle:
                    # Stocker pour logging, mais ne PAS bloquer immédiatement
                    # Les patterns autorisés pourront bypasser ce blocage
                    logger.info(f"⚠️ {symbol}: Conditions END_OF_CYCLE détectées - RSI={rsi:.0f} BB_pos={bb_position:.2f} (bypass possible selon pattern)")
                
                # ══════════════════════════════════════════════════════════════════════
                # PATTERNS D'ACHAT - Évalués dans l'ordre de priorité
                # ══════════════════════════════════════════════════════════════════════
                # 🔴 NOUVEAU SYSTÈME SIMPLIFIÉ (26/01)
                # ══════════════════════════════════════════════════════════════════════
                # ANCIEN SYSTÈME SUPPRIMÉ: Utilisait 24 patterns différents (lignes 1490-1790)
                # NOUVEAU: Utilise UNIQUEMENT smart_criteria pour assigner 6 patterns validés
                # 
                # Patterns autorisés:
                # 1. CREUX_REBOUND - Rebond depuis creux (RSI 15-40, momentum > 0)
                # 2. PULLBACK - Pullback dans tendance haussière (BB ≤ 0.88)
                # 3. TREND_CONTINUATION - Continuation de tendance haussière
                # 4. SQUEEZE_BREAKOUT - Breakout après squeeze
                # 5. HIGH_SCORE_OVERRIDE - Score élevé override (DÉSACTIVÉ - 0% win rate)
                # 6. POSSIBLE_BLOCKED - Signal POSSIBLE (BLOQUÉ - achats à la baisse)
                #
                # Le pattern sera assigné plus tard par smart_criteria (lignes 3000-3220)
                # Cette section ne fait QUE valider les patterns dangereux à bloquer
                # ══════════════════════════════════════════════════════════════════════
                
                # Initialiser pattern par défaut si non défini
                if not hasattr(item, 'pattern') or not item.pattern:
                    item.pattern = 'NEUTRAL'
                
                # BLOCAGE CRITIQUE: Vérifier CREUX_REBOUND si déjà assigné
                if item.pattern == 'CREUX_REBOUND':
                    # 🔥 FIX 27/01: RSI 15-50 + BB<0.50 (LTC raté à 08:34 RSI=32)
                    # 🔧 FIX 30/01: DÉSACTIVER CREUX_REBOUND en BEAR (70% de pertes)
                    # Le pattern CREUX_REBOUND nécessite RSI 15-50 + momentum >0.01% + BB<0.50
                    
                    # Vérifier régime de marché (si disponible)
                    try:
                        from market_regime import MarketRegime
                        if hasattr(self, 'market_regime_detector'):
                            current_regime = self.market_regime_detector.get_current_regime()[0]
                        else:
                            current_regime = 'NEUTRAL'
                    except:
                        current_regime = 'NEUTRAL'
                    
                    # 🔧 BLOCAGE: CREUX_REBOUND interdit en BEAR (pattern destructeur)
                    # 🔥 FIX 01/02: AUTORISER CREUX_REBOUND en BEAR mais avec score élevé (>=75)
                    # 🔧 FIX 27/02: Aussi bloquer si EMA bearish (trading_bot rejettera de toute façon)
                    if current_regime == 'BEAR':
                        # Ne bloquer que si score < 75 OU tendance EMA baissière
                        if item.score < 75 or ema_trend_bearish == 1:
                            item.pattern = 'NEUTRAL'
                            item.status = 'bear_blocked'
                            item.score = int(item.score * 0.5)
                            logger.warning(f"🚫 {symbol}: CREUX_REBOUND bloqué en BEAR market (score<75 ou EMA bearish={ema_trend_bearish})")
                        else:
                            logger.info(f"✅ {symbol}: CREUX_REBOUND autorisé en BEAR (score={item.score} >=75, EMA non-bearish)")
                    else:
                        # 🔧 FIX 09/02: RSI 50→55 et BB 0.50→0.55 pour cohérence avec détection
                        # La détection utilise RSI<=55, la re-validation doit être alignée
                        # Sinon signal détecté à RSI=52 → immédiatement invalidé
                        # 🔧 FIX 14/02: RSI 55→65 aligné avec zone de détection élargie
                        # 🔧 FIX 03/03: Si EMA locale BAISSIÈRE, exiger conditions PLUS strictes
                        # Un vrai CREUX_REBOUND en EMA bearish nécessite RSI vraiment bas + momentum fort
                        # Sinon: faux signal (rebond temporaire dans une tendance baissière)
                        if ema_trend_bearish == 1:
                            # EMA locale bearish → critères TRÈS renforcés (risque falling knife structurel — cas PHA)
                            # PHA: RSI=33, bb<0.40, mom3=+0.2%, mom5>-0.20% → passait TOUJOURS malgré 20h de baisse!
                            # Nouvelle logique : le rebond doit être NET et le bas des BB EXTRÊME
                            is_still_valid_creux = (
                                rsi >= 15 and rsi <= 35 and        # 🔴 DURCI 40→35: RSI doit être EXTRÊMEMENT survendu
                                bb_position < 0.25 and             # 🔴 DURCI 0.40→0.25: prix au BAS ABSOLU des BB (pas juste proche)
                                momentum_3 > 0.25 and              # 🔴 DURCI 0.15→0.25: rebond NET et fort requis
                                momentum_5 > 0.0 and               # 🔴 DURCI -0.20→0.0: 5 bougies en HAUSSE nette (pas stabilisation)
                                ema_slope > -0.03 and              # 🆕 EMA9 doit commencer à se stabiliser (pas encore en chute)
                                item.features.get('consec_green_candles', 0) >= 2  # 🆕 FIX 13/03: rebond soutenu 10min mini
                            )
                            if not is_still_valid_creux:
                                item.pattern = 'NEUTRAL'
                                item.status = 'watching'
                                logger.info(f"🔄 {symbol}: CREUX_REBOUND INVALIDE — EMA BEARISH locale (RSI={rsi:.0f} mom3={momentum_3:.3f}% mom5={momentum_5:.3f}% BB={bb_position:.2f} ema_slope={ema_slope:.4f}) → Reset NEUTRAL")
                        else:
                            # 🔴 FIX 04/03: RSI 65→50, BB 0.60→0.50 — était trop permissif
                            # En EMA non-bearish, RSI 50-65 passait comme CREUX_REBOUND valide!
                            # Exemples: ENSOUSDT RSI=62, ZECUSDT RSI=58, ARBUSDT RSI=56
                            # 🔴 FIX 09/03 v6b COHÉRENCE: momentum_3 > 0.01 → 0.10%
                            # Détection is_creux_rebound_early exige mom>0.10%.
                            # La re-validation ne doit pas garder un signal détécté à 0.12%
                            # qui est redescendu à 0.02% (micro-rebond = essoufflement)
                            # 🔴 FIX 21/03: RSI 50→45, BB 0.50→0.45 — rétro: achats RSI 46-49 = pas de vrais creux
                            # Un CREUX réel en EMA non-bearish doit toujours être en zone basse (RSI<45, BB<0.45)
                            # RSI 45-50 = zone pullback normal → position intermédiaire, pas un vrai creux
                            is_still_valid_creux = (
                                rsi >= 15 and rsi <= 45 and
                                bb_position < 0.45 and
                                momentum_3 > 0.10  # 🔴 DURCI 0.01→0.10% (alignement avec is_creux_rebound_early)
                            )
                            if not is_still_valid_creux:
                                item.pattern = 'NEUTRAL'
                                item.status = 'watching'
                                logger.info(f"🔄 {symbol}: CREUX_REBOUND INVALIDE (RSI={rsi:.0f} BB={bb_position:.2f}) → Reset NEUTRAL")
                
                # ══════════════════════════════════════════════════════════════════════
                # BLOCAGES DE SÉCURITÉ - Patterns dangereux
                # ══════════════════════════════════════════════════════════════════════
                
                # BLOCAGE #1: CRASH ACTIF (momentum très négatif)
                if is_crash and item.status != 'end_of_cycle_blocked':
                    item.status = 'crash_blocked'
                    item.pattern = "ACTIVE_CRASH"
                    item.score = int(item.score * 0.3)
                    logger.warning(f"🚨 {symbol}: ACTIVE_CRASH détecté - Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}%")
                
                # BLOCAGE #2: RSI TRAP (RSI bas dans tendance baissière sans rebond confirmé)
                # 🔧 FIX 27/02: RENFORCÉ - RSI < 20 en bearish = TRAP même sans momentum négatif fort
                # Cas LTC: RSI=12.8, acheté malgré EMA Bearish car momentum pas assez négatif
                is_rsi_trap_stronger = (
                    rsi and rsi < 30 and  # RSI en survente
                    ema_diff < -0.15 and
                    momentum_3 < -0.3
                ) or (
                    rsi and rsi < 20 and  # 🔧 FIX 27/02: RSI EXTRÊME (<20)
                    ema_trend_bearish == 1 and  # En tendance baissière
                    momentum_3 < 0.5  # Pas de retournement violent (>0.5%)
                )
                
                # 🔧 Alternative: RSI < 35 avec BTC en baisse
                btc_is_weak = False
                if hasattr(AIPredictor, '_btc_momentum'):
                    btc_mom = getattr(AIPredictor, '_btc_momentum', 0)
                    # 09/03: -0.0005 (-0.05%) = trop sensible → bruit normal, durci en -0.002 (-0.2%)
                    btc_is_weak = btc_mom < -0.002  # BTC < -0.2%
                
                is_rsi_trap_with_btc = (
                    rsi and rsi < 35 and
                    btc_is_weak and
                    momentum_3 < -0.2
                )
                
                # 🔴 DÉSACTIVÉ 21/03: LSTM REVERSAL override RSI trap supprimé
                # L'override LSTM faisait acheter dans des RSI trap → désactivé globalement
                if (is_rsi_trap_stronger or is_rsi_trap_with_btc) and item.pattern != 'CREUX_REBOUND':
                    item.status = 'rsi_trap_blocked'
                    item.pattern = "RSI_TRAP"
                    item.score = int(item.score * 0.4)
                    logger.info(f"⚠️ {symbol}: RSI trap détecté - RSI={rsi:.0f} en tendance baissière (seuil durci < 30)")
                
                # BLOCAGE #3: TENDANCE BAISSIÈRE FORTE
                elif is_strong_downtrend:
                    item.status = 'downtrend_blocked'
                    item.pattern = "STRONG_DOWNTREND"
                    item.score = int(item.score * 0.5)
                    logger.info(f"⚠️ {symbol}: Strong downtrend détecté - EMA_diff={ema_diff:.2f}%")
                
                # 🆕 BLOCAGE #3B: TENDANCE BAISSIÈRE PERSISTANTE (FIX 01/03)
                elif is_downtrend_persistent:
                    item.status = 'downtrend_persistent_blocked'
                    item.score = int(item.score * 0.6)
                    logger.info(f"📉 {symbol}: Baisse persistante - Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}% accel={mom_accel:.3f}")
                
                # 🆕 BLOCAGE #3C: VENTRE MOU (FIX 01/03)
                elif is_ventre_mou:
                    item.status = 'ventre_mou_blocked'
                    # Pas de pénalité de score, juste bloquer le signal d'achat
                    logger.info(f"😴 {symbol}: Ventre mou (sans direction) - Mom3={momentum_3:.2f}% EMA_slope={ema_slope:.3f}% BB_bw={bb_bandwidth:.1f}")
                
                # BLOCAGE #4: POSSIBLE - DÉSACTIVÉ (conflit avec smart_criteria)
                # 🔴 FIX 26/01 20h35: Ne plus utiliser advanced_profile.signal (ancien système)
                # Le nouveau smart_criteria (ligne 2800+) gère les patterns correctement
                # Si smart_criteria dit POSSIBLE, c'est géré plus tard (ligne 2930-2990)
                # elif advanced_profile.signal == 'POSSIBLE':
                #     item.status = 'blocked'
                #     item.pattern = 'POSSIBLE_BLOCKED'
                #     logger.info(f"🚫 {symbol}: POSSIBLE pattern bloqué")
                
                # Si aucun blocage, le pattern sera assigné par smart_criteria plus tard
                # (lignes 3000-3220) selon les conditions détectées
                
                # Log pour debug
                if item.score >= 60:
                    logger.info(f"🎯 {symbol}: Score={item.score} Pattern={item.pattern} Status={item.status} " +
                               f"EMA_diff={ema_diff:.2f}% Mom3={momentum_3:.2f}% RSI={rsi:.0f}")

                
                # ══════════════════════════════════════════════════════════════════════
                # NOUVEAUX MODULES D'AMÉLIORATION IA - BONUS/MALUS ADDITIONNELS
                # ══════════════════════════════════════════════════════════════════════
                base_score = item.score  # Sauvegarder le score de base pour comparaison
                
                # === 1. MULTI-TIMEFRAME ANALYSIS ===
                if MULTI_TIMEFRAME_AVAILABLE and self.multi_tf_analyzer and self.klines_fetcher:
                    try:
                        mtf_result = self.multi_tf_analyzer.analyze_symbol_multi_tf(symbol, self.klines_fetcher)
                        if mtf_result and 'alignment_bonus' in mtf_result:
                            alignment_bonus = mtf_result['alignment_bonus']
                            item.score = min(100, item.score + alignment_bonus)
                            item.features['mtf_score'] = mtf_result.get('combined_score', 0)
                            item.features['mtf_alignment'] = mtf_result.get('alignment_percentage', 0)
                            if alignment_bonus > 0:
                                logger.info(f"📊 {symbol}: Multi-TF bonus +{alignment_bonus} (alignment={mtf_result.get('alignment_percentage', 0):.0f}%)")
                    except Exception as e:
                        logger.debug(f"Erreur Multi-TF {symbol}: {e}")
                
                # === 2. VOLUME PROFILE ANALYSIS ===
                if VOLUME_PROFILE_AVAILABLE and self.volume_analyzer and self.klines_fetcher:
                    try:
                        vol_result = self.volume_analyzer.analyze_volume_profile(symbol, self.klines_fetcher)
                        if vol_result and 'score_bonus' in vol_result:
                            vol_bonus = vol_result['score_bonus']
                            item.score = min(100, item.score + vol_bonus)
                            item.features['volume_signals'] = vol_result.get('signals', [])
                            item.features['volume_quality'] = vol_result.get('quality_score', 0)
                            if vol_bonus != 0:
                                logger.info(f"📊 {symbol}: Volume bonus {vol_bonus:+d} (signals={len(vol_result.get('signals', []))})")
                    except Exception as e:
                        logger.debug(f"Erreur Volume Profile {symbol}: {e}")
                
                # === 3. TIME PATTERN ANALYSIS ===
                if TIME_PATTERN_AVAILABLE and self.time_analyzer:
                    try:
                        time_bonus = self.time_analyzer.get_time_bonus()
                        if time_bonus != 0:
                            item.score = min(100, item.score + time_bonus)
                            item.features['time_bonus'] = time_bonus
                            item.features['time_favorable'] = time_bonus > 0
                            if abs(time_bonus) >= 5:
                                logger.info(f"⏰ {symbol}: Time pattern {time_bonus:+d}")
                    except Exception as e:
                        logger.debug(f"Erreur Time Pattern {symbol}: {e}")
                
                # === 4. CORRELATION ANALYSIS ===
                if CORRELATION_ANALYZER_AVAILABLE and self.correlation_analyzer:
                    try:
                        # Mettre à jour le prix dans l'analyseur
                        current_price = prices[-1]
                        self.correlation_analyzer.update_price(symbol, current_price)
                        
                        corr_analysis = self.correlation_analyzer.analyze_correlation_strength(symbol)
                        if corr_analysis and 'correlation_bonus' in corr_analysis:
                            corr_bonus = corr_analysis['correlation_bonus']
                            item.score = min(100, item.score + corr_bonus)
                            item.features['btc_correlation'] = corr_analysis.get('btc_correlation', 0)
                            item.features['sector'] = corr_analysis.get('sector', 'UNKNOWN')
                            item.features['is_sector_leader'] = corr_analysis.get('is_sector_leader', False)
                            if corr_bonus > 0:
                                logger.info(f"🔗 {symbol}: Correlation bonus +{corr_bonus} (sector={corr_analysis.get('sector')})")
                    except Exception as e:
                        logger.debug(f"Erreur Correlation {symbol}: {e}")
                
                # === 5. ENSEMBLE PREDICTION ===
                if ENSEMBLE_PREDICTOR_AVAILABLE and self.ensemble:
                    try:
                        # Récupérer les prédictions LSTM existantes
                        lstm_pred = 2 if item.pattern in ['ACHAT', 'SQUEEZE_BREAKOUT', 'CROSSOVER_IMMINENT'] else 1
                        lstm_conf = item.confidence
                        
                        ensemble_result = self.ensemble.predict_ensemble(prices, volumes, lstm_pred, lstm_conf)
                        if ensemble_result and ensemble_result.get('consensus') in ['VERY_STRONG', 'STRONG']:
                            ensemble_bonus = self.ensemble.get_ensemble_bonus(ensemble_result)
                            item.score = min(100, item.score + ensemble_bonus)
                            item.features['ensemble_consensus'] = ensemble_result.get('consensus')
                            item.features['ensemble_confidence'] = ensemble_result.get('confidence', 0)
                            if ensemble_bonus > 0:
                                logger.info(f"🤖 {symbol}: Ensemble bonus +{ensemble_bonus} (consensus={ensemble_result.get('consensus')})")
                    except Exception as e:
                        logger.debug(f"Erreur Ensemble {symbol}: {e}")
                
                # === 6. RISK ADJUSTMENT ===
                if RISK_ADJUSTED_AVAILABLE and self.risk_scorer and len(prices) >= 20:
                    try:
                        risk_metrics = self.risk_scorer.analyze_risk_metrics(prices, volumes)
                        risk_adjusted = self.risk_scorer.adjust_score_by_risk(item.score, risk_metrics, symbol)  # Passer symbol
                        
                        # Appliquer l'ajustement de risque
                        old_score = item.score
                        item.score = risk_adjusted['adjusted_score']
                        adjustment = risk_adjusted['absolute_adjustment']
                        
                        item.features['sharpe_ratio'] = risk_metrics.get('sharpe_ratio', 0)
                        item.features['risk_quality'] = risk_metrics.get('risk_quality', 'UNKNOWN')
                        item.features['risk_multiplier'] = risk_adjusted.get('risk_multiplier', 1.0)
                        
                        if abs(adjustment) >= 5:
                            logger.info(f"⚖️ {symbol}: Risk adjustment {adjustment:+.1f} (Sharpe={risk_metrics.get('sharpe_ratio', 0):.2f})")
                    except Exception as e:
                        logger.debug(f"Erreur Risk Adjustment {symbol}: {e}")
                
                # === 7. MOMENTUM ACCELERATION DETECTION ===
                # Détecte les cryptos en phase d'accélération (momentum faible mais croissant)
                # C'est le signal le plus fort pour capter les opportunités émergentes!
                # 🆕 Intègre aussi les nouvelles features de early_cycle_score
                if len(prices) >= 10:
                    try:
                        current_price = prices[-1]
                        mom_1 = ((current_price - prices[-2]) / prices[-2]) * 100 if len(prices) >= 2 else 0  # 🔧 FIX: was prices[-1]-prices[-1]=0
                        mom_3 = ((current_price - prices[-3]) / prices[-3]) * 100 if len(prices) >= 3 else 0
                        mom_5 = ((current_price - prices[-5]) / prices[-5]) * 100 if len(prices) >= 5 else 0
                        mom_10 = ((current_price - prices[-10]) / prices[-10]) * 100 if len(prices) >= 10 else 0
                        
                        # Accélération = momentum croissant sur différentes périodes
                        # Mom3 > Mom5 > Mom10 = accélération progressive (fort signal!)
                        is_accelerating = (mom_3 > mom_5 and mom_5 > mom_10 and mom_3 > 0)
                        
                        # Accélération forte = Mom3 > 0.5% et Mom5 > Mom10
                        is_strong_acceleration = (mom_3 > 0.5 and mom_5 > 0 and mom_5 < mom_3)
                        
                        # 🆕 MOMENTUM DERIVATIVE: Utiliser la feature momentum_acceleration
                        mom_accel_feature = item.features.get('momentum_acceleration', 0)
                        # Accélération positive = momentum change de direction vers le haut
                        is_turning_up = (mom_accel_feature > 0.15 and mom_3 > -0.3)
                        
                        item.features['momentum_1'] = mom_1
                        item.features['momentum_3'] = mom_3
                        item.features['momentum_5'] = mom_5
                        item.features['momentum_10'] = mom_10
                        item.features['is_accelerating'] = is_accelerating
                        item.features['is_turning_up'] = is_turning_up
                        
                        # BONUS IMPORTANT: Momentum en accélération = opportunité émergente!
                        if is_strong_acceleration:
                            accel_bonus = 8
                            item.score = min(100, item.score + accel_bonus)
                            logger.info(f"🚀 {symbol}: Momentum en accélération +{accel_bonus} (Mom3={mom_3:.2f}% > Mom5={mom_5:.2f}%)")
                        elif is_accelerating:
                            accel_bonus = 5
                            item.score = min(100, item.score + accel_bonus)
                            logger.info(f"⚡ {symbol}: Accélération détectée +{accel_bonus}")
                        elif is_turning_up and not is_accelerating:
                            # 🆕 Momentum qui tourne vers le haut = signal précoce
                            turning_bonus = 3
                            item.score = min(100, item.score + turning_bonus)
                            logger.info(f"↗️ {symbol}: Momentum turning up +{turning_bonus} (accel={mom_accel_feature:.3f})")
                    except Exception as e:
                        logger.debug(f"Erreur momentum acceleration {symbol}: {e}")
                
                # === 8. MONTE CARLO CONFIDENCE ===
                if MONTE_CARLO_AVAILABLE and self.monte_carlo and len(prices) >= 20:
                    try:
                        # Calculer les rendements historiques
                        returns = [(prices[i] - prices[i-1]) / prices[i-1] * 100 for i in range(1, min(len(prices), 50))]
                        current_price = prices[-1]
                        
                        mc_result = self.monte_carlo.simulate_future_prices(current_price, returns, periods_ahead=10)
                        mc_confidence = self.monte_carlo.calculate_confidence_score(mc_result)
                        
                        item.features['mc_confidence_score'] = mc_confidence.get('confidence_score', 50)
                        item.features['mc_probability_profit'] = mc_result.get('probability_profit', 50)
                        item.features['mc_expected_return'] = mc_result.get('expected_return', 0)
                        
                        # Pénalité RÉDUITE si faible confiance Monte Carlo (10→5 trop sévère)
                        if mc_confidence.get('confidence_score', 50) < 30:
                            penalty = 5  # Réduit de 10 à 5
                            item.score = max(0, item.score - penalty)
                            logger.info(f"⚠️ {symbol}: Monte Carlo low confidence penalty -{penalty}")
                        # Bonus si très haute confiance
                        elif mc_confidence.get('confidence_score', 50) >= 80:
                            bonus = 10
                            item.score = min(100, item.score + bonus)
                            logger.info(f"🎯 {symbol}: Monte Carlo high confidence bonus +{bonus}")
                    except Exception as e:
                        logger.debug(f"Erreur Monte Carlo {symbol}: {e}")
                
                # === SCORE FINAL ===
                # 🔧 FIX AUDIT 28/02: Caper le TOTAL des bonuses (pré-base + modules) depuis le score GPU original
                # Avant: seuls les modules (MTF, correlation, etc.) étaient capés → TOP_20(+15), whitelist(+15), volatility(+20) non capés
                # Un TOP_20 avec excellente volatilité et whitelist = +50 pts non capés! 
                # Maintenance: cap total de +30 depuis le score GPU original
                total_from_gpu = item.score - gpu_original_score
                MAX_TOTAL_BONUS_CAP = 50  # 🔧 FIX 28/02: Relevé 30→50 pour laisser les bonuses légitimes s'additionner
                MIN_TOTAL_PENALTY_CAP = -25
                if total_from_gpu > MAX_TOTAL_BONUS_CAP:
                    item.score = gpu_original_score + MAX_TOTAL_BONUS_CAP
                    logger.info(f"⚠️ {symbol}: Bonus TOTAL plafonné à +{MAX_TOTAL_BONUS_CAP} depuis GPU={gpu_original_score} (brut était +{total_from_gpu:.0f})")
                elif total_from_gpu < MIN_TOTAL_PENALTY_CAP:
                    item.score = max(0, gpu_original_score + MIN_TOTAL_PENALTY_CAP)
                    logger.info(f"⚠️ {symbol}: Pénalité TOTALE plafonnée à {MIN_TOTAL_PENALTY_CAP} depuis GPU={gpu_original_score} (brut était {total_from_gpu:.0f})")
                
                total_adjustment = item.score - base_score
                
                # 🔧 FIX 28/02: CAP modules seuls (secondaire — le cap GPU ci-dessus est prioritaire)
                MAX_BONUS_CAP = 20  # 🔧 FIX 28/02: 30→20
                MIN_PENALTY_CAP = -20  # 🆕 PLANCHER PÉNALITÉ: Évite que les pénalités tuent un bon signal
                if total_adjustment > MAX_BONUS_CAP:
                    item.score = base_score + MAX_BONUS_CAP
                    logger.info(f"⚠️ {symbol}: Bonus modules plafonné à +{MAX_BONUS_CAP} (total brut était +{total_adjustment:.0f})")
                    total_adjustment = MAX_BONUS_CAP
                elif total_adjustment < MIN_PENALTY_CAP:
                    item.score = max(0, base_score + MIN_PENALTY_CAP)
                    logger.info(f"⚠️ {symbol}: Pénalité plafonnée à {MIN_PENALTY_CAP} (total brut était {total_adjustment:.0f})")
                    total_adjustment = MIN_PENALTY_CAP
                
                # 🔧 FIX 02/03: BEAR/CORRECTION PENALTY — RÉDUITE et ADAPTATIVE
                # Ancienne: -15 fixe → rendait score 90 inatteignable (75 max après bonus cap)
                # Nouvelle: -8 en BEAR, -5 en CORRECTION, 0 en EARLY_RECOVERY
                # SKIP si LSTM REVERSAL_UP détecté (le LSTM voit le retournement avant le régime)
                try:
                    from market_regime import get_market_regime_detector
                    _regime_detector = get_market_regime_detector()
                    if _regime_detector and hasattr(_regime_detector, 'current_regime'):
                        _current_regime = _regime_detector.current_regime
                        has_lstm_reversal_up = item.features.get('lstm_reversal_label') == 'REVERSAL_UP'
                        
                        if _current_regime == 'BEAR' and not has_lstm_reversal_up:
                            bear_penalty = 8  # 🔧 FIX 02/03: 15→8
                            item.score = max(0, item.score - bear_penalty)
                            logger.info(f"🐻 {symbol}: Pénalité BEAR -{bear_penalty} pts → score={item.score} (base={base_score})")
                        elif _current_regime == 'CORRECTION' and not has_lstm_reversal_up:
                            bear_penalty = 5  # 🔧 FIX 02/03: 15→5 en CORRECTION
                            item.score = max(0, item.score - bear_penalty)
                            logger.info(f"🐻 {symbol}: Pénalité CORRECTION -{bear_penalty} pts → score={item.score} (base={base_score})")
                        elif _current_regime == 'EARLY_RECOVERY':
                            # 🆕 FIX 02/03: Pas de pénalité en EARLY_RECOVERY
                            logger.info(f"🔄 {symbol}: EARLY_RECOVERY — aucune pénalité régime")
                        elif has_lstm_reversal_up and _current_regime in ('BEAR', 'CORRECTION'):
                            logger.info(f"🧠 {symbol}: LSTM REVERSAL_UP bypass pénalité {_current_regime}")
                except Exception:
                    pass  # Si market_regime n'est pas disponible, ignorer
                
                # 🆕 EARLY CYCLE BONUS: Si early_cycle_score élevé, bonus spécial
                # Ce bonus est APRÈS le cap standard car il récompense la détection précoce
                # 🔧 FIX 28/02: Réduit de 5-10 à 3-5 pour limiter l'inflation
                early_cycle_score = item.features.get('early_cycle_score', 0)
                if early_cycle_score >= 50:
                    early_bonus = min(5, int(early_cycle_score / 20))  # 🔧 FIX 28/02: /10→/20, max 10→5
                    item.score = min(100, item.score + early_bonus)
                    logger.info(f"🌅 {symbol}: Early cycle bonus +{early_bonus} (early_score={early_cycle_score:.0f})")
                
                # Sauvegarder le base_score pour que trading_bot.py puisse vérifier la qualité réelle
                item.features['base_score'] = base_score
                
                if abs(total_adjustment) >= 10:
                    logger.info(f"🎯 {symbol}: Score final={item.score} (base={base_score}, adjustment={total_adjustment:+.1f})")
                
                # ══════════════════════════════════════════════════════════════
                # VALIDATION FINALE: Score minimum pour status ready
                # Seuil ULTRA-RÉDUIT de 65→50 pour maximiser la réactivité
                # TOP 20 cryptos (BTC, ETH, etc.) ont seuil encore plus bas: 45
                # ══════════════════════════════════════════════════════════════
                
                # TOP 20 cryptos majeurs (moins risqués, plus réactifs)
                # 🔧 FIX AUDIT 28/02: Utiliser TOP_20_CRYPTOS déjà défini en constante globale (évite doublon)
                is_top20 = symbol in TOP_20_CRYPTOS
                MIN_SCORE_FOR_READY = 45 if is_top20 else 50  # TOP 20: 45, autres: 50
                
                # Exception: Si momentum en accélération OU score IA dashboard > 65, accepter score >= 42
                has_strong_accel = item.features.get('is_accelerating', False) and item.features.get('momentum_3', 0) > 0.5
                has_good_ia_score = item.features.get('lstm_confidence', 0) > 0.65
                
                if has_strong_accel or has_good_ia_score:
                    MIN_SCORE_FOR_READY = 42
                    reason = "accélération" if has_strong_accel else "score IA élevé"
                    logger.info(f"✅ {symbol}: Seuil réduit à 42 ({reason})")
                elif is_top20:
                    logger.info(f"✅ {symbol}: TOP 20 - seuil 45 (vs 50 standard)")
                
                # 🔴 FIX 26/01 20h22: ASSIGNATION STATUS READY SI SCORE SUFFISANT
                # Bug: Le code downgrade ready→watching mais ne fait jamais l'upgrade watching→ready
                # 🔴 FIX 08/02: READY nécessite AUSSI un pattern d'achat valide (pas NEUTRAL)
                # Sinon le dashboard affiche des signaux ACHAT en surachat/bearish
                # 🔧 FIX AUDIT 28/02: TREND_CONTINUATION retiré (0% WR, désactivé)
                BUYABLE_PATTERNS = ['CREUX_REBOUND', 'PULLBACK', 'SQUEEZE_BREAKOUT', 'EARLY_BREAKOUT',
                                    'CONSOLIDATION_BREAKOUT', 'EMA_BULLISH', 'CROSSOVER_IMMINENT',
                                    'VOLUME_REVERSAL', 'RSI_REVERSAL', 'STRONG_UPTREND', 'HIGH_SCORE_OVERRIDE']
                has_buyable_pattern = hasattr(item, 'pattern') and item.pattern in BUYABLE_PATTERNS
                
                if item.score >= MIN_SCORE_FOR_READY and item.status != 'blocked' and 'blocked' not in item.status and has_buyable_pattern:
                    # Signal valide avec score suffisant ET pattern d'achat → READY
                    if item.status != 'ready':
                        logger.info(f"✅ {symbol}: Score {item.score} >= {MIN_SCORE_FOR_READY} + Pattern {item.pattern} → status READY")
                    item.status = 'ready'
                elif item.score >= MIN_SCORE_FOR_READY and not has_buyable_pattern:
                    # Score OK mais pas de pattern d'achat → watching (pas ready!)
                    item.status = 'watching'
                elif item.status == 'ready' and (item.score < MIN_SCORE_FOR_READY or not has_buyable_pattern):
                    # Downgrade si score devient insuffisant
                    logger.warning(f"⚠️ {symbol}: Score final {item.score} < {MIN_SCORE_FOR_READY} - status ready → watching")
                    item.status = 'watching'

                # 🆕 FIX 13/03: GARDE FINALE — EMA bearish + cycle baissier en cours = WATCHING uniquement
                # "Tant que la valeur ne rebondit pas, ne pas la sélectionner" (user requirement)
                # Score élevé + CREUX pattern + EMA bearish + cycle actif = surveillance, PAS achat
                # Exception: 3+ bougies vertes consécutives = rebond confirmé (10+ min) → ready autorisé
                _n_green = item.features.get('consec_green_candles', 0) if hasattr(item, 'features') and item.features else 0
                _bearish_cycle_active = (
                    ema_trend_bearish == 1 and
                    item.features.get('cycle_is_ongoing', False) and
                    not item.features.get('cycle_exhausting', False) and
                    _n_green < 3  # Rebond non confirmé (< 3 bougies vertes consécutives)
                )
                if _bearish_cycle_active and item.status == 'ready':
                    item.status = 'watching'
                    logger.info(f"🔄 {symbol}: EMA bearish + cycle en cours + {_n_green} bougie(s) verte(s) → watching (rebond non confirmé)")
                
                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 ===
        score = 0
        reasons = []
        
        # ════════════════════════════════════════════════════════════════════
        # STRATÉGIE PRIORITAIRE: BOLLINGER SQUEEZE BREAKOUT
        # ════════════════════════════════════════════════════════════════════
        # Conditions OBLIGATOIRES:
        # 1. Bandes de Bollinger très resserrées (squeeze)
        # 2. EMA9 < EMA21 (OBLIGATOIRE - point d'entrée avant breakout)
        # 3. RSI pas en surachat
        
        squeeze_breakout_score = 0
        squeeze_strategy_valid = False  # Flag pour valider la stratégie
        
        # Vérifier les 2 conditions OBLIGATOIRES:
        # 1. BB Squeeze ET 2. EMA9 < EMA21
        has_squeeze = features.get('bb_squeeze_confirmed', 0) or features['bb_squeeze']
        has_ema_condition = features['ema_diff'] < 0  # EMA9 < EMA21
        
        if has_squeeze and has_ema_condition:
            # ✅ Les 2 conditions obligatoires sont remplies
            squeeze_strategy_valid = True
            
            # Points pour le squeeze
            if features.get('bb_squeeze_confirmed', 0):
                squeeze_breakout_score += 30
                reasons.append("🔥 BB SQUEEZE CONFIRMÉ")
            else:
                squeeze_breakout_score += 20
                reasons.append("📊 BB Squeeze détecté")
            
            # Points pour EMA9 < EMA21 (condition obligatoire remplie)
            squeeze_breakout_score += 25
            reasons.append("🎯 EMA9 < EMA21 (point d'entrée optimal)")
            
            # Bonus: Durée du squeeze (plus long = explosion plus forte)
            squeeze_duration = features.get('squeeze_duration', 0)
            if squeeze_duration >= 5:
                squeeze_breakout_score += 15
                reasons.append(f"⏱️ Squeeze {squeeze_duration} bougies")
            elif squeeze_duration >= 3:
                squeeze_breakout_score += 10
            
            # Bonus: RSI favorable (pas suracheté)
            if features['rsi'] < 60 or features['rsi'] == 0:
                squeeze_breakout_score += 10
            elif features['rsi_overbought']:
                squeeze_breakout_score -= 20  # Pénalité si surachat
            
            # Bonus: Breakout en cours
            if features.get('breakout_strength', 0) > 0:
                squeeze_breakout_score += 20
                reasons.append("🚀 BREAKOUT DÉTECTÉ!")
            
            # Bonus: Proche support
            if features.get('near_support', 0):
                squeeze_breakout_score += 10
                reasons.append("🛡️ Proche support")
        
        elif has_squeeze and not has_ema_condition:
            # ❌ Squeeze présent mais EMA9 > EMA21 = NE PAS ACHETER
            # C'est trop tard, le breakout a déjà eu lieu
            # Mais on garde un score indicatif (pas 0)
            reasons.append("⚠️ Squeeze mais EMA9 > EMA21 (trop tard)")
            squeeze_breakout_score = 35  # Score indicatif mais pas suffisant pour acheter
        
        elif not has_squeeze:
            # Pas de squeeze, donner un score de base selon les autres indicateurs
            # AMÉLIORÉ: Valoriser les rebonds techniques sans squeeze
            momentum_3 = features.get('momentum_3', 0)
            ema_slope = features.get('ema_slope', 0)
            
            if features['ema_diff'] < 0:  # EMA9 < EMA21 = creux
                if momentum_3 > 0 or ema_slope > 0:  # Rebond en cours
                    squeeze_breakout_score = 40
                    reasons.append("📈 Creux avec rebond (pas de squeeze)")
                else:
                    squeeze_breakout_score = 30
                    reasons.append("📉 Creux EMA (pas de squeeze)")
            elif features['ema_diff'] > 0:  # EMA9 > EMA21 = tendance haussière
                if momentum_3 > 0 and ema_slope > 0:  # Rebond confirmé
                    squeeze_breakout_score = 45  # Augmenté de 25 à 45
                    reasons.append("📈 Rebond technique confirmé")
                elif momentum_3 > 0 or ema_slope > 0:  # Rebond partiel
                    squeeze_breakout_score = 35
                    reasons.append("📈 EMA montante")
                else:
                    squeeze_breakout_score = 25
            else:
                squeeze_breakout_score = 20  # Score de base
        
        # ════════════════════════════════════════════════════════════════════
        # RÈGLE FONDAMENTALE: CREUX + STABILISATION (pas chute active)
        # ════════════════════════════════════════════════════════════════════
        # Stratégie "Buy the Dip" CORRIGÉE:
        # 1. EMA9 < EMA21 = creux MAIS il faut vérifier la STABILISATION
        # 2. Si le prix CONTINUE de baisser = CHUTE ACTIVE = NE PAS ACHETER
        # 3. Acheter seulement quand le prix STABILISE ou REMONTE
        
        ema_allows_buy = False
        is_early_rebound = False
        
        # Vérifier d'abord si c'est une CHUTE ACTIVE (bloque tout achat)
        momentum_3 = features.get('momentum_3', 0)
        momentum_5 = features.get('momentum_5', 0)
        ema_slope = features.get('ema_slope', 0)
        price_declining = features.get('price_declining', 0)
        
        # CHUTE ACTIVE = prix qui continue de baisser fortement
        is_active_falling = (
            (momentum_3 < -MOMENTUM_DROP_LIGHT and momentum_5 < -MOMENTUM_DROP_MODERATE) or  # -0.2% et -0.3%
            (price_declining == 1 and momentum_3 < -MOMENTUM_REVERSAL_MIN) or  # 3 bougies rouges + mom < -0.1%
            (ema_slope < -0.1 and momentum_3 < -0.15)  # EMA en baisse + mom < -0.15%
        )
        
        if is_active_falling:
            # CHUTE ACTIVE = NE PAS ACHETER même si EMA9 < EMA21
            reasons.append(f"🔻 CHUTE ACTIVE (mom3={momentum_3:.2f}%, mom5={momentum_5:.2f}%) = NO BUY")
            ema_allows_buy = False
        elif features['ema_diff'] < 0:
            # ═══════════════════════════════════════════════════════════════
            # CAS 1: EMA9 < EMA21 = CREUX - MEILLEUR MOMENT POUR ACHETER!
            # ═══════════════════════════════════════════════════════════════
            if momentum_3 >= 0:  # Momentum positif = REBOND EN COURS!
                ema_allows_buy = True
                is_early_rebound = True
                reasons.append(f"🚀 CREUX + REBOND! (diff={features['ema_diff']:.2f}%, mom=+{momentum_3:.2f}%)")
            elif momentum_3 >= -0.15:  # Momentum quasi-stable (>= -0.15%) = stabilisation
                ema_allows_buy = True
                reasons.append(f"✅ Creux EMA + stabilisation (diff={features['ema_diff']:.2f}%, mom={momentum_3:.2f}%)")
            elif ema_slope >= 0:  # EMA commence à remonter même si momentum légèrement négatif
                ema_allows_buy = True
                is_early_rebound = True
                reasons.append(f"📈 Creux + EMA remonte (slope=+{ema_slope:.2f})")
            else:
                reasons.append(f"🚫 Creux mais momentum trop négatif (mom3={momentum_3:.2f}%)")
                ema_allows_buy = False
        elif features['ema_diff'] < 0.15 and features['ema_diff'] >= 0:
            # ═══════════════════════════════════════════════════════════════
            # CAS 2: EMA9 VIENT DE PASSER AU-DESSUS D'EMA21 = DÉBUT DE REBOND
            # ═══════════════════════════════════════════════════════════════
            if features.get('momentum_3', 0) > 0:  # Momentum positif = confirme le rebond
                ema_allows_buy = True
                is_early_rebound = True
                reasons.append(f"🚀 DÉBUT REBOND! (diff=+{features['ema_diff']:.2f}%, mom=+{momentum_3:.2f}%)")
            elif features.get('ema_slope', 0) > 0:  # EMA monte = signal OK
                ema_allows_buy = True
                is_early_rebound = True
                reasons.append(f"📈 EMA9 en remontée (slope=+{features.get('ema_slope', 0):.2f})")
            elif features.get('rsi', 50) < 50:  # RSI bas = potentiel de hausse
                ema_allows_buy = True
                reasons.append(f"📊 EMA proche + RSI bas ({features.get('rsi', 50):.0f})")
            else:
                ema_allows_buy = False
                reasons.append(f"⏳ Attendre confirmation (diff=+{features['ema_diff']:.2f}%)")
        else:
            # EMA9 > EMA21 de plus de 0.15% = tendance HAUSSIÈRE en cours
            # ═══════════════════════════════════════════════════════════════
            # STRATÉGIE TENDANCE HAUSSIÈRE (conditions strictes)
            # ═══════════════════════════════════════════════════════════════
            # TOUTES les conditions doivent être vraies:
            
            momentum_positive = features.get('momentum_3', 0) > 0.05  # Momentum clairement positif
            ema_rising = features.get('ema_slope', 0) > 0  # EMA montante
            rsi_value = features.get('rsi', 50)
            not_overbought = rsi_value < 75  # RSI strict
            
            # Vérifier qu'il n'y a PAS de tendance baissière
            ema_trend_ok = features.get('ema_trend_bearish', 0) == 0
            
            # ACHETER seulement si TOUTES les conditions sont vraies
            if momentum_positive and ema_rising and not_overbought and ema_trend_ok and ema_diff > 0.05:
                # TENDANCE HAUSSIÈRE CONFIRMÉE - ACHETER!
                ema_allows_buy = True
                is_early_rebound = True
                reasons.append(f"📈 TENDANCE HAUSSIÈRE (EMA9>+{ema_diff:.2f}%, mom=+{features.get('momentum_3', 0):.2f}%)")
            elif features.get('bb_position', 0.5) < 0.25 and momentum_positive and ema_trend_ok:
                ema_allows_buy = True
                reasons.append(f"🛡️ Proche BB basse + momentum positif")
            else:
                reasons.append(f"⏳ Attendre (diff={ema_diff:.2f}%, RSI={rsi_value:.0f}, bearish={not ema_trend_ok})")
                ema_allows_buy = False
        
        # ════════════════════════════════════════════════════════════════════
        # RÈGLE 2: TENDANCE - AUTORISER STABILISATION POUR ACHETER AU CREUX
        # ════════════════════════════════════════════════════════════════════
        # Pour acheter un creux (EMA9 < EMA21), on doit voir une STABILISATION
        # pas nécessairement une hausse déjà confirmée
        
        trend_allows_buy = True
        is_squeeze_setup = features.get('bb_squeeze', False) and features['ema_diff'] < -0.05
        
        # Si c'est un squeeze setup, on est plus permissif sur la tendance
        # Au creux, le momentum est TOUJOURS négatif - c'est normal!
        # On cherche une STABILISATION (momentum qui remonte) pas un momentum positif
        if is_squeeze_setup:
            # Pour un squeeze, on autorise si:
            # - Le momentum 3 n'est pas en chute libre (> -0.5%)
            # - OU le prix est proche de la BB basse (rebond probable)
            near_bb_low = features.get('near_bb_lower', 0) == 1 or features.get('bb_position', 0.5) < 0.25
            
            if near_bb_low:
                # Prix proche de la BB basse = zone de rebond = autoriser
                trend_allows_buy = True
                reasons.append("🛡️ Prix proche BB basse (zone rebond)")
            elif features['momentum_3'] < -0.5 and features['ema_slope'] < -0.15:
                # Chute TRÈS forte = attendre
                trend_allows_buy = False
                reasons.append("⬇️ Chute forte en cours")
            elif features.get('price_declining', 0) == 1 and features['momentum_3'] < -0.3:
                trend_allows_buy = False
                reasons.append("🔻 3 bougies en baisse forte")
        else:
            # Pour les autres stratégies, on garde la logique stricte
            # EMA9 en baisse forte = tendance baissière = PAS D'ACHAT
            if features['ema_slope'] < -0.2:
                trend_allows_buy = False
                reasons.append("📉 EMA9 en baisse forte")
            
            # Momentum très négatif = prix en chute = PAS D'ACHAT
            if features['momentum_5'] < -0.5:
                trend_allows_buy = False
                reasons.append("⬇️ Momentum négatif fort")
            
            # 3 bougies en baisse avec momentum fort = PAS D'ACHAT
            if features.get('price_declining', 0) == 1 and features['momentum_3'] < -0.2:
                trend_allows_buy = False
                reasons.append("🔻 3 bougies en baisse")
        
        # Tendance GLOBALE très baissière = PAS D'ACHAT (sauf rebond)
        price_change_20 = features.get('price_change_20', 0)
        if price_change_20 < -0.02 and features['momentum_3'] < 0:
            # Baisse prolongée ET toujours en baisse = trop risqué
            trend_allows_buy = False
            reasons.append(f"🔴 Tendance 20min très baissière ({price_change_20:.1f}%)")
        
        # Prix très loin sous EMA21 ET en chute = pas encore le fond
        if features['ema_diff'] < -0.5 and features['ema_slope'] < -0.15 and features['momentum_3'] < -0.1:
            trend_allows_buy = False
            reasons.append("📉 Chute en cours, attendre stabilisation")
        
        # Combiner les deux conditions
        ema_allows_buy = ema_allows_buy and trend_allows_buy
        
        # ════════════════════════════════════════════════════════════════════
        # TOUTES LES STRATÉGIES BLOQUÉES SI TENDANCE BAISSIÈRE
        # ════════════════════════════════════════════════════════════════════
        if not ema_allows_buy:
            # Tendance non favorable - réduire le score
            # Mais garder un score suffisant pour signaler l'opportunité
            base_score = squeeze_breakout_score if squeeze_breakout_score > 0 else 30
            score = int(base_score * 0.6)  # Réduire de 40% seulement (au lieu de 60%)
            item.pattern = "NO_BUY_BEARISH"
        elif squeeze_strategy_valid and squeeze_breakout_score >= 40:
            # Si stratégie squeeze valide ET tendance OK
            score = squeeze_breakout_score
            item.pattern = "SQUEEZE_BREAKOUT" if features.get('breakout_strength', 0) > 0 else "SQUEEZE_SETUP"
        else:
            # ════════════════════════════════════════════════════════════════
            # STRATÉGIES SECONDAIRES (seulement si EMA permet l'achat)
            # ════════════════════════════════════════════════════════════════
            
            # 1. BOLLINGER SQUEEZE simple
            if features['bb_squeeze']:
                if features['ema_slope'] > 0:
                    score += 30
                    reasons.append("🔥 BB Squeeze + EMA montante")
                else:
                    score += 15
                    reasons.append("📊 BB Squeeze détecté")
            
            # 2. EMA CONFIGURATION
            if features['ema_diff'] < -0.5:  # EMA9 < EMA21 (creux profond)
                if features['ema_slope'] > 0:  # Mais EMA9 remonte
                    score += 30  # Augmenté de 25 à 30
                    reasons.append("📈 Creux EMA profond avec rebond")
                elif features['ema_slope'] > -0.3:  # Stabilisation
                    score += 20  # Augmenté de 15 à 20
                    reasons.append("⚖️ Stabilisation en creux")
            elif is_early_rebound:
                # NOUVEAU: Bonus spécial pour début de rebond détecté
                score += 35
                reasons.append("🚀 Signal précoce de rebond!")
            
            # 3. RSI - bonus si survendu ET tendance stable/haussière
            if features['rsi_oversold'] and features['ema_slope'] >= 0:
                # RSI survendu + EMA stable ou montante = bon signal
                score += 25
                reasons.append("📉 RSI survendu + EMA stable")
            elif features['rsi_oversold']:
                # RSI survendu mais EMA baissière = prudence
                score += 10
                reasons.append("📉 RSI survendu (prudence)")
            elif 30 < features['rsi'] < 50:
                score += 10
                reasons.append("✓ RSI neutre-bas")
        
        # 4. MOMENTUM (bonus si EMA permet l'achat ET momentum positif)
        if ema_allows_buy:
            # ═══════════════════════════════════════════════════════════════════
            # BONUS REBOND TECHNIQUE - NOUVEAU SYSTÈME DE SCORING
            # Détecte les rebonds après une baisse avec retournement confirmé
            # ═══════════════════════════════════════════════════════════════════
            
            ema_diff = features.get('ema_diff', 0)
            momentum_3 = features.get('momentum_3', 0)
            momentum_5 = features.get('momentum_5', 0)
            ema_slope = features.get('ema_slope', 0)
            rsi = features.get('rsi', 50)
            bb_position = features.get('bb_position', 0.5)
            
            # CAS 1: REBOND TECHNIQUE FORT (EMA9 > EMA21 + momentum positif + EMA montante)
            if ema_diff > 0 and momentum_3 > 0 and ema_slope > 0:
                # C'est le meilleur cas ! Tendance haussière confirmée
                base_bonus = 45  # Score de base élevé
                
                # Bonus supplémentaires
                if momentum_3 > 0.2:  # Momentum fort (> 0.20%)
                    base_bonus += 10
                    reasons.append(f"🚀 REBOND FORT! (mom=+{momentum_3:.2f}%)")
                else:
                    reasons.append(f"📈 Rebond technique (mom=+{momentum_3:.2f}%)")
                
                if rsi < 65:  # Pas encore suracheté
                    base_bonus += 5
                if rsi < 50:  # RSI bas = potentiel de hausse
                    base_bonus += 10
                    reasons.append(f"✅ RSI favorable ({rsi:.0f})")
                
                if bb_position < 0.5:  # Prix dans la moitié basse des BB
                    base_bonus += 5
                    reasons.append("🛡️ Prix bas dans BB")
                elif bb_position < 0.65 and momentum_3 > 0.25 and rsi < 65:
                    # 🆕 PATTERN GAGNANT: Momentum fort même en zone médiane
                    # Exemple: OPUSDT +1.95% (bb=0.57, mom=+0.26%, RSI=55.8)
                    # 🔧 OPTIMISÉ: momentum 0.20% → 0.25% (plus sélectif)
                    base_bonus += 8
                    reasons.append(f"🎯 Dynamique haussière forte (mom={momentum_3:.2%}, bb={bb_position:.2f})")
                
                score += base_bonus
            
            # CAS 2: DÉBUT DE REBOND (EMA9 juste passé au-dessus d'EMA21)
            elif ema_diff > 0 and ema_diff < 0.3 and (momentum_3 > 0 or ema_slope > 0):
                base_bonus = 40
                reasons.append(f"📈 Début de rebond (diff=+{ema_diff:.2f}%)")
                
                if momentum_3 > 0:
                    base_bonus += 10
                if rsi < 55:
                    base_bonus += 5
                
                score += base_bonus
            
            # CAS 3: EMA9 > EMA21 mais momentum neutre (prudence)
            elif ema_diff > 0:
                score += 25
                reasons.append(f"📊 EMA haussière (diff=+{ema_diff:.2f}%)")
            
            # CAS 4: RETOURNEMENT (hausse récente après baisse prolongée)
            if momentum_3 > 0 and momentum_5 < 0:
                score += 15
                reasons.append("🔄 Retournement en cours")
            elif momentum_5 > 0 and features.get('momentum_10', 0) < 0:
                score += 10
                reasons.append("🔄 Retournement confirmé")
            
            # 5. VOLUME (confirmation)
            if features['volume_ratio'] > 1.5:
                score += 10
                reasons.append("📊 Volume élevé")
            
            # 6. SUPPORT TECHNIQUE
            if features['near_support']:
                score += 10
                reasons.append("🛡️ Proche support")
            
            # 7. VOLATILITÉ FAIBLE (pré-explosion)
            if features['volatility'] < 0.5 and not features['bb_squeeze']:
                score += 10
                reasons.append("💤 Faible volatilité")
        
        # === PRÉDICTION IA (si modèle disponible) ===
        # L'IA ne peut PAS contourner la règle EMA9 < EMA21
        if self.model and TORCH_AVAILABLE and ema_allows_buy:
            try:
                ai_prediction = self._predict_with_model(prices)
                if ai_prediction:
                    # Ne pas écraser le score si stratégie squeeze active (score >= 40)
                    if squeeze_breakout_score < 40:
                        # Pondération classique pour les autres stratégies
                        score = int(score * 0.6 + ai_prediction['score'] * 0.4)
                    else:
                        # Pour squeeze, bonus IA si direction haussière, sinon garder le score
                        if ai_prediction['direction'] == 'up':
                            score = min(100, score + int(ai_prediction['score'] * 0.2))
                    
                    item.confidence = ai_prediction['confidence']
                    if ai_prediction['direction'] == 'up':
                        reasons.append(f"🤖 IA: Hausse ({ai_prediction['confidence']:.0f}%)")
            except Exception as e:
                logger.warning(f"Erreur prédiction IA: {e}")
        
        # === CALCUL DU GAIN PRÉDIT ===
        # Basé sur le BB bandwidth et le momentum historique
        if features['bb_squeeze']:
            item.predicted_gain = features['bb_bandwidth'] * 2  # Le squeeze libère ~2x son amplitude
        else:
            item.predicted_gain = abs(features['momentum_10']) * 0.5  # Estimation conservatrice
        
        # === COUNTDOWN ESTIMÉ ===
        # Temps estimé avant le signal (basé sur la proximité du croisement EMA)
        if features['ema_cross_distance'] < 0.3:
            item.countdown = 5  # Très proche, ~5 min
        elif features['ema_cross_distance'] < 0.8:
            item.countdown = 15  # Proche, ~15 min
        else:
            item.countdown = 30  # Plus loin, ~30 min
        
        # === FINALISATION ===
        item.score = min(100, max(0, score))
        item.reason = " | ".join(reasons) if reasons else "Aucun signal fort"
        item.pattern = self._identify_pattern(features)
        
        # ════════════════════════════════════════════════════════════════════════
        # 🔴 BLOCAGE END_OF_CYCLE - APPLIQUÉ APRÈS DÉTERMINATION DU PATTERN
        # Patterns autorisés exempts: PULLBACK, TREND_CONTINUATION, SQUEEZE_BREAKOUT, HIGH_SCORE_OVERRIDE
        # Ces patterns ont leurs propres sécurités et peuvent acheter en zone haute
        # ════════════════════════════════════════════════════════════════════════
        rsi = features.get('rsi', 50)
        bb_position = features.get('bb_position', 0.5)
        momentum_3 = features.get('momentum_3', 0)
        vol_ratio = features.get('volume_ratio', 1.0)
        ema_cross_bearish = features.get('ema_cross_bearish', 0)
        
        is_strong_rebound = (momentum_3 > 0.5 and vol_ratio > 1.5)
        is_end_of_cycle = (
            (rsi > 75 and not is_strong_rebound) or
            (rsi > 70 and bb_position > 1.0 and momentum_3 < 0.1) or
            (bb_position > 1.5) or
            (ema_cross_bearish == 1 and bb_position > 0.85) or
            (momentum_3 < -0.01 and bb_position > 0.8)
        )
        
        # Patterns autorisés à bypasser END_OF_CYCLE (achats valides en zone haute)
        # 🔴 FIX 08/02: TREND_CONTINUATION retiré (0% WR)
        allowed_high_patterns = ['PULLBACK', 'SQUEEZE_BREAKOUT', 'HIGH_SCORE_OVERRIDE']
        
        if is_end_of_cycle and item.pattern not in allowed_high_patterns:
            # 🔴 FIN DE CYCLE = BLOCAGE pour patterns non autorisés
            item.status = 'end_of_cycle_blocked'
            item.pattern = "END_OF_CYCLE"
            item.score = 0
            logger.error(f"🔴🔴🔴 {symbol}: FIN DE CYCLE DÉTECTÉ - RSI={rsi:.0f} BB_pos={bb_position:.2f} - ACHAT BLOQUÉ (pattern non autorisé)")
        elif is_end_of_cycle:
            # Pattern autorisé en zone haute - autoriser mais logger
            logger.info(f"✅ {symbol}: END_OF_CYCLE détecté mais pattern {item.pattern} autorisé - RSI={rsi:.0f} BB_pos={bb_position:.2f}")
        
        # ════════════════════════════════════════════════════════════════════════
        # BLOCAGE: Tendance baissière (crash brutal OU baisse progressive)
        # ════════════════════════════════════════════════════════════════════════
        ema_trend_bearish = features.get('ema_trend_bearish', 0)
        ema_slope = features.get('ema_slope', 0)
        ema_slope_long = features.get('ema_slope_long', 0)
        momentum_5 = features.get('momentum_5', 0)
        momentum_3 = features.get('momentum_3', 0)
        price_change_20 = features.get('price_change_20', 0)
        ema_diff = features.get('ema_diff', 0)
        
        # TYPE 1: Crash brutal (chute rapide)
        is_active_crash = (
            momentum_5 < -0.5 and momentum_3 < -0.2  # -0.5% et -0.2%
        )
        
        # TYPE 2: Tendance baissière prolongée (baisse progressive)
        # Détecte les baisses lentes mais constantes comme ATOM
        is_prolonged_downtrend = (
            ema_trend_bearish == 1 and              # EMA9 < EMA21
            ema_slope < 0 and                       # EMA9 en baisse
            (momentum_5 < -0.1 or momentum_3 < 0) and  # Momentum négatif (-0.1% ou <0)
            price_change_20 < -0.5                  # Baisse sur 20 bougies
        )
        
        # TYPE 3: EMA en baisse forte
        is_ema_declining = (
            ema_slope < -0.1 and                    # EMA9 baisse
            ema_slope_long < -0.05 and              # EMA21 baisse aussi
            momentum_3 < 0                          # Momentum négatif
        )
        
        # Combiner les 3 types de tendance baissière
        is_bearish = is_active_crash or is_prolonged_downtrend or is_ema_declining
        
        if is_bearish:
            item.status = "bearish_blocked"  # BLOQUÉ - tendance baissière
            if is_active_crash:
                item.pattern = "ACTIVE_CRASH"
            elif is_prolonged_downtrend:
                item.pattern = "BEARISH_TREND"
            else:
                item.pattern = "EMA_DECLINING"
            # RÉDUIRE LE SCORE pour refléter le risque
            item.score = int(item.score * 0.5)  # Diviser par 2
            logger.debug(f"{symbol}: BLOQUÉ tendance baissière ({item.pattern})")
        elif item.score >= self.SCORE_THRESHOLD:
            # 🔴 FIX 08/02: READY seulement si pattern d'achat valide
            _buyable = ['CREUX_REBOUND', 'PULLBACK', 'SQUEEZE_BREAKOUT', 'EARLY_BREAKOUT',
                        'CONSOLIDATION_BREAKOUT', 'EMA_BULLISH', 'CROSSOVER_IMMINENT',
                        'VOLUME_REVERSAL', 'RSI_REVERSAL', 'STRONG_UPTREND', 'HIGH_SCORE_OVERRIDE']
            if hasattr(item, 'pattern') and item.pattern in _buyable:
                item.status = "ready"  # Prêt à déclencher
            else:
                item.status = "watching"  # Score OK mais pas de pattern d'achat
        elif item.score >= 50:
            item.status = "watching"  # À surveiller
        else:
            item.status = "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
            
            prices = [float(k[4]) for k in klines]  # Close prices
            volumes = [float(k[5]) for k in 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
                        
                        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
                        is_creux_rebound_technical = is_creux and (
                            momentum_3_val > 0.01 and       # 🔴 0.05→0.01% (tout début rebond)
                            (mom_accel_val > 0 or momentum_3_val > 0.10) and  # Accélération OU momentum
                            (ema_slope_val > 0.05 or momentum_3_val > 0.08) and  # 🔴 0.2→0.05 (EMA lente!)
                            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.2 and        # 🔧 Élargi: -0.2% (was -0.1%) = détecter plus tôt
                            ema_slope_val > 0.5 and        # 🔧 Assoupli: 0.5% (was 1%) = détecter pentes douces
                            momentum_3_val > -0.2          # ET momentum pas en chute (> -0.2%)
                        )
                        
                        # 🔴 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         # 🔧 Élargi: < 0.5% (was 0.3%) = fenêtre plus large
                            ema_slope_val > 0.3 and        # 🔧 Assoupli: 0.3% (was 0.5%) = détecter pentes douces
                            momentum_3_val > 0.01 and      # 🔧 Assoupli: > 0.01% (was 0.02%)
                            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        # 🔧 FIX 09/02: Élargi -0.4% (was -0.3%) = capter XRP-like
                            ema_diff_val <= 0.2 and        # 🔧 FIX 09/02: Élargi +0.2% (was +0.1%)
                            ema_slope_val > 0.3 and        # 🔧 FIX 09/02: Assoupli 0.3% (was 0.5%)
                            momentum_3_val > 0.02 and      # 🔧 FIX 09/02: Assoupli 0.02% (was 0.05%)
                            rsi_val > 30 and               # 🔧 FIX 09/02: Assoupli 30 (was 35)
                            momentum_5_val > -0.3          # 🔧 FIX 09/02: Assoupli -0.3% (was -0.2%)
                        )
                        
                        # 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 10/03 BTC: TOP20 rebondissent depuis RSI plus élevé que les altcoins
                        # BTC/ETH: dip est souvent RSI 42-52, pas RSI < 40 comme altcoins illiquides
                        # Garde: EMA slope > 0 (EMA REMONTE vraiment) + momentum positif confirmé
                        if symbol in TOP_20_CRYPTOS and ema_slope_val > 0 and momentum_3_val > 0.10:
                            _rsi_creux_max = min(55, _rsi_creux_max + 10)  # +10pts max (BEAR: 40→50, NEUTRAL: 48→55)
                            logger.debug(f"⭐ {symbol}: TOP20 rebond — _rsi_creux_max assoupli à {_rsi_creux_max} (slope={ema_slope_val:+.2f}%)")
                        
                        # 🔥 PRIORITÉ #0: Pattern CREUX_REBOUND détecté = ACHAT IMMÉDIAT!
                        # 🔴 VALIDATION: Vérifier que les conditions sont ENCORE valides!
                        # Le pattern a sa propre validation stricte, ignorer Smart Criteria
                        # 🔧 FIX 27/01: RSI 15-50 au lieu de 15-40 (LTC raté à 08:34 RSI=32)
                        # 🔧 FIX 14/02: RSI 15-55 → 15-65 en BULL_STRONG (ETH raté RSI=63)
                        # 🔴 FIX 04/03: RSI adaptatif par régime (_rsi_creux_max) — trop de faux rebonds en NEUTRAL/BEAR
                        if item.pattern == 'CREUX_REBOUND' and rsi_val >= 15 and rsi_val <= _rsi_creux_max:
                            # 🆕 FIX 28/01 17h15 + FIX 01/03 + 🧠 LSTM + FIX 04/03: VALIDATION STRICTE DU REBOND
                            # 🔴 FIX 04/03: Protection "falling knife" — le marché doit s'être STABILISÉ
                            # Problème: micro-rebond 1 bougie (mom_accel>0 sur 1 scan) alors que le crash continue
                            # Solution: momentum_3 > 0.05% (au lieu de 0.01%) + mom5 pas en crash
                            # 🔴 FIX URGENT 09/03: Conditions BEAUCOUP PLUS STRICTES
                            # PROBLÈME RACINE: le bot achetait FIL/CHZ sur un seul micro-rebond
                            # (+0.05% mo3, mom5=-0.3%) pendant une baisse intraday continue.
                            # Le 3ème cas autorisait l'achat quand le prix était ENCORE EN BAISSE (mom3 > -0.05%)!
                            # SUPPRIMÉ: le cas "Stabilisation + forte accélération" (achat en baisse)
                            # DURCI: seuil minimum mom3 0.05→0.15%, mom5 -0.35→-0.15%
                            is_real_rebound = (
                                (momentum_3_val > 0.15 and mom_accel_val > 0 and momentum_5_val > -0.15) or  # 🔴 DURCI: Mom solide + accel + tendance 5m pas en crash
                                (momentum_3_val > 0.20 and momentum_5_val > -0.20) or  # Momentum fort + 5m pas en effondrement
                                (lstm_is_reversal and lstm_rev_confidence >= 70 and momentum_3_val > 0.05 and momentum_5_val > -0.20) or  # 🧠 LSTM strict + mom positif requis
                                (lstm_is_reversal and lstm_rev_confidence >= 90 and momentum_3_val > 0.10 and momentum_5_val > -3.0) or  # 🆕 LSTM très haute confiance: mom5 assoupli (rebond début = mom5 encore négatif)
                                (rsi_val <= 40 and momentum_3_val > 0.30 and momentum_5_val > -2.0 and mom_accel_val > 0)  # 🆕 RSI survendu + rebond solide + accélération (ex: ACX 18h46 Mom3=+0.5% RSI=38)
                            )
                            
                            if is_real_rebound:
                                smart_data['signal'] = 'ACHAT'
                                smart_data['eligible'] = True
                                item_data['smart_status'] = 'CREUX_REBOUND'
                                logger.info(f"✅ {symbol}: Pattern CREUX_REBOUND (RSI={rsi_val:.0f}/{_rsi_creux_max} Mom={momentum_3_val:.2%} regime={_regime}) → ACHAT")
                            else:
                                # Faux rebond = RSI bas mais baisse continue (falling knife)
                                logger.warning(f"🚫 {symbol}: CREUX_REBOUND INVALIDE - RSI={rsi_val:.0f} OK mais Mom3={momentum_3_val:.2%} Mom5={momentum_5_val:.2%} (marché non stabilisé)")
                                smart_data['signal'] = 'NO_BUY'
                                smart_data['eligible'] = False
                                item_data['smart_status'] = 'Faux rebond / falling knife'
                        # 🚫 Si pattern CREUX_REBOUND mais RSI hors zone 15-_rsi_creux_max → INVALIDE!
                        # 🔴 FIX 04/03: Seuil adaptatif par régime — NEUTRAL=48, BEAR=40
                        elif item.pattern == 'CREUX_REBOUND':
                            logger.warning(f"⚠️ {symbol}: Pattern CREUX_REBOUND INVALIDE (RSI={rsi_val:.0f} > seuil={_rsi_creux_max} regime={_regime}) → ACHAT BLOQUÉ")
                            smart_data['signal'] = 'NO_BUY'
                            smart_data['eligible'] = False
                            item_data['smart_status'] = 'Pattern invalide (RSI hors zone)'
                            # Pattern invalide = ne PAS acheter
                        # 🆕 FIX 28/01 14h50: BLOCAGE ACHATS TARDIFS - RSI déjà élevé
                        # 🔧 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:
                                smart_data['signal'] = 'ACHAT'
                                smart_data['eligible'] = True
                                # 🆕 FIX 01/03: Bonus volume + accélération = retournement confirmé
                                rev_bonus = 0
                                if vol_ratio_val > 1.3 and mom_accel_val > 0.1:
                                    rev_bonus = 10  # Volume fort + accélération = signal premium
                                elif vol_ratio_val > 1.2 or mom_accel_val > 0.15:
                                    rev_bonus = 5  # Volume OU accélération forte
                                if rev_bonus:
                                    smart_data['score'] = min(100, item.score + rev_bonus)
                                # 🆕 FIX 11/03: Logger l'état du cycle pour diagnostic
                                _cycle_info = f"vel_h1={_sc_vel_h1:.1f}%/h vel_h2={_sc_vel_h2:.1f}%/h drop5h={_sc_drop_5h:.1f}%"
                                if _sc_cycle_exhausting:
                                    item_data['smart_status'] = 'Creux + Rebond (fin cycle)'
                                    logger.info(f"✅ {symbol}: CREUX_REBOUND — essoufflement vendeur confirmé ({_cycle_info})")
                                else:
                                    item_data['smart_status'] = 'Creux + Rebond'
                                    logger.info(f"✅ {symbol}: CREUX_REBOUND → ACHAT ({_cycle_info})")
                            # 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 → PULLBACK
                        # 🔴 FIX 08/02: Était TREND_CONTINUATION (0% WR) → PULLBACK (pattern approprié)
                        elif 'FRESH CROSSOVER' in status:
                            item.pattern = 'PULLBACK'
                        
                        # 🧠 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']:
                                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']
                        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
        })
        
        # 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
    Intègre la gestion de blacklist dynamique
    """
    
    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 = {}
        
        # === BLACKLIST DYNAMIQUE ===
        self.blacklist_manager = None
        self._blacklist_update_counter = 0
        self._blacklist_update_interval = 30  # Mise à jour toutes les 30 itérations (~5 min)
        
        if DYNAMIC_BLACKLIST_AVAILABLE:
            try:
                self.blacklist_manager = get_blacklist_manager()
                logger.info("⛔ Gestionnaire de blacklist dynamique initialisé")
            except Exception as e:
                logger.warning(f"⚠️ Erreur init blacklist: {e}")
    
    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}")
                    
                    # Mise à jour périodique de la blacklist dynamique
                    self._update_blacklist_if_needed()
                    
                    self._analyze_all_symbols()
                    
                    # 🧠 LSTM REVERSAL: Entraînement online périodique (toutes les 30 min)
                    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...")
                                metrics = rev_pred.train_online()
                                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')}")
                        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 _update_blacklist_if_needed(self):
        """Met à jour la blacklist dynamique périodiquement - DÉSACTIVÉ"""
        # 🔴 FIX 27/01 19h: Désactivation de la blacklist automatique IA
        # L'IA sélectionne les valeurs normalement sans contrainte de blacklist dynamique
        # Seuls les stablecoins permanents restent exclus
        return
    
    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,
                'blacklisted_count': len(self.blacklist_manager.get_blacklist()) if self.blacklist_manager else 0,
                'timestamp': _dt.now().isoformat(),
                'pid': _os.getpid()
            }
            
            status_file = _os.path.join(_os.path.dirname(__file__), 'ia_status.json')
            with open(status_file, 'w', encoding='utf-8') as f:
                _json.dump(status, f, ensure_ascii=False, indent=2)
        except Exception as e:
            logger.debug(f"Erreur écriture ia_status.json: {e}")
    
    def is_symbol_blacklisted(self, symbol: str) -> tuple:
        """Vérifie si un symbole est blacklisté par l'IA"""
        if not self.blacklist_manager:
            return False, None
        return self.blacklist_manager.is_blacklisted(symbol)
    
    def get_blacklist_status(self) -> dict:
        """Retourne le statut de la blacklist"""
        if not self.blacklist_manager:
            return {'enabled': False, 'blacklist': {}, 'whitelist': []}
        return {
            'enabled': True,
            'blacklist': self.blacklist_manager.get_blacklist(),
            'whitelist': self.blacklist_manager.whitelist,
            'total_blacklisted': len(self.blacklist_manager.get_blacklist())
        }
    
    def blacklist_action(self, action: str, symbol: str = None, **kwargs) -> dict:
        """Effectue une action sur la blacklist"""
        if not self.blacklist_manager:
            return {'success': False, 'error': 'Blacklist non disponible'}
        
        try:
            if action == 'add' and symbol:
                reason = kwargs.get('reason', 'Manual')
                hours = kwargs.get('hours', 6)
                self.blacklist_manager.force_blacklist(symbol, reason, hours)
                return {'success': True, 'message': f'{symbol} blacklisté pour {hours}h'}
            
            elif action == 'remove' and symbol:
                self.blacklist_manager.force_unblacklist(symbol)
                return {'success': True, 'message': f'{symbol} retiré de la blacklist'}
            
            elif action == 'whitelist' and symbol:
                self.blacklist_manager.add_to_whitelist(symbol)
                return {'success': True, 'message': f'{symbol} protégé (whitelist)'}
            
            elif action == 'unwhitelist' and symbol:
                self.blacklist_manager.remove_from_whitelist(symbol)
                return {'success': True, 'message': f'{symbol} retiré de la whitelist'}
            
            elif action == 'clear':
                count = self.blacklist_manager.clear_blacklist()
                return {'success': True, 'message': f'Blacklist vidée ({count} symboles)'}
            
            elif action == 'update':
                result = self.blacklist_manager.update_blacklist()
                return {'success': True, 'result': result}
            
            elif action == 'report':
                return {'success': True, 'report': self.blacklist_manager.get_performance_report()}
            
            else:
                return {'success': False, 'error': f'Action inconnue: {action}'}
                
        except Exception as e:
            return {'success': False, 'error': str(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
                
                prices = [float(k[4]) for k in klines]
                volumes = [float(k[5]) for k in klines]
                
                item = self.predictor.analyze_symbol(symbol, prices, volumes)
                return (symbol, item)
            except Exception as e:
                logger.debug(f"Erreur {symbol}: {e}")
                return None
        
        self.current_phase = 'SCORING'
        
        # Analyse en parallèle (12 workers — FIX 27/02: 8→12 pour cycles plus rapides)
        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")
            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)
            # Atomic replace — empêche le dashboard de lire un fichier partiellement écrit
            # os.replace() est atomique sur Windows NTFS
            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': {},
            'blacklist': {'count': 0, 'symbols': [], 'details': {}},
            'blacklisted_symbols': [],
            '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")
        
        # ══════════════════════════════════════════════════════════════
        # BLACKLIST DYNAMIQUE: Récupérer les symboles blacklistés
        # ══════════════════════════════════════════════════════════════
        blacklisted_symbols = set()
        if self.blacklist_manager:
            blacklisted_symbols = set(self.blacklist_manager.get_blacklist().keys())
        
        # ══════════════════════════════════════════════════════════════
        # 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', '')
            
            # ⛔ BLACKLIST CHECK: Rejeter les symboles blacklistés
            if symbol in blacklisted_symbols:
                return False
            
            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 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
        # ══════════════════════════════════════════════════════════════
        MIN_SCORE_READY = 65  # 🔴 FIX v6b COHÉRENCE: 60→65 (aligné avec bot MIN_SCORE_ABSOLUTE=65)
        MIN_SCORE_POSSIBLE = 65  # POSSIBLE non utilisé en pratique (filtré par is_valid_buy_signal)
        
        # 🔴 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 symbol in blacklisted_symbols:
                    logger.info(f"🚫 {symbol}: Rejeté (blacklisted)")
                elif 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}")
        
        # ══════════════════════════════════════════════════════════════
        # BLACKLIST STATUS: Préparer les infos pour le dashboard
        # ══════════════════════════════════════════════════════════════
        blacklist_info = {}
        if self.blacklist_manager:
            bl_data = self.blacklist_manager.get_blacklist()
            details = {}
            for sym, data in bl_data.items():
                expires_at = data.get('expires_at', '')
                expires_in = 0
                if expires_at:
                    try:
                        from datetime import datetime
                        if isinstance(expires_at, str):
                            exp_dt = datetime.fromisoformat(expires_at)
                            expires_in = int((exp_dt - datetime.now()).total_seconds() / 60)
                        else:
                            expires_in = int((expires_at - time.time()) / 60)
                    except:
                        expires_in = 0
                details[sym] = {
                    'reason': data.get('reason', 'unknown'),
                    'expires_in': max(0, expires_in)  # minutes restantes
                }
            blacklist_info = {
                'count': len(bl_data),
                'symbols': list(bl_data.keys()),
                'details': details
            }
        
        # Filtrer les symboles blacklistés de top_opportunities
        filtered_opportunities = [
            w for w in valid_opportunities 
            if w.get('symbol', '') not in blacklisted_symbols
        ]
        
        # Marquer les symboles blacklistés dans la watchlist pour le dashboard
        for w in watchlist:
            symbol = w.get('symbol', '')
            w['is_blacklisted'] = symbol in blacklisted_symbols
        
        # ══════════════════════════════════════════════════════════════
        # 🧠 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']]
        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: TOUJOURS lire depuis watchlist.json (source de vérité)
        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)
                    total_symbols = len(watchlist_data.get('symbols', []))
        except:
            # Fallback: utiliser symbols_to_watch ou watchlist analysée
            total_symbols = len(self.symbols_to_watch) if self.symbols_to_watch else len(watchlist)
        
        # 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,
            # 🔧 FIX 28/02: Utiliser total_symbols (watchlist.json) au lieu de len(watchlist)
            # Le predictor interne grossit avec les symboles SPY injectés (+20 tokens)
            # Ce qui donnait 83 "analysés" alors que le bot ne gère que 62 symboles
            # La valeur sera mise à jour par _analyze_all_symbols avec le vrai compteur
            # 🔧 FIX: analyzed ne peut jamais dépasser total_symbols (FILTRAGE ≤ SCAN)
            # Évite l'incohérence après MAJ Binance (symbols_to_watch lag vs watchlist.json)
            'analyzed': min(self.last_smart_summary.get('analyzed', total_symbols), 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': filtered_opportunities[:10],
            'ready_signals': ready,
            'rotation_opportunities': rotation_opportunities[:5],
            'rotation_status': rotation_status,
            'blacklist': blacklist_info,
            'blacklisted_symbols': list(blacklisted_symbols),
            # 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}")
