/** * Pricify Real-Time Data Engine v2 * Client-side real-time data fetcher from Yahoo Finance * Scans page for [data-symbol] elements and updates prices every 30s * Handles ₹ formatting for .BSE stocks with flash animation on change */ const PricifyRT = (function() { 'use strict'; // Configuration const REFRESH_INTERVAL = 30000; // 30 seconds const PROXY_URL = 'https://api.allorigins.win/raw?url='; const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'; // State let priceCache = {}; let updateCallbacks = []; let refreshTimer = null; let isRunning = false; // ================================================================ // YAHOO FINANCE DIRECT FETCH (works from browser with residential IP) // Reuses the same connection pattern as the original, but more robust // ================================================================ function fetchYahooPrice(symbol) { const url = `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(symbol)}?range=1d&interval=1m`; return fetch(url, { headers: { 'User-Agent': USER_AGENT } }) .then(r => { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(data => { if (!data?.chart?.result?.[0]) throw new Error('No chart result'); const result = data.chart.result[0]; const meta = result.meta; const quote = result.indicators?.quote?.[0]; if (!meta || !quote) throw new Error('No quote data'); // Get latest close price const closes = quote.close?.filter(c => c !== null) || []; const lastPrice = closes.length > 0 ? closes[closes.length - 1] : meta.regularMarketPrice || meta.chartPreviousClose; const prevClose = meta.chartPreviousClose || meta.previousClose || lastPrice; const change = lastPrice - prevClose; const changePercent = prevClose > 0 ? (change / prevClose) * 100 : 0; // Calculate high/low from all quotes const highs = quote.high?.filter(h => h !== null) || []; const lows = quote.low?.filter(l => l !== null) || []; const high = highs.length > 0 ? Math.max(...highs) : (meta.regularMarketDayHigh || lastPrice); const low = lows.length > 0 ? Math.min(...lows) : (meta.regularMarketDayLow || lastPrice); // Sum volume const volume = quote.volume?.reduce((a, b) => a + (b || 0), 0) || meta.regularMarketVolume || 0; return { symbol: symbol, price: lastPrice, change: change, changePercent: changePercent, open: meta.regularMarketOpen || (quote.open?.[0] ?? null), high: high, low: low, volume: volume, previousClose: prevClose, source: 'yahoo', updatedAt: Date.now() }; }); } // ================================================================ // FALLBACK: Proxy-based Yahoo Finance (when direct is blocked) // ================================================================ function fetchYahooViaProxy(symbol) { const yahooUrl = `https://query1.finance.yahoo.com/v8/finance/chart/${symbol}?range=1d&interval=1m`; const url = PROXY_URL + encodeURIComponent(yahooUrl); return fetch(url) .then(r => { if (!r.ok) throw new Error('Proxy HTTP ' + r.status); return r.json(); }) .then(data => { if (!data?.chart?.result?.[0]) throw new Error('No proxy chart result'); const result = data.chart.result[0]; const meta = result.meta; const lastPrice = meta.regularMarketPrice || meta.chartPreviousClose; const prevClose = meta.chartPreviousClose || lastPrice; const change = lastPrice - prevClose; const changePercent = prevClose > 0 ? (change / prevClose) * 100 : 0; // Try to extract more data from proxy response const quote = result.indicators?.quote?.[0]; const closes = quote?.close?.filter(c => c !== null) || []; const finalPrice = closes.length > 0 ? closes[closes.length - 1] : lastPrice; const finalChange = finalPrice - prevClose; const finalChangePercent = prevClose > 0 ? (finalChange / prevClose) * 100 : 0; return { symbol: symbol, price: finalPrice, change: finalChange, changePercent: finalChangePercent, open: meta.regularMarketOpen || null, high: meta.regularMarketDayHigh || null, low: meta.regularMarketDayLow || null, volume: meta.regularMarketVolume || 0, previousClose: prevClose, source: 'proxy', updatedAt: Date.now() }; }); } // ================================================================ // FETCH SINGLE STOCK WITH AUTO-FALLBACK // ================================================================ function fetchStockPrice(symbol) { return fetchYahooPrice(symbol) .catch(() => fetchYahooViaProxy(symbol)) .catch(err => { console.warn(`[PricifyRT] All sources failed for ${symbol}:`, err.message); return null; }); } // ================================================================ // FETCH MULTIPLE STOCKS WITH CONCURRENCY LIMIT // ================================================================ function fetchMultiple(symbols) { if (!symbols || symbols.length === 0) return Promise.resolve({}); const concurrency = 5; const results = {}; let index = 0; function processBatch() { const batch = symbols.slice(index, index + concurrency); index += concurrency; if (batch.length === 0) return Promise.resolve(); return Promise.allSettled(batch.map(sym => fetchStockPrice(sym).then(data => { if (data) results[sym] = data; }) )).then(() => { if (index < symbols.length) { return new Promise(r => setTimeout(r, 1000)).then(processBatch); } }); } return processBatch().then(() => results); } // ================================================================ // UPDATE THE DOM WITH NEW PRICES AND FLASH ANIMATION // ================================================================ function updateUI(prices) { // Update any element with [data-symbol] attribute // Supports: table rows (tr[data-symbol]), divs, spans, etc. document.querySelectorAll('[data-symbol]').forEach(el => { const symbol = el.dataset.symbol; const data = prices[symbol] || priceCache[symbol]; if (!data) return; // Cache the data even for elements in case they don't get re-queried priceCache[symbol] = data; // Determine if this is an INR stock (has .BSE suffix) const isINR = symbol.includes('.BSE'); // Try price-cell class first, then data-price attribute, then first numeric cell const priceCell = el.querySelector('.price-cell') || el.querySelector('[data-price]'); const changeCell = el.querySelector('.change-cell') || el.querySelector('[data-change]'); const volumeCell = el.querySelector('.volume-cell') || el.querySelector('[data-volume]'); // Update price cell if (priceCell) { updatePriceCell(priceCell, data, isINR); } else if (el.hasAttribute('data-price')) { // Direct attribute binding const displayEl = el.querySelector('.price-display') || el; updatePriceCell(displayEl, data, isINR); } // Update change cell if (changeCell) { updateChangeCell(changeCell, data); } else if (el.hasAttribute('data-change')) { const displayEl = el.querySelector('.change-display') || el; updateChangeCell(displayEl, data); } // Update volume cell if (volumeCell) { updateVolumeCell(volumeCell, data); } // Update row-level attributes el.dataset.price = data.price; el.dataset.change = data.changePercent; el.dataset.updated = data.updatedAt; }); // Also handle legacy table row format (tr[data-symbol] with vanilla cells) document.querySelectorAll('tr[data-symbol]').forEach(row => { const symbol = row.dataset.symbol; const data = prices[symbol] || priceCache[symbol]; if (!data) return; const isINR = symbol.includes('.BSE'); // Try position-based cells if class selectors didn't match const cells = row.querySelectorAll('td'); if (cells.length >= 3) { // First cell is usually symbol/name (skip) // Second cell might be price const priceTd = cells[1]; const changeTd = cells[2]; // Only update if these cells don't already have a class-based handler if (!priceTd.classList.contains('price-cell') && !priceTd.querySelector('.price-cell')) { updatePriceCell(priceTd, data, isINR); } if (!changeTd.classList.contains('change-cell') && !changeTd.querySelector('.change-cell')) { updateChangeCell(changeTd, data); } } }); // Trigger callbacks updateCallbacks.forEach(cb => cb(prices)); } // ================================================================ // PRICE CELL UPDATE WITH FLASH ANIMATION // ================================================================ function updatePriceCell(el, data, isINR) { const oldText = el.textContent.trim(); const newText = formatPrice(data.price, isINR); if (oldText !== newText) { el.textContent = newText; // Flash animation const flashClass = data.change >= 0 ? 'flash-up' : 'flash-down'; el.classList.remove('flash-up', 'flash-down'); // Force reflow for animation restart void el.offsetWidth; el.classList.add(flashClass); // Also update the row's class if this is inside a tr const row = el.closest('tr'); if (row) { row.dataset.price = data.price; row.dataset.change = data.changePercent; } } } // ================================================================ // CHANGE CELL UPDATE // ================================================================ function updateChangeCell(el, data) { const cls = data.changePercent >= 0 ? 'up' : 'dn'; const arrow = data.changePercent >= 0 ? '\u25B2' : '\u25BC'; const changeText = `${arrow} ${Math.abs(data.changePercent).toFixed(2)}%`; el.textContent = changeText; el.className = el.className .replace(/\bup\b/g, '') .replace(/\bdn\b/g, '') .replace(/\s+/g, ' ') .trim(); el.classList.add(cls); } // ================================================================ // VOLUME CELL UPDATE // ================================================================ function updateVolumeCell(el, data) { el.textContent = formatVolume(data.volume); } // ================================================================ // PRICE FORMATTING // ================================================================ function formatPrice(price, isINR) { if (price == null) return '---'; if (isINR) { return '\u20B9' + price.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } // Default formatting for non-INR (usually USD) if (price >= 1000) { return '$' + price.toLocaleString('en-US', { minimumFractionDigits: 2 }); } if (price >= 1) { return '$' + price.toFixed(2); } if (price >= 0.01) { return '$' + price.toFixed(4); } return '$' + price.toFixed(6); } // ================================================================ // VOLUME FORMATTING // ================================================================ function formatVolume(volume) { if (volume == null || volume === 0) return '—'; if (volume >= 1e7) return (volume / 1e7).toFixed(1) + 'Cr'; if (volume >= 1e5) return (volume / 1e5).toFixed(1) + 'L'; if (volume >= 1e3) return (volume / 1e3).toFixed(1) + 'K'; return volume.toString(); } // ================================================================ // SCAN PAGE FOR SYMBOLS AND START AUTO-REFRESH // ================================================================ function init() { if (isRunning) return []; const symbols = []; document.querySelectorAll('[data-symbol]').forEach(el => { const sym = el.dataset.symbol; if (sym && !symbols.includes(sym)) symbols.push(sym); }); if (symbols.length > 0) { console.log(`[PricifyRT] Found ${symbols.length} symbols, starting real-time updates every 30s`); start(symbols); } else { console.log('[PricifyRT] No [data-symbol] elements found on page'); } return symbols; } // ================================================================ // START AUTO-REFRESH LOOP // ================================================================ function start(symbols) { if (refreshTimer) clearInterval(refreshTimer); isRunning = true; // Immediate first fetch if (symbols && symbols.length > 0) { fetchMultiple(symbols).then(updateUI); } // Periodic refresh refreshTimer = setInterval(() => { if (symbols && symbols.length > 0) { fetchMultiple(symbols).then(updateUI); } }, REFRESH_INTERVAL); return refreshTimer; } // ================================================================ // STOP AUTO-REFRESH // ================================================================ function stop() { if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; } isRunning = false; console.log('[PricifyRT] Stopped'); } // ================================================================ // PUBLIC API // ================================================================ return { init: init, start: start, stop: stop, fetchStock: fetchStockPrice, fetchMultiple: fetchMultiple, getCache: () => Object.assign({}, priceCache), getSymbols: () => { const syms = []; document.querySelectorAll('[data-symbol]').forEach(el => { const sym = el.dataset.symbol; if (sym && !syms.includes(sym)) syms.push(sym); }); return syms; }, isRunning: () => isRunning, onUpdate: function(cb) { if (typeof cb === 'function') updateCallbacks.push(cb); } }; })(); // Auto-initialize on DOMContentLoaded document.addEventListener('DOMContentLoaded', function() { // Small delay to ensure the DOM is fully rendered setTimeout(() => PricifyRT.init(), 500); }); // Also try to initialize immediately if DOM is already loaded if (document.readyState === 'complete' || document.readyState === 'interactive') { setTimeout(() => PricifyRT.init(), 500); }