import os

# ================== THREADS (DEBE ir antes de cualquier import numérico) ==================
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"
os.environ["NUMEXPR_NUM_THREADS"] = "1"

import time
import json
import csv
import requests
import ccxt
import pandas as pd
from ta.momentum import RSIIndicator
from ta.trend import EMAIndicator, ADXIndicator
from ta.volatility import AverageTrueRange
from dotenv import load_dotenv
from datetime import datetime
from config import (
    SYMBOL, TIMEFRAME, LEVERAGE, RISK_PERCENT, STOP_LOSS_PERCENT,
    EMA_FAST, EMA_SLOW, RSI_PERIOD, HEARTBEAT_INTERVAL,
)

# ================== CONSTANTES OPTIMIZADAS (backtest 14d) ==================
TP_MULTIPLIER = 2.0              # Optimizado v2: R:R 2.0:1 — más alcanzable, sube WR de 39→44%
MIN_RR = 1.8                     # R:R mínimo aceptable: trades con RR < 1.8 tienen 100% SL en CSV
MIN_ADX = 15 #18                     # Relajado: ADX 25 bloqueaba todo en mercado lateral
LONG_RSI_MIN = 38                # Ligeramente ampliado
LONG_RSI_MAX = 62 #58                # Ligeramente ampliado
SHORT_RSI_MIN = 48               # Ligeramente ampliado
LONG_VOL_MIN = 0.0008            # Relajado: 0.0012 era muy exigente para 5m
SHORT_VOL_MIN = 0.0012           # Relajado: shorts también necesitan oportunidad
ANTI_FLIP_FLOP_S = 900           # 15 min — flip-flops causan muchos SL
COOLDOWN_SL1_S = 300             # Cooldown 1 SL: 5 min
COOLDOWN_SL2_S = 3600            # Cooldown 2 SL: 1 hora
COOLDOWN_SL3_S = 10800           # Cooldown 3+ SL: 3 horas
COOLDOWN_TP_S = 600              # Cooldown post-TP: 10 min (era 2min — causó flip-flop 14-Apr)
BLOCK_HOUR_START = 22            # Ventana operativa: 8-22 UTC (14 horas)
BLOCK_HOUR_END = 8              # Ampliado: hora 10 y 19-20 tenían WR positivo
TOXIC_HOURS = {10, 11, 12, 13, 14, 15, 16}  # +13h y 14h UTC agregados (múltiples SL detectados en CSV)
MAX_DAILY_LOSSES = 2             # Reducido de 3→2: días con 2+ SL siempre cierran negativos
MAX_DAILY_TRADES = 3             # Reducido de 5→3: más trades/día correlacionan con días negativos
ATR_MULTIPLIER_SL = 1.5          # SL dinámico basado en ATR
ATR_MULTIPLIER_TP = 3.5          # TP dinámico basado en ATR
USE_DYNAMIC_SLTP = True          # Usar SL/TP basado en ATR
ENABLE_SHORTS = False            # Shorts deshabilitados: 25% WR, PNL negativo en backtest
SHORT_ADX_MIN = 28               # Si se habilitan, exigir ADX alto (tendencia clara)
SHORT_CONFIRMATIONS_MIN = 2      # Shorts necesitan 2 de 3 confirmaciones (más estricto)
LONG_CONFIRMATIONS_MIN = 1      # Longs necesitan 1 de 3 confirmaciones (más flexible)

load_dotenv()
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
STATE_FILE = os.path.join(BASE_DIR, "state.json")
TRADES_FILE = os.path.join(BASE_DIR, "trades_ethusdt.csv")
REPORT_FILE = os.path.join(BASE_DIR, "last_report.json")

exchange = ccxt.binance({
    "apiKey": os.getenv("BINANCE_FUTURES_KEY"),
    "secret": os.getenv("BINANCE_FUTURES_SECRET"),
    "enableRateLimit": True,
    "options": {"defaultType": "future"},
})

exchange.set_leverage(LEVERAGE, SYMBOL)
try:
    exchange.set_margin_mode("isolated", SYMBOL)
except Exception:
    pass


# ================== UTILS ==================
def telegram(msg):
    url = f"https://api.telegram.org/bot{os.getenv('TELEGRAM_TOKEN')}/sendMessage"
    requests.post(url, data={"chat_id": os.getenv("TELEGRAM_CHAT_ID"), "text": msg})


def init_csv():
    if not os.path.exists(TRADES_FILE):
        with open(TRADES_FILE, "w", newline="") as f:
            writer = csv.writer(f)
            writer.writerow([
                "timestamp", "symbol", "side",
                "entry", "exit", "qty", "pnl_pct",
                "pnl_usdt", "result", "sl", "tp", "rr",
            ])


def get_position():
    positions = exchange.fetch_positions([SYMBOL])
    for p in positions:
        if abs(float(p["contracts"])) > 0:
            return {
                "side": "long" if float(p["contracts"]) > 0 else "short",
                "qty": abs(float(p["contracts"])),
                "entry": float(p["entryPrice"]),
            }
    return None


def load_state():
    if not os.path.exists(STATE_FILE):
        return {
            "position": False,
            "side": None,
            "entry_price": 0,
            "quantity": 0,
        }
    with open(STATE_FILE, "r") as f:
        return json.load(f)


def save_state(state):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2)


# ================= REPORTE =================
def hourly_report():
    try:
        now = time.time()
        last = 0
        if os.path.exists(REPORT_FILE):
            with open(REPORT_FILE) as f:
                last = json.load(f).get("last", 0)

        if now - last < HEARTBEAT_INTERVAL:
            return

        ohlcv = exchange.fetch_ohlcv(SYMBOL, TIMEFRAME, limit=100)
        df = pd.DataFrame(ohlcv, columns=["t", "o", "h", "l", "c", "v"])
        last_price = df["c"].iloc[-1]

        state = load_state()

        if state["position"]:
            entry = state["entry_price"]
            side = state["side"]
            qty = state.get("quantity", 0)
            entry_time = state.get("entry_time", 0)
            hold_time = int(now - entry_time) if entry_time else 0

            if side == "long":
                pnl_pct = ((last_price - entry) / entry) * 100
            else:
                pnl_pct = ((entry - last_price) / entry) * 100

            telegram(
                f"💓 Bot FUTURES activo\n"
                f"📍 EN POSICION {side.upper()}\n"
                f"📥 Entrada: {entry:.2f}\n"
                f"📤 Precio: {last_price:.2f}\n"
                f"📦 Qty: {qty}\n"
                f"⏱ Tiempo: {hold_time} s\n"
                f"⚙ Leverage: {LEVERAGE}x\n"
                f"📈 PNL aprox: {pnl_pct:.2f}%"
            )
        else:
            telegram(
                f"💓 Bot FUTURES activo\n"
                f"⏳ Sin posicion\n"
                f"📤 Precio: {last_price:.2f}"
            )

        with open(REPORT_FILE, "w") as f:
            json.dump({"last": now}, f)
    except Exception as e:
        try:
            telegram(f"⚠ Error en hourly_report: {e}")
        except Exception:
            pass


# ================== HELPERS ==================
def count_daily_trades():
    """Cuenta trades y SLs del día actual desde el CSV."""
    today = datetime.utcnow().strftime("%Y-%m-%d")
    daily_trades = 0
    daily_losses = 0
    if os.path.exists(TRADES_FILE):
        try:
            with open(TRADES_FILE, "r") as f:
                reader = csv.DictReader(f)
                for row in reader:
                    if row.get("timestamp", "").startswith(today):
                        daily_trades += 1
                        if "SL" in row.get("result", ""):
                            daily_losses += 1
        except Exception:
            pass
    return daily_trades, daily_losses


def compute_atr(df, period=14):
    """Calcula ATR para SL/TP dinámico."""
    atr = AverageTrueRange(df["h"], df["l"], df["c"], window=period)
    return atr.average_true_range().iloc[-1]


def confirm_volume_spike(df, lookback=20, threshold=1.3):
    """Confirma que el volumen actual es superior al promedio."""
    avg_vol = df["v"].tail(lookback).mean()
    current_vol = df["v"].iloc[-1]
    return current_vol > avg_vol * threshold


def confirm_candle_strength(df, side):
    """
    Confirma que la última vela cerrada apoya la dirección del trade.
    Para LONG: vela alcista (close > open) con cuerpo significativo.
    Para SHORT: vela bajista (close < open) con cuerpo significativo.
    """
    last_candle = df.iloc[-2]  # Penúltima vela (la cerrada)
    body = abs(last_candle["c"] - last_candle["o"])
    total_range = last_candle["h"] - last_candle["l"]

    if total_range == 0:
        return False

    body_ratio = body / total_range

    if side == "long":
        return last_candle["c"] > last_candle["o"] and body_ratio > 0.4
    else:
        return last_candle["c"] < last_candle["o"] and body_ratio > 0.4


def get_fill_price(order, fallback):
    """Obtiene el precio real de ejecución de una orden de mercado."""
    try:
        avg = order.get("average") or order.get("price")
        if avg:
            return float(avg)
    except Exception:
        pass
    try:
        o = exchange.fetch_order(order["id"], SYMBOL)
        avg = o.get("average") or o.get("price")
        if avg:
            return float(avg)
    except Exception:
        pass
    return fallback


def place_protective_orders(side, qty, sl, tp, retries=2):
    """
    Coloca TP y SL para una posición ya abierta, con reintentos.
    Devuelve (tp_ok, sl_ok) para que el llamador decida si la posición
    quedó protegida o hay que cerrarla de emergencia.
    """
    close_side = "sell" if side == "long" else "buy"
    tp_ok = sl_ok = False

    for attempt in range(retries):
        if not tp_ok:
            try:
                exchange.create_order(
                    SYMBOL, "TAKE_PROFIT_MARKET", close_side, qty,
                    params={
                        "stopPrice": exchange.price_to_precision(SYMBOL, tp),
                        "closePosition": True,
                    },
                )
                tp_ok = True
            except Exception as e:
                telegram(f"⚠ Intento {attempt + 1}: fallo al colocar TP: {e}")
        if not sl_ok:
            try:
                exchange.create_order(
                    SYMBOL, "STOP_MARKET", close_side, qty,
                    params={
                        "stopPrice": exchange.price_to_precision(SYMBOL, sl),
                        "closePosition": True,
                    },
                )
                sl_ok = True
            except Exception as e:
                telegram(f"⚠ Intento {attempt + 1}: fallo al colocar SL: {e}")
        if tp_ok and sl_ok:
            break
        time.sleep(1)

    return tp_ok, sl_ok


def emergency_close(side, qty, fallback_price):
    """
    Cierra a mercado una posición que no pudo protegerse con TP/SL.
    Devuelve el precio de cierre si tuvo éxito, o None si falló
    (en cuyo caso la posición sigue abierta y desprotegida en Binance).
    """
    try:
        if side == "long":
            order = exchange.create_market_sell_order(SYMBOL, qty, params={"reduceOnly": True})
        else:
            order = exchange.create_market_buy_order(SYMBOL, qty, params={"reduceOnly": True})
        exit_price = get_fill_price(order, fallback_price)
        telegram(
            f"🚨 Posición {side.upper()} cerrada de emergencia: "
            f"no se pudo colocar TP/SL."
        )
        return exit_price
    except Exception as e:
        telegram(
            f"🚨🚨 CRÍTICO: no se pudo colocar TP/SL NI cerrar la posición "
            f"{side.upper()}: {e}. Revisar Binance MANUALMENTE."
        )
        return None


def log_emergency_exit(side, entry, exit_price, qty, sl, tp, entry_time):
    """
    Registra en el CSV y por Telegram un cierre de emergencia, igual que se
    hace para los cierres normales por TP/SL, para que el historial de
    trades no tenga huecos. Devuelve pnl_pct para actualizar el contador de
    SL consecutivos.
    """
    now = time.time()
    timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

    if side == "long":
        pnl_pct = ((exit_price - entry) / entry) * 100
        pnl_usdt = (exit_price - entry) * qty
    else:
        pnl_pct = ((entry - exit_price) / entry) * 100
        pnl_usdt = (entry - exit_price) * qty
    fees = (entry * qty + exit_price * qty) * 0.0004
    pnl_usdt -= fees

    result = "EMERGENCIA 🚨"
    hold_min = (int(now - entry_time) // 60) if entry_time else 0

    risk_value = abs(entry - sl) if sl else 0
    reward_value = abs(tp - entry) if tp else 0
    rr = round(reward_value / risk_value, 2) if risk_value else 0

    with open(TRADES_FILE, "a", newline="") as f:
        writer = csv.writer(f)
        writer.writerow([
            timestamp, SYMBOL, side, entry, exit_price, qty,
            f"{pnl_pct:.2f}", f"{pnl_usdt:.2f}",
            result, sl, tp, rr,
        ])

    telegram(
        f"{result} {SYMBOL}\n"
        f"📥 Entrada: {entry:.2f}\n"
        f"📤 Salida (emergencia): {exit_price:.2f}\n"
        f"📦 Qty: {qty}\n"
        f"📈 Resultado: {pnl_pct:.2f}%\n"
        f"💵 PNL: {pnl_usdt:.2f} USDT\n"
        f"⏱ Hold: {hold_min} min"
    )

    return pnl_pct


def sync_state_with_exchange(state):
    """
    Reconcilia el estado local con la posición real en Binance.
    Si Binance tiene una posición abierta que el bot no está rastreando
    (por ejemplo, porque el proceso se cortó entre la orden de entrada y
    el guardado del estado), reconstruye el estado a partir de la posición
    real y las órdenes abiertas, en vez de perderle el rastro.
    """
    try:
        actual = get_position()
    except Exception:
        return state

    if actual and not state.get("position"):
        sl_price, tp_price = 0, 0
        try:
            open_orders = exchange.fetch_open_orders(SYMBOL)
            for o in open_orders:
                o_type = (o.get("type") or "").lower()
                if o_type == "stop_market":
                    sl_price = float(o.get("stopPrice") or 0)
                elif o_type == "take_profit_market":
                    tp_price = float(o.get("stopPrice") or 0)
        except Exception:
            pass

        state = {
            "position": True,
            "side": actual["side"],
            "entry_price": actual["entry"],
            "quantity": actual["qty"],
            "sl_price": sl_price,
            "tp_price": tp_price,
            "entry_time": state.get("entry_time") or time.time(),
            "last_result": state.get("last_result"),
            "last_exit_time": state.get("last_exit_time", 0),
            "consecutive_sl": state.get("consecutive_sl", 0),
        }
        save_state(state)

        warning = (
            f"⚠️ Posición {actual['side'].upper()} detectada en Binance sin "
            f"seguimiento local. Estado resincronizado.\n"
            f"SL: {sl_price or 'NO ENCONTRADO'} | TP: {tp_price or 'NO ENCONTRADO'}"
        )
        telegram(warning)

        if sl_price == 0 or tp_price == 0:
            telegram(
                "🚨 Faltan órdenes de TP/SL en Binance para la posición "
                "actual. Revisar manualmente."
            )

    return state


# ================== BOT ==================
def run_bot():
    start = time.time()
    init_csv()

    while time.time() - start < 55:
        try:
            hourly_report()

            # 1 Fetch OHLCV
            ohlcv = exchange.fetch_ohlcv(SYMBOL, TIMEFRAME, limit=300)
            df = pd.DataFrame(ohlcv, columns=["t", "o", "h", "l", "c", "v"])
            close = df["c"]

            # 2 Indicadores
            rsi = RSIIndicator(close, RSI_PERIOD).rsi().iloc[-1]
            ema_fast = EMAIndicator(close, EMA_FAST).ema_indicator().iloc[-1]
            ema_slow = EMAIndicator(close, EMA_SLOW).ema_indicator().iloc[-1]
            last_price = close.iloc[-1]

            # 3 Estado
            state = load_state()
            state = sync_state_with_exchange(state)
            in_position = state["position"]
            last_result = state.get("last_result")
            last_exit_time = state.get("last_exit_time", 0)

            # 4 FILTRO DE REGIMEN (rango de precio)
            range_lookback = 24
            market_range = (
                df["h"].tail(range_lookback).max()
                - df["l"].tail(range_lookback).min()
            )
            if market_range < last_price * 0.002:
                continue

            # Filtro anti-lateralidad (separación entre EMAs)
            ema_gap = abs(ema_fast - ema_slow) / last_price
            if ema_gap < 0.0005:
                continue

            # Filtro ADX
            adx = ADXIndicator(df["h"], df["l"], close, window=14).adx().iloc[-1]
            if adx < MIN_ADX:
                continue

            # ================== ENTRADA ==================
            if not in_position:
                # Filtro horario
                current_hour = datetime.utcnow().hour
                if current_hour >= BLOCK_HOUR_START or current_hour < BLOCK_HOUR_END:
                    continue
                if current_hour in TOXIC_HOURS:
                    continue

                # Límite diario de trades y pérdidas
                daily_trades, daily_losses = count_daily_trades()
                if daily_trades >= MAX_DAILY_TRADES:
                    continue
                if daily_losses >= MAX_DAILY_LOSSES:
                    continue

                # Cooldowns por SL consecutivos
                consecutive_sl = state.get("consecutive_sl", 0)
                elapsed = time.time() - last_exit_time

                if consecutive_sl >= 3 and elapsed < COOLDOWN_SL3_S:
                    continue
                if consecutive_sl == 2 and elapsed < COOLDOWN_SL2_S:
                    continue
                if last_result == "SL" and elapsed < COOLDOWN_SL1_S:
                    continue
                if last_result == "TP" and elapsed < COOLDOWN_TP_S:
                    continue

                # Anti-flip-flop
                last_side = state.get("side")
                time_since_exit = elapsed if last_exit_time else 999999

                balance = exchange.fetch_balance()["USDT"]["free"]
                qty = (balance * RISK_PERCENT * LEVERAGE) / last_price
                qty = float(exchange.amount_to_precision(SYMBOL, qty))

                if qty * last_price < 20:
                    time.sleep(2)
                    continue

                ema200_series = EMAIndicator(close, 200).ema_indicator()
                ema_200 = ema200_series.iloc[-1]

                # Pendiente EMA50: detecta tendencia bajista de mediano plazo
                ema50_series = EMAIndicator(close, EMA_SLOW).ema_indicator()
                ema50_now = ema50_series.iloc[-1]
                ema50_12ago = ema50_series.iloc[-13]  # hace 12 velas (1h en 5m)
                ema50_falling = bool(ema50_now < ema50_12ago)  # EMA50 bajando

                # Pendiente EMA200: detecta tendencia bajista de largo plazo
                ema200_24ago = ema200_series.iloc[-25]  # hace 24 velas (2h en 5m)
                ema200_falling = bool(ema_200 < ema200_24ago)

                # Filtro drawdown: no entrar si precio cayó >3% desde máx reciente
                high_48 = df["h"].iloc[-49:-1].max()  # máx últimas 48 velas (4h)
                drawdown_pct = (high_48 - last_price) / high_48
                if drawdown_pct > 0.03:
                    time.sleep(2)
                    continue

                # ATR para SL/TP dinámico
                atr = compute_atr(df)

                lookback_vol = 12
                rango_volatilidad = (
                    df["h"].tail(lookback_vol).max()
                    - df["l"].tail(lookback_vol).min()
                )

                # ================= LONG =================
                filtro_rsi = bool(LONG_RSI_MIN <= rsi <= LONG_RSI_MAX)
                filtro_ema = bool(ema_fast > ema_slow * 1.0002)
                filtro_tendencia = bool(last_price > ema_200 * 1.0005)
                filtro_volatilidad = bool(rango_volatilidad > last_price * LONG_VOL_MIN)
                filtro_momentum_macro = bool(not ema50_falling)
                filtro_tendencia_macro = bool(not ema200_falling)

                # Confirmaciones blandas (2 de 3 necesarias)
                has_volume = bool(confirm_volume_spike(df, threshold=1.2))
                has_candle = bool(confirm_candle_strength(df, "long"))
                adx_confirm = bool(adx > 25)
                confirmaciones = sum([has_volume, has_candle, adx_confirm])
                filtro_confirmacion = bool(confirmaciones >= LONG_CONFIRMATIONS_MIN)

                # Filtro momentum inmediato: no todas las últimas 3 cerradas bajistas
                last3 = df.iloc[-4:-1]
                all_bearish = bool((last3["c"] < last3["o"]).all())
                if all_bearish:
                    time.sleep(2)
                    continue

                flip_long = bool(
                    last_side == "short"
                    and time_since_exit < ANTI_FLIP_FLOP_S
                )

                if (filtro_rsi and filtro_ema and filtro_tendencia
                        and filtro_volatilidad and filtro_confirmacion
                        and filtro_momentum_macro
                        and filtro_tendencia_macro
                        and not flip_long):

                    # Validar R:R mínimo ANTES de operar (con precio estimado).
                    # Antes esto se validaba después de comprar: si el R:R
                    # salía bajo, el bot ya había entrado en Binance y
                    # simplemente hacía "continue" sin colocar TP/SL ni
                    # guardar el estado, dejando una posición abierta, sin
                    # protección y sin rastrear.
                    if USE_DYNAMIC_SLTP:
                        sl_est = last_price - (atr * ATR_MULTIPLIER_SL)
                        tp_est = last_price + (atr * ATR_MULTIPLIER_TP)
                    else:
                        sl_est = last_price * (1 - STOP_LOSS_PERCENT)
                        tp_est = last_price + ((last_price - sl_est) * TP_MULTIPLIER)

                    min_sl_est = last_price * (1 - 0.010)
                    max_sl_est = last_price * (1 - 0.003)
                    sl_est = max(min_sl_est, min(sl_est, max_sl_est))
                    risk_est = last_price - sl_est
                    rr_est = round(abs(tp_est - last_price) / abs(risk_est), 2) if risk_est else 0

                    if rr_est < MIN_RR:
                        time.sleep(2)
                        continue

                    try:
                        exchange.set_margin_mode("isolated", SYMBOL)
                    except Exception:
                        pass

                    order = exchange.create_market_buy_order(SYMBOL, qty)
                    entry = get_fill_price(order, last_price)

                    # SL/TP dinámico basado en ATR, recalculado con el precio real de entrada
                    if USE_DYNAMIC_SLTP:
                        sl = entry - (atr * ATR_MULTIPLIER_SL)
                        tp = entry + (atr * ATR_MULTIPLIER_TP)
                    else:
                        sl = entry * (1 - STOP_LOSS_PERCENT)
                        risk = entry - sl
                        tp = entry + (risk * TP_MULTIPLIER)

                    # Asegurar SL mínimo del 0.3% y máximo del 1.0%
                    min_sl = entry * (1 - 0.010)
                    max_sl = entry * (1 - 0.003)
                    sl = max(min_sl, min(sl, max_sl))

                    risk = entry - sl
                    if not USE_DYNAMIC_SLTP:
                        tp = entry + (risk * TP_MULTIPLIER)

                    rr = round(abs(tp - entry) / abs(entry - sl), 2) if risk else 0
                    entry_time = time.time()

                    # Guardar el estado YA: la posición existe en Binance a
                    # partir de acá aunque todavía falten TP/SL. Así, si algo
                    # falla más abajo, el bot no "pierde" la posición.
                    save_state({
                        "position": True,
                        "side": "long",
                        "entry_price": entry,
                        "quantity": qty,
                        "sl_price": 0,
                        "tp_price": 0,
                        "entry_time": entry_time,
                        "last_result": state.get("last_result"),
                        "last_exit_time": state.get("last_exit_time", 0),
                        "consecutive_sl": state.get("consecutive_sl", 0),
                    })
                    in_position = True

                    tp_ok, sl_ok = place_protective_orders("long", qty, sl, tp)

                    if tp_ok and sl_ok:
                        save_state({
                            "position": True,
                            "side": "long",
                            "entry_price": entry,
                            "quantity": qty,
                            "sl_price": sl,
                            "tp_price": tp,
                            "entry_time": entry_time,
                            "last_result": state.get("last_result"),
                            "last_exit_time": state.get("last_exit_time", 0),
                            "consecutive_sl": state.get("consecutive_sl", 0),
                        })
                        telegram(
                            f"🟢 LONG {SYMBOL}\n"
                            f"Entrada: {entry:.2f}\n"
                            f"TP: {tp:.2f} | SL: {sl:.2f}\n"
                            f"R:R {rr} | ATR: {atr:.2f}\n"
                            f"ADX: {adx:.1f} | RSI: {rsi:.1f}\n"
                            f"Trades hoy: {daily_trades + 1}/{MAX_DAILY_TRADES}"
                        )
                    else:
                        exit_price = emergency_close("long", qty, last_price)
                        if exit_price is not None:
                            pnl_pct = log_emergency_exit(
                                "long", entry, exit_price, qty, sl, tp, entry_time
                            )
                            save_state({
                                "position": False,
                                "side": "long",
                                "entry_price": 0,
                                "quantity": 0,
                                "sl_price": 0,
                                "tp_price": 0,
                                "entry_time": 0,
                                "last_result": "SL" if pnl_pct <= 0 else "TP",
                                "last_exit_time": time.time(),
                                "consecutive_sl": (
                                    0 if pnl_pct > 0
                                    else state.get("consecutive_sl", 0) + 1
                                ),
                            })
                            in_position = False
                        # Si exit_price es None, no se pudo ni proteger ni
                        # cerrar: el estado queda como "position: True" con
                        # sl/tp en 0 (ya guardado arriba) para que se detecte
                        # y se reintente reconciliar en el próximo ciclo.

                # ================= SHORT =================
                if not in_position and ENABLE_SHORTS:
                    filtro_rsi_short = bool(rsi >= SHORT_RSI_MIN)
                    filtro_ema_short = bool(ema_fast < ema_slow * 0.9995)
                    filtro_tendencia_short = bool(last_price < ema_200 * 0.999)
                    filtro_volatilidad_short = bool(rango_volatilidad > last_price * SHORT_VOL_MIN)
                    filtro_adx_short = bool(adx >= SHORT_ADX_MIN)
                    # Confirmaciones blandas para short (2 de 3)
                    has_volume_s = bool(confirm_volume_spike(df, threshold=1.2))
                    has_candle_s = bool(confirm_candle_strength(df, "short"))
                    adx_confirm_s = bool(adx > 30)
                    confirmaciones_s = sum([has_volume_s, has_candle_s, adx_confirm_s])
                    filtro_confirmacion_short = bool(confirmaciones_s >= SHORT_CONFIRMATIONS_MIN)

                    flip_short = bool(
                        last_side == "long"
                        and time_since_exit < ANTI_FLIP_FLOP_S
                    )

                    if (filtro_rsi_short and filtro_ema_short
                            and filtro_tendencia_short
                            and filtro_volatilidad_short
                            and filtro_adx_short
                            and filtro_confirmacion_short
                            and not flip_short):
                        try:
                            exchange.set_margin_mode("isolated", SYMBOL)
                        except Exception:
                            pass

                        order = exchange.create_market_sell_order(SYMBOL, qty)
                        entry = get_fill_price(order, last_price)

                        # SL/TP dinámico basado en ATR, recalculado con el precio real de entrada
                        if USE_DYNAMIC_SLTP:
                            sl = entry + (atr * ATR_MULTIPLIER_SL)
                            tp = entry - (atr * ATR_MULTIPLIER_TP)
                        else:
                            sl = entry * (1 + STOP_LOSS_PERCENT)
                            risk = sl - entry
                            tp = entry - (risk * TP_MULTIPLIER)

                        # Asegurar SL mínimo del 0.3% y máximo del 1.0%
                        min_sl = entry * (1 + 0.003)
                        max_sl = entry * (1 + 0.010)
                        sl = min(max_sl, max(sl, min_sl))

                        risk = sl - entry
                        if not USE_DYNAMIC_SLTP:
                            tp = entry - (risk * TP_MULTIPLIER)

                        rr = round(abs(entry - tp) / abs(sl - entry), 2) if risk else 0
                        entry_time = time.time()

                        # Guardar el estado YA: la posición existe en Binance
                        # a partir de acá aunque todavía falten TP/SL.
                        save_state({
                            "position": True,
                            "side": "short",
                            "entry_price": entry,
                            "quantity": qty,
                            "sl_price": 0,
                            "tp_price": 0,
                            "entry_time": entry_time,
                            "last_result": state.get("last_result"),
                            "last_exit_time": state.get("last_exit_time", 0),
                            "consecutive_sl": state.get("consecutive_sl", 0),
                        })
                        in_position = True

                        tp_ok, sl_ok = place_protective_orders("short", qty, sl, tp)

                        if tp_ok and sl_ok:
                            save_state({
                                "position": True,
                                "side": "short",
                                "entry_price": entry,
                                "quantity": qty,
                                "sl_price": sl,
                                "tp_price": tp,
                                "entry_time": entry_time,
                                "last_result": state.get("last_result"),
                                "last_exit_time": state.get("last_exit_time", 0),
                                "consecutive_sl": state.get("consecutive_sl", 0),
                            })
                            telegram(
                                f"🔴 SHORT {SYMBOL}\n"
                                f"Entrada: {entry:.2f}\n"
                                f"TP: {tp:.2f} | SL: {sl:.2f}\n"
                                f"R:R {rr} | ATR: {atr:.2f}\n"
                                f"ADX: {adx:.1f} | RSI: {rsi:.1f}\n"
                                f"Trades hoy: {daily_trades + 1}/{MAX_DAILY_TRADES}"
                            )
                        else:
                            exit_price = emergency_close("short", qty, last_price)
                            if exit_price is not None:
                                pnl_pct = log_emergency_exit(
                                    "short", entry, exit_price, qty, sl, tp, entry_time
                                )
                                save_state({
                                    "position": False,
                                    "side": "short",
                                    "entry_price": 0,
                                    "quantity": 0,
                                    "sl_price": 0,
                                    "tp_price": 0,
                                    "entry_time": 0,
                                    "last_result": "SL" if pnl_pct <= 0 else "TP",
                                    "last_exit_time": time.time(),
                                    "consecutive_sl": (
                                        0 if pnl_pct > 0
                                        else state.get("consecutive_sl", 0) + 1
                                    ),
                                })
                                in_position = False
                            # Si exit_price es None, la posición queda como
                            # "position: True" con sl/tp en 0 (ya guardado
                            # arriba) para reconciliar en el próximo ciclo.

            # ================== SALIDA ==================
            if in_position:
                positions = exchange.fetch_positions([SYMBOL])

                binance_qty = 0
                for p in positions:
                    if SYMBOL in p.get("symbol", ""):
                        binance_qty = abs(float(p.get("contracts", 0)))

                now = time.time()
                entry_time = state.get("entry_time", 0)
                MIN_HOLD_TIME = 10  # Mínimo 10 seg antes de detectar cierre

                if binance_qty == 0 and now - entry_time > MIN_HOLD_TIME:
                    entry = state["entry_price"]
                    side = state["side"]
                    qty = state.get("quantity", 0)
                    sl = state.get("sl_price", 0)
                    tp = state.get("tp_price", 0)
                    timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")

                    # Obtener precio REAL de ejecución desde Binance
                    exit_price = last_price
                    real_pnl_usdt = None
                    try:
                        symbol_id = SYMBOL.replace("/", "")
                        recent_trades = exchange.fapiPrivate_get_usertrades({
                            "symbol": symbol_id,
                            "limit": 10,
                        })
                        if recent_trades:
                            close_trades = []
                            for t in reversed(recent_trades):
                                is_close = (
                                    (side == "long" and t.get("side") == "SELL")
                                    or (side == "short" and t.get("side") == "BUY")
                                )
                                if is_close:
                                    close_trades.append(t)
                                elif close_trades:
                                    break

                            if close_trades:
                                total_qty_fill = sum(
                                    float(t["qty"]) for t in close_trades
                                )
                                exit_price = sum(
                                    float(t["price"]) * float(t["qty"])
                                    for t in close_trades
                                ) / total_qty_fill
                                real_pnl_usdt = sum(
                                    float(t.get("realizedPnl", 0))
                                    for t in close_trades
                                )
                    except Exception:
                        pass

                    # Calcular PNL
                    if side == "long":
                        if sl == 0:
                            sl = entry * (1 - STOP_LOSS_PERCENT)
                        if tp == 0:
                            risk = entry - sl
                            tp = entry + (risk * TP_MULTIPLIER)
                        pnl_pct = ((exit_price - entry) / entry) * 100
                    else:
                        if sl == 0:
                            sl = entry * (1 + STOP_LOSS_PERCENT)
                        if tp == 0:
                            risk = sl - entry
                            tp = entry - (risk * TP_MULTIPLIER)
                        pnl_pct = ((entry - exit_price) / entry) * 100

                    # PNL en USDT
                    if real_pnl_usdt is not None:
                        pnl_usdt = real_pnl_usdt
                    else:
                        if side == "long":
                            pnl_usdt = (exit_price - entry) * qty
                        else:
                            pnl_usdt = (entry - exit_price) * qty
                        fees = (entry * qty + exit_price * qty) * 0.0004
                        pnl_usdt -= fees

                    result = "TP 🎯" if pnl_pct > 0 else "SL 🛑"

                    # Tiempo en posición
                    hold_secs = int(now - entry_time) if entry_time else 0
                    hold_min = hold_secs // 60

                    telegram(
                        f"{result} {SYMBOL}\n"
                        f"📥 Entrada: {entry:.2f}\n"
                        f"📤 Salida real: {exit_price:.2f}\n"
                        f"📦 Qty: {qty}\n"
                        f"📈 Resultado: {pnl_pct:.2f}%\n"
                        f"💵 PNL: {pnl_usdt:.2f} USDT\n"
                        f"⏱ Hold: {hold_min} min"
                    )

                    # Guardar trade en CSV
                    risk_value = abs(entry - sl)
                    reward_value = abs(tp - entry)
                    rr = round(reward_value / risk_value, 2) if risk_value else 0

                    with open(TRADES_FILE, "a", newline="") as f:
                        writer = csv.writer(f)
                        writer.writerow([
                            timestamp, SYMBOL, side, entry, exit_price, qty,
                            f"{pnl_pct:.2f}", f"{pnl_usdt:.2f}",
                            result, sl, tp, rr,
                        ])

                    new_consecutive_sl = (
                        0 if pnl_pct > 0
                        else state.get("consecutive_sl", 0) + 1
                    )

                    save_state({
                        "position": False,
                        "side": side,
                        "entry_price": 0,
                        "quantity": 0,
                        "sl_price": 0,
                        "tp_price": 0,
                        "entry_time": 0,
                        "last_result": "SL" if pnl_pct <= 0 else "TP",
                        "last_exit_time": time.time(),
                        "consecutive_sl": new_consecutive_sl,
                    })
                    in_position = False

                    # Si acumula 3+ SL consecutivos, notificar
                    if new_consecutive_sl >= 3:
                        telegram(
                            f"⚠️ {new_consecutive_sl} SL consecutivos.\n"
                            f"Bot en cooldown de {COOLDOWN_SL3_S // 60} min."
                        )

        except Exception as e:
            telegram(f"⚠ Error bot futuros: {e}")

        time.sleep(8)  # 8 seg entre ciclos (5m TF no necesita polling rápido)


if __name__ == "__main__":
    run_bot()
