tradingview screener assistant

insert batch copy button, chart copy button ,chart button and blacklist button in tradingview screener

// ==UserScript==
// @name         tradingview screener assistant
// @namespace    http://tampermonkey.net/
// @version      2024-09-20.3
// @description  insert batch copy button, chart copy button ,chart button and blacklist button in tradingview screener
// @author       goodzhuwang
// @match        https://*.tradingview.com/screener/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const ext_name = "tv_assistant";

  console.debug(`${ext_name} running`);

  let batch_button_id = "__batch_copybtn";

  // 代码黑名单
  let blacklist = [];
  // 最新的中概股名单 : https://stockanalysis.com/list/chinese-stocks-us/
  let chinese_stocks = [
    "BABA",
    "NTES",
    "JD",
    "BIDU",
    "PUK",
    "LI",
    "BEKE",
    "ZTO",
    "TME",
    "YUMC",
    "NIO",
    "EDU",
    "HTHT",
    "FUTU",
    "XPEV",
    "YMM",
    "VIPS",
    "BILI",
    "BZ",
    "MNSO",
    "TAL",
    "ZK",
    "QFIN",
    "GDS",
    "LOT",
    "ATHM",
    "ATAT",
    "HCM",
    "MLCO",
    "RLX",
    "ZLAB",
    "IQ",
    "LU",
    "WB",
    "SIMO",
    "FINV",
    "DQ",
    "MOMO",
    "MSC",
    "JKS",
    "SDA",
    "HUYA",
    "VNET",
    "TUYA",
    "XCH",
    "EH",
    "GOTU",
    "TIGR",
    "ECX",
    "DDL",
    "NOAH",
    "HSAI",
    "KC",
    "ZKH",
    "SOHU",
    "RERE",
    "ICG",
    "ZJYL",
    "YRD",
    "DOGZ",
    "WDH",
    "PGHL",
    "DAO",
    "QD",
    "DADA",
    "YSG",
    "ZH",
    "TROO",
    "JFIN",
    "UXIN",
    "YXT",
    "LX",
    "DOYU",
    "RITR",
    "GHG",
    "DSY",
    "XYF",
    "LANV",
    "AHG",
    "BSII",
    "HPH",
    "AGBA",
    "CANG",
    "RTC",
    "BZUN",
    "NIU",
    "KNDI",
    "LSB",
    "EM",
    "YIBO",
    "ZBAO",
    "GSIW",
    "SRL",
    "AZI",
    "SFWL",
    "CMCM",
    "CAAS",
    "TOUR",
    "XNET",
    "THCH",
    "ADAG",
    "CASI",
    "QMMM",
    "LGCL",
    "QH",
    "VIOT",
    "GLAC",
    "CBAT",
    "QSG",
    "STG",
    "GLXG",
    "IH",
    "RGC",
    "WIMI",
    "JVSA",
    "SY",
    "NHTC",
    "LICN",
    "DIST",
    "XHG",
    "CNF",
    "FEBO",
    "FANH",
    "WOK",
    "JUNE",
    "CCG",
    "PTHL",
    "YHNA",
    "NCTY",
    "CHSN",
    "BEDU",
    "OCFT",
    "BGM",
    "NISN",
    "TOP",
    "PMAX",
    "MTC",
    "PRE",
    "YI",
    "FVN",
    "BEST",
    "BYU",
    "ZEPP",
    "UCL",
    "EHGO",
    "RCON",
    "NAAS",
    "HKIT",
    "MATH",
    "SWIN",
    "TWG",
    "EBON",
    "CDTG",
    "ABLV",
    "CLPS",
    "AGMH",
    "FENG",
    "SJ",
    "BNR",
    "KUKE",
    "HUIZ",
    "RAY",
    "AIXI",
    "JG",
    "ICLK",
    "MTEN",
    "HUDI",
    "IZM",
    "UTSI",
    "GMM",
    "CGA",
    "MHUA",
    "RETO",
    "HAO",
    "EPOW",
    "CHR",
    "CCM",
    "MI",
    "HLP",
    "UCAR",
    "PSIG",
    "WETH",
    "CCTG",
    "WLGS",
    "NA",
    "FEDU",
    "UBXG",
    "BHAT",
    "ATGL",
    "HOLO",
    "MGIH",
    "ILAG",
    "ABTS",
    "JFU",
    "JYD",
    "JZ",
    "CPOP",
    "NCI",
    "SEED",
    "SUGP",
    "PT",
    "AACG",
    "EDTK",
    "MOGU",
    "MMV",
    "JZXN",
    "ZKIN",
    "XIN",
    "JDZG",
    "GSUN",
    "CLWT",
    "CJET",
    "INTJ",
    "LOBO",
    "GDHG",
    "AIHS",
    "STEC",
    "JL",
    "PETZ",
    "SOS",
    "YJ",
    "YQ",
    "CJJD",
    "GURE",
    "JXJT",
    "WTO",
    "MFI",
    "GRFX",
    "MEGL",
    "NXTT",
    "TCTM",
    "ROMA",
    "HIHO",
    "EJH",
    "UPC",
    "KRKR",
    "WAFU",
    "CLEU",
    "DTSS",
    "RAYA",
    "BON",
    "CREG",
    "PWM",
    "DDC",
    "SNTG",
    "LKCO",
    "CHNR",
    "YGMZ",
    "OST",
    "TCJH",
    "OCG",
    "TIRX",
    "ANTE",
    "IFBD",
    "TAOP",
    "CPHI",
    "CNET",
    "BTCT",
    "EZGO",
    "SISI",
    "BAOS",
    "KXIN",
    "ZCMD",
    "ATXG",
    "JWEL",
    "TC",
    "WNW",
    "LXEH",
    "DUO",
    "ITP",
    "VSME",
    "FAMI",
    "SXTC",
    "BQ",
    "MLGO",
    "TANH",
    "UK",
    "CNEY",
  ];

  const blacklist_icon_svgstr =
    '<svg class="_LC-finviz-blacklist-icon" style="position: absolute; left: 80px; top: 0;" width="80" height="36" viewport="0 0 80 36" xmlns="http://www.w3.org/2000/svg"> <g> <title>Layer 1</title> <text stroke-width="4" font-weight="bold" xml:space="preserve" text-anchor="start" font-family="\'Bitter\'" font-size="24" id="svg_1" y="27" x="4.5" fill="#bf0000">黑名单</text> <rect stroke="#bf0000" rx="5" fill-opacity="0" id="svg_4" height="30" width="76" y="3" x="2" stroke-width="4" fill="#000000"/> </g> </svg>';
  /**
   * Returns an array of elements which are the symbol items in the list.
   * These are the elements which contain the symbol name and code.
   *
   * @return {HTMLCollectionOf<Element>} the symbol items
   */
  function getSymbolItems() {
    // 列表模式下的选择器: 正则比配: tickerName-
    // 图表模式下的选择器,正则比配:symbolNameBox-. @todo 需要优化,图表模式下,tv会自动删除不必要的图表项目,导致全选无法选中所有的图表。暂时没有办法解决
    let list_mode_items = findElementsByClassRegex(/tickerName-/);
    let chart_mode_items = findElementsByClassRegex(/symbolNameBox-/);

    if (checkIsChartMode()) {
      return chart_mode_items;
    } else {
      return list_mode_items;
    }
  }

  function copyToClipboard(text) {
    // 将文本复制到剪贴板
    navigator.clipboard
      .writeText(text)
      .then(function () {
        console.debug("Text copied to clipboard");
      })
      .catch(function (err) {
        console.error("Failed to copy text to clipboard: ", err);
      });
  }

  /**
   * Finds all elements in the document whose class attribute starts with the
   * given regex pattern.
   * @param {RegExp} classNameRegex the regex pattern to match against the
   *     element's class attribute
   * 注意:这个选择器是根据class属性的值匹配的。如果元素有多个class选择器,但是class属性只有一个哦
   * @return {!Array.<!Element>} an array of elements that match the given regex
   *     pattern
   * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector
   * 不知道为什么 class~=name,会找不到
   */
  function findElementsByClassRegex(classNameRegex) {
    const selector = `*[class*="${classNameRegex.source}"]`;
    return document.querySelectorAll(selector);
  }

  function checkIsChartMode() {
    let el = findElementsByClassRegex(/chartsContent/);
    return el && el.length > 0;
  }
  // 显示Toast消息
  function showToast(message) {
    // 创建一个div元素作为Toast消息容器
    var toast = document.createElement("div");
    toast.style.position = "fixed";
    toast.style.top = "5%";
    toast.style.left = "50%";
    toast.style.transform = "translate(-50%, -50%)";
    toast.style.backgroundColor = "rgba(0, 0, 0, 0.7)";
    toast.style.color = "#fff";
    toast.style.padding = "10px";
    toast.style.borderRadius = "5px";
    toast.style.zIndex = "9999";

    toast.textContent = message;
    document.body.appendChild(toast);
    setTimeout(function () {
      document.body.removeChild(toast);
    }, 2000);
  }

  function insertStyleNode() {
    let style_node_id = "_my_button_style";

    // 获取具有ID为myDiv的节点
    var el = document.getElementById(style_node_id);

    // 从body中删除myDiv节点
    if (el) {
      document.body.removeChild(el);
    }

    // 创建一个style元素
    var style = document.createElement("style");

    style.id = style_node_id;
    // 设置style元素的内容为CSS代码
    style.innerHTML = `
      ._LC-button {
        border: none;
        padding: 3px 5px;
        background-color: #007bff4f;
        color: #fff;
        border-radius: 2px;
        cursor: pointer;
        font-size: 10px;
      }
        ._LC-button:hover {
        background-color: #f0f3fa;
      }

      ._LC-button:active {
        background-color: #0033664f;
      }

      ._LC-batch-copy-button{
        border-radius: 6px;
        font-size: 12px;
        min-width: 34px;
        --ui-lib-light-button-default-color-bg: #0000;
        --ui-lib-light-button-default-color-content: #131722;
        --ui-lib-light-button-default-color-border: #e0e3eb;
        align-items: center;
        background-color: var(--ui-lib-light-button-color-bg, var(--ui-lib-light-button-default-color-bg));
        border-color: var(--ui-lib-light-button-color-border, var(--ui-lib-light-button-default-color-border));
        border-style: solid;
        border-width: 1px;
        box-sizing: border-box;
        color: var(--ui-lib-light-button-color-content, var(--ui-lib-light-button-default-color-content));
        cursor: default;
        display: flex;
        justify-content: center;
        min-width: 36px;
        outline: none;
        padding: 0;
        margin-right: 0;
      }

      ._LC-chart-link,._LC-copy-button,._LC-finviz-add-remove-blacklist-button{
        font-family: -apple-system, BlinkMacSystemFont, Trebuchet MS, Roboto, Ubuntu, sans-serif;
        font-feature-settings: "tnum" on, "lnum" on;
        --ui-lib-typography-line-height: 16px;
        line-height: var(--ui-lib-typography-line-height);
        --ui-lib-typography-font-size: 12px;
        background-color: #f0f3fa;
        border:none;
        border-radius: 6px;
        margin-left:3px;
        box-sizing: border-box;
        color: #131722;
        display: block;
        font-size: var(--ui-lib-typography-font-size);
        font-style: normal;
        font-weight: 600;
        max-width: 96px;
        min-width: 36px;
        overflow: hidden;
        padding: 4px 8px;
        text-align: center;
        text-overflow: ellipsis;
        text-transform: uppercase;
        white-space: nowrap;
        cursor:pointer;
      }
      
    `;

    // 将style元素添加到body中
    document.body.appendChild(style);
  }

  // 插入“复制全部”按钮到页面
  function insertCopyAllButton() {
    let buttonWrapper = document.querySelector(
      ".innerControlContainer-k3vjdDEs"
    );
    if (!buttonWrapper) {
      buttonWrapper = document.getElementById("js-screener-container");
    }
    if (!buttonWrapper) {
      buttonWrapper = document.body;
    }

    // 获取具有ID为myDiv的节点
    var batch_copy_button = document.getElementById(batch_button_id);

    // 从body中删除myDiv节点
    if (batch_copy_button) {
      buttonWrapper.removeChild(batch_copy_button);
    }

    // 创建一个div元素
    batch_copy_button = document.createElement("button");

    // 设置div元素的属性和样式
    batch_copy_button.id = batch_button_id;
    batch_copy_button.classList.add("_LC-button");
    batch_copy_button.classList.add("_LC-batch-copy-button");
    batch_copy_button.innerHTML = `<svg width="28" height="28" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" stroke-width="3" stroke="#000000" fill="none"><rect x="11.13" y="17.72" width="33.92" height="36.85" rx="2.5"/><path d="M19.35,14.23V13.09a3.51,3.51,0,0,1,3.33-3.66H49.54a3.51,3.51,0,0,1,3.33,3.66V42.62a3.51,3.51,0,0,1-3.33,3.66H48.39"/></svg>`;
    // 将div元素插入到body中

    batch_copy_button.title = "批量复制股票代码";

    buttonWrapper.appendChild(batch_copy_button);
    batch_copy_button.addEventListener("click", function (event) {
      const elements = getSymbolItems();
      const texts = [];
      elements.forEach((el) => {
        texts.push(el.innerText);
      });

      if (texts.length === 0) {
        showToast("页面中没有找到代码,请检查脚本选择器");
      } else {
        const res = texts.join(", ");
        copyToClipboard(res);
        // 示例用法
        if (checkIsChartMode()) {
          showToast(
            `已复制 ${texts.length} 个代码到剪贴板。图表模式下可能无法全选复制,请切换到列表模式下复制全部。`
          );
        } else {
          showToast(`已复制 ${texts.length} 个代码到剪贴板`);
        }
      }
    });
  }

  function insertItemButtons() {
    let nodes = findElementsByClassRegex(/symbolNameBox-/);

    if (!nodes.length) {
      console.debug(`${ext_name} not found chart items`);
      return;
    }

    nodes.forEach((e) => {
      let target_el = e;
      let item_wrapper = e.parentNode;
      let href = e.getAttribute("href") || "";

      let exchange_symbol = href
        .replaceAll(/^\/symbols\//g, "")
        .replaceAll(/\/$/g, "")
        .replace("-", ":");

      let symbol = exchange_symbol.replace(/^\w+\:/, "");

      // 添加移除黑名单
      let add_remove_blacklist_button = item_wrapper.querySelector(
        "._LC-finviz-add-remove-blacklist-button"
      );
      if (!add_remove_blacklist_button) {
        add_remove_blacklist_button = document.createElement("button");
        add_remove_blacklist_button.classList.add("_LC-button");
        add_remove_blacklist_button.classList.add(
          "_LC-finviz-add-remove-blacklist-button"
        );
        if (blacklist.includes(symbol)) {
          add_remove_blacklist_button.innerText = "-BL";
        } else {
          add_remove_blacklist_button.innerText = "+BL";
        }
        add_remove_blacklist_button.title = "添加/移除黑名单";
        item_wrapper.appendChild(add_remove_blacklist_button);

        add_remove_blacklist_button.addEventListener("click", function (event) {
          if (blacklist.includes(symbol)) {
            let index = blacklist.indexOf(symbol);
            if (index !== -1) {
              blacklist.splice(index, 1);
            }
            add_remove_blacklist_button.innerText = "+BL";
          } else {
            add_remove_blacklist_button.innerText = "-BL";
            blacklist.push(symbol);
          }

          add_or_remove_blacklist_icon(target_el, symbol);
          localStorage.setItem("blacklist", JSON.stringify(blacklist));
        });
      }

      // 复制按钮
      let copybtn = item_wrapper.querySelector("._LC-copy-button");
      if (!copybtn) {
        copybtn = document.createElement("button");
        copybtn.title = "复制股票代码";
        copybtn.innerText = "复制";
        copybtn.classList.add("_LC-button");
        copybtn.classList.add("_LC-copy-button");

        copybtn.addEventListener("click", function (event) {
          copyToClipboard(symbol);
          showToast(`已复制代码:${symbol}`);
        });
        item_wrapper.appendChild(copybtn);
      }

      // 跳转tv图表按钮。
      let tv_link = item_wrapper.querySelector("._LC-tv-chart-link");
      if (!tv_link) {
        let uri_symbol = encodeURIComponent(exchange_symbol);
        tv_link = document.createElement("a");
        tv_link.classList.add("_LC-button");
        tv_link.classList.add("_LC-chart-link");
        tv_link.classList.add("_LC-tv-chart-link");
        tv_link.href = `https://cn.tradingview.com/chart/700qUKjc/?symbol=${uri_symbol}`;
        tv_link.target = "_blank";
        tv_link.innerText = "图表";
        tv_link.title = "查看tradingview图表";
        item_wrapper.appendChild(tv_link);
      }
      // 跳转finviz图表按钮。
      let finviz_link = item_wrapper.querySelector("._LC-finviz-chart-link");
      if (!finviz_link) {
        finviz_link = document.createElement("a");
        finviz_link.classList.add("_LC-button");
        finviz_link.classList.add("_LC-chart-link");
        finviz_link.classList.add("_LC-finviz-chart-link");

        finviz_link.href = `https://finviz.com/quote.ashx?t=${symbol}&p=d`;
        finviz_link.target = "_blank";
        finviz_link.innerText = "finviz";
        finviz_link.title = "查看finviz图表";
        item_wrapper.appendChild(finviz_link);
      }

      // 插入黑名单标志
      add_or_remove_blacklist_icon(target_el, symbol);
    });
  }

  // 添加或移除黑名单标志
  function add_or_remove_blacklist_icon(target_el, symbol) {
    let item_wrapper = target_el.parentNode;
    let blacklist_icon = item_wrapper.querySelector(
      "._LC-finviz-blacklist-icon"
    );
    if (blacklist.includes(symbol)) {
      if (!blacklist_icon) {
        target_el.insertAdjacentHTML("afterend", blacklist_icon_svgstr);
      }
    } else {
      let blacklist_icon = item_wrapper.querySelector(
        "._LC-finviz-blacklist-icon"
      );
      blacklist_icon && blacklist_icon.remove();
    }
  }

  function init_blacklist() {
    let blacklist_str = localStorage.getItem("blacklist");
    console.debug("初始化blacklist", blacklist);
    if (blacklist_str) {
      try {
        let list = JSON.parse(blacklist_str);
        if (Array.isArray(list)) {
          blacklist = [...chinese_stocks, ...list];
        }
      } catch (error) {}
    } else {
      blacklist = [...chinese_stocks];
    }
  }
  // 初始化blacklist

  init_blacklist();

  insertStyleNode();
  insertCopyAllButton();

  let _interval = setInterval(function () {
    console.debug(`${ext_name}定时检测item按钮是否创建`);
    let nodes = document.querySelectorAll("._LC-chart-link");
    // if (nodes && nodes.length > 0) {
    //     console.debug(`${ext_name}item按钮创建完成`)
    //     // if (_interval) {
    //     //     clearInterval(_interval)
    //     //     _interval = null
    //     // }
    // } else {
    // }

    let items = findElementsByClassRegex(/chartContainer/);

    if (items && items.length) {
      insertItemButtons();
    } else {
      console.debug(
        `${ext_name}没有找到需要添加按钮的item,请检查class属性是否正确: .chartContainer `
      );
    }
  }, 5000);
})();