"""
AUTONOMOUS JACKPOT CORE V2.5 — UNIVERSAL MULTI-EXPIRY SURVEILLANCE & MAKER LIMIT ENGINE
100% EC2 Native Execution | Zero Local Intervention

Features:
  1. Universal Multi-Expiry Option Chain Radar:
     Scans ALL weekly (03-Nov), next-weekly (09-Nov), and monthly (23-Nov) options simultaneously.
  2. Macro Day Trend Bias (User Winning Methodology):
     Detects Intraday Macro Trend (Spot vs Day Open). In a +280 pt Bull Trend, Call dips receive
     a +20% probability bonus; counter-trend Puts are strictly locked.
  3. Passive Maker Limit Order Below Live LTP:
     Places orders strictly at or below the Top Bid (Maker discount), eliminating taker spread slippage.
     Auto-cancels if unfilled within 10s timeout (Zero slippage, zero loss).
  4. Real-Time Account Funds Auto-Sync:
     Maintains live 85% usable margin capacity / 15% untouchable liquid cushion floor.
  5. Decoupled Architecture:
     All surveillance, capital, risk, and limit parameters hot-reload from jackpot_config.json.
  6. Forensic Post-Mortem Intelligence:
     Classifies root-cause diagnosis on exit ("WHY WE FAILED / WHY WE SUCCEEDED") in SQLite & State.
"""

import os
import sys
import time
import json
import sqlite3
import urllib.request
import urllib.parse
from datetime import datetime
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
CONFIG_FILE = BASE_DIR / "jackpot_config.json"
STATE_FILE = BASE_DIR / "state.json"
DB_FILE = BASE_DIR / "jackpot_ledger.db"

def round_tick(price):
    return round(round(float(price) / 0.05) * 0.05, 2)

DEFAULT_CONFIG = {
    "capital_and_risk": {
        "margin_cap_pct": 0.85,
        "cushion_reserve_pct": 0.15,
        "max_daily_loss_pct": 0.10,
        "max_trades_today": 8,
        "single_position_invariant": True
    },
    "confidence_and_probability": {
        "min_confidence_threshold_pct": 80.0,
        "min_depth_ratio": 2.0,
        "target_depth_ratio": 2.5,
        "max_spread_pts": 0.25,
        "acceptable_spread_pts": 0.35,
        "min_slope_pts": 3.0,
        "min_volume": 10000000,
        "target_volume": 30000000,
        "min_oi": 5000000,
        "bid_wall_multiplier": 2.0
    },
    "execution_and_resolution": {
        "order_mode": "BID_ASK_PT_DOWN",
        "bid_ask_pt_down": 0.50,
        "limit_offset_pts": 0.50,
        "limit_fill_timeout_sec": 15.0,
        "micro_cut_pts": 6.0,
        "stall_timeout_sec": 480.0,
        "post_trade_cooloff_sec": 180.0,
        "p_adverse_net_guarantee_floor": 50.0,
        "trailing_start_pts": 8.0,
        "trailing_ratchet_pct": 0.65,
        "lot_size": 65
    },
    "market_surveillance": {
        "index_symbol": "NSE_INDEX|Nifty 50",
        "monitored_expiries": ["2026-11-03", "2026-11-09", "2026-11-23"],
        "strike_corridor_pts": 350.0,
        "trading_time_start": "09:25:00",
        "trading_time_end": "15:20:00"
    }
}

TOKEN_SEARCH_PATHS = [
    Path(r"c:\Users\LENOVO\Desktop\SaasAITrading\secrets\upstox2nov2026.txt"),
    Path("/home/ubuntu/upstox_engine/upstox_token.txt"),
    Path("/home/ubuntu/rust_engine_deploy/token.txt"),
    BASE_DIR / "upstox_token.txt",
    BASE_DIR / "token.txt"
]

def get_token():
    for p in TOKEN_SEARCH_PATHS:
        if p.exists():
            try:
                tok = p.read_text(encoding="utf-8").strip()
                if tok:
                    return tok
            except Exception:
                pass
    return None

def api_request(endpoint, method="GET", payload=None):
    token = get_token()
    if not token:
        return None, "No token found"
    
    url = f"https://api.upstox.com/v2{endpoint}"
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    }
    data = None
    if payload:
        headers["Content-Type"] = "application/json"
        data = json.dumps(payload).encode("utf-8")

    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=8) as resp:
            res = json.loads(resp.read().decode())
            return res.get("data"), None
    except urllib.error.HTTPError as e:
        err_msg = e.read().decode()
        try:
            err_json = json.loads(err_msg)
            return None, err_json.get("message") or err_json.get("errors") or err_msg
        except Exception:
            return None, f"HTTP {e.code}: {err_msg}"
    except Exception as e:
        return None, str(e)

def init_db():
    try:
        conn = sqlite3.connect(DB_FILE)
        cur = conn.cursor()
        cur.execute("""
            CREATE TABLE IF NOT EXISTS live_jackpot_trades (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT,
                trade_num INTEGER,
                strike TEXT,
                direction TEXT,
                lots INTEGER,
                qty INTEGER,
                entry_price REAL,
                exit_price REAL,
                gross_pnl REAL,
                charges REAL,
                net_pnl REAL,
                resolution_type TEXT,
                running_cash REAL,
                confidence REAL DEFAULT 0.0,
                depth_ratio REAL DEFAULT 0.0,
                market_regime TEXT DEFAULT 'UNKNOWN',
                post_mortem_reason TEXT DEFAULT 'N/A'
            )
        """)
        conn.commit()
        conn.close()
    except Exception as e:
        print(f"[DB INIT ERROR] {e}")

def calculate_statutory_deductions(buy_val, sell_val):
    turnover = buy_val + sell_val
    brokerage = 40.0
    stt = round(sell_val * 0.000625, 2)
    exchange_charges = round(turnover * 0.0005, 2)
    sebi = round(turnover * 0.000001, 2)
    stamp_duty = round(buy_val * 0.00003, 2)
    gst = round((brokerage + exchange_charges) * 0.18, 2)
    total_deductions = round(brokerage + stt + exchange_charges + sebi + stamp_duty + gst, 2)
    return total_deductions

def calculate_p_adverse(entry_px, qty, guarantee_floor=50.0):
    for offset_ticks in range(1, 2000):
        target_exit = round_tick(entry_px + (offset_ticks * 0.05))
        gross = round((target_exit - entry_px) * qty, 2)
        ded = calculate_statutory_deductions(entry_px * qty, target_exit * qty)
        net = round(gross - ded, 2)
        if net >= guarantee_floor:
            pts_needed = round(target_exit - entry_px, 2)
            return target_exit, pts_needed, ded
    target_exit = round_tick(entry_px + 2.50)
    return target_exit, 2.50, calculate_statutory_deductions(entry_px * qty, target_exit * qty)

def load_or_init_state(avail_cash):
    state = {
        "engine_state": "READY",
        "active_mode": "LIVE_PRODUCTION",
        "account_id": "8CA8P7",
        "initial_cash": avail_cash,
        "available_cash": avail_cash,
        "realized_pnl_today": 0.0,
        "net_banked_cash_today": 0.0,
        "statutory_deductions_paid": 0.0,
        "trades_completed_today": 0,
        "consecutive_losses": 0,
        "circuit_breaker_active": False,
        "circuit_breaker_reason": None,
        "active_position": None,
        "last_exit_timestamp": 0.0,
        "last_market_regime": "UNKNOWN",
        "last_post_mortem": "N/A"
    }
    if STATE_FILE.exists():
        try:
            with open(STATE_FILE, "r") as f:
                saved = json.load(f)
                state.update(saved)
                state["available_cash"] = avail_cash
        except Exception:
            pass
    save_state(state)
    return state

def save_state(state):
    state["last_update"] = datetime.now().isoformat()
    try:
        with open(STATE_FILE, "w") as f:
            json.dump(state, f, indent=2)
    except Exception as e:
        print(f"[STATE] Save error: {e}")

class AutonomousJackpotEngine:
    def __init__(self):
        print("================================================================================")
        print("   AUTONOMOUS JACKPOT CORE V2.5 — UNIVERSAL MULTI-EXPIRY & MAKER LIMIT ENGINE")
        print("================================================================================")
        init_db()
        self.config_mtime = 0.0
        self.config = {}
        self.depth_history = {}
        self.load_config()

        self.state = None
        self.spot_history = []
        self.last_log_time = 0
        self.last_funds_refresh = 0
        self.day_open_price = 0.0

        self.refresh_funds()
        print(f"[INIT] Live Broker Available Margin: ₹{self.available_margin:,.2f}")
        print(f"[INIT] {int(self.margin_cap_pct*100)}% Max Risk Margin: ₹{self.margin_cap:,.2f} | {int(self.cushion_reserve_pct*100)}% Cushion: ₹{self.cushion:,.2f}")
        print(f"[INIT] Max Daily Loss Cap: ₹{self.daily_loss_cap:,.2f} | Max Trades Today: {self.max_trades_today}")
        print(f"[INIT] Strict Zero-Trust Threshold: {self.min_confidence_threshold_pct:.1f}%")
        print(f"[INIT] Order Execution Mode: {self.order_mode} (Maker Limit strictly below live LTP)")
        print(f"[INIT] Monitored Expiries: {', '.join(self.monitored_expiries)}")
        print(f"[INIT] Decoupled Config File: {CONFIG_FILE} (Hot-Reloading Enabled)")

    def load_config(self):
        if not CONFIG_FILE.exists():
            try:
                with open(CONFIG_FILE, "w") as f:
                    json.dump(DEFAULT_CONFIG, f, indent=2)
            except Exception as e:
                print(f"[CONFIG] Error creating default: {e}")

        try:
            mtime = os.path.getmtime(CONFIG_FILE)
            if mtime != self.config_mtime:
                with open(CONFIG_FILE, "r") as f:
                    cfg = json.load(f)
                self.config = cfg
                self.config_mtime = mtime

                cr = cfg.get("capital_and_risk", {})
                self.margin_cap_pct = float(cr.get("margin_cap_pct", 0.85))
                self.cushion_reserve_pct = float(cr.get("cushion_reserve_pct", 0.15))
                self.max_daily_loss_pct = float(cr.get("max_daily_loss_pct", 0.10))
                self.max_trades_today = int(cr.get("max_trades_today", 8))

                cp = cfg.get("confidence_and_probability", {})
                self.min_confidence_threshold_pct = float(cp.get("min_confidence_threshold_pct", 80.0))
                self.min_depth_ratio = float(cp.get("min_depth_ratio", 2.0))
                self.target_depth_ratio = float(cp.get("target_depth_ratio", 2.5))
                self.max_spread_pts = float(cp.get("max_spread_pts", 0.25))
                self.acceptable_spread_pts = float(cp.get("acceptable_spread_pts", 0.35))
                self.min_slope_pts = float(cp.get("min_slope_pts", 3.0))
                self.min_volume = int(cp.get("min_volume", 10000000))
                self.target_volume = int(cp.get("target_volume", 30000000))
                self.min_oi = int(cp.get("min_oi", 5000000))
                self.bid_wall_multiplier = float(cp.get("bid_wall_multiplier", 2.0))

                er = cfg.get("execution_and_resolution", {})
                self.order_mode = str(er.get("order_mode", "BID_ASK_PT_DOWN"))
                self.bid_ask_pt_down = float(er.get("bid_ask_pt_down", er.get("limit_offset_pts", 0.50)))
                self.limit_offset_pts = self.bid_ask_pt_down
                self.limit_fill_timeout_sec = float(er.get("limit_fill_timeout_sec", 15.0))
                self.micro_cut_pts = float(er.get("micro_cut_pts", 6.0))
                self.stall_timeout_sec = float(er.get("stall_timeout_sec", 480.0))
                self.post_trade_cooloff_sec = float(er.get("post_trade_cooloff_sec", 180.0))
                self.p_adverse_floor = float(er.get("p_adverse_net_guarantee_floor", 50.0))
                self.trailing_start_pts = float(er.get("trailing_start_pts", 8.0))
                self.trailing_ratchet_pct = float(er.get("trailing_ratchet_pct", 0.65))
                self.lot_size = int(er.get("lot_size", 65))

                ms = cfg.get("market_surveillance", {})
                self.index_symbol = ms.get("index_symbol", "NSE_INDEX|Nifty 50")
                self.monitored_expiries = ms.get("monitored_expiries", ["2026-11-03", "2026-11-09", "2026-11-23"])
                self.strike_corridor_pts = float(ms.get("strike_corridor_pts", 350.0))
                self.trading_time_start = ms.get("trading_time_start", "09:25:00")
                self.trading_time_end = ms.get("trading_time_end", "15:20:00")

                if hasattr(self, "available_margin"):
                    self.margin_cap = round(self.available_margin * self.margin_cap_pct, 2)
                    self.cushion = round(self.available_margin * self.cushion_reserve_pct, 2)
                    self.daily_loss_cap = round(self.available_margin * self.max_daily_loss_pct, 2)

                print(f"[CONFIG RELOADED 🔄] Parameters: Margin {int(self.margin_cap_pct*100)}% / Cushion {int(self.cushion_reserve_pct*100)}% | Threshold >= {self.min_confidence_threshold_pct}% | Expiries: {len(self.monitored_expiries)}")
        except Exception as e:
            print(f"[CONFIG ERROR] Failed to parse {CONFIG_FILE}: {e}")

    def refresh_funds(self):
        data, err = api_request("/user/profile")
        if data and "user_id" in data:
            self.account_id = data["user_id"]
        
        data, err = api_request("/user/get-funds-and-margin")
        if data and "equity" in data:
            self.available_margin = float(data["equity"]["available_margin"])
        else:
            self.available_margin = 26274.12
        self.margin_cap = round(self.available_margin * self.margin_cap_pct, 2)
        self.cushion = round(self.available_margin * self.cushion_reserve_pct, 2)
        self.daily_loss_cap = round(self.available_margin * self.max_daily_loss_pct, 2)
        if not self.state:
            self.state = load_or_init_state(self.available_margin)
        else:
            self.state["available_cash"] = self.available_margin
            save_state(self.state)

    def scan_full_option_universe(self):
        """
        Universal Multi-Expiry Option Chain Surveillance Core:
        Scans all weekly, next-weekly, and monthly expiries in ~300ms.
        Extracts Nifty Spot, Day Open, liquid candidates, and live 5-level depth.
        """
        candidates = []
        spot = 0.0
        day_open = 0.0

        # 1. Spot Quote & Day Open Price
        nq, err = api_request("/market-quote/quotes?instrument_key=NSE_INDEX|Nifty%2050")
        if nq:
            ndata = nq.get("NSE_INDEX:Nifty 50", {})
            spot = float(ndata.get("last_price", 0.0))
            day_open = float(ndata.get("ohlc", {}).get("open", 0.0))
            if day_open > 0:
                self.day_open_price = day_open

        # 2. Query all monitored expiries
        for exp in self.monitored_expiries:
            chain, err = api_request(f"/option/chain?instrument_key=NSE_INDEX|Nifty%2050&expiry_date={exp}")
            if not chain:
                continue
            strikes = chain if isinstance(chain, list) else chain.get("data", [])
            if strikes and spot == 0.0:
                spot = float(strikes[0].get("underlying_spot_price", 0.0))
            for d in strikes:
                sp = float(d.get("strike_price", 0.0))
                if spot > 0 and abs(sp - spot) <= self.strike_corridor_pts:
                    for opt_type, opt in [("CE", d.get("call_options", {})), ("PE", d.get("put_options", {}))]:
                        md = opt.get("market_data", {})
                        ltp = float(md.get("ltp", 0.0))
                        vol = int(md.get("volume", 0))
                        oi = float(md.get("oi", 0.0))
                        bid = float(md.get("bid_price", 0.0))
                        ask = float(md.get("ask_price", 0.0))
                        key = opt.get("instrument_key")
                        if 15.0 <= ltp <= 250.0 and vol >= 25000 and key:
                            spread = round(ask - bid, 2) if ask > 0 and bid > 0 else 99.0
                            if spread <= 0.60:
                                candidates.append({
                                    "name": f"NIFTY {int(sp)} {opt_type} ({exp[-5:]})",
                                    "token": key,
                                    "strike": sp,
                                    "type": opt_type,
                                    "expiry": exp,
                                    "ltp": ltp,
                                    "volume": vol,
                                    "oi": int(oi),
                                    "bid": bid,
                                    "ask": ask,
                                    "spread": spread
                                })

        # 3. Stratified Multi-Expiry Selection: Guarantee Top 5 Liquid from EACH Expiry (Weekly, Next-Weekly, Monthly)
        top_candidates = []
        by_expiry = {}
        for c in candidates:
            by_expiry.setdefault(c["expiry"], []).append(c)

        for exp in self.monitored_expiries:
            exp_cands = by_expiry.get(exp, [])
            sorted_exp = sorted(exp_cands, key=lambda x: x["volume"] * x["oi"], reverse=True)
            top_candidates.extend(sorted_exp[:5])

        if top_candidates:
            keys = ",".join(c["token"] for c in top_candidates)
            q_res, err = api_request(f"/market-quote/quotes?instrument_key={keys}")
            if q_res:
                for c in top_candidates:
                    matched_q = None
                    token_num = c["token"].split("|")[-1]
                    for k, v in q_res.items():
                        if c["token"] == v.get("instrument_token") or token_num in k:
                            matched_q = v
                            break
                    c["quote"] = matched_q or {}

        return spot, day_open, top_candidates, len(candidates)

    def place_order(self, symbol, qty, direction, price=0.0, order_type="MARKET"):
        payload = {
            "quantity": qty,
            "product": "I",
            "validity": "DAY",
            "price": round_tick(price) if order_type == "LIMIT" else 0.0,
            "tag": "INTELLIGENCE_JACKPOT",
            "instrument_token": symbol,
            "order_type": order_type,
            "transaction_type": direction.upper(),
            "disclosed_quantity": 0,
            "trigger_price": 0.0,
            "is_amo": False
        }
        data, err = api_request("/order/place", method="POST", payload=payload)
        return data, err

    def get_order_status(self, order_id):
        data, err = api_request(f"/order/details?order_id={order_id}")
        if data and "status" in data:
            return data.get("status"), float(data.get("average_price", 0.0) or 0.0)
        return None, 0.0

    def cancel_order(self, order_id):
        data, err = api_request(f"/order/cancel?order_id={order_id}", method="DELETE")
        return data, err

    def analyze_market_regime(self, nifty_spot):
        if len(self.spot_history) < 10:
            return "INITIALIZING", 0.0, 0.0, 0.0

        fast_slope = round(nifty_spot - (sum(self.spot_history[-5:]) / 5.0), 2)
        med_slope = round(nifty_spot - (sum(self.spot_history[-15:]) / min(len(self.spot_history), 15.0)), 2)
        range_pts = round(max(self.spot_history) - min(self.spot_history), 2)

        if range_pts < 6.0 and abs(fast_slope) < 2.0:
            regime = "DEAD_CONSOLIDATION"
        elif fast_slope >= self.min_slope_pts and med_slope >= 1.5:
            regime = "BULLISH_VELOCITY_EXPANSION"
        elif fast_slope <= -self.min_slope_pts and med_slope <= -1.5:
            regime = "BEARISH_VELOCITY_EXPANSION"
        else:
            regime = "CHOP_CONSOLIDATION_TRAP"

        return regime, fast_slope, med_slope, range_pts

    def evaluate_in_depth_probability(self, c, nifty_spot, day_open, regime, fast_slope, med_slope):
        score = 0.0
        reasons = []

        is_call = (c["type"] == "CE")
        token = c["token"]
        q = c.get("quote", {})
        depth = q.get("depth", {})
        buys = depth.get("buy", [])
        sells = depth.get("sell", [])

        top3_bid_vol = sum([b.get("quantity", 0) for b in buys[:3]]) if buys else int(c.get("bid", 0) > 0) * 1000
        top3_ask_vol = sum([s.get("quantity", 0) for s in sells[:3]]) if sells else 1000
        ratio = round(top3_bid_vol / max(top3_ask_vol, 1), 2)

        if token not in self.depth_history:
            self.depth_history[token] = []
        self.depth_history[token].append(ratio)
        if len(self.depth_history[token]) > 3:
            self.depth_history[token].pop(0)

        spread = c["spread"]
        vol = c["volume"]
        oi = c["oi"]

        # Factor 0: Macro Day Trend Bias (User Winning Methodology)
        # Nifty Day Open vs Current Spot
        if day_open > 0:
            day_change = round(nifty_spot - day_open, 2)
            if day_change >= 40.0:
                # Strong Bull Expansion Day
                if is_call:
                    score += 20.0
                    reasons.append(f"Macro Bull Day (+{day_change:.1f} pts) (+20%)")
                else:
                    score -= 30.0
                    reasons.append(f"Fighting Macro Bull Day (-30%)")
            elif day_change <= -40.0:
                # Strong Bear Expansion Day
                if not is_call:
                    score += 20.0
                    reasons.append(f"Macro Bear Day ({day_change:.1f} pts) (+20%)")
                else:
                    score -= 30.0
                    reasons.append(f"Fighting Macro Bear Day (-30%)")

        # Factor 1: Temporal Depth Persistence & Anti-Spoofing (20%)
        samples = self.depth_history[token]
        if len(samples) >= 3 and all(s >= self.target_depth_ratio for s in samples):
            score += 20.0
            reasons.append(f"Persistent Depth ({samples[-1]}x) (+20%)")
        elif ratio >= self.target_depth_ratio:
            score += 12.0
            reasons.append(f"Depth {ratio}x (+12%)")
        elif ratio >= self.min_depth_ratio:
            score += 8.0
            reasons.append(f"Moderate Depth {ratio}x (+8%)")
        else:
            reasons.append(f"Depth {ratio}x (+0%)")

        # Factor 2: Spread Tightness & Execution Slippage (15%)
        if spread <= self.max_spread_pts:
            score += 15.0
            reasons.append(f"Tight Spread {spread} pts (+15%)")
        elif spread <= self.acceptable_spread_pts:
            score += 8.0
            reasons.append(f"Spread {spread} pts (+8%)")
        else:
            reasons.append(f"Wide Spread {spread} pts (+0%)")

        # Factor 3: Market Regime & Directional Velocity Alignment (20%)
        if is_call and regime == "BULLISH_VELOCITY_EXPANSION":
            score += 20.0
            reasons.append(f"Bullish Expansion +{fast_slope:.1f} pts (+20%)")
        elif not is_call and regime == "BEARISH_VELOCITY_EXPANSION":
            score += 20.0
            reasons.append(f"Bearish Expansion {fast_slope:.1f} pts (+20%)")
        else:
            reasons.append(f"Regime {regime} (+0%)")

        # Factor 4: Institutional Liquidity Foundation (15%)
        if vol >= self.target_volume and oi >= self.min_oi:
            score += 15.0
            reasons.append("Deep Institutional Liquidity (+15%)")
        elif vol >= self.min_volume:
            score += 8.0
            reasons.append("Moderate Liquidity (+8%)")
        else:
            reasons.append("Low Liquidity (+0%)")

        # Factor 5: Top-1 Bid Wall Imbalance (10%)
        top1_bid_qty = buys[0].get("quantity", 0) if buys else 0
        top1_ask_qty = sells[0].get("quantity", 0) if sells else 0
        if top1_bid_qty >= (top1_ask_qty * self.bid_wall_multiplier) and top1_bid_qty > 0:
            score += 10.0
            reasons.append("Top-1 Bid Wall Dominance (+10%)")
        else:
            reasons.append("No Bid Wall (+0%)")

        # Regime Penalty: If market is in Chop Consolidation, hard-cap maximum score at 55.0%
        if "CHOP" in regime or "DEAD" in regime:
            if score > 55.0:
                score = 55.0
                reasons.append("[REGIME PENALTY: Chop Trap Capped at 55%]")

        return round(max(score, 0.0), 1), ratio, spread, reasons

    def perform_post_mortem(self, pos, exit_px, resolution_type, gross_pnl, net_pnl, charges):
        elapsed = round(time.time() - pos["entry_timestamp"], 1)
        pts_diff = round(exit_px - pos["entry_price"], 2)

        diagnosis = ""
        category = ""

        if net_pnl > 0:
            category = "SUCCESSFUL_TRADE"
            if "RUNNER" in resolution_type:
                diagnosis = f"WHY WE SUCCEEDED: Clean high-velocity trend expansion. Locked in {pts_diff:+.2f} pts via trailing ratchet after peak ₹{pos['highest_ltp']:.2f}."
            else:
                diagnosis = f"WHY WE SUCCEEDED: Adverse profit floor protected net cash. Banked {pts_diff:+.2f} pts above ₹{charges:.2f} statutory friction."
        else:
            category = "FAILED_TRADE"
            if "MICRO_CUT" in resolution_type:
                diagnosis = f"WHY WE FAILED: Instant shock cut (-{self.micro_cut_pts} pts) in {elapsed}s. Spot momentum reversed immediately after entry. False breakout or spoofed liquidity trap."
            elif "STALL" in resolution_type:
                diagnosis = f"WHY WE FAILED: Velocity collapse. Position held for {elapsed}s without directional follow-through. Theta decay and friction resulted in scratch."
            else:
                diagnosis = f"WHY WE FAILED: Adverse movement of {pts_diff:+.2f} pts. Friction: -₹{charges:.2f}."

        print(f"\n================================================================================")
        print(f"  [FORENSIC TRADE POST-MORTEM 🔬] {category}")
        print(f"================================================================================")
        print(f"• Strike: {pos['strike_name']} | Result: Net ₹{net_pnl:+,.2f} | Gross: ₹{gross_pnl:+,.2f} | Charges: -₹{charges:.2f}")
        print(f"• Hold Duration: {elapsed} seconds | Resolution: {resolution_type}")
        print(f"• Diagnostic Rationale:\n  {diagnosis}")
        print(f"• Adaptive Action: Enforcing {int(self.post_trade_cooloff_sec)}s mandatory cool-off to prevent tilt/churn.\n")

        return diagnosis

    def run_trading_cycle(self):
        self.load_config()

        now = datetime.now()
        cur_time = now.strftime("%H:%M:%S")

        if cur_time < self.trading_time_start or cur_time > self.trading_time_end:
            return

        if self.state.get("circuit_breaker_active"):
            return

        # Periodically refresh account funds every 60 seconds
        if time.time() - self.last_funds_refresh >= 60:
            self.last_funds_refresh = time.time()
            self.refresh_funds()

        # -------------------------------------------------------------
        # UNIVERSAL MULTI-EXPIRY OPTION CHAIN RADAR
        # -------------------------------------------------------------
        nifty_spot, day_open, top_candidates, total_scanned = self.scan_full_option_universe()
        if nifty_spot <= 0.0 or not top_candidates:
            return

        if nifty_spot > 0:
            self.spot_history.append(nifty_spot)
            if len(self.spot_history) > 30:
                self.spot_history.pop(0)

        regime, fast_slope, med_slope, range_pts = self.analyze_market_regime(nifty_spot)
        self.state["last_market_regime"] = regime

        active_pos = self.state.get("active_position")

        # -------------------------------------------------------------
        # STATE A: NO ACTIVE POSITION -> UNIVERSAL INTELLIGENCE SCAN
        # -------------------------------------------------------------
        if not active_pos:
            if self.state.get("trades_completed_today", 0) >= self.max_trades_today:
                return

            last_exit = self.state.get("last_exit_timestamp", 0.0)
            elapsed_since_exit = time.time() - last_exit
            if elapsed_since_exit < self.post_trade_cooloff_sec:
                remaining_cooloff = int(self.post_trade_cooloff_sec - elapsed_since_exit)
                if remaining_cooloff % 60 == 0 and time.time() - self.last_log_time >= 15:
                    self.last_log_time = time.time()
                    print(f"[COOL-OFF ACTIVE ⏳] Anti-churn pause: {remaining_cooloff}s remaining before next setup evaluation.")
                return

            best_candidate = None
            best_score = 0.0
            best_reasons = []
            best_ratio = 0.0
            best_spread = 0.0
            all_evaluations = []

            for c in top_candidates:
                score, ratio, spread, reasons = self.evaluate_in_depth_probability(c, nifty_spot, day_open, regime, fast_slope, med_slope)
                ltp = c["ltp"]
                all_evaluations.append((c["name"], score, ratio, spread, ltp, c["expiry"][-5:]))

                if score > best_score:
                    best_score = score
                    best_candidate = c
                    best_reasons = reasons
                    best_ratio = ratio
                    best_spread = spread

            if time.time() - self.last_log_time >= 15:
                self.last_log_time = time.time()
                day_trend_pts = round(nifty_spot - day_open, 1) if day_open > 0 else 0.0
                print(f"\n[UNIVERSAL OPTION RADAR 📡] Spot: {nifty_spot:.2f} | Day Trend: {day_trend_pts:+.1f} pts | Regime: {regime} | Scanned: {total_scanned} Options")
                for name, sc, rt, sp, lp, exp in sorted(all_evaluations, key=lambda x: x[1], reverse=True)[:8]:
                    print(f"  • {name:<26} ({exp}): Prob: {sc:5.1f}% | Depth: {rt:4.2f}x | Spread: {sp:.2f} pts | LTP: ₹{lp:.2f}")
                if best_candidate:
                    print(f"  🏆 Top Ranked: {best_candidate['name']} ({best_score:.1f}% vs {self.min_confidence_threshold_pct:.1f}% Gate)")
                    print(f"     Factors: {', '.join(best_reasons)}")

            if best_score < self.min_confidence_threshold_pct or not best_candidate:
                return

            candidate_key = best_candidate["token"]
            candidate_sym = best_candidate["name"]
            ltp = best_candidate["ltp"]
            # Intelligent Dynamic Lot Sizing: Match user's winning trade sizing (1 to 3 lots based on LTP)
            max_alloc = self.margin_cap * 0.55  # Cap single position to max ~55% of risk margin (~₹12,200)
            lots = max(1, min(3, int(max_alloc / max(ltp * self.lot_size, 1.0))))
            qty = lots * self.lot_size
            req_capital = round(ltp * qty, 2)

            if req_capital > self.margin_cap:
                print(f"[CAPITAL REJECT] Capital ₹{req_capital} > Margin Cap ₹{self.margin_cap}")
                return

            p_adv, pts_needed, ded = calculate_p_adverse(ltp, qty, self.p_adverse_floor)

            print(f"\n================================================================================")
            print(f" [INTELLIGENCE-VALIDATED OPPORTUNITY CONFIRMED ✅] {candidate_sym}")
            print(f"================================================================================")
            print(f"• Verified Probability: {best_score:.1f}% (Passes Strict {self.min_confidence_threshold_pct}% Hurdle)")
            print(f"• Macro Day Trend: {nifty_spot - day_open:+.1f} pts | Regime: {regime} (Slope: {fast_slope:+.1f} pts)")
            print(f"• Factors: {', '.join(best_reasons)}")
            print(f"• Nifty Spot: {nifty_spot:.2f} | LTP: ₹{ltp:.2f} | Spread: {best_spread:.2f} pts | Depth: {best_ratio}x")
            print(f"• Action: BUY {qty} Qty (1 Lot) | Required Margin: ₹{req_capital:,.2f}")
            print(f"• Protected Cushion: ₹{self.cushion:,.2f} ({int(self.cushion_reserve_pct*100)}% Untouchable)")
            print(f"• Invariant Adverse Floor (P_adverse): ₹{p_adv:.2f} (+{pts_needed:.2f} pts net guarantee)")

            # -------------------------------------------------------------
            # PASSIVE MAKER LIMIT EXECUTION (BID-ASK POINTS DOWN METHOD)
            # -------------------------------------------------------------
            q = best_candidate.get("quote", {})
            depth_dict = q.get("depth", {})
            buys = depth_dict.get("buy", [])
            sells = depth_dict.get("sell", [])
            top_bid = buys[0].get("price", best_candidate.get("bid", ltp)) if buys else best_candidate.get("bid", ltp)
            top_ask = sells[0].get("price", best_candidate.get("ask", ltp)) if sells else best_candidate.get("ask", ltp)

            if self.order_mode in ["BID_ASK_PT_DOWN", "LIMIT_BELOW_LTP"]:
                # BID-ASK POINTS DOWN FORMULA:
                # Place limit order strictly 'pt down' from Best Bid (and strictly below LTP)
                # This captures the dip without chasing!
                pt_down = getattr(self, "bid_ask_pt_down", 0.50)
                target_limit_px = round_tick(top_bid - pt_down)

                # Enforce strict below-market invariants
                if target_limit_px >= top_bid:
                    target_limit_px = round_tick(top_bid - 0.05)
                if target_limit_px >= ltp:
                    target_limit_px = round_tick(ltp - 0.10)
                if target_limit_px <= 0:
                    target_limit_px = round_tick(ltp * 0.98)

                discount_pts = round(ltp - target_limit_px, 2)
                bid_discount = round(top_bid - target_limit_px, 2)
                print(f"\n[PASSIVE BID-ASK PT-DOWN LIMIT PLACEMENT 🎯]")
                print(f"• Candidate: {candidate_sym}")
                print(f"• Best Bid: ₹{top_bid:.2f} | Best Ask: ₹{top_ask:.2f} | Live LTP: ₹{ltp:.2f}")
                print(f"• Configured Pt Down: -{pt_down:.2f} pts below Bid")
                print(f"• Maker Limit Order Placed At: ₹{target_limit_px:.2f} (-{bid_discount:.2f} pts below Bid / -{discount_pts:.2f} pts below LTP)")
                print(f"• Spread Shield: Zero taker spread slippage! Resting in bid queue for dip fill (Timeout: {self.limit_fill_timeout_sec}s)...")

                order_res, err = self.place_order(candidate_key, qty, "BUY", price=target_limit_px, order_type="LIMIT")
                if err:
                    print(f"[LIMIT ORDER REJECTED ❌] {err}")
                    return

                order_id = order_res.get("order_id")
                if not order_id:
                    print(f"[ORDER ERROR] No order_id returned: {order_res}")
                    return

                # Passive Fill Wait Loop (Anti-Chase Protection)
                t_start = time.time()
                fill_success = False
                actual_fill_px = target_limit_px

                while time.time() - t_start < self.limit_fill_timeout_sec:
                    time.sleep(0.5)
                    status, avg_px = self.get_order_status(order_id)
                    if status == "complete":
                        fill_success = True
                        actual_fill_px = avg_px if avg_px > 0 else target_limit_px
                        break
                    elif status in ["rejected", "cancelled"]:
                        print(f"[ORDER {status.upper()} ❌] Broker status: {status}")
                        return

                if not fill_success:
                    print(f"[LIMIT TIMEOUT ⏳] Order not filled within {self.limit_fill_timeout_sec}s. Cancelling immediately to avoid chasing!")
                    self.cancel_order(order_id)
                    print(f"[ORDER CANCELLED 🛡️] Capital 100% preserved with ZERO slippage.\n")
                    return

                print(f"[LIMIT FILLED ✅] Filled at ₹{actual_fill_px:.2f} (Spread saved!) | Order ID: {order_id}")
            else:
                order_res, err = self.place_order(candidate_key, qty, "BUY")
                if err:
                    print(f"[ORDER REJECTED ❌] {err}")
                    return
                order_id = order_res.get("order_id", f"AUTO_{int(time.time())}")
                actual_fill_px = ltp

            entry_time = time.time()
            p_adv, pts_needed, ded = calculate_p_adverse(actual_fill_px, qty, self.p_adverse_floor)

            self.state["active_position"] = {
                "order_id": order_id,
                "symbol": candidate_key,
                "strike_name": candidate_sym,
                "qty": qty,
                "lots": 1,
                "entry_price": actual_fill_px,
                "highest_ltp": actual_fill_px,
                "current_ltp": actual_fill_px,
                "p_adverse": p_adv,
                "confidence": best_score,
                "depth_ratio": best_ratio,
                "market_regime": regime,
                "entry_timestamp": entry_time,
                "trailing_stop": round(actual_fill_px - self.micro_cut_pts, 2)
            }
            self.state["engine_state"] = "POSITION_ACTIVE"
            save_state(self.state)
            print(f"[POSITION LOCKED 🔒] Entry: ₹{actual_fill_px:.2f} | P_adverse: ₹{p_adv:.2f} (+{pts_needed:.2f} pts)\n")

        # -------------------------------------------------------------
        # STATE B: POSITION IN FLIGHT -> REAL-TIME RESOLUTION MONITOR
        # -------------------------------------------------------------
        else:
            pos = active_pos
            # Fetch single quote for active position
            p_res, err = api_request(f"/market-quote/quotes?instrument_key={urllib.parse.quote(pos['symbol'])}")
            cq = {}
            if p_res:
                token_num = pos["symbol"].split("|")[-1]
                for k, v in p_res.items():
                    if pos["symbol"] == v.get("instrument_token") or token_num in k:
                        cq = v
                        break

            cur_ltp = cq.get("last_price", pos["current_ltp"])
            pos["current_ltp"] = cur_ltp

            if cur_ltp > pos["highest_ltp"]:
                pos["highest_ltp"] = cur_ltp
                gain = cur_ltp - pos["entry_price"]
                if gain >= self.trailing_start_pts:
                    new_trail = round(cur_ltp - (gain * (1.0 - self.trailing_ratchet_pct)), 2)
                    if new_trail > pos["trailing_stop"]:
                        pos["trailing_stop"] = new_trail
                        print(f"[TRAILING RATCHET 📈] New High: ₹{cur_ltp:.2f} | Ratchet Floor: ₹{new_trail:.2f}")

            elapsed_hold = time.time() - pos["entry_timestamp"]
            gross_pnl = round((cur_ltp - pos["entry_price"]) * pos["qty"], 2)
            exit_needed = False
            resolution_type = ""

            if time.time() - self.last_log_time >= 15:
                self.last_log_time = time.time()
                pts_diff = round(cur_ltp - pos["entry_price"], 2)
                print(f"[RUNNING POSITION ✈️] {pos['strike_name']} | Qty: {pos['qty']} | Entry: ₹{pos['entry_price']:.2f} | LTP: ₹{cur_ltp:.2f} ({pts_diff:+.2f} pts) | P&L: ₹{gross_pnl:+,.2f} | Hold: {int(elapsed_hold)}s/{int(self.stall_timeout_sec)}s | Stop: ₹{pos['trailing_stop']:.2f}")

            # Check 1: Invariant Sub-20ms Micro-Cut
            if cur_ltp <= pos["trailing_stop"]:
                exit_needed = True
                resolution_type = "RESOLUTION_1_SUB20MS_SHOCK_CUT"
                print(f"[EXIT TRIGGERED 🚨] Hard cut hit at ₹{cur_ltp:.2f} (Floor: ₹{pos['trailing_stop']:.2f})")

            # Check 2: 180s Time-Decay Stall Defense
            elif elapsed_hold >= self.stall_timeout_sec:
                exit_needed = True
                resolution_type = "RESOLUTION_2_STALL_DEFENSE"
                print(f"[EXIT TRIGGERED 🚨] STALL_DEFENSE: {int(self.stall_timeout_sec)}s elapsed without velocity ignition. Banked at ₹{cur_ltp:.2f}")

            # Check 3: Adverse Floor Net Profit Guarantee
            elif cur_ltp >= pos["p_adverse"] and pos["highest_ltp"] < (pos["entry_price"] + self.trailing_start_pts):
                exit_needed = True
                resolution_type = "RESOLUTION_3_ADVERSE_PROFIT_FLOOR"
                print(f"[EXIT TRIGGERED 🎯] Adverse guarantee floor ₹{pos['p_adverse']:.2f} reached. Net cash locked!")

            if exit_needed:
                exit_res, err = self.place_order(pos["symbol"], pos["qty"], "SELL")
                charges = calculate_statutory_deductions(pos["entry_price"] * pos["qty"], cur_ltp * pos["qty"])
                net_pnl = round(gross_pnl - charges, 2)

                post_mortem_reason = self.perform_post_mortem(pos, cur_ltp, resolution_type, gross_pnl, net_pnl, charges)

                try:
                    conn = sqlite3.connect(DB_FILE)
                    cur = conn.cursor()
                    cur.execute("""
                        INSERT INTO live_jackpot_trades 
                        (timestamp, trade_num, strike, direction, lots, qty, entry_price, exit_price, gross_pnl, charges, net_pnl, resolution_type, running_cash, confidence, depth_ratio, market_regime, post_mortem_reason)
                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
                    """, (
                        datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                        self.state.get("trades_completed_today", 0) + 1,
                        pos["strike_name"],
                        "BUY",
                        pos["lots"],
                        pos["qty"],
                        pos["entry_price"],
                        cur_ltp,
                        gross_pnl,
                        charges,
                        net_pnl,
                        resolution_type,
                        self.state["available_cash"] + net_pnl,
                        pos.get("confidence", 0.0),
                        pos.get("depth_ratio", 0.0),
                        pos.get("market_regime", "UNKNOWN"),
                        post_mortem_reason
                    ))
                    conn.commit()
                    conn.close()
                except Exception as e:
                    print(f"[DB INSERT ERROR] {e}")

                self.state["trades_completed_today"] += 1
                self.state["available_cash"] = round(self.state["available_cash"] + net_pnl, 2)
                self.state["realized_pnl_today"] = round(self.state["realized_pnl_today"] + gross_pnl, 2)
                self.state["statutory_deductions_paid"] = round(self.state["statutory_deductions_paid"] + charges, 2)
                self.state["net_banked_cash_today"] = round(self.state["net_banked_cash_today"] + net_pnl, 2)
                self.state["last_exit_timestamp"] = time.time()
                self.state["last_post_mortem"] = post_mortem_reason
                self.state["active_position"] = None
                self.state["engine_state"] = "READY"

                if net_pnl < 0:
                    self.state["consecutive_losses"] = self.state.get("consecutive_losses", 0) + 1
                    if abs(self.state["net_banked_cash_today"]) >= self.daily_loss_cap:
                        self.state["circuit_breaker_active"] = True
                        self.state["circuit_breaker_reason"] = f"Daily loss cap ₹{self.daily_loss_cap:,.2f} reached"
                        print(f"[CIRCUIT BREAKER ACTIVATED 🛑] Trading halted for today.")
                else:
                    self.state["consecutive_losses"] = 0

                save_state(self.state)
                print(f"[TRADE CLOSED ✅] Gross: ₹{gross_pnl:+,.2f} | Charges: -₹{charges:.2f} | Net Banked: ₹{net_pnl:+,.2f}")
                print(f"[UPDATED CASH] Available: ₹{self.state['available_cash']:,.2f} | Cushion: ₹{self.cushion:,.2f}")
                print(f"[COOL-OFF ENGAGED ⏳] Enforcing {int(self.post_trade_cooloff_sec)}s cool-off pause before next trade.\n")
            else:
                save_state(self.state)

    def start_loop(self):
        print(f"[AUTONOMOUS LOOP ACTIVE] Standing by for high-probability setups...")
        while True:
            try:
                self.run_trading_cycle()
            except Exception as e:
                print(f"[LOOP EXCEPTION] {e}")
            time.sleep(1.0)

if __name__ == "__main__":
    engine = AutonomousJackpotEngine()
    engine.start_loop()
