#!/usr/bin/env python3
"""
═══════════════════════════════════════════════════════════════════════════════
🕵️ MARKET SPY - Scanner indépendant de tout le marché crypto
═══════════════════════════════════════════════════════════════════════════════

Script espion qui balaye TOUTES les paires USDT du marché Binance
(pas seulement les 58 de la watchlist) pour détecter des opportunités
exceptionnelles en temps réel.

FONCTIONNEMENT:
  1. Scan rapide de TOUTES les paires USDT (1 seul appel API)
  2. Filtrage intelligent: volume, variation, exclusion watchlist
  3. Analyse technique approfondie des meilleurs candidats (RSI, BB, momentum)
  4. Scoring et détection de patterns (CREUX, SQUEEZE, BREAKOUT)
  5. Achat automatique si opportunité exceptionnelle (score ≥ 85)
  6. Coordination avec le bot principal via positions.json

USAGE:
  python market_spy.py                 # Mode scan continu (défaut)
  python market_spy.py --once          # Un seul scan puis quitte
  python market_spy.py --dry-run       # Scan sans acheter
  python market_spy.py --min-score 70  # Score minimum personnalisé
  python market_spy.py --top 20        # Analyser top 20 au lieu de 10

Auteur: Bot Trading System
═══════════════════════════════════════════════════════════════════════════════
"""

import os
import sys
import json
import time
import hmac
import hashlib
import argparse
import logging
import traceback
from datetime import datetime, timedelta
from urllib.parse import urlencode

import requests
import numpy as np

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

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

# Charger la config du bot principal
sys.path.insert(0, SCRIPT_DIR)
try:
    from config import (
        BINANCE_API_KEY, BINANCE_API_SECRET, TESTNET_MODE,
        MAX_OPEN_POSITIONS, MIN_ORDER_SIZE, MAX_ORDER_SIZE,
        STOP_LOSS_PERCENT, TAKE_PROFIT_PERCENT,
        TRAILING_STOP_DISTANCE, TRAILING_STOP_ACTIVATION,
        ENABLE_TRAILING_STOP
    )
except ImportError:
    print("❌ Impossible de charger config.py - vérifiez que le fichier existe")
    sys.exit(1)

# --- Paramètres spécifiques au spy ---

# URLs API
PRODUCTION_API = "https://api.binance.com"
TESTNET_API = "https://testnet.binance.vision"
TRADING_API = TESTNET_API if TESTNET_MODE else PRODUCTION_API

# Scan: TOUJOURS sur l'API production (le testnet n'a pas toutes les paires)
SCAN_API = PRODUCTION_API

# ─── FILTRES STRICTS (v2 - haute qualité) ───
# Pré-filtrage ticker 24h
MIN_VOLUME_USDT = 10_000_000     # Volume 24h min en USDT (10M = liquidité réelle)
MIN_PRICE_CHANGE_ABS = 2.0       # Variation 24h minimum en % (absolu)
MAX_PRICE_CHANGE_24H = 15.0      # ANTI-PUMP: exclure >15% hausse 24h (acheter après pump = piège)
MAX_PRICE = 50_000               # Prix max (exclure BTC si trop cher)
MIN_PRICE = 0.001                # Prix min (exclure micro-poussière)
EXCLUDE_STABLECOINS = True       # Exclure BUSD, TUSD, FDUSD, etc.

# Analyse technique
DEEP_ANALYSIS_TOP_N = 15         # Plus de candidats à analyser = meilleur tri
KLINES_INTERVAL_FAST = "5m"      # Intervalle rapide (timing précis)
KLINES_INTERVAL_SLOW = "1h"      # Intervalle lent (tendance de fond)
KLINES_LIMIT_FAST = 100          # Bougies 5m = ~8h de données
KLINES_LIMIT_SLOW = 48           # Bougies 1h = 48h de données

# Scoring SERRÉ (v2)
SPY_MIN_SCORE_BUY = 80           # Score minimum pour achat (exigeant mais réaliste)
SPY_MIN_CONFIRMATIONS = 4        # Au moins 4 signaux confirmants requis
SPY_POSITION_SIZE = 15            # Taille de position en USDT pour le spy
SPY_MAX_TRADES_PER_CYCLE = 1     # Max 1 achat par cycle (patience)

# Filtres de qualité supplémentaires
SPY_MAX_RSI = 62                 # RSI max → pas acheter en zone surachat
SPY_MIN_VOLUME_SPIKE = 1.3       # Volume spike min → confirmer l'intérêt
SPY_MAX_BB_POSITION = 0.70       # BB max → pas acheter en haut de bande
SPY_MIN_TREND_SCORE = 0          # Trend H1 minimum (0 = neutre ok, >0 = bullish)

# Timing
SCAN_INTERVAL_SECONDS = 60       # Intervalle entre les scans (60s = qualité > vitesse)
API_RATE_LIMIT_PAUSE = 0.2       # Pause entre appels API klines (secondes)

# Fichiers
POSITIONS_FILE = os.path.join(SCRIPT_DIR, "positions.json")
WATCHLIST_FILE = os.path.join(SCRIPT_DIR, "watchlist.json")
SPY_LOG_FILE = os.path.join(SCRIPT_DIR, "espion_opportunities.json")
SPY_TRADES_FILE = os.path.join(SCRIPT_DIR, "espion_trades.json")

# Stablecoins à exclure
STABLECOINS = {"BUSDUSDT", "TUSDUSDT", "USDCUSDT", "DAIUSDT", "FDUSDUSDT", 
               "EURUSDT", "GBPUSDT", "USDPUSDT", "USTCUSDT"}

# Symboles de référence marché
BTC_SYMBOL = "BTCUSDT"

# ═══════════════════════════════════════════════════════════════════════════════
# LOGGING
# ═══════════════════════════════════════════════════════════════════════════════

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [SPY] %(message)s',
    datefmt='%H:%M:%S'
)
logger = logging.getLogger("market_spy")

# Fichier log aussi
file_handler = logging.FileHandler(
    os.path.join(SCRIPT_DIR, "market_spy.log"), encoding='utf-8'
)
file_handler.setFormatter(logging.Formatter('%(asctime)s [SPY] %(message)s'))
logger.addHandler(file_handler)


# ═══════════════════════════════════════════════════════════════════════════════
# API CLIENT LÉGER (pas besoin du BinanceClient complet)
# ═══════════════════════════════════════════════════════════════════════════════

class SpyApiClient:
    """Client API léger pour le scanner espion"""
    
    TIME_OFFSET = 0
    _last_sync = 0
    
    def __init__(self):
        self.api_key = BINANCE_API_KEY
        self.api_secret = BINANCE_API_SECRET
        self.session = requests.Session()
        self.session.headers.update({"X-MBX-APIKEY": self.api_key})
        self._sync_server_time()
    
    def _sync_server_time(self):
        """Synchronise l'horloge avec le serveur Binance"""
        try:
            resp = self.session.get(f"{TRADING_API}/api/v3/time", timeout=5)
            server_time = resp.json()['serverTime']
            local_time = int(time.time() * 1000)
            SpyApiClient.TIME_OFFSET = server_time - local_time
            SpyApiClient._last_sync = time.time()
        except Exception:
            SpyApiClient.TIME_OFFSET = 0
    
    def _sign(self, params):
        """Signe une requête 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 public_get(self, url, params=None):
        """Requête publique (pas de signature)"""
        try:
            resp = self.session.get(url, params=params, timeout=15)
            data = resp.json()
            if isinstance(data, dict) and data.get('code', 0) < 0:
                logger.warning(f"API error: {data.get('msg', 'unknown')}")
                return None
            return data
        except Exception as e:
            logger.error(f"Requête échouée: {e}")
            return None
    
    def signed_request(self, method, endpoint, params=None):
        """Requête signée (pour ordres)"""
        if params is None:
            params = {}
        
        # Re-sync toutes les 5 minutes
        if time.time() - SpyApiClient._last_sync > 300:
            self._sync_server_time()
        
        params['timestamp'] = int(time.time() * 1000) + SpyApiClient.TIME_OFFSET
        params['recvWindow'] = 10000
        params['signature'] = self._sign(params)
        
        url = f"{TRADING_API}{endpoint}"
        
        try:
            if method == "GET":
                resp = self.session.get(url, params=params, timeout=15)
            elif method == "POST":
                resp = self.session.post(url, params=params, timeout=15)
            else:
                return None
            
            data = resp.json()
            
            # Retry sur erreur timestamp
            if isinstance(data, dict) and data.get('code') in (-1021, -1022):
                logger.info("⏰ Re-synchronisation horloge...")
                self._sync_server_time()
                params['timestamp'] = int(time.time() * 1000) + SpyApiClient.TIME_OFFSET
                params.pop('signature', None)
                params['signature'] = self._sign(params)
                
                if method == "GET":
                    resp = self.session.get(url, params=params, timeout=15)
                else:
                    resp = self.session.post(url, params=params, timeout=15)
                data = resp.json()
            
            if isinstance(data, dict) and data.get('code', 0) < 0:
                logger.error(f"API signée error: {data.get('msg')}")
                return None
            
            return data
        except Exception as e:
            logger.error(f"Requête signée échouée: {e}")
            return None
    
    def get_account(self):
        """Récupère le compte"""
        return self.signed_request("GET", "/api/v3/account")
    
    def get_balance(self, asset="USDT"):
        """Récupère le solde d'un asset"""
        account = self.get_account()
        if account:
            for b in account.get('balances', []):
                if b['asset'] == asset:
                    return float(b['free'])
        return 0
    
    def market_buy(self, symbol, usdt_amount):
        """Achat market avec montant en USDT"""
        params = {
            "symbol": symbol,
            "side": "BUY",
            "type": "MARKET",
            "quoteOrderQty": f"{usdt_amount:.2f}"
        }
        return self.signed_request("POST", "/api/v3/order", params)
    
    def get_symbol_info(self, symbol):
        """Récupère les infos du symbole (précision, etc.)"""
        data = self.public_get(f"{TRADING_API}/api/v3/exchangeInfo", {"symbol": symbol})
        if data and 'symbols' in data and len(data['symbols']) > 0:
            return data['symbols'][0]
        return None


# ═══════════════════════════════════════════════════════════════════════════════
# INDICATEURS TECHNIQUES
# ═══════════════════════════════════════════════════════════════════════════════

class SpyIndicators:
    """Calcul rapide d'indicateurs techniques pour le spy"""
    
    @staticmethod
    def rsi(prices, period=14):
        """RSI classique"""
        if len(prices) < period + 1:
            return 50.0
        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:])
        
        if avg_loss == 0 and avg_gain == 0:
            return 50.0
        if avg_loss == 0:
            return 70.0
        if avg_gain == 0:
            return 30.0
        
        rs = avg_gain / avg_loss
        return 100 - (100 / (1 + rs))
    
    @staticmethod
    def bollinger_bands(prices, period=20, std_dev=2.0):
        """Bollinger Bands → retourne (upper, middle, lower, bb_position)"""
        if len(prices) < period:
            return None, None, None, 0.5
        
        prices = np.array(prices[-period:], dtype=float)
        middle = np.mean(prices)
        std = np.std(prices)
        upper = middle + std_dev * std
        lower = middle - std_dev * std
        
        current = prices[-1]
        bb_width = upper - lower
        bb_position = (current - lower) / bb_width if bb_width > 0 else 0.5
        
        return upper, middle, lower, bb_position
    
    @staticmethod
    def ema(prices, period):
        """Moyenne mobile exponentielle"""
        if len(prices) < period:
            return prices[-1] if len(prices) > 0 else 0
        
        prices = np.array(prices, dtype=float)
        multiplier = 2.0 / (period + 1)
        ema_val = prices[0]
        for p in prices[1:]:
            ema_val = (p - ema_val) * multiplier + ema_val
        return ema_val
    
    @staticmethod
    def momentum(prices, periods=3):
        """Variation en % sur N périodes"""
        if len(prices) < periods + 1:
            return 0
        old = prices[-(periods + 1)]
        new = prices[-1]
        if old == 0:
            return 0
        return ((new - old) / old) * 100
    
    @staticmethod
    def volume_spike(volumes, lookback=20):
        """Ratio du volume actuel vs moyenne des N derniers"""
        if len(volumes) < lookback + 1:
            return 1.0
        avg_vol = np.mean(volumes[-lookback - 1:-1])
        if avg_vol == 0:
            return 1.0
        return volumes[-1] / avg_vol
    
    @staticmethod
    def atr(highs, lows, closes, period=14):
        """Average True Range"""
        if len(closes) < period + 1:
            return 0
        highs = np.array(highs[-period-1:], dtype=float)
        lows = np.array(lows[-period-1:], dtype=float)
        closes = np.array(closes[-period-1:], dtype=float)
        
        tr = np.maximum(
            highs[1:] - lows[1:],
            np.maximum(
                np.abs(highs[1:] - closes[:-1]),
                np.abs(lows[1:] - closes[:-1])
            )
        )
        return np.mean(tr)
    
    @staticmethod
    def macd(prices, fast=12, slow=26, signal=9):
        """MACD (trend direction + momentum)"""
        if len(prices) < slow + signal:
            return 0, 0, 0  # macd_line, signal_line, histogram
        ema_fast = SpyIndicators.ema(prices, fast)
        ema_slow = SpyIndicators.ema(prices, slow)
        macd_line = ema_fast - ema_slow
        
        # Signal line approximation
        macd_history = []
        for i in range(signal + 1):
            idx = len(prices) - signal - 1 + i
            if idx >= slow:
                ef = SpyIndicators.ema(prices[:idx+1], fast)
                es = SpyIndicators.ema(prices[:idx+1], slow)
                macd_history.append(ef - es)
        
        signal_line = SpyIndicators.ema(macd_history, signal) if len(macd_history) >= signal else macd_line
        histogram = macd_line - signal_line
        return macd_line, signal_line, histogram
    
    @staticmethod
    def support_resistance(lows, highs, closes, lookback=30):
        """Détecte le support et la résistance récents"""
        if len(closes) < lookback:
            lookback = len(closes)
        recent_lows = lows[-lookback:]
        recent_highs = highs[-lookback:]
        support = min(recent_lows)
        resistance = max(recent_highs)
        current = closes[-1]
        # Distance au support en %
        dist_support = ((current - support) / support * 100) if support > 0 else 999
        dist_resistance = ((resistance - current) / current * 100) if current > 0 else 0
        return support, resistance, dist_support, dist_resistance
    
    @staticmethod
    def consecutive_green(closes, max_check=10):
        """Compte les bougies consécutives vertes (haussières) en fin"""
        count = 0
        for i in range(2, min(max_check + 2, len(closes))):
            if closes[-i+1] > closes[-i]:
                count += 1
            else:
                break
        return count
    
    @staticmethod
    def price_vs_vwap(closes, volumes, period=20):
        """Position du prix par rapport au VWAP"""
        if len(closes) < period:
            period = len(closes)
        c = np.array(closes[-period:], dtype=float)
        v = np.array(volumes[-period:], dtype=float)
        total_vol = np.sum(v)
        if total_vol == 0:
            return 0
        vwap = np.sum(c * v) / total_vol
        return ((closes[-1] - vwap) / vwap * 100) if vwap > 0 else 0


# ═══════════════════════════════════════════════════════════════════════════════
# ANALYSE MULTI-TIMEFRAME
# ═══════════════════════════════════════════════════════════════════════════════

class MultiTimeframeAnalyzer:
    """Analyse la tendance sur timeframe lent (1h) pour confirmer les entrées"""
    
    @staticmethod
    def analyze_trend(closes_h1, highs_h1, lows_h1, volumes_h1):
        """
        Analyse la tendance H1 et retourne un score de -100 (bear) à +100 (bull).
        
        Returns:
            dict: {
                'trend_score': float,     # -100 à +100
                'trend_label': str,       # STRONG_BULL, BULL, NEUTRAL, BEAR, STRONG_BEAR
                'h1_rsi': float,
                'h1_ema_trend': float,    # EMA9 vs EMA21 en %
                'h1_momentum': float,     # Momentum 5 périodes
                'h1_macd_hist': float,
            }
        """
        if len(closes_h1) < 30:
            return {
                'trend_score': 0, 'trend_label': 'UNKNOWN',
                'h1_rsi': 50, 'h1_ema_trend': 0, 'h1_momentum': 0, 'h1_macd_hist': 0
            }
        
        rsi_h1 = SpyIndicators.rsi(closes_h1)
        ema9 = SpyIndicators.ema(closes_h1, 9)
        ema21 = SpyIndicators.ema(closes_h1, 21)
        ema_trend = ((ema9 - ema21) / ema21 * 100) if ema21 > 0 else 0
        mom5 = SpyIndicators.momentum(closes_h1, 5)
        _, _, macd_hist = SpyIndicators.macd(closes_h1)
        
        # Score de tendance
        score = 0
        
        # Direction EMA (poids fort)
        if ema_trend > 1.0:
            score += 30  # Tendance haussière nette
        elif ema_trend > 0.2:
            score += 15
        elif ema_trend > -0.2:
            score += 0   # Neutre
        elif ema_trend > -1.0:
            score -= 15
        else:
            score -= 30  # Tendance baissière nette
        
        # RSI
        if 40 <= rsi_h1 <= 60:
            score += 10   # Zone saine
        elif 30 <= rsi_h1 < 40:
            score += 5    # Oversold = potentiel rebond
        elif rsi_h1 < 30:
            score += 15   # Très oversold = gros potentiel
        elif 60 < rsi_h1 <= 70:
            score -= 5
        else:
            score -= 20   # Surachat H1 = danger
        
        # Momentum H1
        if mom5 > 2.0:
            score += 20
        elif mom5 > 0.5:
            score += 10
        elif mom5 > -0.5:
            score += 0
        elif mom5 > -2.0:
            score -= 10
        else:
            score -= 20
        
        # MACD histogram
        if isinstance(macd_hist, (int, float)):
            if macd_hist > 0:
                score += 15
            else:
                score -= 10
        
        # Clamp
        score = max(-100, min(100, score))
        
        # Label
        if score >= 40:
            label = "STRONG_BULL"
        elif score >= 15:
            label = "BULL"
        elif score >= -15:
            label = "NEUTRAL"
        elif score >= -40:
            label = "BEAR"
        else:
            label = "STRONG_BEAR"
        
        return {
            'trend_score': score,
            'trend_label': label,
            'h1_rsi': round(rsi_h1, 1),
            'h1_ema_trend': round(ema_trend, 3),
            'h1_momentum': round(mom5, 3),
            'h1_macd_hist': round(macd_hist, 6) if isinstance(macd_hist, (int, float)) else 0
        }


# ═══════════════════════════════════════════════════════════════════════════════
# FILTRE RÉGIME MARCHÉ (BTC)
# ═══════════════════════════════════════════════════════════════════════════════

class MarketRegimeFilter:
    """Vérifie le régime global du marché via BTC"""
    
    @staticmethod
    def check_btc_regime(client):
        """
        Analyse BTC pour déterminer le régime marché.
        Returns: 'BULLISH', 'NEUTRAL', 'BEARISH', 'STRONG_BEARISH'
        """
        try:
            klines = client.public_get(
                f"{SCAN_API}/api/v3/klines",
                {"symbol": BTC_SYMBOL, "interval": "1h", "limit": 48}
            )
            if not klines or len(klines) < 24:
                return 'NEUTRAL', {}
            
            closes = [float(k[4]) for k in klines]
            
            rsi = SpyIndicators.rsi(closes)
            ema9 = SpyIndicators.ema(closes, 9)
            ema21 = SpyIndicators.ema(closes, 21)
            ema_diff = ((ema9 - ema21) / ema21 * 100) if ema21 > 0 else 0
            mom_4h = SpyIndicators.momentum(closes, 4)  # 4 bougies H1 = momentum 4h
            mom_24h = SpyIndicators.momentum(closes, 24)
            
            info = {
                'btc_rsi_h1': round(rsi, 1),
                'btc_ema_diff': round(ema_diff, 3),
                'btc_mom_4h': round(mom_4h, 3),
                'btc_mom_24h': round(mom_24h, 3),
                'btc_price': closes[-1]
            }
            
            # Déterminer le régime
            if ema_diff < -1.0 and mom_24h < -3:
                return 'STRONG_BEARISH', info
            elif ema_diff < -0.3 or mom_24h < -2:
                return 'BEARISH', info
            elif ema_diff > 0.5 and mom_24h > 1:
                return 'BULLISH', info
            else:
                return 'NEUTRAL', info
                
        except Exception as e:
            logger.warning(f"Erreur check BTC regime: {e}")
            return 'NEUTRAL', {}


# ═══════════════════════════════════════════════════════════════════════════════
# DÉTECTION DE PATTERNS (v2 - HAUTE QUALITÉ)
# ═══════════════════════════════════════════════════════════════════════════════

class SpyPatternDetector:
    """
    Détection de patterns v2 - BEAUCOUP plus sélectif.
    
    Critères v1 vs v2:
    - v1 SQUEEZE: bb_width < 3%, mom3 > 0.1% → TROP PERMISSIF (tout passait)
    - v2 SQUEEZE: bb_width < 1.5%, mom3 > 0.3%, vol_spike > 1.5, RSI < 60 → STRICT
    
    - v1: pas de filtre overbought → achetait BB=1.17 (au-dessus des bandes!)
    - v2: BB position max 0.70, RSI max 62 → JAMAIS acheter en haut
    
    - v1: pas de confirmation volume → achetait VolSpike=0.08 (volume mort)
    - v2: VolSpike minimum 1.3 → VOLUME OBLIGATOIRE pour confirmer
    """
    
    @staticmethod
    def detect(closes, highs, lows, volumes, trend_data=None):
        """
        Analyse les données et retourne le meilleur pattern détecté.
        Inclut maintenant des filtres qualité stricts.
        """
        if len(closes) < 30:
            return {'pattern': 'INSUFFICIENT_DATA', 'score': 0, 'signals': [], 'indicators': {}}
        
        # ─── Calculer TOUS les indicateurs ─────────────────────────────
        rsi = SpyIndicators.rsi(closes)
        upper, middle, lower, bb_pos = SpyIndicators.bollinger_bands(closes)
        ema9 = SpyIndicators.ema(closes, 9)
        ema21 = SpyIndicators.ema(closes, 21)
        ema50 = SpyIndicators.ema(closes, min(50, len(closes)))
        mom3 = SpyIndicators.momentum(closes, 3)
        mom5 = SpyIndicators.momentum(closes, 5)
        mom10 = SpyIndicators.momentum(closes, 10)
        vol_spike = SpyIndicators.volume_spike(volumes)
        atr = SpyIndicators.atr(highs, lows, closes)
        _, _, macd_hist = SpyIndicators.macd(closes)
        support, resistance, dist_support, dist_resistance = SpyIndicators.support_resistance(lows, highs, closes)
        green_candles = SpyIndicators.consecutive_green(closes)
        vwap_pos = SpyIndicators.price_vs_vwap(closes, volumes)
        
        current_price = closes[-1]
        ema_diff = ((ema9 - ema21) / ema21) * 100 if ema21 > 0 else 0
        price_vs_ema50 = ((current_price - ema50) / ema50 * 100) if ema50 > 0 else 0
        
        # Pente EMA9 (direction court terme)
        ema9_prev = SpyIndicators.ema(closes[:-1], 9)
        ema_slope = ((ema9 - ema9_prev) / ema9_prev * 100) if ema9_prev > 0 else 0
        
        # BB Width (pour squeeze)
        bb_width = ((upper - lower) / middle * 100) if middle and middle > 0 else 999
        
        indicators = {
            'rsi': round(rsi, 1),
            'bb_position': round(bb_pos, 3),
            'bb_width': round(bb_width, 2),
            'ema_diff': round(ema_diff, 3),
            'ema_slope': round(ema_slope, 4),
            'price_vs_ema50': round(price_vs_ema50, 2),
            'momentum_3': round(mom3, 3),
            'momentum_5': round(mom5, 3),
            'momentum_10': round(mom10, 3),
            'volume_spike': round(vol_spike, 2),
            'atr': round(atr, 6),
            'macd_histogram': round(macd_hist, 6) if isinstance(macd_hist, (int, float)) else 0,
            'dist_support': round(dist_support, 2),
            'dist_resistance': round(dist_resistance, 2),
            'green_candles': green_candles,
            'vwap_position': round(vwap_pos, 3),
            'price': current_price
        }
        
        # Ajouter les données trend H1 si disponibles
        if trend_data:
            indicators.update(trend_data)
        
        signals = []
        patterns_found = []
        
        # ═══════════════════════════════════════════════════════════════════
        # FILTRES QUALITÉ GLOBAUX (appliqués AVANT tout pattern)
        # Un signal qui ne passe pas ces filtres = REJETÉ
        # ═══════════════════════════════════════════════════════════════════
        
        quality_ok = True
        quality_flags = []
        
        # FILTRE 1: Pas acheter en zone surachat
        if rsi > SPY_MAX_RSI:
            quality_ok = False
            quality_flags.append(f"RSI_TOO_HIGH({rsi:.0f}>{SPY_MAX_RSI})")
        
        # FILTRE 2: Pas acheter en haut des bandes de Bollinger
        if bb_pos > SPY_MAX_BB_POSITION:
            quality_ok = False
            quality_flags.append(f"BB_TOO_HIGH({bb_pos:.2f}>{SPY_MAX_BB_POSITION})")
        
        # FILTRE 3: Volume doit confirmer (pas de volume mort)
        if vol_spike < 0.5:
            quality_ok = False
            quality_flags.append(f"VOLUME_DEAD({vol_spike:.2f})")
        
        # FILTRE 4: Pas de chute libre (momentum 10 bougies très négatif sans rebond)
        if mom10 < -5.0 and mom3 < 0:
            quality_ok = False
            quality_flags.append(f"FREEFALL(m10={mom10:.1f}%)")
        
        # FILTRE 5: Tendance H1 pas fortement bear
        if trend_data and trend_data.get('trend_score', 0) < -30:
            quality_ok = False
            quality_flags.append(f"H1_BEAR({trend_data.get('trend_label')})")
        
        if not quality_ok:
            indicators['quality_rejection'] = quality_flags
            return {
                'pattern': 'QUALITY_REJECTED',
                'score': 0,
                'signals': quality_flags,
                'indicators': indicators,
                'quality_flags': quality_flags
            }
        
        # ═══════════════════════════════════════════════════════════════════
        # PATTERN 1: CREUX_REBOUND (rebond depuis un creux - haute probabilité)
        # Conditions STRICTES: prix au bas des bandes, RSI oversold,
        # premier signe de rebond confirmé par volume
        # ═══════════════════════════════════════════════════════════════════
        creux_score = 0
        creux_signals = []
        
        if bb_pos < 0.30 and rsi < 45 and mom3 > 0.05:
            creux_score = 35  # Base plus basse, il faut accumuler des confirmations
            creux_signals.append("BB_LOW_ZONE")
            
            # RSI oversold (fort signal)
            if rsi < 30:
                creux_score += 20
                creux_signals.append("RSI_DEEPLY_OVERSOLD")
            elif rsi < 38:
                creux_score += 12
                creux_signals.append("RSI_OVERSOLD")
            
            # EMA en retournement (crucial)
            if ema_slope > 0.01:
                creux_score += 12
                creux_signals.append("EMA_TURNING_UP")
            elif ema_slope > 0:
                creux_score += 5
            
            # Volume confirme le rebond
            if vol_spike > 2.0:
                creux_score += 15
                creux_signals.append("STRONG_VOLUME_CONFIRM")
            elif vol_spike > 1.3:
                creux_score += 8
                creux_signals.append("VOLUME_CONFIRM")
            
            # Momentum en hausse
            if mom3 > 0.5:
                creux_score += 10
                creux_signals.append("GOOD_BOUNCE_MOMENTUM")
            elif mom3 > 0.2:
                creux_score += 5
            
            # Proche du support
            if dist_support < 2.0:
                creux_score += 8
                creux_signals.append("NEAR_SUPPORT")
            
            # MACD histogram positif (convergence)
            if isinstance(macd_hist, (int, float)) and macd_hist > 0:
                creux_score += 5
                creux_signals.append("MACD_BULLISH")
            
            # Tendance H1 favorable
            if trend_data and trend_data.get('trend_score', 0) >= 0:
                creux_score += 10
                creux_signals.append("H1_TREND_OK")
            
            if len(creux_signals) >= 4:  # Minimum 4 confirmations
                patterns_found.append(('CREUX_REBOUND', min(creux_score, 100), creux_signals))
        
        # ═══════════════════════════════════════════════════════════════════
        # PATTERN 2: SQUEEZE_BREAKOUT (compression → expansion avec volume)
        # v2: BEAUCOUP plus strict que v1
        # Requis: bandes TRÈS serrées + début explosion volume + momentum
        # ═══════════════════════════════════════════════════════════════════
        squeeze_score = 0
        squeeze_signals = []
        
        if upper and lower and bb_width < 2.0:  # v1 était 3% → v2: 2% max
            if mom3 > 0.3 and vol_spike > SPY_MIN_VOLUME_SPIKE:  # v1: mom3>0.1 sans volume → v2: strict
                squeeze_score = 35
                squeeze_signals.append("SQUEEZE_DETECTED")
                
                # Bandes très très serrées (compression forte)
                if bb_width < 1.0:
                    squeeze_score += 18
                    squeeze_signals.append("EXTREME_SQUEEZE")
                elif bb_width < 1.5:
                    squeeze_score += 10
                    squeeze_signals.append("TIGHT_SQUEEZE")
                
                # Volume breakout (CRUCIAL - v1 ignorait ça)
                if vol_spike > 3.0:
                    squeeze_score += 18
                    squeeze_signals.append("VOLUME_EXPLOSION")
                elif vol_spike > 2.0:
                    squeeze_score += 12
                    squeeze_signals.append("STRONG_VOLUME")
                elif vol_spike > 1.5:
                    squeeze_score += 6
                    squeeze_signals.append("VOLUME_RISING")
                
                # Momentum confirmé
                if mom3 > 1.0:
                    squeeze_score += 12
                    squeeze_signals.append("EXPLOSIVE_MOMENTUM")
                elif mom3 > 0.5:
                    squeeze_score += 7
                    squeeze_signals.append("GOOD_MOMENTUM")
                
                # RSI dans la zone saine (PAS surachat)
                if 35 <= rsi <= 55:
                    squeeze_score += 8
                    squeeze_signals.append("RSI_OPTIMAL")
                elif rsi < 35:
                    squeeze_score += 5
                
                # EMA alignment bullish
                if ema_diff > 0 and ema_slope > 0:
                    squeeze_score += 8
                    squeeze_signals.append("EMA_BULLISH")
                
                # MACD favorable
                if isinstance(macd_hist, (int, float)) and macd_hist > 0:
                    squeeze_score += 5
                    squeeze_signals.append("MACD_POSITIVE")
                
                # Tendance H1 favorable
                if trend_data and trend_data.get('trend_score', 0) > 0:
                    squeeze_score += 8
                    squeeze_signals.append("H1_BULLISH")
                
                if len(squeeze_signals) >= 4:
                    patterns_found.append(('SQUEEZE_BREAKOUT', min(squeeze_score, 100), squeeze_signals))
        
        # ═══════════════════════════════════════════════════════════════════
        # PATTERN 3: PULLBACK (retour sur EMA en tendance haussière confirmée)
        # Requis: tendance H1 bull, prix revient toucher EMA/support, rebond
        # ═══════════════════════════════════════════════════════════════════
        pullback_score = 0
        pullback_signals = []
        
        if ema_diff > 0.1 and 0.20 <= bb_pos <= 0.55:
            if 35 <= rsi <= 58 and mom3 > -0.3:
                pullback_score = 35
                pullback_signals.append("PULLBACK_ZONE")
                
                # Prix proche de l'EMA9 (le pullback parfait)
                price_vs_ema9 = abs((current_price - ema9) / ema9 * 100) if ema9 > 0 else 999
                if price_vs_ema9 < 0.3:
                    pullback_score += 15
                    pullback_signals.append("TOUCHING_EMA9")
                elif price_vs_ema9 < 0.8:
                    pullback_score += 8
                    pullback_signals.append("NEAR_EMA9")
                
                # Tendance intacte (pente EMA21 positive)
                ema21_prev = SpyIndicators.ema(closes[:-3], 21) if len(closes) > 24 else ema21
                ema21_slope = ((ema21 - ema21_prev) / ema21_prev * 100) if ema21_prev > 0 else 0
                if ema21_slope > 0.02:
                    pullback_score += 12
                    pullback_signals.append("TREND_INTACT")
                
                # Volume faible pendant le pullback (sain)
                if vol_spike < 0.8:
                    pullback_score += 8
                    pullback_signals.append("LOW_SELL_VOLUME")
                
                # Début de rebond
                if mom3 > 0.1 and green_candles >= 2:
                    pullback_score += 12
                    pullback_signals.append("BOUNCE_CONFIRMED")
                elif mom3 > 0:
                    pullback_score += 5
                
                # MACD toujours positif
                if isinstance(macd_hist, (int, float)) and macd_hist > 0:
                    pullback_score += 5
                    pullback_signals.append("MACD_STILL_BULLISH")
                
                # H1 bullish (IMPORTANT pour pullback)
                if trend_data and trend_data.get('trend_score', 0) >= 15:
                    pullback_score += 12
                    pullback_signals.append("H1_CONFIRMS_UPTREND")
                elif trend_data and trend_data.get('trend_score', 0) >= 0:
                    pullback_score += 5
                
                if len(pullback_signals) >= 4:
                    patterns_found.append(('PULLBACK', min(pullback_score, 100), pullback_signals))
        
        # ═══════════════════════════════════════════════════════════════════
        # PATTERN 4: VOLUME_BREAKOUT (explosion volume + prix en hausse)
        # Différent de v1 VOLUME_ANOMALY: maintenant exige direction claire
        # ═══════════════════════════════════════════════════════════════════
        vb_score = 0
        vb_signals = []
        
        if vol_spike > 3.0 and mom3 > 0.5 and rsi < 60:
            vb_score = 40
            vb_signals.append("VOLUME_BREAKOUT")
            
            if vol_spike > 5.0:
                vb_score += 15
                vb_signals.append("EXTREME_VOLUME_5X")
            elif vol_spike > 4.0:
                vb_score += 10
                vb_signals.append("VOLUME_4X")
            
            # Prix doit monter avec le volume (pas juste du volume)
            if mom3 > 1.5:
                vb_score += 15
                vb_signals.append("STRONG_PRICE_MOVE")
            elif mom3 > 0.8:
                vb_score += 8
            
            # Prix sous résistance (il y a de la place pour monter)
            if dist_resistance > 3.0:
                vb_score += 8
                vb_signals.append("ROOM_TO_RUN")
            
            # RSI pas trop élevé
            if rsi < 50:
                vb_score += 8
                vb_signals.append("RSI_HEALTHY")
            
            # Au-dessus du VWAP
            if vwap_pos > 0:
                vb_score += 5
                vb_signals.append("ABOVE_VWAP")
            
            if trend_data and trend_data.get('trend_score', 0) >= 0:
                vb_score += 8
                vb_signals.append("H1_NOT_BEAR")
            
            if len(vb_signals) >= 4:
                patterns_found.append(('VOLUME_BREAKOUT', min(vb_score, 100), vb_signals))
        
        # ═══════════════════════════════════════════════════════════════════
        # PATTERN 5: REVERSAL_DIP (capitulation → rebond fort)
        # Le plus risqué mais potentiellement le plus rentable
        # Exige: chute brutale, RSI extreme, volume capitulation, PUIS rebond
        # ═══════════════════════════════════════════════════════════════════
        dip_score = 0
        dip_signals = []
        
        if mom5 < -4.0 and mom3 > 0.3 and rsi < 32:
            # Chute brutale SUIVIE d'un rebond confirmé
            dip_score = 40
            dip_signals.append("DIP_REVERSAL")
            
            if rsi < 22:
                dip_score += 18
                dip_signals.append("EXTREME_OVERSOLD")
            elif rsi < 28:
                dip_score += 10
                dip_signals.append("DEEPLY_OVERSOLD")
            
            # Volume de capitulation puis rebond
            if vol_spike > 2.5:
                dip_score += 12
                dip_signals.append("CAPITULATION_VOLUME")
            
            # Rebond confirmé par 2+ bougies vertes
            if green_candles >= 3:
                dip_score += 12
                dip_signals.append("STRONG_BOUNCE_3_GREEN")
            elif green_candles >= 2:
                dip_score += 7
                dip_signals.append("BOUNCE_2_GREEN")
            
            # Prix très bas dans les bandes
            if bb_pos < 0.10:
                dip_score += 10
                dip_signals.append("BELOW_BANDS")
            elif bb_pos < 0.20:
                dip_score += 5
            
            # Proche du support
            if dist_support < 1.5:
                dip_score += 8
                dip_signals.append("AT_SUPPORT")
            
            if len(dip_signals) >= 4:
                patterns_found.append(('REVERSAL_DIP', min(dip_score, 100), dip_signals))
        
        # ═══════════════════════════════════════════════════════════════════
        # PATTERN 6 (NOUVEAU): ACCUMULATION (range tight + breakout naissant)
        # Volume progressivement croissant dans un range serré
        # ═══════════════════════════════════════════════════════════════════
        acc_score = 0
        acc_signals = []
        
        # Range serré (ATR faible vs prix) + volume croissant
        atr_pct = (atr / current_price * 100) if current_price > 0 else 999
        
        if atr_pct < 1.5 and vol_spike > 1.5:
            # Vérifier que le range est serré depuis un moment
            if bb_width < 2.5 and abs(mom10) < 2.0:
                acc_score = 35
                acc_signals.append("ACCUMULATION_ZONE")
                
                # Volume en augmentation (signe d'accumulation)
                if vol_spike > 2.5:
                    acc_score += 15
                    acc_signals.append("ACCUMULATION_VOLUME")
                elif vol_spike > 1.8:
                    acc_score += 8
                
                # Prix commence à monter après la range
                if mom3 > 0.3 and ema_slope > 0:
                    acc_score += 15
                    acc_signals.append("BREAKOUT_STARTING")
                elif mom3 > 0.1:
                    acc_score += 5
                
                # RSI neutre (pas de surextension)
                if 40 <= rsi <= 55:
                    acc_score += 10
                    acc_signals.append("RSI_NEUTRAL_READY")
                
                # Au-dessus du VWAP
                if vwap_pos > 0:
                    acc_score += 8
                    acc_signals.append("ABOVE_VWAP")
                
                # Tendance H1 pas bear
                if trend_data and trend_data.get('trend_score', 0) >= 0:
                    acc_score += 8
                    acc_signals.append("H1_OK")
                
                if len(acc_signals) >= 4:
                    patterns_found.append(('ACCUMULATION', min(acc_score, 100), acc_signals))
        
        # ═══════════════════════════════════════════════════════════════════
        # SÉLECTION DU MEILLEUR PATTERN
        # ═══════════════════════════════════════════════════════════════════
        if not patterns_found:
            return {
                'pattern': 'NO_PATTERN',
                'score': 0,
                'signals': signals,
                'indicators': indicators
            }
        
        # Trier par score décroissant
        patterns_found.sort(key=lambda x: x[1], reverse=True)
        best_pattern, best_score, best_signals = patterns_found[0]
        
        # BONUS: multi-patterns (si 2+ patterns détectés = confluence = +fiabilité)
        if len(patterns_found) >= 2:
            confluence_bonus = min(len(patterns_found) * 3, 10)
            best_score = min(best_score + confluence_bonus, 100)
            best_signals.append(f"CONFLUENCE_{len(patterns_found)}_PATTERNS")
        
        return {
            'pattern': best_pattern,
            'score': best_score,
            'signals': best_signals,
            'indicators': indicators,
            'all_patterns': [(p, s) for p, s, _ in patterns_found],
            'num_confirmations': len(best_signals)
        }


# ═══════════════════════════════════════════════════════════════════════════════
# MARKET SPY - CLASSE PRINCIPALE
# ═══════════════════════════════════════════════════════════════════════════════

class MarketSpy:
    """Scanner espion du marché crypto complet - v2 haute qualité"""
    
    def __init__(self, dry_run=False, min_score=SPY_MIN_SCORE_BUY, top_n=DEEP_ANALYSIS_TOP_N):
        self.dry_run = dry_run
        self.min_score_buy = min_score
        self.top_n = top_n
        self.client = SpyApiClient()
        self.watchlist = self._load_watchlist()
        self.scan_count = 0
        self.total_opportunities = 0
        self.trades_executed = 0
        self.opportunities_history = []
        self.btc_regime = 'NEUTRAL'
        self.btc_info = {}
        
        logger.info("═" * 60)
        logger.info("🕵️ MARKET SPY v2 - Scanner haute qualité")
        logger.info("═" * 60)
        logger.info(f"   Mode: {'DRY-RUN (pas d achat)' if dry_run else 'LIVE (achats actifs)'}")
        logger.info(f"   API Trading: {TRADING_API}")
        logger.info(f"   API Scan: {SCAN_API}")
        logger.info(f"   Score min achat: {self.min_score_buy}")
        logger.info(f"   Confirmations min: {SPY_MIN_CONFIRMATIONS}")
        logger.info(f"   Watchlist exclue: {len(self.watchlist)} symboles")
        logger.info(f"   Position size: {SPY_POSITION_SIZE} USDT")
        logger.info(f"   Anti-pump max 24h: +{MAX_PRICE_CHANGE_24H}%")
        logger.info(f"   Max RSI: {SPY_MAX_RSI} | Max BB: {SPY_MAX_BB_POSITION}")
        logger.info(f"   Multi-timeframe: 5m + 1h")
        logger.info(f"   BTC regime filter: ON")
        logger.info("═" * 60)
    
    def _load_watchlist(self):
        """Charge la watchlist du bot principal (à exclure du scan)"""
        try:
            if os.path.exists(WATCHLIST_FILE):
                with open(WATCHLIST_FILE, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    return set(data.get('symbols', []))
        except Exception:
            pass
        return set()
    
    def _load_positions(self):
        """Charge les positions actuelles du bot principal"""
        try:
            if os.path.exists(POSITIONS_FILE):
                with open(POSITIONS_FILE, 'r', encoding='utf-8') as f:
                    data = json.load(f)
                    # Exclure les métadonnées (usdt_balance, etc.)
                    return {k: v for k, v in data.items() 
                            if isinstance(v, dict) and 'entry_price' in v}
        except Exception:
            pass
        return {}
    
    def _load_spy_trades(self):
        """Charge les trades du spy"""
        try:
            if os.path.exists(SPY_TRADES_FILE):
                with open(SPY_TRADES_FILE, 'r', encoding='utf-8') as f:
                    return json.load(f)
        except Exception:
            pass
        return {}
    
    def _save_spy_trades(self, trades):
        """Sauvegarde les trades du spy"""
        try:
            with open(SPY_TRADES_FILE, 'w', encoding='utf-8') as f:
                json.dump(trades, f, indent=2, default=str)
        except Exception as e:
            logger.error(f"Erreur sauvegarde spy trades: {e}")
    
    def _count_total_positions(self):
        """Compte toutes les positions (bot + spy)"""
        bot_positions = len(self._load_positions())
        spy_positions = len(self._load_spy_trades())
        return bot_positions + spy_positions
    
    # ─── ÉTAPE 1: Scan rapide du marché complet ────────────────────────────
    
    def scan_market(self):
        """
        Scan ALL USDT pairs on Binance via /ticker/24hr
        v2: filtre anti-pump, meilleur pré-scoring, exclut les coins en chute libre
        """
        logger.info("🔍 Scan du marché complet (v2)...")
        
        # Un seul appel API pour TOUT le marché
        data = self.client.public_get(f"{SCAN_API}/api/v3/ticker/24hr")
        if not data:
            logger.error("❌ Impossible de récupérer les tickers")
            return []
        
        candidates = []
        total_pairs = 0
        rejected_pump = 0
        rejected_volume = 0
        
        for ticker in data:
            symbol = ticker.get('symbol', '')
            
            # Filtrer: seulement USDT pairs
            if not symbol.endswith('USDT'):
                continue
            
            total_pairs += 1
            
            # Exclure stablecoins
            if EXCLUDE_STABLECOINS and symbol in STABLECOINS:
                continue
            
            # Exclure BTC (utilisé comme référence)
            if symbol == BTC_SYMBOL:
                continue
            
            # Exclure les symboles déjà dans la watchlist du bot principal
            if symbol in self.watchlist:
                continue
            
            # Exclure les symboles déjà en position (bot ou spy)
            positions = self._load_positions()
            spy_trades = self._load_spy_trades()
            if symbol in positions or symbol in spy_trades:
                continue
            
            try:
                price = float(ticker.get('lastPrice', 0))
                volume_usdt = float(ticker.get('quoteVolume', 0))
                price_change_pct = float(ticker.get('priceChangePercent', 0))
                high_24h = float(ticker.get('highPrice', 0))
                low_24h = float(ticker.get('lowPrice', 0))
                weighted_avg = float(ticker.get('weightedAvgPrice', 0))
                count_trades = int(ticker.get('count', 0))
            except (ValueError, TypeError):
                continue
            
            # Filtres de base
            if price < MIN_PRICE or price > MAX_PRICE:
                continue
            if volume_usdt < MIN_VOLUME_USDT:
                rejected_volume += 1
                continue
            
            # ═══ ANTI-PUMP FILTER (v2) ═══
            # Si le coin a déjà pumé +15% sur 24h, on ne court PAS après
            if price_change_pct > MAX_PRICE_CHANGE_24H:
                rejected_pump += 1
                continue
            
            # ═══ ANTI-DUMP FILTER (v2) ═══
            # Si le coin a chuté de plus de -20% sur 24h sans rebond, c'est un couteau qui tombe
            if price_change_pct < -20:
                continue
            
            # Mouvement minimum
            abs_change = abs(price_change_pct)
            if abs_change < MIN_PRICE_CHANGE_ABS:
                continue
            
            # Score de pré-filtrage intelligent (v2)
            pre_score = 0
            
            # ─── Volume de qualité ───
            if volume_usdt > 100_000_000:
                pre_score += 15  # Très liquide
            elif volume_usdt > 50_000_000:
                pre_score += 12
            elif volume_usdt > 20_000_000:
                pre_score += 8
            else:
                pre_score += 4
            
            # ─── Nombre de trades (liquidité réelle) ───
            if count_trades > 100_000:
                pre_score += 8
            elif count_trades > 50_000:
                pre_score += 5
            elif count_trades > 20_000:
                pre_score += 3
            
            # ─── Position dans le range 24h (favoriser les creux) ───
            if high_24h > low_24h:
                position_in_range = (price - low_24h) / (high_24h - low_24h)
                if position_in_range < 0.25:
                    pre_score += 15  # Proche du bas 24h → fort potentiel rebond
                elif position_in_range < 0.40:
                    pre_score += 10  # Zone basse
                elif position_in_range < 0.55:
                    pre_score += 5   # Milieu
                elif position_in_range > 0.85:
                    pre_score -= 10  # Trop haut → PÉNALITÉ
            
            # ─── Variation adaptée (v2: on veut des baisses avec rebond potentiel) ───
            if -8 <= price_change_pct <= -3:
                pre_score += 15  # Zone idéale pour rebond (pas trop, pas trop peu)
            elif -12 <= price_change_pct < -8:
                pre_score += 10  # Forte baisse mais pas effondrement
            elif -3 < price_change_pct <= 0:
                pre_score += 5   # Légère baisse
            elif 0 < price_change_pct <= 5:
                pre_score += 3   # Légère hausse (peut être début de tendance)
            # Les hausses > 5% ne reçoivent PAS de bonus (chasing)
            
            # ─── Prix par rapport à la moyenne pondérée ───
            if weighted_avg > 0:
                price_vs_avg = ((price - weighted_avg) / weighted_avg) * 100
                if -3 < price_vs_avg < -0.5:
                    pre_score += 5  # Sous la moyenne = potentiel
            
            candidates.append({
                'symbol': symbol,
                'price': price,
                'volume_usdt': volume_usdt,
                'price_change_24h': price_change_pct,
                'high_24h': high_24h,
                'low_24h': low_24h,
                'count_trades': count_trades,
                'pre_score': pre_score
            })
        
        # Trier par pre_score décroissant
        candidates.sort(key=lambda x: x['pre_score'], reverse=True)
        
        logger.info(f"   📊 {total_pairs} paires USDT scannées")
        logger.info(f"   🚫 {rejected_pump} rejetés (anti-pump >+{MAX_PRICE_CHANGE_24H}%)")
        logger.info(f"   🚫 {rejected_volume} rejetés (volume < {MIN_VOLUME_USDT/1e6:.0f}M)")
        logger.info(f"   🎯 {len(candidates)} candidats après filtrage")
        
        if candidates:
            top3 = candidates[:3]
            for c in top3:
                logger.info(f"   ⭐ {c['symbol']}: {c['price_change_24h']:+.1f}% | "
                          f"Vol: {c['volume_usdt']/1e6:.1f}M | PreScore: {c['pre_score']}")
        
        return candidates[:self.top_n]
    
    # ─── ÉTAPE 2: Analyse technique approfondie MULTI-TIMEFRAME ──────────
    
    def deep_analyze(self, candidate):
        """
        Analyse technique approfondie d'un candidat.
        v2: Multi-timeframe (5m + 1h), filtres qualité, scoring réaliste.
        """
        symbol = candidate['symbol']
        
        # ═══ Récupérer klines 5m (timing précis) ═══
        klines_fast = self.client.public_get(
            f"{SCAN_API}/api/v3/klines",
            {"symbol": symbol, "interval": KLINES_INTERVAL_FAST, "limit": KLINES_LIMIT_FAST}
        )
        
        if not klines_fast or len(klines_fast) < 30:
            return None
        
        time.sleep(API_RATE_LIMIT_PAUSE)
        
        # ═══ Récupérer klines 1h (tendance de fond) ═══
        klines_slow = self.client.public_get(
            f"{SCAN_API}/api/v3/klines",
            {"symbol": symbol, "interval": KLINES_INTERVAL_SLOW, "limit": KLINES_LIMIT_SLOW}
        )
        
        # Parser les klines 5m
        closes = [float(k[4]) for k in klines_fast]
        highs = [float(k[2]) for k in klines_fast]
        lows = [float(k[3]) for k in klines_fast]
        volumes = [float(k[5]) for k in klines_fast]
        
        # Analyse tendance H1 (multi-timeframe)
        trend_data = None
        if klines_slow and len(klines_slow) >= 24:
            closes_h1 = [float(k[4]) for k in klines_slow]
            highs_h1 = [float(k[2]) for k in klines_slow]
            lows_h1 = [float(k[3]) for k in klines_slow]
            volumes_h1 = [float(k[5]) for k in klines_slow]
            trend_data = MultiTimeframeAnalyzer.analyze_trend(closes_h1, highs_h1, lows_h1, volumes_h1)
        
        # Détecter les patterns avec confirmation multi-timeframe
        result = SpyPatternDetector.detect(closes, highs, lows, volumes, trend_data)
        
        # Enrichir avec les données du candidat
        result['symbol'] = symbol
        result['price'] = candidate['price']
        result['volume_usdt'] = candidate['volume_usdt']
        result['price_change_24h'] = candidate['price_change_24h']
        result['pre_score'] = candidate['pre_score']
        result['timestamp'] = datetime.now().isoformat()
        result['btc_regime'] = self.btc_regime
        
        # v2: PAS de bonus pré-score artificiel
        # Le score des patterns est le score final (pas de gonflage)
        result['final_score'] = result['score']
        
        # DERNIER FILTRE: vérifier les confirmations minimum
        num_confirmations = result.get('num_confirmations', 0)
        if num_confirmations < SPY_MIN_CONFIRMATIONS and result['score'] > 0:
            result['final_score'] = min(result['score'], 65)  # Cap à 65 sans assez de confirmations
            if 'signals' not in result:
                result['signals'] = []
            result['signals'].append(f"LOW_CONFIRMATIONS({num_confirmations}/{SPY_MIN_CONFIRMATIONS})")
        
        return result
    
    # ─── ÉTAPE 3: Exécution d'un trade spy (v2 - SL/TP dynamiques) ────────
    
    def execute_spy_trade(self, opportunity):
        """
        Exécute un achat pour une opportunité exceptionnelle.
        v2: SL/TP basés sur l'ATR (adaptatifs à la volatilité).
        """
        symbol = opportunity['symbol']
        score = opportunity['final_score']
        pattern = opportunity['pattern']
        indicators = opportunity.get('indicators', {})
        
        logger.info(f"🎯 OPPORTUNITÉ VALIDÉE: {symbol}")
        logger.info(f"   Pattern: {pattern} | Score: {score} | BTC: {self.btc_regime}")
        logger.info(f"   Prix: {opportunity['price']} | Var 24h: {opportunity['price_change_24h']:+.1f}%")
        logger.info(f"   Signaux: {', '.join(opportunity.get('signals', []))}")
        
        # Vérifier le nombre total de positions
        total_pos = self._count_total_positions()
        if total_pos >= MAX_OPEN_POSITIONS:
            logger.warning(f"   ⚠️ Max positions atteint ({total_pos}/{MAX_OPEN_POSITIONS}), pas d'achat")
            return False
        
        # Vérifier le solde
        balance = self.client.get_balance("USDT")
        if balance < SPY_POSITION_SIZE + 5:  # Marge de sécurité
            logger.warning(f"   ⚠️ Solde insuffisant ({balance:.2f} USDT), pas d'achat")
            return False
        
        if self.dry_run:
            logger.info(f"   🏜️ DRY-RUN: Achat simulé de {SPY_POSITION_SIZE} USDT de {symbol}")
            self._log_opportunity(opportunity, executed=False, reason="dry-run")
            return False
        
        # Exécuter l'achat
        logger.info(f"   💰 Achat de {SPY_POSITION_SIZE} USDT de {symbol}...")
        
        try:
            order = self.client.market_buy(symbol, SPY_POSITION_SIZE)
            
            if order:
                # Calculer les détails
                filled_price = float(order.get('fills', [{}])[0].get('price', opportunity['price']))
                filled_qty = sum(float(f.get('qty', 0)) for f in order.get('fills', []))
                if filled_qty == 0:
                    filled_qty = float(order.get('executedQty', 0))
                if filled_price == 0:
                    cummulative = float(order.get('cummulativeQuoteQty', 0))
                    if filled_qty > 0:
                        filled_price = cummulative / filled_qty
                
                # ═══ SL/TP DYNAMIQUES basés sur ATR (v2) ═══
                atr = indicators.get('atr', 0)
                atr_pct = (atr / filled_price * 100) if filled_price > 0 else 1.5
                
                # SL = max(config SL, 1.5 * ATR%) → s'adapte à la volatilité
                dynamic_sl_pct = max(STOP_LOSS_PERCENT, atr_pct * 1.5)
                dynamic_sl_pct = min(dynamic_sl_pct, 4.0)  # Cap à 4% max
                
                # TP = max(config TP, 2.0 * ATR%) → ratio reward > risk
                dynamic_tp_pct = max(TAKE_PROFIT_PERCENT, atr_pct * 2.5)
                dynamic_tp_pct = min(dynamic_tp_pct, 6.0)  # Cap à 6% max
                
                stop_loss = filled_price * (1 - dynamic_sl_pct / 100)
                take_profit = filled_price * (1 + dynamic_tp_pct / 100)
                
                # Sauvegarder le trade spy
                spy_trades = self._load_spy_trades()
                spy_trades[symbol] = {
                    'entry_price': filled_price,
                    'quantity': filled_qty,
                    'stop_loss': stop_loss,
                    'take_profit': take_profit,
                    'sl_pct': round(dynamic_sl_pct, 2),
                    'tp_pct': round(dynamic_tp_pct, 2),
                    'side': 'BUY',
                    'order_id': order.get('orderId'),
                    'timestamp': datetime.now().isoformat(),
                    'pattern': pattern,
                    'score': score,
                    'source': 'MARKET_SPY',
                    'max_price': filled_price,
                    'max_pnl': 0.0,
                    'btc_regime': self.btc_regime,
                    'signals': opportunity.get('signals', []),
                    'indicators': indicators
                }
                self._save_spy_trades(spy_trades)
                
                # AUSSI écrire dans positions.json pour que le bot principal 
                # puisse gérer les SL/TP/trailing
                self._register_in_main_positions(symbol, spy_trades[symbol])
                
                self.trades_executed += 1
                logger.info(f"   ✅ ACHAT RÉUSSI: {filled_qty} {symbol} @ {filled_price}")
                logger.info(f"      SL: {stop_loss:.6f} (-{dynamic_sl_pct:.1f}%) | "
                          f"TP: {take_profit:.6f} (+{dynamic_tp_pct:.1f}%)")
                logger.info(f"      Ratio R:R = 1:{dynamic_tp_pct/dynamic_sl_pct:.1f}")
                
                self._log_opportunity(opportunity, executed=True)
                return True
            else:
                logger.error(f"   ❌ Ordre échoué pour {symbol}")
                self._log_opportunity(opportunity, executed=False, reason="order_failed")
                return False
                
        except Exception as e:
            logger.error(f"   ❌ Erreur achat {symbol}: {e}")
            traceback.print_exc()
            self._log_opportunity(opportunity, executed=False, reason=str(e))
            return False
    
    def _register_in_main_positions(self, symbol, trade_data):
        """Enregistre le trade spy dans positions.json du bot principal"""
        try:
            positions = {}
            if os.path.exists(POSITIONS_FILE):
                with open(POSITIONS_FILE, 'r', encoding='utf-8') as f:
                    positions = json.load(f)
            
            # Ajouter la position spy avec un marqueur
            positions[symbol] = {
                'entry_price': trade_data['entry_price'],
                'quantity': trade_data['quantity'],
                'stop_loss': trade_data['stop_loss'],
                'take_profit': trade_data['take_profit'],
                'side': 'BUY',
                'order_id': trade_data['order_id'],
                'timestamp': trade_data['timestamp'],
                'pattern': trade_data['pattern'],
                'source': 'MARKET_SPY',
                'max_price': trade_data['max_price'],
                'max_pnl': 0.0,
                'bb_breakout_entry': False
            }
            
            with open(POSITIONS_FILE, 'w', encoding='utf-8') as f:
                json.dump(positions, f, indent=2, default=str)
            
            logger.info(f"   📝 Position enregistrée dans positions.json (bot principal)")
        except Exception as e:
            logger.error(f"   ⚠️ Erreur enregistrement position principale: {e}")
    
    def _log_opportunity(self, opportunity, executed=False, reason=None):
        """Log une opportunité dans l'historique"""
        entry = {
            'timestamp': datetime.now().isoformat(),
            'symbol': opportunity['symbol'],
            'pattern': opportunity['pattern'],
            'score': opportunity['final_score'],
            'price': opportunity['price'],
            'price_change_24h': opportunity['price_change_24h'],
            'volume_usdt': opportunity['volume_usdt'],
            'indicators': opportunity.get('indicators', {}),
            'signals': opportunity.get('signals', []),
            'executed': executed,
            'reason': reason
        }
        
        self.opportunities_history.append(entry)
        self.total_opportunities += 1
        
        # Sauvegarder le log
        try:
            history = []
            if os.path.exists(SPY_LOG_FILE):
                with open(SPY_LOG_FILE, 'r', encoding='utf-8') as f:
                    history = json.load(f)
            
            history.append(entry)
            
            # Garder les 500 dernières opportunités
            if len(history) > 500:
                history = history[-500:]
            
            with open(SPY_LOG_FILE, 'w', encoding='utf-8') as f:
                json.dump(history, f, indent=2, default=str)
        except Exception:
            pass
    
    # ─── CYCLE DE SCAN PRINCIPAL ───────────────────────────────────────────
    
    def run_scan_cycle(self):
        """Exécute un cycle complet de scan (v2 avec BTC regime + multi-TF)"""
        self.scan_count += 1
        start_time = time.time()
        
        logger.info(f"\n{'─' * 50}")
        logger.info(f"🔄 Cycle #{self.scan_count} - {datetime.now().strftime('%H:%M:%S')}")
        logger.info(f"{'─' * 50}")
        
        # Recharger la watchlist (peut avoir changé)
        self.watchlist = self._load_watchlist()
        
        # ═══ ÉTAPE 0: Vérifier le régime marché (BTC) ═══
        self.btc_regime, self.btc_info = MarketRegimeFilter.check_btc_regime(self.client)
        regime_emoji = {
            'BULLISH': '🟢', 'NEUTRAL': '🟡', 
            'BEARISH': '🟠', 'STRONG_BEARISH': '🔴'
        }.get(self.btc_regime, '⚪')
        
        logger.info(f"   {regime_emoji} Régime BTC: {self.btc_regime}")
        if self.btc_info:
            logger.info(f"      BTC RSI={self.btc_info.get('btc_rsi_h1', '?')} "
                       f"EMA={self.btc_info.get('btc_ema_diff', '?')}% "
                       f"Mom4h={self.btc_info.get('btc_mom_4h', '?')}%")
        
        # En marché FORTEMENT baissier → pas d'achat du tout
        if self.btc_regime == 'STRONG_BEARISH':
            logger.warning("   🛑 Marché fortement baissier → AUCUN ACHAT ce cycle")
            return []
        
        # En marché baissier → score minimum rehaussé
        effective_min_score = self.min_score_buy
        if self.btc_regime == 'BEARISH':
            effective_min_score = max(self.min_score_buy, 88)  # Plus exigeant en bear
            logger.info(f"   ⚠️ Marché bear → score minimum relevé à {effective_min_score}")
        
        # Étape 1: Scan rapide
        candidates = self.scan_market()
        
        if not candidates:
            logger.info("   Aucun candidat intéressant trouvé")
            return []
        
        # Étape 2: Analyse approfondie multi-timeframe
        logger.info(f"\n📈 Analyse multi-timeframe de {len(candidates)} candidats...")
        analyzed = []
        quality_rejected = 0
        
        for i, candidate in enumerate(candidates):
            result = self.deep_analyze(candidate)
            
            if result:
                if result['pattern'] == 'QUALITY_REJECTED':
                    quality_rejected += 1
                    flags = result.get('quality_flags', [])
                    logger.info(f"   ⛔ {result['symbol']:12s} | REJETÉ: {', '.join(flags)}")
                elif result['score'] > 0:
                    analyzed.append(result)
                    
                    # Afficher les résultats
                    emoji = "🟢" if result['final_score'] >= effective_min_score else "🟡" if result['final_score'] >= 60 else "⚪"
                    trend_info = ""
                    if result.get('indicators', {}).get('trend_label'):
                        trend_info = f" | H1:{result['indicators']['trend_label']}"
                    
                    logger.info(
                        f"   {emoji} {result['symbol']:12s} | "
                        f"{result['pattern']:18s} | "
                        f"Score: {result['final_score']:5.1f} | "
                        f"RSI: {result['indicators'].get('rsi', 0):5.1f} | "
                        f"BB: {result['indicators'].get('bb_position', 0):.2f} | "
                        f"Mom3: {result['indicators'].get('momentum_3', 0):+.2f}% | "
                        f"Vol: {result['indicators'].get('volume_spike', 0):.1f}x"
                        f"{trend_info}"
                    )
            
            # Rate limiting (2 API calls par candidat maintenant)
            if i < len(candidates) - 1:
                time.sleep(API_RATE_LIMIT_PAUSE)
        
        # Trier par score
        analyzed.sort(key=lambda x: x['final_score'], reverse=True)
        
        if quality_rejected > 0:
            logger.info(f"   ⛔ {quality_rejected} candidats rejetés par filtres qualité")
        
        # Étape 3: Exécuter les trades si opportunités exceptionnelles
        trades_this_cycle = 0
        for opp in analyzed:
            if opp['final_score'] >= effective_min_score:
                # Vérification finale: assez de confirmations?
                num_conf = opp.get('num_confirmations', 0)
                if num_conf < SPY_MIN_CONFIRMATIONS:
                    logger.info(f"   ⚠️ {opp['symbol']}: Score OK ({opp['final_score']:.0f}) "
                              f"mais seulement {num_conf}/{SPY_MIN_CONFIRMATIONS} confirmations")
                    self._log_opportunity(opp, executed=False, reason=f"confirmations_low({num_conf})")
                    continue
                
                success = self.execute_spy_trade(opp)
                if success:
                    trades_this_cycle += 1
                    if trades_this_cycle >= SPY_MAX_TRADES_PER_CYCLE:
                        logger.info(f"   ⚠️ Max {SPY_MAX_TRADES_PER_CYCLE} achat(s) par cycle atteint")
                        break
            else:
                # Logger les bonnes opportunités même sans achat
                if opp['final_score'] >= 55:
                    self._log_opportunity(opp, executed=False, reason="score_insufficient")
        
        elapsed = time.time() - start_time
        logger.info(f"\n📊 Cycle #{self.scan_count} terminé en {elapsed:.1f}s")
        logger.info(f"   BTC: {self.btc_regime} | Analysés: {len(analyzed)} | "
                    f"Rejetés qualité: {quality_rejected} | Trades: {trades_this_cycle}")
        
        return analyzed
    
    # ─── GESTION DES POSITIONS SPY ────────────────────────────────────────
    
    def check_spy_positions(self):
        """
        Vérifie les positions spy et gère SL/TP/trailing.
        Le bot principal PEUT aussi les gérer via positions.json,
        mais on fait un double check ici par sécurité.
        """
        spy_trades = self._load_spy_trades()
        if not spy_trades:
            return
        
        logger.info(f"\n📋 Vérification de {len(spy_trades)} positions spy...")
        
        to_remove = []
        
        for symbol, trade in spy_trades.items():
            try:
                # Récupérer le prix actuel
                price_data = self.client.public_get(
                    f"{SCAN_API}/api/v3/ticker/price", {"symbol": symbol}
                )
                if not price_data:
                    continue
                
                current_price = float(price_data['price'])
                entry_price = trade['entry_price']
                pnl_pct = ((current_price - entry_price) / entry_price) * 100
                
                # Mettre à jour max_price
                if current_price > trade.get('max_price', entry_price):
                    trade['max_price'] = current_price
                    trade['max_pnl'] = pnl_pct
                
                # Check Stop Loss
                if current_price <= trade['stop_loss']:
                    logger.warning(f"   🔴 SL HIT: {symbol} @ {current_price} "
                                 f"(SL: {trade['stop_loss']:.6f}, PnL: {pnl_pct:+.2f}%)")
                    # La vente sera gérée par le bot principal via positions.json
                    # On marque juste pour le log
                    to_remove.append(symbol)
                    continue
                
                # Check Take Profit
                if current_price >= trade['take_profit']:
                    logger.info(f"   🟢 TP HIT: {symbol} @ {current_price} "
                              f"(TP: {trade['take_profit']:.6f}, PnL: {pnl_pct:+.2f}%)")
                    to_remove.append(symbol)
                    continue
                
                # Trailing stop
                if ENABLE_TRAILING_STOP and pnl_pct >= TRAILING_STOP_ACTIVATION:
                    trailing_sl = trade['max_price'] * (1 - TRAILING_STOP_DISTANCE / 100)
                    if trailing_sl > trade['stop_loss']:
                        trade['stop_loss'] = trailing_sl
                        logger.info(f"   📈 Trailing SL relevé: {symbol} → {trailing_sl:.6f}")
                
                # Status
                emoji = "🟢" if pnl_pct > 0 else "🔴"
                logger.info(f"   {emoji} {symbol}: PnL {pnl_pct:+.2f}% | "
                          f"Max: {trade.get('max_pnl', 0):+.2f}% | "
                          f"SL: {trade['stop_loss']:.6f}")
                
            except Exception as e:
                logger.error(f"   ⚠️ Erreur check {symbol}: {e}")
        
        # Retirer les positions fermées
        for symbol in to_remove:
            if symbol in spy_trades:
                del spy_trades[symbol]
        
        self._save_spy_trades(spy_trades)
    
    # ─── AFFICHAGE DU RÉSUMÉ ──────────────────────────────────────────────
    
    def print_summary(self):
        """Affiche un résumé de la session spy"""
        logger.info("\n" + "═" * 60)
        logger.info("📊 RÉSUMÉ SESSION MARKET SPY")
        logger.info("═" * 60)
        logger.info(f"   Scans effectués: {self.scan_count}")
        logger.info(f"   Opportunités détectées: {self.total_opportunities}")
        logger.info(f"   Trades exécutés: {self.trades_executed}")
        
        spy_trades = self._load_spy_trades()
        if spy_trades:
            logger.info(f"   Positions spy actives: {len(spy_trades)}")
            for sym, t in spy_trades.items():
                logger.info(f"      {sym}: {t['pattern']} | Entry: {t['entry_price']}")
        
        bot_pos = self._load_positions()
        logger.info(f"   Positions bot principal: {len(bot_pos)}")
        logger.info(f"   Total positions: {len(bot_pos) + len(spy_trades)}/{MAX_OPEN_POSITIONS}")
        logger.info("═" * 60)


# ═══════════════════════════════════════════════════════════════════════════════
# POINT D'ENTRÉE
# ═══════════════════════════════════════════════════════════════════════════════

def main():
    parser = argparse.ArgumentParser(description="🕵️ Market Spy - Scanner de marché crypto")
    parser.add_argument('--once', action='store_true', help='Un seul scan puis quitte')
    parser.add_argument('--dry-run', action='store_true', help='Mode simulation (pas d achat)')
    parser.add_argument('--min-score', type=int, default=SPY_MIN_SCORE_BUY, 
                       help=f'Score minimum pour achat (défaut: {SPY_MIN_SCORE_BUY})')
    parser.add_argument('--top', type=int, default=DEEP_ANALYSIS_TOP_N,
                       help=f'Nombre de candidats à analyser (défaut: {DEEP_ANALYSIS_TOP_N})')
    parser.add_argument('--interval', type=int, default=SCAN_INTERVAL_SECONDS,
                       help=f'Intervalle entre scans en secondes (défaut: {SCAN_INTERVAL_SECONDS})')
    
    args = parser.parse_args()
    
    spy = MarketSpy(
        dry_run=args.dry_run,
        min_score=args.min_score,
        top_n=args.top
    )
    
    if args.once:
        # Mode single scan
        results = spy.run_scan_cycle()
        spy.check_spy_positions()
        spy.print_summary()
        return
    
    # Mode continu
    logger.info(f"\n🔄 Mode continu - Scan toutes les {args.interval}s (Ctrl+C pour arrêter)")
    
    try:
        while True:
            try:
                results = spy.run_scan_cycle()
                spy.check_spy_positions()
                
                # Afficher résumé toutes les 10 scans
                if spy.scan_count % 10 == 0:
                    spy.print_summary()
                
                logger.info(f"\n⏳ Prochain scan dans {args.interval}s...")
                time.sleep(args.interval)
                
            except KeyboardInterrupt:
                raise
            except Exception as e:
                logger.error(f"❌ Erreur dans le cycle: {e}")
                traceback.print_exc()
                time.sleep(10)  # Pause avant retry
    
    except KeyboardInterrupt:
        logger.info("\n\n🛑 Arrêt du Market Spy...")
        spy.print_summary()
        logger.info("Au revoir! 👋")


if __name__ == "__main__":
    main()
