topsecrettraders commited on
Commit
7232a1a
·
verified ·
1 Parent(s): d660577

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +136 -103
app.py CHANGED
@@ -1,5 +1,5 @@
1
  # ==============================================================================
2
- # PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION
3
  # ==============================================================================
4
 
5
  import os
@@ -189,7 +189,7 @@ def update_subscriptions(symbols_list):
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 ROUTES (Using Official SDK)
193
  # ==========================================
194
  def get_fyers_session():
195
  return fyersModel.SessionModel(
@@ -205,10 +205,10 @@ def 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:#0f172a; display:flex; justify-content:center; align-items:center; height:100vh; font-family:sans-serif; color:white; margin:0;">
209
- <div style="background:#1e293b; padding:40px; border-radius:12px; text-align:center; border:1px solid #334155; box-shadow: 0 10px 30px rgba(0,0,0,0.5);">
210
- <h2 style="margin-top:0;">Fyers Authentication Required</h2>
211
- <a href="{auth_url}" style="display:inline-block; background:#3b82f6; color:white; padding:12px 25px; text-decoration:none; border-radius:6px; font-weight:bold; margin-top: 15px;">LOGIN WITH FYERS</a>
212
  </div>
213
  </body>
214
  """)
@@ -267,7 +267,6 @@ async def websocket_endpoint(websocket: WebSocket):
267
 
268
  try:
269
  while True:
270
- # Check for incoming commands without blocking the broadcast loop
271
  try:
272
  data = await asyncio.wait_for(websocket.receive_json(), timeout=0.3)
273
  action = data.get("action")
@@ -285,7 +284,6 @@ async def websocket_endpoint(websocket: WebSocket):
285
  except asyncio.TimeoutError:
286
  pass
287
 
288
- # Push Live State Snapshot to frontend
289
  if SOCKET_RUNNING and active_client_symbols:
290
  payload = {sym: STATE_MEMORY[sym] for sym in active_client_symbols if sym in STATE_MEMORY}
291
  if payload:
@@ -295,7 +293,7 @@ async def websocket_endpoint(websocket: WebSocket):
295
  print("[Client WS] Disconnected")
296
 
297
  # ==========================================
298
- # 6. FRONTEND UI (WORLD'S BEST DARK MODE)
299
  # ==========================================
300
  HTML_TEMPLATE = """
301
  <!DOCTYPE html>
@@ -303,80 +301,87 @@ HTML_TEMPLATE = """
303
  <head>
304
  <meta charset="UTF-8">
305
  <title>Pro Live Options Chain</title>
306
- <meta name="viewport" content="width=1200">
307
  <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">
308
  <style>
309
  :root {
310
- --bg: #0f172a; --panel: #1e293b; --border: #334155;
311
- --text-main: #f8fafc; --text-sub: #94a3b8;
312
- --accent: #3b82f6; --green: #10b981; --red: #ef4444;
313
- --buy-bg: rgba(16, 185, 129, 0.2); --sell-bg: rgba(239, 68, 68, 0.2);
314
- --atm-bg: rgba(59, 130, 246, 0.15);
315
  }
316
  * { box-sizing: border-box; }
317
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
318
 
319
  /* Top Navigation */
320
- .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: 10; }
321
- .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); }
322
- .btn:hover { border-color: var(--accent); color: var(--accent); background: rgba(59, 130, 246, 0.1); }
323
 
324
- .btn-control { border:none; padding: 10px 20px; font-size: 14px; border-radius: 6px; color: white; transition: 0.3s; box-shadow: 0 4px 10px rgba(0,0,0,0.3); }
325
  .btn-control.playing { background: var(--green); }
 
326
  .btn-control.stopped { background: var(--red); }
 
327
 
328
- .search-box { position: relative; width: 300px; margin-right: 20px; }
329
- .search-input { width: 100%; padding: 12px 15px; border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; background: #0f172a; color: white; transition:0.3s;}
330
- .search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); }
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 30px rgba(0,0,0,0.5); }
332
- .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 700; color:var(--text-main); }
333
- .search-item:hover { background: rgba(59, 130, 246, 0.1); color: var(--accent); }
334
 
335
  /* Chain Layout */
336
- .chain-container { flex: 1; display: flex; flex-direction: column; background: var(--bg); position:relative;}
 
337
 
338
- /* 7 Column Layout */
339
- .chain-header { display: grid; grid-template-columns: 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr 1.5fr; background: var(--panel); 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; }
340
- .header-cell { padding: 15px 15px; border-right: 1px solid var(--border); display:flex; justify-content:center; align-items:center; }
341
- .header-cell.strike { background: #0b1120; border-right: 1px solid var(--border); border-left: 1px solid var(--border); color:var(--accent); }
342
-
343
- .chain-body { flex: 1; overflow-y: auto; scroll-behavior: smooth; position:relative; }
344
- .row { display: grid; grid-template-columns: 1.5fr 1.5fr 1fr 1fr 1fr 1.5fr 1.5fr; border-bottom: 1px solid var(--border); height: 46px; align-items: center; font-family: 'JetBrains Mono'; font-size: 13px; transition: background 0.2s;}
345
- .row:hover { background: rgba(255,255,255,0.02); }
 
 
346
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
347
 
348
- .cell { position: relative; height: 100%; display: flex; align-items: center; border-right: 1px solid rgba(255,255,255,0.05); font-weight: 700; padding: 0 15px; }
349
  .cell.call-col { justify-content: flex-end; }
350
  .cell.put-col { justify-content: flex-start; }
351
- .cell.strike { justify-content: center; background: #0b1120; font-size: 15px; font-weight: 800; color: white; border-right: 1px solid var(--border); border-left: 1px solid var(--border); }
352
 
353
- .val-txt { z-index: 2; text-shadow: 0px 1px 3px rgba(0,0,0,0.8); }
354
- .val-buy { color: var(--green); }
355
- .val-sell { color: var(--red); }
356
 
357
  /* Depth Visualizer Bars */
358
- .bar-bg { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; opacity: 0.9; transition: width 0.3s ease; }
359
- .bar-call { right: 0; } /* Gows towards left */
360
- .bar-put { left: 0; } /* Grows towards right */
361
 
362
  /* Modals & Overlays */
363
- .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:var(--bg); display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
364
- .spinner { width: 40px; height: 40px; border: 3px solid var(--border); border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; }
365
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
366
 
367
- .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.7); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(4px); }
368
- .modal-box { background: var(--panel); width: 420px; border-radius: 12px; border: 1px solid var(--border); padding: 25px; display:flex; flex-direction:column; }
369
  .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
370
- .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:#0f172a; color:var(--text-sub); }
371
- .opt-btn:hover { background: rgba(59, 130, 246, 0.1); color: var(--accent); border-color:var(--accent); }
372
- .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
373
 
374
  ::-webkit-scrollbar { width: 6px; height: 6px; }
375
  ::-webkit-scrollbar-track { background: var(--bg); }
376
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
377
- ::-webkit-scrollbar-thumb:hover { background: var(--text-sub); }
378
 
379
- .live-dot { display:inline-block; width:10px; height:10px; background:white; border-radius:50%; margin-right:8px; box-shadow: 0 0 8px white; }
 
 
380
  </style>
381
  </head>
382
  <body>
@@ -388,7 +393,7 @@ HTML_TEMPLATE = """
388
 
389
  <div class="modal" id="expModal">
390
  <div class="modal-box">
391
- <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid var(--border); padding-bottom:15px; color:white;">
392
  <span>SELECT EXPIRY</span>
393
  <span style="cursor:pointer; color:var(--text-sub);" onclick="document.getElementById('expModal').style.display='none'">✕</span>
394
  </div>
@@ -398,7 +403,7 @@ HTML_TEMPLATE = """
398
 
399
  <div class="top-nav">
400
  <div style="display:flex; align-items:center;">
401
- <div style="font-size:20px; font-weight:800; color:white; margin-right:30px; letter-spacing: -1px;">
402
  <span style="color:var(--accent)">PRO</span>CHAIN<span style="font-size:10px; vertical-align:top; color:var(--green)">LIVE</span>
403
  </div>
404
  <div class="search-box">
@@ -414,27 +419,31 @@ HTML_TEMPLATE = """
414
  <div style="display:flex; gap:20px; align-items:center;">
415
  <div style="text-align:right;">
416
  <div style="font-size:11px; font-weight:800; color:var(--text-sub); letter-spacing:1px;" id="spotLabel">SPOT PRICE</div>
417
- <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:19px; color:var(--green);">0.00</div>
418
  </div>
419
  <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
420
- <span style="color:var(--text-sub)">EXP:</span> <span id="selExpTxt" style="color:white">SELECT</span>
421
  </button>
422
- <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0; background:transparent;">LOGOUT</button>
423
  </div>
424
  </div>
425
 
426
  <div class="chain-container">
427
- <div class="chain-header">
428
- <div class="header-cell">CALL BUY QTY</div>
429
- <div class="header-cell">CALL SELL QTY</div>
430
- <div class="header-cell">CALL LTP</div>
431
- <div class="header-cell strike">STRIKE</div>
432
- <div class="header-cell">PUT LTP</div>
433
- <div class="header-cell">PUT SELL QTY</div>
434
- <div class="header-cell">PUT BUY QTY</div>
435
- </div>
436
- <div class="chain-body" id="chainBody">
437
- <div style="padding:40px; text-align:center; color:var(--text-sub); font-weight:600; font-size:14px;">SEARCH & SELECT A SYMBOL TO LOAD LIVE CHAIN</div>
 
 
 
 
438
  </div>
439
  </div>
440
 
@@ -509,35 +518,49 @@ HTML_TEMPLATE = """
509
  async function doSearch(q) {
510
  const d = document.getElementById('searchRes');
511
  if(q.length<2) { d.style.display='none'; return; }
512
- const r = await fetch(`/api/search?q=${q}`); const j = await r.json();
513
- d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')">${i.display} <span style="font-size:10px; color:var(--accent); float:right;">${i.exch}</span></div>`).join('');
514
- d.style.display='block';
 
 
515
  }
516
 
517
  async function loadSymbol(root) {
518
- document.getElementById('searchInp').value = root; document.getElementById('searchRes').style.display='none';
519
- document.getElementById('loaderMsg').innerText = "LOADING CHAIN..."; document.getElementById('loader').style.display='flex';
520
-
521
- const r = await fetch(`/api/chain?symbol=${root}`);
522
- if(!r.ok) { alert("Symbol not found"); document.getElementById('loader').style.display='none'; return;}
523
-
524
- const d = await r.json();
525
- CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot; SPOT_PRICE=0; GLOBAL_MAX_QTY=1;
526
-
527
- document.getElementById('spotLabel').innerText = SPOT_SYM.includes('INDEX') ? "SPOT PRICE" : "FUT PRICE";
528
- document.getElementById('spotPrice').innerText = "0.00";
529
- document.getElementById('spotPrice').style.color = "var(--text-main)";
530
-
531
- const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');
532
- const exps =[...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);
533
- const today = Math.floor(Date.now()/1000)-86400;
534
- ACTIVE_EXP = exps.find(t=>t>today) || exps[0];
535
-
536
- document.getElementById('selExpTxt').innerText = new Date(ACTIVE_EXP*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
537
- 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('');
538
-
539
- buildChain();
540
- document.getElementById('loader').style.display='none';
 
 
 
 
 
 
 
 
 
 
 
 
541
  }
542
 
543
  function selectExp(ts) {
@@ -545,15 +568,19 @@ HTML_TEMPLATE = """
545
  document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
546
  document.getElementById('expModal').style.display='none';
547
  document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active'));
 
 
 
 
548
  buildChain();
549
  }
550
 
551
  function buildChain() {
552
  OPT_SYMS = CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
553
- const ks = [...new Set(OPT_SYMS.map(x=>x.k))].sort((a,b)=>a-b);
554
- if(ks.length===0) { document.getElementById('chainBody').innerHTML='<div style="padding:20px;text-align:center;">NO DATA</div>'; return; }
555
 
556
- // Construct visual mirrored HTML matrix
557
  document.getElementById('chainBody').innerHTML = ks.map(k=>`
558
  <div class="row" id="row-${k}" data-k="${k}">
559
  <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>
@@ -573,7 +600,7 @@ HTML_TEMPLATE = """
573
 
574
  // --- REALTIME RENDER ENGINE ---
575
  function renderLiveData(data) {
576
- if(data[SPOT_SYM] && data[SPOT_SYM].ltp > 0) {
577
  const newSpot = data[SPOT_SYM].ltp;
578
  const spotEl = document.getElementById('spotPrice');
579
  if(newSpot > SPOT_PRICE) spotEl.style.color = "var(--green)";
@@ -610,10 +637,10 @@ HTML_TEMPLATE = """
610
  if(v.pltp) document.getElementById(`pltp-${k}`).innerText = v.pltp.toFixed(2);
611
 
612
  // Update Visual Depth Bars
613
- if(v.cb) document.getElementById(`cb-bar-${k}`).style.width = `${(v.cb / GLOBAL_MAX_QTY) * 100}%`;
614
- if(v.cs) document.getElementById(`cs-bar-${k}`).style.width = `${(v.cs / GLOBAL_MAX_QTY) * 100}%`;
615
- if(v.pb) document.getElementById(`pb-bar-${k}`).style.width = `${(v.pb / GLOBAL_MAX_QTY) * 100}%`;
616
- if(v.ps) document.getElementById(`ps-bar-${k}`).style.width = `${(v.ps / GLOBAL_MAX_QTY) * 100}%`;
617
  }
618
 
619
  // 3. Highlight ATM & Auto-Scroll (Only once per chain load)
@@ -623,9 +650,15 @@ HTML_TEMPLATE = """
623
  const atmK = ks.reduce((prev, curr) => Math.abs(curr - SPOT_PRICE) < Math.abs(prev - SPOT_PRICE) ? curr : prev);
624
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
625
  const atmRow = document.getElementById(`row-${atmK}`);
 
626
  if(atmRow) {
627
  atmRow.classList.add('atm');
628
- if(!window.scrolled) { atmRow.scrollIntoView({block:'center', behavior:'smooth'}); window.scrolled = true; }
 
 
 
 
 
629
  }
630
  }
631
  }
 
1
  # ==============================================================================
2
+ # PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION (LIGHT MODE)
3
  # ==============================================================================
4
 
5
  import os
 
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 ROUTES
193
  # ==========================================
194
  def get_fyers_session():
195
  return fyersModel.SessionModel(
 
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
  """)
 
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")
 
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:
 
293
  print("[Client WS] Disconnected")
294
 
295
  # ==========================================
296
+ # 6. FRONTEND UI (WORLD'S BEST LIGHT MODE)
297
  # ==========================================
298
  HTML_TEMPLATE = """
299
  <!DOCTYPE html>
 
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: 10; 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: 20px; }
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
 
335
  /* Chain Layout */
336
+ .chain-container { flex: 1; display: flex; flex-direction: column; background: var(--panel); position:relative; overflow: hidden; }
337
+ .scroll-wrapper { flex: 1; overflow: auto; position: relative; }
338
 
339
+ /* 7 Column Layout - Horizontal Min Width for responsiveness */
340
+ .chain-table { min-width: 950px; display: flex; flex-direction: column; }
341
+
342
+ .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: 5;}
343
+ .header-cell { padding: 12px 15px; border-right: 1px solid var(--border); display:flex; justify-content:center; align-items:center; }
344
+ .header-cell.strike { background: #e2e8f0; border-right: 1px solid var(--border); border-left: 1px solid var(--border); color:var(--text-main); }
345
+
346
+ .chain-body { display: flex; flex-direction: column; }
347
+ .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;}
348
+ .row:hover { background: #f8fafc; }
349
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
350
 
351
+ .cell { position: relative; height: 100%; display: flex; align-items: center; padding: 0 15px; font-weight: 700; border-right: 1px solid #f1f5f9; }
352
  .cell.call-col { justify-content: flex-end; }
353
  .cell.put-col { justify-content: flex-start; }
354
+ .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); }
355
 
356
+ .val-txt { z-index: 2; }
357
+ .val-buy { color: #15803d; } /* Darker green for text */
358
+ .val-sell { color: #b91c1c; } /* Darker red for text */
359
 
360
  /* Depth Visualizer Bars */
361
+ .bar-bg { position: absolute; top: 5px; bottom: 5px; z-index: 0; border-radius: 4px; transition: width 0.3s ease; opacity: 0.8;}
362
+ .bar-call { right: 5px; } /* Anchor to right, grows left */
363
+ .bar-put { left: 5px; } /* Anchor to left, grows right */
364
 
365
  /* Modals & Overlays */
366
+ .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; }
367
+ .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; }
368
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
369
 
370
+ .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); }
371
+ .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); }
372
  .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
373
+ .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); }
374
+ .opt-btn:hover { background: #f0f7ff; color: var(--accent); border-color:var(--accent); }
375
+ .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2); }
376
 
377
  ::-webkit-scrollbar { width: 6px; height: 6px; }
378
  ::-webkit-scrollbar-track { background: var(--bg); }
379
+ ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
380
+ ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
381
 
382
+ .live-dot { display:inline-block; width:10px; height:10px; background:rgba(255,255,255,0.4); border-radius:50%; margin-right:8px; }
383
+ .playing .live-dot { background: white; box-shadow: 0 0 6px white; animation: pulse 1.5s infinite; }
384
+ @keyframes pulse { 0% { opacity:0.6; } 50% { opacity:1; } 100% { opacity:0.6; } }
385
  </style>
386
  </head>
387
  <body>
 
393
 
394
  <div class="modal" id="expModal">
395
  <div class="modal-box">
396
+ <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);">
397
  <span>SELECT EXPIRY</span>
398
  <span style="cursor:pointer; color:var(--text-sub);" onclick="document.getElementById('expModal').style.display='none'">✕</span>
399
  </div>
 
403
 
404
  <div class="top-nav">
405
  <div style="display:flex; align-items:center;">
406
+ <div style="font-size:20px; font-weight:800; color:var(--text-main); margin-right:30px; letter-spacing: -1px;">
407
  <span style="color:var(--accent)">PRO</span>CHAIN<span style="font-size:10px; vertical-align:top; color:var(--green)">LIVE</span>
408
  </div>
409
  <div class="search-box">
 
419
  <div style="display:flex; gap:20px; align-items:center;">
420
  <div style="text-align:right;">
421
  <div style="font-size:11px; font-weight:800; color:var(--text-sub); letter-spacing:1px;" id="spotLabel">SPOT PRICE</div>
422
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:19px; color:var(--text-main);">0.00</div>
423
  </div>
424
  <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
425
+ <span style="color:var(--text-sub)">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
426
  </button>
427
+ <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0; background:transparent; box-shadow:none;">LOGOUT</button>
428
  </div>
429
  </div>
430
 
431
  <div class="chain-container">
432
+ <div class="scroll-wrapper">
433
+ <div class="chain-table">
434
+ <div class="chain-header">
435
+ <div class="header-cell">CALL BUY QTY</div>
436
+ <div class="header-cell">CALL SELL QTY</div>
437
+ <div class="header-cell">CALL LTP</div>
438
+ <div class="header-cell strike">STRIKE</div>
439
+ <div class="header-cell">PUT LTP</div>
440
+ <div class="header-cell">PUT SELL QTY</div>
441
+ <div class="header-cell">PUT BUY QTY</div>
442
+ </div>
443
+ <div class="chain-body" id="chainBody">
444
+ <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>
445
+ </div>
446
+ </div>
447
  </div>
448
  </div>
449
 
 
518
  async function doSearch(q) {
519
  const d = document.getElementById('searchRes');
520
  if(q.length<2) { d.style.display='none'; return; }
521
+ try {
522
+ const r = await fetch(`/api/search?q=${q}`); const j = await r.json();
523
+ d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')">${i.display} <span style="font-size:10px; color:var(--text-sub); float:right;">${i.exch}</span></div>`).join('');
524
+ d.style.display='block';
525
+ } catch(e){}
526
  }
527
 
528
  async function loadSymbol(root) {
529
+ try {
530
+ document.getElementById('searchInp').value = root;
531
+ document.getElementById('searchRes').style.display='none';
532
+ document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
533
+ document.getElementById('loader').style.display='flex';
534
+
535
+ const r = await fetch(`/api/chain?symbol=${root}`);
536
+ if(!r.ok) throw new Error("Symbol not found on server");
537
+
538
+ const d = await r.json();
539
+ if(d.error || !d.items || d.items.length === 0) throw new Error(d.error || "No options data found for this symbol");
540
+
541
+ CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot || ""; SPOT_PRICE=0; GLOBAL_MAX_QTY=1;
542
+
543
+ document.getElementById('spotLabel').innerText = (SPOT_SYM && SPOT_SYM.includes('INDEX')) ? "SPOT PRICE" : "FUT PRICE";
544
+ document.getElementById('spotPrice').innerText = "0.00";
545
+ document.getElementById('spotPrice').style.color = "var(--text-main)";
546
+
547
+ const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');
548
+ if(opts.length === 0) throw new Error("No Options contracts found for this symbol");
549
+
550
+ const exps =[...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);
551
+ const today = Math.floor(Date.now()/1000)-86400;
552
+ ACTIVE_EXP = exps.find(t=>t>today) || exps[0];
553
+
554
+ document.getElementById('selExpTxt').innerText = new Date(ACTIVE_EXP*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
555
+ 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('');
556
+
557
+ buildChain();
558
+ } catch(e) {
559
+ console.error(e);
560
+ alert("Error: " + e.message);
561
+ } finally {
562
+ document.getElementById('loader').style.display='none';
563
+ }
564
  }
565
 
566
  function selectExp(ts) {
 
568
  document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
569
  document.getElementById('expModal').style.display='none';
570
  document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active'));
571
+
572
+ const b = document.querySelector(`.opt-btn[onclick="selectExp(${ts})"]`);
573
+ if(b) b.classList.add('active');
574
+
575
  buildChain();
576
  }
577
 
578
  function buildChain() {
579
  OPT_SYMS = CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
580
+ const ks =[...new Set(OPT_SYMS.map(x=>x.k))].sort((a,b)=>a-b);
581
+ 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; }
582
 
583
+ // Construct perfectly mirrored layout
584
  document.getElementById('chainBody').innerHTML = ks.map(k=>`
585
  <div class="row" id="row-${k}" data-k="${k}">
586
  <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>
 
600
 
601
  // --- REALTIME RENDER ENGINE ---
602
  function renderLiveData(data) {
603
+ if(SPOT_SYM && data[SPOT_SYM] && data[SPOT_SYM].ltp > 0) {
604
  const newSpot = data[SPOT_SYM].ltp;
605
  const spotEl = document.getElementById('spotPrice');
606
  if(newSpot > SPOT_PRICE) spotEl.style.color = "var(--green)";
 
637
  if(v.pltp) document.getElementById(`pltp-${k}`).innerText = v.pltp.toFixed(2);
638
 
639
  // Update Visual Depth Bars
640
+ if(v.cb) document.getElementById(`cb-bar-${k}`).style.width = `${(v.cb / GLOBAL_MAX_QTY) * 95}%`;
641
+ if(v.cs) document.getElementById(`cs-bar-${k}`).style.width = `${(v.cs / GLOBAL_MAX_QTY) * 95}%`;
642
+ if(v.pb) document.getElementById(`pb-bar-${k}`).style.width = `${(v.pb / GLOBAL_MAX_QTY) * 95}%`;
643
+ if(v.ps) document.getElementById(`ps-bar-${k}`).style.width = `${(v.ps / GLOBAL_MAX_QTY) * 95}%`;
644
  }
645
 
646
  // 3. Highlight ATM & Auto-Scroll (Only once per chain load)
 
650
  const atmK = ks.reduce((prev, curr) => Math.abs(curr - SPOT_PRICE) < Math.abs(prev - SPOT_PRICE) ? curr : prev);
651
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
652
  const atmRow = document.getElementById(`row-${atmK}`);
653
+
654
  if(atmRow) {
655
  atmRow.classList.add('atm');
656
+ if(!window.scrolled) {
657
+ setTimeout(() => {
658
+ atmRow.scrollIntoView({block:'center', behavior:'smooth'});
659
+ }, 50);
660
+ window.scrolled = true;
661
+ }
662
  }
663
  }
664
  }