"""
V12 — Bot Momentum Taker con Confirmación.

Ciclo por ventana de 5min o 15min:
  1. Descubrir mercado y obtener tokens + strike
  2. Conectar CLOB websocket para best_ask real de YES y NO
  3. Monitorear precio Binance cada segundo
  4. Cuando |price/strike - 1| > MOMENTUM_THRESHOLD durante CONFIRM_TICKS
     ticks consecutivos en la misma dirección → tomar posición al ask real
  5. Al final: esperar settle oficial, calcular PnL

Cambios vs V11:
  - Confirmación de momentum: filtra spikes transitorios ("chop")
  - Aggressive bump reducido: ask+0.01 en vez de ask+0.02
"""
import json, signal, threading, time
from datetime import datetime

from pathlib import Path
from .config import (
    Config, CLOB_WS,
    MOMENTUM_THRESHOLD, CONFIRM_TICKS, AGGRESSIVE_BUMP,
    ENTRY_SIZE_USD, TAKER_FEE_RATE,
    OUTCOME_MAX_WAIT,
    LOSS_STREAK_MAX, LOSS_STREAK_COOL,
    DAILY_LOSS_LIMIT, BLOCKED_HOURS_UTC,
    write_json,
)
from .feeds import BinanceFeed, CLOBFeed
from .market import Market
from .logger import Logger
from .executor import Executor
from .strategy import MomentumStrategy
import requests


class Bot:
    def __init__(self, cfg: Config):
        self.cfg      = cfg
        self.session  = requests.Session()
        self.session.headers.update({"User-Agent": "Mozilla/5.0"})
        self._stop    = False

        self.logger   = Logger(cfg)
        self.strategy = MomentumStrategy(cfg)
        self.executor = Executor(cfg)
        self.binance  = BinanceFeed(cfg.binance_ws)

        self.clob_yes = None
        self.clob_no  = None
        self.market   = None

        # Estado por ventana
        self._entered       = False
        self._entry_side    = ""
        self._entry_price   = 0.0
        self._entry_size    = 0.0
        self._entry_cost    = 0.0
        self._entry_move    = 0.0
        self._entry_token   = ""
        self._fill_verified = False
        self._bal_before    = 0.0   # balance antes de entrar (para PnL real)

        # Counters
        self._windows_count = 0
        self._trades_count  = 0
        self._trade_history = []

        # Defense: loss streak cooldown (per-asset)
        self._loss_streak   = 0
        self._cooldown_left = 0

        # Defense: GLOBAL cooldown (shared across all assets)
        self._global_cooldown_file = cfg.log_dir.parent / "global_cooldown.json"
        self._global_trades_file   = cfg.log_dir.parent / "global_trades.jsonl"

        # Defense: daily drawdown
        self._daily_pnl     = 0.0
        self._daily_date    = datetime.utcnow().strftime("%Y-%m-%d")
        self._dd_stopped    = False
        self._dd_reset_file = cfg.log_dir / "dd_reset"

        live_tag = "LIVE" if cfg.live else "PAPER"
        print(f"[*] V12 Momentum Taker + Confirm {live_tag} [{cfg.asset.upper()}] "
              f"{cfg.window}min")
        print(f"    threshold: {cfg.momentum_threshold:.4%} | "
              f"confirm: {cfg.confirm_ticks}s | "
              f"bump: +${cfg.aggressive_bump:.2f} | "
              f"size: ${self.cfg.entry_size_usd} | fee: {TAKER_FEE_RATE:.0%}")

    # -- Window state ---------------------------------------------------------

    def _reset_window_state(self):
        self._entered     = False
        self._entry_side  = ""
        self._entry_price = 0.0
        self._entry_size  = 0.0
        self._entry_cost  = 0.0
        self._entry_move  = 0.0
        self._entry_token = ""
        self._fill_verified = False
        self._bal_before    = 0.0
        # V12: reset confirmación de momentum para nueva ventana
        self.strategy.reset()

    def _restore_entry_from_csv(self, m):
        """Restaurar entrada de trades.csv si existe para este slug."""
        import csv
        path = self.cfg.trades_csv
        if not path.exists():
            return
        try:
            with open(path) as f:
                reader = csv.DictReader(f)
                for row in reader:
                    if row.get("slug") != m.slug:
                        continue
                    if row.get("side") in ("YES", "NO"):
                        self._entered = True
                        self._entry_side = row["side"]
                        self._entry_price = float(row["price"])
                        self._entry_size = float(row["size"])
                        self._entry_cost = float(row.get("cost", 0))
                        self._entry_move = float(row.get("move_pct", 0))
                        print(f"  [RESTORED] entry {row['side']} @ {row['price']} "
                              f"x {row['size']} sh (de CSV)")
                        return
        except Exception as e:
            print(f"  [warn] no se pudo restaurar entry: {e}")

    # -- Defense mechanisms ---------------------------------------------------

    def _check_daily_reset(self):
        """Reset daily PnL counter at UTC midnight."""
        today = datetime.utcnow().strftime("%Y-%m-%d")
        if today != self._daily_date:
            if self._daily_pnl < 0:
                print(f"\n  [DAILY RESET] {self._daily_date} PnL was "
                      f"${self._daily_pnl:+.2f}, resetting")
            self._daily_date = today
            self._daily_pnl = 0.0
            self._dd_stopped = False

    def _check_dd_resume(self):
        """Check if dashboard sent a manual resume signal."""
        if not self._dd_stopped:
            return
        if self._dd_reset_file.exists():
            try:
                self._dd_reset_file.unlink()
            except OSError:
                pass
            self._dd_stopped = False
            self._daily_pnl = 0.0  # reset counter on manual resume
            self._loss_streak = 0
            self._cooldown_left = 0
            print(f"\n  [RESUMED] drawdown stop cleared manually from dashboard")

    def _update_defense_after_settle(self, pnl_net: float, won: bool):
        """Update streak and daily drawdown after a trade settles."""
        # Loss streak
        if won:
            self._loss_streak = 0
        else:
            self._loss_streak += 1
            if self._loss_streak >= LOSS_STREAK_MAX:
                self._cooldown_left = LOSS_STREAK_COOL
                print(f"\n  [COOLDOWN] {self._loss_streak} losses in a row "
                      f"-> pausing {LOSS_STREAK_COOL} windows")

        # Daily drawdown
        self._daily_pnl += pnl_net
        if self._daily_pnl <= -DAILY_LOSS_LIMIT and not self._dd_stopped:
            self._dd_stopped = True
            print(f"\n  [DD STOP] daily PnL ${self._daily_pnl:+.2f} "
                  f"hit limit -${DAILY_LOSS_LIMIT:.0f} -> STOPPED"
                  f"\n  Resume from dashboard or wait until next UTC day")

    def _log_global_trade(self, won: bool):
        """Append trade result to shared global_trades.jsonl."""
        try:
            entry = json.dumps({
                "ts": int(time.time()),
                "asset": self.cfg.asset,
                "won": won,
            })
            with open(self._global_trades_file, "a") as f:
                f.write(entry + "\n")
        except Exception:
            pass

    def _check_global_cooldown(self) -> int:
        """Check if global cooldown is active.
        Returns windows remaining, or 0 if not in cooldown."""
        try:
            if not self._global_cooldown_file.exists():
                return 0
            data = json.loads(self._global_cooldown_file.read_text())
            until_ts = data.get("until_ts", 0)
            if time.time() < until_ts:
                remaining = int((until_ts - time.time()) / self.cfg.win_secs) + 1
                return remaining
            else:
                # Expired — clean up
                self._global_cooldown_file.unlink(missing_ok=True)
                return 0
        except Exception:
            return 0

    def _trigger_global_cooldown(self):
        """Activate global cooldown for LOSS_STREAK_COOL windows."""
        cooldown_secs = LOSS_STREAK_COOL * self.cfg.win_secs
        until_ts = int(time.time()) + cooldown_secs
        try:
            data = json.dumps({
                "until_ts": until_ts,
                "triggered_by": self.cfg.asset,
                "triggered_at": int(time.time()),
            })
            self._global_cooldown_file.write_text(data)
            print(f"\n  [GLOBAL COOLDOWN] 3 consecutive losses across all bots "
                  f"-> pausing ALL for {LOSS_STREAK_COOL} windows "
                  f"({cooldown_secs}s)")
        except Exception as e:
            print(f"  [GLOBAL COOLDOWN] write error: {e}")

    def _check_global_streak(self):
        """Read last N trades from global file and trigger cooldown if needed."""
        try:
            if not self._global_trades_file.exists():
                return
            lines = self._global_trades_file.read_text().strip().split("\n")
            # Only look at recent trades (last 10)
            recent = lines[-10:] if len(lines) > 10 else lines
            # Count consecutive losses from the end
            streak = 0
            for line in reversed(recent):
                try:
                    entry = json.loads(line)
                    if not entry.get("won"):
                        streak += 1
                    else:
                        break
                except Exception:
                    break
            if streak >= LOSS_STREAK_MAX:
                self._trigger_global_cooldown()
        except Exception:
            pass

    def _should_skip_window(self) -> str | None:
        """Return reason to skip this window, or None if OK to trade."""
        if self._dd_stopped:
            return "DD_STOPPED"
        if self._cooldown_left > 0:
            return "COOLDOWN"
        # Global cooldown check
        gc = self._check_global_cooldown()
        if gc > 0:
            return "GLOBAL_COOLDOWN"
        # Blocked hours filter (UTC)
        if BLOCKED_HOURS_UTC:
            hour_utc = datetime.utcnow().hour
            if hour_utc in BLOCKED_HOURS_UTC:
                return "BLOCKED_HOUR"
        return None

    def _write_defense_state(self, m=None, skip_reason=None):
        """Write state.json with defense info (for dashboard visibility)."""
        bal = self.executor.get_balance(use_cache=True)
        gc_left = self._check_global_cooldown()

        if skip_reason:
            phase = skip_reason.lower()
        elif self._dd_stopped:
            phase = "dd_stopped"
        elif gc_left > 0:
            phase = "global_cooldown"
        elif self._cooldown_left > 0:
            phase = "cooldown"
        else:
            phase = "idle"

        write_json(self.cfg.state_json, {
            "ts": int(time.time()),
            "slug": m.slug if m else "",
            "start_ts": m.start_ts if m else 0,
            "end_ts": m.end_ts if m else 0,
            "t_left": max(0, m.end_ts - int(time.time())) if m else 0,
            "strike": m.strike if m else 0,
            "price": self.binance.last_px,
            "move_pct": 0,
            "threshold": self.cfg.momentum_threshold * 100,
            "entered": False,
            "phase": phase,
            "windows": self._windows_count,
            "trades_total": self._trades_count,
            "balance": round(bal, 4),
            "defense": {
                "loss_streak": self._loss_streak,
                "cooldown_left": self._cooldown_left,
                "global_cooldown_left": gc_left,
                "blocked_hour": datetime.utcnow().hour in BLOCKED_HOURS_UTC if BLOCKED_HOURS_UTC else False,
                "daily_pnl": round(self._daily_pnl, 4),
                "dd_stopped": self._dd_stopped,
                "daily_date": self._daily_date,
            },
        })

    # -- Tick -----------------------------------------------------------------

    def tick(self):
        m = self.market
        if not m:
            return

        price = self.binance.last_px
        if price == 0:
            return

        t_left = max(0, m.end_ts - int(time.time()))
        move = (price - m.strike) / m.strike if m.strike else 0

        # Best ask real del CLOB
        ask_yes = self.clob_yes.best_ask if self.clob_yes else 0
        ask_no  = self.clob_no.best_ask if self.clob_no else 0

        # Check for momentum signal (con precio real del CLOB)
        sig = self.strategy.check_signal(
            price=price,
            strike=m.strike,
            t_left=t_left,
            token_yes=m.token_yes,
            token_no=m.token_no,
            best_ask_yes=ask_yes,
            best_ask_no=ask_no,
            already_entered=self._entered,
        )

        if sig:
            print(f"\n  [SIGNAL] {sig.side} | move={sig.move_pct:+.4f}% | "
                  f"price=${price:,.2f} vs strike=${m.strike:,.2f} | "
                  f"ask={sig.price:.2f} | confirmed={sig.confirm_count}t")

            ok, oid, fill_price, fill_status = self.executor.place_taker_order(
                sig.token_id, sig.price, sig.size,
                f"MOM {sig.side}")

            if ok:
                # CRITICO: marcar entered INMEDIATAMENTE para evitar
                # órdenes duplicadas. NUNCA resetear a False.
                self._entered     = True
                self._entry_side  = sig.side
                self._entry_price = fill_price
                self._entry_size  = sig.size
                self._entry_cost  = round(fill_price * sig.size, 4)
                self._entry_move  = sig.move_pct
                self._entry_token = sig.token_id
                self._trades_count += 1

                # V12: Verificar fill via CLOB get_trades API.
                # NUNCA confiar en CLOB "matched" — verificar SIEMPRE
                # que existe un trade CONFIRMED con transaction_hash.
                # Sin verificación = trade fantasma (bug V11).
                if self.cfg.live:
                    order_ts = int(time.time())
                    bal_before = self.executor.get_balance()
                    verified = False
                    for attempt in range(5):
                        time.sleep(4 if attempt == 0 else 6)
                        pos = self.executor.verify_fill(
                            self.session, sig.token_id,
                            order_ts=order_ts)
                        if pos and pos["size"] > 0:
                            old_p = self._entry_price
                            old_n = self._entry_size
                            if pos["size"] <= sig.size * 1.5:
                                self._entry_size = pos["size"]
                            if pos["avg_price"] > 0:
                                self._entry_price = pos["avg_price"]
                            self._entry_cost = round(
                                self._entry_price * self._entry_size, 4)
                            tx_count = len(pos.get("tx_hashes", []))
                            print(f"  [FILL VERIFIED] {sig.side} "
                                  f"(attempt {attempt+1}): "
                                  f"{self._entry_size:.2f}sh @ "
                                  f"{self._entry_price:.4f}"
                                  f" (was {old_n:.1f}sh @ {old_p:.4f})"
                                  f" [{tx_count} tx confirmed]")
                            verified = True
                            break
                        print(f"  [verify retry {attempt+1}/5] "
                              f"no confirmed fill yet...")

                    self._fill_verified = verified

                    if not verified:
                        # V12: NO HAY FILL TRUST. Si get_trades no
                        # confirma el fill, es un trade fantasma.
                        # Cancelar la orden y NO contar el trade.
                        self.executor.cancel_order(oid)
                        self._trades_count -= 1

                        # Sanity check: balance delta
                        bal_after = self.executor.get_balance()
                        bal_delta = bal_before - bal_after

                        print(f"  [PHANTOM] {sig.side} — CLOB dijo "
                              f"'{fill_status}' pero NO hay fill "
                              f"confirmado on-chain tras 5 intentos.")
                        print(f"  [PHANTOM] Orden cancelada. "
                              f"Trade NO contado en PnL.")
                        print(f"  [PHANTOM] Balance delta: "
                              f"${bal_delta:+.4f} "
                              f"(before=${bal_before:.4f}, "
                              f"after=${bal_after:.4f})")

                        if abs(bal_delta) > 1.0:
                            # Balance bajó significativamente — el dinero
                            # salió pero no hay fill. Posible problema.
                            print(f"  [!! WARNING] Balance dropped "
                                  f"${bal_delta:.2f} without confirmed "
                                  f"fill — investigate manually!")

                if self._fill_verified or not self.cfg.live:
                    self.logger.log_trade(
                        m.slug, sig.side, self._entry_price,
                        self._entry_size, self._entry_cost,
                        sig.move_pct, "", 0, 0, 0)
                else:
                    # Trade fantasma: no logear para no contaminar CSV
                    pass

        # Status line
        entry_tag = ""
        if self._entered:
            entry_tag = (f" | POS={self._entry_side} @ "
                         f"{self._entry_price:.2f}")

        ask_tag = ""
        if ask_yes > 0 or ask_no > 0:
            ask_tag = f" | aY={ask_yes:.2f} aN={ask_no:.2f}"

        ss = self.cfg.signal_start_tleft
        se = self.cfg.signal_stop_tleft
        signal_zone = ss >= t_left >= se
        zone_tag = "SCANNING" if signal_zone and not self._entered else ""
        if self._entered:
            zone_tag = "HOLDING"
        elif t_left > ss:
            zone_tag = "WARMUP"
        elif t_left < se:
            zone_tag = "CUTOFF"

        # V12: mostrar progreso de confirmación
        confirm_tag = ""
        if not self._entered and signal_zone:
            cc = self.strategy.confirm_count
            ct = self.cfg.confirm_ticks
            cs = self.strategy.confirm_side
            if cc > 0:
                confirm_tag = f" | cfm={cc}/{ct}{cs[0] if cs else ''}"

        paper = "" if self.cfg.live else " [PAPER]"
        now_str = datetime.now().strftime('%H:%M:%S')
        print(
            f"[{now_str}] t-{t_left:3d}s | ${price:,.0f} | "
            f"move={move:+.4%} | {zone_tag}{confirm_tag}"
            f"{ask_tag}{entry_tag}{paper}",
            end='\r',
        )

        # State JSON para dashboard (cada 2s)
        if int(time.time()) % 2 == 0:
            bal = self.executor.get_balance(use_cache=True)
            write_json(self.cfg.state_json, {
                "ts": int(time.time()),
                "slug": m.slug,
                "start_ts": m.start_ts,
                "end_ts": m.end_ts,
                "t_left": t_left,
                "strike": m.strike,
                "price": price,
                "move_pct": round(move * 100, 4),
                "threshold": self.cfg.momentum_threshold * 100,
                "ask_yes": ask_yes,
                "ask_no": ask_no,
                "entered": self._entered,
                "entry_side": self._entry_side,
                "entry_price": self._entry_price,
                "entry_size": self._entry_size,
                "phase": zone_tag.lower(),
                "windows": self._windows_count,
                "trades_total": self._trades_count,
                "balance": round(bal, 4),
                # V12: confirmación
                "confirm": {
                    "count": self.strategy.confirm_count,
                    "needed": self.cfg.confirm_ticks,
                    "side": self.strategy.confirm_side,
                },
                "defense": {
                    "loss_streak": self._loss_streak,
                    "cooldown_left": self._cooldown_left,
                    "global_cooldown_left": self._check_global_cooldown(),
                    "blocked_hour": datetime.utcnow().hour in BLOCKED_HOURS_UTC if BLOCKED_HOURS_UTC else False,
                    "daily_pnl": round(self._daily_pnl, 4),
                    "dd_stopped": self._dd_stopped,
                    "daily_date": self._daily_date,
                },
            })

    # -- Settle ---------------------------------------------------------------

    def _settle_window(self, m):
        """Settle: esperar outcome oficial, calcular PnL."""
        # Cancelar órdenes pendientes (live)
        if self.cfg.live:
            self.executor.cancel_all()

        price_final = self.binance.last_px

        # Sin posición: esperar poco (solo para logging)
        # Con posición: esperar más (necesitamos saber si ganamos/perdimos)
        wait = OUTCOME_MAX_WAIT if self._entered else 60

        print(f"\n[settle] {m.slug} — esperando outcome oficial "
              f"(max {wait}s, {'con' if self._entered else 'sin'} posicion)...")
        outcome = self._fetch_outcome(m, wait)

        if not outcome:
            print(f"  [!! OUTCOME DESCONOCIDO] {m.slug} "
                  f"— Polymarket no resolvio en {OUTCOME_MAX_WAIT}s")
            self.logger.log_window(
                m, price_final, "UNKNOWN",
                self._entry_side, self._entry_price, self._entry_size,
                self._entry_move, 0, 0, 0)
            return

        if not self._entered:
            self.logger.log_window(
                m, price_final, outcome,
                "", 0, 0, 0, 0, 0, 0)
            print(f"  [settle] {outcome} — sin posicion")
            return

        # V12: trades no verificados = fantasma. No contar PnL.
        if self.cfg.live and not self._fill_verified:
            print(f"\n[settle] {m.slug} -> {outcome}")
            print(f"  [PHANTOM SETTLE] {self._entry_side} was NOT "
                  f"verified — skipping PnL calculation")
            self.logger.log_window(
                m, price_final, outcome,
                self._entry_side, self._entry_price, self._entry_size,
                self._entry_move, 0, 0, 0)
            return

        # Calcular PnL
        n = self._entry_size
        p = self._entry_price
        side = self._entry_side

        won = (side == "YES" and outcome == "Up") or \
              (side == "NO" and outcome == "Down")

        if self.cfg.live:
            if won:
                pnl_gross = round((1.0 - p) * n, 4)
            else:
                pnl_gross = round(-p * n, 4)
            fee = round(p * (1.0 - p) * n * TAKER_FEE_RATE, 4)
            pnl_net = round(pnl_gross - fee, 4)

            tag = "WIN" if won else "LOSS"
            print(f"\n[settle] {m.slug} -> {outcome}")
            print(f"  {side}: {n:.2f}sh @ {p:.4f} | "
                  f"move={self._entry_move:+.2f}% [VERIFIED]")
            print(f"  PnL: ${pnl_net:+.4f} {tag} "
                  f"(gross: ${pnl_gross:+.4f}, fee: ${fee:.4f})")
        else:
            # PAPER: cálculo teórico
            if won:
                pnl_gross = round((1.0 - p) * n, 4)
            else:
                pnl_gross = round(-p * n, 4)
            fee = round(p * (1.0 - p) * n * TAKER_FEE_RATE, 4)
            pnl_net = round(pnl_gross - fee, 4)

            tag = "WIN" if won else "LOSS"
            print(f"\n[settle] {m.slug} -> {outcome}")
            print(f"  {side}: {n:.0f}sh @ {p:.2f} | "
                  f"move={self._entry_move:+.2f}%")
            print(f"  PnL gross: ${pnl_gross:+.4f} | "
                  f"fee: ${fee:.4f} | net: ${pnl_net:+.4f} {tag}")

        self.logger.log_trade(
            m.slug, side, p, n, self._entry_cost,
            self._entry_move, outcome, pnl_gross, fee, pnl_net)
        self.logger.log_window(
            m, price_final, outcome,
            side, p, n, self._entry_move,
            pnl_gross, fee, pnl_net)

        self._trade_history.append({
            "ts": int(time.time()), "slug": m.slug,
            "side": side, "outcome": outcome,
            "pnl_net": pnl_net, "won": won,
        })

        # Update defense mechanisms
        self._update_defense_after_settle(pnl_net, won)
        if self.cfg.live:
            self._log_global_trade(won)
            self._check_global_streak()

        if self.cfg.live:
            bal = self.executor.get_balance()
            print(f"  [balance] ${bal:.4f}")

    def _fetch_outcome(self, m, max_wait: int) -> str | None:
        deadline = time.time() + max_wait
        attempt  = 0
        while time.time() < deadline:
            attempt += 1
            outcome = m.fetch_outcome(self.session)
            if outcome:
                print(f"  [OK outcome] {outcome} (intento {attempt})")
                return outcome
            remaining = deadline - time.time()
            if remaining <= 0:
                break
            wait = 5 if time.time() - (deadline - max_wait) < 30 else 10
            time.sleep(min(wait, remaining))
        return None

    # -- Loop principal -------------------------------------------------------

    def run(self):
        signal.signal(signal.SIGINT,  lambda *_: self.shutdown())
        signal.signal(signal.SIGTERM, lambda *_: self.shutdown())

        threading.Thread(
            target=self.binance.run, daemon=True, name="binance",
        ).start()

        print("-> esperando primer tick de Binance...")
        while not self._stop and self.binance.last_px == 0:
            time.sleep(0.2)

        ws = self.cfg.win_secs
        live_tag = "LIVE" if self.cfg.live else "PAPER"
        print(f"-> modo: {live_tag} | ventana: {ws}s ({self.cfg.window}min)")
        print(f"-> threshold: {self.cfg.momentum_threshold:.4%} | "
              f"confirm: {self.cfg.confirm_ticks}s | "
              f"bump: +${self.cfg.aggressive_bump:.2f} | "
              f"size: ${self.cfg.entry_size_usd}/trade")

        _consecutive_fails = 0
        while not self._stop:
            try:
                now      = int(time.time())
                start_ts = now - now % ws
                end_ts   = start_ts + ws

                if end_ts - now < 30:
                    start_ts += ws
                    wait = max(0, start_ts - now + 1)
                    print(f"\n-> proxima ventana en {wait}s...")
                    time.sleep(wait)

                self.session.close()
                self.session = requests.Session()
                self.session.headers.update({"User-Agent": "Mozilla/5.0"})

                m = Market(start_ts, self.cfg)
                if not m.fetch_data(self.session, self.binance.last_px):
                    _consecutive_fails += 1
                    delay = min(30, 5 * _consecutive_fails)
                    print(f"[!] No se pudo cargar la ventana "
                          f"(fail #{_consecutive_fails}), retry en {delay}s...")
                    time.sleep(delay)
                    continue
                _consecutive_fails = 0

                self._windows_count += 1
                self._reset_window_state()

                # Defense checks at window start
                self._check_daily_reset()
                self._check_dd_resume()
                skip = self._should_skip_window()

                if skip == "COOLDOWN":
                    self._cooldown_left -= 1
                    remaining = end_ts - int(time.time())
                    print(f"\n  [COOLDOWN] skipping window "
                          f"({self._cooldown_left} more after this)")
                    self._write_defense_state(m, skip_reason="cooldown")
                    time.sleep(max(1, remaining))
                    continue

                if skip == "GLOBAL_COOLDOWN":
                    remaining = end_ts - int(time.time())
                    gc_left = self._check_global_cooldown()
                    print(f"\n  [GLOBAL COOLDOWN] 3+ losses across all bots "
                          f"— skipping ({gc_left} windows left)")
                    self._write_defense_state(m, skip_reason="global_cooldown")
                    time.sleep(max(1, remaining))
                    continue

                if skip == "BLOCKED_HOUR":
                    remaining = end_ts - int(time.time())
                    print(f"\n  [BLOCKED HOUR] {datetime.utcnow().hour:02d}:00 UTC "
                          f"— low WR hour, skipping")
                    self._write_defense_state(m, skip_reason="blocked_hour")
                    time.sleep(max(1, remaining))
                    continue

                if skip == "DD_STOPPED":
                    remaining = end_ts - int(time.time())
                    print(f"\n  [DD STOPPED] daily PnL "
                          f"${self._daily_pnl:+.2f} — waiting for "
                          f"resume or UTC midnight")
                    # Keep writing state so dashboard can show status
                    while not self._stop and int(time.time()) < end_ts:
                        self._check_dd_resume()
                        if not self._dd_stopped:
                            break  # Resumed! Fall through to normal flow
                        self._write_defense_state(m)
                        time.sleep(5)
                    if self._dd_stopped:
                        continue  # Still stopped, next window

                self._restore_entry_from_csv(m)

                # CLOB feeds para precios reales
                if self.clob_yes:
                    self.clob_yes.stop()
                if self.clob_no:
                    self.clob_no.stop()

                self.clob_yes = CLOBFeed(m.token_yes, CLOB_WS)
                self.clob_no  = CLOBFeed(m.token_no, CLOB_WS)
                threading.Thread(
                    target=self.clob_yes.run, daemon=True).start()
                threading.Thread(
                    target=self.clob_no.run, daemon=True).start()
                time.sleep(2)  # Esperar a que se conecten

                known = [t for t in self._trade_history
                         if t.get("outcome") not in ("UNKNOWN", None)]
                wins = sum(1 for t in known if t["won"])
                pnl  = sum(t["pnl_net"] for t in known)

                ask_y = self.clob_yes.best_ask
                ask_n = self.clob_no.best_ask

                # Defense status line
                def_status = ""
                if self._loss_streak > 0:
                    def_status += f" | streak={self._loss_streak}"
                if self._daily_pnl != 0:
                    def_status += f" | day=${self._daily_pnl:+.2f}"

                print(f"\n{'='*72}")
                print(f"  Ventana    : {m.slug}")
                print(f"  Strike     : ${m.strike:,.2f} "
                      f"({m.strike_source})")
                print(f"  Modo       : {live_tag} | V12 MOMENTUM TAKER + CONFIRM")
                print(f"  Threshold  : {self.cfg.momentum_threshold:.4%} "
                      f"| confirm: {self.cfg.confirm_ticks}s "
                      f"| bump: +${self.cfg.aggressive_bump:.2f}")
                print(f"  CLOB asks  : YES={ask_y:.2f} NO={ask_n:.2f}")
                print(f"  Stats      : windows={self._windows_count} "
                      f"trades={self._trades_count}{def_status}")
                if known:
                    nk = len(known)
                    print(f"  PnL acum.  : ${pnl:+.4f} | "
                          f"WR={wins}/{nk} ({wins/nk*100:.0f}%)")
                print(f"{'='*72}")

                self.market = m

                # Tick loop
                while not self._stop and int(time.time()) < m.end_ts:
                    self.tick()
                    time.sleep(1)

                # Settle
                if not self._stop:
                    self._settle_window(m)

            except Exception as e:
                import traceback
                print(f"\n[!! ERROR] {e}")
                traceback.print_exc()
                time.sleep(10)

    def shutdown(self):
        if self._stop:
            return
        self._stop = True
        print("\n-> shutdown...")
        if self.cfg.live:
            self.executor.cancel_all()
        self.binance.stop()
        if self.clob_yes:
            self.clob_yes.stop()
        if self.clob_no:
            self.clob_no.stop()

        known = [t for t in self._trade_history
                 if t.get("outcome") not in ("UNKNOWN", None)]
        if known:
            wins = sum(1 for t in known if t["won"])
            nk = len(known)
            pnl = round(sum(t["pnl_net"] for t in known), 4)
            print(f"\n{'='*50}")
            print(f"  RESUMEN V12 MOMENTUM TAKER + CONFIRM")
            print(f"  Ventanas   : {self._windows_count}")
            print(f"  Trades     : {self._trades_count}")
            if nk:
                print(f"  Win rate   : {wins}/{nk} ({wins/nk*100:.0f}%)")
            print(f"  PnL total  : ${pnl:+.4f}")
            print(f"{'='*50}")
