tradingview screener assistant

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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