CoolFace
Apppublic

topindianhacker/OptionsChart

sourceHugging Faceapache-2.0updated 6mo agoView on Hugging Face
0likes
app.py783 linesDownload Raw Back to root
1# ==============================================================================2# PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION (FYERS LIGHT MODE)3# ==============================================================================4 5import os6import time7import json8import threading9import asyncio10from collections import defaultdict11from typing import Optional12 13import pandas as pd14import uvicorn15from fastapi import FastAPI, Cookie, Query, WebSocket, WebSocketDisconnect16from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse17from fastapi.middleware.cors import CORSMiddleware18from fyers_apiv3 import fyersModel19from fyers_apiv3.FyersWebsocket import data_ws20 21# ==========================================22# 1. EXACT CONFIGURATION (DO NOT CHANGE)23# ==========================================24CLIENT_ID    = "FV3BVDFQTO-100" 25SECRET_KEY   = "ZALDI5GCDF"26REDIRECT_URI = "https://topindianhacker-optionschart.hf.space/callback" 27 28CACHE_FILE   = "master_cache.json"29 30app = FastAPI()31app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])32 33# ==========================================34# 2. MASTER DB LOADER35# ==========================================36MASTER_DB = {}37SEARCH_INDEX =[] 38SYSTEM_STATUS = {"ready": False, "message": "Booting..."}39UPDATE_LOCK = threading.Lock()40 41INDEX_MAP = {42    "NIFTY": "NSE:NIFTY50-INDEX", "BANKNIFTY": "NSE:NIFTYBANK-INDEX",43    "FINNIFTY": "NSE:FINNIFTY-INDEX", "MIDCPNIFTY": "NSE:MIDCPNIFTY-INDEX",44    "SENSEX": "BSE:SENSEX-INDEX", "BANKEX": "BSE:BANKEX-INDEX",45    "SENSEX50": "BSE:SENSEX50-INDEX"46}47 48def get_root_symbol(desc: str, symbol: str) -> str:49    try:50        parts = desc.strip().split(' ')51        return parts[0].upper().replace(':', '').replace('-', '')52    except:53        return symbol.split(':')[1] if ':' in symbol else symbol54 55def update_master_db():56    global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX57    if not UPDATE_LOCK.acquire(blocking=False): return58 59    try:60        SYSTEM_STATUS["message"] = "Downloading Master Data..."61        headers = { "User-Agent": "Mozilla/5.0" }62        63        df_nse = pd.read_csv("https://public.fyers.in/sym_details/NSE_FO.csv", usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)64        df_nse['Exch'] = 'NSE'65        df_mcx = pd.read_csv("https://public.fyers.in/sym_details/MCX_COM.csv", usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)66        df_mcx['Exch'] = 'MCX'67        df_bse = pd.read_csv("https://public.fyers.in/sym_details/BSE_FO.csv", usecols=[0, 1, 8, 9, 13], names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip', storage_options=headers)68        df_bse['Exch'] = 'BSE'69 70        df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)71        df['Symbol'] = df['Symbol'].astype(str)72        df['Desc'] = df['Desc'].astype(str)73        74        temp_db = {}75        for _, row in df.iterrows():76            sym, desc, exch = row['Symbol'], row['Desc'], row['Exch']77            root = get_root_symbol(desc, sym)78            79            if root not in temp_db:80                spot_sym = INDEX_MAP.get(root, f"NSE:{root}-EQ")81                if exch == "MCX": spot_sym = "MCX" 82                elif exch == "BSE": spot_sym = f"BSE:{root}"83                temp_db[root] = { "spot": spot_sym, "exch": exch, "items":[] }84 85            try: exp = int(row['Expiry'])86            except: exp = 087            88            strike, opt_type = 0.0, "FUT"89            if "CE" in sym or "PE" in sym:90                parts = desc.strip().split(' ')91                if len(parts) >= 2:92                    try:93                        val_clean = parts[-2].replace('.', '', 1)94                        if val_clean.isdigit(): strike = float(parts[-2])95                        opt_type = parts[-1] 96                    except: pass97            98            temp_db[root]["items"].append({ "s": sym, "e": exp, "k": strike, "t": opt_type })99 100        final_db = {}101        search_list =[]102        for root, data in temp_db.items():103            final_db[root] = { "spot": data["spot"], "exch": data["exch"], "items": sorted(data["items"], key=lambda x: (x['e'], x['k'])) }104            search_list.append({ "root": root, "exch": data["exch"], "display": root })105 106        MASTER_DB = final_db107        SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])108        109        with open(CACHE_FILE, "w") as f:110            json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)111 112        SYSTEM_STATUS.update({"ready": True, "message": "Ready"})113    except Exception as e:114        SYSTEM_STATUS["message"] = f"Error: {str(e)}"115    finally:116        UPDATE_LOCK.release()117 118@app.on_event("startup")119def startup_event():120    global MASTER_DB, SEARCH_INDEX121    if os.path.exists(CACHE_FILE):122        try:123            with open(CACHE_FILE, "r") as f:124                data = json.load(f)125                MASTER_DB, SEARCH_INDEX = data["db"], data["idx"]126            SYSTEM_STATUS.update({"ready": True, "message": "Ready (Cached)"})127        except: pass128    if not SYSTEM_STATUS["ready"]:129        threading.Thread(target=update_master_db).start()130 131# ==========================================132# 3. FYERS WEBSOCKET ENGINE (BACKEND)133# ==========================================134STATE_MEMORY = defaultdict(lambda: {"ltp": 0, "b": 0, "s": 0})135FS = None136SOCKET_RUNNING = False137ACTIVE_SYMBOLS = set()138GLOBAL_TOKEN = None139 140def on_ws_message(msg):141    ticks = msg if isinstance(msg, list) else [msg]142    for t in ticks:143        sym = t.get('symbol')144        if sym:145            if 'ltp' in t: STATE_MEMORY[sym]['ltp'] = t['ltp']146            if 'tot_buy_qty' in t: STATE_MEMORY[sym]['b'] = t['tot_buy_qty']147            if 'tot_sell_qty' in t: STATE_MEMORY[sym]['s'] = t['tot_sell_qty']148 149def on_ws_error(e): print(f"[Fyers WS] Error: {e}")150def on_ws_close(c): 151    global SOCKET_RUNNING152    SOCKET_RUNNING = False153    print("[Fyers WS] Closed")154def on_ws_open():155    global SOCKET_RUNNING, ACTIVE_SYMBOLS156    SOCKET_RUNNING = True157    print("[Fyers WS] Connected Successfully.")158    if ACTIVE_SYMBOLS:159        FS.subscribe(symbols=list(ACTIVE_SYMBOLS), data_type="SymbolUpdate")160 161def start_fyers_ws(token):162    global FS, SOCKET_RUNNING, GLOBAL_TOKEN163    GLOBAL_TOKEN = token164    if FS and SOCKET_RUNNING: return165    166    FS = data_ws.FyersDataSocket(167        access_token=token, log_path="", litemode=False, write_to_file=False, reconnect=True,168        on_connect=on_ws_open, on_close=on_ws_close, on_error=on_ws_error, on_message=on_ws_message169    )170    threading.Thread(target=FS.connect, daemon=True).start()171 172def stop_fyers_ws():173    global FS, SOCKET_RUNNING174    if FS and SOCKET_RUNNING:175        FS.close_connection()176    SOCKET_RUNNING = False177 178def update_subscriptions(symbols_list):179    global ACTIVE_SYMBOLS, FS, SOCKET_RUNNING180    new_set = set(symbols_list)181    to_add = list(new_set - ACTIVE_SYMBOLS)182    to_remove = list(ACTIVE_SYMBOLS - new_set)183    184    ACTIVE_SYMBOLS = new_set185    if SOCKET_RUNNING and FS:186        if to_add:187            for i in range(0, len(to_add), 50): FS.subscribe(symbols=to_add[i:i+50], data_type="SymbolUpdate")188        if to_remove:189            for i in range(0, len(to_remove), 50): FS.unsubscribe(symbols=to_remove[i:i+50], data_type="SymbolUpdate")190 191# ==========================================192# 4. API & AUTH ROUTES193# ==========================================194def get_fyers_session():195    return fyersModel.SessionModel(196        client_id=CLIENT_ID,197        secret_key=SECRET_KEY,198        redirect_uri=REDIRECT_URI,199        response_type="code",200        grant_type="authorization_code"201    )202 203@app.get("/")204def home(access_token: Optional[str] = Cookie(None)):205    if not access_token:206        auth_url = get_fyers_session().generate_authcode()207        return HTMLResponse(f"""208        <body style="background:#f8fafc; display:flex; justify-content:center; align-items:center; height:100vh; font-family:sans-serif; margin:0;">209            <div style="background:#ffffff; padding:40px; border-radius:12px; text-align:center; border:1px solid #e2e8f0; box-shadow: 0 10px 25px rgba(0,0,0,0.05);">210                <h2 style="margin-top:0; color:#0f172a;">Fyers Authentication Required</h2>211                <a href="{auth_url}" style="display:inline-block; background:#2563eb; color:white; padding:12px 25px; text-decoration:none; border-radius:6px; font-weight:bold; margin-top: 15px; transition:0.2s;">LOGIN WITH FYERS</a>212            </div>213        </body>214        """)215    return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))216 217@app.get("/callback")218def callback(auth_code: Optional[str] = None, code: Optional[str] = None):219    final_code = auth_code if auth_code else code220    if not final_code: return JSONResponse({"error": "No Code Provided"})221    222    session = get_fyers_session()223    session.set_token(final_code)224    try:225        response = session.generate_token()226        if response.get("s") == "ok" and "access_token" in response:227            full_token = f"{CLIENT_ID}:{response['access_token']}"228            resp = RedirectResponse(url="/")229            resp.set_cookie(key="access_token", value=full_token, httponly=True)230            return resp231        else:232            return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(response, indent=2)}</pre>")233    except Exception as e:234        return HTMLResponse(f"<h3>Exception Triggered</h3><pre>{str(e)}</pre>")235 236@app.get("/logout")237def logout():238    response = RedirectResponse(url="/")239    response.delete_cookie("access_token")240    stop_fyers_ws()241    return response242 243@app.get("/api/status")244def get_status(): return JSONResponse(SYSTEM_STATUS)245 246@app.get("/api/search")247def search_symbol(q: str = Query(..., min_length=1)):248    if not SYSTEM_STATUS["ready"]: return JSONResponse([])249    query = q.upper()250    results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]251    if len(results) < 10: results +=[x for x in SEARCH_INDEX if query in x['root'] and x not in results]252    return JSONResponse(results[:20])253 254@app.get("/api/chain")255def get_chain(symbol: str):256    root = symbol.upper()257    if root in MASTER_DB: return JSONResponse(MASTER_DB[root])258    return JSONResponse({"error": "Symbol not found"}, status_code=404)259 260# ==========================================261# 5. FASTAPI WEBSOCKET (STREAM TO BROWSER)262# ==========================================263@app.websocket("/ws")264async def websocket_endpoint(websocket: WebSocket):265    await websocket.accept()266    active_client_symbols =[]267    268    try:269        while True:270            try:271                data = await asyncio.wait_for(websocket.receive_json(), timeout=0.3)272                action = data.get("action")273                274                if action == "init":275                    token = data.get("token")276                    if token: start_fyers_ws(token)277                elif action == "subscribe":278                    active_client_symbols = data.get("symbols",[])279                    update_subscriptions(active_client_symbols)280                elif action == "stop":281                    stop_fyers_ws()282                elif action == "start":283                    if GLOBAL_TOKEN: start_fyers_ws(GLOBAL_TOKEN)284            except asyncio.TimeoutError:285                pass 286 287            if SOCKET_RUNNING and active_client_symbols:288                payload = {sym: STATE_MEMORY[sym] for sym in active_client_symbols if sym in STATE_MEMORY}289                if payload:290                    await websocket.send_json({"type": "data", "payload": payload})291                    292    except WebSocketDisconnect:293        print("[Client WS] Disconnected")294 295# ==========================================296# 6. FRONTEND UI (WORLD'S BEST LIGHT MODE)297# ==========================================298HTML_TEMPLATE = """299<!DOCTYPE html>300<html lang="en">301<head>302    <meta charset="UTF-8">303    <title>Pro Live Options Chain</title>304    <meta name="viewport" content="width=device-width, initial-scale=1.0">305    <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;700;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">306    <style>307        :root { 308            --bg: #f8fafc; --panel: #ffffff; --border: #e2e8f0; 309            --text-main: #0f172a; --text-sub: #64748b;310            --accent: #2563eb; --green: #16a34a; --red: #dc2626;311            --buy-bg: #dcfce7; --sell-bg: #fee2e2;312            --atm-bg: #eff6ff; --strike-bg: #f1f5f9;313        }314        * { box-sizing: border-box; }315        body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }316 317        /* Top Navigation */318        .top-nav { padding: 0 20px; background: var(--panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 70px; z-index: 50; flex-shrink:0; box-shadow: 0 2px 4px rgba(0,0,0,0.02);}319        .btn { background: var(--panel); border: 1px solid var(--border); padding: 8px 16px; font-size: 13px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.02); }320        .btn:hover { border-color: var(--accent); color: var(--accent); background: #f0f7ff; }321        322        .btn-control { border:none; padding: 10px 20px; font-size: 14px; border-radius: 6px; color: white; transition: 0.3s; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }323        .btn-control.playing { background: var(--green); }324        .btn-control.playing:hover { background: #15803d; }325        .btn-control.stopped { background: var(--red); }326        .btn-control.stopped:hover { background: #b91c1c; }327        328        .search-box { position: relative; width: 280px; margin-right: 15px; }329        .search-input { width: 100%; padding: 10px 15px; border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; background: #f8fafc; color: var(--text-main); transition:0.3s;}330        .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); background: #fff; }331        .search-dropdown { position: absolute; top: 110%; left: 0; width: 100%; background: var(--panel); border: 1px solid var(--border); max-height: 300px; overflow-y: auto; display: none; border-radius: 6px; z-index: 100; box-shadow: 0 10px 25px rgba(0,0,0,0.1); }332        .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f1f5f9; font-size: 13px; font-weight: 700; color:var(--text-main); }333        .search-item:hover { background: #f0f7ff; color: var(--accent); }334        .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; float:right; background: #94a3b8;}335 336        /* Ref Menu (Spot/Fut Toggle) */337        .ref-wrap { position: relative; cursor: pointer; user-select: none; padding-right:15px;}338        .ref-dropdown { position: absolute; top: 100%; right: 0; width: 180px; background: white; border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); display: none; z-index: 100; }339        .ref-item { padding: 10px; font-size: 11px; font-weight: 700; border-bottom: 1px solid #f1f5f9; transition: 0.2s; color:var(--text-main);}340        .ref-item:hover { background: #f0f7ff; color: var(--accent); }341        .ref-item.active { background: var(--accent); color: white; }342 343        /* Chain Layout */344        .chain-container { flex: 1; display: flex; flex-direction: column; background: var(--panel); position:relative; overflow: hidden; }345        346        /* Stopped Overlay */347        .stopped-overlay { position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(255,255,255,0.6); backdrop-filter: blur(4px); z-index: 20; display: none; flex-direction: column; justify-content: center; align-items: center; text-align:center; }348        349        .scroll-wrapper { flex: 1; overflow: auto; position: relative; }350        351        /* 7 Column Layout - Horizontal Min Width for responsiveness */352        .chain-table { min-width: 950px; display: flex; flex-direction: column; }353        354        .chain-header { display: grid; grid-template-columns: 1.2fr 1.2fr 1fr 120px 1fr 1.2fr 1.2fr; background: var(--strike-bg); border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 1px; position: sticky; top: 0; z-index: 15; box-shadow: 0 2px 5px rgba(0,0,0,0.05);}355        .header-cell { padding: 12px 15px; border-right: 1px solid var(--border); display:flex; justify-content:center; align-items:center; }356        .header-cell.strike { background: #e2e8f0; border-right: 1px solid var(--border); border-left: 1px solid var(--border); color:var(--text-main); }357 358        .chain-body { display: flex; flex-direction: column; }359        .row { display: grid; grid-template-columns: 1.2fr 1.2fr 1fr 120px 1fr 1.2fr 1.2fr; border-bottom: 1px solid var(--border); height: 42px; align-items: center; font-family: 'JetBrains Mono', monospace; font-size: 13px; transition: background 0.1s;}360        .row:hover { background: #f8fafc; }361        .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }362        363        .cell { position: relative; height: 100%; display: flex; align-items: center; padding: 0 15px; font-weight: 700; border-right: 1px solid #f1f5f9; }364        .cell.call-col { justify-content: flex-end; }365        .cell.put-col { justify-content: flex-start; }366        .cell.strike { justify-content: center; background: var(--strike-bg); font-size: 14px; font-weight: 800; color: var(--text-main); border-right: 1px solid var(--border); border-left: 1px solid var(--border); }367        368        .val-txt { z-index: 2; }369        .val-buy { color: #15803d; }  /* Darker green for text */370        .val-sell { color: #b91c1c; } /* Darker red for text */371        372        /* Depth Visualizer Bars */373        .bar-bg { position: absolute; top: 5px; bottom: 5px; z-index: 0; border-radius: 4px; transition: width 0.3s ease; opacity: 0.8;}374        .bar-call { right: 5px; } /* Anchor to right, grows left */375        .bar-put { left: 5px; }   /* Anchor to left, grows right */376 377        /* Modals & Overlays */378        .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:var(--panel); display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }379        .spinner { width: 40px; height: 40px; border: 3px solid #f1f5f9; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; }380        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }381        382        .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.4); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }383        .modal-box { background: var(--panel); width: 420px; border-radius: 12px; border: 1px solid var(--border); padding: 25px; display:flex; flex-direction:column; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); }384        .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }385        .opt-btn { padding: 12px; text-align: center; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight:700; font-size:13px; transition:0.2s; background:#f8fafc; color:var(--text-sub); }386        .opt-btn:hover { background: #f0f7ff; color: var(--accent); border-color:var(--accent); }387        .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2); }388        389        ::-webkit-scrollbar { width: 6px; height: 6px; }390        ::-webkit-scrollbar-track { background: var(--bg); }391        ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }392        ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }393 394        .live-dot { display:inline-block; width:10px; height:10px; background:rgba(255,255,255,0.4); border-radius:50%; margin-right:8px; }395        .playing .live-dot { background: white; box-shadow: 0 0 6px white; animation: pulse 1.5s infinite; }396        @keyframes pulse { 0% { opacity:0.6; } 50% { opacity:1; } 100% { opacity:0.6; } }397    </style>398</head>399<body>400 401<div class="loader-ov" id="loader">402    <div class="spinner"></div>403    <div id="loaderMsg" style="font-weight:800; color:var(--text-main); font-size:14px; letter-spacing:1px;">INITIALIZING...</div>404</div>405 406<div class="modal" id="expModal">407    <div class="modal-box">408        <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid var(--border); padding-bottom:15px; color:var(--text-main);">409            <span>SELECT EXPIRY</span>410            <span style="cursor:pointer; color:var(--text-sub);" onclick="document.getElementById('expModal').style.display='none'">✕</span>411        </div>412        <div class="modal-grid" id="expGrid"></div>413    </div>414</div>415 416<div class="top-nav">417    <div style="display:flex; align-items:center;">418        <div style="font-size:20px; font-weight:800; color:var(--text-main); margin-right:20px; letter-spacing: -1px;">419            <span style="color:var(--accent)">PRO</span>CHAIN<span style="font-size:10px; vertical-align:top; color:var(--green)">LIVE</span>420        </div>421        <div class="search-box">422            <input type="text" class="search-input" id="searchInp" placeholder="Search (e.g. BANKNIFTY)..." autocomplete="off">423            <div class="search-dropdown" id="searchRes"></div>424        </div>425        426        <button class="btn" id="fmtToggleBtn" onclick="toggleFormat()" style="margin-right:15px;">FMT: COMPACT</button>427 428        <button class="btn btn-control playing" id="wsBtn" onclick="toggleConnection()">429            <div class="live-dot" id="wsStatusDot"></div> <span id="wsBtnTxt">LIVE WEBSOCKET</span>430        </button>431    </div>432    433    <div style="display:flex; gap:20px; align-items:center;">434        <div class="ref-wrap" onclick="toggleRefMenu()">435            <div style="text-align:right;">436                <div style="font-size:11px; font-weight:800; color:var(--text-sub); letter-spacing:1px; display:flex; align-items:center; justify-content:flex-end; gap:4px;">437                    <span id="spotLabel">SPOT PRICE</span> <span style="font-size:9px;">▼</span>438                </div>439                <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:19px; color:var(--text-main);">0.00</div>440            </div>441            <div class="ref-dropdown" id="refMenu"></div>442        </div>443 444        <button class="btn" onclick="openExpModal()">445            <span style="color:var(--text-sub)">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>446        </button>447        <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0; background:transparent; box-shadow:none;">LOGOUT</button>448    </div>449</div>450 451<div class="chain-container">452    <div class="stopped-overlay" id="stoppedOverlay">453        <h2 style="color:var(--red); margin-bottom:5px;">CONNECTION STOPPED</h2>454        <p style="color:var(--text-sub); font-weight:600;">Click "START WEBSOCKET" at the top to resume live data and interactions.</p>455    </div>456 457    <div class="scroll-wrapper">458        <div class="chain-table">459            <div class="chain-header">460                <div class="header-cell">CALL BUY QTY</div>461                <div class="header-cell">CALL SELL QTY</div>462                <div class="header-cell">CALL LTP</div>463                <div class="header-cell strike">STRIKE</div>464                <div class="header-cell">PUT LTP</div>465                <div class="header-cell">PUT SELL QTY</div>466                <div class="header-cell">PUT BUY QTY</div>467            </div>468            <div class="chain-body" id="chainBody">469                <div style="padding:60px; text-align:center; color:var(--text-sub); font-weight:600; font-size:14px;">SEARCH & SELECT A SYMBOL TO LOAD LIVE CHAIN</div>470            </div>471        </div>472    </div>473</div>474 475<script>476    const AUTH_TOKEN = "{{USER_TOKEN}}";477    let CURRENT_ROOT="", CHAIN_DATA=null, ACTIVE_EXP=0, SPOT_SYM="", SELECTED_REF_SYM="", SPOT_PRICE=0;478    let FUT_SYMS = [], OPT_SYMS=[];479    let WS = null, IS_RUNNING = true, SEARCH_TIMER=null;480    let GLOBAL_MAX_QTY = 1;481    let FORMAT_MODE = 'compact'; // 'compact' or 'absolute'482    483    window.onload = () => { 484        if(AUTH_TOKEN.indexOf("{{")!==-1){alert("Auth Error"); return;}485        checkStatus();486        document.getElementById('searchInp').addEventListener('input', (e)=>{ clearTimeout(SEARCH_TIMER); SEARCH_TIMER=setTimeout(()=>doSearch(e.target.value),300); });487        document.addEventListener('click', (e)=>{ 488            if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display='none'; 489            if(!e.target.closest('.ref-wrap')) document.getElementById('refMenu').style.display='none';490        });491        connectWebSocket();492    };493 494    // --- NUMBER FORMATTING ---495    function toggleFormat() {496        FORMAT_MODE = FORMAT_MODE === 'compact' ? 'absolute' : 'compact';497        document.getElementById('fmtToggleBtn').innerText = "FMT: " + FORMAT_MODE.toUpperCase();498        // UI will naturally update on the next tick499    }500 501    function fmt(n) {502        if(!n) return '-'; 503        if(FORMAT_MODE === 'absolute') return n.toLocaleString('en-IN');504        let a = Math.abs(n);505        if(a >= 10000000) return (n/10000000).toFixed(2)+'Cr';506        if(a >= 100000) return (n/100000).toFixed(2)+'L';507        if(a >= 1000) return (n/1000).toFixed(1)+'k';508        return n.toLocaleString('en-IN');509    }510 511    // --- WEBSOCKET ENGINE ---512    function connectWebSocket() {513        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';514        WS = new WebSocket(`${protocol}//${window.location.host}/ws`);515        516        WS.onopen = () => {517            console.log("Browser WS Connected.");518            WS.send(JSON.stringify({ action: "init", token: AUTH_TOKEN }));519            if(OPT_SYMS.length > 0) subscribeCurrentChain();520        };521        522        WS.onmessage = (event) => {523            const msg = JSON.parse(event.data);524            if(msg.type === "data" && IS_RUNNING) {525                renderLiveData(msg.payload);526            }527        };528 529        WS.onclose = () => {530            setTimeout(connectWebSocket, 2000); 531        };532    }533 534    function toggleConnection() {535        IS_RUNNING = !IS_RUNNING;536        const btn = document.getElementById('wsBtn');537        const overlay = document.getElementById('stoppedOverlay');538        539        if(IS_RUNNING) {540            btn.classList.remove('stopped');541            btn.classList.add('playing');542            document.getElementById('wsBtnTxt').innerText = "LIVE WEBSOCKET";543            overlay.style.display = 'none';544            if(WS && WS.readyState === WebSocket.OPEN) WS.send(JSON.stringify({ action: "start" }));545        } else {546            btn.classList.add('stopped');547            btn.classList.remove('playing');548            document.getElementById('wsBtnTxt').innerText = "START WEBSOCKET";549            overlay.style.display = 'flex';550            if(WS && WS.readyState === WebSocket.OPEN) WS.send(JSON.stringify({ action: "stop" }));551        }552    }553 554    function subscribeCurrentChain() {555        if(!WS || WS.readyState !== WebSocket.OPEN) return;556        // Make sure we subscribe to Options AND the selected Reference Symbol (Spot/Fut)557        const syms =[SELECTED_REF_SYM, ...OPT_SYMS.map(x=>x.s)].filter(Boolean);558        WS.send(JSON.stringify({ action: "subscribe", symbols: syms }));559    }560 561    // --- DATA FETCHING & UI ---562    async function checkStatus() {563        try {564            const res = await fetch('/api/status'); const s = await res.json();565            if(s.ready) { document.getElementById('loader').style.display='none'; if(!CURRENT_ROOT) loadSymbol('NIFTY'); }566            else { document.getElementById('loaderMsg').innerText = s.message.toUpperCase(); setTimeout(checkStatus, 2000); }567        } catch(e){ setTimeout(checkStatus, 2000); }568    }569 570    async function doSearch(q) {571        if(!IS_RUNNING) return;572        const d = document.getElementById('searchRes');573        if(q.length<2) { d.style.display='none'; return; }574        try {575            const r = await fetch(`/api/search?q=${q}`); const j = await r.json();576            d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')">${i.display} <span class="exch-badge">${i.exch}</span></div>`).join('');577            d.style.display='block';578        } catch(e){}579    }580 581    async function loadSymbol(root) {582        if(!IS_RUNNING) return;583        try {584            document.getElementById('searchInp').value = root; 585            document.getElementById('searchRes').style.display='none';586            document.getElementById('loaderMsg').innerText = "LOADING CHAIN..."; 587            document.getElementById('loader').style.display='flex';588            589            const r = await fetch(`/api/chain?symbol=${root}`); 590            if(!r.ok) throw new Error("Symbol not found on server");591            592            const d = await r.json();593            if(d.error || !d.items || d.items.length === 0) throw new Error(d.error || "No options data found for this symbol");594 595            CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot || ""; SPOT_PRICE=0; GLOBAL_MAX_QTY=1;596            597            FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b)=>a.e-b.e).slice(0,3);598            599            // Dynamic Reference Logic600            if(SPOT_SYM && SPOT_SYM !== "MCX") {601                SELECTED_REF_SYM = SPOT_SYM;602            } else if(FUT_SYMS.length > 0) {603                SELECTED_REF_SYM = FUT_SYMS[0].s;604            } else {605                SELECTED_REF_SYM = "";606            }607 608            buildRefMenu();609            610            const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');611            if(opts.length === 0) throw new Error("No Options contracts found for this symbol");612 613            const exps =[...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);614            const today = Math.floor(Date.now()/1000)-86400;615            ACTIVE_EXP = exps.find(t=>t>today) || exps[0];616            617            document.getElementById('selExpTxt').innerText = new Date(ACTIVE_EXP*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});618            document.getElementById('expGrid').innerHTML = exps.map(ts=>`<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'})}</div>`).join('');619            620            buildChain();621        } catch(e) {622            console.error(e);623            alert("Error: " + e.message);624        } finally {625            document.getElementById('loader').style.display='none';626        }627    }628 629    // --- REFERENCE DROPDOWN LOGIC ---630    function toggleRefMenu() {631        if(!IS_RUNNING) return;632        const m = document.getElementById('refMenu');633        m.style.display = (m.style.display === 'block') ? 'none' : 'block';634    }635 636    function buildRefMenu() {637        const menu = document.getElementById('refMenu');638        let html = '';639        640        if(SPOT_SYM && SPOT_SYM !== "MCX") {641            const isSel = (SELECTED_REF_SYM === SPOT_SYM);642            html += `<div class="ref-item ${isSel?'active':''}" onclick="selectRef('${SPOT_SYM}', 'SPOT PRICE')">SPOT PRICE</div>`;643            if(isSel) document.getElementById('spotLabel').innerText = "SPOT PRICE";644        }645 646        FUT_SYMS.forEach(f => {647            const label = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'}) + " FUT";648            const isSel = (SELECTED_REF_SYM === f.s);649            html += `<div class="ref-item ${isSel?'active':''}" onclick="selectRef('${f.s}', '${label}')">${label}</div>`;650            if(isSel) document.getElementById('spotLabel').innerText = label;651        });652 653        menu.innerHTML = html;654        document.getElementById('spotPrice').innerText = "0.00";655        document.getElementById('spotPrice').style.color = "var(--text-main)";656    }657 658    function selectRef(key, label) {659        if(!IS_RUNNING) return;660        SELECTED_REF_SYM = key;661        document.getElementById('spotLabel').innerText = label;662        buildRefMenu();663        SPOT_PRICE = 0;664        document.getElementById('spotPrice').innerText = "0.00";665        subscribeCurrentChain(); // Re-subscribe to new reference666    }667 668    function openExpModal() {669        if(!IS_RUNNING) return;670        document.getElementById('expModal').style.display='flex';671    }672 673    function selectExp(ts) { 674        if(!IS_RUNNING) return;675        ACTIVE_EXP=ts; GLOBAL_MAX_QTY=1;676        document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});677        document.getElementById('expModal').style.display='none'; 678        document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active')); 679        680        const b = document.querySelector(`.opt-btn[onclick="selectExp(${ts})"]`);681        if(b) b.classList.add('active');682        683        buildChain(); 684    }685    686    function buildChain() {687        OPT_SYMS = CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));688        const ks =[...new Set(OPT_SYMS.map(x=>x.k))].sort((a,b)=>a-b);689        if(ks.length===0) { document.getElementById('chainBody').innerHTML='<div style="padding:40px;text-align:center; color:var(--text-sub);">NO DATA FOR EXPIRY</div>'; return; }690        691        // Construct perfectly mirrored layout692        document.getElementById('chainBody').innerHTML = ks.map(k=>`693            <div class="row" id="row-${k}" data-k="${k}">694                <div class="cell call-col"><div class="bar-bg bar-call" style="background:var(--buy-bg)" id="cb-bar-${k}"></div><span class="val-txt val-buy" id="cb-${k}">-</span></div>695                <div class="cell call-col"><div class="bar-bg bar-call" style="background:var(--sell-bg)" id="cs-bar-${k}"></div><span class="val-txt val-sell" id="cs-${k}">-</span></div>696                <div class="cell call-col"><span class="val-txt" id="cltp-${k}" style="color:var(--text-main)">-</span></div>697                698                <div class="cell strike">${k}</div>699                700                <div class="cell put-col"><span class="val-txt" id="pltp-${k}" style="color:var(--text-main)">-</span></div>701                <div class="cell put-col"><div class="bar-bg bar-put" style="background:var(--sell-bg)" id="ps-bar-${k}"></div><span class="val-txt val-sell" id="ps-${k}">-</span></div>702                <div class="cell put-col"><div class="bar-bg bar-put" style="background:var(--buy-bg)" id="pb-bar-${k}"></div><span class="val-txt val-buy" id="pb-${k}">-</span></div>703            </div>`).join('');704        705        window.scrolled = false;706        subscribeCurrentChain();707    }708 709    // --- REALTIME RENDER ENGINE ---710    function renderLiveData(data) {711        if(!IS_RUNNING) return;712 713        // Update Reference Price714        if(SELECTED_REF_SYM && data[SELECTED_REF_SYM] && data[SELECTED_REF_SYM].ltp > 0) { 715            const newSpot = data[SELECTED_REF_SYM].ltp;716            const spotEl = document.getElementById('spotPrice');717            if(newSpot > SPOT_PRICE) spotEl.style.color = "var(--green)";718            else if(newSpot < SPOT_PRICE) spotEl.style.color = "var(--red)";719            SPOT_PRICE = newSpot; 720            spotEl.innerText = SPOT_PRICE.toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}); 721        }722 723        const rowUpdates = {};724 725        // 1. Process Raw Data726        OPT_SYMS.forEach(opt => {727            const d = data[opt.s];728            if(!d) return;729            if(!rowUpdates[opt.k]) rowUpdates[opt.k] = { cb:0, cs:0, cltp:0, pb:0, ps:0, pltp:0 };730            731            if(opt.t === 'CE') {732                rowUpdates[opt.k].cb = d.b; rowUpdates[opt.k].cs = d.s; rowUpdates[opt.k].cltp = d.ltp;733                GLOBAL_MAX_QTY = Math.max(GLOBAL_MAX_QTY, d.b, d.s);734            } else if(opt.t === 'PE') {735                rowUpdates[opt.k].pb = d.b; rowUpdates[opt.k].ps = d.s; rowUpdates[opt.k].pltp = d.ltp;736                GLOBAL_MAX_QTY = Math.max(GLOBAL_MAX_QTY, d.b, d.s);737            }738        });739 740        // 2. Fast DOM Update741        for(const [k, v] of Object.entries(rowUpdates)) {742            // Update Texts743            if(v.cb) document.getElementById(`cb-${k}`).innerText = fmt(v.cb);744            if(v.cs) document.getElementById(`cs-${k}`).innerText = fmt(v.cs);745            if(v.cltp) document.getElementById(`cltp-${k}`).innerText = v.cltp.toFixed(2);746            if(v.pb) document.getElementById(`pb-${k}`).innerText = fmt(v.pb);747            if(v.ps) document.getElementById(`ps-${k}`).innerText = fmt(v.ps);748            if(v.pltp) document.getElementById(`pltp-${k}`).innerText = v.pltp.toFixed(2);749 750            // Update Visual Depth Bars751            if(v.cb) document.getElementById(`cb-bar-${k}`).style.width = `${(v.cb / GLOBAL_MAX_QTY) * 95}%`;752            if(v.cs) document.getElementById(`cs-bar-${k}`).style.width = `${(v.cs / GLOBAL_MAX_QTY) * 95}%`;753            if(v.pb) document.getElementById(`pb-bar-${k}`).style.width = `${(v.pb / GLOBAL_MAX_QTY) * 95}%`;754            if(v.ps) document.getElementById(`ps-bar-${k}`).style.width = `${(v.ps / GLOBAL_MAX_QTY) * 95}%`;755        }756 757        // 3. Highlight ATM & Auto-Scroll (Only once per chain load)758        if(SPOT_PRICE > 0) {759            const ks = Object.keys(rowUpdates).map(Number).sort((a,b)=>a-b);760            if(ks.length > 0) {761                const atmK = ks.reduce((prev, curr) => Math.abs(curr - SPOT_PRICE) < Math.abs(prev - SPOT_PRICE) ? curr : prev);762                document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));763                const atmRow = document.getElementById(`row-${atmK}`);764                765                if(atmRow) {766                    atmRow.classList.add('atm');767                    if(!window.scrolled) { 768                        setTimeout(() => {769                            atmRow.scrollIntoView({block:'center', behavior:'auto'}); 770                        }, 50);771                        window.scrolled = true; 772                    }773                }774            }775        }776    }777</script>778</body>779</html>780"""781 782if __name__ == "__main__":783    uvicorn.run(app, host="0.0.0.0", port=7860)