Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -4,13 +4,13 @@ import json
|
|
| 4 |
import threading
|
| 5 |
import os
|
| 6 |
import time
|
| 7 |
-
from datetime import datetime
|
| 8 |
from fastapi import FastAPI, BackgroundTasks
|
| 9 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
|
| 12 |
# ==========================================
|
| 13 |
-
# 1. CONFIGURATION
|
| 14 |
# ==========================================
|
| 15 |
app = FastAPI()
|
| 16 |
|
|
@@ -21,10 +21,9 @@ app.add_middleware(
|
|
| 21 |
allow_headers=["*"],
|
| 22 |
)
|
| 23 |
|
| 24 |
-
# File persistence
|
| 25 |
SYMBOL_FILE = "symbols.json"
|
| 26 |
|
| 27 |
-
#
|
| 28 |
SYMBOL_CACHE = [
|
| 29 |
{"symbol": "NIFTY", "name": "Nifty 50", "exchange": "NSE"},
|
| 30 |
{"symbol": "BANKNIFTY", "name": "Nifty Bank", "exchange": "NSE"},
|
|
@@ -40,18 +39,11 @@ HEADERS = {
|
|
| 40 |
}
|
| 41 |
|
| 42 |
# ==========================================
|
| 43 |
-
# 2. BACKEND LOGIC
|
| 44 |
# ==========================================
|
| 45 |
def fetch_master_list():
|
| 46 |
-
"""
|
| 47 |
-
Runs in background. Loops A-Z to find all optionable instruments.
|
| 48 |
-
"""
|
| 49 |
global SYMBOL_CACHE
|
| 50 |
-
print(">>> [SYSTEM] Starting Background Symbol Update (A-Z)...")
|
| 51 |
-
|
| 52 |
temp_list = []
|
| 53 |
-
|
| 54 |
-
# Loop ASCII 65(A) to 90(Z)
|
| 55 |
for i in range(65, 91):
|
| 56 |
letter = chr(i)
|
| 57 |
try:
|
|
@@ -62,62 +54,40 @@ def fetch_master_list():
|
|
| 62 |
results = data.get("payload", {}).get("results", [])
|
| 63 |
for item in results:
|
| 64 |
details = item.get("item", {})
|
| 65 |
-
# Logic: Must have options chain
|
| 66 |
if details.get("has_options_chain") or details.get("symbol") in ["NIFTY", "BANKNIFTY"]:
|
| 67 |
temp_list.append({
|
| 68 |
"symbol": details.get("symbol"),
|
| 69 |
"name": details.get("name"),
|
| 70 |
"exchange": details.get("exchange")
|
| 71 |
})
|
| 72 |
-
# Polite delay
|
| 73 |
time.sleep(0.1)
|
| 74 |
-
except
|
| 75 |
-
print(f"Failed to fetch letter {letter}: {e}")
|
| 76 |
|
| 77 |
-
# Remove duplicates
|
| 78 |
unique = {v['symbol']: v for v in temp_list}.values()
|
| 79 |
-
|
| 80 |
if len(unique) > 0:
|
| 81 |
SYMBOL_CACHE = list(unique)
|
| 82 |
-
# Save to disk
|
| 83 |
with open(SYMBOL_FILE, "w") as f:
|
| 84 |
json.dump(SYMBOL_CACHE, f)
|
| 85 |
-
print(f">>> [SYSTEM] Update Complete. {len(SYMBOL_CACHE)} symbols saved.")
|
| 86 |
-
else:
|
| 87 |
-
print(">>> [SYSTEM] Update failed or found 0 symbols. Keeping defaults.")
|
| 88 |
|
| 89 |
def load_startup_data():
|
| 90 |
-
"""
|
| 91 |
-
Called on app boot. Checks if local file is valid (<24h).
|
| 92 |
-
"""
|
| 93 |
global SYMBOL_CACHE
|
| 94 |
-
|
| 95 |
-
should_update = True
|
| 96 |
-
|
| 97 |
if os.path.exists(SYMBOL_FILE):
|
| 98 |
try:
|
| 99 |
-
# Check file age
|
| 100 |
file_time = os.path.getmtime(SYMBOL_FILE)
|
| 101 |
-
if (time.time() - file_time) < 86400:
|
| 102 |
with open(SYMBOL_FILE, "r") as f:
|
| 103 |
data = json.load(f)
|
| 104 |
-
if len(data) > 5:
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
except:
|
| 109 |
-
print(">>> [SYSTEM] Cache file corrupted.")
|
| 110 |
-
|
| 111 |
-
if should_update:
|
| 112 |
-
# Run in background so we don't block the UI
|
| 113 |
-
threading.Thread(target=fetch_master_list).start()
|
| 114 |
|
| 115 |
@app.on_event("startup")
|
| 116 |
def startup_event():
|
| 117 |
load_startup_data()
|
| 118 |
|
| 119 |
# ==========================================
|
| 120 |
-
# 3.
|
| 121 |
# ==========================================
|
| 122 |
|
| 123 |
@app.get("/")
|
|
@@ -126,25 +96,18 @@ def home():
|
|
| 126 |
|
| 127 |
@app.get("/api/search")
|
| 128 |
def search(q: str):
|
| 129 |
-
"""
|
| 130 |
-
Searches the in-memory cache.
|
| 131 |
-
"""
|
| 132 |
q = q.upper()
|
| 133 |
-
# Simple algorithm: Symbol starts with Q or Name contains Q
|
| 134 |
res = []
|
| 135 |
for s in SYMBOL_CACHE:
|
| 136 |
if q in s['symbol'] or q in s['name'].upper():
|
| 137 |
res.append(s)
|
| 138 |
-
if len(res) >= 15: break
|
| 139 |
return JSONResponse(res)
|
| 140 |
|
| 141 |
@app.get("/api/reload_db")
|
| 142 |
def force_reload(background_tasks: BackgroundTasks):
|
| 143 |
-
"""
|
| 144 |
-
User triggered manual update.
|
| 145 |
-
"""
|
| 146 |
background_tasks.add_task(fetch_master_list)
|
| 147 |
-
return {"status": "started"
|
| 148 |
|
| 149 |
@app.get("/api/db_status")
|
| 150 |
def db_status():
|
|
@@ -152,9 +115,7 @@ def db_status():
|
|
| 152 |
|
| 153 |
@app.get("/api/fetch")
|
| 154 |
def fetch_data(symbol: str, exchange: str):
|
| 155 |
-
|
| 156 |
-
Proxy to GoCharting.
|
| 157 |
-
"""
|
| 158 |
url = "https://gocharting.com/api/options/chain"
|
| 159 |
params = {
|
| 160 |
"exchange": exchange,
|
|
@@ -164,7 +125,7 @@ def fetch_data(symbol: str, exchange: str):
|
|
| 164 |
"quote_chain": "true"
|
| 165 |
}
|
| 166 |
try:
|
| 167 |
-
r = requests.get(url, headers=HEADERS, params=params, timeout=
|
| 168 |
return JSONResponse(r.json())
|
| 169 |
except Exception as e:
|
| 170 |
return JSONResponse({"status": "error", "msg": str(e)}, status_code=500)
|
|
@@ -177,60 +138,60 @@ FRONTEND_HTML = """
|
|
| 177 |
<html lang="en">
|
| 178 |
<head>
|
| 179 |
<meta charset="UTF-8">
|
| 180 |
-
<title>GEX
|
| 181 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 182 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;800&display=swap" rel="stylesheet">
|
| 183 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 184 |
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
|
| 185 |
<style>
|
| 186 |
:root {
|
| 187 |
-
--bg: #09090b;
|
| 188 |
-
--
|
| 189 |
-
--
|
| 190 |
-
--text: #e4e4e7;
|
| 191 |
-
--text-dim: #71717a;
|
| 192 |
-
--accent: #6366f1;
|
| 193 |
-
--call-up: #ef4444; /* Resistance */
|
| 194 |
-
--put-down: #10b981; /* Support */
|
| 195 |
-
--spot: #3b82f6;
|
| 196 |
}
|
| 197 |
body { margin:0; background:var(--bg); color:var(--text); font-family:'Inter'; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
|
| 198 |
|
| 199 |
/* HEADER */
|
| 200 |
header { height:55px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; justify-content:space-between; }
|
| 201 |
-
.logo { font-family:'JetBrains Mono'; font-weight:800;
|
| 202 |
.logo span { color:var(--accent); }
|
| 203 |
-
|
| 204 |
.search-wrap { position:relative; width:260px; }
|
| 205 |
.search-inp { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px 12px; border-radius:6px; font-family:'JetBrains Mono'; font-size:12px; }
|
| 206 |
.search-inp:focus { border-color:var(--accent); outline:none; }
|
| 207 |
-
.results { position:absolute; top:110%; left:0; width:100%; background:var(--panel); border:1px solid var(--border);
|
| 208 |
-
.res-item { padding:8px
|
| 209 |
.res-item:hover { background:var(--border); }
|
| 210 |
|
| 211 |
-
/* MAIN */
|
| 212 |
.layout { display:flex; flex:1; height:calc(100vh - 55px); }
|
| 213 |
.sidebar { width:260px; background:#0c0c0e; border-right:1px solid var(--border); padding:20px; display:flex; flex-direction:column; gap:20px; flex-shrink:0; }
|
| 214 |
.content { flex:1; position:relative; display:flex; flex-direction:column; padding:20px; }
|
| 215 |
|
| 216 |
-
/*
|
| 217 |
.card { background:var(--panel); border:1px solid var(--border); padding:15px; border-radius:6px; }
|
| 218 |
.lbl { font-size:10px; font-weight:700; color:var(--text-dim); text-transform:uppercase; margin-bottom:5px; }
|
| 219 |
.val { font-family:'JetBrains Mono'; font-size:22px; font-weight:700; }
|
| 220 |
-
|
| 221 |
select, input[type=range] { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px; border-radius:4px; margin-top:5px; }
|
| 222 |
-
|
| 223 |
.btn-group { display:flex; background:#000; border:1px solid var(--border); padding:2px; border-radius:4px; margin-top:5px; }
|
| 224 |
.btn { flex:1; text-align:center; padding:6px; font-size:10px; font-weight:600; cursor:pointer; color:var(--text-dim); border-radius:2px; }
|
| 225 |
.btn.active { background:var(--border); color:#fff; }
|
| 226 |
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
.chart-box { flex:1; position:relative; min-height:0; border:1px solid var(--border); background:#000; border-radius:6px; padding:10px; }
|
| 231 |
-
|
| 232 |
-
/* OVERLAYS */
|
| 233 |
-
.loader { position:absolute; inset:0; background:rgba(9,9,11,0.85); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:50; backdrop-filter:blur(2px); }
|
| 234 |
.spin { width:30px; height:30px; border:3px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:rot 1s linear infinite; }
|
| 235 |
@keyframes rot { to { transform:rotate(360deg); } }
|
| 236 |
</style>
|
|
@@ -238,9 +199,9 @@ FRONTEND_HTML = """
|
|
| 238 |
<body>
|
| 239 |
|
| 240 |
<header>
|
| 241 |
-
<div class="logo">GEX<span>
|
| 242 |
<div class="search-wrap">
|
| 243 |
-
<input type="text" class="search-inp" id="sInp" placeholder="Search
|
| 244 |
<div class="results" id="sRes"></div>
|
| 245 |
</div>
|
| 246 |
</header>
|
|
@@ -251,7 +212,22 @@ FRONTEND_HTML = """
|
|
| 251 |
<div class="card" style="border-left:3px solid var(--spot);">
|
| 252 |
<div class="lbl">Spot Price</div>
|
| 253 |
<div class="val" id="spotPrice">---</div>
|
| 254 |
-
<div style="font-size:10px; color:var(--text-dim); margin-top:4px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
</div>
|
| 256 |
|
| 257 |
<!-- CONTROLS -->
|
|
@@ -271,26 +247,16 @@ FRONTEND_HTML = """
|
|
| 271 |
<div class="btn active" id="btnSplit" onclick="setMode('split')">SPLIT</div>
|
| 272 |
<div class="btn" id="btnNet" onclick="setMode('net')">NET</div>
|
| 273 |
</div>
|
| 274 |
-
<div style="margin-top:5px; font-size:9px; color:#555; line-height:1.4;">
|
| 275 |
-
<b>SPLIT:</b> Call Res. (Red) vs Put Sup. (Green)<br>
|
| 276 |
-
<b>NET:</b> Combined Dealer Exposure
|
| 277 |
-
</div>
|
| 278 |
</div>
|
| 279 |
|
| 280 |
-
<
|
| 281 |
-
|
| 282 |
-
<
|
| 283 |
-
<div style="font-size:11px; color:var(--text-dim);">Symbols: <span id="dbCount">...</span></div>
|
| 284 |
-
<button class="reload-btn" onclick="reloadDB()">↻ Update DB (A-Z)</button>
|
| 285 |
</div>
|
| 286 |
</div>
|
| 287 |
|
| 288 |
<div class="content">
|
| 289 |
-
<div class="loader" id="loader">
|
| 290 |
-
<div class="spin"></div>
|
| 291 |
-
<div style="margin-top:10px; font-size:11px; color:var(--text-dim);">FETCHING CHAIN DATA...</div>
|
| 292 |
-
</div>
|
| 293 |
-
|
| 294 |
<div class="chart-box">
|
| 295 |
<canvas id="chart"></canvas>
|
| 296 |
</div>
|
|
@@ -305,118 +271,77 @@ FRONTEND_HTML = """
|
|
| 305 |
let MODE = 'split';
|
| 306 |
let SYM = 'NIFTY';
|
| 307 |
let EXCH = 'NSE';
|
|
|
|
| 308 |
|
| 309 |
const sInp = document.getElementById('sInp');
|
| 310 |
const sRes = document.getElementById('sRes');
|
| 311 |
|
| 312 |
-
// --- STARTUP ---
|
| 313 |
window.onload = () => {
|
| 314 |
updateDBStats();
|
| 315 |
-
loadData('NIFTY', 'NSE');
|
| 316 |
};
|
| 317 |
|
| 318 |
-
// ---
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
if(confirm("Start background update of symbol list? This takes about 30 seconds.")) {
|
| 329 |
-
fetch('/api/reload_db');
|
| 330 |
-
alert("Update started in background. Check count in a minute.");
|
| 331 |
-
// Poll for update
|
| 332 |
-
let i = setInterval(async () => {
|
| 333 |
-
await updateDBStats();
|
| 334 |
}, 5000);
|
| 335 |
-
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
}
|
| 338 |
|
| 339 |
-
// ---
|
| 340 |
-
|
| 341 |
-
sInp.addEventListener('input', (e) => {
|
| 342 |
-
clearTimeout(debounce);
|
| 343 |
-
const q = e.target.value;
|
| 344 |
-
if(q.length < 1) { sRes.style.display='none'; return; }
|
| 345 |
-
|
| 346 |
-
debounce = setTimeout(async () => {
|
| 347 |
-
const r = await fetch(`/api/search?q=${q}`);
|
| 348 |
-
const data = await r.json();
|
| 349 |
-
sRes.innerHTML = '';
|
| 350 |
-
|
| 351 |
-
if(data.length > 0) {
|
| 352 |
-
sRes.style.display = 'block';
|
| 353 |
-
data.forEach(item => {
|
| 354 |
-
const div = document.createElement('div');
|
| 355 |
-
div.className = 'res-item';
|
| 356 |
-
div.innerHTML = `<span>${item.symbol}</span><span style="color:#555">${item.exchange}</span>`;
|
| 357 |
-
div.onclick = () => {
|
| 358 |
-
sRes.style.display = 'none';
|
| 359 |
-
sInp.value = item.symbol;
|
| 360 |
-
loadData(item.symbol, item.exchange);
|
| 361 |
-
};
|
| 362 |
-
sRes.appendChild(div);
|
| 363 |
-
});
|
| 364 |
-
} else {
|
| 365 |
-
sRes.style.display = 'none';
|
| 366 |
-
}
|
| 367 |
-
}, 300);
|
| 368 |
-
});
|
| 369 |
-
|
| 370 |
-
// Close search on click away
|
| 371 |
-
document.addEventListener('click', (e) => {
|
| 372 |
-
if(!e.target.closest('.search-wrap')) sRes.style.display='none';
|
| 373 |
-
});
|
| 374 |
-
|
| 375 |
-
// --- CORE LOGIC ---
|
| 376 |
-
function setMode(m) {
|
| 377 |
-
MODE = m;
|
| 378 |
-
document.getElementById('btnSplit').className = m==='split' ? 'btn active' : 'btn';
|
| 379 |
-
document.getElementById('btnNet').className = m==='net' ? 'btn active' : 'btn';
|
| 380 |
-
render();
|
| 381 |
-
}
|
| 382 |
-
|
| 383 |
-
function updateRng() {
|
| 384 |
-
document.getElementById('rngTxt').innerText = document.getElementById('rngInp').value;
|
| 385 |
-
render();
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
async function loadData(symbol, exchange) {
|
| 389 |
SYM = symbol;
|
| 390 |
EXCH = exchange;
|
| 391 |
document.getElementById('tickerName').innerText = `${SYM} (${EXCH})`;
|
| 392 |
-
|
|
|
|
|
|
|
| 393 |
|
| 394 |
try {
|
| 395 |
const res = await fetch(`/api/fetch?symbol=${SYM}&exchange=${EXCH}`);
|
| 396 |
const json = await res.json();
|
| 397 |
|
| 398 |
-
if(!json.payload || !json.payload.chain) throw new Error("No
|
| 399 |
|
| 400 |
RAW = json.payload;
|
| 401 |
SPOT = RAW.underlying?.quote?.last_price || 0;
|
|
|
|
|
|
|
| 402 |
document.getElementById('spotPrice').innerText = SPOT.toLocaleString();
|
|
|
|
|
|
|
| 403 |
|
| 404 |
-
// Populate Expiries
|
| 405 |
const expSel = document.getElementById('expSel');
|
| 406 |
-
expSel.
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
|
| 414 |
render();
|
| 415 |
|
| 416 |
} catch(e) {
|
| 417 |
-
|
|
|
|
| 418 |
} finally {
|
| 419 |
-
document.getElementById('loader').style.display = 'none';
|
| 420 |
}
|
| 421 |
}
|
| 422 |
|
|
@@ -425,6 +350,7 @@ FRONTEND_HTML = """
|
|
| 425 |
|
| 426 |
const expIdx = document.getElementById('expSel').value;
|
| 427 |
const range = parseInt(document.getElementById('rngInp').value);
|
|
|
|
| 428 |
const chain = RAW.chain[expIdx].options;
|
| 429 |
|
| 430 |
// Find ATM
|
|
@@ -434,7 +360,6 @@ FRONTEND_HTML = """
|
|
| 434 |
if(diff < min) { min = diff; atmIdx = i; }
|
| 435 |
});
|
| 436 |
|
| 437 |
-
// Slice
|
| 438 |
const start = Math.max(0, atmIdx - range);
|
| 439 |
const end = Math.min(chain.length, atmIdx + range + 1);
|
| 440 |
const view = chain.slice(start, end);
|
|
@@ -443,24 +368,56 @@ FRONTEND_HTML = """
|
|
| 443 |
|
| 444 |
view.forEach(o => {
|
| 445 |
labels.push(o.strike_price);
|
| 446 |
-
|
| 447 |
-
// GEX Calc
|
| 448 |
-
// Using API Gamma directly. Factor = Gamma * OI * Spot
|
| 449 |
const cG = (o.call?.greeks?.gamma || 0) * (o.call?.quote?.open_interest || 0) * SPOT;
|
| 450 |
const pG = (o.put?.greeks?.gamma || 0) * (o.put?.quote?.open_interest || 0) * SPOT;
|
| 451 |
|
| 452 |
if(MODE === 'split') {
|
| 453 |
-
calls.push(cG);
|
| 454 |
-
puts.push(-pG);
|
| 455 |
} else {
|
| 456 |
-
nets.push(cG - pG);
|
| 457 |
}
|
| 458 |
});
|
| 459 |
|
| 460 |
-
// Draw
|
| 461 |
-
if(CHART) CHART.destroy();
|
| 462 |
const ctx = document.getElementById('chart').getContext('2d');
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
let datasets = [];
|
| 465 |
if(MODE === 'split') {
|
| 466 |
datasets = [
|
|
@@ -482,6 +439,7 @@ FRONTEND_HTML = """
|
|
| 482 |
options: {
|
| 483 |
responsive: true,
|
| 484 |
maintainAspectRatio: false,
|
|
|
|
| 485 |
interaction: { mode: 'index', intersect: false },
|
| 486 |
plugins: {
|
| 487 |
legend: { display: false },
|
|
@@ -489,9 +447,7 @@ FRONTEND_HTML = """
|
|
| 489 |
backgroundColor: '#27272a',
|
| 490 |
titleColor: '#fff',
|
| 491 |
bodyFont: { family: 'JetBrains Mono' },
|
| 492 |
-
callbacks: {
|
| 493 |
-
label: (c) => ` ${c.dataset.label}: ${(Math.abs(c.raw)/100000).toFixed(2)} L`
|
| 494 |
-
}
|
| 495 |
},
|
| 496 |
annotation: {
|
| 497 |
annotations: {
|
|
@@ -523,6 +479,50 @@ FRONTEND_HTML = """
|
|
| 523 |
}
|
| 524 |
return strikes.length/2;
|
| 525 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
</script>
|
| 527 |
</body>
|
| 528 |
</html>
|
|
|
|
| 4 |
import threading
|
| 5 |
import os
|
| 6 |
import time
|
| 7 |
+
from datetime import datetime
|
| 8 |
from fastapi import FastAPI, BackgroundTasks
|
| 9 |
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
|
| 12 |
# ==========================================
|
| 13 |
+
# 1. CONFIGURATION
|
| 14 |
# ==========================================
|
| 15 |
app = FastAPI()
|
| 16 |
|
|
|
|
| 21 |
allow_headers=["*"],
|
| 22 |
)
|
| 23 |
|
|
|
|
| 24 |
SYMBOL_FILE = "symbols.json"
|
| 25 |
|
| 26 |
+
# Default Cache
|
| 27 |
SYMBOL_CACHE = [
|
| 28 |
{"symbol": "NIFTY", "name": "Nifty 50", "exchange": "NSE"},
|
| 29 |
{"symbol": "BANKNIFTY", "name": "Nifty Bank", "exchange": "NSE"},
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
# ==========================================
|
| 42 |
+
# 2. BACKEND LOGIC
|
| 43 |
# ==========================================
|
| 44 |
def fetch_master_list():
|
|
|
|
|
|
|
|
|
|
| 45 |
global SYMBOL_CACHE
|
|
|
|
|
|
|
| 46 |
temp_list = []
|
|
|
|
|
|
|
| 47 |
for i in range(65, 91):
|
| 48 |
letter = chr(i)
|
| 49 |
try:
|
|
|
|
| 54 |
results = data.get("payload", {}).get("results", [])
|
| 55 |
for item in results:
|
| 56 |
details = item.get("item", {})
|
|
|
|
| 57 |
if details.get("has_options_chain") or details.get("symbol") in ["NIFTY", "BANKNIFTY"]:
|
| 58 |
temp_list.append({
|
| 59 |
"symbol": details.get("symbol"),
|
| 60 |
"name": details.get("name"),
|
| 61 |
"exchange": details.get("exchange")
|
| 62 |
})
|
|
|
|
| 63 |
time.sleep(0.1)
|
| 64 |
+
except: pass
|
|
|
|
| 65 |
|
|
|
|
| 66 |
unique = {v['symbol']: v for v in temp_list}.values()
|
|
|
|
| 67 |
if len(unique) > 0:
|
| 68 |
SYMBOL_CACHE = list(unique)
|
|
|
|
| 69 |
with open(SYMBOL_FILE, "w") as f:
|
| 70 |
json.dump(SYMBOL_CACHE, f)
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
def load_startup_data():
|
|
|
|
|
|
|
|
|
|
| 73 |
global SYMBOL_CACHE
|
|
|
|
|
|
|
|
|
|
| 74 |
if os.path.exists(SYMBOL_FILE):
|
| 75 |
try:
|
|
|
|
| 76 |
file_time = os.path.getmtime(SYMBOL_FILE)
|
| 77 |
+
if (time.time() - file_time) < 86400:
|
| 78 |
with open(SYMBOL_FILE, "r") as f:
|
| 79 |
data = json.load(f)
|
| 80 |
+
if len(data) > 5: SYMBOL_CACHE = data
|
| 81 |
+
return
|
| 82 |
+
except: pass
|
| 83 |
+
threading.Thread(target=fetch_master_list).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
@app.on_event("startup")
|
| 86 |
def startup_event():
|
| 87 |
load_startup_data()
|
| 88 |
|
| 89 |
# ==========================================
|
| 90 |
+
# 3. ROUTES
|
| 91 |
# ==========================================
|
| 92 |
|
| 93 |
@app.get("/")
|
|
|
|
| 96 |
|
| 97 |
@app.get("/api/search")
|
| 98 |
def search(q: str):
|
|
|
|
|
|
|
|
|
|
| 99 |
q = q.upper()
|
|
|
|
| 100 |
res = []
|
| 101 |
for s in SYMBOL_CACHE:
|
| 102 |
if q in s['symbol'] or q in s['name'].upper():
|
| 103 |
res.append(s)
|
| 104 |
+
if len(res) >= 15: break
|
| 105 |
return JSONResponse(res)
|
| 106 |
|
| 107 |
@app.get("/api/reload_db")
|
| 108 |
def force_reload(background_tasks: BackgroundTasks):
|
|
|
|
|
|
|
|
|
|
| 109 |
background_tasks.add_task(fetch_master_list)
|
| 110 |
+
return {"status": "started"}
|
| 111 |
|
| 112 |
@app.get("/api/db_status")
|
| 113 |
def db_status():
|
|
|
|
| 115 |
|
| 116 |
@app.get("/api/fetch")
|
| 117 |
def fetch_data(symbol: str, exchange: str):
|
| 118 |
+
# Proxy to Live API
|
|
|
|
|
|
|
| 119 |
url = "https://gocharting.com/api/options/chain"
|
| 120 |
params = {
|
| 121 |
"exchange": exchange,
|
|
|
|
| 125 |
"quote_chain": "true"
|
| 126 |
}
|
| 127 |
try:
|
| 128 |
+
r = requests.get(url, headers=HEADERS, params=params, timeout=8)
|
| 129 |
return JSONResponse(r.json())
|
| 130 |
except Exception as e:
|
| 131 |
return JSONResponse({"status": "error", "msg": str(e)}, status_code=500)
|
|
|
|
| 138 |
<html lang="en">
|
| 139 |
<head>
|
| 140 |
<meta charset="UTF-8">
|
| 141 |
+
<title>GEX LIVE</title>
|
| 142 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 143 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=JetBrains+Mono:wght@400;800&display=swap" rel="stylesheet">
|
| 144 |
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 145 |
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
|
| 146 |
<style>
|
| 147 |
:root {
|
| 148 |
+
--bg: #09090b; --panel: #18181b; --border: #27272a;
|
| 149 |
+
--text: #e4e4e7; --text-dim: #71717a; --accent: #6366f1;
|
| 150 |
+
--call: #ef4444; --put: #10b981; --spot: #3b82f6;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
}
|
| 152 |
body { margin:0; background:var(--bg); color:var(--text); font-family:'Inter'; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
|
| 153 |
|
| 154 |
/* HEADER */
|
| 155 |
header { height:55px; background:var(--panel); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; justify-content:space-between; }
|
| 156 |
+
.logo { font-family:'JetBrains Mono'; font-weight:800; font-size:18px; }
|
| 157 |
.logo span { color:var(--accent); }
|
|
|
|
| 158 |
.search-wrap { position:relative; width:260px; }
|
| 159 |
.search-inp { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px 12px; border-radius:6px; font-family:'JetBrains Mono'; font-size:12px; }
|
| 160 |
.search-inp:focus { border-color:var(--accent); outline:none; }
|
| 161 |
+
.results { position:absolute; top:110%; left:0; width:100%; background:var(--panel); border:1px solid var(--border); z-index:100; max-height:300px; overflow-y:auto; display:none; }
|
| 162 |
+
.res-item { padding:8px; border-bottom:1px solid var(--border); cursor:pointer; font-size:12px; display:flex; justify-content:space-between; }
|
| 163 |
.res-item:hover { background:var(--border); }
|
| 164 |
|
| 165 |
+
/* MAIN LAYOUT */
|
| 166 |
.layout { display:flex; flex:1; height:calc(100vh - 55px); }
|
| 167 |
.sidebar { width:260px; background:#0c0c0e; border-right:1px solid var(--border); padding:20px; display:flex; flex-direction:column; gap:20px; flex-shrink:0; }
|
| 168 |
.content { flex:1; position:relative; display:flex; flex-direction:column; padding:20px; }
|
| 169 |
|
| 170 |
+
/* CARDS & CONTROLS */
|
| 171 |
.card { background:var(--panel); border:1px solid var(--border); padding:15px; border-radius:6px; }
|
| 172 |
.lbl { font-size:10px; font-weight:700; color:var(--text-dim); text-transform:uppercase; margin-bottom:5px; }
|
| 173 |
.val { font-family:'JetBrains Mono'; font-size:22px; font-weight:700; }
|
|
|
|
| 174 |
select, input[type=range] { width:100%; background:#000; border:1px solid var(--border); color:#fff; padding:8px; border-radius:4px; margin-top:5px; }
|
|
|
|
| 175 |
.btn-group { display:flex; background:#000; border:1px solid var(--border); padding:2px; border-radius:4px; margin-top:5px; }
|
| 176 |
.btn { flex:1; text-align:center; padding:6px; font-size:10px; font-weight:600; cursor:pointer; color:var(--text-dim); border-radius:2px; }
|
| 177 |
.btn.active { background:var(--border); color:#fff; }
|
| 178 |
|
| 179 |
+
/* LIVE TOGGLE */
|
| 180 |
+
.live-row { display:flex; align-items:center; justify-content:space-between; background:rgba(0,0,0,0.3); padding:10px; border-radius:6px; border:1px solid var(--border); }
|
| 181 |
+
.toggle-sw { position:relative; display:inline-block; width:34px; height:20px; }
|
| 182 |
+
.toggle-sw input { opacity:0; width:0; height:0; }
|
| 183 |
+
.slider { position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background-color:#333; transition:.4s; border-radius:20px; }
|
| 184 |
+
.slider:before { position:absolute; content:""; height:14px; width:14px; left:3px; bottom:3px; background-color:white; transition:.4s; border-radius:50%; }
|
| 185 |
+
input:checked + .slider { background-color:var(--accent); }
|
| 186 |
+
input:checked + .slider:before { transform:translateX(14px); }
|
| 187 |
+
|
| 188 |
+
.live-dot { width:8px; height:8px; background:#333; border-radius:50%; margin-right:5px; }
|
| 189 |
+
.live-dot.on { background:#10b981; box-shadow:0 0 8px #10b981; animation:pulse 1.5s infinite; }
|
| 190 |
+
@keyframes pulse { 0% { opacity:1; } 50% { opacity:0.5; } 100% { opacity:1; } }
|
| 191 |
+
|
| 192 |
+
/* CHART */
|
| 193 |
.chart-box { flex:1; position:relative; min-height:0; border:1px solid var(--border); background:#000; border-radius:6px; padding:10px; }
|
| 194 |
+
.loader { position:absolute; inset:0; background:rgba(9,9,11,0.85); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:50; }
|
|
|
|
|
|
|
| 195 |
.spin { width:30px; height:30px; border:3px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:rot 1s linear infinite; }
|
| 196 |
@keyframes rot { to { transform:rotate(360deg); } }
|
| 197 |
</style>
|
|
|
|
| 199 |
<body>
|
| 200 |
|
| 201 |
<header>
|
| 202 |
+
<div class="logo">GEX<span>LIVE</span></div>
|
| 203 |
<div class="search-wrap">
|
| 204 |
+
<input type="text" class="search-inp" id="sInp" placeholder="Search..." autocomplete="off">
|
| 205 |
<div class="results" id="sRes"></div>
|
| 206 |
</div>
|
| 207 |
</header>
|
|
|
|
| 212 |
<div class="card" style="border-left:3px solid var(--spot);">
|
| 213 |
<div class="lbl">Spot Price</div>
|
| 214 |
<div class="val" id="spotPrice">---</div>
|
| 215 |
+
<div style="font-size:10px; color:var(--text-dim); margin-top:4px; display:flex; justify-content:space-between;">
|
| 216 |
+
<span id="tickerName">BANKNIFTY</span>
|
| 217 |
+
<span id="lastUpd">--:--:--</span>
|
| 218 |
+
</div>
|
| 219 |
+
</div>
|
| 220 |
+
|
| 221 |
+
<!-- LIVE SWITCH -->
|
| 222 |
+
<div class="live-row">
|
| 223 |
+
<div style="display:flex; align-items:center;">
|
| 224 |
+
<div class="live-dot" id="liveDot"></div>
|
| 225 |
+
<div style="font-size:11px; font-weight:700;">AUTO REFRESH</div>
|
| 226 |
+
</div>
|
| 227 |
+
<label class="toggle-sw">
|
| 228 |
+
<input type="checkbox" id="liveCheck" onchange="toggleLive()">
|
| 229 |
+
<span class="slider"></span>
|
| 230 |
+
</label>
|
| 231 |
</div>
|
| 232 |
|
| 233 |
<!-- CONTROLS -->
|
|
|
|
| 247 |
<div class="btn active" id="btnSplit" onclick="setMode('split')">SPLIT</div>
|
| 248 |
<div class="btn" id="btnNet" onclick="setMode('net')">NET</div>
|
| 249 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
</div>
|
| 251 |
|
| 252 |
+
<div style="margin-top:auto; font-size:10px; color:#555;">
|
| 253 |
+
Symbols in DB: <span id="dbCount">...</span> <br>
|
| 254 |
+
<span style="cursor:pointer; color:var(--accent);" onclick="reloadDB()">[Update List]</span>
|
|
|
|
|
|
|
| 255 |
</div>
|
| 256 |
</div>
|
| 257 |
|
| 258 |
<div class="content">
|
| 259 |
+
<div class="loader" id="loader"><div class="spin"></div></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
<div class="chart-box">
|
| 261 |
<canvas id="chart"></canvas>
|
| 262 |
</div>
|
|
|
|
| 271 |
let MODE = 'split';
|
| 272 |
let SYM = 'NIFTY';
|
| 273 |
let EXCH = 'NSE';
|
| 274 |
+
let LIVE_TIMER = null;
|
| 275 |
|
| 276 |
const sInp = document.getElementById('sInp');
|
| 277 |
const sRes = document.getElementById('sRes');
|
| 278 |
|
|
|
|
| 279 |
window.onload = () => {
|
| 280 |
updateDBStats();
|
| 281 |
+
loadData('NIFTY', 'NSE');
|
| 282 |
};
|
| 283 |
|
| 284 |
+
// --- LIVE LOGIC ---
|
| 285 |
+
function toggleLive() {
|
| 286 |
+
const isOn = document.getElementById('liveCheck').checked;
|
| 287 |
+
const dot = document.getElementById('liveDot');
|
| 288 |
+
|
| 289 |
+
if(isOn) {
|
| 290 |
+
dot.classList.add('on');
|
| 291 |
+
// Refresh every 5 seconds
|
| 292 |
+
LIVE_TIMER = setInterval(() => {
|
| 293 |
+
loadData(SYM, EXCH, true); // true = background refresh
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
}, 5000);
|
| 295 |
+
} else {
|
| 296 |
+
dot.classList.remove('on');
|
| 297 |
+
if(LIVE_TIMER) clearInterval(LIVE_TIMER);
|
| 298 |
}
|
| 299 |
}
|
| 300 |
|
| 301 |
+
// --- DATA LOADING ---
|
| 302 |
+
async function loadData(symbol, exchange, isBackground = false) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
SYM = symbol;
|
| 304 |
EXCH = exchange;
|
| 305 |
document.getElementById('tickerName').innerText = `${SYM} (${EXCH})`;
|
| 306 |
+
|
| 307 |
+
// Only show full loader if not a background update
|
| 308 |
+
if(!isBackground) document.getElementById('loader').style.display = 'flex';
|
| 309 |
|
| 310 |
try {
|
| 311 |
const res = await fetch(`/api/fetch?symbol=${SYM}&exchange=${EXCH}`);
|
| 312 |
const json = await res.json();
|
| 313 |
|
| 314 |
+
if(!json.payload || !json.payload.chain) throw new Error("No Data");
|
| 315 |
|
| 316 |
RAW = json.payload;
|
| 317 |
SPOT = RAW.underlying?.quote?.last_price || 0;
|
| 318 |
+
|
| 319 |
+
// Update UI
|
| 320 |
document.getElementById('spotPrice').innerText = SPOT.toLocaleString();
|
| 321 |
+
const now = new Date();
|
| 322 |
+
document.getElementById('lastUpd').innerText = now.toLocaleTimeString();
|
| 323 |
|
| 324 |
+
// Populate Expiries only on first load
|
| 325 |
const expSel = document.getElementById('expSel');
|
| 326 |
+
if(!isBackground || expSel.options.length === 0) {
|
| 327 |
+
const currentVal = expSel.value;
|
| 328 |
+
expSel.innerHTML = '';
|
| 329 |
+
RAW.chain.forEach((c, i) => {
|
| 330 |
+
const opt = document.createElement('option');
|
| 331 |
+
opt.value = i;
|
| 332 |
+
opt.innerText = c.expiry_date;
|
| 333 |
+
expSel.appendChild(opt);
|
| 334 |
+
});
|
| 335 |
+
if(currentVal && currentVal < RAW.chain.length) expSel.value = currentVal;
|
| 336 |
+
}
|
| 337 |
|
| 338 |
render();
|
| 339 |
|
| 340 |
} catch(e) {
|
| 341 |
+
console.error(e);
|
| 342 |
+
if(!isBackground) alert("Error fetching data");
|
| 343 |
} finally {
|
| 344 |
+
if(!isBackground) document.getElementById('loader').style.display = 'none';
|
| 345 |
}
|
| 346 |
}
|
| 347 |
|
|
|
|
| 350 |
|
| 351 |
const expIdx = document.getElementById('expSel').value;
|
| 352 |
const range = parseInt(document.getElementById('rngInp').value);
|
| 353 |
+
if(!RAW.chain[expIdx]) return;
|
| 354 |
const chain = RAW.chain[expIdx].options;
|
| 355 |
|
| 356 |
// Find ATM
|
|
|
|
| 360 |
if(diff < min) { min = diff; atmIdx = i; }
|
| 361 |
});
|
| 362 |
|
|
|
|
| 363 |
const start = Math.max(0, atmIdx - range);
|
| 364 |
const end = Math.min(chain.length, atmIdx + range + 1);
|
| 365 |
const view = chain.slice(start, end);
|
|
|
|
| 368 |
|
| 369 |
view.forEach(o => {
|
| 370 |
labels.push(o.strike_price);
|
|
|
|
|
|
|
|
|
|
| 371 |
const cG = (o.call?.greeks?.gamma || 0) * (o.call?.quote?.open_interest || 0) * SPOT;
|
| 372 |
const pG = (o.put?.greeks?.gamma || 0) * (o.put?.quote?.open_interest || 0) * SPOT;
|
| 373 |
|
| 374 |
if(MODE === 'split') {
|
| 375 |
+
calls.push(cG);
|
| 376 |
+
puts.push(-pG);
|
| 377 |
} else {
|
| 378 |
+
nets.push(cG - pG);
|
| 379 |
}
|
| 380 |
});
|
| 381 |
|
|
|
|
|
|
|
| 382 |
const ctx = document.getElementById('chart').getContext('2d');
|
| 383 |
+
|
| 384 |
+
// SMART UPDATE (Prevents flashing)
|
| 385 |
+
if(CHART) {
|
| 386 |
+
CHART.data.labels = labels;
|
| 387 |
+
CHART.options.plugins.annotation.annotations.line1.value = getSpotX(labels, SPOT);
|
| 388 |
+
CHART.options.plugins.annotation.annotations.line1.label.content = `SPOT: ${SPOT}`;
|
| 389 |
+
|
| 390 |
+
if(MODE === 'split') {
|
| 391 |
+
CHART.data.datasets[0].data = calls;
|
| 392 |
+
CHART.data.datasets[0].label = 'Resistance (Call)';
|
| 393 |
+
CHART.data.datasets[0].backgroundColor = '#ef4444';
|
| 394 |
+
CHART.data.datasets[1].data = puts;
|
| 395 |
+
CHART.data.datasets[1].label = 'Support (Put)';
|
| 396 |
+
CHART.data.datasets[1].backgroundColor = '#10b981';
|
| 397 |
+
// Ensure 2 datasets exist
|
| 398 |
+
if(CHART.data.datasets.length < 2) {
|
| 399 |
+
// If switching from NET to SPLIT, we might need to recreate logic or just push
|
| 400 |
+
CHART.destroy(); // Easier to destroy when switching modes
|
| 401 |
+
createChart(ctx, labels, calls, puts, nets);
|
| 402 |
+
return;
|
| 403 |
+
}
|
| 404 |
+
} else {
|
| 405 |
+
CHART.data.datasets[0].data = nets;
|
| 406 |
+
CHART.data.datasets[0].label = 'Net GEX';
|
| 407 |
+
CHART.data.datasets[0].backgroundColor = nets.map(v => v >= 0 ? '#ef4444' : '#10b981');
|
| 408 |
+
if(CHART.data.datasets.length > 1) {
|
| 409 |
+
CHART.destroy();
|
| 410 |
+
createChart(ctx, labels, calls, puts, nets);
|
| 411 |
+
return;
|
| 412 |
+
}
|
| 413 |
+
}
|
| 414 |
+
CHART.update('none'); // Update without animation for smooth live feed
|
| 415 |
+
} else {
|
| 416 |
+
createChart(ctx, labels, calls, puts, nets);
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
function createChart(ctx, labels, calls, puts, nets) {
|
| 421 |
let datasets = [];
|
| 422 |
if(MODE === 'split') {
|
| 423 |
datasets = [
|
|
|
|
| 439 |
options: {
|
| 440 |
responsive: true,
|
| 441 |
maintainAspectRatio: false,
|
| 442 |
+
animation: false, // Disable initial animation for snappiness
|
| 443 |
interaction: { mode: 'index', intersect: false },
|
| 444 |
plugins: {
|
| 445 |
legend: { display: false },
|
|
|
|
| 447 |
backgroundColor: '#27272a',
|
| 448 |
titleColor: '#fff',
|
| 449 |
bodyFont: { family: 'JetBrains Mono' },
|
| 450 |
+
callbacks: { label: (c) => ` ${c.dataset.label}: ${(Math.abs(c.raw)/100000).toFixed(2)} L` }
|
|
|
|
|
|
|
| 451 |
},
|
| 452 |
annotation: {
|
| 453 |
annotations: {
|
|
|
|
| 479 |
}
|
| 480 |
return strikes.length/2;
|
| 481 |
}
|
| 482 |
+
|
| 483 |
+
// Search Logic
|
| 484 |
+
let debounce;
|
| 485 |
+
sInp.addEventListener('input', (e) => {
|
| 486 |
+
clearTimeout(debounce);
|
| 487 |
+
const q = e.target.value;
|
| 488 |
+
if(q.length < 1) { sRes.style.display='none'; return; }
|
| 489 |
+
debounce = setTimeout(async () => {
|
| 490 |
+
const r = await fetch(`/api/search?q=${q}`);
|
| 491 |
+
const data = await r.json();
|
| 492 |
+
sRes.innerHTML = '';
|
| 493 |
+
if(data.length > 0) {
|
| 494 |
+
sRes.style.display = 'block';
|
| 495 |
+
data.forEach(item => {
|
| 496 |
+
const div = document.createElement('div');
|
| 497 |
+
div.className = 'res-item';
|
| 498 |
+
div.innerHTML = `<span>${item.symbol}</span><span style="color:#555">${item.exchange}</span>`;
|
| 499 |
+
div.onclick = () => {
|
| 500 |
+
sRes.style.display = 'none';
|
| 501 |
+
sInp.value = item.symbol;
|
| 502 |
+
// Reset Live mode to avoid polling wrong symbol
|
| 503 |
+
if(document.getElementById('liveCheck').checked) {
|
| 504 |
+
toggleLive(); document.getElementById('liveCheck').click();
|
| 505 |
+
}
|
| 506 |
+
loadData(item.symbol, item.exchange);
|
| 507 |
+
};
|
| 508 |
+
sRes.appendChild(div);
|
| 509 |
+
});
|
| 510 |
+
} else sRes.style.display = 'none';
|
| 511 |
+
}, 300);
|
| 512 |
+
});
|
| 513 |
+
document.addEventListener('click', (e) => { if(!e.target.closest('.search-wrap')) sRes.style.display='none'; });
|
| 514 |
+
|
| 515 |
+
// Helper
|
| 516 |
+
function setMode(m) {
|
| 517 |
+
MODE = m;
|
| 518 |
+
document.getElementById('btnSplit').className = m==='split' ? 'btn active' : 'btn';
|
| 519 |
+
document.getElementById('btnNet').className = m==='net' ? 'btn active' : 'btn';
|
| 520 |
+
if(CHART) CHART.destroy(); CHART=null; // Force rebuild on mode switch
|
| 521 |
+
render();
|
| 522 |
+
}
|
| 523 |
+
function updateRng() { document.getElementById('rngTxt').innerText = document.getElementById('rngInp').value; render(); }
|
| 524 |
+
async function updateDBStats() { try { const r = await fetch('/api/db_status'); const d = await r.json(); document.getElementById('dbCount').innerText = d.count; } catch {} }
|
| 525 |
+
async function reloadDB() { fetch('/api/reload_db'); alert("Update started."); }
|
| 526 |
</script>
|
| 527 |
</body>
|
| 528 |
</html>
|