// ==UserScript== // @name 4chan Bizantine Numbers // @namespace smg // @match *://boards.4chan.org/biz/* // @match *://boards.4channel.org/biz/* // @connect query1.finance.yahoo.com // @grant GM.info // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM.listValues // @grant GM.xmlHttpRequest // @grant GM.addStyle // @version 1.1 // @author anon // @description See ticker price right where it's mentioned // @run-at document-start // @downloadURL none // ==/UserScript== // quote - lots of metadata var YahooFinancev7 = 'https://query1.finance.yahoo.com/v7/finance/quote'; // + '?symbols='+symbols + 'fields=' (optional) // chart - chart data; https://github.com/ranaroussi/yfinance/blob/main/yfinance/base.py var YahooFinancev8 = 'https://query1.finance.yahoo.com/v8/finance/chart/'; // + symbol + '?range=1wk&interval=1d' // Lock symbol while async query pulls the data. // {"SYMBOL": …} var lock = new Map(); // time to consider data up to date: 15 minutes * 60 seconds * 1000 milliseconds var lifetime = 15 * 60 * 1000; // Load existing data from storage // {"symbol": Yahoo Finance results[{…}, …]} var quotes = new Map(); var notasymbol = new Map(); // indices, currencies, precious metals & crypto var idx = ['SPX', 'GSPC', 'DJI', 'VIX', 'IXIC', 'NASDAQ', 'TNX', 'FTSE', 'N225', 'CMC200']; var forex = ['USD', 'EUR', 'JPY', 'CNY', 'KRW', 'GBP', 'CAD', 'AUD', 'RUB']; var metals = ['silver', 'gold', 'platinum', 'palladium']; var crypto = ['BTC', 'XMR', 'ALGO', 'LTC', 'ETH', 'LINK', 'DOGE']; var cryptopair = ['USD', 'EUR', 'BTC', 'XMR']; var changelog = `Tooltips no longer display above top edge of viewport`; async function yahoo(symbol) { let quote = []; var symbols = null; // special case for indices, forex and crypto if (idx.includes(symbol)) { if (symbol === 'NASDAQ') { symbols = '^IXIC'; } else { symbols = '^' + symbol; } } if (forex.includes(symbol)) { if (symbol === 'USD') { symbols = forex.slice(1).map(x => symbol + x + '=X').toString() + ',XMR-'+symbol + ',BTC-'+symbol; } else { symbols = forex.map(x => symbol + x + '=X').toString() + ',XMR-'+symbol + ',BTC-'+symbol; } } if (metals.includes(symbol)) { switch (symbol) { case 'silver': symbols = 'SI=F' break; case 'gold': symbols = 'GC=F'; break; case 'platinum': symbols = 'PL=F'; break; case 'palladium': symbols = 'PA=F'; break; } } if (crypto.includes(symbol)) { symbols = cryptopair.map(x => symbol + '-' + x).toString() + ',XMR-'+symbol; } // cache if (!quotes.has(symbol)) { // populate cache let rawquote = await GM.getValue('quote.'+symbol, '[]'); quote = JSON.parse(rawquote); if (quote.length > 0) { quotes.set(symbol, quote); } } else { quote = quotes.get(symbol); if (quote.length > 0 && typeof quote[0].regularMarketTime !== 'undefined' && quote[0].regularMarketTime * 1000 < (Date.now() - lifetime)) { // refresh cache let rawquote = await GM.getValue('quote.'+symbol, '[]'); quote = JSON.parse(rawquote); if (quote.length > 0) { quotes.set(symbol, quote); } } } // download fresh data if symbol is missing from cache or quote is too old if (typeof quote === 'undefined' || typeof quote !== 'undefined' && quote.length === 0 || typeof quote !== 'undefined' && quote.length > 0 && typeof quote[0].regularMarketTime === 'undefined' || typeof quote !== 'undefined' && quote.length > 0 && typeof quote[0].regularMarketTime !== 'undefined' && quote[0].regularMarketTime * 1000 < (Date.now() - lifetime)) { let quoteURL = YahooFinancev7 + '?symbols=' + (symbols || symbol); let quoteXHR = GM.xmlHttpRequest({ method: "GET", url: quoteURL, onload: function(response) { let data = JSON.parse(response.responseText); if (data.quoteResponse.error === null) { if (data.quoteResponse.result.length > 0) { let quote = data.quoteResponse.result; quotes.set(symbol, quote); let rawquote = JSON.stringify(quote); GM.setValue('quote.'+symbol, rawquote); populate(symbol, quote); } else if (quotes.has(symbol)) { // potential temporary error, use cache let quote = quotes.get(symbol); populate(symbol, quote); } else { // not a valid symbol notasymbol.set(symbol, true); } } } }); } else { if (quote.length > 0) { populate(symbol, quote); } } lock.delete(symbol); } /* // Old code for v8 API, v7 API is better for our needs function yahoov8(symbol, range='1wk', interval='1d') { let chartURL = YahooFinancev8 + symbol + '?range=' + range + '&interval=' + interval; // fetch data from yahoo var chartXHR = GM.xmlHttpRequest({ method: "GET", url: chartURL, onload: function(response) { let data = JSON.parse(response.responseText); if (data.chart.error === null) { populate(symbol, data); // populate data format changed } } }); } */ /****************** * Thread parsing * ******************/ // Parse all posts once 4chan X's init finishes function init(e) { async function scriptUpdated() { let v = await GM.getValue('version', '0.0'); if (v === '0.0') { notify('Bizantine Numbers installed' + '\nIf you find it useful, feel free to send me some XMR\n' + '88CNheH1h7PQgwK1Ehm6JvcAirBhXPJfyCHUvMhbgHwVCDKLu2c9fd9biPMXrEM4LK3Tta6638B9SDGwcDZDFcjw7ta8MuJ'); GM.setValue('version', GM.info.script.version); } else if (v < GM.info.script.version) { for (let i = 0; i < changelog.length; i++) { if (v < changelog[i].v) { notify(changelog[i].msg); } } notify('Bizantine Numbers updated\n' + changelog + '\nIf you find the script useful, feel free to send me some XMR\n' + '88CNheH1h7PQgwK1Ehm6JvcAirBhXPJfyCHUvMhbgHwVCDKLu2c9fd9biPMXrEM4LK3Tta6638B9SDGwcDZDFcjw7ta8MuJ'); GM.setValue('version', GM.info.script.version); } }; scriptUpdated(); var posts = document.getElementsByClassName('postMessage'); tag(posts); parse(posts); } // Parse new posts on thread update function update(e) { var posts = []; if (is4ChanX) { var newPosts = e.detail.newPosts; for (let i = 0; i < newPosts.length; i++) { posts.push(document.getElementById(newPosts[i].replace(/.+\./g, 'm'))); } } else { let elements = document.getElementsByClassName('postMessage'); for (let i = elements.length - e.detail.count; i < elements.length; i++) { posts.push(elements[i]); } } tag(posts); parse(posts); } // Get all text nodes // @param node Root node to look for text nodes under function textNodesUnder(node){ var all = []; for (node=node.firstChild;node;node=node.nextSibling){ if (node.nodeType==3) all.push(node); else all = all.concat(textNodesUnder(node)); } return all; } // Parse posts looking for symbols, wrapping them in element // @param array Post IDs to parse function tag(posts) { for (let post = 0; post < posts.length; post++) { var nodes = textNodesUnder(posts[post]); for (let node = 0; node < nodes.length; node++) { var n = nodes[node]; var htmlNode = document.createElement('span'); var html = n.textContent .replace(/\b[A-Z0-9]{1,6}([.=][A-Z]{1,2})?\b/g, '$&') .replace(/\b(gold|silver|platinum|palladium)\b/g, '$&'); n.parentNode.insertBefore(htmlNode, n); n.parentNode.removeChild(n); htmlNode.outerHTML = html; } } } // Parse the and start fetch function parse(posts) { // get all elements by tag for (let post = 0; post < posts.length; post++) { var symbols = posts[post].querySelectorAll('data[symbol]'); // extract symbols for (let i = 0; i < symbols.length; i++) { let symbol = symbols[i].getAttribute('symbol'); if (!notasymbol.has(symbol)) { if (!lock.has(symbol) || lock.get(symbol) < (Date.now() - lifetime)) { lock.set(symbol, Date.now()); yahoo(symbol); } } } } } function populate(symbol, quote) { if (typeof quote[0].regularMarketTime !== 'undefined' && typeof quote[0].regularMarketPrice !== 'undefined' && typeof quote[0].regularMarketChangePercent !== 'undefined') { let price = quote[0].regularMarketPrice; let change = quote[0].regularMarketChangePercent; // get all elements by tag and attribute symbol="symbol" var tickers = document.querySelectorAll('data[symbol="'+symbol+'"]'); for (let i = 0; i < tickers.length; i++) { let ticker = tickers[i]; //ticker.setAttribute('title', price+' ('+change.toFixed(2)+'%)'); ticker.setAttribute('price', price); if (change > 0.2) { ticker.style.backgroundColor = 'rgba('+(191-32*change)+','+(191+32*change)+',0,0.2)'; } else if (change < -0.2) { ticker.style.backgroundColor = 'rgba('+(191-32*change)+','+(191+32*change)+',127,0.2)'; } else { ticker.style.backgroundColor = 'rgba('+(255-63*change)+','+(255+63*change)+',0,0.2)'; } ticker.onmouseover = tooltip; } } } // add tooltips on inlined posts function postsInserted(e) { if (e.target.classList.contains('inline')) { console.log(e); // get all elements by tag and attribute symbol var tickers = e.target.querySelectorAll('data[symbol][price]'); for (let i = 0; i < tickers.length; i++) { let ticker = tickers[i]; ticker.onmouseover = tooltip; } } } var currencies = new Map([ ['USD', '$'], ['EUR', '€'], ['JPY', '¥'], ['CNY', '元'], ['GBP', '£'], ['CAD', '🍁'], ['AUD', '🦘'], ]); var cDesc = new Map([ ['USD', 'US Dollar'], ['EUR', 'Euro'], ['JPY', 'Japanese Yen'], ['CNY', 'Chinese Renminbi'], ['GBP', 'British Pound'], ['CAD', 'A Fucking Leaf'], ['AUD', '>money'], ['BTC', 'Bitcoin (shitcoin)'], ['XMR', 'Monero'], ['ALGO', 'Algorand'], ['ETH', 'Gas'], ['LTC', 'Litecoin'], ['LINK', 'Chainlink'], ['DOGE', 'Dogecoin 🚀'], ]); function tooltip(e) { let ticker = e.target; let symbol = ticker.getAttribute('symbol'); let quote = quotes.get(symbol); let tooltip = document.createElement('div'); tooltip.classList.add('dialog', 'tooltip'); if (!is4ChanX) { tooltip.classList.add('reply'); } ticker.append(tooltip); let h = document.createElement('header'); let c = document.createElement('span'); // currency let px = document.createElement('span'); // price h.style.fontSize = '1.5em'; h.style.fontWeight = 300; h.append(symbol, ' ', c); px.style.fontWeight = 500; h.append(px); let ppbr = document.createElement('br'); let pp = document.createElement('small'); pp.style.fontWeight = 400; h.append(ppbr, pp); let br = document.createElement('br'); h.append(br, ' '); let sm = document.createElement('small'); // small text h.append(sm); tooltip.append(h); let fx = document.createElement('table'); fx.classList.add('fx'); for (let i = 0; i < quote.length; i++) { if (i === 0 && (quote[i].quoteType === 'CURRENCY' || quote[i].quoteType === 'CRYPTOCURRENCY')) { tooltip.append(fx); } let el = document.createElement('div'); let currency = quote[i].currency; if (currencies.has(quote[i].currency)) { currency = currencies.get(quote[i].currency); } c.innerHTML = currency; let t = document.createElement('table'); t.style.width = '100%'; switch (quote[i].quoteType) { case 'EQUITY': case 'ETF': case 'FUTURE': case 'INDEX': c.innerHTML = currency; let price = quote[i].regularMarketPrice.toFixed(quote[i].priceHint); let change = quote[i].regularMarketChangePercent.toFixed(2); px.innerHTML = price + ' ('+change+'%)'; let prepost = ''; if (typeof quote[i].postMarketTime !== 'undefined') { if (quote[i].postMarketTime > quote[i].regularMarketTime) { prepost = 'post ' + currency + quote[i].postMarketPrice.toFixed(quote[i].priceHint) + ' (' + quote[i].postMarketChangePercent.toFixed(2) + '%)'; } } if (typeof quote[i].preMarketTime !== 'undefined') { if (quote[i].preMarketTime > quote[i].regularMarketTime) { prepost = 'pre ' + currency + quote[i].preMarketPrice.toFixed(quote[i].priceHint) + ' (' + quote[i].preMarketChangePercent.toFixed(2) + '%)'; } } if (prepost !== '') { pp.innerHTML = prepost; } else { ppbr.remove(); pp.remove(); pp.remove(); } sm.innerHTML = (quote[i].displayName || quote[i].longName || quote[i].shortName); if (typeof quote[i].fiftyTwoWeekLow !== 'undefined' && typeof quote[i].fiftyTwoWeekHigh !== 'undefined' && typeof quote[i].regularMarketDayLow !== 'undefined' && typeof quote[i].regularMarketDayHigh !== 'undefined') { let max = quote[i].fiftyTwoWeekHigh - quote[i].fiftyTwoWeekLow; let hi = quote[i].regularMarketDayHigh - quote[i].fiftyTwoWeekLow; let lo = quote[i].regularMarketDayLow - quote[i].fiftyTwoWeekLow; // min = 0; var range = document.createElement('div'); let lcr = document.createElement('div'); lcr.style.display = 'flex'; lcr.style.justifyContent = 'space-between'; let l = document.createElement('span'); l.append(quote[i].fiftyTwoWeekLow.toFixed(quote[i].priceHint)); let c = document.createElement('span'); c.append(quote[i].regularMarketDayLow.toFixed(quote[i].priceHint), ' - ', quote[i].regularMarketDayHigh.toFixed(quote[i].priceHint)); let r = document.createElement('span'); r.append(quote[i].fiftyTwoWeekHigh.toFixed(quote[i].priceHint)); lcr.append(l, c, r); range.append(lcr); let bar = document.createElement('div'); bar.style.height = '4px'; bar.style.minWidth = '300px'; bar.style.background = 'linear-gradient(to right, #FFFFFF '+(lo/max*100 -1).toFixed(2)+'%, #000000 '+(lo/max*100 -1).toFixed(2)+'% '+ (hi/max*100 +1).toFixed(2)+'%, #FFFFFF '+(lo/max*100 +1).toFixed(2)+'%)'; bar.style.borderBottom = 'solid 2px #777'; range.append(bar); el.append(range); } if (typeof quote[i].averageDailyVolume10Day !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('volume (10d avg.)'); let val = document.createElement('td'); val.append((quote[i].averageDailyVolume10Day/1000/1000).toFixed(3), 'm'); row.append(label, val); t.append(row); } if (typeof quote[i].fiftyDayAverage !== 'undefined' && typeof quote[i].twoHundredDayAverage !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('sma 50, 200'); let val = document.createElement('td'); val.append(quote[i].fiftyDayAverage.toFixed(quote[i].priceHint),', ', quote[i].twoHundredDayAverage.toFixed(quote[i].priceHint)); row.append(label, val); t.append(row); } if (typeof quote[i].epsTrailingTwelveMonths !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('eps ttm'); let val = document.createElement('td'); val.append(quote[i].epsTrailingTwelveMonths); row.append(label, val); t.append(row); } if (typeof quote[i].epsCurrentYear !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('eps current'); let val = document.createElement('td'); val.append(quote[i].epsCurrentYear); row.append(label, val); t.append(row); } if (typeof quote[i].epsForward !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('eps forward'); let val = document.createElement('td'); val.append(quote[i].epsForward); row.append(label, val); t.append(row); } if (typeof quote[i].priceEpsCurrentYear !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('P/E current'); let val = document.createElement('td'); val.append(quote[i].priceEpsCurrentYear.toFixed(quote[i].priceHint)); row.append(label, val); t.append(row); } if (typeof quote[i].forwardPE !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('P/E forward'); let val = document.createElement('td'); val.append(quote[i].forwardPE.toFixed(quote[i].priceHint)); row.append(label, val); t.append(row); } if (typeof quote[i].marketCap !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('mcap'); let val = document.createElement('td'); val.append((quote[i].marketCap/1000/1000/1000).toFixed(2), 'b'); row.append(label, val); t.append(row); } if (typeof quote[i].priceToBook !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('price-to-book'); let val = document.createElement('td'); val.append(quote[i].priceToBook.toFixed(quote[i].priceHint)); row.append(label, val); t.append(row); } if (typeof quote[i].averageAnalystRating !== 'undefined') { let row = document.createElement('tr'); let label = document.createElement('td'); label.append('avg. analyst rating'); let val = document.createElement('td'); val.append(quote[i].averageAnalystRating); row.append(label, val); t.append(row); } break; case 'CURRENCY': case 'CRYPTOCURRENCY': if (i===0) { if (cDesc.has(symbol)) { sm.innerHTML = cDesc.get(symbol); ppbr.remove(); pp.remove(); br.remove(); } if (quote[i].quoteType === 'CRYPTOCURRENCY') { h.removeChild(c); }; } let row = document.createElement('tr'); let c1 = document.createElement('td'); if (quote[i].quoteType === 'CURRENCY') { c1.innerHTML = quote[i].shortName; } if (quote[i].quoteType === 'CRYPTOCURRENCY') { c1.innerHTML = quote[i].symbol.replace('-', '/'); } let c2 = document.createElement('td'); c2.innerHTML = quote[i].regularMarketPrice.toFixed(quote[i].priceHint); let c3 = document.createElement('td'); c3.innerHTML = quote[i].regularMarketChangePercent.toFixed(2)+'%'; row.append(c1, c2, c3); fx.append(row); break; } el.append(t); tooltip.append(el); } let y = e.clientY - 24 - tooltip.getBoundingClientRect().height; tooltip.style.top = (y<24 ? 24 : y) + 'px'; tooltip.style.left = (e.clientX + 24) + 'px'; e.target.onmouseout = function(e) { let ticker = e.target; let tooltip = ticker.getElementsByClassName('tooltip')[0]; e.target.removeChild(tooltip); } } // Notify helper class https://github.com/ccd0/4chan-x/wiki/4chan-X-API#createnotification // @param type One of 'info', 'success', 'warning', or 'error' // @param content Message to display // @param lifetime Show notification for lifetime seconds; 0 = user needs to close it manually function notify(content, type='info', lifetime=0) { var detail = {type: type, content: content, lifetime: lifetime}; // dispatch event if (typeof cloneInto === 'function') { detail = cloneInto(detail, document.defaultView); } var event = new CustomEvent('CreateNotification', {bubbles: true, detail: detail}); document.dispatchEvent(event); console.log(type, content); } var is4ChanX = true; // Add event listeners document.addEventListener('4chanXInitFinished', init, false); document.addEventListener('ThreadUpdate', update, false); document.addEventListener('PostsInserted', postsInserted, false); // No 4chan X document.addEventListener("DOMContentLoaded", function (event) { setTimeout( function () { if (!document.documentElement.classList.contains("fourchan-x")) { is4ChanX = false; document.addEventListener('4chanThreadUpdated', update, false); init(); } }, (1) ); } ); // Add CSS let style = ` .ticker[price] {text-decoration: underline dotted 1px} .ticker .tooltip { position: fixed; padding: 8px; box-shadow: 0 1px 2px rgba(0, 0, 0, .15); font-variant: small-caps tabular-nums; } .ticker .tooltip header { font-size: 1.5em } .ticker .tooltip .fx { font-family: monospace } .ticker[symbol="CLF"] .dialog.tooltip, .ticker[symbol="MVIS"] .dialog.tooltip { background-image: linear-gradient(to bottom, #55CDFC 20%, #F7A8B8 20% 40%, #FFFFFF 40% 60%, #F7A8B8 60% 80%, #55CDFC 80%) !important } `; GM.addStyle(style);