# -*- coding: utf-8 -*-
"""
Bot de Trading Crypto avec Ordres Automatiques
===============================================
⚠️ ATTENTION: Ce bot peut passer des ordres RÉELS si TESTNET_MODE = False
Commencer TOUJOURS en mode TESTNET !
"""

import os
import sys
import asyncio
import json
import time
import hmac
import hashlib
from datetime import datetime
from collections import deque
from urllib.parse import urlencode
import numpy as np
import logging

# Protection d'urgence contre marchés chaotiques
from market_safety import get_market_safety

# Gestionnaire centralisé des patterns de trading
from pattern_manager import get_pattern_manager

# ═══════════════════════════════════════════════════════════════════════════════
# CONFIGURATION DU LOGGING
# ═══════════════════════════════════════════════════════════════════════════════
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_FILE = os.path.join(SCRIPT_DIR, 'trading_bot.log')

# Configurer le logging pour écrire dans le fichier
class FileLogger:
    """Redirige stdout vers fichier uniquement (évite les erreurs Unicode Windows)"""
    def __init__(self, filename, max_size_mb=50):
        # Mode 'a' (append) pour ne pas perdre l'historique en cas de restart
        self.filename = filename
        self.max_size = max_size_mb * 1024 * 1024
        self._rotate_if_needed()
        self.log = open(filename, 'a', encoding='utf-8', buffering=1)  # Line buffered, APPEND
        # Séparateur de session pour faciliter le debug
        self.log.write(f"\n{'='*80}\n")
        self.log.write(f"=== NOUVELLE SESSION — {__import__('datetime').datetime.now().isoformat()} ===\n")
        self.log.write(f"{'='*80}\n\n")
        self.log.flush()
        
    def _rotate_if_needed(self):
        """Rotation si le fichier dépasse max_size_mb"""
        try:
            if os.path.exists(self.filename) and os.path.getsize(self.filename) > self.max_size:
                backup = self.filename + '.prev'
                if os.path.exists(backup):
                    os.remove(backup)
                os.rename(self.filename, backup)
        except:
            pass
        
    def write(self, message):
        try:
            self.log.write(message)
            self.log.flush()
        except:
            pass
        
    def flush(self):
        try:
            self.log.flush()
        except:
            pass

# Activer le logging fichier (sans console pour éviter les erreurs Unicode Windows)
sys.stdout = FileLogger(LOG_FILE)
sys.stderr = sys.stdout

try:
    import websockets
    import requests
except ImportError:
    import subprocess
    _pip_flags = 0
    if sys.platform == 'win32':
        _pip_flags = 0x08000000  # CREATE_NO_WINDOW
    subprocess.check_call(
        [sys.executable, '-m', 'pip', 'install', 'websockets', 'requests'],
        creationflags=_pip_flags
    )
    import websockets
    import requests

from config import *

# Import du TradeLogger pour analyse
try:
    from trade_logger import get_trade_logger
    TRADE_LOGGER_AVAILABLE = True
except ImportError:
    TRADE_LOGGER_AVAILABLE = False
    print("⚠️ Module trade_logger non disponible")

# Import du module IA de prédiction
try:
    from ai_predictor import get_ai_predictor, get_surveillance_service, AIPredictor, get_smart_rotation, SMART_ROTATION_AVAILABLE
    AI_PREDICTOR_AVAILABLE = True
except ImportError:
    AI_PREDICTOR_AVAILABLE = False
    SMART_ROTATION_AVAILABLE = False
    print("⚠️ Module ai_predictor non disponible")

# Import du module IA prédictive de VENTE
try:
    from ai_sell_predictor import get_sell_predictor, AISellPredictor
    AI_SELL_PREDICTOR_AVAILABLE = True
except ImportError:
    AI_SELL_PREDICTOR_AVAILABLE = False
    print("⚠️ Module ai_sell_predictor non disponible")

# Import du système d'analyse de marché
try:
    from market_regime import MarketRegimeDetector
    MARKET_REGIME_AVAILABLE = True
except ImportError:
    MARKET_REGIME_AVAILABLE = False
    print("⚠️ Module market_regime non disponible")

# Import du module FreqAI (Auto-Retraining + Outlier Detection)
try:
    from freqai_integration import get_freqai_manager
    FREQAI_AVAILABLE = True
except ImportError:
    FREQAI_AVAILABLE = False
    print("⚠️ Module freqai_integration non disponible")

# ═══════════════════════════════════════════════════════════════════════════════
# CONVERSION DEVISES
# ═══════════════════════════════════════════════════════════════════════════════

class CurrencyConverter:
    """Convertisseur USD/EUR avec cache"""
    
    def __init__(self):
        self.rate = 1.0
        self.last_update = 0
        self.update_interval = 300  # 5 minutes
        self.symbol = "€" if DISPLAY_CURRENCY == "EUR" else "$"
    
    def update_rate(self):
        """Met à jour le taux EUR/USD"""
        if DISPLAY_CURRENCY != "EUR":
            self.rate = 1.0
            return
        
        if time.time() - self.last_update < self.update_interval:
            return
        
        try:
            # Utiliser l'API Binance pour obtenir EUR/USDT
            response = requests.get("https://api.binance.com/api/v3/ticker/price", 
                                   params={"symbol": "EURUSDT"}, timeout=5)
            data = response.json()
            # EUR/USDT = combien de USDT pour 1 EUR, donc rate = 1/prix
            self.rate = 1 / float(data['price'])
            self.last_update = time.time()
        except:
            # Taux par défaut si l'API échoue
            self.rate = 0.95
    
    def convert(self, usd_amount):
        """Convertit USD en devise d'affichage"""
        self.update_rate()
        return usd_amount * self.rate
    
    def format(self, usd_amount, decimals=3):
        """Formate un montant en devise d'affichage"""
        converted = self.convert(usd_amount)
        if decimals == 0:
            return f"{converted:,.0f}{self.symbol}"
        return f"{converted:,.{decimals}f}{self.symbol}"

# Instance globale
currency = CurrencyConverter()

# ═══════════════════════════════════════════════════════════════════════════════
# CLIENT API BINANCE
# ═══════════════════════════════════════════════════════════════════════════════

class BinanceClient:
    """Client pour l'API Binance (Spot)"""
    
    # Décalage d'horloge global (synchronisé avec le serveur)
    TIME_OFFSET = 0
    _last_sync = 0  # Timestamp de la dernière synchro
    
    def __init__(self, api_key="", api_secret="", testnet=True):
        self.api_key = api_key
        self.api_secret = api_secret
        self.testnet = testnet
        
        if testnet:
            self.base_url = "https://testnet.binance.vision"
            print("   📡 Mode TESTNET (argent fictif)")
        else:
            self.base_url = "https://api.binance.com"
            print("   ⚠️ Mode PRODUCTION (argent réel)")
        
        # Synchroniser l'horloge avec le serveur au démarrage
        self._sync_server_time()
    
    def _sync_server_time(self):
        """Synchronise l'horloge locale avec le serveur Binance"""
        try:
            response = requests.get(f"{self.base_url}/api/v3/time", timeout=5)
            server_time = response.json()['serverTime']
            local_time = int(time.time() * 1000)
            BinanceClient.TIME_OFFSET = server_time - local_time
            BinanceClient._last_sync = time.time()
            if abs(BinanceClient.TIME_OFFSET) > 500:
                print(f"   ⏰ Horloge synchronisée (décalage: {BinanceClient.TIME_OFFSET}ms)")
        except Exception as e:
            print(f"   ⚠️  Impossible de synchroniser l'horloge: {e}")
            BinanceClient.TIME_OFFSET = 0
    
    def _sign(self, params):
        """Signe une requête avec HMAC SHA256"""
        query_string = urlencode(params)
        signature = hmac.new(
            self.api_secret.encode('utf-8'),
            query_string.encode('utf-8'),
            hashlib.sha256
        ).hexdigest()
        return signature
    
    def _request(self, method, endpoint, params=None, signed=False):
        """Effectue une requête API"""
        url = f"{self.base_url}{endpoint}"
        headers = {"X-MBX-APIKEY": self.api_key}
        
        if params is None:
            params = {}
        
        if signed:
            # Re-synchroniser toutes les 5 minutes pour éviter la dérive
            if time.time() - BinanceClient._last_sync > 300:
                self._sync_server_time()
            
            # Utiliser le timestamp synchronisé avec le serveur
            params['timestamp'] = int(time.time() * 1000) + BinanceClient.TIME_OFFSET
            params['recvWindow'] = 10000  # 10s de tolérance (défaut Binance = 5s)
            params['signature'] = self._sign(params)
        
        try:
            if method == "GET":
                response = requests.get(url, params=params, headers=headers, timeout=15)
            elif method == "POST":
                response = requests.post(url, params=params, headers=headers, timeout=15)
            elif method == "DELETE":
                response = requests.delete(url, params=params, headers=headers, timeout=15)
            
            data = response.json()
            
            if 'code' in data and data['code'] < 0:
                # 🔧 FIX: Re-synchroniser automatiquement sur erreur timestamp
                if data['code'] in (-1021, -1022):
                    print(f"   ⏰ Erreur timestamp ({data['code']}), re-synchronisation...")
                    self._sync_server_time()
                    # Retenter une fois avec le nouveau timestamp
                    if signed:
                        params['timestamp'] = int(time.time() * 1000) + BinanceClient.TIME_OFFSET
                        params.pop('signature', None)
                        params['signature'] = self._sign(params)
                    
                    if method == "GET":
                        response = requests.get(url, params=params, headers=headers, timeout=15)
                    elif method == "POST":
                        response = requests.post(url, params=params, headers=headers, timeout=15)
                    elif method == "DELETE":
                        response = requests.delete(url, params=params, headers=headers, timeout=15)
                    
                    data = response.json()
                    if 'code' in data and data['code'] < 0:
                        print(f"   ❌ Erreur API (après resync): {data['msg']}")
                        return None
                    return data
                
                print(f"   ❌ Erreur API: {data['msg']}")
                return None
            
            return data
        except Exception as e:
            print(f"   ❌ Erreur requête: {e}")
            return None
    
    # ─── Informations compte ───────────────────────────────────────────────
    
    def get_account(self):
        """Récupère les informations du compte"""
        return self._request("GET", "/api/v3/account", signed=True)
    
    def get_balance(self, asset="USDT"):
        """Récupère le solde d'un asset"""
        account = self.get_account()
        if account:
            for balance in account.get('balances', []):
                if balance['asset'] == asset:
                    return {
                        'free': float(balance['free']),
                        'locked': float(balance['locked'])
                    }
        return {'free': 0, 'locked': 0}
    
    # ─── Prix et marché ────────────────────────────────────────────────────
    
    def get_price(self, symbol):
        """Récupère le prix actuel"""
        data = self._request("GET", "/api/v3/ticker/price", {"symbol": symbol})
        if data:
            return float(data['price'])
        return None
    
    def get_all_prices(self):
        """Récupère tous les prix en une seule requête (beaucoup plus rapide)"""
        data = self._request("GET", "/api/v3/ticker/price")
        if data:
            return {item['symbol']: float(item['price']) for item in data}
        return {}
    
    def get_top_volume_symbols(self, top_n=60, quote_assets=('USDT', 'USDC'),
                               exclude_stable=True):
        """Retourne les top_n paires par quoteVolume 24h depuis l'API publique Binance.

        Utilisé pour la surveillance IA dynamique (pas d'authentification requise).
        """
        _STABLES = {'BUSD', 'TUSD', 'USDC', 'FDUSD', 'DAI', 'EUR', 'GBP', 'BRL',
                    'UST', 'USDP', 'PAXB', 'SUSD', 'UNIUSD', 'USD1', 'USDX', 'USDD'}
        try:
            import requests as _req
            resp = _req.get('https://api.binance.com/api/v3/ticker/24hr',
                            timeout=10)
            if resp.status_code != 200:
                return []
            tickers = resp.json()
            filtered = []
            for t in tickers:
                sym = t.get('symbol', '')
                # Garder uniquement les paires des quotes demandées
                matched_quote = None
                for q in quote_assets:
                    if sym.endswith(q):
                        matched_quote = q
                        break
                if not matched_quote:
                    continue
                base = sym[:-len(matched_quote)]
                # Exclure les stablecoins vs stablecoins
                if exclude_stable and base in _STABLES:
                    continue
                # Exclure les tokens de levier (3L/3S/UP/DOWN) et noms non-ASCII
                if any(base.endswith(s) for s in ('3L', '3S', 'UP', 'DOWN', 'BULL', 'BEAR')):
                    continue
                if not base.isascii():
                    continue
                try:
                    vol = float(t.get('quoteVolume', 0))
                except (ValueError, TypeError):
                    vol = 0
                filtered.append((sym, vol))
            # Trier par volume décroissant, prendre les top_n
            filtered.sort(key=lambda x: x[1], reverse=True)
            return [sym for sym, _ in filtered[:top_n]]
        except Exception as e:
            logger.warning(f"⚠️ get_top_volume_symbols: {e}")
            return []

    def get_klines(self, symbol, interval="5m", limit=100):
        """Récupère les bougies historiques"""
        params = {"symbol": symbol, "interval": interval, "limit": limit}
        return self._request("GET", "/api/v3/klines", params)
    
    def get_klines_production(self, symbol, interval="5m", limit=100, use_cache=True, cache_ttl=60):
        """
        Récupère les klines depuis l'API PRODUCTION (publique).
        Le testnet n'a souvent pas assez de données de trading.
        cache_ttl: durée de validité du cache en secondes (60s par défaut,
                   utiliser 20-30s pour les checks de validation pré-achat)
        """
        import time as _time
        
        # Initialiser le cache si besoin
        if not hasattr(self, '_klines_cache'):
            self._klines_cache = {}
            self._klines_cache_time = {}
        
        cache_key = f"{symbol}_{interval}_{limit}"
        
        # Vérifier le cache (TTL paramétrique: 60s default, 20-30s pour validation pré-achat)
        if use_cache and cache_key in self._klines_cache:
            cache_age = _time.time() - self._klines_cache_time.get(cache_key, 0)
            if cache_age < cache_ttl:
                return self._klines_cache[cache_key]
        
        try:
            import requests as req
            url = f"https://api.binance.com/api/v3/klines"
            params = {"symbol": symbol, "interval": interval, "limit": limit}
            resp = req.get(url, params=params, timeout=15)  # Timeout augmenté pour API lente
            if resp.status_code == 200:
                data = resp.json()
                if isinstance(data, list) and len(data) > 0:
                    # Stocker dans le cache
                    self._klines_cache[cache_key] = data
                    self._klines_cache_time[cache_key] = _time.time()
                    return data
        except Exception as e:
            pass  # Silencieux
        
        # Retourner le cache expiré plutôt que None
        if cache_key in self._klines_cache:
            return self._klines_cache[cache_key]
        return None
    
    def get_symbol_info(self, symbol):
        """Récupère les infos d'un symbole (précision, etc.)"""
        data = self._request("GET", "/api/v3/exchangeInfo", {"symbol": symbol})
        if data and 'symbols' in data and len(data['symbols']) > 0:
            return data['symbols'][0]
        return None
    
    def get_quantity_precision(self, symbol):
        """Récupère la précision de quantité pour un symbole"""
        info = self.get_symbol_info(symbol)
        if info:
            for f in info.get('filters', []):
                if f['filterType'] == 'LOT_SIZE':
                    step_size = f['stepSize']
                    # Calculer le nombre de décimales à partir de stepSize
                    if '.' in step_size:
                        decimals = len(step_size.rstrip('0').split('.')[1])
                        return decimals
                    return 0
        # Valeur par défaut sûre
        return 0
    
    def format_quantity(self, symbol, quantity):
        """Formate la quantité selon la précision du symbole"""
        precision = self.get_quantity_precision(symbol)
        # Arrondir vers le bas pour éviter les dépassements
        factor = 10 ** precision
        formatted = int(quantity * factor) / factor
        # Si précision 0, retourner un entier
        if precision == 0:
            return int(formatted)
        return formatted
    
    def get_min_order_size(self, symbol):
        """
        Récupère les contraintes minimales pour un ordre (LOT_SIZE, MIN_NOTIONAL)
        
        Returns:
            dict: {
                'min_qty': quantité minimum,
                'step_size': pas de quantité,
                'min_notional': valeur minimum en USDT,
                'can_trade': True si on peut trader avec position_size donné
            }
        """
        info = self.get_symbol_info(symbol)
        result = {
            'min_qty': 0,
            'step_size': 0,
            'min_notional': 5,  # Valeur par défaut Binance
            'can_trade': True
        }
        
        if info:
            for f in info.get('filters', []):
                if f['filterType'] == 'LOT_SIZE':
                    result['min_qty'] = float(f.get('minQty', 0))
                    result['step_size'] = float(f.get('stepSize', 0))
                elif f['filterType'] == 'NOTIONAL' or f['filterType'] == 'MIN_NOTIONAL':
                    result['min_notional'] = float(f.get('minNotional', 5))
        
        return result
    
    def can_trade_symbol(self, symbol, usdt_amount):
        """
        Vérifie si on peut trader un symbole avec le montant donné
        
        Returns:
            tuple: (can_trade: bool, reason: str or None)
        """
        try:
            price = self.get_price(symbol)
            if not price or price <= 0:
                return False, "Prix non disponible"
            
            constraints = self.get_min_order_size(symbol)
            
            # Vérifier MIN_NOTIONAL
            if usdt_amount < constraints['min_notional']:
                return False, f"Montant {usdt_amount}€ < min_notional {constraints['min_notional']}€"
            
            # Calculer la quantité qu'on obtiendrait
            estimated_qty = usdt_amount / price
            
            # Vérifier LOT_SIZE minimum
            if estimated_qty < constraints['min_qty']:
                return False, f"Quantité {estimated_qty:.8f} < min_qty {constraints['min_qty']}"
            
            return True, None
        except Exception as e:
            return False, f"Erreur vérification: {e}"
    
    # ─── Ordres ────────────────────────────────────────────────────────────
    
    def create_order(self, symbol, side, order_type, quantity=None, 
                     quote_quantity=None, price=None, stop_price=None):
        """
        Crée un ordre
        
        Args:
            symbol: Paire (ex: BTCUSDT)
            side: BUY ou SELL
            order_type: MARKET, LIMIT, STOP_LOSS_LIMIT, TAKE_PROFIT_LIMIT
            quantity: Quantité en base asset (ex: 0.001 BTC)
            quote_quantity: Quantité en quote asset (ex: 100 USDT)
            price: Prix limite
            stop_price: Prix de déclenchement pour stop orders
        """
        params = {
            "symbol": symbol,
            "side": side,
            "type": order_type
        }
        
        if quantity:
            # Formatter selon le type (entier ou float)
            if isinstance(quantity, int):
                params["quantity"] = str(quantity)
            else:
                params["quantity"] = f"{quantity:.8f}".rstrip('0').rstrip('.')
        elif quote_quantity:
            params["quoteOrderQty"] = f"{quote_quantity:.2f}"
        
        if order_type == "LIMIT":
            params["timeInForce"] = "GTC"
            params["price"] = f"{price:.2f}"
        
        if stop_price:
            params["stopPrice"] = f"{stop_price:.2f}"
        
        return self._request("POST", "/api/v3/order", params, signed=True)
    
    def market_buy(self, symbol, usdt_amount):
        """Achat au marché avec un montant en USDT"""
        return self.create_order(symbol, "BUY", "MARKET", quote_quantity=usdt_amount)
    
    def market_sell(self, symbol, quantity):
        """Vente au marché"""
        return self.create_order(symbol, "SELL", "MARKET", quantity=quantity)
    
    def limit_buy(self, symbol, quantity, price):
        """Achat limite"""
        return self.create_order(symbol, "BUY", "LIMIT", quantity=quantity, price=price)
    
    def limit_sell(self, symbol, quantity, price):
        """Vente limite"""
        return self.create_order(symbol, "SELL", "LIMIT", quantity=quantity, price=price)
    
    def cancel_order(self, symbol, order_id):
        """Annule un ordre"""
        params = {"symbol": symbol, "orderId": order_id}
        return self._request("DELETE", "/api/v3/order", params, signed=True)
    
    def get_open_orders(self, symbol=None):
        """Récupère les ordres ouverts"""
        params = {}
        if symbol:
            params["symbol"] = symbol
        return self._request("GET", "/api/v3/openOrders", params, signed=True)

# ═══════════════════════════════════════════════════════════════════════════════
# INDICATEURS TECHNIQUES (AMÉLIORÉS)
# ═══════════════════════════════════════════════════════════════════════════════

class TechnicalIndicators:
    @staticmethod
    def rsi(prices, period=14):
        if len(prices) < period + 1:
            return None
        prices = np.array(prices, dtype=float)
        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:])
        
        # 🔧 FIX 07/02: Protection division par zéro avec valeurs RÉALISTES
        # Les anciennes valeurs 5/95 créaient des faux RSI TRAP (50→5 = "chute 45pts")
        # qui déclenchaient 90% des quick-exits pour rien
        if avg_loss == 0 and avg_gain == 0:
            return 50.0  # Marché stagnant = RSI neutre
        if avg_loss == 0:
            return 70.0  # Tendance haussière mais pas extrême (était 95 → faux RSI TRAP)
        if avg_gain == 0:
            return 30.0  # Tendance baissière mais pas extrême (était 5 → faux RSI TRAP)
            
        rs = avg_gain / avg_loss
        return 100 - (100 / (1 + rs))
    
    @staticmethod
    def ema(prices, period):
        if len(prices) < period:
            return None
        multiplier = 2 / (period + 1)
        ema = prices[0]
        for price in prices[1:]:
            ema = (price * multiplier) + (ema * (1 - multiplier))
        return ema
    
    @staticmethod
    def bollinger(prices, period=20, std_dev=2):
        if len(prices) < period:
            return None, None, None
        prices = np.array(prices[-period:])
        sma = np.mean(prices)
        std = np.std(prices)
        return sma + (std_dev * std), sma, sma - (std_dev * std)
    
    @staticmethod
    def momentum(prices, period=10):
        """Calcule le momentum (variation % sur la période)"""
        if len(prices) < period + 1:
            return None
        return ((prices[-1] - prices[-period]) / prices[-period]) * 100
    
    @staticmethod
    def trend_strength(prices, short_period=5, long_period=20):
        """Mesure la force de la tendance (0-100)"""
        if len(prices) < long_period:
            return None, None
        
        # Calcul des EMA
        ema_short = TechnicalIndicators.ema(prices, short_period)
        ema_long = TechnicalIndicators.ema(prices, long_period)
        
        if not ema_short or not ema_long:
            return None, None
        
        # Direction: positif = haussier, négatif = baissier
        spread = ((ema_short - ema_long) / ema_long) * 100
        
        # Force basée sur la pente des dernières bougies
        recent_prices = prices[-5:]
        if len(recent_prices) >= 5:
            slope = (recent_prices[-1] - recent_prices[0]) / recent_prices[0] * 100
        else:
            slope = 0
        
        # Combinaison spread + slope pour force totale
        strength = min(100, abs(spread * 10) + abs(slope * 5))
        direction = "bullish" if spread > 0 else "bearish"
        
        return strength, direction
    
    @staticmethod
    def pullback_detection(prices, ema_period=7):
        """Détecte un pullback dans une tendance haussière (opportunité d'entrée)"""
        if len(prices) < ema_period + 5:
            return False, 0
        
        ema = TechnicalIndicators.ema(prices, ema_period)
        if not ema:
            return False, 0
        
        current_price = prices[-1]
        prev_price = prices[-2]
        
        # Pullback = prix touche ou passe sous l'EMA après être au-dessus
        price_near_ema = abs(current_price - ema) / ema < 0.005  # Dans 0.5% de l'EMA
        was_above = prev_price > ema
        
        # Distance en % par rapport à l'EMA
        distance_pct = ((current_price - ema) / ema) * 100
        
        return price_near_ema and was_above, distance_pct
    
    @staticmethod
    def bollinger_trend(prices, period=20, std_dev=2):
        """Analyse la tendance des bandes de Bollinger
        Retourne: direction ('up', 'down', 'flat'), bandwidth expansion
        """
        if len(prices) < period + 5:
            return None, None, None
        
        # Calculer BB actuel et précédent
        bb_upper, bb_mid, bb_lower = TechnicalIndicators.bollinger(prices, period, std_dev)
        bb_upper_prev, bb_mid_prev, bb_lower_prev = TechnicalIndicators.bollinger(prices[:-3], period, std_dev)
        
        if not all([bb_upper, bb_mid, bb_lower, bb_mid_prev]):
            return None, None, None
        
        # Direction de la bande centrale
        mid_slope = ((bb_mid - bb_mid_prev) / bb_mid_prev) * 100
        
        # Bandwidth expansion/contraction
        bandwidth = (bb_upper - bb_lower) / bb_mid * 100
        bandwidth_prev = (bb_upper_prev - bb_lower_prev) / bb_mid_prev * 100 if bb_mid_prev else bandwidth
        
        if mid_slope > 0.3:
            direction = 'up'
        elif mid_slope < -0.3:
            direction = 'down'
        else:
            direction = 'flat'
        
        expansion = bandwidth > bandwidth_prev * 1.05  # 5% plus large
        
        return direction, expansion, mid_slope
    
    @staticmethod
    def bollinger_squeeze(prices, period=20, std_dev=2, squeeze_threshold=3.0):
        """Détecte un Bollinger Squeeze (compression des bandes = faible volatilité)
        Un squeeze précède souvent une explosion du prix (hausse ou baisse forte)
        
        Retourne: is_squeeze, bandwidth, squeeze_strength, breakout_direction
        - is_squeeze: True si les bandes sont compressées
        - bandwidth: largeur actuelle des bandes en %
        - squeeze_strength: 0-100, plus c'est haut plus le squeeze est serré
        - breakout_direction: 'up' si le prix commence à casser vers le haut, 'down' sinon, None si pas de breakout
        """
        if len(prices) < period + 10:
            return False, 0, 0, None
        
        # Calculer BB actuel
        bb_upper, bb_mid, bb_lower = TechnicalIndicators.bollinger(prices, period, std_dev)
        
        if not all([bb_upper, bb_mid, bb_lower]) or bb_mid == 0:
            return False, 0, 0, None
        
        # Bandwidth actuel (largeur des bandes en %)
        bandwidth = (bb_upper - bb_lower) / bb_mid * 100
        
        # Calculer le bandwidth moyen sur les 20 dernières bougies pour comparaison
        bandwidths = []
        for i in range(10, min(30, len(prices))):
            bb_u, bb_m, bb_l = TechnicalIndicators.bollinger(prices[:-i] if i > 0 else prices, period, std_dev)
            if bb_m and bb_m > 0:
                bw = (bb_u - bb_l) / bb_m * 100
                bandwidths.append(bw)
        
        if not bandwidths:
            return False, bandwidth, 0, None
        
        avg_bandwidth = sum(bandwidths) / len(bandwidths)
        
        # Un squeeze est quand le bandwidth est significativement inférieur à la moyenne
        is_squeeze = bandwidth < squeeze_threshold or bandwidth < avg_bandwidth * 0.7
        
        # Force du squeeze (0-100)
        if avg_bandwidth > 0:
            squeeze_strength = max(0, min(100, (1 - bandwidth / avg_bandwidth) * 100))
        else:
            squeeze_strength = 0
        
        # Détecter la direction du breakout (si le prix commence à sortir)
        current_price = prices[-1]
        prev_price = prices[-2] if len(prices) > 1 else current_price
        
        breakout_direction = None
        if is_squeeze or squeeze_strength > 30:
            # Prix qui sort vers le haut
            if current_price > bb_mid and current_price > prev_price:
                breakout_direction = 'up'
            # Prix qui sort vers le bas
            elif current_price < bb_mid and current_price < prev_price:
                breakout_direction = 'down'
        
        return is_squeeze, bandwidth, squeeze_strength, breakout_direction
    
    @staticmethod
    def ema_trend(prices, short=7, mid=25, long=99):
        """Analyse la configuration EMA
        Retourne: alignement ('bullish', 'bearish', 'mixed'), force, pente
        """
        ema_s = TechnicalIndicators.ema(prices, short)
        ema_m = TechnicalIndicators.ema(prices, mid)
        ema_l = TechnicalIndicators.ema(prices, long) if len(prices) >= long else None
        
        if not all([ema_s, ema_m]):
            return None, 0, 0
        
        # Calculer la pente des EMAs (direction du mouvement)
        # On compare l'EMA actuelle à celle d'il y a 3 périodes
        lookback = 3
        if len(prices) >= short + lookback:
            ema_s_prev = TechnicalIndicators.ema(prices[:-lookback], short)
            ema_m_prev = TechnicalIndicators.ema(prices[:-lookback], mid)
            
            # Pente en pourcentage
            ema_s_slope = ((ema_s - ema_s_prev) / ema_s_prev * 100) if ema_s_prev else 0
            ema_m_slope = ((ema_m - ema_m_prev) / ema_m_prev * 100) if ema_m_prev else 0
            avg_slope = (ema_s_slope + ema_m_slope) / 2
        else:
            avg_slope = 0
        
        # Vérifier l'alignement EMA (golden cross configuration)
        if ema_l:
            if ema_s > ema_m > ema_l:
                # Alignement bullish MAIS vérifier si pente négative
                if avg_slope < -0.3:  # EMAs en chute malgré alignement
                    return 'bearish', 75, avg_slope
                return 'bullish', 100, avg_slope  # Alignement parfait haussier
            elif ema_s < ema_m < ema_l:
                return 'bearish', 100, avg_slope  # Alignement parfait baissier
            elif ema_s > ema_m:
                if avg_slope < -0.5:  # Forte pente négative
                    return 'bearish', 60, avg_slope
                return 'bullish', 50, avg_slope
            else:
                return 'bearish', 50, avg_slope
        else:
            if ema_s > ema_m:
                if avg_slope < -0.5:  # Forte pente négative
                    return 'bearish', 60, avg_slope
                return 'bullish', 50, avg_slope
            else:
                return 'bearish', 50, avg_slope
        
        return 'mixed', 25, avg_slope
    
    @staticmethod
    def breakout_detection(prices, period=20):
        """Détecte un breakout au-dessus de la résistance
        Un breakout est quand le prix dépasse le plus haut des N dernières bougies
        """
        if len(prices) < period + 1:
            return False, 0
        
        # Exclure la bougie actuelle pour le calcul du range
        historical_high = max(prices[-period-1:-1])
        historical_low = min(prices[-period-1:-1])
        current = prices[-1]
        prev = prices[-2]
        
        # Breakout haussier
        if current > historical_high and prev <= historical_high:
            strength = ((current - historical_high) / historical_high) * 100
            return True, strength
        
        return False, 0
    
    @staticmethod
    def atr(prices, period=14):
        """Average True Range - mesure de la volatilité pour trailing stop"""
        if len(prices) < period + 1:
            return None
        
        trs = []
        for i in range(-period, 0):
            high = prices[i]
            low = prices[i]
            prev_close = prices[i-1] if i > -len(prices) else prices[i]
            tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
            trs.append(tr)
        
        return np.mean(trs)
    
    @staticmethod
    def keltner_channels(prices, ema_period=20, atr_period=10, atr_multiplier=2.0):
        """Keltner Channels - Indicateur de volatilité et tendance
        
        Les bandes de Keltner utilisent l'ATR (volatilité réelle) au lieu de l'écart-type.
        Avantages vs Bollinger:
        - Moins de faux signaux (ATR filtre mieux le bruit)
        - Meilleure identification des vraies tendances
        - Breakouts plus fiables
        
        Retourne: (upper_band, middle_line, lower_band)
        - upper_band: EMA + (ATR × multiplicateur)
        - middle_line: EMA (ligne centrale)
        - lower_band: EMA - (ATR × multiplicateur)
        """
        if len(prices) < max(ema_period, atr_period) + 1:
            return None, None, None
        
        # Ligne centrale = EMA
        middle = TechnicalIndicators.ema(prices, ema_period)
        if not middle:
            return None, None, None
        
        # Calcul de l'ATR
        atr = TechnicalIndicators.atr(prices, atr_period)
        if not atr:
            return None, None, None
        
        # Bandes supérieure et inférieure
        upper = middle + (atr * atr_multiplier)
        lower = middle - (atr * atr_multiplier)
        
        return upper, middle, lower

# ═══════════════════════════════════════════════════════════════════════════════
# GESTIONNAIRE DE POSITIONS
# ═══════════════════════════════════════════════════════════════════════════════

# Répertoire du script (pour les chemins relatifs)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

class PositionManager:
    """Gère les positions ouvertes avec stop-loss et take-profit"""
    
    POSITIONS_FILE = os.path.join(SCRIPT_DIR, "positions.json")
    HISTORY_FILE = os.path.join(SCRIPT_DIR, "trade_history.json")
    
    def __init__(self, client):
        self.client = client
        self.positions = {}  # {symbol: {entry_price, quantity, stop_loss, take_profit}}
        self.trade_history = []  # Liste de tous les trades
        self.trade_logger = None  # Référence au trade_logger (sera set par TradingBot)
        self.ai_sell_predictor = None  # 🧠 IA prédictive de vente (sera set par TradingBot)
        self.market_regime = None      # 🔧 FIX 16/03: régime marché (sera set par TradingBot)
        self._load_data()
    
    def _load_data(self):
        """Charge les positions et l'historique depuis les fichiers JSON"""
        # Charger les positions ouvertes
        try:
            if os.path.exists(self.POSITIONS_FILE):
                with open(self.POSITIONS_FILE, 'r') as f:
                    data = json.load(f)
                    
                    # 🔴 FIX 27/02: CHARGER TOUTES LES POSITIONS (plus de limite 2h!)
                    # L'ancienne limite de 2h causait des positions orphelines sur Binance:
                    # - Le bot "oubliait" les positions > 2h au redémarrage
                    # - Le crypto restait sur Binance sans suivi
                    # - Le bot rachetait avec le USDT restant → USDT épuisé
                    # Maintenant: on charge TOUT et on vérifie sur Binance si la position existe encore
                    loaded_count = 0
                    skipped_count = 0
                    
                    for symbol, pos in data.items():
                        pos['timestamp'] = datetime.fromisoformat(pos['timestamp'])
                        
                        # 🔴 FIX 28/01: Valider pattern et bloquer UNKNOWN/None
                        pattern = pos.get('pattern', 'UNKNOWN')
                        if not pattern or pattern in ['None', 'UNKNOWN', '', 'null']:
                            print(f"   ⚠️ Position {symbol} ignorée (pattern invalide: '{pattern}' - position corrompue)")
                            skipped_count += 1
                            continue
                        
                        # Charger la position (plus de filtre d'âge!)
                        # 🔴 FIX 08/03: Ne pas reprendre la gestion des positions market_spy
                        # (elles sont gérées exclusivement par espion_trades.json / market_spy.py)
                        if pos.get('source') == 'MARKET_SPY':
                            print(f"   🔀 {symbol}: ignoré par le bot principal (géré par market_spy)")
                            skipped_count += 1
                            continue
                        age_hours = (datetime.now() - pos['timestamp']).total_seconds() / 3600
                        self.positions[symbol] = pos
                        loaded_count += 1
                        if age_hours > 24:
                            print(f"   ⏰ Position {symbol} ancienne ({age_hours:.0f}h) - sera vérifiée sur Binance")
                    
                    if loaded_count > 0:
                        print(f"   📂 {loaded_count} position(s) restaurée(s)")
                    if skipped_count > 0:
                        print(f"   🗑️ {skipped_count} position(s) corrompues ignorées")
        except Exception as e:
            print(f"   ⚠️ Erreur chargement positions: {e}")
        
        # Charger l'historique des trades
        try:
            if os.path.exists(self.HISTORY_FILE):
                with open(self.HISTORY_FILE, 'r', encoding='utf-8') as f:
                    self.trade_history = json.load(f)
                print(f"   📜 {len(self.trade_history)} trade(s) dans l'historique")
        except Exception as e:
            print(f"   ⚠️ Erreur chargement historique: {e}")
    
    def sync_with_binance(self, watchlist=None):
        """🔴 FIX 27/02: Synchronise positions locales avec les holdings réels sur Binance.
        
        Détecte les positions orphelines (crypto sur Binance non trackée localement)
        et les positions fantômes (trackées localement mais plus sur Binance).
        Évite l'accumulation de crypto non suivie qui bloque le USDT.
        
        Args:
            watchlist: Set de symboles à vérifier. Si None, vérifie tout.
                       Important sur testnet qui a 400+ coins pré-chargés.
        """
        logger = logging.getLogger('SyncBinance')
        logger.info("🔄 Synchronisation avec Binance...")
        
        try:
            account = self.client.get_account()
            if not account:
                logger.warning("⚠️ Impossible de récupérer le compte Binance")
                return
            
            # Collecter les holdings réels sur Binance (> 5 USDT)
            binance_holdings = {}
            stablecoins = {'USDT', 'BUSD', 'USDC', 'DAI', 'USD', 'EUR', 'BRL', 'TRY', 
                          'IDR', 'ARS', 'UAH', 'ZAR', 'PLN', 'RON', 'CZK', 'MXN',
                          'COP', 'JPY', 'TUSD', 'FDUSD', 'USDP', 'AEUR', 'EURI',
                          'BFUSD', 'USD1', 'USDE', 'RLUSD', 'XUSD', 'FRAX', 'U'}
            # Assets à ignorer (non tradables ou spéciaux testnet)
            ignore_assets = {'BNB', 'LDBNB', 'ETHW', 'BETH', 'WBTC', 'WBETH', 'BNSOL', '这是测试币', '456', '币安人生'}
            
            # 🔴 FIX 27/02: Récupérer TOUS les prix en 1 appel (évite 432 appels individuels)
            # Note: BinanceClient wrapper utilise get_all_prices() (pas get_all_tickers())
            price_map = self.client.get_all_prices()
            
            if not price_map:
                logger.warning("⚠️ get_all_prices() a retourné un dict vide")
                return
            
            for balance_item in account.get('balances', []):
                asset = balance_item['asset']
                if asset in stablecoins or asset in ignore_assets:
                    continue
                
                total = float(balance_item['free']) + float(balance_item['locked'])
                if total <= 0:
                    continue
                
                symbol = f"{asset}USDC"
                price = price_map.get(symbol, price_map.get(f"{asset}USDT", 0))
                
                if price > 0:
                    value_usdt = total * price
                    
                    if value_usdt >= 5.0:  # Seuil minimum
                        binance_holdings[symbol] = {
                            'quantity': total,
                            'price': price,
                            'value_usdt': value_usdt
                        }
            
            logger.info(f"📊 {len(binance_holdings)} holdings détectés sur Binance (> 5 USDT)")
            
            # 1. Détecter positions fantômes (dans positions.json mais plus sur Binance)
            # Note: on vérifie dans tous les holdings Binance (pas filtré par watchlist)
            # car si une position est trackée, elle DOIT exister sur Binance
            phantom_positions = []
            for symbol in list(self.positions.keys()):
                if symbol not in binance_holdings:
                    phantom_positions.append(symbol)
                    logger.warning(f"👻 Position fantôme: {symbol} (dans positions.json mais 0 sur Binance)")
            
            # Retirer les positions fantômes
            for symbol in phantom_positions:
                del self.positions[symbol]
            
            if phantom_positions:
                logger.info(f"🧹 {len(phantom_positions)} position(s) fantôme(s) nettoyée(s)")
                self._save_positions()
            
            # 2. Détecter positions orphelines (sur Binance mais pas dans positions.json)
            # 🔴 FIX 27/02: Ne chercher que dans la watchlist (testnet a 400+ coins pré-chargés)
            # 🔴 FIX 08/03: Inclure aussi espion_trades.json dans les "positions trackées"
            #   Sans ça, les positions market_spy (espion_trades) étaient récupérées comme orphelins
            #   avec timestamp=now → le spy perdait l'entrée originale + double-gestion
            import os as _os
            _spy_file = _os.path.join(_os.path.dirname(self.POSITIONS_FILE), 'espion_trades.json')
            _spy_tracked = set()
            try:
                if _os.path.exists(_spy_file):
                    with open(_spy_file, 'r', encoding='utf-8') as _f:
                        _spy_tracked = set(json.load(_f).keys())
            except Exception:
                pass
            orphan_count = 0
            tracked_symbols = set(self.positions.keys()) | _spy_tracked
            
            for symbol, holding in binance_holdings.items():
                # Ignorer les coins hors watchlist (coins testnet pré-chargés)
                if watchlist and symbol not in watchlist:
                    continue
                # 🔴 FIX 08/03: Ne pas récupérer les positions déjà gérées par market_spy
                if symbol in _spy_tracked:
                    continue
                if symbol not in tracked_symbols:
                    orphan_count += 1
                    value = holding['value_usdt']
                    logger.warning(f"🔍 Position orpheline: {symbol} = ${value:.2f} USDT sur Binance (non trackée)")
                    
                    # 🔧 FIX 14/03: RECOVERED_ORPHAN → SL serré (-1.0% au lieu du SL standard)
                    # L'entrée est le prix actuel (approximation), pas le vrai prix d'achat.
                    # On ne sait pas si le coin est en perte réelle ou non → sortir vite
                    # si le prix continue de baisser pour minimiser le dommage.
                    orphan_sl_pct = min(STOP_LOSS_PERCENT, 1.0)
                    self.positions[symbol] = {
                        'entry_price': holding['price'],  # Prix actuel comme approximation
                        'quantity': holding['quantity'],
                        'stop_loss': holding['price'] * (1 - orphan_sl_pct / 100),
                        'take_profit': holding['price'] * (1 + TAKE_PROFIT_PERCENT / 100),
                        'timestamp': datetime.now(),
                        'side': 'BUY',
                        'pattern': 'RECOVERED_ORPHAN',
                        'usdt_invested': value,
                        'max_price': holding['price'],
                        'min_price': holding['price']
                    }
                    logger.info(f"✅ {symbol} récupéré (SL={orphan_sl_pct}% serré orphelin, TP={TAKE_PROFIT_PERCENT}%)")
            
            if orphan_count > 0:
                logger.info(f"🔄 {orphan_count} position(s) orpheline(s) récupérée(s)")
                self._save_positions()
            
            if orphan_count == 0 and len(phantom_positions) == 0:
                logger.info(f"✅ Positions synchronisées ({len(self.positions)} pos. locales, {len(binance_holdings)} holdings Binance)")
            
        except Exception as e:
            logger.error(f"⚠️ Erreur sync Binance: {e}")
            import traceback
            logger.error(traceback.format_exc())
    
    def _validate_and_fix_sl_tp(self, symbol, pos):
        """Valide et corrige automatiquement les Stop Loss et Take Profit invalides"""
        entry_price = pos.get('entry_price', 0)
        stop_loss = pos.get('stop_loss', 0)
        take_profit = pos.get('take_profit', 0)
        side = pos.get('side', 'BUY')
        
        if entry_price <= 0:
            return pos  # Prix d'entrée invalide, ne pas modifier
        
        fixed = False
        
        # Validation pour position BUY
        if side == 'BUY':
            # SL doit être < entry_price (protection baisse)
            if stop_loss >= entry_price:
                pos['stop_loss'] = entry_price * 0.975  # -2.5% par défaut
                print(f"   ⚠️ {symbol}: SL corrigé {stop_loss:.6f} → {pos['stop_loss']:.6f} (entry: {entry_price:.6f})")
                fixed = True
            
            # TP doit être > entry_price (gain hausse)
            if take_profit <= entry_price:
                pos['take_profit'] = entry_price * 1.05  # +5% par défaut
                print(f"   ⚠️ {symbol}: TP corrigé {take_profit:.6f} → {pos['take_profit']:.6f} (entry: {entry_price:.6f})")
                fixed = True
        
        # Validation pour position SELL
        elif side == 'SELL':
            # SL doit être > entry_price (protection hausse)
            if stop_loss <= entry_price:
                pos['stop_loss'] = entry_price * 1.025  # +2.5% par défaut
                print(f"   ⚠️ {symbol}: SL corrigé {stop_loss:.6f} → {pos['stop_loss']:.6f} (entry: {entry_price:.6f})")
                fixed = True
            
            # TP doit être < entry_price (gain baisse)
            if take_profit >= entry_price:
                pos['take_profit'] = entry_price * 0.95  # -5% par défaut
                print(f"   ⚠️ {symbol}: TP corrigé {take_profit:.6f} → {pos['take_profit']:.6f} (entry: {entry_price:.6f})")
                fixed = True
        
        if fixed:
            print(f"   ✅ {symbol}: Position validée et corrigée automatiquement")
        
        return pos
    
    def _save_positions(self):
        """Sauvegarde les positions ouvertes avec validation automatique des SL/TP"""
        try:
            # 🔴 FIX COHERENCE: Si sell_all_signal.json existe, NE PAS écrire les positions!
            # Bug: _save_positions() réécrivait les positions en mémoire dans positions.json
            # juste après que sell_all.py l'avait vidé → les positions fantômes revenaient
            sell_signal_file = os.path.join(os.path.dirname(self.POSITIONS_FILE), 'sell_all_signal.json')
            if os.path.exists(sell_signal_file):
                print(f"   🚫 _save_positions() BLOQUÉ: sell_all_signal.json détecté → ne pas écraser le fichier vidé")
                return
            
            data = {}
            for symbol, pos in self.positions.items():
                # Valider et corriger les SL/TP avant sauvegarde
                validated_pos = self._validate_and_fix_sl_tp(symbol, pos.copy())
                
                # Mettre à jour la position en mémoire avec les valeurs corrigées
                self.positions[symbol].update({
                    'stop_loss': validated_pos['stop_loss'],
                    'take_profit': validated_pos['take_profit']
                })
                
                data[symbol] = {
                    **validated_pos,
                    'timestamp': validated_pos['timestamp'].isoformat()
                }
            with open(self.POSITIONS_FILE, 'w', encoding='utf-8') as f:
                json.dump(data, f, indent=2, ensure_ascii=False)
            print(f"   💾 Positions sauvegardées: {len(data)} position(s)")
            # 🔴 FIX 28/01: Tracer les sauvegardes pour débogage
            print(f"   🔍 Timestamp sauvegarde: {datetime.now().isoformat()}")
        except Exception as e:
            print(f"   ⚠️ Erreur sauvegarde positions: {e}")
            import traceback
            traceback.print_exc()
    
    def _save_history(self):
        """Sauvegarde l'historique des trades (écriture atomique)"""
        try:
            # 🔴 FIX 10/03: Si le fichier sur disque contient PLUS de trades qu'en mémoire
            # (ex: restauration externe depuis archives), fusionner pour ne jamais perdre de données
            history_to_save = self.trade_history
            if os.path.exists(self.HISTORY_FILE):
                try:
                    with open(self.HISTORY_FILE, 'r', encoding='utf-8') as _f:
                        disk_history = json.load(_f)
                    if isinstance(disk_history, list) and len(disk_history) > len(self.trade_history):
                        # Fusionner: ajouter les trades disk non présents en mémoire
                        in_memory_keys = set(
                            (t.get('symbol',''), t.get('entry_time','') or t.get('timestamp',''))
                            for t in self.trade_history
                        )
                        missing = [
                            t for t in disk_history
                            if (t.get('symbol',''), t.get('entry_time','') or t.get('timestamp','')) not in in_memory_keys
                        ]
                        if missing:
                            history_to_save = missing + self.trade_history
                            history_to_save.sort(key=lambda t: t.get('entry_time','') or t.get('timestamp','') or '')
                            print(f"   📜 Fusion historique: {len(missing)} trades disque + {len(self.trade_history)} mémoire = {len(history_to_save)}")
                            self.trade_history = history_to_save
                except Exception:
                    pass  # En cas d'erreur lecture, sauvegarder la mémoire en état

            # 🔴 FIX: Écriture atomique pour éviter lectures partielles par le dashboard
            tmp_file = self.HISTORY_FILE + '.tmp'
            with open(tmp_file, 'w', encoding='utf-8') as f:
                json.dump(history_to_save, f, indent=2, ensure_ascii=False)
            os.replace(tmp_file, self.HISTORY_FILE)
        except Exception as e:
            print(f"   ⚠️ Erreur sauvegarde historique: {e}")
            # Nettoyer le fichier temporaire si nécessaire
            tmp_file = self.HISTORY_FILE + '.tmp'
            if os.path.exists(tmp_file):
                try:
                    os.remove(tmp_file)
                except:
                    pass
    
    def get_total_pnl(self):
        """Calcule le P&L total de tous les trades fermés"""
        total_pnl = sum(t.get('pnl', 0) for t in self.trade_history)
        total_trades = len(self.trade_history)
        wins = sum(1 for t in self.trade_history if t.get('pnl', 0) > 0)
        losses = sum(1 for t in self.trade_history if t.get('pnl', 0) < 0)
        return {
            'total_pnl': total_pnl,
            'total_trades': total_trades,
            'wins': wins,
            'losses': losses,
            'win_rate': (wins / total_trades * 100) if total_trades > 0 else 0
        }
    
    def open_position(self, symbol, side, usdt_amount, stop_loss_pct=STOP_LOSS_PERCENT, 
                      take_profit_pct=TAKE_PROFIT_PERCENT, bb_breakout_entry=False, pattern='UNKNOWN'):
        """Ouvre une nouvelle position
        
        Args:
            bb_breakout_entry: Si True, cette position a été ouverte sur un breakout BB+
                               et sera vendue dès que le prix repasse sous BB+
            pattern: Pattern IA détecté au moment de l'achat (pour statistiques)
        """
        
        # 🔴 FIX 06/04: ACHATS DÉSACTIVÉS — le bot trade à perte, seul le spy est rentable.
        # Le bot continue de tourner pour le monitoring/dashboard mais n'ouvre plus de positions.
        # Pour réactiver: supprimer ce bloc.
        print(f"   🚫 {symbol}: ACHATS BOT DÉSACTIVÉS (spy-only mode) — pattern={pattern}")
        return None
        
        if symbol in self.positions:
            print(f"   ⚠️ Position déjà ouverte sur {symbol}")
            return None
        
        # Vérifier le solde
        balance = self.client.get_balance("USDC")
        if balance['free'] < usdt_amount:
            print(f"   ❌ Solde insuffisant: {balance['free']:.2f} USDC")
            return None
        
        # 🔴 NOUVEAU: Vérifier LOT_SIZE et MIN_NOTIONAL avant l'achat
        can_trade, reason = self.client.can_trade_symbol(symbol, usdt_amount)
        if not can_trade:
            print(f"   ⚠️ {symbol}: Impossible de trader - {reason}")
            print(f"   💡 Conseil: Augmenter POSITION_SIZE ou retirer {symbol} de la watchlist")
            # Marquer le symbole comme problématique (ne bloque pas le script)
            if not hasattr(self, '_lot_size_blacklist'):
                self._lot_size_blacklist = {}
            self._lot_size_blacklist[symbol] = {
                'reason': reason,
                'timestamp': datetime.now(),
                'usdt_amount': usdt_amount
            }
            return None
        
        # Passer l'ordre
        order = self.client.market_buy(symbol, usdt_amount)
        
        if order and 'orderId' in order:
            entry_price = float(order.get('fills', [{}])[0].get('price', 0))
            quantity = float(order.get('executedQty', 0))
            
            if entry_price == 0:
                entry_price = self.client.get_price(symbol)
            
            # Calculer stop-loss et take-profit
            stop_loss = entry_price * (1 - stop_loss_pct / 100)
            take_profit = entry_price * (1 + take_profit_pct / 100)
            
            self.positions[symbol] = {
                'entry_price': entry_price,
                'quantity': quantity,
                'stop_loss': stop_loss,
                'take_profit': take_profit,
                'side': side,
                'order_id': order['orderId'],
                'timestamp': datetime.now(),
                'bb_breakout_entry': bb_breakout_entry,  # Tracker la stratégie d'entrée
                'pattern': pattern  # Pattern IA au moment de l'achat
            }
            
            # Sauvegarder les positions
            self._save_positions()
            
            strategy_label = " [BB+ BREAKOUT]" if bb_breakout_entry else ""
            print(f"\n   ✅ POSITION OUVERTE{strategy_label}: {symbol}")
            print(f"      Prix entrée: {currency.format(entry_price)}")
            print(f"      Quantité: {quantity}")
            print(f"      Stop-Loss: {currency.format(stop_loss)} (-{stop_loss_pct}%)")
            print(f"      Take-Profit: {currency.format(take_profit)} (+{take_profit_pct}%)")
            if bb_breakout_entry:
                print(f"      📊 Stratégie: Vente si prix < BB+")
            
            return order
        
        # 🔴 ORDRE ÉCHOUÉ - Ajouter à la blacklist temporaire
        print(f"   ⚠️ {symbol}: Ordre échoué (liquidité insuffisante ou erreur API)")
        if not hasattr(self, '_lot_size_blacklist'):
            self._lot_size_blacklist = {}
        self._lot_size_blacklist[symbol] = {
            'reason': 'Ordre échoué (liquidité/API)',
            'timestamp': datetime.now(),
            'usdt_amount': usdt_amount
        }
        return None
    
    def close_position(self, symbol, reason="manual"):
        """Ferme une position"""
        
        if symbol not in self.positions:
            print(f"   ⚠️ Pas de position ouverte sur {symbol}")
            return None
        
        position = self.positions[symbol]
        
        # Timestamp d'ouverture pour calcul de durée
        open_timestamp = position.get('timestamp', datetime.now())
        if isinstance(open_timestamp, str):
            try:
                open_timestamp = datetime.fromisoformat(open_timestamp)
            except:
                open_timestamp = datetime.now()
        
        # Formater la quantité selon la précision du symbole
        quantity = self.client.format_quantity(symbol, position['quantity'])
        
        if quantity <= 0:
            print(f"   ⚠️ Quantité trop petite pour {symbol}: {position['quantity']}")
            # Supprimer quand même la position car elle est inutilisable
            del self.positions[symbol]
            self._save_positions()
            return None
        
        print(f"   📊 Vente de {quantity} {symbol} (original: {position['quantity']})")
        
        # Vendre au marché avec gestion d'erreur
        try:
            order = self.client.market_sell(symbol, quantity)
        except Exception as e:
            print(f"   ❌ ERREUR CRITIQUE vente {symbol}: {e}")
            print(f"   🚨 Position {symbol} NON FERMÉE - ALERTE MANUELLE REQUISE!")
            # Marquer la position comme problématique
            position['failed_close_attempts'] = position.get('failed_close_attempts', 0) + 1
            if position['failed_close_attempts'] >= 3:
                print(f"   ⚠️⚠️⚠️ {symbol}: 3+ tentatives échouées! INTERVENTION MANUELLE!")
            self._save_positions()
            return None
        
        if order and 'orderId' in order:
            # Sécurité: vérifier que fills existe et n'est pas vide
            fills = order.get('fills', [])
            if fills and len(fills) > 0:
                exit_price = float(fills[0].get('price', 0))
            else:
                exit_price = 0
            
            if exit_price == 0:
                exit_price = self.client.get_price(symbol)
            
            # 🔴 FIX CRITIQUE: Validation du exit_price pour éviter les prix aberrants
            # Vérifier que le prix de sortie est cohérent avec le marché actuel
            try:
                market_price = self.client.get_price(symbol)
                price_diff_pct = abs((exit_price - market_price) / market_price) * 100
                
                if price_diff_pct > 10:  # Si écart > 10%, c'est suspect
                    print(f"   ⚠️  WARNING: exit_price suspect pour {symbol}")
                    print(f"      Prix API fills: {exit_price:.2f}")
                    print(f"      Prix marché actuel: {market_price:.2f}")
                    print(f"      Écart: {price_diff_pct:.1f}% (> 10%)")
                    print(f"      → Utilisation du prix marché pour éviter données erronées")
                    exit_price = market_price
            except Exception as e:
                print(f"   ⚠️  Erreur validation exit_price: {e}")
                # Continuer avec le prix original si validation échoue
            
            # Calculer le P&L
            pnl = (exit_price - position['entry_price']) * position['quantity']
            pnl_pct = ((exit_price / position['entry_price']) - 1) * 100
            
            # Calculer durée du trade
            duration_seconds = (datetime.now() - open_timestamp).total_seconds()
            
            # Log du trade fermé
            if self.trade_logger:
                try:
                    self.trade_logger.log_trade_close({
                        'timestamp': datetime.now(),
                        'symbol': symbol,
                        'side': 'BUY',
                        'entry_price': position['entry_price'],
                        'exit_price': exit_price,
                        'quantity': position['quantity'],
                        'pnl': pnl,
                        'pnl_pct': pnl_pct,
                        'reason': reason,
                        'duration_seconds': duration_seconds,
                        'stop_loss': position.get('stop_loss', 0),
                        'take_profit': position.get('take_profit', 0),
                        'sl_pct': position.get('sl_pct', 0),
                        'tp_pct': position.get('tp_pct', 0),
                        'is_dynamic_sltp': position.get('is_dynamic_sltp', False),
                        'max_profit_pct': position.get('max_profit_pct', pnl_pct),
                        'max_drawdown_pct': position.get('max_drawdown_pct', 0)
                    })
                except Exception as e:
                    pass  # Silencieux
            
            # Ajouter à l'historique
            trade_record = {
                'symbol': symbol,
                'side': 'BUY',
                'entry_price': position['entry_price'],
                'exit_price': exit_price,
                'quantity': position['quantity'],
                'pnl': pnl,
                'pnl_pct': pnl_pct,
                'reason': reason,
                'entry_time': position['timestamp'].isoformat(),
                'exit_time': datetime.now().isoformat(),
                'pattern': position.get('pattern', 'UNKNOWN')  # Pattern IA au moment de l'achat
            }
            self.trade_history.append(trade_record)
            self._save_history()
            
            # Enregistrer le résultat dans PatternManager
            pattern_name = position.get('pattern', 'UNKNOWN')
            pm = get_pattern_manager()
            pm.record_trade(pattern_name, pnl_pct)
            
            print(f"\n   📤 POSITION FERMÉE: {symbol} ({reason})")
            print(f"      Prix sortie: {currency.format(exit_price)}")
            print(f"      P&L: {currency.format(pnl, 2)} ({pnl_pct:+.2f}%)")
            
            del self.positions[symbol]
            self._save_positions()
            
            # 🆕 FIX 06/02: Cooldown renforcé après une PERTE
            # Éviter de re-rentrer immédiatement sur le même symbole après un trade perdant
            # Les données montrent que re-entrée rapide sur même symbole = nouvelle perte
            if pnl_pct < -0.1:
                # � FIX 09/02: Cooldown UNIFORME 2h pour TOUTES les pertes
                # Données: ETH 3x, XRP 2x, LINK 2x = re-achats répétés = pertes cumulées
                # L'ancien 5min pour non-SL laissait re-acheter immédiatement sur même symbole
                # 2h = temps pour que la situation technique change vraiment
                import time as _time
                self._last_loss_cooldown = self._last_loss_cooldown if hasattr(self, '_last_loss_cooldown') else {}
                
                # 🔴 FIX 04/03: Cooldown DIFFÉRENCIÉ selon gravité de la perte
                # SAHARAUSDT SL à 09:01 (-12.94), racheté à 15:05 = même perte 6h plus tard!
                # Solution: stop-loss réel (< -1.5%) = 8h | perte légère = 2h
                if pnl_pct < -1.5:  # Stop-loss réel
                    cooldown_seconds = 28800  # 8h — éviter 2x la même perte dans la journée
                else:
                    cooldown_seconds = 7200   # 2h pour les pertes légères
                self._last_loss_cooldown[symbol] = _time.time() + cooldown_seconds
                hrs = cooldown_seconds // 3600
                print(f"      🕒 Cooldown PERTE {hrs}h: {symbol} bloqué (perte {pnl_pct:+.2f}%)")
            
            return order
        
        return None
    
    def check_technical_exit(self, symbol, prices_data):
        """
        Vérifie les indicateurs techniques pour une sortie anticipée.
        Appelée à chaque cycle pour les positions ouvertes.
        
        AMÉLIORATIONS v2.0:
        - Détection des ventes massives avec volume anormal
        - RSI TRAP pattern = vente urgente
        - Chute rapide du RSI = signal critique
        - Momentum négatif soudain avec volumes
        - NOUVEAU: Détection retournement après pic (EMA cross + perte de momentum)
        - FIX: Utilise les klines 1m Binance au lieu des ticks pour cohérence
        
        Retourne: (should_exit, reason)
        """
        if symbol not in self.positions:
            return False, ""
        
        position = self.positions[symbol]
        entry_price = position['entry_price']
        
        # 🔴 FIX 03/02: Protection positions récentes CREUX_REBOUND
        # On achète en creux (RSI bas), donc les conditions RSI < 30 ne sont pas des signaux de sortie
        # mais des conditions NORMALES pour ce type d'achat
        pattern = position.get('pattern', '')
        # 🔧 FIX 22/03: même bug datetime/str qu'au quick-exit (ligne 5110)
        # position['timestamp'] est un objet datetime, fromisoformat() lève TypeError silencieux
        # → position_age_minutes reste 0 → is_creux_protected = (… and 0 < 20) = TOUJOURS True
        # → les détecteurs RSI/momentum sont désactivés en permanence pour tout CREUX_REBOUND!
        position_age_minutes = 0
        _ts_raw_creux = position.get('timestamp', None)
        if _ts_raw_creux is not None:
            try:
                if isinstance(_ts_raw_creux, datetime):
                    position_age_minutes = (time.time() - _ts_raw_creux.timestamp()) / 60
                elif isinstance(_ts_raw_creux, str):
                    position_age_minutes = (time.time() - datetime.fromisoformat(_ts_raw_creux).timestamp()) / 60
            except Exception:
                pass
        
        # Si pattern CREUX_REBOUND et position < 20 min → désactiver les sorties RSI bas
        # 🔴 FIX 04/02: Augmenté de 15 à 20 min - les trades > 10min gagnent plus souvent
        # 🔴 FIX 06/04: Protection CONDITIONNELLE — levée si tendance baissière (pas de rebond possible)
        is_creux_protected = (pattern == 'CREUX_REBOUND' and position_age_minutes < 20)
        _creux_protection_lifted = False  # Flag pour savoir si on a levé la protection (set après EMA calc)
        if is_creux_protected:
            print(f"   🛡️ {symbol}: CREUX_REBOUND protégé (age={position_age_minutes:.1f}min < 20min)")
        
        # CORRECTION: Utiliser les klines 1m Binance pour des indicateurs fiables
        klines = []  # 🔴 FIX 09/03 v6b: initialisé ici pour accès hors du bloc try
        try:
            klines = self.client.get_klines(symbol, interval='1m', limit=50)
            if len(klines) < 21:
                return False, ""
            prices_list = [float(k[4]) for k in klines]  # Close prices
            current_price = prices_list[-1]
        except Exception as e:
            # Fallback sur les données tick si erreur
            if len(prices_data) < 21:
                return False, ""
            prices_list = list(prices_data)
            current_price = prices_list[-1]
        
        # 🔴 FIX 09/03 v6b: ISOLATION volumes — extraction séparée pour ne pas perdre prices_list
        # Si k[9] (taker buy vol) est absent/corrompu, on garde les prix et on désactive volumes
        volumes_list = []
        buy_vols_list = []
        try:
            if klines:
                volumes_list = [float(k[5]) for k in klines]
                buy_vols_list = [float(k[9]) for k in klines]
        except Exception:
            volumes_list = []
            buy_vols_list = []
        
        if current_price is None:
            return False, ""
        
        # MISE À JOUR DU MAX_PRICE ici aussi (redondance avec check_stop_loss mais nécessaire)
        if 'max_price' not in position:
            position['max_price'] = current_price
        elif current_price > position['max_price']:
            position['max_price'] = current_price
            position['max_pnl'] = ((current_price / entry_price) - 1) * 100
            self._save_positions()
        
        # Calculer EMA
        ema9 = TechnicalIndicators.ema(prices_list, EMA_SHORT)
        ema21 = TechnicalIndicators.ema(prices_list, EMA_LONG)
        
        if not ema9 or not ema21:
            return False, ""
        
        # 🔴 FIX 06/04: Lever la protection CREUX si tendance BAISSIÈRE
        # Le CREUX_REBOUND achète un DIP pour un REBOND — mais si EMA trend est baissier,
        # il n'y aura PAS de rebond → la protection n'a aucun sens
        if is_creux_protected:
            _ema_gap_early = ((ema9 - ema21) / ema21) * 100
            if _ema_gap_early < -0.10:
                is_creux_protected = False
                _creux_protection_lifted = True
                print(f"   ⚠️ {symbol}: CREUX protection LEVÉE — tendance BAISSIÈRE (EMA gap={_ema_gap_early:.2f}%)")
        
        # Calculer RSI actuel et précédent pour détecter les chutes
        rsi = TechnicalIndicators.rsi(prices_list, RSI_PERIOD)
        rsi_prev = TechnicalIndicators.rsi(prices_list[:-3], RSI_PERIOD) if len(prices_list) > 24 else rsi
        
        # Calculer momentum sur 3 et 5 bougies
        momentum_3 = ((current_price - prices_list[-3]) / prices_list[-3]) * 100 if len(prices_list) >= 3 else 0
        momentum_5 = ((current_price - prices_list[-5]) / prices_list[-5]) * 100 if len(prices_list) >= 5 else 0
        momentum_10 = ((current_price - prices_list[-10]) / prices_list[-10]) * 100 if len(prices_list) >= 10 else 0
        
        # P&L actuel
        current_pnl_pct = ((current_price / entry_price) - 1) * 100
        
        # ═══════════════════════════════════════════════════════════════════
        # 🔴 FIX CRITIQUE 09/02: QUICK-EXIT = PROTECTION DES GAINS UNIQUEMENT
        # Données du jour: 10 quick-exits en perte = -70.97$ (0% WR = 100% pertes!)
        # Marché +5% mais le bot a coupé 10 positions en perte qui auraient récupéré.
        # RÈGLE: Le SL à -1.8% gère les pertes. Quick-exit ne doit JAMAIS couper
        # une position en perte. Seules les positions en profit (>0.3%) sont évaluées
        # pour sécuriser les gains avant un retournement.
        # 🔴 FIX 01/04: EXCEPTION STRESS MARCHÉ — lors d'un retournement brutal,
        # autoriser quick-exit même en léger négatif pour limiter les pertes.
        # Le SL à -1.8% est trop loin quand TOUT le marché chute simultanément.
        # ═══════════════════════════════════════════════════════════════════
        _btc_mom_qe = getattr(AIPredictor, '_btc_momentum', 0)
        _market_stress_qe = (_btc_mom_qe < -1.0)
        _pnl_gate = 0.3 if _market_stress_qe else 0.8  # 🔴 FIX 01/04: gate abaissé à 0.3% en stress
        
        if current_pnl_pct < _pnl_gate:
            # Position sous le seuil de profit significatif → laisser SL/TP/trailing gérer
            # 🔧 FIX 21/03: Relevé de 0.3% → 0.8% — rétro 582 trades: 136 quick-exits à 29% WR
            # causés par des sorties à 0.3-0.8% qui auraient pu atteindre +2.32% (take-profit 100% WR)
            # Exception: crash réel (momentum_3 < -1.5%) évalué même en dessous du seuil
            if current_pnl_pct >= 0.3:
                # Zone 0.3-0.8%: évaluer seulement si crash soudain réel
                _prices_tmp = list(prices_data)
                _mom3_tmp = ((current_price - _prices_tmp[-3]) / _prices_tmp[-3]) * 100 if len(_prices_tmp) >= 3 else 0
                if _mom3_tmp < -1.5:
                    pass  # Crash réel → continuer l'évaluation
                else:
                    return False, ""  # Pas de crash → laisser SL gérer

            # 🆕 FIX 09/03: Exception CREUX_REBOUND — évaluer fin de cycle même à P&L faible
            # Si on avait un profit et qu'on l'a perdu à 50%+, évaluer quand même
            _creux_reversal_check = (
                pattern == 'CREUX_REBOUND' and
                position.get('max_pnl', 0) >= 0.3 and
                current_pnl_pct < position.get('max_pnl', 0) * 0.5 and
                current_pnl_pct > -1.5  # 🔴 FIX v6b: étendu de -0.8 à -1.5 (SL est à -1.8%)
            )
            if not _creux_reversal_check:
                # 🔴 FIX 10/03: COS scenario — CREUX jamais profitable, prix continue de chuter
                # Problème: max_pnl jamais atteint 0.3% → _creux_reversal_check=False → gate bloque TOUT
                # pendant 3h+ jusqu'au SL -1.8% = -8€ de pertes non interceptées
                # Solution: si le pattern CREUX_REBOUND n'a JAMAIS été profitable ET
                # le prix continue de chuter activement → évaluer la sortie quand même
                _failed_creux_check = (
                    pattern == 'CREUX_REBOUND' and
                    position.get('max_pnl', 0) < 0.15 and  # Jamais en profit notable (< +0.15%)
                    current_pnl_pct < -0.3 and             # 🔴 FIX 06/04: Abaissé de -0.6 à -0.3 (réagir PLUS VITE)
                    momentum_5 < -0.1 and                  # 🔴 FIX 06/04: Abaissé de -0.2 à -0.1 (moins exigeant)
                    current_pnl_pct > -1.5                 # Pas encore au SL (SL = -1.8%)
                )
                # 🔴 FIX 06/04: CREUX en tendance BAISSIÈRE — rebond raté, sortir tôt
                # Cas STG: acheté en CREUX_REBOUND, EMA baissier, RSI 7, pnl -0.3% → bloqué!
                # Si la tendance est baissière, le rebond n'arrive pas → évaluer la sortie
                _ema_gap_gate = ((ema9 - ema21) / ema21) * 100 if ema9 and ema21 else 0
                _creux_bearish_exit = (
                    pattern == 'CREUX_REBOUND' and
                    position_age_minutes > 5 and           # Laisser au moins 5 min pour rebondir
                    _ema_gap_gate < -0.10 and              # Tendance baissière confirmée
                    current_pnl_pct < 0.1 and              # Pas en profit notable (near-zero ou perte)
                    current_pnl_pct > -1.5                 # Pas encore au SL
                )
                if _failed_creux_check:
                    print(f"   🔍 {symbol}: CREUX ÉCHOUÉ — jamais profitable (max={position.get('max_pnl',0):.2f}%), pnl={current_pnl_pct:+.2f}% → évaluation sortie")
                elif _creux_bearish_exit:
                    print(f"   🔍 {symbol}: CREUX BEARISH — tendance baissière (EMA={_ema_gap_gate:.2f}%), pas de rebond après {position_age_minutes:.0f}min → évaluation sortie")
                else:
                    if current_pnl_pct < -0.5:
                        print(f"   🛡️ {symbol}: Quick-exit BLOQUÉ (pnl={current_pnl_pct:+.2f}% < 0.3%) → SL gère les pertes")
                    return False, ""
            print(f"   🔍 {symbol}: CREUX cycle retournement détecté (max+{position.get('max_pnl',0):.2f}% → {current_pnl_pct:+.2f}%)")
        
        exit_reasons = []
        exit_score = 0
        urgent_exit = False  # Flag pour sortie URGENTE immédiate
        
        # EMA gap pour déterminer la tendance
        ema_gap_pct = ((ema9 - ema21) / ema21) * 100
        
        # LOG DEBUG pour toutes les positions (aide au diagnostic)
        if current_pnl_pct < -0.3:  # Logging pour positions en perte
            print(f"   📊 [EXIT CHECK] {symbol}: pnl={current_pnl_pct:+.2f}% Mom3={momentum_3:+.2f}% Mom5={momentum_5:+.2f}% RSI={rsi or 0:.0f} EMA_gap={ema_gap_pct:+.2f}%")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🚨 DÉTECTION URGENTE #1: RSI TRAP - CHUTE RSI SOUDAINE
        # Quand le RSI passe de zone haute (>60) à zone basse (<35) rapidement
        # ═══════════════════════════════════════════════════════════════════
        rsi_drop = (rsi_prev or 50) - (rsi or 50)  # Chute du RSI
        # � FIX 08/02: RSI TRAP durci encore — les RSI testnet sont très bruités.
        # Chute > 45pts ET RSI < 15 = seule condition de VRAI effondrement
        # Ancien: 35pts + RSI<25 → déclenchait encore trop de faux positifs
        if rsi_drop > 45 and rsi and rsi < 15:
            urgent_exit = True
            exit_score += 8  # Réduit de 10 à 8
            exit_reasons.append(f"🚨 RSI TRAP: {rsi_prev:.0f}→{rsi:.0f} (chute {rsi_drop:.0f}pts)")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🆕 FIX 29/01: BEAR MARKET - BTC + Crypto en baisse simultanée
        # 🔧 FIX 30/01: Assoupli de BTC < -0.2% à BTC < -0.1% pour meilleure détection
        # 🔴 FIX 03/02: EXCEPTION pour CREUX_REBOUND récent (on a acheté en creux, c'est normal!)
        # Cas BNB 19h18: BTC baisse + BNB baisse = AUCUNE chance de reprise
        # Si BTC momentum < -0.1% ET crypto momentum < -0.05% → EXIT anticipé
        # ═══════════════════════════════════════════════════════════════════
        if hasattr(AIPredictor, '_btc_momentum') and not is_creux_protected:
            btc_mom = getattr(AIPredictor, '_btc_momentum', 0)
            # BEAR MARKET détecté: BTC baisse significative + crypto en chute
            # � FIX 08/02: Seuils RELEVÉS massivement — les anciens (-0.3%) déclenchaient
            # 83% des quick-exits pour du bruit testnet. -1% BTC = fluctuation normale.
            # Seulement un VRAI crash BTC >-2% doit déclencher une sortie urgente.
            # 09/03: BUG CORRIGÉ — _btc_momentum est en DECIMAL (-0.02 = -2%)
            # Le seuil -2.0 signifiait -200% → jamais déclenché! Corrigé en -0.02
            if btc_mom < -0.02 and (momentum_3 < -0.5 or (rsi and rsi < 15)):  # BTC < -2%
                urgent_exit = True
                exit_score += 7
                exit_reasons.append(f"🚨 REAL CRASH: BTC{btc_mom*100:+.2f}% + Mom3={momentum_3:.2f}% (crash général)")
                print(f"   🔴 {symbol}: VRAI CRASH détecté (BTC{btc_mom*100:+.2f}%) → SORTIE IMMINENTE")
            # Pré-alerte: BTC faible + crypto en perte significative (-0.01 decimal = -1%)
            elif btc_mom < -0.01 and current_pnl_pct < -1.0:
                exit_score += 3  # Réduit de 4 à 3
                exit_reasons.append(f"⚠️ BTC faible ({btc_mom:+.2%}) + en perte")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🚨 DÉTECTION URGENTE #2: CRASH SOUDAIN - Momentum très négatif
        # Chute de plus de 1% sur 3 bougies = vente massive en cours
        # 🔴 FIX 03/02: Garder cette détection même pour CREUX_REBOUND (crash = urgence)
        # ═══════════════════════════════════════════════════════════════════
        if momentum_3 < -1.0:
            urgent_exit = True
            exit_score += 8
            exit_reasons.append(f"🚨 CRASH SOUDAIN: {momentum_3:.2f}% sur 3 bougies")
        elif momentum_3 < -0.5 and momentum_5 < -0.8:
            exit_score += 5
            exit_reasons.append(f"⚠️ Chute rapide: Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}%")
        
        # 🆕 FIX 28/01 15h25: DÉTECTION CHUTE PERSISTANTE - RSI trap en cours
        # 🔴 FIX 03/02: EXCEPTION pour CREUX_REBOUND récent
        # Cas WLD: Mom=-0.26%, RSI=28 → Chute active même si pas crash brutal
        # Si momentum négatif persistant (2-3 bougies) ET RSI en baisse → VENDRE
        # 🔧 FIX 07/02: Seuils CHUTE PERSISTANTE durcis — Mom > -0.3% sur 1min = bruit normal
        elif momentum_3 < -0.4 and momentum_5 < -0.4 and not is_creux_protected:
            # Chute significative et PERSISTANTE sur 3 ET 5 bougies
            if rsi and rsi < 25:
                # RSI en zone très basse confirme la faiblesse réelle
                urgent_exit = True
                exit_score += 8
                exit_reasons.append(f"🚨 CHUTE PERSISTANTE: Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}% RSI={rsi:.0f}")
                print(f"   🔴 {symbol}: Chute persistante détectée → VENTE IMMÉDIATE")
            else:
                exit_score += 3
                exit_reasons.append(f"⚠️ Baisse persistante: Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}%")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🚨 DÉTECTION URGENTE #3: FIN DE CYCLE - EMA Death Cross + Momentum négatif
        # EMA9 passe sous EMA21 avec momentum négatif = retournement confirmé
        # ═══════════════════════════════════════════════════════════════════
        # 🔧 FIX 07/02: Death Cross seuils durcis — EMA gap -0.15% sur 1min = bruit normal
        if ema_gap_pct < -0.30 and momentum_3 < -0.1 and momentum_5 < -0.1:
            exit_score += 5
            exit_reasons.append(f"⚠️ Death Cross confirmé: EMA gap={ema_gap_pct:.2f}%")
            if rsi and rsi < 30:
                urgent_exit = True
                exit_score += 4
                exit_reasons.append(f"🚨 + RSI faible ({rsi:.0f})")
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉTECTION #4: RSI en survente APRÈS avoir été en surachat
        # 🔴 FIX 29/01: RSI TRAP renforcé - RSI < 20 = sortie IMMÉDIATE
        # 🔴 FIX 03/02: EXCEPTION pour CREUX_REBOUND récent (on a acheté en creux!)
        # Si on était à RSI>65 et maintenant RSI<35 = on a raté la sortie
        # ═══════════════════════════════════════════════════════════════════
        # 🔧 FIX 07/02: RSI survente seuils durcis — RSI=30 sur 1min klines est NORMAL en consolidation
        # L'ancien seuil RSI < 20 était souvent atteint par le bug RSI=5 artificiel
        if rsi and rsi < 15 and not is_creux_protected:
            # RSI TRAP critique: RSI < 15 = vraie survente extrême (pas un artefact)
            urgent_exit = True
            exit_score += 10
            exit_reasons.append(f"🚨 RSI TRAP CRITIQUE: RSI={rsi:.0f} (survente extrême)")
            print(f"   🔴 {symbol}: RSI TRAP détecté (RSI={rsi:.0f}) → VENTE IMMÉDIATE")
        elif rsi and rsi < 22 and not is_creux_protected:
            # RSI très bas = chute confirmée
            if current_pnl_pct < -0.8:
                # En perte significative avec RSI très bas = sortir
                urgent_exit = True
                exit_score += 7
                exit_reasons.append(f"🚨 RSI survente ({rsi:.0f}) + en perte {current_pnl_pct:.2f}%")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🆕 DÉTECTION #4b: END_OF_CYCLE - RSI surachat + BB haute + momentum négatif
        # 🔴 FIX 28/01 19h: Seuils élargis (RSI>65, BB>0.80) pour sortir PLUS TÔT
        # Condition FIL 13h05: RSI=75-76, BB_pos=0.89-0.94, Mom=-0.16%
        # = PIC atteint, vendre AVANT la chute brutale
        # ═══════════════════════════════════════════════════════════════════
        if rsi and rsi > 65:  # 🔴 FIX: Abaissé 70→65 pour détecter plus tôt
            try:
                # Calculer BB position si pas déjà fait
                bb_upper = TechnicalIndicators.bollinger_upper(prices_list, 20, 2)
                bb_lower = TechnicalIndicators.bollinger_lower(prices_list, 20, 2)
                if bb_upper and bb_lower and bb_upper > bb_lower:
                    bb_pos = (current_price - bb_lower) / (bb_upper - bb_lower)
                    
                    # 💰 END_OF_CYCLE critique: RSI > 75 + BB > 0.90 + momentum négatif
                    # OPTIMISÉ 29/01: Seuils relevés pour LAISSER COURIR la phase parabolique
                    if rsi > 75 and bb_pos > 0.90 and momentum_3 < 0:
                        urgent_exit = True
                        exit_score += 10
                        exit_reasons.append(f"🚨 END_OF_CYCLE: RSI={rsi:.0f} BB_pos={bb_pos:.2f} Mom={momentum_3:.2f}%")
                        print(f"   🔴 {symbol}: PIC EXTRÊME DÉTECTÉ - RSI surachat + BB haute + momentum négatif → VENTE IMMÉDIATE")
                    # END_OF_CYCLE précoce: RSI > 70 + BB > 0.85 + momentum faible
                    elif rsi > 70 and bb_pos > 0.85 and momentum_3 < 0.1:
                        urgent_exit = True
                        exit_score += 8
                        exit_reasons.append(f"⚠️ END_OF_CYCLE PRÉCOCE: RSI={rsi:.0f} BB_pos={bb_pos:.2f} Mom={momentum_3:.2f}%")
                        print(f"   🟡 {symbol}: FIN DE CYCLE détectée - Sortie anticipée")
                    # Pré-alerte: RSI > 68 + BB haute mais momentum encore positif
                    elif rsi > 68 and bb_pos > 0.82 and momentum_3 < 0.3:
                        exit_score += 5
                        exit_reasons.append(f"⚠️ Proche pic: RSI={rsi:.0f} BB_pos={bb_pos:.2f}")
            except:
                pass  # Continuer si erreur calcul BB
        
        # ═══════════════════════════════════════════════════════════════════
        # 🆕 DÉTECTION #4c: VOLUMES DE VENTE ANORMAUX (barres rouges chart)
        # Détecte la pression vendeurs en analysant le ratio buy/sell des klines
        # Cas ORCA: volumes rouges massifs à 18h30 = signal de retournement ignoré
        # ═══════════════════════════════════════════════════════════════════
        if volumes_list and len(volumes_list) >= 5:
            try:
                sell_vols = [volumes_list[i] - buy_vols_list[i] for i in range(len(volumes_list))]
                avg_vol = sum(volumes_list[-21:-1]) / 20
                # Ratio de vente sur les 3 dernières bougies (0=100% achat, 1=100% vente)
                recent_vol_3 = sum(volumes_list[-3:]) / 3
                sell_ratio_3 = sum(sell_vols[-3:]) / max(sum(volumes_list[-3:]), 1e-10)
                vol_ratio = recent_vol_3 / max(avg_vol, 1e-10)  # Multiple de la moyenne
                
                # Spike de volume de vente CRITIQUE: vol 2x + 65%+ sell + prix baisse
                if sell_ratio_3 > 0.65 and vol_ratio > 2.0 and momentum_5 < -0.2:
                    urgent_exit = True
                    exit_score += 9
                    exit_reasons.append(f"🚨 SELL SPIKE: {sell_ratio_3*100:.0f}% vente {vol_ratio:.1f}x avg Mom5={momentum_5:.2f}%")
                    print(f"   🔴 {symbol}: SPIKE VOLUMES VENTE ({sell_ratio_3*100:.0f}% sell, {vol_ratio:.1f}x avg) → EXIT")
                # Volume de vente élevé: vol 1.5x + 65%+ sell + baisse confirmée
                elif sell_ratio_3 > 0.65 and vol_ratio > 1.5 and momentum_3 < -0.2:
                    exit_score += 5
                    exit_reasons.append(f"🔴 VENTE DOMINANTE: {sell_ratio_3*100:.0f}% sell {vol_ratio:.1f}x avg Mom3={momentum_3:.2f}%")
                # Pression vendeurs légère
                elif sell_ratio_3 > 0.60 and momentum_3 < 0 and momentum_5 < -0.1:
                    exit_score += 2
                    exit_reasons.append(f"⚠️ Pression vente: {sell_ratio_3*100:.0f}% sell")
            except Exception:
                pass
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉTECTION #5: Chute depuis le max (trailing stop mental)
        # ═══════════════════════════════════════════════════════════════════
        max_price = position.get('max_price', entry_price)
        max_pnl = position.get('max_pnl', 0)
        drop_from_max = ((current_price - max_price) / max_price) * 100
        if drop_from_max < -2.0:  # Chute de 2% depuis le max (abaissé de 3%)
            exit_score += 4
            exit_reasons.append(f"Chute {drop_from_max:.1f}% depuis max")
        if drop_from_max < -3.5:  # Chute sévère
            exit_score += 3
            urgent_exit = True
            exit_reasons.append(f"🚨 Chute sévère {drop_from_max:.1f}%")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🆕 DÉTECTION #5b: RETOURNEMENT APRÈS PIC - Sécuriser les gains
        # 🔴 FIX 06/04: Seuil abaissé de 0.5% à 0.3% — sécuriser les PETITS gains aussi
        # Si on a atteint un profit >0.3% et maintenant on perd ce gain
        # avec une tendance EMA baissière = VENDRE pour sécuriser
        # ═══════════════════════════════════════════════════════════════════
        if max_pnl > 0.3 and current_pnl_pct < max_pnl * 0.4:  # Perdu 60% du gain max
            if ema_gap_pct < 0:  # EMA baissière confirme le retournement
                exit_score += 6
                exit_reasons.append(f"⚠️ Retournement: max +{max_pnl:.1f}% → maintenant {current_pnl_pct:+.1f}%")
                if current_pnl_pct < 0:  # On est passé en négatif après avoir été positif
                    urgent_exit = True
                    exit_score += 4
                    exit_reasons.append(f"🚨 Profit perdu: était +{max_pnl:.1f}%, maintenant {current_pnl_pct:.1f}%")
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉTECTION #6: Tendance EMA baissière confirmée
        # ═══════════════════════════════════════════════════════════════════
        if ema_gap_pct < -0.3:  # EMA9 nettement sous EMA21
            exit_score += 3
            exit_reasons.append(f"EMA baissière ({ema_gap_pct:.2f}%)")
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉTECTION #7: Prix sous les deux EMA = zone dangereuse
        # ═══════════════════════════════════════════════════════════════════
        if current_price < ema9 and current_price < ema21:
            exit_score += 2
            exit_reasons.append("Prix sous EMA9 et EMA21")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🆕 DÉTECTION #8: FIN DE CYCLE CREUX_REBOUND
        # Un trade CREUX est un cycle COURT (20-40min). Quand le pic est atteint
        # et que le momentum se retourne, vendre IMMÉDIATEMENT même si EMA bullish.
        # Condition: on a eu un profit ≥0.3% et on en a perdu ≥50%.
        # ═══════════════════════════════════════════════════════════════════
        if pattern == 'CREUX_REBOUND':
            _max_pnl = position.get('max_pnl', 0)
            # FIX 09/03: seuil 50% → 25% (vendre dès qu'on a perdu 25% du gain max)
            if _max_pnl >= 0.3 and current_pnl_pct < _max_pnl * 0.75:
                # Perdu 25%+ du gain max → vendre si momentum confirme
                if momentum_3 < 0 and momentum_5 < -0.05:
                    urgent_exit = True
                    exit_score += 9
                    exit_reasons.append(f"🔻 FIN CYCLE CREUX: pic+{_max_pnl:.2f}% → {current_pnl_pct:+.2f}% (mom3={momentum_3:.2f}%, mom5={momentum_5:.2f}%)")
                    print(f"   🔻 {symbol}: FIN DE CYCLE CREUX - Pic atteint et retourné → VENTE")
            # Sortie ultra-rapide si chute prononcée (≥50% des gains perdus avec mom fort)
            elif _max_pnl >= 0.3 and current_pnl_pct < _max_pnl * 0.5 and momentum_3 < -0.2:
                urgent_exit = True
                exit_score += 10
                exit_reasons.append(f"🔻 CREUX CRASH: pic+{_max_pnl:.2f}% → {current_pnl_pct:+.2f}% (mom3={momentum_3:.2f}%)")
                print(f"   🔻 {symbol}: CREUX CRASH DÉTECTÉ → VENTE URGENTE")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🆕 DÉTECTION #8b: CREUX_REBOUND EN TENDANCE BAISSIÈRE - Rebond raté
        # 🔴 FIX 06/04: Le bot achetait en CREUX_REBOUND puis restait bloqué
        # quand la tendance est clairement baissière (cas STG: EMA baissier, RSI 7)
        # Si le creux n'a PAS rebondi et la tendance est baissière → SORTIR
        # ═══════════════════════════════════════════════════════════════════
        if pattern == 'CREUX_REBOUND' and position_age_minutes > 5:
            _max_pnl_creux = position.get('max_pnl', 0)
            # Cas 1: Tendance baissière + pas en profit → le rebond n'arrive pas
            if ema_gap_pct < -0.10 and current_pnl_pct < 0.1 and momentum_5 < 0:
                urgent_exit = True
                exit_score += 8
                exit_reasons.append(f"🔻 CREUX BEARISH: EMA={ema_gap_pct:.2f}% + pnl={current_pnl_pct:+.2f}% + pas de rebond")
                print(f"   🔴 {symbol}: CREUX en tendance BAISSIÈRE — rebond RATÉ → VENTE (age={position_age_minutes:.0f}min)")
            # Cas 2: Petit profit qui fond avec trend négatif → sécuriser avant 0
            elif _max_pnl_creux >= 0.15 and current_pnl_pct < _max_pnl_creux * 0.4 and ema_gap_pct < 0 and momentum_3 < 0:
                urgent_exit = True
                exit_score += 7
                exit_reasons.append(f"🔻 CREUX GAIN PERDU: max+{_max_pnl_creux:.2f}% → {current_pnl_pct:+.2f}% (EMA bear)")
                print(f"   🔴 {symbol}: CREUX gain en train de fondre — trend négatif → VENTE")
        
        # ═══════════════════════════════════════════════════════════════════
        # 🆕 DÉTECTION #9: TENDANCE DÉTÉRIORÉE — Position en perte + trend baissier
        # 🔴 FIX 06/04: Pour TOUS les patterns, pas seulement CREUX_REBOUND.
        # Si la position est en perte et que la tendance est nettement baissière
        # ET que le momentum est négatif → pas la peine d'attendre le SL -1.8%
        # ═══════════════════════════════════════════════════════════════════
        if current_pnl_pct < -0.3 and ema_gap_pct < -0.20 and momentum_5 < -0.1:
            exit_score += 5
            exit_reasons.append(f"📉 TREND DÉTÉRIORÉ: EMA={ema_gap_pct:.2f}% + pnl={current_pnl_pct:+.2f}%")
            if rsi and rsi < 25 and current_pnl_pct < -0.5:
                urgent_exit = True
                exit_score += 5
                exit_reasons.append(f"🚨 + RSI survente ({rsi:.0f}) + perte significative")
                print(f"   🔴 {symbol}: TREND DÉTÉRIORÉ + RSI {rsi:.0f} + perte → VENTE")
        
        # ═══════════════════════════════════════════════════════════════════
        # PROTECTION: Tendance haussière forte = NE PAS VENDRE
        # RENFORCÉE: Ne JAMAIS vendre si EMA bullish + momentum positif + RSI sain
        # ═══════════════════════════════════════════════════════════════════
        # 🔴 FIX 01/04: Détection stress marché pour bypasser protections haussières
        _btc_mom_te = getattr(AIPredictor, '_btc_momentum', 0)
        _market_stress_te = (_btc_mom_te < -1.0)
        
        strong_uptrend = (
            ema_gap_pct > 0.15 and  # EMA9 nettement au-dessus d'EMA21
            momentum_3 > 0.2 and    # Momentum court terme positif
            momentum_5 > 0.3 and    # Momentum moyen terme positif
            current_price > ema9    # Prix au-dessus de EMA9
        )
        
        # RSI élevé OK si tendance forte (surachat peut durer longtemps en tendance!)
        rsi_healthy_for_uptrend = (not rsi) or (rsi >= 40)
        
        # 🔴 FIX 01/04: En stress marché, NE PAS bloquer les sorties même en uptrend
        if strong_uptrend and rsi_healthy_for_uptrend and not urgent_exit and not _market_stress_te:
            # Tendance haussière claire confirmée → NE PAS VENDRE
            # Seul le TP ou un signal urgent peut sortir
            if exit_score > 0:
                print(f"   🚀 {symbol}: Sortie annulée - Tendance FORTE (EMA gap={ema_gap_pct:+.2f}%, Mom3={momentum_3:+.2f}%, RSI={rsi or 0:.0f})")
            return False, ""
        
        # ═══════════════════════════════════════════════════════════════════
        # PROTECTION #2: EMA haussière + RSI sain = NE PAS VENDRE prématurément
        # Même si le momentum tick-by-tick est bruité, si EMA9>EMA21 et RSI>45
        # la tendance est probablement toujours bonne
        # ÉLARGI: RSI jusqu'à 85 (surachat acceptable en forte tendance)
        # ═══════════════════════════════════════════════════════════════════
        ema_bullish_with_healthy_rsi = (ema_gap_pct > 0.1 and rsi and 45 < rsi < 85 and current_price > ema9)
        # 🔴 FIX 01/04: En stress marché, NE PAS bloquer les sorties
        if ema_bullish_with_healthy_rsi and not urgent_exit and exit_score < 10 and not _market_stress_te:
            # Tendance EMA haussière + RSI sain + prix au-dessus de EMA9 = garder
            if exit_score > 0:
                print(f"   ✋ {symbol}: Sortie annulée - EMA bullish + RSI sain ({rsi:.0f})")
            return False, ""
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉTECTION IA DE VENTE ANTICIPÉE (si disponible)
        # ═══════════════════════════════════════════════════════════════════
        if AI_PREDICTOR_AVAILABLE and current_pnl_pct > 0.3 and not urgent_exit:
            try:
                ai_predictor = get_ai_predictor()
                if ai_predictor:
                    ai_should_sell, ai_reason = ai_predictor.should_sell_early(
                        symbol, 
                        prices_list, 
                        entry_price, 
                        current_pnl_pct
                    )
                    if ai_should_sell:
                        exit_score += 4
                        exit_reasons.append(ai_reason)
            except Exception as e:
                pass  # Silencieux si erreur IA
        
        # ═══════════════════════════════════════════════════════════════════
        # 🧠 IA PRÉDICTIVE DE VENTE AVANCÉE (AISellPredictor)
        # Poids FORT: l'IA peut déclencher une sortie même si le score est faible
        # ou ANNULER une sortie si elle prédit une continuation haussière
        # ═══════════════════════════════════════════════════════════════════
        ai_override_hold = False
        if hasattr(self, 'ai_sell_predictor') and self.ai_sell_predictor and not urgent_exit:
            try:
                position = self.positions.get(symbol, {})
                ai_result = self.ai_sell_predictor.predict_sell(
                    symbol, prices_list, position
                )
                ai_action = ai_result.get('action', 'HOLD')
                ai_confidence = ai_result.get('confidence', 0)
                ai_sell_score = ai_result.get('sell_score', 0)
                
                if ai_action == 'STRONG_SELL' and ai_confidence >= 70:
                    # 🔧 FIX 01/03 v2: Seuil 55→70% — trop de sorties prématurées (gains coupés)
                    exit_score += 8
                    exit_reasons.append(f"🧠 AI STRONG_SELL ({ai_confidence:.0f}%)")
                elif ai_action == 'SELL' and ai_confidence >= 65:
                    # 🔧 FIX 01/03 v2: Seuil 50→65% — ratio G/P 0.57:1 = gains trop faibles
                    exit_score += 5
                    exit_reasons.append(f"🧠 AI SELL ({ai_confidence:.0f}%)")
                elif ai_action in ('EXTEND_TP', 'HOLD') and current_pnl_pct > -1.0:
                    # 🔴 FIX 08/02: Protéger AUSSI les positions en petite perte (pas juste > 0)
                    # 🔴 FIX 06/04: NE PAS override si protection CREUX levée pour trend baissier
                    # L'IA dit HOLD mais le trend est clairement contre nous → ne pas bloquer
                    if not _creux_protection_lifted:
                        ai_override_hold = True
                        if exit_score > 0:
                            exit_reasons.append(f"🧠 AI HOLD override ({ai_confidence:.0f}%)")
                    else:
                        if exit_score > 0:
                            exit_reasons.append(f"🧠 AI HOLD IGNORÉ (creux bearish, {ai_confidence:.0f}%)")
            except Exception:
                pass
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉCISION DE SORTIE - Seuils renforcés pour éviter les sorties prématurées
        # ═══════════════════════════════════════════════════════════════════
        
        should_exit = False
        
        # 🆕 FIX 09/03 v6: CREUX_REBOUND urgent exit — ne peut PAS être bloqué par AI override
        # Quand Detection #8 (cycle retourné) ou Detection #4c (sell spike) fire avec urgent_exit=True,
        # l'AI HOLD ne doit PAS bloquer: le cycle CREUX est terminé, il faut sortir.
        # Cas ORCA: Detection #8 → urgent_exit=True, score=9, AI dit HOLD → bloqué! Corrigé ici.
        # 🔴 FIX 06/04: Élargi aux détections #8b (CREUX BEARISH) et #9 (TREND DÉTÉRIORÉ)
        _creux_urgent = (urgent_exit and exit_score >= 8 and pattern == 'CREUX_REBOUND')
        _trend_deteriorated = (urgent_exit and exit_score >= 10 and ema_gap_pct < -0.15)
        
        # 🧠 AI OVERRIDE: Si l'IA ne recommande pas de vendre ET pas d'urgence CREUX/trend
        #    → bloquer les sorties même à score élevé (sauf très haut)
        # 🔴 FIX 01/04: En stress marché, abaisser le seuil AI HOLD à score < 8
        #    pour permettre aux signaux combinés de déclencher une sortie
        _ai_hold_threshold = 8 if _market_stress_te else 18
        if ai_override_hold and exit_score < _ai_hold_threshold and not _creux_urgent and not _trend_deteriorated:
            should_exit = False
        # 🚨 SORTIE URGENTE CREUX_REBOUND (cycle inversé/bearish = sortir même sans AI confirmation)
        elif _creux_urgent:
            should_exit = True
        # 🚨 SORTIE TREND DÉTÉRIORÉ (tendance baissière + en perte = sortir)
        elif _trend_deteriorated:
            should_exit = True
        # 🚨 SORTIE URGENTE IMMÉDIATE (score élevé avec urgence)
        # 🔴 FIX 06/04: Abaissé de 16 à 12 — 16 était TROP conservateur
        # Les nouveaux signaux (#8b, #9) scorent 8-10 + urgent mais étaient ignorés
        elif urgent_exit and exit_score >= 12:
            should_exit = True
        # Score TRÈS élevé = danger clair
        elif exit_score >= 18:
            should_exit = True
        # 🔴 FIX 09/02: SUPPRIMÉ les règles "score + perte" car:
        # 1) La PnL gate bloque déjà les positions en perte (pnl < 0.3%)
        # 2) Ici on ne traite QUE des positions en profit
        # 3) Pour les profits, seules les conditions END_OF_CYCLE/retournement comptent
        # Les anciennes règles (score>=9 + pnl<-1.5%, score>=6 + pnl<-2.0%) sont inutiles
        
        if should_exit:
            urgency = "🚨 URGENT" if urgent_exit else "⚡"
            reason = f"{urgency} EXIT (score={exit_score}): {', '.join(exit_reasons[:3])}"
            return True, reason
        
        # LOG DEBUG pour positions en profit qui ne sortent pas
        if exit_score > 5:
            print(f"   🔍 [DEBUG EXIT] {symbol}: pnl={current_pnl_pct:+.2f}%, score={exit_score}, urgent={urgent_exit}")
            rsi_str = f"{rsi:.1f}" if rsi else "N/A"
            print(f"      RSI={rsi_str}, EMA_gap={ema_gap_pct:+.2f}%, Mom3={momentum_3:+.2f}%")
        
        return False, ""
    
    def check_stop_loss_take_profit(self):
        """
        Vérifie les stop-loss et take-profit avec TRAILING STOP DYNAMIQUE
        
        Le trailing stop permet de:
        - Remonter le stop-loss quand le prix monte
        - Ne pas vendre trop tôt quand une crypto continue à monter
        - Protéger les gains acquis
        
        SÉCURITÉ AJOUTÉE:
        - Circuit breaker si pertes > 10% du capital
        - Stop-loss d'urgence à -5% (au lieu de -1.5%)
        - Détection de positions bloquées
        - Nettoyage automatique des positions invalides
        """
        
        if len(self.positions) == 0:
            return
        
        # CIRCUIT BREAKER: Calculer les pertes totales
        total_pnl_pct = 0
        positions_in_loss = 0
        critical_positions = []
        
        for symbol, position in list(self.positions.items()):
            current_price = self.client.get_price(symbol)
            if current_price:
                entry_price = position['entry_price']
                pnl_pct = ((current_price / entry_price) - 1) * 100
                total_pnl_pct += pnl_pct
                
                if pnl_pct < -3:
                    positions_in_loss += 1
                if pnl_pct < -5:
                    critical_positions.append((symbol, pnl_pct))
        
        # ALERTE si pertes massives
        if len(self.positions) > 0:
            avg_pnl = total_pnl_pct / len(self.positions)
            if avg_pnl < -3:
                print(f"\n🚨 ALERTE: P&L moyen = {avg_pnl:.2f}% | {positions_in_loss}/{len(self.positions)} positions en perte")
            if critical_positions:
                print(f"⚠️ {len(critical_positions)} positions critiques (< -5%): {', '.join([f'{s} {p:.1f}%' for s, p in critical_positions])}")
        
        if len(self.positions) == 0:
            return
        
        for symbol, position in list(self.positions.items()):
            current_price = self.client.get_price(symbol)
            
            if current_price is None or current_price == 0:
                # SÉCURITÉ: Forcer la fermeture si prix indisponible après 5 échecs
                failed_price_checks = position.get('failed_price_checks', 0) + 1
                position['failed_price_checks'] = failed_price_checks
                self._save_positions()
                
                if failed_price_checks >= 5:
                    print(f"   🚨 {symbol}: 5+ échecs de prix → SUPPRESSION AUTOMATIQUE")
                    print(f"      Raison: Symbole potentiellement délisté ou invalide")
                    # Supprimer directement au lieu de tenter une vente impossible
                    del self.positions[symbol]
                    self._save_positions()
                    print(f"   ✅ Position {symbol} supprimée du portefeuille")
                else:
                    print(f"   ⚠️ {symbol}: Prix non disponible (tentative {failed_price_checks}/5)")
                continue
            
            entry_price = position['entry_price']
            current_pnl_pct = ((current_price / entry_price) - 1) * 100

            # Tracker le P&L max/min pour les logs (max_profit_pct / max_drawdown_pct)
            if current_pnl_pct > position.get('max_profit_pct', -999):
                position['max_profit_pct'] = current_pnl_pct
            if current_pnl_pct < position.get('max_drawdown_pct', 999):
                position['max_drawdown_pct'] = current_pnl_pct

            # ═══════════════════════════════════════════════════════════════
            # MAX_HOLD ADAPTATIF AU RÉGIME DE MARCHÉ
            # BULL: 4h  |  NEUTRAL: 3h  |  BEAR/CORRECTION: 1h30
            # En marché baissier, un trade stagnant > 1h30 a très peu de chances
            # de récupérer — mieux vaut couper et préserver le capital.
            # ═══════════════════════════════════════════════════════════════
            try:
                _regime_for_hold = getattr(self, 'current_regime', 'NEUTRAL')
                if _regime_for_hold in ('BEAR', 'CORRECTION'):
                    MAX_HOLDING_MINUTES = MAX_HOLD_MINUTES_BEAR    # 90 min
                elif _regime_for_hold == 'NEUTRAL':
                    MAX_HOLDING_MINUTES = MAX_HOLD_MINUTES_NEUTRAL  # 180 min
                else:
                    MAX_HOLDING_MINUTES = MAX_HOLD_MINUTES_BULL     # 240 min
            except Exception:
                MAX_HOLDING_MINUTES = 240
            try:
                open_ts = position.get('timestamp', '')
                if open_ts:
                    if isinstance(open_ts, str):
                        entry_dt = datetime.fromisoformat(open_ts)
                    else:
                        entry_dt = open_ts
                    holding_minutes = (datetime.now() - entry_dt).total_seconds() / 60
                    if holding_minutes >= MAX_HOLDING_MINUTES:
                        print(f"   ⏱️ {symbol}: Position ouverte depuis {holding_minutes:.0f}min (>{MAX_HOLDING_MINUTES}min) → MAX_HOLD (régime={_regime_for_hold})")
                        self.close_position(symbol, f"MAX_HOLD_{MAX_HOLDING_MINUTES}min ({current_pnl_pct:+.1f}%)")
                        continue
            except Exception as e:
                pass  # Ne pas bloquer si erreur de parsing timestamp
            
            # ═══════════════════════════════════════════════════════════════
            # TRAILING STOP PROGRESSIF PAR PALIERS (GAIN_LOCK)
            # Plus on est en profit, plus le trailing est serré.
            # Chaque palier est défini dans config.py (TRAILING_TIERS).
            # En plus du trailing standard, un "lock" garantit un % du gain actuel.
            # ═══════════════════════════════════════════════════════════════
            
            # Récupérer ou initialiser le prix max atteint
            if 'max_price' not in position:
                position['max_price'] = current_price
            elif current_price > position['max_price']:
                position['max_price'] = current_price
                self._save_positions()

            # 🔧 FIX 08/03: Mise à jour max_price depuis le HIGH de la dernière bougie
            # Capture les piques ultra-rapides (<2s) qui passent entre deux scans
            # Throttlé à 1 appel API / 90s par position pour éviter les rate limits
            if not hasattr(self, '_kline_max_ts'):
                self._kline_max_ts = {}
            if time.time() - self._kline_max_ts.get(symbol, 0) >= 90:
                try:
                    recent_k = self.client.get_klines(symbol, interval='1m', limit=3)
                    if recent_k:
                        kline_high = max(float(k[2]) for k in recent_k)
                        if kline_high > position.get('max_price', 0):
                            position['max_price'] = kline_high
                            kline_pnl = ((kline_high / entry_price) - 1) * 100
                            if kline_pnl > position.get('max_pnl', 0):
                                position['max_pnl'] = round(kline_pnl, 3)
                            # 🔧 FIX 25/03: Recalcul immédiat du trailing SL depuis le kline HIGH
                            # Bug identifié: max_price mis à jour par le kline check, mais le trailing
                            # n'était PAS recalculé si current_price < palier min à cet instant.
                            # Résultat: spike capturé, SL inchangé. Fix: appliquer le trail depuis le HIGH
                            # même si current_price est temporairement plus bas.
                            for _tier_min, _tier_trail, _tier_lock in reversed(TRAILING_TIERS):
                                if kline_pnl >= _tier_min:
                                    _kline_trail_sl = kline_high * (1 - _tier_trail / 100)
                                    if (_kline_trail_sl > position['stop_loss'] and
                                            _kline_trail_sl < current_price * 0.999):
                                        old_sl = position['stop_loss']
                                        position['stop_loss'] = _kline_trail_sl
                                        print(f"   📈 {symbol}: Trail KLINE-HIGH +{_tier_min:.1f}% "
                                              f"(dist={_tier_trail:.2f}%) SL {old_sl:.5f} → {_kline_trail_sl:.5f} "
                                              f"[kline_pnl={kline_pnl:+.2f}%]")
                                    break
                            self._save_positions()
                except Exception:
                    pass
                self._kline_max_ts[symbol] = time.time()

            max_price = position['max_price']

            # Choisir le palier de trailing stop actif (le plus élevé applicable)
            active_tier = None
            for tier_min_pct, tier_trail_pct, tier_lock_ratio in reversed(TRAILING_TIERS):
                if current_pnl_pct >= tier_min_pct:
                    active_tier = (tier_min_pct, tier_trail_pct, tier_lock_ratio)
                    break
            
            if active_tier:
                tier_min_pct, tier_trail_pct, tier_lock_ratio = active_tier
                
                # 🆕 FIX 09/03: CREUX_REBOUND = distance trailing 2x plus serrée
                # Un cycle CREUX est court (20-40min) → sortir rapidement au pic
                _creux_trail_mult = 0.5 if position.get('pattern') == 'CREUX_REBOUND' else 1.0
                _effective_trail = tier_trail_pct * _creux_trail_mult
                
                # 1) Trailing depuis le max atteint
                new_stop_trail = max_price * (1 - _effective_trail / 100)
                
                # 2) Lock (garantir un % du gain actuel) si le palier l'exige
                new_stop_lock = position['stop_loss']
                if tier_lock_ratio > 0:
                    locked_gain = entry_price * (1 + (current_pnl_pct * tier_lock_ratio) / 100)
                    new_stop_lock = max(new_stop_lock, locked_gain)
                
                # 3) Prendre le plus haut des deux (trailing vs lock)
                new_stop = max(new_stop_trail, new_stop_lock)
                
                # 4) Ne jamais baisser le stop-loss ET jamais au-dessus du prix actuel
                if new_stop > position['stop_loss'] and new_stop < current_price * 0.999:
                    old_sl = position['stop_loss']
                    position['stop_loss'] = new_stop
                    lock_str = f" | lock {tier_lock_ratio*100:.0f}% gains" if tier_lock_ratio > 0 else ""
                    creux_str = " [CREUX×0.5]" if _creux_trail_mult < 1.0 else ""
                    print(f"   📈 {symbol}: Trail PALIER+{tier_min_pct:.0f}% (dist={_effective_trail:.2f}%{lock_str}{creux_str}) SL {currency.format(old_sl)} → {currency.format(new_stop)} [P&L={current_pnl_pct:+.1f}%]")
                    self._save_positions()
            
            # ═══════════════════════════════════════════════════════════════
            # 🆕 CREUX PEAK EXIT — vente directe au retournement du pic
            # Opère CHAQUE cycle (pas de contrainte d'âge) et bypass le trailing
            # 🔴 FIX 06/04: Seuil abaissé de 0.3% à 0.15% — sécuriser même les petits gains
            # Conditions: gain max ≥ 0.15% ET chute depuis pic ≥ 0.20% ET encore proche du 0
            # ═══════════════════════════════════════════════════════════════
            if position.get('pattern') == 'CREUX_REBOUND':
                _cpx_max_pnl  = position.get('max_pnl', 0)
                _cpx_drop     = ((current_price - max_price) / max_price) * 100 if max_price > 0 else 0
                if _cpx_max_pnl >= 0.15 and _cpx_drop < -0.20 and current_pnl_pct > -0.5:
                    print(f"   🔻 {symbol}: CREUX PEAK EXIT — pic+{_cpx_max_pnl:.2f}% → {current_pnl_pct:+.2f}% ({_cpx_drop:.2f}% depuis max)")
                    self.close_position(symbol, f"creux-peak-exit ({_cpx_drop:.2f}% du pic, P&L={current_pnl_pct:+.2f}%)")
                    continue
            
            # ═══════════════════════════════════════════════════════════════
            # GESTION DES PALIERS DE PROFIT
            # ═══════════════════════════════════════════════════════════════
            
            # ⚠️ DÉSACTIVÉ en tendance haussière forte car trop agressif!
            # Le problème: Stop au break-even/protection 50% gains arrive trop tôt
            # et tue les grandes progressions haussières (ex: ARB +1.4% au lieu de +10%)
            
            # Vérifier si tendance haussière FORTE (EMA bullish + momentum positif)
            # → Si OUI, laisser courir les gains avec trailing stop uniquement
            is_strong_uptrend = False
            try:
                # Récupérer données récentes via Binance
                klines = self.client.get_klines(symbol, interval='1m', limit=30)
                if len(klines) >= EMA_LONG:
                    prices_list = [float(k[4]) for k in klines]  # Close prices
                    ema9 = TechnicalIndicators.ema(prices_list, EMA_SHORT)
                    ema21 = TechnicalIndicators.ema(prices_list, EMA_LONG)
                    momentum_3 = ((current_price - prices_list[-3]) / prices_list[-3]) * 100 if len(prices_list) >= 3 else 0
                    momentum_5 = ((current_price - prices_list[-5]) / prices_list[-5]) * 100 if len(prices_list) >= 5 else 0
                    
                    if ema9 and ema21:
                        ema_gap_pct = ((ema9 - ema21) / ema21) * 100
                        # Tendance FORTE: EMA gap > +0.15% ET momentum positif sur 3 et 5 bougies
                        is_strong_uptrend = (
                            ema_gap_pct > 0.15 and 
                            momentum_3 > 0.1 and 
                            momentum_5 > 0.2 and
                            current_price > ema9  # Prix au-dessus de EMA9
                        )
                        if is_strong_uptrend:
                            print(f"   🚀 {symbol}: Tendance FORTE détectée - Paliers protection désactivés (EMA gap={ema_gap_pct:+.2f}%, Mom3={momentum_3:+.2f}%)")
            except Exception as e:
                pass
            
            # Appliquer paliers UNIQUEMENT si pas en tendance forte
            if not is_strong_uptrend:
                # Palier 1: +50% du TP → Mettre stop au break-even
                tp_pct = ((position['take_profit'] / entry_price) - 1) * 100
                if current_pnl_pct >= tp_pct * 0.5:  # 50% du take profit atteint
                    breakeven_stop = entry_price * 1.001  # +0.1% pour couvrir les frais
                    # SÉCURITÉ: Le breakeven_stop ne doit JAMAIS être au-dessus du prix actuel
                    if position['stop_loss'] < breakeven_stop < current_price:
                        position['stop_loss'] = breakeven_stop
                        print(f"   🛡️ {symbol}: Stop au break-even (50% du TP atteint)")
                        self._save_positions()
                    elif breakeven_stop >= current_price:
                        print(f"   ⚠️ {symbol}: Break-even stop ignoré (au-dessus du prix actuel {currency.format(current_price)})")
                
                # Palier 2: +75% du TP → Protéger 50% des gains
                if current_pnl_pct >= tp_pct * 0.75:
                    protect_stop = entry_price * (1 + (current_pnl_pct * 0.5) / 100)
                    # SÉCURITÉ: Le protect_stop ne doit JAMAIS être au-dessus du prix actuel
                    # (sinon on vendrait immédiatement après une baisse)
                    if position['stop_loss'] < protect_stop < current_price:
                        position['stop_loss'] = protect_stop
                        print(f"   💰 {symbol}: Protection 50% des gains (75% du TP atteint)")
                        self._save_positions()
                    elif protect_stop >= current_price:
                        print(f"   ⚠️ {symbol}: Protect stop ignoré (au-dessus du prix actuel)")

            
            # ═══════════════════════════════════════════════════════════════
            # VÉRIFICATION STOP-LOSS / TAKE-PROFIT
            # ═══════════════════════════════════════════════════════════════
            
            # ═══════════════════════════════════════════════════════════════
            # 🆕 FIX CRASH-TEST v7: LSTM_DANGER — fermer si REVERSAL_DOWN détecté
            # Le LSTM prédit un retournement baissier → couper si P&L < 0.8%
            # Seuil 0.8% = laisser respirer les trades gagnants (backtest v3)
            # Conditions: REVERSAL_DOWN, confidence >= 70%, probability >= 0.50
            # ═══════════════════════════════════════════════════════════════
            if AI_PREDICTOR_AVAILABLE:
                try:
                    ai_pred = get_ai_predictor()
                    if ai_pred and hasattr(ai_pred, 'reversal_predictor') and ai_pred.reversal_predictor:
                        # Récupérer les prix récents pour le LSTM (besoin de 60 bougies 5min)
                        klines_5m = self.client.get_klines(symbol, interval='5m', limit=65)
                        if klines_5m and len(klines_5m) >= 60:
                            import numpy as np
                            prices_5m = np.array([float(k[4]) for k in klines_5m], dtype=np.float64)
                            volumes_5m = np.array([float(k[5]) for k in klines_5m], dtype=np.float64)
                            
                            lstm_result = ai_pred.reversal_predictor.predict(prices_5m, volumes_5m)
                            
                            if lstm_result and lstm_result.get('is_danger_signal', False):
                                danger_conf = lstm_result.get('confidence', 0)
                                danger_prob = lstm_result.get('reversal_probability', 0)
                                
                                # Seuils stricts: confidence >= 70%, probability >= 0.50
                                if danger_conf >= 70 and danger_prob >= 0.50:
                                    # Ne couper que si P&L < 0.8% (laisser respirer les gains)
                                    if current_pnl_pct < 0.8:
                                        print(f"   🧠 {symbol}: LSTM REVERSAL_DOWN détecté (conf={danger_conf:.0f}%, prob={danger_prob:.2f}) P&L={current_pnl_pct:+.2f}% < 0.8% → FERMETURE")
                                        self.close_position(symbol, f"LSTM_DANGER (conf={danger_conf:.0f}%)")
                                        continue
                                    else:
                                        print(f"   🧠 {symbol}: LSTM DANGER ignoré - P&L={current_pnl_pct:+.2f}% >= 0.8% (laisser courir)")
                except Exception as e:
                    pass  # Ne pas bloquer si erreur LSTM
            
            # ═══════════════════════════════════════════════════════════════
            # STRATÉGIE BB+ BREAKOUT: Vendre dès que prix < BB+ (AVEC PROTECTION TENDANCE HAUSSIÈRE)
            # ═══════════════════════════════════════════════════════════════
            if position.get('bb_breakout_entry', False):
                # Cette position a été ouverte sur un breakout BB+
                # On vend dès que le prix repasse sous la bande supérieure
                # SAUF si la tendance haussière est toujours forte (gains en croissance)
                try:
                    # Récupérer les prix récents pour calculer BB+
                    if symbol in self.prices and len(self.prices[symbol]) >= 20:
                        prices_list = list(self.prices[symbol])
                        bb_upper, bb_mid, bb_lower = TechnicalIndicators.bollinger(prices_list)
                        
                        if bb_upper and current_price < bb_upper:
                            # Prix est repassé sous BB+ → Vérifier si on doit vraiment vendre
                            
                            # NOUVEAU: Calculer Keltner pour vérifier si tendance continue
                            kc_upper, kc_mid, kc_lower = TechnicalIndicators.keltner_channels(prices_list, 20, 10, 2.0)
                            
                            # Calculer le momentum court terme (3 dernières bougies)
                            momentum_3 = ((current_price - prices_list[-3]) / prices_list[-3]) * 100 if len(prices_list) >= 3 else 0
                            momentum_5 = ((current_price - prices_list[-5]) / prices_list[-5]) * 100 if len(prices_list) >= 5 else 0
                            
                            # Tracker le P&L max pour détecter si on commence à perdre des gains
                            if 'max_pnl' not in position:
                                position['max_pnl'] = current_pnl_pct
                            elif current_pnl_pct > position['max_pnl']:
                                position['max_pnl'] = current_pnl_pct
                                self._save_positions()
                            
                            pnl_drop_from_max = current_pnl_pct - position.get('max_pnl', 0)
                            
                            # NOUVELLE LOGIQUE (selon analyse utilisateur):
                            # Ne pas vendre tant que KC > BB+ (tendance haussière confirmée)
                            kc_above_bb = (kc_upper and bb_upper and kc_upper > bb_upper)
                            
                            if kc_above_bb:
                                # KC > BB+ = Vraie tendance haussière continue (ATR confirme)
                                # NE PAS VENDRE même si prix < BB+
                                print(f"   🚀 {symbol}: Maintien position - KC>BB+ (tendance haussière confirmée)")
                                print(f"      Prix: {currency.format(current_price)}, BB+: {currency.format(bb_upper)}, KC+: {currency.format(kc_upper)}")
                                print(f"      P&L: {current_pnl_pct:+.2f}%, Momentum 3min: {momentum_3:+.2f}%")
                                continue  # Garder la position
                            
                            # CONDITION DE VENTE: Prix sous BB+ ET KC <= BB+ (fin de tendance)
                            should_exit = False
                            exit_reason = ""
                            
                            # Cas 1: KC passe sous BB+ = Fin de la vraie tendance (signal principal)
                            if not kc_above_bb:
                                should_exit = True
                                exit_reason = f"KC≤BB+ (fin tendance haussière)"
                            
                            # Cas 2: Momentum négatif sur 3 bougies = baisse active
                            elif momentum_3 < -0.3:
                                should_exit = True
                                exit_reason = f"Baisse active (mom3={momentum_3:.2f}%)"
                            
                            # Cas 3: Momentum négatif sur 5 bougies = tendance baissière confirmée
                            elif momentum_5 < -0.5:
                                should_exit = True
                                exit_reason = f"Tendance baissière (mom5={momentum_5:.2f}%)"
                            
                            # Cas 4: P&L diminue de plus de 0.5% depuis le max
                            elif pnl_drop_from_max < -0.5:
                                should_exit = True
                                exit_reason = f"Gains en baisse ({pnl_drop_from_max:.2f}% depuis max)"
                            
                            # Cas 5: Prix très en dessous de BB+ (> 1%) = sortie probable du momentum
                            bb_distance = ((bb_upper - current_price) / current_price) * 100
                            if bb_distance > 1.0 and momentum_3 < 0.2:
                                should_exit = True
                                exit_reason = f"Loin de BB+ (-{bb_distance:.2f}%) + momentum faible"
                            
                            if should_exit:
                                # VENTE confirmée
                                bb_distance_display = ((bb_upper - current_price) / current_price) * 100
                                print(f"\n   📊 BB+ BREAKOUT EXIT: {symbol}")
                                print(f"      Prix: {currency.format(current_price)} < BB+: {currency.format(bb_upper)} (-{bb_distance_display:.2f}%)")
                                print(f"      P&L actuel: {current_pnl_pct:+.2f}% | P&L max: {position.get('max_pnl', 0):+.2f}%")
                                print(f"      Raison: {exit_reason}")
                                self.close_position(symbol, f"bb-breakout-exit ({exit_reason})")
                                continue
                            else:
                                # MAINTIEN de la position - Tendance haussière toujours active
                                if momentum_3 > 0 or momentum_5 > 0:
                                    print(f"   ✅ {symbol}: BB+ exit suspendu - Hausse continue (mom3={momentum_3:+.2f}%, P&L={current_pnl_pct:+.2f}%)")
                
                except Exception as e:
                    # Si erreur de calcul BB, continuer avec les autres vérifications
                    print(f"   ⚠️ {symbol}: Erreur vérification BB+ - {e}")
            
            # INITIALISATION RÉTROACTIVE: max_pnl pour positions anciennes
            if 'max_pnl' not in position:
                position['max_pnl'] = current_pnl_pct
                self._save_positions()
            
            # ═══════════════════════════════════════════════════════════════
            # 🧠 IA PRÉDICTIVE DE VENTE — PRIORITÉ IA SUR LES RÈGLES FIXES
            # L'IA analyse chaque position et peut:
            #   - Déclencher une vente AVANT que le SL fixe soit touché
            #   - Empêcher une vente prématurée (EXTEND_TP)
            #   - Ajuster dynamiquement les niveaux SL/TP
            # SÉCURITÉ: Le SL d'urgence -5% reste TOUJOURS prioritaire
            # ═══════════════════════════════════════════════════════════════
            if self.ai_sell_predictor:
                try:
                    # Récupérer les prix récents pour l'IA
                    ai_prices = None
                    try:
                        ai_klines = self.client.get_klines(symbol, interval='1m', limit=60)
                        if ai_klines and len(ai_klines) >= 50:
                            ai_prices = [float(k[4]) for k in ai_klines]
                    except:
                        pass
                    
                    if ai_prices and len(ai_prices) >= 50:
                        ai_result = self.ai_sell_predictor.predict_sell(
                            symbol, ai_prices, position
                        )
                        
                        action = ai_result.get('action', 'HOLD')
                        confidence = ai_result.get('confidence', 0)
                        sell_score = ai_result.get('sell_score', 0)
                        ai_reasons = ai_result.get('reasons', [])
                        
                        # Lire le régime de marché (pour protéger les positions en BULL_STRONG)
                        _is_bull_strong = False
                        try:
                            if self.market_regime:
                                _reg_name, _ = self.market_regime.get_current_regime()
                                _is_bull_strong = (_reg_name == 'BULL_STRONG')
                        except:
                            pass
                        
                        # Seuils de confiance selon régime
                        # 🔧 FIX 16/03: En BULL_STRONG, exiger confidence plus élevée
                        #   → les corrections -1% à -3% sont temporaires, laisser le SL gérer
                        _strong_sell_conf = 75 if _is_bull_strong else 60
                        _sell_conf = 65 if _is_bull_strong else 50
                        
                        # === STRONG_SELL: Vente immédiate (IA très confiante) ===
                        if action == 'STRONG_SELL' and confidence >= _strong_sell_conf:
                            reasons_str = ', '.join(ai_reasons[:3])
                            print(f"\n   🧠🔴 AI STRONG_SELL: {symbol} (conf={confidence:.0f}%, score={sell_score:.0f})")
                            print(f"      P&L: {current_pnl_pct:+.2f}% | Dir: {ai_result.get('predicted_direction')} | {reasons_str}")
                            self.close_position(symbol, f"ai-strong-sell ({reasons_str[:60]})")
                            continue
                        
                        # === SELL: Vente recommandée (IA confiante) ===
                        elif action == 'SELL' and confidence >= _sell_conf:
                            # 🔧 FIX 16/03 BULL_STRONG: ne pas couper les petites pertes
                            #   Laisser le SL gérer -1% à -3%, l'IA coupe si > 3% ou bon profit
                            _sell_pnl_ok = (current_pnl_pct > 1.5 or current_pnl_pct < -3.0) if _is_bull_strong else \
                                           (current_pnl_pct > 0.5 or current_pnl_pct < -1.5)
                            if _sell_pnl_ok:
                                reasons_str = ', '.join(ai_reasons[:3])
                                print(f"\n   🧠🟡 AI SELL: {symbol} (conf={confidence:.0f}%, P&L={current_pnl_pct:+.2f}%)")
                                print(f"      Dir: {ai_result.get('predicted_direction')} | {reasons_str}")
                                self.close_position(symbol, f"ai-sell ({reasons_str[:60]})")
                                continue
                        
                        # === EXTEND_TP: L'IA prédit continuation → étendre le TP ===
                        elif action == 'EXTEND_TP' and confidence >= 40:
                            ai_tp = ai_result.get('ai_take_profit')
                            if ai_tp and ai_tp > position.get('take_profit', 0):
                                old_tp = position['take_profit']
                                position['take_profit'] = ai_tp
                                self._save_positions()
                                print(f"   🧠📈 {symbol}: IA étend TP {currency.format(old_tp)} → {currency.format(ai_tp)} (continuation prédite, conf={confidence:.0f}%)")
                        
                        # === SL/TP DYNAMIQUES: Ajuster les niveaux ===
                        ai_sl = ai_result.get('ai_stop_loss')
                        if ai_sl and ai_sl > position.get('stop_loss', 0):
                            old_sl = position['stop_loss']
                            position['stop_loss'] = ai_sl
                            self._save_positions()
                            print(f"   🧠📊 {symbol}: IA resserre SL {currency.format(old_sl)} → {currency.format(ai_sl)} (protection IA)")
                        
                except Exception as e:
                    pass  # Ne jamais bloquer le SL/TP pour une erreur IA
            
            # STOP-LOSS D'URGENCE: -5% (sécurité supplémentaire — TOUJOURS prioritaire)
            if current_pnl_pct <= -5:
                print(f"\n   🚨 STOP-LOSS D'URGENCE: {symbol} (P&L: {current_pnl_pct:.2f}% <= -5%)")
                result = self.close_position(symbol, "stop-loss-urgence")
                if not result:
                    print(f"   ⚠️⚠️⚠️ ÉCHEC fermeture {symbol}! Tentative forcée...")
                    # Tentative de vente forcée avec quantité réduite
                    try:
                        reduced_qty = self.client.format_quantity(symbol, position['quantity'] * 0.99)
                        backup_order = self.client.market_sell(symbol, reduced_qty)
                        if backup_order:
                            print(f"   ✅ Vente forcée réussie: {symbol}")
                    except:
                        print(f"   ❌❌❌ ÉCHEC TOTAL! Position {symbol} BLOQUÉE!")
                continue
            
            # Stop-Loss normal
            if current_price <= position['stop_loss']:
                # Vérifier si position déjà tentée plusieurs fois
                failed_attempts = position.get('failed_close_attempts', 0)
                
                if failed_attempts >= 3:
                    print(f"\n   🚨 {symbol}: 3+ échecs de fermeture → SUPPRESSION FORCÉE")
                    print(f"      P&L estimé: {current_pnl_pct:.2f}% (non réalisé)")
                    del self.positions[symbol]
                    self._save_positions()
                    print(f"   ✅ Position supprimée pour éviter blocage")
                    continue
                
                if current_pnl_pct > 0:
                    print(f"\n   🟡 TRAILING STOP: {symbol} (Prix: {currency.format(current_price)} | P&L: +{current_pnl_pct:.2f}%)")
                    result = self.close_position(symbol, "trailing-stop")
                    if not result:
                        position['failed_close_attempts'] = failed_attempts + 1
                        self._save_positions()
                        print(f"   ⚠️ Échec trailing stop {symbol} (tentative {failed_attempts + 1}/3)")
                else:
                    print(f"\n   🔴 STOP-LOSS: {symbol} (Prix: {currency.format(current_price)} <= SL: {currency.format(position['stop_loss'])})")
                    result = self.close_position(symbol, "stop-loss")
                    if not result:
                        position['failed_close_attempts'] = failed_attempts + 1
                        self._save_positions()
                        print(f"   ⚠️ Échec stop-loss {symbol} (tentative {failed_attempts + 1}/3)")
            
            # Take-Profit (mais considérer de laisser courir si momentum fort)
            elif current_price >= position['take_profit']:
                print(f"\n   🟢 TAKE-PROFIT: {symbol} (Prix: {currency.format(current_price)} >= TP: {currency.format(position['take_profit'])})")
                self.close_position(symbol, "take-profit")

# ═══════════════════════════════════════════════════════════════════════════════
# PORTFOLIO BEAR MELT DETECTOR
# Détecte quand la majorité des positions chutent simultanément
# et vend en priorité les positions profitables avant qu’elles perdent leurs gains.
# ═══════════════════════════════════════════════════════════════════════════════

    def check_portfolio_bear_melt(self):
        """
        Détecteur de collapse global du portefeuille.
        Si PORTFOLIO_BEAR_MELT_RATIO des positions perdent simultanément,
        vend en priorité celles qui sont encore en profit pour sécuriser les gains.
        Ensuite resserre les SL des positions restantes près du prix actuel.
        """
        if len(self.positions) < 3:  # Pas assez de positions pour une détection fiable
            return
        
        # Compter les positions en chute (momentum 5min négatif)
        dropping = []
        profitable = []
        all_positions_data = []
        
        for symbol, pos in list(self.positions.items()):
            try:
                cur_px = self.client.get_price(symbol)
                if not cur_px:
                    continue
                ep = pos.get('entry_price', cur_px)
                cur_pnl = ((cur_px / ep) - 1) * 100
                
                # Momentum 5 bougies 1min
                try:
                    klines = self.client.get_klines(symbol, interval='1m', limit=7)
                    if klines and len(klines) >= 5:
                        px_5ago = float(klines[-5][4])
                        mom5 = ((cur_px - px_5ago) / px_5ago) * 100
                    else:
                        mom5 = 0.0
                except Exception:
                    mom5 = 0.0
                
                all_positions_data.append((symbol, cur_px, cur_pnl, mom5))
                if mom5 < PORTFOLIO_BEAR_MELT_DROP_PCT:
                    dropping.append(symbol)
                if cur_pnl > PORTFOLIO_BEAR_MELT_MIN_PNL:
                    profitable.append((symbol, cur_pnl, cur_px))
            except Exception:
                continue
        
        if len(all_positions_data) == 0:
            return
        
        drop_ratio = len(dropping) / len(all_positions_data)
        
        if drop_ratio < PORTFOLIO_BEAR_MELT_RATIO:
            return  # Pas encore un collapse global
        
        print(f"\n🚨 PORTFOLIO BEAR MELT: {len(dropping)}/{len(all_positions_data)} positions en chute (≥{PORTFOLIO_BEAR_MELT_RATIO*100:.0f}%)")
        print(f"   Dropping: {', '.join(dropping)}")
        
        # Étape 1: Vendre en priorité les positions profitables (sécuriser les gains)
        sold = []
        for symbol, cur_pnl, cur_px in sorted(profitable, key=lambda x: x[1], reverse=True):
            print(f"   💰 Vente BEAR_MELT {symbol} P&L={cur_pnl:+.2f}% → Sécurisation")
            self.close_position(symbol, f"BEAR_MELT_PROFIT ({cur_pnl:+.1f}%)")
            sold.append(symbol)
        
        # Étape 2: Resserrer le SL des positions restantes à -1% du prix actuel
        tightened = 0
        for symbol, cur_px, cur_pnl, mom5 in all_positions_data:
            if symbol in sold:
                continue  # Déjà vendu
            if symbol not in self.positions:
                continue
            pos = self.positions[symbol]
            tight_sl = cur_px * 0.990  # -1% depuis prix actuel
            if tight_sl > pos.get('stop_loss', 0) and tight_sl < cur_px:
                pos['stop_loss'] = tight_sl
                tightened += 1
        
        if tightened > 0:
            self._save_positions()
            print(f"   🛡️ {tightened} SL resserrés à -1% du prix actuel (positions restantes)")


# ═══════════════════════════════════════════════════════════════════════════════
# BOT DE TRADING PRINCIPAL
# ═══════════════════════════════════════════════════════════════════════════════

class TradingBot:
    """Bot de trading automatique"""
    
    SETTINGS_FILE = os.path.join(SCRIPT_DIR, "bot_settings.json")
    WATCHLIST_FILE = os.path.join(SCRIPT_DIR, "watchlist.json")
    
    def __init__(self):
        print("   ══════════════════════════════════")
        print("   🚀 TradingBot.__init__() DÉMARRAGE")
        print("   ══════════════════════════════════")
        self.client = BinanceClient(
            api_key=BINANCE_API_KEY,
            api_secret=BINANCE_API_SECRET,
            testnet=TESTNET_MODE
        )
        
        # Charger la watchlist dynamique (synchronisée avec le dashboard)
        self.watch_symbols = self._load_watchlist()
        self.position_manager = PositionManager(self.client)
        
        # 🔴 FIX 27/02: Synchroniser avec Binance au démarrage pour détecter les orphelins
        self.position_manager.sync_with_binance(watchlist=set(self.watch_symbols))
        
        self.prices = {s: deque(maxlen=100) for s in self.watch_symbols}
        self.running = False
        self.last_signal = {}
        self.last_trade = {}  # Cooldown par symbole après un trade
        
        # PAUSE PERSISTANTE - Charger depuis fichier
        pause_file = os.path.join(os.path.dirname(__file__), 'trading_pause.json')
        try:
            if os.path.exists(pause_file):
                with open(pause_file, 'r') as f:
                    pause_data = json.load(f)
                    self.trading_paused_until = pause_data.get('paused_until', 0)
                    if self.trading_paused_until > time.time():
                        remaining = int(self.trading_paused_until - time.time())
                        print(f"   ⏸️ PAUSE ACTIVE: {remaining//60}min {remaining%60}s restants")
                    else:
                        self.trading_paused_until = 0
            else:
                self.trading_paused_until = 0
        except:
            self.trading_paused_until = 0
        
        self.signal_cooldown = 20   # 20 secondes entre les signaux (haute réactivité)
        self.trend_cooldown = 10    # 10 secondes si tendance forte (très réactif)
        self.trade_cooldown = 120   # 🔧 FIX 06/02: 2 min cooldown (était 1 min) — évite re-entrée trop rapide
        # Note: Le vrai loss cooldown est géré par _last_loss_cooldown (7200s/28800s) dans close_position()
        self.pending_signals = []   # File d'attente pour signaux quand max_positions atteint
        
        # 🚀 HOT SIGNALS: Queue temps réel via callback IA (signaux mid-cycle, réaction immédiate)
        # Sans hot signals: bot attend fin du cycle complet (~80s) pour agir sur un signal
        # Avec hot signals: signal transmis via callback dès détection par un worker (~3s)
        import threading as _thr_init
        self._hot_ai_signals = []
        self._hot_ai_signals_lock = _thr_init.Lock()
        
        self.settings = self._load_settings()
        
        # NOUVEAU: Tracker les achats par heure pour éviter concentration temporelle
        self.buys_per_hour = {}  # {hour: count}
        self.max_buys_per_hour = 5   # 🔴 FIX 04/03: Réduit 8→5 — 13 positions en 30min à 08h = cascade SL
        
        # === SERVICE DE SURVEILLANCE IA ===
        self.ai_predictor = None
        self.surveillance_service = None
        self.ai_watchlist = {}  # Symboles sous surveillance IA
        
        # === ANALYSEUR DE RÉGIME DE MARCHÉ ===
        self.market_regime = None
        self.current_regime = 'NEUTRAL'
        self._last_regime_sellall_time = 0  # Cooldown anti-oscillation vente auto
        print(f"   🔍 MARKET_REGIME_AVAILABLE = {MARKET_REGIME_AVAILABLE}")
        
        if MARKET_REGIME_AVAILABLE:
            print("   🔧 Initialisation Market Regime Detector...")
            try:
                self.market_regime = MarketRegimeDetector(binance_client=self.client)
                # 🔧 FIX 07/02: Aussi initialiser le singleton pour que ai_predictor puisse le lire
                from market_regime import get_market_regime_detector
                get_market_regime_detector(binance_client=self.client)
                # Forcer une détection initiale au démarrage
                regime_name, regime_details = self.market_regime.detect_regime(force_update=True)
                max_pos = self.market_regime.get_max_positions()
                score = regime_details.get('score', 0)
                
                # 🔧 FIX 07/02: Synchroniser le singleton avec le régime détecté
                try:
                    _singleton = get_market_regime_detector()
                    if _singleton:
                        _singleton.current_regime = regime_name
                        _singleton.regime_config = regime_details
                except Exception:
                    pass
                
                print(f"   ✅ Market Regime initialisé: {regime_name} (score: {score}) → {max_pos} positions max")
                
            except Exception as e:
                print(f"   ⚠️ Market Regime ERROR: {e}")
                import traceback
                traceback.print_exc()
        else:
            print("   ⚠️ Market Regime module non chargé (MARKET_REGIME_AVAILABLE=False)")
        
        # === TRADE LOGGER POUR ANALYSE ===
        if TRADE_LOGGER_AVAILABLE:
            try:
                self.trade_logger = get_trade_logger()
                print("   ✅ Trade Logger initialisé (logs dans trade_logs/)")
            except Exception as e:
                print(f"   ⚠️ Trade Logger non disponible: {e}")
                self.trade_logger = None
        else:
            self.trade_logger = None
            print("   ⚠️ Trade Logger non disponible (import échoué)")
        
        # Passer le trade_logger au PositionManager
        self.position_manager.trade_logger = self.trade_logger
        
        if AI_PREDICTOR_AVAILABLE:
            try:
                print("   🔧 Initialisation surveillance IA...")
                self.ai_predictor = get_ai_predictor()
                print(f"   ✓ AIPredictor obtenu: {self.ai_predictor is not None}")
                self.surveillance_service = get_surveillance_service()
                print(f"   ✓ SurveillanceService obtenu: {self.surveillance_service is not None}")
                # Configurer le service
                print(f"   🔧 Configuration avec {len(self.watch_symbols)} symboles...")
                
                # 🔴 FIX CRITIQUE: Utiliser setup_klines_fetcher pour configurer l'instance GLOBALE
                from ai_predictor import setup_klines_fetcher
                setup_klines_fetcher(self._fetch_klines_for_ai)
                print("   ✓ klines_fetcher configuré sur instances globales")
                
                # Configurer les autres paramètres
                self.ai_predictor.set_binance_client(self.client)
                self.surveillance_service.set_on_signal(self._on_ai_signal)
                # 🔧 Surveillance dynamique: top marché + spy + bot watchlist (aucun hardcoding)
                _all_syms = self._get_dynamic_surveillance_symbols()
                self.surveillance_service.set_symbols(_all_syms)
                print(f"   ✅ Service de surveillance IA initialisé ({len(_all_syms)} symboles dynamiques: {len(self.watch_symbols)} bot + {len(_all_syms)-len(self.watch_symbols)} marché/spy)")
            except Exception as e:
                print(f"   ⚠️ Erreur init surveillance IA: {e}")
                import traceback
                traceback.print_exc()
        
        # 🧠 Initialiser l'IA prédictive de VENTE (AI Sell Predictor)
        self.ai_sell_predictor = None
        if AI_SELL_PREDICTOR_AVAILABLE:
            try:
                ai_pred = self.ai_predictor if AI_PREDICTOR_AVAILABLE else None
                self.ai_sell_predictor = get_sell_predictor(ai_pred)
                print(f"   ✅ AI Sell Predictor initialisé (LSTM={self.ai_sell_predictor.model is not None})")
            except Exception as e:
                print(f"   ⚠️ AI Sell Predictor non disponible: {e}")
        
        # Passer le sell_predictor au PositionManager
        self.position_manager.ai_sell_predictor = self.ai_sell_predictor
        # 🔧 FIX 16/03: Passer le market_regime au PositionManager pour gardes BULL_STRONG
        self.position_manager.market_regime = self.market_regime if hasattr(self, 'market_regime') else None
        
        # Initialiser FreqAI Manager (Auto-Retraining + Outlier Detection)
        self.freqai_manager = None
        self.last_freqai_check = time.time()
        # 🆕 FIX 25/03: Nettoyage watchlist périodique (indépendant du reset)
        self.last_watchlist_cleanup = time.time()
        if FREQAI_AVAILABLE:
            try:
                self.freqai_manager = get_freqai_manager()
                if self.freqai_manager:
                    print("   ✅ FreqAI Manager initialisé (Auto-Retraining + Outlier Detection)")
                else:
                    print("   ⚠️ FreqAI Manager non disponible")
            except Exception as e:
                print(f"   ⚠️ Erreur init FreqAI Manager: {e}")
                self.freqai_manager = None
    
    def _fetch_klines_for_ai(self, symbol: str, interval: str, limit: int):
        """Fetcher de klines pour le service IA"""
        return self.client.get_klines_production(symbol, interval, limit)
    
    def _on_ai_signal(self, symbol: str, item):
        """Callback IA → file d'attente prioritaire HOT SIGNAL (réaction immédiate, sans attendre fin de cycle)"""
        import time as _t_hot
        print(f"   🔥 HOT SIGNAL reçu: {symbol} - Score={item.score} Pattern={item.pattern}")
        with self._hot_ai_signals_lock:
            # Remplacer tout signal précédent du même symbole
            self._hot_ai_signals = [s for s in self._hot_ai_signals if s.get('symbol') != symbol]
            self._hot_ai_signals.append({
                'symbol': symbol,
                'score': item.score,
                'pattern': item.pattern or 'NEUTRAL',
                'predicted_gain': item.predicted_gain,
                'confidence': item.confidence,
                'features': item.features or {},
                'status': item.status,
                'detected_at': _t_hot.time(),
                'smart_signal': 'ACHAT',
                'smart_eligible': True,
            })
    
    def _load_watchlist(self):
        """Charge la liste des cryptos pour le trading.

        Ordre de priorité :
        1. symbols[] dans watchlist.json  (si non vide — saisie manuelle dashboard)
        2. Top 30 paires USDT par volume 24h Binance  (dynamique, si symbols vide)
        3. spy_injected{} toujours ajouté par-dessus (confirmations spy)
        """
        try:
            if os.path.exists(self.WATCHLIST_FILE):
                with open(self.WATCHLIST_FILE, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    symbols = list(data.get('symbols', []))

                    # Si liste manuelle vide → top volume dynamique (USDC uniquement pour le trading)
                    if not symbols:
                        try:
                            symbols = self.client.get_top_volume_symbols(
                                top_n=30, quote_assets=('USDC',))
                            print(f"   📋 Watchlist dynamique: top {len(symbols)} paires USDC par volume")
                        except Exception as _e:
                            print(f"   ⚠️ Impossible de charger le top volume: {_e}")
                            symbols = []

                    # Ajouter les symboles injectés par le spy (détections confirmées)
                    spy_injected = data.get('spy_injected', {})
                    symbols_set = set(symbols)
                    for sym in spy_injected:
                        if sym not in symbols_set:
                            symbols.append(sym)
                    spy_count = len([s for s in symbols if s in spy_injected])
                    base_count = len(symbols) - spy_count
                    print(f"   📋 Watchlist chargée: {len(symbols)} cryptos ({base_count} base{f' + {spy_count} spy' if spy_count else ''})")
                    print(f"      {', '.join([s.replace('USDT','').replace('USDC','') for s in symbols[:10]])}{'...' if len(symbols) > 10 else ''}")
                    return symbols
        except Exception as e:
            print(f"   ⚠️ Erreur chargement watchlist: {e}")

        # Fallback absolu : top volume
        try:
            symbols = self.client.get_top_volume_symbols(top_n=30, quote_assets=('USDC',))
            print(f"   📋 Watchlist fallback dynamique: {len(symbols)} paires USDC")
            return symbols
        except Exception:
            pass
        print(f"   ⚠️ Watchlist vide — aucun symbole disponible")
        return []
    
    def reload_watchlist(self):
        """Recharge la watchlist (pour mise à jour dynamique)
        
        🆕 FIX 27/02: Gère aussi l'expiration des symboles injectés par le spy.
        Les symboles spy_injected ont un TTL de 24h. Après expiration, ils sont
        retirés automatiquement sauf si une position est ouverte dessus.
        """
        # 🆕 FIX 27/02: Nettoyer les symboles spy expirés AVANT de recharger
        self._cleanup_expired_spy_symbols()
        
        new_symbols = self._load_watchlist()
        
        # Ajouter les nouveaux symboles
        for symbol in new_symbols:
            if symbol not in self.watch_symbols:
                self.watch_symbols.append(symbol)
                self.prices[symbol] = deque(maxlen=100)
                print(f"   ➕ Ajout {symbol} à la surveillance")
        
        # Retirer les symboles supprimés (sauf si position ouverte)
        for symbol in self.watch_symbols[:]:
            if symbol not in new_symbols:
                if symbol not in self.position_manager.positions:
                    self.watch_symbols.remove(symbol)
                    if symbol in self.prices:
                        del self.prices[symbol]
                    print(f"   ➖ Retrait {symbol} de la surveillance")
                else:
                    print(f"   ⚠️ {symbol} a une position ouverte, conservé")

        # Mettre à jour la surveillance IA avec l'univers dynamique complet
        if hasattr(self, 'surveillance_service') and self.surveillance_service:
            try:
                _all_syms = self._get_dynamic_surveillance_symbols()
                self.surveillance_service.set_symbols(_all_syms)
                print(f"   🔄 Surveillance IA mise à jour: {len(_all_syms)} symboles dynamiques ({len(self.watch_symbols)} bot + {len(_all_syms)-len(self.watch_symbols)} marché/spy)")
            except Exception:
                pass

    def _get_dynamic_surveillance_symbols(self):
        """Construit l'univers de surveillance IA de manière 100% dynamique.

        Logique :
        1. Top 60 paires USDT+USDC par volume 24h sur Binance (marché réel)
        2. Paires auto_added et spy_injected du watchlist.json (spy détections)
        3. Symboles de trading du bot (self.watch_symbols) — toujours inclus
           pour que l'IA puisse scorer les positions existantes
        Le tout dédupliqué, sans hardcoding.
        """
        # 1) Top N du marché
        top_market = []
        if hasattr(self, 'client') and self.client:
            try:
                top_market = self.client.get_top_volume_symbols(top_n=60)
            except Exception as e:
                logger.warning(f"⚠️ _get_dynamic_surveillance_symbols: {e}")

        # 2) Spy additions
        _auto, _inj = [], []
        try:
            with open(self.WATCHLIST_FILE, 'r', encoding='utf-8') as _wf:
                _wdata = json.load(_wf)
            _auto = list(_wdata.get('auto_added', {}).keys())
            _inj  = list(_wdata.get('spy_injected', {}).keys())
        except Exception:
            pass

        # 3) Merge (bot watchlist toujours en tête pour ne pas dropper leurs signaux)
        combined = list(dict.fromkeys(self.watch_symbols + _auto + _inj + top_market))
        return combined

    def _cleanup_expired_spy_symbols(self, deep=False):
        """Retire les symboles injectés par le spy après expiration de leur TTL.

        Mode normal  (deep=False, toutes les 30s) : traite uniquement les entrées
        qui ont une métadonnée 'spy_injected' avec last_surge + ttl_hours.

        Mode profond (deep=True,  toutes les 2h)  : en plus, retire les symboles
        qui n'ont PAS de métadonnée spy_injected mais dont le fichier watchlist.json
        lui-même a été modifié il y a plus de WATCHLIST_BASE_TTL_H heures, ce qui
        signifie qu'ils ont été injectés par le spy lors d'une session passée sans
        jamais être nettoyés.
        """
        WATCHLIST_BASE_TTL_H = 48   # symboles sans métadonnée expirés après 48h
        try:
            if not os.path.exists(self.WATCHLIST_FILE):
                return

            with open(self.WATCHLIST_FILE, 'r', encoding='utf-8') as f:
                data = json.load(f)

            spy_injected = data.get('spy_injected', {})
            symbols = data.get('symbols', [])
            now = datetime.now()
            expired = []
            changed = False

            # ── Nettoyage standard : entrées avec métadonnée ──────────────
            for symbol, info in list(spy_injected.items()):
                ttl_hours = info.get('ttl_hours', 24)
                last_surge_str = info.get('last_surge', info.get('added_at', ''))
                try:
                    last_surge = datetime.fromisoformat(last_surge_str)
                    if (now - last_surge).total_seconds() > ttl_hours * 3600:
                        if symbol not in self.position_manager.positions:
                            expired.append(symbol)
                except (ValueError, TypeError):
                    if symbol not in self.position_manager.positions:
                        expired.append(symbol)

            # ── Nettoyage profond (deep=True) : symboles sans métadonnée ──
            if deep:
                # Recharger la liste des symboles originaux de config.py pour comparaison
                try:
                    from config import WATCH_SYMBOLS as _base_symbols
                    base_set = set(_base_symbols)
                except Exception:
                    base_set = set()

                # Si on a une liste de référence, retirer ce qui n'est plus dedans
                # ET n'a pas de métadonnée (= jamais correctement tracké)
                if base_set:
                    for sym in list(symbols):
                        if sym not in base_set and sym not in spy_injected:
                            if sym not in self.position_manager.positions:
                                expired.append(sym)
                                print(f"   🧹 {sym}: hors liste de base + sans métadonnée TTL → retiré (deep cleanup)")

            # ── Application des suppressions ──────────────────────────────
            if expired:
                for symbol in expired:
                    if symbol in symbols:
                        symbols.remove(symbol)
                    if symbol in spy_injected:
                        del spy_injected[symbol]
                    print(f"   🕵️⏰ {symbol} expiré (spy TTL) → retiré de la watchlist")

                data['symbols'] = symbols
                data['spy_injected'] = spy_injected
                data['updated_at'] = now.isoformat()
                data['count'] = len(symbols)
                changed = True

            if changed:
                with open(self.WATCHLIST_FILE, 'w', encoding='utf-8') as f:
                    json.dump(data, f, indent=2)
                logging.getLogger('WatchlistCleanup').info(
                    f"🕵️ {len(expired)} symbole(s) spy expiré(s) retiré(s): {', '.join(expired)}")

        except Exception:
            pass  # Silencieux pour ne pas bloquer le bot
    
    def verify_and_cleanup_positions(self):
        """
        Vérifie et nettoie les positions au démarrage du bot
        Appelle la méthode verify_and_cleanup_positions du PositionManager
        """
        print("\n" + "="*60)
        print("   🔍 VÉRIFICATION DES POSITIONS AU DÉMARRAGE")
        print("="*60)
        
        if len(self.position_manager.positions) == 0:
            print("   ℹ️  Aucune position à vérifier")
            print("="*60 + "\n")
            return
        
        print(f"   📊 {len(self.position_manager.positions)} position(s) détectée(s)\n")
        
        positions_to_remove = []
        positions_to_close = []
        
        for symbol, position in list(self.position_manager.positions.items()):
            try:
                # Tester si le symbole existe sur Binance
                current_price = self.client.get_price(symbol)
                
                if current_price is None or current_price == 0:
                    print(f"   ❌ {symbol}: Prix indisponible (symbole invalide ou délisté)")
                    positions_to_remove.append(symbol)
                    continue
                
                entry_price = position['entry_price']
                stop_loss = position['stop_loss']
                take_profit = position['take_profit']
                current_pnl_pct = ((current_price / entry_price) - 1) * 100
                
                # Vérifier SL/TP atteints
                if current_price <= stop_loss:
                    print(f"   🔴 {symbol}: SL atteint ({current_price:.6f} <= {stop_loss:.6f}), P&L: {current_pnl_pct:.2f}%")
                    positions_to_close.append((symbol, "stop-loss", current_pnl_pct))
                elif current_price >= take_profit:
                    print(f"   🟢 {symbol}: TP atteint ({current_price:.6f} >= {take_profit:.6f}), P&L: {current_pnl_pct:.2f}%")
                    positions_to_close.append((symbol, "take-profit", current_pnl_pct))
                else:
                    print(f"   ✓ {symbol}: OK (P&L: {current_pnl_pct:+.2f}%)")
            
            except Exception as e:
                print(f"   ⚠️ {symbol}: Erreur vérification - {e}")
                positions_to_remove.append(symbol)
        
        # Fermer les positions avec SL/TP atteints
        for symbol, reason, pnl in positions_to_close:
            print(f"\n   🔄 Fermeture automatique de {symbol} ({reason}, {pnl:+.2f}%)...")
            result = self.position_manager.close_position(symbol, reason)
            if not result:
                print(f"   ⚠️ Échec fermeture, suppression manuelle...")
                positions_to_remove.append(symbol)
        
        # Supprimer les positions invalides
        if positions_to_remove:
            print(f"\n   🗑️ Suppression de {len(positions_to_remove)} position(s) invalide(s)...")
            for symbol in positions_to_remove:
                if symbol in self.position_manager.positions:
                    del self.position_manager.positions[symbol]
            self.position_manager._save_positions()
            print(f"   ✅ Nettoyage terminé")
        
        print(f"\n   📊 Positions finales: {len(self.position_manager.positions)}")
        print("="*60 + "\n")
    
    def _reload_config_module(self):
        """Recharge dynamiquement le module config.py pour obtenir les dernières valeurs"""
        import importlib
        import sys
        try:
            # Recharger le module config
            if 'config' in sys.modules:
                importlib.reload(sys.modules['config'])
                # Mettre à jour les variables globales
                global STOP_LOSS_PERCENT, TAKE_PROFIT_PERCENT, MAX_ORDER_SIZE
                global RSI_OVERSOLD, RSI_OVERBOUGHT, EMA_SHORT, EMA_LONG
                global BB_PERIOD, BB_STD, RSI_PERIOD, REQUIRED_SIGNALS
                
                from config import (
                    STOP_LOSS_PERCENT, TAKE_PROFIT_PERCENT, MAX_ORDER_SIZE,
                    RSI_OVERSOLD, RSI_OVERBOUGHT, EMA_SHORT, EMA_LONG,
                    BB_PERIOD, BB_STD, RSI_PERIOD, REQUIRED_SIGNALS
                )
                return True
        except Exception as e:
            print(f"   ⚠️ Erreur rechargement config.py: {e}")
            return False
    
    def _load_settings(self):
        """Charge les paramètres depuis config.py (fichier maître pour SL/TP) et bot_settings.json (pour positionSize, autoTrade et maxPositions)"""
        # Recharger config.py pour obtenir les valeurs les plus récentes
        self._reload_config_module()
        
        # VALEURS MAÎTRES depuis config.py (SL/TP non modifiables depuis le dashboard)
        settings = {
            'stopLoss': STOP_LOSS_PERCENT,      # ← Toujours depuis config.py
            'takeProfit': TAKE_PROFIT_PERCENT,  # ← Toujours depuis config.py
            'positionSize': MAX_ORDER_SIZE,     # ← Valeur par défaut, peut être override
            'autoTrade': True,
            'maxPositions': MAX_OPEN_POSITIONS
        }
        
        # Charger autoTrade, maxPositions ET positionSize depuis bot_settings.json
        try:
            if os.path.exists(self.SETTINGS_FILE):
                with open(self.SETTINGS_FILE, 'r', encoding='utf-8') as f:
                    user_settings = json.load(f)
                    # Ne garder que les paramètres autorisés (pas SL/TP)
                    if 'autoTrade' in user_settings:
                        settings['autoTrade'] = user_settings['autoTrade']
                    if 'maxPositions' in user_settings:
                        settings['maxPositions'] = user_settings['maxPositions']
                    if 'positionSize' in user_settings:
                        settings['positionSize'] = user_settings['positionSize']
        except Exception as e:
            print(f"   ⚠️ Erreur chargement paramètres: {e}")
        
        print(f"   ⚙️ Paramètres chargés: SL={settings['stopLoss']}% (config.py), TP={settings['takeProfit']}% (config.py), Taille={settings['positionSize']}€, Max={settings['maxPositions']}")
        return settings
    
    def _find_weakest_position(self):
        """
        Trouve la position avec le score IA le plus faible pour remplacement.
        Retourne (symbol, score) ou (None, 0) si aucune position faible.
        """
        if not self.surveillance_service or not self.position_manager.positions:
            return None, 0
        
        try:
            ai_status = self.surveillance_service.get_surveillance_status()
            all_symbols = ai_status.get('all_symbols', [])
            
            weakest_symbol = None
            weakest_score = 100  # Score maximum
            
            for symbol in self.position_manager.positions.keys():
                # Trouver le score IA actuel de cette position
                symbol_data = next((s for s in all_symbols if s.get('symbol') == symbol), None)
                if symbol_data:
                    score = symbol_data.get('score', 50)
                    # Exclure les positions avec score décent (> 50 au lieu de > 40)
                    if score < weakest_score and score < 50:
                        weakest_score = score
                        weakest_symbol = symbol
            
            return weakest_symbol, weakest_score
        except Exception as e:
            print(f"   ⚠️ Erreur _find_weakest_position: {e}")
            return None, 0
    
    def get_dynamic_max_positions(self):
        """
        Obtient le nombre maximum de positions selon le régime de marché actuel.
        Adaptation dynamique en temps réel - PAS DE LIMITE FIXE.
        """
        # Si market_regime est disponible et actif, utiliser sa recommandation
        if self.market_regime:
            try:
                regime_max = self.market_regime.get_max_positions()
                regime_name, _ = self.market_regime.get_current_regime()
                settings_max = self.settings.get('maxPositions', MAX_OPEN_POSITIONS)
                
                # 🔧 FIX 07/02: Sync singleton pour ai_predictor (appelé à chaque cycle)
                try:
                    from market_regime import get_market_regime_detector as _gmrd
                    _s = _gmrd()
                    if _s and _s.current_regime != regime_name:
                        _s.current_regime = regime_name
                except Exception:
                    pass
                
                # 🆕 FIX 28/01: Réduire positions si BULL_WEAK + baisse persistante
                # Vérifier momentum BTC récent (si disponible dans AIPredictor)
                if regime_name == 'BULL_WEAK' and hasattr(AIPredictor, '_btc_momentum'):
                    btc_mom5 = getattr(AIPredictor, '_btc_momentum', 0)
                    # Si BTC baisse modérément (-0.3% à -1.5%), réduire à 75% des positions
                    if -0.015 < btc_mom5 < -0.003:  # Entre -1.5% et -0.3%
                        regime_max = int(regime_max * 0.75)
                        print(f"   ⚠️ BULL_WEAK + baisse modérée (BTC Mom5={btc_mom5:.2%}) → Positions réduites à {regime_max}")
                
                # TOUJOURS logger pour debug
                print(f"   📊 [DEBUG] get_dynamic_max_positions(): regime={regime_name}, regime_max={regime_max}, settings_max={settings_max}")
                
                # Logger uniquement si différent du config (pour visibilité)
                if regime_max != settings_max:
                    print(f"   📊 Adaptation dynamique: {regime_name} → {regime_max} positions max (config ignoré: {settings_max})")
                
                return regime_max
            except Exception as e:
                print(f"   ⚠️ Erreur get_dynamic_max_positions: {e}")
                import traceback
                traceback.print_exc()
                # Fallback sur config
        
        # Sinon, utiliser la valeur de bot_settings.json ou config.py
        fallback = self.settings.get('maxPositions', MAX_OPEN_POSITIONS)
        print(f"   ⚠️ Market Regime non disponible - utilisation config: {fallback} positions")
        return fallback
    
    def load_historical_data(self):
        """Charge les données historiques"""
        print(f"\n📊 Chargement des données historiques pour {len(self.watch_symbols)} cryptos...")
        print(f"⚙️  Paramètres actifs: RSI={RSI_OVERSOLD}/{RSI_OVERBOUGHT}, EMA={EMA_SHORT}/{EMA_LONG}, SL={STOP_LOSS_PERCENT}%, TP={TAKE_PROFIT_PERCENT}%")
        
        for symbol in self.watch_symbols:
            if symbol not in self.prices:
                self.prices[symbol] = deque(maxlen=100)
            klines = self.client.get_klines(symbol, DEFAULT_INTERVAL, 100)
            if klines:
                for k in klines:
                    self.prices[symbol].append(float(k[4]))  # Close price
                print(f"   ✅ {symbol}: {len(self.prices[symbol])} bougies")
    
    def load_ai_scoring(self):
        """Charge le cache IA avec les scores d'opportunités"""
        cache_file = os.path.join(SCRIPT_DIR, "crypto_cache", "crypto_data.json")
        try:
            if os.path.exists(cache_file):
                with open(cache_file, 'r', encoding='utf-8') as f:
                    self.ai_cache = json.load(f)
                print(f"   🧠 Cache IA chargé: {len(self.ai_cache.get('symbols', {}))} cryptos")
                return True
        except Exception as e:
            print(f"   ⚠️ Erreur chargement cache IA: {e}")
        self.ai_cache = {}
        return False
    
    def get_ai_score(self, symbol):
        """Récupère le score IA d'une crypto - utilise le service IA si disponible"""
        
        # PRIORITÉ 1: Service de surveillance IA (temps réel avec modèle PyTorch)
        if self.surveillance_service and self.ai_predictor:
            try:
                status = self.surveillance_service.get_surveillance_status()
                # Chercher dans TOUS les signaux, pas seulement ready_signals
                all_signals = status.get('ready_signals', []) + status.get('watching_signals', [])
                
                # 🔧 FIX 04/02: Utiliser predictor.watchlist (pas surveillance_service.watchlist)
                # La watchlist est dans le predictor, pas directement dans surveillance_service
                predictor = self.surveillance_service.predictor if hasattr(self.surveillance_service, 'predictor') else None
                if predictor and hasattr(predictor, 'watchlist'):
                    for sym, item in predictor.watchlist.items():
                        if sym == symbol:
                            return {
                                'score': item.score if hasattr(item, 'score') else 50,
                                'rsi': item.features.get('rsi', 50) if hasattr(item, 'features') else 50,
                                'trend': 'bullish' if (item.score if hasattr(item, 'score') else 0) >= 60 else 'bearish' if (item.score if hasattr(item, 'score') else 0) < 40 else 'neutral',
                                'pattern': item.pattern if hasattr(item, 'pattern') else 'NEUTRAL',
                                'confidence': item.confidence if hasattr(item, 'confidence') else 0,
                                'reason': item.reason if hasattr(item, 'reason') else '',
                                'price': item.features.get('price_current', 0) if hasattr(item, 'features') else 0,
                                'from_ai_service': True
                            }
                
                for item in all_signals:
                    if item.get('symbol') == symbol:
                        return {
                            'score': item.get('score', 50),
                            'rsi': item.get('features', {}).get('rsi', 50),
                            'trend': 'bullish' if item.get('score', 0) >= 60 else 'bearish' if item.get('score', 0) < 40 else 'neutral',
                            'pattern': item.get('pattern', 'NEUTRAL'),
                            'confidence': item.get('confidence', 0),
                            'reason': item.get('reason', ''),
                            'price': item.get('features', {}).get('price_current', 0),
                            'from_ai_service': True
                        }
            except Exception as e:
                pass  # Fallback au cache
        
        # PRIORITÉ 2: Cache IA (crypto_cache/crypto_data.json)
        if not hasattr(self, 'ai_cache') or not self.ai_cache:
            self.load_ai_scoring()
        
        symbols = self.ai_cache.get('symbols', {})
        if symbol not in symbols:
            return {'score': 50, 'rsi': 50, 'trend': 'neutral', 'pattern': 'NEUTRAL', 'from_ai_service': False}
        
        data = symbols[symbol]
        indicators = data.get('indicators', {})
        signals_cache = data.get('signals', {})
        
        # Calculer un score amélioré basé sur les indicateurs du cache
        score = 50  # Base neutre
        
        # RSI du cache (pondération forte)
        rsi = indicators.get('rsi', 50)
        if rsi < 25:
            score += 25  # RSI très survendu = forte opportunité
        elif rsi < 30:
            score += 18
        elif rsi < 40:
            score += 10
        elif rsi > 75:
            score -= 20
        elif rsi > 70:
            score -= 12
        elif rsi > 60:
            score -= 5
        
        # Trend du cache
        trend = signals_cache.get('trend', 'neutral')
        if trend == 'bullish':
            score += 12
        elif trend == 'bearish':
            score -= 12
        
        # MACD
        macd = indicators.get('macd', {})
        histogram = macd.get('histogram', 0)
        if histogram > 0:
            score += 8
        elif histogram < -0.5:
            score -= 8
        
        # Volatilité RSI
        rsi_signal = signals_cache.get('rsi_signal', 'neutral')
        if rsi_signal == 'oversold':
            score += 12
        elif rsi_signal == 'overbought':
            score -= 12
        
        # Bollinger position
        bb_position = indicators.get('bb_position', 0.5)
        if bb_position < 0.2:  # Proche bande basse
            score += 10
        elif bb_position > 0.8:  # Proche bande haute
            score -= 10
        
        return {
            'score': min(100, max(0, score)),
            'rsi': rsi,
            'trend': trend,
            'pattern': 'NEUTRAL',
            'macd_histogram': histogram,
            'price': data.get('price', 0),
            'change_24h': data.get('priceChangePercent', 0),
            'from_ai_service': False
        }
    
    def get_surveillance_status(self):
        """Retourne le statut de la surveillance IA pour le dashboard"""
        if not self.surveillance_service or not self.ai_predictor:
            return {
                'is_running': False,
                'total_symbols': 0,
                'analyzed': 0,
                'ready_to_buy': 0,
                'watching': 0,
                'top_opportunities': [],
                'ready_signals': [],
                'ai_available': False
            }
        
        status = self.surveillance_service.get_surveillance_status()
        status['ai_available'] = True
        
        return status
    
    def get_ai_watchlist(self):
        """Retourne la watchlist IA triée par score"""
        if not self.ai_predictor:
            return []
        return self.ai_predictor.get_watchlist()
    
    def analyze(self, symbol):
        """
        ANALYSE AMÉLIORÉE v2.0 - Ne jamais acheter en tendance baissière!
        
        Règles strictes:
        1. JAMAIS acheter si EMA/Bollinger orientés à la baisse
        2. Détecter les breakouts pour réagir aux hausses
        3. Utiliser le scoring IA du cache
        4. Trailing stop dynamique pour ne pas vendre tôt
        5. NOUVEAU: Adapter selon régime de marché (BULL/NEUTRAL/BEAR/CRASH)
        """
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉTECTION DU RÉGIME DE MARCHÉ (BTC/ETH)
        # ═══════════════════════════════════════════════════════════════════
        regime_info = {'regime': 'NEUTRAL', 'allow_trading': True}
        
        # Utiliser les VRAIES klines de PRODUCTION pour une analyse précise
        # (le testnet n'a pas assez de données de trading)
        try:
            klines = self.client.get_klines_production(symbol, DEFAULT_INTERVAL, 100)
            if klines and len(klines) >= 50:
                prices = [float(k[4]) for k in klines]  # Prix de clôture des bougies
            else:
                prices = list(self.prices[symbol])
                klines = None
        except:
            prices = list(self.prices[symbol])
            klines = None

        # 🆕 FIX 15/03: Extraire volumes taker buy pour détection accumulation
        volumes_list_a = []
        buy_vols_list_a = []
        try:
            if klines:
                volumes_list_a = [float(k[5]) for k in klines]
                buy_vols_list_a = [float(k[9]) for k in klines]
        except Exception:
            volumes_list_a = []
            buy_vols_list_a = []
        
        if len(prices) < 50:
            return "HOLD", {}, "Pas assez de données"
        
        current_price = prices[-1]
        
        # ═══════════════════════════════════════════════════════════════════
        # INDICATEURS DE BASE
        # ═══════════════════════════════════════════════════════════════════
        rsi = TechnicalIndicators.rsi(prices, RSI_PERIOD)
        ema_short = TechnicalIndicators.ema(prices, EMA_SHORT)
        ema_long = TechnicalIndicators.ema(prices, EMA_LONG)
        bb_upper, bb_mid, bb_lower = TechnicalIndicators.bollinger(prices)
        kc_upper, kc_mid, kc_lower = TechnicalIndicators.keltner_channels(prices, 20, 10, 2.0)
        momentum = TechnicalIndicators.momentum(prices, 10)
        
        # ═══════════════════════════════════════════════════════════════════
        # INDICATEURS DE TENDANCE AVANCÉS
        # ═══════════════════════════════════════════════════════════════════
        bb_direction, bb_expanding, bb_slope = TechnicalIndicators.bollinger_trend(prices)
        ema_alignment, ema_strength, ema_slope = TechnicalIndicators.ema_trend(prices, EMA_SHORT, EMA_LONG, 99)
        is_breakout, breakout_strength = TechnicalIndicators.breakout_detection(prices, 20)
        is_pullback, pullback_distance = TechnicalIndicators.pullback_detection(prices, EMA_SHORT)
        
        # NOUVEAU: Détection du Bollinger Squeeze (compression avant explosion)
        is_squeeze, bb_bandwidth, squeeze_strength, squeeze_breakout = TechnicalIndicators.bollinger_squeeze(prices)
        
        # 🚀🚀🚀 NOUVEAU: Détection du GOLDEN CROSS IMMÉDIAT (1-3 bougies MAX) 🚀🚀🚀
        # PRIORITÉ ABSOLUE: Acheter au moment EXACT du croisement, pas plusieurs minutes après !
        # 🔧 FIX 09/02: Fenêtre élargir à 3 bougies (was 2) + détection pré-croisement
        golden_cross_recent = False
        golden_cross_immediate = False  # Croisement dans la dernière bougie
        golden_cross_imminent = False   # 🆕 EMA qui convergent (pré-croisement)
        if len(prices) >= 4 and ema_short and ema_long:
            # Calculer EMA9 et EMA21 il y a 1 bougie (détection IMMÉDIATE)
            ema9_prev1 = TechnicalIndicators.ema(prices[:-1], EMA_SHORT)
            ema21_prev1 = TechnicalIndicators.ema(prices[:-1], EMA_LONG)
            
            # Calculer il y a 2 bougies (tolérance)
            ema9_prev2 = TechnicalIndicators.ema(prices[:-2], EMA_SHORT)
            ema21_prev2 = TechnicalIndicators.ema(prices[:-2], EMA_LONG)
            
            # Calculer il y a 3 bougies (élargi)
            ema9_prev3 = TechnicalIndicators.ema(prices[:-3], EMA_SHORT)
            ema21_prev3 = TechnicalIndicators.ema(prices[:-3], EMA_LONG)
            
            if ema9_prev1 and ema21_prev1:
                # GOLDEN CROSS IMMÉDIAT = croisement dans la DERNIÈRE bougie
                was_below_1 = ema9_prev1 < ema21_prev1
                is_above_now = ema_short > ema_long
                if was_below_1 and is_above_now:
                    golden_cross_immediate = True  # 🚀 SIGNAL IMMÉDIAT !
                    golden_cross_recent = True
            
            # Tolérance: 2 bougies
            if not golden_cross_recent and ema9_prev2 and ema21_prev2:
                was_below_2 = ema9_prev2 < ema21_prev2
                is_above_now = ema_short > ema_long
                if was_below_2 and is_above_now:
                    golden_cross_recent = True
            
            # 🆕 FIX 09/02: Tolérance 3 bougies (15 min sur 5m) - capter les crossovers après restart
            if not golden_cross_recent and ema9_prev3 and ema21_prev3:
                was_below_3 = ema9_prev3 < ema21_prev3
                is_above_now = ema_short > ema_long
                if was_below_3 and is_above_now:
                    golden_cross_recent = True
            
            # 🆕 FIX 09/02: DÉTECTION PRÉ-CROISEMENT (EMA convergentes)
            # Si EMA9 monte vers EMA21 et l'écart se réduit → croisement imminent
            # 🔧 FIX 13/02: Élargi gap de 0.15% à 0.30% + momentum accéléré
            # SÉCURITÉ: Exiger momentum haussier (pas juste convergence passive)
            if not golden_cross_recent and ema9_prev1 and ema21_prev1:
                ema_gap_now = ((ema_short - ema_long) / ema_long) * 100 if ema_long else 0
                ema_gap_prev = ((ema9_prev1 - ema21_prev1) / ema21_prev1) * 100 if ema21_prev1 else 0
                gap_reducing = ema_gap_now > ema_gap_prev  # L'écart se réduit
                gap_close = abs(ema_gap_now) < 0.30  # 🔧 FIX 13/02: Élargi (0.15→0.30%) pour capter plus tôt
                # Vérifier que le momentum est haussier (sécurité)
                mom_ok = momentum and momentum > 0
                if gap_reducing and gap_close and mom_ok:
                    golden_cross_imminent = True
            
            # 🆕 FIX 13/02: DÉTECTION EARLY MOMENTUM (accélération AVANT le cross)
            # Cas ZAMA: le prix accélère fortement AVANT que EMA9 croise EMA21
            # Détecter: momentum 3min fort + momentum accélère + volume potentiel
            # Ceci permet d'acheter au DÉBUT du mouvement, pas 20min après
            if not golden_cross_recent and not golden_cross_imminent and ema9_prev1 and ema21_prev1:
                ema_gap_now = ((ema_short - ema_long) / ema_long) * 100 if ema_long else 0
                ema_gap_prev = ((ema9_prev1 - ema21_prev1) / ema21_prev1) * 100 if ema21_prev1 else 0
                # Momentum 3 bougies
                mom3 = ((prices[-1] - prices[-3]) / prices[-3]) * 100 if len(prices) >= 3 else 0
                mom3_prev = ((prices[-2] - prices[-4]) / prices[-4]) * 100 if len(prices) >= 4 else 0
                mom_accelerating = mom3 > mom3_prev  # Momentum accélère
                gap_converging = ema_gap_now > ema_gap_prev  # EMA convergent
                
                # EARLY MOMENTUM: EMA convergent + momentum fort accéléré + pas trop écarté
                if (gap_converging and abs(ema_gap_now) < 0.50 and
                    mom3 > 0.3 and mom_accelerating and
                    momentum and momentum > 0):
                    golden_cross_imminent = True  # Traité comme pré-croisement
        
        # Score IA (service temps réel ou cache)
        ai_data = self.get_ai_score(symbol)
        ai_score = ai_data.get('score', 50)
        ai_from_service = ai_data.get('from_ai_service', False)
        # 🔥 FIX 26/01 22h40: Convertir None en NEUTRAL pour éviter "Pattern 'None' inconnu"
        ai_pattern = (ai_data.get('pattern') or 'NEUTRAL') if ai_from_service else None
        
        # ═══════════════════════════════════════════════════════════════════
        # RÈGLE #1: BLOQUER LES ACHATS EN TENDANCE BAISSIÈRE
        # ═══════════════════════════════════════════════════════════════════
        
        is_bearish_trend = False
        bearish_reasons = []

        # ═══════════════════════════════════════════════════════════════════
        # 🆕 FIX 15/03: SQUEEZE BREAKOUT OVERRIDE
        # Le pattern "calme → BB serrés → explosion" (Bollinger Squeeze) est
        # LE setup le plus fiable du marché crypto. Pendant ce pattern :
        # - EMA9 peut être < EMA21 (convergence normale)
        # - Score IA peut être bas (actif peu liquide / pas surveillé)
        # - Tendance 20 bougies peut sembler baissière (range flat)
        # Ces critères sont des FAUX POSITIFS pour ce setup — on les court-circuite.
        # Conditions OBLIGATOIRES pour le squeeze_override :
        #   1. is_squeeze = True  (BB réellement comprimées)
        #   2. squeeze_breakout == 'up'  (direction haussière confirmée)
        # ═══════════════════════════════════════════════════════════════════
        squeeze_override = bool(
            is_squeeze and
            squeeze_breakout == 'up'
        )

        # ══════════════════════════════════════════════════════════════════════
        # CRITÈRES DE BLOCAGE: Uniquement les CHUTES ACTIVES, pas les creux stables
        # La stratégie "Buy the Dip" achète quand EMA9 < EMA21, donc on ne bloque PAS
        # simplement parce que EMA9 < EMA21 - on bloque seulement si CHUTE ACTIVE
        # ══════════════════════════════════════════════════════════════════════
        
        # Critère 1: Bollinger très orientées à la baisse (chute forte)
        if bb_direction == 'down' and ema_slope and ema_slope < -0.3:
            is_bearish_trend = True
            bearish_reasons.append("BB + EMAs en chute")
        
        # Critère 2: Momentum TRÈS négatif (chute en cours)
        if momentum and momentum < -1.5:
            is_bearish_trend = True
            bearish_reasons.append(f"Momentum tres negatif ({momentum:.1f}%)")
        
        # Critère 3: Prix en chute forte sur 3 bougies (> 1.5%)
        if len(prices) >= 3:
            price_3_ago = prices[-3]
            price_change_3 = ((current_price - price_3_ago) / price_3_ago) * 100
            if price_change_3 < -1.5:
                is_bearish_trend = True
                bearish_reasons.append(f"Chute forte ({price_change_3:.2f}%)")
        
        # Critère 4: Prix en chute très forte sur 5 bougies (> 3%)
        if len(prices) >= 5:
            price_5_ago = prices[-5]
            price_change_5 = ((current_price - price_5_ago) / price_5_ago) * 100
            if price_change_5 < -3.0:
                is_bearish_trend = True
                bearish_reasons.append(f"Chute prolongee ({price_change_5:.2f}%)")
        
        # Critère 5: RSI très bas + momentum négatif = danger
        if rsi and rsi < 25 and momentum and momentum < -0.5:
            is_bearish_trend = True
            bearish_reasons.append("RSI extreme + Momentum negatif")
        
        # Critère 6: TENDANCE BAISSIÈRE CONTINUE - Prix < EMA21 ET EMA21 en pente descendante
        # C'est le cas typique où le prix descend lentement mais sûrement
        # 🔧 FIX 15/03: Exempté si squeeze_override (durant un squeeze, prix NATURELLEMENT
        # sous EMA21 avant de percer — c'est la DÉFINITION du setup)
        if ema_long and current_price < ema_long and not squeeze_override:
            # Calculer la pente de EMA21 sur les dernières bougies
            if len(prices) >= 10:
                # Comparer EMA21 estimée il y a 5 bougies vs maintenant
                price_avg_5_ago = sum(prices[-10:-5]) / 5
                price_avg_now = sum(prices[-5:]) / 5
                ema21_falling = price_avg_now < price_avg_5_ago

                # RENFORCEMENT: Bloquer si prix sous EMA21 ET tendance baissière
                if ema21_falling or ema_alignment == 'bearish':
                    is_bearish_trend = True
                    bearish_reasons.append("Prix < EMA21 descendante (tendance baissière continue)")
        
        # Critère 6b: EMA9 < EMA21 avec pente négative sur EMA9 = baisse active
        # Si EMA9 descend PENDANT qu'elle est sous EMA21, c'est une baisse confirmée
        # 🔧 FIX 15/03: Exempté si squeeze_override (EMA9 lag normal après une explosion)
        if ema_short and ema_long and ema_short < ema_long and ema_slope and ema_slope < -0.1 and not squeeze_override:
            is_bearish_trend = True
            bearish_reasons.append(f"EMA9 < EMA21 + pente EMA9 négative ({ema_slope:.2f}%) = Baisse active")
        
        # Critère 6c: EMA9 < EMA21 ET prix en baisse récente (5 bougies) = Tendance baissière CLAIRE
        # C'EST LE CAS DE CHZ : croisement baissier + prix qui continue de descendre
        if ema_short and ema_long and ema_short < ema_long and len(prices) >= 5:
            last_5_change = ((current_price - prices[-5]) / prices[-5]) * 100
            # Si prix baisse sur 5 bougies pendant que EMA9 < EMA21 = baisse confirmée
            if last_5_change < -0.3:  # Baisse même légère = danger
                is_bearish_trend = True
                bearish_reasons.append(f"EMA9 < EMA21 + Prix baisse 5min ({last_5_change:.2f}%) = INTERDICTION ACHAT")
        
        # Critère 7: Score IA faible = pas d'achat
        # 🔧 FIX 15/03: Exempté si squeeze_override — le score IA est lent à réagir
        # aux breakouts soudains d'actifs peu couverts. Le setup technique PRIME ici.
        if ai_score and ai_score < 50 and not squeeze_override:
            is_bearish_trend = True
            bearish_reasons.append(f"Score IA trop faible ({ai_score})")
        
        # ═══════════════════════════════════════════════════════════════════
        # STRATÉGIE D'ACHAT: "BUY THE DIP" + "BOLLINGER SQUEEZE"
        # ═══════════════════════════════════════════════════════════════════
        
        buy_signals = 0
        buy_reasons = []
        
        # STRATÉGIE 1: TENDANCE HAUSSIÈRE CLAIRE (Anti-baisse)
        # EMA9 > EMA21 = tendance haussière = opportunité d'achat
        if ema_alignment == 'bullish' and ema_short and ema_long:
            ema_gap = ((ema_short - ema_long) / ema_long) * 100
            buy_signals += 2  # Signal fort
            buy_reasons.append(f"Tendance haussière EMA (+{ema_gap:.2f}%)")
            
            # RSI pas trop haut (< 50) = bon pour acheter
            # RÉDUIT: 65 → 50 pour éviter achats en zone neutre/surachat
            if rsi and rsi < 50:
                buy_signals += 2  # Augmenté de 1 à 2 (zone vraiment favorable)
                buy_reasons.append(f"RSI FAVORABLE ({rsi:.1f})")
            elif rsi and rsi < 55:  # Zone acceptable mais moins favorable
                buy_signals += 1
                buy_reasons.append(f"RSI acceptable ({rsi:.1f})")
            
            # Momentum positif = confirmation
            if momentum and momentum > 0.2:
                buy_signals += 1
                buy_reasons.append(f"Momentum+ ({momentum:.1f}%)")
            
            # Prix au-dessus des EMAs = force
            if current_price > ema_short and current_price > ema_long:
                buy_signals += 1
                buy_reasons.append("Prix > EMAs")
        
        # STRATÉGIE 2: BOLLINGER SQUEEZE (compression avant explosion)
        # Bandes serrées + EMA bullish ou neutre + breakout vers le haut
        elif is_squeeze or squeeze_strength > 40:
            # Squeeze détecté - faible volatilité, explosion imminente
            if squeeze_breakout == 'up':
                # Breakout vers le haut confirmé!
                buy_signals += 3  # Signal très fort
                buy_reasons.append(f"SQUEEZE BREAKOUT UP (force={squeeze_strength:.0f}%)")
                
                # EMA en bonne disposition (pas fortement bearish)
                if ema_alignment == 'bullish' or (ema_slope and ema_slope > -0.3):
                    buy_signals += 2
                    buy_reasons.append("EMA favorable")
                
                # RSI pas en surachat = encore de la marge
                # Seuil réduit: 65 → 55 pour éviter achats trop hauts
                if rsi and rsi < 55:
                    buy_signals += 1
                    buy_reasons.append(f"RSI marge ({rsi:.0f})")
                    
            elif squeeze_strength > 50 and ema_alignment == 'bullish':
                # Squeeze fort avec EMA bullish = prêt à exploser
                buy_signals += 2
                buy_reasons.append(f"SQUEEZE TENSION (force={squeeze_strength:.0f}%)")
                
                # Momentum positif = direction confirmée
                if momentum and momentum > 0:
                    buy_signals += 1
                    buy_reasons.append("Momentum+")
        

        # STRATÉGIE 3: PULLBACK sur EMA bullish (légère correction dans tendance haussière)
        elif ema_alignment == 'bullish' and is_pullback and pullback_distance:
            if pullback_distance < 1.5:  # Correction légère (< 1.5%)
                buy_signals += 2
                buy_reasons.append(f"PULLBACK EMA ({pullback_distance:.1f}%)")
                # Seuil réduit: 55 → 50 pour éviter achats trop hauts
                if rsi and rsi < 50:
                    buy_signals += 1
                    buy_reasons.append(f"RSI cool ({rsi:.0f})")
        
        # STRATÉGIE 4: BB+ BREAKOUT (Nouveau - breakout au-dessus de Bollinger supérieure)
        # Achat quand le prix franchit et dépasse la bande de Bollinger supérieure
        # C'est un signal de momentum fort = explosion haussière
        # NOUVEAU: Confirmation avec Keltner Channels pour filtrer faux breakouts
        
        # PRÉ-SIGNAL: Prix approche BB+ (97-99%) avec momentum fort
        # NOUVEAU: Vérifier KC > BB+ (vraie tendance selon analyse utilisateur)
        if bb_upper and current_price >= bb_upper * 0.97 and current_price < bb_upper:
            approach_distance = ((bb_upper - current_price) / current_price) * 100
            # Momentum fort = explosion imminente (seuil réduit 1.0→0.5% pour activation réelle)
            if momentum and momentum > 0.5:
                # Vérifier confirmation Keltner (prix > Keltner mid = vraie tendance)
                keltner_confirmed = (kc_mid and current_price > kc_mid)
                
                # NOUVEAU: KC > BB+ = signal haussier fort (vraie volatilité)
                kc_above_bb = (kc_upper and bb_upper and kc_upper > bb_upper)
                
                if keltner_confirmed and kc_above_bb:
                    buy_signals += 4  # Signal maximum: KC>BB+ ET prix>KC_mid
                    buy_reasons.append(f"🎯 SETUP OPTIMAL: KC>BB+ ({approach_distance:.1f}% de BB+, mom={momentum:.1f}%)")
                elif keltner_confirmed:
                    buy_signals += 3  # Confirmation Keltner standard
                    buy_reasons.append(f"PRÉ-BREAKOUT BB+ CONFIRMÉ KC ({approach_distance:.1f}% restant, mom={momentum:.1f}%)")
                else:
                    buy_signals += 2
                    buy_reasons.append(f"PRÉ-BREAKOUT BB+ ({approach_distance:.1f}% restant, mom={momentum:.1f}%)")
                
                # Bonus si EMA bullish
                if ema_alignment == 'bullish':
                    buy_signals += 1
                    buy_reasons.append("EMA + approche BB+ = setup parfait")
        
        # BREAKOUT CONFIRMÉ: Prix au-dessus de BB+
        elif bb_upper and current_price > bb_upper:
            # NOUVEAU: Double confirmation Bollinger + Keltner pour filtrer les faux breakouts
            # Un vrai breakout = prix au-dessus de BB+ ET au-dessus de Keltner supérieur
            keltner_breakout = (kc_upper and current_price > kc_upper)
            
            # Vérifier que c'est un vrai breakout (pas juste un spike)
            if len(prices) >= 3:
                # Le prix doit être au-dessus de BB+ maintenant et juste en dessous avant
                prices_recent = prices[-3:]
                bb_upper_prev = TechnicalIndicators.bollinger(prices[:-1])[0] if len(prices) > 20 else bb_upper
                was_below_bb = prices_recent[0] < bb_upper_prev if bb_upper_prev else False
                
                # Breakout confirmé si prix était sous BB+ et maintenant au-dessus
                # 🔧 FIX AUDIT 28/02: Retiré 'or True' (débug remnant), restaurer la vérification
                if was_below_bb:
                    bb_distance = ((current_price - bb_upper) / bb_upper) * 100
                    
                    if keltner_breakout:
                        # DOUBLE BREAKOUT = signal extrêmement fort
                        buy_signals += 4  # Augmenté de 3 à 4
                        buy_reasons.append(f"🚀 DOUBLE BREAKOUT BB+KC (+{bb_distance:.2f}% BB)")
                    else:
                        buy_signals += 3  # Signal très fort (Bollinger seul)
                        buy_reasons.append(f"BB+ BREAKOUT (+{bb_distance:.2f}% au-dessus)")
                    
                    # Bonus si EMA aussi bullish
                    if ema_alignment == 'bullish':
                        buy_signals += 1
                        buy_reasons.append("EMA + BB+ = double confirmation")
                    
                    # Bonus si momentum positif
                    if momentum and momentum > 0.5:
                        buy_signals += 1
                        buy_reasons.append(f"Momentum fort ({momentum:.1f}%)")

        # NOTE: STRATÉGIE 5 (TREND FOLLOWING) est gérée en priorité dans la section finale
        # après validation, aux lignes 1792-1804

        # ═══════════════════════════════════════════════════════════════════
        # FILTRE ANTI-ACHAT EN ZONE HAUTE (amélioré - distinction tendance forte)
        # ═══════════════════════════════════════════════════════════════════
        # PRINCIPE: Le RSI peut rester en zone haute longtemps en tendance haussière forte
        # On ne bloque que si RSI élevé SANS confirmation de tendance forte
        high_zone_block = False
        
        # RSI > 60 = zone haute (analyse contextuelle)
        if rsi and rsi > 60:
            # Calculer la force de la tendance haussière
            ema_gap_pct = 0
            if ema_short and ema_long and ema_long > 0:
                ema_gap_pct = ((ema_short - ema_long) / ema_long) * 100
            
            # Momentum sur 5 et 10 bougies
            momentum_5 = 0
            momentum_10 = 0
            if len(prices) >= 5:
                momentum_5 = ((current_price - prices[-5]) / prices[-5]) * 100
            if len(prices) >= 10:
                momentum_10 = ((current_price - prices[-10]) / prices[-10]) * 100
            
            # TENDANCE HAUSSIÈRE FORTE = RSI élevé acceptable
            # Critères: EMA gap fort + momentum positif + RSI pas extrême
            # 🔧 FIX 13/02: Critères assouplis pour ne plus bloquer les tendances naissantes
            is_strong_uptrend = (
                ema_gap_pct > 0.2 and           # 🔧 Réduit de 0.5% à 0.2% (détecter plus tôt)
                momentum_5 > 0.2 and            # 🔧 Réduit de 0.5% à 0.2%
                momentum_10 > 0.3 and           # 🔧 Réduit de 1.0% à 0.3%
                rsi < 75                        # RSI pas en zone extrême (< 75)
            )
            
            # Exception 1: Tendance haussière forte confirmée
            if is_strong_uptrend:
                buy_reasons.append(f"✅ RSI {rsi:.0f} accepté (tendance forte: EMA+{ema_gap_pct:.1f}%, Mom5={momentum_5:.1f}%)")
            
            # Exception 2: BB+ breakout avec momentum > 2%
            elif bb_upper and current_price > bb_upper and momentum and momentum > 2.0:
                buy_reasons.append(f"✅ RSI {rsi:.0f} accepté (BB+ breakout + momentum {momentum:.1f}%)")
            
            # RSI 60-70: Pénalité légère sauf exceptions ci-dessus
            # 🔧 FIX 13/02: Pénalité réduite (-1 au lieu de -2) pour ne pas trop pénaliser
            elif rsi <= 70:
                if not is_strong_uptrend:
                    high_zone_block = True
                    buy_signals = max(0, buy_signals - 1)  # 🔧 Pénalité réduite (was -2)
                    buy_reasons.append(f"⚠️ ZONE HAUTE (RSI={rsi:.1f}) sans tendance forte (EMA+{ema_gap_pct:.1f}%)")
            
            # RSI > 70: Zone haute, pénalité modérée (sauf breakout)
            # 🔧 FIX 13/02: Réduit de -4 à -2 (le seuil RSI>72 en sécurité finale gère déjà le blocage)
            else:
                # Exception uniquement pour breakout extrême
                is_extreme_breakout = (bb_upper and current_price > bb_upper and 
                                      momentum and momentum > 3.0 and
                                      ema_gap_pct > 1.0)
                if not is_extreme_breakout:
                    high_zone_block = True
                    buy_signals = max(0, buy_signals - 2)  # 🔧 Réduit (was -4)
                    buy_reasons.append(f"⚠️ RSI ÉLEVÉ ({rsi:.1f} > 70) - pénalité signal")
                else:
                    buy_reasons.append(f"✅ RSI {rsi:.0f} accepté (breakout extrême + momentum {momentum:.1f}%)")
        
        # Prix proche du max 20 bougies (< 2% du max) = sommet probable
        if len(prices) >= 20:
            max_20 = max(prices[-20:])
            distance_from_max = ((max_20 - current_price) / max_20) * 100
            if distance_from_max < 2.0:  # Moins de 2% du max
                # Exception: BB+ breakout actif
                is_bb_active = (bb_upper and current_price > bb_upper)
                if not is_bb_active:
                    high_zone_block = True
                    buy_signals = max(0, buy_signals - 2)
                    buy_reasons.append(f"❌ PROCHE MAX 20min ({distance_from_max:.1f}% du max)")
        
        # ═══════════════════════════════════════════════════════════════════
        # RÈGLE #3: SIGNAUX DE VENTE (PROTÉGER LE CAPITAL)
        # ═══════════════════════════════════════════════════════════════════
        
        sell_signals = 0
        sell_reasons = []
        
        # Calculer momentum_3 et momentum_5 pour strong_uptrend
        momentum_3 = ((current_price - prices[-3]) / prices[-3]) * 100 if len(prices) >= 3 else 0
        momentum_5 = ((current_price - prices[-5]) / prices[-5]) * 100 if len(prices) >= 5 else 0
        momentum_10 = ((current_price - prices[-10]) / prices[-10]) * 100 if len(prices) >= 10 else 0
        
        # Calculer strong_uptrend pour protection anti-vente prématurée
        ema_gap_pct = ((ema_short - ema_long) / ema_long * 100) if (ema_short and ema_long) else 0
        strong_uptrend = (ema_gap_pct > 0 and momentum_3 > 0.3 and momentum_5 > 0.5)
        
        # RSI surachat extrême (sauf si strong uptrend)
        if rsi and rsi > 80:
            if not strong_uptrend:
                sell_signals += 2
                sell_reasons.append(f"RSI surachat extreme ({rsi:.1f})")
        elif rsi and rsi > RSI_OVERBOUGHT:
            if not strong_uptrend:
                sell_signals += 1
                sell_reasons.append(f"RSI surachat ({rsi:.1f})")
        
        # Prix au-dessus bande haute Bollinger (sauf si strong uptrend)
        if bb_upper and current_price > bb_upper:
            if not strong_uptrend:
                sell_signals += 1
                sell_reasons.append("Prix > BB haute")
        
        # Tendance baissière confirmée (pour positions ouvertes)
        if is_bearish_trend and len(bearish_reasons) >= 2:
            sell_signals += 2
            sell_reasons.append("Tendance baissiere confirmee")
        
        # Momentum très négatif (chute rapide)
        if momentum and momentum < -3:
            sell_signals += 2
            sell_reasons.append(f"Chute rapide ({momentum:.1f}%)")
        elif momentum and momentum < -1.5:
            sell_signals += 1
            sell_reasons.append(f"Momentum negatif ({momentum:.1f}%)")
        
        # EMA en croisement baissier (death cross) - sauf si strong uptrend
        if ema_short and ema_long and ema_short < ema_long:
            if not strong_uptrend:
                sell_signals += 1
                sell_reasons.append("EMA death cross")
        
        # Prix sous les deux EMA = signal faible
        if ema_short and ema_long and current_price < ema_short and current_price < ema_long:
            sell_signals += 1
            sell_reasons.append("Prix sous EMAs")
        
        # ═══════════════════════════════════════════════════════════════════
        # DÉCISION FINALE
        # ═══════════════════════════════════════════════════════════════════
        
        indicators = {
            'rsi': rsi,
            'ema_short': ema_short,
            'ema_long': ema_long,
            'bb': (bb_upper, bb_mid, bb_lower),
            'kc': (kc_upper, kc_mid, kc_lower),  # NOUVEAU: Keltner Channels
            'bb_direction': bb_direction,
            'bb_bandwidth': bb_bandwidth,  # Largeur des bandes
            'ema_alignment': ema_alignment,
            'ema_slope': ema_slope,
            'price': current_price,
            'momentum': momentum,
            'is_breakout': is_breakout,
            'breakout_strength': breakout_strength,
            'is_pullback': is_pullback,
            'is_squeeze': is_squeeze,  # NOUVEAU: Bollinger Squeeze
            'squeeze_strength': squeeze_strength,  # Force du squeeze
            'squeeze_breakout': squeeze_breakout,  # Direction du breakout
            'ai_score': ai_score,
            'is_bearish': is_bearish_trend,
            'bearish_reasons': bearish_reasons,
            'buy_signals': buy_signals,
            'sell_signals': sell_signals
        }
        
        # Nombre de positions ouvertes
        current_positions = len(self.position_manager.positions)
        
        # ═══════════════════════════════════════════════════════════════════
        # LOGIQUE STRICTE: N'acheter QUE sur tendance haussière claire
        # ═══════════════════════════════════════════════════════════════════
        
        # NOTE: La logique de vente est maintenant à la FIN de la fonction
        # pour s'assurer qu'on ne vend PAS en perte sauf stop-loss
        
        # 2. Vérifications avant achat
        can_buy = True
        block_reasons = []
        
        # ═══════════════════════════════════════════════════════════════════
        # BLOCAGE PRIORITAIRE: TENDANCE BAISSIÈRE = PAS D'ACHAT
        # ═══════════════════════════════════════════════════════════════════
        if is_bearish_trend:
            can_buy = False
            block_reasons.extend(bearish_reasons)
        
        # ═══════════════════════════════════════════════════════════════════
        # NOTE: Vérification EMA9 > EMA21 est faite AVANT le Golden Cross (sécurité finale)
        # On ne vérifie plus ici pour éviter les redondances
        # ═══════════════════════════════════════════════════════════════════
        
        # CRITÈRE 1: RSI pas en surachat extrême
        # 🔧 FIX 02/03: Relevé 72→78 (aligné avec sécurité finale)
        # En reprise, RSI 72-78 est normal et ne devrait pas bloquer
        if rsi and rsi > 78:
            can_buy = False
            block_reasons.append(f"RSI trop haut ({rsi:.0f})")
        
        # CRITÈRE 2: Le prix doit montrer un momentum positif récent (3 dernières bougies)
        # 🔧 FIX 13/02: Tolérance élargie de -1% à -1.5% pour ne pas bloquer les dips
        if len(prices) >= 3:
            last_3_change = ((current_price - prices[-3]) / prices[-3]) * 100
            if last_3_change < -1.5:  # 🔧 Élargi (was -1%)
                can_buy = False
                block_reasons.append(f"Prix en baisse ({last_3_change:.2f}%)")
        
        # CRITÈRE 3: Momentum 10 bougies doit être raisonnable
        if len(prices) >= 10:
            last_10_change = ((current_price - prices[-10]) / prices[-10]) * 100
            if last_10_change < -1.0:  # Tolérer légère baisse
                can_buy = False
                block_reasons.append(f"Momentum 10min négatif ({last_10_change:.2f}%)")
        
        # CRITÈRE 4: Score IA pour confirmation
        if ai_score < 35:
            can_buy = False
            block_reasons.append(f"Score IA trop bas ({ai_score})")
        elif ai_score >= 60:
            buy_signals += 1
            buy_reasons.append(f"IA: Score {ai_score}")
        
        # CRITÈRE 5: Pas de chute forte sur 5 bougies
        if len(prices) >= 5:
            last_5_change = ((current_price - prices[-5]) / prices[-5]) * 100
            if last_5_change < -2.0:
                can_buy = False
                block_reasons.append(f"Chute 5min ({last_5_change:.2f}%)")
        
        # CRITÈRE 6: TENDANCE GLOBALE sur 20 bougies
        # 🔧 FIX 13/02: Ne pas bloquer si Score IA élevé (>= 75) → l'IA voit le retournement
        # Les meilleurs achats (buy the dip) sont justement après une baisse 20min
        # 🔧 FIX 15/03: Squeeze exempt — 20 bougies plates = SIGNATURE du squeeze, pas une baisse
        if len(prices) >= 20:
            last_20_change = ((current_price - prices[-20]) / prices[-20]) * 100
            if last_20_change < -1.0 and not (ai_score and ai_score >= 75) and not squeeze_override:
                can_buy = False
                block_reasons.append(f"Tendance 20min baissière ({last_20_change:.2f}%)")
        
        # Récupérer max_positions (adapté au régime de marché)
        max_positions = self.get_dynamic_max_positions()
        
        # ═══════════════════════════════════════════════════════════════════
        # 🔍 VALIDATION PATTERN - Skip si pattern générique, laisser le régime gérer
        # ═══════════════════════════════════════════════════════════════════
        # Les patterns génériques (NEUTRAL/None) sont gérés par les seuils du régime uniquement
        if ai_pattern not in [None, 'NEUTRAL', 'neutral']:
            pm = get_pattern_manager()
            pattern_allowed, pattern_reason = pm.is_pattern_allowed(ai_pattern, ai_score)
            
            if not pattern_allowed:
                print(f"   🚫 {symbol}: Pattern {ai_pattern} bloqué - {pattern_reason}")
                return "HOLD", indicators, f"Pattern bloqué: {pattern_reason}"
            
            # Enregistrer le signal (pour statistiques)
            pm.record_signal(ai_pattern)
        
        # ═══════════════════════════════════════════════════════════════════
        # SÉCURITÉ FINALE ABSOLUE: VÉRIFICATIONS AVANT TOUT ACHAT 🔴🔴🔴
        # CES RÈGLES S'APPLIQUENT À TOUS LES ACHATS, MÊME GOLDEN CROSS !
        # ═══════════════════════════════════════════════════════════════════
        
        # RÈGLE #1: END_OF_CYCLE bloque TOUT (RSI > 65, surachat)
        if ai_pattern == 'END_OF_CYCLE':
            print(f"   🔴🔴🔴 {symbol}: PATTERN END_OF_CYCLE détecté - ACHAT BLOQUÉ !!!")
            return "HOLD", indicators, "END_OF_CYCLE pattern - Achat interdit"
        
        # RÈGLE #2: 🔴 EMA9 DOIT ÊTRE AU-DESSUS D'EMA21 🔴
        # 🔧 FIX 13/02: EXCEPTION pour EARLY MOMENTUM ou PRÉ-CROISEMENT
        # 🔧 FIX 15/03: EXCEPTION pour SQUEEZE BREAKOUT
        # Les EMA sont des indicateurs RETARDÉS — lors d'un squeeze breakout,
        # EMA9 croise EMA21 APRÈS la bougie d'explosion. Acheter MAINTENANT = max profit.
        # Condition: squeeze_override ET buy_signals >= 4 (confirmation technique forte)
        if not (ema_short and ema_long and ema_short > ema_long):
            # Exception 1: pré-croisement imminent avec momentum fort
            # Exception 2: squeeze breakout confirmé avec signal suffisant
            if not golden_cross_imminent and not (squeeze_override and buy_signals >= 4):
                return "HOLD", indicators, "EMA9<EMA21 (bearish) - Achat interdit"
        
        # RÈGLE #3: RSI ÉLEVÉ = Prudence
        # 🔧 FIX 02/03: RSI cap relevé 72→78 — en reprise, RSI monte naturellement
        # Les achats bloqués par RSI 72-78 étaient justement les meilleures opportunités
        # SÉCURITÉ: RSI > 78 = toujours bloqué (surachat vrai)
        # RSI 65-78 = bloqué SAUF si tendance forte OU score IA élevé
        if rsi and rsi > 65:
            # Vérifier si tendance forte justifie un RSI élevé
            is_strong_momentum = (momentum_3 > 0.2 and momentum_5 > 0.2)  # 🔧 FIX 02/03: 0.3→0.2
            ema_strongly_bullish = (ema_short and ema_long and 
                                    ((ema_short - ema_long) / ema_long) * 100 > 0.15)  # 🔧 FIX 02/03: 0.2→0.15
            high_ai = (ai_score and ai_score >= 60)  # 🔧 FIX 02/03: 65→60
            
            if rsi > 78:
                # RSI > 78 = toujours bloqué (surachat extrême vrai)
                print(f"   🔴 {symbol}: RSI trop haut ({rsi:.0f} > 78) - FIN DE CYCLE")
                return "HOLD", indicators, f"RSI {rsi:.0f} > 78 - Achat interdit"
            elif rsi > 72 and not (is_strong_momentum and ema_strongly_bullish):
                # RSI 72-78: bloqué sauf forte tendance (momentum + EMA suffisent)
                print(f"   🔴 {symbol}: RSI élevé ({rsi:.0f}) sans tendance forte")
                return "HOLD", indicators, f"RSI {rsi:.0f} > 72 sans momentum - Achat interdit"
            elif rsi > 65 and not (is_strong_momentum or ema_strongly_bullish or high_ai):
                # RSI 65-72: bloqué seulement si AUCUN signal positif
                # 🔧 FIX 02/03: OR au lieu de AND — un seul facteur positif suffit
                print(f"   🔴 {symbol}: RSI élevé ({rsi:.0f}) sans aucun support")
                return "HOLD", indicators, f"RSI {rsi:.0f} > 65 sans support - Achat interdit"
            else:
                # RSI 65-78 AVEC au moins un facteur positif = autorisé
                print(f"   ✅ {symbol}: RSI {rsi:.0f} accepté (Mom3={momentum_3:.1f}%, EMA_bull={ema_strongly_bullish}, AI={ai_score})")
        
        # ═══════════════════════════════════════════════════════════════════
        # �🚀🚀🚀 PRIORITÉ #1 ABSOLUE: GOLDEN CROSS IMMÉDIAT 🚀🚀🚀
        # Acheter AU MOMENT EXACT du croisement EMA9 > EMA21
        # PAS 3-5 minutes après quand c'est trop tard !
        # ═══════════════════════════════════════════════════════════════════
        if golden_cross_immediate and current_positions < max_positions:
            # 🚀 CROISEMENT DÉTECTÉ DANS LA DERNIÈRE BOUGIE - ACHAT IMMÉDIAT ! 🚀
            # Les sécurités critiques (EMA9>EMA21, RSI<65, END_OF_CYCLE) ont déjà été vérifiées
            
            # SEULE VALIDATION RESTANTE: Pas de chute brutale en cours (momentum > -2%)
            momentum_ok = (momentum_3 > -2.0)  # Tolérance large pour ne pas rater le signal
            
            if momentum_ok:
                ema_gap_cross = ((ema_short - ema_long) / ema_long) * 100 if (ema_short and ema_long) else 0
                reason = f"🚀🚀🚀 GOLDEN CROSS IMMÉDIAT (1 bougie) ! EMA9 croise EMA21 (+{ema_gap_cross:.2f}%), RSI={rsi:.0f} - ACHAT PRIORITAIRE"
                return "BUY", indicators, reason
            else:
                print(f"   ⚠️ {symbol}: Golden Cross immédiat IGNORÉ - Chute brutale Mom3={momentum_3:.1f}%")
        
        # Golden Cross dans les 2-3 bougies (moins prioritaire, mais toujours important)
        elif golden_cross_recent and current_positions < max_positions:
            # Les sécurités critiques (EMA9>EMA21, RSI<65, END_OF_CYCLE) ont déjà été vérifiées
            
            # VALIDATION 1: AI score minimum (floor de sécurité)
            if ai_score and ai_score < 35:
                pass  # Score IA trop faible, golden cross probablement faux signal
            # VALIDATION 2: Momentum minimum requis (pas en bearish trend fort)
            elif momentum_10 and momentum_10 < -2.0:
                pass  # Momentum trop bearish malgré golden cross = faux signal
            else:
                # VALIDATION 3: Prix ne doit pas chuter fortement
                last_3_ok = True
                if len(prices) >= 3:
                    last_3_change = ((current_price - prices[-3]) / prices[-3]) * 100
                    last_3_ok = last_3_change > -2.0
                
                if last_3_ok:
                    ema_gap_cross = ((ema_short - ema_long) / ema_long) * 100
                    reason = f"⚡ GOLDEN CROSS (2-3 bougies): EMA9>EMA21 (+{ema_gap_cross:.2f}%), RSI={rsi:.0f}, AI={ai_score}"
                    return "BUY", indicators, reason
        
        # 🆕 FIX 09/02+13/02: PRÉ-CROISEMENT / EARLY MOMENTUM
        # Cas XRP 18:32, ZAMA: Les EMA convergent et/ou le momentum accélère
        # 🔧 FIX 13/02: RSI élargi de 60→68, AI de 45→40,  pour capter plus tôt
        # Ne pas attendre le croisement effectif quand le mouvement est déjà clair
        elif golden_cross_imminent and current_positions < max_positions:
            if ai_score and ai_score >= 40 and rsi and rsi < 68:
                ema_gap_pct = ((ema_short - ema_long) / ema_long) * 100 if (ema_short and ema_long) else 0
                reason = f"🔮 EARLY MOMENTUM: EMA convergentes ({ema_gap_pct:.3f}%), Mom3={momentum_3:.1f}%, RSI={rsi:.0f}, AI={ai_score}"
                return "BUY", indicators, reason
        
        # 3b. Signal d'achat si tendance haussière + pas de blocage + IA favorable
        # PRIORISÉ par score IA pour des achats plus intelligents
        # 🔧 FIX 13/02: Seuil adaptatif selon score IA
        # Score IA >= 75 → 4 points suffisent (signal IA fort = moins de confirmations nécessaires)
        # Score IA < 75 → 5 points requis (exiger plus de confirmations techniques)
        min_buy_signals = 4 if (ai_score and ai_score >= 75) else 5
        if can_buy and current_positions < max_positions and buy_signals >= min_buy_signals:
            ema_gap = ((ema_short - ema_long) / ema_long) * 100 if (ema_short and ema_long) else 0
            reason = f"ACHAT HAUSSIER ({buy_signals}pts/{min_buy_signals}req): EMA9>EMA21 (+{ema_gap:.2f}%), RSI={rsi:.0f}, AI={ai_score}"
            return "BUY", indicators, reason
        elif can_buy and buy_signals < min_buy_signals:
            # Signal trop faible = achat potentiellement tardif
            pass  # Ignorer silencieusement les signaux faibles
        elif not can_buy and block_reasons:
            # Log des raisons de blocage pour debug (désactivé pour éviter erreur)
            pass  # logger.debug(f"   🚫 {symbol}: BLOQUÉ - {', '.join(block_reasons[:3])}")
        
        # ═══════════════════════════════════════════════════════════════════
        # 3c. STRATÉGIE TREND FOLLOWING - Achat sur cassure haussière
        # Cette stratégie est INDÉPENDANTE de Buy the Dip
        # PROTECTION RENFORCÉE: Exiger au moins 6 points de signal
        # ═══════════════════════════════════════════════════════════════════
        if current_positions < max_positions and not is_bearish_trend:
            # Conditions Trend Following:
            # - EMA9 > EMA21 (tendance haussière)
            # - Momentum positif
            # - RSI pas en surachat extrême (< 75)
            # - Score IA >= 50
            # - Pas en chute sur les 3 dernières bougies
            # - AU MOINS 6 POINTS DE SIGNAL (nouveau)
            if ema_short and ema_long and ema_short > ema_long and buy_signals >= 6:
                ema_gap_bullish = ((ema_short - ema_long) / ema_long) * 100
                last_3_ok = True
                if len(prices) >= 3:
                    last_3_change = ((current_price - prices[-3]) / prices[-3]) * 100
                    last_3_ok = last_3_change > -0.5  # Tolérance légère baisse
                
                # PROTECTION ANTI-FOMO: Ne pas acheter si prix proche du pic récent (20min)
                # Si prix actuel > 98% du max 20min, c'est probablement trop tard
                near_peak = False
                if len(prices) >= 20:
                    max_20 = max(prices[-20:])
                    peak_distance = ((max_20 - current_price) / max_20) * 100
                    if peak_distance < 2.0:
                        near_peak = True
                
                if momentum and momentum > 0 and rsi and rsi < 75 and ai_score >= 50 and last_3_ok and not near_peak:
                    reason = f"TREND FOLLOWING ({buy_signals}pts): EMA9>EMA21 (+{ema_gap_bullish:.2f}%), Momentum+, RSI={rsi:.0f}, AI={ai_score}"
                    return "BUY", indicators, reason
        
        # ═══════════════════════════════════════════════════════════════════
        # 4. Signal de vente - VENDRE QUAND ON A FAIT UN PROFIT SIGNIFICATIF
        # ═══════════════════════════════════════════════════════════════════
        if symbol in self.position_manager.positions:
            position = self.position_manager.positions.get(symbol)
            if position:
                entry_price = position.get('entry_price', current_price)
                pnl_pct = ((current_price - entry_price) / entry_price) * 100
                
                # CONDITION 1: Take Profit atteint (+2% minimum)
                if pnl_pct >= 2.0:
                    reason = f"TAKE PROFIT: +{pnl_pct:.2f}%"
                    return "SELL", indicators, reason
                
                # CONDITION 2: Golden Cross SIGNIFICATIF (EMA9 > EMA21 de +0.3% minimum)
                # 🔧 FIX AUDIT 28/02: Exiger profit minimum +1.0% (au lieu de > 0)
                # Avant: vendait dès +0.01% de profit pendant le meilleur moment du trend haussier
                # Maintenant: laisse courir le profit jusqu'au take profit si trend haussier
                if ema_short and ema_long and ema_short > ema_long:
                    ema_gap = ((ema_short - ema_long) / ema_long) * 100
                    if ema_gap > 0.5 and pnl_pct > 1.0:  # 🔧 FIX: gap 0.3→0.5, profit 0→1.0%
                        reason = f"SELL REBOND: EMA9>EMA21 (+{ema_gap:.2f}%), P&L: +{pnl_pct:.2f}%"
                        return "SELL", indicators, reason
                
                # CONDITION 3: RSI très élevé (surachat) = vendre si en profit
                # Protection: Ne pas vendre si strong uptrend actif
                strong_uptrend_pos = False
                if ema_short and ema_long and momentum:
                    ema_diff = ((ema_short - ema_long) / ema_long) * 100
                    momentum_5 = 0
                    if len(prices) >= 5:
                        momentum_5 = ((prices[-1] - prices[-5]) / prices[-5]) * 100
                    if ema_diff > 0 and momentum > 0.3 and momentum_5 > 0.5:
                        strong_uptrend_pos = True
                
                if rsi and rsi > 70 and pnl_pct > 0 and not strong_uptrend_pos:
                    reason = f"SELL SURACHAT: RSI={rsi:.0f}, P&L: +{pnl_pct:.2f}%"
                    return "SELL", indicators, reason
                
                # CONDITION 4: Signaux de vente techniques (NOUVEAU - activer ventes sur signal)
                # Si sell_signals ≥ 4 ET position en perte modérée (> -1.5%)
                # Cela permet de couper les pertes sur tendance baissière confirmée
                if sell_signals >= 4 and pnl_pct > -1.5:
                    reason = f"SELL SIGNAL TECHNIQUE: {sell_signals} signaux baissiers, P&L: {pnl_pct:+.2f}%"
                    return "SELL", indicators, reason
                
                # CONDITION 5: Stop Loss technique (utilise STOP_LOSS_PERCENT de config)
                if pnl_pct < -STOP_LOSS_PERCENT:
                    reason = f"STOP LOSS: {pnl_pct:.2f}%"
                    return "SELL", indicators, reason
        
        # 5. Défaut = HOLD
        return "HOLD", indicators, "Conditions non remplies"
    
    def execute_signal(self, symbol, signal, indicators=None, reason=""):
        """Exécute un signal de trading avec cooldown dynamique"""
        
        # VÉRIFICATION PAUSE GLOBALE (après SELL_ALL)
        if self.trading_paused_until > time.time():
            remaining = int(self.trading_paused_until - time.time())
            minutes = remaining // 60
            seconds = remaining % 60
            print(f"   ⏸️ {symbol}: Trading en pause - {minutes}m {seconds}s restants")
            return
        
        # Déterminer le cooldown selon la force de la tendance
        cooldown = self.signal_cooldown  # 30s par défaut
        if indicators:
            trend_strength = indicators.get('trend_strength')
            trend_direction = indicators.get('trend_direction')
            # Tendance forte haussière = cooldown réduit pour plus de trades
            if trend_strength and trend_strength > 40 and trend_direction == "bullish":
                cooldown = self.trend_cooldown  # 15s
        
        # Vérifier le cooldown
        if symbol in self.last_signal:
            elapsed = time.time() - self.last_signal[symbol]
            if elapsed < cooldown:
                remaining = int(cooldown - elapsed)
                print(f"   Cooldown signal {symbol}: encore {remaining}s")
                return
        
        # Vérifier le cooldown de trade (5 min après chaque trade)
        if symbol in self.last_trade:
            elapsed = time.time() - self.last_trade[symbol]
            if elapsed < self.trade_cooldown:
                remaining = int(self.trade_cooldown - elapsed)
                print(f"   Cooldown trade {symbol}: encore {remaining}s")
                return
        
        # 🔴 FIX 09/02: Vérifier cooldown PERTE renforcé (2h après perte)
        # Bug: le 1er chemin d'achat (process_signal) ne vérifiait PAS le cooldown perte!
        # Résultat: ETH acheté 3x, XRP 2x, LINK 2x → toutes des pertes répétées
        loss_cooldowns = getattr(self.position_manager, '_last_loss_cooldown', {})
        if symbol in loss_cooldowns and time.time() < loss_cooldowns[symbol]:
            remaining = int(loss_cooldowns[symbol] - time.time())
            print(f"   🕒 {symbol}: Cooldown PERTE actif ({remaining//60}min restantes) - BLOQUÉ")
            return
        
        # Recharger les paramètres à chaque signal pour prendre en compte les modifications
        self.settings = self._load_settings()
        
        # Vérifier le nombre maximum de positions (adapté au régime de marché)
        current_positions = len(self.position_manager.positions)
        max_positions = self.get_dynamic_max_positions()
        
        if signal == "BUY" and symbol not in self.position_manager.positions:
            # Vérifier le mode auto-trading
            if not self.settings.get('autoTrade', True):
                print(f"   ⚠️ {symbol}: Trading automatique désactivé")
                return
            
            # === PROTECTION ANTI-BAISSE ASSOUPLIE (LAISSER PASSER) ===
            # DÉSACTIVÉ pour permettre les achats - L'IA fait déjà le filtrage
            if indicators:
                rsi = indicators.get('rsi', 50)
                momentum = indicators.get('momentum', 0)
                
                # BLOQUER uniquement en cas de crash extrême
                if rsi < 20 and momentum < -2.0:
                    print(f"   🚫 {symbol}: ACHAT REFUSÉ - Crash extrême (RSI={rsi:.0f}, Mom={momentum:.2f}%)")
                    return
                
                # Log pour debug mais laisser passer
                if rsi < 40:
                    print(f"   ⚡ {symbol}: Signal accepté malgré RSI bas ({rsi:.0f}) - L'IA a validé")
            
            # Vérifier si on peut ouvrir une nouvelle position
            if current_positions >= max_positions:
                print(f"   ⚠️ {symbol}: Max positions atteint ({current_positions}/{max_positions})")
                return
            
            # 🔧 FIX 01/03: Filtre horaire UTC — bloquer achats pendant heures toxiques
            # Crash-test montre WR < 40% entre 06h-12h UTC → détruit la performance
            # 🔴 FIX 09/02: Supprime le bypass score>=95 (scores IA gonflés par bonus)
            try:
                from config import BLOCKED_TRADING_HOURS_UTC, ENABLE_HOUR_FILTER
                current_hour_utc = datetime.utcnow().hour
                if ENABLE_HOUR_FILTER and current_hour_utc in BLOCKED_TRADING_HOURS_UTC:
                    print(f"   🕐 {symbol}: Heure toxique ({current_hour_utc}h UTC) - achat bloqué (AUCUN bypass)")
                    return
            except ImportError:
                pass

            # NOUVEAU: Vérifier limite d'achats par heure (éviter concentration temporelle)
            # 🆕 FIX 28/01: Bypass pour scores EXCEPTIONNELS (≥ 95) comme MATIC
            current_hour = datetime.now().hour
            buys_this_hour = self.buys_per_hour.get(current_hour, 0)
            
            # Récupérer le score IA pour vérifier si bypass nécessaire
            ai_score = 0
            if self.surveillance_service:
                ai_status = self.surveillance_service.get_surveillance_status()
                signal_data = ai_status.get('ready_signals', [])
                matching_signal = next((s for s in signal_data if s.get('symbol') == symbol), None)
                if matching_signal:
                    ai_score = matching_signal.get('score', 0)
            
            # Exception: Score ≥ 95 = opportunité rare, bypass limite horaire
            if ai_score >= 95:
                print(f"   ⚡ {symbol}: Score EXCEPTIONNEL ({ai_score}) - BYPASS limite horaire ({buys_this_hour}/{self.max_buys_per_hour})")
            elif buys_this_hour >= self.max_buys_per_hour:
                print(f"   ⏳ {symbol}: Limite achats/heure atteinte ({buys_this_hour}/{self.max_buys_per_hour} à {current_hour}h)")
                return
            
            # Calculer le montant
            balance = self.client.get_balance("USDC")
            if not balance:
                print(f"   ⚠️ {symbol}: Impossible de récupérer le solde")
                return
            
            # 🔧 FIX 25/02: Position FIXE basée sur positionSize (500 USDT)
            # Avant: max_risk = balance × 20% → montants minuscules quand balance baisse
            # Maintenant: positionSize direct, seule protection = solde suffisant
            position_size = self.settings.get('positionSize', MAX_ORDER_SIZE)
            order_amount = position_size
            
            # Vérifier que le solde est suffisant
            if balance['free'] < order_amount + 5:
                # Réduire au solde disponible - 5 USDT de marge
                order_amount = max(balance['free'] - 5, 0)
                print(f"   ⚠️ {symbol}: Solde insuffisant, position réduite à {currency.format(order_amount)}")
            
            # Réduction de position si RSI élevé (zone de surachat)
            # 🔧 FIX 25/02: Pas de réduction en BULL_STRONG (RSI>70 est normal en bull)
            if indicators:
                rsi = indicators.get('rsi', 50)
                is_bull_strong = (hasattr(self, 'current_regime') and self.current_regime == 'BULL_STRONG')
                if rsi > 70 and not is_bull_strong:
                    order_amount *= 0.5  # Demi-position si surachat (hors bull fort)
                    print(f"   ⚠️ {symbol}: RSI élevé ({rsi:.1f}) - Réduction position à 50% = {currency.format(order_amount)}")
                elif rsi > 70 and is_bull_strong:
                    print(f"   ✅ {symbol}: RSI {rsi:.1f} élevé mais BULL_STRONG → position maintenue à 100%")
            
            print(f"   💰 {symbol}: Solde libre={currency.format(balance['free'])} | Position cible={currency.format(position_size)} | Ordre={currency.format(order_amount)}")
            
            if order_amount >= MIN_ORDER_SIZE:
                print(f"\nSIGNAL ACHAT: {symbol} (Position {current_positions+1}/{max_positions})")
                
                # === VALIDATION ANTI-BAISSE DÉSACTIVÉE ===
                # L'IA a déjà filtré les signaux, faire confiance à ses scores
                matching_signal = None
                if self.surveillance_service:
                    ai_status = self.surveillance_service.get_surveillance_status()
                    signal_data = ai_status.get('ready_signals', [])
                    matching_signal = next((s for s in signal_data if s.get('symbol') == symbol), None)
                    
                    if matching_signal:
                        features = matching_signal.get('features', {})
                        rsi = features.get('rsi', 50)
                        ai_score = matching_signal.get('score', 0)
                        
                        # Juste logger pour info, ne PLUS bloquer
                        print(f"   📊 {symbol}: Signal IA validé (Score={ai_score}, RSI={rsi:.0f})")
                
                # === UTILISER SL/TP DYNAMIQUES SI DISPONIBLES ===
                stop_loss_pct = STOP_LOSS_PERCENT
                take_profit_pct = TAKE_PROFIT_PERCENT
                
                # 🆕 FIX 29/01: SL DYNAMIQUE selon BTC momentum
                # Si BTC en baisse → SL plus serré (risque marché bear)
                # Si BTC en hausse → SL normal (laisser respirer)
                if hasattr(AIPredictor, '_btc_momentum'):
                    btc_mom = getattr(AIPredictor, '_btc_momentum', 0)
                    # 🔧 FIX 25/02: Pas de resserrement SL en BULL_STRONG
                    # En bull, les micro-corrections BTC sont normales et temporaires
                    is_bull_regime = (hasattr(self, 'current_regime') and self.current_regime in ('BULL_STRONG', 'BULL_WEAK'))
                    if not is_bull_regime:
                        if btc_mom < -0.003:  # BTC < -0.3% = marché baissier
                            stop_loss_pct = 1.5  # SL serré à -1.5%
                            print(f"   ⚠️ SL SERRÉ -1.5%: BTC baissier ({btc_mom:+.2%}) → Protection renforcée")
                        elif btc_mom < -0.001:  # BTC < -0.1% = marché faible
                            stop_loss_pct = 2.0  # SL intermédiaire à -2.0%
                            print(f"   🟡 SL INTERMÉDIAIRE -2.0%: BTC faible ({btc_mom:+.2%})")
                    elif btc_mom < -0.003:
                        print(f"   ✅ SL maintenu 2.5%: BTC {btc_mom:+.2%} mais BULL → pas de resserrement")
                
                # Récupérer les valeurs dynamiques du signal (déjà trouvé ci-dessus)
                is_dynamic_sltp = False
                if matching_signal and 'dynamic_sl' in matching_signal and 'dynamic_tp' in matching_signal:
                    stop_loss_pct = matching_signal['dynamic_sl']
                    take_profit_pct = matching_signal['dynamic_tp']
                    rr_ratio = matching_signal.get('dynamic_rr', 0)
                    is_dynamic_sltp = True
                    print(f"   🎯 SL/TP DYNAMIQUES: SL={stop_loss_pct}% TP={take_profit_pct}% (R/R={rr_ratio}:1)")
                else:
                    print(f"   📊 SL/TP FIXES: SL={stop_loss_pct}% TP={take_profit_pct}%")
                
                # 🆕 FIX 28/01 13h: TP DYNAMIQUE BASÉ SUR BTC MOMENTUM + TENDANCE INDIVIDUELLE
                # 🔴 FIX 28/01 19h: TP AGRESSIF si RSI > 65 (proche pic)
                # Logique: Si BTC monte fort + crypto monte fort → TP élevé (laisser courir)
                #          Si BTC baisse → TP court (sécuriser vite)
                #          Si RSI > 65 → TP très court (1.5%) pour sécuriser profits avant correction
                if not is_dynamic_sltp and hasattr(AIPredictor, '_btc_momentum'):
                    btc_mom = getattr(AIPredictor, '_btc_momentum', 0)
                    
                    # Analyser la force de la tendance individuelle
                    try:
                        klines = self.client.get_klines(symbol, interval='1m', limit=30)
                        if len(klines) >= 25:
                            prices = [float(k[4]) for k in klines]
                            ema9 = TechnicalIndicators.ema(prices, 9)    # 9 périodes sur 1min = 9min
                            ema21 = TechnicalIndicators.ema(prices, 21)  # 21 périodes sur 1min = 21min
                            mom3 = ((prices[-1] - prices[-4]) / prices[-4]) * 100 if len(prices) >= 4 else 0
                            rsi = TechnicalIndicators.rsi(prices, 14) if len(prices) >= 15 else 50
                            
                            # � OPTIMISATION 29/01: LAISSER COURIR LES GAINS
                            # Ne plus forcer TP=1.5% sur RSI>65 → Laisser la logique BTC x Tendance décider
                            # RSI>65 n'est pas un signal de vente si tendance + BTC fort
                            # EXIT se fera naturellement avec END_OF_CYCLE (RSI>70 + BB>0.85 + Mom<0)
                            if ema9 and ema21:
                                ema_gap_pct = ((ema9 - ema21) / ema21) * 100
                                
                                # Classifier la force de la tendance
                                is_strong_trend = (ema_gap_pct > 0.15 and mom3 > 0.2)
                                is_medium_trend = (ema_gap_pct > 0.1 and mom3 > 0.1)
                                
                                # Matrice TP: BTC momentum x Tendance crypto
                                if btc_mom > 0.005:  # BTC > +0.5%
                                    if is_strong_trend:
                                        take_profit_pct = 6.0  # Marché + crypto fort → laisser courir
                                        print(f"   🚀 TP ÉTENDU 6%: BTC+{btc_mom:.2%} + Tendance forte (EMA {ema_gap_pct:+.2f}% Mom3 {mom3:+.2f}%)")
                                    elif is_medium_trend:
                                        take_profit_pct = 4.5
                                        print(f"   📈 TP ÉTENDU 4.5%: BTC+{btc_mom:.2%} + Tendance moyenne")
                                elif btc_mom > 0.002:  # BTC entre +0.2% et +0.5%
                                    if is_strong_trend:
                                        take_profit_pct = 4.5
                                        print(f"   📈 TP ÉTENDU 4.5%: BTC stable+{btc_mom:.2%} + Tendance forte")
                                    elif is_medium_trend:
                                        take_profit_pct = 3.5
                                        print(f"   📊 TP MOYEN 3.5%: BTC stable + Tendance moyenne")
                                elif btc_mom > -0.003:  # BTC entre -0.3% et +0.2%
                                    if is_strong_trend:
                                        take_profit_pct = 3.5
                                        print(f"   ⚠️ TP RÉDUIT 3.5%: BTC neutre{btc_mom:+.2%} mais crypto forte")
                                    else:
                                        take_profit_pct = 2.5
                                        print(f"   ⚠️ TP STANDARD 2.5%: BTC neutre, sécuriser")
                                else:  # BTC < -0.3%
                                    take_profit_pct = 2.5
                                    print(f"   🚨 TP COURT 2.5%: BTC baisse {btc_mom:.2%} → Sécuriser vite")
                    except Exception as e:
                        pass  # Garder TP par défaut si erreur
                
                # === AJUSTEMENT SL/TP SELON RÉGIME DE MARCHÉ ===
                # 🔧 FIX 28/02: BEAR BLOQUE TOUS LES ACHATS — AUCUNE EXCEPTION
                # Historique: Le 27/02, on autorisait score≥85 avec 25% position
                # Résultat: 23 "BEAR EXCEPTION" → 19 pertes sur 20 trades = -47$ 
                # L'inflation des scores IA (bonus TOP20+correlation+time = +45pts) 
                # fait que TOUS les signaux atteignent 85+ → le filtre ne filtrait RIEN
                # SOLUTION: En BEAR/CORRECTION, ZERO achat. Point final.
                if self.market_regime:
                    try:
                        regime_name, regime_details = self.market_regime.detect_regime(force_update=True)
                        
                        # Adapter les SL/TP selon le régime (sauf si dynamiques déjà définis)
                        if not is_dynamic_sltp:
                            if regime_name in ('BEAR', 'CORRECTION'):
                                # 🔧 FIX 28/02: BLOCAGE TOTAL en BEAR — plus aucune exception
                                # Evidence: 23 exceptions le 27-28/02, 19 pertes = taux échec 95%
                                # Les scores IA sont gonflés par les bonus (+30 à +45 pts)
                                # Un "score 85" en BEAR = base_score 40-55 + bonus = FAUX signal
                                print(f"   🐻🛑 REGIME {regime_name}: ACHAT BLOQUÉ (score={ai_score})")
                                print(f"   → FIX 28/02: ZERO achat en BEAR/CORRECTION — Seules les ventes sont actives")
                                self.current_regime = regime_name
                                try:
                                    from market_regime import get_market_regime_detector
                                    _singleton = get_market_regime_detector()
                                    if _singleton:
                                        _singleton.current_regime = regime_name
                                        _singleton.regime_config = regime_details
                                except Exception:
                                    pass
                                return  # Annuler cet achat — AUCUNE exception
                            elif regime_name == 'NEUTRAL':
                                # Neutre: position très réduite + SL serré
                                # 🔧 FIX 26/02: R/R DOIT être ≥ 1:1 (ancien TP=1.3% < SL=1.5% = R/R inversé!)
                                stop_loss_pct = 1.5
                                take_profit_pct = 2.5  # R/R = 1.67:1 (anciennement 1.3% = R/R 0.87:1 PERDANT)
                                order_amount *= 0.35  # 35% seulement - prudence maximale
                                print(f"   ➡️ NEUTRAL: SL={stop_loss_pct}% TP={take_profit_pct}% Position={currency.format(order_amount)} (35%)")
                            # BULL_WEAK/BULL_STRONG: 100% = utiliser les valeurs par défaut
                            
                            # Sauvegarder le régime actuel
                            self.current_regime = regime_name
                            # 🔧 FIX 07/02: Sync singleton pour ai_predictor
                            try:
                                from market_regime import get_market_regime_detector
                                _singleton = get_market_regime_detector()
                                if _singleton:
                                    _singleton.current_regime = regime_name
                                    _singleton.regime_config = regime_details
                            except Exception:
                                pass
                    except Exception as e:
                        print(f"   ⚠️ Erreur ajustement régime: {e}")
                
                # Log du signal avant exécution
                if self.trade_logger and matching_signal:
                    try:
                        self.trade_logger.log_signal({
                            'timestamp': datetime.now(),
                            'symbol': symbol,
                            'signal_type': 'BUY',
                            'ai_score': matching_signal.get('score', 0),
                            # 🔥 FIX 26/01 22h40: or 'UNKNOWN' pour gérer None
                            'pattern': matching_signal.get('pattern') or 'UNKNOWN',
                            'smart_signal': matching_signal.get('smart_signal', ''),
                            'smart_eligible': matching_signal.get('smart_eligible', False),
                            'features': matching_signal.get('features', {}),
                            'dynamic_sltp': {
                                'sl_pct': stop_loss_pct,
                                'tp_pct': take_profit_pct,
                                'rr_ratio': rr_ratio if is_dynamic_sltp else (take_profit_pct/stop_loss_pct),
                                'is_dynamic': is_dynamic_sltp
                            },
                            'executed': True,
                            'reason': reason
                        })
                    except Exception as e:
                        pass  # Silencieux
                
                # Détecter si c'est un achat sur breakout BB+
                bb_breakout_entry = False
                if matching_signal:
                    features = matching_signal.get('features', {})
                    buy_reasons_str = ' '.join(matching_signal.get('buy_reasons', []))
                    # Si "BB+ BREAKOUT" est dans les raisons d'achat
                    if 'BB+ BREAKOUT' in buy_reasons_str or 'BB+ BREAKOUT' in reason:
                        bb_breakout_entry = True
                        
                        # TP ADAPTATIF: Augmenter le TP si breakout très fort
                        # Extraire la distance BB+ depuis les raisons
                        for r in matching_signal.get('buy_reasons', []):
                            if 'BB+ BREAKOUT' in r and '+' in r:
                                try:
                                    # Format: "BB+ BREAKOUT (+1.23% au-dessus)"
                                    bb_dist_str = r.split('(+')[1].split('%')[0]
                                    bb_distance = float(bb_dist_str)
                                    
                                    # Si breakout très fort (>1%), augmenter le TP
                                    if bb_distance > 1.0:
                                        # TP adaptatif: +0.5% de TP par % au-dessus de BB+
                                        tp_bonus = bb_distance * 0.5
                                        take_profit_pct += tp_bonus
                                        print(f"   🎯 TP ADAPTATIF: Breakout fort (+{bb_distance:.2f}%) → TP augmenté à {take_profit_pct:.1f}%")
                                    break
                                except:
                                    pass
                        
                        print(f"   📊 Détection: Entrée sur BREAKOUT BB+ - Vente auto si prix < BB+")
                
                # Récupérer le pattern du signal
                # 🔥 FIX 26/01 22h40: or 'UNKNOWN' pour gérer None
                # 🔧 FIX 14/02: Préserver le pattern INITIAL validé par l'IA
                # Éviter la race condition où le pattern est invalidé entre validation et exécution
                # Chercher d'abord dans le signal initial (reason), puis dans matching_signal
                signal_pattern = 'UNKNOWN'
                if matching_signal:
                    # Priorité 1: Pattern du signal actuel
                    signal_pattern = matching_signal.get('pattern') or 'UNKNOWN'
                    
                    # Priorité 2: Si pattern UNKNOWN, essayer de récupérer depuis smart_signal/buy_reasons
                    if signal_pattern in ['UNKNOWN', 'None', '', None]:
                        buy_reasons_list = matching_signal.get('buy_reasons', [])
                        for br in buy_reasons_list:
                            if 'CREUX' in br:
                                signal_pattern = 'CREUX_REBOUND'
                                break
                            elif 'BREAKOUT' in br:
                                signal_pattern = 'SQUEEZE_BREAKOUT' 
                                break
                            elif 'EXCEPTIONNEL' in br:
                                signal_pattern = 'SCORE_EXCEPTIONNEL'
                                break
                            elif 'GOLDEN' in br:
                                signal_pattern = 'GOLDEN_CROSS'
                                break
                            elif 'EARLY' in br:
                                signal_pattern = 'EARLY_MOMENTUM'
                                break
                            elif 'TREND' in br:
                                signal_pattern = 'TREND_FOLLOWING'
                                break
                    
                    # Priorité 3: Si toujours UNKNOWN mais score IA >= 80, utiliser SMART_VALIDATED
                    if signal_pattern in ['UNKNOWN', 'None', '', None]:
                        ai_sc = matching_signal.get('score', 0)
                        if ai_sc >= 80:
                            signal_pattern = 'SMART_VALIDATED'
                            print(f"   🔧 {symbol}: Pattern récupéré depuis score IA ({ai_sc}) → {signal_pattern}")
                
                # Dernier fallback: déduire du reason d'achat
                if signal_pattern in ['UNKNOWN', 'None', '', None] and reason:
                    if 'GOLDEN CROSS' in reason:
                        signal_pattern = 'GOLDEN_CROSS'
                    elif 'EARLY MOMENTUM' in reason:
                        signal_pattern = 'EARLY_MOMENTUM'
                    elif 'TREND FOLLOWING' in reason:
                        signal_pattern = 'TREND_FOLLOWING'
                    elif 'ACHAT HAUSSIER' in reason:
                        signal_pattern = 'ACHAT_HAUSSIER'
                    if signal_pattern != 'UNKNOWN':
                        print(f"   🔧 {symbol}: Pattern déduit de la raison d'achat → {signal_pattern}")
                
                # � FIX 31/03b: Dernier fallback — utiliser le pattern du signal original (indicators)
                # Bug: HOT_SIGNAL → matching_signal absent au moment execute_signal → signal_pattern='UNKNOWN'
                if signal_pattern in ['UNKNOWN', 'None', '', None] and indicators:
                    _ind_pattern = indicators.get('pattern', 'UNKNOWN')
                    if _ind_pattern and _ind_pattern not in ['UNKNOWN', 'None', '', None]:
                        signal_pattern = _ind_pattern
                        print(f"   🔧 {symbol}: Pattern récupéré depuis indicators → {signal_pattern}")
                
                # �🔴 FIX 02/03: Vérifier blacklist pattern sur le PREMIER chemin d'achat aussi!
                # Bug: process_signal n'avait AUCUNE vérification blacklist!
                # Résultat: positions PULLBACK ouvertes malgré blacklist
                try:
                    from pattern_manager import PatternManager
                    _pm = PatternManager()
                    if not _pm.is_pattern_allowed(signal_pattern):
                        print(f"   🚫 {symbol}: Pattern {signal_pattern} BLACKLISTÉ → achat annulé")
                        return
                except Exception:
                    pass
                
                # Ouvrir position avec flag BB breakout et pattern
                # 🆕 FIX 25/03: Ajustement TP si résistance 15min détectée entre entry et TP
                # Problème observé (KITE 25/03): TP=+3% fixé au-dessus d'un cluster de highs 15min
                # (0.2539-0.2552) → le coin rebondit sur la résistance et sort via MAX_HOLD, pas TP.
                # Fix: si une résistance significative se situe entre le prix actuel et le TP,
                # cap le TP juste en dessous de cette résistance (resistance × 0.995).
                try:
                    _current_px = self.client.get_price(symbol)
                    if _current_px:
                        _tp_price = _current_px * (1 + take_profit_pct / 100)
                        _k15 = self.client.get_klines(symbol, interval='15m', limit=100)  # 🔧 FIX 25/03: 48→100 (25h) — capture les résistances J-1
                        if _k15 and len(_k15) >= 10:
                            _highs_15 = [float(k[2]) for k in _k15]
                            _vols_15  = [float(k[5]) for k in _k15]
                            # Regrouper les highs dans des bandes de 1% pour trouver les clusters
                            _resistance_clusters = {}
                            for _h, _v in zip(_highs_15, _vols_15):
                                if _current_px < _h <= _tp_price:  # Entre current et TP
                                    _band = round(_h / (_current_px * 0.01)) * (_current_px * 0.01)
                                    if _band not in _resistance_clusters:
                                        _resistance_clusters[_band] = {'count': 0, 'vol': 0, 'max_h': 0}
                                    _resistance_clusters[_band]['count'] += 1
                                    _resistance_clusters[_band]['vol']   += _v
                                    _resistance_clusters[_band]['max_h'] = max(_resistance_clusters[_band]['max_h'], _h)
                            # Trouver la PREMIÈRE résistance significative (count≥2) qui permet un TP ≥ 1%
                            # Logique: cap au premier mur que le prix devra franchir en montant vers le TP
                            # On itère du plus bas vers le plus haut pour prendre la 1ère résistance valide
                            _best_cluster = None
                            for _band in sorted(_resistance_clusters.keys()):
                                _info = _resistance_clusters[_band]
                                if _info['count'] >= 2:
                                    _trial_tp_px  = _info['max_h'] * 0.995
                                    _trial_tp_pct = (_trial_tp_px / _current_px - 1) * 100
                                    if _trial_tp_pct >= 1.0:
                                        _best_cluster = _info
                                        break  # Première résistance valide (ordre croissant)
                            if _best_cluster:
                                _resist_px = _best_cluster['max_h']
                                _new_tp_px = _resist_px * 0.995  # Cap 0.5% sous la résistance
                                _new_tp_pct = (_new_tp_px / _current_px - 1) * 100
                                if _new_tp_pct >= 1.0 and _new_tp_pct < take_profit_pct:
                                    print(f"   🧱 {symbol}: Résistance 15min détectée @ {_resist_px:.5f} "
                                          f"({_best_cluster['count']} highs) → TP {take_profit_pct:.1f}%→{_new_tp_pct:.1f}%")
                                    take_profit_pct = round(_new_tp_pct, 2)
                except Exception:
                    pass  # Pas de résistance détectée → garder le TP original

                order = self.position_manager.open_position(
                    symbol, 
                    "BUY", 
                    order_amount, 
                    stop_loss_pct, 
                    take_profit_pct,
                    bb_breakout_entry=bb_breakout_entry,
                    pattern=signal_pattern
                )
                
                # Incrémenter compteur d'achats pour cette heure
                if order:
                    self.buys_per_hour[current_hour] = buys_this_hour + 1
                    # Nettoyer les heures passées (garder seulement dernière heure)
                    hours_to_keep = [current_hour, (current_hour - 1) % 24]
                    self.buys_per_hour = {h: c for h, c in self.buys_per_hour.items() if h in hours_to_keep}
                
                # Log du trade ouvert
                if self.trade_logger and order and matching_signal:
                    try:
                        entry_price = float(order.get('fills', [{}])[0].get('price', 0))
                        if entry_price == 0:
                            entry_price = self.client.get_price(symbol)
                        
                        self.trade_logger.log_trade_open({
                            'timestamp': datetime.now(),
                            'symbol': symbol,
                            'side': 'BUY',
                            'entry_price': entry_price,
                            'quantity': float(order.get('executedQty', 0)),
                            'order_size_usd': order_amount,
                            'stop_loss': entry_price * (1 - stop_loss_pct / 100),
                            'take_profit': entry_price * (1 + take_profit_pct / 100),
                            'sl_pct': stop_loss_pct,
                            'tp_pct': take_profit_pct,
                            'rr_ratio': take_profit_pct / stop_loss_pct,
                            'is_dynamic_sltp': is_dynamic_sltp,
                            'ai_score': matching_signal.get('score', 0),
                            # 🔥 FIX 26/01 22h40: or 'UNKNOWN' pour gérer None
                            'pattern': matching_signal.get('pattern') or 'UNKNOWN',
                            'reason': reason,
                            'features': matching_signal.get('features', {})
                        })
                    except Exception as e:
                        pass  # Silencieux
                self.last_signal[symbol] = time.time()
                self.last_trade[symbol] = time.time()  # Cooldown de trade
            else:
                print(f"   {symbol}: Montant insuffisant ({currency.format(order_amount)} < {currency.format(MIN_ORDER_SIZE)})")
        
        elif signal == "SELL" and symbol in self.position_manager.positions:
            print(f"\nSIGNAL VENTE: {symbol}")
            self.position_manager.close_position(symbol, "signal")
            self.last_signal[symbol] = time.time()
            self.last_trade[symbol] = time.time()  # Cooldown de trade
    
    def display_status(self):
        """Affiche le statut du bot"""
        print("\033[2J\033[H")
        print("=" * 70)
        print("  🤖 TRADING BOT - ORDRES AUTOMATIQUES")
        print("=" * 70)
        print(f"  ⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print(f"  📡 Mode: {'🧪 TESTNET' if TESTNET_MODE else '⚠️ PRODUCTION'}")
        print(f"  💱 Devise: {DISPLAY_CURRENCY}")
        
        # Solde
        balance = self.client.get_balance("USDC")
        print(f"  💰 Solde: {currency.format(balance['free'])} (libre) | {currency.format(balance['locked'])} (bloqué)")
        print("=" * 70)
        
        # Positions ouvertes
        current_positions = len(self.position_manager.positions)
        max_positions = self.get_dynamic_max_positions()  # Utiliser la limite dynamique du market regime
        auto_status = "🟢 AUTO" if self.settings.get('autoTrade', True) else "🔴 MANUEL"
        print(f"\n📊 POSITIONS: {current_positions}/{max_positions} | {auto_status} | SL: {STOP_LOSS_PERCENT}% | TP: {TAKE_PROFIT_PERCENT}%")
        
        if self.position_manager.positions:
            for symbol, pos in self.position_manager.positions.items():
                current = self.client.get_price(symbol) or pos['entry_price']
                pnl_pct = ((current / pos['entry_price']) - 1) * 100
                icon = "🟢" if pnl_pct > 0 else "🔴"
                print(f"   {icon} {symbol}: {pos['quantity']:.6f} @ {currency.format(pos['entry_price'])} → {currency.format(current)} ({pnl_pct:+.2f}%)")
        else:
            print("\n📊 Aucune position ouverte")
        
        # Analyse des symboles
        print("\n📈 ANALYSE EN TEMPS RÉEL:")
        required_signals = 2 if current_positions >= (max_positions // 2) else 1
        print(f"   (Signaux requis pour achat: {required_signals}/3)")
        
        # Cache les résultats analyze() pour réutilisation dans save_analysis_data()
        self._cached_analysis = {}
        
        for symbol in self.watch_symbols:
            prices = list(self.prices[symbol])
            if len(prices) < 20:
                continue
            
            signal, indicators, reason = self.analyze(symbol)
            self._cached_analysis[symbol] = (signal, indicators, reason)
            price = indicators.get('price', 0)
            rsi = indicators.get('rsi', 0) or 50
            ai_score = indicators.get('ai_score', 50)
            bb_direction = indicators.get('bb_direction', 'flat')
            ema_alignment = indicators.get('ema_alignment', 'mixed')
            buy_signals_count = indicators.get('buy_signals', 0)
            is_bearish = indicators.get('is_bearish', False)
            
            # Indicateurs visuels
            trend_icon = "📈" if ema_alignment == 'bullish' else "📉" if ema_alignment == 'bearish' else "➡️"
            bb_icon = "⬆️" if bb_direction == 'up' else "⬇️" if bb_direction == 'down' else "➖"
            
            in_position = "📍" if symbol in self.position_manager.positions else ""
            icon = "🟢" if signal == "BUY" else "🔴" if signal == "SELL" else "🚫" if is_bearish else "⚪"
            
            # Affichage compact mais informatif
            print(f"   {icon} {symbol}{in_position}: {currency.format(price)} | RSI:{rsi:.0f} AI:{ai_score} | {trend_icon}EMA {bb_icon}BB | Sig:{buy_signals_count}")
        
        # Afficher l'historique des trades
        stats = self.position_manager.get_total_pnl()
        if stats['total_trades'] > 0:
            pnl_icon = "🟢" if stats['total_pnl'] >= 0 else "🔴"
            print(f"\n📜 HISTORIQUE ({stats['total_trades']} trades):")
            # P&L déjà en EUR, pas besoin de conversion
            print(f"   {pnl_icon} P&L Total: {stats['total_pnl']:+.2f}€")
            print(f"   📊 Win Rate: {stats['win_rate']:.1f}% ({stats['wins']}W / {stats['losses']}L)")
            
            # Derniers trades
            recent = self.position_manager.trade_history[-3:]
            if recent:
                print("   📝 Derniers trades:")
                for t in reversed(recent):
                    t_pnl = t.get('pnl', 0.0)  # Défaut si absent
                    t_icon = "🟢" if t_pnl >= 0 else "🔴"
                    # P&L déjà en EUR, pas besoin de conversion
                    pnl_pct = t.get('pnl_pct', 0.0)  # Défaut si absent (ancienne structure)
                    print(f"      {t_icon} {t['symbol']}: {t_pnl:+.2f}€ ({pnl_pct:+.2f}%) - {t['reason']}")
        
        print("\n" + "=" * 70)
        print("  [Ctrl+C pour arrêter]")
        
        # Sauvegarder les données d'analyse pour le dashboard
        self.save_analysis_data()
    
    def save_analysis_data(self):
        """Sauvegarde les données d'analyse en temps réel pour le dashboard"""
        try:
            print("   💾 [DEBUG] save_analysis_data: Début...")
            current_positions = len(self.position_manager.positions)
            max_positions = self.get_dynamic_max_positions()
            stats = self.position_manager.get_total_pnl()
            
            # Données des cryptos analysées - réutiliser le cache de display_status()
            cryptos_data = []
            for symbol in self.watch_symbols:
                prices = list(self.prices.get(symbol, []))
                if len(prices) < 20:
                    continue
                
                # Réutiliser les résultats cachés de display_status() si disponibles
                # 🔧 FIX 31/03: Supprimé le re-fetch klines_production ici.
                # AVANT: on refaisait 96 appels API get_klines_production pour recalculer RSI
                # → données désynchronisées (RSI frais mais AI score ancien du cache)
                # → 96 appels API supplémentaires ralentissant le cycle
                # APRÈS: on utilise les données de _cached_analysis (cohérentes entre elles)
                if hasattr(self, '_cached_analysis') and symbol in self._cached_analysis:
                    signal, indicators, reason = self._cached_analysis[symbol]
                else:
                    signal, indicators, reason = self.analyze(symbol)
                price = indicators.get('price', 0)
                rsi = indicators.get('rsi', 0) or 50
                ai_score = indicators.get('ai_score', 50)
                bb_direction = indicators.get('bb_direction', 'flat')
                ema_alignment = indicators.get('ema_alignment', 'mixed')
                buy_signals_count = indicators.get('buy_signals', 0)
                is_bearish = indicators.get('is_bearish', False)
                momentum = indicators.get('momentum', 0) or 0
                
                in_position = symbol in self.position_manager.positions
                
                cryptos_data.append({
                    'symbol': symbol,
                    'price': price,
                    'rsi': round(rsi, 1),
                    'aiScore': ai_score,
                    'emaAlignment': ema_alignment,
                    'bbDirection': bb_direction,
                    'buySignals': buy_signals_count,
                    'signal': signal,
                    'isBearish': is_bearish,
                    'inPosition': in_position,
                    'momentum': round(momentum, 2),
                    'reason': reason
                })
            
            # Positions ouvertes avec détails
            positions_data = []
            for symbol, pos in self.position_manager.positions.items():
                current = self.client.get_price(symbol) or pos['entry_price']
                pnl_pct = ((current / pos['entry_price']) - 1) * 100
                positions_data.append({
                    'symbol': symbol,
                    'quantity': pos['quantity'],
                    'entryPrice': pos['entry_price'],
                    'currentPrice': current,
                    'pnlPct': round(pnl_pct, 2),
                    'invested': round(pos['entry_price'] * pos['quantity'], 2)
                })
            
            # Logs récents (derniers trades)
            logs = []
            for t in self.position_manager.trade_history[-10:]:
                t_pnl = t.get('pnl', 0.0)
                log_type = 'success' if t_pnl >= 0 else 'error'
                logs.append({
                    'type': log_type,
                    'message': f"{t['symbol']}: {t_pnl:+.2f}€ ({t.get('pnl_pct', 0.0):+.2f}%) - {t['reason']}",
                    'timestamp': t.get('exit_time', datetime.now().isoformat())
                })
            
            # Compter les signaux
            buy_signals_total = sum(1 for c in cryptos_data if c['signal'] == 'BUY')
            sell_signals_total = sum(1 for c in cryptos_data if c['signal'] == 'SELL')
            
            # Analyse du marché - pourquoi le bot n'achète pas
            total_cryptos = len(cryptos_data)
            bearish_count = sum(1 for c in cryptos_data if c['isBearish'])
            bullish_count = total_cryptos - bearish_count
            rsi_oversold = [c for c in cryptos_data if c['rsi'] < RSI_OVERSOLD]
            rsi_overbought = [c for c in cryptos_data if c['rsi'] > RSI_OVERBOUGHT]
            high_ai = [c for c in cryptos_data if c['aiScore'] >= MIN_AI_SCORE_FOR_BUY]
            with_signals = [c for c in cryptos_data if c['buySignals'] >= MIN_BUY_SIGNALS]
            
            # Identifier les blocages
            blockers = []
            if bearish_count > total_cryptos * 0.7:
                blockers.append(f"Marché baissier: {bearish_count}/{total_cryptos} cryptos en tendance baissière ({bearish_count*100//total_cryptos}%)")
            if len(with_signals) == 0:
                blockers.append(f"Aucune crypto n'atteint {MIN_BUY_SIGNALS} signaux d'achat (paramètre MIN_BUY_SIGNALS)")
            if len(high_ai) == 0:
                blockers.append(f"Aucune crypto n'a un AI Score >= {MIN_AI_SCORE_FOR_BUY} (paramètre MIN_AI_SCORE)")
            if len(rsi_oversold) == 0:
                blockers.append(f"Aucune crypto en survente (RSI < {RSI_OVERSOLD})")
            
            # Top opportunités (même si bloquées)
            top_opportunities = sorted(cryptos_data, key=lambda x: (-x['buySignals'], -x['aiScore']))[:5]
            
            market_analysis = {
                'totalCryptos': total_cryptos,
                'bearishCount': bearish_count,
                'bullishCount': bullish_count,
                'bearishPercent': round(bearish_count * 100 / total_cryptos, 1) if total_cryptos > 0 else 0,
                'rsiOversold': len(rsi_oversold),
                'rsiOverbought': len(rsi_overbought),
                'highAiCount': len(high_ai),
                'withSignalsCount': len(with_signals),
                'blockers': blockers,
                'topOpportunities': [{
                    'symbol': c['symbol'],
                    'buySignals': c['buySignals'],
                    'aiScore': c['aiScore'],
                    'rsi': c['rsi'],
                    'isBearish': c['isBearish'],
                    'reason': c['reason']
                } for c in top_opportunities],
                'requirements': {
                    'minBuySignals': MIN_BUY_SIGNALS,
                    'minAiScore': MIN_AI_SCORE_FOR_BUY,
                    'rsiOversold': RSI_OVERSOLD,
                    'rsiOverbought': RSI_OVERBOUGHT
                }
            }
            
            # Données complètes
            analysis_data = {
                'timestamp': datetime.now().isoformat(),
                'stats': {
                    'positions': current_positions,
                    'maxPositions': max_positions,
                    'winRate': round(stats.get('win_rate', 0), 1),
                    'pnl': round(stats.get('total_pnl', 0), 2),
                    'totalTrades': stats.get('total_trades', 0),
                    'wins': stats.get('wins', 0),
                    'losses': stats.get('losses', 0)
                },
                'signals': {
                    'buy': buy_signals_total,
                    'sell': sell_signals_total
                },
                'marketAnalysis': market_analysis,
                'positions': positions_data,
                'cryptos': cryptos_data,
                'logs': logs,
                'settings': {
                    'autoTrade': self.settings.get('autoTrade', True),
                    'stopLoss': STOP_LOSS_PERCENT,
                    'takeProfit': TAKE_PROFIT_PERCENT,
                    'testnet': TESTNET_MODE
                }
            }
            
            # Sauvegarder dans le fichier
            analysis_file = os.path.join(SCRIPT_DIR, 'bot_analysis.json')
            with open(analysis_file, 'w', encoding='utf-8') as f:
                json.dump(analysis_data, f, ensure_ascii=False, indent=2)
            print(f"   ✅ [DEBUG] bot_analysis.json saved: {len(cryptos_data)} cryptos, {len(positions_data)} pos")
                
        except Exception as e:
            # Afficher l'erreur pour débugger
            print(f"   ❌ [DEBUG] Erreur save_analysis_data: {e}")
            import traceback
            traceback.print_exc()
    
    async def price_updater(self, symbol):
        """Met à jour les prix via WebSocket (ou API en testnet)"""
        
        # En mode testnet, pas de WebSocket disponible, utiliser l'API REST
        if TESTNET_MODE:
            while self.running:
                try:
                    price = self.client.get_price(symbol)
                    if price:
                        self.prices[symbol].append(price)
                    await asyncio.sleep(2)  # Mise à jour toutes les 2 secondes
                except Exception as e:
                    if self.running:
                        await asyncio.sleep(5)
        else:
            # Mode production: utiliser WebSocket
            stream = symbol.lower() + "@trade"
            url = f"wss://stream.binance.com:9443/ws/{stream}"
            
            while self.running:
                try:
                    async with websockets.connect(url) as ws:
                        async for message in ws:
                            if not self.running:
                                break
                            data = json.loads(message)
                            self.prices[symbol].append(float(data['p']))
                except:
                    if self.running:
                        await asyncio.sleep(5)
    
    async def trading_loop(self):
        """Boucle principale de trading"""
        # Yield immédiat pour permettre aux autres tâches (check_triggers_loop) de démarrer
        await asyncio.sleep(0)
        
        print(f"   🔍 [TRADING_LOOP] Démarrage - self.running={self.running}")
        
        watchlist_check_counter = 0
        klines_refresh_counter = 0
        market_regime_counter = 0  # Compteur pour actualisation market regime
        _last_display_time = 0  # 🔧 FIX 30/03: Throttle display_status (était appelé chaque cycle → 96 API calls!)
        
        iteration = 0
        while self.running:
            iteration += 1
            if iteration <= 3:
                print(f"   🔍 [TRADING_LOOP] Itération #{iteration} - self.running={self.running}")
            
            # ═══════════════════════════════════════════════════════════════
            # 🚀 FAST-PATH HOT SIGNALS: Traitement IMMÉDIAT avant toute opération lourde
            # Les HOT SIGNALS arrivent via callback IA en temps réel (~3s après détection)  
            # AVANT ce fix: ils attendaient après market_regime + klines_refresh + display_status
            # APRÈS: traités en priorité absolue dès le début du cycle (< 10ms)
            # ═══════════════════════════════════════════════════════════════
            if hasattr(self, '_hot_ai_signals') and self._hot_ai_signals:
                _now_fast = time.time()
                _fast_candidates = []
                with self._hot_ai_signals_lock:
                    _fresh = [s for s in self._hot_ai_signals if _now_fast - s.get('detected_at', 0) < 240]
                    self._hot_ai_signals = _fresh
                
                if _fresh and len(self.position_manager.positions) < self.get_dynamic_max_positions():
                    # Vérifier sécurité marché rapidement (sans appels lourds)
                    _market_ok = True
                    if self.trading_paused_until > _now_fast:
                        _market_ok = False
                    if _market_ok:
                        try:
                            _ms = get_market_safety()
                            _safe, _ = _ms.check_safety()
                            if not _safe:
                                _market_ok = False
                        except Exception:
                            pass
                    
                    if _market_ok:
                        for _hot in _fresh:
                            _hot_sym = _hot.get('symbol')
                            _hot_age = _now_fast - _hot.get('detected_at', 0)
                            _hot_score = _hot.get('score', 0)
                            # Seulement les signaux très frais (< 30s) pour le fast-path
                            # Les plus vieux seront traités normalement dans la section classique
                            if _hot_age < 30 and _hot_score >= 70:
                                print(f"   ⚡ FAST-PATH: {_hot_sym} détecté il y a {_hot_age:.0f}s (score={_hot_score})")
            
            # ═══════════════════════════════════════════════════════════════
            # ACTUALISATION MARKET REGIME toutes les 30 itérations (~15s)
            # Adaptation dynamique en temps réel selon les conditions du marché
            # Détection rapide des retournements pour réactivité maximale
            # ═══════════════════════════════════════════════════════════════
            market_regime_counter += 1
            if market_regime_counter >= 30:  # ~15 secondes (ultra-ultra-réactif)
                if self.market_regime:
                    try:
                        # Récupérer le régime actuel avant mise à jour
                        old_regime_name, _ = self.market_regime.get_current_regime()
                        old_max = self.market_regime.get_max_positions()
                        
                        # Forcer la détection du nouveau régime
                        new_regime_name, regime_details = self.market_regime.detect_regime(force_update=True)
                        new_max = self.market_regime.get_max_positions()
                        
                        # Logger si le régime a changé
                        if old_regime_name != new_regime_name or old_max != new_max:
                            score = self.market_regime.market_data.get('global_score', 0)
                            print(f"\n📊 MARKET REGIME CHANGÉ: {old_regime_name} ({old_max} pos) → {new_regime_name} ({new_max} pos) [Score: {score}]")
                            
                            # Si passage à un régime plus défensif (CORRECTION ou BEAR), resserrer les SL
                            if new_regime_name in ['CORRECTION', 'BEAR'] and old_regime_name in ['BULL_STRONG', 'BULL_WEAK', 'NEUTRAL']:
                                print(f"   🛡️ Resserrage des Stop-Loss (passage à {new_regime_name}) — SL depuis PRIX ACTUEL pour sécuriser les gains")
                                for symbol in list(self.position_manager.positions.keys()):
                                    pos = self.position_manager.positions[symbol]
                                    if 'stop_loss' in pos and 'entry_price' in pos:
                                        ep = pos['entry_price']
                                        try:
                                            cur_px = self.client.get_price(symbol) or ep
                                        except Exception:
                                            cur_px = ep
                                        cur_pnl = ((cur_px / ep) - 1) * 100
                                        
                                        if cur_pnl > 1.0:
                                            # En profit > 1% → SL à -1.5% depuis le PRIX ACTUEL (garde au moins 85% du gain)
                                            new_sl = cur_px * 0.985
                                            lock_label = f"prix actuel -1.5% (P&L={cur_pnl:+.1f}%)"
                                        else:
                                            # Quasi break-even ou perte : SL serré depuis l'entrée (-1% au lieu de -1.5%)
                                            new_sl = ep * 0.990
                                            lock_label = f"entrée -1.0% (P&L={cur_pnl:+.1f}%)"
                                        
                                        if new_sl > pos['stop_loss'] and new_sl < cur_px:
                                            old_sl = pos['stop_loss']
                                            sl_pct = ((new_sl - ep) / ep) * 100
                                            pos['stop_loss'] = new_sl
                                            print(f"      • {symbol}: SL {old_sl:.8f} → {new_sl:.8f} ({sl_pct:+.1f}% depuis entrée) [{lock_label}]")
                                try:
                                    self.position_manager._save_positions()
                                except Exception:
                                    pass

                                # ═══════════════════════════════════════════════════════════
                                # 🚨 AUTO SELL-ALL: Vente globale au retournement de marché
                                # Objectif: sécuriser les gains BULL avant que CORRECTION s'aggrave
                                # Cooldown 30min pour éviter l'oscillation BULL↔CORRECTION rapide
                                # ═══════════════════════════════════════════════════════════
                                n_pos = len(self.position_manager.positions)
                                _cooldown_ok = (time.time() - getattr(self, '_last_regime_sellall_time', 0)) > 1800
                                if n_pos > 0 and _cooldown_ok:
                                    symbols_to_sell = list(self.position_manager.positions.keys())
                                    sell_signal = {
                                        'action': 'SELL_ALL_COOLDOWN',
                                        'symbols': symbols_to_sell,
                                        'reason': f'AUTO-REGIME: {old_regime_name} → {new_regime_name}',
                                        'timestamp': datetime.now().isoformat()
                                    }
                                    sell_all_path = os.path.join(SCRIPT_DIR, 'sell_all_signal.json')
                                    try:
                                        with open(sell_all_path, 'w', encoding='utf-8') as _f:
                                            json.dump(sell_signal, _f, indent=2)
                                        self._last_regime_sellall_time = time.time()
                                        # Pause de 30 minutes (le marché peut rester en CORRECTION)
                                        _pause_until = time.time() + 1800
                                        pause_path = os.path.join(SCRIPT_DIR, 'trading_pause.json')
                                        with open(pause_path, 'w', encoding='utf-8') as _f:
                                            json.dump({
                                                'paused_until': _pause_until,
                                                'reason': f'AUTO_SELL_{new_regime_name}',
                                                'timestamp': datetime.now().isoformat()
                                            }, _f)
                                        self.trading_paused_until = _pause_until
                                        _resume = datetime.fromtimestamp(_pause_until).strftime('%H:%M:%S')
                                        print(f"   🚨 AUTO SELL-ALL déclenché: {n_pos} positions → {new_regime_name}")
                                        print(f"   📤 Vente: {symbols_to_sell}")
                                        print(f"   ⏸️ Trading en PAUSE 30min — reprise à {_resume}")
                                    except Exception as _e:
                                        print(f"   ⚠️ Erreur écriture sell_all_signal: {_e}")
                                elif n_pos > 0 and not _cooldown_ok:
                                    _remaining = int(1800 - (time.time() - self._last_regime_sellall_time))
                                    print(f"   ⏳ AUTO SELL-ALL: cooldown actif ({_remaining}s restants)")
                        else:
                            # Afficher le score même si stable pour monitoring
                            score = self.market_regime.market_data.get('global_score', 0)
                            print(f"   📊 Market Regime stable: {new_regime_name} ({new_max} pos) [Score: {score}]")
                    except Exception as e:
                        print(f"   ⚠️ Erreur actualisation market regime: {e}")
                        import traceback
                        traceback.print_exc()
                
                market_regime_counter = 0
            
            # Rafraîchir les klines toutes les 40 itérations (~20s) pour RSI ultra-réactif
            klines_refresh_counter += 1
            if klines_refresh_counter >= 40:
                try:
                    for symbol in self.watch_symbols[:20]:  # Limiter pour performance
                        klines = self.client.get_klines(symbol, DEFAULT_INTERVAL, 50)
                        if klines:
                            # Remplacer les données par les vrais prix de clôture des bougies
                            self.prices[symbol] = deque([float(k[4]) for k in klines], maxlen=100)
                except Exception as e:
                    pass  # Silencieux
                klines_refresh_counter = 0
            
            # ═══════════════════════════════════════════════════════════════
            # 🔄 FREQAI: CHECK RETRAINING PÉRIODIQUE (toutes les heures)
            # Auto-retraining adaptatif selon performances et régime de marché
            # ═══════════════════════════════════════════════════════════════
            if self.freqai_manager:
                current_time = time.time()
                if (current_time - self.last_freqai_check) >= 3600:  # 1 heure = 3600 secondes
                    try:
                        print("\n🔍 FreqAI: Vérification retraining périodique...")
                        retraining_results = self.freqai_manager.periodic_check()
                        
                        if retraining_results:
                            print("="*60)
                            print("✅ FREQAI: RETRAINING EFFECTUÉ")
                            print("="*60)
                            print(f"   • Raison: {retraining_results.get('reason', 'N/A')}")
                            print(f"   • Durée: {retraining_results.get('duration', 0):.1f}s")
                            models = retraining_results.get('models_retrained', [])
                            if models:
                                print(f"   • Modèles: {', '.join(models)}")
                            print("="*60)
                        else:
                            print("   ✓ Pas de retraining nécessaire")
                    except Exception as e:
                        print(f"   ⚠️ Erreur FreqAI periodic check: {e}")
                    
                    self.last_freqai_check = current_time
            
            # En mode testnet, ajouter le dernier prix pour réactivité
            if TESTNET_MODE:
                try:
                    all_prices = self.client.get_all_prices()
                    for symbol in self.watch_symbols:
                        if symbol in all_prices:
                            self.prices[symbol].append(all_prices[symbol])
                except Exception as e:
                    print(f"   Erreur récupération prix: {e}")
            
            # Vérifier stop-loss / take-profit AVANT quick-exit (priorisation SL)
            self.position_manager.check_stop_loss_take_profit()
            
            # ═══════════════════════════════════════════════════════════════
            # 🔴 FIX 01/04: PROTECTION RETOURNEMENT MARCHÉ
            # Quand le régime se dégrade (BULL→NEUTRAL ou BULL→BEAR/CORRECTION),
            # resserrer immédiatement les SL de toutes les positions ouvertes.
            # Quand BTC momentum < -1%, vendre les positions en profit et 
            # resserrer les SL à -0.8% du prix actuel.
            # ═══════════════════════════════════════════════════════════════
            try:
                _current_regime_rt, _ = self.market_regime.get_current_regime()
                _prev_regime_rt = getattr(self, '_last_known_regime', _current_regime_rt)
                
                # Détecter la transition de régime
                _bull_regimes = ('BULL_STRONG', 'BULL_WEAK')
                _was_bull = _prev_regime_rt in _bull_regimes
                _is_bearish_now = _current_regime_rt in ('BEAR', 'CORRECTION', 'NEUTRAL')
                _regime_degraded = _was_bull and _is_bearish_now
                
                # Détecter stress BTC (momentum très négatif)
                _btc_mom_rt = getattr(AIPredictor, '_btc_momentum', 0)
                _btc_stress = _btc_mom_rt < -1.0
                
                if (_regime_degraded or _btc_stress) and len(self.position_manager.positions) > 0:
                    _last_rt_action = getattr(self, '_last_regime_protection_time', 0)
                    if time.time() - _last_rt_action > 60:  # Max 1 fois par minute
                        _trigger = "RÉGIME DÉGRADÉ" if _regime_degraded else f"BTC STRESS (mom={_btc_mom_rt:.2f}%)"
                        print(f"\n🚨 PROTECTION RETOURNEMENT: {_trigger} ({_prev_regime_rt}→{_current_regime_rt})")
                        
                        _sold_rt = 0
                        _tightened_rt = 0
                        for _sym_rt, _pos_rt in list(self.position_manager.positions.items()):
                            try:
                                _cur_px_rt = self.position_manager.client.get_price(_sym_rt)
                                if not _cur_px_rt:
                                    continue
                                _ep_rt = _pos_rt.get('entry_price', _cur_px_rt)
                                _pnl_rt = ((_cur_px_rt / _ep_rt) - 1) * 100
                                
                                # Vendre si en profit > 0.2% (sécuriser les gains)
                                if _pnl_rt > 0.2:
                                    print(f"   💰 Vente PROTECTION {_sym_rt}: P&L={_pnl_rt:+.2f}% → Sécurisation")
                                    self.position_manager.close_position(_sym_rt, f"REGIME_PROTECT ({_pnl_rt:+.1f}%)")
                                    _sold_rt += 1
                                else:
                                    # Resserrer le SL à -0.8% du prix actuel
                                    _tight_sl = _cur_px_rt * 0.992  # -0.8%
                                    _old_sl = _pos_rt.get('stop_loss', 0)
                                    if _tight_sl > _old_sl:
                                        _pos_rt['stop_loss'] = _tight_sl
                                        _tightened_rt += 1
                                        print(f"   🛡️ SL RESSERRÉ {_sym_rt}: {_old_sl:.6f}→{_tight_sl:.6f} (-0.8% du prix actuel)")
                            except Exception:
                                pass
                        
                        if _sold_rt > 0 or _tightened_rt > 0:
                            self.position_manager._save_positions()
                            print(f"   📊 Bilan: {_sold_rt} vendues, {_tightened_rt} SL resserrés")
                        
                        self._last_regime_protection_time = time.time()
                
                self._last_known_regime = _current_regime_rt
            except Exception as _e_rt:
                pass  # Ne jamais bloquer le cycle principal
            
            # Portfolio Bear Melt: détection collapse global toutes les 3 itérations
            if not hasattr(self, '_bear_melt_counter'):
                self._bear_melt_counter = 0
            self._bear_melt_counter += 1
            if self._bear_melt_counter >= 3:  # ~3 cycles de 0.5s = 1.5s
                try:
                    self.position_manager.check_portfolio_bear_melt()
                except Exception as _bm_e:
                    pass  # Ne jamais bloquer le cycle principal
                self._bear_melt_counter = 0
            
            # Note: Vérification sell_all déplacée dans check_triggers_loop()
            # Note: Vérification rotation manuelle déplacée dans check_triggers_loop()
            # pour garantir une vérification toutes les 2 secondes même si le cycle est long
            
            # ═══════════════════════════════════════════════════════════════
            # QUICK EXIT: Vérification technique anticipée pour les positions
            # 🔧 FIX 30/01: Désactivé pour positions < 15min en bear market
            # ═══════════════════════════════════════════════════════════════
            for symbol in list(self.position_manager.positions.keys()):
                if symbol in self.prices and len(self.prices[symbol]) >= 21:
                    # 🔧 FIX 30/01: Skip quick-exit si position trop récente en bear market
                    # 🔴 FIX 03/02: Utiliser 'timestamp' (ISO string) au lieu de 'entry_time' (inexistant)
                    position = self.position_manager.positions.get(symbol)
                    if position:
                        # Convertir timestamp ISO en secondes depuis epoch
                        # 🔧 FIX 22/03: DATETIME TYPE BUG — position['timestamp'] est un
                        # objet datetime (datetime.now()), PAS une string. fromisoformat()
                        # échoue silencieusement → entry_time=0 → âge=28M min → protection jamais active
                        _ts_raw = position.get('timestamp', None)
                        if isinstance(_ts_raw, datetime):
                            entry_time = _ts_raw.timestamp()  # datetime object → directement .timestamp()
                        elif isinstance(_ts_raw, str):
                            try:
                                entry_time = datetime.fromisoformat(_ts_raw).timestamp()
                            except Exception:
                                entry_time = time.time()  # Fallback sûr: traiter comme toute fraîche
                        else:
                            entry_time = time.time()  # Aucun timestamp → traiter comme fraîche
                        position_age_minutes = (time.time() - entry_time) / 60
                        current_regime = self.market_regime.get_current_regime()[0] if hasattr(self, 'market_regime') else 'NEUTRAL'
                        
                        # 🔧 FIX 07/02: Protection time-based RENFORCÉE pour TOUS les régimes
                        # 🔴 FIX 09/02: AUGMENTÉ À 120min pour TOUS les régimes
                        # Données: sur 10 quick-exits (0% WR, -70.97$), les ages étaient 14-163min
                        # Les trades ont besoin de TEMPS pour se développer - le quick-exit
                        # ne sécurise que les gains sur positions MATURES (>2h)
                        # 🆕 FIX 09/03: CREUX_REBOUND = cycle court (20-40min) → protection 30min seulement
                        # 🔧 FIX 21/03: Rétro 582 trades → bucket 30-120min = 54% WR vs <30min = 29% WR
                        # CREUX_REBOUND min_age relevé à 60min pour laisser le cycle se développer
                        # 🔵 FIX 25/03: EMA_BULLISH = croisement EMA (tendance à court terme)
                        # Un EMA_BULLISH qui plonge immédiatement n'a pas besoin de 2h protection
                        # → Protection réduite à 45min (laisse 3 cycles 15min se développer)
                        _pattern_pos = position.get('pattern', '')
                        if _pattern_pos == 'CREUX_REBOUND':
                            min_age_minutes = 60
                        elif _pattern_pos == 'EMA_BULLISH':
                            min_age_minutes = 45
                        else:
                            min_age_minutes = 120
                        
                        # 🔴 FIX 01/04: BYPASS protection temps si marché en stress
                        # Problème: lors d'un retournement brutal, la protection 60-120min
                        # empêche le quick-exit de vendre → tous les gains sont anéantis.
                        # Solution: si le BTC momentum est très négatif (retournement marché),
                        # réduire la protection à 5min pour permettre la vente rapide.
                        _market_stress = False
                        try:
                            _btc_mom = getattr(AIPredictor, '_btc_momentum', 0)
                            _reg_qe, _ = self.market_regime.get_current_regime()
                            # Stress si: BTC momentum < -1% OU régime BEAR/CORRECTION
                            _market_stress = (_btc_mom < -1.0 or _reg_qe in ('BEAR', 'CORRECTION'))
                        except Exception:
                            pass
                        
                        if _market_stress:
                            min_age_minutes = 5  # En stress marché, autoriser quick-exit après 5min seulement
                        
                        if position_age_minutes < min_age_minutes:
                            print(f"   🛡️ {symbol}: Skip quick-exit ({current_regime} protection, age={position_age_minutes:.1f}min < {min_age_minutes}min)")
                            continue  # Skip quick-exit, laisser SL gérer
                    
                    should_exit, exit_reason = self.position_manager.check_technical_exit(symbol, self.prices[symbol])
                    if should_exit:
                        print(f"\n   ⚡ {symbol}: {exit_reason}")
                        self.position_manager.close_position(symbol, "quick-exit")
                        self.last_trade[symbol] = time.time()
            
            # Recharger la watchlist toutes les 30 secondes (15 itérations)
            watchlist_check_counter += 1
            if watchlist_check_counter >= 15:
                self.reload_watchlist()
                watchlist_check_counter = 0

            # 🆕 FIX 25/03: Nettoyage profond watchlist toutes les 2 heures
            # Supprime les symboles spy expirés MÊME sans métadonnée (sessions passées)
            if (time.time() - self.last_watchlist_cleanup) >= 7200:  # 2h
                print("\n🧹 Nettoyage profond watchlist (deep TTL check)...")
                self._cleanup_expired_spy_symbols(deep=True)
                self.last_watchlist_cleanup = time.time()
            
            # ═══════════════════════════════════════════════════════════════
            # VÉRIFICATION PAUSE GLOBALE (après SELL_ALL)
            # ═══════════════════════════════════════════════════════════════
            if self.trading_paused_until > time.time():
                remaining = int(self.trading_paused_until - time.time())
                minutes = remaining // 60
                seconds = remaining % 60
                print(f"\n⏸️ TRADING EN PAUSE - Temps restant: {minutes}m {seconds}s")
                print(f"   🔒 Aucun achat autorisé (protection après vente totale)")
                # Sauvegarder les données d'analyse même en pause
                self.save_analysis_data()
                await asyncio.sleep(2)
                continue  # Passer au cycle suivant sans traiter d'achats
            
            # ═══════════════════════════════════════════════════════════════
            # PROTECTION D'URGENCE - Vérifier sécurité du marché
            # ═══════════════════════════════════════════════════════════════
            market_safety = get_market_safety()
            is_safe, safety_reason = market_safety.check_safety()
            
            print(f"🔍 [DEBUG] Market safety: {is_safe} ({safety_reason})")
            
            if not is_safe:
                # Marché dangereux - bloquer les achats
                print(f"\n🛑 ACHATS BLOQUÉS - {safety_reason}")
                print("   💡 Le trading reprendra automatiquement quand le marché se stabilisera")
                # Sauvegarder les données d'analyse même quand bloqué
                self.save_analysis_data()
                await asyncio.sleep(2)
                continue  # Passer au cycle suivant sans traiter d'achats
            
            print("🔍 [DEBUG-AFTER-SAFETY] Market safety OK, continuons vers section achat IA...")
            
            # ═══════════════════════════════════════════════════════════════
            # PRIORITÉ 1: SIGNAUX IA "READY" (Score >= 70, détectés par surveillance)
            # ═══════════════════════════════════════════════════════════════
            ai_buy_candidates = []
            current_positions = len(self.position_manager.positions)
            max_positions = self.get_dynamic_max_positions()
            
            print(f"\n🔍 [DEBUG-ENTRY] SECTION ACHAT IA: Positions={current_positions}/{max_positions}, Surveillance={self.surveillance_service is not None}")
            print(f"   [DEBUG-ENTRY] Condition: surveillance={self.surveillance_service is not None}, positions_ok={current_positions < max_positions}")
            
            if self.surveillance_service and current_positions < max_positions:
                try:
                    ai_status = self.surveillance_service.get_surveillance_status()
                    ready_signals = ai_status.get('ready_signals', [])
                    
                    # 🚀 MERGE HOT SIGNALS: signaux temps réel via callback (détectés mid-cycle)
                    # Un HOT SIGNAL = détecté par un worker IA immédiatement (~3s vs ~80s d'attente)
                    if hasattr(self, '_hot_ai_signals') and self._hot_ai_signals:
                        _now_hot = time.time()
                        with self._hot_ai_signals_lock:
                            _fresh_hot = [s for s in self._hot_ai_signals
                                          if _now_hot - s.get('detected_at', 0) < 180]
                            self._hot_ai_signals = _fresh_hot
                            _existing_syms = {s.get('symbol') for s in ready_signals}
                            _added_hot = 0
                            for _hot_sig in _fresh_hot:
                                if _hot_sig.get('symbol') not in _existing_syms:
                                    ready_signals.insert(0, _hot_sig)  # Priorité en tête
                                    _existing_syms.add(_hot_sig['symbol'])
                                    _added_hot += 1
                            if _added_hot > 0:
                                print(f"   🚀 {_added_hot} HOT SIGNAL(S) ajouté(s) en priorité")
                    
                    print(f"🔍 [DEBUG] Ready signals reçus: {len(ready_signals)} (dont hot)")
                    
                    # 🔴 CORRECTION CRITIQUE: Récupérer la watchlist FRAÎCHE pour revalider les scores
                    # Le cache peut avoir des scores obsolètes (avant détection END_OF_CYCLE, etc.)
                    fresh_watchlist = {}
                    # HOLD ajouté 17/01: Pattern de MAINTIEN, pas d'ACHAT (ETH 07h36 -2.76%)
                    # � 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', 'HOLD']
                    # 🔧 FIX: Score minimum abaissé 75→65 — smart_criteria + is_valid_buy_signal 
                    # valident déjà la qualité du signal, score 65+ est suffisant
                    # 🔧 FIX 31/03b: Score minimum adaptatif selon le régime
                    # BULL_STRONG: 55 (marché haussier, l'IA voit des setups légitimes en score 55-64)
                    # Autres: 65 (standard)
                    _min_score_regime = 'NEUTRAL'
                    try:
                        _min_score_regime = self.ai_predictor._get_market_regime()
                    except Exception:
                        pass
                    MIN_SCORE_ABSOLUTE = 55 if _min_score_regime == 'BULL_STRONG' else 65
                    
                    if hasattr(self, 'ai_predictor') and self.ai_predictor:
                        try:
                            wl = self.ai_predictor.get_watchlist()
                            fresh_watchlist = {w.get('symbol'): w for w in wl}
                        except Exception as e:
                            print(f"   ⚠️ Impossible de récupérer watchlist fraîche: {e}")
                    
                    for sig in ready_signals:
                        symbol = sig.get('symbol')
                        
                        # 🔴 FIX CRITIQUE: Définir les variables dès le début pour éviter UnboundLocalError
                        score = sig.get('score', 0)
                        pattern = sig.get('pattern', 'NEUTRAL')
                        
                        # 🔴 FIX 26/01: Valider pattern non vide/None
                        if not pattern or pattern in ['None', '', 'null']:
                            pattern = 'NEUTRAL'
                        
                        # DEBUG: Log chaque signal reçu
                        print(f"   🔎 {symbol}: Traitement signal (score={score} pattern={pattern})")
                        
                        # 🔧 FIX 06/02: Vérifier pattern blacklist (jamais vérifié dans le chemin IA!)
                        # 🔧 PERF: PatternManager singleton (évite ré-instanciation à chaque signal)
                        try:
                            if not hasattr(self, '_pattern_manager_cache'):
                                from pattern_manager import PatternManager
                                self._pattern_manager_cache = PatternManager()
                            pm = self._pattern_manager_cache
                            if not pm.is_pattern_allowed(pattern):
                                print(f"   🚫 {symbol}: Pattern {pattern} BLACKLISTÉ → signal ignoré")
                                continue
                        except Exception:
                            pass  # Si PatternManager indisponible, continuer sans bloquer
                        
                        # 🔴 FIX 09/02: Filtre horaire AUSSI sur le 2ème chemin d'achat
                        # Bug: le ready_signals path n'avait PAS de filtre horaire!
                        # Résultat: 10 achats pendant crash 03h-08h UTC = toutes des pertes
                        try:
                            from config import BLOCKED_TRADING_HOURS_UTC, ENABLE_HOUR_FILTER
                            current_hour_utc = datetime.utcnow().hour
                            if ENABLE_HOUR_FILTER and current_hour_utc in BLOCKED_TRADING_HOURS_UTC:
                                # 🆕 FIX 24/03: BYPASS pour CREUX_REBOUND extrême (RSI≤30)
                                # Un fond de creux profond en heure bloquée est trop rare pour être ignoré
                                # Seule condition: RSI≤30 (oversold extrême) + pattern confirmé CREUX_REBOUND
                                _pre_rsi = sig.get('features', {}).get('rsi', 50)
                                if pattern == 'CREUX_REBOUND' and _pre_rsi <= 30:
                                    print(f"   ⚡ {symbol}: CREUX_REBOUND extrême (RSI={_pre_rsi:.0f}≤30) — BYPASS heure bloquée ({current_hour_utc}h UTC)")
                                else:
                                    print(f"   🕐 {symbol}: Heure toxique ({current_hour_utc}h UTC) - achat bloqué")
                                    continue
                        except ImportError:
                            pass
                        
                        # ═══════════════════════════════════════════════════════════
                        # 🔴 SIGNAUX REVALIDÉS - Accepter signaux en cache revalidés
                        # 🔧 FIX TEMPORAL: MAX_SIGNAL_AGE réduit 600s→180s
                        # Une watchlist fraîche (~180s cache) + cycle bot (~115s) = max 5min de décalage
                        # Un signal de 10min dans un marché volatile = données complètement obsolètes
                        # ═══════════════════════════════════════════════════════════
                        detected_at = sig.get('detected_at', 0)
                        signal_age = time.time() - detected_at
                        # 🔧 FIX 30/03: Signal age adaptatif selon qualité
                        # AVANT: 180s fixe → signaux score 75+ expiraient de 10-30s (INIT, XAI)
                        # Le cycle IA (~17s) + sleep 5s + bot main loop + display overhead = délai cumulé
                        MAX_SIGNAL_AGE = 240  # Base: 4min (était 3min — trop serré avec cycle IA ~20s)
                        if pattern == 'CREUX_REBOUND' and score >= 75:
                            MAX_SIGNAL_AGE = 360  # CREUX fort: 6min (signal rare et précieux)
                        elif score >= 80:
                            MAX_SIGNAL_AGE = 300  # Signal haute qualité: 5min
                        
                        print(f"   🔎 [TR1] {symbol}: age={signal_age:.0f}s max={MAX_SIGNAL_AGE}s")
                        if signal_age > MAX_SIGNAL_AGE:
                            print(f"   ⏰ {symbol}: Signal TROP VIEUX ({signal_age:.0f}s > {MAX_SIGNAL_AGE}s) - IGNORÉ")
                            continue
                        
                        # 🔍 FILTRES QUALITÉ SIGNAL (issus analyse 22/03 sur 50 trades réels)
                        # Source: crash-test mars 18-21 — corrélation features → résultat
                        _sig_features = sig.get('features', {})
                        _sig_ema_slope = _sig_features.get('ema_slope', None)
                        _sig_cross_age = _sig_features.get('candles_since_bullish_cross', None)
                        
                        # Filtre 1: CREUX_REBOUND avec EMA déjà en hausse = entrée post-creux
                        # 🔧 FIX 31/03: Filtre adaptatif selon régime de marché
                        # AVANT: ema_slope>=0.05 + RSI>35 → bloquait 100% des signaux en BULL_STRONG
                        # car en bull, les creux ont RSI 40-65 et EMA slope 0.05-0.20 = C'EST NORMAL
                        # APRÈS: Seuils relevés en BULL, conservé en BEAR/NEUTRAL
                        if pattern == 'CREUX_REBOUND' and _sig_ema_slope is not None:
                            _sig_rsi_f1 = _sig_features.get('rsi', 50)
                            # 🔧 FIX 31/03: Utiliser market_regime directement au lieu de current_regime
                            # current_regime peut rester 'NEUTRAL' au démarrage → seuils BEAR appliqués en BULL
                            try:
                                _bot_regime_f1, _ = self.market_regime.get_current_regime()
                            except Exception:
                                _bot_regime_f1 = getattr(self, 'current_regime', 'NEUTRAL')
                            
                            # Seuils adaptatifs — en bull, le slope est naturellement positif
                            if _bot_regime_f1 == 'BULL_STRONG':
                                _slope_fort = 0.40     # Rebond très avancé (vs 0.15)
                                _slope_post = 0.20     # Post-creux slope (vs 0.05)
                                _rsi_post   = 60       # RSI post-creux (vs 35)
                            elif _bot_regime_f1 == 'BULL_WEAK':
                                _slope_fort = 0.30
                                _slope_post = 0.12
                                _rsi_post   = 50
                            else:  # NEUTRAL / BEAR / CORRECTION
                                _slope_fort = 0.15
                                _slope_post = 0.05
                                _rsi_post   = 35
                            
                            _ema_fort = (_sig_ema_slope >= _slope_fort)
                            _post_creux = (_sig_ema_slope >= _slope_post and _sig_rsi_f1 > _rsi_post)
                            if _ema_fort or _post_creux:
                                _rej_reason = (
                                    f"EMA slope={_sig_ema_slope:.2f}>={_slope_fort} (rebond entamé, régime={_bot_regime_f1})"
                                    if _ema_fort else
                                    f"EMA slope={_sig_ema_slope:.2f}>={_slope_post} + RSI={_sig_rsi_f1:.0f}>{_rsi_post} (post-creux, régime={_bot_regime_f1})"
                                )
                                print(f"   🚫 {symbol}: CREUX_REBOUND rejeté — {_rej_reason}")
                                continue

                        # Filtre 2: Golden cross très récent (<5 bougies) → WR=11%, avg=-1.06%
                        # Un cross à peine formé se retourne souvent (faux signal technique)
                        if _sig_cross_age is not None and 0 < _sig_cross_age < 5:
                            print(f"   🚫 {symbol}: Golden cross TROP RÉCENT ({_sig_cross_age} bougies) → signal peu fiable (WR historique=11%)")
                            continue

                        # Filtre 3: CREUX_REBOUND avec EMA21 en forte baisse = "couteau qui tombe"
                        # ema21_slope = variation EMA21 sur 10 bougies (50min en 5m)
                        # Un CREUX_REBOUND acheté pendant une baisse prolongée de l'EMA21 est un faux fond.
                        # Seuils: BEAR/CORRECTION = -0.3% toléré | NEUTRAL/autres = -0.5% toléré
                        _sig_ema21_slope = _sig_features.get('ema21_slope', None)
                        if pattern == 'CREUX_REBOUND' and _sig_ema21_slope is not None:
                            try:
                                _bot_regime, _ = self.market_regime.get_current_regime()
                            except Exception:
                                _bot_regime = getattr(self, 'current_regime', 'NEUTRAL')
                            _ema21_threshold = -0.3 if _bot_regime in ('BEAR', 'CORRECTION') else -0.5
                            if _sig_ema21_slope < _ema21_threshold:
                                print(f"   🚫 {symbol}: CREUX_REBOUND rejeté — EMA21 slope={_sig_ema21_slope:.2f}% < {_ema21_threshold}% (couteau qui tombe, régime={_bot_regime})")
                                continue
                        
                        # 🔴 REVALIDATION FRAÎCHE: Utiliser le score ACTUEL de la watchlist
                        fresh_data = fresh_watchlist.get(symbol, {})
                        fresh_score = fresh_data.get('score', 0)
                        fresh_pattern = fresh_data.get('pattern', 'NEUTRAL')
                        fresh_features = fresh_data.get('features', {})
                        print(f"   [TR2] {symbol}: fresh={bool(fresh_data)} fscore={fresh_score} fpattern={fresh_pattern}")
                        
                        # 🔧 FIX TEMPORAL: Vérifier l'âge des données fraîches
                        # La watchlist elle-même est cachée 180s → fresh_data peut avoir 3min de retard
                        # Si timestamp disponible, rejeter les données trop vieilles
                        fresh_data_ts = fresh_data.get('timestamp', fresh_data.get('detected_at', 0))
                        fresh_data_age = time.time() - fresh_data_ts if fresh_data_ts else 0
                        if fresh_data_ts and fresh_data_age > 240:  # >4min = données périmées
                            print(f"   ⏰ {symbol}: Données fraîches PÉRIMÉES ({fresh_data_age:.0f}s) - watchlist obsolète, signal ignoré")
                            continue
                        
                        # 🔧 FIX TEMPORAL: Vérification 1h trend par coin pour patterns DIP
                        # Pont entre régime (1h BTC global) et signal (5min individuel)
                        # Un CREUX_REBOUND sur 5min pendant une tendance 1h bearish = piège
                        # 🆕 FIX 10/03 PHA: Utiliser pattern CACHÉ (pas fresh_pattern_check) comme trigger
                        # BUG: si fresh_data retourne NEUTRAL (signal émisé), le check 1h était évité
                        # même si le signal caché était CREUX_REBOUND → 20h de baisse PHA non détecté
                        fresh_pattern_check = fresh_data.get('pattern', pattern)
                        if pattern in ['CREUX_REBOUND', 'PULLBACK'] or fresh_pattern_check in ['CREUX_REBOUND', 'PULLBACK']:
                            try:
                                klines_1h = self.client.get_klines_production(symbol, '1h', 24, cache_ttl=30)  # 🔧 PERF: cache 30s (bougie 1h ne change pas en 30s)
                                if klines_1h and len(klines_1h) >= 10:
                                    c1h = [float(k[4]) for k in klines_1h]  # Close prices (EMA, mom)
                                    h1h = [float(k[2]) for k in klines_1h]  # HIGH prices (détection pic intraday)
                                    l1h = [float(k[3]) for k in klines_1h]  # 🆕 FIX 09/03 v4: LOW prices (détection creux)
                                    ema9_1h = sum(c1h[-EMA_SHORT:]) / EMA_SHORT  # SMA EMA court terme (1h bars)
                                    ema21_1h = sum(c1h[-min(EMA_LONG, len(c1h)):]) / min(EMA_LONG, len(c1h))
                                    mom_3h_coin = (c1h[-1] - c1h[-4]) / c1h[-4] * 100 if len(c1h) >= 4 else 0
                                    mom_6h_coin = (c1h[-1] - c1h[-7]) / c1h[-7] * 100 if len(c1h) >= 7 else 0
                                    # 🔴 FIX 09/03 v2: BUG CRITIQUE — utiliser HIGH 1h pas CLOSE!
                                    # BUG v1: spike intraday (KNC 11:20 → 0.1118) invisible dans close 11h
                                    # → fall_from_3h_high ≈ 0% alors que la chute réelle est -1.7%
                                    # FIX: price_3h_max = max des HIGHS des 4 dernières bougies 1h
                                    price_3h_max_high = max(h1h[-4:]) if len(h1h) >= 4 else h1h[-1]
                                    fall_from_3h_high = (c1h[-1] - price_3h_max_high) / price_3h_max_high * 100
                                    # 🆕 FIX 09/03 v4: DISCRIMINATEUR "AU FOND" — si prix proche du LOW 4h,
                                    # c'est un VRAI creux (ex: BTC 14h30), PAS un essoufflement post-pump.
                                    # Ex AUDIO: pas près du low 4h → still blocked ✓
                                    # Ex BTC 14h30: prix ≈ low 4h → near_4h_bottom=True → pas bloqué ✓
                                    low_4h = min(l1h[-4:]) if len(l1h) >= 4 else l1h[-1]
                                    near_4h_bottom = (c1h[-1] - low_4h) / max(low_4h, 1e-10) * 100 < 0.5
                                    # 🆕 FIX 10/03: Exemption SQUEEZE (modèle XLM) — si range 4h très étroit
                                    # (<2.5%), c'est une consolidation en squeeze, PAS un downtrend.
                                    # Dans un squeeze, fall_from_3h_high peut être -1%+ juste à cause
                                    # de l'oscillation dans le range (haut vs bas). Ex XLM: range 1.8%
                                    _1h_range_4h = (max(h1h[-4:]) - min(l1h[-4:])) / max(min(l1h[-4:]), 1e-10) * 100
                                    # 🔴 FIX TLM: Un post-pump retracement N'EST PAS un squeeze!
                                    # BUG: TLM range 4h=2.2% après pump 18h38→18h50 → squeeze=True → coin_1h_bearish=False → achat 19h04
                                    # LOGIQUE: un "squeeze" = compression AVANT breakout, PAS après un pump déjà réalisé
                                    # Si fall_from_3h_high < -0.5%, le prix a DÉJÀ pumpé puis retombé → pas un squeeze
                                    _near_squeeze_breakout = _1h_range_4h < 2.5 and mom_6h_coin > -0.8 and fall_from_3h_high > -0.5
                                    # 🔧 FIX 16/03: FILTRE COIN PLAT — range total 24h < 4%
                                    # Un coin qui n'a pas bougé de plus de 4% en 24h ne mérite pas d'achat
                                    # EXCEPTION: Grandes caps (BTC/ETH/BNB/SOL...) en accumulation
                                    _MAJOR_COINS_SPY = {'BTCUSDC','ETHUSDC','BNBUSDC','SOLUSDC','XRPUSDC','LTCUSDC','ADAUSDC','DOGEUSDC','DOTUSDC','AVAXUSDC'}
                                    _range_24h_total = (max(h1h) - min(l1h)) / max(min(l1h), 1e-10) * 100
                                    if _range_24h_total < 4.0 and not _near_squeeze_breakout and symbol not in _MAJOR_COINS_SPY:
                                        print(f"   🚫📊 {symbol}: COIN PLAT — range 24h={_range_24h_total:.1f}% < 4% → aucun potentiel")
                                        continue
                                    coin_1h_bearish = (
                                        (ema9_1h < ema21_1h and mom_3h_coin < -0.3) or  # EMA bearish + déclin 3h
                                        (mom_3h_coin < -1.5) or                         # Forte chute 3h (>1.5%)
                                        (mom_6h_coin < -1.5) or                         # déclin prolongé 6h (cas COS: -1.7% all-day)
                                        # � FIX 16/03: Condition 4 durcir — fall_from_3h_high seul trop sensible
                                        # BUG: ETH avec EMA haussière (+0.04% mom) classifié bearish car 1.06% < 3h high
                                        # FIX: exiger EMA bearish OU déclin 3h confirmé (>-0.5%) en plus du fall
                                        (fall_from_3h_high < -0.7 and not _near_squeeze_breakout and
                                         not near_4h_bottom and
                                         (ema9_1h < ema21_1h or mom_3h_coin < -0.5))  # 🔧 FIX 16/03
                                    )
                                    if coin_1h_bearish:
                                        # 🆕 FIX 10/03 BTC REBOND: Le rebond commence avec 1h metrics encore bearish
                                        # EMA9_1h < EMA21_1h + mom_3h négatif = NORMAL au début d'un retournement!
                                        # Les indicateurs 1h sont RETARDÉS de 1-3h. Discrimination vs TON falling knife:
                                        # near_4h_bottom (vrai fond) + momentum 5min fort positif
                                        _fres_feats_1h = fresh_data.get('features', sig.get('features', {}))
                                        _fresh_mom3_btc = _fres_feats_1h.get('momentum_3', 0)
                                        _fresh_rsi_btc = _fres_feats_1h.get('rsi', 50)
                                        _btc_at_genuine_bottom = (
                                            symbol == 'BTCUSDC' and
                                            near_4h_bottom and           # Prix réellement au fond du creux 4h
                                            mom_3h_coin > -3.0 and       # Déclin 3h pas catastrophique (TON: -6%)
                                            _fresh_mom3_btc > 0.20 and   # Rebond 5min confirmé (TON avait 0.05%)
                                            _fresh_rsi_btc > 28           # Pas en free-fall extrême
                                        )
                                        # 🔴 FIX TLM 18h38: ALTCOIN rebond réel au fond du creux 4h
                                        # Conditions PLUS STRICTES que BTC (altcoins = falling knife plus dangereux)
                                        # TLM 18h38: near_4h_bottom=True, RSI<35, Mom5m>+0.4% → doit passer ✓
                                        # TLM 19h04: near_4h_bottom=False (1% au-dessus du creux) → bloqué ✓
                                        _altcoin_at_genuine_bottom = (
                                            symbol != 'BTCUSDC' and
                                            near_4h_bottom and              # Prix AU FOND du creux 4h (<0.5% du low)
                                            _fresh_rsi_btc < 38 and         # RSI en VRAIE survente (≠ RSI=48 TLM 19h04)
                                            _fresh_mom3_btc > 0.40 and      # Rebond 5min FORT — pas micro-bounce
                                            mom_3h_coin > -3.0              # Déclin 3h pas catastrophique
                                        )
                                        # 🔧 FIX 16/03: BULL_STRONG + score élevé → confiance IA suffisante
                                        # En marché haussier, l'IA a déjà validé le setup CREUX → pas besoin de bloquer 1h
                                        # 🔧 FIX BUG 16/03b: self.current_regime reste 'NEUTRAL' avant le 1er buy
                                        # → utiliser ai_predictor._get_market_regime() qui est toujours à jour
                                        _sig_score_1h = sig.get('score', 0)
                                        _is_bull_strong_override = False
                                        try:
                                            _is_bull_strong_override = self.ai_predictor._get_market_regime() == 'BULL_STRONG'
                                        except Exception:
                                            _is_bull_strong_override = (hasattr(self, 'current_regime') and self.current_regime == 'BULL_STRONG')
                                        _bull_high_score_override = (
                                            _is_bull_strong_override and      # 🔧 FIX 16/03: UNIQUEMENT en BULL_STRONG
                                            _sig_score_1h >= 55 and           # 🔧 FIX 31/03b: 70→55 — en BULL_STRONG, score IA 55+ suffit (DENT=59 bloqué à 70)
                                            mom_3h_coin > -1.0 and            # 🔧 FIX 31/03: -3.0→-1.0 — chute 3h modérée OK
                                            mom_6h_coin > -3.0 and            # 🔧 FIX 31/03: -5.0→-3.0 — déclin 6h pas trop fort
                                            fall_from_3h_high > -4.0 and      # 🔧 FIX 31/03: -8.0→-4.0 — pas trop loin du high
                                            (near_4h_bottom or not (ema9_1h < ema21_1h and mom_3h_coin < -0.8))  # 🔧 FIX 16/03: NearBot exempt EMA
                                        )
                                        if _btc_at_genuine_bottom:
                                            print(f"   ⭐🕐 {symbol}: BTC rebond au creux — override 1h bearish (mom3h={mom_3h_coin:+.2f}%, mom5m={_fresh_mom3_btc:+.2f}%, RSI={_fresh_rsi_btc:.0f})")
                                        elif _altcoin_at_genuine_bottom:
                                            print(f"   ⭐🕐 {symbol}: CREUX RÉEL — override 1h bearish (bottom 4h ✓, RSI={_fresh_rsi_btc:.0f}<38, Mom5m={_fresh_mom3_btc:+.2f}%>0.40%)")
                                        elif _bull_high_score_override:
                                            print(f"   ⭐📈 {symbol}: SCORE_IA_OVERRIDE — score={_sig_score_1h} ≥ 90 en tendance 1h douteuse (Mom3h={mom_3h_coin:+.2f}%, DepuisHigh={fall_from_3h_high:+.2f}%)")
                                        else:
                                            print(f"   🚫🕐 {symbol}: CREUX rejeté — tendance baissière (EMA<EMA21={ema9_1h<ema21_1h}, Mom3h={mom_3h_coin:+.2f}%, Mom6h={mom_6h_coin:+.2f}%, DepuisHigh={fall_from_3h_high:+.2f}%, NearBot={near_4h_bottom}, Squeeze={_near_squeeze_breakout}[{_1h_range_4h:.1f}%])")
                                            print(f"      → Essoufflement post-pump ou déclin continu détecté")
                                            continue
                                    else:
                                        print(f"   ✅🕐 {symbol}: Tendance 1h OK pour CREUX (Mom3h={mom_3h_coin:+.2f}%, Mom6h={mom_6h_coin:+.2f}%, DepuisHigh={fall_from_3h_high:+.2f}%, NearBot={near_4h_bottom}, Squeeze={_near_squeeze_breakout}[{_1h_range_4h:.1f}%])")
                            except Exception as e:
                                print(f"   ⚠️ {symbol}: Vérif 1h indisponible ({e}) → confiance 5min seulement")

                        # 🆕 FIX 09/03 v3: CHECK 5min DIRECT POST-PUMP sur signal CACHé
                        # Problème: si fresh_pattern=NEUTRAL (IA bloque), le signal CACHé CREUX_REBOUND survit
                        # Solution: vérifier directement les bougies 5min pour l'exhaustion
                        if pattern in ['CREUX_REBOUND', 'PULLBACK']:
                            try:
                                klines_5m_pp = self.client.get_klines_production(symbol, '5m', 30, cache_ttl=20)  # 🔧 PERF: cache 20s (bougies fermées ne changent pas)
                                if klines_5m_pp and len(klines_5m_pp) >= 24:
                                    c5m = [float(k[4]) for k in klines_5m_pp]  # closes
                                    h5m = [float(k[2]) for k in klines_5m_pp]  # highs
                                    peak_5m = max(h5m[-24:])  # pic dans les 120 dernières min (🔴 DURCI 10/03: 90→120min)
                                    drop_5m = (c5m[-1] - peak_5m) / peak_5m * 100 if peak_5m > 0 else 0
                                    prior_low_5m = min(c5m[-26:-6]) if len(c5m) >= 26 else min(c5m[:-6] if len(c5m) > 6 else c5m)
                                    pump_amp_5m = (peak_5m - prior_low_5m) / prior_low_5m * 100 if prior_low_5m > 0 else 0
                                    mom5_5m = (c5m[-1] - c5m[-6]) / c5m[-6] * 100 if len(c5m) >= 6 else 0
                                    # 🆕 FIX 09/03 v4: DISCRIMINATEUR "AU FOND" — si prix près du LOW 5min,
                                    # c'est un VRAI creux, pas un essoufflement post-pump.
                                    # Ex BTC 14h30: prix ≈ min des 90min → is_at_bottom_5m=True → pas bloqué ✓
                                    # Ex AUDIO: prix ≠ bottom (déjà monté puis descend) → still blocked ✓
                                    bottom_5m = min(c5m[-24:])  # 🔴 DURCI 10/03: fenêtre 120min
                                    is_at_bottom_5m = (c5m[-1] - bottom_5m) / max(bottom_5m, 1e-10) * 100 < 0.3
                                    # 🔧 FIX 16/03 v2: REBOND DEPUIS LE FOND — si prix a déjà rebondi >2% du fond,
                                    # c'est une VRAIE REPRISE, pas un essoufflement post-pump à bloquer.
                                    # BUG FET 19:14 → fond=0.210, cours=0.217 → recovery=+3.2% → CREUX_REBOUND
                                    # légitime bloqué à tort car drop_5m=-4.59% depuis le PIC (pas depuis le fond)
                                    recovery_from_bottom_5m = (c5m[-1] - bottom_5m) / max(bottom_5m, 1e-10) * 100
                                    is_recovering_from_bottom = recovery_from_bottom_5m > 2.0
                                    # 🔧 FIX 13/03: En BULL_STRONG, une consolidation post-pump est NORMALE
                                    # BUG: drop -0.7% après un rally marché = tous les signaux bloqués
                                    # BULL_STRONG: n'annuler que si la chute EST forte (-1.5%) ou mom5 vraiment négatif (-0.3%)
                                    _is_bull_strong_now = False
                                    try:
                                        _is_bull_strong_now = self.ai_predictor._get_market_regime() in ('BULL_STRONG', 'BULL_WEAK')
                                    except Exception:
                                        pass
                                    # ℹ️ INFO 5min — affichage indicateurs sans blocage heuristique
                                    _vol_ratio_info = 0.0
                                    try:
                                        o5m_i = [float(k[1]) for k in klines_5m_pp]
                                        v5m_i = [float(k[5]) for k in klines_5m_pp]
                                        _buy_4c = sum(v5m_i[-4+i] for i in range(4) if c5m[-4+i] >= o5m_i[-4+i])
                                        _sell_4c = sum(v5m_i[-4+i] for i in range(4) if c5m[-4+i] < o5m_i[-4+i])
                                        _vol_ratio_info = _sell_4c / max(_buy_4c, 1)
                                    except Exception:
                                        pass
                                    print(f"   ✅⏱ {symbol}: 5min info — drop={drop_5m:+.2f}%, pump={pump_amp_5m:+.2f}%, mom5={mom5_5m:+.2f}%, atBottom={is_at_bottom_5m}, recovery={recovery_from_bottom_5m:+.2f}%, sell/buy={_vol_ratio_info:.1f}x")
                                    
                                    # 🔴 FIX: Guard post-pump CREUX_REBOUND — bloquer si prix hors du fond après pump
                                    # Un VRAI creux = prix AU fond (is_at_bottom) OU reprise >2% depuis fond
                                    # Si pump > 0.5% et prix entre fond et sommet (0.3-2%) = chasse de pump, pas un creux
                                    # BCH 18h01: pump=+0.92%, atBottom=False, recovery=+0.65% → bloqué ✓
                                    # 🔵 FIX 25/03: Exception RSI extrême — RSI≤35 + pump<2% = rebond légitime
                                    # BTC 13:56: RSI=29, pump=+1.12%, recovery=+0.81% → bloqué à tort
                                    # Prudence: pump≥2% ou RSI>35 → toujours bloqué (chasse-pump réelle)
                                    _sig_rsi_antipump = sig.get('features', {}).get('rsi', 50)
                                    _pump_exception_extreme_rsi = (_sig_rsi_antipump <= 35 and pump_amp_5m < 2.0)
                                    # 🔧 FIX 31/03: En BULL_STRONG/BULL_WEAK, les micro-pumps (+0.5-3%) sont
                                    # le mouvement NORMAL du marché. Bloquer seulement les gros pumps (>3%).
                                    # Bug: AVAX score=77, SUN score=68 bloqués alors que pump=+0.74% à +2.71%
                                    # en marché haussier → 0 signaux IA depuis 2h!
                                    _bull_pump_threshold = 3.0 if _is_bull_strong_now else 0.5
                                    if pattern == 'CREUX_REBOUND' and not is_at_bottom_5m and not is_recovering_from_bottom and pump_amp_5m > _bull_pump_threshold:
                                        if _pump_exception_extreme_rsi:
                                            print(f"   ⚡ {symbol}: anti-pump BYPASSÉ — RSI survente={_sig_rsi_antipump:.0f}≤35 + pump modéré={pump_amp_5m:+.2f}%<2% → rebond légitime autorisé")
                                        else:
                                            print(f"   🚫 {symbol}: CREUX_REBOUND BLOQUÉ post-pump — pump={pump_amp_5m:+.2f}% (seuil={_bull_pump_threshold:.1f}%) et hors du fond (recovery={recovery_from_bottom_5m:+.2f}% < 2% seuil) → chasse-pump évité")
                                            continue
                            except Exception as _e5m:
                                print(f"   ⚠️ {symbol}: Vérif 5min post-pump indisponible ({_e5m})")
                        
                        if fresh_data:
                            cache_score = sig.get('score', 0)
                            cache_pattern = sig.get('pattern', 'NEUTRAL')
                            
                            # 🔴 FIX 26/01: Valider fresh_pattern non vide/None
                            if not fresh_pattern or fresh_pattern in ['None', '', 'null']:
                                fresh_pattern = 'NEUTRAL'
                            
                            # 🎯 RÈGLE SIMPLE: Si données fraîches disponibles, les UTILISER!
                            # Pas de tolérance sur cache obsolète, c'est le marché en temps réel qui compte
                            
                            # ⛔ BLOCAGE ABSOLU: Pattern dangereux (crash actif)
                            if fresh_pattern in DANGEROUS_PATTERNS:
                                print(f"   🚫 {symbol}: CRASH ACTIF ({fresh_pattern}) - BLOQUÉ SÉCURITÉ")
                                continue
                            
                            # ⛔ BLOCAGE: Score frais trop bas (< seuil régime)
                            if fresh_score < MIN_SCORE_ABSOLUTE:
                                print(f"   🚫 {symbol}: Score FRAIS={fresh_score} < minimum ({MIN_SCORE_ABSOLUTE}) - BLOQUÉ")
                                continue
                            
                            # ✅ OK: Pattern safe + Score suffisant
                            # → Utiliser les données FRAÎCHES pour la décision (pas le cache)
                            if abs(fresh_score - cache_score) >= 15:
                                print(f"   ⚡ {symbol}: Utilisation score FRAIS={fresh_score} (cache={cache_score} obsolète)")
                            
                            # Écraser le cache avec les données fraîches
                            sig['score'] = fresh_score
                            # � FIX 01/04: TOUJOURS utiliser le pattern FRAIS pour les décisions d'achat
                            # BUG AXS: cached CREUX_REBOUND + fresh NEUTRAL → is_dip_pattern=True → achat en baisse
                            # L'ancien code gardait le pattern caché quand frais=NEUTRAL, mais le pattern caché
                            # représente un état PASSÉ qui n'existe plus. Les stratégies (is_dip_buy, etc.)
                            # utilisent la variable locale `pattern` → DOIT refléter l'état ACTUEL.
                            if fresh_pattern and fresh_pattern != 'NEUTRAL':
                                # 🔴 FIX 09/02: Vérifier blacklist sur le FRESH pattern aussi!
                                try:
                                    from pattern_manager import PatternManager
                                    pm_fresh = PatternManager()
                                    if not pm_fresh.is_pattern_allowed(fresh_pattern):
                                        print(f"   🚫 {symbol}: Fresh pattern {fresh_pattern} BLACKLISTÉ → signal ignoré")
                                        continue
                                except Exception:
                                    pass
                                sig['pattern'] = fresh_pattern
                                pattern = fresh_pattern  # 🔴 FIX 01/04: Mettre à jour la variable locale aussi
                            else:
                                # 🔴 FIX 01/04: Pattern frais=NEUTRAL → forcer NEUTRAL pour les stratégies
                                # L'IA ne détecte PLUS le pattern caché → ne pas l'utiliser pour acheter
                                if pattern != 'NEUTRAL':
                                    print(f"   ⚠️ {symbol}: Pattern caché={pattern} OBSOLÈTE (frais=NEUTRAL) → forcé NEUTRAL pour décisions")
                                pattern = 'NEUTRAL'
                                sig['pattern'] = 'NEUTRAL'
                            sig['features'] = fresh_features
                            
                            # 🔴 FIX COHERENCE: Sync smart_signal/smart_eligible depuis données fraîches
                            # Bug: si l'IA change ACHAT→HOLD entre cache et fresh, le bot utilisait le cache
                            # 🔧 FIX 31/03: NE PAS écraser si HOT_SIGNAL avait smart_signal='ACHAT'
                            # Bug: HOT_SIGNAL a smart_signal='ACHAT' (set dans _on_ai_signal)
                            # mais fresh_data de la watchlist le remplace par 'HOLD'/'NO_BUY'
                            # → tous les signaux IA refusés malgré HOT_SIGNAL!
                            # 🔴 FIX 01/04: Exception — si le pattern frais est NEUTRAL (pattern caché obsolète),
                            # TOUJOURS appliquer le smart_signal frais même si HOT_SIGNAL. 
                            # Un HOT_SIGNAL dont le pattern a disparu = conditions changées = signal obsolète.
                            _was_hot_achat = (sig.get('smart_signal') == 'ACHAT' and sig.get('smart_eligible', False))
                            _pattern_still_valid = (fresh_pattern and fresh_pattern != 'NEUTRAL')
                            _allow_hot_override = (_was_hot_achat and _pattern_still_valid)
                            if 'smart_signal' in fresh_data and not _allow_hot_override:
                                sig['smart_signal'] = fresh_data['smart_signal']
                            if 'smart_eligible' in fresh_data and not _allow_hot_override:
                                sig['smart_eligible'] = fresh_data['smart_eligible']
                            
                            # ═══════════════════════════════════════════════════════════
                            # 🎯 STRATÉGIES D'ACHAT MULTIPLES - COMPLÉMENTAIRES
                            # ═══════════════════════════════════════════════════════════
                            bb_position = fresh_features.get('bb_position', 0.5)
                            ema_diff = fresh_features.get('ema_diff', 0)
                            fresh_rsi = fresh_features.get('rsi', 50)
                            momentum_3 = fresh_features.get('momentum_3', 0)
                            momentum_5 = fresh_features.get('momentum_5', 0)
                            # 🔧 FIX: default=0 provoquait un FAUX "non-bearish" quand la feature était absente.
                            # On déduit la valeur depuis ema_diff si la feature n'est pas dans les données fraîches.
                            ema_trend_bearish_feat = fresh_features.get(
                                'ema_trend_bearish',
                                1 if ema_diff < -0.05 else 0  # EMA9 < EMA21 de plus de 0.05% → bearish
                            )
                            
                            # ══════════════════════════════════════════════════════════
                            # 🔧 FIX 27/02: PROTECTION ANTI-ACHAT EN BAISSE ABSOLUE
                            # INTERDICTION d'acheter quand la tendance est clairement baissière
                            # RSI extrême + EMA bearish = piège à achat (RSI TRAP)
                            # ══════════════════════════════════════════════════════════
                            
                            # 🚫 BLOCAGE #0A: RSI <= 20 + tendance bearish = PIÈGE ABSOLU
                            # RSI 12.8 comme LTC = chute libre, PAS un rebond
                            # 🔧 FIX: <= au lieu de < pour capturer RSI=20.0 exactement (boundary bug)
                            # Exception: momentum_3 > 0.5% = retournement violent confirmé
                            if fresh_rsi <= 20 and ema_trend_bearish_feat == 1:
                                if momentum_3 <= 0.5:  # Pas de retournement violent (>0.5%)
                                    print(f"   🚫🔴 {symbol}: RSI TRAP ABSOLU - RSI={fresh_rsi:.1f} + EMA Bearish → ACHAT INTERDIT")
                                    print(f"      → Momentum={momentum_3:.2f}% insuffisant (besoin >0.5% pour confirmer retournement)")
                                    continue
                                else:
                                    print(f"   ⚡ {symbol}: RSI={fresh_rsi:.1f} extrême MAIS retournement violent Mom3={momentum_3:.2f}% → autorisé")
                            
                            # 🚫 BLOCAGE #0B: RSI < 25 + EMA bearish + momentum négatif = DANGER
                            # Même avec score élevé, acheter en survente profonde en baisse = suicide
                            if fresh_rsi < 25 and ema_trend_bearish_feat == 1 and momentum_3 < 0.3:
                                print(f"   🚫🔴 {symbol}: SURVENTE EN BAISSE - RSI={fresh_rsi:.1f} EMA_bearish Mom3={momentum_3:.2f}% → BLOQUÉ")
                                print(f"      → Acheter en survente dans tendance baissière = piège (besoin Mom3>0.3% min)")
                                continue
                            
                            # 🚫 BLOCAGE #0C: EMA fortement bearish + RSI < 35 + pas de momentum positif
                            # Tendance baissière établie sans signe de retournement
                            if ema_diff < -0.15 and fresh_rsi < 35 and ema_trend_bearish_feat == 1 and momentum_3 < 0.2:
                                print(f"   🚫 {symbol}: TENDANCE BAISSIÈRE FORTE - EMA_diff={ema_diff:.2f}% RSI={fresh_rsi:.1f} → BLOQUÉ")
                                print(f"      → Attendre retournement confirmé (Mom3>0.2% minimum)")
                                continue
                            
                            # ══════════════════════════════════════════════════════════
                            # 🚫 PROTECTION ANTI-FIN-DE-CYCLE (FIX 04/02, amélioré 06/02)
                            # Bloquer les achats quand le marché est en phase descendante
                            # même si un pattern a été détecté plus tôt
                            # ══════════════════════════════════════════════════════════
                            is_end_of_cycle = (
                                momentum_3 < -0.3 and        # Momentum 3min < -0.30% (chute active)
                                momentum_5 < -0.3 and        # 🔧 FIX 06/02: Momentum 5min aussi négatif (confirme la tendance)
                                ema_diff < 0                  # EMA9 < EMA21 (tendance baissière)
                            )
                            
                            # 🔧 FIX 09/02: Exception CREUX_REBOUND — par définition ema_diff < 0 et momentum_5 peut être négatif
                            # Un CREUX achète dans la zone de survente, donc fin-de-cycle ne s'applique pas
                            # Seuls les vrais crashs (momentum_3 < -1.5%) restent bloqués
                            is_dip_exempt = (pattern in ['CREUX_REBOUND', 'PULLBACK'] and momentum_3 > -1.5)
                            
                            if is_end_of_cycle and not is_dip_exempt:
                                pattern_cache = sig.get('pattern', 'NEUTRAL')
                                print(f"   🚫 {symbol}: FIN DE CYCLE détectée - Momentum en chute (Mom3={momentum_3:.2f}% Mom5={momentum_5:.2f}%)")
                                print(f"      → Pattern={pattern_cache} EMA_diff={ema_diff:.2f}% → ACHAT BLOQUÉ")
                                continue
                            
                            # 🔧 FIX 07/02: BAISSE ACTIVE — EMA bearish + momentum négatif = NO BUY
                            # Cas UNI 12h22: EMA Trend Bearish, RSI=48, momentum négatif
                            # mais score cache=90 CREUX_REBOUND → achat en pleine baisse
                            # 🔧 FIX 09/02: EXCEPTION CREUX_REBOUND — un achat de creux a FORCÉMENT
                            # ema_diff < 0 et momentum_5 négatif (c'est la définition d'un creux!)
                            # Seuls les crashs violents (momentum_5 < -0.8%) restent bloqués pour CREUX
                            is_baisse_active = (
                                ema_diff < -0.05 and         # EMA9 < EMA21 (tendance baissière)
                                momentum_5 < -0.1            # Momentum 5min négatif (baisse)
                            )
                            
                            # 🔴 FIX URGENT 09/03: Durcissement de l'exemption CREUX_REBOUND
                            # AVANT: momentum_3 > 0.0 = quasiment zéro accepté
                            # BUG: Un rebond d'1 candle à +0.02% = exempt → achat en pleine baisse!
                            # 🔴 FIX 09/03 v5: Exiger aussi smart_signal=ACHAT (pas POSSIBLE)
                            # BUG SUI: EMA Croisement↑ (bearish_feat=0), POSSIBLE, RSI=42 → creux_exempt=True
                            _sig_is_achat = (sig.get('smart_signal') == 'ACHAT' and sig.get('smart_eligible', False))
                            is_creux_exempt = (
                                pattern in ['CREUX_REBOUND', 'PULLBACK'] and
                                momentum_3 > 0.15 and        # 🔴 DURCI 0.0→0.15%: vrai rebond requis, pas micro-bounce
                                momentum_5 > -0.25 and       # 🔴 DURCI -0.5→-0.25%: pas en chute active
                                fresh_rsi < 40 and           # 🔴 DURCI 45→40: RSI doit être en VRAI creux (40-45 = pullback normal)
                                fresh_rsi > 20 and           # RSI < 20 = JAMAIS exempt (piège absolu)
                                ema_trend_bearish_feat == 0 and  # Interdit en tendance bearish confirmée
                                ema_diff > -0.3 and          # 🔴 DURCI -0.5→-0.3%: EMA pas divergent
                                _sig_is_achat                # 🔴 FIX v5: signal doit être ACHAT confirmé (pas POSSIBLE)
                            ) or (
                                # 🆕 FIX 10/03 BTC: CREUX_REBOUND autorisé même si EMA encore bearish
                                # EMA est un indicateur RETARDÉ: le retournement commence AVANT le croisement
                                # Garde plus stricte que altcoins pour compenser l'absence du filtre EMA
                                symbol == 'BTCUSDC' and
                                pattern in ['CREUX_REBOUND', 'PULLBACK'] and
                                momentum_3 > 0.25 and        # 🔴 PLUS STRICT (0.15→0.25%): BTC doit vraiment partir
                                momentum_5 > -0.20 and       # 5min pas en chute active
                                fresh_rsi < 55 and           # BTC creux légitime: RSI 42-52 (pas 40 strict altcoins)
                                fresh_rsi > 25 and           # Pas en free-fall extrême
                                ema_diff > -0.4 and          # EMA pas trop divergent (pas falling knife à -0.6%+)
                                _sig_is_achat                # smart_signal doit confirmer ACHAT
                            )
                            
                            if is_baisse_active and not is_creux_exempt:
                                print(f"   🚫 {symbol}: BAISSE ACTIVE - EMA_diff={ema_diff:.2f}% Mom5={momentum_5:.2f}% → ACHAT BLOQUÉ")
                                continue
                            elif is_baisse_active and is_creux_exempt:
                                print(f"   ✅ {symbol}: CREUX_REBOUND en zone basse - EMA_diff={ema_diff:.2f}% RSI={fresh_rsi:.0f} → ACHAT AUTORISÉ (creux légitime)")

                            # 🆕 FIX 09/03: FILTRE BTC DIRECTIONNEL POUR CREUX
                            # Même si is_creux_exempt = True, refuser si BTC baisse > 0.3%
                            # 🔧 FIX 16/03: _btc_momentum est en POURCENTAGE (momentum_5 * 100 dans features)
                            # BUG: threshold était -0.003 comme si en décimal → bloquait pour toute chute > 0.003%
                            # FIX: threshold -0.3 (= -0.3% de chute BTC en 5 bougies)
                            if pattern in ['CREUX_REBOUND', 'PULLBACK'] and symbol != 'BTCUSDC':
                                _btc_direc = getattr(AIPredictor, '_btc_momentum', 0)
                                if _btc_direc < -0.3:  # 🔧 FIX 16/03: -0.003→-0.3 (unité %)
                                    print(f"   ⛔🔴 {symbol}: CREUX refusé — BTC en chute ({_btc_direc:+.2f}%)")  # plus de *100
                                    continue

                            # ══════════════════════════════════════════════════════════
                            # 🔴 PATTERNS AUTORISÉS (26/01) - SYNCHRONISÉ avec ai_predictor.py
                            # Après nettoyage: 6 patterns validés uniquement
                            # ══════════════════════════════════════════════════════════
                            
                            # Patterns CREUX (achat en baisse/correction)
                            DIP_PATTERNS = ['CREUX_REBOUND', 'PULLBACK']
                            
                            # Patterns BREAKOUT (achat momentum haussier)
                            # 🔧 FIX 27/02: TREND_CONTINUATION retiré (désactivé dans pattern_manager)
                            BREAKOUT_PATTERNS = ['SQUEEZE_BREAKOUT']
                            
                            # Patterns EXCEPTIONNELS (IA ultra-confiante)
                            EXCEPTIONAL_PATTERNS = ['HIGH_SCORE_OVERRIDE']
                            
                            # Patterns BLOQUÉS (pour référence)
                            BLOCKED_PATTERNS = ['POSSIBLE_BLOCKED']
                            
                            # ══════════════════════════════════════════════════════════
                            # STRATÉGIE 1: CREUX (Achat à la baisse)
                            # ══════════════════════════════════════════════════════════
                            # 🔧 FIX 06/02: Conditions durcies - un vrai CREUX montre des signes de stabilisation
                            # RSI < 45 (pas juste < 60), BB < 0.50 (moitié basse), momentum en stabilisation
                            # 🔧 FIX 09/02: RSI 45→50 — un CREUX_REBOUND détecté à RSI 48 est normal
                            # L'ancien seuil RSI<45 rejetait des creux précoces légitimes
                            # 🔧 FIX 21/03: RSI 50→42 — rétro 582 trades: achats à RSI 43-49 = pas de vrais creux
                            # CREUX réel = prix dans zone BASSE des BB + RSI en survente (<42)
                            # RSI 42-50 = pullback normal, PAS un creux → is_dip_conditions FAUX
                            # Correction couplée: bb_position 0.55→0.45 (prix vraiment bas)
                            is_dip_conditions = (ema_diff < 0.05 and fresh_rsi < 42 and bb_position < 0.45)
                            is_dip_pattern = (pattern in DIP_PATTERNS)
                            momentum_stabilizing = (momentum_3 > -0.3)  # 🔧 FIX 06/02: -0.30% max (était -0.50%)
                            
                            # 🔴 FIX 09/03 v5: CREUX_REBOUND depuis cache DOIT avoir score frais ET smart_signal valide
                            # BUG SUI: pattern=CREUX_REBOUND (cache), fresh_pattern=NEUTRAL, score=50 → is_dip_buy=True
                            # La 2ème condition (is_dip_pattern) passait sans vérif du score frais
                            _fresh_smart_ok = (sig.get('smart_signal') == 'ACHAT' and sig.get('smart_eligible', False))
                            # 🔴 FIX 09/03 v6b: Les DEUX bras exigent smart_signal=ACHAT
                            # BUG résiduel: POSSIBLE score>=65 + is_dip_conditions (EMA slight bearish, RSI<50)
                            # → premier bras True sans _fresh_smart_ok → achat sur POSSIBLE!
                            is_dip_buy = (
                                _fresh_smart_ok and
                                fresh_score >= MIN_SCORE_ABSOLUTE and
                                momentum_stabilizing and
                                (is_dip_conditions or is_dip_pattern)
                            )
                            
                            # ══════════════════════════════════════════════════════════
                            # STRATÉGIE 2: BREAKOUT (Achat momentum haussier)
                            # ══════════════════════════════════════════════════════════
                            # 🔧 FIX 28/02: Exiger base_score >= 80 (avant bonus) au lieu de fresh_score >= 85
                            # Les bonus IA gonflent les scores de +30 à +45 pts
                            # Un base_score=40 + bonus=45 = fresh_score=85 = FAUX breakout
                            is_breakout_pattern = (pattern in BREAKOUT_PATTERNS)
                            _base_score_breakout = fresh_features.get('base_score', fresh_score)
                            is_high_confidence = (_base_score_breakout >= 80 and fresh_score >= 85)
                            has_momentum = (momentum_3 > 0.1)  # 🔧 FIX 28/02: momentum > +0.1% (était > 0)
                            
                            # 🔧 FIX 06/02: BREAKOUT exige un momentum minimal même avec pattern
                            # Un BREAKOUT avec momentum négatif = faux signal
                            is_breakout_buy = (
                                (is_breakout_pattern and momentum_3 > -0.2) or  # Pattern BREAKOUT + pas en chute
                                (is_high_confidence and has_momentum)
                            )
                            
                            # ══════════════════════════════════════════════════════════
                            # STRATÉGIE 3: SCORE EXCEPTIONNEL (IA très confiante)
                            # ══════════════════════════════════════════════════════════
                            # 🔧 FIX 28/02: base_score >= 80 (était 70) + momentum positif obligatoire
                            # Evidence: 19/20 trades perdants avec base_score 40-55 + bonus → score 90+
                            # Un VRAI score exceptionnel = base solide AVANT les bonus
                            base_score = fresh_features.get('base_score', fresh_score)
                            is_exceptional_score = (
                                # 🔴 FIX 10/03 TON: EMA baissière + rebond temporaire (mom>0) = score 90 atteignable!
                                ema_trend_bearish_feat == 0 and  # JAMAIS "exceptionnel" en EMA baissière
                                (
                                    ((fresh_score >= 90 and base_score >= 80) and momentum_3 > 0) or
                                    (pattern in EXCEPTIONAL_PATTERNS and momentum_3 > 0)
                                )
                            ) or (
                                # 🆕 FIX 10/03 BTC REBOND: BTC/ETH peuvent rebondir avec EMA encore bearish
                                # EMA est un indicateur RETARDÉ — le retournement commence AVANT le croisement
                                # Garde: momentum fort (>0.40%) + EMA pas trop divergent (pas falling knife à -0.5%+)
                                symbol == 'BTCUSDC' and
                                fresh_score >= 90 and base_score >= 80 and
                                momentum_3 > 0.40 and            # Vrais rebond fort (TON avait ~0.05%, trop faible)
                                ema_diff > -0.5 and              # EMA pas trop divergent (proche du croisement)
                                fresh_rsi > 28                   # Pas en mode crash extrême (< 28 = CREUX path)
                            )
                            
                            # ══════════════════════════════════════════════════════════
                            # STRATÉGIE 4: NEUTRAL AVEC SCORE ÉLEVÉ (marché BEAR)
                            # ══════════════════════════════════════════════════════════
                            # 🔥 FIX 01/02: Autoriser NEUTRAL avec score >= 80 en BEAR
                            # 🔥 FIX 04/02: Exiger momentum pas trop négatif
                            is_neutral_high_score = (
                                # 🔴 FIX 10/03 TON: NEUTRAL + score=80 en EMA bearish = pas de tendance = dangereux!
                                ema_trend_bearish_feat == 0 and  # NEUTRAL autorisé seulement si EMA haussière
                                pattern == 'NEUTRAL' and 
                                fresh_score >= 80 and 
                                momentum_3 > -0.5
                            ) or (
                                # 🆕 FIX 10/03 BTC: NEUTRAL_HIGH autorisé pour BTC si momentum fort et positif
                                symbol == 'BTCUSDC' and
                                pattern == 'NEUTRAL' and
                                fresh_score >= 85 and             # Seuil plus exigeant (5pts de plus)
                                momentum_3 > 0.35 and             # Mouvement positif confirmé
                                ema_diff > -0.4                   # EMA pas en chute libre
                            )
                            
                            # 🔧 FIX 08/02: STRATÉGIE 5 — Signal validé par smart_criteria
                            # Si ai_predictor.py a donné smart_signal='ACHAT' + smart_eligible=True,
                            # le signal a déjà passé toutes les validations (RSI, EMA, BB, patterns)
                            # 🔧 FIX 28/02: Ajouter base_score >= 60 + momentum positif
                            # Avant: fresh_score >= 65 suffisait → TOUS les signaux passaient
                            _base_score_smart = fresh_features.get('base_score', fresh_score)
                            is_smart_validated = (
                                sig.get('smart_signal') == 'ACHAT' and
                                sig.get('smart_eligible', False) and
                                fresh_score >= 70 and          # 🔧 FIX 28/02: 65→70
                                _base_score_smart >= 60 and     # 🔧 FIX 28/02: NEW — base_score minimum
                                momentum_3 > -0.5             # 🔧 FIX 28/02: > -0.5% (not in freefall)
                            )
                            
                            if not (is_dip_buy or is_breakout_buy or is_exceptional_score or is_neutral_high_score or is_smart_validated):
                                # Aucune stratégie validée - BLOQUER avec raison claire
                                reasons = []
                                if ema_diff >= 0.15 and fresh_rsi >= 60:
                                    reasons.append(f"Ni CREUX (EMA+{ema_diff:.2f}% RSI={fresh_rsi:.0f})")
                                if not is_breakout_pattern and not is_high_confidence:
                                    reasons.append(f"Ni BREAKOUT (pattern={pattern} score={fresh_score})")
                                if fresh_score < 90:
                                    reasons.append(f"Ni EXCEPTIONNEL (score={fresh_score})")
                                
                                # 🔥 FIX 04/02: Indiquer si momentum négatif bloque
                                if momentum_3 <= -0.5:
                                    reasons.append(f"Mom3={momentum_3:.2f}% (chute)")
                                
                                if bb_position > 0.70:
                                    reasons.append(f"BB_pos={bb_position:.2f} trop haut")
                                
                                print(f"   🚫 {symbol}: Aucune stratégie validée - {' + '.join(reasons)}")
                                continue
                            
                            # Au moins une stratégie validée - LOG la stratégie utilisée
                            strategies_used = []
                            if is_dip_buy:
                                strategies_used.append(f"CREUX (EMA{ema_diff:+.2f}% RSI={fresh_rsi:.0f})")
                            if is_breakout_buy:
                                strategies_used.append(f"BREAKOUT ({pattern})")
                            if is_exceptional_score:
                                strategies_used.append(f"EXCEPTIONNEL (score={fresh_score})")
                            if is_neutral_high_score:
                                strategies_used.append(f"NEUTRAL_HIGH (score={fresh_score})")
                            if is_smart_validated:
                                strategies_used.append(f"SMART_VALIDATED (score={fresh_score})")
                            
                            print(f"   ✅ {symbol}: Stratégie(s) validée(s) - {' + '.join(strategies_used)}")
                            
                            # Utiliser les données fraîches
                            score = fresh_score
                            pattern = fresh_pattern
                        else:
                            # 🔴 FIX 09/03 v5: Pas de données fraîches = symbole sorti de la watchlist
                            # Si le symbole n'est plus dans la watchlist, c'est car le score est trop bas.
                            # BLOQUER si le score en cache est < MIN_SCORE_ABSOLUTE
                            score = sig.get('score', 0)
                            pattern = sig.get('pattern', 'NEUTRAL')
                            print(f"   ⚠️ {symbol}: Pas de données fraîches - Utilisation cache (score={score}, pattern={pattern})")
                            
                            # 🆕 Bloquer si score cache insuffisant (symbole sorti de la watchlist car trop faible)
                            if score < MIN_SCORE_ABSOLUTE:
                                print(f"   🚫 {symbol}: Score cache={score} < {MIN_SCORE_ABSOLUTE} ET absent watchlist → BLOQUÉ (qualité insuffisante)")
                                continue
                            
                            # Vérification anti-baisse sur les prix temps réel
                            if symbol in self.prices and len(self.prices[symbol]) >= 30:
                                _prices = list(self.prices[symbol])[-50:]
                                _rsi = TechnicalIndicators.rsi(_prices, 14)
                                _ema9 = TechnicalIndicators.ema(_prices, EMA_SHORT)
                                _ema21 = TechnicalIndicators.ema(_prices, EMA_LONG)
                                _mom3 = (_prices[-1] - _prices[-4]) / _prices[-4] if len(_prices) >= 4 else 0
                                _mom5 = (_prices[-1] - _prices[-6]) / _prices[-6] if len(_prices) >= 6 else 0
                                
                                if _ema9 and _ema21 and _ema21 > 0:
                                    _ema_diff = (_ema9 - _ema21) / _ema21
                                    
                                    # BAISSE ACTIVE: EMA9 < EMA21 + momentum négatif
                                    if _ema_diff < -0.0005 and _mom5 < -0.001:
                                        rsi_str = f"{_rsi:.0f}" if _rsi else "?"
                                        print(f"   🚫 {symbol}: BAISSE ACTIVE (cache) - EMA_diff={_ema_diff:.4f} Mom5={_mom5:.4f} RSI={rsi_str} → BLOQUÉ")
                                        continue
                                    
                                    # FIN DE CYCLE: Momentum très négatif
                                    if _mom3 < -0.003 and _mom5 < -0.003 and _ema_diff < 0:
                                        print(f"   🚫 {symbol}: FIN DE CYCLE (cache) - Mom3={_mom3*100:.2f}% Mom5={_mom5*100:.2f}% → BLOQUÉ")
                                        continue
                        
                        # ══════════════════════════════════════════════════════════
                        # 🛡️ FREQAI: CHECK OUTLIER DETECTION
                        # Protection contre pumps artificiels et flash crashes
                        # ══════════════════════════════════════════════════════════
                        if self.freqai_manager:
                            try:
                                # Récupérer données historiques pour outlier detection
                                if symbol in self.prices and len(self.prices[symbol]) >= 20:
                                    prices_list = list(self.prices[symbol])[-50:]  # 50 dernières bougies
                                    volumes_list = sig.get('features', {}).get('volumes', [])
                                    if not volumes_list and hasattr(self.client, 'get_volumes'):
                                        try:
                                            klines = self.client.get_klines(symbol, DEFAULT_INTERVAL, 50)
                                            volumes_list = [float(k[5]) for k in klines] if klines else []
                                        except:
                                            volumes_list = [1000000] * len(prices_list)  # Fallback
                                    
                                    # Check outlier
                                    should_block, outlier_analysis = self.freqai_manager.should_check_outliers(
                                        symbol=symbol,
                                        prices=prices_list,
                                        volumes=volumes_list if volumes_list else [1000000] * len(prices_list),
                                        rsi=fresh_features.get('rsi', fresh_rsi) if fresh_data else None,
                                        bb_position=fresh_features.get('bb_position', bb_position) if fresh_data else None,
                                        block_on_outlier=True
                                    )
                                    
                                    if should_block and outlier_analysis:
                                        # ℹ️ Outlier détecté — affiché informatif, score IA prédomine
                                        print(f"   ℹ️ {symbol}: Outlier ({outlier_analysis.method}, {outlier_analysis.confidence:.0%}) — score IA={score} prédomine")
                            except Exception as e:
                                print(f"   ⚠️ {symbol}: Erreur FreqAI outlier check: {e}")
                        
                        # Logger les signaux avec score trop bas
                        if score < 45:
                            if self.trade_logger:
                                try:
                                    self.trade_logger.log_signal({
                                        'timestamp': datetime.now(),
                                        'symbol': symbol,
                                        'signal_type': 'BUY',
                                        'ai_score': score,
                                        'pattern': pattern,
                                        'smart_signal': sig.get('smart_signal', 'ACHAT'),
                                        'smart_eligible': sig.get('smart_eligible', True),
                                        'features': sig.get('features', {}),
                                        'dynamic_sltp': sig.get('dynamic_sltp', {}),
                                        'executed': False,
                                        'rejection_reason': f'SCORE_TOO_LOW ({score} < 45)',
                                        'reason': f'📊 Signal {pattern} rejeté (score faible)'
                                    })
                                except Exception:
                                    pass
                            continue
                        
                        # Logger les signaux pour symboles déjà en position
                        if symbol in self.position_manager.positions:
                            if self.trade_logger:
                                try:
                                    self.trade_logger.log_signal({
                                        'timestamp': datetime.now(),
                                        'symbol': symbol,
                                        'signal_type': 'BUY',
                                        'ai_score': score,
                                        'pattern': pattern,
                                        'smart_signal': sig.get('smart_signal', 'ACHAT'),
                                        'smart_eligible': sig.get('smart_eligible', True),
                                        'features': sig.get('features', {}),
                                        'dynamic_sltp': sig.get('dynamic_sltp', {}),
                                        'executed': False,
                                        'rejection_reason': 'ALREADY_IN_POSITION',
                                        'reason': f'♻️ Signal {pattern} rejeté (position existante)'
                                    })
                                except Exception:
                                    pass
                            continue
                        
                        # 🚨 VÉRIFICATION CRITIQUE: smart_eligible doit être True
                        # Cette vérification bloque les achats en tendance BEARISH
                        # 🔴 FIX: Default False/HOLD (safe) — un signal sans validation IA = HOLD
                        smart_eligible = sig.get('smart_eligible', False)
                        smart_signal = sig.get('smart_signal', 'HOLD')
                        
                        if not smart_eligible or smart_signal != 'ACHAT':
                            if self.trade_logger:
                                try:
                                    self.trade_logger.log_signal({
                                        'timestamp': datetime.now(),
                                        'symbol': symbol,
                                        'signal_type': 'BUY',
                                        'ai_score': score,
                                        'pattern': pattern,
                                        'smart_signal': smart_signal,
                                        'smart_eligible': smart_eligible,
                                        'features': sig.get('features', {}),
                                        'dynamic_sltp': sig.get('dynamic_sltp', {}),
                                        'executed': False,
                                        'rejection_reason': f'SMART_BLOCKED (eligible={smart_eligible}, signal={smart_signal})',
                                        'reason': f'🚫 Signal {pattern} rejeté (BEARISH/NON-ÉLIGIBLE)'
                                    })
                                except Exception:
                                    pass
                            print(f"   🚫 {symbol}: Signal BLOQUÉ (smart_eligible={smart_eligible}, signal={smart_signal})")
                            continue
                        
                        # 🔴 NOUVEAU: Blocage patterns dangereux (double vérification)
                        # � FIX: SQUEEZE_BREAKOUT retiré - validé par smart_criteria
                        DANGEROUS_PATTERNS = ['ACTIVE_CRASH', 'PRICE_CORRECTION', 'RSI_TRAP', 'STRONG_DOWNTREND', 'BEARISH_TREND', 'CREUX_TOO_DEEP', 'END_OF_CYCLE', 'HOLD']
                        if pattern in DANGEROUS_PATTERNS:
                            print(f"   🚫 {symbol}: Pattern DANGEREUX bloqué ({pattern})")
                            continue
                        
                        # 🔴 NOUVEAU: Vérifier si le symbole est dans la blacklist LOT_SIZE
                        lot_size_blacklist = getattr(self.position_manager, '_lot_size_blacklist', {})
                        if symbol in lot_size_blacklist:
                            blacklist_time = lot_size_blacklist[symbol].get('timestamp')
                            # Réessayer après 1 heure
                            if blacklist_time and (datetime.now() - blacklist_time).total_seconds() < 3600:
                                continue  # Skip silencieusement
                            else:
                                # Expirée, on peut réessayer
                                del lot_size_blacklist[symbol]
                        
                        # ══════════════════════════════════════════════════════
                        # FILTRE CRITIQUE: Respecter le min_score du régime de marché
                        # 🔧 FIX 07/02: Synchronisé avec MIN_SCORE_ABSOLUTE=75
                        # BEAR (score < 30): min_score = 85
                        # CORRECTION (30 ≤ score < 45): min_score = 80
                        # NEUTRAL (45 ≤ score < 60): min_score = 75
                        # BULL_WEAK (60 ≤ score < 75): min_score = 75
                        # BULL_STRONG (score ≥ 75): min_score = 75
                        # + Modulation RSI: si BTC RSI > 75 → min_score +5
                        # ══════════════════════════════════════════════════════
                        regime_min_score = 65  # 🔧 FIX 08/02: Défaut abaissé — smart_criteria valide déjà la qualité
                        regime_name = "NEUTRAL"
                        
                        if self.market_regime:
                            try:
                                regime_name, regime_details = self.market_regime.get_current_regime()
                                regime_min_score = regime_details.get('min_score', 65)
                            except Exception as e:
                                pass  # Utiliser la valeur par défaut
                        
                        # Vérifier si le score respecte le min_score du régime
                        if score < regime_min_score:
                            if self.trade_logger:
                                try:
                                    self.trade_logger.log_signal({
                                        'timestamp': datetime.now(),
                                        'symbol': symbol,
                                        'signal_type': 'BUY',
                                        'ai_score': score,
                                        'pattern': pattern,
                                        'smart_signal': sig.get('smart_signal', 'ACHAT'),
                                        'smart_eligible': sig.get('smart_eligible', True),
                                        'features': sig.get('features', {}),
                                        'dynamic_sltp': sig.get('dynamic_sltp', {}),
                                        'executed': False,
                                        'rejection_reason': f'REGIME_MIN_SCORE (score={score} < min={regime_min_score}, regime={regime_name})',
                                        'reason': f'🛡️ Score {score} insuffisant pour régime {regime_name} (min: {regime_min_score})'
                                    })
                                except Exception:
                                    pass
                            print(f"   🛡️ {symbol}: Score {score} < {regime_min_score} (régime {regime_name}) - BLOQUÉ")
                            continue
                        
                        # ══════════════════════════════════════════════════════
                        # 🔧 FIX 01/03 v2: FILTRE RÉGIME PAR PATTERN
                        # En dehors du BULL, seuls les patterns "rebond" sont rentables
                        # 🔴 FIX 02/03: Ajouter CROSSOVER, LSTM, EARLY patterns
                        # Ces patterns détectent des RETOURNEMENTS = exactement ce qu'on veut!
                        # ══════════════════════════════════════════════════════
                        # 🔴 FIX COHERENCE: Synchronisé avec BUYABLE_PATTERNS de ai_predictor.py
                        # Avant: 10 patterns fantômes que l'IA n'envoyait jamais
                        REBOUND_PATTERNS = [
                            'CREUX_REBOUND', 'PULLBACK', 'SQUEEZE_BREAKOUT', 'EARLY_BREAKOUT',
                            'CONSOLIDATION_BREAKOUT', 'EMA_BULLISH', 'CROSSOVER_IMMINENT',
                            'VOLUME_REVERSAL', 'RSI_REVERSAL', 'STRONG_UPTREND', 'HIGH_SCORE_OVERRIDE'
                        ]
                        
                        if regime_name == 'NEUTRAL' and pattern not in REBOUND_PATTERNS:
                            # En NEUTRAL: seuls les patterns de rebond sont autorisés
                            # Les autres patterns (PULLBACK, SQUEEZE, TREND_CONTINUATION) perdent en neutral
                            print(f"   🛡️ {symbol}: Pattern {pattern} BLOQUÉ en régime NEUTRAL (seuls rebonds autorisés)")
                            continue
                        
                        if regime_name in ('CORRECTION', 'BEAR') and pattern not in REBOUND_PATTERNS:
                            # En CORRECTION/BEAR: encore plus strict, seuls les creux profonds
                            print(f"   🛡️ {symbol}: Pattern {pattern} BLOQUÉ en régime {regime_name} (seuls rebonds autorisés)")
                            continue
                        
                        if regime_name in ('CORRECTION', 'BEAR') and pattern in REBOUND_PATTERNS and score < 80:
                            # Même les rebonds en CORRECTION/BEAR doivent avoir un score très élevé
                            print(f"   🛡️ {symbol}: Score {score} < 80 pour rebond en {regime_name} - BLOQUÉ")
                            continue
                        
                        # Signal IA avec score >= min_score du régime ET smart_eligible = ACHAT PRIORITAIRE
                        # Les ready_signals sont maintenant doublement validés par ai_predictor
                        # 🔧 FIX 06/02: Score minimum harmonisé avec MIN_SCORE_ABSOLUTE (75)
                        if score >= MIN_SCORE_ABSOLUTE and symbol not in self.position_manager.positions:
                            # 🆕 FIX 06/02: Vérifier cooldown PERTE renforcé (5 min après perte)
                            loss_cooldowns = getattr(self.position_manager, '_last_loss_cooldown', {})
                            if symbol in loss_cooldowns and time.time() < loss_cooldowns[symbol]:
                                remaining = int(loss_cooldowns[symbol] - time.time())
                                print(f"   🕒 {symbol}: Cooldown PERTE actif ({remaining}s restantes) - BLOQUÉ")
                                continue
                            
                            # Vérifier le cooldown standard
                            if symbol in self.last_trade:
                                elapsed = time.time() - self.last_trade[symbol]
                                if elapsed < self.trade_cooldown:
                                    # Logger le signal rejeté (cooldown)
                                    if self.trade_logger:
                                        try:
                                            self.trade_logger.log_signal({
                                                'timestamp': datetime.now(),
                                                'symbol': symbol,
                                                'signal_type': 'BUY',
                                                'ai_score': score,
                                                'pattern': pattern,
                                                'smart_signal': sig.get('smart_signal', 'ACHAT'),
                                                'smart_eligible': sig.get('smart_eligible', True),
                                                'features': sig.get('features', {}),
                                                'dynamic_sltp': sig.get('dynamic_sltp', {}),
                                                'executed': False,
                                                'rejection_reason': f'COOLDOWN ({int(self.trade_cooldown - elapsed)}s restantes)',
                                                'reason': f'🕒 Signal {pattern} rejeté (cooldown)'
                                            })
                                        except Exception:
                                            pass
                                    continue
                            
                            # ══════════════════════════════════════════════════════
                            # VALIDATION ANTI-BAISSE CRITIQUE (ASSOUPLIE)
                            # Bloquer uniquement les situations vraiment dangereuses
                            # Accepter les rebonds en survente (RSI 30-40 + momentum+)
                            # ══════════════════════════════════════════════════════
                            features = sig.get('features', {})
                            rsi = features.get('rsi', 50)
                            ema_trend_bearish = features.get('ema_trend_bearish', 0)
                            ema_slope = features.get('ema_slope', 0)
                            momentum_3 = features.get('momentum_3', 0)
                            momentum_5 = features.get('momentum_5', 0)
                            
                            # VALIDATION ASSOUPLIE - Bloquer uniquement les cas extrêmes
                            is_bearish_signal = False
                            bearish_reasons = []
                            
                            # 🆕 CONDITION 0: Pattern ACTIVE_CRASH = TOUJOURS bloquer !
                            if pattern == 'ACTIVE_CRASH':
                                is_bearish_signal = True
                                bearish_reasons.append(f"ACTIVE_CRASH(Mom3={momentum_3:.1f}%,Mom5={momentum_5:.1f}%)")
                            
                            # CONDITION 1: Crash violent uniquement (RSI < 20 ET momentum < -2%)
                            if rsi < 20 and momentum_5 < -2.0:
                                is_bearish_signal = True
                                bearish_reasons.append(f"Crash_Violent(RSI={rsi:.0f},Mom={momentum_5:.1f}%)")
                            
                            # CONDITION 2: Chute extrême uniquement (pente < -1.0)
                            if ema_slope < -1.0 and rsi < 25:
                                is_bearish_signal = True
                                bearish_reasons.append(f"Chute_Extreme(Slope={ema_slope:.2f})")
                            
                            # 🆕 CONDITION 3: EMA baissière + Score faible = Danger
                            if ema_trend_bearish > 0 and score < 70 and momentum_5 < 0:
                                is_bearish_signal = True
                                bearish_reasons.append(f"EMA_Bearish(Score={score},Mom5={momentum_5*100:.1f}%)")
                            
                            # 🔧 FIX 27/02: CONDITION 3B: RSI EXTRÊME en tendance bearish = TOUJOURS bloquer
                            # Un score élevé ne justifie PAS d'acheter à RSI < 25 en baisse!
                            # Cas LTC: Score=100 mais RSI=12.8 + EMA Bearish = piège absolu
                            if ema_trend_bearish > 0 and rsi < 25:
                                is_bearish_signal = True
                                bearish_reasons.append(f"RSI_TRAP_ABSOLU(RSI={rsi:.0f},EMA_Bearish)")
                            
                            # 🆕 CONDITION 4: CROSSOVER_IMMINENT trop tardif = Opportunité ratée
                            if pattern == 'CROSSOVER_IMMINENT':
                                ema_cross_fresh = features.get('ema_cross_fresh', 0)
                                ema_cross_bullish = features.get('ema_cross_bullish', 0)
                                if ema_cross_fresh == 0 and ema_cross_bullish == 0:
                                    is_bearish_signal = True
                                    bearish_reasons.append(f"CROSSOVER_LATE(cross_fresh={ema_cross_fresh},cross_bull={ema_cross_bullish})")
                                    print(f"   ⏰ {symbol}: CROSSOVER trop tardif - Opportunité ratée (>2min après croisement)")

                            # 🆕 CONDITION 5 (FIX 15/03): DOWNTREND STRUCTUREL
                            # candles_since_bullish_cross > 30 = EMA9 < EMA21 depuis 30+ bougies (≥2.5h en 5min)
                            # Dans ce contexte, "CREUX_REBOUND" est un couteau qui tombe, pas un rebond.
                            # TLMUSDT: 99 bougies sans golden cross → achat illégitime stoppé ici.
                            _candles_cross = int(features.get('candles_since_bullish_cross', 0))
                            # 🔧 FIX 06/04: DOWNTREND_STRUCTURAL override PROPORTIONNEL aux bougies
                            # Bug AIXBT: 99 bougies sans golden cross (8h+ en downtrend) mais override
                            # car BULL_STRONG + score 71 >= 65 → achat illégitime à -1.29%
                            # Solution: seuil de score proportionnel à la durée du downtrend
                            #   30-50 bougies (2.5-4h): score >= 75 en BULL_STRONG
                            #   50-80 bougies (4-6.5h): score >= 82 en BULL_STRONG
                            #   80+ bougies  (6.5h+):   JAMAIS d'override (downtrend structurel)
                            _dt_bull_override = False
                            try:
                                _reg_dt = self.ai_predictor._get_market_regime()
                                if _candles_cross <= 50:
                                    _dt_bull_override = (
                                        (_reg_dt == 'BULL_STRONG' and score >= 75) or
                                        (_reg_dt == 'BULL_WEAK' and score >= 82)
                                    )
                                elif _candles_cross <= 80:
                                    _dt_bull_override = (
                                        (_reg_dt == 'BULL_STRONG' and score >= 82)
                                        # BULL_WEAK: pas d'override au-delà de 50 bougies
                                    )
                                # 80+ bougies: AUCUN override — downtrend structurel confirmé
                            except Exception:
                                pass
                            if (ema_trend_bearish > 0 and _candles_cross > 30
                                    and pattern in ['CREUX_REBOUND', 'PULLBACK']
                                    and not _dt_bull_override):
                                is_bearish_signal = True
                                bearish_reasons.append(
                                    f"DOWNTREND_STRUCTURAL({_candles_cross} bougies sans golden cross)"
                                )
                                print(f"   🚫 {symbol}: DOWNTREND STRUCTUREL — {_candles_cross} bougies sans EMA cross → CREUX impossible")
                            elif _candles_cross > 30 and _dt_bull_override:
                                print(f"   ⭐📈 {symbol}: DOWNTREND_STRUCTURAL ignoré — {_reg_dt} + score={score} ({_candles_cross} bougies)")


                            # 🆕 CONDITION 6 (FIX 15/03): EMA baissière + pente active + RSI non extrême
                            # ema_slope < -0.15 en trend bearish = la baisse CONTINUE, ce n'est pas un plateau.
                            # TLMUSDT: ema_slope=-0.29, rsi=36.8 → baisse active, pas un creux de rebond.
                            # 🔧 FIX 01/04: En BULL_STRONG/BULL_WEAK, un dip avec EMA falling EST le creux
                            # qu'on veut acheter. Bloquer seulement les chutes extrêmes (slope < -0.8).
                            _ema_fall_regime = 'NEUTRAL'
                            try:
                                _ema_fall_regime = self.ai_predictor._get_market_regime()
                            except Exception:
                                pass
                            _ema_fall_threshold = -0.80 if _ema_fall_regime == 'BULL_STRONG' else -0.60 if _ema_fall_regime == 'BULL_WEAK' else -0.15
                            if ema_trend_bearish > 0 and ema_slope < _ema_fall_threshold and rsi < 45:
                                is_bearish_signal = True
                                bearish_reasons.append(
                                    f"EMA_ACTIVELY_FALLING(slope={ema_slope:.2f}<{_ema_fall_threshold}, RSI={rsi:.0f}, regime={_ema_fall_regime})"
                                )

                            # 🆕 CONDITION 7 (FIX 15/03): Prix EN HAUT des Bollinger en tendance bearish ≠ creux
                            # bb_position > 0.70 = prix dans les 30% supérieurs des BB → résistance, pas creux.
                            # PUMPUSDT: bb_position=0.878, ema_trend_bearish=1 → piège classique RSI TRAP.
                            _bb_pos = features.get('bb_position', 0.5)
                            if (ema_trend_bearish > 0 and _bb_pos > 0.70
                                    and pattern in ['CREUX_REBOUND', 'PULLBACK']):
                                is_bearish_signal = True
                                bearish_reasons.append(
                                    f"NOT_A_CREUX(bb_position={_bb_pos:.2f} > 0.70, prix haut + EMA bearish)"
                                )
                                print(f"   🚫 {symbol}: Pas un creux — prix à {_bb_pos:.0%} des BB avec EMA bearish")

                            # Ne PLUS bloquer si score >= 45 (l'IA a validé)
                            
                            if is_bearish_signal and len(bearish_reasons) >= 1:
                                print(f"   🚫 {symbol}: Signal IA REJETÉ - Danger confirmé ({', '.join(bearish_reasons)})")
                                # Logger le signal rejeté (baisse)
                                if self.trade_logger:
                                    try:
                                        bearish_str = ', '.join(bearish_reasons)
                                        self.trade_logger.log_signal({
                                            'timestamp': datetime.now(),
                                            'symbol': symbol,
                                            'signal_type': 'BUY',
                                            'ai_score': score,
                                            'pattern': pattern,
                                            'smart_signal': sig.get('smart_signal', 'ACHAT'),
                                            'smart_eligible': sig.get('smart_eligible', True),
                                            'features': features,
                                            'dynamic_sltp': sig.get('dynamic_sltp', {}),
                                            'executed': False,
                                            'rejection_reason': f'BEARISH_DANGER ({bearish_str})',
                                            'reason': f'🚫 Signal {pattern} rejeté (danger)'
                                        })
                                    except Exception:
                                        pass
                                continue
                            
                            # Signal accepté malgré RSI bas ? Afficher info
                            if rsi < 40 and momentum_5 > 0:
                                print(f"   ⚡ {symbol}: Rebond détecté - RSI={rsi:.0f} + Momentum positif ({momentum_5:.2f})")
                            
                            reason = sig.get('reason', f'IA: {pattern}')
                            
                            ai_buy_candidates.append({
                                'symbol': symbol,
                                'signal': 'BUY',
                                'indicators': {'ai_score': score, 'rsi': rsi, 'pattern': pattern},
                                'reason': f"🤖 IA SIGNAL: {pattern} (Score={score}, RSI={rsi:.0f})",
                                'ai_score': score,
                                'rsi': rsi,
                                'priority': score + (30 if rsi < 30 else 15 if rsi < 40 else 0),
                                'from_ai_service': True
                            })
                except Exception as e:
                    print(f"   ⚠️ ERREUR CRITIQUE dans traitement signaux IA: {e}")
                    import traceback
                    traceback.print_exc()
            
            # Logger les signaux rejetés car max positions atteint
            if self.surveillance_service and current_positions >= max_positions:
                try:
                    ai_status = self.surveillance_service.get_surveillance_status()
                    ready_signals = ai_status.get('ready_signals', [])
                    for sig in ready_signals:
                        if self.trade_logger:
                            try:
                                self.trade_logger.log_signal({
                                    'timestamp': datetime.now(),
                                    'symbol': sig.get('symbol'),
                                    'signal_type': 'BUY',
                                    'ai_score': sig.get('score', 0),
                                    'pattern': sig.get('pattern', 'NEUTRAL'),
                                    'smart_signal': sig.get('smart_signal', 'ACHAT'),
                                    'smart_eligible': sig.get('smart_eligible', True),
                                    'features': sig.get('features', {}),
                                    'dynamic_sltp': sig.get('dynamic_sltp', {}),
                                    'executed': False,
                                    'rejection_reason': f'MAX_POSITIONS ({current_positions}/{max_positions})',
                                    'reason': f'⛔ Signal {sig.get("pattern")} rejeté (limite positions)'
                                })
                            except Exception:
                                pass
                except Exception:
                    pass
            
            # Traiter les signaux IA en PRIORITÉ - ACHATS MULTIPLES
            if ai_buy_candidates:
                ai_buy_candidates.sort(key=lambda x: x['priority'], reverse=True)
                print(f"\n🤖 SIGNAUX IA PRÊTS ({len(ai_buy_candidates)}):")
                for i, cand in enumerate(ai_buy_candidates[:5]):
                    icon = "🔥" if i == 0 else "⚡" if i == 1 else "✨"
                    print(f"   {icon} {cand['symbol']}: Score={cand['ai_score']} Pattern={cand['indicators'].get('pattern')} RSI={cand['rsi']:.0f}")
                
                # 🔴 FIX 04/03: CIRCUIT BREAKER — cluster de stop-losses en 60 min
                # Problème: 4-5 stop-losses en 30 minutes = marché en crash → éviter nouveaux CREUX_REBOUND
                # Solution: si >= 3 SL en 60 min → CREUX_REBOUND suspendu pendant 45 min
                _stoploss_cluster_pause = False
                _sl_count_60m = 0
                try:
                    import json as _json_cb
                    from datetime import datetime as _dt_cb, timedelta as _td_cb
                    _cutoff_cb = _dt_cb.utcnow() - _td_cb(minutes=60)
                    _tlog_path = os.path.join(os.path.dirname(__file__), 'trade_logs', 'trades_log.jsonl')
                    if os.path.exists(_tlog_path):
                        with open(_tlog_path, 'r', encoding='utf-8') as _f_cb:
                            _lines_cb = _f_cb.readlines()[-150:]
                        for _line_cb in _lines_cb:
                            try:
                                _t_cb = _json_cb.loads(_line_cb.strip())
                                if _t_cb.get('reason', '').startswith('stop-loss') and float(_t_cb.get('pnl', 0)) < 0:
                                    _ts_str_cb = str(_t_cb.get('timestamp', ''))[:19]
                                    _ts_cb = _dt_cb.strptime(_ts_str_cb, '%Y-%m-%d %H:%M:%S')
                                    if _ts_cb > _cutoff_cb:
                                        _sl_count_60m += 1
                            except Exception:
                                pass
                    if _sl_count_60m >= 3:
                        _stoploss_cluster_pause = True
                        print(f"\n🛑 CIRCUIT BREAKER ACTIF: {_sl_count_60m} stop-losses en 60 min → CREUX_REBOUND suspendu")
                except Exception as _e_cb:
                    pass  # Circuit breaker non bloquant
                
                # 🔴 FIX 04/03: Max 2 achats par cycle (was 3) — 3 achats simultanés = sur-exposition
                # Problème: 3 SQUEEZE_BREAKOUT en 1 cycle → DENT/ALICE/USUAL tous en SL le même jour
                max_buys_per_cycle = 2
                buys_done = 0
                for best_ai in ai_buy_candidates:
                    # 🔴 FIX 04/03: Bloquer CREUX_REBOUND durant un cluster de stop-losses
                    if _stoploss_cluster_pause and best_ai['indicators'].get('pattern') == 'CREUX_REBOUND':
                        print(f"   🛑 {best_ai['symbol']}: CREUX_REBOUND bloqué (circuit breaker — {_sl_count_60m} SL/60min)")
                        continue
                    if current_positions < max_positions and buys_done < max_buys_per_cycle:
                        if best_ai['symbol'] not in self.position_manager.positions:
                            print(f"\n🤖 ACHAT IA: {best_ai['symbol']} (Score: {best_ai['priority']})")
                            self.execute_signal(best_ai['symbol'], 'BUY', best_ai['indicators'], best_ai['reason'])
                            buys_done += 1
                            current_positions += 1
                    elif current_positions >= max_positions and buys_done < max_buys_per_cycle:
                        # REMPLACEMENT INTELLIGENT: Si limite atteinte, comparer avec la position la plus faible
                        if best_ai['symbol'] not in self.position_manager.positions:
                            weakest_symbol, weakest_score = self._find_weakest_position()
                            if weakest_symbol and best_ai['priority'] > weakest_score + 5:  # Nouveau signal meilleur (+5 au lieu de +10)
                                print(f"\n🔄 REMPLACEMENT INTELLIGENT: {weakest_symbol} (Score={weakest_score:.0f}) → {best_ai['symbol']} (Score={best_ai['priority']:.0f})")
                                # Vendre la position la plus faible
                                self.position_manager.close_position(weakest_symbol, "remplacement-prioritaire")
                                self.last_trade[weakest_symbol] = time.time()
                                current_positions -= 1
                                # Acheter immédiatement le nouveau signal
                                await asyncio.sleep(0.5)  # Petit délai pour la clôture
                                print(f"\n🤖 ACHAT IA (remplacement): {best_ai['symbol']} (Score: {best_ai['priority']})")
                                self.execute_signal(best_ai['symbol'], 'BUY', best_ai['indicators'], best_ai['reason'])
                                buys_done += 1
                                current_positions += 1
                            else:
                                # Signal pas assez meilleur, mettre en file d'attente
                                print(f"   ⏳ {best_ai['symbol']}: En attente (max {current_positions}/{max_positions})")
                                self.pending_signals.append(best_ai)
            
            # ═══════════════════════════════════════════════════════════════
            # ANALYSE TECHNIQUE DÉSACTIVÉE - TOUT PASSE PAR AI_PREDICTOR
            # L'IA gère maintenant 100% des décisions d'achat
            # ═══════════════════════════════════════════════════════════════
            # ANCIEN CODE: Analyse technique redondante supprimée pour éviter
            # les conflits avec les décisions IA. Tous les signaux proviennent
            # maintenant de ai_predictor.get_watchlist() qui combine :
            #   - Smart Criteria (conditions d'entrée optimales)
            #   - AI Advanced Scorer (8 composantes)
            #   - Pattern Recognition (8 patterns)
            #   - Validation multicouche
            #
            # Cette consolidation élimine :
            #   ❌ Double traitement des symboles
            #   ❌ Seuils contradictoires
            #   ❌ Logiques de scoring concurrentes
            #   ✅ Décision unique, cohérente, basée IA
            
            
            # ═══════════════════════════════════════════════════════════════
            # CODE TECHNIQUE SUPPRIMÉ - Décisions 100% via ai_predictor
            # ═══════════════════════════════════════════════════════════════
            # L'ancien code d'analyse technique (EMA, RSI, BB) créait des
            # conflits avec l'IA. Désormais, SEUL ai_predictor.get_watchlist()
            # génère les signaux d'achat après validation multicouche.
            
            # Récapitulatif
            current_positions = len(self.position_manager.positions)
            max_positions = self.get_dynamic_max_positions()
            ai_signals_count = len(ai_buy_candidates) if ai_buy_candidates else 0
            
            print(f"\n💡 Cycle terminé: {ai_signals_count} signaux IA | Positions: {current_positions}/{max_positions}")
            
            # 🔧 FIX 05/04: Nettoyage mémoire périodique pour éviter OOM
            # Le cache klines et l'historique grandissent sans limite → 4GB+ → OOM kill
            _now_cleanup = time.time()
            if not hasattr(self, '_last_mem_cleanup'):
                self._last_mem_cleanup = 0
            if _now_cleanup - self._last_mem_cleanup >= 300:  # Toutes les 5 minutes
                self._last_mem_cleanup = _now_cleanup
                try:
                    # Purger le cache klines expiré
                    if hasattr(self.client, '_klines_cache'):
                        expired = [k for k, t in self.client._klines_cache_time.items()
                                   if _now_cleanup - t > 120]
                        for k in expired:
                            self.client._klines_cache.pop(k, None)
                            self.client._klines_cache_time.pop(k, None)
                    # Limiter trade_history en mémoire (garder les 500 derniers)
                    if len(self.position_manager.trade_history) > 500:
                        self.position_manager.trade_history = self.position_manager.trade_history[-500:]
                    # Purger les dicts de tracking périmés
                    _stale_cutoff = _now_cleanup - 86400  # 24h
                    for attr in ('last_signal', 'last_trade', '_last_loss_cooldown'):
                        d = getattr(self, attr, None)
                        if isinstance(d, dict) and len(d) > 200:
                            stale = [k for k, v in d.items()
                                     if isinstance(v, (int, float)) and v < _stale_cutoff]
                            for k in stale:
                                del d[k]
                    import gc
                    gc.collect()
                except Exception:
                    pass
            
            # 🔧 FIX 30/03: Throttle display_status() à 30s max
            # AVANT: appelé CHAQUE cycle (0.5s) → analyze() + get_klines_production() × 96 symboles
            # = ~192 appels API Binance par cycle = bot lent, signaux expirés avant traitement
            # APRÈS: display_status complet toutes les 30s, résumé léger entre-temps
            _now_disp = time.time()
            if _now_disp - _last_display_time >= 30:
                self.display_status()
                _last_display_time = _now_disp
            
            await asyncio.sleep(0.5)  # Mise à jour ultra-rapide (500ms) pour réactivité maximale
    
    async def check_triggers_loop(self):
        """Boucle dédiée pour vérifier les triggers (rotation, sell_all) toutes les 2 secondes"""
        try:
            print("   🔍 Vérificateur de triggers démarré")
            
            while self.running:
                try:
                    # ═══════════════════════════════════════════════════════════════
                    # RELOAD PAUSE depuis le fichier (écrit par routes.py ou check_triggers_loop)
                    # ═══════════════════════════════════════════════════════════════
                    try:
                        pause_file_path = os.path.join(SCRIPT_DIR, 'trading_pause.json')
                        if os.path.exists(pause_file_path):
                            with open(pause_file_path, 'r') as f:
                                pause_data = json.load(f)
                            disk_pause = pause_data.get('paused_until', 0)
                            # Si le fichier a une pause plus récente que la mémoire, l'appliquer
                            if isinstance(disk_pause, (int, float)) and disk_pause > self.trading_paused_until:
                                self.trading_paused_until = disk_pause
                                remaining = int(disk_pause - time.time())
                                if remaining > 0:
                                    print(f"   ⏸️ PAUSE DÉTECTÉE depuis fichier: {remaining//60}min {remaining%60}s restants")
                    except Exception:
                        pass
                    
                    # ═══════════════════════════════════════════════════════════════
                    # VÉRIFICATION ROTATION MANUELLE depuis le Dashboard
                    # ═══════════════════════════════════════════════════════════════
                    rotation_trigger_file = os.path.join(SCRIPT_DIR, 'rotation_trigger.json')
                    if os.path.exists(rotation_trigger_file):
                        print(f"   🔔 Trigger rotation détecté!")
                        try:
                            with open(rotation_trigger_file, 'r', encoding='utf-8') as f:
                                trigger = json.load(f)
                            
                            sell_symbol = trigger.get('sell_symbol')
                            buy_symbol = trigger.get('buy_symbol')
                            is_manual = trigger.get('manual', False)
                            
                            if sell_symbol and buy_symbol and is_manual:
                                # VÉRIFIER LA PAUSE GLOBALE
                                if self.trading_paused_until > time.time():
                                    remaining = int(self.trading_paused_until - time.time())
                                    minutes = remaining // 60
                                    seconds = remaining % 60
                                    print(f"\n⏸️ ROTATION BLOQUÉE - Trading en pause: {minutes}m {seconds}s restants")
                                    # Supprimer le fichier pour ne pas réessayer
                                    try:
                                        os.remove(rotation_trigger_file)
                                    except:
                                        pass
                                    continue  # Passer au cycle suivant
                                
                                print(f"\n🔄 ROTATION MANUELLE reçue du Dashboard!")
                                print(f"   📤 Vendre: {sell_symbol}")
                                print(f"   📥 Acheter: {buy_symbol}")
                                
                                # Vérifier que la position à vendre existe
                                if sell_symbol in self.position_manager.positions:
                                    # Créer une rotation manuelle pour exécution
                                    manual_rotation = {
                                        'sell_symbol': sell_symbol,
                                        'buy_symbol': buy_symbol,
                                        'sell_score': 100,
                                        'buy_score': 100,
                                        'advantage': 100,
                                        'manual': True,
                                        'sell_reasons': ['Rotation manuelle Dashboard'],
                                        'buy_reasons': ['Rotation manuelle Dashboard']
                                    }
                                    
                                    if SMART_ROTATION_AVAILABLE:
                                        rotation_mgr = get_smart_rotation()
                                        
                                        # Callback pour exécuter les ordres
                                        def execute_order(symbol, action, reason):
                                            if action == 'SELL':
                                                result = self.position_manager.close_position(symbol, reason)
                                                if result:
                                                    print(f"      ✅ Vente {symbol} exécutée")
                                                return result
                                            elif action == 'BUY':
                                                # Calculer le montant à investir
                                                account = self.client.get_account()
                                                if not account:
                                                    return False
                                                free_usdt = float(account['balances'][self.usdt_idx]['free'])
                                                max_positions = self.get_dynamic_max_positions()
                                                order_amount = free_usdt / max_positions if max_positions > 0 else 0
                                                
                                                # Ouvrir la position
                                                # 🔥 FIX 28/01: Passer pattern 'MANUAL_ROTATION' au lieu de 'UNKNOWN'
                                                result = self.position_manager.open_position(symbol, "BUY", order_amount, 
                                                                                             pattern='MANUAL_ROTATION')
                                                if result:
                                                    print(f"      ✅ Achat {symbol} exécuté")
                                                return result
                                            return False
                                        
                                        # Exécuter la rotation
                                        result = rotation_mgr.execute_rotation(manual_rotation, execute_order)
                                        
                                        if result.get('success'):
                                            print(f"   ✅ ROTATION MANUELLE RÉUSSIE!")
                                        else:
                                            print(f"   ❌ Rotation échouée: {result.get('reason', 'Erreur inconnue')}")
                                    else:
                                        print(f"   ⚠️ Module SmartRotation non disponible")
                                else:
                                    print(f"   ⚠️ {sell_symbol} n'est pas en position!")
                            
                            # Supprimer le fichier trigger après traitement
                            os.remove(rotation_trigger_file)
                            print(f"   🗑️ Trigger rotation traité et supprimé")
                            
                        except Exception as e:
                            print(f"   ⚠️ Erreur traitement rotation trigger: {e}")
                            try:
                                os.remove(rotation_trigger_file)
                            except:
                                pass
                    
                    # ═══════════════════════════════════════════════════════════════
                    # SYNC DISK → MÉMOIRE (détecte si positions.json a été vidé externement)
                    # 🔴 FIX COHERENCE: Utilise aussi le mtime du fichier pour détecter
                    # les modifications externes, au lieu de seulement comparer le contenu
                    # ═══════════════════════════════════════════════════════════════
                    try:
                        disk_positions_file = os.path.join(SCRIPT_DIR, 'positions.json')
                        if os.path.exists(disk_positions_file):
                            with open(disk_positions_file, 'r') as f:
                                disk_positions = json.load(f)
                        else:
                            disk_positions = {}
                        
                        mem_count = len(self.position_manager.positions)
                        disk_count = len(disk_positions)
                        
                        # 🔴 FIX: Vérifier aussi sell_all_signal.json comme signal de force-clear
                        sell_signal_file = os.path.join(SCRIPT_DIR, 'sell_all_signal.json')
                        force_clear = False
                        if os.path.exists(sell_signal_file):
                            # sell_all_signal.json existe = sell_all.py a été exécuté
                            # Même si positions.json a été réécrit par _save_positions, on force le clear
                            force_clear = True
                            print(f"   🚨 SELL_ALL_SIGNAL détecté dans check_triggers → force clear mémoire")
                        
                        # Si le disque a MOINS de positions que la mémoire → sync externe (sell-all, etc.)
                        if disk_count < mem_count or force_clear:
                            removed_symbols = [s for s in self.position_manager.positions if s not in disk_positions]
                            # En mode force_clear, si positions.json a été réécrit, virer TOUTES les positions
                            if force_clear and not removed_symbols and mem_count > 0:
                                removed_symbols = list(self.position_manager.positions.keys())
                            if removed_symbols:
                                print(f"   🔄 SYNC DISK: {mem_count} positions en mémoire, {disk_count} sur disque")
                                print(f"   🔄 Positions supprimées externement: {removed_symbols}")
                                for s in removed_symbols:
                                    if s in self.position_manager.positions:
                                        del self.position_manager.positions[s]
                                    self.last_trade[s] = time.time() + 300  # Cooldown 5 min
                                # 🔴 FIX: Sauvegarder immédiatement pour que positions.json reflète la réalité
                                self.position_manager._save_positions()
                                print(f"   ✅ Mémoire synchronisée: {len(self.position_manager.positions)} positions")
                            # Nettoyer le signal après traitement
                            if force_clear and os.path.exists(sell_signal_file):
                                try:
                                    os.remove(sell_signal_file)
                                    print(f"   🗑️ sell_all_signal.json supprimé (traité par disk sync)")
                                except:
                                    pass
                    except Exception as e:
                        pass  # Ne pas bloquer la boucle pour un check de sécurité
                    
                    # ═══════════════════════════════════════════════════════════════
                    # VÉRIFICATION SELL ALL depuis le Dashboard
                    # ═══════════════════════════════════════════════════════════════
                    sell_all_file = os.path.join(SCRIPT_DIR, 'sell_all_signal.json')
                    if os.path.exists(sell_all_file):
                        try:
                            with open(sell_all_file, 'r') as f:
                                signal = json.load(f)
                            
                                action = signal.get('action', '')
                                # Accepter SELL_ALL et SELL_ALL_COOLDOWN
                                if action in ['SELL_ALL', 'SELL_ALL_COOLDOWN']:
                                    print(f"\n🚨 SIGNAL VENDRE TOUT reçu du Dashboard! (action={action})")
                                    
                                    # PRIORITÉ 1: Utiliser les symbols du signal (dashboard connaît les vraies positions)
                                    positions_to_sell = signal.get('symbols', [])
                                    
                                    # PRIORITÉ 2: Si pas de symbols dans signal, lire positions.json
                                    if not positions_to_sell:
                                        positions_to_sell = list(self.position_manager.positions.keys())
                                    
                                    # PRIORITÉ 3: Vérifier réellement sur Binance en dernier recours
                                    if not positions_to_sell:
                                        print("   ⚠️ Aucune position dans signal/positions.json, vérification Binance...")
                                        try:
                                            account = self.client.get_account()
                                            for balance in account['balances']:
                                                asset = balance['asset']
                                                if asset in ['USDT', 'BUSD', 'USDC', 'DAI']:
                                                    continue
                                                free = float(balance['free'])
                                                locked = float(balance['locked'])
                                                if free + locked > 0:
                                                    symbol = f"{asset}USDC"
                                                    positions_to_sell.append(symbol)
                                                    print(f"      Détecté sur Binance: {symbol} ({free+locked:.8f})")
                                        except Exception as e:
                                            print(f"   ❌ Erreur vérification Binance: {e}")
                                    
                                    if positions_to_sell:
                                        print(f"   📋 Vente de {len(positions_to_sell)} positions: {positions_to_sell}")
                                        
                                        for symbol in positions_to_sell:
                                            try:
                                                result = self.position_manager.close_position(symbol, "manual_sell_all")
                                                if result:
                                                    print(f"   ✅ {symbol} vendu")
                                                    # Ajouter un cooldown long sur ce symbole
                                                    self.last_trade[symbol] = time.time() + 300  # 5 minutes de cooldown
                                                    # Retirer immédiatement de positions.json
                                                    if symbol in self.position_manager.positions:
                                                        del self.position_manager.positions[symbol]
                                                        self.position_manager._save_positions()
                                                else:
                                                    print(f"   ❌ {symbol} échec de vente")
                                                    # Forcer la suppression de la position du dictionnaire
                                                    # car échec = souvent "insufficient balance" = plus rien à vendre
                                                    if symbol in self.position_manager.positions:
                                                        del self.position_manager.positions[symbol]
                                                        self.position_manager._save_positions()
                                                        print(f"   🗑️ {symbol} retiré du dictionnaire des positions")
                                            except Exception as e:
                                                print(f"   ❌ {symbol} erreur: {e}")
                                                # En cas d'erreur, aussi retirer la position
                                                if symbol in self.position_manager.positions:
                                                    del self.position_manager.positions[symbol]
                                                    self.position_manager._save_positions()
                                                    print(f"   🗑️ {symbol} retiré du dictionnaire (erreur)")
                                        
                                        # 🔴 FIX 22/01: VIDER COMPLÈTEMENT positions après vente totale
                                        # Sauvegarder une dernière fois pour être sûr
                                        self.position_manager._save_positions()
                                        positions_left = len(self.position_manager.positions)
                                        
                                        # Si toutes les positions vendues, vider le fichier complètement
                                        if positions_left == 0:
                                            # Forcer l'écriture d'un fichier vide
                                            with open(self.position_manager.POSITIONS_FILE, 'w') as f:
                                                json.dump({}, f, indent=2)
                                            print(f"   📊 Vente terminée! positions.json vidé complètement")
                                        else:
                                            print(f"   📊 Vente terminée! ({positions_left} positions restantes dans positions.json)")
                                    else:
                                        print(f"   ℹ️ Aucune position ouverte à vendre")
                                    
                                    # PAUSE GLOBALE DE 5 MINUTES APRÈS SELL_ALL (éviter rachats immédiats)
                                    self.trading_paused_until = time.time() + 300  # 5 minutes
                                    
                                    # SAUVEGARDER LA PAUSE dans un fichier (persistence entre redémarrages)
                                    pause_file = os.path.join(SCRIPT_DIR, 'trading_pause.json')
                                    try:
                                        with open(pause_file, 'w') as f:
                                            json.dump({'paused_until': self.trading_paused_until, 
                                                      'reason': 'SELL_ALL', 
                                                      'timestamp': datetime.now().isoformat()}, f)
                                    except Exception as e:
                                        print(f"   ⚠️ Impossible de sauvegarder pause: {e}")
                                    
                                    pause_until_str = datetime.fromtimestamp(self.trading_paused_until).strftime('%H:%M:%S')
                                    print(f"   ⏸️ TRADING EN PAUSE jusqu'à {pause_until_str} (5 minutes)")
                                    print(f"   🔒 Aucun achat ne sera effectué pendant cette période")
                                    print(f"   💾 Pause sauvegardée (active même après redémarrage)")
                                    
                                    # Supprimer le fichier avec retry
                                    for attempt in range(3):
                                        try:
                                            if os.path.exists(sell_all_file):
                                                os.remove(sell_all_file)
                                                print(f"   🗑️ Signal traité et supprimé")
                                            break
                                        except PermissionError:
                                            if attempt < 2:
                                                await asyncio.sleep(0.5)
                                            else:
                                                print(f"   ⚠️ Impossible de supprimer le fichier signal (verrouillé)")
                        
                        except Exception as e:
                            print(f"   ⚠️ Erreur traitement sell_all: {e}")
                            # Tenter de supprimer même en cas d'erreur
                            for attempt in range(2):
                                try:
                                    if os.path.exists(sell_all_file):
                                        os.remove(sell_all_file)
                                    break
                                except:
                                    if attempt < 1:
                                        await asyncio.sleep(0.3)

                    # ═══════════════════════════════════════════════════════════════
                    # 🔴 FIX: RÉDUCTION PROACTIVE DES POSITIONS EN BEAR/CORRECTION
                    # Ferme immédiatement les positions perdantes quand le régime
                    # bascule en BEAR — sans attendre le SL naturel (~2-3% de plus)
                    # Throttle: vérification toutes les 30s max
                    # ═══════════════════════════════════════════════════════════════
                    _now_br = time.time()
                    if not hasattr(self, '_last_bear_reduction_check'):
                        self._last_bear_reduction_check = 0.0
                    if _now_br - self._last_bear_reduction_check >= 30:
                        self._last_bear_reduction_check = _now_br
                        try:
                            if self.market_regime and len(self.position_manager.positions) > 0:
                                _regime_now, _ = self.market_regime.get_current_regime()
                                if _regime_now in ('BEAR', 'CORRECTION'):
                                    _bear_closures = []
                                    for _sym, _pos in list(self.position_manager.positions.items()):
                                        try:
                                            _price_now = self.client.get_price(_sym)
                                            if _price_now and _pos.get('entry_price', 0) > 0:
                                                _pnl_pct = ((_price_now / _pos['entry_price']) - 1) * 100
                                                if _pnl_pct < -1.5:
                                                    _bear_closures.append((_sym, _pnl_pct))
                                        except Exception:
                                            pass
                                    if _bear_closures:
                                        print(f"\n🐻 RÉDUCTION PROACTIVE ({_regime_now}): {len(_bear_closures)} position(s) en perte")
                                        for _sym, _pnl in _bear_closures:
                                            print(f"   ↩️  {_sym}: PnL={_pnl:.2f}% → clôture immédiate")
                                            try:
                                                self.position_manager.close_position(_sym, f"bear-proactive ({_regime_now})")
                                            except Exception as _e_close:
                                                print(f"   ⚠️ Erreur clôture {_sym}: {_e_close}")
                        except Exception as _e_br:
                            pass  # Non bloquant

                except Exception as e:
                    print(f"   ⚠️ Erreur dans check_triggers_loop: {e}")
                
                # Attendre 2 secondes avant la prochaine vérification
                await asyncio.sleep(2)
        
        except Exception as fatal_error:
            print(f"   ❌ ERREUR FATALE dans check_triggers_loop: {fatal_error}")
            import traceback
            traceback.print_exc()
    
    async def run(self):
        """Lance le bot"""
        print("\n🚀 Démarrage du Bot de Trading...")
        print(f"   🔍 Trade Logger status: {'✅ Actif' if self.trade_logger else '❌ Non disponible'}")
        
        # Vérifier les clés API
        if not BINANCE_API_KEY or not BINANCE_API_SECRET:
            print("\n❌ ERREUR: Clés API non configurées!")
            print("   Édite config.py et ajoute tes clés Binance")
            print("   Pour le testnet: https://testnet.binance.vision/")
            return
        
        # Test connexion avec retry (testnet peut être temporairement down)
        account = None
        _degraded_mode = False
        for _retry in range(5):
            account = self.client.get_account()
            if account:
                break
            wait_sec = 15 * (_retry + 1)
            print(f"   ⚠️ Connexion Binance échouée (tentative {_retry+1}/5), retry dans {wait_sec}s...")
            await asyncio.sleep(wait_sec)
        if not account:
            print("   ⚠️ Impossible de se connecter au compte Binance après 5 tentatives")
            print("   🔄 Démarrage en MODE DÉGRADÉ (surveillance IA active, trading désactivé)")
            _degraded_mode = True
        else:
            print("   ✅ Connexion réussie!")
        
        if not _degraded_mode:
            # Vérifier et nettoyer les positions au démarrage
            self.verify_and_cleanup_positions()
        
        # Charger l'historique
        self.load_historical_data()
        
        # Démarrer le service de surveillance IA
        print(f"   🔍 Vérification surveillance: service={self.surveillance_service is not None}")
        if self.surveillance_service:
            symbols_count = len(self.surveillance_service.symbols) if hasattr(self.surveillance_service, 'symbols') else 0
            print(f"   🔍 Démarrage surveillance avec {symbols_count} symboles...")
            self.surveillance_service.start()
            print("   ✅ Surveillance IA démarrée")
        else:
            print("   ⚠️ Pas de service de surveillance disponible!")
        
        self.running = True
        
        # En mode testnet, optimiser: une seule tâche pour tous les prix
        if TESTNET_MODE:
            print("   ⚡ Mode TESTNET optimisé (API REST au lieu de WebSocket)")
            print("   📋 Création de 2 tâches: trading_loop + check_triggers_loop")
            print(f"   🔍 DEBUG: self.running = {self.running}")
            print("   🔍 DEBUG: Création task 1 (trading_loop)...")
            task1 = asyncio.create_task(self.trading_loop())
            print("   🔍 DEBUG: Création task 2 (check_triggers_loop)...")
            task2 = asyncio.create_task(self.check_triggers_loop())
            tasks = [task1, task2]
            print(f"   ✅ Tâches créées: {len(tasks)}")
        else:
            # Mode production: WebSocket pour chaque crypto
            tasks = [
                asyncio.create_task(self.price_updater(s)) for s in self.watch_symbols
            ]
            tasks.append(asyncio.create_task(self.trading_loop()))
            tasks.append(asyncio.create_task(self.check_triggers_loop()))  # Vérification triggers parallèle
        
        try:
            print(f"   ▶️ Lancement de {len(tasks)} tâche(s) asynchrone(s)...")
            await asyncio.gather(*tasks)
        except asyncio.CancelledError:
            print("   ⚠️ Tâches annulées")
            pass
        except Exception as e:
            print(f"   ❌ Erreur dans la boucle principale: {e}")
            import traceback
            traceback.print_exc()
        finally:
            self.running = False

def main():
    # Le logging fichier est déjà configuré en début de fichier (FileLogger)
    # Ne pas toucher à sys.stdout ici
    
    print("""
╔══════════════════════════════════════════════════════════════════════╗
║           🤖 CRYPTO TRADING BOT - ORDRES AUTOMATIQUES 🤖             ║
╠══════════════════════════════════════════════════════════════════════╣
║  ⚠️  Ce bot peut passer des ordres RÉELS !                          ║
║  🧪 Mode TESTNET activé par défaut (argent fictif)                  ║
║  📊 Stratégie: RSI + EMA + Bollinger Bands                          ║
╚══════════════════════════════════════════════════════════════════════╝
    """)
    
    if not TESTNET_MODE:
        print("⚠️  ATTENTION: Mode PRODUCTION activé!")
        confirm = input("   Confirmer ? (oui/non): ")
        if confirm.lower() != "oui":
            print("   Annulé.")
            return

    # Tuer toute instance précédente avant de démarrer (évite double bot watchdog+manuel)
    pid = os.getpid()
    pid_file = os.path.join(SCRIPT_DIR, "bot.pid")
    try:
        import psutil as _psutil
        import time as _t
        killed_any = False
        for proc in _psutil.process_iter(['pid', 'name', 'cmdline']):
            try:
                if proc.pid == pid:
                    continue
                cmdline = ' '.join(proc.info.get('cmdline') or [])
                if 'trading_bot.py' in cmdline and 'python' in proc.info.get('name', '').lower():
                    proc.terminate()
                    print(f"   🛑 Instance précédente terminée (PID {proc.pid})")
                    killed_any = True
            except (_psutil.NoSuchProcess, _psutil.AccessDenied):
                pass
        if killed_any:
            _t.sleep(2)  # Laisser le temps aux processus de se terminer
    except Exception as e:
        print(f"   ⚠️ Nettoyage instances précédentes: {e}")

    # Sauvegarder le PID pour permettre l'arrêt/redémarrage
    try:
        with open(pid_file, 'w') as f:
            f.write(str(pid))
        print(f"   📝 PID sauvegardé: {pid}")
    except Exception as e:
        print(f"   ⚠️  Impossible de sauvegarder le PID: {e}")
    
    print("🔧 [DEBUG] Création de TradingBot()...")
    try:
        bot = TradingBot()
        print("✅ [DEBUG] TradingBot créé avec succès")
    except Exception as e:
        print(f"❌ [DEBUG] Erreur création TradingBot: {e}")
        import traceback
        traceback.print_exc()
        return
    
    print("🔧 [DEBUG] Lancement asyncio.run()...")
    try:
        asyncio.run(bot.run())
    except KeyboardInterrupt:
        print("\n\n👋 Bot arrêté.")
    finally:
        # Nettoyer le fichier PID à l'arrêt
        try:
            if os.path.exists(pid_file):
                os.remove(pid_file)
                print("   🗑️  PID nettoyé")
        except:
            pass

if __name__ == "__main__":
    main()
