# source: https://raw.githubusercontent.com/johan1727/botrading-/9c5636fb434674b596e81b99f82c4b516e2f64a5/user_data/strategies/GeminiStrategy.py
"""
Github_johan1727_botrading___GeminiStrategy__20260511_222253.py
Estrategia custom para Freqtrade que usa Google Gemini AI
como motor de decisión de trading.

Autor: GeminiTradingBot
Version: 1.0.0
"""

import json
import logging
import math
import os
import random
import time
import threading
import requests
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional

try:
    from dotenv import load_dotenv
    load_dotenv(Path(__file__).parent.parent.parent / ".env")
except ImportError:
    pass

# from google import genai  # Eliminado - solo usamos Groq
# from google.genai import types as genai_types  # Eliminado - solo usamos Groq
try:
    from groq import Groq as GroqClient
except ImportError:
    GroqClient = None
import pandas as pd
from pandas import DataFrame

from freqtrade.strategy import DecimalParameter, IStrategy, IntParameter

logger = logging.getLogger(__name__)

# Configuración de IA - Solo Groq
MIN_CONFIDENCE = int(os.getenv("MIN_CONFIDENCE", "50"))
RATE_LIMIT_SECONDS = 5.0

# Pool de APIs - Solo Groq para evitar bloqueos
GROQ_API_POOL = [
    {"key": os.getenv("GROQ_KEY_1", ""), "model": "llama-3.1-8b-instant", "daily_limit": 14400, "label": "Groq-1"},
    {"key": os.getenv("GROQ_KEY_2", ""), "model": "llama-3.1-8b-instant", "daily_limit": 14400, "label": "Groq-2"},
    {"key": os.getenv("GROQ_KEY_3", ""), "model": "llama-3.1-8b-instant", "daily_limit": 14400, "label": "Groq-3"},
]

# Telegram directo (sin python-telegram-bot)
TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")

# CryptoPanic — noticias crypto en tiempo real (tier gratuito, sin API key)
CRYPTO_NEWS_CACHE: dict = {"data": [], "sentiment": 50, "ts": 0.0}
CRYPTO_NEWS_TTL = 900  # refrescar cada 15 minutos

FEAR_GREED_CACHE: dict = {"value": 50, "label": "Neutral", "ts": 0.0}
FEAR_GREED_TTL = 3600  # refrescar cada hora

# Reddit sentiment — menciones en r/cryptocurrency (gratis, sin auth)
REDDIT_SENTIMENT_CACHE: dict = {}  # {coin: {"score": 50, "mentions": 0, "ts": 0.0}}
REDDIT_SENTIMENT_TTL = 1800  # refrescar cada 30 minutos

# CoinGecko trending — coins trending socialmente (gratis)
TRENDING_CACHE: dict = {"coins": [], "ts": 0.0}
TRENDING_TTL = 3600  # refrescar cada hora


def _fetch_fear_greed() -> tuple:
    """Obtiene el Fear & Greed Index actual. Caché 1 hora. Retorna (valor, label)."""
    global FEAR_GREED_CACHE
    now = time.time()
    if now - FEAR_GREED_CACHE["ts"] > FEAR_GREED_TTL:
        try:
            resp = requests.get("https://api.alternative.me/fng/?limit=1", timeout=5)
            if resp.status_code == 200:
                data = resp.json()["data"][0]
                FEAR_GREED_CACHE["value"] = int(data["value"])
                FEAR_GREED_CACHE["label"] = data["value_classification"]
                FEAR_GREED_CACHE["ts"] = now
        except Exception:
            pass
    return FEAR_GREED_CACHE["value"], FEAR_GREED_CACHE["label"]


def _fetch_crypto_news(coin: str) -> tuple:
    """Obtiene titulares + sentiment de noticias. Caché 15 min. Retorna (headlines_str, sentiment_score)."""
    global CRYPTO_NEWS_CACHE
    now = time.time()
    if now - CRYPTO_NEWS_CACHE["ts"] > CRYPTO_NEWS_TTL:
        try:
            coin_symbol = coin.split("/")[0].lower()
            url = f"https://cryptopanic.com/api/free/v1/posts/?auth_token=anonymous&currencies={coin_symbol}&kind=news&public=true"
            resp = requests.get(url, timeout=5)
            if resp.status_code == 200:
                items = resp.json().get("results", [])[:5]
                CRYPTO_NEWS_CACHE["data"] = [i.get("title", "") for i in items]
                # Calcular sentiment de votos bullish/bearish
                bullish = sum(i.get("votes", {}).get("positive", 0) for i in items)
                bearish = sum(i.get("votes", {}).get("negative", 0) for i in items)
                total_votes = bullish + bearish
                if total_votes > 0:
                    CRYPTO_NEWS_CACHE["sentiment"] = int(bullish / total_votes * 100)
                else:
                    CRYPTO_NEWS_CACHE["sentiment"] = 50
                CRYPTO_NEWS_CACHE["ts"] = now
        except Exception:
            pass
    headlines = CRYPTO_NEWS_CACHE["data"]
    headline_str = " | ".join(f"• {h}" for h in headlines[:3]) if headlines else "Sin noticias recientes."
    return headline_str, CRYPTO_NEWS_CACHE["sentiment"]


def _fetch_reddit_sentiment(coin: str) -> tuple:
    """Scraping Reddit r/cryptocurrency para sentiment. Caché 30 min. Retorna (score 0-100, menciones)."""
    global REDDIT_SENTIMENT_CACHE
    coin_symbol = coin.split("/")[0].upper()
    now = time.time()
    cached = REDDIT_SENTIMENT_CACHE.get(coin_symbol, {"score": 50, "mentions": 0, "ts": 0.0})
    if now - cached["ts"] > REDDIT_SENTIMENT_TTL:
        try:
            url = f"https://www.reddit.com/r/cryptocurrency/search.json?q={coin_symbol}&sort=new&t=day&limit=15"
            headers = {"User-Agent": "GeminiTradingBot/1.0"}
            resp = requests.get(url, headers=headers, timeout=8)
            if resp.status_code == 200:
                posts = resp.json().get("data", {}).get("children", [])
                mentions = len(posts)
                if mentions > 0:
                    # Score basado en upvote ratio y score de posts
                    total_score = sum(p["data"].get("score", 0) for p in posts)
                    avg_upvote = sum(p["data"].get("upvote_ratio", 0.5) for p in posts) / mentions
                    # Normalizar: >0 score promedio = bullish, ratio > 0.6 = bullish
                    sentiment = int(avg_upvote * 100)
                    sentiment = max(10, min(90, sentiment))  # clamp 10-90
                else:
                    sentiment = 50
                REDDIT_SENTIMENT_CACHE[coin_symbol] = {"score": sentiment, "mentions": mentions, "ts": now}
            else:
                REDDIT_SENTIMENT_CACHE[coin_symbol] = {"score": 50, "mentions": 0, "ts": now}
        except Exception:
            REDDIT_SENTIMENT_CACHE[coin_symbol] = {"score": cached.get("score", 50), "mentions": cached.get("mentions", 0), "ts": now}
    cached = REDDIT_SENTIMENT_CACHE.get(coin_symbol, {"score": 50, "mentions": 0})
    return cached["score"], cached["mentions"]


def _fetch_trending_coins() -> list:
    """Obtiene lista de coins trending en CoinGecko. Caché 1 hora. Retorna lista de símbolos."""
    global TRENDING_CACHE
    now = time.time()
    if now - TRENDING_CACHE["ts"] > TRENDING_TTL:
        try:
            resp = requests.get("https://api.coingecko.com/api/v3/search/trending", timeout=8)
            if resp.status_code == 200:
                items = resp.json().get("coins", [])
                TRENDING_CACHE["coins"] = [i["item"]["symbol"].upper() for i in items[:10]]
                TRENDING_CACHE["ts"] = now
        except Exception:
            pass
    return TRENDING_CACHE["coins"]


def _tg(msg: str) -> None:
    """Envía mensaje a Telegram en background, sin bloquear el bot."""
    def _send():
        try:
            url = f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage"
            requests.post(url, json={"chat_id": TELEGRAM_CHAT_ID, "text": msg, "parse_mode": "HTML"}, timeout=5)
        except Exception:
            pass
    threading.Thread(target=_send, daemon=True).start()


class Github_johan1727_botrading___GeminiStrategy__20260511_222253(IStrategy):
    """
    Estrategia de trading basada en Gemini AI.

    Usa indicadores técnicos (RSI, EMA20, EMA50, volumen) como contexto
    y llama a Gemini API cada vela para obtener la decisión de trading.

    Timeframe: 5m
    Exchange: KuCoin | 5 pares: BTC, ETH, XRP, SOL, BNB
    """

    INTERFACE_VERSION = 3
    timeframe = "5m"
    can_short = False
    # REFINADO: 3 slots con stake 30-33% para capital eficiente
    # Si solo usamos 3 trades, mejor concentrar 90-100% del capital en los mejores setups
    max_open_trades = 3
    stoploss = -0.015
    minimal_roi = {"0": 0.020, "30": 0.015, "60": 0.010, "90": 0.008}
    # ROI para alta confianza — se aplica dinámicamente en custom_exit
    _roi_high_confidence = {"0": 0.035, "60": 0.025, "120": 0.015}
    trailing_stop = True
    trailing_stop_positive = 0.015
    trailing_stop_positive_offset = 0.025
    trailing_only_offset_is_reached = True
    use_custom_stoploss = True
    startup_candle_count: int = 210

    rsi_buy_threshold = IntParameter(30, 60, default=55, space="buy")
    rsi_sell_threshold = IntParameter(65, 85, default=70, space="sell")
    ema_fast = IntParameter(10, 30, default=20, space="buy")
    ema_slow = IntParameter(30, 70, default=50, space="buy")

    def __init__(self, config: dict) -> None:
        super().__init__(config)
        self._last_gemini_call = 0.0
        self._gemini_decisions = {}
        self._last_pair_refresh = 0.0
        self._selected_pair = None
        # Tracking diario
        self._daily_trades = []  # lista de {pair, profit_usd, profit_pct, won}
        self._last_summary_date = None
        self._daily_loss_usd = 0.0        # pérdida acumulada hoy en USD
        self._daily_loss_date = None      # fecha del último reset de pérdidas
        self._stoploss_cooldown: dict = {}  # par -> timestamp del último stop-loss
        self._loss_cooldown: dict = {}    # par -> timestamp hasta el que no reentrar (2h)
        self._daily_losses: dict = {}     # par -> [timestamps] de losses en 24h
        self._MAX_DAILY_LOSSES = 3        # max losses por par antes de bloquearlo el dia
        # Memoria de aprendizaje: últimos 50 resultados de trades
        self._trade_memory: list = []  # {pair, accion, confianza, rsi, profit_usd, won, razon}
        self._daily_trades_lock = threading.Lock()

        # Cache de datos 1h por par — instancia, no clase
        self._cache_1h: dict = {}
        self._cache_1h_ttl: float = 3600.0
        self._cache_1h_lock = threading.Lock()

        # ── Q-Learning Tabular ──────────────────────────────────────────────
        self._qtable_path = Path(__file__).parent / "qtable.json"
        self._q_actions = ["HOLD", "BUY", "SELL", "CLOSE"]  # índices 0-3
        self._q_states = 100          # estados del mercado (tendencia×volatilidad×momentum)
        self._q_alpha = 0.1           # tasa de aprendizaje
        self._q_gamma = 0.9           # descuento de recompensa futura
        self._q_epsilon = 0.3         # exploración inicial (30% aleatorio)
        self._q_epsilon_min = 0.05    # exploración mínima (5%)
        self._q_epsilon_decay = 0.995 # decaimiento por episodio
        self._q_episodes = 0          # operaciones totales realizadas
        self._experience_replay: list = []  # buffer de hasta 500 experiencias
        self._q_table: list = self._load_qtable()
        self._q_lock = threading.Lock()

        # ── Régimen de Mercado ──────────────────────────────────────────────
        self._market_regime = "UNKNOWN"  # estado actual del mercado
        self._regime_stake_mult = 1.0    # multiplicador de stake según régimen
        self._regime_sl_mult = 1.0       # multiplicador de stop-loss según régimen

        # ── Autopausa ───────────────────────────────────────────────────────
        self._autopause_until: float = 0.0   # timestamp hasta el que está pausado
        self._autopause_reason: str = ""
        self._trades_since_check: int = 0    # contador para evaluar métricas cada N trades

        # ── Checklist demo → live ───────────────────────────────────────────
        self._live_ready_notified: bool = False  # evitar repetir notificación

        # Sistema de rotación de APIs
        # Gemini free-trial expirado: arrancar directamente en Groq
        _today = datetime.now(timezone.utc).date()
        self._api_usage = {entry["label"]: {"count": 0, "date": datetime.now(timezone.utc).date()} for entry in GROQ_API_POOL}
        self._gemini_decisions = {}
        self._gemini_client = None
        self._gemini_model_active = None
        self._api_index = 0  # índice del pool de APIs activo
        self._api_lock = threading.Lock()
        self._last_gemini_call = 0
        self._init_gemini_client()  # inicializar cliente Groq al arrancar

        # Arrancar scheduler de resumen diario en background
        threading.Thread(target=self._daily_summary_scheduler, daemon=True).start()

        # Thread que resetea contadores de API a medianoche UTC (cuando Google las recarga)
        threading.Thread(target=self._api_reset_scheduler, daemon=True).start()

        # Pre-cargar datos 1h de los pares fijos al arrancar
        # Los pares dinámicos (VolumePairList) se cargan cuando entran
        static_pairs = [
            "BTC/USDT", "ETH/USDT",
        ]
        for p in static_pairs:
            threading.Thread(target=self._refresh_1h_background, args=(p,), daemon=True).start()

    def _api_reset_scheduler(self) -> None:
        """Resetea contadores de API a medianoche UTC cuando Google los recarga."""
        from datetime import timedelta
        while True:
            now = datetime.now(timezone.utc)
            # Siguiente medianoche UTC
            midnight = (now + timedelta(days=1)).replace(hour=0, minute=0, second=30, microsecond=0)
            time.sleep((midnight - now).total_seconds())
            # Resetear todos los contadores
            today = datetime.now(timezone.utc).date()
            with self._api_lock:
                for entry in GROQ_API_POOL:
                    self._api_usage[entry["label"]]["count"] = 0
                    self._api_usage[entry["label"]]["date"] = today
                self._api_index = 0
                first = GROQ_API_POOL[0]
                self._gemini_client = GroqClient(api_key=first["key"])
            logger.info("[RESET] Contadores de API reseteados a medianoche UTC (reset diario)")
            _tg(
                "APIS RECARGADAS\n"
                "Nueva cuota diaria disponible (medianoche UTC).\n"
                "El bot retoma el analisis con Gemini normalmente."
            )

    def _init_gemini_client(self) -> None:
        """Inicializa el cliente de Groq."""
        entry = GROQ_API_POOL[self._api_index]
        self._gemini_client = GroqClient(api_key=entry["key"])
        self._gemini_model_active = entry["model"]
        logger.info(f"[OK] API activa: {entry['label']} | proveedor: Groq | modelo: {entry['model']}")

    def _call_llm(self, entry: dict, prompt: str) -> str:
        """Llama siempre a Groq. Retorna texto crudo."""
        if GroqClient is None:
            raise RuntimeError("groq SDK no instalado")
        gc = GroqClient(api_key=entry["key"])
        r = gc.chat.completions.create(
            model=entry["model"],
            messages=[{"role": "user", "content": prompt}],
            max_tokens=120,
            temperature=0.1,
        )
        return r.choices[0].message.content.strip()

    # ── Q-Learning ─────────────────────────────────────────────────────────

    def _load_qtable(self) -> list:
        """Carga la Q-table desde disco o crea una nueva con ceros."""
        if self._qtable_path.exists():
            try:
                data = json.loads(self._qtable_path.read_text())
                self._q_epsilon = data.get("epsilon", 0.3)
                self._q_episodes = data.get("episodes", 0)
                logger.info(f"[QLEARN] Q-table cargada: {self._q_episodes} episodios, epsilon={self._q_epsilon:.3f}")
                return data["qtable"]
            except Exception as e:
                logger.warning(f"[QLEARN] Error cargando Q-table: {e}, creando nueva")
        return [[0.0] * 4 for _ in range(self._q_states)]

    def _save_qtable(self) -> None:
        """Guarda Q-table, epsilon y episodios en disco."""
        try:
            with self._q_lock:
                data = {"qtable": self._q_table, "epsilon": self._q_epsilon, "episodes": self._q_episodes}
            self._qtable_path.write_text(json.dumps(data))
        except Exception as e:
            logger.warning(f"[QLEARN] Error guardando Q-table: {e}")

    def _encode_state(self, dataframe: DataFrame) -> int:
        """Codifica el estado del mercado en un número 0-99 basado en tendencia, volatilidad y momentum."""
        last = dataframe.iloc[-1]
        # Tendencia: 0-4 basado en EMA cruce y posición vs EMA200
        if last.get("ema200_signal") == "ABOVE" and last.get("ema_signal") == "BULL":
            trend = 4
        elif last.get("ema_signal") == "BULL":
            trend = 3
        elif last.get("ema200_signal") == "ABOVE":
            trend = 2
        elif last.get("ema_signal") == "BEAR":
            trend = 1
        else:
            trend = 0
        # Volatilidad: 0-3 basado en ATR normalizado
        atr_pct = last.get("atr_pct", 0.5)
        if atr_pct < 0.3:
            vol = 0
        elif atr_pct < 0.6:
            vol = 1
        elif atr_pct < 1.2:
            vol = 2
        else:
            vol = 3
        # Momentum: 0-4 basado en RSI
        rsi = last.get("rsi", 50)
        if rsi < 30:
            mom = 0
        elif rsi < 45:
            mom = 1
        elif rsi < 55:
            mom = 2
        elif rsi < 70:
            mom = 3
        else:
            mom = 4
        # Combinar en estado 0-99: trend(5) × vol(4) × mom(5) = 100
        state = (trend * 20) + (vol * 5) + mom
        return min(state, 99)

    def _q_get_action(self, state: int, force_exploit: bool = False) -> int:
        """Selecciona acción por epsilon-greedy. Retorna índice de acción (0=HOLD,1=BUY,2=SELL,3=CLOSE)."""
        with self._q_lock:
            if not force_exploit and random.random() < self._q_epsilon:
                return random.randint(0, 3)
            return int(max(range(4), key=lambda a: self._q_table[state][a]))

    def _q_update(self, state: int, action: int, reward: float, next_state: int) -> None:
        """Actualiza Q-table con la fórmula de Bellman."""
        with self._q_lock:
            best_next = max(self._q_table[next_state])
            old_q = self._q_table[state][action]
            self._q_table[state][action] = old_q + self._q_alpha * (reward + self._q_gamma * best_next - old_q)
            self._q_epsilon = max(self._q_epsilon_min, self._q_epsilon * self._q_epsilon_decay)
            self._q_episodes += 1

    def _q_reward(self, profit_pct: float, stake: float) -> float:
        """Calcula recompensa: positiva si gana (suavizada por raíz), negativa si pierde (penalización 1.5×)."""
        if profit_pct > 0:
            reward = math.sqrt(abs(profit_pct) * 100)
            rr = abs(profit_pct) / 0.015  # ratio vs stop-loss
            if rr > 1.5:
                reward += 1.0  # bonus de calidad por buen RR
        else:
            reward = -math.sqrt(abs(profit_pct) * 100) * 1.5
        return round(reward, 4)

    def _experience_replay_train(self) -> None:
        """Repasa 32 experiencias aleatorias del buffer para acelerar el aprendizaje."""
        if len(self._experience_replay) < 32:
            return
        batch = random.sample(self._experience_replay, 32)
        for exp in batch:
            self._q_update(exp["state"], exp["action"], exp["reward"], exp["next_state"])

    # ── Régimen de Mercado ────────────────────────────────────────────────

    def _detect_regime(self, dataframe: DataFrame) -> str:
        """Clasifica el mercado en uno de 10 regímenes usando ADX real, ATR y EMAs."""
        if len(dataframe) < 30:
            return "UNKNOWN"
        last = dataframe.iloc[-1]
        # Usar ADX real calculado en populate_indicators (mas preciso que ema_diff)
        adx_val = float(last.get("adx", 0)) if not pd.isna(last.get("adx", float('nan'))) else 0.0
        atr_pct = last.get("atr_pct", 0.5)
        rsi = last.get("rsi", 50)
        ema_bull = last.get("ema_signal") == "BULL"
        ema200 = last.get("ema200_signal") == "ABOVE"
        # Determinar tendencia, volatilidad y régimen usando ADX real
        strong_trend = adx_val > 20
        volatile = atr_pct > 1.0
        very_volatile = atr_pct > 2.0
        if very_volatile and not strong_trend:
            regime = "CAOS_VOLATIL"
        elif strong_trend and ema_bull and ema200 and volatile:
            regime = "TENDENCIA_ALCISTA_VOLATIL"
        elif strong_trend and ema_bull and ema200:
            regime = "TENDENCIA_ALCISTA_NORMAL" if atr_pct > 0.4 else "TENDENCIA_ALCISTA_CALMADA"
        elif strong_trend and not ema_bull and volatile:
            regime = "TENDENCIA_BAJISTA_VOLATIL"
        elif strong_trend and not ema_bull:
            regime = "TENDENCIA_BAJISTA_NORMAL" if atr_pct > 0.4 else "TENDENCIA_BAJISTA_CALMADA"
        elif not strong_trend and volatile:
            regime = "RANGO_VOLATIL"
        elif not strong_trend:
            regime = "RANGO_TRANQUILO"
        else:
            regime = "TRANSICION"
        # Ajustar multiplicadores de stake y stop segun regimen
        regime_params = {
            "TENDENCIA_ALCISTA_CALMADA":  (1.0,  1.0),
            "TENDENCIA_ALCISTA_NORMAL":   (1.0,  1.2),
            "TENDENCIA_ALCISTA_VOLATIL":  (0.7,  1.8),
            "TENDENCIA_BAJISTA_CALMADA":  (0.5,  1.0),
            "TENDENCIA_BAJISTA_NORMAL":   (0.3,  1.5),
            "TENDENCIA_BAJISTA_VOLATIL":  (0.0,  2.0),
            "RANGO_TRANQUILO":            (0.8,  0.8),
            "RANGO_VOLATIL":              (0.5,  1.2),
            "CAOS_VOLATIL":               (0.0,  2.5),
            "TRANSICION":                 (0.7,  1.3),
            "UNKNOWN":                    (0.5,  1.0),
        }
        self._regime_stake_mult, self._regime_sl_mult = regime_params.get(regime, (0.5, 1.0))
        if regime != self._market_regime:
            logger.info(f"[REGIME] Cambio de regimen: {self._market_regime} -> {regime} | stake_mult={self._regime_stake_mult} sl_mult={self._regime_sl_mult}")
            self._market_regime = regime
        return regime

    # ── Autopausa ─────────────────────────────────────────────────────────

    def _check_autopause(self) -> None:
        """Evalúa métricas y activa autopausa si el bot está rindiendo mal."""
        recent = self._trade_memory[-20:] if len(self._trade_memory) >= 3 else []
        if not recent:
            return
        wins = sum(1 for t in recent if t["won"])
        losses = len(recent) - wins
        winrate = wins / len(recent)
        total_profit = sum(t["profit_usd"] for t in recent if t["won"])
        total_loss = abs(sum(t["profit_usd"] for t in recent if not t["won"]))
        profit_factor = total_profit / total_loss if total_loss > 0 else 99.0
        drawdown_pct = self._daily_loss_usd / max(self._daily_loss_usd + 50, 50) * 100
        pause_duration = 0
        reason = ""
        if drawdown_pct > 5.0:
            pause_duration = 7200  # 2 horas
            reason = f"Drawdown diario {drawdown_pct:.1f}% > 5%"
        elif winrate < 0.35 and len(recent) >= 10:
            pause_duration = 3600  # 1 hora
            reason = f"Winrate {winrate*100:.0f}% < 35% en últimos {len(recent)} trades"
        elif profit_factor < 0.7 and len(recent) >= 15:
            pause_duration = 1800  # 30 min
            reason = f"Profit factor {profit_factor:.2f} < 0.7"
        if pause_duration > 0:
            self._autopause_until = time.time() + pause_duration
            self._autopause_reason = reason
            mins = pause_duration // 60
            logger.warning(f"[AUTOPAUSA] Bot pausado {mins}min | Razón: {reason}")
            _tg(
                f"AUTOPAUSA ACTIVADA\n"
                f"Razon: {reason}\n"
                f"WinRate: {winrate*100:.0f}% | PF: {profit_factor:.2f} | DD: {drawdown_pct:.1f}%\n"
                f"Bot pausado {mins} minutos. Se reanuda automaticamente."
            )


    def _check_live_readiness(self) -> None:
        """Evalúa si el bot está listo para operar con dinero real y avisa por Telegram."""
        if self._live_ready_notified:
            return
        mem = self._trade_memory
        if len(mem) < 15:
            return
        wins = [t for t in mem if t["won"]]
        losses = [t for t in mem if not t["won"]]
        win_rate = len(wins) / len(mem)
        total_profit = sum(t["profit_usd"] for t in wins)
        total_loss = abs(sum(t["profit_usd"] for t in losses)) if losses else 0
        profit_factor = total_profit / total_loss if total_loss > 0 else 99.0
        max_dd = self._daily_loss_usd
        balance_approx = 1000.0  # wallet demo base
        max_dd_pct = (max_dd / balance_approx) * 100 if balance_approx > 0 else 0
        # Criterios acelerados: 15 trades, WR≥55%, PF≥1.2, DD<12%
        ready = (win_rate >= 0.55 and profit_factor >= 1.2 and max_dd_pct < 12.0)
        logger.info(
            f"[LIVE-CHECK] trades={len(mem)} WR={win_rate*100:.0f}% PF={profit_factor:.2f} "
            f"DD={max_dd_pct:.1f}% | READY={ready}"
        )
        if ready:
            self._live_ready_notified = True
            _tg(
                f"🟢 BOT LISTO PARA LIVE\n"
                f"---\n"
                f"Trades demo: {len(mem)} | WR: {win_rate*100:.0f}% | PF: {profit_factor:.2f}\n"
                f"Max DD: {max_dd_pct:.1f}% | Episodios Q: {self._q_episodes}\n"
                f"---\n"
                f"Para activar: cambiar dry_run=false en config.json\n"
                f"RECUERDA: solo capital que puedes perder"
            )

    def _fetch_1h_trend(self, pair: str) -> tuple:
        """Devuelve tendencia 1h desde caché. El refresco ocurre en background."""
        cached = self._cache_1h.get(pair)
        if cached:
            return cached["trend"], cached["rsi"]
        # Primera vez: lanzar fetch en background y retornar UNKNOWN
        threading.Thread(target=self._refresh_1h_background, args=(pair,), daemon=True).start()
        return "UNKNOWN", 50.0

    def _refresh_1h_background(self, pair: str) -> None:
        """Refresca datos 1h en un thread daemon para no bloquear el hilo principal."""
        now = time.time()
        with self._cache_1h_lock:
            cached = self._cache_1h.get(pair)
            if cached and now - cached["ts"] < self._cache_1h_ttl:
                return
        try:
            symbol = pair.replace("/", "-") if "-" not in pair else pair
            url = f"https://api.kucoin.com/api/v1/market/candles?type=1hour&symbol={symbol}&startAt={int(now)-7200*100}&endAt={int(now)}"
            resp = requests.get(url, timeout=8)
            if resp.status_code == 200:
                candles = resp.json().get("data", [])
                if len(candles) >= 50:
                    closes = pd.Series([float(c[2]) for c in reversed(candles)])
                    ema50 = float(self._calc_ema(closes, 50).iloc[-1])
                    rsi_val = float(self._calc_rsi(closes, 14).iloc[-1])
                    trend = "BULL" if closes.iloc[-1] > ema50 else "BEAR"
                    with self._cache_1h_lock:
                        self._cache_1h[pair] = {"trend": trend, "rsi": rsi_val, "ts": now}
        except Exception as e:
            logger.debug(f"1h fetch fallido para {pair}: {e}")

    def _rotate_api(self) -> bool:
        """Rota a la siguiente API disponible. Retorna False si todas agotadas."""
        today = datetime.now(timezone.utc).date()
        with self._api_lock:
            for _ in range(len(GROQ_API_POOL)):
                self._api_index = (self._api_index + 1) % len(GROQ_API_POOL)
                entry = GROQ_API_POOL[self._api_index]
                label = entry["label"]
                usage = self._api_usage[label]
                # Reset conteo si es nuevo día
                if usage["date"] != today:
                    usage["count"] = 0
                    usage["date"] = today
                if usage["count"] < entry["daily_limit"] and entry["key"]:
                    self._gemini_client = GroqClient(api_key=entry["key"])
                    self._gemini_model_active = entry["model"]
                    logger.info(f"[ROTATE] Rotando a {label} ({usage['count']}/{entry['daily_limit']} usadas)")
                    _tg(f"API rotada a {label}\nModelo: {entry['model']}\nUso: {usage['count']}/{entry['daily_limit']}")
                    return True
        logger.warning("[WARN] Todas las APIs agotadas por hoy. Bot en modo HOLD hasta manana.")
        _tg("TODAS LAS APIS AGOTADAS\nEl bot esta en HOLD hasta la medianoche (reset diario).")
        return False

    def _get_active_api_entry(self) -> Optional[dict]:
        """Devuelve la entrada activa del pool, rotando si está agotada."""
        today = datetime.now(timezone.utc).date()
        with self._api_lock:
            entry = GROQ_API_POOL[self._api_index]
            label = entry["label"]
            usage = self._api_usage[label]
            if usage["date"] != today:
                usage["count"] = 0
                usage["date"] = today
            if usage["count"] < entry["daily_limit"] and entry["key"]:
                usage["count"] += 1
                return entry
            needs_rotate = True
        # Lock liberado antes de llamar a _rotate_api para evitar deadlock
        if needs_rotate:
            if self._rotate_api():
                return GROQ_API_POOL[self._api_index]
        return None

    def _daily_summary_scheduler(self) -> None:
        """Envía resumen diario a las 11:59 PM cada día."""
        from datetime import timedelta
        while True:
            now = datetime.now(timezone.utc)
            target = now.replace(hour=23, minute=59, second=0, microsecond=0)
            if now >= target:
                target = target + timedelta(days=1)
            sleep_secs = (target - now).total_seconds()
            time.sleep(sleep_secs)
            self._send_daily_summary()

    def _send_daily_summary(self) -> None:
        """Construye y envía el resumen del día por Telegram."""
        today = datetime.now(timezone.utc).strftime("%d/%m/%Y")
        with self._daily_trades_lock:
            trades = self._daily_trades[:]
            self._daily_trades.clear()

        if not trades:
            _tg(
                f"Resumen del dia {today}\n"
                f"Sin operaciones hoy.\n"
                f"El bot siguio analizando 10 pares."
            )
            return

        total_usd = sum(t["profit_usd"] for t in trades)
        ganadas = sum(1 for t in trades if t["won"])
        perdidas = len(trades) - ganadas
        mejor = max(trades, key=lambda t: t["profit_usd"])
        peor = min(trades, key=lambda t: t["profit_usd"])
        winrate = (ganadas / len(trades)) * 100

        # Stats de Q-Learning para el reporte
        with self._q_lock:
            q_vals_buy = [(i, self._q_table[i][1]) for i in range(self._q_states)]
        top_estados = sorted(q_vals_buy, key=lambda x: x[1], reverse=True)[:3]
        top_str = " | ".join([f"E{e[0]}={e[1]:.2f}" for e in top_estados if e[1] > 0])
        if not top_str:
            top_str = "aun aprendiendo..."

        # Mejor par por win rate en memoria
        pares_mem = {}
        for t in self._trade_memory:
            p = t["pair"]
            pares_mem.setdefault(p, {"w": 0, "l": 0})
            if t["won"]:
                pares_mem[p]["w"] += 1
            else:
                pares_mem[p]["l"] += 1
        mejor_par_wr = max(pares_mem.items(), key=lambda x: x[1]["w"] / max(x[1]["w"] + x[1]["l"], 1), default=None)
        mejor_par_str = f"{mejor_par_wr[0]} ({mejor_par_wr[1]['w']}W/{mejor_par_wr[1]['l']}L)" if mejor_par_wr else "sin datos"

        signo_total = "+" if total_usd >= 0 else ""

        # Pedir a Groq un análisis del día en lenguaje natural (Opción C)
        analisis_ia = self._get_daily_analysis_from_groq(trades, total_usd, winrate, ganadas, perdidas)

        msg = (
            f"Resumen del dia {today}\n"
            f"Resultado total: {signo_total}${total_usd:.2f}\n"
            f"Operaciones: {len(trades)} ({ganadas} ganadoras {perdidas} perdedoras)\n"
            f"Winrate: {winrate:.0f}%\n"
            f"Mejor trade: {mejor['pair']} +${mejor['profit_usd']:.2f}\n"
            f"Peor trade: {peor['pair']} ${peor['profit_usd']:.2f}\n"
            f"\nAprendizaje Q-Learning:\n"
            f"  Episodios: {self._q_episodes} | Epsilon: {self._q_epsilon:.3f}\n"
            f"  Top estados rentables: {top_str}\n"
            f"  Mejor par historico: {mejor_par_str}"
        )
        if analisis_ia:
            msg += f"\n\nAnalisis IA:\n{analisis_ia}"
        msg += "\nEl bot sigue operando"
        _tg(msg)

    def _get_daily_analysis_from_groq(self, trades: list, total_usd: float, winrate: float, ganadas: int, perdidas: int) -> Optional[str]:
        """Pide a Groq un análisis del día en lenguaje natural. 1 llamada/día."""
        if not self._gemini_client or not trades:
            return None
        try:
            pairs_won = [t["pair"] for t in trades if t["won"]]
            pairs_lost = [t["pair"] for t in trades if not t["won"]]
            signo = "+" if total_usd >= 0 else ""
            prompt = f"""Eres el analista de un bot de trading crypto. Resume el dia en 3 frases cortas y directas en español.

DATOS DEL DIA:
- Trades totales: {len(trades)} ({ganadas} ganados, {perdidas} perdidos)
- Win rate: {winrate:.0f}%
- Resultado: {signo}${total_usd:.2f} USDT
- Pares ganadores: {', '.join(pairs_won[:5]) if pairs_won else 'ninguno'}
- Pares perdedores: {', '.join(pairs_lost[:5]) if pairs_lost else 'ninguno'}
- Regimen de mercado predominante: {self._market_regime}

INSTRUCCIONES: 3 frases maximo. Frase 1: resultado general. Frase 2: que funcionó. Frase 3: que mejorar mañana. Sin emojis. Sin formato especial."""

            api_entry = self._get_active_api_entry()
            if not api_entry:
                return None
            raw = self._call_llm(api_entry, prompt)
            self._last_gemini_call = time.time()
            return raw.strip()[:400]
        except Exception as e:
            logger.debug(f"[DAILY-IA] Error en analisis diario: {e}")
            return None

    @staticmethod
    def _calc_rsi(series: pd.Series, period: int = 14) -> pd.Series:
        """RSI calculado con pandas puro, sin TA-Lib."""
        delta = series.diff()
        gain = delta.clip(lower=0).ewm(com=period - 1, min_periods=period).mean()
        loss = (-delta.clip(upper=0)).ewm(com=period - 1, min_periods=period).mean()
        rs = gain / loss.replace(0, 1e-10)
        return 100 - (100 / (1 + rs))

    @staticmethod
    def _calc_ema(series: pd.Series, period: int) -> pd.Series:
        """EMA calculada con pandas puro."""
        return series.ewm(span=period, adjust=False).mean()

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Calcula indicadores técnicos que se pasan a Gemini como contexto."""
        dataframe["rsi"] = self._calc_rsi(dataframe["close"], 14)
        dataframe["ema20"] = self._calc_ema(dataframe["close"], self.ema_fast.value)
        dataframe["ema50"] = self._calc_ema(dataframe["close"], self.ema_slow.value)
        dataframe["ema_signal"] = (
            dataframe["ema20"] > dataframe["ema50"]
        ).map({True: "BULL", False: "BEAR"})
        dataframe["volume_ratio"] = (
            dataframe["volume"] / dataframe["volume"].rolling(20).mean()
        )
        dataframe["price_vs_ema20"] = (
            (dataframe["close"] - dataframe["ema20"]) / dataframe["ema20"] * 100
        )
        # MACD (12, 26, 9)
        ema12 = self._calc_ema(dataframe["close"], 12)
        ema26 = self._calc_ema(dataframe["close"], 26)
        dataframe["macd"] = ema12 - ema26
        dataframe["macd_signal"] = self._calc_ema(dataframe["macd"], 9)
        dataframe["macd_hist"] = dataframe["macd"] - dataframe["macd_signal"]
        # Bollinger Bands (20, 2)
        bb_mid = dataframe["close"].rolling(20).mean()
        bb_std = dataframe["close"].rolling(20).std()
        dataframe["bb_upper"] = bb_mid + 2 * bb_std
        dataframe["bb_lower"] = bb_mid - 2 * bb_std
        dataframe["bb_mid"] = bb_mid
        dataframe["bb_pct"] = (dataframe["close"] - dataframe["bb_lower"]) / (dataframe["bb_upper"] - dataframe["bb_lower"]) * 100
        # EMA200 — soporte/resistencia institucional
        dataframe["ema200"] = self._calc_ema(dataframe["close"], 200)
        dataframe["ema200_signal"] = (
            dataframe["close"] > dataframe["ema200"]
        ).map({True: "ABOVE", False: "BELOW"})
        # ATR(14) — volatilidad real para stop-loss dinámico
        high_low = dataframe["high"] - dataframe["low"]
        high_close = (dataframe["high"] - dataframe["close"].shift()).abs()
        low_close = (dataframe["low"] - dataframe["close"].shift()).abs()
        true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
        dataframe["atr"] = true_range.ewm(span=14, adjust=False).mean()
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"] * 100
        # Divergencia RSI: precio sube pero RSI baja = señal de salida
        rsi_prev = dataframe["rsi"].shift(3)
        close_prev = dataframe["close"].shift(3)
        dataframe["rsi_divergence"] = (
            (dataframe["close"] > close_prev) & (dataframe["rsi"] < rsi_prev - 5)
        ).map({True: "BEARISH_DIV", False: "OK"})

        # Stochastic RSI (14,3,3) — momentum sobrecompra/sobreventa más sensible que RSI
        rsi_min = dataframe["rsi"].rolling(14).min()
        rsi_max = dataframe["rsi"].rolling(14).max()
        stoch_rsi_k = (dataframe["rsi"] - rsi_min) / (rsi_max - rsi_min + 1e-10) * 100
        dataframe["stoch_rsi_k"] = stoch_rsi_k.rolling(3).mean()
        dataframe["stoch_rsi_d"] = dataframe["stoch_rsi_k"].rolling(3).mean()

        # OBV (On Balance Volume) — confirma si el volumen apoya la tendencia
        obv = (pd.Series(
            [0] + [v if c > p else (-v if c < p else 0)
                   for c, p, v in zip(dataframe["close"][1:], dataframe["close"][:-1], dataframe["volume"][1:])],
            index=dataframe.index
        )).cumsum()
        dataframe["obv"] = obv
        dataframe["obv_signal"] = (obv > obv.rolling(10).mean()).map({True: "BULL", False: "BEAR"})

        # Williams %R (14) — otro oscilador de sobrecompra/sobreventa
        highest_high = dataframe["high"].rolling(14).max()
        lowest_low = dataframe["low"].rolling(14).min()
        dataframe["williams_r"] = (highest_high - dataframe["close"]) / (highest_high - lowest_low + 1e-10) * -100

        # CCI (20) — Commodity Channel Index, detecta reversiones
        tp = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
        tp_ma = tp.rolling(20).mean()
        tp_std = tp.rolling(20).std()
        dataframe["cci"] = (tp - tp_ma) / (0.015 * tp_std + 1e-10)

        # Soporte y Resistencia multicapa: 20, 50 (swing) y 100 velas (institucional)
        dataframe["support_20"] = dataframe["low"].rolling(20).min()
        dataframe["resistance_20"] = dataframe["high"].rolling(20).max()
        dataframe["support_50"] = dataframe["low"].rolling(50).min()
        dataframe["resistance_50"] = dataframe["high"].rolling(50).max()
        dataframe["support_100"] = dataframe["low"].rolling(100).min()
        dataframe["resistance_100"] = dataframe["high"].rolling(100).max()
        # Usar el soporte/resistencia más cercano al precio actual
        dataframe["support"] = dataframe[["support_20", "support_50"]].max(axis=1)
        dataframe["resistance"] = dataframe[["resistance_20", "resistance_50"]].min(axis=1)
        dataframe["dist_support_pct"] = (dataframe["close"] - dataframe["support"]) / dataframe["close"] * 100
        dataframe["dist_resistance_pct"] = (dataframe["resistance"] - dataframe["close"]) / dataframe["close"] * 100
        # Niveles institucionales (100 velas) para referencia en el prompt
        dataframe["dist_support_100_pct"] = (dataframe["close"] - dataframe["support_100"]) / dataframe["close"] * 100
        dataframe["dist_resistance_100_pct"] = (dataframe["resistance_100"] - dataframe["close"]) / dataframe["close"] * 100

        # Número redondo más cercano (psicología del mercado)
        def nearest_round(price):
            if price <= 0:
                return 0.0
            if price < 1.0:
                # Para precios < 1 usar multiplos de 0.1, 0.01 según magnitud
                magnitude = 10 ** (math.floor(math.log10(price)))
            else:
                magnitude = 10 ** (len(str(int(price))) - 1)
            rounded = round(price / magnitude) * magnitude
            dist_pct = abs(price - rounded) / price * 100
            return round(dist_pct, 2)
        dataframe["dist_round_number_pct"] = dataframe["close"].apply(nearest_round)

        # Patrones de velas japonesas (últimas 3 velas)
        def detect_candle_pattern(df):
            patterns = []
            for i in range(len(df)):
                if i < 2:
                    patterns.append("NEUTRAL")
                    continue
                o, h, l, c = df["open"].iloc[i], df["high"].iloc[i], df["low"].iloc[i], df["close"].iloc[i]
                po, ph, pl, pc = df["open"].iloc[i-1], df["high"].iloc[i-1], df["low"].iloc[i-1], df["close"].iloc[i-1]
                po2, pc2 = df["open"].iloc[i-2], df["close"].iloc[i-2]
                body = abs(c - o)
                prev_body = abs(pc - po)
                body2 = abs(pc2 - po2)
                upper_wick = h - max(o, c)
                lower_wick = min(o, c) - l
                # Morning Star — suelo de 3 velas (bajista + pequeña + alcista)
                if (pc2 < po2 and body2 > 0 and
                        body < (h - l) * 0.35 and
                        c > o and
                        c > (po2 + pc2) / 2):
                    patterns.append("MORNING_STAR")
                # Evening Star — techo de 3 velas (alcista + pequeña + bajista)
                elif (pc2 > po2 and body2 > 0 and
                        body < (h - l) * 0.35 and
                        c < o and
                        c < (po2 + pc2) / 2):
                    patterns.append("EVENING_STAR")
                # Hammer (martillo) — reversión alcista
                elif lower_wick > body * 2 and upper_wick < body * 0.5 and c > o:
                    patterns.append("HAMMER")
                # Shooting star — reversión bajista
                elif upper_wick > body * 2 and lower_wick < body * 0.5 and c < o:
                    patterns.append("SHOOTING_STAR")
                # Engulfing alcista
                elif c > o and pc < po and c > po and o < pc:
                    patterns.append("BULL_ENGULF")
                # Engulfing bajista
                elif c < o and pc > po and c < po and o > pc:
                    patterns.append("BEAR_ENGULF")
                # Doji (indecisión)
                elif body < (h - l) * 0.1:
                    patterns.append("DOJI")
                else:
                    patterns.append("NEUTRAL")
            return patterns
        dataframe["candle_pattern"] = detect_candle_pattern(dataframe)

        # Fibonacci retracement — niveles 38.2%, 50%, 61.8% de las últimas 50 velas
        high50 = dataframe["high"].rolling(50).max()
        low50 = dataframe["low"].rolling(50).min()
        fib_range = high50 - low50
        dataframe["fib_382"] = high50 - fib_range * 0.382
        dataframe["fib_500"] = high50 - fib_range * 0.500
        dataframe["fib_618"] = high50 - fib_range * 0.618
        # ¿Está el precio cerca de algún nivel Fibonacci? (<0.5%)
        def fib_zone(row):
            price = row["close"]
            for level, name in [(row["fib_382"], "FIB38"), (row["fib_500"], "FIB50"), (row["fib_618"], "FIB61")]:
                if abs(price - level) / price < 0.005:
                    return name
            return "NONE"
        dataframe["fib_zone"] = dataframe.apply(fib_zone, axis=1)

        # ADX (14) — fuerza de la tendencia (no dirección)
        # +DI y -DI para el cálculo completo de ADX
        high_diff = dataframe["high"].diff()
        low_diff = dataframe["low"].diff()
        plus_dm = high_diff.where((high_diff > 0) & (high_diff > -low_diff), 0.0)
        minus_dm = (-low_diff).where((-low_diff > 0) & (-low_diff > high_diff), 0.0)
        atr14 = dataframe["atr"]
        plus_di = 100 * (plus_dm.ewm(span=14, adjust=False).mean() / atr14.replace(0, 1e-10))
        minus_di = 100 * (minus_dm.ewm(span=14, adjust=False).mean() / atr14.replace(0, 1e-10))
        dx = (abs(plus_di - minus_di) / (plus_di + minus_di + 1e-10)) * 100
        dataframe["adx"] = dx.ewm(span=14, adjust=False).mean()
        dataframe["adx_trend"] = dataframe["adx"].apply(
            lambda x: "FUERTE" if x > 25 else "LATERAL"
        )

        # MFI (14) — Money Flow Index: RSI ponderado por volumen
        typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
        money_flow = typical_price * dataframe["volume"]
        tp_prev = typical_price.shift(1)
        pos_flow = money_flow.where(typical_price > tp_prev, 0.0).rolling(14).sum()
        neg_flow = money_flow.where(typical_price < tp_prev, 0.0).rolling(14).sum()
        mfi_ratio = pos_flow / neg_flow.replace(0, 1e-10)
        dataframe["mfi"] = 100 - (100 / (1 + mfi_ratio))
        dataframe["mfi_signal"] = dataframe["mfi"].apply(
            lambda x: "SOBREVENTA" if x < 20 else ("SOBRECOMPRA" if x > 80 else "NEUTRO")
        )

        # VWAP — indicador institucional (precio ponderado por volumen)
        # Ventana rodante 288 velas = 1 día en 5m (evita lookahead bias del cumsum total)
        typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
        tp_vol = (typical_price * dataframe["volume"]).rolling(288, min_periods=1).sum()
        vol_sum = dataframe["volume"].rolling(288, min_periods=1).sum()
        dataframe["vwap"] = tp_vol / vol_sum.replace(0, 1e-10)
        dataframe["dist_vwap_pct"] = (dataframe["close"] - dataframe["vwap"]) / dataframe["vwap"] * 100

        # Bollinger Band Squeeze — bandas comprimidas = explosion inminente
        dataframe["bb_width"] = (dataframe["bb_upper"] - dataframe["bb_lower"]) / dataframe["bb_mid"] * 100
        dataframe["bb_squeeze"] = dataframe["bb_width"] < dataframe["bb_width"].rolling(20).mean() * 0.6

        # Divergencia alcista RSI — precio hace minimo mas bajo pero RSI no = suelo proximo
        dataframe["rsi_bull_div"] = (
            (dataframe["close"] < dataframe["close"].shift(3)) &
            (dataframe["rsi"] > dataframe["rsi"].shift(3) + 3)
        ).map({True: "BULL_DIV", False: "OK"})

        # EMA200 — tendencia macro institucional (usada en _detect_regime)
        dataframe["ema200"] = self._calc_ema(dataframe["close"], 200)
        dataframe["ema200_signal"] = (dataframe["close"] > dataframe["ema200"]).map({True: "ABOVE", False: "BELOW"})

        dataframe["gemini_buy"] = 0
        dataframe["gemini_sell"] = 0
        dataframe["gemini_confidence"] = 0.0
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Llama a Gemini para la última vela y guarda la decisión."""
        pair = metadata["pair"]
        logger.info(f"[ENTRY] populate_entry_trend | {pair} | velas={len(dataframe)}")
        if len(dataframe) == 0:
            return dataframe

        # Autopausa: si el bot está en pausa, no abrir nuevos trades
        if time.time() < self._autopause_until:
            mins_left = (self._autopause_until - time.time()) / 60
            logger.info(f"[AUTOPAUSA] Bot en pausa, faltan {mins_left:.0f} min | {self._autopause_reason}")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # Detectar régimen de mercado y actualizar multiplicadores
        regime = self._detect_regime(dataframe)

        # Régimen CAOS o BAJISTA_VOLATIL: no abrir nuevas posiciones
        if self._regime_stake_mult == 0.0:
            logger.info(f"[REGIME] {regime} — no se abren trades en este régimen")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 3: blacklist dinámica (3 losses en 24h = bloqueado) ────────
        now_ts = time.time()
        pair_losses_24h = [t for t in self._daily_losses.get(pair, []) if now_ts - t < 86400]
        self._daily_losses[pair] = pair_losses_24h
        if len(pair_losses_24h) >= self._MAX_DAILY_LOSSES:
            logger.debug(f"[BLACKLIST-DIN] {pair} bloqueado: {len(pair_losses_24h)} losses en 24h")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 2: cooldown 2h tras pérdida ───────────────────────────────
        loss_cooldown_until = self._loss_cooldown.get(pair, 0)
        if now_ts < loss_cooldown_until:
            mins_left = (loss_cooldown_until - now_ts) / 60
            logger.debug(f"[LOSS-COOLDOWN] {pair} en cooldown, faltan {mins_left:.0f} min")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Umbrales adaptativos según régimen (estilo NostalgiaForInfinityX7) ──
        # Mercado alcista calmado → más permisivo (RSI<65)
        # Transición / rango    → equilibrado   (RSI<55)
        # Bajista / volátil     → estricto       (RSI<40, solo capitulaciones)
        if regime in ("TENDENCIA_ALCISTA_CALMADA", "TENDENCIA_ALCISTA_NORMAL"):
            rsi_umbral, macd_req, f2_score_min = 60, False, 2
        elif regime in ("TENDENCIA_ALCISTA_VOLATIL", "TRANSICION", "RANGO_ESTRECHO"):
            rsi_umbral, macd_req, f2_score_min = 55, True, 3
        else:  # BAJISTA_*, CAOS, UNKNOWN
            rsi_umbral, macd_req, f2_score_min = 50, True, 4

        # ── NIVEL 1: prefiltro open source (RSI + MACD alcista + EMA) ─────────
        # Basado en EnhancedIndicatorStrategy y NostalgiaForInfinityX7 (GitHub)
        last = dataframe.iloc[-1]
        prev = dataframe.iloc[-2]
        macd_alcista = last['macd_hist'] > 0 or (last['macd_hist'] > prev['macd_hist'] and last['macd_hist'] > -0.0001)
        ema_ok = (last['ema20'] > last['ema50']) or (last['rsi'] < rsi_umbral * 0.85)
        macd_ok = macd_alcista if macd_req else True
        pass_l1 = (last['rsi'] < rsi_umbral) and macd_ok and ema_ok
        logger.info(f"[NIVEL1] {pair} | RSI={last['rsi']:.1f}(umbral={rsi_umbral},{regime}) | MACD_ok={macd_alcista}(req={macd_req}) | EMA_ok={ema_ok} | PASS={pass_l1}")
        if not pass_l1:
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 1: volumen vela actual ─────────────────────────────────
        # REFINADO: Volumen > 50% de media (vs 20% anterior) para confirmar interés real
        vol_mean_20 = dataframe['volume'].rolling(20).mean().iloc[-1]
        vol_threshold = 0.50  # 50% de media móvil 20 velas (NFI-style confirmation)
        if vol_mean_20 > 0 and last['volume'] < vol_mean_20 * vol_threshold:
            logger.debug(f"[VOL-FILTER] {pair} vela sin volumen ({last['volume']:.0f} < {vol_mean_20*vol_threshold:.0f}, umbral={vol_threshold*100:.0f}%)")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 2: ADX > 20 (evitar mercados laterales) ─────────────────
        # REFINADO: Solo operar cuando hay fuerza de tendencia clara (NFI protection)
        if last.get('adx', 0) < 20:
            logger.debug(f"[ADX-FILTER] {pair} ADX={last.get('adx',0):.1f} < 20 — mercado lateral, skip")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 3: confirmacion RSI (umbral dinámico según régimen) ───────
        prev = dataframe.iloc[-2]
        rsi_confirma = last['rsi'] < (rsi_umbral * 0.7) or last['rsi'] < prev['rsi']
        score_previo = sum([
            int(last['rsi'] < 48),
            int(last['stoch_rsi_k'] < 40),
            int(last['bb_pct'] < 35),
            int(last['macd_hist'] > 0),
            int(last['volume_ratio'] > 1.2),
        ])
        if not rsi_confirma and score_previo < f2_score_min:
            logger.debug(f"[F3-RSI] {pair} RSI no confirma y score bajo ({score_previo}/{f2_score_min}) regimen={regime} — skip")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 4: no entrar tras vela bajista grande ───────────────────────
        # En mercado alcista calmado se omite — NFI permite entradas post-rechazo
        prev_body = abs(prev['close'] - prev['open'])
        prev_range = (prev['high'] - prev['low']) if (prev['high'] - prev['low']) > 0 else 1e-10
        f4_activo = regime not in ("TENDENCIA_ALCISTA_CALMADA", "TENDENCIA_ALCISTA_NORMAL")
        if f4_activo and prev['close'] < prev['open'] and prev_body > prev_range * 0.6:
            logger.debug(f"[F4-CANDLE] {pair} vela bajista grande ({prev_body/prev_range*100:.0f}%) regimen={regime} — esperando confirmacion")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 5: soporte real bajo SL — buffer contra wicks ────────────
        dist_to_support_pct = (last['close'] - last['support_20']) / last['close'] * 100
        if dist_to_support_pct < 0.3:
            logger.debug(f"[F5-SL] {pair} soporte demasiado cerca {dist_to_support_pct:.2f}% — riesgo wick al SL")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── Filtro 6: momentum rebote proxima vela ────────────────────────────
        v1 = dataframe.iloc[-4]  # 3 velas atras
        v2 = dataframe.iloc[-3]  # 2 velas atras
        v3 = dataframe.iloc[-2]  # vela anterior
        momentum_rebote = sum([
            int(v3['close'] > v2['close']),                            # v_anterior cerro arriba
            int(v3['volume'] > v2['volume']),                          # volumen creciendo
            int(v2['low'] < v1['low']),                                # nuevo minimo (capitulacion)
            int(float(last['rsi']) > float(v3['rsi'])),                # RSI subiendo (last vs v_anterior)
            int(float(last['macd_hist']) > float(v3['macd_hist'])),    # MACD mejorando
        ])
        # En mercado alcista calmado basta 0/5 (cualquier vela es válida) — NFI
        f6_min = 0 if regime in ("TENDENCIA_ALCISTA_CALMADA", "TENDENCIA_ALCISTA_NORMAL") else 1
        if momentum_rebote < f6_min:
            logger.debug(f"[F6-MOMENTUM] {pair} sin senales de rebote ({momentum_rebote}/5) regimen={regime} — skip")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # ── NIVEL 2: scoring técnico REFINADO ───────────────────────────────
        # REFINADO: Mayor peso a indicadores con mejor histórico de éxito
        # Basado en análisis: trades >120min tienen 76% WR vs trades <60min con 7-31% WR
        score = 0
        max_score = 20  # Actualizado con nuevos pesos
        
        # RSI: Zona óptima 30-45 (sweet spot de rebote) — máximo 3 pts
        rsi_val = last['rsi']
        score += 3 if 30 <= rsi_val < 45 else (2 if 45 <= rsi_val < 50 else (1 if 20 <= rsi_val < 30 else 0))
        
        # StochRSI: Confirmación de sobreventa — 2 pts
        score += 2 if last['stoch_rsi_k'] < 30 else (1 if last['stoch_rsi_k'] < 40 else 0)
        
        # Bollinger: Zona de rebote < 25% — 2 pts  
        score += 2 if last['bb_pct'] < 25 else (1 if last['bb_pct'] < 35 else 0)
        
        # MACD + Volumen: Tendencia con fuerza — hasta 3 pts
        if last['macd_hist'] > 0:
            score += 3 if last['volume_ratio'] > 2.0 else (2 if last['volume_ratio'] > 1.5 else 1)
        
        # ADX: Fuerza de tendencia >25 — 2 pts (nuevo, crítico para evitar laterales)
        score += 2 if last.get('adx', 0) > 25 else (1 if last.get('adx', 0) > 20 else 0)
        
        # Patrones candlestick fuertes — 2 pts
        score += 2 if last['candle_pattern'] in ['HAMMER', 'BULL_ENGULF', 'MORNING_STAR'] else 0
        
        # Soporte técnico cercano pero no demasiado — hasta 2 pts
        score += 2 if 0.3 <= dist_to_support_pct < 1.0 else (1 if 1.0 <= dist_to_support_pct < 2.0 else 0)
        
        # Momentum de rebote fuerte — hasta 3 pts
        score += 3 if momentum_rebote >= 4 else (2 if momentum_rebote == 3 else (1 if momentum_rebote >= 2 else 0))
        
        # Señales adicionales de confirmación — 1 pt cada una
        score += 1 if last.get('ema_signal') == 'ABOVE' else 0
        score += 1 if last.get('rsi_bull_div') == 'BULL_DIV' else 0
        score += 1 if last.get('dist_vwap_pct', 0) < -0.5 else 0  # Debajo de VWAP
        score += 2 if (last.get('bb_squeeze', False) and last['macd_hist'] > 0) else 0  # Squeeze + breakout
        
        logger.debug(f"[SCORE] {pair} | score={score}/{max_score}")
        
        # REFINADO: Mínimo 4 pts (vs 2 anterior) para filtrar setups débiles
        # Basado en análisis: setups < 5 score correlacionan con trades perdedores <60min
        if score < 4:
            logger.debug(f"[SCORE-FILTER] {pair} score={score} < 4 — setup débil, skip")
            dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
            return dataframe

        # Codificar estado para Q-Learning
        q_state = self._encode_state(dataframe)

        # Score hint dinámico: la IA recibe contexto según la fuerza del setup
        # REFINADO: Umbrales ajustados a nuevo scoring /20
        if score >= 12:
            score_hint = f"SCORE_LOCAL={score}/{max_score} -> SETUP MUY FUERTE. Alta probabilidad de éxito. Entra salvo peligro claro."
        elif score >= 7:
            score_hint = f"SCORE_LOCAL={score}/{max_score} -> Setup moderado-fuerte. Entra si la mayoria de indicadores son alcistas."
        else:
            score_hint = f"SCORE_LOCAL={score}/{max_score} -> Setup debil. Solo entra si hay señal clara de rebote y confirmación de volumen."

        logger.info(f"[GROQ-CALL] {pair} | score={score}/{max_score} — llamando a Groq")
        decision = self._get_gemini_decision(pair, dataframe, score_hint=score_hint)

        if decision:
            logger.info(f"[GROQ-RESP] {pair} | accion={decision.get('accion')} | confianza={decision.get('confianza')} | razon={decision.get('razon','N/A')[:60]}")

        if decision and decision.get("accion") == "BUY":
            confidence = decision.get("confianza", 0)
            # Guardar el RSI actual en la decisión para la memoria de aprendizaje
            decision["rsi"] = float(dataframe["rsi"].iloc[-1])
            decision["q_state"] = q_state
            if confidence >= MIN_CONFIDENCE:
                dataframe.loc[dataframe.index[-1], "gemini_buy"] = 1
                dataframe.loc[dataframe.index[-1], "gemini_confidence"] = confidence
                logger.info(
                    f"[BUY] GEMINI BUY | {pair} | "
                    f"Confianza: {confidence}% | Régimen: {regime} | "
                    f"Q-estado: {q_state} | Razon: {decision.get('razon', 'N/A')}"
                )

        dataframe.loc[dataframe["gemini_buy"] == 1, "enter_long"] = 1
        return dataframe

    def custom_stake_amount(
        self, current_time, current_rate, proposed_stake,
        min_stake, max_stake, leverage, entry_tag, side, **kwargs
    ) -> float:
        """Escala el capital según la confianza de Gemini.
        Incluye protección drawdown diario y cooldown tras stop-loss.
        """
        pair = kwargs.get("pair", "")

        # Reset diario de pérdidas
        today = datetime.now(timezone.utc).date()
        if self._daily_loss_date != today:
            self._daily_loss_usd = 0.0
            self._daily_loss_date = today

        # Protección drawdown diario: si se perdió >5% del balance, no operar más hoy
        try:
            balance = self.wallets.get_available_capital()
        except Exception:
            balance = 45.0  # fallback demo
        max_daily_loss = balance * 0.05
        if self._daily_loss_usd >= max_daily_loss:
            logger.warning(
                f"[RISK] Drawdown diario ${self._daily_loss_usd:.2f} >= limite ${max_daily_loss:.2f}. "
                f"Sin nuevas entradas hasta mañana."
            )
            _tg(
                f"PROTECCION ACTIVADA\n"
                f"Perdida del dia: ${self._daily_loss_usd:.2f}\n"
                f"Limite 5% alcanzado. Sin nuevas entradas hasta mañana."
            )
            return 0.0

        # Cooldown tras stop-loss: esperar 15 min antes de reentrar al mismo par
        cooldown_ts = self._stoploss_cooldown.get(pair, 0)
        mins_restantes = (900 - (time.time() - cooldown_ts)) / 60
        if time.time() - cooldown_ts < 900:
            logger.info(f"[COOLDOWN] {pair} en cooldown, faltan {mins_restantes:.0f} min")
            return 0.0

        # Capital escalado por nivel de confianza de Groq/Gemini
        cache_key = next((k for k in self._gemini_decisions if k.startswith(pair + "_")), None)
        confidence = self._gemini_decisions[cache_key].get("confianza", 0) if cache_key else 0

        # REFINADO: Stake 30-33% para 3 trades = ~100% capital usado eficientemente
        # Menos trades pero de mayor calidad y tamaño
        if confidence >= 85:
            base_pct = 0.33   # excelente — 33% (3 trades × 33% = ~100%)
            nivel_txt = "[EXCELENTE]"
        elif confidence >= 70:
            base_pct = 0.30   # buena — 30%
            nivel_txt = "[BUENA]"
        elif confidence >= 60:
            base_pct = 0.25   # media — 25%
            nivel_txt = "[NORMAL]"
        else:
            base_pct = 0.20   # débil — 20% mínimo
            nivel_txt = "[DÉBIL]"

        stake = min(max(balance * base_pct, min_stake), max_stake)
        
        # ── Ajuste dinámico por racha reciente ──────────────────────────────
        # REFINADO: Protección de capital en rachas perdedoras, aprovechar momentum ganador
        racha_mult = 1.0
        if len(self._trade_memory) >= 3:
            recent_5 = self._trade_memory[-5:]  # últimos 5 trades
            wins_r = sum(1 for t in recent_5 if t["won"])
            losses_r = len(recent_5) - wins_r
            
            if losses_r >= 3:
                # Racha perdedora: reducir 50% stake para proteger capital
                racha_mult = 0.5
                logger.warning(f"[STAKE-RACHA] {pair} Racha {losses_r} losses — stake reducido 50%")
            elif wins_r >= 3:
                # Racha ganadora: aumentar 20% stake (momentum a favor)
                racha_mult = 1.2
                logger.info(f"[STAKE-RACHA] {pair} Racha {wins_r} wins — stake aumentado 20%")
        
        stake = round(stake * racha_mult * self._regime_stake_mult, 2)
        stake = max(stake, min_stake) if stake > 0 else 0.0
        logger.info(f"[STAKE] {nivel_txt} {stake:.2f}$ ({base_pct*racha_mult*100:.0f}% balance) | {pair} | conf={confidence}% | racha_mult={racha_mult} | regime_mult={self._regime_stake_mult}")
        return stake

    def custom_stoploss(
        self, pair: str, trade, current_time, current_rate: float,
        current_profit: float, after_fill: bool, **kwargs
    ) -> float:
        """Stop loss dinámico basado en ATR × multiplicador según volatilidad y régimen."""
        # Obtener ATR% real del par desde decisión cacheada (guardado en _get_gemini_decision)
        last_decision = next((v for k, v in self._gemini_decisions.items() if k.startswith(pair + "_")), {})
        atr_pct = last_decision.get("atr_pct", 1.0)  # % de ATR real, default 1%
        regime = self._market_regime
        if "CALMADA" in regime or regime == "RANGO_TRANQUILO":
            multiplier = 1.2  # stop más ajustado en calma
        elif "VOLATIL" in regime or "CAOS" in regime:
            multiplier = 2.0  # stop más amplio en volatilidad
        elif "TRANSICION" in regime:
            multiplier = 1.5
        else:
            multiplier = 1.5  # default neutro

        # Calcular stop distance: ATR% * multiplicador + buffer 0.3% anti-wick
        stop_distance = max(0.012, min(0.030, (atr_pct / 100) * multiplier + 0.003))
        # Si ya estamos en profit > 2%, activar trailing más ajustado
        if current_profit > 0.02:
            return -0.005  # trailing muy ajustado una vez en ganancia
        return -stop_distance

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Señal de salida basada en RSI sobrecomprado o decisión cacheada de Gemini."""
        pair = metadata["pair"]
        cache_key = f"{pair}_{dataframe.index[-1]}"
        decision = self._gemini_decisions.get(cache_key)

        # Bug fix: limpiar decisiones viejas >2h para evitar fuga de memoria
        now_ts = time.time()
        cutoff = now_ts - 7200
        stale = [k for k, v in self._gemini_decisions.items()
                 if v.get("_ts", now_ts) < cutoff]
        for k in stale:
            del self._gemini_decisions[k]

        # Bug fix: RSI sobrecomprado SOLO en la última vela, no en todas las históricas
        last_rsi = float(dataframe["rsi"].iloc[-1])
        if last_rsi > self.rsi_sell_threshold.value:
            dataframe.loc[dataframe.index[-1], "gemini_sell"] = 1

        # Evening Star — techo de 3 velas detectado = salir antes de la caida
        last_candle = dataframe["candle_pattern"].iloc[-1]
        if last_candle == "EVENING_STAR":
            dataframe.loc[dataframe.index[-1], "gemini_sell"] = 1
            logger.info(f"[SELL] EVENING_STAR detectado en {pair} — salida por techo")

        if decision and decision.get("accion") in ["SELL", "CLOSE"]:
            dataframe.loc[dataframe.index[-1], "gemini_sell"] = 1
            logger.info(
                f"[SELL] GEMINI SELL | {pair} | "
                f"Razon: {decision.get('razon', 'N/A')}"
            )

        dataframe.loc[dataframe["gemini_sell"] == 1, "exit_long"] = 1
        return dataframe

    def _get_gemini_exit_decision(self, pair: str, current_profit: float, trade_duration_min: float) -> Optional[str]:
        """Pregunta a Groq si conviene cerrar el trade ahora o esperar más.
        Solo se llama cuando hay profit >0.5% o pérdida >0.8% para ahorrar llamadas.
        Retorna 'cerrar' si la IA recomienda salir, None si recomienda esperar.
        """
        if not self._gemini_client:
            return None

        elapsed = time.time() - self._last_gemini_call
        if elapsed < RATE_LIMIT_SECONDS:
            time.sleep(RATE_LIMIT_SECONDS - elapsed)

        try:
            last_decision = next((v for k, v in self._gemini_decisions.items() if k.startswith(pair + "_")), {})
            atr_pct = last_decision.get("atr_pct", 1.0)
            regime = self._market_regime
            profit_pct = round(current_profit * 100, 2)
            signo = "+" if profit_pct >= 0 else ""

            prompt = f"""Eres un trader crypto. Trade abierto en {pair}. RESPONDE SOLO JSON.

ESTADO DEL TRADE:
- Profit actual: {signo}{profit_pct}%
- Duracion: {trade_duration_min:.0f} minutos
- ATR (volatilidad): {atr_pct:.2f}%
- Regimen actual: {regime}

PREGUNTA: ¿Cierro el trade ahora o espero más?

REGLAS:
- Cierra si: profit > ATR*1.5 y hay señal de reversión
- Cierra si: perdida > 0.8% y regime BAJISTA
- Cierra si: duracion > 90min y profit < 0.5%
- Espera si: profit creciendo y regime ALCISTA
- Espera si: profit < 1.0% y trade < 30min

JSON: {{"accion":"CERRAR","razon":"max8palabras"}} o {{"accion":"ESPERAR","razon":"max8palabras"}}"""

            api_entry = self._get_active_api_entry()
            if not api_entry:
                return None

            raw = self._call_llm(api_entry, prompt)
            self._last_gemini_call = time.time()
            raw = raw.replace("```json", "").replace("```", "").strip()
            decision = json.loads(raw)
            accion = decision.get("accion", "ESPERAR").upper()
            razon = decision.get("razon", "")[:50]
            logger.info(f"[EXIT-IA] {pair} | {accion} | profit={signo}{profit_pct}% | razon={razon}")
            return "ia_exit" if accion == "CERRAR" else None

        except Exception as e:
            logger.debug(f"[EXIT-IA] Error en decision de salida: {e}")
            return None

    def custom_exit(self, pair: str, trade, current_time, current_rate: float,
                    current_profit: float, **kwargs) -> Optional[str]:
        """TP dinamico + IA decide salida - REFINADO para balance riesgo/reward."""
        last_decision = next((v for k, v in self._gemini_decisions.items() if k.startswith(pair + "_")), {})
        confidence = last_decision.get("confianza", 0)
        trade_duration = (current_time - trade.open_date_utc).total_seconds() / 60
        profit_pct = current_profit * 100

        # ── REGLA 1: Breakeven stop tras +2% profit (proteger capital) ──
        # REFINADO: Una vez en +2%, no permitir que vuelva a pérdida
        if current_profit >= 0.02 and current_profit < 0.025:
            # Activar stop en breakeven + 0.3% (cubre fees + spread)
            if trade_duration > 20:  # Dar 20min de margen para consolidar
                logger.info(f"[BREAKEVEN] {pair} profit={profit_pct:.2f}% — stop activado en +0.3%")
                return "breakeven_stop"

        # ── REGLA 2: No cerrar trades fuertes antes de 60min ──
        # REFINADO: Trades <60min tienen WR 7-31% vs +120min con 76% WR
        if trade_duration < 60 and confidence >= 70:
            if current_profit > 0 and current_profit < 0.015:
                # Profit pequeño pero setup fuerte — dar tiempo a desarrollarse
                logger.debug(f"[EXIT-DELAY] {pair} profit={profit_pct:.2f}% dur={trade_duration:.0f}min — esperando desarrollo")
                return None

        # ── REGLA 3: TP escalonado según confianza ──
        if confidence >= 85:
            # Alta confianza: TP más agresivo pero con tiempo de desarrollo
            if trade_duration >= 90 and current_profit >= 0.03:
                logger.info(f"[TP-ALTO] {pair} | conf={confidence}% | profit={profit_pct:.2f}% >= 3% tras 90min")
                return "tp_alta_confianza_90min"
            elif trade_duration >= 150 and current_profit >= 0.025:
                logger.info(f"[TP-MEDIO] {pair} | conf={confidence}% | profit={profit_pct:.2f}% >= 2.5% tras 150min")
                return "tp_alta_confianza_150min"
        elif confidence >= 60:
            # Confianza media: dejar correr más tiempo
            if trade_duration >= 180 and current_profit >= 0.02:
                return "tp_media_confianza"

        # ── REGLA 4: IA decide salida con umbrales más exigentes ──
        # REFINADO: Umbral 1.0% / -1.0% (vs 0.8% / -0.7%) para evitar cierres prematuros
        consultar_ia = (
            (profit_pct >= 1.0 and trade_duration >= 20) or   # profit >1.0% y ya lleva 20min
            (profit_pct <= -1.0 and trade_duration >= 15) or   # pérdida >1.0% tras 15min
            (trade_duration >= 240 and profit_pct < 0.5)       # tiempo >4h con profit <0.5%
        )
        if consultar_ia:
            exit_signal = self._get_gemini_exit_decision(pair, current_profit, trade_duration)
            if exit_signal:
                return exit_signal

        return None

    def _get_gemini_decision(
        self, pair: str, dataframe: DataFrame, score_hint: str = ""
    ) -> Optional[dict]:
        """
        Llama a Gemini API con el contexto del mercado.
        Respeta rate limit: máx 1 llamada por vela por par.
        Fallback: retorna HOLD si hay cualquier error.
        """
        if not self._gemini_client:
            return {"accion": "HOLD", "confianza": 0, "razon": "Gemini no configurado"}

        cache_key = f"{pair}_{dataframe.index[-1]}"
        if cache_key in self._gemini_decisions:
            return self._gemini_decisions[cache_key]

        elapsed = time.time() - self._last_gemini_call
        if elapsed < RATE_LIMIT_SECONDS:
            time.sleep(RATE_LIMIT_SECONDS - elapsed)

        try:
            last = dataframe.iloc[-1]

            # Detectar momentum: precio de las últimas 3 velas
            closes = dataframe['close'].tail(4).values
            momentum = "SUBIENDO" if closes[-1] > closes[-2] > closes[-3] else ("BAJANDO" if closes[-1] < closes[-2] < closes[-3] else "LATERAL")

            macd_trend = "ALCISTA" if last['macd_hist'] > 0 else "BAJISTA"
            bb_pos = "SOBRECOMPRA" if last['bb_pct'] > 80 else ("SOBREVENTA" if last['bb_pct'] < 20 else "MEDIO")
            news, news_sentiment = _fetch_crypto_news(pair)
            fg_value, fg_label = _fetch_fear_greed()
            trend_1h, rsi_1h = self._fetch_1h_trend(pair)
            reddit_score, reddit_mentions = _fetch_reddit_sentiment(pair)
            trending_coins = _fetch_trending_coins()
            coin_symbol = pair.split("/")[0]
            is_trending = "SI" if coin_symbol in trending_coins else "NO"
            # Sentiment compuesto: promedio de FG, noticias y reddit
            social_avg = int((news_sentiment + reddit_score) / 2)
            social_label = "BULLISH" if social_avg > 60 else ("BEARISH" if social_avg < 40 else "NEUTRAL")

            # Hora UTC — evitar operar entre 23:00-04:00 UTC (caída nocturna ampliada)
            hora_utc = datetime.now(timezone.utc).hour
            hora_peligro = "SI" if (hora_utc >= 23 or hora_utc < 4) else "NO"

            # Memoria activa: aprender de errores por par específico
            pair_trades = [t for t in self._trade_memory if t["pair"] == pair]
            losses = [t for t in pair_trades if not t["won"]]
            wins = [t for t in pair_trades if t["won"]]
            memory_ctx = ""
            if pair_trades:
                win_rate = len(wins) / len(pair_trades) * 100
                memory_ctx = f" Historial {pair}: {len(wins)}W/{len(losses)}L ({win_rate:.0f}% WR)"
                if losses:
                    last_loss = losses[-1]
                    memory_ctx += f" UltimaLoss: RSI={last_loss.get('rsi', '?')} conf={last_loss.get('confianza', '?')}%"
                    if len(losses) >= 2 and losses[-2].get('rsi'):
                        memory_ctx += f" PATRON_PERDIDA: evitar RSI>{min(l.get('rsi',99) for l in losses[-3:] if l.get('rsi'))}"

            rsi_status = "SOBRECOMPRADO" if last['rsi'] > 70 else ("SOBREVENTA" if last['rsi'] < 30 else "OK")
            stoch_status = "SOBRECOMPRADO" if last['stoch_rsi_k'] > 80 else ("SOBREVENTA" if last['stoch_rsi_k'] < 20 else "OK")
            cci_status = "SOBRECOMPRADO" if last['cci'] > 100 else ("SOBREVENTA" if last['cci'] < -100 else "NEUTRO")
            wr_status = "SOBRECOMPRADO" if last['williams_r'] > -20 else ("SOBREVENTA" if last['williams_r'] < -80 else "NEUTRO")

            # Q-Learning hint: acción preferida según experiencia acumulada
            q_state_now = self._encode_state(dataframe)
            with self._q_lock:
                q_vals = self._q_table[q_state_now]
            q_best_action = self._q_actions[q_vals.index(max(q_vals))]
            q_confidence = max(q_vals)
            q_hint = f"Q-Learning recomienda {q_best_action} (valor={q_confidence:.2f}, episodios={self._q_episodes})"

            adx_status = "FUERTE" if last['adx'] > 40 else ("TENDENCIA" if last['adx'] > 25 else "LATERAL")
            mfi_status = last['mfi_signal']

            prompt = f"""Eres un trader crypto. Par: {pair}. RESPONDE SOLO JSON, sin texto adicional.

{score_hint}
INDICADORES: RSI={last['rsi']:.0f} MACD={macd_trend} StochRSI={last['stoch_rsi_k']:.0f} Vol={last['volume_ratio']:.1f}x BB={last['bb_pct']:.0f}% Vela={last['candle_pattern']}
TENDENCIA: Regimen={self._market_regime} EMA50={last['ema_signal']} EMA200={last['ema200_signal']} 1H={trend_1h} RSI1H={rsi_1h:.0f}
NIVELES: Soporte={last['dist_support_pct']:.1f}%abajo Resistencia={last['dist_resistance_pct']:.1f}%arriba ATR={last['atr_pct']:.2f}%
MERCADO: FG={fg_value}[{fg_label}] Social={social_label} Trending={is_trending}
PELIGRO: DivRSI={last['rsi_divergence']} HoraBaja={hora_peligro}{' | ' + memory_ctx if memory_ctx else ''}

VETO_OBLIGATORIO (responde HOLD si cualquiera): DivRSI=BEARISH_DIV | HoraBaja=SI | Vela=SHOOTING_STAR o BEAR_ENGULF | Regimen=CAOS_VOLATIL | Vol<0.3x
VENTA (responde SELL si): RSI>75 y MACD=BAJISTA | StochRSI>85 y MFI>80

Sigue el SCORE_LOCAL como guia principal. Si no hay VETO activo, prioriza BUY.
JSON: {{"accion":"BUY","confianza":65,"razon":"max10palabras"}}"""

            api_entry = self._get_active_api_entry()
            if not api_entry:
                return {"accion": "HOLD", "confianza": 0, "razon": "Todas las APIs agotadas"}

            raw = self._call_llm(api_entry, prompt)
            self._last_gemini_call = time.time()
            raw = raw.replace("```json", "").replace("```", "").strip()
            # Intentar parsear JSON, si falla intentar extraer con regex
            try:
                decision = json.loads(raw)
            except json.JSONDecodeError:
                import re
                accion_m = re.search(r'"accion"\s*:\s*"(BUY|SELL|HOLD|CLOSE)"', raw, re.IGNORECASE)
                conf_m = re.search(r'"confianza"\s*:\s*(\d+)', raw)
                razon_m = re.search(r'"razon"\s*:\s*"([^"]{1,80})', raw)
                if accion_m:
                    decision = {
                        "accion": accion_m.group(1).upper(),
                        "confianza": int(conf_m.group(1)) if conf_m else 50,
                        "razon": razon_m.group(1) if razon_m else "parsed",
                    }
                else:
                    raise

            if "accion" not in decision:
                raise ValueError("Respuesta inválida de Gemini")

            valid_actions = {"BUY", "SELL", "HOLD", "CLOSE"}
            if decision["accion"] not in valid_actions:
                decision["accion"] = "HOLD"

            decision["confianza"] = max(0, min(100, int(decision.get("confianza", 0))))

            logger.info(
                f"[GEMINI] {pair} | "
                f"{decision['accion']} ({decision.get('confianza', 0)}%) | "
                f"{decision.get('razon', '')}"
            )

            decision["_ts"] = time.time()
            decision["atr_pct"] = float(last.get("atr_pct", 1.0))
            decision["adx"] = float(last.get("adx", 0))
            decision["mfi"] = float(last.get("mfi", 50))
            self._gemini_decisions[cache_key] = decision
            if len(self._gemini_decisions) > 100:
                oldest = list(self._gemini_decisions.keys())[0]
                del self._gemini_decisions[oldest]

            return decision

        except json.JSONDecodeError as e:
            logger.warning(f"[WARN] Gemini JSON invalido: {e} | Raw: {raw}")
            return {"accion": "HOLD", "confianza": 0, "razon": "JSON parse error"}

        except Exception as e:
            err_str = str(e)
            if "429" in err_str or "quota" in err_str.lower() or "exhausted" in err_str.lower() or "RESOURCE_EXHAUSTED" in err_str:
                # Marcar la API actual como agotada en el contador local
                # para que _rotate_api la salte y no siga intentando
                with self._api_lock:
                    label = GROQ_API_POOL[self._api_index]["label"]
                    limit = GROQ_API_POOL[self._api_index]["daily_limit"]
                    self._api_usage[label]["count"] = limit
                logger.warning(f"[WARN] API {label} agotada en Google, marcando como usada y rotando...")
                if not self._rotate_api():
                    _tg(
                        "APIS AGOTADAS\n"
                        "Todas las APIs de Gemini llegaron al limite diario de Google.\n"
                        "El bot seguira analizando pero en modo HOLD hasta medianoche UTC.\n"
                        "Reset automatico a las 00:00 UTC."
                    )
            elif "503" in err_str or "UNAVAILABLE" in err_str or "ServiceUnavailable" in err_str:
                logger.warning("[WARN] Gemini 503, esperando 5s y rotando API...")
                time.sleep(5)
                self._rotate_api()
            else:
                logger.error(f"[ERROR] Llamada a IA: {e}")
            return {"accion": "HOLD", "confianza": 0, "razon": f"Error: {str(e)[:50]}"}

    def confirm_trade_entry(
        self, pair, order_type, amount, rate, time_in_force,
        current_time, entry_tag, side, **kwargs
    ) -> bool:
        """Última verificación antes de abrir trade — notifica por Telegram."""
        total_cost = amount * rate
        confidence = 0
        razon = ""
        for k, v in self._gemini_decisions.items():
            if k.startswith(pair + "_"):
                confidence = v.get("confianza", 0)
                razon = v.get("razon", "")[:60]
                break
        nivel = "[EXCELENTE]" if confidence >= 85 else ("[BUENA]" if confidence >= 75 else "[NORMAL]")
        moneda = pair.split("/")[0]
        last_dec_e = next((v for k, v in self._gemini_decisions.items() if k.startswith(pair + "_")), {})
        atr_e = last_dec_e.get("atr_pct", 1.0)
        reg = self._market_regime
        sl_m = 1.2 if ("CALMADA" in reg or reg == "RANGO_TRANQUILO") else (2.0 if ("VOLATIL" in reg or "CAOS" in reg) else 1.5)
        stop_pct = max(0.8, min(3.0, atr_e * sl_m))
        ganancia_esperada = total_cost * 0.02
        perdida_max = total_cost * (stop_pct / 100)
        rr = ganancia_esperada / perdida_max if perdida_max > 0 else 0
        adx_e = last_dec_e.get("adx", 0)
        mfi_e = last_dec_e.get("mfi", 50)
        logger.info(
            f"[ENTRY_OK] {pair} | Precio: {rate:.6f} | Capital: ${total_cost:.2f} | ADX={adx_e:.0f} | Stop={stop_pct:.1f}%"
        )
        _tg(
            f"[COMPRA] {moneda}/USDT\n"
            f"---------------------------\n"
            f"Precio:   {rate:.4f} USDT\n"
            f"Capital:  ${total_cost:.2f}\n"
            f"TP:       +2.5%  (+${ganancia_esperada:.2f})\n"
            f"SL:       -{stop_pct:.1f}%  (-${perdida_max:.2f})\n"
            f"---------------------------\n"
            f"IA:       {confidence}% {nivel}\n"
            f"Mercado:  {self._market_regime}\n"
            f"Razon:    {razon}"
        )
        return True

    def confirm_trade_exit(
        self, pair, trade, order_type, amount, rate,
        time_in_force, exit_reason, current_time, **kwargs
    ) -> bool:
        """Log y notificación Telegram cuando se cierra un trade."""
        profit_pct = trade.calc_profit_ratio(rate) * 100
        ganó = profit_pct > 0
        profit_usd = trade.stake_amount * (profit_pct / 100)
        resultado = "GANANCIA" if ganó else "PERDIDA"
        signo = "+" if ganó else ""
        moneda = pair.split("/")[0]
        logger.info(
            f"[{'WIN' if ganó else 'LOSS'}] Trade cerrado | {pair} | "
            f"P&L: {profit_pct:.2f}% | Razon: {exit_reason}"
        )
        with self._daily_trades_lock:
          self._daily_trades.append({
            "pair": pair, "profit_usd": profit_usd,
            "profit_pct": profit_pct, "won": ganó
          })
        # Activar cooldown si fue stop-loss para evitar reentrar inmediatamente
        if exit_reason in ("stop_loss", "stoploss", "trailing_stop_loss") and not ganó:
            self._stoploss_cooldown[pair] = time.time()
            self._daily_loss_usd += abs(profit_usd)
            logger.info(f"[COOLDOWN] {pair} en cooldown 15min tras stop-loss. Perdida hoy: ${self._daily_loss_usd:.2f}")

        # Filtro 2: cooldown 2h si cualquier pérdida (no solo stop-loss)
        if not ganó:
            self._loss_cooldown[pair] = time.time() + 1800
            logger.info(f"[LOSS-COOLDOWN] {pair} en cooldown 30min tras perdida {profit_pct:.2f}%")
            # Filtro 3: registrar para blacklist dinámica
            self._daily_losses.setdefault(pair, []).append(time.time())
            recent = [t for t in self._daily_losses[pair] if time.time() - t < 86400]
            self._daily_losses[pair] = recent
            if len(recent) >= self._MAX_DAILY_LOSSES:
                logger.warning(f"[BLACKLIST-DIN] {pair} bloqueado automaticamente: {len(recent)} losses en 24h")

        # Guardar en memoria de aprendizaje con RSI y contexto para detección de patrones
        last_decision = next((v for k, v in self._gemini_decisions.items() if k.startswith(pair + "_")), {})
        self._trade_memory.append({
            "pair": pair,
            "accion": last_decision.get("accion", "BUY"),
            "confianza": last_decision.get("confianza", 0),
            "rsi": last_decision.get("rsi", None),
            "razon": last_decision.get("razon", ""),
            "exit_reason": exit_reason,
            "profit_usd": round(profit_usd, 3),
            "profit_pct": round(profit_pct, 2),
            "won": ganó,
        })
        if len(self._trade_memory) > 50:
            self._trade_memory.pop(0)

        # ── Q-Learning: actualizar tabla con resultado del trade ──
        last_decision = next((v for k, v in self._gemini_decisions.items() if k.startswith(pair + "_")), {})
        q_state = last_decision.get("q_state", 50)
        q_action = 1  # BUY=1 (la acción que tomamos fue comprar)
        reward = self._q_reward(profit_pct / 100, trade.stake_amount)
        next_state = q_state  # aproximación conservadora
        with self._q_lock:
            q_val_antes = self._q_table[q_state][q_action]
        self._q_update(q_state, q_action, reward, next_state)
        with self._q_lock:
            q_val_despues = self._q_table[q_state][q_action]
        # Guardar experiencia en buffer de replay
        exp = {"state": q_state, "action": q_action, "reward": reward, "next_state": next_state}
        self._experience_replay.append(exp)
        if len(self._experience_replay) > 500:
            self._experience_replay.pop(0)
        # Experience Replay: repasar 32 experiencias pasadas
        self._experience_replay_train()

        # ── Epsilon dinámico: ajustar exploración según racha ──────────────
        recientes = self._trade_memory[-5:] if len(self._trade_memory) >= 3 else []
        if recientes:
            racha_wins = sum(1 for t in recientes[-3:] if t["won"])
            racha_losses = sum(1 for t in recientes[-3:] if not t["won"])
            if racha_wins >= 3:
                # 3 wins seguidos: confiar más en lo aprendido (bajar exploración)
                self._q_epsilon = max(self._q_epsilon_min, self._q_epsilon * 0.92)
                logger.info(f"[EPSILON] Racha de 3 WINS — epsilon reducido a {self._q_epsilon:.3f} (más explotación)")
            elif racha_losses >= 2:
                # 2 losses seguidos: explorar más para salir del patrón
                self._q_epsilon = min(0.4, self._q_epsilon * 1.15)
                logger.info(f"[EPSILON] Racha de 2 LOSSES — epsilon subido a {self._q_epsilon:.3f} (más exploración)")

        # Guardar Q-table en disco
        self._save_qtable()

        # ── Log de aprendizaje visible ──────────────────────────────────────
        resultado_emoji = "✅ WIN" if ganó else "❌ LOSS"
        patron_rsi = last_decision.get("rsi", "?")
        confianza_ia = last_decision.get("confianza", "?")
        logger.info(
            f"[APRENDIZAJE] {resultado_emoji} | {pair} | {profit_pct:+.2f}% | "
            f"RSI_entrada={patron_rsi} | IA_conf={confianza_ia}% | "
            f"Q-estado={q_state} | Q-val: {q_val_antes:.3f} → {q_val_despues:.3f} | "
            f"reward={reward:.3f} | epsilon={self._q_epsilon:.3f} | episodios={self._q_episodes}"
        )
        # Detectar y loguear patrón aprendido si hay suficiente historial
        pair_hist = [t for t in self._trade_memory if t["pair"] == pair]
        if len(pair_hist) >= 3:
            wins_par = [t for t in pair_hist if t["won"]]
            wr_par = len(wins_par) / len(pair_hist) * 100
            logger.info(f"[PATRON] {pair} | Historial: {len(wins_par)}W/{len(pair_hist)-len(wins_par)}L ({wr_par:.0f}% WR) | episodios_totales={self._q_episodes}")

        # ── Autopausa + Checklist live: evaluar métricas cada 5 trades ──
        self._trades_since_check += 1
        if self._trades_since_check >= 5:
            self._trades_since_check = 0
            self._check_autopause()
        self._check_live_readiness()

        razones = {
            "roi": "Objetivo de ganancia alcanzado",
            "stop_loss": "Stop loss activado",
            "trailing_stop_loss": "Stop loss dinamico activado",
            "stoploss": "Stop loss activado",
            "force_sell": "Venta manual",
            "sell_signal": "Senal de venta de Gemini",
            "exit_signal": "Senal de salida de Gemini",
            # REFINADO: Nuevas razones de salida mejoradas
            "breakeven_stop": "Stop en breakeven tras +2%",
            "tp_alta_confianza_90min": "TP alto tras 90min (conf 85%)",
            "tp_alta_confianza_150min": "TP medio tras 150min (conf 85%)",
            "tp_media_confianza": "TP confianza media tras 180min",
            "ia_exit": "IA decidio cerrar posicion",
        }
        razon_es = razones.get(exit_reason, exit_reason)
        last_dec_x = next((v for k, v in self._gemini_decisions.items() if k.startswith(pair + "_")), {})
        adx_x = last_dec_x.get("adx", 0)
        mfi_x = last_dec_x.get("mfi", 50)
        resultado_txt = "[WIN]" if ganó else "[LOSS]"
        _tg(
            f"{resultado_txt} {moneda}/USDT\n"
            f"---------------------------\n"
            f"Resultado: {signo}{profit_pct:.2f}%  ({signo}${abs(profit_usd):.2f})\n"
            f"Capital:   ${trade.stake_amount:.2f}\n"
            f"Motivo:    {razon_es}\n"
            f"---------------------------\n"
            f"Mercado:   {self._market_regime}"
        )
        return True
