#!/usr/bin/env python3
"""
═══════════════════════════════════════════════════════════════════════════════
🧠 MARKET BEHAVIOR DETECTOR — Régime comportemental des participants
═══════════════════════════════════════════════════════════════════════════════

Au lieu de mesurer le marché par des indicateurs techniques (RSI, EMA, volume),
ce module mesure le COMPORTEMENT HUMAIN des participants du marché à travers
la qualité des surges détectés par le spy.

PRINCIPE:
  En période de conviction (marché directionnel), les surges ont du "follow-through":
  le prix continue à monter après le spike initial. Les investisseurs restent.
  
  En période de spéculation (incertitude géopolitique, navigation à vue),
  les surges sont des coups de sonde: spike puis reversal immédiat.
  Les investisseurs entrent et sortent dans la même fenêtre de 10-30 secondes.

MÉTRIQUES:
  1. Follow-Through Rate (FTR): % des trades récents où le prix a monté
     au-delà de +0.1% après l'entrée (max_pnl > 0.1%)
  2. Instant Reversal Ratio (IRR): % des trades récents finissant en
     INSTANT_REVERSAL (max_pnl = 0.00%, hold < 45s)
  3. Avg Winner Hold vs Avg Loser Hold: en marché sain, les winners
     durent plus longtemps. En clapot, tout est court.

RÉGIMES:
  CONVICTION    — FTR ≥ 50%, IRR < 25%  → Trading normal
  SPECULATION   — FTR 25-50% ou IRR 25-40% → Sélectivité accrue
  PANIQUE       — FTR < 25% ou IRR ≥ 40% → Observation uniquement

═══════════════════════════════════════════════════════════════════════════════
"""

import os
import json
import time
import logging
from datetime import datetime, timezone
from collections import deque

logger = logging.getLogger('market_behavior')

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
BEHAVIOR_STATE_FILE = os.path.join(SCRIPT_DIR, "market_behavior_state.json")

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

# Fenêtre glissante d'analyse
WINDOW_SIZE = 30                # Nombre de trades récents analysés

# Seuils Follow-Through Rate
FTR_CONVICTION_MIN = 0.50       # ≥ 50% des trades ont vu max_pnl > 0.1%
FTR_SPECULATION_MIN = 0.25      # ≥ 25% → spéculation, < 25% → panique

# Seuils Instant Reversal Ratio
IRR_CONVICTION_MAX = 0.25       # < 25% d'INSTANT_REVERSAL → conviction
IRR_PANIQUE_MIN = 0.40          # ≥ 40% → panique

# Seuil surge Follow-Through de NOS propres trades
# surge_FT = % des positions ouvertes dont le prix a suivi après détection
# Si < 30%, nos entrées sont trop tardives → brider à SPECULATION même si FTR global est bon
SURGE_FT_CONVICTION_MIN = 0.30  # 🔧 FIX 12/04: < 30% surge_FT = on achète après la mèche

# Seuil min max_pnl pour considérer un "follow-through"
FT_MIN_PNL = 0.10               # Le prix a monté d'au moins 0.1% après l'entrée

# Follow-through post-surge (surges non tradés)
# Mesure si les surges détectés continuent à monter 60s après
SURGE_FT_WINDOW = 60            # Secondes après détection
SURGE_FT_MIN_CONTINUATION = 0.3 # +0.3% supplémentaire après le surge initial

# Transition douce: il faut N trades dans le nouveau régime pour basculer
REGIME_STABILITY_COUNT = 5      # Anti-oscillation


class BehaviorRegime:
    """Les trois régimes comportementaux."""
    CONVICTION = "CONVICTION"
    SPECULATION = "SPECULATION"
    PANIQUE = "PANIQUE"


class MarketBehaviorDetector:
    """
    Détecteur de régime comportemental basé sur la qualité des trades spy.
    
    Consomme les résultats des trades (via feed_trade_result) et les surges
    observés (via feed_surge_observation) pour qualifier le comportement
    collectif des participants du marché.
    """
    
    def __init__(self):
        # Historique glissant des trades récents
        self.trade_results = deque(maxlen=WINDOW_SIZE * 2)  # Garder 2x pour lisser
        
        # Observations de surges (tradés ou non) pour le Follow-Through post-surge
        self.surge_observations = deque(maxlen=100)
        
        # Régime actuel
        self.current_regime = BehaviorRegime.CONVICTION
        self.regime_since = time.time()
        self.regime_consecutive = 0  # Nombre de mesures consécutives dans ce régime
        
        # Métriques calculées
        self.metrics = {
            'ftr': 1.0,            # Follow-Through Rate (commence optimiste)
            'irr': 0.0,            # Instant Reversal Ratio
            'avg_winner_hold': 0,  # Secondes
            'avg_loser_hold': 0,   # Secondes
            'sample_size': 0,      # Nombre de trades dans la fenêtre
            'surge_ft_rate': 1.0,  # Follow-through rate des surges observés
            'surge_observations': 0,
        }
        
        # Chargement état persistant
        self._load_state()
        
        logger.info(f"🧠 MarketBehaviorDetector initialisé — régime: {self.current_regime} "
                     f"(FTR={self.metrics['ftr']:.0%}, IRR={self.metrics['irr']:.0%}, "
                     f"samples={self.metrics['sample_size']})")
    
    # ─── ALIMENTATION EN DONNÉES ────────────────────────────────────────────
    
    def feed_trade_result(self, trade_data):
        """
        Appelé après chaque trade spy terminé (vente).
        
        trade_data: dict avec au minimum:
            - max_pnl: float (% max atteint pendant le trade)
            - pnl_pct: float (% final à la vente)
            - hold_seconds: float
            - exit_reason: str
            - symbol: str
            - timestamp: float (epoch)
        """
        self.trade_results.append({
            'max_pnl': trade_data.get('max_pnl', 0),
            'pnl_pct': trade_data.get('pnl_pct', 0),
            'hold_seconds': trade_data.get('hold_seconds', 0),
            'exit_reason': trade_data.get('exit_reason', ''),
            'symbol': trade_data.get('symbol', ''),
            'timestamp': trade_data.get('timestamp', time.time()),
            'had_follow_through': trade_data.get('max_pnl', 0) >= FT_MIN_PNL,
            'was_instant_reversal': 'INSTANT_REVERSAL' in trade_data.get('exit_reason', ''),
        })
        
        # Recalculer les métriques et le régime
        self._recalculate()
        self._save_state()
    
    def feed_surge_observation(self, symbol, surge_price, check_price, elapsed_seconds):
        """
        Appelé pour mesurer le "follow-through" d'un surge APRÈS sa détection.
        
        Le spy peut vérifier le prix 60s après un surge (tradé ou non) pour
        savoir si le mouvement a continué. C'est le thermomètre le plus pur
        du comportement collectif.
        
        symbol: str
        surge_price: float (prix au moment de la détection)
        check_price: float (prix N secondes plus tard)
        elapsed_seconds: float
        """
        if surge_price <= 0:
            return
        
        continuation_pct = ((check_price - surge_price) / surge_price) * 100
        had_continuation = continuation_pct >= SURGE_FT_MIN_CONTINUATION
        
        self.surge_observations.append({
            'symbol': symbol,
            'surge_price': surge_price,
            'check_price': check_price,
            'continuation_pct': continuation_pct,
            'had_continuation': had_continuation,
            'elapsed': elapsed_seconds,
            'timestamp': time.time(),
        })
        
        # Mettre à jour la métrique surge FT
        recent_obs = [o for o in self.surge_observations
                      if time.time() - o['timestamp'] < 3600]  # Dernière heure
        if recent_obs:
            self.metrics['surge_ft_rate'] = (
                sum(1 for o in recent_obs if o['had_continuation']) / len(recent_obs)
            )
            self.metrics['surge_observations'] = len(recent_obs)
    
    # ─── CALCUL DU RÉGIME ────────────────────────────────────────────────────
    
    def _recalculate(self):
        """Recalcule métriques et régime à partir de la fenêtre glissante."""
        # Prendre les N derniers trades
        recent = list(self.trade_results)[-WINDOW_SIZE:]
        n = len(recent)
        
        if n < 5:
            # Pas assez de données, rester en mode courant
            self.metrics['sample_size'] = n
            return
        
        # 1. Follow-Through Rate
        ft_count = sum(1 for t in recent if t['had_follow_through'])
        ftr = ft_count / n
        
        # 2. Instant Reversal Ratio
        ir_count = sum(1 for t in recent if t['was_instant_reversal'])
        irr = ir_count / n
        
        # 3. Hold times winners vs losers
        winners = [t for t in recent if t['pnl_pct'] > 0]
        losers = [t for t in recent if t['pnl_pct'] < 0]
        avg_winner_hold = (sum(t['hold_seconds'] for t in winners) / len(winners)) if winners else 0
        avg_loser_hold = (sum(t['hold_seconds'] for t in losers) / len(losers)) if losers else 0
        
        self.metrics.update({
            'ftr': round(ftr, 3),
            'irr': round(irr, 3),
            'avg_winner_hold': round(avg_winner_hold, 1),
            'avg_loser_hold': round(avg_loser_hold, 1),
            'sample_size': n,
        })
        
        # Déterminer le régime candidat
        candidate = self._classify_regime(ftr, irr, self.metrics.get('surge_ft_rate', 1.0))
        
        # Transition avec stabilité (anti-oscillation)
        if candidate == self.current_regime:
            self.regime_consecutive += 1
        else:
            self.regime_consecutive += 1
            if self.regime_consecutive >= REGIME_STABILITY_COUNT:
                old = self.current_regime
                self.current_regime = candidate
                self.regime_since = time.time()
                self.regime_consecutive = 0
                logger.info(
                    f"🧠 CHANGEMENT DE RÉGIME: {old} → {candidate} "
                    f"(FTR={ftr:.0%}, IRR={irr:.0%}, "
                    f"surge_FT={self.metrics['surge_ft_rate']:.0%}, "
                    f"n={n})"
                )
            # Si le candidat est PLUS restrictif (PANIQUE), basculer immédiatement
            # pour protéger le capital sans attendre la stabilité
            elif self._severity(candidate) > self._severity(self.current_regime):
                old = self.current_regime
                self.current_regime = candidate
                self.regime_since = time.time()
                self.regime_consecutive = 0
                logger.warning(
                    f"🧠⚠️ RÉGIME DÉGRADÉ: {old} → {candidate} (immédiat) "
                    f"(FTR={ftr:.0%}, IRR={irr:.0%}, n={n})"
                )
    
    def _classify_regime(self, ftr, irr, surge_ft_rate=1.0):
        """Classifie le régime à partir des métriques."""
        # PANIQUE: surges sans follow-through OU trop d'INSTANT_REVERSAL
        if ftr < FTR_SPECULATION_MIN or irr >= IRR_PANIQUE_MIN:
            return BehaviorRegime.PANIQUE

        # CONVICTION: bon follow-through ET peu de reversals ET surges qui suivent
        if ftr >= FTR_CONVICTION_MIN and irr < IRR_CONVICTION_MAX and surge_ft_rate >= SURGE_FT_CONVICTION_MIN:
            return BehaviorRegime.CONVICTION

        # SPECULATION: entre les deux (ou surge_FT trop faible malgré bon FTR global)
        return BehaviorRegime.SPECULATION
    
    @staticmethod
    def _severity(regime):
        """Score de sévérité pour transitions immédiates vers le pire."""
        return {
            BehaviorRegime.CONVICTION: 0,
            BehaviorRegime.SPECULATION: 1,
            BehaviorRegime.PANIQUE: 2,
        }.get(regime, 0)
    
    # ─── API PUBLIQUE ────────────────────────────────────────────────────────
    
    def get_regime(self):
        """Retourne le régime actuel et les métriques."""
        return {
            'regime': self.current_regime,
            'regime_since': self.regime_since,
            'regime_duration_min': round((time.time() - self.regime_since) / 60, 1),
            'metrics': dict(self.metrics),
            'should_trade': self.current_regime != BehaviorRegime.PANIQUE,
            'position_multiplier': self.get_position_multiplier(),
        }
    
    def get_position_multiplier(self):
        """
        Multiplicateur de taille de position selon le régime.
        
        CONVICTION  → 1.0  (100%, trading normal)
        SPECULATION → 0.5  (50%, taille réduite, sélectivité)
        PANIQUE     → 0.0  (0%, observation uniquement)
        """
        return {
            BehaviorRegime.CONVICTION: 1.0,
            BehaviorRegime.SPECULATION: 0.5,
            BehaviorRegime.PANIQUE: 0.0,
        }.get(self.current_regime, 0.5)
    
    def should_trade(self):
        """True si le régime permet de trader."""
        return self.current_regime != BehaviorRegime.PANIQUE
    
    def get_min_surge_strength(self):
        """
        Surge strength minimum adapté au comportement des participants.
        
        En CONVICTION, on fait confiance aux surges normaux.
        En SPECULATION, on exige des surges plus forts (filtrer le bruit).
        En PANIQUE, on ne trade pas (mais on retourne le seuil pour le log).
        """
        return {
            BehaviorRegime.CONVICTION: 1.0,   # Normal
            BehaviorRegime.SPECULATION: 1.1,   # 🔧 TEST 17/04: 1.3→1.1 — test seuil abaissé cohérent avec BREAKOUT 1.1%
            BehaviorRegime.PANIQUE: 3.0,       # Seuil théorique (pas de trade)
        }.get(self.current_regime, 1.5)
    
    def get_status_line(self):
        """Ligne de log compacte pour le statut."""
        m = self.metrics
        regime = self.current_regime
        emoji = {'CONVICTION': '🟢', 'SPECULATION': '🟡', 'PANIQUE': '🔴'}.get(regime, '⚪')
        duration = (time.time() - self.regime_since) / 60
        return (
            f"{emoji} Comportement: {regime} "
            f"(FTR={m['ftr']:.0%} IRR={m['irr']:.0%} "
            f"surge_FT={m['surge_ft_rate']:.0%} "
            f"n={m['sample_size']} "
            f"depuis {duration:.0f}min)"
        )
    
    # ─── PERSISTANCE ─────────────────────────────────────────────────────────
    
    def _save_state(self):
        """Sauvegarde l'état sur disque pour survie aux redémarrages."""
        try:
            state = {
                'regime': self.current_regime,
                'regime_since': self.regime_since,
                'regime_consecutive': self.regime_consecutive,
                'metrics': self.metrics,
                'trade_results': list(self.trade_results),
                'surge_observations': list(self.surge_observations)[-50:],
                'saved_at': datetime.now(timezone.utc).isoformat(),
            }
            tmp = BEHAVIOR_STATE_FILE + '.tmp'
            with open(tmp, 'w', encoding='utf-8') as f:
                json.dump(state, f, indent=2, default=str)
            os.replace(tmp, BEHAVIOR_STATE_FILE)
        except Exception as e:
            logger.debug(f"Behavior state save error: {e}")
    
    def _load_state(self):
        """Charge l'état depuis le disque."""
        try:
            if not os.path.exists(BEHAVIOR_STATE_FILE):
                # Premier démarrage: bootstrapper depuis l'historique spy existant
                self._bootstrap_from_history()
                return
            
            with open(BEHAVIOR_STATE_FILE, 'r', encoding='utf-8') as f:
                state = json.load(f)
            
            self.current_regime = state.get('regime', BehaviorRegime.CONVICTION)
            self.regime_since = state.get('regime_since', time.time())
            self.regime_consecutive = state.get('regime_consecutive', 0)
            self.metrics = state.get('metrics', self.metrics)
            
            for t in state.get('trade_results', []):
                self.trade_results.append(t)
            for o in state.get('surge_observations', []):
                self.surge_observations.append(o)
            
            logger.info(f"   📂 Behavior state chargé: régime={self.current_regime}, "
                        f"samples={len(self.trade_results)}")
        except Exception as e:
            logger.debug(f"Behavior state load error: {e}")
            self._bootstrap_from_history()
    
    def _bootstrap_from_history(self):
        """
        Bootstrap initial: charger espion_history.json pour avoir un état
        de départ basé sur les trades réels récents.
        """
        history_file = os.path.join(SCRIPT_DIR, "espion_history.json")
        try:
            if not os.path.exists(history_file):
                logger.info("   📂 Pas d'historique spy — démarrage en CONVICTION (optimiste)")
                return
            
            with open(history_file, 'r', encoding='utf-8') as f:
                history = json.load(f)
            
            if not history:
                return
            
            # Prendre les N derniers trades
            for trade in history[-WINDOW_SIZE:]:
                self.trade_results.append({
                    'max_pnl': trade.get('max_pnl', 0),
                    'pnl_pct': trade.get('pnl_pct', 0),
                    'hold_seconds': trade.get('hold_seconds', 0),
                    'exit_reason': trade.get('exit_reason', ''),
                    'symbol': trade.get('symbol', ''),
                    'timestamp': time.time(),  # Pas de timestamp exact dans l'historique
                    'had_follow_through': trade.get('max_pnl', 0) >= FT_MIN_PNL,
                    'was_instant_reversal': 'INSTANT_REVERSAL' in trade.get('exit_reason', ''),
                })
            
            # Calculer immédiatement
            self._recalculate()
            logger.info(f"   📂 Bootstrap depuis espion_history: {len(self.trade_results)} trades → "
                        f"régime={self.current_regime}")
            self._save_state()
            
        except Exception as e:
            logger.debug(f"Bootstrap error: {e}")


# ═══════════════════════════════════════════════════════════════════════════════
# SINGLETON — Une seule instance partagée
# ═══════════════════════════════════════════════════════════════════════════════

_detector_instance = None

def get_behavior_detector():
    """Retourne l'instance singleton du détecteur comportemental."""
    global _detector_instance
    if _detector_instance is None:
        _detector_instance = MarketBehaviorDetector()
    return _detector_instance
