topsecrettraders commited on
Commit
307ecae
·
verified ·
1 Parent(s): b2e6cdd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +136 -119
app.py CHANGED
@@ -48,12 +48,9 @@ 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
- # 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,7 +75,7 @@ def update_master_db():
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,7 +105,6 @@ def update_master_db():
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"
@@ -123,14 +119,11 @@ def update_master_db():
123
  strike = 0.0
124
  opt_type = "FUT"
125
 
126
- # Parse Strike & Option Type
127
  if "CE" in sym or "PE" in sym:
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,14 +138,12 @@ def update_master_db():
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
 
@@ -205,8 +196,6 @@ def home(access_token: Optional[str] = Cookie(None)):
205
  </div>
206
  </div>
207
  """)
208
-
209
- # Inject token
210
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
211
 
212
  @app.get("/callback")
@@ -236,7 +225,7 @@ def logout():
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,7 +233,6 @@ def favicon():
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([])
@@ -253,14 +241,10 @@ def legacy_init():
253
  def search_symbol(q: str = Query(..., min_length=1)):
254
  if not SYSTEM_STATUS["ready"]:
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
-
264
  return JSONResponse(results[:20])
265
 
266
  @app.get("/api/chain")
@@ -278,7 +262,7 @@ def force_refresh(background_tasks: BackgroundTasks):
278
  return JSONResponse({"s": "ok"})
279
 
280
  # ==========================================
281
- # 4. FRONTEND
282
  # ==========================================
283
 
284
  HTML_TEMPLATE = """
@@ -286,8 +270,8 @@ HTML_TEMPLATE = """
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>
293
  :root {
@@ -300,110 +284,130 @@ HTML_TEMPLATE = """
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; }
336
- .exp-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; font-weight:600; font-size:12px; }
337
- .exp-btn:hover { background: #f0f7ff; border-color: var(--accent); }
338
- .exp-btn.active { background: var(--accent); color: white; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
  .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
341
- .spinner { width: 30px; height: 30px; border: 4px solid #f3f3f3; border-top: 4px solid var(--accent); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; }
342
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
343
 
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>
350
 
351
  <div class="loader-ov" id="loader">
352
  <div class="spinner"></div>
353
- <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px;">CONNECTING...</div>
354
  </div>
355
 
356
  <div class="modal" id="expModal">
357
  <div class="modal-box">
358
  <div style="display:flex; justify-content:space-between; font-weight:700; border-bottom:1px solid #eee; padding-bottom:10px;">
359
- <span id="expTitle">SELECT EXPIRY</span>
360
- <span style="cursor:pointer" onclick="closeModal()">✕</span>
361
  </div>
362
  <div class="modal-grid" id="expGrid"></div>
363
  </div>
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
 
@@ -450,7 +454,7 @@ HTML_TEMPLATE = """
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();
@@ -472,7 +476,7 @@ HTML_TEMPLATE = """
472
  async function loadSymbol(root) {
473
  document.getElementById('searchInp').value = root;
474
  document.getElementById('searchRes').style.display = 'none';
475
- document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
476
  document.getElementById('loader').style.display = 'flex';
477
 
478
  try {
@@ -483,12 +487,13 @@ HTML_TEMPLATE = """
483
  CURRENT_ROOT = 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";
@@ -516,7 +521,7 @@ HTML_TEMPLATE = """
516
  const grid = document.getElementById('expGrid');
517
  grid.innerHTML = validExps.map(ts => {
518
  const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
519
- return `<div class="exp-btn" onclick="selectExp(${ts})">${dStr}</div>`;
520
  }).join('');
521
 
522
  buildChain();
@@ -545,24 +550,23 @@ HTML_TEMPLATE = """
545
 
546
  body.innerHTML = strikes.map(k => `
547
  <div class="row" id="row-${k}" data-k="${k}">
548
- <div class="side-col" id="ce-${k}"><span>-</span><span>-</span></div>
549
  <div class="mid-col">
550
  <div class="visual-bar" id="bar-${k}"></div>
551
  <span class="strike-txt">${k}</span>
552
- <span class="delta-txt" id="val-${k}">-</span>
553
  </div>
554
- <div class="side-col" id="pe-${k}"><span>-</span><span>-</span></div>
555
  </div>
556
  `).join('');
557
 
558
- window.scrolled = false;
559
  restartPoller();
560
  }
561
 
562
  function restartPoller() {
563
  if(POLLER) clearInterval(POLLER);
564
  POLLER = setInterval(pollData, 1500);
565
- pollData();
566
  }
567
 
568
  async function pollData() {
@@ -570,38 +574,46 @@ HTML_TEMPLATE = """
570
  IS_POLLING = true;
571
 
572
  try {
 
573
  let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
 
 
 
 
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);
591
- const start = Math.max(0, centerIdx - 12);
592
- const end = Math.min(kList.length, centerIdx + 12);
593
- const viewKs = kList.slice(start, end);
594
-
595
- OPT_SYMS.forEach(o => {
596
- if(viewKs.includes(o.k)) symbolsToFetch.push(o.s);
597
- });
598
- } else {
599
- const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
600
- const mid = Math.floor(kList.length/2);
601
- const viewKs = kList.slice(Math.max(0, mid-10), mid+10);
602
- OPT_SYMS.forEach(o => { if(viewKs.includes(o.k)) symbolsToFetch.push(o.s); });
603
  }
604
 
 
 
 
 
 
 
 
 
 
605
  const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
606
  if(uniqueSyms.length === 0) return;
607
 
@@ -628,6 +640,7 @@ HTML_TEMPLATE = """
628
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
629
  }
630
 
 
631
  const futHTML = FUT_SYMS.map(f => {
632
  const d = data[f.s];
633
  if(!d) return '';
@@ -636,20 +649,21 @@ HTML_TEMPLATE = """
636
  const bPct = (d.totalbuyqty/tot)*100;
637
  return `
638
  <div class="fut-card">
639
- <div style="display:flex; justify-content:space-between; font-weight:700;">
640
- <span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
641
  </div>
642
  <div class="meter-bg">
643
  <div style="width:${bPct}%; background:var(--green)"></div>
644
  <div style="width:${100-bPct}%; background:var(--red)"></div>
645
  </div>
646
- <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:2px;">
647
- <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
648
  </div>
649
  </div>`;
650
  }).join('');
651
  if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
652
 
 
653
  const rowData = {};
654
  for(const [sym, d] of Object.entries(data)) {
655
  const meta = OPT_SYMS.find(x => x.s === sym);
@@ -667,11 +681,14 @@ HTML_TEMPLATE = """
667
  const delta = r.pe - r.ce;
668
  const ceEl = document.getElementById(`ce-${k}`);
669
  const peEl = document.getElementById(`pe-${k}`);
670
- if(ceEl) ceEl.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
671
- if(peEl) peEl.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
 
 
672
 
673
  const bar = document.getElementById(`bar-${k}`);
674
  const val = document.getElementById(`val-${k}`);
 
675
  if(bar && maxDiff > 0) {
676
  const pct = (Math.abs(delta) / maxDiff) * 100;
677
  if(delta >= 0) {
@@ -696,7 +713,7 @@ HTML_TEMPLATE = """
696
  let abs = Math.abs(n);
697
  if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr';
698
  if(abs>=100000) return (n/100000).toFixed(2)+'L';
699
- if(abs>=1000) return (n/1000).toFixed(1)+'k';
700
  return n;
701
  }
702
  </script>
 
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
  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
  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"
 
119
  strike = 0.0
120
  opt_type = "FUT"
121
 
 
122
  if "CE" in sym or "PE" in sym:
123
  parts = desc.strip().split(' ')
124
  if len(parts) >= 2:
125
  try:
 
126
  val = parts[-2]
 
127
  val_clean = val.replace('.', '', 1)
128
  if val_clean.isdigit():
129
  strike = float(val)
 
138
  # Final Sort
139
  final_db = {}
140
  for root, data in temp_db.items():
 
141
  sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
142
  final_db[root] = { "spot": data["spot"], "items": sorted_items }
143
 
144
  MASTER_DB = final_db
145
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
146
 
 
147
  with open(CACHE_FILE, "w") as f:
148
  json.dump(final_db, f)
149
 
 
196
  </div>
197
  </div>
198
  """)
 
 
199
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
200
 
201
  @app.get("/callback")
 
225
 
226
  @app.get("/favicon.ico")
227
  def favicon():
228
+ return Response(status_code=204)
229
 
230
  # --- API ---
231
 
 
233
  def get_status():
234
  return JSONResponse(SYSTEM_STATUS)
235
 
 
236
  @app.get("/api/init")
237
  def legacy_init():
238
  return JSONResponse([])
 
241
  def search_symbol(q: str = Query(..., min_length=1)):
242
  if not SYSTEM_STATUS["ready"]:
243
  return JSONResponse([])
 
244
  query = q.upper()
 
245
  results = [x for x in SEARCH_INDEX if x.startswith(query)]
 
246
  if len(results) < 10:
247
  results += [x for x in SEARCH_INDEX if query in x and x not in results]
 
248
  return JSONResponse(results[:20])
249
 
250
  @app.get("/api/chain")
 
262
  return JSONResponse({"s": "ok"})
263
 
264
  # ==========================================
265
+ # 4. FRONTEND (MOBILE OPTIMIZED)
266
  # ==========================================
267
 
268
  HTML_TEMPLATE = """
 
270
  <html lang="en">
271
  <head>
272
  <meta charset="UTF-8">
273
+ <title>PRO CHAIN MOBILE</title>
274
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
275
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
276
  <style>
277
  :root {
 
284
  * { box-sizing: border-box; }
285
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
286
 
287
+ /* NAV BAR */
288
+ .top-nav { padding: 0 10px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 55px; z-index: 10; flex-shrink: 0; }
289
+
290
+ .nav-left { display: flex; align-items: center; flex: 1; min-width: 0; gap: 8px; }
291
+ .nav-right { display: flex; gap: 8px; align-items: center; }
292
+
293
+ .btn { background: #fff; border: 1px solid var(--border); padding: 6px 12px; font-size: 11px; border-radius: 4px; cursor: pointer; font-weight: 700; display: flex; gap: 5px; align-items: center; white-space:nowrap; height: 32px; }
294
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
295
+ .btn-red { color: var(--red); border-color: rgba(213,0,0,0.3); }
296
+
297
+ /* SEARCH */
298
+ .search-box { position: relative; flex: 1; max-width: 180px; }
299
+ .search-input { width: 100%; padding: 0 10px; height: 32px; border: 1px solid #ccc; border-radius: 4px; font-family: 'JetBrains Mono'; font-weight: bold; font-size: 13px; outline: none; background: #f9f9f9; }
300
+ .search-input:focus { border-color: var(--accent); background: #fff; }
301
+ .search-dropdown { position: absolute; top: 35px; left: 0; width: 220px; background: white; border: 1px solid var(--border); max-height: 250px; overflow-y: auto; display: none; box-shadow: 0 4px 10px rgba(0,0,0,0.1); border-radius: 4px; z-index: 100; }
302
+ .search-item { padding: 10px; border-bottom: 1px solid #f0f0f0; font-size: 13px; font-weight: 600; }
303
 
304
+ /* LAYOUT */
305
+ .grid-layout { display: flex; flex: 1; overflow: hidden; position: relative; }
 
 
 
 
 
 
 
 
 
306
 
307
+ /* OPTION CHAIN PANEL */
308
+ .chain-panel { flex: 1; display: flex; flex-direction: column; height: 100%; }
309
+ .chain-header { display: grid; grid-template-columns: 1fr 1.3fr 1fr; padding: 8px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 10px; font-weight: 700; text-align: center; color: var(--text-sub); }
310
+ .chain-body { flex: 1; overflow-y: auto; background: #fff; -webkit-overflow-scrolling: touch; }
311
+
312
+ .row { display: grid; grid-template-columns: 1fr 1.3fr 1fr; border-bottom: 1px solid #f0f0f0; height: 42px; align-items: center; font-family: 'JetBrains Mono'; font-size: 11px; }
313
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
314
 
315
+ .mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid #eee; border-right: 1px solid #eee; overflow: hidden; }
316
+ .visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 2px; opacity: 0.6; }
317
+ .strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: #000; }
318
+ .delta-txt { z-index: 2; font-size: 9px; font-weight: 600; background: rgba(255,255,255,0.8); padding: 0 3px; border-radius: 2px; margin-top: -2px; }
319
 
320
+ .side-col { padding: 0 5px; display: flex; flex-direction: column; justify-content: center; align-items: center; color: #444; font-size: 10px; font-weight: 600; text-align: center; line-height: 1.2; }
321
 
322
+ /* FUTURES PANEL */
323
+ .fut-panel { width: 260px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
324
+ .fut-card { padding: 10px; background: #fff; border-bottom: 1px solid var(--border); }
325
+ .meter-bg { height: 5px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 4px; }
326
 
327
+ /* RESPONSIVE (MOBILE) */
328
+ @media (max-width: 768px) {
329
+ .grid-layout { flex-direction: column; }
330
+ /* Futures move to bottom */
331
+ .fut-panel { width: 100%; height: auto; min-height: 80px; max-height: 130px; border-left: none; border-top: 1px solid var(--border); flex-direction: row; overflow-x: auto; order: 2; }
332
+ .fut-card { min-width: 140px; border-right: 1px solid var(--border); border-bottom: none; }
333
+ .chain-panel { order: 1; }
334
+
335
+ /* Hide non-essential buttons on super small screens */
336
+ .hide-mobile { display: none; }
337
+ .search-box { max-width: 120px; }
338
+
339
+ .row, .chain-header { grid-template-columns: 0.8fr 1.2fr 0.8fr; }
340
+ .side-col span { font-size: 10px; }
341
+ }
342
+
343
+ /* MODALS & LOADERS */
344
+ .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 2000; display: none; justify-content: center; align-items: center; }
345
+ .modal-box { background: #fff; width: 90%; max-width: 350px; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.2); padding: 15px; max-height: 70vh; display:flex; flex-direction:column;}
346
+ .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 15px; overflow-y: auto; }
347
+ .exp-btn { padding: 10px 5px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; font-weight:600; font-size:12px; }
348
+ .exp-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
349
 
350
  .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
351
+ .spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 10px; }
352
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
353
 
354
  @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
355
  .live { animation: blink 1s infinite; background: var(--green) !important; }
 
356
  </style>
357
  </head>
358
  <body>
359
 
360
  <div class="loader-ov" id="loader">
361
  <div class="spinner"></div>
362
+ <div id="loaderMsg" style="font-weight:700; font-size:12px;">CONNECTING...</div>
363
  </div>
364
 
365
  <div class="modal" id="expModal">
366
  <div class="modal-box">
367
  <div style="display:flex; justify-content:space-between; font-weight:700; border-bottom:1px solid #eee; padding-bottom:10px;">
368
+ <span>SELECT EXPIRY</span>
369
+ <span style="cursor:pointer; padding:0 5px;" onclick="closeModal()">✕</span>
370
  </div>
371
  <div class="modal-grid" id="expGrid"></div>
372
  </div>
373
  </div>
374
 
375
  <div class="top-nav">
376
+ <div class="nav-left">
377
  <div class="search-box">
378
+ <input type="text" class="search-input" id="searchInp" placeholder="SEARCH..." autocomplete="off">
379
  <div class="search-dropdown" id="searchRes"></div>
380
  </div>
381
+ <button class="btn hide-mobile" onclick="refreshDB()">RELOAD DB</button>
 
 
 
 
382
  </div>
383
 
384
+ <div class="nav-right">
385
+ <div style="text-align:right; line-height:1;">
386
+ <div style="font-size:9px; font-weight:700; color:#888;" id="spotLabel">SPOT</div>
387
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:14px;">0.00</div>
388
  </div>
389
  <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
390
+ <span id="selExpTxt">EXPIRY</span>
391
  </button>
392
+ <button class="btn btn-red" onclick="location.href='/logout'">EXIT</button>
393
+ <div id="statusDot" style="width:8px; height:8px; background:#ccc; border-radius:50%;"></div>
394
  </div>
395
  </div>
396
 
397
  <div class="grid-layout">
398
  <div class="chain-panel">
399
  <div class="chain-header">
400
+ <div>CALL OI</div>
401
+ <div>NET FLOW (P-C)</div>
402
+ <div>PUT OI</div>
403
  </div>
404
  <div class="chain-body" id="chainBody">
405
+ <div style="padding:40px; text-align:center; color:#999; font-weight:600; font-size:12px;">SEARCH SYMBOL TO LOAD</div>
406
  </div>
407
  </div>
408
 
409
+ <div class="fut-panel" id="futBody">
410
+ <!-- Futures Cards go here -->
 
411
  </div>
412
  </div>
413
 
 
454
  }
455
 
456
  async function refreshDB() {
457
+ if(!confirm("Force refresh? Takes ~10s.")) return;
458
  document.getElementById('loader').style.display = 'flex';
459
  await fetch('/api/refresh');
460
  checkStatus();
 
476
  async function loadSymbol(root) {
477
  document.getElementById('searchInp').value = root;
478
  document.getElementById('searchRes').style.display = 'none';
479
+ document.getElementById('loaderMsg').innerText = "LOADING...";
480
  document.getElementById('loader').style.display = 'flex';
481
 
482
  try {
 
487
  CURRENT_ROOT = root;
488
  CHAIN_DATA = data.items;
489
  SPOT_SYM = data.spot;
490
+ SPOT_PRICE = 0; // Reset spot price so next poll triggers centering
491
+ window.scrolled = false; // Reset scroll lock
492
 
 
493
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
494
 
495
  if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
496
+ SPOT_SYM = FUT_SYMS[0].s;
497
  }
498
 
499
  document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ')) ? "SPOT" : "FUT REF";
 
521
  const grid = document.getElementById('expGrid');
522
  grid.innerHTML = validExps.map(ts => {
523
  const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
524
+ return `<div class="exp-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${dStr}</div>`;
525
  }).join('');
526
 
527
  buildChain();
 
550
 
551
  body.innerHTML = strikes.map(k => `
552
  <div class="row" id="row-${k}" data-k="${k}">
553
+ <div class="side-col" id="ce-${k}"><span>-</span></div>
554
  <div class="mid-col">
555
  <div class="visual-bar" id="bar-${k}"></div>
556
  <span class="strike-txt">${k}</span>
557
+ <span class="delta-txt" id="val-${k}"></span>
558
  </div>
559
+ <div class="side-col" id="pe-${k}"><span>-</span></div>
560
  </div>
561
  `).join('');
562
 
 
563
  restartPoller();
564
  }
565
 
566
  function restartPoller() {
567
  if(POLLER) clearInterval(POLLER);
568
  POLLER = setInterval(pollData, 1500);
569
+ pollData(); // Immediate call
570
  }
571
 
572
  async function pollData() {
 
574
  IS_POLLING = true;
575
 
576
  try {
577
+ // Determine range to fetch
578
  let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
579
+ const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
580
+
581
+ // Logic to determine fetching range
582
+ let targetK = kList[Math.floor(kList.length/2)]; // Default middle
583
 
584
  if(SPOT_PRICE > 0) {
585
+ // Find closest strike to spot
586
+ let minDiff = 999999;
587
+ kList.forEach(k => {
588
+ const diff = Math.abs(SPOT_PRICE - k);
589
+ if(diff < minDiff) { minDiff = diff; targetK = k; }
590
+ });
591
+
592
+ // Center the view ONCE per symbol load
593
+ if(!window.scrolled) {
594
+ const atmRow = document.getElementById(`row-${targetK}`);
595
+ if(atmRow) {
596
+ atmRow.scrollIntoView({block:'center', behavior:'auto'});
597
+ atmRow.classList.add('atm');
598
+ window.scrolled = true;
599
+ }
600
+ }
601
+
602
+ // Highlight ATM
603
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
604
+ const currAtm = document.getElementById(`row-${targetK}`);
605
+ if(currAtm) currAtm.classList.add('atm');
 
 
 
 
 
 
606
  }
607
 
608
+ // Fetch range around targetK (Atm or Middle)
609
+ const centerIdx = kList.indexOf(targetK);
610
+ const range = 12; // 12 above, 12 below
611
+ const start = Math.max(0, centerIdx - range);
612
+ const end = Math.min(kList.length, centerIdx + range);
613
+ const viewKs = kList.slice(start, end);
614
+
615
+ OPT_SYMS.forEach(o => { if(viewKs.includes(o.k)) symbolsToFetch.push(o.s); });
616
+
617
  const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
618
  if(uniqueSyms.length === 0) return;
619
 
 
640
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
641
  }
642
 
643
+ // Update Futures (Horizontal scroll on mobile)
644
  const futHTML = FUT_SYMS.map(f => {
645
  const d = data[f.s];
646
  if(!d) return '';
 
649
  const bPct = (d.totalbuyqty/tot)*100;
650
  return `
651
  <div class="fut-card">
652
+ <div style="display:flex; justify-content:space-between; font-weight:700; font-size:11px;">
653
+ <span>${dt}</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
654
  </div>
655
  <div class="meter-bg">
656
  <div style="width:${bPct}%; background:var(--green)"></div>
657
  <div style="width:${100-bPct}%; background:var(--red)"></div>
658
  </div>
659
+ <div style="display:flex; justify-content:space-between; font-size:9px; color:#666; margin-top:2px;">
660
+ <span>B:${fmt(d.totalbuyqty)}</span> <span>S:${fmt(d.totalsellqty)}</span>
661
  </div>
662
  </div>`;
663
  }).join('');
664
  if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
665
 
666
+ // Process Chain Data
667
  const rowData = {};
668
  for(const [sym, d] of Object.entries(data)) {
669
  const meta = OPT_SYMS.find(x => x.s === sym);
 
681
  const delta = r.pe - r.ce;
682
  const ceEl = document.getElementById(`ce-${k}`);
683
  const peEl = document.getElementById(`pe-${k}`);
684
+
685
+ // Simplified Mobile Text
686
+ if(ceEl) ceEl.innerHTML = `<span><strong>${fmt(r.ce)}</strong></span>`;
687
+ if(peEl) peEl.innerHTML = `<span><strong>${fmt(r.pe)}</strong></span>`;
688
 
689
  const bar = document.getElementById(`bar-${k}`);
690
  const val = document.getElementById(`val-${k}`);
691
+
692
  if(bar && maxDiff > 0) {
693
  const pct = (Math.abs(delta) / maxDiff) * 100;
694
  if(delta >= 0) {
 
713
  let abs = Math.abs(n);
714
  if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr';
715
  if(abs>=100000) return (n/100000).toFixed(2)+'L';
716
+ if(abs>=1000) return (n/1000).toFixed(0)+'k';
717
  return n;
718
  }
719
  </script>