topindianhacker commited on
Commit
f09fbf4
·
verified ·
1 Parent(s): e751982

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -189
app.py CHANGED
@@ -4,13 +4,13 @@ import json
4
  import threading
5
  import os
6
  import time
7
- from datetime import datetime, timedelta
8
  from fastapi import FastAPI, BackgroundTasks
9
  from fastapi.responses import HTMLResponse, JSONResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
 
12
  # ==========================================
13
- # 1. CONFIGURATION & STATE
14
  # ==========================================
15
  app = FastAPI()
16
 
@@ -21,10 +21,9 @@ app.add_middleware(
21
  allow_headers=["*"],
22
  )
23
 
24
- # File persistence
25
  SYMBOL_FILE = "symbols.json"
26
 
27
- # In-Memory Cache (Starts with defaults so app works instantly)
28
  SYMBOL_CACHE = [
29
  {"symbol": "NIFTY", "name": "Nifty 50", "exchange": "NSE"},
30
  {"symbol": "BANKNIFTY", "name": "Nifty Bank", "exchange": "NSE"},
@@ -40,18 +39,11 @@ HEADERS = {
40
  }
41
 
42
  # ==========================================
43
- # 2. BACKEND LOGIC (The "A-Z" Loader)
44
  # ==========================================
45
  def fetch_master_list():
46
- """
47
- Runs in background. Loops A-Z to find all optionable instruments.
48
- """
49
  global SYMBOL_CACHE
50
- print(">>> [SYSTEM] Starting Background Symbol Update (A-Z)...")
51
-
52
  temp_list = []
53
-
54
- # Loop ASCII 65(A) to 90(Z)
55
  for i in range(65, 91):
56
  letter = chr(i)
57
  try:
@@ -62,62 +54,40 @@ def fetch_master_list():
62
  results = data.get("payload", {}).get("results", [])
63
  for item in results:
64
  details = item.get("item", {})
65
- # Logic: Must have options chain
66
  if details.get("has_options_chain") or details.get("symbol") in ["NIFTY", "BANKNIFTY"]:
67
  temp_list.append({
68
  "symbol": details.get("symbol"),
69
  "name": details.get("name"),
70
  "exchange": details.get("exchange")
71
  })
72
- # Polite delay
73
  time.sleep(0.1)
74
- except Exception as e:
75
- print(f"Failed to fetch letter {letter}: {e}")
76
 
77
- # Remove duplicates
78
  unique = {v['symbol']: v for v in temp_list}.values()
79
-
80
  if len(unique) > 0:
81
  SYMBOL_CACHE = list(unique)
82
- # Save to disk
83
  with open(SYMBOL_FILE, "w") as f:
84
  json.dump(SYMBOL_CACHE, f)
85
- print(f">>> [SYSTEM] Update Complete. {len(SYMBOL_CACHE)} symbols saved.")
86
- else:
87
- print(">>> [SYSTEM] Update failed or found 0 symbols. Keeping defaults.")
88
 
89
  def load_startup_data():
90
- """
91
- Called on app boot. Checks if local file is valid (<24h).
92
- """
93
  global SYMBOL_CACHE
94
-
95
- should_update = True
96
-
97
  if os.path.exists(SYMBOL_FILE):
98
  try:
99
- # Check file age
100
  file_time = os.path.getmtime(SYMBOL_FILE)
101
- if (time.time() - file_time) < 86400: # 24 hours
102
  with open(SYMBOL_FILE, "r") as f:
103
  data = json.load(f)
104
- if len(data) > 5:
105
- SYMBOL_CACHE = data
106
- should_update = False
107
- print(f">>> [SYSTEM] Loaded {len(data)} symbols from local cache.")
108
- except:
109
- print(">>> [SYSTEM] Cache file corrupted.")
110
-
111
- if should_update:
112
- # Run in background so we don't block the UI
113
- threading.Thread(target=fetch_master_list).start()
114
 
115
  @app.on_event("startup")
116
  def startup_event():
117
  load_startup_data()
118
 
119
  # ==========================================
120
- # 3. API ROUTES
121
  # ==========================================
122
 
123
  @app.get("/")
@@ -126,25 +96,18 @@ def home():
126
 
127
  @app.get("/api/search")
128
  def search(q: str):
129
- """
130
- Searches the in-memory cache.
131
- """
132
  q = q.upper()
133
- # Simple algorithm: Symbol starts with Q or Name contains Q
134
  res = []
135
  for s in SYMBOL_CACHE:
136
  if q in s['symbol'] or q in s['name'].upper():
137
  res.append(s)
138
- if len(res) >= 15: break # Limit results
139
  return JSONResponse(res)
140
 
141
  @app.get("/api/reload_db")
142
  def force_reload(background_tasks: BackgroundTasks):
143
- """
144
- User triggered manual update.
145
- """
146
  background_tasks.add_task(fetch_master_list)
147
- return {"status": "started", "msg": "Background update started."}
148
 
149
  @app.get("/api/db_status")
150
  def db_status():
@@ -152,9 +115,7 @@ def db_status():
152
 
153
  @app.get("/api/fetch")
154
  def fetch_data(symbol: str, exchange: str):
155
- """
156
- Proxy to GoCharting.
157
- """
158
  url = "https://gocharting.com/api/options/chain"
159
  params = {
160
  "exchange": exchange,
@@ -164,7 +125,7 @@ def fetch_data(symbol: str, exchange: str):
164
  "quote_chain": "true"
165
  }
166
  try:
167
- r = requests.get(url, headers=HEADERS, params=params, timeout=10)
168
  return JSONResponse(r.json())
169
  except Exception as e:
170
  return JSONResponse({"status": "error", "msg": str(e)}, status_code=500)
@@ -177,60 +138,60 @@ FRONTEND_HTML = """
177
  <html lang="en">
178
  <head>
179
  <meta charset="UTF-8">
180
- <title>GEX TERMINAL [HF]</title>
181
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
182
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;800&display=swap" rel="stylesheet">
183
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
184
  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
185
  <style>
186
  :root {
187
- --bg: #09090b;
188
- --panel: #18181b;
189
- --border: #27272a;
190
- --text: #e4e4e7;
191
- --text-dim: #71717a;
192
- --accent: #6366f1;
193
- --call-up: #ef4444; /* Resistance */
194
- --put-down: #10b981; /* Support */
195
- --spot: #3b82f6;
196
  }
197
  body { margin:0; background:var(--bg); color:var(--text); font-family:'Inter'; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
198
 
199
  /* HEADER */
200
  header { height:55px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; justify-content:space-between; }
201
- .logo { font-family:'JetBrains Mono'; font-weight:800; letter-spacing:-1px; font-size:18px; }
202
  .logo span { color:var(--accent); }
203
-
204
  .search-wrap { position:relative; width:260px; }
205
  .search-inp { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px 12px; border-radius:6px; font-family:'JetBrains Mono'; font-size:12px; }
206
  .search-inp:focus { border-color:var(--accent); outline:none; }
207
- .results { position:absolute; top:110%; left:0; width:100%; background:var(--panel); border:1px solid var(--border); border-radius:6px; z-index:100; max-height:300px; overflow-y:auto; display:none; }
208
- .res-item { padding:8px 12px; border-bottom:1px solid var(--border); cursor:pointer; display:flex; justify-content:space-between; font-size:12px; }
209
  .res-item:hover { background:var(--border); }
210
 
211
- /* MAIN */
212
  .layout { display:flex; flex:1; height:calc(100vh - 55px); }
213
  .sidebar { width:260px; background:#0c0c0e; border-right:1px solid var(--border); padding:20px; display:flex; flex-direction:column; gap:20px; flex-shrink:0; }
214
  .content { flex:1; position:relative; display:flex; flex-direction:column; padding:20px; }
215
 
216
- /* COMPONENTS */
217
  .card { background:var(--panel); border:1px solid var(--border); padding:15px; border-radius:6px; }
218
  .lbl { font-size:10px; font-weight:700; color:var(--text-dim); text-transform:uppercase; margin-bottom:5px; }
219
  .val { font-family:'JetBrains Mono'; font-size:22px; font-weight:700; }
220
-
221
  select, input[type=range] { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px; border-radius:4px; margin-top:5px; }
222
-
223
  .btn-group { display:flex; background:#000; border:1px solid var(--border); padding:2px; border-radius:4px; margin-top:5px; }
224
  .btn { flex:1; text-align:center; padding:6px; font-size:10px; font-weight:600; cursor:pointer; color:var(--text-dim); border-radius:2px; }
225
  .btn.active { background:var(--border); color:#fff; }
226
 
227
- .reload-btn { font-size:10px; color:var(--accent); cursor:pointer; margin-top:10px; text-decoration:underline; background:none; border:none; padding:0; text-align:left; }
228
-
229
- /* CANVAS */
 
 
 
 
 
 
 
 
 
 
 
230
  .chart-box { flex:1; position:relative; min-height:0; border:1px solid var(--border); background:#000; border-radius:6px; padding:10px; }
231
-
232
- /* OVERLAYS */
233
- .loader { position:absolute; inset:0; background:rgba(9,9,11,0.85); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:50; backdrop-filter:blur(2px); }
234
  .spin { width:30px; height:30px; border:3px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:rot 1s linear infinite; }
235
  @keyframes rot { to { transform:rotate(360deg); } }
236
  </style>
@@ -238,9 +199,9 @@ FRONTEND_HTML = """
238
  <body>
239
 
240
  <header>
241
- <div class="logo">GEX<span>HF</span></div>
242
  <div class="search-wrap">
243
- <input type="text" class="search-inp" id="sInp" placeholder="Search (e.g. NIFTY, RELIANCE)..." autocomplete="off">
244
  <div class="results" id="sRes"></div>
245
  </div>
246
  </header>
@@ -251,7 +212,22 @@ FRONTEND_HTML = """
251
  <div class="card" style="border-left:3px solid var(--spot);">
252
  <div class="lbl">Spot Price</div>
253
  <div class="val" id="spotPrice">---</div>
254
- <div style="font-size:10px; color:var(--text-dim); margin-top:4px;" id="tickerName">BANKNIFTY</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  </div>
256
 
257
  <!-- CONTROLS -->
@@ -271,26 +247,16 @@ FRONTEND_HTML = """
271
  <div class="btn active" id="btnSplit" onclick="setMode('split')">SPLIT</div>
272
  <div class="btn" id="btnNet" onclick="setMode('net')">NET</div>
273
  </div>
274
- <div style="margin-top:5px; font-size:9px; color:#555; line-height:1.4;">
275
- <b>SPLIT:</b> Call Res. (Red) vs Put Sup. (Green)<br>
276
- <b>NET:</b> Combined Dealer Exposure
277
- </div>
278
  </div>
279
 
280
- <!-- DB INFO -->
281
- <div style="margin-top:auto; padding-top:20px; border-top:1px solid var(--border);">
282
- <div class="lbl">Database Status</div>
283
- <div style="font-size:11px; color:var(--text-dim);">Symbols: <span id="dbCount">...</span></div>
284
- <button class="reload-btn" onclick="reloadDB()">↻ Update DB (A-Z)</button>
285
  </div>
286
  </div>
287
 
288
  <div class="content">
289
- <div class="loader" id="loader">
290
- <div class="spin"></div>
291
- <div style="margin-top:10px; font-size:11px; color:var(--text-dim);">FETCHING CHAIN DATA...</div>
292
- </div>
293
-
294
  <div class="chart-box">
295
  <canvas id="chart"></canvas>
296
  </div>
@@ -305,118 +271,77 @@ FRONTEND_HTML = """
305
  let MODE = 'split';
306
  let SYM = 'NIFTY';
307
  let EXCH = 'NSE';
 
308
 
309
  const sInp = document.getElementById('sInp');
310
  const sRes = document.getElementById('sRes');
311
 
312
- // --- STARTUP ---
313
  window.onload = () => {
314
  updateDBStats();
315
- loadData('NIFTY', 'NSE'); // Load NIFTY by default
316
  };
317
 
318
- // --- DATABASE ---
319
- async function updateDBStats() {
320
- try {
321
- const r = await fetch('/api/db_status');
322
- const d = await r.json();
323
- document.getElementById('dbCount').innerText = d.count;
324
- } catch {}
325
- }
326
-
327
- async function reloadDB() {
328
- if(confirm("Start background update of symbol list? This takes about 30 seconds.")) {
329
- fetch('/api/reload_db');
330
- alert("Update started in background. Check count in a minute.");
331
- // Poll for update
332
- let i = setInterval(async () => {
333
- await updateDBStats();
334
  }, 5000);
335
- setTimeout(() => clearInterval(i), 60000);
 
 
336
  }
337
  }
338
 
339
- // --- SEARCH ---
340
- let debounce;
341
- sInp.addEventListener('input', (e) => {
342
- clearTimeout(debounce);
343
- const q = e.target.value;
344
- if(q.length < 1) { sRes.style.display='none'; return; }
345
-
346
- debounce = setTimeout(async () => {
347
- const r = await fetch(`/api/search?q=${q}`);
348
- const data = await r.json();
349
- sRes.innerHTML = '';
350
-
351
- if(data.length > 0) {
352
- sRes.style.display = 'block';
353
- data.forEach(item => {
354
- const div = document.createElement('div');
355
- div.className = 'res-item';
356
- div.innerHTML = `<span>${item.symbol}</span><span style="color:#555">${item.exchange}</span>`;
357
- div.onclick = () => {
358
- sRes.style.display = 'none';
359
- sInp.value = item.symbol;
360
- loadData(item.symbol, item.exchange);
361
- };
362
- sRes.appendChild(div);
363
- });
364
- } else {
365
- sRes.style.display = 'none';
366
- }
367
- }, 300);
368
- });
369
-
370
- // Close search on click away
371
- document.addEventListener('click', (e) => {
372
- if(!e.target.closest('.search-wrap')) sRes.style.display='none';
373
- });
374
-
375
- // --- CORE LOGIC ---
376
- function setMode(m) {
377
- MODE = m;
378
- document.getElementById('btnSplit').className = m==='split' ? 'btn active' : 'btn';
379
- document.getElementById('btnNet').className = m==='net' ? 'btn active' : 'btn';
380
- render();
381
- }
382
-
383
- function updateRng() {
384
- document.getElementById('rngTxt').innerText = document.getElementById('rngInp').value;
385
- render();
386
- }
387
-
388
- async function loadData(symbol, exchange) {
389
  SYM = symbol;
390
  EXCH = exchange;
391
  document.getElementById('tickerName').innerText = `${SYM} (${EXCH})`;
392
- document.getElementById('loader').style.display = 'flex';
 
 
393
 
394
  try {
395
  const res = await fetch(`/api/fetch?symbol=${SYM}&exchange=${EXCH}`);
396
  const json = await res.json();
397
 
398
- if(!json.payload || !json.payload.chain) throw new Error("No Option Chain Data");
399
 
400
  RAW = json.payload;
401
  SPOT = RAW.underlying?.quote?.last_price || 0;
 
 
402
  document.getElementById('spotPrice').innerText = SPOT.toLocaleString();
 
 
403
 
404
- // Populate Expiries
405
  const expSel = document.getElementById('expSel');
406
- expSel.innerHTML = '';
407
- RAW.chain.forEach((c, i) => {
408
- const opt = document.createElement('option');
409
- opt.value = i;
410
- opt.innerText = c.expiry_date;
411
- expSel.appendChild(opt);
412
- });
 
 
 
 
413
 
414
  render();
415
 
416
  } catch(e) {
417
- alert(e.message);
 
418
  } finally {
419
- document.getElementById('loader').style.display = 'none';
420
  }
421
  }
422
 
@@ -425,6 +350,7 @@ FRONTEND_HTML = """
425
 
426
  const expIdx = document.getElementById('expSel').value;
427
  const range = parseInt(document.getElementById('rngInp').value);
 
428
  const chain = RAW.chain[expIdx].options;
429
 
430
  // Find ATM
@@ -434,7 +360,6 @@ FRONTEND_HTML = """
434
  if(diff < min) { min = diff; atmIdx = i; }
435
  });
436
 
437
- // Slice
438
  const start = Math.max(0, atmIdx - range);
439
  const end = Math.min(chain.length, atmIdx + range + 1);
440
  const view = chain.slice(start, end);
@@ -443,24 +368,56 @@ FRONTEND_HTML = """
443
 
444
  view.forEach(o => {
445
  labels.push(o.strike_price);
446
-
447
- // GEX Calc
448
- // Using API Gamma directly. Factor = Gamma * OI * Spot
449
  const cG = (o.call?.greeks?.gamma || 0) * (o.call?.quote?.open_interest || 0) * SPOT;
450
  const pG = (o.put?.greeks?.gamma || 0) * (o.put?.quote?.open_interest || 0) * SPOT;
451
 
452
  if(MODE === 'split') {
453
- calls.push(cG); // Positive Y (Red)
454
- puts.push(-pG); // Negative Y (Green)
455
  } else {
456
- nets.push(cG - pG); // Net
457
  }
458
  });
459
 
460
- // Draw
461
- if(CHART) CHART.destroy();
462
  const ctx = document.getElementById('chart').getContext('2d');
463
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  let datasets = [];
465
  if(MODE === 'split') {
466
  datasets = [
@@ -482,6 +439,7 @@ FRONTEND_HTML = """
482
  options: {
483
  responsive: true,
484
  maintainAspectRatio: false,
 
485
  interaction: { mode: 'index', intersect: false },
486
  plugins: {
487
  legend: { display: false },
@@ -489,9 +447,7 @@ FRONTEND_HTML = """
489
  backgroundColor: '#27272a',
490
  titleColor: '#fff',
491
  bodyFont: { family: 'JetBrains Mono' },
492
- callbacks: {
493
- label: (c) => ` ${c.dataset.label}: ${(Math.abs(c.raw)/100000).toFixed(2)} L`
494
- }
495
  },
496
  annotation: {
497
  annotations: {
@@ -523,6 +479,50 @@ FRONTEND_HTML = """
523
  }
524
  return strikes.length/2;
525
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  </script>
527
  </body>
528
  </html>
 
4
  import threading
5
  import os
6
  import time
7
+ from datetime import datetime
8
  from fastapi import FastAPI, BackgroundTasks
9
  from fastapi.responses import HTMLResponse, JSONResponse
10
  from fastapi.middleware.cors import CORSMiddleware
11
 
12
  # ==========================================
13
+ # 1. CONFIGURATION
14
  # ==========================================
15
  app = FastAPI()
16
 
 
21
  allow_headers=["*"],
22
  )
23
 
 
24
  SYMBOL_FILE = "symbols.json"
25
 
26
+ # Default Cache
27
  SYMBOL_CACHE = [
28
  {"symbol": "NIFTY", "name": "Nifty 50", "exchange": "NSE"},
29
  {"symbol": "BANKNIFTY", "name": "Nifty Bank", "exchange": "NSE"},
 
39
  }
40
 
41
  # ==========================================
42
+ # 2. BACKEND LOGIC
43
  # ==========================================
44
  def fetch_master_list():
 
 
 
45
  global SYMBOL_CACHE
 
 
46
  temp_list = []
 
 
47
  for i in range(65, 91):
48
  letter = chr(i)
49
  try:
 
54
  results = data.get("payload", {}).get("results", [])
55
  for item in results:
56
  details = item.get("item", {})
 
57
  if details.get("has_options_chain") or details.get("symbol") in ["NIFTY", "BANKNIFTY"]:
58
  temp_list.append({
59
  "symbol": details.get("symbol"),
60
  "name": details.get("name"),
61
  "exchange": details.get("exchange")
62
  })
 
63
  time.sleep(0.1)
64
+ except: pass
 
65
 
 
66
  unique = {v['symbol']: v for v in temp_list}.values()
 
67
  if len(unique) > 0:
68
  SYMBOL_CACHE = list(unique)
 
69
  with open(SYMBOL_FILE, "w") as f:
70
  json.dump(SYMBOL_CACHE, f)
 
 
 
71
 
72
  def load_startup_data():
 
 
 
73
  global SYMBOL_CACHE
 
 
 
74
  if os.path.exists(SYMBOL_FILE):
75
  try:
 
76
  file_time = os.path.getmtime(SYMBOL_FILE)
77
+ if (time.time() - file_time) < 86400:
78
  with open(SYMBOL_FILE, "r") as f:
79
  data = json.load(f)
80
+ if len(data) > 5: SYMBOL_CACHE = data
81
+ return
82
+ except: pass
83
+ threading.Thread(target=fetch_master_list).start()
 
 
 
 
 
 
84
 
85
  @app.on_event("startup")
86
  def startup_event():
87
  load_startup_data()
88
 
89
  # ==========================================
90
+ # 3. ROUTES
91
  # ==========================================
92
 
93
  @app.get("/")
 
96
 
97
  @app.get("/api/search")
98
  def search(q: str):
 
 
 
99
  q = q.upper()
 
100
  res = []
101
  for s in SYMBOL_CACHE:
102
  if q in s['symbol'] or q in s['name'].upper():
103
  res.append(s)
104
+ if len(res) >= 15: break
105
  return JSONResponse(res)
106
 
107
  @app.get("/api/reload_db")
108
  def force_reload(background_tasks: BackgroundTasks):
 
 
 
109
  background_tasks.add_task(fetch_master_list)
110
+ return {"status": "started"}
111
 
112
  @app.get("/api/db_status")
113
  def db_status():
 
115
 
116
  @app.get("/api/fetch")
117
  def fetch_data(symbol: str, exchange: str):
118
+ # Proxy to Live API
 
 
119
  url = "https://gocharting.com/api/options/chain"
120
  params = {
121
  "exchange": exchange,
 
125
  "quote_chain": "true"
126
  }
127
  try:
128
+ r = requests.get(url, headers=HEADERS, params=params, timeout=8)
129
  return JSONResponse(r.json())
130
  except Exception as e:
131
  return JSONResponse({"status": "error", "msg": str(e)}, status_code=500)
 
138
  <html lang="en">
139
  <head>
140
  <meta charset="UTF-8">
141
+ <title>GEX LIVE</title>
142
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
143
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;800&display=swap" rel="stylesheet">
144
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
145
  <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
146
  <style>
147
  :root {
148
+ --bg: #09090b; --panel: #18181b; --border: #27272a;
149
+ --text: #e4e4e7; --text-dim: #71717a; --accent: #6366f1;
150
+ --call: #ef4444; --put: #10b981; --spot: #3b82f6;
 
 
 
 
 
 
151
  }
152
  body { margin:0; background:var(--bg); color:var(--text); font-family:'Inter'; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
153
 
154
  /* HEADER */
155
  header { height:55px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; justify-content:space-between; }
156
+ .logo { font-family:'JetBrains Mono'; font-weight:800; font-size:18px; }
157
  .logo span { color:var(--accent); }
 
158
  .search-wrap { position:relative; width:260px; }
159
  .search-inp { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px 12px; border-radius:6px; font-family:'JetBrains Mono'; font-size:12px; }
160
  .search-inp:focus { border-color:var(--accent); outline:none; }
161
+ .results { position:absolute; top:110%; left:0; width:100%; background:var(--panel); border:1px solid var(--border); z-index:100; max-height:300px; overflow-y:auto; display:none; }
162
+ .res-item { padding:8px; border-bottom:1px solid var(--border); cursor:pointer; font-size:12px; display:flex; justify-content:space-between; }
163
  .res-item:hover { background:var(--border); }
164
 
165
+ /* MAIN LAYOUT */
166
  .layout { display:flex; flex:1; height:calc(100vh - 55px); }
167
  .sidebar { width:260px; background:#0c0c0e; border-right:1px solid var(--border); padding:20px; display:flex; flex-direction:column; gap:20px; flex-shrink:0; }
168
  .content { flex:1; position:relative; display:flex; flex-direction:column; padding:20px; }
169
 
170
+ /* CARDS & CONTROLS */
171
  .card { background:var(--panel); border:1px solid var(--border); padding:15px; border-radius:6px; }
172
  .lbl { font-size:10px; font-weight:700; color:var(--text-dim); text-transform:uppercase; margin-bottom:5px; }
173
  .val { font-family:'JetBrains Mono'; font-size:22px; font-weight:700; }
 
174
  select, input[type=range] { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px; border-radius:4px; margin-top:5px; }
 
175
  .btn-group { display:flex; background:#000; border:1px solid var(--border); padding:2px; border-radius:4px; margin-top:5px; }
176
  .btn { flex:1; text-align:center; padding:6px; font-size:10px; font-weight:600; cursor:pointer; color:var(--text-dim); border-radius:2px; }
177
  .btn.active { background:var(--border); color:#fff; }
178
 
179
+ /* LIVE TOGGLE */
180
+ .live-row { display:flex; align-items:center; justify-content:space-between; background:rgba(0,0,0,0.3); padding:10px; border-radius:6px; border:1px solid var(--border); }
181
+ .toggle-sw { position:relative; display:inline-block; width:34px; height:20px; }
182
+ .toggle-sw input { opacity:0; width:0; height:0; }
183
+ .slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#333; transition:.4s; border-radius:20px; }
184
+ .slider:before { position:absolute; content:""; height:14px; width:14px; left:3px; bottom:3px; background-color:white; transition:.4s; border-radius:50%; }
185
+ input:checked + .slider { background-color:var(--accent); }
186
+ input:checked + .slider:before { transform:translateX(14px); }
187
+
188
+ .live-dot { width:8px; height:8px; background:#333; border-radius:50%; margin-right:5px; }
189
+ .live-dot.on { background:#10b981; box-shadow:0 0 8px #10b981; animation:pulse 1.5s infinite; }
190
+ @keyframes pulse { 0% { opacity:1; } 50% { opacity:0.5; } 100% { opacity:1; } }
191
+
192
+ /* CHART */
193
  .chart-box { flex:1; position:relative; min-height:0; border:1px solid var(--border); background:#000; border-radius:6px; padding:10px; }
194
+ .loader { position:absolute; inset:0; background:rgba(9,9,11,0.85); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:50; }
 
 
195
  .spin { width:30px; height:30px; border:3px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:rot 1s linear infinite; }
196
  @keyframes rot { to { transform:rotate(360deg); } }
197
  </style>
 
199
  <body>
200
 
201
  <header>
202
+ <div class="logo">GEX<span>LIVE</span></div>
203
  <div class="search-wrap">
204
+ <input type="text" class="search-inp" id="sInp" placeholder="Search..." autocomplete="off">
205
  <div class="results" id="sRes"></div>
206
  </div>
207
  </header>
 
212
  <div class="card" style="border-left:3px solid var(--spot);">
213
  <div class="lbl">Spot Price</div>
214
  <div class="val" id="spotPrice">---</div>
215
+ <div style="font-size:10px; color:var(--text-dim); margin-top:4px; display:flex; justify-content:space-between;">
216
+ <span id="tickerName">BANKNIFTY</span>
217
+ <span id="lastUpd">--:--:--</span>
218
+ </div>
219
+ </div>
220
+
221
+ <!-- LIVE SWITCH -->
222
+ <div class="live-row">
223
+ <div style="display:flex; align-items:center;">
224
+ <div class="live-dot" id="liveDot"></div>
225
+ <div style="font-size:11px; font-weight:700;">AUTO REFRESH</div>
226
+ </div>
227
+ <label class="toggle-sw">
228
+ <input type="checkbox" id="liveCheck" onchange="toggleLive()">
229
+ <span class="slider"></span>
230
+ </label>
231
  </div>
232
 
233
  <!-- CONTROLS -->
 
247
  <div class="btn active" id="btnSplit" onclick="setMode('split')">SPLIT</div>
248
  <div class="btn" id="btnNet" onclick="setMode('net')">NET</div>
249
  </div>
 
 
 
 
250
  </div>
251
 
252
+ <div style="margin-top:auto; font-size:10px; color:#555;">
253
+ Symbols in DB: <span id="dbCount">...</span> <br>
254
+ <span style="cursor:pointer; color:var(--accent);" onclick="reloadDB()">[Update List]</span>
 
 
255
  </div>
256
  </div>
257
 
258
  <div class="content">
259
+ <div class="loader" id="loader"><div class="spin"></div></div>
 
 
 
 
260
  <div class="chart-box">
261
  <canvas id="chart"></canvas>
262
  </div>
 
271
  let MODE = 'split';
272
  let SYM = 'NIFTY';
273
  let EXCH = 'NSE';
274
+ let LIVE_TIMER = null;
275
 
276
  const sInp = document.getElementById('sInp');
277
  const sRes = document.getElementById('sRes');
278
 
 
279
  window.onload = () => {
280
  updateDBStats();
281
+ loadData('NIFTY', 'NSE');
282
  };
283
 
284
+ // --- LIVE LOGIC ---
285
+ function toggleLive() {
286
+ const isOn = document.getElementById('liveCheck').checked;
287
+ const dot = document.getElementById('liveDot');
288
+
289
+ if(isOn) {
290
+ dot.classList.add('on');
291
+ // Refresh every 5 seconds
292
+ LIVE_TIMER = setInterval(() => {
293
+ loadData(SYM, EXCH, true); // true = background refresh
 
 
 
 
 
 
294
  }, 5000);
295
+ } else {
296
+ dot.classList.remove('on');
297
+ if(LIVE_TIMER) clearInterval(LIVE_TIMER);
298
  }
299
  }
300
 
301
+ // --- DATA LOADING ---
302
+ async function loadData(symbol, exchange, isBackground = false) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  SYM = symbol;
304
  EXCH = exchange;
305
  document.getElementById('tickerName').innerText = `${SYM} (${EXCH})`;
306
+
307
+ // Only show full loader if not a background update
308
+ if(!isBackground) document.getElementById('loader').style.display = 'flex';
309
 
310
  try {
311
  const res = await fetch(`/api/fetch?symbol=${SYM}&exchange=${EXCH}`);
312
  const json = await res.json();
313
 
314
+ if(!json.payload || !json.payload.chain) throw new Error("No Data");
315
 
316
  RAW = json.payload;
317
  SPOT = RAW.underlying?.quote?.last_price || 0;
318
+
319
+ // Update UI
320
  document.getElementById('spotPrice').innerText = SPOT.toLocaleString();
321
+ const now = new Date();
322
+ document.getElementById('lastUpd').innerText = now.toLocaleTimeString();
323
 
324
+ // Populate Expiries only on first load
325
  const expSel = document.getElementById('expSel');
326
+ if(!isBackground || expSel.options.length === 0) {
327
+ const currentVal = expSel.value;
328
+ expSel.innerHTML = '';
329
+ RAW.chain.forEach((c, i) => {
330
+ const opt = document.createElement('option');
331
+ opt.value = i;
332
+ opt.innerText = c.expiry_date;
333
+ expSel.appendChild(opt);
334
+ });
335
+ if(currentVal && currentVal < RAW.chain.length) expSel.value = currentVal;
336
+ }
337
 
338
  render();
339
 
340
  } catch(e) {
341
+ console.error(e);
342
+ if(!isBackground) alert("Error fetching data");
343
  } finally {
344
+ if(!isBackground) document.getElementById('loader').style.display = 'none';
345
  }
346
  }
347
 
 
350
 
351
  const expIdx = document.getElementById('expSel').value;
352
  const range = parseInt(document.getElementById('rngInp').value);
353
+ if(!RAW.chain[expIdx]) return;
354
  const chain = RAW.chain[expIdx].options;
355
 
356
  // Find ATM
 
360
  if(diff < min) { min = diff; atmIdx = i; }
361
  });
362
 
 
363
  const start = Math.max(0, atmIdx - range);
364
  const end = Math.min(chain.length, atmIdx + range + 1);
365
  const view = chain.slice(start, end);
 
368
 
369
  view.forEach(o => {
370
  labels.push(o.strike_price);
 
 
 
371
  const cG = (o.call?.greeks?.gamma || 0) * (o.call?.quote?.open_interest || 0) * SPOT;
372
  const pG = (o.put?.greeks?.gamma || 0) * (o.put?.quote?.open_interest || 0) * SPOT;
373
 
374
  if(MODE === 'split') {
375
+ calls.push(cG);
376
+ puts.push(-pG);
377
  } else {
378
+ nets.push(cG - pG);
379
  }
380
  });
381
 
 
 
382
  const ctx = document.getElementById('chart').getContext('2d');
383
+
384
+ // SMART UPDATE (Prevents flashing)
385
+ if(CHART) {
386
+ CHART.data.labels = labels;
387
+ CHART.options.plugins.annotation.annotations.line1.value = getSpotX(labels, SPOT);
388
+ CHART.options.plugins.annotation.annotations.line1.label.content = `SPOT: ${SPOT}`;
389
+
390
+ if(MODE === 'split') {
391
+ CHART.data.datasets[0].data = calls;
392
+ CHART.data.datasets[0].label = 'Resistance (Call)';
393
+ CHART.data.datasets[0].backgroundColor = '#ef4444';
394
+ CHART.data.datasets[1].data = puts;
395
+ CHART.data.datasets[1].label = 'Support (Put)';
396
+ CHART.data.datasets[1].backgroundColor = '#10b981';
397
+ // Ensure 2 datasets exist
398
+ if(CHART.data.datasets.length < 2) {
399
+ // If switching from NET to SPLIT, we might need to recreate logic or just push
400
+ CHART.destroy(); // Easier to destroy when switching modes
401
+ createChart(ctx, labels, calls, puts, nets);
402
+ return;
403
+ }
404
+ } else {
405
+ CHART.data.datasets[0].data = nets;
406
+ CHART.data.datasets[0].label = 'Net GEX';
407
+ CHART.data.datasets[0].backgroundColor = nets.map(v => v >= 0 ? '#ef4444' : '#10b981');
408
+ if(CHART.data.datasets.length > 1) {
409
+ CHART.destroy();
410
+ createChart(ctx, labels, calls, puts, nets);
411
+ return;
412
+ }
413
+ }
414
+ CHART.update('none'); // Update without animation for smooth live feed
415
+ } else {
416
+ createChart(ctx, labels, calls, puts, nets);
417
+ }
418
+ }
419
+
420
+ function createChart(ctx, labels, calls, puts, nets) {
421
  let datasets = [];
422
  if(MODE === 'split') {
423
  datasets = [
 
439
  options: {
440
  responsive: true,
441
  maintainAspectRatio: false,
442
+ animation: false, // Disable initial animation for snappiness
443
  interaction: { mode: 'index', intersect: false },
444
  plugins: {
445
  legend: { display: false },
 
447
  backgroundColor: '#27272a',
448
  titleColor: '#fff',
449
  bodyFont: { family: 'JetBrains Mono' },
450
+ callbacks: { label: (c) => ` ${c.dataset.label}: ${(Math.abs(c.raw)/100000).toFixed(2)} L` }
 
 
451
  },
452
  annotation: {
453
  annotations: {
 
479
  }
480
  return strikes.length/2;
481
  }
482
+
483
+ // Search Logic
484
+ let debounce;
485
+ sInp.addEventListener('input', (e) => {
486
+ clearTimeout(debounce);
487
+ const q = e.target.value;
488
+ if(q.length < 1) { sRes.style.display='none'; return; }
489
+ debounce = setTimeout(async () => {
490
+ const r = await fetch(`/api/search?q=${q}`);
491
+ const data = await r.json();
492
+ sRes.innerHTML = '';
493
+ if(data.length > 0) {
494
+ sRes.style.display = 'block';
495
+ data.forEach(item => {
496
+ const div = document.createElement('div');
497
+ div.className = 'res-item';
498
+ div.innerHTML = `<span>${item.symbol}</span><span style="color:#555">${item.exchange}</span>`;
499
+ div.onclick = () => {
500
+ sRes.style.display = 'none';
501
+ sInp.value = item.symbol;
502
+ // Reset Live mode to avoid polling wrong symbol
503
+ if(document.getElementById('liveCheck').checked) {
504
+ toggleLive(); document.getElementById('liveCheck').click();
505
+ }
506
+ loadData(item.symbol, item.exchange);
507
+ };
508
+ sRes.appendChild(div);
509
+ });
510
+ } else sRes.style.display = 'none';
511
+ }, 300);
512
+ });
513
+ document.addEventListener('click', (e) => { if(!e.target.closest('.search-wrap')) sRes.style.display='none'; });
514
+
515
+ // Helper
516
+ function setMode(m) {
517
+ MODE = m;
518
+ document.getElementById('btnSplit').className = m==='split' ? 'btn active' : 'btn';
519
+ document.getElementById('btnNet').className = m==='net' ? 'btn active' : 'btn';
520
+ if(CHART) CHART.destroy(); CHART=null; // Force rebuild on mode switch
521
+ render();
522
+ }
523
+ function updateRng() { document.getElementById('rngTxt').innerText = document.getElementById('rngInp').value; render(); }
524
+ async function updateDBStats() { try { const r = await fetch('/api/db_status'); const d = await r.json(); document.getElementById('dbCount').innerText = d.count; } catch {} }
525
+ async function reloadDB() { fetch('/api/reload_db'); alert("Update started."); }
526
  </script>
527
  </body>
528
  </html>