Enhanced stock market interface with customizable settings, sorted favorites, API integration, and smart momentum analysis.
// ==UserScript==
// @name Torn Stock Market Enhancer
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description Enhanced stock market interface with customizable settings, sorted favorites, API integration, and smart momentum analysis.
// @author Legaci [2100546]
// @match https://www.torn.com/page.php?sid=stocks*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect api.torn.com
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION (Loaded from Storage) ---
const CONFIG = {
API_KEY: localStorage.getItem('TSME_API_KEY') || '',
// Default 60s (60000ms)
UPDATE_INTERVAL: parseInt(localStorage.getItem('TSME_UPDATE_INTERVAL')) || 60000,
CACHE_DURATION: 300000,
// Default 0.25%
ALERT_THRESHOLD_HOURLY: parseFloat(localStorage.getItem('TSME_ALERT_THRESHOLD')) || 0.25,
};
// --- STATE MANAGEMENT ---
const state = {
stocks: {},
historicalData: {},
lastPrices: {},
inMemoryCache: {},
pinnedStocks: JSON.parse(localStorage.getItem('TSME_pinnedStocks') || '[]'),
intervalId: null,
initialized: false
};
// --- LOGGING & UTILITIES ---
const Log = {
debug: (...args) => console.log(`[TSME DEBUG]`, ...args),
error: (...args) => console.error(`[TSME ERROR]`, ...args),
info: (...args) => console.info(`[TSME INFO]`, ...args)
};
const Cache = {
get(key) {
const item = state.inMemoryCache[key];
if (!item) return null;
if (Date.now() - item.timestamp > CONFIG.CACHE_DURATION) {
delete state.inMemoryCache[key];
return null;
}
return item.data;
},
set(key, data) {
state.inMemoryCache[key] = { data: data, timestamp: Date.now() };
},
};
function savePinnedStocks() {
localStorage.setItem('TSME_pinnedStocks', JSON.stringify(state.pinnedStocks));
}
function extractStockAcronym(stockElement) {
const nameEl = stockElement.querySelector('.nameContainer___bxIrG');
if (!nameEl) return null;
const acronymMatch = nameEl.textContent.match(/\(([A-Z]+)\)/);
return acronymMatch ? acronymMatch[1] : null;
}
function getStockIdByAcronym(acronym) {
if (!state.stocks) return null;
const stock = Object.values(state.stocks).find(s => s.acronym === acronym);
return stock ? stock.stock_id : null;
}
function formatNumber(num) {
if (typeof num !== 'number') return 'N/A';
return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
// --- DOM ELEMENT HELPERS ---
function createStarIcon(isPinned = false) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '14');
svg.setAttribute('height', '14');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', isPinned ? '#FFD700' : 'none');
svg.setAttribute('stroke', '#FFD700');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.classList.add('star-icon');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z');
svg.appendChild(path);
return svg;
}
function createPinButton(stockId) {
const button = document.createElement('button');
button.className = 'pin-button';
const isPinned = state.pinnedStocks.includes(stockId);
button.title = isPinned ? 'Unpin stock' : 'Pin stock';
const star = createStarIcon(isPinned);
button.appendChild(star);
button.addEventListener('click', (e) => {
e.stopPropagation();
togglePinStock(stockId);
button.innerHTML = '';
button.appendChild(createStarIcon(state.pinnedStocks.includes(stockId)));
});
return button;
}
// --- API HANDLERS ---
async function apiRequest(url, selection, key = CONFIG.API_KEY) {
return new Promise((resolve, reject) => {
if (!key) return reject(new Error('API key is missing.'));
GM_xmlhttpRequest({
method: 'GET',
url: `https://api.torn.com/${url}?key=${key}&selections=${selection}`,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
if (data.error) return reject(data.error);
resolve(data);
} catch (e) {
reject({ error: 'Invalid JSON response.' });
}
},
onerror: (error) => reject({ error: `Network error: ${error.status}` })
});
});
}
async function fetchStocksData() {
const cacheKey = 'all_stocks';
const cached = Cache.get(cacheKey);
if (cached) return cached;
const data = await apiRequest('torn', 'stocks');
Cache.set(cacheKey, data.stocks);
return data.stocks;
}
async function fetchStockHistory(stockId) {
const cacheKey = `stock_hist_${stockId}`;
const cached = Cache.get(cacheKey);
if (cached) return cached;
const data = await apiRequest(`torn/${stockId}`, 'stocks');
const stockData = data.stocks[stockId];
Cache.set(cacheKey, stockData);
return stockData;
}
// --- UI LOGIC ---
function togglePinStock(stockId) {
const index = state.pinnedStocks.indexOf(stockId);
if (index === -1) {
state.pinnedStocks.push(stockId);
} else {
state.pinnedStocks.splice(index, 1);
}
savePinnedStocks();
reorganizeStockList();
updateAnalytics();
}
function addPinButtons() {
document.querySelectorAll('.stock___ElSDB').forEach(stockEl => {
if (stockEl.querySelector('.pin-button')) return;
const acronym = extractStockAcronym(stockEl);
const stockId = getStockIdByAcronym(acronym);
if (!stockId) return;
const logoContainer = stockEl.querySelector('.logoContainer___M_y5p');
if (logoContainer) {
if (window.getComputedStyle(logoContainer).position === 'static') {
logoContainer.style.position = 'relative';
}
const pinButton = createPinButton(stockId);
logoContainer.appendChild(pinButton);
}
});
}
function reorganizeStockList() {
const stockRows = Array.from(document.querySelectorAll('.stock___ElSDB'));
if (!stockRows.length) return;
const parent = stockRows[0].parentElement;
if (!parent) return;
const pinnedRows = [];
const unpinnedRows = [];
stockRows.forEach(row => {
const acronym = extractStockAcronym(row);
const stockId = getStockIdByAcronym(acronym);
if (stockId && state.pinnedStocks.includes(stockId)) {
row.classList.add('pinned-stock');
pinnedRows.push(row);
} else {
row.classList.remove('pinned-stock');
unpinnedRows.push(row);
}
});
pinnedRows.sort((a, b) => {
const aId = getStockIdByAcronym(extractStockAcronym(a));
const bId = getStockIdByAcronym(extractStockAcronym(b));
return state.pinnedStocks.indexOf(aId) - state.pinnedStocks.indexOf(bId);
});
const fragment = document.createDocumentFragment();
[...pinnedRows, ...unpinnedRows].forEach(row => fragment.appendChild(row));
parent.appendChild(fragment);
addPinButtons();
}
function calculateTrend(stock) {
const hist = state.historicalData[stock.stock_id];
if (!hist) return null;
const momentum = hist.last_hour?.change_percentage || 0;
const dailyTrend = hist.last_day?.change_percentage || 0;
const weeklyTrend = hist.last_week?.change_percentage || 0;
let trend = 'neutral';
let speed = 'Stagnant';
if (momentum > 0.5) {
trend = 'strong-up';
speed = 'Fast Rising 🚀';
} else if (momentum > 0) {
trend = 'up';
speed = 'Slow Rising ↗️';
} else if (momentum < -0.5) {
trend = 'strong-down';
speed = 'Fast Falling 📉';
} else if (momentum < 0) {
trend = 'down';
speed = 'Slow Falling ↘️';
}
return { trend, speed, momentum, dailyTrend, weeklyTrend };
}
function addTrendIndicators() {
document.querySelectorAll('.stock___ElSDB').forEach(stockEl => {
const nameEl = stockEl.querySelector('.nameContainer___bxIrG');
if (!nameEl) return;
const acronym = extractStockAcronym(stockEl);
const stock = Object.values(state.stocks).find(s => s.acronym === acronym);
if (!stock) return;
const existingDot = nameEl.querySelector('.stock-trend-indicator');
if (existingDot) existingDot.remove();
const existingWrapper = nameEl.querySelector('.stock-momentum-wrapper');
if (existingWrapper) existingWrapper.remove();
const trend = calculateTrend(stock);
if (!trend) return;
const indicator = document.createElement('span');
indicator.className = `stock-trend-indicator trend-${trend.trend}`;
indicator.title = `${trend.speed}\nHourly: ${formatNumber(trend.momentum)}%\nDaily: ${formatNumber(trend.dailyTrend)}%\nWeekly: ${formatNumber(trend.weeklyTrend)}%`;
nameEl.appendChild(indicator);
});
}
function updateLivePrices() {
document.querySelectorAll('.stock___ElSDB').forEach(stockEl => {
const acronym = extractStockAcronym(stockEl);
const stock = Object.values(state.stocks).find(s => s.acronym === acronym);
if (!stock) return;
const priceEl = stockEl.querySelector('.priceContainer___c5v5F .value___YpnUQ');
if (!priceEl) return;
const currentUIPrice = parseFloat(priceEl.textContent.replace(/[$,]/g, '')) || 0;
const newAPIPrice = stock.current_price;
if (currentUIPrice !== newAPIPrice) {
const isUp = newAPIPrice > currentUIPrice;
priceEl.textContent = '$' + newAPIPrice.toLocaleString();
priceEl.classList.add('live-price-update', isUp ? 'price-up' : 'price-down');
setTimeout(() => {
priceEl.classList.remove('live-price-update', 'price-up', 'price-down');
}, 1000);
}
});
}
function checkAndDisplayAlerts(stock, newTrend, oldTrend) {
if (!newTrend || !oldTrend) return;
const momentum = newTrend.momentum;
const oldMomentum = oldTrend.momentum;
const threshold = CONFIG.ALERT_THRESHOLD_HOURLY;
const acronym = stock.acronym;
let alertMessage = null;
if (momentum >= threshold && oldMomentum < threshold) {
alertMessage = `📈 **${acronym}** surged past ${threshold}% hourly! (+${formatNumber(momentum)}%)`;
} else if (momentum <= -threshold && oldMomentum > -threshold) {
alertMessage = `📉 **${acronym}** dropped past -${threshold}% hourly! (${formatNumber(momentum)}%)`;
}
if (alertMessage) {
const container = document.getElementById('stock-enhancer-alerts');
if (container) {
const alertEl = document.createElement('div');
alertEl.className = `alert-box alert-${momentum > 0 ? 'buy' : 'sell'}`;
alertEl.innerHTML = `<strong>ALERT:</strong> ${alertMessage}`;
container.prepend(alertEl);
setTimeout(() => alertEl.classList.add('fading'), 5000);
setTimeout(() => alertEl.remove(), 7000);
}
}
}
function calculatePortfolioMetrics() {
const ownedStocks = [];
document.querySelectorAll('.stock___ElSDB').forEach(stockEl => {
const acronym = extractStockAcronym(stockEl);
const ownedCountEl = stockEl.querySelector('.stockOwned___eXJed .count___Al4Wq');
if (ownedCountEl && ownedCountEl.textContent !== 'None') {
const count = parseInt(ownedCountEl.textContent.replace(/,/g, ''));
if (count > 0 && acronym) {
ownedStocks.push({ acronym, count });
}
}
});
return ownedStocks;
}
function updateAnalytics() {
const content = document.getElementById('analytics-content');
if (!content) return;
const portfolio = calculatePortfolioMetrics();
const trackedStocks = new Set(portfolio.map(p => getStockIdByAcronym(p.acronym)).filter(Boolean).concat(state.pinnedStocks)).size;
const loadedHistorical = Object.keys(state.historicalData).length;
let html = '';
if (!CONFIG.API_KEY) {
html += `<div class="alert-box alert-sell" style="margin-bottom: 10px; cursor: pointer;" id="missing-key-alert">🔑 **API Key Missing!** Click to configure.</div>`;
}
html += '<div class="stock-metrics">';
const totalValueEl = document.querySelector('.tt-total-stock-value .value');
const totalProfitEl = document.querySelector('.tt-total-stock-value .profit');
if (totalValueEl) html += `<div class="metric-card"><div class="metric-label">Value</div><div class="metric-value">${totalValueEl.textContent}</div></div>`;
if (totalProfitEl) {
const profitText = totalProfitEl.textContent.trim();
const isNegative = profitText.startsWith('-');
html += `<div class="metric-card"><div class="metric-label">Profit</div><div class="metric-value ${isNegative ? 'negative' : 'positive'}">${profitText}</div></div>`;
}
html += `
<div class="metric-card"><div class="metric-label">Pinned</div><div class="metric-value">${state.pinnedStocks.length}</div></div>
<div class="metric-card"><div class="metric-label">Data</div><div class="metric-value">${loadedHistorical} / ${trackedStocks}</div></div>
</div>`;
const lastUpdateText = state.lastUpdate ? new Date(state.lastUpdate).toLocaleTimeString() : 'Waiting...';
const intervalSecs = Math.round(CONFIG.UPDATE_INTERVAL / 1000);
html += `<div class="update-time">Updated: ${lastUpdateText} (Every ${intervalSecs}s)</div>`;
content.innerHTML = html;
const alertBox = content.querySelector('#missing-key-alert');
if (alertBox) alertBox.addEventListener('click', () => showSettingsModal(false));
}
// --- MAIN LOOP ---
function startUpdateInterval() {
if (state.intervalId) clearInterval(state.intervalId);
if (CONFIG.API_KEY) state.intervalId = setInterval(updateData, CONFIG.UPDATE_INTERVAL);
}
async function updateData() {
try {
state.stocks = await fetchStocksData();
const portfolio = calculatePortfolioMetrics();
const ownedAcronyms = portfolio.map(p => p.acronym);
const stocksToFetchIds = new Set(ownedAcronyms.map(getStockIdByAcronym).filter(Boolean).concat(state.pinnedStocks));
for (const stockId of stocksToFetchIds) {
const stock = Object.values(state.stocks).find(s => s.stock_id === stockId);
if (!stock) continue;
const oldTrend = state.historicalData[stockId] ? calculateTrend(stock) : null;
const histData = await fetchStockHistory(stockId);
state.historicalData[stockId] = histData;
const newTrend = calculateTrend(stock);
if ((ownedAcronyms.includes(stock.acronym) || state.pinnedStocks.includes(stockId)) && oldTrend && newTrend) {
checkAndDisplayAlerts(stock, newTrend, oldTrend);
}
await new Promise(resolve => setTimeout(resolve, 50));
}
state.lastUpdate = Date.now();
updateAnalytics();
updateLivePrices();
addTrendIndicators();
addPinButtons();
reorganizeStockList();
} catch (error) {
if (error && error.code === 2) {
clearInterval(state.intervalId);
showSettingsModal(true);
}
updateAnalytics();
}
}
// --- MODALS & BUTTONS ---
function createApiKeyButton() {
const btn = document.createElement('button');
btn.innerHTML = '⚙️ Settings';
btn.style.cssText = `background: #444; color: white; border: 1px solid #666; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; margin-left: 5px; float: right; transform: translateY(-3px);`;
btn.addEventListener('click', () => showSettingsModal(false));
return btn;
}
function createHelpButton() {
const btn = document.createElement('button');
btn.innerHTML = '❓ Help';
btn.style.cssText = `background: #444; color: white; border: 1px solid #666; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; margin-left: 10px; float: right; transform: translateY(-3px);`;
btn.addEventListener('click', () => showHelpModal());
return btn;
}
// --- SETTINGS MODAL (Replaces showApiKeyModal) ---
async function showSettingsModal(wasInvalid = false) {
let modal = document.getElementById('tsme-modal');
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = 'tsme-modal';
modal.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); display: flex; justify-content: center; align-items: center; z-index: 99999;`;
const content = document.createElement('div');
content.style.cssText = `background: #1a1a1a; padding: 30px; border-radius: 8px; border: 1px solid #333; max-width: 420px; width: 90%; color: white;`;
let warning = wasInvalid ? `<p style="color: #ff4444; font-weight: bold;">⚠️ Invalid API Key</p>` : '';
// Display update interval in seconds (e.g. 60)
const currentIntervalSeconds = CONFIG.UPDATE_INTERVAL / 1000;
content.innerHTML = `
<h3 style="margin-top: 0; color: #4CAF50;">Configuration</h3>
${warning}
<div style="margin-bottom: 20px;">
<label style="display:block; margin-bottom:5px; color:#ccc; font-size:12px;">Torn API Key (Requires "Stocks")</label>
<input type="password" id="api-key-input" style="width: 100%; padding: 10px; background: #333; border: 1px solid #444; color: white; border-radius: 4px; box-sizing: border-box;" value="${CONFIG.API_KEY}">
</div>
<div style="display: flex; gap: 15px; margin-bottom: 25px;">
<div style="flex: 1;">
<label style="display:block; margin-bottom:5px; color:#ccc; font-size:12px;">Update Interval (seconds)</label>
<input type="number" id="interval-input" min="30" style="width: 100%; padding: 10px; background: #333; border: 1px solid #444; color: white; border-radius: 4px; box-sizing: border-box;" value="${currentIntervalSeconds}">
</div>
<div style="flex: 1;">
<label style="display:block; margin-bottom:5px; color:#ccc; font-size:12px;">Alert Threshold (%)</label>
<input type="number" id="threshold-input" step="0.01" min="0.01" style="width: 100%; padding: 10px; background: #333; border: 1px solid #444; color: white; border-radius: 4px; box-sizing: border-box;" value="${CONFIG.ALERT_THRESHOLD_HOURLY}">
</div>
</div>
<div style="display: flex; justify-content: flex-end;">
<button id="cancel-api-btn" style="padding: 8px 16px; background: #666; border: none; color: white; border-radius: 4px; cursor: pointer;">Close</button>
<button id="save-api-btn" style="padding: 8px 16px; background: #4CAF50; border: none; color: white; border-radius: 4px; cursor: pointer; margin-left: 10px;">Save & Apply</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
modal.querySelector('#save-api-btn').addEventListener('click', async () => {
const apiKeyInput = modal.querySelector('#api-key-input');
const intervalInput = modal.querySelector('#interval-input');
const thresholdInput = modal.querySelector('#threshold-input');
const saveBtn = modal.querySelector('#save-api-btn');
const key = apiKeyInput.value.trim();
const newInterval = parseInt(intervalInput.value) * 1000; // Convert back to ms
const newThreshold = parseFloat(thresholdInput.value);
if (key) {
apiKeyInput.disabled = true;
saveBtn.textContent = 'Validating...';
try {
await apiRequest('torn', 'stocks', key);
// Save to CONFIG & Storage
CONFIG.API_KEY = key;
CONFIG.UPDATE_INTERVAL = newInterval >= 30000 ? newInterval : 60000; // Minimum 30s safeguard
CONFIG.ALERT_THRESHOLD_HOURLY = newThreshold || 0.25;
localStorage.setItem('TSME_API_KEY', CONFIG.API_KEY);
localStorage.setItem('TSME_UPDATE_INTERVAL', CONFIG.UPDATE_INTERVAL);
localStorage.setItem('TSME_ALERT_THRESHOLD', CONFIG.ALERT_THRESHOLD_HOURLY);
modal.remove();
startUpdateInterval(); // Restart with new interval
updateData();
Log.info(`Configuration Saved: Interval=${CONFIG.UPDATE_INTERVAL}ms, Threshold=${CONFIG.ALERT_THRESHOLD_HOURLY}%`);
} catch (error) {
alert(`Invalid Key or Network Error`);
apiKeyInput.disabled = false;
saveBtn.textContent = 'Save & Apply';
}
} else {
alert('API Key is required.');
}
});
modal.querySelector('#cancel-api-btn').addEventListener('click', () => modal.remove());
}
// --- HELP MODAL (Updated) ---
function showHelpModal() {
let modal = document.getElementById('tsme-modal');
if (modal) modal.remove();
modal = document.createElement('div');
modal.id = 'tsme-modal';
modal.style.cssText = `position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); display: flex; justify-content: center; align-items: center; z-index: 99999;`;
const content = document.createElement('div');
content.style.cssText = `background: #1a1a1a; padding: 30px; border-radius: 8px; border: 1px solid #333; max-width: 500px; width: 90%; color: white; font-size: 14px; line-height: 1.6;`;
content.innerHTML = `
<h3 style="margin-top: 0; color: #4CAF50;">Enhancer Guide</h3>
<div style="margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 10px;">
<h4 style="color: #FFD700; margin: 10px 0 5px;">⭐ How to Pin Stocks</h4>
<p style="margin: 0; color: #ccc;">Hover your mouse over any <strong>Stock Logo</strong> (left side) to reveal the star button. Click it to pin that stock to the top.</p>
</div>
<div style="margin-bottom: 20px; border-bottom: 1px solid #333; padding-bottom: 10px;">
<h4 style="color: #4CAF50; margin: 10px 0 5px;">📊 Momentum Dots</h4>
<div style="display: grid; grid-template-columns: 20px auto; gap: 10px; align-items: center;">
<span class="stock-trend-indicator trend-strong-up" style="display:inline-block; margin:0;"></span> <span><strong>Glowing Green:</strong> Fast Rising (>0.5%/hr)</span>
<span class="stock-trend-indicator trend-up" style="display:inline-block; margin:0;"></span> <span><strong>Light Green:</strong> Slow Rising</span>
<span class="stock-trend-indicator trend-neutral" style="display:inline-block; margin:0;"></span> <span><strong>Yellow:</strong> Neutral / Stagnant</span>
<span class="stock-trend-indicator trend-down" style="display:inline-block; margin:0;"></span> <span><strong>Light Red:</strong> Slow Falling</span>
<span class="stock-trend-indicator trend-strong-down" style="display:inline-block; margin:0;"></span> <span><strong>Glowing Red:</strong> Fast Falling (<-0.5%/hr)</span>
</div>
</div>
<div style="margin-bottom: 20px;">
<h4 style="color: #2196F3; margin: 10px 0 5px;">⚡ Alerts & Updates</h4>
<p style="margin: 0; color: #ccc;">
Prices update every <strong>${CONFIG.UPDATE_INTERVAL / 1000} seconds</strong> (Configurable).<br>
Alerts trigger if a pinned/owned stock moves by <strong>${CONFIG.ALERT_THRESHOLD_HOURLY}%</strong> in one hour (Configurable).
</p>
</div>
<div style="display: flex; justify-content: flex-end;">
<button id="close-help-btn" style="padding: 8px 16px; background: #666; border: none; color: white; border-radius: 4px; cursor: pointer;">Close</button>
</div>
`;
modal.appendChild(content);
document.body.appendChild(modal);
modal.querySelector('#close-help-btn').addEventListener('click', () => modal.remove());
}
function createEnhancedUI() {
const style = `
.stock-enhancer-panel { background: #1a1a1a; border: 1px solid #333; border-radius: 5px; padding: 15px; margin: 10px 0; color: #fff; }
#stock-enhancer-alerts { min-height: 40px; margin-bottom: 10px; }
.alert-box { padding: 10px; margin-bottom: 5px; border-radius: 4px; font-size: 13px; border-left: 5px solid; opacity: 1; transition: opacity 2s ease-in-out; }
.alert-box.fading { opacity: 0; }
.alert-buy { background: #1a2a1a; border-left-color: #00ff00; }
.alert-sell { background: #2a1a2a; border-left-color: #ff4444; }
.stock-trend-indicator { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-left: 5px; cursor: help; vertical-align: middle; }
.trend-strong-up { background: #00ff00; box-shadow: 0 0 5px #00ff00; }
.trend-up { background: #90ee90; }
.trend-neutral { background: #ffff00; }
.trend-down { background: #ff6b6b; }
.trend-strong-down { background: #ff0000; box-shadow: 0 0 5px #ff0000; }
.stock-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 10px; margin-top: 10px; }
.metric-card { background: #252525; padding: 10px; border-radius: 4px; }
.metric-label { font-size: 11px; color: #888; margin-bottom: 5px; text-transform: uppercase; }
.metric-value { font-size: 16px; font-weight: bold; }
.positive { color: #85c742 !important; } .negative { color: #e55c5c !important; }
.star-icon { color: #FFD700; transition: color 0.2s; }
.pin-button {
position: absolute; top: -5px; right: -5px;
background: rgba(0,0,0,0.5); border-radius: 50%;
border: none; cursor: pointer; padding: 2px;
display: none; align-items: center; justify-content: center;
z-index: 10;
}
.logoContainer___M_y5p:hover .pin-button { display: flex; }
.pin-button:hover { background: rgba(0,0,0,0.8); }
.pinned-stock { border-left: 3px solid #FFD700; background: rgba(255, 215, 0, 0.05) !important; order: -1; }
.update-time { font-size: 11px; color: #666; text-align: right; margin-top: 10px; }
.live-price-update { font-weight: bold; }
.price-up { color: #00ff00; background-color: rgba(0, 255, 0, 0.1); transition: background-color 0.5s ease-out; }
.price-down { color: #ff4444; background-color: rgba(255, 68, 68, 0.1); transition: background-color 0.5s ease-out; }
`;
GM_addStyle(style);
const panel = document.createElement('div');
panel.id = 'stock-enhancer-main';
panel.className = 'stock-enhancer-panel';
panel.innerHTML = '<h3>📈 Stock Enhancer</h3><div id="stock-enhancer-alerts"></div><div id="analytics-content">Loading...</div>';
const header = document.querySelector('.appHeaderWrapper___uyPti');
if (header) header.parentNode.insertBefore(panel, header.nextSibling);
const h3 = panel.querySelector('h3');
h3.appendChild(createApiKeyButton());
h3.appendChild(createHelpButton());
}
function init() {
createEnhancedUI();
const listObserver = new MutationObserver((mutations, obs) => {
if (document.querySelector('.stock___ElSDB')) {
obs.disconnect();
updateData();
startUpdateInterval();
}
});
const root = document.getElementById('content-root') || document.body;
listObserver.observe(root, { childList: true, subtree: true });
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();