topsecrettraders commited on
Commit
c887ab6
·
verified ·
1 Parent(s): aaa7d51

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +69 -71
app.py CHANGED
@@ -7,8 +7,7 @@ import time
7
  import urllib.parse
8
  import os
9
  import threading
10
- import httpx # Required for async non-blocking requests
11
- from fastapi import FastAPI, Request, Response, Cookie, BackgroundTasks, Depends
12
  from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
13
  from typing import Optional
14
 
@@ -24,13 +23,11 @@ CACHE_FILE = "nifty_master.json"
24
 
25
  app = FastAPI()
26
 
27
- # Global Async Client for high-performance proxying
28
- client = httpx.AsyncClient()
29
-
30
  # ==========================================
31
  # 2. ROBUST DATA LOADER (THREAD SAFE)
32
  # ==========================================
33
 
 
34
  SYSTEM_STATUS = {
35
  "ready": False,
36
  "message": "Booting..."
@@ -40,18 +37,20 @@ UPDATE_LOCK = threading.Lock()
40
  def update_master_db():
41
  """
42
  Runs in background. Downloads, filters, and saves JSON.
43
- Only keeps NIFTY symbols and Expiry details.
44
  """
45
  global SYSTEM_STATUS
46
 
 
47
  if not UPDATE_LOCK.acquire(blocking=False):
48
- return
49
 
50
  try:
51
  SYSTEM_STATUS["message"] = "Downloading Fyers Master CSV..."
52
  print(">>> [BG TASK] STARTING DOWNLOAD...")
53
 
54
- headers = {"User-Agent": "Mozilla/5.0"}
 
 
55
  url = "https://public.fyers.in/sym_details/NSE_FO.csv"
56
 
57
  df = pd.read_csv(
@@ -63,12 +62,12 @@ def update_master_db():
63
  storage_options=headers
64
  )
65
 
66
- SYSTEM_STATUS["message"] = "Processing NIFTY Data..."
67
 
68
  df['Symbol'] = df['Symbol'].astype(str)
69
  df['Desc'] = df['Desc'].astype(str)
70
 
71
- # Filter: Only NIFTY
72
  mask = (
73
  df['Symbol'].str.contains('NIFTY') &
74
  ~df['Symbol'].str.contains('BANKNIFTY') &
@@ -77,7 +76,6 @@ def update_master_db():
77
  )
78
  df = df[mask]
79
 
80
- # Structure optimized for Frontend logic
81
  results = [{"s": "NSE:NIFTY50-INDEX", "e": 0, "k": 0}]
82
 
83
  for _, row in df.iterrows():
@@ -90,7 +88,7 @@ def update_master_db():
90
 
91
  strike = 0.0
92
 
93
- # Logic: Parse Strike from Desc
94
  if "CE" in sym or "PE" in sym:
95
  parts = desc.strip().split(' ')
96
  if len(parts) >= 2:
@@ -101,6 +99,7 @@ def update_master_db():
101
 
102
  results.append({"s": sym, "e": exp, "k": strike})
103
 
 
104
  with open(CACHE_FILE, "w") as f:
105
  json.dump(results, f)
106
 
@@ -115,29 +114,24 @@ def update_master_db():
115
  UPDATE_LOCK.release()
116
 
117
  @app.on_event("startup")
118
- async def startup_event():
119
- # Initialize async client
120
- global client
121
- client = httpx.AsyncClient()
122
-
123
  if os.path.exists(CACHE_FILE):
124
  SYSTEM_STATUS["ready"] = True
125
  SYSTEM_STATUS["message"] = "Loaded from Cache"
126
  print(">>> STARTUP: Cache found. System Ready.")
127
  else:
 
128
  thread = threading.Thread(target=update_master_db)
129
  thread.start()
130
 
131
- @app.on_event("shutdown")
132
- async def shutdown_event():
133
- await client.aclose()
134
-
135
  # ==========================================
136
  # 3. ROUTES
137
  # ==========================================
138
 
139
  @app.get("/")
140
  def home(access_token: Optional[str] = Cookie(None)):
 
141
  if not access_token:
142
  state = "init"
143
  enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="")
@@ -155,7 +149,10 @@ def home(access_token: Optional[str] = Cookie(None)):
155
  </div>
156
  """)
157
 
158
- return HTMLResponse(HTML_TEMPLATE)
 
 
 
159
 
160
  @app.get("/callback")
161
  def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
@@ -163,7 +160,6 @@ def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
163
  if not final_code: return JSONResponse({"error": "No Code"})
164
 
165
  app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
166
- # Initial auth can remain synchronous as it happens once
167
  resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
168
  "grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
169
  })
@@ -172,6 +168,7 @@ def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
172
  if "access_token" in data:
173
  full_token = f"{CLIENT_ID}:{data['access_token']}"
174
  response = RedirectResponse(url="/")
 
175
  response.set_cookie(key="access_token", value=full_token, httponly=True)
176
  return response
177
  else:
@@ -183,6 +180,7 @@ def logout():
183
  response.delete_cookie("access_token")
184
  return response
185
 
 
186
  @app.get("/api/status")
187
  def get_status():
188
  return JSONResponse(SYSTEM_STATUS)
@@ -190,10 +188,12 @@ def get_status():
190
  @app.get("/api/init")
191
  def get_master_data():
192
  """
193
- Returns the Master JSON (Symbols & Expiry) only.
 
194
  """
195
  if not SYSTEM_STATUS["ready"]:
196
  return JSONResponse({"s": "wait", "msg": SYSTEM_STATUS["message"]})
 
197
  try:
198
  with open(CACHE_FILE, "r") as f:
199
  data = json.load(f)
@@ -208,37 +208,10 @@ def force_refresh(background_tasks: BackgroundTasks):
208
  background_tasks.add_task(update_master_db)
209
  return JSONResponse({"s": "ok"})
210
 
211
- # --- HIGH PERFORMANCE ASYNC PROXY ---
212
- @app.get("/api/depth")
213
- async def get_market_depth(symbols: str, response: Response, access_token: Optional[str] = Cookie(None)):
214
- """
215
- Async Proxy:
216
- 1. Receives list of symbols from Frontend.
217
- 2. Uses persistent async connection to fetch from Fyers.
218
- 3. Returns raw JSON to Frontend.
219
- Note: We must proxy because calling Fyers directly from Browser causes CORS errors.
220
- This method is non-blocking, so multiple users won't lag each other.
221
- """
222
- if not access_token:
223
- return JSONResponse(status_code=401, content={"s":"error", "code": "no_token"})
224
-
225
- response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
226
-
227
- url = f"https://api-t1.fyers.in/data/depth?symbol={symbols}&ohlcv_flag=1"
228
-
229
- try:
230
- # Use the global async client (non-blocking IO)
231
- fyers_resp = await client.get(url, headers={"Authorization": access_token})
232
-
233
- if fyers_resp.status_code == 401:
234
- return JSONResponse(status_code=401, content={"s": "auth_expired"})
235
-
236
- return JSONResponse(content=fyers_resp.json())
237
- except Exception as e:
238
- return JSONResponse(status_code=500, content={"s": "error", "msg": str(e)})
239
 
240
  # ==========================================
241
- # 4. FRONTEND
242
  # ==========================================
243
 
244
  HTML_TEMPLATE = """
@@ -246,7 +219,7 @@ HTML_TEMPLATE = """
246
  <html lang="en">
247
  <head>
248
  <meta charset="UTF-8">
249
- <title>NIFTY PRO CHAIN (ASYNC)</title>
250
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
251
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
252
  <style>
@@ -356,15 +329,26 @@ HTML_TEMPLATE = """
356
  </div>
357
 
358
  <script>
 
 
 
 
359
  let DB = [];
360
  let SPOT_SYM = "NSE:NIFTY50-INDEX";
361
  let ACTIVE_EXP = 0;
362
  let SPOT_PRICE = 0;
363
  let POLLER = null;
364
  let FUT_SYMS = [];
 
365
 
366
  // === STARTUP SEQUENCE ===
367
- window.onload = () => { checkStatus(); };
 
 
 
 
 
 
368
 
369
  async function checkStatus() {
370
  const msg = document.getElementById('loaderMsg');
@@ -385,7 +369,6 @@ HTML_TEMPLATE = """
385
  }
386
  }
387
 
388
- // 1. Backend only loads Expiry and Symbol (Master DB)
389
  async function loadDB() {
390
  try {
391
  const res = await fetch('/api/init');
@@ -406,14 +389,12 @@ HTML_TEMPLATE = """
406
  if(!confirm("Force download fresh data from NSE? This takes 10-20s.")) return;
407
  const icon = document.getElementById('refIcon');
408
  icon.classList.add('spin-icon');
409
-
410
  await fetch('/api/refresh');
411
  document.getElementById('loader').style.display = 'flex';
412
  checkStatus();
413
  }
414
 
415
  function initApp() {
416
- // Logic on Frontend: Find Futures
417
  FUT_SYMS = DB.filter(i => i.s.startsWith('NSE:NIFTY') && i.s.endsWith('FUT') && /\\d/.test(i.s))
418
  .sort((a,b) => a.e - b.e).slice(0,3);
419
  openExpModal();
@@ -447,7 +428,6 @@ HTML_TEMPLATE = """
447
  function buildChain() {
448
  const body = document.getElementById('chainBody');
449
  body.innerHTML = '';
450
- // Frontend filtering logic
451
  const relevant = DB.filter(i => i.e === ACTIVE_EXP && (i.s.endsWith('CE') || i.s.endsWith('PE')));
452
  const strikes = [...new Set(relevant.map(i => i.k))].sort((a,b) => a-b);
453
 
@@ -464,15 +444,17 @@ HTML_TEMPLATE = """
464
  `).join('');
465
 
466
  if(POLLER) clearInterval(POLLER);
467
- POLLER = setInterval(poll, 1500);
468
  poll();
469
  }
470
 
471
- // 2. Poll Logic: Decides what to fetch based on Scroll/Spot
472
  async function poll() {
 
 
 
 
473
  let syms = [SPOT_SYM, ...FUT_SYMS.map(f => f.s)];
474
-
475
- // Calculate visible range on Frontend to reduce data load
476
  if(SPOT_PRICE > 0) {
477
  const rows = Array.from(document.querySelectorAll('.row'));
478
  let closest = rows[0], min = 999999;
@@ -480,6 +462,8 @@ HTML_TEMPLATE = """
480
  const diff = Math.abs(SPOT_PRICE - parseFloat(r.dataset.k));
481
  if(diff < min) { min = diff; closest = r; }
482
  });
 
 
483
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
484
  closest.classList.add('atm');
485
  if(!window.scrolled) { closest.scrollIntoView({block:'center'}); window.scrolled = true; }
@@ -487,33 +471,47 @@ HTML_TEMPLATE = """
487
  const idx = rows.indexOf(closest);
488
  const viewRows = rows.slice(Math.max(0, idx-15), Math.min(rows.length, idx+15));
489
  const viewKs = viewRows.map(r => parseFloat(r.dataset.k));
490
-
491
- // Filter DB for symbols in view
492
  DB.forEach(i => { if(i.e === ACTIVE_EXP && viewKs.includes(i.k)) syms.push(i.s); });
493
  }
494
 
 
 
495
  try {
496
- const ts = Date.now();
497
- // Call the Async Proxy
498
- const res = await fetch(`/api/depth?symbols=${encodeURIComponent([...new Set(syms)].join(','))}&_=${ts}`);
499
 
 
 
 
 
 
 
 
 
500
  if(res.status === 401) {
501
- location.href = '/'; // Token expired
502
  return;
503
  }
504
 
505
  const json = await res.json();
 
506
  if(json.s === 'ok') {
507
  const dot = document.getElementById('statusDot');
508
  dot.style.background = 'var(--green)';
509
  dot.classList.add('live');
510
  setTimeout(() => dot.classList.remove('live'), 500);
511
  updateUI(json.d);
 
 
512
  }
513
- } catch(e) { document.getElementById('statusDot').style.background = 'var(--red)'; }
 
 
 
 
 
514
  }
515
 
516
- // 3. Update UI on Frontend
517
  function updateUI(data) {
518
  if(data[SPOT_SYM]) {
519
  SPOT_PRICE = data[SPOT_SYM].ltp;
 
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
 
 
23
 
24
  app = FastAPI()
25
 
 
 
 
26
  # ==========================================
27
  # 2. ROBUST DATA LOADER (THREAD SAFE)
28
  # ==========================================
29
 
30
+ # Global State
31
  SYSTEM_STATUS = {
32
  "ready": False,
33
  "message": "Booting..."
 
37
  def update_master_db():
38
  """
39
  Runs in background. Downloads, filters, and saves JSON.
 
40
  """
41
  global SYSTEM_STATUS
42
 
43
+ # Acquire lock to prevent multiple downloads
44
  if not UPDATE_LOCK.acquire(blocking=False):
45
+ return # Already updating
46
 
47
  try:
48
  SYSTEM_STATUS["message"] = "Downloading Fyers Master CSV..."
49
  print(">>> [BG TASK] STARTING DOWNLOAD...")
50
 
51
+ headers = {
52
+ "User-Agent": "Mozilla/5.0"
53
+ }
54
  url = "https://public.fyers.in/sym_details/NSE_FO.csv"
55
 
56
  df = pd.read_csv(
 
62
  storage_options=headers
63
  )
64
 
65
+ SYSTEM_STATUS["message"] = "Filtering NIFTY Data..."
66
 
67
  df['Symbol'] = df['Symbol'].astype(str)
68
  df['Desc'] = df['Desc'].astype(str)
69
 
70
+ # Strict Filter: KEEP NIFTY, REMOVE BANK/FIN/MID
71
  mask = (
72
  df['Symbol'].str.contains('NIFTY') &
73
  ~df['Symbol'].str.contains('BANKNIFTY') &
 
76
  )
77
  df = df[mask]
78
 
 
79
  results = [{"s": "NSE:NIFTY50-INDEX", "e": 0, "k": 0}]
80
 
81
  for _, row in df.iterrows():
 
88
 
89
  strike = 0.0
90
 
91
+ # Logic: Parse Strike from Desc (e.g., "NIFTY 23 JAN 21500 CE")
92
  if "CE" in sym or "PE" in sym:
93
  parts = desc.strip().split(' ')
94
  if len(parts) >= 2:
 
99
 
100
  results.append({"s": sym, "e": exp, "k": strike})
101
 
102
+ # Save to disk
103
  with open(CACHE_FILE, "w") as f:
104
  json.dump(results, f)
105
 
 
114
  UPDATE_LOCK.release()
115
 
116
  @app.on_event("startup")
117
+ def startup_event():
118
+ # check if cache exists
 
 
 
119
  if os.path.exists(CACHE_FILE):
120
  SYSTEM_STATUS["ready"] = True
121
  SYSTEM_STATUS["message"] = "Loaded from Cache"
122
  print(">>> STARTUP: Cache found. System Ready.")
123
  else:
124
+ # Start background thread immediately
125
  thread = threading.Thread(target=update_master_db)
126
  thread.start()
127
 
 
 
 
 
128
  # ==========================================
129
  # 3. ROUTES
130
  # ==========================================
131
 
132
  @app.get("/")
133
  def home(access_token: Optional[str] = Cookie(None)):
134
+ # 1. Handle Login
135
  if not access_token:
136
  state = "init"
137
  enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="")
 
149
  </div>
150
  """)
151
 
152
+ # 2. Inject Token into HTML for Direct Client-Side Fetching
153
+ # This allows the browser to call Fyers directly (No Server Lag)
154
+ rendered_html = HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token)
155
+ return HTMLResponse(rendered_html)
156
 
157
  @app.get("/callback")
158
  def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
 
160
  if not final_code: return JSONResponse({"error": "No Code"})
161
 
162
  app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
 
163
  resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
164
  "grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
165
  })
 
168
  if "access_token" in data:
169
  full_token = f"{CLIENT_ID}:{data['access_token']}"
170
  response = RedirectResponse(url="/")
171
+ # Save token to cookie so python sees it on reload
172
  response.set_cookie(key="access_token", value=full_token, httponly=True)
173
  return response
174
  else:
 
180
  response.delete_cookie("access_token")
181
  return response
182
 
183
+ # --- STATUS & MASTER DATA ---
184
  @app.get("/api/status")
185
  def get_status():
186
  return JSONResponse(SYSTEM_STATUS)
 
188
  @app.get("/api/init")
189
  def get_master_data():
190
  """
191
+ Returns the cached list of symbols.
192
+ This is the ONLY data heavy lifting the python server does.
193
  """
194
  if not SYSTEM_STATUS["ready"]:
195
  return JSONResponse({"s": "wait", "msg": SYSTEM_STATUS["message"]})
196
+
197
  try:
198
  with open(CACHE_FILE, "r") as f:
199
  data = json.load(f)
 
208
  background_tasks.add_task(update_master_db)
209
  return JSONResponse({"s": "ok"})
210
 
211
+ # NOTE: /api/depth REMOVED to force client-side fetching
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
 
213
  # ==========================================
214
+ # 4. FRONTEND (Client-Side Fetch)
215
  # ==========================================
216
 
217
  HTML_TEMPLATE = """
 
219
  <html lang="en">
220
  <head>
221
  <meta charset="UTF-8">
222
+ <title>NIFTY PRO CHAIN (Direct)</title>
223
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
224
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
225
  <style>
 
329
  </div>
330
 
331
  <script>
332
+ // === CONFIGURATION ===
333
+ // Token is injected by Python backend securely
334
+ const AUTH_TOKEN = "{{USER_TOKEN}}";
335
+
336
  let DB = [];
337
  let SPOT_SYM = "NSE:NIFTY50-INDEX";
338
  let ACTIVE_EXP = 0;
339
  let SPOT_PRICE = 0;
340
  let POLLER = null;
341
  let FUT_SYMS = [];
342
+ let IS_POLLING = false;
343
 
344
  // === STARTUP SEQUENCE ===
345
+ window.onload = () => {
346
+ if(AUTH_TOKEN.indexOf("{{") !== -1) {
347
+ alert("Auth Error. Please Reload.");
348
+ return;
349
+ }
350
+ checkStatus();
351
+ };
352
 
353
  async function checkStatus() {
354
  const msg = document.getElementById('loaderMsg');
 
369
  }
370
  }
371
 
 
372
  async function loadDB() {
373
  try {
374
  const res = await fetch('/api/init');
 
389
  if(!confirm("Force download fresh data from NSE? This takes 10-20s.")) return;
390
  const icon = document.getElementById('refIcon');
391
  icon.classList.add('spin-icon');
 
392
  await fetch('/api/refresh');
393
  document.getElementById('loader').style.display = 'flex';
394
  checkStatus();
395
  }
396
 
397
  function initApp() {
 
398
  FUT_SYMS = DB.filter(i => i.s.startsWith('NSE:NIFTY') && i.s.endsWith('FUT') && /\\d/.test(i.s))
399
  .sort((a,b) => a.e - b.e).slice(0,3);
400
  openExpModal();
 
428
  function buildChain() {
429
  const body = document.getElementById('chainBody');
430
  body.innerHTML = '';
 
431
  const relevant = DB.filter(i => i.e === ACTIVE_EXP && (i.s.endsWith('CE') || i.s.endsWith('PE')));
432
  const strikes = [...new Set(relevant.map(i => i.k))].sort((a,b) => a-b);
433
 
 
444
  `).join('');
445
 
446
  if(POLLER) clearInterval(POLLER);
447
+ POLLER = setInterval(poll, 1100); // Fast Polling (1.1s)
448
  poll();
449
  }
450
 
451
+ // === DIRECT FYERS API CALL ===
452
  async function poll() {
453
+ if(IS_POLLING) return; // Prevent overlap
454
+ IS_POLLING = true;
455
+
456
+ // 1. Determine symbols to fetch
457
  let syms = [SPOT_SYM, ...FUT_SYMS.map(f => f.s)];
 
 
458
  if(SPOT_PRICE > 0) {
459
  const rows = Array.from(document.querySelectorAll('.row'));
460
  let closest = rows[0], min = 999999;
 
462
  const diff = Math.abs(SPOT_PRICE - parseFloat(r.dataset.k));
463
  if(diff < min) { min = diff; closest = r; }
464
  });
465
+
466
+ // Highlight ATM
467
  document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
468
  closest.classList.add('atm');
469
  if(!window.scrolled) { closest.scrollIntoView({block:'center'}); window.scrolled = true; }
 
471
  const idx = rows.indexOf(closest);
472
  const viewRows = rows.slice(Math.max(0, idx-15), Math.min(rows.length, idx+15));
473
  const viewKs = viewRows.map(r => parseFloat(r.dataset.k));
 
 
474
  DB.forEach(i => { if(i.e === ACTIVE_EXP && viewKs.includes(i.k)) syms.push(i.s); });
475
  }
476
 
477
+ const symbolStr = [...new Set(syms)].join(',');
478
+
479
  try {
480
+ // DIRECT CALL TO FYERS FROM BROWSER
481
+ const url = `https://api-t1.fyers.in/data/depth?symbol=${symbolStr}&ohlcv_flag=1`;
 
482
 
483
+ const res = await fetch(url, {
484
+ method: 'GET',
485
+ headers: {
486
+ 'Authorization': AUTH_TOKEN,
487
+ 'Content-Type': 'application/json'
488
+ }
489
+ });
490
+
491
  if(res.status === 401) {
492
+ location.href = '/logout'; // Token expired
493
  return;
494
  }
495
 
496
  const json = await res.json();
497
+
498
  if(json.s === 'ok') {
499
  const dot = document.getElementById('statusDot');
500
  dot.style.background = 'var(--green)';
501
  dot.classList.add('live');
502
  setTimeout(() => dot.classList.remove('live'), 500);
503
  updateUI(json.d);
504
+ } else {
505
+ console.warn("API Error:", json);
506
  }
507
+ } catch(e) {
508
+ document.getElementById('statusDot').style.background = 'var(--red)';
509
+ console.error("Fetch Error:", e);
510
+ } finally {
511
+ IS_POLLING = false;
512
+ }
513
  }
514
 
 
515
  function updateUI(data) {
516
  if(data[SPOT_SYM]) {
517
  SPOT_PRICE = data[SPOT_SYM].ltp;