Greasy Fork

4chan Bizantine Numbers

See ticker price right where it's mentioned

目前为 2021-04-08 提交的版本。查看 最新版本

// ==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);