topindianhacker/OptionsChart
0
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)