// ==UserScript== // @name 4chan Bizantine Numbers // @namespace smg // @match *://boards.4chan.org/biz/* // @match *://boards.4channel.org/biz/* // @connect query1.finance.yahoo.com // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM.listValues // @grant GM.xmlHttpRequest // @grant GM.addStyle // @version 0.1 // @author anon // @description See ticker price right where it's mentioned // @run-at document-start // @downloadURL none // ==/UserScript== // https://github.com/ranaroussi/yfinance/blob/main/yfinance/base.py var YahooFinance = 'https://query1.finance.yahoo.com/v8/finance/chart/'; // + ticker + '?range=1wk&interval=1d' // Load existing data from storage // {"TICK": Yahoo data} var cache = {}; // Lock ticker while async query pulls the data. // {"TICK": , …} var lock = new Map(); // time to consider data up to date: 15 minutes * 60 seconds * 1000 milliseconds var lifetime = 15 * 60 * 1000; function yahoo(ticker, range='1wk', interval='1d') { let url = YahooFinance + ticker + '?range=' + range + '&interval=' + interval; let cacheAge = 0; if (typeof cache[ticker] !== 'undefined' && cache[ticker].chart.error.code !== 'Not Found') cacheAge = cache[ticker].chart.result[0].meta.regularMarketTime; if (typeof cache[ticker] !== 'undefined' && cache[ticker].chart.error.code === 'Not Found') { // Not a ticker } else { if (typeof cacheAge === 'undefined' || cacheAge*1000 < (Date.now() - lifetime)) { // fetch data from yahoo console.log('Fetching data from Yahoo'); var xhr = GM.xmlHttpRequest({ method: "GET", url: url, onload: function(response) { let data = JSON.parse(response.responseText); cache[ticker] = JSON.parse(response.responseText); //GM.setValue('tickers', cache); if (data.chart.error === null) { populate(ticker, data); } } }); } else { // fetch data from cache console.log('Fetching data from cache'); populate(ticker, cache[ticker]); } } lock.delete(ticker); } /****************** * Thread parsing * ******************/ // Parse all posts once 4chan X's init finishes function init(e) { var posts = document.getElementsByClassName('postMessage'); tag(posts); parse(posts); } // Parse new posts on thread update function update(e) { var newPosts = e.detail.newPosts; var posts = []; for (let i = 0; i < newPosts.length; i++) { posts.push(document.getElementById(newPosts[i].replace(/.+\./g, 'm'))); } 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 tickers, 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-Z]{2,5}\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 tickers = posts[post].querySelectorAll('data[ticker]'); // extract tickers for (let i = 0; i < tickers.length; i++) { let ticker = tickers[i].getAttribute('ticker'); if (!lock.has(ticker) || lock.get(ticker) < (Date.now() - lifetime)) { lock.set(ticker, Date.now()); yahoo(ticker); } } } } function populate(ticker, data) { if (['EQUITY', 'ETF'].includes(data.chart.result[0].meta.instrumentType)) { // get all elements by tag and attribute ticker=ticker var tickers = document.querySelectorAll('data[ticker='+ticker+']'); let range = data.chart.result[0].meta.range; let interval = data.chart.result[0].meta.dataGranularity; let previous = data.chart.result[0].meta.chartPreviousClose; let open = data.chart.result[0].indicators.quote[0].open; let close = data.chart.result[0].indicators.quote[0].close; let price = data.chart.result[0].meta.regularMarketPrice; let change = {} change[range] = (((price/previous)-1)*100).toFixed(2); change[interval] = (((price/close[close.length-2])-1)*100).toFixed(2); for (let i = 0; i < tickers.length; i++) { let ticker = tickers[i]; ticker.setAttribute('title', price+' ('+change[interval]+'%)'); if (change['1d'] < -0.2) { ticker.classList.remove('green', 'crab'); ticker.classList.add('red'); } else if (change['1d'] > 0.2) { ticker.classList.remove('red', 'crab'); ticker.classList.add('green'); } else { ticker.classList.remove('red', 'green'); ticker.classList.add('crab'); } } } // update the data fields } // 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 function notify(type, content, lifetime) { content = 'Thread got updated\n' + e.detail.newPosts + ' - newPosts\n' + e.detail.deletedPosts + ' - deletedPosts'; 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); } // Add event listeners document.addEventListener('4chanXInitFinished', init, false); document.addEventListener('ThreadUpdate', update, false); // Add CSS let style = ` .ticker[title] {text-decoration: underline dotted 1px} .ticker.red {background-color: rgba(255,0,0,0.2)} .ticker.green {background-color: rgba(0,255,0,0.2)} .ticker.crab {background-color: rgba(255,255,0,0.2)} `; GM.addStyle(style);