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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +122 -55
app.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import pandas as pd
2
  import requests
3
  import uvicorn
@@ -38,22 +40,27 @@ SYSTEM_STATUS = {
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",
44
  "FINNIFTY": "NSE:NIFTYFIN-INDEX",
45
- "MIDCPNIFTY": "NSE:MIDCPNIFTY-INDEX"
 
 
 
46
  }
47
 
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
@@ -65,7 +72,7 @@ def update_master_db():
65
  return
66
 
67
  try:
68
- SYSTEM_STATUS["message"] = "Downloading Master Data..."
69
  print(">>> [BG TASK] STARTING DOWNLOAD...")
70
 
71
  headers = { "User-Agent": "Mozilla/5.0" }
@@ -77,19 +84,30 @@ def update_master_db():
77
  names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
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],
85
  names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
86
  header=0, on_bad_lines='skip', storage_options=headers
87
  )
 
 
 
 
 
 
 
 
 
 
88
 
89
  SYSTEM_STATUS["message"] = "Processing Data..."
90
 
91
  # Merge Dataframes
92
- df = pd.concat([df_nse, df_mcx], ignore_index=True)
93
 
94
  df['Symbol'] = df['Symbol'].astype(str)
95
  df['Desc'] = df['Desc'].astype(str)
@@ -99,21 +117,28 @@ def update_master_db():
99
  for _, row in df.iterrows():
100
  sym = row['Symbol']
101
  desc = row['Desc']
 
102
 
103
  root = get_root_symbol(desc, sym)
104
 
105
  # Initialize Root Entry
106
  if root not in temp_db:
107
  spot_sym = ""
 
 
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"
115
 
116
- temp_db[root] = { "spot": spot_sym, "items": [] }
117
 
118
  try:
119
  exp = int(row['Expiry'])
@@ -124,13 +149,14 @@ def update_master_db():
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)
@@ -142,19 +168,32 @@ def update_master_db():
142
  "s": sym, "e": exp, "k": strike, "t": opt_type
143
  })
144
 
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
 
159
  SYSTEM_STATUS["ready"] = True
160
  SYSTEM_STATUS["message"] = "Ready"
@@ -173,8 +212,9 @@ def startup_event():
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()))
 
178
  SYSTEM_STATUS["ready"] = True
179
  SYSTEM_STATUS["message"] = "Ready (Cached)"
180
  except:
@@ -236,7 +276,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 +284,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([])
@@ -255,11 +294,14 @@ def search_symbol(q: str = Query(..., min_length=1)):
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
 
@@ -286,7 +328,7 @@ 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>
@@ -296,6 +338,10 @@ HTML_TEMPLATE = """
296
  --accent: #2962ff; --green: #00c853; --red: #d50000;
297
  --green-soft: rgba(0, 200, 83, 0.3); --red-soft: rgba(213, 0, 0, 0.3);
298
  --atm-bg: rgba(41, 98, 255, 0.1);
 
 
 
 
299
  }
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; }
@@ -304,12 +350,18 @@ HTML_TEMPLATE = """
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; }
@@ -366,7 +418,7 @@ HTML_TEMPLATE = """
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
 
@@ -450,7 +502,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();
@@ -462,8 +514,12 @@ HTML_TEMPLATE = """
462
  try {
463
  const res = await fetch(`/api/search?q=${q}`);
464
  const data = await res.json();
465
- resDiv.innerHTML = data.map(sym =>
466
- `<div class="search-item" onclick="loadSymbol('${sym}')">${sym}</div>`
 
 
 
 
467
  ).join('');
468
  resDiv.style.display = 'block';
469
  } catch(e) { console.error(e); }
@@ -487,11 +543,12 @@ HTML_TEMPLATE = """
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";
495
 
496
  setupExpiries();
497
  document.getElementById('loader').style.display = 'none';
@@ -508,7 +565,13 @@ HTML_TEMPLATE = """
508
  const today = Math.floor(Date.now()/1000) - 86400;
509
 
510
  const validExps = exps.filter(ts => ts > today);
511
- if(validExps.length === 0) { alert("No active expiries."); return; }
 
 
 
 
 
 
512
 
513
  ACTIVE_EXP = validExps[0];
514
  updateExpText(ACTIVE_EXP);
@@ -572,30 +635,34 @@ HTML_TEMPLATE = """
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);
 
1
+ --- START OF FILE app.py.txt ---
2
+
3
  import pandas as pd
4
  import requests
5
  import uvicorn
 
40
  UPDATE_LOCK = threading.Lock()
41
 
42
  # Manual mapping for Index Spot symbols
43
+ # Added BSE Indices (SENSEX, BANKEX)
44
  INDEX_MAP = {
45
  "NIFTY": "NSE:NIFTY50-INDEX",
46
  "BANKNIFTY": "NSE:NIFTYBANK-INDEX",
47
  "FINNIFTY": "NSE:NIFTYFIN-INDEX",
48
+ "MIDCPNIFTY": "NSE:MIDCPNIFTY-INDEX",
49
+ "SENSEX": "BSE:SENSEX-INDEX",
50
+ "BANKEX": "BSE:BANKEX-INDEX",
51
+ "SENSEX50": "BSE:SENSEX50-INDEX"
52
  }
53
 
54
  def get_root_symbol(desc: str, symbol: str) -> str:
55
+ """Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
56
  try:
57
+ # Standard Format: "SENSEX 19 JAN 72000 CE" or "RELIANCE 24 JAN..."
 
58
  parts = desc.strip().split(' ')
59
+ root = parts[0].upper()
60
+ # Clean up potential artifacts
61
+ return root.replace(':', '').replace('-', '')
62
  except:
63
+ # Fallback to symbol parsing if description fails
64
  if ':' in symbol:
65
  return symbol.split(':')[1]
66
  return symbol
 
72
  return
73
 
74
  try:
75
+ SYSTEM_STATUS["message"] = "Downloading Master Data (NSE, MCX, BSE)..."
76
  print(">>> [BG TASK] STARTING DOWNLOAD...")
77
 
78
  headers = { "User-Agent": "Mozilla/5.0" }
 
84
  names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
85
  header=0, on_bad_lines='skip', storage_options=headers
86
  )
87
+ df_nse['Exch'] = 'NSE'
88
 
89
+ # 2. Download MCX F&O
90
  url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
91
  df_mcx = pd.read_csv(
92
  url_mcx, usecols=[0, 1, 8, 9, 13],
93
  names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
94
  header=0, on_bad_lines='skip', storage_options=headers
95
  )
96
+ df_mcx['Exch'] = 'MCX'
97
+
98
+ # 3. Download BSE F&O (For SENSEX etc.)
99
+ url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
100
+ df_bse = pd.read_csv(
101
+ url_bse, usecols=[0, 1, 8, 9, 13],
102
+ names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
103
+ header=0, on_bad_lines='skip', storage_options=headers
104
+ )
105
+ df_bse['Exch'] = 'BSE'
106
 
107
  SYSTEM_STATUS["message"] = "Processing Data..."
108
 
109
  # Merge Dataframes
110
+ df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
111
 
112
  df['Symbol'] = df['Symbol'].astype(str)
113
  df['Desc'] = df['Desc'].astype(str)
 
117
  for _, row in df.iterrows():
118
  sym = row['Symbol']
119
  desc = row['Desc']
120
+ exch = row['Exch']
121
 
122
  root = get_root_symbol(desc, sym)
123
 
124
  # Initialize Root Entry
125
  if root not in temp_db:
126
  spot_sym = ""
127
+
128
+ # Check for Index Map match first
129
  if root in INDEX_MAP:
130
  spot_sym = INDEX_MAP[root]
131
+ elif exch == "MCX":
132
+ # MCX has no "Equity Spot", usually we track the Future
133
  spot_sym = "MCX"
134
+ elif exch == "BSE":
135
+ # BSE Equity Spot usually "BSE:Symbol"
136
+ spot_sym = f"BSE:{root}"
137
  else:
138
+ # NSE Equity Spot
139
  spot_sym = f"NSE:{root}-EQ"
140
 
141
+ temp_db[root] = { "spot": spot_sym, "exch": exch, "items": [] }
142
 
143
  try:
144
  exp = int(row['Expiry'])
 
149
  opt_type = "FUT"
150
 
151
  # Parse Strike & Option Type
152
+ # Logic handles "SENSEX 23 JAN 72000 CE"
153
  if "CE" in sym or "PE" in sym:
154
  parts = desc.strip().split(' ')
155
  if len(parts) >= 2:
156
  try:
157
+ # Logic: val is usually 2nd last item
158
  val = parts[-2]
159
+ # Remove decimal for digit check
160
  val_clean = val.replace('.', '', 1)
161
  if val_clean.isdigit():
162
  strike = float(val)
 
168
  "s": sym, "e": exp, "k": strike, "t": opt_type
169
  })
170
 
171
+ # Final Sort & Search Index Creation
172
  final_db = {}
173
+ search_list = []
174
+
175
  for root, data in temp_db.items():
176
  # Sort by Expiry (asc) then Strike (asc)
177
  sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
178
+ final_db[root] = {
179
+ "spot": data["spot"],
180
+ "exch": data["exch"],
181
+ "items": sorted_items
182
+ }
183
+ # Create a rich search object
184
+ search_list.append({
185
+ "root": root,
186
+ "exch": data["exch"],
187
+ "display": f"{root}"
188
+ })
189
 
190
  MASTER_DB = final_db
191
+ # Sort search index alphabetically
192
+ SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
193
 
194
  # Save cache
195
  with open(CACHE_FILE, "w") as f:
196
+ json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
197
 
198
  SYSTEM_STATUS["ready"] = True
199
  SYSTEM_STATUS["message"] = "Ready"
 
212
  try:
213
  print(">>> STARTUP: Loading Cache...")
214
  with open(CACHE_FILE, "r") as f:
215
+ data = json.load(f)
216
+ MASTER_DB = data["db"]
217
+ SEARCH_INDEX = data["idx"]
218
  SYSTEM_STATUS["ready"] = True
219
  SYSTEM_STATUS["message"] = "Ready (Cached)"
220
  except:
 
276
 
277
  @app.get("/favicon.ico")
278
  def favicon():
279
+ return Response(status_code=204)
280
 
281
  # --- API ---
282
 
 
284
  def get_status():
285
  return JSONResponse(SYSTEM_STATUS)
286
 
 
287
  @app.get("/api/init")
288
  def legacy_init():
289
  return JSONResponse([])
 
294
  return JSONResponse([])
295
 
296
  query = q.upper()
297
+ # SEARCH_INDEX is now a list of dicts: {'root': 'NIFTY', 'exch': 'NSE', 'display': 'NIFTY'}
298
+
299
  # 1. Starts with
300
+ results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
301
+
302
  # 2. Contains (if few results)
303
  if len(results) < 10:
304
+ results += [x for x in SEARCH_INDEX if query in x['root'] and x not in results]
305
 
306
  return JSONResponse(results[:20])
307
 
 
328
  <html lang="en">
329
  <head>
330
  <meta charset="UTF-8">
331
+ <title>PRO CHAIN (V3)</title>
332
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
333
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
334
  <style>
 
338
  --accent: #2962ff; --green: #00c853; --red: #d50000;
339
  --green-soft: rgba(0, 200, 83, 0.3); --red-soft: rgba(213, 0, 0, 0.3);
340
  --atm-bg: rgba(41, 98, 255, 0.1);
341
+ /* Badge Colors */
342
+ --badge-nse: #00c853;
343
+ --badge-bse: #ff6d00;
344
+ --badge-mcx: #2962ff;
345
  }
346
  * { box-sizing: border-box; }
347
  body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
 
350
  .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; }
351
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
352
 
353
+ .search-box { position: relative; width: 350px; margin-right: 20px; }
354
  .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; }
355
  .search-input:focus { border-color: var(--accent); }
356
+ .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; }
357
+
358
+ .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; }
359
  .search-item:hover { background: #f0f7ff; color: var(--accent); }
360
+
361
+ .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
362
+ .badge-NSE { background: var(--badge-nse); }
363
+ .badge-BSE { background: var(--badge-bse); }
364
+ .badge-MCX { background: var(--badge-mcx); }
365
 
366
  .grid-layout { display: flex; height: calc(100vh - 60px); }
367
  .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
 
418
  <div class="top-nav">
419
  <div style="display:flex; align-items:center;">
420
  <div class="search-box">
421
+ <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g., SENSEX, NIFTY)..." autocomplete="off">
422
  <div class="search-dropdown" id="searchRes"></div>
423
  </div>
424
 
 
502
  }
503
 
504
  async function refreshDB() {
505
+ if(!confirm("Download fresh Master Data (NSE, MCX, BSE)? This takes ~15s.")) return;
506
  document.getElementById('loader').style.display = 'flex';
507
  await fetch('/api/refresh');
508
  checkStatus();
 
514
  try {
515
  const res = await fetch(`/api/search?q=${q}`);
516
  const data = await res.json();
517
+ // Data is now list of {root, exch, display}
518
+ resDiv.innerHTML = data.map(item =>
519
+ `<div class="search-item" onclick="loadSymbol('${item.root}')">
520
+ <span>${item.display}</span>
521
+ <span class="exch-badge badge-${item.exch}">${item.exch}</span>
522
+ </div>`
523
  ).join('');
524
  resDiv.style.display = 'block';
525
  } catch(e) { console.error(e); }
 
543
  // Sort futures: Filter items with type 'FUT'
544
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
545
 
546
+ // Handle MCX specific spot logic
547
  if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
548
+ SPOT_SYM = FUT_SYMS[0].s;
549
  }
550
 
551
+ document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ') || SPOT_SYM.startsWith('BSE:')) ? "SPOT" : "FUT REF";
552
 
553
  setupExpiries();
554
  document.getElementById('loader').style.display = 'none';
 
565
  const today = Math.floor(Date.now()/1000) - 86400;
566
 
567
  const validExps = exps.filter(ts => ts > today);
568
+ if(validExps.length === 0) {
569
+ alert("No active options found for this symbol.");
570
+ // Still render Futures if available
571
+ document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
572
+ updateUI({});
573
+ return;
574
+ }
575
 
576
  ACTIVE_EXP = validExps[0];
577
  updateExpText(ACTIVE_EXP);
 
635
  try {
636
  let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
637
 
638
+ // Logic to fetch only visible/near options
639
  if(SPOT_PRICE > 0) {
640
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
641
+ if(kList.length > 0) {
642
+ let closestK = kList[0], minDiff = 999999;
643
+ kList.forEach(k => {
644
+ const diff = Math.abs(SPOT_PRICE - k);
645
+ if(diff < minDiff) { minDiff = diff; closestK = k; }
646
+ });
647
+
648
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
649
+ const atmRow = document.getElementById(`row-${closestK}`);
650
+ if(atmRow) {
651
+ atmRow.classList.add('atm');
652
+ if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
653
+ }
654
+
655
+ const centerIdx = kList.indexOf(closestK);
656
+ const start = Math.max(0, centerIdx - 12);
657
+ const end = Math.min(kList.length, centerIdx + 12);
658
+ const viewKs = kList.slice(start, end);
659
+
660
+ OPT_SYMS.forEach(o => {
661
+ if(viewKs.includes(o.k)) symbolsToFetch.push(o.s);
662
+ });
663
  }
 
 
 
 
 
 
 
 
 
664
  } else {
665
+ // Fallback if no spot price yet (initial load)
666
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
667
  const mid = Math.floor(kList.length/2);
668
  const viewKs = kList.slice(Math.max(0, mid-10), mid+10);