topindianhacker commited on
Commit
a5cd403
·
verified ·
1 Parent(s): 70a8fbd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +391 -216
app.py CHANGED
@@ -1,7 +1,10 @@
1
  import requests
2
  import uvicorn
3
  import json
4
- from fastapi import FastAPI
 
 
 
5
  from fastapi.responses import HTMLResponse, JSONResponse
6
  from fastapi.middleware.cors import CORSMiddleware
7
 
@@ -10,7 +13,6 @@ from fastapi.middleware.cors import CORSMiddleware
10
  # ==========================================
11
  app = FastAPI()
12
 
13
- # Allow CORS to prevent browser blocking
14
  app.add_middleware(
15
  CORSMiddleware,
16
  allow_origins=["*"],
@@ -18,54 +20,131 @@ app.add_middleware(
18
  allow_headers=["*"],
19
  )
20
 
21
- # GoCharting Headers (Crucial for data fetching)
 
 
 
 
 
 
22
  HEADERS = {
23
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
24
- "Accept": "application/json, text/plain, */*",
25
- "Referer": "https://gocharting.com/option-chain/NSE/BANKNIFTY",
26
  "Origin": "https://gocharting.com"
27
  }
28
 
29
  # ==========================================
30
- # 2. PROXY ROUTE (NO SERVER MATH)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  # ==========================================
32
 
33
  @app.get("/")
34
- def ui():
35
  return HTMLResponse(FRONTEND_HTML)
36
 
37
- @app.get("/api/proxy")
38
- def get_gocharting_data(symbol: str = "BANKNIFTY", exchange: str = "NSE"):
39
  """
40
- 1. Connects to GoCharting.
41
- 2. Downloads the raw JSON.
42
- 3. Sends it DIRECTLY to the frontend.
43
- No calculation is done here.
 
 
 
 
 
 
 
 
 
 
 
44
  """
45
  url = "https://gocharting.com/api/options/chain"
46
  params = {
47
- "exchange": exchange.upper(),
48
  "segment": "SPOT",
49
- "symbol": symbol.upper(),
50
  "quote_underlying": "true",
51
  "quote_chain": "true"
52
  }
53
-
54
  try:
55
- # High timeout to handle large data
56
- response = requests.get(url, headers=HEADERS, params=params, timeout=15)
57
-
58
- if response.status_code == 200:
59
- # Send raw JSON to frontend
60
- return JSONResponse(response.json())
61
- else:
62
- return JSONResponse({"status": "error", "msg": f"GoCharting API Error: {response.status_code}"})
63
-
64
  except Exception as e:
65
  return JSONResponse({"status": "error", "msg": str(e)})
66
 
 
 
 
 
 
67
  # ==========================================
68
- # 3. PROFESSIONAL FRONTEND
69
  # ==========================================
70
  FRONTEND_HTML = """
71
  <!DOCTYPE html>
@@ -73,288 +152,384 @@ FRONTEND_HTML = """
73
  <head>
74
  <meta charset="UTF-8">
75
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
76
- <title>GEX TERMINAL</title>
77
- <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;700&display=swap" rel="stylesheet">
 
78
  <style>
79
  :root {
80
- --bg: #000000;
81
- --panel: #0d0d0d;
82
- --border: #222;
83
- --text: #cfcfcf;
84
- --accent: #2962ff;
85
- --call: #00ff88;
86
- --put: #ff0055;
87
- --atm: #222;
88
  }
89
 
90
  * { box-sizing: border-box; outline: none; }
91
- body { margin: 0; background: var(--bg); color: var(--text); font-family: 'Roboto Mono', monospace; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
92
 
93
  /* HEADER */
94
- .header { height: 45px; border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 15px; justify-content: space-between; background: var(--panel); }
95
- .logo { font-weight: 700; font-size: 14px; letter-spacing: -0.5px; color: #fff; }
96
- .logo span { color: var(--accent); }
97
- .status { font-size: 10px; color: #666; font-weight: 700; }
98
-
99
- /* LAYOUT */
100
- .workspace { display: flex; flex: 1; overflow: hidden; }
101
- .sidebar { width: 260px; background: #080808; border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 15px; gap: 15px; z-index: 2; }
102
- .main { flex: 1; display: flex; flex-direction: column; position: relative; }
103
-
104
- /* INPUTS */
105
- .field { display: flex; flex-direction: column; gap: 5px; }
106
- label { font-size: 9px; color: #666; font-weight: 700; text-transform: uppercase; }
107
- input, select { background: #111; border: 1px solid #333; color: #fff; padding: 8px; font-family: inherit; font-size: 11px; border-radius: 2px; }
108
- input:focus, select:focus { border-color: var(--accent); }
109
-
110
- .btn { background: var(--accent); color: #fff; border: none; padding: 10px; cursor: pointer; font-weight: 700; font-size: 11px; border-radius: 2px; }
111
- .btn:hover { opacity: 0.9; }
112
-
113
- /* SPOT DISPLAY */
114
- .spot-box { background: #111; border: 1px solid #333; padding: 15px; text-align: center; border-radius: 4px; }
115
- .spot-val { font-size: 20px; font-weight: 700; color: #fff; margin: 5px 0; }
116
- .spot-lbl { font-size: 9px; color: #666; }
117
-
118
- /* CHART HEADER */
119
- .chart-head { height: 35px; border-bottom: 1px solid var(--border); display: grid; grid-template-columns: 1fr 60px 1fr; align-items: center; padding: 0 20px; font-size: 10px; font-weight: 700; background: var(--panel); color: #888; }
120
- .chart-head div:nth-child(1) { text-align: left; color: var(--put); }
121
- .chart-head div:nth-child(2) { text-align: center; }
122
- .chart-head div:nth-child(3) { text-align: right; color: var(--call); }
123
-
124
- /* CHART BODY */
125
- .chart-area { flex: 1; overflow-y: auto; padding: 10px 0; }
126
- .row { display: grid; grid-template-columns: 1fr 60px 1fr; height: 20px; align-items: center; margin-bottom: 2px; opacity: 0.8; transition: 0.1s; }
127
- .row:hover { opacity: 1; background: #111; }
128
- .row.atm { background: #1a1a1a; border-top: 1px solid #333; border-bottom: 1px solid #333; opacity: 1; }
129
-
130
- .strike { text-align: center; font-size: 11px; font-weight: 700; color: #fff; }
131
- .atm .strike { color: var(--accent); }
132
-
133
- /* BARS */
134
- .bar-wrap { position: relative; height: 100%; display: flex; align-items: center; }
135
- .bar-wrap.left { justify-content: flex-end; padding-right: 10px; border-right: 1px dashed #222; }
136
- .bar-wrap.right { justify-content: flex-start; padding-left: 10px; border-left: 1px dashed #222; }
137
-
138
- .bar { height: 12px; transition: width 0.4s ease; min-width: 1px; }
139
- .bar.put { background: var(--put); border-top-left-radius: 2px; border-bottom-left-radius: 2px; }
140
- .bar.call { background: var(--call); border-top-right-radius: 2px; border-bottom-right-radius: 2px; }
141
 
142
- .val { font-size: 9px; color: #aaa; margin: 0 5px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
  /* LOADER */
145
- .loader { position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.9); display: flex; justify-content: center; align-items: center; flex-direction: column; z-index: 10; display: none; }
146
- .spinner { width: 30px; height: 30px; border: 2px solid #333; border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s infinite linear; }
147
  @keyframes spin { to { transform: rotate(360deg); } }
148
 
 
 
 
149
  </style>
150
  </head>
151
  <body>
152
 
153
- <div class="header">
154
- <div class="logo">GOCHARTING <span>GEX</span></div>
155
- <div class="status" id="lastUpdate">WAITING FOR DATA</div>
 
 
 
 
 
 
156
  </div>
157
 
158
- <div class="workspace">
159
  <div class="sidebar">
160
- <div class="field">
161
- <label>SYMBOL</label>
162
- <input type="text" id="symInp" value="BANKNIFTY" style="text-transform: uppercase;">
163
- </div>
164
-
165
- <div class="field">
166
- <label>EXCHANGE</label>
167
- <select id="exchInp">
168
- <option value="NSE">NSE</option>
169
- <option value="MCX">MCX</option>
170
- </select>
171
  </div>
172
 
173
- <button class="btn" onclick="fetchChain()">LOAD DATA</button>
174
-
175
- <div class="spot-box">
176
- <div class="spot-lbl">SPOT PRICE</div>
177
- <div class="spot-val" id="spotPrice">----</div>
178
- </div>
179
-
180
- <div class="field">
181
- <label>EXPIRY</label>
182
  <select id="expSel" onchange="renderChart()"></select>
183
  </div>
184
 
185
- <div class="field">
186
- <label>STRIKE RANGE</label>
187
- <input type="range" id="rangeSld" min="10" max="50" value="20" oninput="renderChart()">
188
  </div>
189
-
190
- <div style="margin-top:auto; font-size:9px; color:#444;">
191
- GEX = OI * Gamma * Spot * 0.01<br>
192
- Powered by GoCharting API
193
  </div>
194
  </div>
195
 
196
- <div class="main">
197
- <div class="loader" id="loader">
198
  <div class="spinner"></div>
199
- <div style="margin-top:10px; font-size:10px; color:#888;">FETCHING FROM GOCHARTING...</div>
200
  </div>
201
 
202
- <div class="chart-head">
203
- <div>PUT GAMMA (SHORT)</div>
204
- <div>STRIKE</div>
205
- <div>CALL GAMMA (LONG)</div>
 
 
 
206
  </div>
207
- <div class="chart-area" id="chartBody"></div>
208
  </div>
209
  </div>
210
 
211
  <script>
 
212
  let RAW_DATA = null;
213
- let CHAIN_MAP = {};
214
  let SPOT = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
- window.onload = () => { fetchChain(); };
 
 
 
217
 
218
- async function fetchChain() {
219
- const sym = document.getElementById('symInp').value;
220
- const exch = document.getElementById('exchInp').value;
221
- const loader = document.getElementById('loader');
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
 
 
223
  loader.style.display = 'flex';
 
224
 
225
  try {
226
- // Call Python Proxy
227
- const res = await fetch(`/api/proxy?symbol=${sym}&exchange=${exch}`);
228
  const json = await res.json();
229
 
230
- // Validate based on Data example.txt structure
231
- if (!json.payload || !json.payload.chain) {
232
- alert("Invalid Data received from GoCharting. Check Symbol.");
233
  loader.style.display = 'none';
234
  return;
235
  }
236
 
237
  RAW_DATA = json.payload;
238
 
239
- // 1. Get Spot Price
240
- if (RAW_DATA.underlying && RAW_DATA.underlying.quote) {
241
  SPOT = RAW_DATA.underlying.quote.last_price;
242
  } else {
243
- SPOT = 0;
244
  }
245
- document.getElementById('spotPrice').innerText = SPOT.toFixed(2);
246
 
247
- // 2. Parse Expiries
248
  const expSel = document.getElementById('expSel');
249
  expSel.innerHTML = '';
250
-
251
- // GoCharting returns 'chain' array where each item is an expiry object
252
- RAW_DATA.chain.forEach((expObj, index) => {
253
  const opt = document.createElement('option');
254
- opt.value = index; // Store index to access data array later
255
- opt.innerText = expObj.expiry_date;
256
  expSel.appendChild(opt);
257
  });
258
 
259
- document.getElementById('lastUpdate').innerText = "UPDATED: " + new Date().toLocaleTimeString();
260
-
261
  renderChart();
262
 
263
- } catch (e) {
264
- alert("Error: " + e);
 
265
  } finally {
266
  loader.style.display = 'none';
267
  }
268
  }
269
 
270
  function renderChart() {
271
- if (!RAW_DATA) return;
272
 
273
  const expIndex = document.getElementById('expSel').value;
274
- const range = parseInt(document.getElementById('rangeSld').value);
275
- const container = document.getElementById('chartBody');
276
-
277
- // Access specific expiry data
278
- const currentChain = RAW_DATA.chain[expIndex];
279
- const optionsList = currentChain.options; // Array of strikes
280
 
281
- if (!optionsList) return;
282
 
283
- // Find ATM index
284
  let minDiff = Infinity;
285
- let atmIndex = 0;
286
-
287
- optionsList.forEach((opt, idx) => {
288
  const diff = Math.abs(opt.strike_price - SPOT);
289
- if (diff < minDiff) {
290
  minDiff = diff;
291
- atmIndex = idx;
292
  }
293
  });
294
 
295
- // Slice data
296
- const start = Math.max(0, atmIndex - range);
297
- const end = Math.min(optionsList.length, atmIndex + range + 1);
298
- const viewData = optionsList.slice(start, end);
299
 
300
- // Process GEX
301
- let maxGex = 0;
302
- const rows = viewData.map(opt => {
 
 
 
 
303
  const k = opt.strike_price;
304
-
305
- // Safe Access based on Data example.txt
306
- // Structure: options[i].call.greeks.gamma
307
-
308
- // Call Data
309
  const cGamma = opt.call?.greeks?.gamma || 0;
310
  const cOI = opt.call?.quote?.open_interest || 0;
311
- const cGex = (cGamma * cOI * SPOT * 0.01);
312
 
313
- // Put Data
314
  const pGamma = opt.put?.greeks?.gamma || 0;
315
  const pOI = opt.put?.quote?.open_interest || 0;
316
- const pGex = (pGamma * pOI * SPOT * 0.01);
317
 
318
- if (cGex > maxGex) maxGex = cGex;
319
- if (pGex > maxGex) maxGex = pGex;
320
-
321
- return { k, c: cGex, p: pGex };
322
- });
323
-
324
- // Build HTML
325
- let html = '';
326
-
327
- rows.forEach(r => {
328
- const isAtm = r.k === optionsList[atmIndex].strike_price;
329
 
330
- // Calculate widths %
331
- const wC = maxGex > 0 ? (r.c / maxGex) * 100 : 0;
332
- const wP = maxGex > 0 ? (r.p / maxGex) * 100 : 0;
333
-
334
- // Format numbers (Millions)
335
- const fmt = (n) => (n / 1000000).toFixed(2) + "M";
336
-
337
- html += `
338
- <div class="row ${isAtm ? 'atm' : ''}" id="${isAtm ? 'atmRow' : ''}">
339
- <div class="bar-wrap left">
340
- <span class="val">${fmt(r.p)}</span>
341
- <div class="bar put" style="width:${wP}%"></div>
342
- </div>
343
- <div class="strike">${r.k}</div>
344
- <div class="bar-wrap right">
345
- <div class="bar call" style="width:${wC}%"></div>
346
- <span class="val">${fmt(r.c)}</span>
347
- </div>
348
- </div>`;
349
  });
350
 
351
- container.innerHTML = html;
 
 
 
352
 
353
- // Auto scroll to ATM
354
- setTimeout(() => {
355
- const atmEl = document.getElementById('atmRow');
356
- if (atmEl) atmEl.scrollIntoView({ block: "center", behavior: "smooth" });
357
- }, 100);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
  }
359
  </script>
360
 
 
1
  import requests
2
  import uvicorn
3
  import json
4
+ import threading
5
+ import os
6
+ import time
7
+ from fastapi import FastAPI, BackgroundTasks
8
  from fastapi.responses import HTMLResponse, JSONResponse
9
  from fastapi.middleware.cors import CORSMiddleware
10
 
 
13
  # ==========================================
14
  app = FastAPI()
15
 
 
16
  app.add_middleware(
17
  CORSMiddleware,
18
  allow_origins=["*"],
 
20
  allow_headers=["*"],
21
  )
22
 
23
+ # File to store cached symbols
24
+ SYMBOL_FILE = "symbols.json"
25
+
26
+ # In-memory cache for fast searching
27
+ SYMBOL_CACHE = []
28
+
29
+ # Headers for GoCharting
30
  HEADERS = {
31
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
32
+ "Referer": "https://gocharting.com/",
 
33
  "Origin": "https://gocharting.com"
34
  }
35
 
36
  # ==========================================
37
+ # 2. BACKGROUND SYMBOL LOADER (PHP REPLICA)
38
+ # ==========================================
39
+ def update_symbol_master():
40
+ """
41
+ Replicates the PHP loop: A-Z search to build a master list
42
+ of all instruments that have an options chain.
43
+ """
44
+ global SYMBOL_CACHE
45
+ print(">>> [SYSTEM] Updating Symbol Master (A-Z)...")
46
+
47
+ temp_list = []
48
+
49
+ # Loop A to Z (ASCII 65 to 90)
50
+ for i in range(65, 91):
51
+ letter = chr(i)
52
+ try:
53
+ url = f"https://gocharting.com/api/instruments_with_options/search?q={letter}"
54
+ r = requests.get(url, headers=HEADERS, timeout=10)
55
+ if r.status_code == 200:
56
+ data = r.json()
57
+ results = data.get("payload", {}).get("results", [])
58
+
59
+ for item in results:
60
+ details = item.get("item", {})
61
+ # Check if it has options chain (logic from your PHP)
62
+ if details.get("has_options_chain") == True:
63
+ temp_list.append({
64
+ "symbol": details.get("symbol"),
65
+ "name": details.get("name"),
66
+ "exchange": details.get("exchange")
67
+ })
68
+ time.sleep(0.2) # Be polite to API
69
+ except Exception as e:
70
+ print(f"Error fetching {letter}: {e}")
71
+
72
+ # Remove duplicates
73
+ unique_list = {v['symbol']: v for v in temp_list}.values()
74
+ SYMBOL_CACHE = list(unique_list)
75
+
76
+ # Save to disk
77
+ with open(SYMBOL_FILE, "w") as f:
78
+ json.dump(SYMBOL_CACHE, f)
79
+
80
+ print(f">>> [SYSTEM] Master List Updated: {len(SYMBOL_CACHE)} Symbols found.")
81
+
82
+ def load_cache_on_startup():
83
+ global SYMBOL_CACHE
84
+ if os.path.exists(SYMBOL_FILE):
85
+ try:
86
+ with open(SYMBOL_FILE, "r") as f:
87
+ SYMBOL_CACHE = json.load(f)
88
+ print(f">>> [SYSTEM] Loaded {len(SYMBOL_CACHE)} symbols from cache.")
89
+ except:
90
+ pass
91
+
92
+ # If cache empty or old, run update in background
93
+ if not SYMBOL_CACHE:
94
+ threading.Thread(target=update_symbol_master).start()
95
+
96
+ @app.on_event("startup")
97
+ def startup_event():
98
+ load_cache_on_startup()
99
+
100
+ # ==========================================
101
+ # 3. API ROUTES
102
  # ==========================================
103
 
104
  @app.get("/")
105
+ def home():
106
  return HTMLResponse(FRONTEND_HTML)
107
 
108
+ @app.get("/api/search")
109
+ def search_symbols(q: str):
110
  """
111
+ Fast search from memory. Returns symbol, name, and exchange.
112
+ """
113
+ q = q.upper()
114
+ results = []
115
+ # Simple search algorithm
116
+ for item in SYMBOL_CACHE:
117
+ if q in item["symbol"] or q in item["name"].upper():
118
+ results.append(item)
119
+ if len(results) >= 10: break # Limit results
120
+ return JSONResponse(results)
121
+
122
+ @app.get("/api/fetch")
123
+ def proxy_data(symbol: str, exchange: str):
124
+ """
125
+ Proxies the specific option chain request.
126
  """
127
  url = "https://gocharting.com/api/options/chain"
128
  params = {
129
+ "exchange": exchange,
130
  "segment": "SPOT",
131
+ "symbol": symbol,
132
  "quote_underlying": "true",
133
  "quote_chain": "true"
134
  }
 
135
  try:
136
+ r = requests.get(url, headers=HEADERS, params=params, timeout=15)
137
+ return JSONResponse(r.json())
 
 
 
 
 
 
 
138
  except Exception as e:
139
  return JSONResponse({"status": "error", "msg": str(e)})
140
 
141
+ @app.get("/api/force_update")
142
+ def force_update(background_tasks: BackgroundTasks):
143
+ background_tasks.add_task(update_symbol_master)
144
+ return {"status": "update_started"}
145
+
146
  # ==========================================
147
+ # 4. FRONTEND (Professional Chart UI)
148
  # ==========================================
149
  FRONTEND_HTML = """
150
  <!DOCTYPE html>
 
152
  <head>
153
  <meta charset="UTF-8">
154
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
155
+ <title>PRO GEX ANALYZER</title>
156
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
157
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
158
  <style>
159
  :root {
160
+ --bg: #09090b;
161
+ --panel: #18181b;
162
+ --border: #27272a;
163
+ --accent: #3b82f6;
164
+ --text-main: #f4f4f5;
165
+ --text-sub: #a1a1aa;
166
+ --call: #10b981;
167
+ --put: #ef4444;
168
  }
169
 
170
  * { box-sizing: border-box; outline: none; }
171
+ body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; }
172
 
173
  /* HEADER */
174
+ .top-nav { height: 60px; border-bottom: 1px solid var(--border); background: var(--panel); display: flex; align-items: center; padding: 0 20px; justify-content: space-between; flex-shrink: 0; }
175
+ .brand { font-family: 'JetBrains Mono'; font-weight: 800; font-size: 18px; letter-spacing: -1px; }
176
+ .brand span { color: var(--accent); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
+ .search-container { position: relative; width: 300px; }
179
+ .search-input { width: 100%; background: #000; border: 1px solid var(--border); color: #fff; padding: 10px 12px; border-radius: 6px; font-family: 'JetBrains Mono'; font-size: 12px; }
180
+ .search-input:focus { border-color: var(--accent); }
181
+
182
+ .search-dropdown { position: absolute; top: 105%; 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; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
183
+ .search-item { padding: 10px; border-bottom: 1px solid var(--border); cursor: pointer; display: flex; justify-content: space-between; align-items: center; }
184
+ .search-item:hover { background: var(--border); }
185
+ .s-sym { font-weight: 700; font-size: 13px; color: #fff; }
186
+ .s-name { font-size: 10px; color: var(--text-sub); }
187
+ .s-exch { font-size: 9px; background: var(--accent); color: #fff; padding: 2px 4px; border-radius: 3px; font-weight: 700; }
188
+
189
+ /* MAIN LAYOUT */
190
+ .layout { display: flex; flex: 1; overflow: hidden; }
191
+
192
+ /* SIDEBAR CONTROLS */
193
+ .sidebar { width: 260px; background: #000; border-right: 1px solid var(--border); padding: 20px; display: flex; flex-direction: column; gap: 20px; flex-shrink: 0; }
194
+ .stat-card { background: var(--panel); border: 1px solid var(--border); padding: 15px; border-radius: 8px; text-align: center; }
195
+ .stat-val { font-size: 24px; font-family: 'JetBrains Mono'; font-weight: 700; color: #fff; }
196
+ .stat-lbl { font-size: 10px; color: var(--text-sub); font-weight: 600; margin-top: 5px; text-transform: uppercase; }
197
+
198
+ .ctrl-group label { display: block; font-size: 10px; font-weight: 700; color: var(--text-sub); margin-bottom: 8px; }
199
+ select, input[type="range"] { width: 100%; background: var(--panel); border: 1px solid var(--border); color: #fff; padding: 8px; border-radius: 4px; font-family: 'Inter'; font-size: 12px; }
200
+
201
+ /* CHART AREA */
202
+ .chart-wrapper { flex: 1; padding: 20px; display: flex; flex-direction: column; position: relative; }
203
+ .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
204
+ .chart-title { font-size: 14px; font-weight: 700; }
205
+ .canvas-container { flex: 1; position: relative; min-height: 0; }
206
 
207
  /* LOADER */
208
+ .loader-overlay { position: absolute; top:0; left:0; width:100%; height:100%; background: rgba(9,9,11,0.8); z-index: 50; display: none; justify-content: center; align-items: center; flex-direction: column; backdrop-filter: blur(2px); }
209
+ .spinner { width: 40px; height: 40px; border: 3px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s infinite linear; }
210
  @keyframes spin { to { transform: rotate(360deg); } }
211
 
212
+ /* BADGE */
213
+ .live-badge { display: inline-flex; align-items: center; gap: 6px; font-size: 10px; font-weight: 700; color: var(--call); background: rgba(16, 185, 129, 0.1); padding: 4px 8px; border-radius: 20px; border: 1px solid rgba(16, 185, 129, 0.2); }
214
+ .dot { width: 6px; height: 6px; background: currentColor; border-radius: 50%; box-shadow: 0 0 6px currentColor; }
215
  </style>
216
  </head>
217
  <body>
218
 
219
+ <div class="top-nav">
220
+ <div class="brand">GEX<span>EXPLORER</span></div>
221
+
222
+ <div class="search-container">
223
+ <input type="text" class="search-input" id="searchInp" placeholder="Search (e.g. NIFTY, RELIANCE)..." autocomplete="off">
224
+ <div class="search-dropdown" id="searchRes"></div>
225
+ </div>
226
+
227
+ <div class="live-badge"><div class="dot"></div> <span id="statusTxt">READY</span></div>
228
  </div>
229
 
230
+ <div class="layout">
231
  <div class="sidebar">
232
+ <div class="stat-card">
233
+ <div class="stat-val" id="spotPrice">----</div>
234
+ <div class="stat-lbl" id="spotLbl">SPOT PRICE</div>
 
 
 
 
 
 
 
 
235
  </div>
236
 
237
+ <div class="ctrl-group">
238
+ <label>EXPIRY DATE</label>
 
 
 
 
 
 
 
239
  <select id="expSel" onchange="renderChart()"></select>
240
  </div>
241
 
242
+ <div class="ctrl-group">
243
+ <label>STRIKE RANGE (ATM ± <span id="rngVal">15</span>)</label>
244
+ <input type="range" id="rngSld" min="5" max="40" value="15" oninput="updateRange()">
245
  </div>
246
+
247
+ <div class="stat-card" style="margin-top:auto; border-color: var(--accent);">
248
+ <div class="stat-lbl" style="color: var(--accent);">NET GEX</div>
249
+ <div class="stat-val" id="netGex" style="font-size: 16px;">0.00 Cr</div>
250
  </div>
251
  </div>
252
 
253
+ <div class="chart-wrapper">
254
+ <div class="loader-overlay" id="loader">
255
  <div class="spinner"></div>
256
+ <div style="margin-top:10px; font-size:12px; color: var(--text-sub);">PROCESSING DATA...</div>
257
  </div>
258
 
259
+ <div class="chart-header">
260
+ <div class="chart-title">GAMMA EXPOSURE PROFILE</div>
261
+ <div style="font-size: 10px; color: var(--text-sub);">Formula: OI × Gamma × Spot × 0.01</div>
262
+ </div>
263
+
264
+ <div class="canvas-container">
265
+ <canvas id="gexChart"></canvas>
266
  </div>
 
267
  </div>
268
  </div>
269
 
270
  <script>
271
+ // GLOBAL STATE
272
  let RAW_DATA = null;
 
273
  let SPOT = 0;
274
+ let CHART_INSTANCE = null;
275
+ let CURRENT_SYM = "BANKNIFTY";
276
+ let CURRENT_EXCH = "NSE";
277
+
278
+ // --- SEARCH LOGIC ---
279
+ const searchInp = document.getElementById('searchInp');
280
+ const searchRes = document.getElementById('searchRes');
281
+ let debounceTimer;
282
+
283
+ searchInp.addEventListener('input', (e) => {
284
+ clearTimeout(debounceTimer);
285
+ const q = e.target.value;
286
+ if(q.length < 1) { searchRes.style.display='none'; return; }
287
+
288
+ debounceTimer = setTimeout(async () => {
289
+ const r = await fetch(`/api/search?q=${q}`);
290
+ const data = await r.json();
291
+
292
+ searchRes.innerHTML = '';
293
+ if(data.length > 0){
294
+ searchRes.style.display = 'block';
295
+ data.forEach(item => {
296
+ const div = document.createElement('div');
297
+ div.className = 'search-item';
298
+ div.innerHTML = `
299
+ <div>
300
+ <div class="s-sym">${item.symbol}</div>
301
+ <div class="s-name">${item.name}</div>
302
+ </div>
303
+ <div class="s-exch">${item.exchange}</div>
304
+ `;
305
+ div.onclick = () => loadSymbol(item.symbol, item.exchange);
306
+ searchRes.appendChild(div);
307
+ });
308
+ } else {
309
+ searchRes.style.display = 'none';
310
+ }
311
+ }, 300);
312
+ });
313
 
314
+ // Close dropdown on outside click
315
+ document.addEventListener('click', (e) => {
316
+ if(!e.target.closest('.search-container')) searchRes.style.display='none';
317
+ });
318
 
319
+ // --- MAIN APP LOGIC ---
320
+ window.onload = () => loadSymbol('BANKNIFTY', 'NSE');
321
+
322
+ function updateRange() {
323
+ document.getElementById('rngVal').innerText = document.getElementById('rngSld').value;
324
+ renderChart();
325
+ }
326
+
327
+ async function loadSymbol(sym, exch) {
328
+ CURRENT_SYM = sym;
329
+ CURRENT_EXCH = exch;
330
+ searchInp.value = sym;
331
+ searchRes.style.display = 'none';
332
+ document.getElementById('spotLbl').innerText = `${sym} (${exch})`;
333
+
334
+ await fetchData();
335
+ }
336
 
337
+ async function fetchData() {
338
+ const loader = document.getElementById('loader');
339
  loader.style.display = 'flex';
340
+ document.getElementById('statusTxt').innerText = "FETCHING...";
341
 
342
  try {
343
+ const res = await fetch(`/api/fetch?symbol=${CURRENT_SYM}&exchange=${CURRENT_EXCH}`);
 
344
  const json = await res.json();
345
 
346
+ if(!json.payload || !json.payload.chain) {
347
+ alert("No Options Data Found for " + CURRENT_SYM);
 
348
  loader.style.display = 'none';
349
  return;
350
  }
351
 
352
  RAW_DATA = json.payload;
353
 
354
+ // Get Spot
355
+ if(RAW_DATA.underlying && RAW_DATA.underlying.quote) {
356
  SPOT = RAW_DATA.underlying.quote.last_price;
357
  } else {
358
+ SPOT = 0; // Fallback
359
  }
360
+ document.getElementById('spotPrice').innerText = SPOT.toLocaleString();
361
 
362
+ // Populate Expiries
363
  const expSel = document.getElementById('expSel');
364
  expSel.innerHTML = '';
365
+ RAW_DATA.chain.forEach((exp, idx) => {
 
 
366
  const opt = document.createElement('option');
367
+ opt.value = idx;
368
+ opt.innerText = exp.expiry_date;
369
  expSel.appendChild(opt);
370
  });
371
 
372
+ document.getElementById('statusTxt').innerText = "LIVE";
 
373
  renderChart();
374
 
375
+ } catch(e) {
376
+ console.error(e);
377
+ document.getElementById('statusTxt').innerText = "ERROR";
378
  } finally {
379
  loader.style.display = 'none';
380
  }
381
  }
382
 
383
  function renderChart() {
384
+ if(!RAW_DATA) return;
385
 
386
  const expIndex = document.getElementById('expSel').value;
387
+ const range = parseInt(document.getElementById('rngSld').value);
388
+ const chainData = RAW_DATA.chain[expIndex].options;
 
 
 
 
389
 
390
+ if(!chainData) return;
391
 
392
+ // Find ATM
393
  let minDiff = Infinity;
394
+ let atmIdx = 0;
395
+ chainData.forEach((opt, i) => {
 
396
  const diff = Math.abs(opt.strike_price - SPOT);
397
+ if(diff < minDiff) {
398
  minDiff = diff;
399
+ atmIdx = i;
400
  }
401
  });
402
 
403
+ // Slice Data
404
+ const start = Math.max(0, atmIdx - range);
405
+ const end = Math.min(chainData.length, atmIdx + range + 1);
406
+ const viewData = chainData.slice(start, end);
407
 
408
+ // Prepare Arrays for Chart
409
+ const labels = [];
410
+ const callGex = [];
411
+ const putGex = []; // Will store as negative for visual
412
+ let totalNetGex = 0;
413
+
414
+ viewData.forEach(opt => {
415
  const k = opt.strike_price;
416
+ labels.push(k);
417
+
418
+ // GEX Calc: Gamma * OI * Spot * 0.01
 
 
419
  const cGamma = opt.call?.greeks?.gamma || 0;
420
  const cOI = opt.call?.quote?.open_interest || 0;
421
+ const cVal = cGamma * cOI * SPOT * 0.01;
422
 
 
423
  const pGamma = opt.put?.greeks?.gamma || 0;
424
  const pOI = opt.put?.quote?.open_interest || 0;
425
+ const pVal = pGamma * pOI * SPOT * 0.01;
426
 
427
+ callGex.push(cVal);
428
+ putGex.push(-pVal); // Negative for putting on left/down side
 
 
 
 
 
 
 
 
 
429
 
430
+ totalNetGex += (cVal - pVal);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  });
432
 
433
+ // Update Net Gex Stats
434
+ const netEl = document.getElementById('netGex');
435
+ netEl.innerText = (totalNetGex / 10000000).toFixed(2) + " Cr";
436
+ netEl.style.color = totalNetGex >= 0 ? 'var(--call)' : 'var(--put)';
437
 
438
+ // DESTROY OLD CHART
439
+ if(CHART_INSTANCE) CHART_INSTANCE.destroy();
440
+
441
+ // CREATE NEW CHART (Professional Look)
442
+ const ctx = document.getElementById('gexChart').getContext('2d');
443
+
444
+ // Define Gradient Colors
445
+ const gradCall = ctx.createLinearGradient(0, 0, 0, 400);
446
+ gradCall.addColorStop(0, 'rgba(16, 185, 129, 0.8)');
447
+ gradCall.addColorStop(1, 'rgba(16, 185, 129, 0.2)');
448
+
449
+ const gradPut = ctx.createLinearGradient(0, 0, 0, 400);
450
+ gradPut.addColorStop(0, 'rgba(239, 68, 68, 0.2)');
451
+ gradPut.addColorStop(1, 'rgba(239, 68, 68, 0.8)');
452
+
453
+ CHART_INSTANCE = new Chart(ctx, {
454
+ type: 'bar',
455
+ data: {
456
+ labels: labels,
457
+ datasets: [
458
+ {
459
+ label: 'Call GEX',
460
+ data: callGex,
461
+ backgroundColor: '#10b981',
462
+ borderRadius: 2,
463
+ barPercentage: 0.6,
464
+ order: 1
465
+ },
466
+ {
467
+ label: 'Put GEX',
468
+ data: putGex,
469
+ backgroundColor: '#ef4444',
470
+ borderRadius: 2,
471
+ barPercentage: 0.6,
472
+ order: 2
473
+ }
474
+ ]
475
+ },
476
+ options: {
477
+ responsive: true,
478
+ maintainAspectRatio: false,
479
+ indexAxis: 'x', // Vertical bars, Strike on X axis (Professional standard)
480
+ interaction: {
481
+ mode: 'index',
482
+ intersect: false,
483
+ },
484
+ plugins: {
485
+ legend: { display: true, labels: { color: '#a1a1aa', font: {family: 'Inter', size: 10} } },
486
+ tooltip: {
487
+ backgroundColor: '#18181b',
488
+ titleColor: '#fff',
489
+ bodyColor: '#a1a1aa',
490
+ borderColor: '#27272a',
491
+ borderWidth: 1,
492
+ callbacks: {
493
+ label: function(ctx) {
494
+ let val = ctx.raw;
495
+ val = Math.abs(val); // Show positive value for Put tooltip
496
+ let valStr = (val/100000).toFixed(2) + " L";
497
+ return ctx.dataset.label + ": " + valStr;
498
+ }
499
+ }
500
+ },
501
+ annotation: {
502
+ annotations: {
503
+ line1: {
504
+ type: 'line',
505
+ scaleID: 'x',
506
+ value: (ctx) => {
507
+ // Identify index close to spot
508
+ // This requires advanced mapping, simplistic for now
509
+ return null;
510
+ },
511
+ borderColor: '#3b82f6',
512
+ borderWidth: 1,
513
+ borderDash: [5, 5],
514
+ }
515
+ }
516
+ }
517
+ },
518
+ scales: {
519
+ x: {
520
+ grid: { color: '#27272a' },
521
+ ticks: { color: '#a1a1aa', font: {family: 'JetBrains Mono'} },
522
+ stacked: true,
523
+ title: { display: true, text: 'STRIKE PRICE', color: '#52525b', font: {size: 10, weight: 700} }
524
+ },
525
+ y: {
526
+ grid: { color: '#27272a' },
527
+ ticks: { color: '#a1a1aa', callback: function(value) { return (value/10000000).toFixed(1) + "Cr"; } },
528
+ stacked: true
529
+ }
530
+ }
531
+ }
532
+ });
533
  }
534
  </script>
535