topsecrettraders commited on
Commit
802a00d
·
verified ·
1 Parent(s): c4bf025

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +463 -463
app.py CHANGED
@@ -1,464 +1,464 @@
1
- import pandas as pd
2
- import requests
3
- import uvicorn
4
- import hashlib
5
- import json
6
- import io
7
- import time
8
- from fastapi import FastAPI, Request, Response, Cookie
9
- from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
10
- from fastapi.middleware.cors import CORSMiddleware
11
- from typing import Optional
12
-
13
- # ==========================================
14
- # 1. CONFIGURATION (EDIT THESE)
15
- # ==========================================
16
- CLIENT_ID = "J4PNT5QI22-100" # Format: XPxxxxx-100
17
- SECRET_KEY = "ABRSWC1PMR"
18
- # IMPORTANT: Change this to your Space's URL + /callback
19
- # Example: https://huggingface-username-space-name.hf.space/callback
20
- REDIRECT_URI = "https://your-space-url.hf.space/callback"
21
-
22
- # Global Cache
23
- MASTER_DATA = []
24
- LAST_UPDATE = 0
25
-
26
- app = FastAPI()
27
-
28
- # ==========================================
29
- # 2. PYTHON BACKEND (Optimized Data Loader)
30
- # ==========================================
31
-
32
- def load_nifty_master():
33
- """
34
- Downloads the large CSV, filters ONLY NIFTY 50 and NIFTY FUTS using Pandas.
35
- This runs in seconds and stays in RAM.
36
- """
37
- global MASTER_DATA, LAST_UPDATE
38
-
39
- print(">>> DOWNLOADING MASTER DATA...")
40
- try:
41
- url = "https://public.fyers.in/sym_details/NSE_FO.csv"
42
- # Read CSV directly into Pandas
43
- df = pd.read_csv(url, usecols=[0, 1, 8, 9, 13], names=['RecType','Desc','Exch','Expiry','Symbol','Unknown'], header=0)
44
-
45
- # 1. FILTER: Symbol must contain 'NIFTY' but NOT 'BANK', 'FIN', 'MID'
46
- # We only want pure NIFTY (NIFTY 50)
47
- mask = (
48
- df['Symbol'].str.contains('NIFTY', na=False) &
49
- ~df['Symbol'].str.contains('BANKNIFTY', na=False) &
50
- ~df['Symbol'].str.contains('FINNIFTY', na=False) &
51
- ~df['Symbol'].str.contains('MIDCPNIFTY', na=False)
52
- )
53
- df = df[mask]
54
-
55
- results = []
56
-
57
- # Add Spot manually
58
- results.append({"s": "NSE:NIFTY50-INDEX", "e": 0, "k": 0})
59
-
60
- # Process Rows
61
- for _, row in df.iterrows():
62
- sym = row['Symbol']
63
- desc = str(row['Desc'])
64
- exp = int(row['Expiry'])
65
-
66
- # Extract Strike
67
- strike = 0.0
68
- if "CE" in sym or "PE" in sym:
69
- # Desc format usually: "NIFTY 06 Jan 20500 CE"
70
- parts = desc.strip().split(' ')
71
- # Try getting the 2nd to last item
72
- if len(parts) >= 2:
73
- val = parts[-2]
74
- if val.replace('.','',1).isdigit():
75
- strike = float(val)
76
-
77
- results.append({
78
- "s": sym,
79
- "e": exp,
80
- "k": strike
81
- })
82
-
83
- MASTER_DATA = results
84
- LAST_UPDATE = time.time()
85
- print(f">>> LOADED {len(results)} NIFTY SYMBOLS.")
86
- return True
87
- except Exception as e:
88
- print(f"Error loading master: {e}")
89
- return False
90
-
91
- # Initialize on startup
92
- @app.on_event("startup")
93
- def startup_event():
94
- load_nifty_master()
95
-
96
- # ==========================================
97
- # 3. ROUTES
98
- # ==========================================
99
-
100
- @app.get("/")
101
- def home(access_token: Optional[str] = Cookie(None)):
102
- if not access_token:
103
- # Generate Auth URL
104
- state = "init"
105
- auth_url = (
106
- f"https://api-t1.fyers.in/api/v3/generate-authcode?"
107
- f"client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state={state}"
108
- )
109
- return HTMLResponse(f"""
110
- <div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif;">
111
- <h1>FYERS NIFTY CHAIN (PYTHON)</h1>
112
- <p>Please update REDIRECT_URI in app.py to match this HuggingFace Space URL.</p>
113
- <a href="{auth_url}" style="background:#2962ff; color:white; padding:15px 30px; text-decoration:none; border-radius:5px; font-weight:bold;">LOGIN WITH FYERS</a>
114
- </div>
115
- """)
116
-
117
- # If logged in, serve the App
118
- return HTMLResponse(HTML_TEMPLATE)
119
-
120
- @app.get("/callback")
121
- def callback(code: str):
122
- # Validate Auth Code
123
- app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
124
-
125
- payload = {
126
- "grant_type": "authorization_code",
127
- "appIdHash": app_id_hash,
128
- "code": code
129
- }
130
-
131
- resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json=payload)
132
- data = resp.json()
133
-
134
- if "access_token" in data:
135
- full_token = f"{CLIENT_ID}:{data['access_token']}"
136
- response = RedirectResponse(url="/")
137
- response.set_cookie(key="access_token", value=full_token, httponly=True)
138
- return response
139
- else:
140
- return JSONResponse(content={"error": "Login Failed", "details": data})
141
-
142
- @app.get("/logout")
143
- def logout():
144
- response = RedirectResponse(url="/")
145
- response.delete_cookie("access_token")
146
- return response
147
-
148
- @app.get("/api/init")
149
- def get_master_data(force: bool = False):
150
- global MASTER_DATA
151
- if force or not MASTER_DATA:
152
- load_nifty_master()
153
- return JSONResponse(content=MASTER_DATA)
154
-
155
- @app.get("/api/depth")
156
- def get_market_depth(symbols: str, access_token: Optional[str] = Cookie(None)):
157
- if not access_token:
158
- return JSONResponse(content={"s": "error", "message": "No Token"}, status_code=401)
159
-
160
- url = f"https://api-t1.fyers.in/data/depth?symbol={symbols}&ohlcv_flag=1"
161
- headers = {"Authorization": access_token}
162
-
163
- resp = requests.get(url, headers=headers)
164
- return JSONResponse(content=resp.json())
165
-
166
-
167
- # ==========================================
168
- # 4. FRONTEND (Single Page Application)
169
- # ==========================================
170
-
171
- HTML_TEMPLATE = """
172
- <!DOCTYPE html>
173
- <html lang="en">
174
- <head>
175
- <meta charset="UTF-8">
176
- <title>NIFTY PYTHON CHAIN</title>
177
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
178
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
179
- <style>
180
- :root {
181
- --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
182
- --text-main: #1a1a1a; --text-sub: #555555;
183
- --accent: #2962ff; --green: #00c853; --red: #d50000;
184
- --green-soft: rgba(0, 200, 83, 0.3); --red-soft: rgba(213, 0, 0, 0.3);
185
- --atm-bg: rgba(41, 98, 255, 0.1);
186
- }
187
- * { box-sizing: border-box; }
188
- body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
189
-
190
- .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; }
191
- .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; }
192
- .btn:hover { border-color: var(--accent); color: var(--accent); }
193
-
194
- .grid-layout { display: flex; height: calc(100vh - 60px); }
195
- .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
196
- .chain-header { display: grid; grid-template-columns: 1fr 180px 1fr; padding: 10px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 700; text-align: center; color: var(--text-sub); }
197
- .chain-body { flex: 1; overflow-y: auto; background: #fff; }
198
-
199
- .row { display: grid; grid-template-columns: 1fr 180px 1fr; border-bottom: 1px solid #f0f0f0; height: 45px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
200
- .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
201
-
202
- .mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); }
203
- .visual-bar { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; transition: 0.3s; opacity: 0.8; }
204
- .strike-txt { z-index: 2; font-weight: 800; font-size: 14px; }
205
- .delta-txt { z-index: 2; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.7); padding: 0 4px; border-radius: 3px; }
206
-
207
- .side-col { padding: 0 15px; display: flex; justify-content: space-between; color: #666; font-size: 11px; }
208
-
209
- .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
210
- .fut-card { padding: 15px; background: #fff; border-bottom: 1px solid var(--border); }
211
- .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
212
-
213
- /* Modal */
214
- .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(255,255,255,0.9); z-index: 1000; display: none; justify-content: center; align-items: center; }
215
- .modal-box { background: #fff; border: 1px solid #ccc; width: 400px; border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); padding: 20px; }
216
- .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 50vh; overflow-y: auto; }
217
- .exp-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; }
218
- .exp-btn:hover { background: #f0f7ff; border-color: var(--accent); }
219
- .exp-btn.active { background: var(--accent); color: white; }
220
-
221
- .loader { position: absolute; top:0; left:0; width:100%; height:100%; background:rgba(255,255,255,0.95); display:flex; justify-content:center; align-items:center; z-index: 2000; font-weight: 700; color: var(--accent); }
222
- </style>
223
- </head>
224
- <body>
225
-
226
- <div class="loader" id="loader">INITIALIZING PYTHON DB...</div>
227
-
228
- <!-- MODAL -->
229
- <div class="modal" id="expModal">
230
- <div class="modal-box">
231
- <div style="display:flex; justify-content:space-between; font-weight:700;">
232
- <span>SELECT EXPIRY</span>
233
- <span style="cursor:pointer" onclick="document.getElementById('expModal').style.display='none'">✕</span>
234
- </div>
235
- <div class="modal-grid" id="expGrid"></div>
236
- </div>
237
- </div>
238
-
239
- <div class="top-nav">
240
- <div style="font-weight:800; font-size:18px; color:var(--accent);">NIFTY 50 (PYTHON)</div>
241
-
242
- <div style="display:flex; gap:20px; align-items:center;">
243
- <div style="text-align:right;">
244
- <div style="font-size:10px; font-weight:700; color:#888;">SPOT</div>
245
- <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:16px;">0.00</div>
246
- </div>
247
- <button class="btn" onclick="openExpModal()">
248
- <span style="color:#888">EXP:</span> <span id="selExpTxt">SELECT</span>
249
- </button>
250
- <button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
251
- <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
252
- </div>
253
- </div>
254
-
255
- <div class="grid-layout">
256
- <div class="chain-panel">
257
- <div class="chain-header">
258
- <div>CALL SELLERS (OI)</div>
259
- <div>NET FLOW (PUT S - CALL S)</div>
260
- <div>PUT SELLERS (OI)</div>
261
- </div>
262
- <div class="chain-body" id="chainBody"></div>
263
- </div>
264
-
265
- <div class="fut-panel">
266
- <div style="padding:15px; font-weight:700; border-bottom:1px solid var(--border); background:#fff;">FUTURES</div>
267
- <div id="futBody"></div>
268
- </div>
269
- </div>
270
-
271
- <script>
272
- let DB = [];
273
- let SPOT_SYM = "NSE:NIFTY50-INDEX";
274
- let ACTIVE_EXP = 0;
275
- let SPOT_PRICE = 0;
276
- let POLLER = null;
277
- let FUT_SYMS = [];
278
-
279
- window.onload = async () => {
280
- try {
281
- // Load from Python Backend
282
- const res = await fetch('/api/init');
283
- DB = await res.json();
284
- document.getElementById('loader').style.display = 'none';
285
-
286
- if(DB.length === 0) { alert("DB Empty. Check Python Logs."); return; }
287
-
288
- // Setup Futures
289
- FUT_SYMS = DB.filter(i => i.s.startsWith('NSE:NIFTY') && i.s.endsWith('FUT') && /\\d/.test(i.s))
290
- .sort((a,b) => a.e - b.e).slice(0,3);
291
-
292
- openExpModal();
293
- } catch(e) {
294
- document.getElementById('loader').innerText = "ERROR LOADING DATA";
295
- }
296
- };
297
-
298
- function openExpModal() {
299
- const grid = document.getElementById('expGrid');
300
- grid.innerHTML = '';
301
- const ops = DB.filter(i => i.s.endsWith('CE') || i.s.endsWith('PE'));
302
- const exps = [...new Set(ops.map(i => i.e))].sort((a,b) => a-b);
303
- const today = Math.floor(Date.now()/1000) - 86400;
304
-
305
- exps.forEach(ts => {
306
- if(ts < today) return;
307
- const d = new Date(ts * 1000);
308
- const dStr = d.toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
309
- const div = document.createElement('div');
310
- div.className = `exp-btn ${ts === ACTIVE_EXP ? 'active' : ''}`;
311
- div.innerText = dStr;
312
- div.onclick = () => {
313
- ACTIVE_EXP = ts;
314
- document.getElementById('selExpTxt').innerText = dStr;
315
- document.getElementById('expModal').style.display = 'none';
316
- buildChain();
317
- }
318
- grid.appendChild(div);
319
- });
320
- document.getElementById('expModal').style.display = 'flex';
321
- }
322
-
323
- function buildChain() {
324
- const body = document.getElementById('chainBody');
325
- body.innerHTML = '';
326
-
327
- const relevant = DB.filter(i => i.e === ACTIVE_EXP && (i.s.endsWith('CE') || i.s.endsWith('PE')));
328
- const strikes = [...new Set(relevant.map(i => i.k))].sort((a,b) => a-b);
329
-
330
- body.innerHTML = strikes.map(k => `
331
- <div class="row" id="row-${k}" data-k="${k}">
332
- <div class="side-col" id="ce-${k}"><span>-</span><span>-</span></div>
333
- <div class="mid-col">
334
- <div class="visual-bar" id="bar-${k}"></div>
335
- <span class="strike-txt">${k}</span>
336
- <span class="delta-txt" id="val-${k}">-</span>
337
- </div>
338
- <div class="side-col" id="pe-${k}"><span>-</span><span>-</span></div>
339
- </div>
340
- `).join('');
341
-
342
- if(POLLER) clearInterval(POLLER);
343
- POLLER = setInterval(poll, 2000);
344
- poll();
345
- }
346
-
347
- async function poll() {
348
- let syms = [SPOT_SYM, ...FUT_SYMS.map(f => f.s)];
349
-
350
- // Optimize visible rows
351
- if(SPOT_PRICE > 0) {
352
- const rows = Array.from(document.querySelectorAll('.row'));
353
- let closest = rows[0], min = 999999;
354
- rows.forEach(r => {
355
- const diff = Math.abs(SPOT_PRICE - parseFloat(r.dataset.k));
356
- if(diff < min) { min = diff; closest = r; }
357
- });
358
- document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
359
- closest.classList.add('atm');
360
-
361
- if(!window.scrolled) { closest.scrollIntoView({block:'center'}); window.scrolled = true; }
362
-
363
- const idx = rows.indexOf(closest);
364
- const viewRows = rows.slice(Math.max(0, idx-15), Math.min(rows.length, idx+15));
365
- const viewKs = viewRows.map(r => parseFloat(r.dataset.k));
366
-
367
- DB.forEach(i => {
368
- if(i.e === ACTIVE_EXP && viewKs.includes(i.k)) syms.push(i.s);
369
- });
370
- }
371
-
372
- try {
373
- const res = await fetch(`/api/depth?symbols=${encodeURIComponent([...new Set(syms)].join(','))}`);
374
- const json = await res.json();
375
- if(json.s === 'ok') {
376
- document.getElementById('statusDot').style.background = 'var(--green)';
377
- updateUI(json.d);
378
- }
379
- } catch(e) { document.getElementById('statusDot').style.background = 'var(--red)'; }
380
- }
381
-
382
- function updateUI(data) {
383
- if(data[SPOT_SYM]) {
384
- SPOT_PRICE = data[SPOT_SYM].ltp;
385
- document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
386
- }
387
-
388
- // Futures
389
- document.getElementById('futBody').innerHTML = FUT_SYMS.map(obj => {
390
- const d = data[obj.s];
391
- if(!d) return '';
392
- const tot = (d.totalbuyqty + d.totalsellqty) || 1;
393
- const dt = new Date(obj.e * 1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
394
- return `
395
- <div class="fut-card">
396
- <div style="display:flex; justify-content:space-between; font-weight:700;">
397
- <span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
398
- </div>
399
- <div class="meter-bg">
400
- <div style="width:${(d.totalbuyqty/tot)*100}%; background:var(--green)"></div>
401
- <div style="width:${(d.totalsellqty/tot)*100}%; background:var(--red)"></div>
402
- </div>
403
- <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:2px;">
404
- <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
405
- </div>
406
- </div>`;
407
- }).join('');
408
-
409
- // Chain
410
- const rowData = {};
411
- for(const [sym, d] of Object.entries(data)) {
412
- const meta = DB.find(x => x.s === sym);
413
- if(!meta || meta.e !== ACTIVE_EXP) continue;
414
- if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
415
- if(sym.endsWith('CE')) rowData[meta.k].ce = d.totalsellqty;
416
- if(sym.endsWith('PE')) rowData[meta.k].pe = d.totalsellqty;
417
- }
418
-
419
- let maxD = 0;
420
- for(const k in rowData) maxD = Math.max(maxD, Math.abs(rowData[k].pe - rowData[k].ce));
421
-
422
- for(const k in rowData) {
423
- const r = rowData[k];
424
- const delta = r.pe - r.ce; // Put Sellers - Call Sellers
425
-
426
- const ceDiv = document.getElementById(`ce-${k}`);
427
- const peDiv = document.getElementById(`pe-${k}`);
428
- if(ceDiv) ceDiv.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
429
- if(peDiv) peDiv.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
430
-
431
- const bar = document.getElementById(`bar-${k}`);
432
- const val = document.getElementById(`val-${k}`);
433
-
434
- if(bar) {
435
- const pct = maxD > 0 ? (Math.abs(delta)/maxD)*100 : 0;
436
- if(delta >= 0) {
437
- bar.style.background = 'var(--green-soft)';
438
- bar.style.left = '50%';
439
- bar.style.width = (pct/2) + '%';
440
- val.style.color = 'var(--green)';
441
- val.innerText = "+" + fmt(delta);
442
- } else {
443
- bar.style.background = 'var(--red-soft)';
444
- bar.style.left = (50 - (pct/2)) + '%';
445
- bar.style.width = (pct/2) + '%';
446
- val.style.color = 'var(--red)';
447
- val.innerText = fmt(delta);
448
- }
449
- }
450
- }
451
- }
452
-
453
- function fmt(n) {
454
- if(n>=100000) return (n/100000).toFixed(2)+'L';
455
- if(n>=1000) return (n/1000).toFixed(1)+'k';
456
- return n;
457
- }
458
- </script>
459
- </body>
460
- </html>
461
- """
462
-
463
- if __name__ == "__main__":
464
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ import pandas as pd
2
+ import requests
3
+ import uvicorn
4
+ import hashlib
5
+ import json
6
+ import io
7
+ import time
8
+ from fastapi import FastAPI, Request, Response, Cookie
9
+ from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from typing import Optional
12
+
13
+ # ==========================================
14
+ # 1. CONFIGURATION (EDIT THESE)
15
+ # ==========================================
16
+ CLIENT_ID = "J4PNT5QI22-100" # Format: XPxxxxx-100
17
+ SECRET_KEY = "ABRSWC1PMR"
18
+ # IMPORTANT: Change this to your Space's URL + /callback
19
+ # Example: https://huggingface-username-space-name.hf.space/callback
20
+ REDIRECT_URI = "https://topsecrettraders-depthchain.hf.space/callback"
21
+
22
+ # Global Cache
23
+ MASTER_DATA = []
24
+ LAST_UPDATE = 0
25
+
26
+ app = FastAPI()
27
+
28
+ # ==========================================
29
+ # 2. PYTHON BACKEND (Optimized Data Loader)
30
+ # ==========================================
31
+
32
+ def load_nifty_master():
33
+ """
34
+ Downloads the large CSV, filters ONLY NIFTY 50 and NIFTY FUTS using Pandas.
35
+ This runs in seconds and stays in RAM.
36
+ """
37
+ global MASTER_DATA, LAST_UPDATE
38
+
39
+ print(">>> DOWNLOADING MASTER DATA...")
40
+ try:
41
+ url = "https://public.fyers.in/sym_details/NSE_FO.csv"
42
+ # Read CSV directly into Pandas
43
+ df = pd.read_csv(url, usecols=[0, 1, 8, 9, 13], names=['RecType','Desc','Exch','Expiry','Symbol','Unknown'], header=0)
44
+
45
+ # 1. FILTER: Symbol must contain 'NIFTY' but NOT 'BANK', 'FIN', 'MID'
46
+ # We only want pure NIFTY (NIFTY 50)
47
+ mask = (
48
+ df['Symbol'].str.contains('NIFTY', na=False) &
49
+ ~df['Symbol'].str.contains('BANKNIFTY', na=False) &
50
+ ~df['Symbol'].str.contains('FINNIFTY', na=False) &
51
+ ~df['Symbol'].str.contains('MIDCPNIFTY', na=False)
52
+ )
53
+ df = df[mask]
54
+
55
+ results = []
56
+
57
+ # Add Spot manually
58
+ results.append({"s": "NSE:NIFTY50-INDEX", "e": 0, "k": 0})
59
+
60
+ # Process Rows
61
+ for _, row in df.iterrows():
62
+ sym = row['Symbol']
63
+ desc = str(row['Desc'])
64
+ exp = int(row['Expiry'])
65
+
66
+ # Extract Strike
67
+ strike = 0.0
68
+ if "CE" in sym or "PE" in sym:
69
+ # Desc format usually: "NIFTY 06 Jan 20500 CE"
70
+ parts = desc.strip().split(' ')
71
+ # Try getting the 2nd to last item
72
+ if len(parts) >= 2:
73
+ val = parts[-2]
74
+ if val.replace('.','',1).isdigit():
75
+ strike = float(val)
76
+
77
+ results.append({
78
+ "s": sym,
79
+ "e": exp,
80
+ "k": strike
81
+ })
82
+
83
+ MASTER_DATA = results
84
+ LAST_UPDATE = time.time()
85
+ print(f">>> LOADED {len(results)} NIFTY SYMBOLS.")
86
+ return True
87
+ except Exception as e:
88
+ print(f"Error loading master: {e}")
89
+ return False
90
+
91
+ # Initialize on startup
92
+ @app.on_event("startup")
93
+ def startup_event():
94
+ load_nifty_master()
95
+
96
+ # ==========================================
97
+ # 3. ROUTES
98
+ # ==========================================
99
+
100
+ @app.get("/")
101
+ def home(access_token: Optional[str] = Cookie(None)):
102
+ if not access_token:
103
+ # Generate Auth URL
104
+ state = "init"
105
+ auth_url = (
106
+ f"https://api-t1.fyers.in/api/v3/generate-authcode?"
107
+ f"client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&response_type=code&state={state}"
108
+ )
109
+ return HTMLResponse(f"""
110
+ <div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif;">
111
+ <h1>FYERS NIFTY CHAIN (PYTHON)</h1>
112
+ <p>Please update REDIRECT_URI in app.py to match this HuggingFace Space URL.</p>
113
+ <a href="{auth_url}" style="background:#2962ff; color:white; padding:15px 30px; text-decoration:none; border-radius:5px; font-weight:bold;">LOGIN WITH FYERS</a>
114
+ </div>
115
+ """)
116
+
117
+ # If logged in, serve the App
118
+ return HTMLResponse(HTML_TEMPLATE)
119
+
120
+ @app.get("/callback")
121
+ def callback(code: str):
122
+ # Validate Auth Code
123
+ app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
124
+
125
+ payload = {
126
+ "grant_type": "authorization_code",
127
+ "appIdHash": app_id_hash,
128
+ "code": code
129
+ }
130
+
131
+ resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json=payload)
132
+ data = resp.json()
133
+
134
+ if "access_token" in data:
135
+ full_token = f"{CLIENT_ID}:{data['access_token']}"
136
+ response = RedirectResponse(url="/")
137
+ response.set_cookie(key="access_token", value=full_token, httponly=True)
138
+ return response
139
+ else:
140
+ return JSONResponse(content={"error": "Login Failed", "details": data})
141
+
142
+ @app.get("/logout")
143
+ def logout():
144
+ response = RedirectResponse(url="/")
145
+ response.delete_cookie("access_token")
146
+ return response
147
+
148
+ @app.get("/api/init")
149
+ def get_master_data(force: bool = False):
150
+ global MASTER_DATA
151
+ if force or not MASTER_DATA:
152
+ load_nifty_master()
153
+ return JSONResponse(content=MASTER_DATA)
154
+
155
+ @app.get("/api/depth")
156
+ def get_market_depth(symbols: str, access_token: Optional[str] = Cookie(None)):
157
+ if not access_token:
158
+ return JSONResponse(content={"s": "error", "message": "No Token"}, status_code=401)
159
+
160
+ url = f"https://api-t1.fyers.in/data/depth?symbol={symbols}&ohlcv_flag=1"
161
+ headers = {"Authorization": access_token}
162
+
163
+ resp = requests.get(url, headers=headers)
164
+ return JSONResponse(content=resp.json())
165
+
166
+
167
+ # ==========================================
168
+ # 4. FRONTEND (Single Page Application)
169
+ # ==========================================
170
+
171
+ HTML_TEMPLATE = """
172
+ <!DOCTYPE html>
173
+ <html lang="en">
174
+ <head>
175
+ <meta charset="UTF-8">
176
+ <title>NIFTY PYTHON CHAIN</title>
177
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
178
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
179
+ <style>
180
+ :root {
181
+ --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
182
+ --text-main: #1a1a1a; --text-sub: #555555;
183
+ --accent: #2962ff; --green: #00c853; --red: #d50000;
184
+ --green-soft: rgba(0, 200, 83, 0.3); --red-soft: rgba(213, 0, 0, 0.3);
185
+ --atm-bg: rgba(41, 98, 255, 0.1);
186
+ }
187
+ * { box-sizing: border-box; }
188
+ body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
189
+
190
+ .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; }
191
+ .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; }
192
+ .btn:hover { border-color: var(--accent); color: var(--accent); }
193
+
194
+ .grid-layout { display: flex; height: calc(100vh - 60px); }
195
+ .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
196
+ .chain-header { display: grid; grid-template-columns: 1fr 180px 1fr; padding: 10px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 700; text-align: center; color: var(--text-sub); }
197
+ .chain-body { flex: 1; overflow-y: auto; background: #fff; }
198
+
199
+ .row { display: grid; grid-template-columns: 1fr 180px 1fr; border-bottom: 1px solid #f0f0f0; height: 45px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
200
+ .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
201
+
202
+ .mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); }
203
+ .visual-bar { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; transition: 0.3s; opacity: 0.8; }
204
+ .strike-txt { z-index: 2; font-weight: 800; font-size: 14px; }
205
+ .delta-txt { z-index: 2; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.7); padding: 0 4px; border-radius: 3px; }
206
+
207
+ .side-col { padding: 0 15px; display: flex; justify-content: space-between; color: #666; font-size: 11px; }
208
+
209
+ .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
210
+ .fut-card { padding: 15px; background: #fff; border-bottom: 1px solid var(--border); }
211
+ .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
212
+
213
+ /* Modal */
214
+ .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(255,255,255,0.9); z-index: 1000; display: none; justify-content: center; align-items: center; }
215
+ .modal-box { background: #fff; border: 1px solid #ccc; width: 400px; border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); padding: 20px; }
216
+ .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 50vh; overflow-y: auto; }
217
+ .exp-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; }
218
+ .exp-btn:hover { background: #f0f7ff; border-color: var(--accent); }
219
+ .exp-btn.active { background: var(--accent); color: white; }
220
+
221
+ .loader { position: absolute; top:0; left:0; width:100%; height:100%; background:rgba(255,255,255,0.95); display:flex; justify-content:center; align-items:center; z-index: 2000; font-weight: 700; color: var(--accent); }
222
+ </style>
223
+ </head>
224
+ <body>
225
+
226
+ <div class="loader" id="loader">INITIALIZING PYTHON DB...</div>
227
+
228
+ <!-- MODAL -->
229
+ <div class="modal" id="expModal">
230
+ <div class="modal-box">
231
+ <div style="display:flex; justify-content:space-between; font-weight:700;">
232
+ <span>SELECT EXPIRY</span>
233
+ <span style="cursor:pointer" onclick="document.getElementById('expModal').style.display='none'">✕</span>
234
+ </div>
235
+ <div class="modal-grid" id="expGrid"></div>
236
+ </div>
237
+ </div>
238
+
239
+ <div class="top-nav">
240
+ <div style="font-weight:800; font-size:18px; color:var(--accent);">NIFTY 50 (PYTHON)</div>
241
+
242
+ <div style="display:flex; gap:20px; align-items:center;">
243
+ <div style="text-align:right;">
244
+ <div style="font-size:10px; font-weight:700; color:#888;">SPOT</div>
245
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:16px;">0.00</div>
246
+ </div>
247
+ <button class="btn" onclick="openExpModal()">
248
+ <span style="color:#888">EXP:</span> <span id="selExpTxt">SELECT</span>
249
+ </button>
250
+ <button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
251
+ <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
252
+ </div>
253
+ </div>
254
+
255
+ <div class="grid-layout">
256
+ <div class="chain-panel">
257
+ <div class="chain-header">
258
+ <div>CALL SELLERS (OI)</div>
259
+ <div>NET FLOW (PUT S - CALL S)</div>
260
+ <div>PUT SELLERS (OI)</div>
261
+ </div>
262
+ <div class="chain-body" id="chainBody"></div>
263
+ </div>
264
+
265
+ <div class="fut-panel">
266
+ <div style="padding:15px; font-weight:700; border-bottom:1px solid var(--border); background:#fff;">FUTURES</div>
267
+ <div id="futBody"></div>
268
+ </div>
269
+ </div>
270
+
271
+ <script>
272
+ let DB = [];
273
+ let SPOT_SYM = "NSE:NIFTY50-INDEX";
274
+ let ACTIVE_EXP = 0;
275
+ let SPOT_PRICE = 0;
276
+ let POLLER = null;
277
+ let FUT_SYMS = [];
278
+
279
+ window.onload = async () => {
280
+ try {
281
+ // Load from Python Backend
282
+ const res = await fetch('/api/init');
283
+ DB = await res.json();
284
+ document.getElementById('loader').style.display = 'none';
285
+
286
+ if(DB.length === 0) { alert("DB Empty. Check Python Logs."); return; }
287
+
288
+ // Setup Futures
289
+ FUT_SYMS = DB.filter(i => i.s.startsWith('NSE:NIFTY') && i.s.endsWith('FUT') && /\\d/.test(i.s))
290
+ .sort((a,b) => a.e - b.e).slice(0,3);
291
+
292
+ openExpModal();
293
+ } catch(e) {
294
+ document.getElementById('loader').innerText = "ERROR LOADING DATA";
295
+ }
296
+ };
297
+
298
+ function openExpModal() {
299
+ const grid = document.getElementById('expGrid');
300
+ grid.innerHTML = '';
301
+ const ops = DB.filter(i => i.s.endsWith('CE') || i.s.endsWith('PE'));
302
+ const exps = [...new Set(ops.map(i => i.e))].sort((a,b) => a-b);
303
+ const today = Math.floor(Date.now()/1000) - 86400;
304
+
305
+ exps.forEach(ts => {
306
+ if(ts < today) return;
307
+ const d = new Date(ts * 1000);
308
+ const dStr = d.toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
309
+ const div = document.createElement('div');
310
+ div.className = `exp-btn ${ts === ACTIVE_EXP ? 'active' : ''}`;
311
+ div.innerText = dStr;
312
+ div.onclick = () => {
313
+ ACTIVE_EXP = ts;
314
+ document.getElementById('selExpTxt').innerText = dStr;
315
+ document.getElementById('expModal').style.display = 'none';
316
+ buildChain();
317
+ }
318
+ grid.appendChild(div);
319
+ });
320
+ document.getElementById('expModal').style.display = 'flex';
321
+ }
322
+
323
+ function buildChain() {
324
+ const body = document.getElementById('chainBody');
325
+ body.innerHTML = '';
326
+
327
+ const relevant = DB.filter(i => i.e === ACTIVE_EXP && (i.s.endsWith('CE') || i.s.endsWith('PE')));
328
+ const strikes = [...new Set(relevant.map(i => i.k))].sort((a,b) => a-b);
329
+
330
+ body.innerHTML = strikes.map(k => `
331
+ <div class="row" id="row-${k}" data-k="${k}">
332
+ <div class="side-col" id="ce-${k}"><span>-</span><span>-</span></div>
333
+ <div class="mid-col">
334
+ <div class="visual-bar" id="bar-${k}"></div>
335
+ <span class="strike-txt">${k}</span>
336
+ <span class="delta-txt" id="val-${k}">-</span>
337
+ </div>
338
+ <div class="side-col" id="pe-${k}"><span>-</span><span>-</span></div>
339
+ </div>
340
+ `).join('');
341
+
342
+ if(POLLER) clearInterval(POLLER);
343
+ POLLER = setInterval(poll, 2000);
344
+ poll();
345
+ }
346
+
347
+ async function poll() {
348
+ let syms = [SPOT_SYM, ...FUT_SYMS.map(f => f.s)];
349
+
350
+ // Optimize visible rows
351
+ if(SPOT_PRICE > 0) {
352
+ const rows = Array.from(document.querySelectorAll('.row'));
353
+ let closest = rows[0], min = 999999;
354
+ rows.forEach(r => {
355
+ const diff = Math.abs(SPOT_PRICE - parseFloat(r.dataset.k));
356
+ if(diff < min) { min = diff; closest = r; }
357
+ });
358
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
359
+ closest.classList.add('atm');
360
+
361
+ if(!window.scrolled) { closest.scrollIntoView({block:'center'}); window.scrolled = true; }
362
+
363
+ const idx = rows.indexOf(closest);
364
+ const viewRows = rows.slice(Math.max(0, idx-15), Math.min(rows.length, idx+15));
365
+ const viewKs = viewRows.map(r => parseFloat(r.dataset.k));
366
+
367
+ DB.forEach(i => {
368
+ if(i.e === ACTIVE_EXP && viewKs.includes(i.k)) syms.push(i.s);
369
+ });
370
+ }
371
+
372
+ try {
373
+ const res = await fetch(`/api/depth?symbols=${encodeURIComponent([...new Set(syms)].join(','))}`);
374
+ const json = await res.json();
375
+ if(json.s === 'ok') {
376
+ document.getElementById('statusDot').style.background = 'var(--green)';
377
+ updateUI(json.d);
378
+ }
379
+ } catch(e) { document.getElementById('statusDot').style.background = 'var(--red)'; }
380
+ }
381
+
382
+ function updateUI(data) {
383
+ if(data[SPOT_SYM]) {
384
+ SPOT_PRICE = data[SPOT_SYM].ltp;
385
+ document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
386
+ }
387
+
388
+ // Futures
389
+ document.getElementById('futBody').innerHTML = FUT_SYMS.map(obj => {
390
+ const d = data[obj.s];
391
+ if(!d) return '';
392
+ const tot = (d.totalbuyqty + d.totalsellqty) || 1;
393
+ const dt = new Date(obj.e * 1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
394
+ return `
395
+ <div class="fut-card">
396
+ <div style="display:flex; justify-content:space-between; font-weight:700;">
397
+ <span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
398
+ </div>
399
+ <div class="meter-bg">
400
+ <div style="width:${(d.totalbuyqty/tot)*100}%; background:var(--green)"></div>
401
+ <div style="width:${(d.totalsellqty/tot)*100}%; background:var(--red)"></div>
402
+ </div>
403
+ <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:2px;">
404
+ <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
405
+ </div>
406
+ </div>`;
407
+ }).join('');
408
+
409
+ // Chain
410
+ const rowData = {};
411
+ for(const [sym, d] of Object.entries(data)) {
412
+ const meta = DB.find(x => x.s === sym);
413
+ if(!meta || meta.e !== ACTIVE_EXP) continue;
414
+ if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
415
+ if(sym.endsWith('CE')) rowData[meta.k].ce = d.totalsellqty;
416
+ if(sym.endsWith('PE')) rowData[meta.k].pe = d.totalsellqty;
417
+ }
418
+
419
+ let maxD = 0;
420
+ for(const k in rowData) maxD = Math.max(maxD, Math.abs(rowData[k].pe - rowData[k].ce));
421
+
422
+ for(const k in rowData) {
423
+ const r = rowData[k];
424
+ const delta = r.pe - r.ce; // Put Sellers - Call Sellers
425
+
426
+ const ceDiv = document.getElementById(`ce-${k}`);
427
+ const peDiv = document.getElementById(`pe-${k}`);
428
+ if(ceDiv) ceDiv.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
429
+ if(peDiv) peDiv.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
430
+
431
+ const bar = document.getElementById(`bar-${k}`);
432
+ const val = document.getElementById(`val-${k}`);
433
+
434
+ if(bar) {
435
+ const pct = maxD > 0 ? (Math.abs(delta)/maxD)*100 : 0;
436
+ if(delta >= 0) {
437
+ bar.style.background = 'var(--green-soft)';
438
+ bar.style.left = '50%';
439
+ bar.style.width = (pct/2) + '%';
440
+ val.style.color = 'var(--green)';
441
+ val.innerText = "+" + fmt(delta);
442
+ } else {
443
+ bar.style.background = 'var(--red-soft)';
444
+ bar.style.left = (50 - (pct/2)) + '%';
445
+ bar.style.width = (pct/2) + '%';
446
+ val.style.color = 'var(--red)';
447
+ val.innerText = fmt(delta);
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ function fmt(n) {
454
+ if(n>=100000) return (n/100000).toFixed(2)+'L';
455
+ if(n>=1000) return (n/1000).toFixed(1)+'k';
456
+ return n;
457
+ }
458
+ </script>
459
+ </body>
460
+ </html>
461
+ """
462
+
463
+ if __name__ == "__main__":
464
  uvicorn.run(app, host="0.0.0.0", port=7860)