Utkarsh Pathak commited on
Commit
7bb81a6
·
1 Parent(s): 18e0bea

Live price ticker every 5s + configurable 1/2/5min full rescan

Browse files
Files changed (2) hide show
  1. main.py +24 -0
  2. static/index.html +93 -50
main.py CHANGED
@@ -134,6 +134,30 @@ def scan(top: int = 8, min_volume: float = None, account: float = 100000,
134
  raise HTTPException(503, str(e))
135
 
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  @app.get("/api/card/{symbol}")
138
  def card(symbol: str, account: float = 100000, risk_pct: float = 1.0):
139
  try:
 
134
  raise HTTPException(503, str(e))
135
 
136
 
137
+ @app.get("/api/prices")
138
+ def prices(symbols: str = ""):
139
+ """Lightweight price ticker — returns current price + 24h change for a
140
+ comma-separated list of symbols. Used for real-time card price updates."""
141
+ try:
142
+ src = get_source()
143
+ requested = {s.strip() for s in symbols.split(",") if s.strip()}
144
+ tickers = src.tickers()
145
+ result = {}
146
+ for t in tickers:
147
+ sym = t.get("symbol", "")
148
+ if not requested or sym in requested:
149
+ result[sym] = {
150
+ "price": t.get("last_price"),
151
+ "change_24h_pct": t.get("change_24h_pct"),
152
+ "high": t.get("high"),
153
+ "low": t.get("low"),
154
+ "quote_volume_usd": t.get("quote_volume_usd"),
155
+ }
156
+ return {"ts": datetime.now(timezone.utc).isoformat(), "prices": result}
157
+ except Exception as e:
158
+ raise HTTPException(503, str(e))
159
+
160
+
161
  @app.get("/api/card/{symbol}")
162
  def card(symbol: str, account: float = 100000, risk_pct: float = 1.0):
163
  try:
static/index.html CHANGED
@@ -639,25 +639,19 @@ h1.hero-title .accent {
639
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
640
  }
641
 
642
- /* ── Auto-refresh button ─────────────────────────────────────────────── */
643
- #auto-btn {
644
- display: inline-flex; align-items: center; gap: 6px;
645
- background: rgba(255,255,255,0.7);
646
- border: 1px solid rgba(0,0,0,0.08);
647
- border-radius: 10px; padding: 9px 14px;
648
- font-size: 13px; font-weight: 700; font-family: var(--font);
649
- color: var(--t2); cursor: pointer;
650
- backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
651
- transition: all 0.2s var(--ease);
652
- box-shadow: inset 0 1px 0 rgba(255,255,255,0.8);
653
- }
654
- #auto-btn:hover { border-color: rgba(13,148,136,0.3); color: var(--teal); }
655
- #auto-btn.on {
656
- background: rgba(209,250,229,0.7);
657
- border-color: rgba(13,148,136,0.3);
658
- color: var(--teal);
659
  }
660
- #auto-btn.on #auto-icon { animation: spin 2s linear infinite; }
661
 
662
  #refresh-countdown {
663
  font-size: 11px; font-weight: 700; letter-spacing: 0.3px;
@@ -750,10 +744,15 @@ h1.hero-title .accent {
750
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
751
  Scan
752
  </button>
753
- <button id="auto-btn" title="Toggle auto-refresh">
754
- <svg id="auto-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
755
- <span id="auto-label">Auto</span>
756
- </button>
 
 
 
 
 
757
  <span id="refresh-countdown" style="display:none"></span>
758
  </div>
759
  </nav>
@@ -829,41 +828,30 @@ const fu = n => {
829
  };
830
  const pc = n => n == null ? null : (n>=0?'+':'')+( +n).toFixed(2)+'%';
831
 
832
- /* ── Auto-refresh ──────────────────────────────────────────────────── */
833
- const AUTO_INTERVAL = 5 * 60; // 5 minutes in seconds
834
- let autoOn = false;
835
  let autoCountdown = 0;
836
- let autoTimer = null;
837
  let countdownTimer = null;
 
 
 
838
 
839
- function setAutoRefresh(on) {
840
- autoOn = on;
841
- const btn = $('auto-btn'), lbl = $('auto-label'), cd = $('refresh-countdown');
842
- if (on) {
843
- btn.classList.add('on');
844
- lbl.textContent = '5 min';
845
- cd.style.display = 'inline-flex';
846
- startCountdown();
847
- } else {
848
- btn.classList.remove('on');
849
- lbl.textContent = 'Auto';
850
- cd.style.display = 'none';
851
- clearInterval(autoTimer);
852
- clearInterval(countdownTimer);
853
- }
854
- }
855
 
856
  function startCountdown() {
857
- autoCountdown = AUTO_INTERVAL;
858
  clearInterval(countdownTimer);
859
- clearInterval(autoTimer);
 
 
 
 
 
860
  updateCountdownDisplay();
861
  countdownTimer = setInterval(() => {
862
  autoCountdown--;
863
  updateCountdownDisplay();
864
  if (autoCountdown <= 0) {
865
  clearInterval(countdownTimer);
866
- if (autoOn) { runScan(); startCountdown(); }
867
  }
868
  }, 1000);
869
  }
@@ -873,10 +861,62 @@ function updateCountdownDisplay() {
873
  const m = Math.floor(autoCountdown / 60);
874
  const s = autoCountdown % 60;
875
  cd.textContent = `↻ ${m}:${String(s).padStart(2,'0')}`;
876
- cd.className = autoCountdown <= 30 ? 'urgent' : '';
877
  }
878
 
879
- $('auto-btn').addEventListener('click', () => setAutoRefresh(!autoOn));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
880
 
881
  /* Status */
882
  async function checkStatus() {
@@ -932,13 +972,14 @@ async function runScan() {
932
  const sm = $('stat-market'); if (sm) sm.textContent = data.market.includes('BingX') ? 'USDT Perps' : data.market.split('(')[0].trim();
933
 
934
  if (!data.cards.length) { empty.style.display = 'flex'; return; }
935
- // Reset countdown after successful scan
936
- if (autoOn) startCountdown();
937
  data.cards.forEach((c, i) => {
938
  const el = buildCard(c, account, risk);
939
  el.style.animationDelay = (i * 55) + 'ms';
940
  grid.appendChild(el);
 
941
  });
 
942
  } catch(e) {
943
  loader.style.display = 'none';
944
  empty.style.display = 'flex';
@@ -1101,7 +1142,9 @@ function buildCard(c, account, risk) {
1101
  }
1102
 
1103
  checkStatus();
1104
- setInterval(checkStatus, 60000);
 
 
1105
  </script>
1106
  </body>
1107
  </html>
 
639
  backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
640
  }
641
 
642
+ /* ── Auto-refresh select ─────────────────────────────────────────────── */
643
+ #auto-wrap { transition: border-color 0.2s, background 0.2s; }
644
+ #auto-wrap.active {
645
+ border-color: rgba(13,148,136,0.35) !important;
646
+ background: rgba(209,250,229,0.7) !important;
647
+ }
648
+ #auto-wrap.active label { color: var(--teal); }
649
+ #auto-interval {
650
+ background: none; border: none; outline: none;
651
+ color: var(--t1); font-family: var(--font);
652
+ font-size: 13px; font-weight: 700; cursor: pointer;
653
+ padding-right: 2px;
 
 
 
 
 
654
  }
 
655
 
656
  #refresh-countdown {
657
  font-size: 11px; font-weight: 700; letter-spacing: 0.3px;
 
744
  <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
745
  Scan
746
  </button>
747
+ <div class="input-chip" id="auto-wrap">
748
+ <label for="auto-interval">Auto</label>
749
+ <select id="auto-interval">
750
+ <option value="0">Off</option>
751
+ <option value="60">1 min</option>
752
+ <option value="120">2 min</option>
753
+ <option value="300" selected>5 min</option>
754
+ </select>
755
+ </div>
756
  <span id="refresh-countdown" style="display:none"></span>
757
  </div>
758
  </nav>
 
828
  };
829
  const pc = n => n == null ? null : (n>=0?'+':'')+( +n).toFixed(2)+'%';
830
 
831
+ /* ── Auto-refresh (full scan) ──────────────────────────────────────── */
 
 
832
  let autoCountdown = 0;
 
833
  let countdownTimer = null;
834
+ let liveTickerTimer = null;
835
+ // track which symbols are currently on screen + their card elements
836
+ const liveCards = new Map(); // symbol → {el, account, risk}
837
 
838
+ function getAutoInterval() { return parseInt($('auto-interval').value) || 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
839
 
840
  function startCountdown() {
 
841
  clearInterval(countdownTimer);
842
+ const secs = getAutoInterval();
843
+ const cd = $('refresh-countdown');
844
+ if (!secs) { cd.style.display = 'none'; $('auto-wrap').classList.remove('active'); return; }
845
+ $('auto-wrap').classList.add('active');
846
+ cd.style.display = 'inline-flex';
847
+ autoCountdown = secs;
848
  updateCountdownDisplay();
849
  countdownTimer = setInterval(() => {
850
  autoCountdown--;
851
  updateCountdownDisplay();
852
  if (autoCountdown <= 0) {
853
  clearInterval(countdownTimer);
854
+ runScan().then(() => startCountdown());
855
  }
856
  }, 1000);
857
  }
 
861
  const m = Math.floor(autoCountdown / 60);
862
  const s = autoCountdown % 60;
863
  cd.textContent = `↻ ${m}:${String(s).padStart(2,'0')}`;
864
+ cd.className = autoCountdown <= 15 ? 'urgent' : '';
865
  }
866
 
867
+ $('auto-interval').addEventListener('change', () => startCountdown());
868
+
869
+ /* ── Live price ticker (every 5 seconds) ──────────────────────────── */
870
+ async function tickPrices() {
871
+ if (liveCards.size === 0) return;
872
+ const syms = [...liveCards.keys()].join(',');
873
+ try {
874
+ const data = await fetch(`/api/prices?symbols=${encodeURIComponent(syms)}`).then(r => r.json());
875
+ const now = new Date(data.ts).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});
876
+ for (const [sym, {el}] of liveCards) {
877
+ const p = data.prices[sym];
878
+ if (!p) continue;
879
+ // update price
880
+ const priceEl = el.querySelector('.lv.entry .lv-val');
881
+ if (priceEl) {
882
+ const oldPrice = parseFloat(priceEl.dataset.raw || priceEl.textContent.replace(/[^0-9.]/g,''));
883
+ const newPrice = p.price;
884
+ priceEl.textContent = sf(newPrice);
885
+ priceEl.dataset.raw = newPrice;
886
+ // flash green/red on price change
887
+ if (oldPrice && newPrice !== oldPrice) {
888
+ priceEl.style.transition = 'color 0.3s';
889
+ priceEl.style.color = newPrice > oldPrice ? 'var(--green)' : 'var(--red)';
890
+ setTimeout(() => { priceEl.style.color = ''; }, 1200);
891
+ }
892
+ }
893
+ // update 24h change
894
+ const chgEl = el.querySelector('.chg');
895
+ if (chgEl && p.change_24h_pct != null) {
896
+ const chg = +p.change_24h_pct;
897
+ chgEl.textContent = (chg >= 0 ? '+' : '') + chg.toFixed(2) + '%';
898
+ chgEl.className = 'chg ' + (chg >= 0 ? 'pos' : 'neg');
899
+ }
900
+ // update volume
901
+ const volEl = el.querySelector('.vol');
902
+ if (volEl && p.quote_volume_usd) volEl.textContent = fu(p.quote_volume_usd);
903
+ // update live timestamp on meta
904
+ const meta = $('meta');
905
+ if (meta && meta.textContent) {
906
+ meta.textContent = meta.textContent.replace(/· \d{1,2}:\d{2}.*$/, `· ${now} · live`);
907
+ }
908
+ }
909
+ } catch { /* silent fail — next tick will retry */ }
910
+ }
911
+
912
+ function startLiveTicker() {
913
+ clearInterval(liveTickerTimer);
914
+ liveTickerTimer = setInterval(tickPrices, 5000);
915
+ }
916
+ function stopLiveTicker() {
917
+ clearInterval(liveTickerTimer);
918
+ liveCards.clear();
919
+ }
920
 
921
  /* Status */
922
  async function checkStatus() {
 
972
  const sm = $('stat-market'); if (sm) sm.textContent = data.market.includes('BingX') ? 'USDT Perps' : data.market.split('(')[0].trim();
973
 
974
  if (!data.cards.length) { empty.style.display = 'flex'; return; }
975
+ stopLiveTicker();
 
976
  data.cards.forEach((c, i) => {
977
  const el = buildCard(c, account, risk);
978
  el.style.animationDelay = (i * 55) + 'ms';
979
  grid.appendChild(el);
980
+ if (c.symbol) liveCards.set(c.symbol, {el, account, risk});
981
  });
982
+ startLiveTicker();
983
  } catch(e) {
984
  loader.style.display = 'none';
985
  empty.style.display = 'flex';
 
1142
  }
1143
 
1144
  checkStatus();
1145
+ setInterval(checkStatus, 30000);
1146
+ // start countdown if a non-Off interval is pre-selected
1147
+ if (getAutoInterval()) startCountdown();
1148
  </script>
1149
  </body>
1150
  </html>