// ==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.4
// @author anon
// @description See ticker price right where it's mentioned
// @run-at document-start
// ==/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 === null) cacheAge = cache[ticker].chart.result[0].meta.regularMarketTime;
if (typeof cache[ticker] !== 'undefined' && cache[ticker].chart.error !== null) {
// Not a ticker
} else {
if (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 <data> 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,5}(\.[A-Z]{1,2})?\b/g, '<data class="ticker" ticker="$&">$&</data>');
n.parentNode.insertBefore(htmlNode, n);
n.parentNode.removeChild(n);
htmlNode.outerHTML = html;
}
}
}
// Parse the <data> and start fetch
function parse(posts) {
// get all elements by tag <data>
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 <data> 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);