Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ==============================================================================
|
| 2 |
-
# PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION
|
| 3 |
# ==============================================================================
|
| 4 |
|
| 5 |
import os
|
|
@@ -189,7 +189,7 @@ def update_subscriptions(symbols_list):
|
|
| 189 |
for i in range(0, len(to_remove), 50): FS.unsubscribe(symbols=to_remove[i:i+50], data_type="SymbolUpdate")
|
| 190 |
|
| 191 |
# ==========================================
|
| 192 |
-
# 4. API & AUTH ROUTES
|
| 193 |
# ==========================================
|
| 194 |
def get_fyers_session():
|
| 195 |
return fyersModel.SessionModel(
|
|
@@ -205,10 +205,10 @@ def home(access_token: Optional[str] = Cookie(None)):
|
|
| 205 |
if not access_token:
|
| 206 |
auth_url = get_fyers_session().generate_authcode()
|
| 207 |
return HTMLResponse(f"""
|
| 208 |
-
<body style="background:#
|
| 209 |
-
<div style="background:#
|
| 210 |
-
<h2 style="margin-top:0;">Fyers Authentication Required</h2>
|
| 211 |
-
<a href="{auth_url}" style="display:inline-block; background:#
|
| 212 |
</div>
|
| 213 |
</body>
|
| 214 |
""")
|
|
@@ -267,7 +267,6 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 267 |
|
| 268 |
try:
|
| 269 |
while True:
|
| 270 |
-
# Check for incoming commands without blocking the broadcast loop
|
| 271 |
try:
|
| 272 |
data = await asyncio.wait_for(websocket.receive_json(), timeout=0.3)
|
| 273 |
action = data.get("action")
|
|
@@ -285,7 +284,6 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 285 |
except asyncio.TimeoutError:
|
| 286 |
pass
|
| 287 |
|
| 288 |
-
# Push Live State Snapshot to frontend
|
| 289 |
if SOCKET_RUNNING and active_client_symbols:
|
| 290 |
payload = {sym: STATE_MEMORY[sym] for sym in active_client_symbols if sym in STATE_MEMORY}
|
| 291 |
if payload:
|
|
@@ -295,7 +293,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|
| 295 |
print("[Client WS] Disconnected")
|
| 296 |
|
| 297 |
# ==========================================
|
| 298 |
-
# 6. FRONTEND UI (WORLD'S BEST
|
| 299 |
# ==========================================
|
| 300 |
HTML_TEMPLATE = """
|
| 301 |
<!DOCTYPE html>
|
|
@@ -303,80 +301,87 @@ HTML_TEMPLATE = """
|
|
| 303 |
<head>
|
| 304 |
<meta charset="UTF-8">
|
| 305 |
<title>Pro Live Options Chain</title>
|
| 306 |
-
<meta name="viewport" content="width=
|
| 307 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;700;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
| 308 |
<style>
|
| 309 |
:root {
|
| 310 |
-
--bg: #
|
| 311 |
-
--text-main: #
|
| 312 |
-
--accent: #
|
| 313 |
-
--buy-bg:
|
| 314 |
-
--atm-bg:
|
| 315 |
}
|
| 316 |
* { box-sizing: border-box; }
|
| 317 |
body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
| 318 |
|
| 319 |
/* Top Navigation */
|
| 320 |
-
.top-nav { padding: 0 20px; background: var(--panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 70px; z-index: 10; }
|
| 321 |
-
.btn { background: var(--panel); border: 1px solid var(--border); padding: 8px 16px; font-size: 13px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; color: var(--text-main); }
|
| 322 |
-
.btn:hover { border-color: var(--accent); color: var(--accent); background:
|
| 323 |
|
| 324 |
-
.btn-control { border:none; padding: 10px 20px; font-size: 14px; border-radius: 6px; color: white; transition: 0.3s; box-shadow: 0
|
| 325 |
.btn-control.playing { background: var(--green); }
|
|
|
|
| 326 |
.btn-control.stopped { background: var(--red); }
|
|
|
|
| 327 |
|
| 328 |
-
.search-box { position: relative; width:
|
| 329 |
-
.search-input { width: 100%; padding:
|
| 330 |
-
.search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(
|
| 331 |
-
.search-dropdown { position: absolute; top: 110%; left: 0; width: 100%; background: var(--panel); border: 1px solid var(--border); max-height: 300px; overflow-y: auto; display: none; border-radius: 6px; z-index: 100; box-shadow: 0 10px
|
| 332 |
-
.search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid
|
| 333 |
-
.search-item:hover { background:
|
| 334 |
|
| 335 |
/* Chain Layout */
|
| 336 |
-
.chain-container { flex: 1; display: flex; flex-direction: column; background: var(--
|
|
|
|
| 337 |
|
| 338 |
-
/* 7 Column Layout */
|
| 339 |
-
.chain-
|
| 340 |
-
|
| 341 |
-
.
|
| 342 |
-
|
| 343 |
-
.
|
| 344 |
-
|
| 345 |
-
.
|
|
|
|
|
|
|
| 346 |
.row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
|
| 347 |
|
| 348 |
-
.cell { position: relative; height: 100%; display: flex; align-items: center;
|
| 349 |
.cell.call-col { justify-content: flex-end; }
|
| 350 |
.cell.put-col { justify-content: flex-start; }
|
| 351 |
-
.cell.strike { justify-content: center; background:
|
| 352 |
|
| 353 |
-
.val-txt { z-index: 2;
|
| 354 |
-
.val-buy { color:
|
| 355 |
-
.val-sell { color:
|
| 356 |
|
| 357 |
/* Depth Visualizer Bars */
|
| 358 |
-
.bar-bg { position: absolute; top:
|
| 359 |
-
.bar-call { right:
|
| 360 |
-
.bar-put { left:
|
| 361 |
|
| 362 |
/* Modals & Overlays */
|
| 363 |
-
.loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:var(--
|
| 364 |
-
.spinner { width: 40px; height: 40px; border: 3px solid
|
| 365 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 366 |
|
| 367 |
-
.modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.
|
| 368 |
-
.modal-box { background: var(--panel); width: 420px; border-radius: 12px; border: 1px solid var(--border); padding: 25px; display:flex; flex-direction:column; }
|
| 369 |
.modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
|
| 370 |
-
.opt-btn { padding: 12px; text-align: center; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight:700; font-size:13px; transition:0.2s; background:#
|
| 371 |
-
.opt-btn:hover { background:
|
| 372 |
-
.opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
| 373 |
|
| 374 |
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 375 |
::-webkit-scrollbar-track { background: var(--bg); }
|
| 376 |
-
::-webkit-scrollbar-thumb { background:
|
| 377 |
-
::-webkit-scrollbar-thumb:hover { background:
|
| 378 |
|
| 379 |
-
.live-dot { display:inline-block; width:10px; height:10px; background:
|
|
|
|
|
|
|
| 380 |
</style>
|
| 381 |
</head>
|
| 382 |
<body>
|
|
@@ -388,7 +393,7 @@ HTML_TEMPLATE = """
|
|
| 388 |
|
| 389 |
<div class="modal" id="expModal">
|
| 390 |
<div class="modal-box">
|
| 391 |
-
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid var(--border); padding-bottom:15px; color:
|
| 392 |
<span>SELECT EXPIRY</span>
|
| 393 |
<span style="cursor:pointer; color:var(--text-sub);" onclick="document.getElementById('expModal').style.display='none'">✕</span>
|
| 394 |
</div>
|
|
@@ -398,7 +403,7 @@ HTML_TEMPLATE = """
|
|
| 398 |
|
| 399 |
<div class="top-nav">
|
| 400 |
<div style="display:flex; align-items:center;">
|
| 401 |
-
<div style="font-size:20px; font-weight:800; color:
|
| 402 |
<span style="color:var(--accent)">PRO</span>CHAIN<span style="font-size:10px; vertical-align:top; color:var(--green)">LIVE</span>
|
| 403 |
</div>
|
| 404 |
<div class="search-box">
|
|
@@ -414,27 +419,31 @@ HTML_TEMPLATE = """
|
|
| 414 |
<div style="display:flex; gap:20px; align-items:center;">
|
| 415 |
<div style="text-align:right;">
|
| 416 |
<div style="font-size:11px; font-weight:800; color:var(--text-sub); letter-spacing:1px;" id="spotLabel">SPOT PRICE</div>
|
| 417 |
-
<div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:19px; color:var(--
|
| 418 |
</div>
|
| 419 |
<button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
|
| 420 |
-
<span style="color:var(--text-sub)">EXP:</span> <span id="selExpTxt" style="color:
|
| 421 |
</button>
|
| 422 |
-
<button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0; background:transparent;">LOGOUT</button>
|
| 423 |
</div>
|
| 424 |
</div>
|
| 425 |
|
| 426 |
<div class="chain-container">
|
| 427 |
-
<div class="
|
| 428 |
-
<div class="
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
</div>
|
| 439 |
</div>
|
| 440 |
|
|
@@ -509,35 +518,49 @@ HTML_TEMPLATE = """
|
|
| 509 |
async function doSearch(q) {
|
| 510 |
const d = document.getElementById('searchRes');
|
| 511 |
if(q.length<2) { d.style.display='none'; return; }
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
| 515 |
}
|
| 516 |
|
| 517 |
async function loadSymbol(root) {
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
}
|
| 542 |
|
| 543 |
function selectExp(ts) {
|
|
@@ -545,15 +568,19 @@ HTML_TEMPLATE = """
|
|
| 545 |
document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
|
| 546 |
document.getElementById('expModal').style.display='none';
|
| 547 |
document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active'));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
buildChain();
|
| 549 |
}
|
| 550 |
|
| 551 |
function buildChain() {
|
| 552 |
OPT_SYMS = CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
|
| 553 |
-
const ks =
|
| 554 |
-
if(ks.length===0) { document.getElementById('chainBody').innerHTML='<div style="padding:
|
| 555 |
|
| 556 |
-
// Construct
|
| 557 |
document.getElementById('chainBody').innerHTML = ks.map(k=>`
|
| 558 |
<div class="row" id="row-${k}" data-k="${k}">
|
| 559 |
<div class="cell call-col"><div class="bar-bg bar-call" style="background:var(--buy-bg)" id="cb-bar-${k}"></div><span class="val-txt val-buy" id="cb-${k}">-</span></div>
|
|
@@ -573,7 +600,7 @@ HTML_TEMPLATE = """
|
|
| 573 |
|
| 574 |
// --- REALTIME RENDER ENGINE ---
|
| 575 |
function renderLiveData(data) {
|
| 576 |
-
if(data[SPOT_SYM] && data[SPOT_SYM].ltp > 0) {
|
| 577 |
const newSpot = data[SPOT_SYM].ltp;
|
| 578 |
const spotEl = document.getElementById('spotPrice');
|
| 579 |
if(newSpot > SPOT_PRICE) spotEl.style.color = "var(--green)";
|
|
@@ -610,10 +637,10 @@ HTML_TEMPLATE = """
|
|
| 610 |
if(v.pltp) document.getElementById(`pltp-${k}`).innerText = v.pltp.toFixed(2);
|
| 611 |
|
| 612 |
// Update Visual Depth Bars
|
| 613 |
-
if(v.cb) document.getElementById(`cb-bar-${k}`).style.width = `${(v.cb / GLOBAL_MAX_QTY) *
|
| 614 |
-
if(v.cs) document.getElementById(`cs-bar-${k}`).style.width = `${(v.cs / GLOBAL_MAX_QTY) *
|
| 615 |
-
if(v.pb) document.getElementById(`pb-bar-${k}`).style.width = `${(v.pb / GLOBAL_MAX_QTY) *
|
| 616 |
-
if(v.ps) document.getElementById(`ps-bar-${k}`).style.width = `${(v.ps / GLOBAL_MAX_QTY) *
|
| 617 |
}
|
| 618 |
|
| 619 |
// 3. Highlight ATM & Auto-Scroll (Only once per chain load)
|
|
@@ -623,9 +650,15 @@ HTML_TEMPLATE = """
|
|
| 623 |
const atmK = ks.reduce((prev, curr) => Math.abs(curr - SPOT_PRICE) < Math.abs(prev - SPOT_PRICE) ? curr : prev);
|
| 624 |
document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
|
| 625 |
const atmRow = document.getElementById(`row-${atmK}`);
|
|
|
|
| 626 |
if(atmRow) {
|
| 627 |
atmRow.classList.add('atm');
|
| 628 |
-
if(!window.scrolled) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
}
|
| 630 |
}
|
| 631 |
}
|
|
|
|
| 1 |
# ==============================================================================
|
| 2 |
+
# PRO OPTIONS CHAIN V4 - REAL-TIME WEBSOCKET EDITION (LIGHT MODE)
|
| 3 |
# ==============================================================================
|
| 4 |
|
| 5 |
import os
|
|
|
|
| 189 |
for i in range(0, len(to_remove), 50): FS.unsubscribe(symbols=to_remove[i:i+50], data_type="SymbolUpdate")
|
| 190 |
|
| 191 |
# ==========================================
|
| 192 |
+
# 4. API & AUTH ROUTES
|
| 193 |
# ==========================================
|
| 194 |
def get_fyers_session():
|
| 195 |
return fyersModel.SessionModel(
|
|
|
|
| 205 |
if not access_token:
|
| 206 |
auth_url = get_fyers_session().generate_authcode()
|
| 207 |
return HTMLResponse(f"""
|
| 208 |
+
<body style="background:#f8fafc; display:flex; justify-content:center; align-items:center; height:100vh; font-family:sans-serif; margin:0;">
|
| 209 |
+
<div style="background:#ffffff; padding:40px; border-radius:12px; text-align:center; border:1px solid #e2e8f0; box-shadow: 0 10px 25px rgba(0,0,0,0.05);">
|
| 210 |
+
<h2 style="margin-top:0; color:#0f172a;">Fyers Authentication Required</h2>
|
| 211 |
+
<a href="{auth_url}" style="display:inline-block; background:#2563eb; color:white; padding:12px 25px; text-decoration:none; border-radius:6px; font-weight:bold; margin-top: 15px; transition:0.2s;">LOGIN WITH FYERS</a>
|
| 212 |
</div>
|
| 213 |
</body>
|
| 214 |
""")
|
|
|
|
| 267 |
|
| 268 |
try:
|
| 269 |
while True:
|
|
|
|
| 270 |
try:
|
| 271 |
data = await asyncio.wait_for(websocket.receive_json(), timeout=0.3)
|
| 272 |
action = data.get("action")
|
|
|
|
| 284 |
except asyncio.TimeoutError:
|
| 285 |
pass
|
| 286 |
|
|
|
|
| 287 |
if SOCKET_RUNNING and active_client_symbols:
|
| 288 |
payload = {sym: STATE_MEMORY[sym] for sym in active_client_symbols if sym in STATE_MEMORY}
|
| 289 |
if payload:
|
|
|
|
| 293 |
print("[Client WS] Disconnected")
|
| 294 |
|
| 295 |
# ==========================================
|
| 296 |
+
# 6. FRONTEND UI (WORLD'S BEST LIGHT MODE)
|
| 297 |
# ==========================================
|
| 298 |
HTML_TEMPLATE = """
|
| 299 |
<!DOCTYPE html>
|
|
|
|
| 301 |
<head>
|
| 302 |
<meta charset="UTF-8">
|
| 303 |
<title>Pro Live Options Chain</title>
|
| 304 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 305 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;700;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
| 306 |
<style>
|
| 307 |
:root {
|
| 308 |
+
--bg: #f8fafc; --panel: #ffffff; --border: #e2e8f0;
|
| 309 |
+
--text-main: #0f172a; --text-sub: #64748b;
|
| 310 |
+
--accent: #2563eb; --green: #16a34a; --red: #dc2626;
|
| 311 |
+
--buy-bg: #dcfce7; --sell-bg: #fee2e2;
|
| 312 |
+
--atm-bg: #eff6ff; --strike-bg: #f1f5f9;
|
| 313 |
}
|
| 314 |
* { box-sizing: border-box; }
|
| 315 |
body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
| 316 |
|
| 317 |
/* Top Navigation */
|
| 318 |
+
.top-nav { padding: 0 20px; background: var(--panel); border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 70px; z-index: 10; flex-shrink:0; box-shadow: 0 2px 4px rgba(0,0,0,0.02);}
|
| 319 |
+
.btn { background: var(--panel); border: 1px solid var(--border); padding: 8px 16px; font-size: 13px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; color: var(--text-main); box-shadow: 0 1px 2px rgba(0,0,0,0.02); }
|
| 320 |
+
.btn:hover { border-color: var(--accent); color: var(--accent); background: #f0f7ff; }
|
| 321 |
|
| 322 |
+
.btn-control { border:none; padding: 10px 20px; font-size: 14px; border-radius: 6px; color: white; transition: 0.3s; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
|
| 323 |
.btn-control.playing { background: var(--green); }
|
| 324 |
+
.btn-control.playing:hover { background: #15803d; }
|
| 325 |
.btn-control.stopped { background: var(--red); }
|
| 326 |
+
.btn-control.stopped:hover { background: #b91c1c; }
|
| 327 |
|
| 328 |
+
.search-box { position: relative; width: 280px; margin-right: 20px; }
|
| 329 |
+
.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; background: #f8fafc; color: var(--text-main); transition:0.3s;}
|
| 330 |
+
.search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); background: #fff; }
|
| 331 |
+
.search-dropdown { position: absolute; top: 110%; left: 0; width: 100%; background: var(--panel); border: 1px solid var(--border); max-height: 300px; overflow-y: auto; display: none; border-radius: 6px; z-index: 100; box-shadow: 0 10px 25px rgba(0,0,0,0.1); }
|
| 332 |
+
.search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f1f5f9; font-size: 13px; font-weight: 700; color:var(--text-main); }
|
| 333 |
+
.search-item:hover { background: #f0f7ff; color: var(--accent); }
|
| 334 |
|
| 335 |
/* Chain Layout */
|
| 336 |
+
.chain-container { flex: 1; display: flex; flex-direction: column; background: var(--panel); position:relative; overflow: hidden; }
|
| 337 |
+
.scroll-wrapper { flex: 1; overflow: auto; position: relative; }
|
| 338 |
|
| 339 |
+
/* 7 Column Layout - Horizontal Min Width for responsiveness */
|
| 340 |
+
.chain-table { min-width: 950px; display: flex; flex-direction: column; }
|
| 341 |
+
|
| 342 |
+
.chain-header { display: grid; grid-template-columns: 1.2fr 1.2fr 1fr 120px 1fr 1.2fr 1.2fr; background: var(--strike-bg); border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 1px; position: sticky; top: 0; z-index: 5;}
|
| 343 |
+
.header-cell { padding: 12px 15px; border-right: 1px solid var(--border); display:flex; justify-content:center; align-items:center; }
|
| 344 |
+
.header-cell.strike { background: #e2e8f0; border-right: 1px solid var(--border); border-left: 1px solid var(--border); color:var(--text-main); }
|
| 345 |
+
|
| 346 |
+
.chain-body { display: flex; flex-direction: column; }
|
| 347 |
+
.row { display: grid; grid-template-columns: 1.2fr 1.2fr 1fr 120px 1fr 1.2fr 1.2fr; border-bottom: 1px solid var(--border); height: 42px; align-items: center; font-family: 'JetBrains Mono', monospace; font-size: 13px; transition: background 0.1s;}
|
| 348 |
+
.row:hover { background: #f8fafc; }
|
| 349 |
.row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
|
| 350 |
|
| 351 |
+
.cell { position: relative; height: 100%; display: flex; align-items: center; padding: 0 15px; font-weight: 700; border-right: 1px solid #f1f5f9; }
|
| 352 |
.cell.call-col { justify-content: flex-end; }
|
| 353 |
.cell.put-col { justify-content: flex-start; }
|
| 354 |
+
.cell.strike { justify-content: center; background: var(--strike-bg); font-size: 14px; font-weight: 800; color: var(--text-main); border-right: 1px solid var(--border); border-left: 1px solid var(--border); }
|
| 355 |
|
| 356 |
+
.val-txt { z-index: 2; }
|
| 357 |
+
.val-buy { color: #15803d; } /* Darker green for text */
|
| 358 |
+
.val-sell { color: #b91c1c; } /* Darker red for text */
|
| 359 |
|
| 360 |
/* Depth Visualizer Bars */
|
| 361 |
+
.bar-bg { position: absolute; top: 5px; bottom: 5px; z-index: 0; border-radius: 4px; transition: width 0.3s ease; opacity: 0.8;}
|
| 362 |
+
.bar-call { right: 5px; } /* Anchor to right, grows left */
|
| 363 |
+
.bar-put { left: 5px; } /* Anchor to left, grows right */
|
| 364 |
|
| 365 |
/* Modals & Overlays */
|
| 366 |
+
.loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:var(--panel); display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
|
| 367 |
+
.spinner { width: 40px; height: 40px; border: 3px solid #f1f5f9; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; }
|
| 368 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 369 |
|
| 370 |
+
.modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.4); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
|
| 371 |
+
.modal-box { background: var(--panel); width: 420px; border-radius: 12px; border: 1px solid var(--border); padding: 25px; display:flex; flex-direction:column; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); }
|
| 372 |
.modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
|
| 373 |
+
.opt-btn { padding: 12px; text-align: center; border: 1px solid var(--border); border-radius: 6px; cursor: pointer; font-weight:700; font-size:13px; transition:0.2s; background:#f8fafc; color:var(--text-sub); }
|
| 374 |
+
.opt-btn:hover { background: #f0f7ff; color: var(--accent); border-color:var(--accent); }
|
| 375 |
+
.opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2); }
|
| 376 |
|
| 377 |
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 378 |
::-webkit-scrollbar-track { background: var(--bg); }
|
| 379 |
+
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
|
| 380 |
+
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
|
| 381 |
|
| 382 |
+
.live-dot { display:inline-block; width:10px; height:10px; background:rgba(255,255,255,0.4); border-radius:50%; margin-right:8px; }
|
| 383 |
+
.playing .live-dot { background: white; box-shadow: 0 0 6px white; animation: pulse 1.5s infinite; }
|
| 384 |
+
@keyframes pulse { 0% { opacity:0.6; } 50% { opacity:1; } 100% { opacity:0.6; } }
|
| 385 |
</style>
|
| 386 |
</head>
|
| 387 |
<body>
|
|
|
|
| 393 |
|
| 394 |
<div class="modal" id="expModal">
|
| 395 |
<div class="modal-box">
|
| 396 |
+
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid var(--border); padding-bottom:15px; color:var(--text-main);">
|
| 397 |
<span>SELECT EXPIRY</span>
|
| 398 |
<span style="cursor:pointer; color:var(--text-sub);" onclick="document.getElementById('expModal').style.display='none'">✕</span>
|
| 399 |
</div>
|
|
|
|
| 403 |
|
| 404 |
<div class="top-nav">
|
| 405 |
<div style="display:flex; align-items:center;">
|
| 406 |
+
<div style="font-size:20px; font-weight:800; color:var(--text-main); margin-right:30px; letter-spacing: -1px;">
|
| 407 |
<span style="color:var(--accent)">PRO</span>CHAIN<span style="font-size:10px; vertical-align:top; color:var(--green)">LIVE</span>
|
| 408 |
</div>
|
| 409 |
<div class="search-box">
|
|
|
|
| 419 |
<div style="display:flex; gap:20px; align-items:center;">
|
| 420 |
<div style="text-align:right;">
|
| 421 |
<div style="font-size:11px; font-weight:800; color:var(--text-sub); letter-spacing:1px;" id="spotLabel">SPOT PRICE</div>
|
| 422 |
+
<div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:19px; color:var(--text-main);">0.00</div>
|
| 423 |
</div>
|
| 424 |
<button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
|
| 425 |
+
<span style="color:var(--text-sub)">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
|
| 426 |
</button>
|
| 427 |
+
<button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0; background:transparent; box-shadow:none;">LOGOUT</button>
|
| 428 |
</div>
|
| 429 |
</div>
|
| 430 |
|
| 431 |
<div class="chain-container">
|
| 432 |
+
<div class="scroll-wrapper">
|
| 433 |
+
<div class="chain-table">
|
| 434 |
+
<div class="chain-header">
|
| 435 |
+
<div class="header-cell">CALL BUY QTY</div>
|
| 436 |
+
<div class="header-cell">CALL SELL QTY</div>
|
| 437 |
+
<div class="header-cell">CALL LTP</div>
|
| 438 |
+
<div class="header-cell strike">STRIKE</div>
|
| 439 |
+
<div class="header-cell">PUT LTP</div>
|
| 440 |
+
<div class="header-cell">PUT SELL QTY</div>
|
| 441 |
+
<div class="header-cell">PUT BUY QTY</div>
|
| 442 |
+
</div>
|
| 443 |
+
<div class="chain-body" id="chainBody">
|
| 444 |
+
<div style="padding:60px; text-align:center; color:var(--text-sub); font-weight:600; font-size:14px;">SEARCH & SELECT A SYMBOL TO LOAD LIVE CHAIN</div>
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
</div>
|
| 448 |
</div>
|
| 449 |
|
|
|
|
| 518 |
async function doSearch(q) {
|
| 519 |
const d = document.getElementById('searchRes');
|
| 520 |
if(q.length<2) { d.style.display='none'; return; }
|
| 521 |
+
try {
|
| 522 |
+
const r = await fetch(`/api/search?q=${q}`); const j = await r.json();
|
| 523 |
+
d.innerHTML = j.map(i => `<div class="search-item" onclick="loadSymbol('${i.root}')">${i.display} <span style="font-size:10px; color:var(--text-sub); float:right;">${i.exch}</span></div>`).join('');
|
| 524 |
+
d.style.display='block';
|
| 525 |
+
} catch(e){}
|
| 526 |
}
|
| 527 |
|
| 528 |
async function loadSymbol(root) {
|
| 529 |
+
try {
|
| 530 |
+
document.getElementById('searchInp').value = root;
|
| 531 |
+
document.getElementById('searchRes').style.display='none';
|
| 532 |
+
document.getElementById('loaderMsg').innerText = "LOADING CHAIN...";
|
| 533 |
+
document.getElementById('loader').style.display='flex';
|
| 534 |
+
|
| 535 |
+
const r = await fetch(`/api/chain?symbol=${root}`);
|
| 536 |
+
if(!r.ok) throw new Error("Symbol not found on server");
|
| 537 |
+
|
| 538 |
+
const d = await r.json();
|
| 539 |
+
if(d.error || !d.items || d.items.length === 0) throw new Error(d.error || "No options data found for this symbol");
|
| 540 |
+
|
| 541 |
+
CURRENT_ROOT=root; CHAIN_DATA=d.items; SPOT_SYM=d.spot || ""; SPOT_PRICE=0; GLOBAL_MAX_QTY=1;
|
| 542 |
+
|
| 543 |
+
document.getElementById('spotLabel').innerText = (SPOT_SYM && SPOT_SYM.includes('INDEX')) ? "SPOT PRICE" : "FUT PRICE";
|
| 544 |
+
document.getElementById('spotPrice').innerText = "0.00";
|
| 545 |
+
document.getElementById('spotPrice').style.color = "var(--text-main)";
|
| 546 |
+
|
| 547 |
+
const opts = CHAIN_DATA.filter(x=>x.t==='CE'||x.t==='PE');
|
| 548 |
+
if(opts.length === 0) throw new Error("No Options contracts found for this symbol");
|
| 549 |
+
|
| 550 |
+
const exps =[...new Set(opts.map(i=>i.e))].sort((a,b)=>a-b);
|
| 551 |
+
const today = Math.floor(Date.now()/1000)-86400;
|
| 552 |
+
ACTIVE_EXP = exps.find(t=>t>today) || exps[0];
|
| 553 |
+
|
| 554 |
+
document.getElementById('selExpTxt').innerText = new Date(ACTIVE_EXP*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
|
| 555 |
+
document.getElementById('expGrid').innerHTML = exps.map(ts=>`<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'})}</div>`).join('');
|
| 556 |
+
|
| 557 |
+
buildChain();
|
| 558 |
+
} catch(e) {
|
| 559 |
+
console.error(e);
|
| 560 |
+
alert("Error: " + e.message);
|
| 561 |
+
} finally {
|
| 562 |
+
document.getElementById('loader').style.display='none';
|
| 563 |
+
}
|
| 564 |
}
|
| 565 |
|
| 566 |
function selectExp(ts) {
|
|
|
|
| 568 |
document.getElementById('selExpTxt').innerText = new Date(ts*1000).toLocaleDateString('en-GB',{day:'2-digit',month:'short'});
|
| 569 |
document.getElementById('expModal').style.display='none';
|
| 570 |
document.querySelectorAll('.opt-btn').forEach(b=>b.classList.remove('active'));
|
| 571 |
+
|
| 572 |
+
const b = document.querySelector(`.opt-btn[onclick="selectExp(${ts})"]`);
|
| 573 |
+
if(b) b.classList.add('active');
|
| 574 |
+
|
| 575 |
buildChain();
|
| 576 |
}
|
| 577 |
|
| 578 |
function buildChain() {
|
| 579 |
OPT_SYMS = CHAIN_DATA.filter(x=>x.e===ACTIVE_EXP&&(x.t==='CE'||x.t==='PE'));
|
| 580 |
+
const ks =[...new Set(OPT_SYMS.map(x=>x.k))].sort((a,b)=>a-b);
|
| 581 |
+
if(ks.length===0) { document.getElementById('chainBody').innerHTML='<div style="padding:40px;text-align:center; color:var(--text-sub);">NO DATA FOR EXPIRY</div>'; return; }
|
| 582 |
|
| 583 |
+
// Construct perfectly mirrored layout
|
| 584 |
document.getElementById('chainBody').innerHTML = ks.map(k=>`
|
| 585 |
<div class="row" id="row-${k}" data-k="${k}">
|
| 586 |
<div class="cell call-col"><div class="bar-bg bar-call" style="background:var(--buy-bg)" id="cb-bar-${k}"></div><span class="val-txt val-buy" id="cb-${k}">-</span></div>
|
|
|
|
| 600 |
|
| 601 |
// --- REALTIME RENDER ENGINE ---
|
| 602 |
function renderLiveData(data) {
|
| 603 |
+
if(SPOT_SYM && data[SPOT_SYM] && data[SPOT_SYM].ltp > 0) {
|
| 604 |
const newSpot = data[SPOT_SYM].ltp;
|
| 605 |
const spotEl = document.getElementById('spotPrice');
|
| 606 |
if(newSpot > SPOT_PRICE) spotEl.style.color = "var(--green)";
|
|
|
|
| 637 |
if(v.pltp) document.getElementById(`pltp-${k}`).innerText = v.pltp.toFixed(2);
|
| 638 |
|
| 639 |
// Update Visual Depth Bars
|
| 640 |
+
if(v.cb) document.getElementById(`cb-bar-${k}`).style.width = `${(v.cb / GLOBAL_MAX_QTY) * 95}%`;
|
| 641 |
+
if(v.cs) document.getElementById(`cs-bar-${k}`).style.width = `${(v.cs / GLOBAL_MAX_QTY) * 95}%`;
|
| 642 |
+
if(v.pb) document.getElementById(`pb-bar-${k}`).style.width = `${(v.pb / GLOBAL_MAX_QTY) * 95}%`;
|
| 643 |
+
if(v.ps) document.getElementById(`ps-bar-${k}`).style.width = `${(v.ps / GLOBAL_MAX_QTY) * 95}%`;
|
| 644 |
}
|
| 645 |
|
| 646 |
// 3. Highlight ATM & Auto-Scroll (Only once per chain load)
|
|
|
|
| 650 |
const atmK = ks.reduce((prev, curr) => Math.abs(curr - SPOT_PRICE) < Math.abs(prev - SPOT_PRICE) ? curr : prev);
|
| 651 |
document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
|
| 652 |
const atmRow = document.getElementById(`row-${atmK}`);
|
| 653 |
+
|
| 654 |
if(atmRow) {
|
| 655 |
atmRow.classList.add('atm');
|
| 656 |
+
if(!window.scrolled) {
|
| 657 |
+
setTimeout(() => {
|
| 658 |
+
atmRow.scrollIntoView({block:'center', behavior:'smooth'});
|
| 659 |
+
}, 50);
|
| 660 |
+
window.scrolled = true;
|
| 661 |
+
}
|
| 662 |
}
|
| 663 |
}
|
| 664 |
}
|