| import pandas as pd |
| import requests |
| import uvicorn |
| import hashlib |
| import json |
| import time |
| import threading |
| import urllib.parse |
| from datetime import datetime |
| from fastapi import FastAPI, Request, Response, Cookie |
| from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse |
| from typing import Optional |
|
|
| |
| |
| |
| CLIENT_ID = "K4F0VGIBE1-100" |
| SECRET_KEY = "8V31L6J578" |
| REDIRECT_URI = "https://topsecrettraders-depthchainindiaq.hf.space/callback" |
|
|
| app = FastAPI() |
|
|
| |
| |
| |
|
|
| SYSTEM_STATUS = { |
| "ready": False, |
| "msg": "Booting System...", |
| "source": "Initializing" |
| } |
|
|
| |
| |
| DB = { "index": [], "stocks": [] } |
| UPDATE_LOCK = threading.Lock() |
|
|
| def get_nse_constituents(): |
| """ Connects DIRECTLY to NSE to get the live NIFTY 50 list. """ |
| url = "https://www.nseindia.com/api/equity-stockIndices?index=NIFTY 50" |
| headers = { |
| 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', |
| 'Accept': '*/*', |
| 'Accept-Language': 'en-US,en;q=0.9', |
| 'Referer': 'https://www.nseindia.com/' |
| } |
| |
| session = requests.Session() |
| try: |
| print(">>> [NSE] Handshake...") |
| session.get("https://www.nseindia.com", headers=headers, timeout=5) |
| |
| print(">>> [NSE] Fetching Constituents...") |
| response = session.get(url, headers=headers, timeout=5) |
| |
| if response.status_code == 200: |
| data = response.json().get('data', []) |
| stocks = [x for x in data if x['priority'] == 0] |
| total_ffmc = sum(x.get('ffmc', 0) for x in stocks) |
| |
| result = {} |
| for x in stocks: |
| sym = x['symbol'] |
| ffmc = x.get('ffmc', 0) |
| wgt = (ffmc / total_ffmc) * 100 if total_ffmc > 0 else 0 |
| result[sym] = wgt |
| |
| print(f">>> [NSE] Success. Loaded {len(result)} Symbols.") |
| return result |
| else: |
| print(f">>> [NSE] Failed: Status {response.status_code}") |
| return None |
| except Exception as e: |
| print(f">>> [NSE] Connection Error: {e}") |
| return None |
|
|
| def update_db(): |
| global SYSTEM_STATUS, DB |
| |
| if not UPDATE_LOCK.acquire(blocking=False): return |
| |
| try: |
| SYSTEM_STATUS["msg"] = "Connecting to NSE..." |
| nse_data = get_nse_constituents() |
| |
| if not nse_data: |
| SYSTEM_STATUS["msg"] = "NSE Connection Failed. Retrying..." |
| time.sleep(2) |
| return |
|
|
| now_ts = int(time.time()) - 86400 |
|
|
| |
| |
| |
| SYSTEM_STATUS["msg"] = "Downloading NSE Futures..." |
| csv_nse = "https://public.fyers.in/sym_details/NSE_FO.csv" |
| try: |
| df_nse = pd.read_csv(csv_nse, usecols=[0, 8, 9, 13], names=['Token', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip') |
| df_nse = df_nse[(df_nse['Expiry'] > now_ts) & (df_nse['Symbol'].str.endswith('FUT'))] |
| except Exception as e: |
| print(f">>> [NSE CSV] Error: {e}") |
| df_nse = pd.DataFrame(columns=['Token', 'Expiry', 'Symbol', 'Inst']) |
|
|
| |
| |
| |
| SYSTEM_STATUS["msg"] = "Downloading BSE Master..." |
| csv_bse = "https://public.fyers.in/sym_details/BSE_CM.csv" |
| bse_map = {} |
| try: |
| |
| |
| |
| |
| |
| df_bse = pd.read_csv(csv_bse, header=None, usecols=[9], names=['Symbol'], on_bad_lines='skip') |
| |
| |
| |
| for _, row in df_bse.iterrows(): |
| full_sym = str(row['Symbol']) |
| if full_sym.startswith("BSE:"): |
| |
| |
| |
| |
| try: |
| clean = full_sym.replace("BSE:", "") |
| if "-" in clean: |
| |
| parts = clean.rsplit('-', 1) |
| root = parts[0] |
| bse_map[root] = full_sym |
| |
| if '&' in root: |
| bse_map[root.replace('&', '_')] = full_sym |
| except: |
| continue |
| print(f">>> [BSE] Mapped {len(bse_map)} symbols.") |
| |
| except Exception as e: |
| print(f">>> [BSE CSV] Error: {e}") |
|
|
| SYSTEM_STATUS["msg"] = "Mapping Global Data..." |
| |
| final_stocks = [] |
| target_roots = list(nse_data.keys()) |
| |
| |
| nse_map = {} |
| index_futs = [] |
| |
| for _, row in df_nse.iterrows(): |
| sym = str(row['Symbol']) |
| if "NIFTY" in sym and "BANK" not in sym and "FIN" not in sym and "MID" not in sym: |
| if sym.startswith("NSE:NIFTY") and "NIFTYIT" not in sym: |
| index_futs.append(row) |
| continue |
| for root in target_roots: |
| prefix = f"NSE:{root}" |
| if sym.startswith(prefix): |
| rem = sym[len(prefix):] |
| if len(rem) > 0 and rem[0].isdigit(): |
| if root not in nse_map: nse_map[root] = [] |
| nse_map[root].append(row) |
| break |
| |
| |
| for root in target_roots: |
| |
| nse_eq_sym = f"NSE:{root}-EQ" |
| |
| nse_futs = [] |
| if root in nse_map: |
| items = sorted(nse_map[root], key=lambda x: x['Expiry']) |
| for x in items[:3]: |
| dt_lbl = datetime.fromtimestamp(x['Expiry']).strftime('%d %b').upper() |
| nse_futs.append({"s": x['Symbol'], "e": x['Expiry'], "l": dt_lbl}) |
| |
| |
| |
| bse_eq_sym = bse_map.get(root) |
| if not bse_eq_sym: |
| |
| bse_eq_sym = bse_map.get(root.replace('&', '_')) |
| |
| |
| |
| if not bse_eq_sym: |
| print(f"!!! [BSE] Missing: {root}") |
| bse_eq_sym = "" |
|
|
| final_stocks.append({ |
| "r": root, |
| "w": nse_data[root], |
| "eq": nse_eq_sym, |
| "f": nse_futs, |
| "beq": bse_eq_sym |
| }) |
| |
| final_stocks.sort(key=lambda x: x['w'], reverse=True) |
| |
| |
| index_items = sorted(index_futs, key=lambda x: x['Expiry'])[:3] |
| final_index = [] |
| for x in index_items: |
| dt_lbl = datetime.fromtimestamp(x['Expiry']).strftime('%d %b').upper() |
| final_index.append({"s": x['Symbol'], "e": x['Expiry'], "l": dt_lbl}) |
|
|
| DB = { "index": final_index, "stocks": final_stocks } |
| SYSTEM_STATUS["ready"] = True |
| SYSTEM_STATUS["msg"] = "Live" |
| SYSTEM_STATUS["source"] = "NSE & BSE Live API" |
| |
| except Exception as e: |
| print(f"Error: {e}") |
| SYSTEM_STATUS["msg"] = f"Error: {str(e)[:20]}" |
| finally: |
| UPDATE_LOCK.release() |
|
|
| @app.on_event("startup") |
| def startup(): |
| t = threading.Thread(target=update_db) |
| t.start() |
|
|
| |
| |
| |
|
|
| @app.get("/") |
| def home(access_token: Optional[str] = Cookie(None)): |
| if not access_token: |
| state = "init" |
| enc_redirect = urllib.parse.quote(REDIRECT_URI, safe="") |
| auth_url = (f"https://api-t1.fyers.in/api/v3/generate-authcode?client_id={CLIENT_ID}&redirect_uri={enc_redirect}&response_type=code&state={state}") |
| return HTMLResponse(f""" |
| <body style="background:#000;color:#fff;display:flex;justify-content:center;align-items:center;height:100vh;font-family:sans-serif;"> |
| <div style="text-align:center;border:1px solid #333;padding:40px;border-radius:8px;"> |
| <h2 style="color:#2979ff;margin-bottom:10px;">BLOOMBERG NIFTY 50</h2> |
| <p style="color:#666;font-size:12px;margin-bottom:20px;">AUTONOMOUS DATA TERMINAL</p> |
| <a href="{auth_url}" style="background:#2979ff;color:white;padding:10px 20px;text-decoration:none;border-radius:4px;font-weight:bold;">LOGIN</a> |
| </div> |
| </body>""") |
| return HTMLResponse(FRONTEND.replace("{{USER_TOKEN}}", access_token)) |
|
|
| @app.get("/callback") |
| def callback(auth_code: Optional[str]=None, code: Optional[str]=None): |
| c = auth_code or code |
| h = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest() |
| r = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={"grant_type":"authorization_code","appIdHash":h,"code":c}) |
| if r.status_code==200: |
| resp = RedirectResponse("/") |
| resp.set_cookie("access_token", f"{CLIENT_ID}:{r.json()['access_token']}", httponly=True) |
| return resp |
| return JSONResponse(r.json()) |
|
|
| @app.get("/logout") |
| def logout(): |
| r = RedirectResponse("/") |
| r.delete_cookie("access_token") |
| return r |
|
|
| @app.get("/api/config") |
| def get_conf(expiry: int = 0): |
| if not SYSTEM_STATUS["ready"] or not DB["stocks"]: |
| if not UPDATE_LOCK.locked(): threading.Thread(target=update_db).start() |
| return JSONResponse({"s": "wait", "msg": SYSTEM_STATUS["msg"]}) |
| |
| idx = max(0, min(expiry, 2)) |
| |
| stk_list = [] |
| |
| idx_sym = DB["index"][idx]["s"] if len(DB["index"]) > idx else "" |
| idx_lbl = DB["index"][idx]["l"] if len(DB["index"]) > idx else "--" |
|
|
| for s in DB["stocks"]: |
| |
| nse_f = s["f"][idx]["s"] if len(s["f"]) > idx else "" |
| nse_fl = s["f"][idx]["l"] if len(s["f"]) > idx else "" |
| |
| stk_list.append({ |
| "r": s["r"], |
| "w": s["w"], |
| "eq": s["eq"], |
| "fut": nse_f, |
| "fl": nse_fl, |
| "beq": s["beq"] |
| }) |
| |
| return JSONResponse({ |
| "s": "ok", |
| "idx": idx_sym, |
| "idx_l": idx_lbl, |
| "stocks": stk_list |
| }) |
|
|
| |
| |
| |
|
|
| FRONTEND = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>NIFTY 50 TERMINAL</title> |
| <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;700&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #000000; |
| --panel: #111111; |
| --border: #222222; |
| --accent: #ff9100; /* Bloomberg Amber/Orange */ |
| --text-main: #e0e0e0; |
| --text-muted: #666666; |
| --green: #00e676; |
| --red: #ff1744; |
| --card-bg: #0a0a0a; |
| --sel-bg: #222; |
| } |
| * { box-sizing: border-box; margin: 0; padding: 0; outline: none; } |
| body { background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } |
| |
| /* HEADER */ |
| .head { height: 40px; background: #050505; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 15px; flex-shrink: 0; } |
| .logo { font-weight: 800; font-size: 13px; letter-spacing: 1px; color: var(--accent); font-family: 'Roboto Mono'; } |
| .ctrls { display: flex; gap: 8px; } |
| .btn { background: #1a1a1a; color: #888; border: 1px solid #333; padding: 3px 10px; border-radius: 2px; font-size: 10px; font-weight: 700; cursor: pointer; font-family: 'Roboto Mono'; transition: 0.2s; text-transform: uppercase; } |
| .btn:hover { border-color: #555; color: #fff; } |
| .btn.active { background: var(--accent); color: #000; border-color: var(--accent); } |
| select.btn { padding: 3px 6px; } |
| |
| /* EXCHANGE & SEGMENT TOGGLE */ |
| .grp-sw { display: flex; border: 1px solid #333; border-radius: 2px; overflow: hidden; margin-right:5px; } |
| .grp-opt { padding: 3px 10px; font-size: 10px; font-weight: 700; cursor: pointer; background: #111; color: #666; font-family: 'Roboto Mono'; transition:0.2s; border-right:1px solid #222; } |
| .grp-opt:last-child { border-right:none; } |
| .grp-opt.on { background: #eee; color: #000; } |
| .grp-opt.on-bse { background: #00bcd4; color: #000; } |
| .grp-opt.disabled { opacity: 0.3; pointer-events: none; background: #000; } |
| |
| /* LAYOUT */ |
| .layout { display: flex; flex: 1; overflow: hidden; } |
| |
| /* SIDEBAR (AGGREGATES) */ |
| .side { width: 260px; background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; } |
| .side-head { padding: 10px; font-size: 12px; font-weight: 700; color: #fff; background:#222; border-bottom: 1px solid var(--border); letter-spacing: 1px; font-family: 'Roboto Mono'; display:flex; justify-content:space-between; } |
| .agg-list { flex: 1; overflow-y: auto; padding: 15px; } |
| |
| /* LARGER SIDEBAR FONTS */ |
| .agg-sec-t { font-size:12px; color:var(--accent); font-weight:800; margin: 15px 0 8px 0; font-family:'Roboto Mono'; text-transform: uppercase; border-bottom:1px solid #222; padding-bottom:4px; } |
| .agg-sec-t.bse { color: #00bcd4; } |
| |
| .agg-card { background: #080808; border: 1px solid #222; padding: 10px 12px; margin-bottom: 6px; border-radius: 2px; display:flex; justify-content:space-between; align-items:center; } |
| .agg-name { font-size: 11px; font-weight: 700; color: #999; text-transform: uppercase; } |
| .agg-val { font-family: 'Roboto Mono'; font-size: 14px; font-weight: 700; letter-spacing: -0.5px; } |
| |
| /* CONTENT */ |
| .content { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: var(--bg); } |
| |
| /* INDEX TICKER */ |
| .idx-strip { height: 45px; background: #080808; border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 15px; gap: 20px; flex-shrink: 0; } |
| .idx-grp { display: flex; align-items: baseline; gap: 8px; } |
| .idx-lbl { font-size: 11px; font-weight: 800; color: var(--accent); font-family: 'Roboto Mono'; } |
| .idx-val { font-family: 'Roboto Mono'; font-size: 16px; font-weight: 700; color: #fff; } |
| .idx-chg { font-family: 'Roboto Mono'; font-size: 12px; font-weight: 700; } |
| .idx-net { margin-left: auto; font-family: 'Roboto Mono'; font-size: 12px; color: #888; font-weight: 700; } |
| |
| /* GRID SYSTEM */ |
| .grid-wrap { flex: 1; overflow-y: auto; padding: 10px; } |
| .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 4px; } |
| |
| /* CARD (COMPACT) */ |
| .card { |
| background: var(--card-bg); |
| border: 1px solid #1a1a1a; |
| border-radius: 2px; |
| padding: 6px 8px; |
| display: flex; |
| flex-direction: column; |
| justify-content: center; |
| min-height: 52px; /* Super Compact */ |
| position: relative; |
| transition: all 0.1s; |
| cursor: default; |
| } |
| .card:hover { border-color: #444; z-index: 10; background: #111; } |
| |
| /* Dynamic Backgrounds */ |
| .card.bull { border-left: 2px solid var(--green); } |
| .card.bear { border-left: 2px solid var(--red); } |
| .card.na { border-left: 2px solid #333; opacity: 0.5; } |
| |
| /* Main View */ |
| .c-main { display: flex; justify-content: space-between; align-items: center; } |
| .sym { font-size: 11px; font-weight: 800; color: #ddd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 65px; } |
| .wgt { font-size: 8px; font-family: 'Roboto Mono'; color: #444; } |
| .calc-val { font-family: 'Roboto Mono'; font-size: 11px; font-weight: 700; text-align: right; } |
| |
| /* Expanded View */ |
| .c-details { display: none; margin-top: 6px; padding-top: 4px; border-top: 1px solid #222; font-family: 'Roboto Mono'; } |
| .card.expanded { min-height: 85px; justify-content: space-between; } |
| .card.expanded .c-details { display: block; } |
| |
| .det-r { display: flex; justify-content: space-between; font-size: 9px; margin-bottom: 1px; color: #666; } |
| .det-v { color: #ccc; font-weight: 700; } |
| |
| /* COLORS */ |
| .up { color: var(--green); } .dn { color: var(--red); } |
| ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: #000; } ::-webkit-scrollbar-thumb { background: #333; border-radius: 3px; } |
| |
| /* MODAL */ |
| .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.9); z-index: 100; display: none; justify-content: center; align-items: center; } |
| .modal-box { background: var(--panel); width: 500px; padding: 20px; border: 1px solid #333; box-shadow: 0 0 20px rgba(0,0,0,0.5); } |
| textarea, input { width: 100%; background: #000; color: var(--accent); border: 1px solid #333; padding: 10px; font-family: 'Roboto Mono'; font-size: 11px; margin-top: 5px; } |
| label { font-size: 10px; font-weight: 700; color: #888; font-family: 'Roboto Mono'; } |
| </style> |
| </head> |
| <body> |
| |
| <div class="modal" id="setModal"> |
| <div class="modal-box"> |
| <div style="font-family:'Roboto Mono'; color:#fff; font-size:14px; margin-bottom:15px; border-bottom:1px solid #333; padding-bottom:10px;">TERMINAL CONFIG</div> |
| |
| <label>CARD FORMULA (Grid)</label> |
| <input type="text" id="cardForm" style="margin-bottom:15px;"> |
| |
| <label>AGGREGATES JSON (Sidebar)</label> |
| <textarea id="aggJson" style="height:150px;"></textarea> |
| |
| <div style="display:flex; gap:10px; margin-top:20px;"> |
| <button class="btn active" style="flex:1; padding:8px;" onclick="saveConfig()">APPLY</button> |
| <button class="btn" style="flex:1; padding:8px;" onclick="closeModal()">CANCEL</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="head"> |
| <div class="logo">BLOOMBERG<span>NIFTY</span></div> |
| <div class="ctrls"> |
| <!-- Exchange Toggle --> |
| <div class="grp-sw"> |
| <div class="grp-opt on" id="exNSE" onclick="setEx('NSE')">NSE</div> |
| <div class="grp-opt" id="exBSE" onclick="setEx('BSE')">BSE</div> |
| </div> |
| |
| <!-- Segment Toggle --> |
| <div class="grp-sw"> |
| <div class="grp-opt" id="segEq" onclick="setSeg('eq')">EQ</div> |
| <div class="grp-opt on" id="segFut" onclick="setSeg('fut')">FUT</div> |
| </div> |
| |
| <select id="sortSel" class="btn" onchange="render()"><option value="w">SORT: WEIGHT</option><option value="calc_h">VAL: HIGH</option><option value="calc_l">VAL: LOW</option></select> |
| |
| <!-- Dynamic Expiry Text --> |
| <select id="expSel" class="btn" onchange="init()"> |
| <option value="0" id="optNear">NEAR</option> |
| <option value="1" id="optNext">NEXT</option> |
| </select> |
| |
| <button class="btn" id="togBtn" onclick="toggleDetails()">DTL</button> |
| <button class="btn" onclick="openModal()">CFG</button> |
| <button class="btn" style="color:var(--red); border-color:rgba(255,23,68,0.3)" onclick="location.href='/logout'">X</button> |
| </div> |
| </div> |
| |
| <div class="layout"> |
| <div class="side"> |
| <div class="side-head"><span>MARKET DEPTH</span> <span id="expBadge" style="color:var(--accent)">--</span></div> |
| <div class="agg-list" id="aggList"></div> |
| </div> |
| <div class="content"> |
| <div class="idx-strip"> |
| <div class="idx-grp"> |
| <span class="idx-lbl">NIFTY FUT</span> |
| <span class="idx-val" id="ixP">--.--</span> |
| <span class="idx-chg" id="ixC">--%</span> |
| </div> |
| <div class="idx-net" id="ixQ">NET: --</div> |
| </div> |
| <div class="grid-wrap"> |
| <div id="grid" class="grid"></div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| const TOKEN = "{{USER_TOKEN}}"; |
| |
| // DEFAULT CONFIG |
| let CONFIG = { |
| cardFormula: "(TBQ - TSQ) * LTP * WGT", |
| aggregates: [ |
| { "name": "NET MONEY FLOW", "formula": "(TBQ - TSQ) * LTP * WGT" }, |
| { "name": "WEIGHTED DELTA", "formula": "(TBQ - TSQ) * LTP * WGT" }, |
| { "name": "BUY PRESSURE", "formula": "TBQ * LTP * WGT" }, |
| { "name": "SELL PRESSURE", "formula": "-1 * TSQ * LTP * WGT" } |
| ] |
| }; |
| |
| let API_CONF = null; |
| let DATA = {}; |
| let POLLER = null; |
| let SHOW_DETAILS = false; |
| let SEGMENT = 'fut'; // 'eq' or 'fut' |
| let EXCHANGE = 'NSE'; // 'NSE' or 'BSE' |
| |
| window.onload = () => { |
| const stored = localStorage.getItem('n50_v9_conf'); |
| if(stored) CONFIG = JSON.parse(stored); |
| document.getElementById('cardForm').value = CONFIG.cardFormula; |
| document.getElementById('aggJson').value = JSON.stringify(CONFIG.aggregates, null, 2); |
| init(); |
| }; |
| |
| function setEx(e) { |
| EXCHANGE = e; |
| document.getElementById('exNSE').className = `grp-opt ${e==='NSE'?'on':''}`; |
| document.getElementById('exBSE').className = `grp-opt ${e==='BSE'?'on-bse':''}`; |
| |
| const futBtn = document.getElementById('segFut'); |
| const eqBtn = document.getElementById('segEq'); |
| |
| if(e === 'BSE') { |
| // BSE only has EQ allowed here |
| setSeg('eq'); |
| futBtn.classList.add('disabled'); |
| } else { |
| // NSE allows both |
| futBtn.classList.remove('disabled'); |
| } |
| render(); |
| } |
| |
| function setSeg(s) { |
| if(EXCHANGE === 'BSE' && s === 'fut') return; // Block FUT for BSE |
| SEGMENT = s; |
| document.getElementById('segEq').className = `grp-opt ${s==='eq'?'on':''}`; |
| document.getElementById('segFut').className = `grp-opt ${s==='fut'?'on':''}`; |
| render(); |
| } |
| |
| function toggleDetails() { |
| SHOW_DETAILS = !SHOW_DETAILS; |
| document.getElementById('togBtn').classList.toggle('active', SHOW_DETAILS); |
| render(); |
| } |
| |
| async function init() { |
| document.getElementById('grid').innerHTML = '<div style="padding:20px; color:#444; font-family:Roboto Mono; font-size:12px;">ESTABLISHING CONNECTION...</div>'; |
| const exp = document.getElementById('expSel').value; |
| try { |
| const r = await fetch(`/api/config?expiry=${exp}`); |
| const j = await r.json(); |
| if(j.s === 'ok') { |
| API_CONF = j; |
| |
| // Update Expiry Labels |
| if(API_CONF.stocks.length > 0) { |
| const lbl = API_CONF.stocks[0].fl || "--"; |
| document.getElementById('expBadge').innerText = lbl; |
| const selOpt = document.getElementById('expSel').options[document.getElementById('expSel').selectedIndex]; |
| const pre = document.getElementById('expSel').selectedIndex === 0 ? "NEAR" : "NEXT"; |
| selOpt.text = `${pre}: ${lbl}`; |
| } |
| |
| render(); |
| startPoll(); |
| } |
| else { |
| document.getElementById('grid').innerHTML = '<div style="padding:20px; color:#ff9100; font-family:Roboto Mono; font-size:12px;">'+j.msg.toUpperCase()+'<br>RETRYING IN 3s...</div>'; |
| setTimeout(init, 3000); |
| } |
| } catch(e) { setTimeout(init, 3000); } |
| } |
| |
| function startPoll() { |
| if(POLLER) clearInterval(POLLER); |
| fetchData(); |
| POLLER = setInterval(fetchData, 1000); |
| } |
| |
| async function fetchData() { |
| if(!API_CONF) return; |
| |
| // Fetch ALL symbols: Index + NSE(Eq,Fut) + BSE(Eq) |
| const syms = [API_CONF.idx]; |
| API_CONF.stocks.forEach(s => { |
| if(s.eq) syms.push(s.eq); |
| if(s.fut) syms.push(s.fut); |
| if(s.beq) syms.push(s.beq); |
| }); |
| |
| // Unique filter to avoid duplicates |
| const uniqueSyms = [...new Set(syms)].filter(Boolean); |
| |
| const chunks = []; |
| for (let i=0; i < uniqueSyms.length; i+=50) chunks.push(uniqueSyms.slice(i, i+50)); |
| |
| try { |
| let nd = {}; |
| await Promise.all(chunks.map(async (list) => { |
| if(!list.length) return; |
| // Encode Component handles M&M -> M%26M correctly for Fyers API |
| const q = list.map(x=>encodeURIComponent(x)).join(','); |
| const r = await fetch(`https://api-t1.fyers.in/data/depth?symbol=${q}&ohlcv_flag=1`, {headers:{'Authorization':TOKEN}}); |
| const j = await r.json(); |
| if(j.s==='ok') Object.assign(nd, j.d); |
| })); |
| DATA = nd; |
| update(); |
| } catch(e){} |
| } |
| |
| // MATH ENGINE |
| function evalF(form, d, wgt) { |
| if(!d) return 0; |
| try { |
| const TBQ = d.totalbuyqty || 0; |
| const TSQ = d.totalsellqty || 0; |
| const LTP = d.ltp || 0; |
| const WGT = wgt || 0; |
| return new Function('TBQ','TSQ','LTP','WGT','return '+form)(TBQ, TSQ, LTP, WGT); |
| } catch(e){ return 0; } |
| } |
| |
| // FORMATTER |
| function fmt(n) { |
| if(!n) return "-"; |
| let a = Math.abs(n); |
| let s = n<0?"-":"+"; |
| if(a >= 10000000) return s+(a/10000000).toFixed(2)+" Cr"; |
| if(a >= 100000) return s+(a/100000).toFixed(2)+" L"; |
| if(a >= 1000) return s+(a/1000).toFixed(1)+" k"; |
| return s+a.toFixed(0); |
| } |
| |
| function update() { |
| if(!API_CONF) return; |
| |
| // 1. Index Ticker (Futures) |
| const i = DATA[API_CONF.idx]; |
| if(i) { |
| document.getElementById('ixP').innerText = i.ltp; |
| const c = i.chp||0; |
| const ce = document.getElementById('ixC'); |
| ce.innerText = c.toFixed(2)+"%"; ce.className = "idx-chg "+(c>=0?"up":"dn"); |
| const nq = (i.totalbuyqty||0) - (i.totalsellqty||0); |
| const qe = document.getElementById('ixQ'); |
| qe.innerText = "NET: " + fmt(nq); qe.style.color = nq>=0?"var(--green)":"var(--red)"; |
| } |
| |
| // 2. Aggregates (3 Buckets now: NSE Fut, NSE Eq, BSE Eq) |
| const sumsNSEF = new Array(CONFIG.aggregates.length).fill(0); |
| const sumsNSEE = new Array(CONFIG.aggregates.length).fill(0); |
| const sumsBSEE = new Array(CONFIG.aggregates.length).fill(0); |
| |
| API_CONF.stocks.forEach(stk => { |
| if(stk.fut && DATA[stk.fut]) CONFIG.aggregates.forEach((ag, idx) => sumsNSEF[idx] += evalF(ag.formula, DATA[stk.fut], stk.w)); |
| if(stk.eq && DATA[stk.eq]) CONFIG.aggregates.forEach((ag, idx) => sumsNSEE[idx] += evalF(ag.formula, DATA[stk.eq], stk.w)); |
| if(stk.beq && DATA[stk.beq]) CONFIG.aggregates.forEach((ag, idx) => sumsBSEE[idx] += evalF(ag.formula, DATA[stk.beq], stk.w)); |
| |
| // 3. Grid Card Update |
| let activeSym = ""; |
| if(EXCHANGE === 'NSE') activeSym = SEGMENT === 'fut' ? stk.fut : stk.eq; |
| else activeSym = stk.beq; // BSE is always EQ |
| |
| const d = DATA[activeSym]; |
| const el = document.getElementById(`c-${stk.r}`); |
| |
| if(el) { |
| if(d) { |
| const cVal = evalF(CONFIG.cardFormula, d, stk.w); |
| const cv = el.querySelector('.calc-val'); |
| cv.innerText = fmt(cVal); |
| cv.style.color = cVal>=0 ? "var(--green)" : "var(--red)"; |
| el.className = "card " + (cVal>=0 ? "bull" : "bear") + (SHOW_DETAILS ? " expanded" : ""); |
| el.style.opacity = "1"; |
| |
| if(SHOW_DETAILS) { |
| el.querySelector('.v-prc').innerText = d.ltp; |
| const c = d.chp||0; |
| const ec = el.querySelector('.v-chg'); |
| ec.innerText = c.toFixed(2)+"%"; ec.style.color = c>=0?"var(--green)":"var(--red)"; |
| el.querySelector('.v-b').innerText = fmt(d.totalbuyqty); |
| el.querySelector('.v-s').innerText = fmt(d.totalsellqty); |
| } |
| } else { |
| el.className = "card na" + (SHOW_DETAILS ? " expanded" : ""); |
| el.querySelector('.calc-val').innerText = "--"; |
| el.querySelector('.calc-val').style.color = "#444"; |
| } |
| } |
| }); |
| |
| // Helper for Sidebar Items |
| const renderAgg = (title, vals, cls="") => { |
| let h = `<div class="agg-sec-t ${cls}">${title}</div>`; |
| h += CONFIG.aggregates.map((ag, i) => ` |
| <div class="agg-card"> |
| <div class="agg-name">${ag.name}</div> |
| <div class="agg-val" style="color:${vals[i]>=0?'var(--green)':'var(--red)'}">${fmt(vals[i])}</div> |
| </div> |
| `).join(''); |
| return h; |
| }; |
| |
| let html = renderAgg(`NSE FUTURES (${API_CONF.idx_l})`, sumsNSEF); |
| html += renderAgg(`NSE CASH`, sumsNSEE, "style='margin-top:20px;'"); |
| html += renderAgg(`BSE CASH`, sumsBSEE, "bse style='margin-top:20px;'"); |
| |
| document.getElementById('aggList').innerHTML = html; |
| } |
| |
| function render() { |
| if(!API_CONF) return; |
| const sort = document.getElementById('sortSel').value; |
| let lst = [...API_CONF.stocks]; |
| |
| // Sorting logic |
| if(sort.includes('calc')) { |
| lst.sort((a,b) => { |
| let symA = EXCHANGE==='NSE' ? (SEGMENT==='fut'?a.fut:a.eq) : a.beq; |
| let symB = EXCHANGE==='NSE' ? (SEGMENT==='fut'?b.fut:b.eq) : b.beq; |
| const va = evalF(CONFIG.cardFormula, DATA[symA], a.w); |
| const vb = evalF(CONFIG.cardFormula, DATA[symB], b.w); |
| return sort==='calc_h' ? vb-va : va-vb; |
| }); |
| } |
| |
| document.getElementById('grid').innerHTML = lst.map(s => ` |
| <div class="card ${SHOW_DETAILS?'expanded':''}" id="c-${s.r}"> |
| <div class="c-main"> |
| <div style="display:flex;flex-direction:column;"> |
| <span class="sym">${s.r}</span> |
| <span class="wgt">${s.w.toFixed(2)}%</span> |
| </div> |
| <div class="calc-val">--</div> |
| </div> |
| <div class="c-details"> |
| <div class="det-r"><span>LTP</span> <span class="det-v v-prc">--</span></div> |
| <div class="det-r"><span>CHG</span> <span class="det-v v-chg">--%</span></div> |
| <div class="det-r"><span>BID</span> <span class="det-v v-b" style="color:var(--green)">--</span></div> |
| <div class="det-r"><span>ASK</span> <span class="det-v v-s" style="color:var(--red)">--</span></div> |
| </div> |
| </div> |
| `).join(''); |
| |
| if(Object.keys(DATA).length>0) update(); |
| } |
| |
| function openModal() { document.getElementById('setModal').style.display='flex'; } |
| function closeModal() { document.getElementById('setModal').style.display='none'; } |
| function saveConfig() { |
| try { |
| const cf = document.getElementById('cardForm').value; |
| const ag = JSON.parse(document.getElementById('aggJson').value); |
| CONFIG = { cardFormula: cf, aggregates: ag }; |
| localStorage.setItem('n50_v9_conf', JSON.stringify(CONFIG)); |
| closeModal(); render(); update(); |
| } catch(e) { alert("Invalid JSON"); } |
| } |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |