Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -241,7 +241,7 @@ def decompress_payload(rich_data: List[Dict]) -> Dict:
|
|
| 241 |
"totalsellqty": item.get('q', [0,0])[1],
|
| 242 |
"oi": item.get('o', [0,0])[0],
|
| 243 |
"oipercent": item.get('o', [0,0])[1],
|
| 244 |
-
"expiry": expiry_ts
|
| 245 |
}
|
| 246 |
return fyers_data
|
| 247 |
|
|
@@ -328,10 +328,9 @@ def get_hist_dates(symbol: str):
|
|
| 328 |
try:
|
| 329 |
dates = set()
|
| 330 |
offset = 0
|
| 331 |
-
limit = 1000
|
| 332 |
|
| 333 |
while True:
|
| 334 |
-
# Loop to fetch ALL records
|
| 335 |
res = supabase.table('snapshots')\
|
| 336 |
.select('time_signature')\
|
| 337 |
.eq('symbol', symbol.upper())\
|
|
@@ -352,7 +351,6 @@ def get_hist_dates(symbol: str):
|
|
| 352 |
break
|
| 353 |
|
| 354 |
offset += limit
|
| 355 |
-
# Safety break at 50k rows to prevent timeout if excessive
|
| 356 |
if offset > 100000: break
|
| 357 |
|
| 358 |
return JSONResponse(sorted(list(dates), reverse=True))
|
|
@@ -361,7 +359,6 @@ def get_hist_dates(symbol: str):
|
|
| 361 |
|
| 362 |
@app.get("/api/hist/minutes")
|
| 363 |
def get_hist_minutes(symbol: str, date: str):
|
| 364 |
-
"""Fetches ALL available minutes for a specific date using pagination"""
|
| 365 |
try:
|
| 366 |
minutes = set()
|
| 367 |
offset = 0
|
|
@@ -382,11 +379,10 @@ def get_hist_minutes(symbol: str, date: str):
|
|
| 382 |
|
| 383 |
for item in data:
|
| 384 |
ts_str = item.get('time_signature', '')
|
| 385 |
-
# Format: YYYY-MM-DD HH:MM:SS -> Extract HH:MM
|
| 386 |
parts = ts_str.split(' ')
|
| 387 |
if len(parts) > 1:
|
| 388 |
-
time_part = parts[1]
|
| 389 |
-
minutes.add(time_part[:5])
|
| 390 |
|
| 391 |
if len(data) < limit:
|
| 392 |
break
|
|
@@ -398,10 +394,6 @@ def get_hist_minutes(symbol: str, date: str):
|
|
| 398 |
|
| 399 |
@app.get("/api/hist/load")
|
| 400 |
def get_hist_data(symbol: str, date: str, minute: str):
|
| 401 |
-
"""
|
| 402 |
-
Fetches ALL snapshots for a specific minute.
|
| 403 |
-
Returns a list of { time: "SS", data: {...reconstructed...} }
|
| 404 |
-
"""
|
| 405 |
try:
|
| 406 |
query_sig = f"{date} {minute}"
|
| 407 |
timeline = []
|
|
@@ -448,9 +440,246 @@ def get_hist_data(symbol: str, date: str, minute: str):
|
|
| 448 |
print(e)
|
| 449 |
return JSONResponse({"error": str(e)}, status_code=500)
|
| 450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
|
| 452 |
# ==========================================
|
| 453 |
-
#
|
| 454 |
# ==========================================
|
| 455 |
|
| 456 |
HTML_TEMPLATE = """
|
|
@@ -502,10 +731,10 @@ HTML_TEMPLATE = """
|
|
| 502 |
.sec-pill:hover { background: #ddd; color: #000; }
|
| 503 |
.sec-pill.active { background: var(--accent); color: white; font-weight: bold; }
|
| 504 |
|
| 505 |
-
/* CORRUPTED DATA INDICATOR */
|
| 506 |
-
.sec-pill.bad { background: #ff5252; color: white; opacity:
|
| 507 |
.sec-pill.bad:hover { background: #d50000; }
|
| 508 |
-
.sec-pill.bad.active { background: #b71c1c; border:
|
| 509 |
|
| 510 |
.grid-layout { display: flex; height: calc(100vh - 60px); }
|
| 511 |
|
|
@@ -526,6 +755,10 @@ HTML_TEMPLATE = """
|
|
| 526 |
.graph-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px solid var(--border); cursor: crosshair; }
|
| 527 |
.visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.9; }
|
| 528 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
.strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); }
|
| 530 |
.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; }
|
| 531 |
|
|
@@ -924,7 +1157,7 @@ HTML_TEMPLATE = """
|
|
| 924 |
const res = await fetch(`/api/hist/load?symbol=${CURRENT_ROOT}&date=${date}&minute=${time}`);
|
| 925 |
const json = await res.json();
|
| 926 |
|
| 927 |
-
CACHED_TIMELINE = json.timeline; // List of {s: "
|
| 928 |
|
| 929 |
renderSecondBar();
|
| 930 |
|
|
@@ -1056,22 +1289,25 @@ HTML_TEMPLATE = """
|
|
| 1056 |
|
| 1057 |
// Update UI with the historical packet
|
| 1058 |
updateUI(data);
|
| 1059 |
-
|
| 1060 |
-
// Also ensure Expiries are valid for this historical snapshot
|
| 1061 |
-
// We look into the data items to find distinct expiries present in this snapshot
|
| 1062 |
-
const presentExpiries = new Set();
|
| 1063 |
-
for(let key in data) {
|
| 1064 |
-
// In reconstructed data, expiry is a field if available
|
| 1065 |
-
if(data[key].expiry) {
|
| 1066 |
-
// Format usually YYYY-MM-DD from v16 reconstruction
|
| 1067 |
-
// But UI expects Unix Timestamp.
|
| 1068 |
-
// We need to match it with CHAIN_DATA items.
|
| 1069 |
-
// Actually, simpler: Let the user filter expiries based on what the Master DB has.
|
| 1070 |
-
// History mode is just "Overwriting" the live price values.
|
| 1071 |
-
}
|
| 1072 |
-
}
|
| 1073 |
}
|
| 1074 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1075 |
|
| 1076 |
// ===================================
|
| 1077 |
// EXISTING APP LOGIC
|
|
@@ -1454,9 +1690,17 @@ HTML_TEMPLATE = """
|
|
| 1454 |
const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
|
| 1455 |
OPT_SYMS = relevant;
|
| 1456 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1457 |
const strikes = [...new Set(relevant.map(x => x.k))].sort((a,b) => a-b);
|
| 1458 |
|
| 1459 |
-
body.innerHTML = strikes.map(k =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1460 |
<div class="row" id="row-${k}" data-k="${k}">
|
| 1461 |
<div class="graph-col">
|
| 1462 |
<div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
|
|
@@ -1465,6 +1709,13 @@ HTML_TEMPLATE = """
|
|
| 1465 |
<span class="delta-txt" id="valL-${k}">-</span>
|
| 1466 |
</div>
|
| 1467 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1468 |
<div class="graph-col">
|
| 1469 |
<div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
|
| 1470 |
<div class="visual-bar" id="barR-${k}"></div>
|
|
@@ -1473,7 +1724,8 @@ HTML_TEMPLATE = """
|
|
| 1473 |
</div>
|
| 1474 |
</div>
|
| 1475 |
</div>
|
| 1476 |
-
|
|
|
|
| 1477 |
|
| 1478 |
window.scrolled = false;
|
| 1479 |
|
|
@@ -1581,7 +1833,11 @@ HTML_TEMPLATE = """
|
|
| 1581 |
return `
|
| 1582 |
<div class="fut-card">
|
| 1583 |
<div style="display:flex; justify-content:space-between; font-weight:700;">
|
| 1584 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1585 |
</div>
|
| 1586 |
<div class="meter-bg">
|
| 1587 |
<div style="width:${bPct}%; background:var(--green)"></div>
|
|
|
|
| 241 |
"totalsellqty": item.get('q', [0,0])[1],
|
| 242 |
"oi": item.get('o', [0,0])[0],
|
| 243 |
"oipercent": item.get('o', [0,0])[1],
|
| 244 |
+
"expiry": expiry_ts
|
| 245 |
}
|
| 246 |
return fyers_data
|
| 247 |
|
|
|
|
| 328 |
try:
|
| 329 |
dates = set()
|
| 330 |
offset = 0
|
| 331 |
+
limit = 1000
|
| 332 |
|
| 333 |
while True:
|
|
|
|
| 334 |
res = supabase.table('snapshots')\
|
| 335 |
.select('time_signature')\
|
| 336 |
.eq('symbol', symbol.upper())\
|
|
|
|
| 351 |
break
|
| 352 |
|
| 353 |
offset += limit
|
|
|
|
| 354 |
if offset > 100000: break
|
| 355 |
|
| 356 |
return JSONResponse(sorted(list(dates), reverse=True))
|
|
|
|
| 359 |
|
| 360 |
@app.get("/api/hist/minutes")
|
| 361 |
def get_hist_minutes(symbol: str, date: str):
|
|
|
|
| 362 |
try:
|
| 363 |
minutes = set()
|
| 364 |
offset = 0
|
|
|
|
| 379 |
|
| 380 |
for item in data:
|
| 381 |
ts_str = item.get('time_signature', '')
|
|
|
|
| 382 |
parts = ts_str.split(' ')
|
| 383 |
if len(parts) > 1:
|
| 384 |
+
time_part = parts[1]
|
| 385 |
+
minutes.add(time_part[:5])
|
| 386 |
|
| 387 |
if len(data) < limit:
|
| 388 |
break
|
|
|
|
| 394 |
|
| 395 |
@app.get("/api/hist/load")
|
| 396 |
def get_hist_data(symbol: str, date: str, minute: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 397 |
try:
|
| 398 |
query_sig = f"{date} {minute}"
|
| 399 |
timeline = []
|
|
|
|
| 440 |
print(e)
|
| 441 |
return JSONResponse({"error": str(e)}, status_code=500)
|
| 442 |
|
| 443 |
+
# --- CHARTING ENDPOINT (SERVER-SIDE PROCESSING) ---
|
| 444 |
+
@app.get("/api/hist/series")
|
| 445 |
+
def get_series_data(root: str, symbol: str, date: str):
|
| 446 |
+
"""
|
| 447 |
+
Fetches ALL snapshots for a root+date, extracts only the requested symbol,
|
| 448 |
+
and returns a clean time-series array of {t, b, s}.
|
| 449 |
+
"""
|
| 450 |
+
try:
|
| 451 |
+
series = []
|
| 452 |
+
offset = 0
|
| 453 |
+
limit = 1000
|
| 454 |
+
|
| 455 |
+
while True:
|
| 456 |
+
# Query all snapshots for this date
|
| 457 |
+
res = supabase.table('snapshots')\
|
| 458 |
+
.select('recorded_at, payload')\
|
| 459 |
+
.eq('symbol', root.upper())\
|
| 460 |
+
.like('time_signature', f'{date}%')\
|
| 461 |
+
.order('recorded_at', desc=False)\
|
| 462 |
+
.range(offset, offset + limit - 1)\
|
| 463 |
+
.execute()
|
| 464 |
+
|
| 465 |
+
data = res.data
|
| 466 |
+
if not data:
|
| 467 |
+
break
|
| 468 |
+
|
| 469 |
+
# Process batch server-side to save bandwidth
|
| 470 |
+
for row in data:
|
| 471 |
+
ts = row['recorded_at']
|
| 472 |
+
raw_payload = row['payload']
|
| 473 |
+
|
| 474 |
+
# Normalize payload
|
| 475 |
+
if isinstance(raw_payload, str):
|
| 476 |
+
try: raw_payload = json.loads(raw_payload)
|
| 477 |
+
except: raw_payload = []
|
| 478 |
+
|
| 479 |
+
if not isinstance(raw_payload, list):
|
| 480 |
+
continue
|
| 481 |
+
|
| 482 |
+
# Find specific symbol
|
| 483 |
+
target_data = None
|
| 484 |
+
for item in raw_payload:
|
| 485 |
+
if item.get('u') == symbol:
|
| 486 |
+
target_data = item
|
| 487 |
+
break
|
| 488 |
+
|
| 489 |
+
if target_data:
|
| 490 |
+
# 'q' is [BuyQty, SellQty]
|
| 491 |
+
q = target_data.get('q', [0, 0])
|
| 492 |
+
buy = q[0]
|
| 493 |
+
sell = q[1]
|
| 494 |
+
|
| 495 |
+
# VALIDATION: Skip if both are zero (noise reduction)
|
| 496 |
+
if buy > 0 or sell > 0:
|
| 497 |
+
series.append({
|
| 498 |
+
"t": ts * 1000, # Chart.js needs Milliseconds
|
| 499 |
+
"b": buy,
|
| 500 |
+
"s": sell
|
| 501 |
+
})
|
| 502 |
+
|
| 503 |
+
if len(data) < limit:
|
| 504 |
+
break
|
| 505 |
+
offset += limit
|
| 506 |
+
|
| 507 |
+
# Safety brake for massive days
|
| 508 |
+
if offset > 50000: break
|
| 509 |
+
|
| 510 |
+
return JSONResponse({"s": "ok", "data": series, "symbol": symbol, "date": date})
|
| 511 |
+
|
| 512 |
+
except Exception as e:
|
| 513 |
+
return JSONResponse({"error": str(e)}, status_code=500)
|
| 514 |
+
|
| 515 |
+
@app.get("/chart")
|
| 516 |
+
def chart_view(root: str, symbol: str, date: str):
|
| 517 |
+
"""Renders the Charting Page"""
|
| 518 |
+
return HTMLResponse(CHART_HTML
|
| 519 |
+
.replace("{{ROOT}}", root)
|
| 520 |
+
.replace("{{SYMBOL}}", symbol)
|
| 521 |
+
.replace("{{DATE}}", date)
|
| 522 |
+
)
|
| 523 |
+
|
| 524 |
+
# ==========================================
|
| 525 |
+
# 4. CHART HTML TEMPLATE
|
| 526 |
+
# ==========================================
|
| 527 |
+
CHART_HTML = """
|
| 528 |
+
<!DOCTYPE html>
|
| 529 |
+
<html lang="en">
|
| 530 |
+
<head>
|
| 531 |
+
<meta charset="UTF-8">
|
| 532 |
+
<title>Chart: {{SYMBOL}}</title>
|
| 533 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 534 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500;800&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
| 535 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
| 536 |
+
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
|
| 537 |
+
<style>
|
| 538 |
+
body { background: #0f172a; color: #f8fafc; font-family: 'Inter', sans-serif; margin: 0; padding: 20px; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; }
|
| 539 |
+
.header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 20px; border-bottom: 1px solid #334155; margin-bottom: 20px; }
|
| 540 |
+
.title { font-size: 18px; font-weight: 800; }
|
| 541 |
+
.subtitle { font-family: 'JetBrains Mono'; color: #94a3b8; font-size: 12px; margin-top: 5px; }
|
| 542 |
+
.chart-container { flex: 1; position: relative; width: 100%; min-height: 0; }
|
| 543 |
+
.loader { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; }
|
| 544 |
+
.spinner { width: 40px; height: 40px; border: 4px solid #334155; border-top: 4px solid #2563eb; border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 10px; }
|
| 545 |
+
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
|
| 546 |
+
|
| 547 |
+
.badge { background: #1e293b; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 700; border: 1px solid #334155; }
|
| 548 |
+
.btn-close { background: #ef4444; border: none; color: white; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-weight: bold; }
|
| 549 |
+
</style>
|
| 550 |
+
</head>
|
| 551 |
+
<body>
|
| 552 |
+
|
| 553 |
+
<div class="header">
|
| 554 |
+
<div>
|
| 555 |
+
<div class="title">ORDER FLOW IMBALANCE</div>
|
| 556 |
+
<div class="subtitle">
|
| 557 |
+
<span class="badge">{{ROOT}}</span>
|
| 558 |
+
<span class="badge" style="color:#fbbf24">{{SYMBOL}}</span>
|
| 559 |
+
<span class="badge">{{DATE}}</span>
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
<button class="btn-close" onclick="window.close()">CLOSE TAB</button>
|
| 563 |
+
</div>
|
| 564 |
+
|
| 565 |
+
<div class="chart-container">
|
| 566 |
+
<div class="loader" id="loader">
|
| 567 |
+
<div class="spinner"></div>
|
| 568 |
+
<div style="font-size:12px; font-weight:bold; color:#cbd5e1;">FETCHING SERVER DATA...</div>
|
| 569 |
+
</div>
|
| 570 |
+
<canvas id="myChart"></canvas>
|
| 571 |
+
</div>
|
| 572 |
+
|
| 573 |
+
<script>
|
| 574 |
+
const ROOT = "{{ROOT}}";
|
| 575 |
+
const SYMBOL = "{{SYMBOL}}";
|
| 576 |
+
const DATE = "{{DATE}}";
|
| 577 |
+
|
| 578 |
+
async function init() {
|
| 579 |
+
try {
|
| 580 |
+
const res = await fetch(`/api/hist/series?root=${ROOT}&symbol=${SYMBOL}&date=${DATE}`);
|
| 581 |
+
const json = await res.json();
|
| 582 |
+
|
| 583 |
+
if(json.s !== 'ok') throw new Error(json.error || "Unknown Error");
|
| 584 |
+
|
| 585 |
+
document.getElementById('loader').style.display = 'none';
|
| 586 |
+
renderChart(json.data);
|
| 587 |
+
|
| 588 |
+
} catch(e) {
|
| 589 |
+
document.getElementById('loader').innerHTML = `<div style='color:#ef4444; font-weight:bold;'>ERROR: ${e.message}</div>`;
|
| 590 |
+
}
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
function renderChart(data) {
|
| 594 |
+
if(data.length === 0) {
|
| 595 |
+
document.getElementById('loader').innerHTML = "<div style='color:#cbd5e1;'>NO DATA FOR THIS DATE</div>";
|
| 596 |
+
document.getElementById('loader').style.display = 'block';
|
| 597 |
+
return;
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
const ctx = document.getElementById('myChart').getContext('2d');
|
| 601 |
+
|
| 602 |
+
// Extract Arrays
|
| 603 |
+
const labels = data.map(d => d.t);
|
| 604 |
+
const buys = data.map(d => d.b);
|
| 605 |
+
const sells = data.map(d => d.s);
|
| 606 |
+
|
| 607 |
+
new Chart(ctx, {
|
| 608 |
+
type: 'line',
|
| 609 |
+
data: {
|
| 610 |
+
labels: labels,
|
| 611 |
+
datasets: [
|
| 612 |
+
{
|
| 613 |
+
label: 'TOTAL BUY QTY',
|
| 614 |
+
data: buys,
|
| 615 |
+
borderColor: '#00e676', // Green
|
| 616 |
+
backgroundColor: 'rgba(0, 230, 118, 0.1)',
|
| 617 |
+
borderWidth: 2,
|
| 618 |
+
pointRadius: 0,
|
| 619 |
+
tension: 0.2,
|
| 620 |
+
fill: false
|
| 621 |
+
},
|
| 622 |
+
{
|
| 623 |
+
label: 'TOTAL SELL QTY',
|
| 624 |
+
data: sells,
|
| 625 |
+
borderColor: '#ff1744', // Red
|
| 626 |
+
backgroundColor: 'rgba(255, 23, 68, 0.1)',
|
| 627 |
+
borderWidth: 2,
|
| 628 |
+
pointRadius: 0,
|
| 629 |
+
tension: 0.2,
|
| 630 |
+
fill: false
|
| 631 |
+
}
|
| 632 |
+
]
|
| 633 |
+
},
|
| 634 |
+
options: {
|
| 635 |
+
responsive: true,
|
| 636 |
+
maintainAspectRatio: false,
|
| 637 |
+
interaction: {
|
| 638 |
+
mode: 'index',
|
| 639 |
+
intersect: false,
|
| 640 |
+
},
|
| 641 |
+
scales: {
|
| 642 |
+
x: {
|
| 643 |
+
type: 'time',
|
| 644 |
+
time: {
|
| 645 |
+
unit: 'minute',
|
| 646 |
+
displayFormats: { minute: 'HH:mm' }
|
| 647 |
+
},
|
| 648 |
+
grid: { color: '#334155' },
|
| 649 |
+
ticks: { color: '#94a3b8' }
|
| 650 |
+
},
|
| 651 |
+
y: {
|
| 652 |
+
grid: { color: '#334155' },
|
| 653 |
+
ticks: { color: '#94a3b8' }
|
| 654 |
+
}
|
| 655 |
+
},
|
| 656 |
+
plugins: {
|
| 657 |
+
legend: { labels: { color: '#f8fafc', font: {family: 'JetBrains Mono'} } },
|
| 658 |
+
tooltip: {
|
| 659 |
+
backgroundColor: '#1e293b',
|
| 660 |
+
titleColor: '#fff',
|
| 661 |
+
bodyColor: '#cbd5e1',
|
| 662 |
+
borderColor: '#475569',
|
| 663 |
+
borderWidth: 1,
|
| 664 |
+
callbacks: {
|
| 665 |
+
label: function(ctx) {
|
| 666 |
+
return ctx.dataset.label + ": " + ctx.raw.toLocaleString();
|
| 667 |
+
}
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
}
|
| 672 |
+
});
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
init();
|
| 676 |
+
</script>
|
| 677 |
+
</body>
|
| 678 |
+
</html>
|
| 679 |
+
"""
|
| 680 |
|
| 681 |
# ==========================================
|
| 682 |
+
# 5. MAIN FRONTEND TEMPLATE
|
| 683 |
# ==========================================
|
| 684 |
|
| 685 |
HTML_TEMPLATE = """
|
|
|
|
| 731 |
.sec-pill:hover { background: #ddd; color: #000; }
|
| 732 |
.sec-pill.active { background: var(--accent); color: white; font-weight: bold; }
|
| 733 |
|
| 734 |
+
/* CORRUPTED DATA INDICATOR - RED */
|
| 735 |
+
.sec-pill.bad { background: #ff5252; color: white; opacity: 1; font-weight: 800; border: 1px solid #d50000; }
|
| 736 |
.sec-pill.bad:hover { background: #d50000; }
|
| 737 |
+
.sec-pill.bad.active { background: #b71c1c; border-color: #fff; }
|
| 738 |
|
| 739 |
.grid-layout { display: flex; height: calc(100vh - 60px); }
|
| 740 |
|
|
|
|
| 755 |
.graph-col { position: relative; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; border-right: 1px solid var(--border); cursor: crosshair; }
|
| 756 |
.visual-bar { position: absolute; top: 8px; bottom: 8px; z-index: 0; border-radius: 4px; transition: width 0.3s, left 0.3s; opacity: 0.9; }
|
| 757 |
|
| 758 |
+
/* Graph Icon */
|
| 759 |
+
.graph-trigger { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); z-index: 5; opacity: 0.2; cursor: pointer; }
|
| 760 |
+
.graph-trigger:hover { opacity: 1; color: var(--accent); }
|
| 761 |
+
|
| 762 |
.strike-txt { z-index: 2; font-weight: 800; font-size: 13px; color: var(--text-main); }
|
| 763 |
.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; }
|
| 764 |
|
|
|
|
| 1157 |
const res = await fetch(`/api/hist/load?symbol=${CURRENT_ROOT}&date=${date}&minute=${time}`);
|
| 1158 |
const json = await res.json();
|
| 1159 |
|
| 1160 |
+
CACHED_TIMELINE = json.timeline; // List of {s: "00", d: { ... }}
|
| 1161 |
|
| 1162 |
renderSecondBar();
|
| 1163 |
|
|
|
|
| 1289 |
|
| 1290 |
// Update UI with the historical packet
|
| 1291 |
updateUI(data);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1292 |
}
|
| 1293 |
|
| 1294 |
+
function openChart(symbol) {
|
| 1295 |
+
// Find Date
|
| 1296 |
+
let date = "";
|
| 1297 |
+
if(IS_HISTORY) {
|
| 1298 |
+
date = document.getElementById('histDate').value;
|
| 1299 |
+
if(!date || date === 'DATE') { alert("Select a date first!"); return; }
|
| 1300 |
+
} else {
|
| 1301 |
+
// Default to today if live (experimental)
|
| 1302 |
+
const d = new Date();
|
| 1303 |
+
date = d.toISOString().split('T')[0];
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
if(!CURRENT_ROOT) { alert("Load a symbol first"); return; }
|
| 1307 |
+
|
| 1308 |
+
const url = `/chart?root=${CURRENT_ROOT}&symbol=${symbol}&date=${date}`;
|
| 1309 |
+
window.open(url, '_blank');
|
| 1310 |
+
}
|
| 1311 |
|
| 1312 |
// ===================================
|
| 1313 |
// EXISTING APP LOGIC
|
|
|
|
| 1690 |
const relevant = CHAIN_DATA.filter(x => x.e === ACTIVE_EXP && (x.t === 'CE' || x.t === 'PE'));
|
| 1691 |
OPT_SYMS = relevant;
|
| 1692 |
|
| 1693 |
+
// --- PREPARE MAP FOR LOOKUP ---
|
| 1694 |
+
const map = {};
|
| 1695 |
+
relevant.forEach(i => map[i.k + i.t] = i.s);
|
| 1696 |
+
|
| 1697 |
const strikes = [...new Set(relevant.map(x => x.k))].sort((a,b) => a-b);
|
| 1698 |
|
| 1699 |
+
body.innerHTML = strikes.map(k => {
|
| 1700 |
+
const ceSym = map[k + 'CE'] || '';
|
| 1701 |
+
const peSym = map[k + 'PE'] || '';
|
| 1702 |
+
|
| 1703 |
+
return `
|
| 1704 |
<div class="row" id="row-${k}" data-k="${k}">
|
| 1705 |
<div class="graph-col">
|
| 1706 |
<div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
|
|
|
|
| 1709 |
<span class="delta-txt" id="valL-${k}">-</span>
|
| 1710 |
</div>
|
| 1711 |
</div>
|
| 1712 |
+
|
| 1713 |
+
<!-- Center Icons (Graph) -->
|
| 1714 |
+
<div style="position:absolute; left:50%; transform:translateX(-50%); z-index:10; display:flex; gap:120px;">
|
| 1715 |
+
${ceSym ? `<svg onclick="openChart('${ceSym}')" class="graph-trigger" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H9M17 7V15"/></svg>` : ''}
|
| 1716 |
+
${peSym ? `<svg onclick="openChart('${peSym}')" class="graph-trigger" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H9M17 7V15"/></svg>` : ''}
|
| 1717 |
+
</div>
|
| 1718 |
+
|
| 1719 |
<div class="graph-col">
|
| 1720 |
<div style="width:160px; height:100%; position:relative; display:flex; flex-direction:column; justify-content:center; align-items:center;">
|
| 1721 |
<div class="visual-bar" id="barR-${k}"></div>
|
|
|
|
| 1724 |
</div>
|
| 1725 |
</div>
|
| 1726 |
</div>
|
| 1727 |
+
`;
|
| 1728 |
+
}).join('');
|
| 1729 |
|
| 1730 |
window.scrolled = false;
|
| 1731 |
|
|
|
|
| 1833 |
return `
|
| 1834 |
<div class="fut-card">
|
| 1835 |
<div style="display:flex; justify-content:space-between; font-weight:700;">
|
| 1836 |
+
<div style="display:flex; align-items:center; gap:5px;">
|
| 1837 |
+
<span>${dt} FUT</span>
|
| 1838 |
+
<svg onclick="openChart('${f.s}')" style="cursor:pointer; color:#aaa; width:12px;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M7 17L17 7M17 7H9M17 7V15"/></svg>
|
| 1839 |
+
</div>
|
| 1840 |
+
<span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
|
| 1841 |
</div>
|
| 1842 |
<div class="meter-bg">
|
| 1843 |
<div style="width:${bPct}%; background:var(--green)"></div>
|