Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -38,7 +38,6 @@ SYSTEM_STATUS = {
|
|
| 38 |
UPDATE_LOCK = threading.Lock()
|
| 39 |
|
| 40 |
# Manual mapping for Index Spot symbols
|
| 41 |
-
# Added BSE Indices (SENSEX, BANKEX)
|
| 42 |
INDEX_MAP = {
|
| 43 |
"NIFTY": "NSE:NIFTY50-INDEX",
|
| 44 |
"BANKNIFTY": "NSE:NIFTYBANK-INDEX",
|
|
@@ -52,13 +51,10 @@ INDEX_MAP = {
|
|
| 52 |
def get_root_symbol(desc: str, symbol: str) -> str:
|
| 53 |
"""Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
|
| 54 |
try:
|
| 55 |
-
# Standard Format: "SENSEX 19 JAN 72000 CE" or "RELIANCE 24 JAN..."
|
| 56 |
parts = desc.strip().split(' ')
|
| 57 |
root = parts[0].upper()
|
| 58 |
-
# Clean up potential artifacts
|
| 59 |
return root.replace(':', '').replace('-', '')
|
| 60 |
except:
|
| 61 |
-
# Fallback to symbol parsing if description fails
|
| 62 |
if ':' in symbol:
|
| 63 |
return symbol.split(':')[1]
|
| 64 |
return symbol
|
|
@@ -70,12 +66,12 @@ def update_master_db():
|
|
| 70 |
return
|
| 71 |
|
| 72 |
try:
|
| 73 |
-
SYSTEM_STATUS["message"] = "Downloading Master Data
|
| 74 |
print(">>> [BG TASK] STARTING DOWNLOAD...")
|
| 75 |
|
| 76 |
headers = { "User-Agent": "Mozilla/5.0" }
|
| 77 |
|
| 78 |
-
# 1.
|
| 79 |
url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
|
| 80 |
df_nse = pd.read_csv(
|
| 81 |
url_nse, usecols=[0, 1, 8, 9, 13],
|
|
@@ -84,7 +80,7 @@ def update_master_db():
|
|
| 84 |
)
|
| 85 |
df_nse['Exch'] = 'NSE'
|
| 86 |
|
| 87 |
-
# 2.
|
| 88 |
url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
|
| 89 |
df_mcx = pd.read_csv(
|
| 90 |
url_mcx, usecols=[0, 1, 8, 9, 13],
|
|
@@ -93,7 +89,7 @@ def update_master_db():
|
|
| 93 |
)
|
| 94 |
df_mcx['Exch'] = 'MCX'
|
| 95 |
|
| 96 |
-
# 3.
|
| 97 |
url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
|
| 98 |
df_bse = pd.read_csv(
|
| 99 |
url_bse, usecols=[0, 1, 8, 9, 13],
|
|
@@ -104,9 +100,7 @@ def update_master_db():
|
|
| 104 |
|
| 105 |
SYSTEM_STATUS["message"] = "Processing Data..."
|
| 106 |
|
| 107 |
-
# Merge Dataframes
|
| 108 |
df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
|
| 109 |
-
|
| 110 |
df['Symbol'] = df['Symbol'].astype(str)
|
| 111 |
df['Desc'] = df['Desc'].astype(str)
|
| 112 |
|
|
@@ -119,21 +113,15 @@ def update_master_db():
|
|
| 119 |
|
| 120 |
root = get_root_symbol(desc, sym)
|
| 121 |
|
| 122 |
-
# Initialize Root Entry
|
| 123 |
if root not in temp_db:
|
| 124 |
spot_sym = ""
|
| 125 |
-
|
| 126 |
-
# Check for Index Map match first
|
| 127 |
if root in INDEX_MAP:
|
| 128 |
spot_sym = INDEX_MAP[root]
|
| 129 |
elif exch == "MCX":
|
| 130 |
-
# MCX has no "Equity Spot", usually we track the Future
|
| 131 |
spot_sym = "MCX"
|
| 132 |
elif exch == "BSE":
|
| 133 |
-
# BSE Equity Spot usually "BSE:Symbol"
|
| 134 |
spot_sym = f"BSE:{root}"
|
| 135 |
else:
|
| 136 |
-
# NSE Equity Spot
|
| 137 |
spot_sym = f"NSE:{root}-EQ"
|
| 138 |
|
| 139 |
temp_db[root] = { "spot": spot_sym, "exch": exch, "items": [] }
|
|
@@ -146,15 +134,11 @@ def update_master_db():
|
|
| 146 |
strike = 0.0
|
| 147 |
opt_type = "FUT"
|
| 148 |
|
| 149 |
-
# Parse Strike & Option Type
|
| 150 |
-
# Logic handles "SENSEX 23 JAN 72000 CE"
|
| 151 |
if "CE" in sym or "PE" in sym:
|
| 152 |
parts = desc.strip().split(' ')
|
| 153 |
if len(parts) >= 2:
|
| 154 |
try:
|
| 155 |
-
# Logic: val is usually 2nd last item
|
| 156 |
val = parts[-2]
|
| 157 |
-
# Remove decimal for digit check
|
| 158 |
val_clean = val.replace('.', '', 1)
|
| 159 |
if val_clean.isdigit():
|
| 160 |
strike = float(val)
|
|
@@ -166,19 +150,16 @@ def update_master_db():
|
|
| 166 |
"s": sym, "e": exp, "k": strike, "t": opt_type
|
| 167 |
})
|
| 168 |
|
| 169 |
-
# Final Sort & Search Index Creation
|
| 170 |
final_db = {}
|
| 171 |
search_list = []
|
| 172 |
|
| 173 |
for root, data in temp_db.items():
|
| 174 |
-
# Sort by Expiry (asc) then Strike (asc)
|
| 175 |
sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
|
| 176 |
final_db[root] = {
|
| 177 |
"spot": data["spot"],
|
| 178 |
"exch": data["exch"],
|
| 179 |
"items": sorted_items
|
| 180 |
}
|
| 181 |
-
# Create a rich search object
|
| 182 |
search_list.append({
|
| 183 |
"root": root,
|
| 184 |
"exch": data["exch"],
|
|
@@ -186,10 +167,8 @@ def update_master_db():
|
|
| 186 |
})
|
| 187 |
|
| 188 |
MASTER_DB = final_db
|
| 189 |
-
# Sort search index alphabetically
|
| 190 |
SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
|
| 191 |
|
| 192 |
-
# Save cache
|
| 193 |
with open(CACHE_FILE, "w") as f:
|
| 194 |
json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
|
| 195 |
|
|
@@ -244,7 +223,6 @@ def home(access_token: Optional[str] = Cookie(None)):
|
|
| 244 |
</div>
|
| 245 |
""")
|
| 246 |
|
| 247 |
-
# Inject token
|
| 248 |
return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
|
| 249 |
|
| 250 |
@app.get("/callback")
|
|
@@ -272,32 +250,17 @@ def logout():
|
|
| 272 |
response.delete_cookie("access_token")
|
| 273 |
return response
|
| 274 |
|
| 275 |
-
@app.get("/favicon.ico")
|
| 276 |
-
def favicon():
|
| 277 |
-
return Response(status_code=204)
|
| 278 |
-
|
| 279 |
-
# --- API ---
|
| 280 |
-
|
| 281 |
@app.get("/api/status")
|
| 282 |
def get_status():
|
| 283 |
return JSONResponse(SYSTEM_STATUS)
|
| 284 |
|
| 285 |
-
@app.get("/api/init")
|
| 286 |
-
def legacy_init():
|
| 287 |
-
return JSONResponse([])
|
| 288 |
-
|
| 289 |
@app.get("/api/search")
|
| 290 |
def search_symbol(q: str = Query(..., min_length=1)):
|
| 291 |
if not SYSTEM_STATUS["ready"]:
|
| 292 |
return JSONResponse([])
|
| 293 |
|
| 294 |
query = q.upper()
|
| 295 |
-
# SEARCH_INDEX is now a list of dicts: {'root': 'NIFTY', 'exch': 'NSE', 'display': 'NIFTY'}
|
| 296 |
-
|
| 297 |
-
# 1. Starts with
|
| 298 |
results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
|
| 299 |
-
|
| 300 |
-
# 2. Contains (if few results)
|
| 301 |
if len(results) < 10:
|
| 302 |
results += [x for x in SEARCH_INDEX if query in x['root'] and x not in results]
|
| 303 |
|
|
@@ -326,34 +289,34 @@ HTML_TEMPLATE = """
|
|
| 326 |
<html lang="en">
|
| 327 |
<head>
|
| 328 |
<meta charset="UTF-8">
|
| 329 |
-
<title>PRO CHAIN
|
| 330 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 331 |
-
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
| 332 |
<style>
|
| 333 |
:root {
|
| 334 |
--bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
|
| 335 |
--text-main: #1a1a1a; --text-sub: #555555;
|
| 336 |
--accent: #2962ff; --green: #00c853; --red: #d50000;
|
| 337 |
-
--green-soft: rgba(0, 200, 83, 0.
|
| 338 |
-
--atm-bg: rgba(41, 98, 255, 0.
|
| 339 |
-
|
| 340 |
-
--badge-nse: #00c853;
|
| 341 |
-
--badge-bse: #ff6d00;
|
| 342 |
-
--badge-mcx: #2962ff;
|
| 343 |
}
|
| 344 |
* { box-sizing: border-box; }
|
| 345 |
body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
| 346 |
|
| 347 |
-
.top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; }
|
| 348 |
-
.btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; }
|
| 349 |
.btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
|
| 350 |
|
|
|
|
|
|
|
|
|
|
| 351 |
.search-box { position: relative; width: 350px; margin-right: 20px; }
|
| 352 |
-
.search-input { width: 100%; padding: 10px 15px; border:
|
| 353 |
-
.search-input:focus { border-color: var(--accent); }
|
| 354 |
-
.search-dropdown { position: absolute; top:
|
| 355 |
|
| 356 |
-
.search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #
|
| 357 |
.search-item:hover { background: #f0f7ff; color: var(--accent); }
|
| 358 |
|
| 359 |
.exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
|
|
@@ -362,80 +325,135 @@ HTML_TEMPLATE = """
|
|
| 362 |
.badge-MCX { background: var(--badge-mcx); }
|
| 363 |
|
| 364 |
.grid-layout { display: flex; height: calc(100vh - 60px); }
|
|
|
|
|
|
|
| 365 |
.chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
|
| 366 |
-
.chain-header { display: grid; grid-template-columns: 1fr
|
| 367 |
-
.chain-body { flex: 1; overflow-y: auto; background: #fff; }
|
| 368 |
|
| 369 |
-
.row { display: grid; grid-template-columns: 1fr
|
|
|
|
| 370 |
.row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
|
| 371 |
|
| 372 |
.mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); cursor: crosshair; }
|
| 373 |
-
.visual-bar { position: absolute; top:
|
| 374 |
-
.strike-txt { z-index: 2; font-weight: 800; font-size: 13px; }
|
| 375 |
-
.delta-txt { z-index: 2; font-size: 10px; font-weight:
|
| 376 |
|
| 377 |
-
.side-col { padding: 0 15px; display: flex; justify-content: space-between; color:
|
|
|
|
| 378 |
|
| 379 |
-
|
| 380 |
-
.fut-
|
| 381 |
-
.meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 5px; }
|
| 382 |
|
| 383 |
-
.
|
| 384 |
-
.
|
| 385 |
-
.modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; overflow-y: auto; }
|
| 386 |
-
.exp-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 4px; cursor: pointer; font-weight:600; font-size:12px; }
|
| 387 |
-
.exp-btn:hover { background: #f0f7ff; border-color: var(--accent); }
|
| 388 |
-
.exp-btn.active { background: var(--accent); color: white; }
|
| 389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
.loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
|
| 391 |
-
.spinner { width: 30px; height: 30px; border:
|
| 392 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 393 |
|
| 394 |
@keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
|
| 395 |
-
.live {
|
| 396 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
</style>
|
| 398 |
</head>
|
| 399 |
<body>
|
| 400 |
|
|
|
|
| 401 |
<div class="loader-ov" id="loader">
|
| 402 |
<div class="spinner"></div>
|
| 403 |
-
<div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px;">CONNECTING...</div>
|
| 404 |
</div>
|
| 405 |
|
|
|
|
| 406 |
<div class="modal" id="expModal">
|
| 407 |
<div class="modal-box">
|
| 408 |
-
<div style="display:flex; justify-content:space-between; font-weight:
|
| 409 |
-
<span
|
| 410 |
-
<span style="cursor:pointer" onclick="closeModal()">✕</span>
|
| 411 |
</div>
|
| 412 |
<div class="modal-grid" id="expGrid"></div>
|
| 413 |
</div>
|
| 414 |
</div>
|
| 415 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 416 |
<div class="top-nav">
|
| 417 |
<div style="display:flex; align-items:center;">
|
| 418 |
<div class="search-box">
|
| 419 |
-
<input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g.
|
| 420 |
<div class="search-dropdown" id="searchRes"></div>
|
| 421 |
</div>
|
| 422 |
|
| 423 |
-
<button class="btn" onclick="refreshDB()"
|
| 424 |
-
<svg
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
| 426 |
</button>
|
| 427 |
</div>
|
| 428 |
|
| 429 |
<div style="display:flex; gap:20px; align-items:center;">
|
| 430 |
<div style="text-align:right;">
|
| 431 |
-
<div style="font-size:10px; font-weight:700; color:#
|
| 432 |
-
<div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:
|
| 433 |
</div>
|
| 434 |
<button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
|
| 435 |
-
<span style="color:#888">EXP:</span> <span id="selExpTxt">SELECT</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
</button>
|
| 437 |
-
<button class="btn" onclick="location.href='/logout'" style="border-color:var(--red); color:var(--red);">LOGOUT</button>
|
| 438 |
-
<div id="statusDot" style="width:10px; height:10px; background:#ccc; border-radius:50%;"></div>
|
| 439 |
</div>
|
| 440 |
</div>
|
| 441 |
|
|
@@ -443,17 +461,35 @@ HTML_TEMPLATE = """
|
|
| 443 |
<div class="chain-panel">
|
| 444 |
<div class="chain-header">
|
| 445 |
<div>CALL SELLERS (OI)</div>
|
| 446 |
-
<div>NET FLOW (PUT-CALL)</div>
|
| 447 |
<div>PUT SELLERS (OI)</div>
|
| 448 |
</div>
|
| 449 |
<div class="chain-body" id="chainBody">
|
| 450 |
-
<div style="padding:40px; text-align:center; color:#999; font-weight:600;">USE SEARCH TO LOAD
|
| 451 |
</div>
|
| 452 |
</div>
|
| 453 |
|
| 454 |
<div class="fut-panel">
|
| 455 |
-
|
| 456 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
</div>
|
| 458 |
</div>
|
| 459 |
|
|
@@ -470,9 +506,15 @@ HTML_TEMPLATE = """
|
|
| 470 |
let POLLER = null;
|
| 471 |
let IS_POLLING = false;
|
| 472 |
let SEARCH_TIMER = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
window.onload = () => {
|
| 475 |
if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
|
|
|
|
|
|
|
| 476 |
checkStatus();
|
| 477 |
|
| 478 |
const inp = document.getElementById('searchInp');
|
|
@@ -485,6 +527,26 @@ HTML_TEMPLATE = """
|
|
| 485 |
});
|
| 486 |
};
|
| 487 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
async function checkStatus() {
|
| 489 |
try {
|
| 490 |
const res = await fetch('/api/status');
|
|
@@ -501,6 +563,7 @@ HTML_TEMPLATE = """
|
|
| 501 |
|
| 502 |
async function refreshDB() {
|
| 503 |
if(!confirm("Download fresh Master Data (NSE, MCX, BSE)? This takes ~15s.")) return;
|
|
|
|
| 504 |
document.getElementById('loader').style.display = 'flex';
|
| 505 |
await fetch('/api/refresh');
|
| 506 |
checkStatus();
|
|
@@ -512,7 +575,6 @@ HTML_TEMPLATE = """
|
|
| 512 |
try {
|
| 513 |
const res = await fetch(`/api/search?q=${q}`);
|
| 514 |
const data = await res.json();
|
| 515 |
-
// Data is now list of {root, exch, display}
|
| 516 |
resDiv.innerHTML = data.map(item =>
|
| 517 |
`<div class="search-item" onclick="loadSymbol('${item.root}')">
|
| 518 |
<span>${item.display}</span>
|
|
@@ -538,15 +600,15 @@ HTML_TEMPLATE = """
|
|
| 538 |
CHAIN_DATA = data.items;
|
| 539 |
SPOT_SYM = data.spot;
|
| 540 |
|
| 541 |
-
//
|
| 542 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 543 |
|
| 544 |
-
// Handle MCX specific spot logic
|
| 545 |
if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
|
| 546 |
SPOT_SYM = FUT_SYMS[0].s;
|
| 547 |
}
|
| 548 |
|
| 549 |
-
document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ') || SPOT_SYM.startsWith('BSE:')) ? "SPOT" : "FUT
|
| 550 |
|
| 551 |
setupExpiries();
|
| 552 |
document.getElementById('loader').style.display = 'none';
|
|
@@ -564,10 +626,7 @@ HTML_TEMPLATE = """
|
|
| 564 |
|
| 565 |
const validExps = exps.filter(ts => ts > today);
|
| 566 |
if(validExps.length === 0) {
|
| 567 |
-
alert("No active options found for this symbol.");
|
| 568 |
-
// Still render Futures if available
|
| 569 |
document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
|
| 570 |
-
updateUI({});
|
| 571 |
return;
|
| 572 |
}
|
| 573 |
|
|
@@ -577,7 +636,7 @@ HTML_TEMPLATE = """
|
|
| 577 |
const grid = document.getElementById('expGrid');
|
| 578 |
grid.innerHTML = validExps.map(ts => {
|
| 579 |
const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
|
| 580 |
-
return `<div class="
|
| 581 |
}).join('');
|
| 582 |
|
| 583 |
buildChain();
|
|
@@ -586,7 +645,14 @@ HTML_TEMPLATE = """
|
|
| 586 |
function selectExp(ts) {
|
| 587 |
ACTIVE_EXP = ts;
|
| 588 |
updateExpText(ts);
|
| 589 |
-
closeModal();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
buildChain();
|
| 591 |
}
|
| 592 |
|
|
@@ -595,8 +661,6 @@ HTML_TEMPLATE = """
|
|
| 595 |
document.getElementById('selExpTxt').innerText = dStr;
|
| 596 |
}
|
| 597 |
|
| 598 |
-
function closeModal() { document.getElementById('expModal').style.display = 'none'; }
|
| 599 |
-
|
| 600 |
function buildChain() {
|
| 601 |
const body = document.getElementById('chainBody');
|
| 602 |
const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
|
|
@@ -606,13 +670,13 @@ HTML_TEMPLATE = """
|
|
| 606 |
|
| 607 |
body.innerHTML = strikes.map(k => `
|
| 608 |
<div class="row" id="row-${k}" data-k="${k}">
|
| 609 |
-
<div class="side-col" id="ce-${k}"><span>-</span><span>-</span></div>
|
| 610 |
<div class="mid-col">
|
| 611 |
<div class="visual-bar" id="bar-${k}"></div>
|
| 612 |
<span class="strike-txt">${k}</span>
|
| 613 |
<span class="delta-txt" id="val-${k}">-</span>
|
| 614 |
</div>
|
| 615 |
-
<div class="side-col" id="pe-${k}"><span>-</span><span>-</span></div>
|
| 616 |
</div>
|
| 617 |
`).join('');
|
| 618 |
|
|
@@ -626,6 +690,7 @@ HTML_TEMPLATE = """
|
|
| 626 |
pollData();
|
| 627 |
}
|
| 628 |
|
|
|
|
| 629 |
async function pollData() {
|
| 630 |
if(IS_POLLING || !CURRENT_ROOT) return;
|
| 631 |
IS_POLLING = true;
|
|
@@ -633,45 +698,52 @@ HTML_TEMPLATE = """
|
|
| 633 |
try {
|
| 634 |
let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
|
| 635 |
|
| 636 |
-
//
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
OPT_SYMS.forEach(o => {
|
| 659 |
-
if(viewKs.includes(o.k)) symbolsToFetch.push(o.s);
|
| 660 |
-
});
|
| 661 |
}
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
}
|
| 669 |
|
| 670 |
const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
|
| 671 |
if(uniqueSyms.length === 0) return;
|
| 672 |
|
| 673 |
const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
|
| 674 |
-
const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN
|
| 675 |
|
| 676 |
if(res.status === 401) { location.href = '/logout'; return; }
|
| 677 |
|
|
@@ -679,24 +751,28 @@ HTML_TEMPLATE = """
|
|
| 679 |
if(json.s === 'ok') {
|
| 680 |
updateUI(json.d);
|
| 681 |
const dot = document.getElementById('statusDot');
|
| 682 |
-
dot.
|
| 683 |
-
dot.classList.
|
| 684 |
-
setTimeout(()=>dot.classList.remove('live'), 500);
|
| 685 |
}
|
| 686 |
-
} catch(e) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
finally { IS_POLLING = false; }
|
| 688 |
}
|
| 689 |
|
| 690 |
function updateUI(data) {
|
| 691 |
-
//
|
| 692 |
if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
|
| 693 |
SPOT_PRICE = data[SPOT_SYM].ltp;
|
| 694 |
document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
|
| 695 |
}
|
| 696 |
|
|
|
|
| 697 |
const futHTML = FUT_SYMS.map(f => {
|
| 698 |
const d = data[f.s];
|
| 699 |
-
// Fix: Check if future data and ltp are valid
|
| 700 |
if(!d || typeof d.ltp !== 'number') return '';
|
| 701 |
const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
|
| 702 |
const tot = (d.totalbuyqty + d.totalsellqty) || 1;
|
|
@@ -710,51 +786,94 @@ HTML_TEMPLATE = """
|
|
| 710 |
<div style="width:${bPct}%; background:var(--green)"></div>
|
| 711 |
<div style="width:${100-bPct}%; background:var(--red)"></div>
|
| 712 |
</div>
|
| 713 |
-
<div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:
|
| 714 |
<span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
|
| 715 |
</div>
|
| 716 |
</div>`;
|
| 717 |
}).join('');
|
| 718 |
if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
|
| 719 |
|
|
|
|
| 720 |
const rowData = {};
|
| 721 |
-
for(const [sym, d] of Object.entries(data)) {
|
| 722 |
-
const meta = OPT_SYMS.find(x => x.s === sym);
|
| 723 |
-
if(!meta) continue;
|
| 724 |
-
if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
|
| 725 |
-
if(meta.t === 'CE') rowData[meta.k].ce = d.totalsellqty;
|
| 726 |
-
if(meta.t === 'PE') rowData[meta.k].pe = d.totalsellqty;
|
| 727 |
-
}
|
| 728 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
let maxDiff = 0;
|
| 730 |
for(const k in rowData) maxDiff = Math.max(maxDiff, Math.abs(rowData[k].pe - rowData[k].ce));
|
| 731 |
|
| 732 |
for(const k in rowData) {
|
| 733 |
const r = rowData[k];
|
| 734 |
const delta = r.pe - r.ce;
|
|
|
|
|
|
|
| 735 |
const ceEl = document.getElementById(`ce-${k}`);
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 739 |
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
val.innerText = "+" + fmt(delta);
|
| 750 |
-
} else {
|
| 751 |
-
bar.style.background = 'var(--red-soft)';
|
| 752 |
-
bar.style.left = (50 - (pct/2)) + '%';
|
| 753 |
-
bar.style.width = (pct/2) + '%';
|
| 754 |
-
val.style.color = 'var(--red)';
|
| 755 |
-
val.innerText = fmt(delta);
|
| 756 |
}
|
| 757 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
}
|
| 759 |
}
|
| 760 |
|
|
@@ -764,7 +883,7 @@ HTML_TEMPLATE = """
|
|
| 764 |
if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr';
|
| 765 |
if(abs>=100000) return (n/100000).toFixed(2)+'L';
|
| 766 |
if(abs>=1000) return (n/1000).toFixed(1)+'k';
|
| 767 |
-
return n;
|
| 768 |
}
|
| 769 |
</script>
|
| 770 |
</body>
|
|
|
|
| 38 |
UPDATE_LOCK = threading.Lock()
|
| 39 |
|
| 40 |
# Manual mapping for Index Spot symbols
|
|
|
|
| 41 |
INDEX_MAP = {
|
| 42 |
"NIFTY": "NSE:NIFTY50-INDEX",
|
| 43 |
"BANKNIFTY": "NSE:NIFTYBANK-INDEX",
|
|
|
|
| 51 |
def get_root_symbol(desc: str, symbol: str) -> str:
|
| 52 |
"""Extracts root symbol (e.g. 'RELIANCE', 'SENSEX') from description."""
|
| 53 |
try:
|
|
|
|
| 54 |
parts = desc.strip().split(' ')
|
| 55 |
root = parts[0].upper()
|
|
|
|
| 56 |
return root.replace(':', '').replace('-', '')
|
| 57 |
except:
|
|
|
|
| 58 |
if ':' in symbol:
|
| 59 |
return symbol.split(':')[1]
|
| 60 |
return symbol
|
|
|
|
| 66 |
return
|
| 67 |
|
| 68 |
try:
|
| 69 |
+
SYSTEM_STATUS["message"] = "Downloading Master Data..."
|
| 70 |
print(">>> [BG TASK] STARTING DOWNLOAD...")
|
| 71 |
|
| 72 |
headers = { "User-Agent": "Mozilla/5.0" }
|
| 73 |
|
| 74 |
+
# 1. NSE F&O
|
| 75 |
url_nse = "https://public.fyers.in/sym_details/NSE_FO.csv"
|
| 76 |
df_nse = pd.read_csv(
|
| 77 |
url_nse, usecols=[0, 1, 8, 9, 13],
|
|
|
|
| 80 |
)
|
| 81 |
df_nse['Exch'] = 'NSE'
|
| 82 |
|
| 83 |
+
# 2. MCX F&O
|
| 84 |
url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
|
| 85 |
df_mcx = pd.read_csv(
|
| 86 |
url_mcx, usecols=[0, 1, 8, 9, 13],
|
|
|
|
| 89 |
)
|
| 90 |
df_mcx['Exch'] = 'MCX'
|
| 91 |
|
| 92 |
+
# 3. BSE F&O
|
| 93 |
url_bse = "https://public.fyers.in/sym_details/BSE_FO.csv"
|
| 94 |
df_bse = pd.read_csv(
|
| 95 |
url_bse, usecols=[0, 1, 8, 9, 13],
|
|
|
|
| 100 |
|
| 101 |
SYSTEM_STATUS["message"] = "Processing Data..."
|
| 102 |
|
|
|
|
| 103 |
df = pd.concat([df_nse, df_mcx, df_bse], ignore_index=True)
|
|
|
|
| 104 |
df['Symbol'] = df['Symbol'].astype(str)
|
| 105 |
df['Desc'] = df['Desc'].astype(str)
|
| 106 |
|
|
|
|
| 113 |
|
| 114 |
root = get_root_symbol(desc, sym)
|
| 115 |
|
|
|
|
| 116 |
if root not in temp_db:
|
| 117 |
spot_sym = ""
|
|
|
|
|
|
|
| 118 |
if root in INDEX_MAP:
|
| 119 |
spot_sym = INDEX_MAP[root]
|
| 120 |
elif exch == "MCX":
|
|
|
|
| 121 |
spot_sym = "MCX"
|
| 122 |
elif exch == "BSE":
|
|
|
|
| 123 |
spot_sym = f"BSE:{root}"
|
| 124 |
else:
|
|
|
|
| 125 |
spot_sym = f"NSE:{root}-EQ"
|
| 126 |
|
| 127 |
temp_db[root] = { "spot": spot_sym, "exch": exch, "items": [] }
|
|
|
|
| 134 |
strike = 0.0
|
| 135 |
opt_type = "FUT"
|
| 136 |
|
|
|
|
|
|
|
| 137 |
if "CE" in sym or "PE" in sym:
|
| 138 |
parts = desc.strip().split(' ')
|
| 139 |
if len(parts) >= 2:
|
| 140 |
try:
|
|
|
|
| 141 |
val = parts[-2]
|
|
|
|
| 142 |
val_clean = val.replace('.', '', 1)
|
| 143 |
if val_clean.isdigit():
|
| 144 |
strike = float(val)
|
|
|
|
| 150 |
"s": sym, "e": exp, "k": strike, "t": opt_type
|
| 151 |
})
|
| 152 |
|
|
|
|
| 153 |
final_db = {}
|
| 154 |
search_list = []
|
| 155 |
|
| 156 |
for root, data in temp_db.items():
|
|
|
|
| 157 |
sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
|
| 158 |
final_db[root] = {
|
| 159 |
"spot": data["spot"],
|
| 160 |
"exch": data["exch"],
|
| 161 |
"items": sorted_items
|
| 162 |
}
|
|
|
|
| 163 |
search_list.append({
|
| 164 |
"root": root,
|
| 165 |
"exch": data["exch"],
|
|
|
|
| 167 |
})
|
| 168 |
|
| 169 |
MASTER_DB = final_db
|
|
|
|
| 170 |
SEARCH_INDEX = sorted(search_list, key=lambda x: x['root'])
|
| 171 |
|
|
|
|
| 172 |
with open(CACHE_FILE, "w") as f:
|
| 173 |
json.dump({"db": final_db, "idx": SEARCH_INDEX}, f)
|
| 174 |
|
|
|
|
| 223 |
</div>
|
| 224 |
""")
|
| 225 |
|
|
|
|
| 226 |
return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
|
| 227 |
|
| 228 |
@app.get("/callback")
|
|
|
|
| 250 |
response.delete_cookie("access_token")
|
| 251 |
return response
|
| 252 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
@app.get("/api/status")
|
| 254 |
def get_status():
|
| 255 |
return JSONResponse(SYSTEM_STATUS)
|
| 256 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
@app.get("/api/search")
|
| 258 |
def search_symbol(q: str = Query(..., min_length=1)):
|
| 259 |
if not SYSTEM_STATUS["ready"]:
|
| 260 |
return JSONResponse([])
|
| 261 |
|
| 262 |
query = q.upper()
|
|
|
|
|
|
|
|
|
|
| 263 |
results = [x for x in SEARCH_INDEX if x['root'].startswith(query)]
|
|
|
|
|
|
|
| 264 |
if len(results) < 10:
|
| 265 |
results += [x for x in SEARCH_INDEX if query in x['root'] and x not in results]
|
| 266 |
|
|
|
|
| 289 |
<html lang="en">
|
| 290 |
<head>
|
| 291 |
<meta charset="UTF-8">
|
| 292 |
+
<title>PRO CHAIN V3</title>
|
| 293 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 294 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
|
| 295 |
<style>
|
| 296 |
:root {
|
| 297 |
--bg: #ffffff; --panel: #f4f6f8; --border: #e0e0e0;
|
| 298 |
--text-main: #1a1a1a; --text-sub: #555555;
|
| 299 |
--accent: #2962ff; --green: #00c853; --red: #d50000;
|
| 300 |
+
--green-soft: rgba(0, 200, 83, 0.15); --red-soft: rgba(213, 0, 0, 0.15);
|
| 301 |
+
--atm-bg: rgba(41, 98, 255, 0.08);
|
| 302 |
+
--badge-nse: #00c853; --badge-bse: #ff6d00; --badge-mcx: #2962ff;
|
|
|
|
|
|
|
|
|
|
| 303 |
}
|
| 304 |
* { box-sizing: border-box; }
|
| 305 |
body { margin: 0; background: var(--bg); color: var(--text-main); font-family: 'Inter', sans-serif; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
| 306 |
|
| 307 |
+
.top-nav { padding: 0 20px; background: #fff; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; height: 60px; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.03); }
|
| 308 |
+
.btn { background: #fff; border: 1px solid var(--border); padding: 8px 16px; font-size: 12px; border-radius: 6px; cursor: pointer; font-weight: 700; display: flex; gap: 8px; align-items: center; transition:0.2s; white-space:nowrap; color: var(--text-sub); }
|
| 309 |
.btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
|
| 310 |
|
| 311 |
+
.icon-btn { padding: 8px; border-radius: 50%; border: 1px solid transparent; background: transparent; cursor: pointer; color: var(--text-sub); }
|
| 312 |
+
.icon-btn:hover { background: #f0f0f0; color: var(--text-main); }
|
| 313 |
+
|
| 314 |
.search-box { position: relative; width: 350px; margin-right: 20px; }
|
| 315 |
+
.search-input { width: 100%; padding: 10px 15px; border: 1px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; transition: 0.3s; background: #f9f9f9; }
|
| 316 |
+
.search-input:focus { border-color: var(--accent); background: #fff; box-shadow: 0 0 0 3px rgba(41, 98, 255, 0.1); }
|
| 317 |
+
.search-dropdown { position: absolute; top: 105%; left: 0; width: 100%; background: white; border: 1px solid var(--border); max-height: 400px; overflow-y: auto; display: none; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 6px; z-index: 100; }
|
| 318 |
|
| 319 |
+
.search-item { padding: 12px 15px; cursor: pointer; border-bottom: 1px solid #f5f5f5; font-size: 13px; font-weight: 700; display: flex; justify-content: space-between; align-items: center; }
|
| 320 |
.search-item:hover { background: #f0f7ff; color: var(--accent); }
|
| 321 |
|
| 322 |
.exch-badge { font-size: 10px; padding: 2px 6px; border-radius: 4px; color: white; font-weight: 800; min-width: 40px; text-align: center; }
|
|
|
|
| 325 |
.badge-MCX { background: var(--badge-mcx); }
|
| 326 |
|
| 327 |
.grid-layout { display: flex; height: calc(100vh - 60px); }
|
| 328 |
+
|
| 329 |
+
/* Chain Panel */
|
| 330 |
.chain-panel { flex: 1; display: flex; flex-direction: column; position: relative; }
|
| 331 |
+
.chain-header { display: grid; grid-template-columns: 1fr 160px 1fr; padding: 12px 0; background: #f8f9fa; border-bottom: 1px solid var(--border); font-size: 11px; font-weight: 800; text-align: center; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; }
|
| 332 |
+
.chain-body { flex: 1; overflow-y: auto; background: #fff; scroll-behavior: smooth; }
|
| 333 |
|
| 334 |
+
.row { display: grid; grid-template-columns: 1fr 160px 1fr; border-bottom: 1px solid #f0f0f0; height: 42px; align-items: center; font-family: 'JetBrains Mono'; font-size: 12px; }
|
| 335 |
+
.row:hover { background: #fafafa; }
|
| 336 |
.row.atm { background: var(--atm-bg); border-top: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
|
| 337 |
|
| 338 |
.mid-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-left: 1px solid var(--border); border-right: 1px solid var(--border); cursor: crosshair; }
|
| 339 |
+
.visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.8; }
|
| 340 |
+
.strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); }
|
| 341 |
+
.delta-txt { z-index: 2; font-size: 10px; font-weight: 700; background: rgba(255,255,255,0.85); padding: 0 5px; border-radius: 3px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); margin-top: -2px; }
|
| 342 |
|
| 343 |
+
.side-col { padding: 0 15px; display: flex; justify-content: space-between; color: var(--text-sub); font-size: 11px; font-weight: 600; }
|
| 344 |
+
.col-val { font-weight: 700; color: var(--text-main); }
|
| 345 |
|
| 346 |
+
/* Futures / Sidebar Panel */
|
| 347 |
+
.fut-panel { width: 300px; background: var(--panel); border-left: 1px solid var(--border); display: flex; flex-direction: column; overflow-y: auto; }
|
|
|
|
| 348 |
|
| 349 |
+
.panel-section { padding: 15px; border-bottom: 1px solid var(--border); background: #fff; margin-bottom: 10px; }
|
| 350 |
+
.panel-title { font-size: 11px; font-weight: 800; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display:flex; justify-content:space-between; align-items:center; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
|
| 352 |
+
.fut-card { padding: 12px; background: #fff; border: 1px solid #eee; border-radius: 8px; margin-bottom: 10px; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
| 353 |
+
.meter-bg { height: 6px; background: #eee; border-radius: 3px; display: flex; overflow: hidden; margin-top: 8px; }
|
| 354 |
+
|
| 355 |
+
.netflow-card { background: #fff; padding: 15px; border-radius: 8px; border: 1px solid var(--border); text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.03); }
|
| 356 |
+
.nf-val { font-size: 24px; font-family: 'JetBrains Mono'; font-weight: 800; margin: 10px 0; }
|
| 357 |
+
.nf-sub { font-size: 10px; color: #888; font-weight: 600; }
|
| 358 |
+
|
| 359 |
+
/* Modals */
|
| 360 |
+
.modal { position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.5); z-index: 1000; display: none; justify-content: center; align-items: center; backdrop-filter: blur(2px); }
|
| 361 |
+
.modal-box { background: #fff; width: 400px; border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); padding: 25px; display:flex; flex-direction:column; animation: popIn 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
|
| 362 |
+
@keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
| 363 |
+
|
| 364 |
+
.modal-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 15px; max-height: 300px; overflow-y: auto; }
|
| 365 |
+
.opt-btn { padding: 10px; text-align: center; border: 1px solid #eee; border-radius: 6px; cursor: pointer; font-weight:600; font-size:12px; transition:0.2s; background:#f9f9f9; }
|
| 366 |
+
.opt-btn:hover { background: #fff; border-color: var(--accent); box-shadow: 0 2px 5px rgba(0,0,0,0.05); }
|
| 367 |
+
.opt-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
|
| 368 |
+
|
| 369 |
+
.inp-group { display:flex; gap:10px; margin-top:15px; }
|
| 370 |
+
.modal-inp { flex:1; padding:10px; border:1px solid #ddd; border-radius:6px; font-family:'JetBrains Mono'; font-weight:bold; }
|
| 371 |
+
.modal-action { background:var(--accent); color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; }
|
| 372 |
+
|
| 373 |
+
/* Loader */
|
| 374 |
.loader-ov { position: fixed; top:0; left:0; width:100%; height:100%; background:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; z-index: 9999; }
|
| 375 |
+
.spinner { width: 30px; height: 30px; border: 3px solid #f3f3f3; border-top: 3px solid var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; margin-bottom: 15px; }
|
| 376 |
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 377 |
|
| 378 |
@keyframes blink { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
|
| 379 |
+
.live-dot { width:8px; height:8px; background:#ccc; border-radius:50%; transition:0.3s; }
|
| 380 |
+
.live-dot.active { background: var(--green); animation: blink 1.5s infinite; box-shadow: 0 0 5px var(--green); }
|
| 381 |
+
.live-dot.error { background: var(--red); }
|
| 382 |
+
|
| 383 |
+
/* Scrollbar */
|
| 384 |
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
| 385 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 386 |
+
::-webkit-scrollbar-thumb { background: #ddd; border-radius: 3px; }
|
| 387 |
+
::-webkit-scrollbar-thumb:hover { background: #bbb; }
|
| 388 |
</style>
|
| 389 |
</head>
|
| 390 |
<body>
|
| 391 |
|
| 392 |
+
<!-- LOADER -->
|
| 393 |
<div class="loader-ov" id="loader">
|
| 394 |
<div class="spinner"></div>
|
| 395 |
+
<div id="loaderMsg" style="font-weight:700; color:var(--text-main); font-size:14px; letter-spacing:1px;">CONNECTING...</div>
|
| 396 |
</div>
|
| 397 |
|
| 398 |
+
<!-- EXPIRY MODAL -->
|
| 399 |
<div class="modal" id="expModal">
|
| 400 |
<div class="modal-box">
|
| 401 |
+
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
|
| 402 |
+
<span>SELECT EXPIRY</span>
|
| 403 |
+
<span style="cursor:pointer; color:#999;" onclick="closeModal('expModal')">✕</span>
|
| 404 |
</div>
|
| 405 |
<div class="modal-grid" id="expGrid"></div>
|
| 406 |
</div>
|
| 407 |
</div>
|
| 408 |
|
| 409 |
+
<!-- SETTINGS MODAL -->
|
| 410 |
+
<div class="modal" id="setModal">
|
| 411 |
+
<div class="modal-box" style="width:350px;">
|
| 412 |
+
<div style="display:flex; justify-content:space-between; font-weight:800; font-size:16px; border-bottom:1px solid #eee; padding-bottom:15px;">
|
| 413 |
+
<span>DASHBOARD SETTINGS</span>
|
| 414 |
+
<span style="cursor:pointer; color:#999;" onclick="closeModal('setModal')">✕</span>
|
| 415 |
+
</div>
|
| 416 |
+
<div style="margin-top:20px;">
|
| 417 |
+
<label style="font-size:12px; font-weight:700; color:#666; display:block; margin-bottom:5px;">NET FLOW RANGE (STRIKES)</label>
|
| 418 |
+
<div style="font-size:11px; color:#999; margin-bottom:10px;">Calculates total Put-Call flow for strikes above/below ATM.</div>
|
| 419 |
+
<div class="inp-group">
|
| 420 |
+
<input type="number" id="rangeInp" class="modal-inp" value="10" min="1" max="50">
|
| 421 |
+
<button class="modal-action" onclick="saveSettings()">SAVE</button>
|
| 422 |
+
</div>
|
| 423 |
+
</div>
|
| 424 |
+
</div>
|
| 425 |
+
</div>
|
| 426 |
+
|
| 427 |
+
<!-- TOP NAV -->
|
| 428 |
<div class="top-nav">
|
| 429 |
<div style="display:flex; align-items:center;">
|
| 430 |
<div class="search-box">
|
| 431 |
+
<input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g. NIFTY, GOLD)..." autocomplete="off">
|
| 432 |
<div class="search-dropdown" id="searchRes"></div>
|
| 433 |
</div>
|
| 434 |
|
| 435 |
+
<button class="icon-btn" onclick="refreshDB()" title="Refresh Master DB">
|
| 436 |
+
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
|
| 437 |
+
</button>
|
| 438 |
+
|
| 439 |
+
<button class="icon-btn" onclick="openSettings()" title="Settings" style="margin-left:5px;">
|
| 440 |
+
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
| 441 |
</button>
|
| 442 |
</div>
|
| 443 |
|
| 444 |
<div style="display:flex; gap:20px; align-items:center;">
|
| 445 |
<div style="text-align:right;">
|
| 446 |
+
<div style="font-size:10px; font-weight:700; color:#aaa; letter-spacing:1px;" id="spotLabel">SPOT</div>
|
| 447 |
+
<div id="spotPrice" style="font-family:'JetBrains Mono'; font-weight:800; font-size:17px; color:var(--text-main);">0.00</div>
|
| 448 |
</div>
|
| 449 |
<button class="btn" onclick="document.getElementById('expModal').style.display='flex'">
|
| 450 |
+
<span style="color:#888">EXP:</span> <span id="selExpTxt" style="color:var(--text-main)">SELECT</span>
|
| 451 |
+
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 9l-7 7-7-7"></path></svg>
|
| 452 |
+
</button>
|
| 453 |
+
<div class="live-dot" id="statusDot" title="Live Connection Status"></div>
|
| 454 |
+
<button class="btn" onclick="location.href='/logout'" style="border:none; color:var(--red); padding:0;">
|
| 455 |
+
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"></path></svg>
|
| 456 |
</button>
|
|
|
|
|
|
|
| 457 |
</div>
|
| 458 |
</div>
|
| 459 |
|
|
|
|
| 461 |
<div class="chain-panel">
|
| 462 |
<div class="chain-header">
|
| 463 |
<div>CALL SELLERS (OI)</div>
|
| 464 |
+
<div>NET FLOW (PUT - CALL)</div>
|
| 465 |
<div>PUT SELLERS (OI)</div>
|
| 466 |
</div>
|
| 467 |
<div class="chain-body" id="chainBody">
|
| 468 |
+
<div style="padding:40px; text-align:center; color:#999; font-weight:600; font-size:14px;">USE SEARCH BAR TO LOAD DATA</div>
|
| 469 |
</div>
|
| 470 |
</div>
|
| 471 |
|
| 472 |
<div class="fut-panel">
|
| 473 |
+
|
| 474 |
+
<!-- NET FLOW CARD -->
|
| 475 |
+
<div class="panel-section">
|
| 476 |
+
<div class="panel-title">
|
| 477 |
+
<span>NET FLOW</span>
|
| 478 |
+
<span id="nfRangeLabel" style="background:#eee; padding:2px 6px; border-radius:4px; font-size:9px;">ATM ±10</span>
|
| 479 |
+
</div>
|
| 480 |
+
<div class="netflow-card">
|
| 481 |
+
<div class="nf-val" id="totalNetFlow">0</div>
|
| 482 |
+
<div class="nf-sub">Put Sell - Call Sell (Cumulative)</div>
|
| 483 |
+
<div id="nfSignal" style="font-size:10px; font-weight:800; margin-top:5px; text-transform:uppercase;">NEUTRAL</div>
|
| 484 |
+
</div>
|
| 485 |
+
</div>
|
| 486 |
+
|
| 487 |
+
<!-- FUTURES CARD -->
|
| 488 |
+
<div class="panel-section">
|
| 489 |
+
<div class="panel-title">FUTURES CONTRACTS</div>
|
| 490 |
+
<div id="futBody"></div>
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
</div>
|
| 494 |
</div>
|
| 495 |
|
|
|
|
| 506 |
let POLLER = null;
|
| 507 |
let IS_POLLING = false;
|
| 508 |
let SEARCH_TIMER = null;
|
| 509 |
+
let ATM_STRIKE = 0;
|
| 510 |
+
|
| 511 |
+
// Settings
|
| 512 |
+
let SETTING_RANGE = parseInt(localStorage.getItem('nf_range')) || 10;
|
| 513 |
|
| 514 |
window.onload = () => {
|
| 515 |
if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
|
| 516 |
+
document.getElementById('rangeInp').value = SETTING_RANGE;
|
| 517 |
+
document.getElementById('nfRangeLabel').innerText = `ATM ±${SETTING_RANGE}`;
|
| 518 |
checkStatus();
|
| 519 |
|
| 520 |
const inp = document.getElementById('searchInp');
|
|
|
|
| 527 |
});
|
| 528 |
};
|
| 529 |
|
| 530 |
+
function openSettings() {
|
| 531 |
+
document.getElementById('setModal').style.display = 'flex';
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
function saveSettings() {
|
| 535 |
+
const val = parseInt(document.getElementById('rangeInp').value);
|
| 536 |
+
if(val > 0 && val <= 50) {
|
| 537 |
+
SETTING_RANGE = val;
|
| 538 |
+
localStorage.setItem('nf_range', val);
|
| 539 |
+
document.getElementById('nfRangeLabel').innerText = `ATM ±${SETTING_RANGE}`;
|
| 540 |
+
closeModal('setModal');
|
| 541 |
+
// Force data refresh to include new range immediately
|
| 542 |
+
pollData();
|
| 543 |
+
} else {
|
| 544 |
+
alert("Please enter a range between 1 and 50");
|
| 545 |
+
}
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
function closeModal(id) { document.getElementById(id).style.display = 'none'; }
|
| 549 |
+
|
| 550 |
async function checkStatus() {
|
| 551 |
try {
|
| 552 |
const res = await fetch('/api/status');
|
|
|
|
| 563 |
|
| 564 |
async function refreshDB() {
|
| 565 |
if(!confirm("Download fresh Master Data (NSE, MCX, BSE)? This takes ~15s.")) return;
|
| 566 |
+
document.getElementById('loaderMsg').innerText = "DOWNLOADING...";
|
| 567 |
document.getElementById('loader').style.display = 'flex';
|
| 568 |
await fetch('/api/refresh');
|
| 569 |
checkStatus();
|
|
|
|
| 575 |
try {
|
| 576 |
const res = await fetch(`/api/search?q=${q}`);
|
| 577 |
const data = await res.json();
|
|
|
|
| 578 |
resDiv.innerHTML = data.map(item =>
|
| 579 |
`<div class="search-item" onclick="loadSymbol('${item.root}')">
|
| 580 |
<span>${item.display}</span>
|
|
|
|
| 600 |
CHAIN_DATA = data.items;
|
| 601 |
SPOT_SYM = data.spot;
|
| 602 |
|
| 603 |
+
// Handle Futures sorting
|
| 604 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 605 |
|
| 606 |
+
// Handle MCX specific spot logic (Use Near Future as Spot)
|
| 607 |
if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
|
| 608 |
SPOT_SYM = FUT_SYMS[0].s;
|
| 609 |
}
|
| 610 |
|
| 611 |
+
document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ') || SPOT_SYM.startsWith('BSE:')) ? "SPOT PRICE" : "FUT REFERENCE";
|
| 612 |
|
| 613 |
setupExpiries();
|
| 614 |
document.getElementById('loader').style.display = 'none';
|
|
|
|
| 626 |
|
| 627 |
const validExps = exps.filter(ts => ts > today);
|
| 628 |
if(validExps.length === 0) {
|
|
|
|
|
|
|
| 629 |
document.getElementById('chainBody').innerHTML = '<div style="padding:20px; text-align:center;">NO OPTIONS DATA</div>';
|
|
|
|
| 630 |
return;
|
| 631 |
}
|
| 632 |
|
|
|
|
| 636 |
const grid = document.getElementById('expGrid');
|
| 637 |
grid.innerHTML = validExps.map(ts => {
|
| 638 |
const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
|
| 639 |
+
return `<div class="opt-btn ${ts===ACTIVE_EXP?'active':''}" onclick="selectExp(${ts})">${dStr}</div>`;
|
| 640 |
}).join('');
|
| 641 |
|
| 642 |
buildChain();
|
|
|
|
| 645 |
function selectExp(ts) {
|
| 646 |
ACTIVE_EXP = ts;
|
| 647 |
updateExpText(ts);
|
| 648 |
+
closeModal('expModal');
|
| 649 |
+
// Update active class in modal
|
| 650 |
+
document.querySelectorAll('.opt-btn').forEach(b => {
|
| 651 |
+
if(b.innerText === new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'}))
|
| 652 |
+
b.classList.add('active');
|
| 653 |
+
else
|
| 654 |
+
b.classList.remove('active');
|
| 655 |
+
});
|
| 656 |
buildChain();
|
| 657 |
}
|
| 658 |
|
|
|
|
| 661 |
document.getElementById('selExpTxt').innerText = dStr;
|
| 662 |
}
|
| 663 |
|
|
|
|
|
|
|
| 664 |
function buildChain() {
|
| 665 |
const body = document.getElementById('chainBody');
|
| 666 |
const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
|
|
|
|
| 670 |
|
| 671 |
body.innerHTML = strikes.map(k => `
|
| 672 |
<div class="row" id="row-${k}" data-k="${k}">
|
| 673 |
+
<div class="side-col" id="ce-${k}"><span>-</span><span class="col-val">-</span></div>
|
| 674 |
<div class="mid-col">
|
| 675 |
<div class="visual-bar" id="bar-${k}"></div>
|
| 676 |
<span class="strike-txt">${k}</span>
|
| 677 |
<span class="delta-txt" id="val-${k}">-</span>
|
| 678 |
</div>
|
| 679 |
+
<div class="side-col" id="pe-${k}"><span class="col-val">-</span><span>-</span></div>
|
| 680 |
</div>
|
| 681 |
`).join('');
|
| 682 |
|
|
|
|
| 690 |
pollData();
|
| 691 |
}
|
| 692 |
|
| 693 |
+
// Main Data Fetching Logic
|
| 694 |
async function pollData() {
|
| 695 |
if(IS_POLLING || !CURRENT_ROOT) return;
|
| 696 |
IS_POLLING = true;
|
|
|
|
| 698 |
try {
|
| 699 |
let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
|
| 700 |
|
| 701 |
+
// Determine ATM and Viewport
|
| 702 |
+
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 703 |
+
|
| 704 |
+
if(kList.length > 0) {
|
| 705 |
+
let closestK = kList[0], minDiff = 999999;
|
| 706 |
+
const refPrice = SPOT_PRICE > 0 ? SPOT_PRICE : kList[Math.floor(kList.length/2)];
|
| 707 |
+
|
| 708 |
+
let atmIdx = 0;
|
| 709 |
+
kList.forEach((k, idx) => {
|
| 710 |
+
const diff = Math.abs(refPrice - k);
|
| 711 |
+
if(diff < minDiff) { minDiff = diff; closestK = k; atmIdx = idx; }
|
| 712 |
+
});
|
| 713 |
+
|
| 714 |
+
ATM_STRIKE = closestK;
|
| 715 |
+
|
| 716 |
+
// Highlight ATM
|
| 717 |
+
document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
|
| 718 |
+
const atmRow = document.getElementById(`row-${closestK}`);
|
| 719 |
+
if(atmRow) {
|
| 720 |
+
atmRow.classList.add('atm');
|
| 721 |
+
if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 722 |
}
|
| 723 |
+
|
| 724 |
+
// 1. Fetch data for VIEWPORT (what user sees)
|
| 725 |
+
const start = Math.max(0, atmIdx - 20);
|
| 726 |
+
const end = Math.min(kList.length, atmIdx + 20);
|
| 727 |
+
const viewKs = kList.slice(start, end);
|
| 728 |
+
|
| 729 |
+
// 2. Fetch data for NET FLOW CALCULATION (User Setting Range)
|
| 730 |
+
const rangeStart = Math.max(0, atmIdx - SETTING_RANGE);
|
| 731 |
+
const rangeEnd = Math.min(kList.length, atmIdx + SETTING_RANGE + 1);
|
| 732 |
+
const calcKs = kList.slice(rangeStart, rangeEnd);
|
| 733 |
+
|
| 734 |
+
// Combine keys
|
| 735 |
+
const allNeededKs = new Set([...viewKs, ...calcKs]);
|
| 736 |
+
|
| 737 |
+
OPT_SYMS.forEach(o => {
|
| 738 |
+
if(allNeededKs.has(o.k)) symbolsToFetch.push(o.s);
|
| 739 |
+
});
|
| 740 |
}
|
| 741 |
|
| 742 |
const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
|
| 743 |
if(uniqueSyms.length === 0) return;
|
| 744 |
|
| 745 |
const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
|
| 746 |
+
const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN }});
|
| 747 |
|
| 748 |
if(res.status === 401) { location.href = '/logout'; return; }
|
| 749 |
|
|
|
|
| 751 |
if(json.s === 'ok') {
|
| 752 |
updateUI(json.d);
|
| 753 |
const dot = document.getElementById('statusDot');
|
| 754 |
+
dot.classList.add('active');
|
| 755 |
+
dot.classList.remove('error');
|
|
|
|
| 756 |
}
|
| 757 |
+
} catch(e) {
|
| 758 |
+
console.error(e);
|
| 759 |
+
const dot = document.getElementById('statusDot');
|
| 760 |
+
dot.classList.remove('active');
|
| 761 |
+
dot.classList.add('error');
|
| 762 |
+
}
|
| 763 |
finally { IS_POLLING = false; }
|
| 764 |
}
|
| 765 |
|
| 766 |
function updateUI(data) {
|
| 767 |
+
// 1. Update Spot
|
| 768 |
if(data[SPOT_SYM] && typeof data[SPOT_SYM].ltp === 'number') {
|
| 769 |
SPOT_PRICE = data[SPOT_SYM].ltp;
|
| 770 |
document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
|
| 771 |
}
|
| 772 |
|
| 773 |
+
// 2. Update Futures
|
| 774 |
const futHTML = FUT_SYMS.map(f => {
|
| 775 |
const d = data[f.s];
|
|
|
|
| 776 |
if(!d || typeof d.ltp !== 'number') return '';
|
| 777 |
const dt = new Date(f.e*1000).toLocaleDateString('en-GB', {day:'numeric', month:'short'});
|
| 778 |
const tot = (d.totalbuyqty + d.totalsellqty) || 1;
|
|
|
|
| 786 |
<div style="width:${bPct}%; background:var(--green)"></div>
|
| 787 |
<div style="width:${100-bPct}%; background:var(--red)"></div>
|
| 788 |
</div>
|
| 789 |
+
<div style="display:flex; justify-content:space-between; font-size:10px; color:#666; margin-top:4px;">
|
| 790 |
<span>B: ${fmt(d.totalbuyqty)}</span> <span>S: ${fmt(d.totalsellqty)}</span>
|
| 791 |
</div>
|
| 792 |
</div>`;
|
| 793 |
}).join('');
|
| 794 |
if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
|
| 795 |
|
| 796 |
+
// 3. Process Options & Calculate Net Flow
|
| 797 |
const rowData = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
|
| 799 |
+
// We iterate through OPT_SYMS which is filtered by expiry
|
| 800 |
+
for(const item of OPT_SYMS) {
|
| 801 |
+
const d = data[item.s];
|
| 802 |
+
if(!d) continue;
|
| 803 |
+
|
| 804 |
+
if(!rowData[item.k]) rowData[item.k] = { ce:0, pe:0 };
|
| 805 |
+
|
| 806 |
+
if(item.t === 'CE') rowData[item.k].ce = d.totalsellqty;
|
| 807 |
+
if(item.t === 'PE') rowData[item.k].pe = d.totalsellqty;
|
| 808 |
+
}
|
| 809 |
+
|
| 810 |
+
// --- VISUALIZATION UPDATE ---
|
| 811 |
let maxDiff = 0;
|
| 812 |
for(const k in rowData) maxDiff = Math.max(maxDiff, Math.abs(rowData[k].pe - rowData[k].ce));
|
| 813 |
|
| 814 |
for(const k in rowData) {
|
| 815 |
const r = rowData[k];
|
| 816 |
const delta = r.pe - r.ce;
|
| 817 |
+
|
| 818 |
+
// Only update DOM if element exists (visible viewport)
|
| 819 |
const ceEl = document.getElementById(`ce-${k}`);
|
| 820 |
+
if(ceEl) {
|
| 821 |
+
ceEl.querySelector('.col-val').innerText = fmt(r.ce);
|
| 822 |
+
document.getElementById(`pe-${k}`).querySelector('.col-val').innerText = fmt(r.pe);
|
| 823 |
+
|
| 824 |
+
const bar = document.getElementById(`bar-${k}`);
|
| 825 |
+
const val = document.getElementById(`val-${k}`);
|
| 826 |
+
|
| 827 |
+
if(maxDiff > 0) {
|
| 828 |
+
const pct = (Math.abs(delta) / maxDiff) * 100;
|
| 829 |
+
if(delta >= 0) {
|
| 830 |
+
bar.style.background = 'var(--green-soft)';
|
| 831 |
+
bar.style.left = '50%';
|
| 832 |
+
bar.style.width = (pct/2) + '%';
|
| 833 |
+
val.style.color = 'var(--green)';
|
| 834 |
+
val.innerText = "+" + fmt(delta);
|
| 835 |
+
} else {
|
| 836 |
+
bar.style.background = 'var(--red-soft)';
|
| 837 |
+
bar.style.left = (50 - (pct/2)) + '%';
|
| 838 |
+
bar.style.width = (pct/2) + '%';
|
| 839 |
+
val.style.color = 'var(--red)';
|
| 840 |
+
val.innerText = fmt(delta);
|
| 841 |
+
}
|
| 842 |
+
}
|
| 843 |
+
}
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
// --- NET FLOW CALCULATION (ATM +/- Setting) ---
|
| 847 |
+
if(ATM_STRIKE > 0) {
|
| 848 |
+
let totalFlow = 0;
|
| 849 |
+
const kList = Object.keys(rowData).map(parseFloat).sort((a,b)=>a-b);
|
| 850 |
+
const atmIdx = kList.indexOf(ATM_STRIKE);
|
| 851 |
|
| 852 |
+
if(atmIdx !== -1) {
|
| 853 |
+
const start = Math.max(0, atmIdx - SETTING_RANGE);
|
| 854 |
+
const end = Math.min(kList.length, atmIdx + SETTING_RANGE + 1);
|
| 855 |
+
|
| 856 |
+
for(let i=start; i<end; i++) {
|
| 857 |
+
const k = kList[i];
|
| 858 |
+
if(rowData[k]) {
|
| 859 |
+
totalFlow += (rowData[k].pe - rowData[k].ce);
|
| 860 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 861 |
}
|
| 862 |
}
|
| 863 |
+
|
| 864 |
+
const nfEl = document.getElementById('totalNetFlow');
|
| 865 |
+
const nfSig = document.getElementById('nfSignal');
|
| 866 |
+
|
| 867 |
+
nfEl.innerText = (totalFlow > 0 ? "+" : "") + fmt(totalFlow);
|
| 868 |
+
nfEl.style.color = totalFlow >= 0 ? 'var(--green)' : 'var(--red)';
|
| 869 |
+
|
| 870 |
+
if(totalFlow > 0) {
|
| 871 |
+
nfSig.innerText = "BULLISH (PUTS DOMINANT)";
|
| 872 |
+
nfSig.style.color = "var(--green)";
|
| 873 |
+
} else {
|
| 874 |
+
nfSig.innerText = "BEARISH (CALLS DOMINANT)";
|
| 875 |
+
nfSig.style.color = "var(--red)";
|
| 876 |
+
}
|
| 877 |
}
|
| 878 |
}
|
| 879 |
|
|
|
|
| 883 |
if(abs>=10000000) return (n/10000000).toFixed(2)+'Cr';
|
| 884 |
if(abs>=100000) return (n/100000).toFixed(2)+'L';
|
| 885 |
if(abs>=1000) return (n/1000).toFixed(1)+'k';
|
| 886 |
+
return n.toLocaleString();
|
| 887 |
}
|
| 888 |
</script>
|
| 889 |
</body>
|