Spaces:
Sleeping
Sleeping
| import requests | |
| import uvicorn | |
| import json | |
| import threading | |
| import os | |
| import time | |
| from fastapi import FastAPI | |
| from fastapi.responses import HTMLResponse, JSONResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| # ========================================== | |
| # 1. CONFIGURATION & BACKEND | |
| # ========================================== | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| SYMBOL_FILE = "symbols.json" | |
| SYMBOL_CACHE = [] | |
| 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", | |
| "Referer": "https://gocharting.com/", | |
| "Accept": "application/json" | |
| } | |
| # --- BACKGROUND DATABASE WORKER --- | |
| def update_symbol_master(): | |
| global SYMBOL_CACHE | |
| print(">>> [SYSTEM] Updating Symbol DB...") | |
| temp_list = [] | |
| defaults = [ | |
| {"symbol": "NIFTY", "name": "Nifty 50", "exchange": "NSE"}, | |
| {"symbol": "BANKNIFTY", "name": "Nifty Bank", "exchange": "NSE"}, | |
| {"symbol": "NIFTYFINANCE", "name": "Nifty Fin Service", "exchange": "NSE"} | |
| ] | |
| temp_list.extend(defaults) | |
| for i in range(65, 91): # A-Z | |
| letter = chr(i) | |
| try: | |
| url = f"https://gocharting.com/api/instruments_with_options/search?q={letter}" | |
| r = requests.get(url, headers=HEADERS, timeout=5) | |
| if r.status_code == 200: | |
| data = r.json() | |
| results = data.get("payload", {}).get("results", []) | |
| for item in results: | |
| details = item.get("item", {}) | |
| if details.get("has_options_chain"): | |
| temp_list.append({ | |
| "symbol": details.get("symbol"), | |
| "name": details.get("name"), | |
| "exchange": details.get("exchange") | |
| }) | |
| time.sleep(0.1) | |
| except: pass | |
| seen = set() | |
| unique_list = [] | |
| for d in temp_list: | |
| t = tuple(d.items()) | |
| if t not in seen: | |
| seen.add(t) | |
| unique_list.append(d) | |
| SYMBOL_CACHE = unique_list | |
| with open(SYMBOL_FILE, "w") as f: | |
| json.dump(SYMBOL_CACHE, f) | |
| print(f">>> [SYSTEM] DB Updated: {len(SYMBOL_CACHE)} symbols.") | |
| def load_cache_on_startup(): | |
| global SYMBOL_CACHE | |
| if os.path.exists(SYMBOL_FILE): | |
| try: | |
| with open(SYMBOL_FILE, "r") as f: | |
| SYMBOL_CACHE = json.load(f) | |
| except: pass | |
| if len(SYMBOL_CACHE) < 5: | |
| threading.Thread(target=update_symbol_master).start() | |
| def startup_event(): | |
| load_cache_on_startup() | |
| # ========================================== | |
| # 2. API ROUTES | |
| # ========================================== | |
| def home(): | |
| return HTMLResponse(FRONTEND_HTML) | |
| def search_symbols(q: str): | |
| q = q.upper() | |
| results = [item for item in SYMBOL_CACHE if q in item["symbol"] or q in item["name"].upper()] | |
| return JSONResponse(results[:15]) | |
| def proxy_data(symbol: str, exchange: str): | |
| # This fetches the FULL chain (all expiries) | |
| url = "https://gocharting.com/api/options/chain" | |
| params = {"exchange": exchange, "segment": "SPOT", "symbol": symbol, "quote_underlying": "true", "quote_chain": "true"} | |
| try: | |
| r = requests.get(url, headers=HEADERS, params=params, timeout=10) | |
| return JSONResponse(r.json()) | |
| except Exception as e: | |
| return JSONResponse({"status": "error", "msg": str(e)}, status_code=500) | |
| # ========================================== | |
| # 3. FRONTEND (MULTI-EXPIRY AGGREGATOR) | |
| # ========================================== | |
| FRONTEND_HTML = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>GEX TERMINAL | MULTI-EXPIRY</title> | |
| <!-- FONTS --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <!-- CHART.JS --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script> | |
| <style> | |
| :root { | |
| /* TERMINAL THEME */ | |
| --bg-main: #050505; | |
| --bg-panel: #0e0e0e; | |
| --bg-input: #111; | |
| --border: #27272a; | |
| --text-pri: #f4f4f5; | |
| --text-sec: #a1a1aa; | |
| --text-muted: #52525b; | |
| --accent: #6366f1; | |
| --spot: #3b82f6; | |
| --call: #ef4444; | |
| --put: #10b981; | |
| } | |
| * { box-sizing: border-box; outline: none; -webkit-font-smoothing: antialiased; } | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: var(--bg-main); } | |
| ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } | |
| body { margin: 0; background: var(--bg-main); color: var(--text-pri); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; } | |
| /* HEADER */ | |
| header { height: 50px; background: var(--bg-panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; flex-shrink: 0; } | |
| .brand { font-family: 'JetBrains Mono'; font-weight: 800; font-size: 16px; display: flex; align-items: center; gap: 8px; } | |
| .brand span { color: var(--accent); } | |
| /* SEARCH */ | |
| .search-wrap { position: relative; width: 280px; } | |
| .search-inp { width: 100%; background: #000; border: 1px solid var(--border); color: #fff; padding: 6px 10px 6px 30px; border-radius: 4px; font-family: 'JetBrains Mono'; font-size: 12px; } | |
| .search-inp:focus { border-color: var(--accent); } | |
| .search-icon { position: absolute; left: 8px; top: 8px; width: 12px; height: 12px; fill: var(--text-muted); } | |
| .dropdown { position: absolute; top: 105%; left: 0; width: 100%; background: var(--bg-panel); border: 1px solid var(--border); border-radius: 4px; z-index: 100; max-height: 400px; overflow-y: auto; display: none; } | |
| .dd-item { padding: 8px 10px; border-bottom: 1px solid var(--border); cursor: pointer; display: flex; justify-content: space-between; font-size: 11px; } | |
| .dd-item:hover { background: var(--border); } | |
| /* MAIN */ | |
| .layout { display: flex; height: calc(100vh - 50px); } | |
| /* SIDEBAR */ | |
| .sidebar { width: 300px; background: #080808; border-right: 1px solid var(--border); padding: 15px; display: flex; flex-direction: column; gap: 15px; overflow-y: auto; flex-shrink: 0; } | |
| .panel { background: var(--bg-panel); border: 1px solid var(--border); border-radius: 6px; padding: 12px; } | |
| .lbl { font-size: 10px; color: var(--text-sec); font-weight: 600; text-transform: uppercase; margin-bottom: 8px; display: flex; justify-content: space-between; align-items: center; } | |
| /* SPOT DISPLAY */ | |
| .spot-display { border-left: 3px solid var(--spot); } | |
| .spot-val { font-family: 'JetBrains Mono'; font-size: 22px; font-weight: 700; color: #fff; } | |
| .spot-sub { font-size: 10px; color: var(--text-muted); margin-top: 2px; display: flex; justify-content: space-between; } | |
| /* MULTI-SELECT EXPIRY */ | |
| .exp-container { display: flex; flex-direction: column; gap: 5px; height: 180px; } | |
| .exp-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; } | |
| .btn-tiny { background: var(--border); border: none; color: var(--text-sec); font-size: 9px; padding: 2px 6px; border-radius: 3px; cursor: pointer; } | |
| .btn-tiny:hover { background: var(--accent); color: #fff; } | |
| .exp-list { | |
| flex: 1; overflow-y: auto; background: #000; border: 1px solid var(--border); border-radius: 4px; | |
| padding: 4px; | |
| } | |
| .exp-item { | |
| display: flex; align-items: center; gap: 8px; padding: 6px; | |
| font-family: 'JetBrains Mono'; font-size: 11px; color: var(--text-sec); | |
| cursor: pointer; border-radius: 2px; | |
| } | |
| .exp-item:hover { background: #1a1a1a; } | |
| .exp-item input { accent-color: var(--accent); cursor: pointer; } | |
| .exp-item.selected { color: #fff; background: #111; } | |
| /* SLIDERS & INPUTS */ | |
| .rng-row { display: flex; align-items: center; gap: 10px; } | |
| input[type=range] { flex: 1; accent-color: var(--text-sec); height: 4px; background: #333; border-radius: 2px; appearance: none; } | |
| input[type=number] { width: 50px; background: #000; border: 1px solid var(--border); color: #fff; padding: 4px; font-family: 'JetBrains Mono'; font-size: 11px; text-align: center; border-radius: 3px; } | |
| /* TOGGLES */ | |
| .toggle-group { display: flex; background: #000; border: 1px solid var(--border); border-radius: 4px; padding: 2px; } | |
| .toggle-opt { flex: 1; text-align: center; font-size: 10px; padding: 6px; cursor: pointer; color: var(--text-muted); font-weight: 600; border-radius: 2px; transition: 0.2s; } | |
| .toggle-opt.active { background: var(--bg-panel); color: #fff; border: 1px solid var(--border); } | |
| /* STATS */ | |
| .stat-row { display: flex; justify-content: space-between; font-size: 11px; padding: 4px 0; border-bottom: 1px solid #1a1a1a; } | |
| .stat-row:last-child { border: none; } | |
| .val-call { color: var(--call); font-family: 'JetBrains Mono'; } | |
| .val-put { color: var(--put); font-family: 'JetBrains Mono'; } | |
| /* LIVE SWITCH */ | |
| .live-wrap { margin-top: auto; display: flex; align-items: center; justify-content: space-between; padding: 10px; background: rgba(16, 185, 129, 0.05); border: 1px solid rgba(16, 185, 129, 0.2); border-radius: 6px; } | |
| .pulse { width: 6px; height: 6px; background: #333; border-radius: 50%; } | |
| .pulse.on { background: #10b981; box-shadow: 0 0 6px #10b981; animation: blink 1.5s infinite; } | |
| @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } } | |
| /* CHART */ | |
| .chart-area { flex: 1; padding: 20px; display: flex; flex-direction: column; position: relative; } | |
| .chart-canv { flex: 1; position: relative; border: 1px solid var(--border); border-radius: 6px; background: #080808; padding: 10px; } | |
| #loader { position: absolute; inset: 0; background: rgba(0,0,0,0.8); z-index: 50; display: none; align-items: center; justify-content: center; flex-direction: column; } | |
| .spin { width: 24px; height: 24px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: rot 1s infinite linear; } | |
| @keyframes rot { to { transform: rotate(360deg); } } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="brand"> | |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="white"><path d="M3 3h18v2H3V3zm0 16h18v2H3v-2zm0-8h18v2H3v-2zm0-4h18v2H3V7zm0 12h18v2H3v-2z"/></svg> | |
| GEX<span>TERMINAL</span> | |
| </div> | |
| <div class="search-wrap"> | |
| <svg class="search-icon" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg> | |
| <input type="text" class="search-inp" id="sInp" placeholder="SEARCH (e.g. NIFTY)..." autocomplete="off"> | |
| <div class="dropdown" id="sRes"></div> | |
| </div> | |
| <div style="font-size: 10px; color: var(--text-sec);">CONNECTED <span style="color:var(--put)">●</span></div> | |
| </header> | |
| <div class="layout"> | |
| <div class="sidebar"> | |
| <!-- SPOT --> | |
| <div class="panel spot-display"> | |
| <div class="lbl">Underlying Spot</div> | |
| <div class="spot-val" id="spotPrice">----.--</div> | |
| <div class="spot-sub"> | |
| <span id="tickerName">NIFTY</span> | |
| <span id="lastTime">--:--:--</span> | |
| </div> | |
| </div> | |
| <!-- MULTI EXPIRY SELECTOR --> | |
| <div class="panel exp-container"> | |
| <div class="exp-toolbar"> | |
| <div class="lbl" style="margin:0">Expiries</div> | |
| <div> | |
| <button class="btn-tiny" onclick="selectAllExp(true)">ALL</button> | |
| <button class="btn-tiny" onclick="selectAllExp(false)">NONE</button> | |
| </div> | |
| </div> | |
| <div class="exp-list" id="expiryList"> | |
| <div style="padding:10px; font-size:10px; color:#555; text-align:center;">Load a symbol...</div> | |
| </div> | |
| </div> | |
| <!-- RANGE --> | |
| <div class="panel"> | |
| <div class="lbl">Strike Range (ATM ±)</div> | |
| <div class="rng-row"> | |
| <input type="range" id="rngSld" min="5" max="100" value="15" oninput="updateRange(this.value)"> | |
| <input type="number" id="rngInp" value="15" onchange="updateRange(this.value)"> | |
| </div> | |
| </div> | |
| <!-- MODE --> | |
| <div class="panel"> | |
| <div class="lbl">Chart Mode</div> | |
| <div class="toggle-group"> | |
| <div class="toggle-opt active" id="btnSplit" onclick="setMode('split')">SPLIT</div> | |
| <div class="toggle-opt" id="btnNet" onclick="setMode('net')">NET</div> | |
| </div> | |
| </div> | |
| <!-- AGGREGATED STATS --> | |
| <div class="panel"> | |
| <div class="lbl">Aggregated Exposure</div> | |
| <div class="stat-row"> | |
| <span>Total Call GEX</span> | |
| <span class="val-call" id="statCall">0.00 Cr</span> | |
| </div> | |
| <div class="stat-row"> | |
| <span>Total Put GEX</span> | |
| <span class="val-put" id="statPut">0.00 Cr</span> | |
| </div> | |
| <div class="stat-row" style="margin-top:4px; padding-top:4px; border-top:1px dashed #333;"> | |
| <span style="color:#fff">NET GEX</span> | |
| <span style="font-weight:700" id="statNet">0.00 Cr</span> | |
| </div> | |
| </div> | |
| <!-- LIVE --> | |
| <div class="live-wrap"> | |
| <div style="display:flex; align-items:center; gap:8px;"> | |
| <div class="pulse" id="pulse"></div> | |
| <div style="font-size:11px; font-weight:700;">AUTO REFRESH</div> | |
| </div> | |
| <input type="checkbox" id="liveCheck" onchange="toggleLive()"> | |
| </div> | |
| </div> | |
| <div class="chart-area"> | |
| <div id="loader"><div class="spin"></div><div style="margin-top:10px; font-size:10px;">AGGREGATING DATA...</div></div> | |
| <div style="display:flex; justify-content:space-between; margin-bottom:10px;"> | |
| <div style="font-size:12px; font-weight:700; color:var(--text-sec)">COMBINED GAMMA PROFILE</div> | |
| <div style="display:flex; gap:15px; font-size:11px; color:var(--text-sec)"> | |
| <div style="display:flex; align-items:center; gap:5px"><div style="width:8px; height:8px; background:var(--call)"></div> Resistance</div> | |
| <div style="display:flex; align-items:center; gap:5px"><div style="width:8px; height:8px; background:var(--put)"></div> Support</div> | |
| </div> | |
| </div> | |
| <div class="chart-canv"> | |
| <canvas id="gexChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // --- GLOBAL STATE --- | |
| Chart.defaults.color = '#52525b'; | |
| Chart.defaults.font.family = 'Inter'; | |
| let CHART = null; | |
| let RAW_DATA = null; // Stores the full JSON payload | |
| let SPOT_PRICE = 0; | |
| let CONFIG = { | |
| symbol: 'NIFTY', | |
| exchange: 'NSE', | |
| mode: 'split', // split | net | |
| range: 15, | |
| selectedIndices: new Set([0]) // Default to first expiry | |
| }; | |
| let TIMER = null; | |
| // --- INIT --- | |
| window.onload = () => loadData('NIFTY', 'NSE'); | |
| // --- SEARCH --- | |
| const sInp = document.getElementById('sInp'); | |
| const sRes = document.getElementById('sRes'); | |
| let debounce; | |
| sInp.addEventListener('input', (e) => { | |
| clearTimeout(debounce); | |
| const q = e.target.value; | |
| if(q.length < 1) { sRes.style.display='none'; return; } | |
| debounce = setTimeout(async () => { | |
| try { | |
| const r = await fetch(`/api/search?q=${q}`); | |
| const d = await r.json(); | |
| sRes.innerHTML = ''; | |
| if(d.length > 0) { | |
| sRes.style.display = 'block'; | |
| d.forEach(i => { | |
| const div = document.createElement('div'); | |
| div.className = 'dd-item'; | |
| div.innerHTML = `<span>${i.symbol}</span><span>${i.exchange}</span>`; | |
| div.onclick = () => { | |
| loadData(i.symbol, i.exchange); | |
| sRes.style.display = 'none'; | |
| sInp.value = i.symbol; | |
| }; | |
| sRes.appendChild(div); | |
| }); | |
| } else sRes.style.display='none'; | |
| } catch {} | |
| }, 300); | |
| }); | |
| document.addEventListener('click', (e) => { | |
| if(!e.target.closest('.search-wrap')) sRes.style.display='none'; | |
| }); | |
| // --- DATA LOADING --- | |
| async function loadData(sym, exch, isBackground=false) { | |
| if(!isBackground) document.getElementById('loader').style.display = 'flex'; | |
| CONFIG.symbol = sym; | |
| CONFIG.exchange = exch; | |
| document.getElementById('tickerName').innerText = sym; | |
| try { | |
| const res = await fetch(`/api/fetch?symbol=${sym}&exchange=${exch}`); | |
| const json = await res.json(); | |
| if(!json.payload || !json.payload.chain) throw new Error("No Data"); | |
| RAW_DATA = json.payload; | |
| SPOT_PRICE = RAW_DATA.underlying?.quote?.last_price || 0; | |
| // UI Update | |
| document.getElementById('spotPrice').innerText = SPOT_PRICE.toLocaleString(undefined, {minimumFractionDigits: 2}); | |
| document.getElementById('lastTime').innerText = new Date().toLocaleTimeString(); | |
| // Build Expiry List (Only if full reload or empty) | |
| if(!isBackground || document.getElementById('expiryList').children.length === 0) { | |
| buildExpiryList(); | |
| } | |
| processAndRender(); | |
| } catch(e) { | |
| console.error(e); | |
| if(!isBackground) alert("Could not fetch data."); | |
| } finally { | |
| document.getElementById('loader').style.display = 'none'; | |
| } | |
| } | |
| // --- EXPIRY LIST LOGIC --- | |
| function buildExpiryList() { | |
| const container = document.getElementById('expiryList'); | |
| container.innerHTML = ''; | |
| // Reset selection if new symbol, default to 0 (nearest) | |
| // If background refresh, keep selection | |
| if(CONFIG.selectedIndices.size === 0) CONFIG.selectedIndices.add(0); | |
| RAW_DATA.chain.forEach((c, idx) => { | |
| const div = document.createElement('div'); | |
| div.className = 'exp-item'; | |
| if(CONFIG.selectedIndices.has(idx)) div.classList.add('selected'); | |
| // Checkbox | |
| const chk = document.createElement('input'); | |
| chk.type = 'checkbox'; | |
| chk.checked = CONFIG.selectedIndices.has(idx); | |
| chk.onclick = (e) => { e.stopPropagation(); toggleExpiry(idx, chk.checked); }; | |
| // Text | |
| const txt = document.createElement('span'); | |
| txt.innerText = c.expiry_date; | |
| div.appendChild(chk); | |
| div.appendChild(txt); | |
| div.onclick = () => { chk.checked = !chk.checked; toggleExpiry(idx, chk.checked); }; | |
| container.appendChild(div); | |
| }); | |
| } | |
| function toggleExpiry(idx, isChecked) { | |
| if(isChecked) CONFIG.selectedIndices.add(idx); | |
| else CONFIG.selectedIndices.delete(idx); | |
| // Update Visuals | |
| const items = document.querySelectorAll('.exp-item'); | |
| if(items[idx]) { | |
| if(isChecked) items[idx].classList.add('selected'); | |
| else items[idx].classList.remove('selected'); | |
| } | |
| processAndRender(); | |
| } | |
| function selectAllExp(all) { | |
| const checkboxes = document.querySelectorAll('.exp-item input'); | |
| const rows = document.querySelectorAll('.exp-item'); | |
| checkboxes.forEach((chk, idx) => { | |
| chk.checked = all; | |
| if(all) { | |
| CONFIG.selectedIndices.add(idx); | |
| rows[idx].classList.add('selected'); | |
| } else { | |
| CONFIG.selectedIndices.delete(idx); | |
| rows[idx].classList.remove('selected'); | |
| } | |
| }); | |
| processAndRender(); | |
| } | |
| // --- AGGREGATION & RENDER ENGINE --- | |
| function processAndRender() { | |
| if(!RAW_DATA) return; | |
| // 1. Aggregate Data across selected expiries | |
| // Map: Strike -> { callGex, putGex } | |
| const strikeMap = new Map(); | |
| let totalCall = 0; | |
| let totalPut = 0; | |
| let totalNet = 0; | |
| CONFIG.selectedIndices.forEach(idx => { | |
| const expiryData = RAW_DATA.chain[idx]; | |
| if(!expiryData) return; | |
| expiryData.options.forEach(opt => { | |
| const k = opt.strike_price; | |
| // GEX Formula: Gamma * OI * Spot * 100 | |
| const cG = (opt.call?.greeks?.gamma || 0) * (opt.call?.quote?.open_interest || 0) * SPOT_PRICE * 100; | |
| const pG = (opt.put?.greeks?.gamma || 0) * (opt.put?.quote?.open_interest || 0) * SPOT_PRICE * 100; | |
| if(!strikeMap.has(k)) strikeMap.set(k, { c: 0, p: 0 }); | |
| const node = strikeMap.get(k); | |
| node.c += cG; | |
| node.p += pG; | |
| totalCall += cG; | |
| totalPut += pG; | |
| totalNet += (cG - pG); | |
| }); | |
| }); | |
| // 2. Convert Map to Sorted Array | |
| let strikesArr = Array.from(strikeMap.keys()).sort((a, b) => a - b); | |
| // 3. Find ATM and Slice Range | |
| // Interpolate nearest strike to spot | |
| let atmIndex = 0; | |
| let minDiff = Infinity; | |
| for(let i=0; i<strikesArr.length; i++) { | |
| const diff = Math.abs(strikesArr[i] - SPOT_PRICE); | |
| if(diff < minDiff) { minDiff = diff; atmIndex = i; } | |
| } | |
| const range = parseInt(CONFIG.range); | |
| const startIndex = Math.max(0, atmIndex - range); | |
| const endIndex = Math.min(strikesArr.length, atmIndex + range + 1); | |
| const slicedStrikes = strikesArr.slice(startIndex, endIndex); | |
| // 4. Prepare Chart Datasets | |
| const labels = []; | |
| const dsCall = []; | |
| const dsPut = []; | |
| const dsNet = []; | |
| slicedStrikes.forEach(k => { | |
| labels.push(k); | |
| const val = strikeMap.get(k); | |
| if(CONFIG.mode === 'split') { | |
| dsCall.push(val.c); | |
| dsPut.push(-val.p); // Negative for rendering below axis | |
| } else { | |
| dsNet.push(val.c - val.p); | |
| } | |
| }); | |
| // 5. Update Stats Panel | |
| const toCr = v => (v / 10000000).toFixed(2) + " Cr"; | |
| document.getElementById('statCall').innerText = toCr(totalCall); | |
| document.getElementById('statPut').innerText = toCr(totalPut); | |
| const netEl = document.getElementById('statNet'); | |
| netEl.innerText = toCr(totalNet); | |
| netEl.style.color = totalNet >= 0 ? 'var(--call)' : 'var(--put)'; | |
| // 6. Draw Chart | |
| drawChart(labels, dsCall, dsPut, dsNet); | |
| } | |
| function drawChart(labels, calls, puts, nets) { | |
| const ctx = document.getElementById('gexChart').getContext('2d'); | |
| let datasets = []; | |
| if(CONFIG.mode === 'split') { | |
| datasets = [ | |
| { | |
| label: 'Call Wall', | |
| data: calls, | |
| backgroundColor: '#ef4444', | |
| borderRadius: 3, | |
| barPercentage: 0.6, | |
| order: 1 | |
| }, | |
| { | |
| label: 'Put Wall', | |
| data: puts, | |
| backgroundColor: '#10b981', | |
| borderRadius: 3, | |
| barPercentage: 0.6, | |
| order: 2 | |
| } | |
| ]; | |
| } else { | |
| datasets = [{ | |
| label: 'Net Exposure', | |
| data: nets, | |
| backgroundColor: nets.map(v => v >= 0 ? '#ef4444' : '#10b981'), | |
| borderRadius: 3, | |
| barPercentage: 0.6 | |
| }]; | |
| } | |
| // Spot Line Annotation Logic | |
| const spotX = getSpotInterpolation(labels, SPOT_PRICE); | |
| if(CHART) { | |
| CHART.data.labels = labels; | |
| CHART.data.datasets = datasets; | |
| CHART.options.plugins.annotation.annotations.line1.value = spotX; | |
| CHART.options.plugins.annotation.annotations.line1.label.content = `SPOT: ${SPOT_PRICE}`; | |
| // Adjust Y Axis to center 0 | |
| if(CONFIG.mode === 'split') { | |
| const max = Math.max(...calls, ...puts.map(Math.abs)) * 1.1; | |
| CHART.options.scales.y.min = -max; | |
| CHART.options.scales.y.max = max; | |
| } else { | |
| delete CHART.options.scales.y.min; | |
| delete CHART.options.scales.y.max; | |
| } | |
| CHART.update('none'); | |
| } else { | |
| CHART = new Chart(ctx, { | |
| type: 'bar', | |
| data: { labels, datasets }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| interaction: { mode: 'index', intersect: false }, | |
| plugins: { | |
| legend: { display: false }, | |
| tooltip: { | |
| backgroundColor: '#111', | |
| bodyFont: { family: 'JetBrains Mono' }, | |
| callbacks: { | |
| label: (ctx) => { | |
| let v = ctx.raw; | |
| let l = ctx.dataset.label || ''; | |
| return `${l}: ${(Math.abs(v)/100000).toFixed(2)} L`; | |
| } | |
| } | |
| }, | |
| annotation: { | |
| annotations: { | |
| line1: { | |
| type: 'line', | |
| scaleID: 'x', | |
| value: spotX, | |
| borderColor: '#3b82f6', | |
| borderWidth: 2, | |
| borderDash: [5, 5], | |
| label: { | |
| display: true, | |
| content: `SPOT: ${SPOT_PRICE}`, | |
| position: 'start', | |
| backgroundColor: '#3b82f6', | |
| font: { size: 10, weight:'bold' } | |
| } | |
| } | |
| } | |
| } | |
| }, | |
| scales: { | |
| x: { | |
| stacked: true, | |
| grid: { display: false }, | |
| ticks: { color: '#71717a', font: { family: 'JetBrains Mono', size: 9 }, maxRotation: 90, minRotation: 90 } | |
| }, | |
| y: { | |
| stacked: true, | |
| grid: { color: '#27272a', borderDash: [2, 2] }, | |
| ticks: { display: false } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| function getSpotInterpolation(strikes, spot) { | |
| for(let i=0; i<strikes.length-1; i++) { | |
| if(spot >= strikes[i] && spot <= strikes[i+1]) { | |
| const range = strikes[i+1] - strikes[i]; | |
| const dist = spot - strikes[i]; | |
| return i + (dist/range); | |
| } | |
| } | |
| return strikes.length/2; | |
| } | |
| // --- CONTROLS --- | |
| function updateRange(val) { | |
| CONFIG.range = val; | |
| document.getElementById('rngSld').value = val; | |
| document.getElementById('rngInp').value = val; | |
| processAndRender(); | |
| } | |
| function setMode(mode) { | |
| CONFIG.mode = mode; | |
| document.getElementById('btnSplit').className = mode === 'split' ? 'toggle-opt active' : 'toggle-opt'; | |
| document.getElementById('btnNet').className = mode === 'net' ? 'toggle-opt active' : 'toggle-opt'; | |
| if(CHART) { CHART.destroy(); CHART = null; } | |
| processAndRender(); | |
| } | |
| function toggleLive() { | |
| const on = document.getElementById('liveCheck').checked; | |
| const pulse = document.getElementById('pulse'); | |
| if(on) { | |
| pulse.classList.add('on'); | |
| TIMER = setInterval(() => loadData(CONFIG.symbol, CONFIG.exchange, true), 5000); | |
| } else { | |
| pulse.classList.remove('on'); | |
| clearInterval(TIMER); | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |