| import pandas as pd |
| import requests |
| import uvicorn |
| import json |
| import time |
| import os |
| import threading |
| from datetime import datetime |
| from fastapi import FastAPI, Cookie, BackgroundTasks, Query |
| from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse |
| from typing import Optional |
|
|
| |
| |
| |
| |
| API_KEY = "13c3ac18-b78f-4e5b-a6b8-d8f66de0b52c" |
| API_SECRET = "qymj4zn0i1" |
| REDIRECT_URI = "https://topsecrettraders-depthchainindiaup.hf.space/callback" |
|
|
| CACHE_FILE = "upstox_master_cache.json" |
|
|
| app = FastAPI() |
|
|
| |
| |
| |
|
|
| MASTER_DB = {} |
| SEARCH_INDEX = [] |
|
|
| SYSTEM_STATUS = { |
| "ready": False, |
| "message": "Booting..." |
| } |
| UPDATE_LOCK = threading.Lock() |
|
|
| |
| INDEX_MAP = { |
| "NIFTY": "NSE_INDEX|Nifty 50", |
| "BANKNIFTY": "NSE_INDEX|Nifty Bank", |
| "FINNIFTY": "NSE_INDEX|Nifty Fin Service", |
| "MIDCPNIFTY": "NSE_INDEX|NIFTY MID SELECT", |
| "SENSEX": "BSE_INDEX|SENSEX", |
| "BANKEX": "BSE_INDEX|BANKEX", |
| "SENSEX50": "BSE_INDEX|SENSEX50" |
| } |
|
|
| def update_master_db(): |
| global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX |
| |
| if not UPDATE_LOCK.acquire(blocking=False): |
| return |
|
|
| try: |
| SYSTEM_STATUS["message"] = "Downloading Upstox Master Data..." |
| print(">>> [BG TASK] STARTING DOWNLOAD (NSE, BSE, MCX)...") |
| |
| |
| url_nse = "https://assets.upstox.com/market-quote/instruments/exchange/NSE.json.gz" |
| df_nse = pd.read_json(url_nse, compression='gzip') |
| |
| |
| url_bse = "https://assets.upstox.com/market-quote/instruments/exchange/BSE.json.gz" |
| df_bse = pd.read_json(url_bse, compression='gzip') |
|
|
| |
| url_mcx = "https://assets.upstox.com/market-quote/instruments/exchange/MCX.json.gz" |
| df_mcx = pd.read_json(url_mcx, compression='gzip') |
|
|
| SYSTEM_STATUS["message"] = "Processing Data..." |
| |
| |
| df = pd.concat([df_nse, df_bse, df_mcx], ignore_index=True) |
| |
| |
| mask = df['segment'].isin(['NSE_FO', 'BSE_FO', 'MCX_FO', 'NSE_INDEX', 'BSE_INDEX', 'NSE_EQ']) |
| df = df[mask] |
| |
| temp_db = {} |
| search_list = [] |
| |
| |
| |
| |
| |
| spot_lookup = {} |
|
|
| for _, row in df.iterrows(): |
| seg = row['segment'] |
| |
| |
| if seg == 'NSE_EQ': |
| sym = str(row['trading_symbol']).strip().upper() |
| key = row['instrument_key'] |
| spot_lookup[sym] = key |
| |
| |
| elif seg == 'NSE_INDEX' or seg == 'BSE_INDEX': |
| sym = str(row['trading_symbol']).strip().upper() |
| key = row['instrument_key'] |
| spot_lookup[sym] = key |
| |
| |
| name = str(row['name']).strip().upper() |
| spot_lookup[name] = key |
|
|
| |
| |
| |
| for _, row in df.iterrows(): |
| seg = row['segment'] |
| |
| |
| if seg not in ['NSE_FO', 'BSE_FO', 'MCX_FO']: |
| continue |
| |
| sym = row['trading_symbol'] |
| key = row['instrument_key'] |
| |
| |
| |
| |
| |
| root = "" |
| |
| |
| us = row.get('underlying_symbol') |
| if pd.notna(us) and str(us).strip() != "": |
| root = str(us).strip().upper() |
| |
| |
| if not root: |
| asym = row.get('asset_symbol') |
| if pd.notna(asym) and str(asym).strip() != "": |
| root = str(asym).strip().upper() |
| |
| |
| if not root: |
| nm = row.get('name') |
| if pd.notna(nm): |
| root = str(nm).strip().upper() |
|
|
| |
| clean_root = root.replace(' ', '') |
|
|
| |
| if clean_root not in temp_db: |
| |
| spot_key = "" |
| |
| |
| if clean_root in INDEX_MAP: |
| spot_key = INDEX_MAP[clean_root] |
| |
| |
| elif root in spot_lookup: |
| spot_key = spot_lookup[root] |
| |
| |
| elif clean_root in spot_lookup: |
| spot_key = spot_lookup[clean_root] |
| |
| |
| exch_code = seg.split('_')[0] |
| |
| |
| |
| |
| display_name = row['name'] if pd.notna(row['name']) else root |
|
|
| temp_db[clean_root] = { |
| "spot": spot_key, |
| "exch": exch_code, |
| "name": display_name, |
| "items": [] |
| } |
|
|
| inst_type = row.get('instrument_type', '') |
| |
| |
| exp = 0 |
| raw_exp = row.get('expiry') |
| if pd.notna(raw_exp): |
| try: |
| if isinstance(raw_exp, (int, float)): |
| exp = int(raw_exp / 1000) |
| elif isinstance(raw_exp, str): |
| dt = datetime.strptime(raw_exp, "%Y-%m-%d") |
| exp = int(dt.replace(tzinfo=None).timestamp()) |
| except: pass |
|
|
| strike = float(row.get('strike_price', 0.0)) |
| |
| |
| t_mapped = "FUT" |
| if inst_type == "CE": t_mapped = "CE" |
| elif inst_type == "PE": t_mapped = "PE" |
| elif inst_type in ["FUTIDX", "FUTSTK", "FUTCOM", "FUTBLN", "FUTENR"]: t_mapped = "FUT" |
| |
| temp_db[clean_root]["items"].append({ |
| "s": key, |
| "dis": sym, |
| "e": exp, |
| "k": strike, |
| "t": t_mapped |
| }) |
|
|
| |
| |
| |
| final_db = {} |
| for root, data in temp_db.items(): |
| if not data["items"]: continue |
| |
| sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k'])) |
| |
| final_db[root] = { |
| "spot": data["spot"], |
| "exch": data["exch"], |
| "items": sorted_items |
| } |
| |
| |
| |
| |
| search_list.append({ |
| "root": root, |
| "name": data["name"], |
| "exch": data["exch"], |
| "display": f"{data['name']} ({root})" |
| }) |
|
|
| MASTER_DB = final_db |
| SEARCH_INDEX = sorted(search_list, key=lambda x: x['root']) |
| |
| with open(CACHE_FILE, "w") as f: |
| json.dump({"db": final_db, "idx": SEARCH_INDEX}, f) |
|
|
| SYSTEM_STATUS["ready"] = True |
| SYSTEM_STATUS["message"] = "Ready" |
| print(f">>> [BG TASK] COMPLETE. Loaded {len(SEARCH_INDEX)} roots. Fixed Underlying Symbol Logic.") |
|
|
| except Exception as e: |
| print(f">>> [BG TASK] ERROR: {e}") |
| SYSTEM_STATUS["message"] = f"Error: {str(e)}" |
| finally: |
| UPDATE_LOCK.release() |
|
|
| @app.on_event("startup") |
| def startup_event(): |
| global MASTER_DB, SEARCH_INDEX |
| if os.path.exists(CACHE_FILE): |
| try: |
| print(">>> STARTUP: Loading Cache...") |
| with open(CACHE_FILE, "r") as f: |
| data = json.load(f) |
| MASTER_DB = data["db"] |
| SEARCH_INDEX = data["idx"] |
| SYSTEM_STATUS["ready"] = True |
| SYSTEM_STATUS["message"] = "Ready (Cached)" |
| except: pass |
| |
| if not SYSTEM_STATUS["ready"]: |
| thread = threading.Thread(target=update_master_db) |
| thread.start() |
|
|
| |
| |
| |
|
|
| @app.get("/") |
| def home(access_token: Optional[str] = Cookie(None)): |
| if not access_token: |
| auth_url = ( |
| f"https://api.upstox.com/v2/login/authorization/dialog?" |
| f"response_type=code&client_id={API_KEY}&redirect_uri={REDIRECT_URI}" |
| ) |
| return HTMLResponse(f""" |
| <div style="height:100vh; display:flex; flex-direction:column; justify-content:center; align-items:center; font-family:sans-serif; background:#f5f5f5;"> |
| <div style="background:white; padding:40px; border-radius:10px; box-shadow:0 10px 30px rgba(0,0,0,0.1); text-align:center;"> |
| <h2 style="margin-top:0;">Authentication Required</h2> |
| <a href="{auth_url}" style="display:inline-block; background:#2962ff; color:white; padding:12px 25px; text-decoration:none; border-radius:5px; font-weight:bold; margin-top:10px;">LOGIN WITH UPSTOX</a> |
| </div> |
| </div> |
| """) |
| |
| return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token)) |
|
|
| @app.get("/callback") |
| def callback(code: Optional[str] = None): |
| if not code: return JSONResponse({"error": "No Code Provided"}) |
|
|
| url = "https://api.upstox.com/v2/login/authorization/token" |
| headers = { 'accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' } |
| data = { |
| 'code': code, |
| 'client_id': API_KEY, |
| 'client_secret': API_SECRET, |
| 'redirect_uri': REDIRECT_URI, |
| 'grant_type': 'authorization_code' |
| } |
| |
| resp = requests.post(url, headers=headers, data=data) |
| json_resp = resp.json() |
| |
| if "access_token" in json_resp: |
| response = RedirectResponse(url="/") |
| response.set_cookie(key="access_token", value=json_resp['access_token'], httponly=True) |
| return response |
| else: |
| return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(json_resp, indent=2)}</pre>") |
|
|
| @app.get("/logout") |
| def logout(): |
| response = RedirectResponse(url="/") |
| response.delete_cookie("access_token") |
| return response |
|
|
| @app.get("/api/status") |
| def get_status(): |
| return JSONResponse(SYSTEM_STATUS) |
|
|
| @app.get("/api/search") |
| def search_symbol(q: str = Query(..., min_length=1)): |
| if not SYSTEM_STATUS["ready"]: return JSONResponse([]) |
| query = q.upper() |
| |
| results = [] |
| |
| results += [x for x in SEARCH_INDEX if x['root'].startswith(query)] |
| |
| |
| if len(results) < 20: |
| results += [x for x in SEARCH_INDEX if query in x['name'].upper() and x not in results][:10] |
| |
| return JSONResponse(results[:30]) |
|
|
| @app.get("/api/chain") |
| def get_chain(symbol: str): |
| root = symbol.upper() |
| if root in MASTER_DB: |
| return JSONResponse(MASTER_DB[root]) |
| return JSONResponse({"error": "Symbol not found"}, status_code=404) |
|
|
| @app.get("/api/refresh") |
| def force_refresh(background_tasks: BackgroundTasks): |
| SYSTEM_STATUS["ready"] = False |
| SYSTEM_STATUS["message"] = "Refreshing..." |
| background_tasks.add_task(update_master_db) |
| return JSONResponse({"s": "ok"}) |
|
|
|
|
| |
| |
| |
|
|
| HTML_TEMPLATE = """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <title>PRO CHAIN (UPSTOX)</title> |
| <meta name="viewport" content="width=1100"> |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0; |
| --text-main: #1a1a1a; --text-sub: #555555; |
| --accent: #6c4af5; --green: #00c853; --red: #d50000; |
| --green-soft: rgba(0, 200, 83, 0.27); --red-soft: rgba(213, 0, 0, 0.18); |
| --atm-bg: rgba(108, 74, 245, 0.08); |
| --badge-nse: #00c853; --badge-bse: #ff6d00; --badge-mcx: #2962ff; |
| } |
| * { box-sizing: border-box; } |
| body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } |
| |
| .top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.03); } |
| .btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; color: var(--text-sub); } |
| .btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; } |
| .btn.active { background: var(--accent); color: white; border-color: var(--accent); } |
| |
| .icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); transition:0.2s; } |
| .icon-btn:hover { background: #f0f0f0; color: var(--text-main); } |
| |
| .search-box { position: relative; width: 350px; margin-right: 20px; } |
| .search-input { width: 100%; padding: 10px 15px; border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; transition: 0.3s; background: #f9f9f9; } |
| .search-input:focus { border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px rgba(108, 74, 245, 0.1); } |
| .search-dropdown { position: absolute; top: 105%; left: 0; width: 100%; background: white; border: 1px solid var(--border); max-height: 400px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 6px; z-index: 100; } |
| |
| .search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f5f5f5; font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; } |
| .search-item:hover { background: #f0f7ff; color: var(--accent); } |
| |
| .ref-wrap { position: relative; cursor: pointer; user-select: none; } |
| .ref-dropdown { position: absolute; top: 100%; right: 0; width: 180px; background: white; border: 1px solid var(--border); border-radius: 6px; box-shadow: 0 10px 20px rgba(0,0,0,0.1); display: none; z-index: 100; } |
| .ref-item { padding: 10px; font-size: 11px; font-weight: 700; border-bottom: 1px solid #f5f5f5; transition: 0.2s; } |
| .ref-item:hover { background: #f0f7ff; color: var(--accent); } |
| .ref-item.active { background: var(--accent); color: white; } |
| |
| .exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; } |
| .badge-NSE { background: var(--badge-nse); } |
| .badge-BSE { background: var(--badge-bse); } |
| .badge-MCX { background: var(--badge-mcx); } |
| |
| .grid-layout { display: flex; height: calc(100vh - 60px); } |
| |
| /* Chain Panel */ |
| .chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; } |
| |
| .chain-header { display: grid; grid-template-columns: 1fr 1fr; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; } |
| .header-cell { padding: 12px 5px; display: flex; align-items: center; justify-content: center; gap: 8px; position:relative; border-right: 1px solid var(--border); } |
| .header-icon { opacity: 0.3; cursor: pointer; transition: 0.2s; } |
| .header-icon:hover { opacity: 1; color: var(--accent); } |
| |
| .chain-body { flex: 1; overflow-y: auto; background: #fff; scroll-behavior: smooth; } |
| |
| .row { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid #f0f0f0; height: 42px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; } |
| .row:hover { background: #fafafa; } |
| .row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); } |
| |
| .graph-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px solid var(--border); cursor: crosshair; } |
| .visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.9; } |
| |
| .strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); } |
| .delta-txt { z-index: 2; font-size: 10px; font-weight: 700; background: rgba(255,255,255,0.85); padding: 0 5px; border-radius: 3px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); margin-top: -2px; } |
| |
| /* Futures / Sidebar Panel */ |
| .fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; } |
| |
| .panel-section { padding: 15px; border-bottom: 1px solid var(--border); background: #fff; margin-bottom: 10px; } |
| .panel-title { font-size: 11px; font-weight: 800; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display:flex; justify-content:space-between; align-items:center; } |
| |
| .fut-card { padding: 12px; background: #fff; border: 1px solid #eee; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); } |
| .meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 8px; } |
| |
| .netflow-card { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid var(--border); text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.03); } |
| |
| .nf-sec { margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; } |
| .nf-sec:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; } |
| .nf-title { font-size: 10px; font-weight: 800; color: #888; text-transform: uppercase; margin-bottom: 8px; text-align: left; } |
| |
| .aggr-row { display: flex; width: 100%; border: 1px solid transparent; } |
| .aggr-item { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; } |
| |
| .aggr-val { font-size: 20px; font-family: 'JetBrains Mono'; font-weight: 800; line-height: 1; margin-bottom: 2px; } |
| .aggr-lbl { font-size: 9px; font-weight: 700; color: #aaa; background: transparent; padding: 0; border-radius: 0; margin-top: 2px; display:flex; align-items:center; gap:3px; } |
| |
| .info-bubble { width:12px; height:12px; background:#eee; color:#666; border-radius:50%; font-size:8px; display:inline-flex; align-items:center; justify-content:center; cursor:pointer; font-family:'Inter'; font-weight:800; } |
| .info-bubble:hover { background:var(--accent); color:white; } |
| |
| /* Modals */ |
| .modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); } |
| .modal-box { background: #fff; width: 420px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); } |
| @keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } } |
| |
| .modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; } |
| .opt-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 6px; cursor: pointer; font-weight:600; font-size:12px; transition:0.2s; background:#f9f9f9; } |
| .opt-btn:hover { background: #fff; border-color: var(--accent); box-shadow: 0 2px 5px rgba(0,0,0,0.05); } |
| .opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); } |
| |
| .inp-group { margin-top:15px; } |
| .inp-label { font-size:11px; font-weight:800; color:#888; margin-bottom:5px; display:block; } |
| .modal-inp { width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:'JetBrains Mono'; font-weight:bold; font-size:14px; } |
| .modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; margin-top:20px; } |
| .modal-action.sec { background:#eee; color:#333; margin-top:10px; } |
| |
| .info-table { width:100%; font-family:'JetBrains Mono'; font-size:10px; border-collapse: collapse; margin-top:10px; } |
| .info-table th { text-align:left; color:#888; border-bottom:1px solid #eee; padding:5px; } |
| .info-table td { padding:5px; border-bottom:1px solid #f9f9f9; color:#333; } |
| .info-row-res { background: #f0f7ff; font-weight:800; } |
| .info-row-atm { border-top: 2px solid var(--accent); background: rgba(108, 74, 245, 0.05); } |
| |
| .conf-list { max-height: 250px; overflow-y: auto; margin-bottom: 15px; border: 1px solid #eee; border-radius: 6px; background: #fafafa; } |
| .conf-item { background: #fff; border-bottom: 1px solid #eee; padding: 10px; } |
| .conf-item:last-child { border-bottom: none; } |
| .conf-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; } |
| .conf-body { margin-top: 10px; display: none; padding-top: 10px; border-top: 1px solid #f5f5f5; } |
| .conf-item.expanded .conf-body { display: block; } |
| .conf-item.expanded .conf-header { color: var(--accent); } |
| .del-btn { color: var(--red); cursor: pointer; font-size: 10px; font-weight: bold; border: 1px solid var(--red-soft); padding: 2px 6px; border-radius: 4px; } |
| .add-btn { width: 100%; border: 1px dashed #ccc; padding: 10px; text-align: center; font-size: 11px; font-weight: bold; color: #888; cursor: pointer; border-radius: 6px; margin-bottom: 15px; transition: 0.2s; background: #fff; } |
| .add-btn:hover { border-color: var(--accent); color: var(--accent); background: #f0f7ff; } |
| |
| .loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; } |
| .spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; } |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
| |
| @keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } } |
| .live-dot { width:8px; height:8px; background:#ccc; border-radius:50%; transition:0.3s; } |
| .live-dot.active { background: var(--green); animation: blink 1.5s infinite; box-shadow: 0 0 5px var(--green); } |
| .live-dot.error { background: var(--red); } |
| |
| ::-webkit-scrollbar { width: 6px; height: 6px; } |
| ::-webkit-scrollbar-track { background: transparent; } |
| ::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; } |
| ::-webkit-scrollbar-thumb:hover { background: #bbb; } |
| |
| .code-box { background:#f4f4f4; padding:10px; border-radius:6px; font-family:'JetBrains Mono'; font-size:11px; color:#555; margin-top:5px; display:none; } |
| .var-tag { color:var(--accent); font-weight:bold; } |
| </style> |
| </head> |
| <body> |
| |
| <!-- LOADER --> |
| <div class="loader-ov" id="loader"> |
| <div class="spinner"></div> |
| <div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px; letter-spacing:1px;">CONNECTING...</div> |
| </div> |
| |
| <!-- EXPIRY MODAL --> |
| <div class="modal" id="expModal"> |
| <div class="modal-box"> |
| <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;"> |
| <span>SELECT EXPIRY</span> |
| <span style="cursor:pointer; color:#999;" onclick="closeModal('expModal')">✕</span> |
| </div> |
| <div class="modal-grid" id="expGrid"></div> |
| </div> |
| </div> |
| |
| <!-- INFO BREAKDOWN MODAL --> |
| <div class="modal" id="infoModal"> |
| <div class="modal-box" style="width: 500px; max-width:95%;"> |
| <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;"> |
| <span>CALCULATION BREAKDOWN</span> |
| <span style="cursor:pointer; color:#999;" onclick="closeModal('infoModal')">✕</span> |
| </div> |
| <div style="margin-top:15px; font-size:12px;"> |
| <div><strong>LIVE SPOT:</strong> <span id="infoSpot" style="font-family:'JetBrains Mono'; color:var(--accent);">0.00</span></div> |
| <div style="margin-top:5px;"><strong>FORMULA:</strong> <span id="infoForm" style="font-family:'JetBrains Mono'; background:#f4f4f4; padding:2px 6px; border-radius:4px;"></span></div> |
| </div> |
| <div style="overflow-y:auto; max-height:400px; margin-top:10px;"> |
| <table class="info-table" id="infoTable"> |
| <!-- Dynamic Content --> |
| </table> |
| </div> |
| <div style="margin-top:10px; font-size:10px; color:#888; text-align:center;">* Uses Raw Integer Values for Accuracy</div> |
| </div> |
| </div> |
| |
| <!-- CONFIG MODAL --> |
| <div class="modal" id="confModal"> |
| <div class="modal-box"> |
| <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;"> |
| <span id="confTitle">COLUMN SETTINGS</span> |
| <span style="cursor:pointer; color:#999;" onclick="closeModal('confModal')">✕</span> |
| </div> |
| <div class="inp-group"> |
| <span class="inp-label">DISPLAY TITLE</span> |
| <input type="text" id="confName" class="modal-inp" placeholder="e.g. NET FLOW"> |
| </div> |
| <div class="inp-group"> |
| <span class="inp-label">CUSTOM FORMULA</span> |
| <input type="text" id="confForm" class="modal-inp" placeholder="e.g. PS - CS"> |
| </div> |
| <button class="modal-action" onclick="saveConfig()">SAVE CONFIGURATION</button> |
| <button class="modal-action sec" onclick="resetConfig()">RESET TO DEFAULTS</button> |
| </div> |
| </div> |
| |
| <!-- SIDEBAR AGGREGATE SETTINGS MODAL --> |
| <div class="modal" id="sideConfModal"> |
| <div class="modal-box"> |
| <div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;"> |
| <span>SIDEBAR CONFIG</span> |
| <span style="cursor:pointer; color:#999;" onclick="closeModal('sideConfModal')">✕</span> |
| </div> |
| <div style="margin-top:10px; font-size:11px; font-weight:700; color:#888; margin-bottom:10px; display:flex; justify-content:space-between; align-items:center;"> |
| <span>CONFIGURE CARDS & RANGES</span> |
| <span onclick="toggleVarHelp()" style="color:var(--accent); cursor:pointer; font-weight:800;">[?] VARIABLES</span> |
| </div> |
| <div class="code-box" id="sideVarHelp"> |
| <div>VARIABLES AVAILABLE:</div> |
| <div style="margin-top:4px;"> |
| <span class="var-tag">CS</span> = Call Sell <span class="var-tag">CB</span> = Call Buy <span class="var-tag">CO</span> = Call OI <span class="var-tag">CLTP</span> = Call LTP <span class="var-tag">CV</span> = Call Vol<br> |
| <span class="var-tag">PS</span> = Put Sell <span class="var-tag">PB</span> = Put Buy <span class="var-tag">PO</span> = Put OI <span class="var-tag">PLTP</span> = Put LTP <span class="var-tag">PV</span> = Put Vol |
| </div> |
| <div style="margin-top:4px; font-size:10px; color:#888;">Supports +, -, *, /, ( ) arithmetic.</div> |
| </div> |
| <div class="conf-list" id="sideListContainer"></div> |
| <div class="add-btn" onclick="addSideItem()">+ ADD NEW AGGREGATE CARD</div> |
| <button class="modal-action" onclick="saveSideConfig()">SAVE SETTINGS</button> |
| <button class="modal-action sec" onclick="resetSideConfig()">RESET DEFAULTS</button> |
| </div> |
| </div> |
| |
| <!-- TOP NAV --> |
| <div class="top-nav"> |
| <div style="display:flex; align-items:center;"> |
| <div class="search-box"> |
| <input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. SBIN, CRUDE)..." autocomplete="off"> |
| <div class="search-dropdown" id="searchRes"></div> |
| </div> |
| </div> |
| |
| <div style="display:flex; gap:20px; align-items:center;"> |
| <button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB"> |
| <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg> |
| </button> |
| <div class="ref-wrap" onclick="toggleRefMenu()"> |
| <div style="text-align:right;"> |
| <div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px; display:flex; align-items:center; justify-content:flex-end; gap:4px;"> |
| <span id="spotLabel">SPOT</span> <span>▼</span> |
| </div> |
| <div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div> |
| </div> |
| <div class="ref-dropdown" id="refMenu"></div> |
| </div> |
| <button class="btn" onclick="document.getElementById('expModal').style.display='flex'"> |
| <span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span> |
| <svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg> |
| </button> |
| <div class="live-dot" id="statusDot" title="Connection Status"></div> |
| <button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;"> |
| <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg> |
| </button> |
| </div> |
| </div> |
| |
| <div class="grid-layout"> |
| <div class="chain-panel"> |
| <div class="chain-header"> |
| <div class="header-cell"> |
| <span id="headL_txt">NET FLOW</span> |
| <svg class="header-icon" onclick="openConfig('left')" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> |
| </div> |
| <div class="header-cell"> |
| <span id="headR_txt">OVERALL FLOW</span> |
| <svg class="header-icon" onclick="openConfig('right')" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> |
| </div> |
| </div> |
| <div class="chain-body" id="chainBody"> |
| <div style="padding:40px; text-align:center; color:#999; font-weight:600; font-size:14px;">USE SEARCH BAR TO LOAD DATA</div> |
| </div> |
| </div> |
| |
| <div class="fut-panel"> |
| <div class="panel-section"> |
| <div class="panel-title"> |
| <span>AGGREGATE FLOW</span> |
| <div style="display:flex; align-items:center; gap:5px;"> |
| <svg onclick="openSideConfig()" style="cursor:pointer; opacity:0.5;" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> |
| </div> |
| </div> |
| <div class="netflow-card" id="sideContainer"> |
| <div style="padding:20px; color:#ccc; font-size:10px;">LOADING CONFIG...</div> |
| </div> |
| </div> |
| <div class="panel-section"> |
| <div class="panel-title">FUTURES CONTRACTS</div> |
| <div id="futBody"></div> |
| </div> |
| </div> |
| </div> |
| |
| <script> |
| const UPSTOX_TOKEN = "{{USER_TOKEN}}"; |
| |
| // Core State |
| let CURRENT_ROOT = ""; |
| let CHAIN_DATA = null; |
| let ACTIVE_EXP = 0; |
| let SPOT_SYM = ""; |
| let SELECTED_REF_SYM = ""; // The actual symbol key used for ATM/Calcs |
| let SPOT_PRICE = 0; |
| let FUT_SYMS = []; |
| let OPT_SYMS = []; |
| let POLLER = null; |
| let IS_POLLING = false; |
| let SEARCH_TIMER = null; |
| let ATM_STRIKE = 0; |
| let LATEST_ROW_DATA = {}; |
| |
| // Default Configurations |
| const DEFAULTS = { |
| left: { name: "NET FLOW", formula: "PS - CS" }, |
| right: { name: "OVERALL FLOW", formula: "CB + PS - (PB + CS)" }, |
| side: [ |
| { id: 1, name: "NET FLOW", formula: "PS - CS", ranges: [7,10], _expanded: true }, |
| { id: 2, name: "OVERALL", formula: "CB + PS - (PB + CS)", ranges: [7,10], _expanded: false }, |
| { id: 3, name: "NET OI", formula: "PO - CO", ranges: [10], _expanded: false } |
| ] |
| }; |
| |
| // Load Config from LocalStorage |
| let CONFIG = JSON.parse(localStorage.getItem('tst_chain_config')) || JSON.parse(JSON.stringify(DEFAULTS)); |
| if(!Array.isArray(CONFIG.side)) CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side)); |
| |
| let EDITING_COL = null; |
| |
| window.onload = () => { |
| applyConfig(); |
| checkStatus(); |
| const inp = document.getElementById('searchInp'); |
| inp.addEventListener('input', (e) => { |
| clearTimeout(SEARCH_TIMER); |
| SEARCH_TIMER = setTimeout(() => doSearch(e.target.value), 300); |
| }); |
| document.addEventListener('click', (e) => { |
| if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none'; |
| }); |
| }; |
| |
| // =================================== |
| // MATH ENGINE |
| // =================================== |
| function evaluateFormula(formula, vars) { |
| try { |
| let exp = formula; |
| exp = exp.replace(/CS/g, vars.CS || 0); |
| exp = exp.replace(/CB/g, vars.CB || 0); |
| exp = exp.replace(/PS/g, vars.PS || 0); |
| exp = exp.replace(/PB/g, vars.PB || 0); |
| |
| exp = exp.replace(/CO/g, vars.CO || 0); |
| exp = exp.replace(/PO/g, vars.PO || 0); |
| exp = exp.replace(/CLTP/g, vars.CLTP || 0); |
| exp = exp.replace(/PLTP/g, vars.PLTP || 0); |
| exp = exp.replace(/CV/g, vars.CV || 0); |
| exp = exp.replace(/PV/g, vars.PV || 0); |
| |
| if(/[^0-9+\-*/(). ]/.test(exp)) return 0; |
| return new Function('return ' + exp)(); |
| } catch(e) { return 0; } |
| } |
| |
| // =================================== |
| // CONFIG LOGIC |
| // =================================== |
| function applyConfig() { |
| document.getElementById('headL_txt').innerText = CONFIG.left.name; |
| document.getElementById('headR_txt').innerText = CONFIG.right.name; |
| renderSidebar(); |
| } |
| |
| function renderSidebar() { |
| const con = document.getElementById('sideContainer'); |
| if(!CONFIG.side || CONFIG.side.length === 0) { |
| con.innerHTML = '<div style="padding:10px; color:#aaa; font-size:10px;">NO METRICS CONFIGURED</div>'; |
| return; |
| } |
| |
| con.innerHTML = CONFIG.side.map((item, idx) => { |
| const boxes = item.ranges.map((r, rIdx) => ` |
| <div class="aggr-item"> |
| <span class="aggr-val" id="sv-${idx}-${rIdx}">0</span> |
| <span class="aggr-lbl"> |
| ATM ±${r} |
| <span class="info-bubble" onclick="showInfo(${idx}, ${rIdx})">i</span> |
| </span> |
| </div> |
| `).join(''); |
| |
| return ` |
| <div class="nf-sec"> |
| <div class="nf-title">${item.name}</div> |
| <div class="aggr-row"> |
| ${boxes} |
| </div> |
| </div>`; |
| }).join(''); |
| } |
| |
| function showInfo(cardIdx, rangeIdx) { |
| if(!CONFIG.side[cardIdx]) return; |
| const cfg = CONFIG.side[cardIdx]; |
| const range = cfg.ranges[rangeIdx]; |
| const formula = cfg.formula; |
| |
| document.getElementById('infoSpot').innerText = SPOT_PRICE.toFixed(2); |
| document.getElementById('infoForm').innerText = formula; |
| |
| const kList = Object.keys(LATEST_ROW_DATA).map(parseFloat).sort((a,b)=>a-b); |
| const lowers = kList.filter(k => k < SPOT_PRICE); |
| const uppers = kList.filter(k => k >= SPOT_PRICE); |
| |
| const selLow = lowers.slice(-range); |
| const selHigh = uppers.slice(0, range); |
| const allSel = [...selLow, ...selHigh].sort((a,b)=>a-b); |
| let grandTotal = 0; |
| |
| const table = document.getElementById('infoTable'); |
| |
| let hHTML = `<tr><th style="width:50px;">STRIKE</th><th>CS</th><th>CB</th><th>PS</th><th>PB</th><th>CALC</th></tr>`; |
| |
| let rowsHTML = allSel.map((k, idx) => { |
| const d = LATEST_ROW_DATA[k] || {}; |
| const val = evaluateFormula(formula, d); |
| grandTotal += val; |
| |
| // ATM Line Logic |
| let rowClass = ""; |
| if (k >= SPOT_PRICE) { |
| const prev = allSel[idx-1]; |
| if (prev === undefined || prev < SPOT_PRICE) { |
| rowClass = "info-row-atm"; |
| } |
| } |
| |
| return `<tr class="${rowClass}"> |
| <td style="font-weight:bold;">${k}</td> |
| <td>${d.CS||0}</td><td>${d.CB||0}</td><td>${d.PS||0}</td><td>${d.PB||0}</td> |
| <td style="font-weight:bold; color:${val>=0?'var(--green)':'var(--red)'}">${val}</td> |
| </tr>`; |
| }).join(''); |
| |
| rowsHTML += `<tr class="info-row-res"><td colspan="5" style="text-align:right;">TOTAL</td><td style="color:${grandTotal>=0?'var(--green)':'var(--red)'}">${grandTotal}</td></tr>`; |
| table.innerHTML = hHTML + rowsHTML; |
| document.getElementById('infoModal').style.display = 'flex'; |
| } |
| |
| function openConfig(col) { |
| EDITING_COL = col; |
| const cfg = CONFIG[col]; |
| document.getElementById('confTitle').innerText = (col === 'left' ? "LEFT COLUMN" : "RIGHT COLUMN") + " SETTINGS"; |
| document.getElementById('confName').value = cfg.name; |
| document.getElementById('confForm').value = cfg.formula; |
| document.getElementById('confModal').style.display = 'flex'; |
| } |
| |
| function saveConfig() { |
| const name = document.getElementById('confName').value; |
| const form = document.getElementById('confForm').value.toUpperCase(); |
| if(!name || !form) { alert("Fields cannot be empty"); return; } |
| CONFIG[EDITING_COL] = { name: name, formula: form }; |
| saveAndReload(); |
| closeModal('confModal'); |
| } |
| |
| function resetConfig() { |
| if(confirm("Reset Main Columns to Default?")) { |
| CONFIG.left = DEFAULTS.left; |
| CONFIG.right = DEFAULTS.right; |
| saveAndReload(); |
| closeModal('confModal'); |
| } |
| } |
| |
| // --- SIDEBAR DYNAMIC CONFIG --- |
| let TEMP_SIDE_CONFIG = []; |
| |
| function openSideConfig() { |
| TEMP_SIDE_CONFIG = JSON.parse(JSON.stringify(CONFIG.side)); |
| renderSideConfigList(); |
| document.getElementById('sideConfModal').style.display = 'flex'; |
| } |
| |
| function renderSideConfigList() { |
| const list = document.getElementById('sideListContainer'); |
| list.innerHTML = TEMP_SIDE_CONFIG.map((item, idx) => ` |
| <div class="conf-item ${item._expanded ? 'expanded' : ''}"> |
| <div class="conf-header" onclick="toggleSideItem(${idx})"> |
| <span style="font-weight:700; font-size:12px;">${idx+1}. ${item.name}</span> |
| <div style="display:flex; align-items:center; gap:10px;"> |
| <span style="font-size:10px; color:#aaa;">${item.ranges.length} RANGES</span> |
| <span class="del-btn" onclick="removeSideItem(event, ${idx})">DEL</span> |
| </div> |
| </div> |
| <div class="conf-body" onclick="event.stopPropagation()"> |
| <div class="inp-group" style="margin-top:0;"> |
| <span class="inp-label">TITLE</span> |
| <input type="text" class="modal-inp" value="${item.name}" onchange="updateSideItem(${idx}, 'name', this.value)"> |
| </div> |
| <div class="inp-group"> |
| <span class="inp-label">FORMULA</span> |
| <input type="text" class="modal-inp" value="${item.formula}" onchange="updateSideItem(${idx}, 'formula', this.value)"> |
| </div> |
| <div class="inp-group"> |
| <span class="inp-label">RANGES (Comma separated, max 3 recommended)</span> |
| <input type="text" class="modal-inp" value="${item.ranges.join(', ')}" onchange="updateSideItem(${idx}, 'ranges', this.value)"> |
| </div> |
| </div> |
| </div> |
| `).join(''); |
| } |
| |
| function toggleSideItem(idx) { TEMP_SIDE_CONFIG[idx]._expanded = !TEMP_SIDE_CONFIG[idx]._expanded; renderSideConfigList(); } |
| function toggleVarHelp() { const box = document.getElementById('sideVarHelp'); box.style.display = (box.style.display === 'none' || box.style.display === '') ? 'block' : 'none'; } |
| function addSideItem() { TEMP_SIDE_CONFIG.push({ id: Date.now(), name: "NEW METRIC", formula: "PS - CS", ranges: [10], _expanded: true }); renderSideConfigList(); } |
| function removeSideItem(e, idx) { e.stopPropagation(); if(confirm("Delete this card?")) { TEMP_SIDE_CONFIG.splice(idx, 1); renderSideConfigList(); } } |
| function updateSideItem(idx, field, val) { |
| if(field === 'ranges') { |
| const nums = val.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0 && n <= 50); |
| TEMP_SIDE_CONFIG[idx][field] = nums.length > 0 ? nums : [10]; |
| } else if (field === 'formula') { TEMP_SIDE_CONFIG[idx][field] = val.toUpperCase(); |
| } else { TEMP_SIDE_CONFIG[idx][field] = val; } |
| } |
| function saveSideConfig() { |
| const clean = TEMP_SIDE_CONFIG.map(x => { const { _expanded, ...rest } = x; return rest; }); |
| CONFIG.side = clean; |
| saveAndReload(); |
| closeModal('sideConfModal'); |
| } |
| function resetSideConfig() { if(confirm("Reset Sidebar to defaults?")) { CONFIG.side = JSON.parse(JSON.stringify(DEFAULTS.side)); saveAndReload(); closeModal('sideConfModal'); } } |
| function saveAndReload() { localStorage.setItem('tst_chain_config', JSON.stringify(CONFIG)); applyConfig(); pollData(); } |
| function closeModal(id) { document.getElementById(id).style.display = 'none'; } |
| |
| // =================================== |
| // UPSTOX DATA LOGIC |
| // =================================== |
| async function checkStatus() { |
| try { |
| const res = await fetch('/api/status'); |
| const status = await res.json(); |
| if(status.ready) { |
| document.getElementById('loader').style.display = 'none'; |
| if(!CURRENT_ROOT) loadSymbol('NIFTY'); |
| } else { |
| document.getElementById('loaderMsg').innerText = status.message.toUpperCase(); |
| setTimeout(checkStatus, 2000); |
| } |
| } catch(e) { setTimeout(checkStatus, 2000); } |
| } |
| |
| async function refreshDB() { |
| if(!confirm("Download fresh Master Data? (Takes ~15s)")) return; |
| document.getElementById('loaderMsg').innerText = "DOWNLOADING..."; |
| document.getElementById('loader').style.display = 'flex'; |
| await fetch('/api/refresh'); |
| checkStatus(); |
| } |
| |
| async function doSearch(q) { |
| const resDiv = document.getElementById('searchRes'); |
| if(q.length < 2) { resDiv.style.display = 'none'; return; } |
| try { |
| const res = await fetch(`/api/search?q=${q}`); |
| const data = await res.json(); |
| |
| // Format: "Company Name (Symbol) [Exchange]" |
| resDiv.innerHTML = data.map(item => |
| `<div class="search-item" onclick="loadSymbol('${item.root}')"> |
| <span>${item.name} <small style="color:#888; font-weight:normal;">(${item.root})</small></span> |
| <span class="exch-badge badge-${item.exch}">${item.exch}</span> |
| </div>` |
| ).join(''); |
| resDiv.style.display = 'block'; |
| } catch(e) { console.error(e); } |
| } |
| |
| async function loadSymbol(root) { |
| document.getElementById('searchInp').value = root; |
| document.getElementById('searchRes').style.display = 'none'; |
| document.getElementById('loaderMsg').innerText = "LOADING CHAIN..."; |
| document.getElementById('loader').style.display = 'flex'; |
| |
| // --- FIX 1: CLEAR OLD DATA IMMEDIATELY --- |
| SPOT_PRICE = 0; |
| document.getElementById('spotPrice').innerText = "0.00"; |
| // ----------------------------------------- |
| |
| try { |
| const res = await fetch(`/api/chain?symbol=${root}`); |
| if(!res.ok) throw new Error("Symbol not found"); |
| const data = await res.json(); |
| |
| CURRENT_ROOT = root; |
| CHAIN_DATA = data.items; |
| SPOT_SYM = data.spot; |
| |
| // Find valid futures (closest expiry) |
| FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3); |
| |
| // --- FIX 2: ROBUST DEFAULT SELECTION LOGIC --- |
| // If Spot Symbol exists AND is not empty, use it. |
| // Otherwise, default to the first Future contract. |
| if (SPOT_SYM && SPOT_SYM !== "") { |
| SELECTED_REF_SYM = SPOT_SYM; |
| } else if (FUT_SYMS.length > 0) { |
| SELECTED_REF_SYM = FUT_SYMS[0].s; |
| } else { |
| SELECTED_REF_SYM = ""; // No valid reference |
| } |
| // --------------------------------------------- |
| |
| buildRefMenu(); |
| setupExpiries(); |
| document.getElementById('loader').style.display = 'none'; |
| |
| } catch(e) { |
| alert("Error: " + e.message); |
| document.getElementById('loader').style.display = 'none'; |
| } |
| } |
| |
| function buildRefMenu() { |
| const menu = document.getElementById('refMenu'); |
| let html = ''; |
| |
| // Option 1: Spot (Only if exists) |
| if(SPOT_SYM && SPOT_SYM !== "") { |
| const isSel = (SELECTED_REF_SYM === SPOT_SYM); |
| html += `<div class="ref-item ${isSel?'active':''}" onclick="selectRef('${SPOT_SYM}', 'SPOT PRICE')">SPOT PRICE</div>`; |
| if(isSel) document.getElementById('spotLabel').innerText = "SPOT PRICE"; |
| } |
| |
| // Option 2: Futures |
| FUT_SYMS.forEach(f => { |
| const label = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'}) + " FUT"; |
| const isSel = (SELECTED_REF_SYM === f.s); |
| html += `<div class="ref-item ${isSel?'active':''}" onclick="selectRef('${f.s}', '${label}')">${label}</div>`; |
| if(isSel) document.getElementById('spotLabel').innerText = label; |
| }); |
| |
| menu.innerHTML = html; |
| |
| // If we defaulted to a Future because Spot was missing, update the label immediately |
| if (SELECTED_REF_SYM !== "" && SELECTED_REF_SYM !== SPOT_SYM) { |
| const foundFut = FUT_SYMS.find(f => f.s === SELECTED_REF_SYM); |
| if(foundFut) { |
| const label = new Date(foundFut.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'}) + " FUT"; |
| document.getElementById('spotLabel').innerText = label; |
| } |
| } |
| } |
| |
| function toggleRefMenu() { |
| const m = document.getElementById('refMenu'); |
| m.style.display = (m.style.display === 'block') ? 'none' : 'block'; |
| } |
| |
| function selectRef(key, label) { |
| SELECTED_REF_SYM = key; |
| document.getElementById('spotLabel').innerText = label; |
| buildRefMenu(); |
| SPOT_PRICE = 0; |
| document.getElementById('spotPrice').innerText = "0.00"; // Clear visual |
| pollData(); |
| } |
| |
| function setupExpiries() { |
| const opts = CHAIN_DATA.filter(x => x.t === 'CE' || x.t === 'PE'); |
| const exps = [...new Set(opts.map(i => i.e))].sort((a,b) => a-b); |
| const today = Math.floor(Date.now()/1000) - 86400; |
| const validExps = exps.filter(ts => ts > today); |
| |
| if(validExps.length === 0) { |
| document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>'; |
| return; |
| } |
| |
| ACTIVE_EXP = validExps[0]; |
| updateExpText(ACTIVE_EXP); |
| |
| const grid = document.getElementById('expGrid'); |
| grid.innerHTML = validExps.map(ts => { |
| const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'}); |
| return `<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${dStr}</div>`; |
| }).join(''); |
| |
| buildChain(); |
| } |
| |
| function selectExp(ts) { |
| ACTIVE_EXP = ts; |
| updateExpText(ts); |
| closeModal('expModal'); |
| document.querySelectorAll('.opt-btn').forEach(b => { |
| if(b.innerText === new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'})) |
| b.classList.add('active'); |
| else b.classList.remove('active'); |
| }); |
| buildChain(); |
| } |
| |
| function updateExpText(ts) { |
| document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'}); |
| } |
| |
| function buildChain() { |
| const body = document.getElementById('chainBody'); |
| const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE')); |
| OPT_SYMS = relevant; |
| |
| const strikes = [...new Set(relevant.map(x => x.k))].sort((a,b) => a-b); |
| |
| body.innerHTML = strikes.map(k => ` |
| <div class="row" id="row-${k}" data-k="${k}"> |
| <div class="graph-col"> |
| <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;"> |
| <div class="visual-bar" id="barL-${k}"></div> |
| <span class="strike-txt">${k}</span> |
| <span class="delta-txt" id="valL-${k}">-</span> |
| </div> |
| </div> |
| <div class="graph-col"> |
| <div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;"> |
| <div class="visual-bar" id="barR-${k}"></div> |
| <span class="strike-txt">${k}</span> |
| <span class="delta-txt" id="valR-${k}">-</span> |
| </div> |
| </div> |
| </div> |
| `).join(''); |
| |
| window.scrolled = false; |
| restartPoller(); |
| } |
| |
| function restartPoller() { |
| if(POLLER) clearInterval(POLLER); |
| POLLER = setInterval(pollData, 1500); |
| pollData(); |
| } |
| |
| async function pollData() { |
| if(IS_POLLING || !CURRENT_ROOT) return; |
| IS_POLLING = true; |
| |
| try { |
| // Ensure SELECTED_REF_SYM is included in fetch list |
| let symbolsToFetch = []; |
| if(SELECTED_REF_SYM) symbolsToFetch.push(SELECTED_REF_SYM); |
| |
| symbolsToFetch = [...symbolsToFetch, ...FUT_SYMS.map(x => x.s)]; |
| |
| const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k)); |
| |
| if(kList.length > 0) { |
| // Use SPOT_PRICE if valid, else fallback to middle strike for initial centering |
| const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)]; |
| let selectedK = kList.find(k => k >= refPrice); |
| if (selectedK === undefined) selectedK = kList[kList.length - 1]; |
| |
| ATM_STRIKE = selectedK; |
| const atmIdx = kList.indexOf(selectedK); |
| |
| document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm')); |
| const atmRow = document.getElementById(`row-${selectedK}`); |
| if(atmRow) { |
| atmRow.classList.add('atm'); |
| if(!window.scrolled && SPOT_PRICE > 0) { |
| atmRow.scrollIntoView({block:'center', behavior:'auto'}); |
| window.scrolled = true; |
| } |
| } |
| |
| let maxRange = 10; |
| if(CONFIG.side && Array.isArray(CONFIG.side)) { |
| CONFIG.side.forEach(item => { if(item.ranges) { const m = Math.max(...item.ranges); if(m > maxRange) maxRange = m; } }); |
| } |
| |
| const start = Math.max(0, atmIdx - 20); |
| const end = Math.min(kList.length, atmIdx + 20); |
| const viewKs = kList.slice(start, end); |
| |
| const rangeStart = Math.max(0, atmIdx - maxRange); |
| const rangeEnd = Math.min(kList.length, atmIdx + maxRange + 1); |
| const calcKs = kList.slice(rangeStart, rangeEnd); |
| |
| const allNeededKs = new Set([...viewKs, ...calcKs]); |
| |
| OPT_SYMS.forEach(o => { |
| if(allNeededKs.has(o.k)) symbolsToFetch.push(o.s); |
| }); |
| } |
| |
| const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2); |
| if(uniqueSyms.length === 0) return; |
| |
| const url = `https://api.upstox.com/v2/market-quote/quotes?instrument_key=${uniqueSyms.join(',')}`; |
| const res = await fetch(url, { headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${UPSTOX_TOKEN}` }}); |
| |
| if(res.status === 401) { location.href = '/logout'; return; } |
| |
| const json = await res.json(); |
| if(json.status === 'success') { |
| updateUI(json.data); |
| const dot = document.getElementById('statusDot'); |
| dot.classList.add('active'); |
| dot.classList.remove('error'); |
| } |
| } catch(e) { |
| console.error(e); |
| const dot = document.getElementById('statusDot'); |
| dot.classList.remove('active'); |
| dot.classList.add('error'); |
| } |
| finally { IS_POLLING = false; } |
| } |
| |
| function updateUI(raw_data) { |
| // Create a Token Map for easy lookup |
| const tokenMap = {}; |
| for(const key in raw_data) { |
| const item = raw_data[key]; |
| if(item && item.instrument_token) { |
| tokenMap[item.instrument_token] = item; |
| } |
| } |
| |
| // Update Reference Price |
| // Check if SELECTED_REF_SYM exists in the returned data |
| const refData = tokenMap[SELECTED_REF_SYM]; |
| if(refData) { |
| SPOT_PRICE = refData.last_price; |
| document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2); |
| } else { |
| // If data for selected symbol didn't come back, keep existing SPOT_PRICE |
| // (Don't set to 0 to avoid flickering if one packet drops) |
| } |
| |
| // Update Futures |
| const futHTML = FUT_SYMS.map(f => { |
| const d = tokenMap[f.s]; |
| if(!d) return ''; |
| const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'}); |
| const b = d.total_buy_quantity || 0; |
| const s = d.total_sell_quantity || 0; |
| const ch = d.net_change || 0; |
| const tot = (b + s) || 1; |
| const bPct = (b/tot)*100; |
| return ` |
| <div class="fut-card"> |
| <div style="display:flex; justify-content:space-between; font-weight:700;"> |
| <span>${dt} FUT</span> <span style="color:${ch>=0?'var(--green)':'var(--red)'}">${d.last_price}</span> |
| </div> |
| <div class="meter-bg"> |
| <div style="width:${bPct}%; background:var(--green)"></div> |
| <div style="width:${100-bPct}%; background:var(--red)"></div> |
| </div> |
| <div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;"> |
| <span>B: ${fmt(b)}</span> <span>S: ${fmt(s)}</span> |
| </div> |
| </div>`; |
| }).join(''); |
| if(futHTML) document.getElementById('futBody').innerHTML = futHTML; |
| |
| // Process Options |
| const rowData = {}; |
| for(const item of OPT_SYMS) { |
| const d = tokenMap[item.s]; |
| if(!d) continue; |
| |
| if(!rowData[item.k]) rowData[item.k] = { |
| CS:0, CB:0, PS:0, PB:0, |
| CO:0, PO:0, CLTP:0, PLTP:0, CV:0, PV:0 |
| }; |
| |
| if(item.t === 'CE') { |
| rowData[item.k].CS = d.total_sell_quantity || 0; |
| rowData[item.k].CB = d.total_buy_quantity || 0; |
| rowData[item.k].CO = d.oi || 0; |
| rowData[item.k].CLTP = d.last_price || 0; |
| rowData[item.k].CV = d.volume || 0; |
| } |
| if(item.t === 'PE') { |
| rowData[item.k].PS = d.total_sell_quantity || 0; |
| rowData[item.k].PB = d.total_buy_quantity || 0; |
| rowData[item.k].PO = d.oi || 0; |
| rowData[item.k].PLTP = d.last_price || 0; |
| rowData[item.k].PV = d.volume || 0; |
| } |
| } |
| |
| LATEST_ROW_DATA = rowData; |
| |
| // --- RENDER CHAIN --- |
| let maxL = 0, maxR = 0; |
| const processedRows = {}; |
| |
| for(const k in rowData) { |
| const vars = rowData[k]; |
| processedRows[k] = { |
| left: evaluateFormula(CONFIG.left.formula, vars), |
| right: evaluateFormula(CONFIG.right.formula, vars) |
| }; |
| maxL = Math.max(maxL, Math.abs(processedRows[k].left)); |
| maxR = Math.max(maxR, Math.abs(processedRows[k].right)); |
| } |
| |
| for(const k in processedRows) { |
| const r = processedRows[k]; |
| |
| const valL = document.getElementById(`valL-${k}`); |
| if(valL) { |
| valL.innerText = (r.left>0?"+":"") + fmt(r.left); |
| valL.style.color = r.left >= 0 ? 'var(--green)' : 'var(--red)'; |
| |
| const barL = document.getElementById(`barL-${k}`); |
| if(maxL > 0) { |
| const pct = (Math.abs(r.left) / maxL) * 100; |
| barL.style.left = '0%'; |
| barL.style.width = pct + '%'; |
| barL.style.background = r.left >= 0 ? 'var(--green-soft)' : 'var(--red-soft)'; |
| } |
| } |
| |
| const valR = document.getElementById(`valR-${k}`); |
| if(valR) { |
| valR.innerText = (r.right>0?"+":"") + fmt(r.right); |
| valR.style.color = r.right >= 0 ? 'var(--green)' : 'var(--red)'; |
| |
| const barR = document.getElementById(`barR-${k}`); |
| if(maxR > 0) { |
| const pct = (Math.abs(r.right) / maxR) * 100; |
| barR.style.left = '0%'; |
| barR.style.width = pct + '%'; |
| barR.style.background = r.right >= 0 ? 'var(--green-soft)' : 'var(--red-soft)'; |
| } |
| } |
| } |
| |
| // --- SIDEBAR AGGREGATION --- |
| if(SPOT_PRICE > 0 && CONFIG.side && CONFIG.side.length > 0) { |
| const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b); |
| const lowers = kList.filter(k => k < SPOT_PRICE); |
| const uppers = kList.filter(k => k >= SPOT_PRICE); |
| |
| CONFIG.side.forEach((item, idx) => { |
| item.ranges.forEach((rng, rIdx) => { |
| const el = document.getElementById(`sv-${idx}-${rIdx}`); |
| if(el) { |
| const selLow = lowers.slice(-rng); |
| const selHigh = uppers.slice(0, rng); |
| const targetStrikes = [...selLow, ...selHigh]; |
| |
| let total = 0; |
| for(const k of targetStrikes) { |
| if(rowData[k]) total += evaluateFormula(item.formula, rowData[k]); |
| } |
| el.innerText = (total > 0 ? "+" : "") + fmt(total); |
| el.style.color = total >= 0 ? 'var(--green)' : 'var(--red)'; |
| } |
| }); |
| }); |
| } |
| } |
| |
| function fmt(n) { |
| if(!n) return '0'; |
| let abs = Math.abs(n); |
| if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr'; |
| if(abs>=100000) return (n/100000).toFixed(2)+'L'; |
| if(abs>=1000) return (n/1000).toFixed(1)+'k'; |
| return n.toLocaleString(); |
| } |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |