Greasy Fork

Greasy Fork is available in English.

4chan Bizantine Numbers

See ticker price right where it's mentioned

当前为 2021-04-08 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.2
// @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-Z]{2,5}\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);