| 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://topindianhacker-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. |
| No hardcoded backup. If this fails, the app waits and retries. |
| """ |
| 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 |
|
|
| SYSTEM_STATUS["msg"] = "Downloading Fyers Master..." |
| |
| |
| csv_url = "https://public.fyers.in/sym_details/NSE_FO.csv" |
| |
| df = pd.read_csv(csv_url, usecols=[0, 8, 9, 13], names=['Token', 'Expiry', 'Symbol', 'Inst'], header=0, on_bad_lines='skip') |
| |
| |
| now_ts = int(time.time()) - 86400 |
| df = df[(df['Expiry'] > now_ts) & (df['Symbol'].str.endswith('FUT'))] |
| |
| SYSTEM_STATUS["msg"] = "Mapping Symbols..." |
| |
| final_stocks = [] |
| target_roots = list(nse_data.keys()) |
| |
| |
| |
| |
| |
| |
| fyers_map = {} |
| index_futs = [] |
| |
| for _, row in df.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): |
| remainder = sym[len(prefix):] |
| if len(remainder) > 0 and remainder[0].isdigit(): |
| if root not in fyers_map: fyers_map[root] = [] |
| fyers_map[root].append(row) |
| break |
| |
| |
| for root in target_roots: |
| if root in fyers_map: |
| |
| items = sorted(fyers_map[root], key=lambda x: x['Expiry']) |
| |
| futs = [{"s": x['Symbol'], "e": x['Expiry']} for x in items[:3]] |
| |
| final_stocks.append({ |
| "r": root, |
| "w": nse_data[root], |
| "f": futs |
| }) |
| |
| |
| final_stocks.sort(key=lambda x: x['w'], reverse=True) |
| |
| |
| index_items = sorted(index_futs, key=lambda x: x['Expiry'])[:3] |
| final_index = [{"s": x['Symbol'], "e": x['Expiry']} for x in index_items] |
|
|
| DB = { "index": final_index, "stocks": final_stocks } |
| SYSTEM_STATUS["ready"] = True |
| SYSTEM_STATUS["msg"] = "Live" |
| SYSTEM_STATUS["source"] = "NSE Live API" |
| |
| except Exception as e: |
| print(f"Error: {e}") |
| SYSTEM_STATUS["msg"] = "Processing Error" |
| 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): |
| """ |
| Returns symbols for requested expiry. |
| If DB is empty (NSE failed), it triggers a retry. |
| """ |
| 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 = [] |
| for s in DB["stocks"]: |
| |
| if len(s["f"]) > idx: |
| stk_list.append({"r": s["r"], "w": s["w"], "s": s["f"][idx]["s"]}) |
| |
| idx_sym = DB["index"][idx]["s"] if len(DB["index"]) > idx else "" |
| |
| return JSONResponse({"s": "ok", "idx": idx_sym, "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; |
| } |
| * { 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: 10px; } |
| .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; } |
| |
| /* LAYOUT */ |
| .layout { display: flex; flex: 1; overflow: hidden; } |
| |
| /* SIDEBAR (AGGREGATES) */ |
| .side { width: 220px; background: var(--panel); border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; } |
| .side-head { padding: 10px; font-size: 10px; font-weight: 700; color: var(--accent); border-bottom: 1px solid var(--border); letter-spacing: 1px; font-family: 'Roboto Mono'; } |
| .agg-list { flex: 1; overflow-y: auto; padding: 10px; } |
| .agg-card { background: #080808; border: 1px solid #222; padding: 8px; margin-bottom: 5px; border-radius: 2px; } |
| .agg-name { font-size: 9px; font-weight: 700; color: #666; margin-bottom: 2px; text-transform: uppercase; } |
| .agg-val { font-family: 'Roboto Mono'; font-size: 13px; 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); } |
| |
| /* 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</label> |
| <input type="text" id="cardForm" style="margin-bottom:15px;"> |
| |
| <label>AGGREGATES JSON</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"> |
| <button class="btn" id="togBtn" onclick="toggleDetails()">DETAILS: OFF</button> |
| <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> |
| <select id="expSel" class="btn" onchange="init()"><option value="0">NEAR</option><option value="1">NEXT</option></select> |
| <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'">EXIT</button> |
| </div> |
| </div> |
| |
| <div class="layout"> |
| <div class="side"> |
| <div class="side-head">MARKET SUMS</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 50</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 (WEIGHTED FORMULAS) |
| 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; |
| |
| window.onload = () => { |
| const stored = localStorage.getItem('n50_v8_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 toggleDetails() { |
| SHOW_DETAILS = !SHOW_DETAILS; |
| document.getElementById('togBtn').innerText = "DETAILS: " + (SHOW_DETAILS ? "ON" : "OFF"); |
| 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; 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; |
| const s = [API_CONF.idx, ...API_CONF.stocks.map(x=>x.s)]; |
| // Batching |
| const b1 = s.slice(0,40); const b2 = s.slice(40); |
| try { |
| let nd = {}; |
| const call = async (list) => { |
| if(!list.length) return; |
| 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); |
| }; |
| await Promise.all([call(b1), call(b2)]); |
| 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; |
| // Dividing by 10^7 inside calc to keep numbers manageable before format |
| return new Function('TBQ','TSQ','LTP','WGT','return '+form)(TBQ, TSQ, LTP, WGT); |
| } catch(e){ return 0; } |
| } |
| |
| // FORMATTER (Auto Crores/Lakhs) |
| function fmt(n) { |
| if(!n) return "-"; |
| let a = Math.abs(n); |
| let s = n<0?"-":"+"; |
| |
| // Logic for huge numbers (weighted money flow) |
| 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() { |
| // Index |
| 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"); |
| |
| // Raw Net Qty for Index |
| 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)"; |
| } |
| |
| // Aggregates |
| const sums = new Array(CONFIG.aggregates.length).fill(0); |
| |
| API_CONF.stocks.forEach(stk => { |
| const d = DATA[stk.s]; |
| if(!d) return; |
| |
| CONFIG.aggregates.forEach((ag, idx) => sums[idx] += evalF(ag.formula, d, stk.w)); |
| |
| const el = document.getElementById(`c-${stk.r}`); |
| if(el) { |
| // Calculation |
| 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)"; |
| |
| // Color Bar |
| el.className = "card " + (cVal>=0 ? "bull" : "bear") + (SHOW_DETAILS ? " expanded" : ""); |
| |
| 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); |
| } |
| } |
| }); |
| |
| document.getElementById('aggList').innerHTML = CONFIG.aggregates.map((ag, i) => ` |
| <div class="agg-card"> |
| <div class="agg-name">${ag.name}</div> |
| <div class="agg-val" style="color:${sums[i]>=0?'var(--green)':'var(--red)'}">${fmt(sums[i])}</div> |
| </div> |
| `).join(''); |
| } |
| |
| function render() { |
| if(!API_CONF) return; |
| const sort = document.getElementById('sortSel').value; |
| let lst = [...API_CONF.stocks]; |
| |
| if(sort.includes('calc')) { |
| lst.sort((a,b) => { |
| const va = evalF(CONFIG.cardFormula, DATA[a.s], a.w); |
| const vb = evalF(CONFIG.cardFormula, DATA[b.s], 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_v8_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) |