Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -17,7 +17,7 @@ from typing import Optional, List, Dict
|
|
| 17 |
# ==========================================
|
| 18 |
CLIENT_ID = "J4PNT5QI22-100"
|
| 19 |
SECRET_KEY = "ABRSWC1PMR"
|
| 20 |
-
# Ensure this matches your Space URL exactly
|
| 21 |
REDIRECT_URI = "https://topsecrettraders-depthchain.hf.space/callback"
|
| 22 |
|
| 23 |
CACHE_FILE = "master_cache.json"
|
|
@@ -28,10 +28,8 @@ app = FastAPI()
|
|
| 28 |
# 2. ROBUST DATA LOADER (THREAD SAFE)
|
| 29 |
# ==========================================
|
| 30 |
|
| 31 |
-
# Global In-Memory Database
|
| 32 |
-
# Structure: { "NIFTY": { "spot": "NSE:...", "data": [...] }, "RELIANCE": ... }
|
| 33 |
MASTER_DB = {}
|
| 34 |
-
SEARCH_INDEX = []
|
| 35 |
|
| 36 |
SYSTEM_STATUS = {
|
| 37 |
"ready": False,
|
|
@@ -39,7 +37,7 @@ SYSTEM_STATUS = {
|
|
| 39 |
}
|
| 40 |
UPDATE_LOCK = threading.Lock()
|
| 41 |
|
| 42 |
-
# Manual mapping for Index Spot symbols
|
| 43 |
INDEX_MAP = {
|
| 44 |
"NIFTY": "NSE:NIFTY50-INDEX",
|
| 45 |
"BANKNIFTY": "NSE:NIFTYBANK-INDEX",
|
|
@@ -48,27 +46,26 @@ INDEX_MAP = {
|
|
| 48 |
}
|
| 49 |
|
| 50 |
def get_root_symbol(desc: str, symbol: str) -> str:
|
| 51 |
-
"""Extracts
|
| 52 |
-
# Description format is usually "SYMBOL YY MMM STRIKE TYPE"
|
| 53 |
-
# e.g., "BANKNIFTY 24 JAN 48000 CE" -> BANKNIFTY
|
| 54 |
-
# e.g., "M&M 24 JAN ..." -> M&M
|
| 55 |
try:
|
|
|
|
|
|
|
| 56 |
parts = desc.strip().split(' ')
|
| 57 |
return parts[0].upper()
|
| 58 |
except:
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
def update_master_db():
|
| 62 |
-
"""
|
| 63 |
-
Downloads NSE and MCX F&O files, processes them, and builds a search index.
|
| 64 |
-
"""
|
| 65 |
global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
|
| 66 |
|
| 67 |
if not UPDATE_LOCK.acquire(blocking=False):
|
| 68 |
return
|
| 69 |
|
| 70 |
try:
|
| 71 |
-
SYSTEM_STATUS["message"] = "Downloading Master Data
|
| 72 |
print(">>> [BG TASK] STARTING DOWNLOAD...")
|
| 73 |
|
| 74 |
headers = { "User-Agent": "Mozilla/5.0" }
|
|
@@ -81,8 +78,8 @@ def update_master_db():
|
|
| 81 |
header=0, on_bad_lines='skip', storage_options=headers
|
| 82 |
)
|
| 83 |
|
| 84 |
-
# 2. Download MCX F&O
|
| 85 |
-
url_mcx = "https://public.fyers.in/sym_details/
|
| 86 |
df_mcx = pd.read_csv(
|
| 87 |
url_mcx, usecols=[0, 1, 8, 9, 13],
|
| 88 |
names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
|
|
@@ -97,86 +94,71 @@ def update_master_db():
|
|
| 97 |
df['Symbol'] = df['Symbol'].astype(str)
|
| 98 |
df['Desc'] = df['Desc'].astype(str)
|
| 99 |
|
| 100 |
-
# Temporary dictionary to build structure
|
| 101 |
temp_db = {}
|
| 102 |
|
| 103 |
-
count = 0
|
| 104 |
for _, row in df.iterrows():
|
| 105 |
sym = row['Symbol']
|
| 106 |
desc = row['Desc']
|
| 107 |
|
| 108 |
-
# Extract Root
|
| 109 |
root = get_root_symbol(desc, sym)
|
| 110 |
|
| 111 |
-
# Initialize Root Entry
|
| 112 |
if root not in temp_db:
|
| 113 |
-
# Determine Spot Symbol
|
| 114 |
spot_sym = ""
|
| 115 |
if root in INDEX_MAP:
|
| 116 |
spot_sym = INDEX_MAP[root]
|
| 117 |
elif sym.startswith("MCX"):
|
| 118 |
-
|
|
|
|
| 119 |
else:
|
| 120 |
-
# Stock Spot assumption
|
| 121 |
spot_sym = f"NSE:{root}-EQ"
|
| 122 |
|
| 123 |
-
temp_db[root] = {
|
| 124 |
-
"spot": spot_sym,
|
| 125 |
-
"items": []
|
| 126 |
-
}
|
| 127 |
|
| 128 |
-
# Parse Details
|
| 129 |
try:
|
| 130 |
exp = int(row['Expiry'])
|
| 131 |
except:
|
| 132 |
exp = 0
|
| 133 |
|
| 134 |
strike = 0.0
|
| 135 |
-
opt_type = "FUT"
|
| 136 |
|
|
|
|
| 137 |
if "CE" in sym or "PE" in sym:
|
| 138 |
-
# Logic: Parse Strike from Desc (e.g., "NIFTY 23 JAN 21500 CE")
|
| 139 |
parts = desc.strip().split(' ')
|
| 140 |
-
# Iterate backwards to find the number before CE/PE
|
| 141 |
if len(parts) >= 2:
|
| 142 |
try:
|
| 143 |
-
#
|
| 144 |
val = parts[-2]
|
|
|
|
| 145 |
val_clean = val.replace('.', '', 1)
|
| 146 |
if val_clean.isdigit():
|
| 147 |
strike = float(val)
|
| 148 |
-
opt_type = parts[-1]
|
| 149 |
except:
|
| 150 |
pass
|
| 151 |
|
| 152 |
temp_db[root]["items"].append({
|
| 153 |
-
"s": sym,
|
| 154 |
-
"e": exp, # Expiry Epoch
|
| 155 |
-
"k": strike, # Strike
|
| 156 |
-
"t": opt_type # Type: CE, PE, FUT
|
| 157 |
})
|
| 158 |
-
count += 1
|
| 159 |
|
| 160 |
-
#
|
| 161 |
final_db = {}
|
| 162 |
for root, data in temp_db.items():
|
|
|
|
| 163 |
sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
|
| 164 |
-
final_db[root] = {
|
| 165 |
-
"spot": data["spot"],
|
| 166 |
-
"items": sorted_items
|
| 167 |
-
}
|
| 168 |
|
| 169 |
-
# Update Global State
|
| 170 |
MASTER_DB = final_db
|
| 171 |
SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
|
| 172 |
|
| 173 |
-
# Save cache
|
| 174 |
with open(CACHE_FILE, "w") as f:
|
| 175 |
json.dump(final_db, f)
|
| 176 |
|
| 177 |
SYSTEM_STATUS["ready"] = True
|
| 178 |
SYSTEM_STATUS["message"] = "Ready"
|
| 179 |
-
print(f">>> [BG TASK] COMPLETE.
|
| 180 |
|
| 181 |
except Exception as e:
|
| 182 |
print(f">>> [BG TASK] ERROR: {e}")
|
|
@@ -189,17 +171,16 @@ def startup_event():
|
|
| 189 |
global MASTER_DB, SEARCH_INDEX
|
| 190 |
if os.path.exists(CACHE_FILE):
|
| 191 |
try:
|
| 192 |
-
print(">>> STARTUP: Loading
|
| 193 |
with open(CACHE_FILE, "r") as f:
|
| 194 |
MASTER_DB = json.load(f)
|
| 195 |
SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
|
| 196 |
SYSTEM_STATUS["ready"] = True
|
| 197 |
SYSTEM_STATUS["message"] = "Ready (Cached)"
|
| 198 |
except:
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
else:
|
| 203 |
thread = threading.Thread(target=update_master_db)
|
| 204 |
thread.start()
|
| 205 |
|
|
@@ -225,27 +206,27 @@ def home(access_token: Optional[str] = Cookie(None)):
|
|
| 225 |
</div>
|
| 226 |
""")
|
| 227 |
|
| 228 |
-
|
| 229 |
-
return HTMLResponse(
|
| 230 |
|
| 231 |
@app.get("/callback")
|
| 232 |
def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
|
| 233 |
final_code = auth_code if auth_code else code
|
| 234 |
-
if not final_code: return JSONResponse({"error": "No Code"})
|
| 235 |
|
| 236 |
app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
|
| 237 |
resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
|
| 238 |
"grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
|
| 239 |
})
|
| 240 |
-
data = resp.json()
|
| 241 |
|
|
|
|
| 242 |
if "access_token" in data:
|
| 243 |
full_token = f"{CLIENT_ID}:{data['access_token']}"
|
| 244 |
response = RedirectResponse(url="/")
|
| 245 |
response.set_cookie(key="access_token", value=full_token, httponly=True)
|
| 246 |
return response
|
| 247 |
else:
|
| 248 |
-
return HTMLResponse(f"Login Failed
|
| 249 |
|
| 250 |
@app.get("/logout")
|
| 251 |
def logout():
|
|
@@ -253,43 +234,41 @@ def logout():
|
|
| 253 |
response.delete_cookie("access_token")
|
| 254 |
return response
|
| 255 |
|
| 256 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 257 |
|
| 258 |
@app.get("/api/status")
|
| 259 |
def get_status():
|
| 260 |
return JSONResponse(SYSTEM_STATUS)
|
| 261 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
@app.get("/api/search")
|
| 263 |
def search_symbol(q: str = Query(..., min_length=1)):
|
| 264 |
-
"""
|
| 265 |
-
Real-time search against loaded master data.
|
| 266 |
-
"""
|
| 267 |
if not SYSTEM_STATUS["ready"]:
|
| 268 |
return JSONResponse([])
|
| 269 |
|
| 270 |
query = q.upper()
|
| 271 |
-
#
|
| 272 |
results = [x for x in SEARCH_INDEX if x.startswith(query)]
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
if len(results) < 5:
|
| 276 |
results += [x for x in SEARCH_INDEX if query in x and x not in results]
|
| 277 |
|
| 278 |
-
return JSONResponse(results[:20])
|
| 279 |
|
| 280 |
@app.get("/api/chain")
|
| 281 |
def get_chain(symbol: str):
|
| 282 |
-
"""
|
| 283 |
-
Returns the option chain structure for a specific root symbol.
|
| 284 |
-
"""
|
| 285 |
-
if not SYSTEM_STATUS["ready"]:
|
| 286 |
-
return JSONResponse({"error": "System not ready"}, status_code=503)
|
| 287 |
-
|
| 288 |
root = symbol.upper()
|
| 289 |
if root in MASTER_DB:
|
| 290 |
return JSONResponse(MASTER_DB[root])
|
| 291 |
-
|
| 292 |
-
return JSONResponse({"error": "Symbol not found"}, status_code=404)
|
| 293 |
|
| 294 |
@app.get("/api/refresh")
|
| 295 |
def force_refresh(background_tasks: BackgroundTasks):
|
|
@@ -307,7 +286,7 @@ HTML_TEMPLATE = """
|
|
| 307 |
<html lang="en">
|
| 308 |
<head>
|
| 309 |
<meta charset="UTF-8">
|
| 310 |
-
<title>PRO
|
| 311 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 312 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
| 313 |
<style>
|
|
@@ -325,7 +304,6 @@ HTML_TEMPLATE = """
|
|
| 325 |
.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; }
|
| 326 |
.btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
|
| 327 |
|
| 328 |
-
/* SEARCH BAR */
|
| 329 |
.search-box { position: relative; width: 300px; margin-right: 20px; }
|
| 330 |
.search-input { width: 100%; padding: 10px 15px; border: 2px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; transition: 0.3s; }
|
| 331 |
.search-input:focus { border-color: var(--accent); }
|
|
@@ -387,9 +365,8 @@ HTML_TEMPLATE = """
|
|
| 387 |
|
| 388 |
<div class="top-nav">
|
| 389 |
<div style="display:flex; align-items:center;">
|
| 390 |
-
<!-- SEARCH BAR -->
|
| 391 |
<div class="search-box">
|
| 392 |
-
<input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g., NIFTY,
|
| 393 |
<div class="search-dropdown" id="searchRes"></div>
|
| 394 |
</div>
|
| 395 |
|
|
@@ -433,33 +410,26 @@ HTML_TEMPLATE = """
|
|
| 433 |
<script>
|
| 434 |
const AUTH_TOKEN = "{{USER_TOKEN}}";
|
| 435 |
|
| 436 |
-
// State
|
| 437 |
let CURRENT_ROOT = "";
|
| 438 |
let CHAIN_DATA = null;
|
| 439 |
let ACTIVE_EXP = 0;
|
| 440 |
let SPOT_SYM = "";
|
| 441 |
let SPOT_PRICE = 0;
|
| 442 |
-
|
| 443 |
let FUT_SYMS = [];
|
| 444 |
let OPT_SYMS = [];
|
| 445 |
-
|
| 446 |
let POLLER = null;
|
| 447 |
let IS_POLLING = false;
|
| 448 |
let SEARCH_TIMER = null;
|
| 449 |
|
| 450 |
-
// === STARTUP ===
|
| 451 |
window.onload = () => {
|
| 452 |
if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
|
| 453 |
checkStatus();
|
| 454 |
|
| 455 |
-
// Setup Search
|
| 456 |
const inp = document.getElementById('searchInp');
|
| 457 |
inp.addEventListener('input', (e) => {
|
| 458 |
clearTimeout(SEARCH_TIMER);
|
| 459 |
SEARCH_TIMER = setTimeout(() => doSearch(e.target.value), 300);
|
| 460 |
});
|
| 461 |
-
|
| 462 |
-
// Hide dropdown on click outside
|
| 463 |
document.addEventListener('click', (e) => {
|
| 464 |
if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
|
| 465 |
});
|
|
@@ -471,7 +441,6 @@ HTML_TEMPLATE = """
|
|
| 471 |
const status = await res.json();
|
| 472 |
if(status.ready) {
|
| 473 |
document.getElementById('loader').style.display = 'none';
|
| 474 |
-
// Default load NIFTY if not loaded
|
| 475 |
if(!CURRENT_ROOT) loadSymbol('NIFTY');
|
| 476 |
} else {
|
| 477 |
document.getElementById('loaderMsg').innerText = status.message.toUpperCase();
|
|
@@ -481,21 +450,18 @@ HTML_TEMPLATE = """
|
|
| 481 |
}
|
| 482 |
|
| 483 |
async function refreshDB() {
|
| 484 |
-
if(!confirm("Download fresh Master Data
|
| 485 |
document.getElementById('loader').style.display = 'flex';
|
| 486 |
await fetch('/api/refresh');
|
| 487 |
checkStatus();
|
| 488 |
}
|
| 489 |
|
| 490 |
-
// === SEARCH LOGIC ===
|
| 491 |
async function doSearch(q) {
|
| 492 |
const resDiv = document.getElementById('searchRes');
|
| 493 |
if(q.length < 2) { resDiv.style.display = 'none'; return; }
|
| 494 |
-
|
| 495 |
try {
|
| 496 |
const res = await fetch(`/api/search?q=${q}`);
|
| 497 |
const data = await res.json();
|
| 498 |
-
|
| 499 |
resDiv.innerHTML = data.map(sym =>
|
| 500 |
`<div class="search-item" onclick="loadSymbol('${sym}')">${sym}</div>`
|
| 501 |
).join('');
|
|
@@ -503,7 +469,6 @@ HTML_TEMPLATE = """
|
|
| 503 |
} catch(e) { console.error(e); }
|
| 504 |
}
|
| 505 |
|
| 506 |
-
// === LOAD SYMBOL DATA ===
|
| 507 |
async function loadSymbol(root) {
|
| 508 |
document.getElementById('searchInp').value = root;
|
| 509 |
document.getElementById('searchRes').style.display = 'none';
|
|
@@ -519,13 +484,11 @@ HTML_TEMPLATE = """
|
|
| 519 |
CHAIN_DATA = data.items;
|
| 520 |
SPOT_SYM = data.spot;
|
| 521 |
|
| 522 |
-
// Sort futures
|
| 523 |
-
// Backend provides 't': 'FUT'
|
| 524 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 525 |
|
| 526 |
-
// If Spot symbol is MCX (placeholder), use nearest Future as spot
|
| 527 |
if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
|
| 528 |
-
SPOT_SYM = FUT_SYMS[0].s;
|
| 529 |
}
|
| 530 |
|
| 531 |
document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ')) ? "SPOT" : "FUT REF";
|
|
@@ -534,7 +497,7 @@ HTML_TEMPLATE = """
|
|
| 534 |
document.getElementById('loader').style.display = 'none';
|
| 535 |
|
| 536 |
} catch(e) {
|
| 537 |
-
alert("Error
|
| 538 |
document.getElementById('loader').style.display = 'none';
|
| 539 |
}
|
| 540 |
}
|
|
@@ -545,13 +508,11 @@ HTML_TEMPLATE = """
|
|
| 545 |
const today = Math.floor(Date.now()/1000) - 86400;
|
| 546 |
|
| 547 |
const validExps = exps.filter(ts => ts > today);
|
| 548 |
-
if(validExps.length === 0) { alert("No active expiries
|
| 549 |
|
| 550 |
-
// Auto select nearest
|
| 551 |
ACTIVE_EXP = validExps[0];
|
| 552 |
updateExpText(ACTIVE_EXP);
|
| 553 |
|
| 554 |
-
// Build Grid
|
| 555 |
const grid = document.getElementById('expGrid');
|
| 556 |
grid.innerHTML = validExps.map(ts => {
|
| 557 |
const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
|
|
@@ -575,11 +536,8 @@ HTML_TEMPLATE = """
|
|
| 575 |
|
| 576 |
function closeModal() { document.getElementById('expModal').style.display = 'none'; }
|
| 577 |
|
| 578 |
-
// === BUILD UI ===
|
| 579 |
function buildChain() {
|
| 580 |
const body = document.getElementById('chainBody');
|
| 581 |
-
|
| 582 |
-
// Filter options for active expiry
|
| 583 |
const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
|
| 584 |
OPT_SYMS = relevant;
|
| 585 |
|
|
@@ -597,7 +555,7 @@ HTML_TEMPLATE = """
|
|
| 597 |
</div>
|
| 598 |
`).join('');
|
| 599 |
|
| 600 |
-
window.scrolled = false;
|
| 601 |
restartPoller();
|
| 602 |
}
|
| 603 |
|
|
@@ -607,27 +565,21 @@ HTML_TEMPLATE = """
|
|
| 607 |
pollData();
|
| 608 |
}
|
| 609 |
|
| 610 |
-
// === POLLING ===
|
| 611 |
async function pollData() {
|
| 612 |
if(IS_POLLING || !CURRENT_ROOT) return;
|
| 613 |
IS_POLLING = true;
|
| 614 |
|
| 615 |
try {
|
| 616 |
-
// Logic to fetch only visible strikes + spot + futures
|
| 617 |
let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
|
| 618 |
|
| 619 |
-
// If we have a spot price, filter strikes around it
|
| 620 |
if(SPOT_PRICE > 0) {
|
| 621 |
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 622 |
-
|
| 623 |
-
// Find closest strike index
|
| 624 |
let closestK = kList[0], minDiff = 999999;
|
| 625 |
kList.forEach(k => {
|
| 626 |
const diff = Math.abs(SPOT_PRICE - k);
|
| 627 |
if(diff < minDiff) { minDiff = diff; closestK = k; }
|
| 628 |
});
|
| 629 |
|
| 630 |
-
// Highlight ATM
|
| 631 |
document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
|
| 632 |
const atmRow = document.getElementById(`row-${closestK}`);
|
| 633 |
if(atmRow) {
|
|
@@ -635,32 +587,26 @@ HTML_TEMPLATE = """
|
|
| 635 |
if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
|
| 636 |
}
|
| 637 |
|
| 638 |
-
// Get range of strikes to fetch (ATM +/- 12)
|
| 639 |
const centerIdx = kList.indexOf(closestK);
|
| 640 |
const start = Math.max(0, centerIdx - 12);
|
| 641 |
const end = Math.min(kList.length, centerIdx + 12);
|
| 642 |
const viewKs = kList.slice(start, end);
|
| 643 |
|
| 644 |
-
// Add option symbols to fetch list
|
| 645 |
OPT_SYMS.forEach(o => {
|
| 646 |
if(viewKs.includes(o.k)) symbolsToFetch.push(o.s);
|
| 647 |
});
|
| 648 |
} else {
|
| 649 |
-
// First run, fetch middle strikes
|
| 650 |
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 651 |
const mid = Math.floor(kList.length/2);
|
| 652 |
const viewKs = kList.slice(Math.max(0, mid-10), mid+10);
|
| 653 |
OPT_SYMS.forEach(o => { if(viewKs.includes(o.k)) symbolsToFetch.push(o.s); });
|
| 654 |
}
|
| 655 |
|
| 656 |
-
const uniqueSyms = [...new Set(symbolsToFetch)];
|
| 657 |
if(uniqueSyms.length === 0) return;
|
| 658 |
|
| 659 |
const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
|
| 660 |
-
|
| 661 |
-
const res = await fetch(url, {
|
| 662 |
-
headers: { 'Authorization': AUTH_TOKEN, 'Content-Type': 'application/json' }
|
| 663 |
-
});
|
| 664 |
|
| 665 |
if(res.status === 401) { location.href = '/logout'; return; }
|
| 666 |
|
|
@@ -672,22 +618,16 @@ HTML_TEMPLATE = """
|
|
| 672 |
dot.classList.add('live');
|
| 673 |
setTimeout(()=>dot.classList.remove('live'), 500);
|
| 674 |
}
|
| 675 |
-
} catch(e) {
|
| 676 |
-
|
| 677 |
-
document.getElementById('statusDot').style.background = 'var(--red)';
|
| 678 |
-
} finally {
|
| 679 |
-
IS_POLLING = false;
|
| 680 |
-
}
|
| 681 |
}
|
| 682 |
|
| 683 |
function updateUI(data) {
|
| 684 |
-
// 1. Spot
|
| 685 |
if(data[SPOT_SYM]) {
|
| 686 |
SPOT_PRICE = data[SPOT_SYM].ltp;
|
| 687 |
document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
|
| 688 |
}
|
| 689 |
|
| 690 |
-
// 2. Futures
|
| 691 |
const futHTML = FUT_SYMS.map(f => {
|
| 692 |
const d = data[f.s];
|
| 693 |
if(!d) return '';
|
|
@@ -710,13 +650,10 @@ HTML_TEMPLATE = """
|
|
| 710 |
}).join('');
|
| 711 |
if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
|
| 712 |
|
| 713 |
-
// 3. Chain
|
| 714 |
const rowData = {};
|
| 715 |
for(const [sym, d] of Object.entries(data)) {
|
| 716 |
-
// Find which option this is
|
| 717 |
const meta = OPT_SYMS.find(x => x.s === sym);
|
| 718 |
if(!meta) continue;
|
| 719 |
-
|
| 720 |
if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
|
| 721 |
if(meta.t === 'CE') rowData[meta.k].ce = d.totalsellqty;
|
| 722 |
if(meta.t === 'PE') rowData[meta.k].pe = d.totalsellqty;
|
|
@@ -728,17 +665,13 @@ HTML_TEMPLATE = """
|
|
| 728 |
for(const k in rowData) {
|
| 729 |
const r = rowData[k];
|
| 730 |
const delta = r.pe - r.ce;
|
| 731 |
-
|
| 732 |
-
// Update Text
|
| 733 |
const ceEl = document.getElementById(`ce-${k}`);
|
| 734 |
const peEl = document.getElementById(`pe-${k}`);
|
| 735 |
if(ceEl) ceEl.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
|
| 736 |
if(peEl) peEl.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
|
| 737 |
|
| 738 |
-
// Update Bars
|
| 739 |
const bar = document.getElementById(`bar-${k}`);
|
| 740 |
const val = document.getElementById(`val-${k}`);
|
| 741 |
-
|
| 742 |
if(bar && maxDiff > 0) {
|
| 743 |
const pct = (Math.abs(delta) / maxDiff) * 100;
|
| 744 |
if(delta >= 0) {
|
|
|
|
| 17 |
# ==========================================
|
| 18 |
CLIENT_ID = "J4PNT5QI22-100"
|
| 19 |
SECRET_KEY = "ABRSWC1PMR"
|
| 20 |
+
# IMPORTANT: Ensure this matches your Space URL exactly
|
| 21 |
REDIRECT_URI = "https://topsecrettraders-depthchain.hf.space/callback"
|
| 22 |
|
| 23 |
CACHE_FILE = "master_cache.json"
|
|
|
|
| 28 |
# 2. ROBUST DATA LOADER (THREAD SAFE)
|
| 29 |
# ==========================================
|
| 30 |
|
|
|
|
|
|
|
| 31 |
MASTER_DB = {}
|
| 32 |
+
SEARCH_INDEX = []
|
| 33 |
|
| 34 |
SYSTEM_STATUS = {
|
| 35 |
"ready": False,
|
|
|
|
| 37 |
}
|
| 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",
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
def get_root_symbol(desc: str, symbol: str) -> str:
|
| 49 |
+
"""Extracts root symbol (e.g. 'RELIANCE', 'CRUDEOIL') from description."""
|
|
|
|
|
|
|
|
|
|
| 50 |
try:
|
| 51 |
+
# MCX often has description "CRUDEOIL 19 JAN 6000 CE"
|
| 52 |
+
# NSE often has "RELIANCE 24 JAN..."
|
| 53 |
parts = desc.strip().split(' ')
|
| 54 |
return parts[0].upper()
|
| 55 |
except:
|
| 56 |
+
# Fallback to symbol parsing
|
| 57 |
+
if ':' in symbol:
|
| 58 |
+
return symbol.split(':')[1]
|
| 59 |
+
return symbol
|
| 60 |
|
| 61 |
def update_master_db():
|
|
|
|
|
|
|
|
|
|
| 62 |
global SYSTEM_STATUS, MASTER_DB, SEARCH_INDEX
|
| 63 |
|
| 64 |
if not UPDATE_LOCK.acquire(blocking=False):
|
| 65 |
return
|
| 66 |
|
| 67 |
try:
|
| 68 |
+
SYSTEM_STATUS["message"] = "Downloading Master Data..."
|
| 69 |
print(">>> [BG TASK] STARTING DOWNLOAD...")
|
| 70 |
|
| 71 |
headers = { "User-Agent": "Mozilla/5.0" }
|
|
|
|
| 78 |
header=0, on_bad_lines='skip', storage_options=headers
|
| 79 |
)
|
| 80 |
|
| 81 |
+
# 2. Download MCX F&O (Correct URL is MCX_COM.csv)
|
| 82 |
+
url_mcx = "https://public.fyers.in/sym_details/MCX_COM.csv"
|
| 83 |
df_mcx = pd.read_csv(
|
| 84 |
url_mcx, usecols=[0, 1, 8, 9, 13],
|
| 85 |
names=['Token', 'Desc', 'Expiry', 'Symbol', 'Inst'],
|
|
|
|
| 94 |
df['Symbol'] = df['Symbol'].astype(str)
|
| 95 |
df['Desc'] = df['Desc'].astype(str)
|
| 96 |
|
|
|
|
| 97 |
temp_db = {}
|
| 98 |
|
|
|
|
| 99 |
for _, row in df.iterrows():
|
| 100 |
sym = row['Symbol']
|
| 101 |
desc = row['Desc']
|
| 102 |
|
|
|
|
| 103 |
root = get_root_symbol(desc, sym)
|
| 104 |
|
| 105 |
+
# Initialize Root Entry
|
| 106 |
if root not in temp_db:
|
|
|
|
| 107 |
spot_sym = ""
|
| 108 |
if root in INDEX_MAP:
|
| 109 |
spot_sym = INDEX_MAP[root]
|
| 110 |
elif sym.startswith("MCX"):
|
| 111 |
+
# MCX has no "Index Spot", usually we track the Future
|
| 112 |
+
spot_sym = "MCX"
|
| 113 |
else:
|
|
|
|
| 114 |
spot_sym = f"NSE:{root}-EQ"
|
| 115 |
|
| 116 |
+
temp_db[root] = { "spot": spot_sym, "items": [] }
|
|
|
|
|
|
|
|
|
|
| 117 |
|
|
|
|
| 118 |
try:
|
| 119 |
exp = int(row['Expiry'])
|
| 120 |
except:
|
| 121 |
exp = 0
|
| 122 |
|
| 123 |
strike = 0.0
|
| 124 |
+
opt_type = "FUT"
|
| 125 |
|
| 126 |
+
# Parse Strike & Option Type
|
| 127 |
if "CE" in sym or "PE" in sym:
|
|
|
|
| 128 |
parts = desc.strip().split(' ')
|
|
|
|
| 129 |
if len(parts) >= 2:
|
| 130 |
try:
|
| 131 |
+
# Logic: "NIFTY 23 JAN 21500 CE" -> 21500 is 2nd last
|
| 132 |
val = parts[-2]
|
| 133 |
+
# Remove one decimal point to check isdigit
|
| 134 |
val_clean = val.replace('.', '', 1)
|
| 135 |
if val_clean.isdigit():
|
| 136 |
strike = float(val)
|
| 137 |
+
opt_type = parts[-1]
|
| 138 |
except:
|
| 139 |
pass
|
| 140 |
|
| 141 |
temp_db[root]["items"].append({
|
| 142 |
+
"s": sym, "e": exp, "k": strike, "t": opt_type
|
|
|
|
|
|
|
|
|
|
| 143 |
})
|
|
|
|
| 144 |
|
| 145 |
+
# Final Sort
|
| 146 |
final_db = {}
|
| 147 |
for root, data in temp_db.items():
|
| 148 |
+
# Sort by Expiry (asc) then Strike (asc)
|
| 149 |
sorted_items = sorted(data["items"], key=lambda x: (x['e'], x['k']))
|
| 150 |
+
final_db[root] = { "spot": data["spot"], "items": sorted_items }
|
|
|
|
|
|
|
|
|
|
| 151 |
|
|
|
|
| 152 |
MASTER_DB = final_db
|
| 153 |
SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
|
| 154 |
|
| 155 |
+
# Save cache
|
| 156 |
with open(CACHE_FILE, "w") as f:
|
| 157 |
json.dump(final_db, f)
|
| 158 |
|
| 159 |
SYSTEM_STATUS["ready"] = True
|
| 160 |
SYSTEM_STATUS["message"] = "Ready"
|
| 161 |
+
print(f">>> [BG TASK] COMPLETE. Loaded {len(SEARCH_INDEX)} symbols.")
|
| 162 |
|
| 163 |
except Exception as e:
|
| 164 |
print(f">>> [BG TASK] ERROR: {e}")
|
|
|
|
| 171 |
global MASTER_DB, SEARCH_INDEX
|
| 172 |
if os.path.exists(CACHE_FILE):
|
| 173 |
try:
|
| 174 |
+
print(">>> STARTUP: Loading Cache...")
|
| 175 |
with open(CACHE_FILE, "r") as f:
|
| 176 |
MASTER_DB = json.load(f)
|
| 177 |
SEARCH_INDEX = sorted(list(MASTER_DB.keys()))
|
| 178 |
SYSTEM_STATUS["ready"] = True
|
| 179 |
SYSTEM_STATUS["message"] = "Ready (Cached)"
|
| 180 |
except:
|
| 181 |
+
pass
|
| 182 |
+
|
| 183 |
+
if not SYSTEM_STATUS["ready"]:
|
|
|
|
| 184 |
thread = threading.Thread(target=update_master_db)
|
| 185 |
thread.start()
|
| 186 |
|
|
|
|
| 206 |
</div>
|
| 207 |
""")
|
| 208 |
|
| 209 |
+
# Inject token
|
| 210 |
+
return HTMLResponse(HTML_TEMPLATE.replace("{{USER_TOKEN}}", access_token))
|
| 211 |
|
| 212 |
@app.get("/callback")
|
| 213 |
def callback(auth_code: Optional[str] = None, code: Optional[str] = None):
|
| 214 |
final_code = auth_code if auth_code else code
|
| 215 |
+
if not final_code: return JSONResponse({"error": "No Code Provided"})
|
| 216 |
|
| 217 |
app_id_hash = hashlib.sha256(f"{CLIENT_ID}:{SECRET_KEY}".encode()).hexdigest()
|
| 218 |
resp = requests.post("https://api-t1.fyers.in/api/v3/validate-authcode", json={
|
| 219 |
"grant_type": "authorization_code", "appIdHash": app_id_hash, "code": final_code
|
| 220 |
})
|
|
|
|
| 221 |
|
| 222 |
+
data = resp.json()
|
| 223 |
if "access_token" in data:
|
| 224 |
full_token = f"{CLIENT_ID}:{data['access_token']}"
|
| 225 |
response = RedirectResponse(url="/")
|
| 226 |
response.set_cookie(key="access_token", value=full_token, httponly=True)
|
| 227 |
return response
|
| 228 |
else:
|
| 229 |
+
return HTMLResponse(f"<h3>Login Failed</h3><pre>{json.dumps(data, indent=2)}</pre>")
|
| 230 |
|
| 231 |
@app.get("/logout")
|
| 232 |
def logout():
|
|
|
|
| 234 |
response.delete_cookie("access_token")
|
| 235 |
return response
|
| 236 |
|
| 237 |
+
@app.get("/favicon.ico")
|
| 238 |
+
def favicon():
|
| 239 |
+
return Response(status_code=204) # No content, stops 404 errors
|
| 240 |
+
|
| 241 |
+
# --- API ---
|
| 242 |
|
| 243 |
@app.get("/api/status")
|
| 244 |
def get_status():
|
| 245 |
return JSONResponse(SYSTEM_STATUS)
|
| 246 |
|
| 247 |
+
# COMPATIBILITY ROUTE: Returns empty list to stop 404s on old clients
|
| 248 |
+
@app.get("/api/init")
|
| 249 |
+
def legacy_init():
|
| 250 |
+
return JSONResponse([])
|
| 251 |
+
|
| 252 |
@app.get("/api/search")
|
| 253 |
def search_symbol(q: str = Query(..., min_length=1)):
|
|
|
|
|
|
|
|
|
|
| 254 |
if not SYSTEM_STATUS["ready"]:
|
| 255 |
return JSONResponse([])
|
| 256 |
|
| 257 |
query = q.upper()
|
| 258 |
+
# 1. Starts with
|
| 259 |
results = [x for x in SEARCH_INDEX if x.startswith(query)]
|
| 260 |
+
# 2. Contains (if few results)
|
| 261 |
+
if len(results) < 10:
|
|
|
|
| 262 |
results += [x for x in SEARCH_INDEX if query in x and x not in results]
|
| 263 |
|
| 264 |
+
return JSONResponse(results[:20])
|
| 265 |
|
| 266 |
@app.get("/api/chain")
|
| 267 |
def get_chain(symbol: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
root = symbol.upper()
|
| 269 |
if root in MASTER_DB:
|
| 270 |
return JSONResponse(MASTER_DB[root])
|
| 271 |
+
return JSONResponse({"error": "Symbol not found"}, status_code=404)
|
|
|
|
| 272 |
|
| 273 |
@app.get("/api/refresh")
|
| 274 |
def force_refresh(background_tasks: BackgroundTasks):
|
|
|
|
| 286 |
<html lang="en">
|
| 287 |
<head>
|
| 288 |
<meta charset="UTF-8">
|
| 289 |
+
<title>PRO CHAIN (V2)</title>
|
| 290 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 291 |
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
| 292 |
<style>
|
|
|
|
| 304 |
.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; }
|
| 305 |
.btn:hover { border-color: var(--accent); color: var(--accent); background:#f0f7ff; }
|
| 306 |
|
|
|
|
| 307 |
.search-box { position: relative; width: 300px; margin-right: 20px; }
|
| 308 |
.search-input { width: 100%; padding: 10px 15px; border: 2px solid var(--border); border-radius: 6px; font-family: 'JetBrains Mono', monospace; font-weight: bold; font-size: 14px; outline: none; transition: 0.3s; }
|
| 309 |
.search-input:focus { border-color: var(--accent); }
|
|
|
|
| 365 |
|
| 366 |
<div class="top-nav">
|
| 367 |
<div style="display:flex; align-items:center;">
|
|
|
|
| 368 |
<div class="search-box">
|
| 369 |
+
<input type="text" class="search-input" id="searchInp" placeholder="SEARCH (e.g., NIFTY, CRUDE)..." autocomplete="off">
|
| 370 |
<div class="search-dropdown" id="searchRes"></div>
|
| 371 |
</div>
|
| 372 |
|
|
|
|
| 410 |
<script>
|
| 411 |
const AUTH_TOKEN = "{{USER_TOKEN}}";
|
| 412 |
|
|
|
|
| 413 |
let CURRENT_ROOT = "";
|
| 414 |
let CHAIN_DATA = null;
|
| 415 |
let ACTIVE_EXP = 0;
|
| 416 |
let SPOT_SYM = "";
|
| 417 |
let SPOT_PRICE = 0;
|
|
|
|
| 418 |
let FUT_SYMS = [];
|
| 419 |
let OPT_SYMS = [];
|
|
|
|
| 420 |
let POLLER = null;
|
| 421 |
let IS_POLLING = false;
|
| 422 |
let SEARCH_TIMER = null;
|
| 423 |
|
|
|
|
| 424 |
window.onload = () => {
|
| 425 |
if(AUTH_TOKEN.indexOf("{{") !== -1) { alert("Auth Error"); return; }
|
| 426 |
checkStatus();
|
| 427 |
|
|
|
|
| 428 |
const inp = document.getElementById('searchInp');
|
| 429 |
inp.addEventListener('input', (e) => {
|
| 430 |
clearTimeout(SEARCH_TIMER);
|
| 431 |
SEARCH_TIMER = setTimeout(() => doSearch(e.target.value), 300);
|
| 432 |
});
|
|
|
|
|
|
|
| 433 |
document.addEventListener('click', (e) => {
|
| 434 |
if(!e.target.closest('.search-box')) document.getElementById('searchRes').style.display = 'none';
|
| 435 |
});
|
|
|
|
| 441 |
const status = await res.json();
|
| 442 |
if(status.ready) {
|
| 443 |
document.getElementById('loader').style.display = 'none';
|
|
|
|
| 444 |
if(!CURRENT_ROOT) loadSymbol('NIFTY');
|
| 445 |
} else {
|
| 446 |
document.getElementById('loaderMsg').innerText = status.message.toUpperCase();
|
|
|
|
| 450 |
}
|
| 451 |
|
| 452 |
async function refreshDB() {
|
| 453 |
+
if(!confirm("Download fresh Master Data? This takes ~10s.")) return;
|
| 454 |
document.getElementById('loader').style.display = 'flex';
|
| 455 |
await fetch('/api/refresh');
|
| 456 |
checkStatus();
|
| 457 |
}
|
| 458 |
|
|
|
|
| 459 |
async function doSearch(q) {
|
| 460 |
const resDiv = document.getElementById('searchRes');
|
| 461 |
if(q.length < 2) { resDiv.style.display = 'none'; return; }
|
|
|
|
| 462 |
try {
|
| 463 |
const res = await fetch(`/api/search?q=${q}`);
|
| 464 |
const data = await res.json();
|
|
|
|
| 465 |
resDiv.innerHTML = data.map(sym =>
|
| 466 |
`<div class="search-item" onclick="loadSymbol('${sym}')">${sym}</div>`
|
| 467 |
).join('');
|
|
|
|
| 469 |
} catch(e) { console.error(e); }
|
| 470 |
}
|
| 471 |
|
|
|
|
| 472 |
async function loadSymbol(root) {
|
| 473 |
document.getElementById('searchInp').value = root;
|
| 474 |
document.getElementById('searchRes').style.display = 'none';
|
|
|
|
| 484 |
CHAIN_DATA = data.items;
|
| 485 |
SPOT_SYM = data.spot;
|
| 486 |
|
| 487 |
+
// Sort futures: Filter items with type 'FUT'
|
|
|
|
| 488 |
FUT_SYMS = CHAIN_DATA.filter(x => x.t === 'FUT').sort((a,b) => a.e - b.e).slice(0, 3);
|
| 489 |
|
|
|
|
| 490 |
if(SPOT_SYM === "MCX" && FUT_SYMS.length > 0) {
|
| 491 |
+
SPOT_SYM = FUT_SYMS[0].s; // Use Near Future as Spot for MCX
|
| 492 |
}
|
| 493 |
|
| 494 |
document.getElementById('spotLabel').innerText = (SPOT_SYM.includes('INDEX') || SPOT_SYM.includes('-EQ')) ? "SPOT" : "FUT REF";
|
|
|
|
| 497 |
document.getElementById('loader').style.display = 'none';
|
| 498 |
|
| 499 |
} catch(e) {
|
| 500 |
+
alert("Error: " + e.message);
|
| 501 |
document.getElementById('loader').style.display = 'none';
|
| 502 |
}
|
| 503 |
}
|
|
|
|
| 508 |
const today = Math.floor(Date.now()/1000) - 86400;
|
| 509 |
|
| 510 |
const validExps = exps.filter(ts => ts > today);
|
| 511 |
+
if(validExps.length === 0) { alert("No active expiries."); return; }
|
| 512 |
|
|
|
|
| 513 |
ACTIVE_EXP = validExps[0];
|
| 514 |
updateExpText(ACTIVE_EXP);
|
| 515 |
|
|
|
|
| 516 |
const grid = document.getElementById('expGrid');
|
| 517 |
grid.innerHTML = validExps.map(ts => {
|
| 518 |
const dStr = new Date(ts*1000).toLocaleDateString('en-GB', {day:'2-digit', month:'short'});
|
|
|
|
| 536 |
|
| 537 |
function closeModal() { document.getElementById('expModal').style.display = 'none'; }
|
| 538 |
|
|
|
|
| 539 |
function buildChain() {
|
| 540 |
const body = document.getElementById('chainBody');
|
|
|
|
|
|
|
| 541 |
const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
|
| 542 |
OPT_SYMS = relevant;
|
| 543 |
|
|
|
|
| 555 |
</div>
|
| 556 |
`).join('');
|
| 557 |
|
| 558 |
+
window.scrolled = false;
|
| 559 |
restartPoller();
|
| 560 |
}
|
| 561 |
|
|
|
|
| 565 |
pollData();
|
| 566 |
}
|
| 567 |
|
|
|
|
| 568 |
async function pollData() {
|
| 569 |
if(IS_POLLING || !CURRENT_ROOT) return;
|
| 570 |
IS_POLLING = true;
|
| 571 |
|
| 572 |
try {
|
|
|
|
| 573 |
let symbolsToFetch = [SPOT_SYM, ...FUT_SYMS.map(x => x.s)];
|
| 574 |
|
|
|
|
| 575 |
if(SPOT_PRICE > 0) {
|
| 576 |
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
|
|
|
|
|
|
| 577 |
let closestK = kList[0], minDiff = 999999;
|
| 578 |
kList.forEach(k => {
|
| 579 |
const diff = Math.abs(SPOT_PRICE - k);
|
| 580 |
if(diff < minDiff) { minDiff = diff; closestK = k; }
|
| 581 |
});
|
| 582 |
|
|
|
|
| 583 |
document.querySelectorAll('.atm').forEach(e => e.classList.remove('atm'));
|
| 584 |
const atmRow = document.getElementById(`row-${closestK}`);
|
| 585 |
if(atmRow) {
|
|
|
|
| 587 |
if(!window.scrolled) { atmRow.scrollIntoView({block:'center'}); window.scrolled = true; }
|
| 588 |
}
|
| 589 |
|
|
|
|
| 590 |
const centerIdx = kList.indexOf(closestK);
|
| 591 |
const start = Math.max(0, centerIdx - 12);
|
| 592 |
const end = Math.min(kList.length, centerIdx + 12);
|
| 593 |
const viewKs = kList.slice(start, end);
|
| 594 |
|
|
|
|
| 595 |
OPT_SYMS.forEach(o => {
|
| 596 |
if(viewKs.includes(o.k)) symbolsToFetch.push(o.s);
|
| 597 |
});
|
| 598 |
} else {
|
|
|
|
| 599 |
const kList = Array.from(document.querySelectorAll('.row')).map(r => parseFloat(r.dataset.k));
|
| 600 |
const mid = Math.floor(kList.length/2);
|
| 601 |
const viewKs = kList.slice(Math.max(0, mid-10), mid+10);
|
| 602 |
OPT_SYMS.forEach(o => { if(viewKs.includes(o.k)) symbolsToFetch.push(o.s); });
|
| 603 |
}
|
| 604 |
|
| 605 |
+
const uniqueSyms = [...new Set(symbolsToFetch)].filter(s => s && s.length > 2);
|
| 606 |
if(uniqueSyms.length === 0) return;
|
| 607 |
|
| 608 |
const url = `https://api-t1.fyers.in/data/depth?symbol=${uniqueSyms.join(',')}&ohlcv_flag=1`;
|
| 609 |
+
const res = await fetch(url, { headers: { 'Authorization': AUTH_TOKEN, 'Content-Type': 'application/json' }});
|
|
|
|
|
|
|
|
|
|
| 610 |
|
| 611 |
if(res.status === 401) { location.href = '/logout'; return; }
|
| 612 |
|
|
|
|
| 618 |
dot.classList.add('live');
|
| 619 |
setTimeout(()=>dot.classList.remove('live'), 500);
|
| 620 |
}
|
| 621 |
+
} catch(e) { console.error(e); document.getElementById('statusDot').style.background = 'var(--red)'; }
|
| 622 |
+
finally { IS_POLLING = false; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
}
|
| 624 |
|
| 625 |
function updateUI(data) {
|
|
|
|
| 626 |
if(data[SPOT_SYM]) {
|
| 627 |
SPOT_PRICE = data[SPOT_SYM].ltp;
|
| 628 |
document.getElementById('spotPrice').innerText = SPOT_PRICE.toFixed(2);
|
| 629 |
}
|
| 630 |
|
|
|
|
| 631 |
const futHTML = FUT_SYMS.map(f => {
|
| 632 |
const d = data[f.s];
|
| 633 |
if(!d) return '';
|
|
|
|
| 650 |
}).join('');
|
| 651 |
if(futHTML) document.getElementById('futBody').innerHTML = futHTML;
|
| 652 |
|
|
|
|
| 653 |
const rowData = {};
|
| 654 |
for(const [sym, d] of Object.entries(data)) {
|
|
|
|
| 655 |
const meta = OPT_SYMS.find(x => x.s === sym);
|
| 656 |
if(!meta) continue;
|
|
|
|
| 657 |
if(!rowData[meta.k]) rowData[meta.k] = { ce:0, pe:0 };
|
| 658 |
if(meta.t === 'CE') rowData[meta.k].ce = d.totalsellqty;
|
| 659 |
if(meta.t === 'PE') rowData[meta.k].pe = d.totalsellqty;
|
|
|
|
| 665 |
for(const k in rowData) {
|
| 666 |
const r = rowData[k];
|
| 667 |
const delta = r.pe - r.ce;
|
|
|
|
|
|
|
| 668 |
const ceEl = document.getElementById(`ce-${k}`);
|
| 669 |
const peEl = document.getElementById(`pe-${k}`);
|
| 670 |
if(ceEl) ceEl.innerHTML = `<span>S: <strong>${fmt(r.ce)}</strong></span>`;
|
| 671 |
if(peEl) peEl.innerHTML = `<span>S: <strong>${fmt(r.pe)}</strong></span>`;
|
| 672 |
|
|
|
|
| 673 |
const bar = document.getElementById(`bar-${k}`);
|
| 674 |
const val = document.getElementById(`val-${k}`);
|
|
|
|
| 675 |
if(bar && maxDiff > 0) {
|
| 676 |
const pct = (Math.abs(delta) / maxDiff) * 100;
|
| 677 |
if(delta >= 0) {
|