"""
LIVE TRADING BOT - BTC 15-Min Kalshi Markets
Smart tiered bet sizing. 2:1 reward/risk ratio.
Live dashboard: open C:\kalshibot\dashboard.html in Chrome
"""
import os, base64, datetime, time, uuid, json, threading, requests
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

try:
    import websocket
    WS_AVAILABLE = True
except ImportError:
    WS_AVAILABLE = False

load_dotenv()
KEY_ID   = os.getenv("KALSHI_API_KEY_ID")
KEY_PATH = os.getenv("KALSHI_PRIVATE_KEY_PATH")
BASE_URL = "https://api.elections.kalshi.com"

# ── Core Parameters ─────────────────────────────────────────────────
PROFIT_TARGET      = 0.40
STOP_LOSS          = 0.20
MAX_ENTRY_PRICE    = 0.70
MIN_ENTRY_PRICE    = 0.15
MOMENTUM_SECONDS   = 120
MOMENTUM_THRESHOLD = 0.0015
MAX_POSITIONS      = 2
SCAN_INTERVAL      = 10
MIN_SECS_LEFT      = 180

# ── Tiered Bet Sizing ────────────────────────────────────────────────
BET_WEAK   = 0.75
BET_BASE   = 1.00
BET_MEDIUM = 2.00
BET_STRONG = 3.50
BET_MAX    = 5.00
# ────────────────────────────────────────────────────────────────────

prices        = {"binance": None, "coinbase": None, "kraken": None}
price_times   = {"binance": None, "coinbase": None, "kraken": None}
price_history = []
price_lock    = threading.Lock()
open_positions = []
trade_log      = []
traded_tickers = set()   # one entry per 15-min window, cleared on new market

def load_private_key(path):
    with open(path, "rb") as f:
        return serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())

def sign_request(pk, ts, method, path):
    msg = (ts + method + path).encode("utf-8")
    sig = pk.sign(msg, padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.DIGEST_LENGTH), hashes.SHA256())
    return base64.b64encode(sig).decode("utf-8")

def get_headers(method, path):
    ts = str(int(datetime.datetime.now().timestamp() * 1000))
    pk = load_private_key(KEY_PATH)
    return {"KALSHI-ACCESS-KEY": KEY_ID, "KALSHI-ACCESS-TIMESTAMP": ts, "KALSHI-ACCESS-SIGNATURE": sign_request(pk, ts, method, path), "Content-Type": "application/json"}

def binance_ws():
    if not WS_AVAILABLE: return
    def on_msg(ws, msg):
        try:
            data = json.loads(msg)
            price = float(data.get("p") or data.get("c") or 0)
            if price > 0:
                now = time.time()
                with price_lock:
                    prices["binance"] = price
                    price_times["binance"] = now
                    price_history.append((now, price))
        except: pass
    def on_close(ws, *args):
        time.sleep(3)
        binance_ws()
    ws = websocket.WebSocketApp("wss://stream.binance.com:9443/ws/btcusdt@trade", on_message=on_msg, on_close=on_close)
    ws.run_forever()

def poll_coinbase():
    while True:
        try:
            r = requests.get("https://api.coinbase.com/v2/prices/BTC-USD/spot", timeout=5)
            if r.status_code == 200:
                with price_lock:
                    prices["coinbase"] = float(r.json()["data"]["amount"])
                    price_times["coinbase"] = time.time()
        except: pass
        time.sleep(5)

def poll_kraken():
    while True:
        try:
            r = requests.get("https://api.kraken.com/0/public/Ticker?pair=XBTUSD", timeout=5)
            if r.status_code == 200:
                result = r.json().get("result", {})
                price = float(result.get("XXBTZUSD", {}).get("c", [0])[0])
                if price > 0:
                    with price_lock:
                        prices["kraken"] = price
                        price_times["kraken"] = time.time()
        except: pass
        time.sleep(5)

def poll_binance_rest():
    while True:
        try:
            r = requests.get("https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT", timeout=5)
            if r.status_code == 200:
                price = float(r.json()["price"])
                now = time.time()
                with price_lock:
                    prices["binance"] = price
                    price_times["binance"] = now
                    price_history.append((now, price))
        except: pass
        time.sleep(10)

def get_best_price():
    with price_lock:
        snap_p = dict(prices)
        snap_t = dict(price_times)
    now = time.time()
    valid = [(src, snap_p[src]) for src in snap_p if snap_p[src] and snap_t[src] and (now - snap_t[src]) < 30]
    if not valid: return None, None, None
    price = snap_p["binance"] or valid[0][1]
    srcs = "+".join(s for s, _ in valid)
    edge = None
    b, c, k = snap_p["binance"], snap_p["coinbase"], snap_p["kraken"]
    if b and c and k:
        avg = (c + k) / 2
        div = (b - avg) / avg
        if abs(div) > 0.0005:
            edge = f"EDGE {div*100:+.3f}% Binance vs others"
    return price, srcs, edge

def get_momentum():
    now = time.time()
    cutoff = now - MOMENTUM_SECONDS
    with price_lock:
        recent = [(t, p) for t, p in price_history if t >= cutoff]
        price_history[:] = [(t, p) for t, p in price_history if t >= now - 600]
    if len(recent) < 4: return None, None, None
    oldest = recent[0][1]
    newest = recent[-1][1]
    mom = (newest - oldest) / oldest
    pct = f"{mom*100:+.3f}%"
    if mom >= MOMENTUM_THRESHOLD: return mom, "UP", pct
    elif mom <= -MOMENTUM_THRESHOLD: return mom, "DOWN", pct
    return mom, "FLAT", pct

def get_bet_size(momentum, edge, balance):
    if momentum is None: return BET_BASE, "BASE"
    strength = abs(momentum) / MOMENTUM_THRESHOLD
    if strength < 2.0:   bet, tier = BET_WEAK,   "WEAK"
    elif strength < 3.0: bet, tier = BET_BASE,   "BASE"
    elif strength < 4.0: bet, tier = BET_MEDIUM, "MEDIUM"
    else:                bet, tier = BET_STRONG, "STRONG"
    if edge and strength >= 2.0:
        bet = min(bet + 1.00, BET_MAX)
        tier = tier + "+EDGE"
    bet = min(bet, BET_MAX, balance * 0.10)
    return round(bet, 2), tier

def get_balance():
    path = "/trade-api/v2/portfolio/balance"
    headers = get_headers("GET", path)
    r = requests.get(BASE_URL + path, headers=headers)
    if r.status_code == 200:
        return r.json().get("balance", 0) / 100
    return None

def get_15m_market():
    path = "/trade-api/v2/markets"
    params = "?status=open&limit=10&series_ticker=KXBTC15M"
    headers = get_headers("GET", path)
    r = requests.get(BASE_URL + path + params, headers=headers)
    if r.status_code != 200: return None
    now = datetime.datetime.now(datetime.timezone.utc)
    for m in r.json().get("markets", []):
        try:
            open_t  = datetime.datetime.fromisoformat(m["open_time"].replace("Z", "+00:00"))
            close_t = datetime.datetime.fromisoformat(m["close_time"].replace("Z", "+00:00"))
            if open_t <= now <= close_t:
                secs = (close_t - now).total_seconds()
                return {"ticker": m["ticker"], "yes_ask": float(m.get("yes_ask_dollars") or 0), "yes_bid": float(m.get("yes_bid_dollars") or 0), "no_ask": float(m.get("no_ask_dollars") or 0), "no_bid": float(m.get("no_bid_dollars") or 0), "floor": float(m.get("floor_strike") or 0), "secs_left": secs, "volume": float(m.get("volume_24h_fp") or 0)}
        except: continue
    return None

def get_live_price(ticker):
    path = f"/trade-api/v2/markets/{ticker}"
    headers = get_headers("GET", path)
    r = requests.get(BASE_URL + path, headers=headers)
    if r.status_code == 200:
        m = r.json().get("market", {})
        return float(m.get("yes_bid_dollars") or 0), float(m.get("yes_ask_dollars") or 0), float(m.get("no_bid_dollars") or 0), float(m.get("no_ask_dollars") or 0)
    return None, None, None, None

def place_order(ticker, side, count, price_dollars, action="buy"):
    path = "/trade-api/v2/portfolio/orders"
    headers = get_headers("POST", path)
    price_cents = int(round(price_dollars * 100))
    body = {"ticker": ticker, "client_order_id": str(uuid.uuid4()), "type": "limit", "action": action, "side": side, "count": count, "yes_price": price_cents if side == "yes" else (100 - price_cents)}
    r = requests.post(BASE_URL + path, headers=headers, json=body)
    if r.status_code in (200, 201): return True, r.json().get("order", {})
    return False, r.text

def get_btc_volatility():
    with price_lock:
        recent = [p for _, p in price_history[-60:]]
    if len(recent) < 10: return 0.0
    mean = sum(recent) / len(recent)
    variance = sum((p - mean) ** 2 for p in recent) / len(recent)
    return round((variance ** 0.5) / mean * 100, 4)

def log_analytics(pos, event, exit_price=None, pnl=None, exit_reason=None):
    """Shared with paper bot — feeds the same trade_analytics.json."""
    now = datetime.datetime.now()
    record = {
        "event"            : event,
        "ticker"           : pos["ticker"],
        "timestamp"        : time.time(),
        "time_str"         : now.strftime("%H:%M:%S"),
        "hour"             : now.hour,
        "day_of_week"      : now.weekday(),
        "direction"        : pos["direction"],
        "signal_type"      : "MOMENTUM",
        "tier"             : pos.get("tier", "?"),
        "entry_price"      : pos["entry_price"],
        "momentum_pct"     : round(abs(pos.get("momentum_raw", 0)) * 100, 4),
        "momentum_strength": round(abs(pos.get("momentum_raw", 0)) / MOMENTUM_THRESHOLD, 2),
        "btc_volatility"   : get_btc_volatility(),
        "market_yes_ask"   : pos.get("market_yes_ask", 0),
        "market_spread"    : round(pos.get("market_yes_ask", 0) - pos.get("market_yes_bid", 0), 3),
        "secs_left"        : pos.get("secs_left", 0),
        "btc_price"        : pos.get("entry_btc", 0),
        "source"           : "LIVE",
    }
    if event == "exit" and exit_price is not None:
        record.update({
            "exit_price" : exit_price,
            "pnl"        : round(pnl, 4),
            "pnl_pct"    : round((exit_price - pos["entry_price"]) / pos["entry_price"] * 100, 2),
            "outcome"    : "WIN" if (pnl or 0) > 0 else "LOSS",
            "exit_reason": exit_reason,
            "hold_secs"  : int(time.time() - pos.get("entry_time", time.time())),
        })
    analytics_path = "C:\\kalshibot\\trade_analytics.json"
    try:
        try:
            with open(analytics_path) as f: data = json.load(f)
        except: data = []
        data.append(record)
        with open(analytics_path, "w") as f: json.dump(data, f, indent=2, default=str)
    except: pass

def save_log():
    try:
        with open("C:\\kalshibot\\trade_log.json", "w") as f:
            json.dump(trade_log, f, indent=2, default=str)
    except: pass

def save_state(btc, sources, edge, momentum, direction, market, balance, bet_tier="BASE", high_water_mark=None, session_halted=False):
    exits = [t for t in trade_log if t.get("event") == "exit"]
    hwm = high_water_mark or balance or 0
    stop_level = round(hwm * 0.80, 2)
    pos_data = []
    for p in open_positions:
        current = p.get("current_price", p["entry_price"])
        pnl_abs = round((current - p["entry_price"]) * p["count"], 4)
        pnl_pct = round((current - p["entry_price"]) / p["entry_price"] * 100, 2) if p["entry_price"] else 0
        held_secs = int(time.time() - p.get("entry_time", time.time()))
        pos_data.append({
            "ticker"        : p["ticker"],
            "side"          : p["side"],
            "direction"     : p["direction"],
            "signal_type"   : "MOMENTUM",
            "tier"          : p.get("tier", "?"),
            "entry_price"   : p["entry_price"],
            "current_price" : current,
            "target"        : p["target"],
            "stop"          : p["stop"],
            "count"         : p["count"],
            "cost"          : p["cost"],
            "pnl_abs"       : pnl_abs,
            "pnl_pct"       : pnl_pct,
            "floor"         : p["floor"],
            "held_secs"     : held_secs,
            "entry_time_str": datetime.datetime.fromtimestamp(
                p.get("entry_time", time.time())).strftime("%H:%M:%S"),
        })
    state = {
        "mode": "live", "last_update": time.time(),
        "btc_price": btc, "btc_source": sources, "edge": edge or "",
        "balance": round(balance, 2) if balance else 0,
        "starting_balance": balance or 0,
        "high_water_mark": round(hwm, 2),
        "stop_level": stop_level,
        "momentum": momentum or 0, "direction": direction or "FLAT",
        "bet_tier": bet_tier, "session_halted": session_halted,
        "market": market, "open_positions": pos_data, "completed_trades": exits
    }
    try:
        with open("C:\\kalshibot\\bot_state.json", "w") as f:
            json.dump(state, f, default=str)
    except: pass

def enter_trade(market, direction, btc_price, momentum, edge, balance):
    side        = "yes" if direction == "UP" else "no"
    entry_price = market["yes_ask"] if side == "yes" else market["no_ask"]
    if not (MIN_ENTRY_PRICE <= entry_price <= MAX_ENTRY_PRICE): return False, "SKIP"

    bet, tier = get_bet_size(momentum, edge, balance)
    count     = max(1, int(bet / entry_price))
    cost      = round(entry_price * count, 2)
    target    = round(entry_price * (1 + PROFIT_TARGET), 4)
    stop      = round(entry_price * (1 - STOP_LOSS), 4)
    max_gain  = round((target - entry_price) * count, 2)
    max_loss  = round((entry_price - stop) * count, 2)
    rr        = round(max_gain / max_loss, 1) if max_loss > 0 else 0
    mins      = int(market["secs_left"] // 60)
    secs      = int(market["secs_left"] % 60)

    print(f"\n  {'='*62}")
    print(f"  LIVE TRADE  --  {'YES  BTC UP' if side=='yes' else 'NO   BTC DOWN'}  [{tier}]")
    print(f"  BTC       : ${btc_price:,.2f}  |  Floor: ${market['floor']:,.2f}")
    print(f"  Bet tier  : {tier}  ({abs(momentum or 0)/MOMENTUM_THRESHOLD:.1f}x threshold)")
    print(f"  Entry     : ${entry_price:.3f}  x{count}  =  ${cost:.2f}")
    print(f"  Max gain  : +${max_gain:.2f}  (exit ${target:.3f})  [{rr}:1 R:R]")
    print(f"  Max loss  : -${max_loss:.2f}  (stop ${stop:.3f})")
    print(f"  Expires   : {mins}m {secs}s")
    print(f"  {'='*62}")

    ok, result = place_order(market["ticker"], side, count, entry_price, "buy")
    if ok:
        order_id = result.get("order_id", "")
        pos = {"ticker": market["ticker"], "order_id": order_id, "side": side,
               "entry_price": entry_price, "current_price": entry_price,
               "count": count, "cost": cost, "target": target, "stop": stop,
               "entry_time": time.time(), "entry_btc": btc_price,
               "direction": direction, "floor": market["floor"], "tier": tier,
               "momentum_raw": momentum or 0,
               "market_yes_ask": market["yes_ask"],
               "market_yes_bid": market["yes_bid"],
               "secs_left": market["secs_left"]}
        open_positions.append(pos)
        traded_tickers.add(market["ticker"])
        trade_log.append({**pos, "event": "entry", "time_str": datetime.datetime.now().strftime("%H:%M:%S")})
        log_analytics(pos, "entry")
        save_log()
        print(f"  ORDER PLACED  ID: {order_id}")
        return True, tier
    else:
        print(f"  ORDER FAILED: {result}")
        return False, tier

def monitor_positions():
    to_close = []
    for pos in open_positions:
        yb, ya, nb, na = get_live_price(pos["ticker"])
        if yb is None: continue
        current  = yb if pos["side"] == "yes" else nb
        pos["current_price"] = current   # keep dashboard in sync
        entry    = pos["entry_price"]
        pnl_pct  = (current - entry) / entry
        pnl_abs  = round((current - entry) * pos["count"], 4)
        reason   = None
        if current >= pos["target"]:   reason = f"TARGET HIT +{pnl_pct*100:.1f}%"
        elif current <= pos["stop"]:   reason = f"STOP HIT {pnl_pct*100:.1f}%"
        elif time.time() - pos["entry_time"] > 780: reason = f"TIME EXIT {pnl_pct*100:+.1f}%"
        if reason:
            ok, res = place_order(pos["ticker"], pos["side"], pos["count"], current, "sell")
            if ok:
                print(f"\n  EXIT -- {reason}  P&L: ${pnl_abs:+.2f}")
                trade_log.append({**pos, "event": "exit", "exit_price": current, "pnl": pnl_abs, "reason": reason, "time_str": datetime.datetime.now().strftime("%H:%M:%S")})
                log_analytics(pos, "exit", exit_price=current, pnl=pnl_abs, exit_reason=reason)
                save_log()
                to_close.append(pos)
            else:
                print(f"  SELL FAILED: {res}")
        else:
            print(f"  HOLD  {'YES' if pos['side']=='yes' else 'NO'}  ${entry:.3f}->${current:.3f}  ({pnl_pct*100:+.1f}%)")
    for pos in to_close:
        open_positions.remove(pos)

def run():
    print("\n" + "="*65)
    print("  LIVE TRADING BOT  --  Kalshi KXBTC15M")
    print("  Binance WS + Coinbase + Kraken")
    print("="*65)
    print(f"  Bet tiers     : WEAK $0.75  |  BASE $1.00  |  MED $2.00  |  STRONG $3.50  |  MAX $5.00")
    print(f"  Profit target : +{PROFIT_TARGET*100:.0f}%  |  Stop: -{STOP_LOSS*100:.0f}%  |  R:R = {PROFIT_TARGET/STOP_LOSS:.1f}:1")
    print(f"  Dashboard     : C:\\kalshibot\\dashboard.html")
    print("="*65)

    balance = get_balance()
    if balance is not None:
        print(f"  Kalshi balance: ${balance:.2f}")
        signal_only = balance < 0.50
    else:
        signal_only = True
        balance = 0

    high_water_mark = balance

    # Load auto-tuned params from learning engine
    good_hours = []; avoid_hours = []
    auto_params_path = "C:\\kalshibot\\auto_params.json"
    if os.path.exists(auto_params_path):
        try:
            with open(auto_params_path) as f: ap = json.load(f)
            good_hours  = ap.get("good_hours", [])
            avoid_hours = ap.get("avoid_hours", [])
            print(f"  ✅ Auto-params loaded ({ap.get('trades_analyzed','?')} trades analyzed)")
            for note in ap.get("notes", []): print(f"     {note}")
        except Exception as e:
            print(f"  ⚠  Could not load auto_params.json: {e}")

    if signal_only:
        print(f"  SIGNAL ONLY MODE  --  fund Kalshi to trade live")
    else:
        print(f"  LIVE TRADING ENABLED")
    print("="*65)

    if WS_AVAILABLE:
        threading.Thread(target=binance_ws, daemon=True).start()
        print("  Binance WebSocket connecting...")
    else:
        threading.Thread(target=poll_binance_rest, daemon=True).start()
    threading.Thread(target=poll_coinbase, daemon=True).start()
    threading.Thread(target=poll_kraken, daemon=True).start()

    print("  Waiting for price feeds...")
    for _ in range(15):
        time.sleep(1)
        with price_lock:
            has = any(p for p in prices.values() if p)
        if has:
            with price_lock:
                p = prices["binance"] or prices["coinbase"] or prices["kraken"]
            print(f"  Connected  BTC ${p:,.2f}")
            break
    else:
        print("  Slow, continuing anyway")

    print(f"  Running  |  Ctrl+C to stop\n")

    last_entry   = 0
    last_market_ticker = None
    current_tier = "BASE"
    starting_bal = balance

    while True:
        try:
            time.sleep(SCAN_INTERVAL)
            now = time.time()

            btc, sources, edge = get_best_price()
            if not btc:
                print(f"  [{datetime.datetime.now().strftime('%H:%M:%S')}] No price data")
                continue

            # Refresh balance + high water mark every 5 minutes
            if int(now) % 300 < SCAN_INTERVAL:
                bal = get_balance()
                if bal is not None:
                    balance = bal
                    starting_bal = starting_bal or bal
                    if balance > high_water_mark:
                        high_water_mark = balance

            momentum, direction, pct = get_momentum()
            market = get_15m_market()
            warmup = len(price_history) < 8

            # Clear per-market lock on new window
            if market and market["ticker"] != last_market_ticker:
                traded_tickers.clear()
                last_market_ticker = market["ticker"]

            if not warmup and momentum is not None:
                _, current_tier = get_bet_size(momentum, edge, balance)

            mom_str = "warming up" if warmup else f"{'UP' if direction=='UP' else 'DOWN' if direction=='DOWN' else 'FLAT'} {pct} [{current_tier}]"
            mkt_str = "waiting..." if not market else f"{int(market['secs_left']//60)}m{int(market['secs_left']%60)}s Y:{market['yes_ask']:.2f} N:{market['no_ask']:.2f}"

            print(f"  [{datetime.datetime.now().strftime('%H:%M:%S')}]  BTC ${btc:,.2f}  {mom_str}  |  {mkt_str}  |  ${balance:.2f}  [{len(open_positions)}/{MAX_POSITIONS}]")
            if edge: print(f"  {edge}")

            if open_positions:
                monitor_positions()

            save_state(btc, sources, edge, momentum, direction, market, balance, current_tier,
                       high_water_mark=high_water_mark)

            current_hour = datetime.datetime.now().hour
            hour_blocked = (avoid_hours and current_hour in avoid_hours
                            and not (good_hours and current_hour in good_hours))

            if (not signal_only
                    and not warmup
                    and market
                    and direction in ("UP","DOWN")
                    and len(open_positions) < MAX_POSITIONS
                    and now - last_entry > 120
                    and market["secs_left"] > MIN_SECS_LEFT
                    and market["ticker"] not in traded_tickers
                    and balance >= 0.50
                    and not hour_blocked):

                entered, tier = enter_trade(market, direction, btc, momentum, edge, balance)
                if entered:
                    last_entry = now

        except KeyboardInterrupt:
            exits = [t for t in trade_log if t.get("event") == "exit"]
            if exits:
                total = sum(t.get("pnl",0) for t in exits)
                wins  = len([t for t in exits if t.get("pnl",0) > 0])
                print(f"\n  Session: {wins}/{len(exits)} wins  P&L: ${total:+.2f}")
            save_log()
            break
        except Exception as e:
            print(f"  Error: {e}")
            time.sleep(10)

if __name__ == "__main__":
    run()
