#!/usr/bin/env python3
"""
Monte Carlo Probability: BTC 15-min binaries
Kalshi KXBTC15M — MC crossing probability vs market mid
6-exchange CCXT weighted average spot price
No GBM — bootstrap resampling of actual 1s tick-to-tick changes.
"""

import os, sys, time, math, base64, hashlib, hmac, asyncio, urllib.parse, json, subprocess, signal, argparse, random
import concurrent.futures
import requests
from collections import deque
from dataclasses import dataclass
from typing import Optional, List, Dict
from datetime import datetime, timezone
from dotenv import load_dotenv
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
import aiohttp
from aiohttp import web
import ccxt

load_dotenv()

# ── Telegram (non-blocking) ────────────────────────────────────────
_tg_pool = concurrent.futures.ThreadPoolExecutor(max_workers=2, thread_name_prefix="tg")

class TelegramNotifier:
    def __init__(self, bot_token: str, chat_id: str):
        self.bot_token = bot_token
        self.chat_id = chat_id
        self.base_url = f"https://api.telegram.org/bot{bot_token}"

    def _do_send(self, text: str):
        try:
            requests.post(f"{self.base_url}/sendMessage",
                json={"chat_id": self.chat_id, "text": text, "parse_mode": "HTML"},
                timeout=5)
        except Exception as e:
            print(f"⚠️  Telegram send failed: {e}")

    def send(self, text: str):
        _tg_pool.submit(self._do_send, text)

# ── DB (optional) ───────────────────────────────────────────────────
try:
    from db import get_conn as _get_pg, create_all_tables, write_trade
    _pg = _get_pg(); create_all_tables(_pg)
    print("✓ PostgreSQL connected"); PG = True
except Exception as e:
    print(f"⚠️  No PG: {e}"); _pg = None; PG = False

def pg():
    global _pg
    if not PG: return None
    try: _pg.cursor().execute("SELECT 1"); return _pg
    except Exception:
        try: _pg = _get_pg(); return _pg
        except Exception: return None

# ── BTC Price Feed (6-exchange CCXT weighted average) ──────────────
class BTCPriceFeed:
    def __init__(self):
        self.exchanges = {
            'cryptocom': ccxt.cryptocom(),
            'coinbase': ccxt.coinbase(),
            'bitstamp': ccxt.bitstamp(),
            'kraken': ccxt.kraken(),
            'gemini': ccxt.gemini(),
            'bullish': ccxt.bullish(),
        }
        print(f"✓ CCXT price feed: {', '.join(self.exchanges.keys())}")

    def fetch_prices(self) -> Dict[str, Optional[float]]:
        prices = {}
        for name, exchange in self.exchanges.items():
            try:
                ticker = exchange.fetch_ticker('BTC/USD')
                prices[name] = ticker['last']
            except:
                prices[name] = None
        return prices

    def get_average_price(self) -> Optional[float]:
        prices = self.fetch_prices()
        weighted_sum = 0
        total_weight = 0
        for name, price in prices.items():
            if price is not None:
                weight = 2 if name in ['coinbase', 'cryptocom'] else 1
                weighted_sum += price * weight
                total_weight += weight
        if total_weight == 0:
            return None
        return weighted_sum / total_weight

# ── Monte Carlo Crossing Probability ───────────────────────────────
def mc_crossing_prob(spot: float, strike: float, returns: list,
                     remaining_sec: int, n_sims: int = 500) -> Optional[float]:
    """Bootstrap MC: resample actual 1s returns to estimate crossing probability.
    Returns probability that price finishes ABOVE strike."""
    if not returns or remaining_sec <= 0:
        return None
    crossing_count = 0
    for _ in range(n_sims):
        price = spot
        for _ in range(remaining_sec):
            r = random.choice(returns)
            price = price * math.exp(r)
        if price >= strike:
            crossing_count += 1
    return crossing_count / n_sims

def realized_vol(prices, interval_sec):
    if len(prices) < 10: return None
    lr = [math.log(prices[i]/prices[i-1]) for i in range(1,len(prices))
          if prices[i]>0 and prices[i-1]>0]
    if len(lr) < 5: return None
    mu = sum(lr)/len(lr)
    var = sum((r-mu)**2 for r in lr)/(len(lr)-1)
    return math.sqrt(var * 365.25*24*3600/interval_sec)

# ── Strategy ────────────────────────────────────────────────────────

WARMUP_SAMPLES = 900   # 15 minutes at 1s sampling
MARKET_DURATION = 900  # 15-min market = 900 seconds
TRADE_WINDOW_START = 180  # start placing orders at 3 min in
TRADE_WINDOW_END = 360    # stop placing orders at 6 min in

@dataclass
class Pos:
    side: Optional[str] = None
    contracts: int = 0
    avg_price: float = 0.0
    market_id: Optional[str] = None
    entry_mc: float = 0.0
    entry_mid: float = 0.0
    closing: bool = False

class MCBot:
    def __init__(self, k_key, k_sec):
        # Kalshi
        self.k_key = k_key
        self.k_base = "https://api.elections.kalshi.com/trade-api/v2"
        self.k_pk = None
        if k_sec:
            kd = open(k_sec).read() if os.path.isfile(k_sec) else k_sec
            self.k_pk = serialization.load_pem_private_key(
                kd.encode() if isinstance(kd,str) else kd,
                password=None, backend=default_backend())
            print("✓ Kalshi key loaded")

        # CCXT price feed
        self.price_feed = BTCPriceFeed()

        # State
        self.spot: Optional[float] = None
        self.spot_hist: deque = deque(maxlen=900)  # 15 min of 1s samples
        self.sample_iv = 1  # sample every 1 second
        self.last_sample = 0.0

        self.strike: Optional[float] = None
        self.market_id: Optional[str] = os.getenv("KALSHI_MARKET_ID")
        self.market_interval_minutes: Optional[int] = None
        self._parse_interval()
        self.yes_bid: Optional[float] = None
        self.yes_ask: Optional[float] = None
        self.mid: Optional[float] = None

        self.rv: Optional[float] = None
        self.mc_prob: Optional[float] = None  # MC crossing probability (above strike)
        self.mc_sims = 1000

        self.pos = Pos()
        self.edge_enter = 0.07    # 7% edge to enter
        self.contracts = 1

        # Pending maker order
        self.pending_oid: Optional[str] = None
        self.pending_side: Optional[str] = None
        self.pending_price: Optional[float] = None
        self.pending_count: int = 0

        # Cached fills (refreshed every tick, like auto1)
        self.cached_fills: list = []

        # One trade per market
        self.traded_this_market: bool = False

        # Full orderbook for dashboard
        self.ob_yes: list = []
        self.ob_no: list = []

        # Event log
        self.events: deque = deque(maxlen=100)

        # Websocket clients
        self.ws_clients: set = set()

        # Telegram
        self.telegram: Optional[TelegramNotifier] = None

        # Drawdown guard ($15 limit = 1500 cents)
        self.DRAWDOWN_LIMIT = 1500
        self.balance: int = 0
        self.peak_balance: int = 0

        # RV filter: skip trades if RV outside range
        self.RV_MIN = 0.25
        self.RV_MAX = 0.75

        self.running = False
        self.active = False
        self.paused = True
        self.auto_unpause = False
        self.task: Optional[asyncio.Task] = None

    # ── Kalshi API ──
    def _k_sign(self, ts, method, path):
        if not self.k_pk: return ""
        msg = ts + method + "/trade-api/v2" + path.split('?')[0]
        sig = self.k_pk.sign(msg.encode(),
            padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
                        salt_length=padding.PSS.DIGEST_LENGTH), hashes.SHA256())
        return base64.b64encode(sig).decode()

    async def _k(self, method, ep, data=None):
        url = f"{self.k_base}{ep}"
        ts = str(int(time.time()*1000))
        h = {'KALSHI-ACCESS-KEY': self.k_key,
             'KALSHI-ACCESS-SIGNATURE': self._k_sign(ts, method, ep),
             'KALSHI-ACCESS-TIMESTAMP': ts, 'Content-Type': 'application/json'}
        try:
            to = aiohttp.ClientTimeout(total=5)
            async with aiohttp.ClientSession(timeout=to) as s:
                fn = {'GET':s.get,'POST':s.post,'DELETE':s.delete}[method]
                kw = {'headers':h}
                if method=='POST': kw['json']=data
                async with fn(url, **kw) as r: return await r.json() or {}
        except: return {}

    # ── Data ──
    async def fetch_spot(self):
        loop = asyncio.get_event_loop()
        price = await loop.run_in_executor(None, self.price_feed.get_average_price)
        if price:
            self.spot = price
        return self.spot

    def _parse_interval(self):
        if not self.market_id: return
        ticker = self.market_id.split('-')[0]
        if 'M' in ticker:
            try:
                m_idx = ticker.rfind('M')
                s = ""
                for i in range(m_idx - 1, -1, -1):
                    if ticker[i].isdigit():
                        s = ticker[i] + s
                    else:
                        break
                if s:
                    self.market_interval_minutes = int(s)
                    print(f"✓ Interval: {self.market_interval_minutes} minutes")
            except:
                pass

    def minutes_into_market(self) -> Optional[float]:
        parts = self.market_id.split('-')
        if len(parts) < 2: return None
        tp = parts[1]
        try:
            month_map = {'JAN':1,'FEB':2,'MAR':3,'APR':4,'MAY':5,'JUN':6,
                         'JUL':7,'AUG':8,'SEP':9,'OCT':10,'NOV':11,'DEC':12}
            year   = 2000 + int(tp[0:2])
            month  = month_map.get(tp[2:5].upper())
            day    = int(tp[5:7])
            hour   = int(tp[7:9])
            minute = int(tp[9:11])
            if not month or not self.market_interval_minutes:
                return None
            import datetime as dt_mod
            expiry = dt_mod.datetime(year, month, day, hour, minute)
            start  = expiry - dt_mod.timedelta(minutes=self.market_interval_minutes)
            return (dt_mod.datetime.now() - start).total_seconds() / 60
        except:
            return None

    async def check_market_expiry(self) -> bool:
        if not self.market_interval_minutes:
            return False
        parts = self.market_id.split('-')
        if len(parts) < 2:
            return False
        tp = parts[1]
        try:
            import datetime as dt_mod
            month_map = {'JAN':1,'FEB':2,'MAR':3,'APR':4,'MAY':5,'JUN':6,
                         'JUL':7,'AUG':8,'SEP':9,'OCT':10,'NOV':11,'DEC':12}
            year   = 2000 + int(tp[0:2])
            month  = month_map.get(tp[2:5].upper())
            day    = int(tp[5:7])
            hour   = int(tp[7:9])
            minute = int(tp[9:11])
            if not month:
                return False
            expiry = dt_mod.datetime(year, month, day, hour, minute)
            if dt_mod.datetime.now() >= expiry:
                return await self.switch_to_next_market()
        except Exception as e:
            print(f"⚠️ Error parsing expiry: {e}")
        return False

    async def switch_to_next_market(self) -> bool:
        if not self.market_interval_minutes:
            return False
        parts = self.market_id.split('-')
        if len(parts) < 3:
            return False
        tp     = parts[1]
        prefix = parts[0]
        try:
            import datetime as dt_mod
            month_map = {'JAN':1,'FEB':2,'MAR':3,'APR':4,'MAY':5,'JUN':6,
                         'JUL':7,'AUG':8,'SEP':9,'OCT':10,'NOV':11,'DEC':12}
            rev_month = {v: k for k, v in month_map.items()}
            year   = 2000 + int(tp[0:2])
            month  = month_map.get(tp[2:5].upper())
            day    = int(tp[5:7])
            hour   = int(tp[7:9])
            minute = int(tp[9:11])
            if not month:
                return False
            nxt = (dt_mod.datetime(year, month, day, hour, minute)
                   + dt_mod.timedelta(minutes=self.market_interval_minutes))
            next_tp  = (f"{nxt.year%100:02d}{rev_month[nxt.month]}"
                        f"{nxt.day:02d}{nxt.hour:02d}{nxt.minute:02d}")
            next_sfx = f"{nxt.minute:02d}"
            next_id  = f"{prefix}-{next_tp}-{next_sfx}"
        except Exception as e:
            print(f"⚠️ Error computing next market: {e}")
            return False

        old_id = self.market_id
        await self.cancel_pending()
        self.market_id = next_id
        self.pos = Pos()
        self.traded_this_market = False
        self.cached_fills = []
        self.strike = None
        self.mid = None
        self.yes_bid = None
        self.yes_ask = None
        self.ob_yes = []
        self.ob_no = []
        self.mc_prob = None
        self.paused = True

        print(f"\n🔄 Market switched: {old_id} → {next_id}")
        for attempt in range(30):
            r = await self._k("GET", f"/markets/{self.market_id}")
            m = r.get("market", r)
            if m:
                floor_strike = m.get("floor_strike")
                if floor_strike is not None:
                    self.strike = float(floor_strike)
                    print(f"  Strike: ${self.strike:,.2f}")
                    break
            if attempt == 0:
                print(f"  Waiting for strike...")
            await asyncio.sleep(10)
        if not self.strike:
            print(f"  ⚠️ Could not fetch strike after retries")
        self.log_event("switch", f"{old_id} → {next_id}")
        if self.telegram:
            self.telegram.send(f"🔄 Market switched: {old_id[-12:]} → {next_id[-12:]}")
        await self.check_balance()
        if self.auto_unpause and not self.paused:
            pass
        elif self.auto_unpause:
            self.paused = False
            print(f"  ▶️ Auto-resumed for next market")
        return True

    async def load_market(self):
        if not self.market_id:
            print("❌ Set KALSHI_MARKET_ID in .env")
            return False
        r = await self._k("GET", f"/markets/{self.market_id}")
        m = r.get("market", r)
        if not m:
            print(f"❌ Market not found: {self.market_id}")
            return False
        floor_strike = m.get("floor_strike")
        if floor_strike is not None:
            self.strike = float(floor_strike)
            print(f"  Strike: ${self.strike:,.2f} (floor_strike)")
        else:
            print(f"⚠️  No floor_strike in market data for {self.market_id}")
        elapsed_min = self.minutes_into_market()
        print(f"  Market: {self.market_id}")
        if elapsed_min is not None:
            print(f"  Elapsed: {elapsed_min:.1f} min ({elapsed_min*60:.0f}s)")
            print(f"  TTE:    {(self.market_interval_minutes or 15) - elapsed_min:.1f} min")
        return True

    def market_elapsed(self) -> float:
        m = self.minutes_into_market()
        return m * 60 if m is not None else 0

    def remaining_seconds(self) -> int:
        m = self.minutes_into_market()
        if m is None or not self.market_interval_minutes:
            return 0
        return max(int((self.market_interval_minutes - m) * 60), 0)

    async def fetch_ob(self):
        if not self.market_id: return False
        r = await self._k("GET", f"/markets/{self.market_id}/orderbook")
        ob = r.get("orderbook_fp", {})
        yb = ob.get("yes_dollars", []); nb = ob.get("no_dollars", [])
        self.ob_yes = sorted([[float(p), int(float(s))] for p, s in yb],
                             key=lambda x: x[0], reverse=True)
        self.ob_no = sorted([[float(p), int(float(s))] for p, s in nb],
                            key=lambda x: x[0], reverse=True)
        self.yes_bid = self.ob_yes[0][0] if self.ob_yes else None
        if self.ob_no:
            self.yes_ask = round(1.0 - self.ob_no[0][0], 4)
        else: self.yes_ask = None
        if self.yes_bid is not None and self.yes_ask is not None:
            self.mid = round((self.yes_bid + self.yes_ask)/2, 4)
        else: self.mid = self.yes_bid or self.yes_ask
        return self.mid is not None

    # ── MC Calcs ──
    def get_returns(self) -> list:
        """Get log returns from spot history."""
        prices = list(self.spot_hist)
        if len(prices) < 10:
            return []
        return [math.log(prices[i]/prices[i-1]) for i in range(1, len(prices))
                if prices[i] > 0 and prices[i-1] > 0]

    def calc_mc(self):
        """Run Monte Carlo simulation to estimate crossing probability."""
        if not self.spot or not self.strike:
            self.mc_prob = None
            return
        returns = self.get_returns()
        if len(returns) < 30:
            self.mc_prob = None
            return
        rem = self.remaining_seconds()
        if rem <= 0:
            self.mc_prob = 1.0 if self.spot >= self.strike else 0.0
            return
        self.mc_prob = mc_crossing_prob(self.spot, self.strike, returns,
                                        rem, self.mc_sims)

    def calc_rv(self):
        if len(self.spot_hist) >= 10:
            self.rv = realized_vol(list(self.spot_hist), self.sample_iv)

    def get_edge(self) -> Optional[float]:
        """Edge = mc_prob - mid. Positive = YES underpriced, negative = NO underpriced."""
        if self.mc_prob is not None and self.mid is not None:
            return self.mc_prob - self.mid
        return None

    # ── Trading ──
    async def kalshi_order(self, side, price, count):
        d = {"ticker": self.market_id, "side": side, "action": "buy",
             "count": count, "type": "limit",
             "client_order_id": f"mc-{side}-{int(time.time()*1000)}",
             f"{side}_price": int(round(price*100))}
        r = await self._k("POST", "/portfolio/orders", d)
        oid = r.get("order",{}).get("order_id") if r else None
        if oid:
            conn = pg()
            if conn:
                try: write_trade(conn, self.market_id, "placement", side, price, count, "montecarlo", order_id=oid)
                except: pass
        return oid

    async def cancel_pending(self):
        if self.pending_oid:
            fills = self.get_filled_count()
            filled_count = fills.get(self.pending_side, 0)
            if filled_count >= self.pending_count:
                print(f"\n  ✓ Order already filled — keeping position")
                self.log_event("fill", f"FILL {self.pending_count} {self.pending_side.upper()} (detected at cancel)")
                if self.telegram:
                    self.telegram.send(
                        f"✅ FILL (detected at cancel): {self.pending_count} {self.pending_side.upper()}\n"
                        f"{self._tg_info()}")
                self.pos.side = self.pending_side
                self.pos.contracts += self.pending_count
                self.pos.avg_price = self.pending_price
                self.pos.market_id = self.market_id
                self.pos.entry_mc = self.mc_prob or 0
                self.pos.entry_mid = self.mid or 0
                self.pending_oid = None
                self.pending_side = None
                self.pending_price = None
                self.pending_count = 0
                return

            await self._k("DELETE", f"/portfolio/orders/{self.pending_oid}")
            print(f"\n  ✗ Cancelled pending {self.pending_side.upper()} order")
            self.log_event("cancel", f"Cancelled {self.pending_side.upper()} @ ${self.pending_price:.3f}")
            self.traded_this_market = False
            self.pending_oid = None
            self.pending_side = None
            self.pending_price = None
            self.pending_count = 0

    async def buy_yes(self):
        if not self.yes_bid or not self.market_id:
            print("❌ No bid"); return
        if self.pending_oid:
            print("⚠️  Already have pending order"); return
        price = self.yes_bid
        print(f"\n📈 BUY YES: {self.contracts} @ ${price:.3f} (joining bid)")
        oid = await self.kalshi_order("yes", price, self.contracts)
        if oid:
            self.pending_oid = oid
            self.pending_side = "yes"
            self.pending_price = price
            self.pending_count = self.contracts
            print(f"  ✓ Resting: {oid}")
        else:
            print("  ❌ Failed")

    async def buy_no(self):
        if not self.yes_ask or not self.market_id:
            print("❌ No ask"); return
        if self.pending_oid:
            print("⚠️  Already have pending order"); return
        no_price = round(1.0 - self.yes_ask, 4)
        print(f"\n📉 BUY NO: {self.contracts} @ ${no_price:.3f} (joining bid)")
        oid = await self.kalshi_order("no", no_price, self.contracts)
        if oid:
            self.pending_oid = oid
            self.pending_side = "no"
            self.pending_price = no_price
            self.pending_count = self.contracts
            print(f"  ✓ Resting: {oid}")
        else:
            print("  ❌ Failed")

    def check_pending_fill(self):
        if not self.pending_oid:
            return
        fills = self.get_filled_count()
        filled_count = fills.get(self.pending_side, 0)
        if filled_count >= self.pending_count:
            side = self.pending_side
            price = self.pending_price
            count = self.pending_count

            print(f"\n✓ FILLED: {count} {side.upper()} @ ${price:.3f}")
            self.log_event("fill", f"FILLED {count} {side.upper()} @ ${price:.3f}")
            if self.telegram:
                self.telegram.send(
                    f"✅ FILL: {count} {side.upper()} @ ${price:.3f}\n"
                    f"{self._tg_info()}")
            self.pos.side = side
            self.pos.contracts += count
            self.pos.avg_price = price
            self.pos.market_id = self.market_id
            self.pos.entry_mc = self.mc_prob or 0
            self.pos.entry_mid = self.mid or 0

            conn = pg()
            if conn:
                try:
                    write_trade(conn, self.market_id, "fill", side,
                                price, count, "montecarlo", order_id=self.pending_oid)
                except: pass

            self.pending_oid = None
            self.pending_side = None
            self.pending_price = None
            self.pending_count = 0

    async def follow_bid(self):
        if not self.pending_oid: return
        fills = self.get_filled_count()
        if fills.get(self.pending_side, 0) >= self.pending_count:
            return
        if self.pending_side == "yes":
            current_bid = self.yes_bid
        else:
            current_bid = round(1.0 - self.yes_ask, 4) if self.yes_ask else None
        if current_bid is None: return
        if abs(current_bid - self.pending_price) < 0.001: return
        old_price = self.pending_price
        amend_data = {
            "ticker": self.market_id,
            "side": self.pending_side,
            "action": "buy",
            "count": self.pending_count,
            f"{self.pending_side}_price": int(round(current_bid * 100))}
        r = await self._k("POST", f"/portfolio/orders/{self.pending_oid}/amend", amend_data)
        if r and r.get("order") and r["order"].get("status") != "canceled":
            self.pending_price = current_bid
            direction = "↑" if current_bid > old_price else "↓"
            print(f"\n{direction} {self.pending_side.upper()}: ${old_price:.3f} → ${current_bid:.3f}")
        else:
            self.pending_oid = None
            self.pending_side = None
            self.pending_price = None
            self.pending_count = 0

    # ── Drawdown guard ──
    async def check_balance(self):
        result = await self._k("GET", "/portfolio/balance")
        if not result or 'balance' not in result:
            return
        self.balance = result['balance']
        if self.balance > self.peak_balance:
            self.peak_balance = self.balance
        drawdown = self.peak_balance - self.balance
        if drawdown >= self.DRAWDOWN_LIMIT and not self.paused:
            self.paused = True
            msg = (f"🚨 DRAWDOWN GUARD: Balance ${self.balance/100:.2f} is "
                   f"${drawdown/100:.2f} below peak ${self.peak_balance/100:.2f} "
                   f"— auto-paused")
            print(f"\n{msg}")
            self.log_event("drawdown", msg)
            if self.telegram:
                self.telegram.send(msg)

    # ── Telegram helpers ──
    def _tg_info(self) -> str:
        parts = []
        if self.mc_prob is not None: parts.append(f"MC prob: {self.mc_prob*100:.1f}%")
        if self.mid is not None: parts.append(f"Mid: ${self.mid:.3f} ({self.mid*100:.1f}%)")
        if self.rv is not None: parts.append(f"RV: {self.rv*100:.1f}%")
        if self.spot is not None and self.strike is not None:
            dist = self.spot - self.strike
            parts.append(f"Spot-Strike: ${dist:+,.2f}")
        parts.append(f"Samples: {len(self.spot_hist)}")
        return "\n".join(parts)

    # ── Event log ──
    def log_event(self, kind, msg):
        self.events.append({"ts": time.time(), "kind": kind, "msg": msg})

    # ── Loop ──
    async def fetch_fills(self):
        r = await self._k("GET", f"/portfolio/fills?ticker={self.market_id}&limit=100")
        if r and isinstance(r, dict):
            fills = r.get("fills")
            if fills and isinstance(fills, list):
                self.cached_fills = fills

    def get_filled_count(self) -> dict:
        counts = {"yes": 0, "no": 0}
        for f in self.cached_fills:
            side = f.get("side")
            count = int(float(f.get("count_fp", "0")))
            if side in ("yes", "no"):
                counts[side] += count
        return counts

    async def refresh(self):
        await asyncio.gather(self.fetch_spot(), self.fetch_ob(), self.fetch_fills(), return_exceptions=True)
        now = time.time()
        if self.spot and now - self.last_sample >= self.sample_iv:
            self.spot_hist.append(self.spot); self.last_sample = now
        self.calc_rv()
        self.calc_mc()

    def status(self):
        sp = f"${self.spot:,.2f}" if self.spot else "?"
        k = f"${self.strike:,.2f}" if self.strike else "?"
        mc = f"{self.mc_prob*100:.1f}%" if self.mc_prob is not None else "?"
        md = f"{self.mid*100:.1f}%" if self.mid else "?"
        rv = f"{self.rv*100:.1f}%" if self.rv else "?"
        edge = self.get_edge()
        if edge is not None:
            es = f"{edge*100:+.1f}%"
            sig = "YES" if edge > self.edge_enter else ("NO" if edge < -self.edge_enter else "FLAT")
        else:
            es = "?"; sig = "WAIT"
        elapsed = self.market_elapsed()
        rem = self.remaining_seconds()
        ps = "FLAT"
        if self.pos.contracts > 0:
            ps = f"{self.pos.side.upper()}x{self.pos.contracts}"
        pnd = ""
        if self.pending_oid:
            pnd = f" PND:{self.pending_side.upper()}x{self.pending_count}@{self.pending_price:.3f}"
        pause = " PAUSED" if self.paused else ""
        print(f"\r[MC] {sp} K:{k} {rem}s | MC:{mc} Mid:{md} {es} [{sig}] | "
              f"RV:{rv}({len(self.spot_hist)}) | {ps}{pnd} W:{elapsed:.0f}s{pause}", end="")
        sys.stdout.flush()

    async def loop(self):
        ls = time.time()
        while self.running and self.active:
          try:
            await self.check_market_expiry()
            await self.refresh()

            elapsed = self.market_elapsed()
            in_window = TRADE_WINDOW_START <= elapsed <= TRADE_WINDOW_END

            if not self.paused:
                self.check_pending_fill()

                if self.pending_oid:
                    await self.follow_bid()

                edge = self.get_edge()

                # Cancel if edge goes null
                if edge is None and self.pending_oid:
                    print(f"\n⚠️ MC/Mid null — cancelling resting order")
                    self.log_event("cancel", "MC/Mid null — cancelled")
                    await self.cancel_pending()

                if in_window:
                    if self.pos.contracts == 0 and not self.pending_oid and not self.traded_this_market and edge is not None:
                        # RV filter
                        rv_ok = self.rv is not None and self.RV_MIN <= self.rv <= self.RV_MAX
                        if not rv_ok and abs(edge) > self.edge_enter and self.rv is not None:
                            rv_pct = self.rv * 100
                            msg = f"⏭ SKIP: RV {rv_pct:.1f}% outside {self.RV_MIN*100:.0f}-{self.RV_MAX*100:.0f}% filter | Edge {edge:+.3f}"
                            print(f"\n{msg}")
                            self.log_event("skip", msg)
                            if self.telegram:
                                self.telegram.send(f"{msg}\n{self._tg_info()}")
                            self.traded_this_market = True

                        elif rv_ok and edge > self.edge_enter:
                            # MC says YES is underpriced
                            print(f"\n📈 MC Edge +{edge:.3f} ({edge*100:.1f}%) — YES")
                            self.log_event("signal", f"MC EDGE +{edge:.3f} → YES")
                            self.traded_this_market = True
                            if self.telegram:
                                self.telegram.send(
                                    f"📈 ORDER: YES x{self.contracts} @ ${self.yes_bid:.3f}\n"
                                    f"MC Edge: +{edge:.3f} ({edge*100:.1f}%)\n"
                                    f"{self._tg_info()}")
                            await self.buy_yes()

                        elif rv_ok and edge < -self.edge_enter:
                            # MC says NO is underpriced
                            no_price = round(1.0 - (self.yes_ask or 0.99), 4) if self.yes_ask else 0.01
                            print(f"\n📉 MC Edge {edge:.3f} ({edge*100:.1f}%) — NO")
                            self.log_event("signal", f"MC EDGE {edge:.3f} → NO")
                            self.traded_this_market = True
                            if self.telegram:
                                self.telegram.send(
                                    f"📉 ORDER: NO x{self.contracts} @ ${no_price:.3f}\n"
                                    f"MC Edge: {edge:.3f} ({edge*100:.1f}%)\n"
                                    f"{self._tg_info()}")
                            await self.buy_no()

                    # Cancel if edge reverts
                    if self.pending_oid and self.pos.contracts == 0 and edge is not None:
                        if abs(edge) < self.edge_enter:
                            print(f"\n↩ Edge reverted to {edge:+.3f} — cancelling")
                            await self.cancel_pending()

                else:
                    if self.pending_oid:
                        print(f"\n⏸ Outside window ({elapsed:.0f}s) — cancelling orders")
                        self.log_event("window", f"Outside window ({elapsed:.0f}s) — cancelled")
                        await self.cancel_pending()
                    if elapsed > TRADE_WINDOW_END:
                        print(f"\n⏸ Past trading window ({elapsed:.0f}s > {TRADE_WINDOW_END}s) — waiting for next market")
                        self.log_event("window", f"Past window — paused")
                        self.paused = True

            if time.time()-ls >= 2: self.status(); ls = time.time()
            await asyncio.sleep(0.25)
          except Exception as e:
            print(f"\n❌ Loop error: {e}")
            import traceback; traceback.print_exc()
            await asyncio.sleep(1)
        print(f"\n✓ Loop ended (running={self.running}, active={self.active})")
        if self.telegram:
            self.telegram.send(f"⚠️ Loop ended (running={self.running}, active={self.active})")

    async def warmup(self):
        print(f"\n⏳ Warming up — collecting {WARMUP_SAMPLES} spot samples (15 min)...")
        print("   Collecting 1s BTC ticks for MC simulation\n")

        await self.check_balance()
        if self.balance > 0:
            print(f"💰 Balance: ${self.balance/100:.2f}")

        print("🔍 Loading market...")
        market_ok = await self.load_market()
        if not market_ok:
            print("⚠️  Market not loaded — will collect samples only")

        while self.running and len(self.spot_hist) < WARMUP_SAMPLES:
            if market_ok:
                await asyncio.gather(self.fetch_spot(), self.fetch_ob(), return_exceptions=True)
            else:
                await self.fetch_spot()

            if self.spot:
                now = time.time()
                if now - self.last_sample >= self.sample_iv:
                    self.spot_hist.append(self.spot)
                    self.last_sample = now

                n = len(self.spot_hist)
                if n >= 10:
                    self.calc_rv()
                    self.calc_mc()

                mc_str = f" | MC: {self.mc_prob*100:.1f}%" if self.mc_prob is not None else ""
                rv_str = f" | RV: {self.rv*100:.1f}%" if self.rv else ""
                print(f"\r  [{n}/{WARMUP_SAMPLES}] ${self.spot:,.2f}{rv_str}{mc_str}", end="")
                sys.stdout.flush()

            await asyncio.sleep(0.25)

        if not self.running: return

        self.calc_rv()
        self.calc_mc()
        print(f"\n\n✓ Warmup complete — {len(self.spot_hist)} samples")
        if self.rv: print(f"  RV: {self.rv*100:.1f}%")
        if self.mc_prob is not None: print(f"  MC prob: {self.mc_prob*100:.1f}%")

        if not market_ok:
            print("\n🔍 Loading market...")
            if not await self.load_market():
                return

        print("\n🔍 Fetching orderbook...")
        if await self.fetch_ob():
            if self.yes_bid: print(f"  YES bid: ${self.yes_bid:.3f}")
            if self.yes_ask: print(f"  YES ask: ${self.yes_ask:.3f}")
            if self.mid: print(f"  Mid: ${self.mid:.3f} ({self.mid*100:.1f}%)")

        edge = self.get_edge()
        if edge is not None:
            print(f"  MC Edge: {edge:+.3f} ({edge*100:+.1f}%)")

        elapsed = self.market_elapsed()
        print(f"\n  Market elapsed: {elapsed:.0f}s")
        print(f"  Trading window: {TRADE_WINDOW_START}-{TRADE_WINDOW_END}s")

        self.active = True
        if self.auto_unpause:
            self.paused = False
            print(f"\n✓ Ready — AUTO-TRADING")
        else:
            self.paused = True
            print(f"\n✓ Ready — PAUSED. Press R to start trading.")
        print(f"  Auto-trades when |MC edge| > {self.edge_enter*100:.1f}%")
        print(f"  Orders only in {TRADE_WINDOW_START}-{TRADE_WINDOW_END}s window")
        print(f"  RV filter: {self.RV_MIN*100:.0f}-{self.RV_MAX*100:.0f}%")
        print(f"  MC simulations: {self.mc_sims}")
        if self.telegram:
            self.telegram.send(
                f"🎲 MC bot started\n"
                f"Market: {self.market_id}\n"
                f"Edge threshold: {self.edge_enter*100:.1f}%\n"
                f"MC sims: {self.mc_sims}\n"
                f"RV filter: {self.RV_MIN*100:.0f}-{self.RV_MAX*100:.0f}%\n"
                f"Drawdown limit: ${self.DRAWDOWN_LIMIT/100:.0f}")


async def main():
    parser = argparse.ArgumentParser(description="Monte Carlo Prob — BTC 15-min Binaries")
    parser.add_argument("--telegram", action="store_true",
                        help="Headless mode: telegram notifications, no dashboard")
    parser.add_argument("--account", type=str, default=None,
                        help="Account name (e.g. mom) — reads KALSHI_MOM_API_KEY etc.")
    args = parser.parse_args()

    if args.account:
        prefix = args.account.upper()
        kk = os.getenv(f"KALSHI_{prefix}_API_KEY")
        ks = os.getenv(f"KALSHI_{prefix}_API_SECRET")
        if not kk or not ks:
            print(f"❌ Missing KALSHI_{prefix}_API_KEY/KALSHI_{prefix}_API_SECRET"); sys.exit(1)
    else:
        kk = os.getenv("KALSHI_API_KEY")
        ks = os.getenv("KALSHI_API_SECRET")
    mid = os.getenv("KALSHI_MARKET_ID")
    if not kk or not ks: print("❌ Missing KALSHI_API_KEY/SECRET"); sys.exit(1)
    if not mid: print("❌ Missing KALSHI_MARKET_ID in .env"); sys.exit(1)

    a = MCBot(kk, ks)
    a.running = True

    if args.telegram:
        if args.account:
            prefix = args.account.upper()
            tg_token = os.getenv(f"TELEGRAM_{prefix}_BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
            tg_chat = os.getenv(f"TELEGRAM_{prefix}_CHAT_ID") or os.getenv("TELEGRAM_CHAT_ID")
        else:
            tg_token = os.getenv("TELEGRAM_BOT_TOKEN")
            tg_chat = os.getenv("TELEGRAM_CHAT_ID")
        if not tg_token or not tg_chat:
            print("❌ Missing TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID"); sys.exit(1)
        a.telegram = TelegramNotifier(tg_token, tg_chat)
        a.auto_unpause = True
        acct = f" ({args.account})" if args.account else ""
        print(f"✓ Telegram mode{acct} — headless, auto-unpause")

    print("\n" + "="*70)
    print("MONTE CARLO — BTC 15-min Binaries")
    print("Bootstrap MC crossing probability vs Kalshi mid")
    print("="*70)
    print(f"\n  KALSHI_MARKET_ID = {mid}")
    print(f"  Trading window  = {TRADE_WINDOW_START}-{TRADE_WINDOW_END}s into market")
    print(f"  MC simulations  = 500")
    print(f"  RV filter       = 25-75%")
    print(f"  Drawdown limit  = $15")
    mode = "telegram (headless)" if args.telegram else "interactive"
    print(f"  Mode            = {mode}\n")

    if args.telegram:
        await a.warmup()
        while a.running:
            if a.task and not a.task.done():
                await asyncio.sleep(5)
            else:
                print("\n⚠️ Loop task ended — restarting")
                if a.telegram:
                    a.telegram.send("⚠️ Loop task ended — restarting")
                a.active = True
                a.task = asyncio.create_task(a.loop())
    else:
        print("Controls:")
        print("  [G] Start (3-min warmup → auto-trade)")
        print("  [R] Pause/resume")
        print("  [N] Cancel pending order")
        print("  [Q] Quit")
        print("="*70)

        lp = asyncio.get_event_loop()
        try:
            while a.running:
                c = await lp.run_in_executor(None, lambda: input("Command: ").strip().upper())
                if c == "G":
                    await a.warmup()
                elif c == "R":
                    a.paused = not a.paused
                    if not a.paused:
                        a.peak_balance = a.balance
                    print(f"\n  {'Paused' if a.paused else 'Resumed'}")
                elif c == "N":
                    await a.cancel_pending()
                elif c == "Q":
                    a.running = False
                    a.active = False
                else:
                    print("❌ Invalid")
        finally:
            sys.exit(0)

if __name__ == "__main__":
    asyncio.run(main())
