topsecrettraders commited on
Commit
2b57a56
·
verified ·
1 Parent(s): 03fdad1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +290 -34
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 # Converted to Epoch for UI compatibility
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 # Supabase max
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] # HH:MM:SS
389
- minutes.add(time_part[:5]) # HH:MM
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
- # 4. FRONTEND
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: 0.9; }
507
  .sec-pill.bad:hover { background: #d50000; }
508
- .sec-pill.bad.active { background: #b71c1c; border: 1px solid #fff; }
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: "05", d: { ... }}
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
- `).join('');
 
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
- <span>${dt} FUT</span> <span style="color:${d.ch>=0?'var(--green)':'var(--red)'}">${d.ltp}</span>
 
 
 
 
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>