topsecrettraders commited on
Commit
b2e6cdd
·
verified ·
1 Parent(s): 0ed0c4f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +64 -131
app.py CHANGED
@@ -17,7 +17,7 @@ from typing import Optional, List, Dict
17
  # ==========================================
18
  CLIENT_ID = "J4PNT5QI22-100"
19
  SECRET_KEY = "ABRSWC1PMR"
20
- # Ensure this matches your Space URL exactly
21
  REDIRECT_URI = "https://topsecrettraders-depthchain.hf.space/callback"
22
 
23
  CACHE_FILE = "master_cache.json"
@@ -28,10 +28,8 @@ app = FastAPI()
28
  # 2. ROBUST DATA LOADER (THREAD SAFE)
29
  # ==========================================
30
 
31
- # Global In-Memory Database
32
- # Structure: { "NIFTY": { "spot": "NSE:...", "data": [...] }, "RELIANCE": ... }
33
  MASTER_DB = {}
34
- SEARCH_INDEX = [] # List of available root symbols for autocomplete
35
 
36
  SYSTEM_STATUS = {
37
  "ready": False,
@@ -39,7 +37,7 @@ SYSTEM_STATUS = {
39
  }
40
  UPDATE_LOCK = threading.Lock()
41
 
42
- # Manual mapping for Index Spot symbols (Fyers specific)
43
  INDEX_MAP = {
44
  "NIFTY": "NSE:NIFTY50-INDEX",
45
  "BANKNIFTY": "NSE:NIFTYBANK-INDEX",
@@ -48,27 +46,26 @@ INDEX_MAP = {
48
  }
49
 
50
  def get_root_symbol(desc: str, symbol: str) -> str:
51
- """Extracts the root symbol (e.g. 'RELIANCE', 'CRUDEOIL') from description."""
52
- # Description format is usually "SYMBOL YY MMM STRIKE TYPE"
53
- # e.g., "BANKNIFTY 24 JAN 48000 CE" -> BANKNIFTY
54
- # e.g., "M&M 24 JAN ..." -> M&M
55
  try:
 
 
56
  parts = desc.strip().split(' ')
57
  return parts[0].upper()
58
  except:
59
- return symbol.split(':')[1] # Fallback
 
 
 
60
 
61
  def update_master_db():
62
- """
63
- Downloads NSE and MCX F&O files, processes them, and builds a search index.
64
- """
65
  global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
66
 
67
  if not UPDATE_LOCK.acquire(blocking=False):
68
  return
69
 
70
  try:
71
- SYSTEM_STATUS["message"] = "Downloading Master Data (NSE & MCX)..."
72
  print(">>> [BG TASK] STARTING DOWNLOAD...")
73
 
74
  headers = { "User-Agent": "Mozilla/5.0" }
@@ -81,8 +78,8 @@ def update_master_db():
81
  header=0, on_bad_lines='skip', storage_options=headers
82
  )
83
 
84
- # 2. Download MCX F&O
85
- url_mcx = "https://public.fyers.in/sym_details/MCX_FO.csv"
86
  df_mcx = pd.read_csv(
87
  url_mcx, usecols=[0, 1, 8, 9, 13],
88
  names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
@@ -97,86 +94,71 @@ def update_master_db():
97
  df['Symbol'] = df['Symbol'].astype(str)
98
  df['Desc'] = df['Desc'].astype(str)
99
 
100
- # Temporary dictionary to build structure
101
  temp_db = {}
102
 
103
- count = 0
104
  for _, row in df.iterrows():
105
  sym = row['Symbol']
106
  desc = row['Desc']
107
 
108
- # Extract Root
109
  root = get_root_symbol(desc, sym)
110
 
111
- # Initialize Root Entry if missing
112
  if root not in temp_db:
113
- # Determine Spot Symbol
114
  spot_sym = ""
115
  if root in INDEX_MAP:
116
  spot_sym = INDEX_MAP[root]
117
  elif sym.startswith("MCX"):
118
- spot_sym = "MCX" # Placeholder, MCX uses Future as spot
 
119
  else:
120
- # Stock Spot assumption
121
  spot_sym = f"NSE:{root}-EQ"
122
 
123
- temp_db[root] = {
124
- "spot": spot_sym,
125
- "items": []
126
- }
127
 
128
- # Parse Details
129
  try:
130
  exp = int(row['Expiry'])
131
  except:
132
  exp = 0
133
 
134
  strike = 0.0
135
- opt_type = "FUT" # Default
136
 
 
137
  if "CE" in sym or "PE" in sym:
138
- # Logic: Parse Strike from Desc (e.g., "NIFTY 23 JAN 21500 CE")
139
  parts = desc.strip().split(' ')
140
- # Iterate backwards to find the number before CE/PE
141
  if len(parts) >= 2:
142
  try:
143
- # Usually the second to last is strike, last is CE/PE
144
  val = parts[-2]
 
145
  val_clean = val.replace('.', '', 1)
146
  if val_clean.isdigit():
147
  strike = float(val)
148
- opt_type = parts[-1] # CE or PE
149
  except:
150
  pass
151
 
152
  temp_db[root]["items"].append({
153
- "s": sym, # Symbol
154
- "e": exp, # Expiry Epoch
155
- "k": strike, # Strike
156
- "t": opt_type # Type: CE, PE, FUT
157
  })
158
- count += 1
159
 
160
- # Optimize: Sort items by expiry then strike for each root
161
  final_db = {}
162
  for root, data in temp_db.items():
 
163
  sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
164
- final_db[root] = {
165
- "spot": data["spot"],
166
- "items": sorted_items
167
- }
168
 
169
- # Update Global State
170
  MASTER_DB = final_db
171
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
172
 
173
- # Save cache (Optional, for faster reload)
174
  with open(CACHE_FILE, "w") as f:
175
  json.dump(final_db, f)
176
 
177
  SYSTEM_STATUS["ready"] = True
178
  SYSTEM_STATUS["message"] = "Ready"
179
- print(f">>> [BG TASK] COMPLETE. Processed {len(SEARCH_INDEX)} assets ({count} symbols).")
180
 
181
  except Exception as e:
182
  print(f">>> [BG TASK] ERROR: {e}")
@@ -189,17 +171,16 @@ def startup_event():
189
  global MASTER_DB, SEARCH_INDEX
190
  if os.path.exists(CACHE_FILE):
191
  try:
192
- print(">>> STARTUP: Loading from Cache...")
193
  with open(CACHE_FILE, "r") as f:
194
  MASTER_DB = json.load(f)
195
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
196
  SYSTEM_STATUS["ready"] = True
197
  SYSTEM_STATUS["message"] = "Ready (Cached)"
198
  except:
199
- # If cache fails, start fresh
200
- thread = threading.Thread(target=update_master_db)
201
- thread.start()
202
- else:
203
  thread = threading.Thread(target=update_master_db)
204
  thread.start()
205
 
@@ -225,27 +206,27 @@ def home(access_token: Optional[str] = Cookie(None)):
225
  </div>
226
  """)
227
 
228
- rendered_html = HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token)
229
- return HTMLResponse(rendered_html)
230
 
231
  @app.get("/callback")
232
  def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
233
  final_code = auth_code if auth_code else code
234
- if not final_code: return JSONResponse({"error": "No Code"})
235
 
236
  app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
237
  resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
238
  "grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
239
  })
240
- data = resp.json()
241
 
 
242
  if "access_token" in data:
243
  full_token = f"{CLIENT_ID}:{data['access_token']}"
244
  response = RedirectResponse(url="/")
245
  response.set_cookie(key="access_token", value=full_token, httponly=True)
246
  return response
247
  else:
248
- return HTMLResponse(f"Login Failed: {json.dumps(data)}")
249
 
250
  @app.get("/logout")
251
  def logout():
@@ -253,43 +234,41 @@ def logout():
253
  response.delete_cookie("access_token")
254
  return response
255
 
256
- # --- API ENDPOINTS ---
 
 
 
 
257
 
258
  @app.get("/api/status")
259
  def get_status():
260
  return JSONResponse(SYSTEM_STATUS)
261
 
 
 
 
 
 
262
  @app.get("/api/search")
263
  def search_symbol(q: str = Query(..., min_length=1)):
264
- """
265
- Real-time search against loaded master data.
266
- """
267
  if not SYSTEM_STATUS["ready"]:
268
  return JSONResponse([])
269
 
270
  query = q.upper()
271
- # Simple logic: Filter list where root starts with QUERY
272
  results = [x for x in SEARCH_INDEX if x.startswith(query)]
273
-
274
- # If few results, try 'contains'
275
- if len(results) < 5:
276
  results += [x for x in SEARCH_INDEX if query in x and x not in results]
277
 
278
- return JSONResponse(results[:20]) # Limit to 20 results
279
 
280
  @app.get("/api/chain")
281
  def get_chain(symbol: str):
282
- """
283
- Returns the option chain structure for a specific root symbol.
284
- """
285
- if not SYSTEM_STATUS["ready"]:
286
- return JSONResponse({"error": "System not ready"}, status_code=503)
287
-
288
  root = symbol.upper()
289
  if root in MASTER_DB:
290
  return JSONResponse(MASTER_DB[root])
291
- else:
292
- return JSONResponse({"error": "Symbol not found"}, status_code=404)
293
 
294
  @app.get("/api/refresh")
295
  def force_refresh(background_tasks: BackgroundTasks):
@@ -307,7 +286,7 @@ HTML_TEMPLATE = """
307
  <html lang="en">
308
  <head>
309
  <meta charset="UTF-8">
310
- <title>PRO TRADER CHAIN</title>
311
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
312
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
313
  <style>
@@ -325,7 +304,6 @@ HTML_TEMPLATE = """
325
  .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; }
326
  .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
327
 
328
- /* SEARCH BAR */
329
  .search-box { position: relative; width: 300px; margin-right: 20px; }
330
  .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; }
331
  .search-input:focus { border-color: var(--accent); }
@@ -387,9 +365,8 @@ HTML_TEMPLATE = """
387
 
388
  <div class="top-nav">
389
  <div style="display:flex; align-items:center;">
390
- <!-- SEARCH BAR -->
391
  <div class="search-box">
392
- <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g., NIFTY, RELIANCE)..." autocomplete="off">
393
  <div class="search-dropdown" id="searchRes"></div>
394
  </div>
395
 
@@ -433,33 +410,26 @@ HTML_TEMPLATE = """
433
  <script>
434
  const AUTH_TOKEN = "{{USER_TOKEN}}";
435
 
436
- // State
437
  let CURRENT_ROOT = "";
438
  let CHAIN_DATA = null;
439
  let ACTIVE_EXP = 0;
440
  let SPOT_SYM = "";
441
  let SPOT_PRICE = 0;
442
-
443
  let FUT_SYMS = [];
444
  let OPT_SYMS = [];
445
-
446
  let POLLER = null;
447
  let IS_POLLING = false;
448
  let SEARCH_TIMER = null;
449
 
450
- // === STARTUP ===
451
  window.onload = () => {
452
  if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
453
  checkStatus();
454
 
455
- // Setup Search
456
  const inp = document.getElementById('searchInp');
457
  inp.addEventListener('input', (e) => {
458
  clearTimeout(SEARCH_TIMER);
459
  SEARCH_TIMER = setTimeout(() => doSearch(e.target.value), 300);
460
  });
461
-
462
- // Hide dropdown on click outside
463
  document.addEventListener('click', (e) => {
464
  if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
465
  });
@@ -471,7 +441,6 @@ HTML_TEMPLATE = """
471
  const status = await res.json();
472
  if(status.ready) {
473
  document.getElementById('loader').style.display = 'none';
474
- // Default load NIFTY if not loaded
475
  if(!CURRENT_ROOT) loadSymbol('NIFTY');
476
  } else {
477
  document.getElementById('loaderMsg').innerText = status.message.toUpperCase();
@@ -481,21 +450,18 @@ HTML_TEMPLATE = """
481
  }
482
 
483
  async function refreshDB() {
484
- if(!confirm("Download fresh Master Data from Fyers? Takes ~10s.")) return;
485
  document.getElementById('loader').style.display = 'flex';
486
  await fetch('/api/refresh');
487
  checkStatus();
488
  }
489
 
490
- // === SEARCH LOGIC ===
491
  async function doSearch(q) {
492
  const resDiv = document.getElementById('searchRes');
493
  if(q.length < 2) { resDiv.style.display = 'none'; return; }
494
-
495
  try {
496
  const res = await fetch(`/api/search?q=${q}`);
497
  const data = await res.json();
498
-
499
  resDiv.innerHTML = data.map(sym =>
500
  `<div class="search-item" onclick="loadSymbol('${sym}')">${sym}</div>`
501
  ).join('');
@@ -503,7 +469,6 @@ HTML_TEMPLATE = """
503
  } catch(e) { console.error(e); }
504
  }
505
 
506
- // === LOAD SYMBOL DATA ===
507
  async function loadSymbol(root) {
508
  document.getElementById('searchInp').value = root;
509
  document.getElementById('searchRes').style.display = 'none';
@@ -519,13 +484,11 @@ HTML_TEMPLATE = """
519
  CHAIN_DATA = data.items;
520
  SPOT_SYM = data.spot;
521
 
522
- // Sort futures (filter items ending in FUT, usually no expiry in symbol but checked via type)
523
- // Backend provides 't': 'FUT'
524
  FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
525
 
526
- // If Spot symbol is MCX (placeholder), use nearest Future as spot
527
  if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
528
- SPOT_SYM = FUT_SYMS[0].s;
529
  }
530
 
531
  document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ')) ? "SPOT" : "FUT REF";
@@ -534,7 +497,7 @@ HTML_TEMPLATE = """
534
  document.getElementById('loader').style.display = 'none';
535
 
536
  } catch(e) {
537
- alert("Error loading symbol: " + e.message);
538
  document.getElementById('loader').style.display = 'none';
539
  }
540
  }
@@ -545,13 +508,11 @@ HTML_TEMPLATE = """
545
  const today = Math.floor(Date.now()/1000) - 86400;
546
 
547
  const validExps = exps.filter(ts => ts > today);
548
- if(validExps.length === 0) { alert("No active expiries found."); return; }
549
 
550
- // Auto select nearest
551
  ACTIVE_EXP = validExps[0];
552
  updateExpText(ACTIVE_EXP);
553
 
554
- // Build Grid
555
  const grid = document.getElementById('expGrid');
556
  grid.innerHTML = validExps.map(ts => {
557
  const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
@@ -575,11 +536,8 @@ HTML_TEMPLATE = """
575
 
576
  function closeModal() { document.getElementById('expModal').style.display = 'none'; }
577
 
578
- // === BUILD UI ===
579
  function buildChain() {
580
  const body = document.getElementById('chainBody');
581
-
582
- // Filter options for active expiry
583
  const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
584
  OPT_SYMS = relevant;
585
 
@@ -597,7 +555,7 @@ HTML_TEMPLATE = """
597
  </div>
598
  `).join('');
599
 
600
- window.scrolled = false; // Reset scroll lock
601
  restartPoller();
602
  }
603
 
@@ -607,27 +565,21 @@ HTML_TEMPLATE = """
607
  pollData();
608
  }
609
 
610
- // === POLLING ===
611
  async function pollData() {
612
  if(IS_POLLING || !CURRENT_ROOT) return;
613
  IS_POLLING = true;
614
 
615
  try {
616
- // Logic to fetch only visible strikes + spot + futures
617
  let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
618
 
619
- // If we have a spot price, filter strikes around it
620
  if(SPOT_PRICE > 0) {
621
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
622
-
623
- // Find closest strike index
624
  let closestK = kList[0], minDiff = 999999;
625
  kList.forEach(k => {
626
  const diff = Math.abs(SPOT_PRICE - k);
627
  if(diff < minDiff) { minDiff = diff; closestK = k; }
628
  });
629
 
630
- // Highlight ATM
631
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
632
  const atmRow = document.getElementById(`row-${closestK}`);
633
  if(atmRow) {
@@ -635,32 +587,26 @@ HTML_TEMPLATE = """
635
  if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
636
  }
637
 
638
- // Get range of strikes to fetch (ATM +/- 12)
639
  const centerIdx = kList.indexOf(closestK);
640
  const start = Math.max(0, centerIdx - 12);
641
  const end = Math.min(kList.length, centerIdx + 12);
642
  const viewKs = kList.slice(start, end);
643
 
644
- // Add option symbols to fetch list
645
  OPT_SYMS.forEach(o => {
646
  if(viewKs.includes(o.k)) symbolsToFetch.push(o.s);
647
  });
648
  } else {
649
- // First run, fetch middle strikes
650
  const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
651
  const mid = Math.floor(kList.length/2);
652
  const viewKs = kList.slice(Math.max(0, mid-10), mid+10);
653
  OPT_SYMS.forEach(o => { if(viewKs.includes(o.k)) symbolsToFetch.push(o.s); });
654
  }
655
 
656
- const uniqueSyms = [...new Set(symbolsToFetch)];
657
  if(uniqueSyms.length === 0) return;
658
 
659
  const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
660
-
661
- const res = await fetch(url, {
662
- headers: { 'Authorization': AUTH_TOKEN, 'Content-Type': 'application/json' }
663
- });
664
 
665
  if(res.status === 401) { location.href = '/logout'; return; }
666
 
@@ -672,22 +618,16 @@ HTML_TEMPLATE = """
672
  dot.classList.add('live');
673
  setTimeout(()=>dot.classList.remove('live'), 500);
674
  }
675
- } catch(e) {
676
- console.error(e);
677
- document.getElementById('statusDot').style.background = 'var(--red)';
678
- } finally {
679
- IS_POLLING = false;
680
- }
681
  }
682
 
683
  function updateUI(data) {
684
- // 1. Spot
685
  if(data[SPOT_SYM]) {
686
  SPOT_PRICE = data[SPOT_SYM].ltp;
687
  document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
688
  }
689
 
690
- // 2. Futures
691
  const futHTML = FUT_SYMS.map(f => {
692
  const d = data[f.s];
693
  if(!d) return '';
@@ -710,13 +650,10 @@ HTML_TEMPLATE = """
710
  }).join('');
711
  if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
712
 
713
- // 3. Chain
714
  const rowData = {};
715
  for(const [sym, d] of Object.entries(data)) {
716
- // Find which option this is
717
  const meta = OPT_SYMS.find(x => x.s === sym);
718
  if(!meta) continue;
719
-
720
  if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
721
  if(meta.t === 'CE') rowData[meta.k].ce = d.totalsellqty;
722
  if(meta.t === 'PE') rowData[meta.k].pe = d.totalsellqty;
@@ -728,17 +665,13 @@ HTML_TEMPLATE = """
728
  for(const k in rowData) {
729
  const r = rowData[k];
730
  const delta = r.pe - r.ce;
731
-
732
- // Update Text
733
  const ceEl = document.getElementById(`ce-${k}`);
734
  const peEl = document.getElementById(`pe-${k}`);
735
  if(ceEl) ceEl.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
736
  if(peEl) peEl.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
737
 
738
- // Update Bars
739
  const bar = document.getElementById(`bar-${k}`);
740
  const val = document.getElementById(`val-${k}`);
741
-
742
  if(bar && maxDiff > 0) {
743
  const pct = (Math.abs(delta) / maxDiff) * 100;
744
  if(delta >= 0) {
 
17
  # ==========================================
18
  CLIENT_ID = "J4PNT5QI22-100"
19
  SECRET_KEY = "ABRSWC1PMR"
20
+ # IMPORTANT: Ensure this matches your Space URL exactly
21
  REDIRECT_URI = "https://topsecrettraders-depthchain.hf.space/callback"
22
 
23
  CACHE_FILE = "master_cache.json"
 
28
  # 2. ROBUST DATA LOADER (THREAD SAFE)
29
  # ==========================================
30
 
 
 
31
  MASTER_DB = {}
32
+ SEARCH_INDEX = []
33
 
34
  SYSTEM_STATUS = {
35
  "ready": False,
 
37
  }
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",
 
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
60
 
61
  def update_master_db():
 
 
 
62
  global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
63
 
64
  if not UPDATE_LOCK.acquire(blocking=False):
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" }
 
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'],
 
94
  df['Symbol'] = df['Symbol'].astype(str)
95
  df['Desc'] = df['Desc'].astype(str)
96
 
 
97
  temp_db = {}
98
 
 
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'])
120
  except:
121
  exp = 0
122
 
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)
137
+ opt_type = parts[-1]
138
  except:
139
  pass
140
 
141
  temp_db[root]["items"].append({
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"
161
+ print(f">>> [BG TASK] COMPLETE. Loaded {len(SEARCH_INDEX)} symbols.")
162
 
163
  except Exception as e:
164
  print(f">>> [BG TASK] ERROR: {e}")
 
171
  global MASTER_DB, SEARCH_INDEX
172
  if os.path.exists(CACHE_FILE):
173
  try:
174
+ print(">>> STARTUP: Loading Cache...")
175
  with open(CACHE_FILE, "r") as f:
176
  MASTER_DB = json.load(f)
177
  SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
178
  SYSTEM_STATUS["ready"] = True
179
  SYSTEM_STATUS["message"] = "Ready (Cached)"
180
  except:
181
+ pass
182
+
183
+ if not SYSTEM_STATUS["ready"]:
 
184
  thread = threading.Thread(target=update_master_db)
185
  thread.start()
186
 
 
206
  </div>
207
  """)
208
 
209
+ # Inject token
210
+ return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
211
 
212
  @app.get("/callback")
213
  def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
214
  final_code = auth_code if auth_code else code
215
+ if not final_code: return JSONResponse({"error": "No Code Provided"})
216
 
217
  app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
218
  resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
219
  "grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
220
  })
 
221
 
222
+ data = resp.json()
223
  if "access_token" in data:
224
  full_token = f"{CLIENT_ID}:{data['access_token']}"
225
  response = RedirectResponse(url="/")
226
  response.set_cookie(key="access_token", value=full_token, httponly=True)
227
  return response
228
  else:
229
+ return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(data, indent=2)}</pre>")
230
 
231
  @app.get("/logout")
232
  def logout():
 
234
  response.delete_cookie("access_token")
235
  return response
236
 
237
+ @app.get("/favicon.ico")
238
+ def favicon():
239
+ return Response(status_code=204) # No content, stops 404 errors
240
+
241
+ # --- API ---
242
 
243
  @app.get("/api/status")
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([])
251
+
252
  @app.get("/api/search")
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")
267
  def get_chain(symbol: str):
 
 
 
 
 
 
268
  root = symbol.upper()
269
  if root in MASTER_DB:
270
  return JSONResponse(MASTER_DB[root])
271
+ return JSONResponse({"error": "Symbol not found"}, status_code=404)
 
272
 
273
  @app.get("/api/refresh")
274
  def force_refresh(background_tasks: BackgroundTasks):
 
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>
 
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); }
 
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
 
 
410
  <script>
411
  const AUTH_TOKEN = "{{USER_TOKEN}}";
412
 
 
413
  let CURRENT_ROOT = "";
414
  let CHAIN_DATA = null;
415
  let ACTIVE_EXP = 0;
416
  let SPOT_SYM = "";
417
  let SPOT_PRICE = 0;
 
418
  let FUT_SYMS = [];
419
  let OPT_SYMS = [];
 
420
  let POLLER = null;
421
  let IS_POLLING = false;
422
  let SEARCH_TIMER = null;
423
 
 
424
  window.onload = () => {
425
  if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
426
  checkStatus();
427
 
 
428
  const inp = document.getElementById('searchInp');
429
  inp.addEventListener('input', (e) => {
430
  clearTimeout(SEARCH_TIMER);
431
  SEARCH_TIMER = setTimeout(() => doSearch(e.target.value), 300);
432
  });
 
 
433
  document.addEventListener('click', (e) => {
434
  if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
435
  });
 
441
  const status = await res.json();
442
  if(status.ready) {
443
  document.getElementById('loader').style.display = 'none';
 
444
  if(!CURRENT_ROOT) loadSymbol('NIFTY');
445
  } else {
446
  document.getElementById('loaderMsg').innerText = status.message.toUpperCase();
 
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();
457
  }
458
 
 
459
  async function doSearch(q) {
460
  const resDiv = document.getElementById('searchRes');
461
  if(q.length < 2) { resDiv.style.display = 'none'; return; }
 
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('');
 
469
  } catch(e) { console.error(e); }
470
  }
471
 
 
472
  async function loadSymbol(root) {
473
  document.getElementById('searchInp').value = root;
474
  document.getElementById('searchRes').style.display = 'none';
 
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";
 
497
  document.getElementById('loader').style.display = 'none';
498
 
499
  } catch(e) {
500
+ alert("Error: " + e.message);
501
  document.getElementById('loader').style.display = 'none';
502
  }
503
  }
 
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);
515
 
 
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'});
 
536
 
537
  function closeModal() { document.getElementById('expModal').style.display = 'none'; }
538
 
 
539
  function buildChain() {
540
  const body = document.getElementById('chainBody');
 
 
541
  const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
542
  OPT_SYMS = relevant;
543
 
 
555
  </div>
556
  `).join('');
557
 
558
+ window.scrolled = false;
559
  restartPoller();
560
  }
561
 
 
565
  pollData();
566
  }
567
 
 
568
  async function pollData() {
569
  if(IS_POLLING || !CURRENT_ROOT) return;
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) {
 
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
 
608
  const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
609
+ const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN, 'Content-Type': 'application/json' }});
 
 
 
610
 
611
  if(res.status === 401) { location.href = '/logout'; return; }
612
 
 
618
  dot.classList.add('live');
619
  setTimeout(()=>dot.classList.remove('live'), 500);
620
  }
621
+ } catch(e) { console.error(e); document.getElementById('statusDot').style.background = 'var(--red)'; }
622
+ finally { IS_POLLING = false; }
 
 
 
 
623
  }
624
 
625
  function updateUI(data) {
 
626
  if(data[SPOT_SYM]) {
627
  SPOT_PRICE = data[SPOT_SYM].ltp;
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 '';
 
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);
656
  if(!meta) continue;
 
657
  if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
658
  if(meta.t === 'CE') rowData[meta.k].ce = d.totalsellqty;
659
  if(meta.t === 'PE') rowData[meta.k].pe = d.totalsellqty;
 
665
  for(const k in rowData) {
666
  const r = rowData[k];
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) {