topsecrettraders commited on
Commit
9441a5c
·
verified ·
1 Parent(s): 713ff62

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +485 -680
app.py CHANGED
@@ -7,823 +7,628 @@ import time
7
  import urllib.parse
8
  import os
9
  import threading
10
- import sqlite3
11
- import pytz
12
- from datetime import datetime
13
- from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Body
14
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
15
- from typing import Optional, List, Dict
16
 
17
- # ==========================================
18
- # 1. CONFIGURATION
19
- # ==========================================
20
- CLIENT_ID = "J4PNT5QI22-100"
 
21
  SECRET_KEY = "ABRSWC1PMR"
22
- # IMPORTANT: Ensure this matches your Space URL exactly
23
- REDIRECT_URI = "https://topsecrettraders-depthchain.hf.space/callback"
 
 
24
 
25
  CACHE_FILE = "nifty_master.json"
26
- DB_FILE = "history.db"
27
- IST = pytz.timezone('Asia/Kolkata')
28
 
29
  app = FastAPI()
30
 
31
- # ==========================================
32
- # 2. DATABASE & MASTER DATA (Server Side)
33
- # ==========================================
 
34
 
35
- SYSTEM_STATUS = {"ready": False, "message": "Booting..."}
 
 
 
36
  UPDATE_LOCK = threading.Lock()
37
- RECORDING_LOCK = threading.Lock()
38
- ACTIVE_JOBS = {} # To keep track of running threads
39
-
40
- def init_db():
41
- """Initialize SQLite Database"""
42
- conn = sqlite3.connect(DB_FILE)
43
- c = conn.cursor()
44
- # Jobs Table
45
- c.execute('''CREATE TABLE IF NOT EXISTS jobs (
46
- id INTEGER PRIMARY KEY AUTOINCREMENT,
47
- expiry_ts INTEGER,
48
- date_str TEXT,
49
- status TEXT,
50
- created_at INTEGER
51
- )''')
52
- # Snapshots Table (Stores full JSON response for perfect playback)
53
- c.execute('''CREATE TABLE IF NOT EXISTS snapshots (
54
- id INTEGER PRIMARY KEY AUTOINCREMENT,
55
- job_id INTEGER,
56
- timestamp INTEGER,
57
- spot_price REAL,
58
- json_data TEXT
59
- )''')
60
- conn.commit()
61
- conn.close()
62
-
63
- def get_db_connection():
64
- conn = sqlite3.connect(DB_FILE)
65
- conn.row_factory = sqlite3.Row
66
- return conn
67
 
68
  def update_master_db():
69
- """Downloads Fyers Master CSV."""
70
- global SYSTEM_STATUS
71
- if not UPDATE_LOCK.acquire(blocking=False): return
72
- try:
73
- SYSTEM_STATUS["message"] = "Downloading Master CSV..."
74
- print(">>> [BG] Downloading Master...")
75
- headers = {"User-Agent": "Mozilla/5.0"}
76
- url = "https://public.fyers.in/sym_details/NSE_FO.csv"
77
- df = pd.read_csv(url, usecols=[0,1,8,9], names=['Token','Desc','Expiry','Symbol'], header=0, on_bad_lines='skip', storage_options=headers)
78
-
79
- SYSTEM_STATUS["message"] = "Filtering NIFTY..."
80
- df['Symbol'] = df['Symbol'].astype(str)
81
- df['Desc'] = df['Desc'].astype(str)
82
- mask = (df['Symbol'].str.contains('NIFTY') & ~df['Symbol'].str.contains('BANK') & ~df['Symbol'].str.contains('FIN') & ~df['Symbol'].str.contains('MID'))
83
- df = df[mask]
84
-
85
- results = [{"s": "NSE:NIFTY50-INDEX", "e": 0, "k": 0}]
86
- for _, row in df.iterrows():
87
- sym = row['Symbol']
88
- desc = row['Desc']
89
- try: exp = int(row['Expiry'])
90
- except: exp = 0
91
- strike = 0.0
92
- if "CE" in sym or "PE" in sym:
93
- parts = desc.strip().split(' ')
94
- if len(parts) >= 2:
95
- val = parts[-2].replace('.', '', 1)
96
- if val.isdigit(): strike = float(parts[-2])
97
- results.append({"s": sym, "e": exp, "k": strike})
98
-
99
- with open(CACHE_FILE, "w") as f: json.dump(results, f)
100
- SYSTEM_STATUS["ready"] = True
101
- SYSTEM_STATUS["message"] = "Ready"
102
- print(f">>> [BG] Saved {len(results)} symbols.")
103
- except Exception as e:
104
- print(f"Error: {e}")
105
- SYSTEM_STATUS["message"] = f"Error: {str(e)}"
106
- finally:
107
- UPDATE_LOCK.release()
108
-
109
- # ==========================================
110
- # 3. RECORDING LOGIC (Background Thread)
111
- # ==========================================
112
-
113
- def recording_task(job_id: int, expiry_ts: int, access_token: str):
114
- """
115
- Runs in a separate thread. Fetches data from Fyers and saves to SQLite.
116
- """
117
- print(f">>> [RECORDER] Started Job {job_id} for Expiry {expiry_ts}")
118
-
119
- # 1. Load Symbols
120
- try:
121
- with open(CACHE_FILE, "r") as f: master = json.load(f)
122
- except:
123
- print(">>> [RECORDER] Master DB not found")
124
- return
125
-
126
- # Filter symbols for this expiry + Spot + Futures
127
- spot_sym = "NSE:NIFTY50-INDEX"
128
-
129
- # Get Futures (Next 3 months)
130
- futs = [x for x in master if x['s'].startswith('NSE:NIFTY') and x['s'].endswith('FUT')]
131
- futs.sort(key=lambda x: x['e'])
132
- futs = futs[:3]
133
 
134
- # Get Options for specific expiry
135
- opts = [x for x in master if x['e'] == expiry_ts]
 
 
136
 
137
- # Initial Symbol List (Spot + Fut + All Options for Expiry)
138
- # Note: Fyers allows max 50-100 symbols usually. If too many, we might need to filter by range.
139
- # For NIFTY, one expiry has ~100 strikes * 2 = 200 symbols.
140
- # We will fetch ALL. If URL too long, we split.
141
- # Logic: Fetch Spot + Fut + Range around ATM.
 
 
 
142
 
143
- # To keep it simple and robust: We fetch Spot first, find ATM, then fetch -20 to +20 strikes.
144
 
145
- conn = get_db_connection()
 
146
 
147
- try:
148
- while True:
149
- now_ist = datetime.now(IST)
150
- current_time_str = now_ist.strftime("%H:%M")
151
-
152
- # Stop condition (Auto stop at 3:35 PM)
153
- if now_ist.hour >= 15 and now_ist.minute >= 35:
154
- print(f">>> [RECORDER] Auto-stopping Job {job_id} at market close.")
155
- c = conn.cursor()
156
- c.execute("UPDATE jobs SET status = 'COMPLETED' WHERE id = ?", (job_id,))
157
- conn.commit()
158
- break
159
-
160
- # Check if job manually stopped
161
- cur_job = conn.execute("SELECT status FROM jobs WHERE id = ?", (job_id,)).fetchone()
162
- if not cur_job or cur_job['status'] != 'RUNNING':
163
- print(f">>> [RECORDER] Job {job_id} stopped manually.")
164
- break
165
-
166
- # 1. Fetch Spot to determine ATM
167
- try:
168
- spot_url = f"https://api-t1.fyers.in/data/depth?symbol={spot_sym}&ohlcv_flag=1"
169
- res = requests.get(spot_url, headers={"Authorization": access_token})
170
- if res.status_code != 200:
171
- print(f">>> [RECORDER] API Error: {res.status_code}")
172
- time.sleep(5)
173
- continue
174
-
175
- spot_data = res.json()
176
- if spot_data['s'] != 'ok':
177
- time.sleep(5)
178
- continue
179
-
180
- ltp = spot_data['d'][spot_sym]['ltp']
181
-
182
- # 2. Filter Symbols (Spot + Fut + +/- 25 Strikes)
183
- # This ensures we don't overload the API URL limit
184
- relevant_strikes = sorted(list(set([x['k'] for x in opts])))
185
- closest_strike = min(relevant_strikes, key=lambda x: abs(x - ltp))
186
- idx = relevant_strikes.index(closest_strike)
187
- start_idx = max(0, idx - 25)
188
- end_idx = min(len(relevant_strikes), idx + 25)
189
- selected_strikes = relevant_strikes[start_idx:end_idx]
190
-
191
- target_syms = [spot_sym] + [f['s'] for f in futs]
192
- for o in opts:
193
- if o['k'] in selected_strikes:
194
- target_syms.append(o['s'])
195
-
196
- # 3. Fetch Full Depth
197
- sym_str = ",".join(target_syms)
198
- depth_url = f"https://api-t1.fyers.in/data/depth?symbol={sym_str}&ohlcv_flag=1"
199
- depth_res = requests.get(depth_url, headers={"Authorization": access_token})
200
- final_json = depth_res.json()
201
-
202
- if final_json['s'] == 'ok':
203
- ts = int(time.time())
204
- # Store as JSON String
205
- json_str = json.dumps(final_json['d'])
206
- conn.execute(
207
- "INSERT INTO snapshots (job_id, timestamp, spot_price, json_data) VALUES (?, ?, ?, ?)",
208
- (job_id, ts, ltp, json_str)
209
- )
210
- conn.commit()
211
- print(f">>> [RECORDER] Saved Snapshot {ts} | LTP: {ltp} | Syms: {len(target_syms)}")
212
-
213
- except Exception as e:
214
- print(f">>> [RECORDER] Fetch Error: {e}")
215
 
216
- time.sleep(15) # Wait 15 seconds
217
-
218
- except Exception as e:
219
- print(f">>> [RECORDER] Critical Error: {e}")
220
- finally:
221
- conn.close()
222
- if job_id in ACTIVE_JOBS:
223
- del ACTIVE_JOBS[job_id]
224
 
225
- # ==========================================
226
- # 4. APP STARTUP & ROUTES
227
- # ==========================================
 
 
228
 
229
  @app.on_event("startup")
230
  def startup_event():
231
- init_db()
232
- if os.path.exists(CACHE_FILE):
233
- SYSTEM_STATUS["ready"] = True
234
- SYSTEM_STATUS["message"] = "Ready"
235
- else:
236
- t = threading.Thread(target=update_master_db)
237
- t.start()
 
 
 
 
 
 
238
 
239
  @app.get("/")
240
  def home(access_token: Optional[str] = Cookie(None)):
241
- if not access_token:
242
- state = "init"
243
- enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="")
244
- auth_url = (f"https://api-t1.fyers.in/api/v3/generate-authcode?"
245
- f"client_id={CLIENT_ID}&redirect_uri={enc_redirect}&response_type=code&state={state}")
246
- return HTMLResponse(f"""
247
- <div style="height:100vh; display:flex; justify-content:center; align-items:center; font-family:sans-serif; background:#f5f5f5;">
248
- <div style="background:white; padding:40px; border-radius:10px; text-align:center; box-shadow:0 5px 20px rgba(0,0,0,0.1);">
249
- <h2>Authentication Required</h2>
250
- <a href="{auth_url}" style="background:#2962ff; color:white; padding:12px 25px; text-decoration:none; border-radius:5px; font-weight:bold;">LOGIN WITH FYERS</a>
251
- </div>
252
- </div>""")
253
-
254
- return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
 
 
 
 
 
 
 
 
 
 
 
 
 
255
 
256
  @app.get("/callback")
257
  def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
258
- final_code = auth_code if auth_code else code
259
- if not final_code: return JSONResponse({"error": "No Code"})
260
- app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
261
- resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
262
- "grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
263
- })
264
- data = resp.json()
265
- if "access_token" in data:
266
- full_token = f"{CLIENT_ID}:{data['access_token']}"
267
- response = RedirectResponse(url="/")
268
- response.set_cookie(key="access_token", value=full_token, httponly=True)
269
- return response
 
 
 
 
 
 
 
 
 
270
  return HTMLResponse(f"Login Failed: {json.dumps(data)}")
271
 
272
  @app.get("/logout")
273
  def logout():
274
- r = RedirectResponse(url="/")
275
- r.delete_cookie("access_token")
276
- return r
 
 
277
 
278
  @app.get("/api/status")
279
- def get_status(): return JSONResponse(SYSTEM_STATUS)
 
280
 
281
  @app.get("/api/init")
282
  def get_master_data():
283
- if not SYSTEM_STATUS["ready"]: return JSONResponse({"s": "wait"})
284
- try:
285
- with open(CACHE_FILE, "r") as f: return JSONResponse(json.load(f))
286
- except: return JSONResponse([])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
  @app.get("/api/refresh")
289
  def force_refresh(background_tasks: BackgroundTasks):
290
- SYSTEM_STATUS["ready"] = False
291
- background_tasks.add_task(update_master_db)
292
- return JSONResponse({"s": "ok"})
 
293
 
294
- # --- RECORDING & HISTORY API ---
295
-
296
- @app.post("/api/jobs/create")
297
- def create_job(req: Request, payload: Dict = Body(...), access_token: Optional[str] = Cookie(None)):
298
- if not access_token: return JSONResponse({"s": "error", "m": "No Token"})
299
-
300
- expiry = payload.get("expiry")
301
- if not expiry: return JSONResponse({"s": "error", "m": "No Expiry"})
302
-
303
- conn = get_db_connection()
304
- c = conn.cursor()
305
-
306
- # Check if job exists for today and this expiry
307
- today_str = datetime.now(IST).strftime("%Y-%m-%d")
308
- existing = c.execute("SELECT id, status FROM jobs WHERE date_str = ? AND expiry_ts = ? AND status = 'RUNNING'",
309
- (today_str, expiry)).fetchone()
310
-
311
- if existing:
312
- return JSONResponse({"s": "ok", "msg": "Job already running", "id": existing['id']})
313
-
314
- c.execute("INSERT INTO jobs (expiry_ts, date_str, status, created_at) VALUES (?, ?, 'RUNNING', ?)",
315
- (expiry, today_str, int(time.time())))
316
- job_id = c.lastrowid
317
- conn.commit()
318
- conn.close()
319
-
320
- # Start Thread
321
- t = threading.Thread(target=recording_task, args=(job_id, expiry, access_token))
322
- t.daemon = True
323
- t.start()
324
- ACTIVE_JOBS[job_id] = t
325
-
326
- return JSONResponse({"s": "ok", "msg": "Recording Started", "id": job_id})
327
-
328
- @app.get("/api/jobs/list")
329
- def list_jobs():
330
- conn = get_db_connection()
331
- jobs = conn.execute("SELECT * FROM jobs ORDER BY id DESC LIMIT 20").fetchall()
332
- res = []
333
- for j in jobs:
334
- res.append(dict(j))
335
- conn.close()
336
- return JSONResponse(res)
337
-
338
- @app.get("/api/jobs/stop")
339
- def stop_job(id: int):
340
- conn = get_db_connection()
341
- conn.execute("UPDATE jobs SET status = 'STOPPED' WHERE id = ?", (id,))
342
- conn.commit()
343
- conn.close()
344
- return JSONResponse({"s": "ok"})
345
-
346
- @app.get("/api/history/snapshots")
347
- def get_snapshots(job_id: int):
348
- conn = get_db_connection()
349
- # Return timestamps and spot price for the slider
350
- snaps = conn.execute("SELECT timestamp, spot_price FROM snapshots WHERE job_id = ? ORDER BY timestamp ASC", (job_id,)).fetchall()
351
- conn.close()
352
- return JSONResponse([dict(s) for s in snaps])
353
-
354
- @app.get("/api/history/data")
355
- def get_history_data(job_id: int, timestamp: int):
356
- conn = get_db_connection()
357
- row = conn.execute("SELECT json_data, spot_price FROM snapshots WHERE job_id = ? AND timestamp = ?", (job_id, timestamp)).fetchone()
358
- conn.close()
359
-
360
- if row:
361
- # Reconstruct the exact format the frontend expects from Fyers
362
- return JSONResponse({
363
- "s": "ok",
364
- "d": json.loads(row['json_data'])
365
- })
366
- return JSONResponse({"s": "error"})
367
-
368
- # ==========================================
369
- # 5. FRONTEND
370
- # ==========================================
371
 
372
  HTML_TEMPLATE = """
 
373
  <!DOCTYPE html>
 
374
  <html lang="en">
375
  <head>
376
  <meta charset="UTF-8">
377
- <title>NIFTY PRO - HYBRID</title>
378
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
379
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
380
  <style>
381
- :root { --bg:#fff; --panel:#f4f6f8; --border:#e0e0e0; --accent:#2962ff; --green:#00c853; --red:#d50000; }
382
- body { margin:0; background:var(--bg); color:#1a1a1a; font-family:'Inter', sans-serif; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
383
-
384
- /* LAYOUT */
385
- .top-nav { height:60px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; padding:0 20px; background:#fff; z-index:10; }
386
- .grid-layout { display:flex; height:calc(100vh - 60px); }
387
- .chain-panel { flex:1; display:flex; flex-direction:column; }
388
- .fut-panel { width:280px; background:var(--panel); border-left:1px solid var(--border); overflow-y:auto; }
389
-
390
- /* UI ELEMENTS */
391
- .btn { padding:8px 14px; border:1px solid var(--border); background:#fff; border-radius:6px; cursor:pointer; font-weight:700; font-size:12px; transition:0.2s; }
392
- .btn:hover { border-color:var(--accent); color:var(--accent); background:#f0f7ff; }
393
- .btn.primary { background:var(--accent); color:white; border:none; }
394
- .btn.stop { color:var(--red); border-color:var(--red); }
395
-
396
- /* CHAIN */
397
- .chain-header { display:grid; grid-template-columns:1fr 140px 1fr; background:#f8f9fa; border-bottom:1px solid var(--border); padding:8px 0; font-size:11px; font-weight:700; text-align:center; color:#555; }
398
- .chain-body { flex:1; overflow-y:auto; }
399
- .row { display:grid; grid-template-columns:1fr 140px 1fr; height:40px; border-bottom:1px solid #f0f0f0; align-items:center; font-family:'JetBrains Mono'; font-size:12px; }
400
- .row.atm { background:rgba(41,98,255,0.1); border-top:1px solid var(--accent); border-bottom:1px solid var(--accent); }
401
- .mid-col { display:flex; justify-content:center; align-items:center; border-left:1px solid var(--border); border-right:1px solid var(--border); height:100%; position:relative; }
402
- .side-col { display:flex; justify-content:space-between; padding:0 10px; color:#666; font-size:11px; }
403
- .visual-bar { position:absolute; top:5px; bottom:5px; border-radius:4px; opacity:0.8; z-index:0; }
404
-
405
- /* MODALS */
406
- .modal { position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.5); display:none; justify-content:center; align-items:center; z-index:100; }
407
- .modal-box { background:#fff; width:500px; padding:20px; border-radius:8px; box-shadow:0 10px 30px rgba(0,0,0,0.2); }
408
-
409
- /* CONTROLS */
410
- .history-ctrl { background:#222; color:white; padding:10px 20px; display:flex; gap:15px; align-items:center; font-size:12px; }
411
- .slider-cont { flex:1; display:flex; flex-direction:column; }
412
- input[type=range] { width:100%; }
413
-
414
- .live-tag { background:var(--green); color:white; padding:2px 6px; border-radius:4px; font-size:10px; font-weight:800; }
415
- .hist-tag { background:#ff9800; color:white; padding:2px 6px; border-radius:4px; font-size:10px; font-weight:800; }
416
-
417
- .fut-card { background:#fff; padding:10px; border-bottom:1px solid var(--border); }
418
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  </head>
420
  <body>
421
 
422
- <!-- MODALS -->
423
- <div class="modal" id="jobModal">
 
 
 
 
424
  <div class="modal-box">
425
- <h3>Recording Manager</h3>
426
- <p style="font-size:12px; color:#666;">Create a job to record NIFTY Data (15s intervals). Server fetches data using your token.</p>
427
- <div style="display:flex; gap:10px; margin-bottom:15px;">
428
- <select id="jobExpirySel" style="padding:8px; width:200px;"></select>
429
- <button class="btn primary" onclick="startRecording()">START NEW RECORDING</button>
430
  </div>
431
- <div style="max-height:300px; overflow-y:auto; border:1px solid #eee;">
432
- <table style="width:100%; font-size:12px; border-collapse:collapse;" id="jobTable"></table>
433
- </div>
434
- <button class="btn" style="margin-top:10px; width:100%" onclick="closeModal('jobModal')">CLOSE</button>
435
  </div>
436
  </div>
437
 
438
- <!-- NAV -->
439
  <div class="top-nav">
440
  <div style="display:flex; align-items:center; gap:15px;">
441
- <div style="font-weight:800; color:var(--accent); font-size:18px;">NIFTY PRO</div>
442
- <div id="modeTag" class="live-tag">LIVE</div>
443
- <button class="btn" onclick="openJobModal()">🔴 RECORDINGS</button>
444
- <button class="btn" onclick="toggleMode()">🔄 SWITCH TO <span id="switchTxt">HISTORY</span></button>
 
445
  </div>
446
- <div style="display:flex; align-items:center; gap:15px;">
447
- <div style="text-align:right;">
448
- <div style="font-size:10px; font-weight:700; color:#888;">SPOT</div>
449
- <div id="spotPrice" style="font-weight:800; font-size:16px;">0.00</div>
450
- </div>
451
- <div style="text-align:right;">
452
- <div style="font-size:10px; font-weight:700; color:#888;">EXPIRY</div>
453
- <select id="mainExpirySel" onchange="changeExpiry()" style="border:none; font-weight:700; background:transparent; font-size:14px; text-align:right; cursor:pointer;"></select>
454
- </div>
455
- <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
456
- </div>
457
- </div>
458
 
459
- <!-- HISTORY CONTROLS -->
460
- <div class="history-ctrl" id="histCtrl" style="display:none;">
461
- <div>PLAYBACK</div>
462
- <select id="histJobSel" onchange="loadJobSnapshots()" style="background:#333; color:white; border:1px solid #555; padding:5px;"></select>
463
- <div class="slider-cont">
464
- <div style="display:flex; justify-content:space-between; margin-bottom:2px;">
465
- <span id="histTime">00:00:00</span>
466
- <span id="histSpot">Spot: 0</span>
467
- </div>
468
- <input type="range" id="timeSlider" min="0" max="100" value="0" step="1" oninput="onSliderMove()">
469
  </div>
 
 
 
 
 
 
470
  </div>
471
 
472
- <!-- MAIN GRID -->
473
  <div class="grid-layout">
474
  <div class="chain-panel">
475
  <div class="chain-header">
476
- <div>CALL SELL (OI)</div>
477
- <div>NET FLOW</div>
478
- <div>PUT SELL (OI)</div>
479
  </div>
480
  <div class="chain-body" id="chainBody"></div>
481
  </div>
482
- <div class="fut-panel" id="futBody"></div>
 
 
 
 
 
 
 
 
 
483
  </div>
484
 
485
  <script>
 
 
486
  const AUTH_TOKEN = "{{USER_TOKEN}}";
487
- let DB = [];
488
- let MODE = 'LIVE'; // LIVE or HIST
 
489
  let ACTIVE_EXP = 0;
490
  let SPOT_PRICE = 0;
491
-
492
- // Live State
493
- let LIVE_POLLER = null;
494
  let FUT_SYMS = [];
495
-
496
- // History State
497
- let SNAPSHOTS = []; // [{ts: 123, spot: 21000}, ...]
498
-
499
- window.onload = async () => {
500
- await loadMaster();
 
 
 
501
  };
502
 
503
- async function loadMaster() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  try {
505
  const res = await fetch('/api/init');
506
  DB = await res.json();
507
-
508
- // Populate Expiry Dropdowns
509
- const ops = DB.filter(i => i.s.endsWith('CE'));
510
- const exps = [...new Set(ops.map(i => i.e))].sort((a,b) => a-b);
511
- const today = Math.floor(Date.now()/1000) - 86400;
512
- const validExps = exps.filter(e => e > today);
513
-
514
- ACTIVE_EXP = validExps[0];
515
-
516
- const sel = document.getElementById('mainExpirySel');
517
- const jobSel = document.getElementById('jobExpirySel');
518
-
519
- validExps.forEach(ts => {
520
- const d = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
521
- sel.add(new Option(d, ts));
522
- jobSel.add(new Option(d, ts));
523
- });
524
-
525
  initApp();
526
- } catch(e) { console.error(e); }
 
 
527
  }
528
 
529
- function initApp() {
530
- FUT_SYMS = DB.filter(i => i.s.startsWith('NSE:NIFTY') && i.s.endsWith('FUT') && /\\d/.test(i.s)).sort((a,b)=>a.e-b.e).slice(0,3);
531
- startLiveMode();
 
 
 
 
532
  }
533
 
534
- function changeExpiry() {
535
- ACTIVE_EXP = parseInt(document.getElementById('mainExpirySel').value);
536
- if(MODE === 'LIVE') {
537
- buildChainUI();
538
- pollLive();
539
- } else {
540
- // In history mode, expiry change just rebuilds grid, data depends on recorded job
541
- buildChainUI();
542
- }
543
  }
544
 
545
- // ================= SWITCHING MODES =================
546
-
547
- function toggleMode() {
548
- if(MODE === 'LIVE') {
549
- MODE = 'HIST';
550
- document.getElementById('modeTag').innerText = "HISTORY";
551
- document.getElementById('modeTag').className = "hist-tag";
552
- document.getElementById('switchTxt').innerText = "LIVE";
553
- document.getElementById('histCtrl').style.display = "flex";
554
- stopLive();
555
- loadJobsForHistory();
556
- } else {
557
- MODE = 'LIVE';
558
- document.getElementById('modeTag').innerText = "LIVE";
559
- document.getElementById('modeTag').className = "live-tag";
560
- document.getElementById('switchTxt').innerText = "HISTORY";
561
- document.getElementById('histCtrl').style.display = "none";
562
- startLiveMode();
563
- }
 
 
 
 
564
  }
565
 
566
- // ================= LIVE LOGIC =================
 
 
 
 
567
 
568
- function startLiveMode() {
569
- buildChainUI();
570
- if(LIVE_POLLER) clearInterval(LIVE_POLLER);
571
- LIVE_POLLER = setInterval(pollLive, 1500);
572
- pollLive();
573
- }
 
 
 
 
 
574
 
575
- function stopLive() {
576
- if(LIVE_POLLER) clearInterval(LIVE_POLLER);
 
577
  }
578
 
579
- async function pollLive() {
580
- if(MODE !== 'LIVE') return;
581
-
582
- let syms = ["NSE:NIFTY50-INDEX", ...FUT_SYMS.map(f=>f.s)];
583
-
584
- // Smart range selector based on Spot
585
- const rows = Array.from(document.querySelectorAll('.row'));
586
- if(SPOT_PRICE > 0 && rows.length > 0) {
587
- let closest = rows.reduce((prev, curr) => Math.abs(parseFloat(curr.dataset.k) - SPOT_PRICE) < Math.abs(parseFloat(prev.dataset.k) - SPOT_PRICE) ? curr : prev);
588
-
589
- // Auto Scroll
590
- if(!window.isScrolled) { closest.scrollIntoView({block:'center'}); window.isScrolled=true; }
 
 
591
 
592
  // Highlight ATM
593
- document.querySelectorAll('.atm').forEach(e=>e.classList.remove('atm'));
594
  closest.classList.add('atm');
 
595
 
596
- // Select Symbols (+/- 20 strikes)
597
  const idx = rows.indexOf(closest);
598
- const subset = rows.slice(Math.max(0, idx-20), Math.min(rows.length, idx+20));
599
- subset.forEach(r => {
600
- const k = parseFloat(r.dataset.k);
601
- DB.forEach(d => { if(d.e === ACTIVE_EXP && d.k === k) syms.push(d.s); });
602
- });
603
- } else {
604
- // Fallback: fetch random range if no spot yet
605
- DB.slice(0, 50).forEach(d => { if(d.e === ACTIVE_EXP) syms.push(d.s); });
606
  }
607
 
 
 
608
  try {
609
- const url = `https://api-t1.fyers.in/data/depth?symbol=${[...new Set(syms)].join(',')}&ohlcv_flag=1`;
610
- const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN } });
611
- if(res.status === 401) location.href = "/logout";
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  const json = await res.json();
 
613
  if(json.s === 'ok') {
614
- blinkStatus('var(--green)');
 
 
 
615
  updateUI(json.d);
 
 
616
  }
617
- } catch(e) { blinkStatus('var(--red)'); }
618
- }
619
-
620
- // ================= HISTORY LOGIC =================
621
-
622
- async function loadJobsForHistory() {
623
- const res = await fetch('/api/jobs/list');
624
- const jobs = await res.json();
625
- const sel = document.getElementById('histJobSel');
626
- sel.innerHTML = '';
627
- jobs.forEach(j => {
628
- const dt = new Date(j.created_at * 1000).toLocaleString();
629
- const expDt = new Date(j.expiry_ts * 1000).toLocaleDateString();
630
- sel.add(new Option(`Job #${j.id} (${j.date_str}) - Exp: ${expDt}`, j.id));
631
- });
632
- if(jobs.length > 0) loadJobSnapshots();
633
- }
634
-
635
- async function loadJobSnapshots() {
636
- const id = document.getElementById('histJobSel').value;
637
- const res = await fetch(`/api/history/snapshots?job_id=${id}`);
638
- SNAPSHOTS = await res.json();
639
- const slider = document.getElementById('timeSlider');
640
- slider.max = SNAPSHOTS.length - 1;
641
- slider.value = 0;
642
- onSliderMove();
643
- }
644
-
645
- async function onSliderMove() {
646
- const idx = document.getElementById('timeSlider').value;
647
- if(!SNAPSHOTS[idx]) return;
648
-
649
- const snap = SNAPSHOTS[idx];
650
- const date = new Date(snap.timestamp * 1000);
651
- document.getElementById('histTime').innerText = date.toLocaleTimeString();
652
- document.getElementById('histSpot').innerText = "Spot: " + snap.spot_price;
653
-
654
- // Fetch data for this timestamp
655
- const jId = document.getElementById('histJobSel').value;
656
- const res = await fetch(`/api/history/data?job_id=${jId}&timestamp=${snap.timestamp}`);
657
- const json = await res.json();
658
- if(json.s === 'ok') {
659
- updateUI(json.d);
660
- // Sync Spot manually for history
661
- SPOT_PRICE = snap.spot_price;
662
- document.getElementById('spotPrice').innerText = SPOT_PRICE;
663
- highlightATM(SPOT_PRICE);
664
  }
665
  }
666
 
667
- // ================= SHARED UI =================
668
-
669
- function buildChainUI() {
670
- const body = document.getElementById('chainBody');
671
- const relevant = DB.filter(i => i.e === ACTIVE_EXP && (i.s.endsWith('CE') || i.s.endsWith('PE')));
672
- const strikes = [...new Set(relevant.map(i => i.k))].sort((a,b)=>a-b);
673
-
674
- body.innerHTML = strikes.map(k => `
675
- <div class="row" data-k="${k}" id="row-${k}">
676
- <div class="side-col" id="ce-${k}">-</div>
677
- <div class="mid-col">
678
- <div class="visual-bar" id="bar-${k}"></div>
679
- <span style="z-index:2; font-weight:800;">${k}</span>
680
- <span style="z-index:2; font-size:10px; background:#fff; margin-left:5px; padding:0 3px;" id="val-${k}"></span>
681
- </div>
682
- <div class="side-col" id="pe-${k}">-</div>
683
- </div>
684
- `).join('');
685
- }
686
-
687
  function updateUI(data) {
688
- // Update Futures
689
- if(data["NSE:NIFTY50-INDEX"]) SPOT_PRICE = data["NSE:NIFTY50-INDEX"].ltp;
690
- document.getElementById('spotPrice').innerText = SPOT_PRICE;
691
-
692
- const fBody = document.getElementById('futBody');
693
- let fHtml = '';
694
- FUT_SYMS.forEach(f => {
695
- const d = data[f.s];
696
- if(d) {
697
- const tot = (d.totalbuyqty + d.totalsellqty) || 1;
698
- fHtml += `
699
- <div class="fut-card">
700
- <div style="display:flex; justify-content:space-between; font-weight:700; font-size:12px;">
701
- <span>${new Date(f.e*1000).toLocaleDateString()} FUT</span>
702
- <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
703
- </div>
704
- <div style="height:4px; background:#eee; display:flex; margin-top:5px; border-radius:2px; overflow:hidden;">
705
- <div style="width:${(d.totalbuyqty/tot)*100}%; background:var(--green)"></div>
706
- <div style="width:${(d.totalsellqty/tot)*100}%; background:var(--red)"></div>
707
- </div>
708
- <div style="display:flex; justify-content:space-between; font-size:10px; color:#888;">
709
- <span>B: ${fmt(d.totalbuyqty)}</span><span>S: ${fmt(d.totalsellqty)}</span>
710
- </div>
711
- </div>`;
712
- }
713
- });
714
- fBody.innerHTML = fHtml;
715
 
716
- // Process OI
717
  const rowData = {};
718
  for(const [sym, d] of Object.entries(data)) {
719
  const meta = DB.find(x => x.s === sym);
720
  if(!meta || meta.e !== ACTIVE_EXP) continue;
721
- if(!rowData[meta.k]) rowData[meta.k] = {ce:0, pe:0};
722
  if(sym.endsWith('CE')) rowData[meta.k].ce = d.totalsellqty;
723
  if(sym.endsWith('PE')) rowData[meta.k].pe = d.totalsellqty;
724
  }
725
 
726
- let maxDiff = 0;
727
- for(let k in rowData) maxDiff = Math.max(maxDiff, Math.abs(rowData[k].pe - rowData[k].ce));
728
 
729
- for(let k in rowData) {
730
  const r = rowData[k];
731
- const diff = r.pe - r.ce; // Put Sell - Call Sell (Bullish if +ve)
732
-
733
- const ceEl = document.getElementById(`ce-${k}`);
734
- const peEl = document.getElementById(`pe-${k}`);
735
- if(ceEl) ceEl.innerHTML = `<span>S: <b>${fmt(r.ce)}</b></span>`;
736
- if(peEl) peEl.innerHTML = `<span>S: <b>${fmt(r.pe)}</b></span>`;
737
-
738
  const bar = document.getElementById(`bar-${k}`);
739
  const val = document.getElementById(`val-${k}`);
740
  if(bar) {
741
- const pct = maxDiff > 0 ? (Math.abs(diff)/maxDiff)*100 : 0;
742
- bar.style.width = (pct/2) + '%';
743
- if(diff > 0) {
744
- bar.style.background = 'var(--green-soft)';
745
- bar.style.left = '50%';
746
- val.innerText = "+" + fmt(diff);
747
- val.style.color = 'var(--green)';
748
  } else {
749
- bar.style.background = 'var(--red-soft)';
750
- bar.style.left = (50 - (pct/2)) + '%';
751
- val.innerText = fmt(diff);
752
- val.style.color = 'var(--red)';
 
753
  }
754
  }
755
  }
756
  }
757
 
758
- function highlightATM(spot) {
759
- document.querySelectorAll('.atm').forEach(e=>e.classList.remove('atm'));
760
- const rows = Array.from(document.querySelectorAll('.row'));
761
- if(rows.length===0) return;
762
- const closest = rows.reduce((prev, curr) => Math.abs(parseFloat(curr.dataset.k) - spot) < Math.abs(parseFloat(prev.dataset.k) - spot) ? curr : prev);
763
- closest.classList.add('atm');
764
- }
765
-
766
- // ================= JOB MANAGER =================
767
-
768
- function openJobModal() {
769
- document.getElementById('jobModal').style.display = 'flex';
770
- fetchJobs();
771
- }
772
- function closeModal(id) { document.getElementById(id).style.display = 'none'; }
773
-
774
- async function fetchJobs() {
775
- const res = await fetch('/api/jobs/list');
776
- const jobs = await res.json();
777
- const t = document.getElementById('jobTable');
778
- t.innerHTML = `<tr><th>ID</th><th>Date</th><th>Expiry</th><th>Status</th><th>Action</th></tr>` +
779
- jobs.map(j => `
780
- <tr>
781
- <td>${j.id}</td>
782
- <td>${j.date_str}</td>
783
- <td>${new Date(j.expiry_ts*1000).toLocaleDateString()}</td>
784
- <td style="color:${j.status==='RUNNING'?'green':'red'}">${j.status}</td>
785
- <td>
786
- ${j.status==='RUNNING' ? `<button class="btn stop" onclick="stopJob(${j.id})">STOP</button>` : '-'}
787
- </td>
788
- </tr>
789
- `).join('');
790
- }
791
-
792
- async function startRecording() {
793
- const sel = document.getElementById('jobExpirySel');
794
- const ts = parseInt(sel.value);
795
- if(!confirm("Start recording for expiry " + sel.options[sel.selectedIndex].text + "?")) return;
796
-
797
- const res = await fetch('/api/jobs/create', {
798
- method:'POST',
799
- headers:{'Content-Type':'application/json'},
800
- body: JSON.stringify({expiry: ts})
801
- });
802
- const json = await res.json();
803
- if(json.s === 'ok') { alert("Started Job #" + json.id); fetchJobs(); }
804
- else alert("Error: " + json.msg);
805
- }
806
-
807
- async function stopJob(id) {
808
- await fetch(`/api/jobs/stop?id=${id}`);
809
- fetchJobs();
810
- }
811
-
812
- function blinkStatus(color) {
813
- const d = document.getElementById('statusDot');
814
- d.style.background = color;
815
- setTimeout(()=> d.style.background='#ccc', 200);
816
- }
817
-
818
  function fmt(n) {
819
- if(n>=100000) return (n/100000).toFixed(2)+'L';
820
- if(n>=1000) return (n/1000).toFixed(1)+'k';
 
821
  return n;
822
  }
823
  </script>
 
824
  </body>
825
  </html>
826
  """
827
 
828
- if __name__ == "__main__":
829
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
 
7
  import urllib.parse
8
  import os
9
  import threading
10
+ from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks
 
 
 
11
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
12
+ from typing import Optional
13
 
14
+ ==========================================
15
+ 1. CONFIGURATION
16
+ ==========================================
17
+
18
+ CLIENT_ID = "J4PNT5QI22-100"
19
  SECRET_KEY = "ABRSWC1PMR"
20
+
21
+ IMPORTANT: Ensure this matches your Space URL exactly
22
+
23
+ REDIRECT_URI = "https://topsecrettraders-depthchain.hf.space/callback"
24
 
25
  CACHE_FILE = "nifty_master.json"
 
 
26
 
27
  app = FastAPI()
28
 
29
+ ==========================================
30
+ 2. ROBUST DATA LOADER (THREAD SAFE)
31
+ ==========================================
32
+ Global State
33
 
34
+ SYSTEM_STATUS = {
35
+ "ready": False,
36
+ "message": "Booting..."
37
+ }
38
  UPDATE_LOCK = threading.Lock()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  def update_master_db():
41
+ """
42
+ Runs in background. Downloads, filters, and saves JSON.
43
+ """
44
+ global SYSTEM_STATUS
45
+
46
+ code
47
+ Code
48
+ download
49
+ content_copy
50
+ expand_less
51
+ # Acquire lock to prevent multiple downloads
52
+ if not UPDATE_LOCK.acquire(blocking=False):
53
+ return # Already updating
54
+
55
+ try:
56
+ SYSTEM_STATUS["message"] = "Downloading Fyers Master CSV..."
57
+ print(">>> [BG TASK] STARTING DOWNLOAD...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
+ headers = {
60
+ "User-Agent": "Mozilla/5.0"
61
+ }
62
+ url = "https://public.fyers.in/sym_details/NSE_FO.csv"
63
 
64
+ df = pd.read_csv(
65
+ url,
66
+ usecols=[0, 1, 8, 9],
67
+ names=['Token', 'Desc', 'Expiry', 'Symbol'],
68
+ header=0,
69
+ on_bad_lines='skip',
70
+ storage_options=headers
71
+ )
72
 
73
+ SYSTEM_STATUS["message"] = "Filtering NIFTY Data..."
74
 
75
+ df['Symbol'] = df['Symbol'].astype(str)
76
+ df['Desc'] = df['Desc'].astype(str)
77
 
78
+ # Strict Filter: KEEP NIFTY, REMOVE BANK/FIN/MID
79
+ mask = (
80
+ df['Symbol'].str.contains('NIFTY') &
81
+ ~df['Symbol'].str.contains('BANKNIFTY') &
82
+ ~df['Symbol'].str.contains('FINNIFTY') &
83
+ ~df['Symbol'].str.contains('MIDCPNIFTY')
84
+ )
85
+ df = df[mask]
86
+
87
+ results = [{"s": "NSE:NIFTY50-INDEX", "e": 0, "k": 0}]
88
+
89
+ for _, row in df.iterrows():
90
+ sym = row['Symbol']
91
+ desc = row['Desc']
92
+ try:
93
+ exp = int(row['Expiry'])
94
+ except:
95
+ exp = 0
96
+
97
+ strike = 0.0
98
+
99
+ # Logic: Parse Strike from Desc (e.g., "NIFTY 23 JAN 21500 CE")
100
+ if "CE" in sym or "PE" in sym:
101
+ parts = desc.strip().split(' ')
102
+ if len(parts) >= 2:
103
+ val = parts[-2]
104
+ val_clean = val.replace('.', '', 1)
105
+ if val_clean.isdigit():
106
+ strike = float(val)
107
+
108
+ results.append({"s": sym, "e": exp, "k": strike})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ # Save to disk
111
+ with open(CACHE_FILE, "w") as f:
112
+ json.dump(results, f)
113
+
114
+ SYSTEM_STATUS["ready"] = True
115
+ SYSTEM_STATUS["message"] = "Ready"
116
+ print(f">>> [BG TASK] COMPLETE. Saved {len(results)} symbols.")
 
117
 
118
+ except Exception as e:
119
+ print(f">>> [BG TASK] ERROR: {e}")
120
+ SYSTEM_STATUS["message"] = f"Error: {str(e)}"
121
+ finally:
122
+ UPDATE_LOCK.release()
123
 
124
  @app.on_event("startup")
125
  def startup_event():
126
+ # check if cache exists
127
+ if os.path.exists(CACHE_FILE):
128
+ SYSTEM_STATUS["ready"] = True
129
+ SYSTEM_STATUS["message"] = "Loaded from Cache"
130
+ print(">>> STARTUP: Cache found. System Ready.")
131
+ else:
132
+ # Start background thread immediately
133
+ thread = threading.Thread(target=update_master_db)
134
+ thread.start()
135
+
136
+ ==========================================
137
+ 3. ROUTES
138
+ ==========================================
139
 
140
  @app.get("/")
141
  def home(access_token: Optional[str] = Cookie(None)):
142
+ # 1. Handle Login
143
+ if not access_token:
144
+ state = "init"
145
+ enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="")
146
+ auth_url = (
147
+ f"https://api-t1.fyers.in/api/v3/generate-authcode?"
148
+ f"client_id={CLIENT_ID}&redirect_uri={enc_redirect}&response_type=code&state={state}"
149
+ )
150
+
151
+ code
152
+ Code
153
+ download
154
+ content_copy
155
+ expand_less
156
+ return HTMLResponse(f"""
157
+ <div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif; background:#f5f5f5;">
158
+ <div style="background:white; padding:40px; border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,0.1); text-align:center;">
159
+ <h2 style="margin-top:0;">Authentication Required</h2>
160
+ <a href="{auth_url}" style="display:inline-block; background:#2962ff; color:white; padding:12px 25px; text-decoration:none; border-radius:5px; font-weight:bold; margin-top:10px;">LOGIN WITH FYERS</a>
161
+ </div>
162
+ </div>
163
+ """)
164
+
165
+ # 2. Inject Token into HTML for Direct Client-Side Fetching
166
+ # This allows the browser to call Fyers directly (No Server Lag)
167
+ rendered_html = HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token)
168
+ return HTMLResponse(rendered_html)
169
 
170
  @app.get("/callback")
171
  def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
172
+ final_code = auth_code if auth_code else code
173
+ if not final_code: return JSONResponse({"error": "No Code"})
174
+
175
+ code
176
+ Code
177
+ download
178
+ content_copy
179
+ expand_less
180
+ app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
181
+ resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
182
+ "grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
183
+ })
184
+ data = resp.json()
185
+
186
+ if "access_token" in data:
187
+ full_token = f"{CLIENT_ID}:{data['access_token']}"
188
+ response = RedirectResponse(url="/")
189
+ # Save token to cookie so python sees it on reload
190
+ response.set_cookie(key="access_token", value=full_token, httponly=True)
191
+ return response
192
+ else:
193
  return HTMLResponse(f"Login Failed: {json.dumps(data)}")
194
 
195
  @app.get("/logout")
196
  def logout():
197
+ response = RedirectResponse(url="/")
198
+ response.delete_cookie("access_token")
199
+ return response
200
+
201
+ --- STATUS & MASTER DATA ---
202
 
203
  @app.get("/api/status")
204
+ def get_status():
205
+ return JSONResponse(SYSTEM_STATUS)
206
 
207
  @app.get("/api/init")
208
  def get_master_data():
209
+ """
210
+ Returns the cached list of symbols.
211
+ This is the ONLY data heavy lifting the python server does.
212
+ """
213
+ if not SYSTEM_STATUS["ready"]:
214
+ return JSONResponse({"s": "wait", "msg": SYSTEM_STATUS["message"]})
215
+
216
+ code
217
+ Code
218
+ download
219
+ content_copy
220
+ expand_less
221
+ try:
222
+ with open(CACHE_FILE, "r") as f:
223
+ data = json.load(f)
224
+ return JSONResponse(data)
225
+ except:
226
+ return JSONResponse([])
227
 
228
  @app.get("/api/refresh")
229
  def force_refresh(background_tasks: BackgroundTasks):
230
+ SYSTEM_STATUS["ready"] = False
231
+ SYSTEM_STATUS["message"] = "Refreshing..."
232
+ background_tasks.add_task(update_master_db)
233
+ return JSONResponse({"s": "ok"})
234
 
235
+ NOTE: /api/depth REMOVED to force client-side fetching
236
+ ==========================================
237
+ 4. FRONTEND (Client-Side Fetch)
238
+ ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  HTML_TEMPLATE = """
241
+
242
  <!DOCTYPE html>
243
+
244
  <html lang="en">
245
  <head>
246
  <meta charset="UTF-8">
247
+ <title>NIFTY PRO CHAIN (Direct)</title>
248
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
249
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
250
  <style>
251
+ :root {
252
+ --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
253
+ --text-main: #1a1a1a; --text-sub: #555555;
254
+ --accent: #2962ff; --green: #00c853; --red: #d50000;
255
+ --green-soft: rgba(0, 200, 83, 0.3); --red-soft: rgba(213, 0, 0, 0.3);
256
+ --atm-bg: rgba(41, 98, 255, 0.1);
257
+ }
258
+ * { box-sizing: border-box; }
259
+ body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
260
+
261
+ code
262
+ Code
263
+ download
264
+ content_copy
265
+ expand_less
266
+ .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; }
267
+ .btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; }
268
+ .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
269
+
270
+ .grid-layout { display: flex; height: calc(100vh - 60px); }
271
+ .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
272
+ .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); }
273
+ .chain-body { flex: 1; overflow-y: auto; background: #fff; }
274
+
275
+ .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; }
276
+ .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
277
+
278
+ .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); }
279
+ .visual-bar { position: absolute; top: 6px; bottom: 6px; z-index: 0; border-radius: 4px; transition: 0.3s; opacity: 0.8; }
280
+ .strike-txt { z-index: 2; font-weight: 800; font-size: 14px; }
281
+ .delta-txt { z-index: 2; font-size: 10px; font-weight: 600; background: rgba(255,255,255,0.7); padding: 0 4px; border-radius: 3px; }
282
+
283
+ .side-col { padding: 0 15px; display: flex; justify-content: space-between; color: #666; font-size: 11px; }
284
+
285
+ .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; }
286
+ .fut-card { padding: 15px; background: #fff; border-bottom: 1px solid var(--border); }
287
+ .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
288
+
289
+ .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; }
290
+ .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; }
291
+ .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 50vh; overflow-y: auto; }
292
+ .exp-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; }
293
+ .exp-btn:hover { background: #f0f7ff; border-color: var(--accent); }
294
+ .exp-btn.active { background: var(--accent); color: white; }
295
+
296
+ /* LOADER OVERLAY */
297
+ .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
298
+ .spinner { width: 30px; height: 30px; border: 4px solid #f3f3f3; border-top: 4px solid var(--accent); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 15px; }
299
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
300
+
301
+ @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
302
+ .live { animation: blink 1s infinite; background: var(--green) !important; }
303
+ .spin-icon { animation: spin 1s linear infinite; }
304
+ </style>
305
  </head>
306
  <body>
307
 
308
+ <div class="loader-ov" id="loader">
309
+ <div class="spinner"></div>
310
+ <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px;">CONNECTING...</div>
311
+ </div>
312
+
313
+ <div class="modal" id="expModal">
314
  <div class="modal-box">
315
+ <div style="display:flex; justify-content:space-between; font-weight:700;">
316
+ <span>SELECT EXPIRY</span>
317
+ <span style="cursor:pointer" onclick="document.getElementById('expModal').style.display='none'">✕</span>
 
 
318
  </div>
319
+ <div class="modal-grid" id="expGrid"></div>
 
 
 
320
  </div>
321
  </div>
322
 
 
323
  <div class="top-nav">
324
  <div style="display:flex; align-items:center; gap:15px;">
325
+ <div style="font-weight:800; font-size:18px; color:var(--accent);">NIFTY 50</div>
326
+ <button class="btn" onclick="refreshDB()" id="refreshBtn">
327
+ <svg id="refIcon" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
328
+ REFRESH DB
329
+ </button>
330
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
331
 
332
+ code
333
+ Code
334
+ download
335
+ content_copy
336
+ expand_less
337
+ <div style="display:flex; gap:20px; align-items:center;">
338
+ <div style="text-align:right;">
339
+ <div style="font-size:10px; font-weight:700; color:#888;">SPOT</div>
340
+ <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:16px;">0.00</div>
 
341
  </div>
342
+ <button class="btn" onclick="openExpModal()">
343
+ <span style="color:#888">EXP:</span> <span id="selExpTxt">SELECT</span>
344
+ </button>
345
+ <button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
346
+ <div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
347
+ </div>
348
  </div>
349
 
 
350
  <div class="grid-layout">
351
  <div class="chain-panel">
352
  <div class="chain-header">
353
+ <div>CALL SELLERS (OI)</div>
354
+ <div>NET FLOW (PUT S - CALL S)</div>
355
+ <div>PUT SELLERS (OI)</div>
356
  </div>
357
  <div class="chain-body" id="chainBody"></div>
358
  </div>
359
+
360
+ code
361
+ Code
362
+ download
363
+ content_copy
364
+ expand_less
365
+ <div class="fut-panel">
366
+ <div style="padding:15px; font-weight:700; border-bottom:1px solid var(--border); background:#fff;">FUTURES</div>
367
+ <div id="futBody"></div>
368
+ </div>
369
  </div>
370
 
371
  <script>
372
+ // === CONFIGURATION ===
373
+ // Token is injected by Python backend securely
374
  const AUTH_TOKEN = "{{USER_TOKEN}}";
375
+
376
+ let DB = [];
377
+ let SPOT_SYM = "NSE:NIFTY50-INDEX";
378
  let ACTIVE_EXP = 0;
379
  let SPOT_PRICE = 0;
380
+ let POLLER = null;
 
 
381
  let FUT_SYMS = [];
382
+ let IS_POLLING = false;
383
+
384
+ // === STARTUP SEQUENCE ===
385
+ window.onload = () => {
386
+ if(AUTH_TOKEN.indexOf("{{") !== -1) {
387
+ alert("Auth Error. Please Reload.");
388
+ return;
389
+ }
390
+ checkStatus();
391
  };
392
 
393
+ async function checkStatus() {
394
+ const msg = document.getElementById('loaderMsg');
395
+ try {
396
+ const res = await fetch('/api/status');
397
+ const status = await res.json();
398
+
399
+ if(status.ready) {
400
+ msg.innerText = "LOADING DATA...";
401
+ loadDB();
402
+ } else {
403
+ msg.innerText = status.message.toUpperCase();
404
+ setTimeout(checkStatus, 2000);
405
+ }
406
+ } catch(e) {
407
+ msg.innerText = "CONNECTION LOST. RETRYING...";
408
+ setTimeout(checkStatus, 2000);
409
+ }
410
+ }
411
+
412
+ async function loadDB() {
413
  try {
414
  const res = await fetch('/api/init');
415
  DB = await res.json();
416
+ if(!Array.isArray(DB) || DB.length === 0) {
417
+ alert("DB Load Error. Retrying...");
418
+ refreshDB();
419
+ return;
420
+ }
421
+ document.getElementById('loader').style.display = 'none';
 
 
 
 
 
 
 
 
 
 
 
 
422
  initApp();
423
+ } catch(e) {
424
+ console.error(e);
425
+ }
426
  }
427
 
428
+ async function refreshDB() {
429
+ if(!confirm("Force download fresh data from NSE? This takes 10-20s.")) return;
430
+ const icon = document.getElementById('refIcon');
431
+ icon.classList.add('spin-icon');
432
+ await fetch('/api/refresh');
433
+ document.getElementById('loader').style.display = 'flex';
434
+ checkStatus();
435
  }
436
 
437
+ function initApp() {
438
+ FUT_SYMS = DB.filter(i => i.s.startsWith('NSE:NIFTY') && i.s.endsWith('FUT') && /\\d/.test(i.s))
439
+ .sort((a,b) => a.e - b.e).slice(0,3);
440
+ openExpModal();
 
 
 
 
 
441
  }
442
 
443
+ function openExpModal() {
444
+ const grid = document.getElementById('expGrid');
445
+ grid.innerHTML = '';
446
+ const ops = DB.filter(i => i.s.endsWith('CE') || i.s.endsWith('PE'));
447
+ const exps = [...new Set(ops.map(i => i.e))].sort((a,b) => a-b);
448
+ const today = Math.floor(Date.now()/1000) - 86400;
449
+
450
+ exps.forEach(ts => {
451
+ if(ts < today) return;
452
+ const d = new Date(ts * 1000);
453
+ const dStr = d.toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
454
+ const div = document.createElement('div');
455
+ div.className = `exp-btn ${ts === ACTIVE_EXP ? 'active' : ''}`;
456
+ div.innerText = dStr;
457
+ div.onclick = () => {
458
+ ACTIVE_EXP = ts;
459
+ document.getElementById('selExpTxt').innerText = dStr;
460
+ document.getElementById('expModal').style.display = 'none';
461
+ buildChain();
462
+ }
463
+ grid.appendChild(div);
464
+ });
465
+ document.getElementById('expModal').style.display = 'flex';
466
  }
467
 
468
+ function buildChain() {
469
+ const body = document.getElementById('chainBody');
470
+ body.innerHTML = '';
471
+ const relevant = DB.filter(i => i.e === ACTIVE_EXP && (i.s.endsWith('CE') || i.s.endsWith('PE')));
472
+ const strikes = [...new Set(relevant.map(i => i.k))].sort((a,b) => a-b);
473
 
474
+ body.innerHTML = strikes.map(k => `
475
+ <div class="row" id="row-${k}" data-k="${k}">
476
+ <div class="side-col" id="ce-${k}"><span>-</span><span>-</span></div>
477
+ <div class="mid-col">
478
+ <div class="visual-bar" id="bar-${k}"></div>
479
+ <span class="strike-txt">${k}</span>
480
+ <span class="delta-txt" id="val-${k}">-</span>
481
+ </div>
482
+ <div class="side-col" id="pe-${k}"><span>-</span><span>-</span></div>
483
+ </div>
484
+ `).join('');
485
 
486
+ if(POLLER) clearInterval(POLLER);
487
+ POLLER = setInterval(poll, 1100); // Fast Polling (1.1s)
488
+ poll();
489
  }
490
 
491
+ // === DIRECT FYERS API CALL ===
492
+ async function poll() {
493
+ if(IS_POLLING) return; // Prevent overlap
494
+ IS_POLLING = true;
495
+
496
+ // 1. Determine symbols to fetch
497
+ let syms = [SPOT_SYM, ...FUT_SYMS.map(f => f.s)];
498
+ if(SPOT_PRICE > 0) {
499
+ const rows = Array.from(document.querySelectorAll('.row'));
500
+ let closest = rows[0], min = 999999;
501
+ rows.forEach(r => {
502
+ const diff = Math.abs(SPOT_PRICE - parseFloat(r.dataset.k));
503
+ if(diff < min) { min = diff; closest = r; }
504
+ });
505
 
506
  // Highlight ATM
507
+ document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
508
  closest.classList.add('atm');
509
+ if(!window.scrolled) { closest.scrollIntoView({block:'center'}); window.scrolled = true; }
510
 
 
511
  const idx = rows.indexOf(closest);
512
+ const viewRows = rows.slice(Math.max(0, idx-15), Math.min(rows.length, idx+15));
513
+ const viewKs = viewRows.map(r => parseFloat(r.dataset.k));
514
+ DB.forEach(i => { if(i.e === ACTIVE_EXP && viewKs.includes(i.k)) syms.push(i.s); });
 
 
 
 
 
515
  }
516
 
517
+ const symbolStr = [...new Set(syms)].join(',');
518
+
519
  try {
520
+ // DIRECT CALL TO FYERS FROM BROWSER
521
+ const url = `https://api-t1.fyers.in/data/depth?symbol=${symbolStr}&ohlcv_flag=1`;
522
+
523
+ const res = await fetch(url, {
524
+ method: 'GET',
525
+ headers: {
526
+ 'Authorization': AUTH_TOKEN,
527
+ 'Content-Type': 'application/json'
528
+ }
529
+ });
530
+
531
+ if(res.status === 401) {
532
+ location.href = '/logout'; // Token expired
533
+ return;
534
+ }
535
+
536
  const json = await res.json();
537
+
538
  if(json.s === 'ok') {
539
+ const dot = document.getElementById('statusDot');
540
+ dot.style.background = 'var(--green)';
541
+ dot.classList.add('live');
542
+ setTimeout(() => dot.classList.remove('live'), 500);
543
  updateUI(json.d);
544
+ } else {
545
+ console.warn("API Error:", json);
546
  }
547
+ } catch(e) {
548
+ document.getElementById('statusDot').style.background = 'var(--red)';
549
+ console.error("Fetch Error:", e);
550
+ } finally {
551
+ IS_POLLING = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  }
553
  }
554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  function updateUI(data) {
556
+ if(data[SPOT_SYM]) {
557
+ SPOT_PRICE = data[SPOT_SYM].ltp;
558
+ document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
559
+ }
560
+ document.getElementById('futBody').innerHTML = FUT_SYMS.map(obj => {
561
+ const d = data[obj.s];
562
+ if(!d) return '';
563
+ const tot = (d.totalbuyqty + d.totalsellqty) || 1;
564
+ const dt = new Date(obj.e * 1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
565
+ return `
566
+ <div class="fut-card">
567
+ <div style="display:flex; justify-content:space-between; font-weight:700;">
568
+ <span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
569
+ </div>
570
+ <div class="meter-bg">
571
+ <div style="width:${(d.totalbuyqty/tot)*100}%; background:var(--green)"></div>
572
+ <div style="width:${(d.totalsellqty/tot)*100}%; background:var(--red)"></div>
573
+ </div>
574
+ <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:2px;">
575
+ <span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
576
+ </div>
577
+ </div>`;
578
+ }).join('');
 
 
 
 
579
 
 
580
  const rowData = {};
581
  for(const [sym, d] of Object.entries(data)) {
582
  const meta = DB.find(x => x.s === sym);
583
  if(!meta || meta.e !== ACTIVE_EXP) continue;
584
+ if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
585
  if(sym.endsWith('CE')) rowData[meta.k].ce = d.totalsellqty;
586
  if(sym.endsWith('PE')) rowData[meta.k].pe = d.totalsellqty;
587
  }
588
 
589
+ let maxD = 0;
590
+ for(const k in rowData) maxD = Math.max(maxD, Math.abs(rowData[k].pe - rowData[k].ce));
591
 
592
+ for(const k in rowData) {
593
  const r = rowData[k];
594
+ const delta = r.pe - r.ce;
595
+ const ceDiv = document.getElementById(`ce-${k}`);
596
+ const peDiv = document.getElementById(`pe-${k}`);
597
+ if(ceDiv) ceDiv.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
598
+ if(peDiv) peDiv.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
 
 
599
  const bar = document.getElementById(`bar-${k}`);
600
  const val = document.getElementById(`val-${k}`);
601
  if(bar) {
602
+ const pct = maxD > 0 ? (Math.abs(delta)/maxD)*100 : 0;
603
+ if(delta >= 0) {
604
+ bar.style.background = 'var(--green-soft)';
605
+ bar.style.left = '50%';
606
+ bar.style.width = (pct/2) + '%';
607
+ val.style.color = 'var(--green)';
608
+ val.innerText = "+" + fmt(delta);
609
  } else {
610
+ bar.style.background = 'var(--red-soft)';
611
+ bar.style.left = (50 - (pct/2)) + '%';
612
+ bar.style.width = (pct/2) + '%';
613
+ val.style.color = 'var(--red)';
614
+ val.innerText = fmt(delta);
615
  }
616
  }
617
  }
618
  }
619
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
620
  function fmt(n) {
621
+ let abs = Math.abs(n);
622
+ if(abs>=100000) return (n/100000).toFixed(2)+'L';
623
+ if(abs>=1000) return (n/1000).toFixed(1)+'k';
624
  return n;
625
  }
626
  </script>
627
+
628
  </body>
629
  </html>
630
  """
631
 
632
+
633
+ if name == "main":
634
+ uvicorn.run(app, host="0.0.0.0", port=7860)