// Function to load stock attributes and add indicators function loadStockAttributes() { // Add cache-busting timestamp to force fresh load const cacheBuster = new Date().getTime(); fetch(`stockattributes.json?t=${cacheBuster}`) .then(response => { if (!response.ok) { throw new Error('stockattributes.json not found'); } return response.json(); }) .then(attributes => { addIndicatorsToTable(attributes); }) .catch(error => { console.log('No attributes file found'); }); } // Function to add indicators to the stock table (fully dynamic) function addIndicatorsToTable(attributes) { $('#stockTable tbody tr').each(function() { const $row = $(this); // Get the stock symbol let $stockNameElem = null; let stockSymbol = ''; if ($row.find('td:first-child .stock-name-bold').length) { $stockNameElem = $row.find('td:first-child .stock-name-bold'); stockSymbol = $stockNameElem.clone().children().remove().end().text().trim(); } else if ($row.find('td:first-child strong').length) { $stockNameElem = $row.find('td:first-child strong'); stockSymbol = $stockNameElem.text().trim(); } else { $stockNameElem = $row.find('td:first-child'); stockSymbol = $stockNameElem.text().trim(); } // Clean symbol (remove dot and everything after, also remove any numbers like x98) let cleanSymbol = stockSymbol.split(' ')[0]; cleanSymbol = cleanSymbol.split('.')[0]; // Check if this stock has attributes if (attributes[cleanSymbol]) { const attrs = attributes[cleanSymbol]; let indicators = []; // Add cyclic indicator if present – dynamic: no hardcoded mapping if (attrs.cyclic) { // Optional: pretty formatting (first letter uppercase, replace underscores) let cyclicText = attrs.cyclic.replace(/_/g, ' '); cyclicText = cyclicText.charAt(0).toUpperCase() + cyclicText.slice(1); // Special case for 'halfyearly' -> 'Half-yearly' if (cyclicText.toLowerCase() === 'Halfyearly') cyclicText = 'Halfyearly'; indicators.push(`${cyclicText}`); } // Add volatile indicator only if 'yes' if (attrs.volatile === 'yes') { indicators.push(`volatile`); } // Add indicators if (indicators.length > 0) { $stockNameElem.after(' ' + indicators.join(' ')); } } }); } // Add CSS styles for attributes function addStyles() { const style = ` `; $('head').append(style); } // ========== NEW: EXCHANGE HOURS & TIMER FUNCTIONALITY ========== // Exchange market hours (all times in Europe/Berlin timezone) const exchangeMarketHours = { 'XETRA': { name: 'Xetra', sessions: [{ open: '09:00', close: '17:30' }] }, 'FRA': { name: 'Frankfurt', sessions: [{ open: '09:00', close: '17:30' }] }, 'ETR': { name: 'XETRA', sessions: [{ open: '09:00', close: '17:30' }] }, 'NASDAQ': { name: 'NASDAQ', sessions: [{ open: '15:30', close: '22:00' }] }, 'NYSE': { name: 'NYSE', sessions: [{ open: '15:30', close: '22:00' }] }, 'LON': { name: 'London', sessions: [{ open: '09:00', close: '17:30' }] }, 'LSE': { name: 'London', sessions: [{ open: '09:00', close: '17:30' }] }, 'TYO': { name: 'Tokyo', sessions: [{ open: '01:00', close: '07:00' }] }, 'HKG': { name: 'Hong Kong', sessions: [{ open: '02:30', close: '09:00' }] }, 'Hong Kong': { name: 'Hong Kong', sessions: [{ open: '02:30', close: '09:00' }] }, 'OTC Markets': { name: 'OTC', sessions: [{ open: '15:30', close: '22:00' }] }, 'default': { name: 'US Market', sessions: [{ open: '15:30', close: '22:00' }] } }; // Get market status (open/closed and next event) function getMarketStatus(exchangeName) { const market = exchangeMarketHours[exchangeName] || exchangeMarketHours['default']; const now = new Date(); // Get Berlin time (Germany local) const berlinTime = new Date(now.toLocaleString('en-US', { timeZone: 'Europe/Berlin' })); const dayOfWeek = berlinTime.getDay(); // 0 = Sunday, 6 = Saturday const currentMinutes = berlinTime.getHours() * 60 + berlinTime.getMinutes(); // Weekend check if (dayOfWeek === 0 || dayOfWeek === 6) { // Calculate days until Monday const daysUntilMonday = dayOfWeek === 0 ? 1 : 2; const nextOpen = new Date(berlinTime); nextOpen.setDate(berlinTime.getDate() + daysUntilMonday); const [openHour, openMin] = market.sessions[0].open.split(':').map(Number); nextOpen.setHours(openHour, openMin, 0, 0); const diffMs = nextOpen - berlinTime; const hoursLeft = Math.floor(diffMs / (1000 * 60 * 60)); const minsLeft = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60)); return { status: 'closed', statusText: '🔴 CLOSED', nextEvent: `Opens Monday ${market.sessions[0].open} (in ${hoursLeft}h ${minsLeft}m)`, color: '#dc3545' }; } // Check each session let isOpen = false; let closeTime = null; let nextOpenTime = null; for (const session of market.sessions) { const [openHour, openMin] = session.open.split(':').map(Number); const [closeHour, closeMin] = session.close.split(':').map(Number); const openMinutes = openHour * 60 + openMin; const closeMinutes = closeHour * 60 + closeMin; // Check if currently open if (currentMinutes >= openMinutes && currentMinutes <= closeMinutes) { isOpen = true; closeTime = closeMinutes; break; } // Find next open (for today) if (openMinutes > currentMinutes) { if (nextOpenTime === null || openMinutes < nextOpenTime) { nextOpenTime = openMinutes; } } } if (isOpen && closeTime !== null) { // Market is OPEN - calculate time until close let minsUntilClose = closeTime - currentMinutes; if (minsUntilClose < 0) minsUntilClose += 24 * 60; const hoursLeft = Math.floor(minsUntilClose / 60); const minsLeft = minsUntilClose % 60; const closeHour = Math.floor(closeTime / 60); const closeMin = closeTime % 60; return { status: 'open', statusText: '🟢 OPEN', nextEvent: `Closes at ${closeHour.toString().padStart(2,'0')}:${closeMin.toString().padStart(2,'0')} (in ${hoursLeft}h ${minsLeft}m)`, color: '#28a745' }; } if (nextOpenTime !== null) { // Market is CLOSED but opens later today let minsUntilOpen = nextOpenTime - currentMinutes; if (minsUntilOpen < 0) minsUntilOpen += 24 * 60; const hoursLeft = Math.floor(minsUntilOpen / 60); const minsLeft = minsUntilOpen % 60; const openHour = Math.floor(nextOpenTime / 60); const openMin = nextOpenTime % 60; return { status: 'closed', statusText: '🔴 CLOSED', nextEvent: `Opens at ${openHour.toString().padStart(2,'0')}:${openMin.toString().padStart(2,'0')} (in ${hoursLeft}h ${minsLeft}m)`, color: '#ff9800' }; } // Market closed for the day - next open tomorrow const nextSession = market.sessions[0]; const [nextHour, nextMin] = nextSession.open.split(':').map(Number); let minsUntilOpen = (nextHour * 60 + nextMin) + (24 * 60) - currentMinutes; const hoursLeft = Math.floor(minsUntilOpen / 60); const minsLeft = minsUntilOpen % 60; return { status: 'closed', statusText: '🔴 CLOSED', nextEvent: `Opens tomorrow at ${nextSession.open} (in ${hoursLeft}h ${minsLeft}m)`, color: '#ff9800' }; } // Add exchange status indicators to each row function addExchangeStatusIndicators() { $('#stockTable tbody tr').each(function() { const $row = $(this); // Find the exchange cell (8th column, index 7) const $exchangeCell = $row.find('td').eq(7); if ($exchangeCell.length) { // Get the exchange name from the badge inside const $badge = $exchangeCell.find('.exchange-badge'); let exchangeName = $badge.length ? $badge.text().trim() : $exchangeCell.text().trim(); // Map common exchange names if (exchangeName === 'ETR') exchangeName = 'XETRA'; if (exchangeName === 'FRA') exchangeName = 'XETRA'; const status = getMarketStatus(exchangeName); // Remove any existing status div $exchangeCell.find('.exchange-status').remove(); // Create and append status HTML const statusHtml = `
${status.statusText} ${status.nextEvent}
`; $exchangeCell.append(statusHtml); } }); } // Update all exchange timers (runs every minute) let updateInterval = null; function startExchangeTimerUpdates() { if (updateInterval) clearInterval(updateInterval); function updateAllStatuses() { $('#stockTable tbody tr').each(function() { const $row = $(this); const $exchangeCell = $row.find('td').eq(7); if ($exchangeCell.length) { const $badge = $exchangeCell.find('.exchange-badge'); let exchangeName = $badge.length ? $badge.text().trim() : $exchangeCell.text().trim(); if (exchangeName === 'ETR') exchangeName = 'XETRA'; if (exchangeName === 'FRA') exchangeName = 'XETRA'; const status = getMarketStatus(exchangeName); let $statusDiv = $exchangeCell.find('.exchange-status'); if ($statusDiv.length === 0) { $statusDiv = $('
'); $exchangeCell.append($statusDiv); } $statusDiv.html(` ${status.statusText} ${status.nextEvent} `); } }); } updateAllStatuses(); updateInterval = setInterval(updateAllStatuses, 60000); // Update every minute } // Add CSS styles for exchange status function addExchangeStyles() { const style = ` `; $('head').append(style); } // ========== MODIFIED DOCUMENT READY ========== // Run when document is ready $(document).ready(function() { addStyles(); addExchangeStyles(); // Wait a bit for the table to be fully populated setTimeout(function() { loadStockAttributes(); addExchangeStatusIndicators(); startExchangeTimerUpdates(); }, 500); });