Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,8 +7,7 @@ import time
|
|
| 7 |
import urllib.parse
|
| 8 |
import os
|
| 9 |
import threading
|
| 10 |
-
|
| 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 = {
|
|
|
|
|
|
|
| 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"] = "
|
| 67 |
|
| 68 |
df['Symbol'] = df['Symbol'].astype(str)
|
| 69 |
df['Desc'] = df['Desc'].astype(str)
|
| 70 |
|
| 71 |
-
# Filter:
|
| 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 |
-
|
| 119 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 |
-
#
|
| 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 (
|
| 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 = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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,
|
| 468 |
poll();
|
| 469 |
}
|
| 470 |
|
| 471 |
-
//
|
| 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 |
-
|
| 497 |
-
|
| 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) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|