topsecrettraders commited on
Commit
021ae66
·
verified ·
1 Parent(s): 08b7277

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -69
app.py CHANGED
@@ -48,9 +48,12 @@ INDEX_MAP = {
48
  def get_root_symbol(desc: str, symbol: str) -> str:
49
  """Extracts root symbol (e.g. 'RELIANCE', 'CRUDEOIL') from description."""
50
  try:
 
 
51
  parts = desc.strip().split(' ')
52
  return parts[0].upper()
53
  except:
 
54
  if ':' in symbol:
55
  return symbol.split(':')[1]
56
  return symbol
@@ -75,7 +78,7 @@ def update_master_db():
75
  header=0, on_bad_lines='skip', storage_options=headers
76
  )
77
 
78
- # 2. Download MCX F&O
79
  url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
80
  df_mcx = pd.read_csv(
81
  url_mcx, usecols=[0, 1, 8, 9, 13],
@@ -105,6 +108,7 @@ def update_master_db():
105
  if root in INDEX_MAP:
106
  spot_sym = INDEX_MAP[root]
107
  elif sym.startswith("MCX"):
 
108
  spot_sym = "MCX"
109
  else:
110
  spot_sym = f"NSE:{root}-EQ"
@@ -124,7 +128,9 @@ def update_master_db():
124
  parts = desc.strip().split(' ')
125
  if len(parts) >= 2:
126
  try:
 
127
  val = parts[-2]
 
128
  val_clean = val.replace('.', '', 1)
129
  if val_clean.isdigit():
130
  strike = float(val)
@@ -139,12 +145,14 @@ def update_master_db():
139
  # Final Sort
140
  final_db = {}
141
  for root, data in temp_db.items():
 
142
  sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
143
  final_db[root] = { "spot": data["spot"], "items": sorted_items }
144
 
145
  MASTER_DB = final_db
146
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
147
 
 
148
  with open(CACHE_FILE, "w") as f:
149
  json.dump(final_db, f)
150
 
@@ -163,6 +171,7 @@ def startup_event():
163
  global MASTER_DB, SEARCH_INDEX
164
  if os.path.exists(CACHE_FILE):
165
  try:
 
166
  with open(CACHE_FILE, "r") as f:
167
  MASTER_DB = json.load(f)
168
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
@@ -197,6 +206,7 @@ def home(access_token: Optional[str] = Cookie(None)):
197
  </div>
198
  """)
199
 
 
200
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
201
 
202
  @app.get("/callback")
@@ -226,7 +236,7 @@ def logout():
226
 
227
  @app.get("/favicon.ico")
228
  def favicon():
229
- return Response(status_code=204)
230
 
231
  # --- API ---
232
 
@@ -234,6 +244,7 @@ def favicon():
234
  def get_status():
235
  return JSONResponse(SYSTEM_STATUS)
236
 
 
237
  @app.get("/api/init")
238
  def legacy_init():
239
  return JSONResponse([])
@@ -244,7 +255,9 @@ def search_symbol(q: str = Query(..., min_length=1)):
244
  return JSONResponse([])
245
 
246
  query = q.upper()
 
247
  results = [x for x in SEARCH_INDEX if x.startswith(query)]
 
248
  if len(results) < 10:
249
  results += [x for x in SEARCH_INDEX if query in x and x not in results]
250
 
@@ -273,7 +286,7 @@ HTML_TEMPLATE = """
273
  <html lang="en">
274
  <head>
275
  <meta charset="UTF-8">
276
- <title>PRO CHAIN (Full)</title>
277
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
278
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
279
  <style>
@@ -287,42 +300,36 @@ HTML_TEMPLATE = """
287
  * { box-sizing: border-box; }
288
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
289
 
290
- /* HEADER */
291
- .top-nav { padding: 0 15px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; flex-shrink: 0; }
292
- .btn { background: #fff; border: 1px solid var(--border); padding: 8px 14px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; }
293
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
294
 
295
- .search-box { position: relative; width: 250px; margin-right: 15px; }
296
- .search-input { width: 100%; padding: 10px 12px; border: 2px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; }
297
  .search-input:focus { border-color: var(--accent); }
298
- .search-dropdown { position: absolute; top: 100%; left: 0; width: 100%; background: white; border: 1px solid var(--border); border-top: none; max-height: 250px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 0 0 6px 6px; z-index: 100; }
299
  .search-item { padding: 10px 15px; cursor: pointer; border-bottom: 1px solid #eee; font-size: 13px; font-weight: 600; }
300
  .search-item:hover { background: #f0f7ff; color: var(--accent); }
301
 
302
- /* MAIN GRID */
303
- .grid-layout { display: flex; height: calc(100vh - 60px); width: 100%; }
304
-
305
- /* OPTION CHAIN */
306
- .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; min-width: 0; }
307
- .chain-header { display: grid; grid-template-columns: 1fr 140px 1fr; padding: 10px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 700; text-align: center; color: var(--text-sub); }
308
  .chain-body { flex: 1; overflow-y: auto; background: #fff; }
309
 
310
- .row { display: grid; grid-template-columns: 1fr 140px 1fr; border-bottom: 1px solid #f0f0f0; height: 45px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
311
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
312
 
313
- .mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); }
314
  .visual-bar { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; transition: 0.3s; opacity: 0.8; }
315
- .strike-txt { z-index: 2; font-weight: 800; font-size: 14px; }
316
  .delta-txt { z-index: 2; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.7); padding: 0 4px; border-radius: 3px; }
317
 
318
- .side-col { padding: 0 12px; display: flex; justify-content: space-between; color: #666; font-size: 11px; }
319
 
320
- /* FUTURES PANEL */
321
- .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
322
- .fut-card { padding: 15px; background: #fff; border-bottom: 1px solid var(--border); }
323
  .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
324
 
325
- /* MODAL */
326
  .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; }
327
  .modal-box { background: #fff; width: 400px; border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); padding: 20px; max-height: 80vh; display:flex; flex-direction:column;}
328
  .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; overflow-y: auto; }
@@ -337,22 +344,6 @@ HTML_TEMPLATE = """
337
  @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
338
  .live { animation: blink 1s infinite; background: var(--green) !important; }
339
  .spin-icon { animation: spin 1s linear infinite; }
340
-
341
- /* --- MOBILE TWEAKS --- */
342
- @media (max-width: 768px) {
343
- /* Force vertical stack on mobile to give chain full width */
344
- .grid-layout { flex-direction: column; }
345
-
346
- /* Chain takes 65% height */
347
- .chain-panel { flex: none; height: 65%; border-bottom: 1px solid var(--border); }
348
-
349
- /* Futures takes rest */
350
- .fut-panel { width: 100%; flex: 1; border-left: none; }
351
-
352
- /* Allow Header to scroll horizontally if crowded */
353
- .top-nav { overflow-x: auto; gap: 10px; padding: 0 10px; }
354
- .search-box { width: 180px; flex-shrink: 0; }
355
- }
356
  </style>
357
  </head>
358
  <body>
@@ -373,43 +364,47 @@ HTML_TEMPLATE = """
373
  </div>
374
 
375
  <div class="top-nav">
376
- <div class="search-box">
377
- <input type="text" class="search-input" id="searchInp" placeholder="SEARCH..." autocomplete="off">
378
- <div class="search-dropdown" id="searchRes"></div>
379
- </div>
 
380
 
381
- <div style="display:flex; align-items:center; gap:10px;">
382
  <button class="btn" onclick="refreshDB()" id="refreshBtn">
383
  <svg id="refIcon" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
 
384
  </button>
385
-
 
 
386
  <div style="text-align:right;">
387
- <div style="font-size:9px; font-weight:700; color:#888;" id="spotLabel">SPOT</div>
388
- <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:14px;">0.00</div>
389
  </div>
390
-
391
  <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
392
- <span id="selExpTxt">EXP</span>
393
  </button>
394
-
395
  <button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
396
- <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%; flex-shrink:0;"></div>
397
  </div>
398
  </div>
399
 
400
  <div class="grid-layout">
401
  <div class="chain-panel">
402
  <div class="chain-header">
403
- <div>CALL OI</div>
404
- <div>NET (P-C)</div>
405
- <div>PUT OI</div>
406
  </div>
407
  <div class="chain-body" id="chainBody">
408
- <div style="padding:40px; text-align:center; color:#999; font-weight:600;">USE SEARCH TO LOAD</div>
409
  </div>
410
  </div>
411
 
412
- <div class="fut-panel" id="futBody"></div>
 
 
 
413
  </div>
414
 
415
  <script>
@@ -425,7 +420,6 @@ HTML_TEMPLATE = """
425
  let POLLER = null;
426
  let IS_POLLING = false;
427
  let SEARCH_TIMER = null;
428
- let SCROLLED_ONCE = false;
429
 
430
  window.onload = () => {
431
  if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
@@ -456,7 +450,7 @@ HTML_TEMPLATE = """
456
  }
457
 
458
  async function refreshDB() {
459
- if(!confirm("Force Download Master Data?")) return;
460
  document.getElementById('loader').style.display = 'flex';
461
  await fetch('/api/refresh');
462
  checkStatus();
@@ -480,7 +474,6 @@ HTML_TEMPLATE = """
480
  document.getElementById('searchRes').style.display = 'none';
481
  document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
482
  document.getElementById('loader').style.display = 'flex';
483
- SCROLLED_ONCE = false; // Reset scroll flag
484
 
485
  try {
486
  const res = await fetch(`/api/chain?symbol=${root}`);
@@ -491,10 +484,11 @@ HTML_TEMPLATE = """
491
  CHAIN_DATA = data.items;
492
  SPOT_SYM = data.spot;
493
 
 
494
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
495
 
496
  if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
497
- SPOT_SYM = FUT_SYMS[0].s;
498
  }
499
 
500
  document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ')) ? "SPOT" : "FUT REF";
@@ -530,7 +524,6 @@ HTML_TEMPLATE = """
530
 
531
  function selectExp(ts) {
532
  ACTIVE_EXP = ts;
533
- SCROLLED_ONCE = false;
534
  updateExpText(ts);
535
  closeModal();
536
  buildChain();
@@ -562,6 +555,7 @@ HTML_TEMPLATE = """
562
  </div>
563
  `).join('');
564
 
 
565
  restartPoller();
566
  }
567
 
@@ -580,24 +574,17 @@ HTML_TEMPLATE = """
580
 
581
  if(SPOT_PRICE > 0) {
582
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
583
-
584
- // Find closest strike
585
  let closestK = kList[0], minDiff = 999999;
586
  kList.forEach(k => {
587
  const diff = Math.abs(SPOT_PRICE - k);
588
  if(diff < minDiff) { minDiff = diff; closestK = k; }
589
  });
590
 
591
- // Highlight ATM
592
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
593
  const atmRow = document.getElementById(`row-${closestK}`);
594
  if(atmRow) {
595
  atmRow.classList.add('atm');
596
- // AUTO SCROLL TO ATM (Center of Screen) - Runs once per selection
597
- if(!SCROLLED_ONCE) {
598
- atmRow.scrollIntoView({block: "center", behavior: "auto"});
599
- SCROLLED_ONCE = true;
600
- }
601
  }
602
 
603
  const centerIdx = kList.indexOf(closestK);
 
48
  def get_root_symbol(desc: str, symbol: str) -> str:
49
  """Extracts root symbol (e.g. 'RELIANCE', 'CRUDEOIL') from description."""
50
  try:
51
+ # MCX often has description "CRUDEOIL 19 JAN 6000 CE"
52
+ # NSE often has "RELIANCE 24 JAN..."
53
  parts = desc.strip().split(' ')
54
  return parts[0].upper()
55
  except:
56
+ # Fallback to symbol parsing
57
  if ':' in symbol:
58
  return symbol.split(':')[1]
59
  return symbol
 
78
  header=0, on_bad_lines='skip', storage_options=headers
79
  )
80
 
81
+ # 2. Download MCX F&O (Correct URL is MCX_COM.csv)
82
  url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
83
  df_mcx = pd.read_csv(
84
  url_mcx, usecols=[0, 1, 8, 9, 13],
 
108
  if root in INDEX_MAP:
109
  spot_sym = INDEX_MAP[root]
110
  elif sym.startswith("MCX"):
111
+ # MCX has no "Index Spot", usually we track the Future
112
  spot_sym = "MCX"
113
  else:
114
  spot_sym = f"NSE:{root}-EQ"
 
128
  parts = desc.strip().split(' ')
129
  if len(parts) >= 2:
130
  try:
131
+ # Logic: "NIFTY 23 JAN 21500 CE" -> 21500 is 2nd last
132
  val = parts[-2]
133
+ # Remove one decimal point to check isdigit
134
  val_clean = val.replace('.', '', 1)
135
  if val_clean.isdigit():
136
  strike = float(val)
 
145
  # Final Sort
146
  final_db = {}
147
  for root, data in temp_db.items():
148
+ # Sort by Expiry (asc) then Strike (asc)
149
  sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
150
  final_db[root] = { "spot": data["spot"], "items": sorted_items }
151
 
152
  MASTER_DB = final_db
153
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
154
 
155
+ # Save cache
156
  with open(CACHE_FILE, "w") as f:
157
  json.dump(final_db, f)
158
 
 
171
  global MASTER_DB, SEARCH_INDEX
172
  if os.path.exists(CACHE_FILE):
173
  try:
174
+ print(">>> STARTUP: Loading Cache...")
175
  with open(CACHE_FILE, "r") as f:
176
  MASTER_DB = json.load(f)
177
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
 
206
  </div>
207
  """)
208
 
209
+ # Inject token
210
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
211
 
212
  @app.get("/callback")
 
236
 
237
  @app.get("/favicon.ico")
238
  def favicon():
239
+ return Response(status_code=204) # No content, stops 404 errors
240
 
241
  # --- API ---
242
 
 
244
  def get_status():
245
  return JSONResponse(SYSTEM_STATUS)
246
 
247
+ # COMPATIBILITY ROUTE: Returns empty list to stop 404s on old clients
248
  @app.get("/api/init")
249
  def legacy_init():
250
  return JSONResponse([])
 
255
  return JSONResponse([])
256
 
257
  query = q.upper()
258
+ # 1. Starts with
259
  results = [x for x in SEARCH_INDEX if x.startswith(query)]
260
+ # 2. Contains (if few results)
261
  if len(results) < 10:
262
  results += [x for x in SEARCH_INDEX if query in x and x not in results]
263
 
 
286
  <html lang="en">
287
  <head>
288
  <meta charset="UTF-8">
289
+ <title>PRO CHAIN (V2)</title>
290
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
291
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
292
  <style>
 
300
  * { box-sizing: border-box; }
301
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
302
 
303
+ .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; }
304
+ .btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; }
 
305
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
306
 
307
+ .search-box { position: relative; width: 300px; margin-right: 20px; }
308
+ .search-input { width: 100%; padding: 10px 15px; border: 2px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; transition: 0.3s; }
309
  .search-input:focus { border-color: var(--accent); }
310
+ .search-dropdown { position: absolute; top: 100%; left: 0; width: 100%; background: white; border: 1px solid var(--border); border-top: none; max-height: 300px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 0 0 6px 6px; }
311
  .search-item { padding: 10px 15px; cursor: pointer; border-bottom: 1px solid #eee; font-size: 13px; font-weight: 600; }
312
  .search-item:hover { background: #f0f7ff; color: var(--accent); }
313
 
314
+ .grid-layout { display: flex; height: calc(100vh - 60px); }
315
+ .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
316
+ .chain-header { display: grid; grid-template-columns: 1fr 180px 1fr; padding: 10px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 700; text-align: center; color: var(--text-sub); }
 
 
 
317
  .chain-body { flex: 1; overflow-y: auto; background: #fff; }
318
 
319
+ .row { display: grid; grid-template-columns: 1fr 180px 1fr; border-bottom: 1px solid #f0f0f0; height: 40px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
320
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
321
 
322
+ .mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); cursor: crosshair; }
323
  .visual-bar { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; transition: 0.3s; opacity: 0.8; }
324
+ .strike-txt { z-index: 2; font-weight: 800; font-size: 13px; }
325
  .delta-txt { z-index: 2; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.7); padding: 0 4px; border-radius: 3px; }
326
 
327
+ .side-col { padding: 0 15px; display: flex; justify-content: space-between; color: #666; font-size: 11px; }
328
 
329
+ .fut-panel { width: 280px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
330
+ .fut-card { padding: 12px; background: #fff; border-bottom: 1px solid var(--border); }
 
331
  .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
332
 
 
333
  .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; }
334
  .modal-box { background: #fff; width: 400px; border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.2); padding: 20px; max-height: 80vh; display:flex; flex-direction:column;}
335
  .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; overflow-y: auto; }
 
344
  @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
345
  .live { animation: blink 1s infinite; background: var(--green) !important; }
346
  .spin-icon { animation: spin 1s linear infinite; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  </style>
348
  </head>
349
  <body>
 
364
  </div>
365
 
366
  <div class="top-nav">
367
+ <div style="display:flex; align-items:center;">
368
+ <div class="search-box">
369
+ <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g., NIFTY, CRUDE)..." autocomplete="off">
370
+ <div class="search-dropdown" id="searchRes"></div>
371
+ </div>
372
 
 
373
  <button class="btn" onclick="refreshDB()" id="refreshBtn">
374
  <svg id="refIcon" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
375
+ RELOAD DB
376
  </button>
377
+ </div>
378
+
379
+ <div style="display:flex; gap:20px; align-items:center;">
380
  <div style="text-align:right;">
381
+ <div style="font-size:10px; font-weight:700; color:#888;" id="spotLabel">SPOT</div>
382
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:16px;">0.00</div>
383
  </div>
 
384
  <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
385
+ <span style="color:#888">EXP:</span> <span id="selExpTxt">SELECT</span>
386
  </button>
 
387
  <button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
388
+ <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
389
  </div>
390
  </div>
391
 
392
  <div class="grid-layout">
393
  <div class="chain-panel">
394
  <div class="chain-header">
395
+ <div>CALL SELLERS (OI)</div>
396
+ <div>NET FLOW (PUT-CALL)</div>
397
+ <div>PUT SELLERS (OI)</div>
398
  </div>
399
  <div class="chain-body" id="chainBody">
400
+ <div style="padding:40px; text-align:center; color:#999; font-weight:600;">USE SEARCH TO LOAD A SYMBOL</div>
401
  </div>
402
  </div>
403
 
404
+ <div class="fut-panel">
405
+ <div style="padding:15px; font-weight:700; border-bottom:1px solid var(--border); background:#fff;">FUTURES</div>
406
+ <div id="futBody"></div>
407
+ </div>
408
  </div>
409
 
410
  <script>
 
420
  let POLLER = null;
421
  let IS_POLLING = false;
422
  let SEARCH_TIMER = null;
 
423
 
424
  window.onload = () => {
425
  if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
 
450
  }
451
 
452
  async function refreshDB() {
453
+ if(!confirm("Download fresh Master Data? This takes ~10s.")) return;
454
  document.getElementById('loader').style.display = 'flex';
455
  await fetch('/api/refresh');
456
  checkStatus();
 
474
  document.getElementById('searchRes').style.display = 'none';
475
  document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
476
  document.getElementById('loader').style.display = 'flex';
 
477
 
478
  try {
479
  const res = await fetch(`/api/chain?symbol=${root}`);
 
484
  CHAIN_DATA = data.items;
485
  SPOT_SYM = data.spot;
486
 
487
+ // Sort futures: Filter items with type 'FUT'
488
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
489
 
490
  if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
491
+ SPOT_SYM = FUT_SYMS[0].s; // Use Near Future as Spot for MCX
492
  }
493
 
494
  document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ')) ? "SPOT" : "FUT REF";
 
524
 
525
  function selectExp(ts) {
526
  ACTIVE_EXP = ts;
 
527
  updateExpText(ts);
528
  closeModal();
529
  buildChain();
 
555
  </div>
556
  `).join('');
557
 
558
+ window.scrolled = false;
559
  restartPoller();
560
  }
561
 
 
574
 
575
  if(SPOT_PRICE > 0) {
576
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
 
 
577
  let closestK = kList[0], minDiff = 999999;
578
  kList.forEach(k => {
579
  const diff = Math.abs(SPOT_PRICE - k);
580
  if(diff < minDiff) { minDiff = diff; closestK = k; }
581
  });
582
 
 
583
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
584
  const atmRow = document.getElementById(`row-${closestK}`);
585
  if(atmRow) {
586
  atmRow.classList.add('atm');
587
+ if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
 
 
 
 
588
  }
589
 
590
  const centerIdx = kList.indexOf(closestK);