File size: 39,107 Bytes
78999cc
7208a94
78999cc
 
 
aaa7d51
78999cc
aaa7d51
78999cc
 
d660577
78999cc
 
 
d660577
aaa7d51
78999cc
d660577
78999cc
802a00d
c4e10d9
d660577
c4e10d9
96f3438
 
ae3bc83
d660577
78999cc
100a1ce
aaa7d51
78999cc
802a00d
c4e10d9
78999cc
c4e10d9
0ed0c4f
78999cc
 
9c26cef
b41de0e
17b61e9
78999cc
 
 
17b61e9
 
 
 
0ed0c4f
17b61e9
78999cc
17b61e9
78999cc
0ed0c4f
aaa7d51
17b61e9
78999cc
c4e10d9
 
a95d9b5
0ed0c4f
 
78999cc
d580d82
78999cc
d580d82
78999cc
d580d82
0ed0c4f
d580d82
c4e10d9
 
9441a5c
0ed0c4f
c4e10d9
78999cc
17b61e9
f83ba98
17b61e9
78999cc
 
61f7711
78999cc
0ed0c4f
61f7711
 
c4e10d9
78999cc
c4e10d9
 
 
0ed0c4f
78999cc
 
b2e6cdd
61f7711
c4e10d9
61f7711
0ed0c4f
 
d660577
17b61e9
78999cc
 
c4e10d9
0ed0c4f
d580d82
0ed0c4f
c4e10d9
d580d82
0ed0c4f
78999cc
c4e10d9
 
 
 
713ff62
aaa7d51
c887ab6
0ed0c4f
c4e10d9
0ed0c4f
 
d580d82
78999cc
 
61f7711
b2e6cdd
78999cc
c4e10d9
 
78999cc
c4e10d9
d660577
78999cc
 
 
 
 
 
d660577
78999cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aaa7d51
78999cc
7232a1a
78999cc
d660577
 
 
 
 
 
 
 
 
aaa7d51
 
c4e10d9
d660577
c4e10d9
7232a1a
 
 
 
c4e10d9
78999cc
c4e10d9
b2e6cdd
aaa7d51
 
 
c4e10d9
b2e6cdd
d660577
 
 
 
 
 
 
 
 
 
 
 
 
 
aaa7d51
 
 
c4e10d9
 
78999cc
c4e10d9
aaa7d51
 
61f7711
aaa7d51
0ed0c4f
 
61f7711
0ed0c4f
17b61e9
7208a94
b2e6cdd
0ed0c4f
 
 
17b61e9
61f7711
b2e6cdd
aaa7d51
78999cc
 
 
 
 
 
 
 
100a1ce
78999cc
 
d660577
78999cc
 
 
 
 
 
 
 
 
 
 
 
 
d660577
78999cc
 
 
 
 
 
 
 
2b57a56
 
7232a1a
c4e10d9
 
aaa7d51
 
 
 
78999cc
7232a1a
78999cc
aaa7d51
9441a5c
7232a1a
 
 
 
 
9441a5c
08b7277
9441a5c
 
78999cc
7208a94
7232a1a
 
d660577
7232a1a
d660577
7232a1a
d660577
7232a1a
78999cc
7208a94
7232a1a
 
 
 
 
7208a94
 
 
 
 
 
 
 
78999cc
 
7232a1a
7208a94
 
 
 
7232a1a
307ecae
7232a1a
 
 
7208a94
7232a1a
 
 
 
 
 
c4e10d9
6915d67
7232a1a
d660577
 
7232a1a
a95d9b5
7232a1a
 
 
a95d9b5
78999cc
7232a1a
 
 
78999cc
 
7232a1a
 
c4e10d9
a95d9b5
7232a1a
 
78999cc
7232a1a
 
 
9e3b445
78999cc
 
7232a1a
 
78999cc
7232a1a
 
 
c4e10d9
aaa7d51
 
 
9441a5c
 
78999cc
9441a5c
 
 
aaa7d51
7232a1a
a95d9b5
78999cc
aaa7d51
9441a5c
aaa7d51
 
 
 
021ae66
7208a94
78999cc
 
021ae66
78999cc
021ae66
 
d660577
7208a94
 
d660577
 
78999cc
021ae66
 
 
7208a94
 
 
 
 
 
 
 
c4e10d9
7208a94
 
7232a1a
78999cc
7232a1a
713ff62
 
 
78999cc
7208a94
 
 
 
 
7232a1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
021ae66
aaa7d51
 
 
c887ab6
7208a94
 
78999cc
d660577
7208a94
78999cc
9441a5c
61f7711
78999cc
61f7711
7208a94
 
 
 
78999cc
c887ab6
aaa7d51
7208a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78999cc
 
 
 
100a1ce
78999cc
 
 
 
 
100a1ce
78999cc
 
 
 
100a1ce
78999cc
1947cab
78999cc
d660577
78999cc
1947cab
 
78999cc
 
 
7208a94
 
d660577
78999cc
d660577
 
7208a94
d660577
61f7711
78999cc
d660577
7208a94
 
d660577
61f7711
 
100a1ce
78999cc
 
7208a94
 
78999cc
ca5eacb
9e3b445
78999cc
9441a5c
 
61f7711
 
 
 
aaa7d51
 
0ed0c4f
7208a94
61f7711
 
7232a1a
 
7208a94
7232a1a
 
aaa7d51
 
0ed0c4f
7208a94
7232a1a
 
 
 
 
 
 
 
 
 
 
 
 
 
7208a94
 
 
 
 
 
 
 
 
 
 
 
7232a1a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78999cc
 
7208a94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78999cc
7208a94
d660577
78999cc
 
 
7232a1a
 
 
 
78999cc
0ed0c4f
 
9441a5c
78999cc
7232a1a
 
78999cc
7232a1a
61f7711
9441a5c
d660577
 
 
 
78999cc
d660577
 
 
 
61f7711
78999cc
 
 
 
 
 
 
7208a94
 
 
 
 
78999cc
 
 
 
7208a94
9e3b445
 
78999cc
100a1ce
78999cc
 
 
 
 
 
 
 
d660577
78999cc
 
d660577
78999cc
 
 
 
 
 
 
 
 
 
 
 
 
 
7232a1a
 
 
 
61f7711
ca5eacb
d660577
78999cc
 
 
 
 
 
7232a1a
78999cc
 
7232a1a
 
7208a94
7232a1a
 
 
78999cc
 
aaa7d51
 
 
 
 
 
 
c4e10d9
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
# ==============================================================================
# PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION (FYERS LIGHT MODE)
# ==============================================================================

import os
import time
import json
import threading
import asyncio
from collections import defaultdict
from typing import Optional

import pandas as pd
import uvicorn
from fastapi import FastAPI, Cookie, Query, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from fyers_apiv3 import fyersModel
from fyers_apiv3.FyersWebsocket import data_ws

# ==========================================
# 1. EXACT CONFIGURATION (DO NOT CHANGE)
# ==========================================
CLIENT_ID    = "FV3BVDFQTO-100" 
SECRET_KEY   = "ZALDI5GCDF"
REDIRECT_URI = "https://topindianhacker-optionschart.hf.space/callback" 

CACHE_FILE   = "master_cache.json"

app = FastAPI()
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])

# ==========================================
# 2. MASTER DB LOADER
# ==========================================
MASTER_DB = {}
SEARCH_INDEX =[] 
SYSTEM_STATUS = {"ready": False, "message": "Booting..."}
UPDATE_LOCK = threading.Lock()

INDEX_MAP = {
    "NIFTY": "NSE:NIFTY50-INDEX", "BANKNIFTY": "NSE:NIFTYBANK-INDEX",
    "FINNIFTY": "NSE:FINNIFTY-INDEX", "MIDCPNIFTY": "NSE:MIDCPNIFTY-INDEX",
    "SENSEX": "BSE:SENSEX-INDEX", "BANKEX": "BSE:BANKEX-INDEX",
    "SENSEX50": "BSE:SENSEX50-INDEX"
}

def get_root_symbol(desc: str, symbol: str) -> str:
    try:
        parts = desc.strip().split(' ')
        return parts[0].upper().replace(':', '').replace('-', '')
    except:
        return symbol.split(':')[1] if ':' in symbol else symbol

def update_master_db():
    global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
    if not UPDATE_LOCK.acquire(blocking=False): return

    try:
        SYSTEM_STATUS["message"] = "Downloading Master Data..."
        headers = { "User-Agent": "Mozilla/5.0" }
        
        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)
        df_nse['Exch'] = 'NSE'
        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)
        df_mcx['Exch'] = 'MCX'
        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)
        df_bse['Exch'] = 'BSE'

        df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
        df['Symbol'] = df['Symbol'].astype(str)
        df['Desc'] = df['Desc'].astype(str)
        
        temp_db = {}
        for _, row in df.iterrows():
            sym, desc, exch = row['Symbol'], row['Desc'], row['Exch']
            root = get_root_symbol(desc, sym)
            
            if root not in temp_db:
                spot_sym = INDEX_MAP.get(root, f"NSE:{root}-EQ")
                if exch == "MCX": spot_sym = "MCX" 
                elif exch == "BSE": spot_sym = f"BSE:{root}"
                temp_db[root] = { "spot": spot_sym, "exch": exch, "items":[] }

            try: exp = int(row['Expiry'])
            except: exp = 0
            
            strike, opt_type = 0.0, "FUT"
            if "CE" in sym or "PE" in sym:
                parts = desc.strip().split(' ')
                if len(parts) >= 2:
                    try:
                        val_clean = parts[-2].replace('.', '', 1)
                        if val_clean.isdigit(): strike = float(parts[-2])
                        opt_type = parts[-1] 
                    except: pass
            
            temp_db[root]["items"].append({ "s": sym, "e": exp, "k": strike, "t": opt_type })

        final_db = {}
        search_list =[]
        for root, data in temp_db.items():
            final_db[root] = { "spot": data["spot"], "exch": data["exch"], "items": sorted(data["items"], key=lambda x: (x['e'], x['k'])) }
            search_list.append({ "root": root, "exch": data["exch"], "display": root })

        MASTER_DB = final_db
        SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
        
        with open(CACHE_FILE, "w") as f:
            json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)

        SYSTEM_STATUS.update({"ready": True, "message": "Ready"})
    except Exception as e:
        SYSTEM_STATUS["message"] = f"Error: {str(e)}"
    finally:
        UPDATE_LOCK.release()

@app.on_event("startup")
def startup_event():
    global MASTER_DB, SEARCH_INDEX
    if os.path.exists(CACHE_FILE):
        try:
            with open(CACHE_FILE, "r") as f:
                data = json.load(f)
                MASTER_DB, SEARCH_INDEX = data["db"], data["idx"]
            SYSTEM_STATUS.update({"ready": True, "message": "Ready (Cached)"})
        except: pass
    if not SYSTEM_STATUS["ready"]:
        threading.Thread(target=update_master_db).start()

# ==========================================
# 3. FYERS WEBSOCKET ENGINE (BACKEND)
# ==========================================
STATE_MEMORY = defaultdict(lambda: {"ltp": 0, "b": 0, "s": 0})
FS = None
SOCKET_RUNNING = False
ACTIVE_SYMBOLS = set()
GLOBAL_TOKEN = None

def on_ws_message(msg):
    ticks = msg if isinstance(msg, list) else [msg]
    for t in ticks:
        sym = t.get('symbol')
        if sym:
            if 'ltp' in t: STATE_MEMORY[sym]['ltp'] = t['ltp']
            if 'tot_buy_qty' in t: STATE_MEMORY[sym]['b'] = t['tot_buy_qty']
            if 'tot_sell_qty' in t: STATE_MEMORY[sym]['s'] = t['tot_sell_qty']

def on_ws_error(e): print(f"[Fyers WS] Error: {e}")
def on_ws_close(c): 
    global SOCKET_RUNNING
    SOCKET_RUNNING = False
    print("[Fyers WS] Closed")
def on_ws_open():
    global SOCKET_RUNNING, ACTIVE_SYMBOLS
    SOCKET_RUNNING = True
    print("[Fyers WS] Connected Successfully.")
    if ACTIVE_SYMBOLS:
        FS.subscribe(symbols=list(ACTIVE_SYMBOLS), data_type="SymbolUpdate")

def start_fyers_ws(token):
    global FS, SOCKET_RUNNING, GLOBAL_TOKEN
    GLOBAL_TOKEN = token
    if FS and SOCKET_RUNNING: return
    
    FS = data_ws.FyersDataSocket(
        access_token=token, log_path="", litemode=False, write_to_file=False, reconnect=True,
        on_connect=on_ws_open, on_close=on_ws_close, on_error=on_ws_error, on_message=on_ws_message
    )
    threading.Thread(target=FS.connect, daemon=True).start()

def stop_fyers_ws():
    global FS, SOCKET_RUNNING
    if FS and SOCKET_RUNNING:
        FS.close_connection()
    SOCKET_RUNNING = False

def update_subscriptions(symbols_list):
    global ACTIVE_SYMBOLS, FS, SOCKET_RUNNING
    new_set = set(symbols_list)
    to_add = list(new_set - ACTIVE_SYMBOLS)
    to_remove = list(ACTIVE_SYMBOLS - new_set)
    
    ACTIVE_SYMBOLS = new_set
    if SOCKET_RUNNING and FS:
        if to_add:
            for i in range(0, len(to_add), 50): FS.subscribe(symbols=to_add[i:i+50], data_type="SymbolUpdate")
        if to_remove:
            for i in range(0, len(to_remove), 50): FS.unsubscribe(symbols=to_remove[i:i+50], data_type="SymbolUpdate")

# ==========================================
# 4. API & AUTH ROUTES
# ==========================================
def get_fyers_session():
    return fyersModel.SessionModel(
        client_id=CLIENT_ID,
        secret_key=SECRET_KEY,
        redirect_uri=REDIRECT_URI,
        response_type="code",
        grant_type="authorization_code"
    )

@app.get("/")
def home(access_token: Optional[str] = Cookie(None)):
    if not access_token:
        auth_url = get_fyers_session().generate_authcode()
        return HTMLResponse(f"""
        <body style="background:#f8fafc; display:flex; justify-content:center; align-items:center; height:100vh; font-family:sans-serif; margin:0;">
            <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);">
                <h2 style="margin-top:0; color:#0f172a;">Fyers Authentication Required</h2>
                <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>
            </div>
        </body>
        """)
    return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))

@app.get("/callback")
def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
    final_code = auth_code if auth_code else code
    if not final_code: return JSONResponse({"error": "No Code Provided"})
    
    session = get_fyers_session()
    session.set_token(final_code)
    try:
        response = session.generate_token()
        if response.get("s") == "ok" and "access_token" in response:
            full_token = f"{CLIENT_ID}:{response['access_token']}"
            resp = RedirectResponse(url="/")
            resp.set_cookie(key="access_token", value=full_token, httponly=True)
            return resp
        else:
            return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(response, indent=2)}</pre>")
    except Exception as e:
        return HTMLResponse(f"<h3>Exception Triggered</h3><pre>{str(e)}</pre>")

@app.get("/logout")
def logout():
    response = RedirectResponse(url="/")
    response.delete_cookie("access_token")
    stop_fyers_ws()
    return response

@app.get("/api/status")
def get_status(): return JSONResponse(SYSTEM_STATUS)

@app.get("/api/search")
def search_symbol(q: str = Query(..., min_length=1)):
    if not SYSTEM_STATUS["ready"]: return JSONResponse([])
    query = q.upper()
    results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
    if len(results) < 10: results +=[x for x in SEARCH_INDEX if query in x['root'] and x not in results]
    return JSONResponse(results[:20])

@app.get("/api/chain")
def get_chain(symbol: str):
    root = symbol.upper()
    if root in MASTER_DB: return JSONResponse(MASTER_DB[root])
    return JSONResponse({"error": "Symbol not found"}, status_code=404)

# ==========================================
# 5. FASTAPI WEBSOCKET (STREAM TO BROWSER)
# ==========================================
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    active_client_symbols =[]
    
    try:
        while True:
            try:
                data = await asyncio.wait_for(websocket.receive_json(), timeout=0.3)
                action = data.get("action")
                
                if action == "init":
                    token = data.get("token")
                    if token: start_fyers_ws(token)
                elif action == "subscribe":
                    active_client_symbols = data.get("symbols",[])
                    update_subscriptions(active_client_symbols)
                elif action == "stop":
                    stop_fyers_ws()
                elif action == "start":
                    if GLOBAL_TOKEN: start_fyers_ws(GLOBAL_TOKEN)
            except asyncio.TimeoutError:
                pass 

            if SOCKET_RUNNING and active_client_symbols:
                payload = {sym: STATE_MEMORY[sym] for sym in active_client_symbols if sym in STATE_MEMORY}
                if payload:
                    await websocket.send_json({"type": "data", "payload": payload})
                    
    except WebSocketDisconnect:
        print("[Client WS] Disconnected")

# ==========================================
# 6. FRONTEND UI (WORLD'S BEST LIGHT MODE)
# ==========================================
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Pro Live Options Chain</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <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">
    <style>
        :root { 
            --bg: #f8fafc; --panel: #ffffff; --border: #e2e8f0; 
            --text-main: #0f172a; --text-sub: #64748b;
            --accent: #2563eb; --green: #16a34a; --red: #dc2626;
            --buy-bg: #dcfce7; --sell-bg: #fee2e2;
            --atm-bg: #eff6ff; --strike-bg: #f1f5f9;
        }
        * { box-sizing: border-box; }
        body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }

        /* Top Navigation */
        .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);}
        .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); }
        .btn:hover { border-color: var(--accent); color: var(--accent); background: #f0f7ff; }
        
        .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); }
        .btn-control.playing { background: var(--green); }
        .btn-control.playing:hover { background: #15803d; }
        .btn-control.stopped { background: var(--red); }
        .btn-control.stopped:hover { background: #b91c1c; }
        
        .search-box { position: relative; width: 280px; margin-right: 15px; }
        .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;}
        .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); background: #fff; }
        .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); }
        .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f1f5f9; font-size: 13px; font-weight: 700; color:var(--text-main); }
        .search-item:hover { background: #f0f7ff; color: var(--accent); }
        .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; float:right; background: #94a3b8;}

        /* Ref Menu (Spot/Fut Toggle) */
        .ref-wrap { position: relative; cursor: pointer; user-select: none; padding-right:15px;}
        .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; }
        .ref-item { padding: 10px; font-size: 11px; font-weight: 700; border-bottom: 1px solid #f1f5f9; transition: 0.2s; color:var(--text-main);}
        .ref-item:hover { background: #f0f7ff; color: var(--accent); }
        .ref-item.active { background: var(--accent); color: white; }

        /* Chain Layout */
        .chain-container { flex: 1; display: flex; flex-direction: column; background: var(--panel); position:relative; overflow: hidden; }
        
        /* Stopped Overlay */
        .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; }
        
        .scroll-wrapper { flex: 1; overflow: auto; position: relative; }
        
        /* 7 Column Layout - Horizontal Min Width for responsiveness */
        .chain-table { min-width: 950px; display: flex; flex-direction: column; }
        
        .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);}
        .header-cell { padding: 12px 15px; border-right: 1px solid var(--border); display:flex; justify-content:center; align-items:center; }
        .header-cell.strike { background: #e2e8f0; border-right: 1px solid var(--border); border-left: 1px solid var(--border); color:var(--text-main); }

        .chain-body { display: flex; flex-direction: column; }
        .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;}
        .row:hover { background: #f8fafc; }
        .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
        
        .cell { position: relative; height: 100%; display: flex; align-items: center; padding: 0 15px; font-weight: 700; border-right: 1px solid #f1f5f9; }
        .cell.call-col { justify-content: flex-end; }
        .cell.put-col { justify-content: flex-start; }
        .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); }
        
        .val-txt { z-index: 2; }
        .val-buy { color: #15803d; }  /* Darker green for text */
        .val-sell { color: #b91c1c; } /* Darker red for text */
        
        /* Depth Visualizer Bars */
        .bar-bg { position: absolute; top: 5px; bottom: 5px; z-index: 0; border-radius: 4px; transition: width 0.3s ease; opacity: 0.8;}
        .bar-call { right: 5px; } /* Anchor to right, grows left */
        .bar-put { left: 5px; }   /* Anchor to left, grows right */

        /* Modals & Overlays */
        .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; }
        .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; }
        @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        
        .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); }
        .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); }
        .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
        .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); }
        .opt-btn:hover { background: #f0f7ff; color: var(--accent); border-color:var(--accent); }
        .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2); }
        
        ::-webkit-scrollbar { width: 6px; height: 6px; }
        ::-webkit-scrollbar-track { background: var(--bg); }
        ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
        ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }

        .live-dot { display:inline-block; width:10px; height:10px; background:rgba(255,255,255,0.4); border-radius:50%; margin-right:8px; }
        .playing .live-dot { background: white; box-shadow: 0 0 6px white; animation: pulse 1.5s infinite; }
        @keyframes pulse { 0% { opacity:0.6; } 50% { opacity:1; } 100% { opacity:0.6; } }
    </style>
</head>
<body>

<div class="loader-ov" id="loader">
    <div class="spinner"></div>
    <div id="loaderMsg" style="font-weight:800; color:var(--text-main); font-size:14px; letter-spacing:1px;">INITIALIZING...</div>
</div>

<div class="modal" id="expModal">
    <div class="modal-box">
        <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);">
            <span>SELECT EXPIRY</span>
            <span style="cursor:pointer; color:var(--text-sub);" onclick="document.getElementById('expModal').style.display='none'">✕</span>
        </div>
        <div class="modal-grid" id="expGrid"></div>
    </div>
</div>

<div class="top-nav">
    <div style="display:flex; align-items:center;">
        <div style="font-size:20px; font-weight:800; color:var(--text-main); margin-right:20px; letter-spacing: -1px;">
            <span style="color:var(--accent)">PRO</span>CHAIN<span style="font-size:10px; vertical-align:top; color:var(--green)">LIVE</span>
        </div>
        <div class="search-box">
            <input type="text" class="search-input" id="searchInp" placeholder="Search (e.g. BANKNIFTY)..." autocomplete="off">
            <div class="search-dropdown" id="searchRes"></div>
        </div>
        
        <button class="btn" id="fmtToggleBtn" onclick="toggleFormat()" style="margin-right:15px;">FMT: COMPACT</button>

        <button class="btn btn-control playing" id="wsBtn" onclick="toggleConnection()">
            <div class="live-dot" id="wsStatusDot"></div> <span id="wsBtnTxt">LIVE WEBSOCKET</span>
        </button>
    </div>
    
    <div style="display:flex; gap:20px; align-items:center;">
        <div class="ref-wrap" onclick="toggleRefMenu()">
            <div style="text-align:right;">
                <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;">
                    <span id="spotLabel">SPOT PRICE</span> <span style="font-size:9px;">▼</span>
                </div>
                <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:19px; color:var(--text-main);">0.00</div>
            </div>
            <div class="ref-dropdown" id="refMenu"></div>
        </div>

        <button class="btn" onclick="openExpModal()">
            <span style="color:var(--text-sub)">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
        </button>
        <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0; background:transparent; box-shadow:none;">LOGOUT</button>
    </div>
</div>

<div class="chain-container">
    <div class="stopped-overlay" id="stoppedOverlay">
        <h2 style="color:var(--red); margin-bottom:5px;">CONNECTION STOPPED</h2>
        <p style="color:var(--text-sub); font-weight:600;">Click "START WEBSOCKET" at the top to resume live data and interactions.</p>
    </div>

    <div class="scroll-wrapper">
        <div class="chain-table">
            <div class="chain-header">
                <div class="header-cell">CALL BUY QTY</div>
                <div class="header-cell">CALL SELL QTY</div>
                <div class="header-cell">CALL LTP</div>
                <div class="header-cell strike">STRIKE</div>
                <div class="header-cell">PUT LTP</div>
                <div class="header-cell">PUT SELL QTY</div>
                <div class="header-cell">PUT BUY QTY</div>
            </div>
            <div class="chain-body" id="chainBody">
                <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>
            </div>
        </div>
    </div>
</div>

<script>
    const AUTH_TOKEN = "{{USER_TOKEN}}";
    let CURRENT_ROOT="", CHAIN_DATA=null, ACTIVE_EXP=0, SPOT_SYM="", SELECTED_REF_SYM="", SPOT_PRICE=0;
    let FUT_SYMS = [], OPT_SYMS=[];
    let WS = null, IS_RUNNING = true, SEARCH_TIMER=null;
    let GLOBAL_MAX_QTY = 1;
    let FORMAT_MODE = 'compact'; // 'compact' or 'absolute'
    
    window.onload = () => { 
        if(AUTH_TOKEN.indexOf("{{")!==-1){alert("Auth Error"); return;}
        checkStatus();
        document.getElementById('searchInp').addEventListener('input', (e)=>{ clearTimeout(SEARCH_TIMER); SEARCH_TIMER=setTimeout(()=>doSearch(e.target.value),300); });
        document.addEventListener('click', (e)=>{ 
            if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display='none'; 
            if(!e.target.closest('.ref-wrap')) document.getElementById('refMenu').style.display='none';
        });
        connectWebSocket();
    };

    // --- NUMBER FORMATTING ---
    function toggleFormat() {
        FORMAT_MODE = FORMAT_MODE === 'compact' ? 'absolute' : 'compact';
        document.getElementById('fmtToggleBtn').innerText = "FMT: " + FORMAT_MODE.toUpperCase();
        // UI will naturally update on the next tick
    }

    function fmt(n) {
        if(!n) return '-'; 
        if(FORMAT_MODE === 'absolute') return n.toLocaleString('en-IN');
        let a = Math.abs(n);
        if(a >= 10000000) return (n/10000000).toFixed(2)+'Cr';
        if(a >= 100000) return (n/100000).toFixed(2)+'L';
        if(a >= 1000) return (n/1000).toFixed(1)+'k';
        return n.toLocaleString('en-IN');
    }

    // --- WEBSOCKET ENGINE ---
    function connectWebSocket() {
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        WS = new WebSocket(`${protocol}//${window.location.host}/ws`);
        
        WS.onopen = () => {
            console.log("Browser WS Connected.");
            WS.send(JSON.stringify({ action: "init", token: AUTH_TOKEN }));
            if(OPT_SYMS.length > 0) subscribeCurrentChain();
        };
        
        WS.onmessage = (event) => {
            const msg = JSON.parse(event.data);
            if(msg.type === "data" && IS_RUNNING) {
                renderLiveData(msg.payload);
            }
        };

        WS.onclose = () => {
            setTimeout(connectWebSocket, 2000); 
        };
    }

    function toggleConnection() {
        IS_RUNNING = !IS_RUNNING;
        const btn = document.getElementById('wsBtn');
        const overlay = document.getElementById('stoppedOverlay');
        
        if(IS_RUNNING) {
            btn.classList.remove('stopped');
            btn.classList.add('playing');
            document.getElementById('wsBtnTxt').innerText = "LIVE WEBSOCKET";
            overlay.style.display = 'none';
            if(WS && WS.readyState === WebSocket.OPEN) WS.send(JSON.stringify({ action: "start" }));
        } else {
            btn.classList.add('stopped');
            btn.classList.remove('playing');
            document.getElementById('wsBtnTxt').innerText = "START WEBSOCKET";
            overlay.style.display = 'flex';
            if(WS && WS.readyState === WebSocket.OPEN) WS.send(JSON.stringify({ action: "stop" }));
        }
    }

    function subscribeCurrentChain() {
        if(!WS || WS.readyState !== WebSocket.OPEN) return;
        // Make sure we subscribe to Options AND the selected Reference Symbol (Spot/Fut)
        const syms =[SELECTED_REF_SYM, ...OPT_SYMS.map(x=>x.s)].filter(Boolean);
        WS.send(JSON.stringify({ action: "subscribe", symbols: syms }));
    }

    // --- DATA FETCHING & UI ---
    async function checkStatus() {
        try {
            const res = await fetch('/api/status'); const s = await res.json();
            if(s.ready) { document.getElementById('loader').style.display='none'; if(!CURRENT_ROOT) loadSymbol('NIFTY'); }
            else { document.getElementById('loaderMsg').innerText = s.message.toUpperCase(); setTimeout(checkStatus, 2000); }
        } catch(e){ setTimeout(checkStatus, 2000); }
    }

    async function doSearch(q) {
        if(!IS_RUNNING) return;
        const d = document.getElementById('searchRes');
        if(q.length<2) { d.style.display='none'; return; }
        try {
            const r = await fetch(`/api/search?q=${q}`); const j = await r.json();
            d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')">${i.display} <span class="exch-badge">${i.exch}</span></div>`).join('');
            d.style.display='block';
        } catch(e){}
    }

    async function loadSymbol(root) {
        if(!IS_RUNNING) return;
        try {
            document.getElementById('searchInp').value = root; 
            document.getElementById('searchRes').style.display='none';
            document.getElementById('loaderMsg').innerText = "LOADING CHAIN..."; 
            document.getElementById('loader').style.display='flex';
            
            const r = await fetch(`/api/chain?symbol=${root}`); 
            if(!r.ok) throw new Error("Symbol not found on server");
            
            const d = await r.json();
            if(d.error || !d.items || d.items.length === 0) throw new Error(d.error || "No options data found for this symbol");

            CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot || ""; SPOT_PRICE=0; GLOBAL_MAX_QTY=1;
            
            FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b)=>a.e-b.e).slice(0,3);
            
            // Dynamic Reference Logic
            if(SPOT_SYM && SPOT_SYM !== "MCX") {
                SELECTED_REF_SYM = SPOT_SYM;
            } else if(FUT_SYMS.length > 0) {
                SELECTED_REF_SYM = FUT_SYMS[0].s;
            } else {
                SELECTED_REF_SYM = "";
            }

            buildRefMenu();
            
            const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');
            if(opts.length === 0) throw new Error("No Options contracts found for this symbol");

            const exps =[...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);
            const today = Math.floor(Date.now()/1000)-86400;
            ACTIVE_EXP = exps.find(t=>t>today) || exps[0];
            
            document.getElementById('selExpTxt').innerText = new Date(ACTIVE_EXP*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
            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('');
            
            buildChain();
        } catch(e) {
            console.error(e);
            alert("Error: " + e.message);
        } finally {
            document.getElementById('loader').style.display='none';
        }
    }

    // --- REFERENCE DROPDOWN LOGIC ---
    function toggleRefMenu() {
        if(!IS_RUNNING) return;
        const m = document.getElementById('refMenu');
        m.style.display = (m.style.display === 'block') ? 'none' : 'block';
    }

    function buildRefMenu() {
        const menu = document.getElementById('refMenu');
        let html = '';
        
        if(SPOT_SYM && SPOT_SYM !== "MCX") {
            const isSel = (SELECTED_REF_SYM === SPOT_SYM);
            html += `<div class="ref-item ${isSel?'active':''}" onclick="selectRef('${SPOT_SYM}', 'SPOT PRICE')">SPOT PRICE</div>`;
            if(isSel) document.getElementById('spotLabel').innerText = "SPOT PRICE";
        }

        FUT_SYMS.forEach(f => {
            const label = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'}) + " FUT";
            const isSel = (SELECTED_REF_SYM === f.s);
            html += `<div class="ref-item ${isSel?'active':''}" onclick="selectRef('${f.s}', '${label}')">${label}</div>`;
            if(isSel) document.getElementById('spotLabel').innerText = label;
        });

        menu.innerHTML = html;
        document.getElementById('spotPrice').innerText = "0.00";
        document.getElementById('spotPrice').style.color = "var(--text-main)";
    }

    function selectRef(key, label) {
        if(!IS_RUNNING) return;
        SELECTED_REF_SYM = key;
        document.getElementById('spotLabel').innerText = label;
        buildRefMenu();
        SPOT_PRICE = 0;
        document.getElementById('spotPrice').innerText = "0.00";
        subscribeCurrentChain(); // Re-subscribe to new reference
    }

    function openExpModal() {
        if(!IS_RUNNING) return;
        document.getElementById('expModal').style.display='flex';
    }

    function selectExp(ts) { 
        if(!IS_RUNNING) return;
        ACTIVE_EXP=ts; GLOBAL_MAX_QTY=1;
        document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
        document.getElementById('expModal').style.display='none'; 
        document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active')); 
        
        const b = document.querySelector(`.opt-btn[onclick="selectExp(${ts})"]`);
        if(b) b.classList.add('active');
        
        buildChain(); 
    }
    
    function buildChain() {
        OPT_SYMS = CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
        const ks =[...new Set(OPT_SYMS.map(x=>x.k))].sort((a,b)=>a-b);
        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; }
        
        // Construct perfectly mirrored layout
        document.getElementById('chainBody').innerHTML = ks.map(k=>`
            <div class="row" id="row-${k}" data-k="${k}">
                <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>
                <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>
                <div class="cell call-col"><span class="val-txt" id="cltp-${k}" style="color:var(--text-main)">-</span></div>
                
                <div class="cell strike">${k}</div>
                
                <div class="cell put-col"><span class="val-txt" id="pltp-${k}" style="color:var(--text-main)">-</span></div>
                <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>
                <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>
            </div>`).join('');
        
        window.scrolled = false;
        subscribeCurrentChain();
    }

    // --- REALTIME RENDER ENGINE ---
    function renderLiveData(data) {
        if(!IS_RUNNING) return;

        // Update Reference Price
        if(SELECTED_REF_SYM && data[SELECTED_REF_SYM] && data[SELECTED_REF_SYM].ltp > 0) { 
            const newSpot = data[SELECTED_REF_SYM].ltp;
            const spotEl = document.getElementById('spotPrice');
            if(newSpot > SPOT_PRICE) spotEl.style.color = "var(--green)";
            else if(newSpot < SPOT_PRICE) spotEl.style.color = "var(--red)";
            SPOT_PRICE = newSpot; 
            spotEl.innerText = SPOT_PRICE.toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2}); 
        }

        const rowUpdates = {};

        // 1. Process Raw Data
        OPT_SYMS.forEach(opt => {
            const d = data[opt.s];
            if(!d) return;
            if(!rowUpdates[opt.k]) rowUpdates[opt.k] = { cb:0, cs:0, cltp:0, pb:0, ps:0, pltp:0 };
            
            if(opt.t === 'CE') {
                rowUpdates[opt.k].cb = d.b; rowUpdates[opt.k].cs = d.s; rowUpdates[opt.k].cltp = d.ltp;
                GLOBAL_MAX_QTY = Math.max(GLOBAL_MAX_QTY, d.b, d.s);
            } else if(opt.t === 'PE') {
                rowUpdates[opt.k].pb = d.b; rowUpdates[opt.k].ps = d.s; rowUpdates[opt.k].pltp = d.ltp;
                GLOBAL_MAX_QTY = Math.max(GLOBAL_MAX_QTY, d.b, d.s);
            }
        });

        // 2. Fast DOM Update
        for(const [k, v] of Object.entries(rowUpdates)) {
            // Update Texts
            if(v.cb) document.getElementById(`cb-${k}`).innerText = fmt(v.cb);
            if(v.cs) document.getElementById(`cs-${k}`).innerText = fmt(v.cs);
            if(v.cltp) document.getElementById(`cltp-${k}`).innerText = v.cltp.toFixed(2);
            if(v.pb) document.getElementById(`pb-${k}`).innerText = fmt(v.pb);
            if(v.ps) document.getElementById(`ps-${k}`).innerText = fmt(v.ps);
            if(v.pltp) document.getElementById(`pltp-${k}`).innerText = v.pltp.toFixed(2);

            // Update Visual Depth Bars
            if(v.cb) document.getElementById(`cb-bar-${k}`).style.width = `${(v.cb / GLOBAL_MAX_QTY) * 95}%`;
            if(v.cs) document.getElementById(`cs-bar-${k}`).style.width = `${(v.cs / GLOBAL_MAX_QTY) * 95}%`;
            if(v.pb) document.getElementById(`pb-bar-${k}`).style.width = `${(v.pb / GLOBAL_MAX_QTY) * 95}%`;
            if(v.ps) document.getElementById(`ps-bar-${k}`).style.width = `${(v.ps / GLOBAL_MAX_QTY) * 95}%`;
        }

        // 3. Highlight ATM & Auto-Scroll (Only once per chain load)
        if(SPOT_PRICE > 0) {
            const ks = Object.keys(rowUpdates).map(Number).sort((a,b)=>a-b);
            if(ks.length > 0) {
                const atmK = ks.reduce((prev, curr) => Math.abs(curr - SPOT_PRICE) < Math.abs(prev - SPOT_PRICE) ? curr : prev);
                document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
                const atmRow = document.getElementById(`row-${atmK}`);
                
                if(atmRow) {
                    atmRow.classList.add('atm');
                    if(!window.scrolled) { 
                        setTimeout(() => {
                            atmRow.scrollIntoView({block:'center', behavior:'auto'}); 
                        }, 50);
                        window.scrolled = true; 
                    }
                }
            }
        }
    }
</script>
</body>
</html>
"""

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)