backpack tool

backpack script 刷成交量脚本,支持手动开启关闭,设置买入卖出点

目前為 2024-04-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         backpack tool
// @namespace    https://github.com/zhowiny
// @version      0.1.0
// @author       zhowiny
// @description  backpack script 刷成交量脚本,支持手动开启关闭,设置买入卖出点
// @icon         https://backpack.exchange/favicon-32x32.png
// @match        https://backpack.exchange/trade/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.global.prod.js
// @require      data:application/javascript,%3Bwindow.Vue%3DVue%3B
// @grant        GM_addStyle
// ==/UserScript==

(t=>{if(typeof GM_addStyle=="function"){GM_addStyle(t);return}const e=document.createElement("style");e.textContent=t,document.head.append(e)})(' *,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.fixed{position:fixed}.mt-4{margin-top:1rem}.flex{display:flex}.grid{display:grid}.h-12{height:3rem}.h-2{height:.5rem}.h-full{height:100%}.w-12{width:3rem}.flex-1{flex:1 1 0%}.snap-mandatory{--tw-scroll-snap-strictness: mandatory}.flex-col{flex-direction:column}.gap-2{gap:.5rem}.self-center{align-self:center}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.border{border-width:1px}.border-t{border-top-width:1px}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity))}.bg-black\\/25{background-color:#00000040}.p-2{padding:.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pt-2{padding-top:.5rem}.text-center{text-align:center}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-sm{font-size:.875rem;line-height:1.25rem}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.opacity-10{opacity:.1}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)} ');

(function (vue) {
  'use strict';

  const _hoisted_1 = {
    class: "backpack-tool grid gap-2 text-sm text-white bg-base-700 p-2 rounded",
    style: { "grid-template-areas": "'a a . .' 'a a . .' 'a a . .' 'a a . .' 'b b b b' '. . . .'" }
  };
  const _hoisted_2 = /* @__PURE__ */ vue.createElementVNode("span", null, "限价:", -1);
  const _hoisted_3 = ["disabled"];
  const _hoisted_4 = /* @__PURE__ */ vue.createElementVNode("span", null, "市场:", -1);
  const _hoisted_5 = ["disabled"];
  const _hoisted_6 = ["disabled"];
  const _hoisted_7 = ["disabled"];
  const _hoisted_8 = /* @__PURE__ */ vue.createElementVNode("span", null, "超时时间(秒):", -1);
  const _hoisted_9 = ["disabled"];
  const _hoisted_10 = /* @__PURE__ */ vue.createElementVNode("p", {
    class: "mt-4 px-2 pt-2 border-t",
    style: { "grid-area": "b" }
  }, [
    /* @__PURE__ */ vue.createTextVNode(" 超时时间:超时自动取消订单,"),
    /* @__PURE__ */ vue.createElementVNode("code", null, "0"),
    /* @__PURE__ */ vue.createTextVNode("为不取消! ")
  ], -1);
  const _hoisted_11 = { style: { "color": "#afa" } };
  const _hoisted_12 = { style: { "color": "#faf" } };
  const _hoisted_13 = { style: { "color": "#ffa" } };
  const MIN_WAIT_MS = 300;
  const MAX_WAIT_MS = 1e3;
  const MIN_SWITCH_MS = 500;
  const MAX_SWITCH_MS = 2e3;
  const _sfc_main$1 = {
    __name: "BackpackTool",
    setup(__props) {
      const LANG_MAP = {
        Limit: "限制",
        Market: "市场",
        Max: "最大",
        Buy: "购买",
        Sell: "出售",
        Cancel: "取消"
      };
      const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
      const getRandomWait = (min, max) => Math.floor(Math.random() * max + min);
      function findElementsByText(text, tag, parent = document) {
        const elements = parent.querySelectorAll(`${tag}:not(:empty):not(:has(*))`);
        return Array.from(elements).filter((ele) => ele.textContent === text || ele.textContent === LANG_MAP[text]);
      }
      function getElement(text, tag) {
        let element = findElementsByText(text, tag)[0];
        if (!element) {
          element = findElementsByText(LANG_MAP[text] || text, tag)[0];
          if (!element)
            return;
        }
        return element;
      }
      async function clickElementByText(text, tag) {
        const element = getElement(text, tag);
        if (!element || !window.running)
          return;
        element.click();
        await sleep(getRandomWait(MIN_WAIT_MS, MAX_WAIT_MS));
      }
      function getPriceCnt() {
        return document.querySelector(".flex.flex-col.no-scrollbar.h-full.flex-1.snap-mandatory.overflow-y-auto.font-sans");
      }
      function getPriceElement(type, num) {
        const isBuy = type === "Buy";
        const priceCnt = getPriceCnt();
        return priceCnt.querySelector(`& > div:${isBuy ? "last" : "first"}-child > div > div:nth-child(${num}) button div`);
      }
      async function setPrice(type, num) {
        const price = getPriceElement(type, num);
        price.classList.add("border");
        price.click();
        await sleep(300);
      }
      let buyCount = 0;
      let sellCount = 0;
      let cancelCount = 0;
      async function clickTradeButton(type) {
        const element = getElement(type, "button");
        if (!element)
          return;
        element.addEventListener("click", () => {
          if (type === "Buy") {
            buyCount++;
            console.log(`%c第${buyCount}次买入`, "color: #afa;");
          } else {
            sellCount++;
            console.log(`%c第${sellCount}次卖出`, "color: #faf;");
          }
        }, { once: true });
        element.click();
        await sleep(getRandomWait(MIN_WAIT_MS, MAX_WAIT_MS));
      }
      async function executeTrade(type, params) {
        if (!window.running)
          return console.log("已暂停");
        await clickElementByText(type, "p");
        await clickElementByText(params.mode || "Limit", "div");
        await setPrice(type, params[type]);
        await clickElementByText("Max", "div");
        await clickTradeButton(type);
      }
      async function performTradeCycle(params) {
        try {
          await executeTrade("Buy", params);
          await sleep(getRandomWait(MIN_SWITCH_MS, MAX_SWITCH_MS));
          await executeTrade("Sell", params);
          await sleep(getRandomWait(MIN_SWITCH_MS, MAX_SWITCH_MS));
        } catch (error) {
          console.error("发生错误:", error);
        }
      }
      const orderTimeoutMap = /* @__PURE__ */ new Map();
      function checkOrderTimeout(orderList) {
        orderList.forEach((order) => {
          const timeoutTime = orderTimeoutMap.get(order.orderText);
          if (!timeoutTime)
            return;
          if (Date.now() > timeoutTime) {
            order.cancel();
            orderTimeoutMap.delete(order.orderText);
            cancelCount++;
            console.log(`%c订单【${order.orderText}】超时未成交,已取消!订单取消次数:${cancelCount}`, "color: #ffa;");
          }
        });
      }
      function getTabs() {
        const anchorElement = findElementsByText("My Assets", "div")[0];
        const tabsElement = anchorElement.parentElement.parentElement;
        const openOrderTab = tabsElement.children[0];
        return {
          openOrderTab,
          tabsElement
        };
      }
      const getOrderListElement = (tabsElement) => tabsElement.parentElement.nextElementSibling.querySelector("tbody");
      function getOrderList(tradingParams2) {
        const element = getOrderListElement(getTabs().tabsElement);
        const { timeout = 0 } = tradingParams2;
        return [...(element == null ? void 0 : element.children) ?? []].reduce((res, ele) => {
          const orderText = ele.textContent;
          if (orderText.includes("No open Orders"))
            return [];
          const order = {
            orderText,
            ele,
            cancel: () => findElementsByText("Cancel", "p", ele)[0].click(),
            data: ele.textContent.split("\n").filter((i) => i)
          };
          res.push(order);
          const timeoutTime = timeout ? orderTimeoutMap.get(orderText) || Date.now() + timeout * 1e3 : 0;
          orderTimeoutMap.set(orderText, timeoutTime);
          return res;
        }, []);
      }
      const running = vue.ref(false);
      const tradingParams = vue.ref({
        Buy: 2,
        Sell: 2,
        timeout: 0,
        mode: "Market"
      });
      const orderCount = vue.ref({
        buy: 0,
        sell: 0,
        cancel: 0
      });
      const currentOrder = vue.ref([]);
      function handleNumberInput(e, type, options = { min: 1, max: 20 }) {
        const { min, max } = options;
        let value = Number.parseInt(e.target.value);
        if (value > max)
          value = max;
        if (value < min || Number.isNaN(value))
          value = min;
        tradingParams.value[type] = Math.max(min, Math.min(max, value));
      }
      async function startTrading() {
        await performTradeCycle(tradingParams.value);
        orderCount.value.buy = buyCount;
        orderCount.value.sell = sellCount;
        await sleep(3e3);
        if (running.value)
          window.requestAnimationFrame(() => startTrading());
      }
      async function queryOrderListTask() {
        const { openOrderTab } = getTabs();
        openOrderTab.click();
        await sleep(300);
        const orderList = getOrderList(tradingParams.value);
        currentOrder.value = orderList;
        checkOrderTimeout(orderList);
        orderCount.value.cancel = cancelCount;
        await sleep(2e3);
        if (running.value)
          window.requestAnimationFrame(() => queryOrderListTask());
      }
      async function handleStart() {
        window.running = running.value = !running.value;
        console.log(running.value ? "start" : "stop");
        running.value && await startTrading();
        running.value && await queryOrderListTask();
        !running.value && getPriceElement("Buy", tradingParams.value.Buy).classList.remove("border");
        !running.value && getPriceElement("Sell", tradingParams.value.Sell).classList.remove("border");
      }
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [
          vue.createElementVNode("button", {
            class: vue.normalizeClass(["bg-greenText rounded p-2 h-12 self-center", { "bg-redText": running.value }]),
            style: { "grid-area": "a" },
            onClick: handleStart
          }, vue.toDisplayString(running.value ? "脚本运行中,点击关闭交易" : "启动脚本,点击开始交易"), 3),
          vue.createElementVNode("label", null, [
            _hoisted_2,
            vue.withDirectives(vue.createElementVNode("input", {
              "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => tradingParams.value.mode = $event),
              type: "radio",
              value: "Limit",
              disabled: running.value
            }, null, 8, _hoisted_3), [
              [vue.vModelRadio, tradingParams.value.mode]
            ])
          ]),
          vue.createElementVNode("label", null, [
            _hoisted_4,
            vue.withDirectives(vue.createElementVNode("input", {
              "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => tradingParams.value.mode = $event),
              type: "radio",
              value: "Market",
              disabled: running.value
            }, null, 8, _hoisted_5), [
              [vue.vModelRadio, tradingParams.value.mode]
            ])
          ]),
          vue.createElementVNode("span", {
            class: vue.normalizeClass({ "opacity-10": tradingParams.value.mode === "Market" })
          }, "第几个买入:", 2),
          vue.withDirectives(vue.createElementVNode("input", {
            "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => tradingParams.value.Buy = $event),
            class: vue.normalizeClass(["w-12 h-2 py-2 text-center bg-black text-greenText", { "bg-black/25": running.value, "opacity-10": tradingParams.value.mode === "Market" }]),
            type: "number",
            min: 1,
            max: 20,
            step: 1,
            disabled: running.value || tradingParams.value.mode === "Market",
            onInput: _cache[3] || (_cache[3] = (e) => handleNumberInput(e, "Buy"))
          }, null, 42, _hoisted_6), [
            [
              vue.vModelText,
              tradingParams.value.Buy,
              void 0,
              { number: true }
            ]
          ]),
          vue.createElementVNode("span", {
            class: vue.normalizeClass({ "opacity-10": tradingParams.value.mode === "Market" })
          }, "第几个卖出:", 2),
          vue.withDirectives(vue.createElementVNode("input", {
            "onUpdate:modelValue": _cache[4] || (_cache[4] = ($event) => tradingParams.value.Sell = $event),
            class: vue.normalizeClass(["w-12 h-2 py-2 text-center bg-black text-redText", { "bg-black/25": running.value, "opacity-10": tradingParams.value.mode === "Market" }]),
            type: "number",
            min: 1,
            max: 20,
            step: 1,
            disabled: running.value || tradingParams.value.mode === "Market",
            onInput: _cache[5] || (_cache[5] = (e) => handleNumberInput(e, "Sell"))
          }, null, 42, _hoisted_7), [
            [
              vue.vModelText,
              tradingParams.value.Sell,
              void 0,
              { number: true }
            ]
          ]),
          _hoisted_8,
          vue.withDirectives(vue.createElementVNode("input", {
            "onUpdate:modelValue": _cache[6] || (_cache[6] = ($event) => tradingParams.value.timeout = $event),
            class: vue.normalizeClass(["w-12 h-2 py-2 text-center bg-black text-accentBlue", { "bg-black/25": running.value }]),
            type: "number",
            min: 0,
            max: 600,
            step: 1,
            disabled: running.value,
            onInput: _cache[7] || (_cache[7] = (e) => handleNumberInput(e, "timeout", { min: 0, max: 600 }))
          }, null, 42, _hoisted_9), [
            [
              vue.vModelText,
              tradingParams.value.timeout,
              void 0,
              { number: true }
            ]
          ]),
          _hoisted_10,
          vue.createElementVNode("div", null, "当前订单数:" + vue.toDisplayString(currentOrder.value.length), 1),
          vue.createElementVNode("div", null, [
            vue.createTextVNode("买入次数:"),
            vue.createElementVNode("span", _hoisted_11, vue.toDisplayString(orderCount.value.buy), 1)
          ]),
          vue.createElementVNode("div", null, [
            vue.createTextVNode("卖出次数:"),
            vue.createElementVNode("span", _hoisted_12, vue.toDisplayString(orderCount.value.sell), 1)
          ]),
          vue.createElementVNode("div", null, [
            vue.createTextVNode("取消次数:"),
            vue.createElementVNode("span", _hoisted_13, vue.toDisplayString(orderCount.value.cancel), 1)
          ])
        ]);
      };
    }
  };
  const _sfc_main = {
    __name: "App",
    setup(__props) {
      return (_ctx, _cache) => {
        return vue.openBlock(), vue.createBlock(_sfc_main$1);
      };
    }
  };
  const containerID = "tampermonkey_vue_app".toUpperCase();
  vue.createApp(_sfc_main).mount((() => {
    const app = document.createElement("div");
    app.id = containerID;
    app.style.cssText = "position: fixed;top: 10px;left:50%;z-index:1000;transform: translateX(-50%);";
    document.body.append(app);
    return app;
  })());

})(Vue);