topsecrettraders commited on
Commit
a95d9b5
·
verified ·
1 Parent(s): f03c1fb

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +289 -170
app.py CHANGED
@@ -38,7 +38,6 @@ SYSTEM_STATUS = {
38
  UPDATE_LOCK = threading.Lock()
39
 
40
  # Manual mapping for Index Spot symbols
41
- # Added BSE Indices (SENSEX, BANKEX)
42
  INDEX_MAP = {
43
  "NIFTY": "NSE:NIFTY50-INDEX",
44
  "BANKNIFTY": "NSE:NIFTYBANK-INDEX",
@@ -52,13 +51,10 @@ INDEX_MAP = {
52
  def get_root_symbol(desc: str, symbol: str) -> str:
53
  """Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
54
  try:
55
- # Standard Format: "SENSEX 19 JAN 72000 CE" or "RELIANCE 24 JAN..."
56
  parts = desc.strip().split(' ')
57
  root = parts[0].upper()
58
- # Clean up potential artifacts
59
  return root.replace(':', '').replace('-', '')
60
  except:
61
- # Fallback to symbol parsing if description fails
62
  if ':' in symbol:
63
  return symbol.split(':')[1]
64
  return symbol
@@ -70,12 +66,12 @@ def update_master_db():
70
  return
71
 
72
  try:
73
- SYSTEM_STATUS["message"] = "Downloading Master Data (NSE, MCX, BSE)..."
74
  print(">>> [BG TASK] STARTING DOWNLOAD...")
75
 
76
  headers = { "User-Agent": "Mozilla/5.0" }
77
 
78
- # 1. Download NSE F&O
79
  url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
80
  df_nse = pd.read_csv(
81
  url_nse, usecols=[0, 1, 8, 9, 13],
@@ -84,7 +80,7 @@ def update_master_db():
84
  )
85
  df_nse['Exch'] = 'NSE'
86
 
87
- # 2. Download MCX F&O
88
  url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
89
  df_mcx = pd.read_csv(
90
  url_mcx, usecols=[0, 1, 8, 9, 13],
@@ -93,7 +89,7 @@ def update_master_db():
93
  )
94
  df_mcx['Exch'] = 'MCX'
95
 
96
- # 3. Download BSE F&O (For SENSEX etc.)
97
  url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
98
  df_bse = pd.read_csv(
99
  url_bse, usecols=[0, 1, 8, 9, 13],
@@ -104,9 +100,7 @@ def update_master_db():
104
 
105
  SYSTEM_STATUS["message"] = "Processing Data..."
106
 
107
- # Merge Dataframes
108
  df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
109
-
110
  df['Symbol'] = df['Symbol'].astype(str)
111
  df['Desc'] = df['Desc'].astype(str)
112
 
@@ -119,21 +113,15 @@ def update_master_db():
119
 
120
  root = get_root_symbol(desc, sym)
121
 
122
- # Initialize Root Entry
123
  if root not in temp_db:
124
  spot_sym = ""
125
-
126
- # Check for Index Map match first
127
  if root in INDEX_MAP:
128
  spot_sym = INDEX_MAP[root]
129
  elif exch == "MCX":
130
- # MCX has no "Equity Spot", usually we track the Future
131
  spot_sym = "MCX"
132
  elif exch == "BSE":
133
- # BSE Equity Spot usually "BSE:Symbol"
134
  spot_sym = f"BSE:{root}"
135
  else:
136
- # NSE Equity Spot
137
  spot_sym = f"NSE:{root}-EQ"
138
 
139
  temp_db[root] = { "spot": spot_sym, "exch": exch, "items": [] }
@@ -146,15 +134,11 @@ def update_master_db():
146
  strike = 0.0
147
  opt_type = "FUT"
148
 
149
- # Parse Strike & Option Type
150
- # Logic handles "SENSEX 23 JAN 72000 CE"
151
  if "CE" in sym or "PE" in sym:
152
  parts = desc.strip().split(' ')
153
  if len(parts) >= 2:
154
  try:
155
- # Logic: val is usually 2nd last item
156
  val = parts[-2]
157
- # Remove decimal for digit check
158
  val_clean = val.replace('.', '', 1)
159
  if val_clean.isdigit():
160
  strike = float(val)
@@ -166,19 +150,16 @@ def update_master_db():
166
  "s": sym, "e": exp, "k": strike, "t": opt_type
167
  })
168
 
169
- # Final Sort & Search Index Creation
170
  final_db = {}
171
  search_list = []
172
 
173
  for root, data in temp_db.items():
174
- # Sort by Expiry (asc) then Strike (asc)
175
  sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
176
  final_db[root] = {
177
  "spot": data["spot"],
178
  "exch": data["exch"],
179
  "items": sorted_items
180
  }
181
- # Create a rich search object
182
  search_list.append({
183
  "root": root,
184
  "exch": data["exch"],
@@ -186,10 +167,8 @@ def update_master_db():
186
  })
187
 
188
  MASTER_DB = final_db
189
- # Sort search index alphabetically
190
  SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
191
 
192
- # Save cache
193
  with open(CACHE_FILE, "w") as f:
194
  json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
195
 
@@ -244,7 +223,6 @@ def home(access_token: Optional[str] = Cookie(None)):
244
  </div>
245
  """)
246
 
247
- # Inject token
248
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
249
 
250
  @app.get("/callback")
@@ -272,32 +250,17 @@ def logout():
272
  response.delete_cookie("access_token")
273
  return response
274
 
275
- @app.get("/favicon.ico")
276
- def favicon():
277
- return Response(status_code=204)
278
-
279
- # --- API ---
280
-
281
  @app.get("/api/status")
282
  def get_status():
283
  return JSONResponse(SYSTEM_STATUS)
284
 
285
- @app.get("/api/init")
286
- def legacy_init():
287
- return JSONResponse([])
288
-
289
  @app.get("/api/search")
290
  def search_symbol(q: str = Query(..., min_length=1)):
291
  if not SYSTEM_STATUS["ready"]:
292
  return JSONResponse([])
293
 
294
  query = q.upper()
295
- # SEARCH_INDEX is now a list of dicts: {'root': 'NIFTY', 'exch': 'NSE', 'display': 'NIFTY'}
296
-
297
- # 1. Starts with
298
  results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
299
-
300
- # 2. Contains (if few results)
301
  if len(results) < 10:
302
  results += [x for x in SEARCH_INDEX if query in x['root'] and x not in results]
303
 
@@ -326,34 +289,34 @@ HTML_TEMPLATE = """
326
  <html lang="en">
327
  <head>
328
  <meta charset="UTF-8">
329
- <title>PRO CHAIN (V3)</title>
330
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
331
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
332
  <style>
333
  :root {
334
  --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
335
  --text-main: #1a1a1a; --text-sub: #555555;
336
  --accent: #2962ff; --green: #00c853; --red: #d50000;
337
- --green-soft: rgba(0, 200, 83, 0.3); --red-soft: rgba(213, 0, 0, 0.3);
338
- --atm-bg: rgba(41, 98, 255, 0.1);
339
- /* Badge Colors */
340
- --badge-nse: #00c853;
341
- --badge-bse: #ff6d00;
342
- --badge-mcx: #2962ff;
343
  }
344
  * { box-sizing: border-box; }
345
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
346
 
347
- .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; }
348
- .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; }
349
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
350
 
 
 
 
351
  .search-box { position: relative; width: 350px; margin-right: 20px; }
352
- .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; }
353
- .search-input:focus { border-color: var(--accent); }
354
- .search-dropdown { position: absolute; top: 100%; left: 0; width: 100%; background: white; border: 1px solid var(--border); border-top: none; max-height: 400px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 0 0 6px 6px; }
355
 
356
- .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #eee; font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
357
  .search-item:hover { background: #f0f7ff; color: var(--accent); }
358
 
359
  .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
@@ -362,80 +325,135 @@ HTML_TEMPLATE = """
362
  .badge-MCX { background: var(--badge-mcx); }
363
 
364
  .grid-layout { display: flex; height: calc(100vh - 60px); }
 
 
365
  .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
366
- .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); }
367
- .chain-body { flex: 1; overflow-y: auto; background: #fff; }
368
 
369
- .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; }
 
370
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
371
 
372
  .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; }
373
- .visual-bar { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; transition: 0.3s; opacity: 0.8; }
374
- .strike-txt { z-index: 2; font-weight: 800; font-size: 13px; }
375
- .delta-txt { z-index: 2; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.7); padding: 0 4px; border-radius: 3px; }
376
 
377
- .side-col { padding: 0 15px; display: flex; justify-content: space-between; color: #666; font-size: 11px; }
 
378
 
379
- .fut-panel { width: 280px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
380
- .fut-card { padding: 12px; background: #fff; border-bottom: 1px solid var(--border); }
381
- .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
382
 
383
- .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; }
384
- .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;}
385
- .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; overflow-y: auto; }
386
- .exp-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; font-weight:600; font-size:12px; }
387
- .exp-btn:hover { background: #f0f7ff; border-color: var(--accent); }
388
- .exp-btn.active { background: var(--accent); color: white; }
389
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  .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; }
391
- .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; }
392
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
393
 
394
  @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
395
- .live { animation: blink 1s infinite; background: var(--green) !important; }
396
- .spin-icon { animation: spin 1s linear infinite; }
 
 
 
 
 
 
 
397
  </style>
398
  </head>
399
  <body>
400
 
 
401
  <div class="loader-ov" id="loader">
402
  <div class="spinner"></div>
403
- <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px;">CONNECTING...</div>
404
  </div>
405
 
 
406
  <div class="modal" id="expModal">
407
  <div class="modal-box">
408
- <div style="display:flex; justify-content:space-between; font-weight:700; border-bottom:1px solid #eee; padding-bottom:10px;">
409
- <span id="expTitle">SELECT EXPIRY</span>
410
- <span style="cursor:pointer" onclick="closeModal()">✕</span>
411
  </div>
412
  <div class="modal-grid" id="expGrid"></div>
413
  </div>
414
  </div>
415
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
416
  <div class="top-nav">
417
  <div style="display:flex; align-items:center;">
418
  <div class="search-box">
419
- <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g., SENSEX, NIFTY)..." autocomplete="off">
420
  <div class="search-dropdown" id="searchRes"></div>
421
  </div>
422
 
423
- <button class="btn" onclick="refreshDB()" id="refreshBtn">
424
- <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>
425
- RELOAD DB
 
 
 
426
  </button>
427
  </div>
428
 
429
  <div style="display:flex; gap:20px; align-items:center;">
430
  <div style="text-align:right;">
431
- <div style="font-size:10px; font-weight:700; color:#888;" id="spotLabel">SPOT</div>
432
- <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:16px;">0.00</div>
433
  </div>
434
  <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
435
- <span style="color:#888">EXP:</span> <span id="selExpTxt">SELECT</span>
 
 
 
 
 
436
  </button>
437
- <button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
438
- <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
439
  </div>
440
  </div>
441
 
@@ -443,17 +461,35 @@ HTML_TEMPLATE = """
443
  <div class="chain-panel">
444
  <div class="chain-header">
445
  <div>CALL SELLERS (OI)</div>
446
- <div>NET FLOW (PUT-CALL)</div>
447
  <div>PUT SELLERS (OI)</div>
448
  </div>
449
  <div class="chain-body" id="chainBody">
450
- <div style="padding:40px; text-align:center; color:#999; font-weight:600;">USE SEARCH TO LOAD A SYMBOL</div>
451
  </div>
452
  </div>
453
 
454
  <div class="fut-panel">
455
- <div style="padding:15px; font-weight:700; border-bottom:1px solid var(--border); background:#fff;">FUTURES</div>
456
- <div id="futBody"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  </div>
458
  </div>
459
 
@@ -470,9 +506,15 @@ HTML_TEMPLATE = """
470
  let POLLER = null;
471
  let IS_POLLING = false;
472
  let SEARCH_TIMER = null;
 
 
 
 
473
 
474
  window.onload = () => {
475
  if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
 
 
476
  checkStatus();
477
 
478
  const inp = document.getElementById('searchInp');
@@ -485,6 +527,26 @@ HTML_TEMPLATE = """
485
  });
486
  };
487
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
  async function checkStatus() {
489
  try {
490
  const res = await fetch('/api/status');
@@ -501,6 +563,7 @@ HTML_TEMPLATE = """
501
 
502
  async function refreshDB() {
503
  if(!confirm("Download fresh Master Data (NSE, MCX, BSE)? This takes ~15s.")) return;
 
504
  document.getElementById('loader').style.display = 'flex';
505
  await fetch('/api/refresh');
506
  checkStatus();
@@ -512,7 +575,6 @@ HTML_TEMPLATE = """
512
  try {
513
  const res = await fetch(`/api/search?q=${q}`);
514
  const data = await res.json();
515
- // Data is now list of {root, exch, display}
516
  resDiv.innerHTML = data.map(item =>
517
  `<div class="search-item" onclick="loadSymbol('${item.root}')">
518
  <span>${item.display}</span>
@@ -538,15 +600,15 @@ HTML_TEMPLATE = """
538
  CHAIN_DATA = data.items;
539
  SPOT_SYM = data.spot;
540
 
541
- // Sort futures: Filter items with type 'FUT'
542
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
543
 
544
- // Handle MCX specific spot logic
545
  if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
546
  SPOT_SYM = FUT_SYMS[0].s;
547
  }
548
 
549
- document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ') || SPOT_SYM.startsWith('BSE:')) ? "SPOT" : "FUT REF";
550
 
551
  setupExpiries();
552
  document.getElementById('loader').style.display = 'none';
@@ -564,10 +626,7 @@ HTML_TEMPLATE = """
564
 
565
  const validExps = exps.filter(ts => ts > today);
566
  if(validExps.length === 0) {
567
- alert("No active options found for this symbol.");
568
- // Still render Futures if available
569
  document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
570
- updateUI({});
571
  return;
572
  }
573
 
@@ -577,7 +636,7 @@ HTML_TEMPLATE = """
577
  const grid = document.getElementById('expGrid');
578
  grid.innerHTML = validExps.map(ts => {
579
  const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
580
- return `<div class="exp-btn" onclick="selectExp(${ts})">${dStr}</div>`;
581
  }).join('');
582
 
583
  buildChain();
@@ -586,7 +645,14 @@ HTML_TEMPLATE = """
586
  function selectExp(ts) {
587
  ACTIVE_EXP = ts;
588
  updateExpText(ts);
589
- closeModal();
 
 
 
 
 
 
 
590
  buildChain();
591
  }
592
 
@@ -595,8 +661,6 @@ HTML_TEMPLATE = """
595
  document.getElementById('selExpTxt').innerText = dStr;
596
  }
597
 
598
- function closeModal() { document.getElementById('expModal').style.display = 'none'; }
599
-
600
  function buildChain() {
601
  const body = document.getElementById('chainBody');
602
  const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
@@ -606,13 +670,13 @@ HTML_TEMPLATE = """
606
 
607
  body.innerHTML = strikes.map(k => `
608
  <div class="row" id="row-${k}" data-k="${k}">
609
- <div class="side-col" id="ce-${k}"><span>-</span><span>-</span></div>
610
  <div class="mid-col">
611
  <div class="visual-bar" id="bar-${k}"></div>
612
  <span class="strike-txt">${k}</span>
613
  <span class="delta-txt" id="val-${k}">-</span>
614
  </div>
615
- <div class="side-col" id="pe-${k}"><span>-</span><span>-</span></div>
616
  </div>
617
  `).join('');
618
 
@@ -626,6 +690,7 @@ HTML_TEMPLATE = """
626
  pollData();
627
  }
628
 
 
629
  async function pollData() {
630
  if(IS_POLLING || !CURRENT_ROOT) return;
631
  IS_POLLING = true;
@@ -633,45 +698,52 @@ HTML_TEMPLATE = """
633
  try {
634
  let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
635
 
636
- // Logic to fetch only visible/near options
637
- if(SPOT_PRICE > 0) {
638
- const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
639
- if(kList.length > 0) {
640
- let closestK = kList[0], minDiff = 999999;
641
- kList.forEach(k => {
642
- const diff = Math.abs(SPOT_PRICE - k);
643
- if(diff < minDiff) { minDiff = diff; closestK = k; }
644
- });
645
-
646
- document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
647
- const atmRow = document.getElementById(`row-${closestK}`);
648
- if(atmRow) {
649
- atmRow.classList.add('atm');
650
- if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
651
- }
652
-
653
- const centerIdx = kList.indexOf(closestK);
654
- const start = Math.max(0, centerIdx - 30);
655
- const end = Math.min(kList.length, centerIdx + 30);
656
- const viewKs = kList.slice(start, end);
657
-
658
- OPT_SYMS.forEach(o => {
659
- if(viewKs.includes(o.k)) symbolsToFetch.push(o.s);
660
- });
661
  }
662
- } else {
663
- // Fallback if no spot price yet (initial load)
664
- const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
665
- const mid = Math.floor(kList.length/2);
666
- const viewKs = kList.slice(Math.max(0, mid-30), mid+30);
667
- OPT_SYMS.forEach(o => { if(viewKs.includes(o.k)) symbolsToFetch.push(o.s); });
 
 
 
 
 
 
 
 
 
 
 
668
  }
669
 
670
  const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
671
  if(uniqueSyms.length === 0) return;
672
 
673
  const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
674
- const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN, 'Content-Type': 'application/json' }});
675
 
676
  if(res.status === 401) { location.href = '/logout'; return; }
677
 
@@ -679,24 +751,28 @@ HTML_TEMPLATE = """
679
  if(json.s === 'ok') {
680
  updateUI(json.d);
681
  const dot = document.getElementById('statusDot');
682
- dot.style.background = 'var(--green)';
683
- dot.classList.add('live');
684
- setTimeout(()=>dot.classList.remove('live'), 500);
685
  }
686
- } catch(e) { console.error(e); document.getElementById('statusDot').style.background = 'var(--red)'; }
 
 
 
 
 
687
  finally { IS_POLLING = false; }
688
  }
689
 
690
  function updateUI(data) {
691
- // Fix: Check if data exists and ltp is a valid number
692
  if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
693
  SPOT_PRICE = data[SPOT_SYM].ltp;
694
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
695
  }
696
 
 
697
  const futHTML = FUT_SYMS.map(f => {
698
  const d = data[f.s];
699
- // Fix: Check if future data and ltp are valid
700
  if(!d || typeof d.ltp !== 'number') return '';
701
  const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
702
  const tot = (d.totalbuyqty + d.totalsellqty) || 1;
@@ -710,51 +786,94 @@ HTML_TEMPLATE = """
710
  <div style="width:${bPct}%; background:var(--green)"></div>
711
  <div style="width:${100-bPct}%; background:var(--red)"></div>
712
  </div>
713
- <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:2px;">
714
  <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
715
  </div>
716
  </div>`;
717
  }).join('');
718
  if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
719
 
 
720
  const rowData = {};
721
- for(const [sym, d] of Object.entries(data)) {
722
- const meta = OPT_SYMS.find(x => x.s === sym);
723
- if(!meta) continue;
724
- if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
725
- if(meta.t === 'CE') rowData[meta.k].ce = d.totalsellqty;
726
- if(meta.t === 'PE') rowData[meta.k].pe = d.totalsellqty;
727
- }
728
 
 
 
 
 
 
 
 
 
 
 
 
 
729
  let maxDiff = 0;
730
  for(const k in rowData) maxDiff = Math.max(maxDiff, Math.abs(rowData[k].pe - rowData[k].ce));
731
 
732
  for(const k in rowData) {
733
  const r = rowData[k];
734
  const delta = r.pe - r.ce;
 
 
735
  const ceEl = document.getElementById(`ce-${k}`);
736
- const peEl = document.getElementById(`pe-${k}`);
737
- if(ceEl) ceEl.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
738
- if(peEl) peEl.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
739
 
740
- const bar = document.getElementById(`bar-${k}`);
741
- const val = document.getElementById(`val-${k}`);
742
- if(bar && maxDiff > 0) {
743
- const pct = (Math.abs(delta) / maxDiff) * 100;
744
- if(delta >= 0) {
745
- bar.style.background = 'var(--green-soft)';
746
- bar.style.left = '50%';
747
- bar.style.width = (pct/2) + '%';
748
- val.style.color = 'var(--green)';
749
- val.innerText = "+" + fmt(delta);
750
- } else {
751
- bar.style.background = 'var(--red-soft)';
752
- bar.style.left = (50 - (pct/2)) + '%';
753
- bar.style.width = (pct/2) + '%';
754
- val.style.color = 'var(--red)';
755
- val.innerText = fmt(delta);
756
  }
757
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
  }
759
  }
760
 
@@ -764,7 +883,7 @@ HTML_TEMPLATE = """
764
  if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr';
765
  if(abs>=100000) return (n/100000).toFixed(2)+'L';
766
  if(abs>=1000) return (n/1000).toFixed(1)+'k';
767
- return n;
768
  }
769
  </script>
770
  </body>
 
38
  UPDATE_LOCK = threading.Lock()
39
 
40
  # Manual mapping for Index Spot symbols
 
41
  INDEX_MAP = {
42
  "NIFTY": "NSE:NIFTY50-INDEX",
43
  "BANKNIFTY": "NSE:NIFTYBANK-INDEX",
 
51
  def get_root_symbol(desc: str, symbol: str) -> str:
52
  """Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
53
  try:
 
54
  parts = desc.strip().split(' ')
55
  root = parts[0].upper()
 
56
  return root.replace(':', '').replace('-', '')
57
  except:
 
58
  if ':' in symbol:
59
  return symbol.split(':')[1]
60
  return symbol
 
66
  return
67
 
68
  try:
69
+ SYSTEM_STATUS["message"] = "Downloading Master Data..."
70
  print(">>> [BG TASK] STARTING DOWNLOAD...")
71
 
72
  headers = { "User-Agent": "Mozilla/5.0" }
73
 
74
+ # 1. NSE F&O
75
  url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
76
  df_nse = pd.read_csv(
77
  url_nse, usecols=[0, 1, 8, 9, 13],
 
80
  )
81
  df_nse['Exch'] = 'NSE'
82
 
83
+ # 2. MCX F&O
84
  url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
85
  df_mcx = pd.read_csv(
86
  url_mcx, usecols=[0, 1, 8, 9, 13],
 
89
  )
90
  df_mcx['Exch'] = 'MCX'
91
 
92
+ # 3. BSE F&O
93
  url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
94
  df_bse = pd.read_csv(
95
  url_bse, usecols=[0, 1, 8, 9, 13],
 
100
 
101
  SYSTEM_STATUS["message"] = "Processing Data..."
102
 
 
103
  df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
 
104
  df['Symbol'] = df['Symbol'].astype(str)
105
  df['Desc'] = df['Desc'].astype(str)
106
 
 
113
 
114
  root = get_root_symbol(desc, sym)
115
 
 
116
  if root not in temp_db:
117
  spot_sym = ""
 
 
118
  if root in INDEX_MAP:
119
  spot_sym = INDEX_MAP[root]
120
  elif exch == "MCX":
 
121
  spot_sym = "MCX"
122
  elif exch == "BSE":
 
123
  spot_sym = f"BSE:{root}"
124
  else:
 
125
  spot_sym = f"NSE:{root}-EQ"
126
 
127
  temp_db[root] = { "spot": spot_sym, "exch": exch, "items": [] }
 
134
  strike = 0.0
135
  opt_type = "FUT"
136
 
 
 
137
  if "CE" in sym or "PE" in sym:
138
  parts = desc.strip().split(' ')
139
  if len(parts) >= 2:
140
  try:
 
141
  val = parts[-2]
 
142
  val_clean = val.replace('.', '', 1)
143
  if val_clean.isdigit():
144
  strike = float(val)
 
150
  "s": sym, "e": exp, "k": strike, "t": opt_type
151
  })
152
 
 
153
  final_db = {}
154
  search_list = []
155
 
156
  for root, data in temp_db.items():
 
157
  sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
158
  final_db[root] = {
159
  "spot": data["spot"],
160
  "exch": data["exch"],
161
  "items": sorted_items
162
  }
 
163
  search_list.append({
164
  "root": root,
165
  "exch": data["exch"],
 
167
  })
168
 
169
  MASTER_DB = final_db
 
170
  SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
171
 
 
172
  with open(CACHE_FILE, "w") as f:
173
  json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
174
 
 
223
  </div>
224
  """)
225
 
 
226
  return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
227
 
228
  @app.get("/callback")
 
250
  response.delete_cookie("access_token")
251
  return response
252
 
 
 
 
 
 
 
253
  @app.get("/api/status")
254
  def get_status():
255
  return JSONResponse(SYSTEM_STATUS)
256
 
 
 
 
 
257
  @app.get("/api/search")
258
  def search_symbol(q: str = Query(..., min_length=1)):
259
  if not SYSTEM_STATUS["ready"]:
260
  return JSONResponse([])
261
 
262
  query = q.upper()
 
 
 
263
  results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
 
 
264
  if len(results) < 10:
265
  results += [x for x in SEARCH_INDEX if query in x['root'] and x not in results]
266
 
 
289
  <html lang="en">
290
  <head>
291
  <meta charset="UTF-8">
292
+ <title>PRO CHAIN V3</title>
293
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
294
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
295
  <style>
296
  :root {
297
  --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
298
  --text-main: #1a1a1a; --text-sub: #555555;
299
  --accent: #2962ff; --green: #00c853; --red: #d50000;
300
+ --green-soft: rgba(0, 200, 83, 0.15); --red-soft: rgba(213, 0, 0, 0.15);
301
+ --atm-bg: rgba(41, 98, 255, 0.08);
302
+ --badge-nse: #00c853; --badge-bse: #ff6d00; --badge-mcx: #2962ff;
 
 
 
303
  }
304
  * { box-sizing: border-box; }
305
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
306
 
307
+ .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; box-shadow: 0 2px 5px rgba(0,0,0,0.03); }
308
+ .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; color: var(--text-sub); }
309
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
310
 
311
+ .icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); }
312
+ .icon-btn:hover { background: #f0f0f0; color: var(--text-main); }
313
+
314
  .search-box { position: relative; width: 350px; margin-right: 20px; }
315
+ .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; transition: 0.3s; background: #f9f9f9; }
316
+ .search-input:focus { border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px rgba(41, 98, 255, 0.1); }
317
+ .search-dropdown { position: absolute; top: 105%; left: 0; width: 100%; background: white; border: 1px solid var(--border); max-height: 400px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 6px; z-index: 100; }
318
 
319
+ .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f5f5f5; font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
320
  .search-item:hover { background: #f0f7ff; color: var(--accent); }
321
 
322
  .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
 
325
  .badge-MCX { background: var(--badge-mcx); }
326
 
327
  .grid-layout { display: flex; height: calc(100vh - 60px); }
328
+
329
+ /* Chain Panel */
330
  .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
331
+ .chain-header { display: grid; grid-template-columns: 1fr 160px 1fr; padding: 12px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; }
332
+ .chain-body { flex: 1; overflow-y: auto; background: #fff; scroll-behavior: smooth; }
333
 
334
+ .row { display: grid; grid-template-columns: 1fr 160px 1fr; border-bottom: 1px solid #f0f0f0; height: 42px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
335
+ .row:hover { background: #fafafa; }
336
  .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
337
 
338
  .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; }
339
+ .visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.8; }
340
+ .strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); }
341
+ .delta-txt { z-index: 2; font-size: 10px; font-weight: 700; background: rgba(255,255,255,0.85); padding: 0 5px; border-radius: 3px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); margin-top: -2px; }
342
 
343
+ .side-col { padding: 0 15px; display: flex; justify-content: space-between; color: var(--text-sub); font-size: 11px; font-weight: 600; }
344
+ .col-val { font-weight: 700; color: var(--text-main); }
345
 
346
+ /* Futures / Sidebar Panel */
347
+ .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
 
348
 
349
+ .panel-section { padding: 15px; border-bottom: 1px solid var(--border); background: #fff; margin-bottom: 10px; }
350
+ .panel-title { font-size: 11px; font-weight: 800; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display:flex; justify-content:space-between; align-items:center; }
 
 
 
 
351
 
352
+ .fut-card { padding: 12px; background: #fff; border: 1px solid #eee; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
353
+ .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 8px; }
354
+
355
+ .netflow-card { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid var(--border); text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.03); }
356
+ .nf-val { font-size: 24px; font-family: 'JetBrains Mono'; font-weight: 800; margin: 10px 0; }
357
+ .nf-sub { font-size: 10px; color: #888; font-weight: 600; }
358
+
359
+ /* Modals */
360
+ .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
361
+ .modal-box { background: #fff; width: 400px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
362
+ @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
363
+
364
+ .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
365
+ .opt-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 6px; cursor: pointer; font-weight:600; font-size:12px; transition:0.2s; background:#f9f9f9; }
366
+ .opt-btn:hover { background: #fff; border-color: var(--accent); box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
367
+ .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
368
+
369
+ .inp-group { display:flex; gap:10px; margin-top:15px; }
370
+ .modal-inp { flex:1; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:'JetBrains Mono'; font-weight:bold; }
371
+ .modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; }
372
+
373
+ /* Loader */
374
  .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; }
375
+ .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: 15px; }
376
  @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
377
 
378
  @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
379
+ .live-dot { width:8px; height:8px; background:#ccc; border-radius:50%; transition:0.3s; }
380
+ .live-dot.active { background: var(--green); animation: blink 1.5s infinite; box-shadow: 0 0 5px var(--green); }
381
+ .live-dot.error { background: var(--red); }
382
+
383
+ /* Scrollbar */
384
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
385
+ ::-webkit-scrollbar-track { background: transparent; }
386
+ ::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
387
+ ::-webkit-scrollbar-thumb:hover { background: #bbb; }
388
  </style>
389
  </head>
390
  <body>
391
 
392
+ <!-- LOADER -->
393
  <div class="loader-ov" id="loader">
394
  <div class="spinner"></div>
395
+ <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px; letter-spacing:1px;">CONNECTING...</div>
396
  </div>
397
 
398
+ <!-- EXPIRY MODAL -->
399
  <div class="modal" id="expModal">
400
  <div class="modal-box">
401
+ <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
402
+ <span>SELECT EXPIRY</span>
403
+ <span style="cursor:pointer; color:#999;" onclick="closeModal('expModal')">✕</span>
404
  </div>
405
  <div class="modal-grid" id="expGrid"></div>
406
  </div>
407
  </div>
408
 
409
+ <!-- SETTINGS MODAL -->
410
+ <div class="modal" id="setModal">
411
+ <div class="modal-box" style="width:350px;">
412
+ <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
413
+ <span>DASHBOARD SETTINGS</span>
414
+ <span style="cursor:pointer; color:#999;" onclick="closeModal('setModal')">✕</span>
415
+ </div>
416
+ <div style="margin-top:20px;">
417
+ <label style="font-size:12px; font-weight:700; color:#666; display:block; margin-bottom:5px;">NET FLOW RANGE (STRIKES)</label>
418
+ <div style="font-size:11px; color:#999; margin-bottom:10px;">Calculates total Put-Call flow for strikes above/below ATM.</div>
419
+ <div class="inp-group">
420
+ <input type="number" id="rangeInp" class="modal-inp" value="10" min="1" max="50">
421
+ <button class="modal-action" onclick="saveSettings()">SAVE</button>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+
427
+ <!-- TOP NAV -->
428
  <div class="top-nav">
429
  <div style="display:flex; align-items:center;">
430
  <div class="search-box">
431
+ <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. NIFTY, GOLD)..." autocomplete="off">
432
  <div class="search-dropdown" id="searchRes"></div>
433
  </div>
434
 
435
+ <button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB">
436
+ <svg width="18" height="18" 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>
437
+ </button>
438
+
439
+ <button class="icon-btn" onclick="openSettings()" title="Settings" style="margin-left:5px;">
440
+ <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
441
  </button>
442
  </div>
443
 
444
  <div style="display:flex; gap:20px; align-items:center;">
445
  <div style="text-align:right;">
446
+ <div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px;" id="spotLabel">SPOT</div>
447
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div>
448
  </div>
449
  <button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
450
+ <span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
451
+ <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
452
+ </button>
453
+ <div class="live-dot" id="statusDot" title="Live Connection Status"></div>
454
+ <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;">
455
+ <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
456
  </button>
 
 
457
  </div>
458
  </div>
459
 
 
461
  <div class="chain-panel">
462
  <div class="chain-header">
463
  <div>CALL SELLERS (OI)</div>
464
+ <div>NET FLOW (PUT - CALL)</div>
465
  <div>PUT SELLERS (OI)</div>
466
  </div>
467
  <div class="chain-body" id="chainBody">
468
+ <div style="padding:40px; text-align:center; color:#999; font-weight:600; font-size:14px;">USE SEARCH BAR TO LOAD DATA</div>
469
  </div>
470
  </div>
471
 
472
  <div class="fut-panel">
473
+
474
+ <!-- NET FLOW CARD -->
475
+ <div class="panel-section">
476
+ <div class="panel-title">
477
+ <span>NET FLOW</span>
478
+ <span id="nfRangeLabel" style="background:#eee; padding:2px 6px; border-radius:4px; font-size:9px;">ATM ±10</span>
479
+ </div>
480
+ <div class="netflow-card">
481
+ <div class="nf-val" id="totalNetFlow">0</div>
482
+ <div class="nf-sub">Put Sell - Call Sell (Cumulative)</div>
483
+ <div id="nfSignal" style="font-size:10px; font-weight:800; margin-top:5px; text-transform:uppercase;">NEUTRAL</div>
484
+ </div>
485
+ </div>
486
+
487
+ <!-- FUTURES CARD -->
488
+ <div class="panel-section">
489
+ <div class="panel-title">FUTURES CONTRACTS</div>
490
+ <div id="futBody"></div>
491
+ </div>
492
+
493
  </div>
494
  </div>
495
 
 
506
  let POLLER = null;
507
  let IS_POLLING = false;
508
  let SEARCH_TIMER = null;
509
+ let ATM_STRIKE = 0;
510
+
511
+ // Settings
512
+ let SETTING_RANGE = parseInt(localStorage.getItem('nf_range')) || 10;
513
 
514
  window.onload = () => {
515
  if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
516
+ document.getElementById('rangeInp').value = SETTING_RANGE;
517
+ document.getElementById('nfRangeLabel').innerText = `ATM ±${SETTING_RANGE}`;
518
  checkStatus();
519
 
520
  const inp = document.getElementById('searchInp');
 
527
  });
528
  };
529
 
530
+ function openSettings() {
531
+ document.getElementById('setModal').style.display = 'flex';
532
+ }
533
+
534
+ function saveSettings() {
535
+ const val = parseInt(document.getElementById('rangeInp').value);
536
+ if(val > 0 && val <= 50) {
537
+ SETTING_RANGE = val;
538
+ localStorage.setItem('nf_range', val);
539
+ document.getElementById('nfRangeLabel').innerText = `ATM ±${SETTING_RANGE}`;
540
+ closeModal('setModal');
541
+ // Force data refresh to include new range immediately
542
+ pollData();
543
+ } else {
544
+ alert("Please enter a range between 1 and 50");
545
+ }
546
+ }
547
+
548
+ function closeModal(id) { document.getElementById(id).style.display = 'none'; }
549
+
550
  async function checkStatus() {
551
  try {
552
  const res = await fetch('/api/status');
 
563
 
564
  async function refreshDB() {
565
  if(!confirm("Download fresh Master Data (NSE, MCX, BSE)? This takes ~15s.")) return;
566
+ document.getElementById('loaderMsg').innerText = "DOWNLOADING...";
567
  document.getElementById('loader').style.display = 'flex';
568
  await fetch('/api/refresh');
569
  checkStatus();
 
575
  try {
576
  const res = await fetch(`/api/search?q=${q}`);
577
  const data = await res.json();
 
578
  resDiv.innerHTML = data.map(item =>
579
  `<div class="search-item" onclick="loadSymbol('${item.root}')">
580
  <span>${item.display}</span>
 
600
  CHAIN_DATA = data.items;
601
  SPOT_SYM = data.spot;
602
 
603
+ // Handle Futures sorting
604
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
605
 
606
+ // Handle MCX specific spot logic (Use Near Future as Spot)
607
  if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
608
  SPOT_SYM = FUT_SYMS[0].s;
609
  }
610
 
611
+ document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ') || SPOT_SYM.startsWith('BSE:')) ? "SPOT PRICE" : "FUT REFERENCE";
612
 
613
  setupExpiries();
614
  document.getElementById('loader').style.display = 'none';
 
626
 
627
  const validExps = exps.filter(ts => ts > today);
628
  if(validExps.length === 0) {
 
 
629
  document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
 
630
  return;
631
  }
632
 
 
636
  const grid = document.getElementById('expGrid');
637
  grid.innerHTML = validExps.map(ts => {
638
  const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
639
+ return `<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${dStr}</div>`;
640
  }).join('');
641
 
642
  buildChain();
 
645
  function selectExp(ts) {
646
  ACTIVE_EXP = ts;
647
  updateExpText(ts);
648
+ closeModal('expModal');
649
+ // Update active class in modal
650
+ document.querySelectorAll('.opt-btn').forEach(b => {
651
+ if(b.innerText === new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'}))
652
+ b.classList.add('active');
653
+ else
654
+ b.classList.remove('active');
655
+ });
656
  buildChain();
657
  }
658
 
 
661
  document.getElementById('selExpTxt').innerText = dStr;
662
  }
663
 
 
 
664
  function buildChain() {
665
  const body = document.getElementById('chainBody');
666
  const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
 
670
 
671
  body.innerHTML = strikes.map(k => `
672
  <div class="row" id="row-${k}" data-k="${k}">
673
+ <div class="side-col" id="ce-${k}"><span>-</span><span class="col-val">-</span></div>
674
  <div class="mid-col">
675
  <div class="visual-bar" id="bar-${k}"></div>
676
  <span class="strike-txt">${k}</span>
677
  <span class="delta-txt" id="val-${k}">-</span>
678
  </div>
679
+ <div class="side-col" id="pe-${k}"><span class="col-val">-</span><span>-</span></div>
680
  </div>
681
  `).join('');
682
 
 
690
  pollData();
691
  }
692
 
693
+ // Main Data Fetching Logic
694
  async function pollData() {
695
  if(IS_POLLING || !CURRENT_ROOT) return;
696
  IS_POLLING = true;
 
698
  try {
699
  let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
700
 
701
+ // Determine ATM and Viewport
702
+ const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
703
+
704
+ if(kList.length > 0) {
705
+ let closestK = kList[0], minDiff = 999999;
706
+ const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)];
707
+
708
+ let atmIdx = 0;
709
+ kList.forEach((k, idx) => {
710
+ const diff = Math.abs(refPrice - k);
711
+ if(diff < minDiff) { minDiff = diff; closestK = k; atmIdx = idx; }
712
+ });
713
+
714
+ ATM_STRIKE = closestK;
715
+
716
+ // Highlight ATM
717
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
718
+ const atmRow = document.getElementById(`row-${closestK}`);
719
+ if(atmRow) {
720
+ atmRow.classList.add('atm');
721
+ if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
 
 
 
 
722
  }
723
+
724
+ // 1. Fetch data for VIEWPORT (what user sees)
725
+ const start = Math.max(0, atmIdx - 20);
726
+ const end = Math.min(kList.length, atmIdx + 20);
727
+ const viewKs = kList.slice(start, end);
728
+
729
+ // 2. Fetch data for NET FLOW CALCULATION (User Setting Range)
730
+ const rangeStart = Math.max(0, atmIdx - SETTING_RANGE);
731
+ const rangeEnd = Math.min(kList.length, atmIdx + SETTING_RANGE + 1);
732
+ const calcKs = kList.slice(rangeStart, rangeEnd);
733
+
734
+ // Combine keys
735
+ const allNeededKs = new Set([...viewKs, ...calcKs]);
736
+
737
+ OPT_SYMS.forEach(o => {
738
+ if(allNeededKs.has(o.k)) symbolsToFetch.push(o.s);
739
+ });
740
  }
741
 
742
  const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
743
  if(uniqueSyms.length === 0) return;
744
 
745
  const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
746
+ const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN }});
747
 
748
  if(res.status === 401) { location.href = '/logout'; return; }
749
 
 
751
  if(json.s === 'ok') {
752
  updateUI(json.d);
753
  const dot = document.getElementById('statusDot');
754
+ dot.classList.add('active');
755
+ dot.classList.remove('error');
 
756
  }
757
+ } catch(e) {
758
+ console.error(e);
759
+ const dot = document.getElementById('statusDot');
760
+ dot.classList.remove('active');
761
+ dot.classList.add('error');
762
+ }
763
  finally { IS_POLLING = false; }
764
  }
765
 
766
  function updateUI(data) {
767
+ // 1. Update Spot
768
  if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
769
  SPOT_PRICE = data[SPOT_SYM].ltp;
770
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
771
  }
772
 
773
+ // 2. Update Futures
774
  const futHTML = FUT_SYMS.map(f => {
775
  const d = data[f.s];
 
776
  if(!d || typeof d.ltp !== 'number') return '';
777
  const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
778
  const tot = (d.totalbuyqty + d.totalsellqty) || 1;
 
786
  <div style="width:${bPct}%; background:var(--green)"></div>
787
  <div style="width:${100-bPct}%; background:var(--red)"></div>
788
  </div>
789
+ <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;">
790
  <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
791
  </div>
792
  </div>`;
793
  }).join('');
794
  if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
795
 
796
+ // 3. Process Options & Calculate Net Flow
797
  const rowData = {};
 
 
 
 
 
 
 
798
 
799
+ // We iterate through OPT_SYMS which is filtered by expiry
800
+ for(const item of OPT_SYMS) {
801
+ const d = data[item.s];
802
+ if(!d) continue;
803
+
804
+ if(!rowData[item.k]) rowData[item.k] = { ce:0, pe:0 };
805
+
806
+ if(item.t === 'CE') rowData[item.k].ce = d.totalsellqty;
807
+ if(item.t === 'PE') rowData[item.k].pe = d.totalsellqty;
808
+ }
809
+
810
+ // --- VISUALIZATION UPDATE ---
811
  let maxDiff = 0;
812
  for(const k in rowData) maxDiff = Math.max(maxDiff, Math.abs(rowData[k].pe - rowData[k].ce));
813
 
814
  for(const k in rowData) {
815
  const r = rowData[k];
816
  const delta = r.pe - r.ce;
817
+
818
+ // Only update DOM if element exists (visible viewport)
819
  const ceEl = document.getElementById(`ce-${k}`);
820
+ if(ceEl) {
821
+ ceEl.querySelector('.col-val').innerText = fmt(r.ce);
822
+ document.getElementById(`pe-${k}`).querySelector('.col-val').innerText = fmt(r.pe);
823
+
824
+ const bar = document.getElementById(`bar-${k}`);
825
+ const val = document.getElementById(`val-${k}`);
826
+
827
+ if(maxDiff > 0) {
828
+ const pct = (Math.abs(delta) / maxDiff) * 100;
829
+ if(delta >= 0) {
830
+ bar.style.background = 'var(--green-soft)';
831
+ bar.style.left = '50%';
832
+ bar.style.width = (pct/2) + '%';
833
+ val.style.color = 'var(--green)';
834
+ val.innerText = "+" + fmt(delta);
835
+ } else {
836
+ bar.style.background = 'var(--red-soft)';
837
+ bar.style.left = (50 - (pct/2)) + '%';
838
+ bar.style.width = (pct/2) + '%';
839
+ val.style.color = 'var(--red)';
840
+ val.innerText = fmt(delta);
841
+ }
842
+ }
843
+ }
844
+ }
845
+
846
+ // --- NET FLOW CALCULATION (ATM +/- Setting) ---
847
+ if(ATM_STRIKE > 0) {
848
+ let totalFlow = 0;
849
+ const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
850
+ const atmIdx = kList.indexOf(ATM_STRIKE);
851
 
852
+ if(atmIdx !== -1) {
853
+ const start = Math.max(0, atmIdx - SETTING_RANGE);
854
+ const end = Math.min(kList.length, atmIdx + SETTING_RANGE + 1);
855
+
856
+ for(let i=start; i<end; i++) {
857
+ const k = kList[i];
858
+ if(rowData[k]) {
859
+ totalFlow += (rowData[k].pe - rowData[k].ce);
860
+ }
 
 
 
 
 
 
 
861
  }
862
  }
863
+
864
+ const nfEl = document.getElementById('totalNetFlow');
865
+ const nfSig = document.getElementById('nfSignal');
866
+
867
+ nfEl.innerText = (totalFlow > 0 ? "+" : "") + fmt(totalFlow);
868
+ nfEl.style.color = totalFlow >= 0 ? 'var(--green)' : 'var(--red)';
869
+
870
+ if(totalFlow > 0) {
871
+ nfSig.innerText = "BULLISH (PUTS DOMINANT)";
872
+ nfSig.style.color = "var(--green)";
873
+ } else {
874
+ nfSig.innerText = "BEARISH (CALLS DOMINANT)";
875
+ nfSig.style.color = "var(--red)";
876
+ }
877
  }
878
  }
879
 
 
883
  if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr';
884
  if(abs>=100000) return (n/100000).toFixed(2)+'L';
885
  if(abs>=1000) return (n/1000).toFixed(1)+'k';
886
+ return n.toLocaleString();
887
  }
888
  </script>
889
  </body>