FTXのWebアプリMod

FTXのWebアプリの独自のトレード機能を追加したりします。

// ==UserScript==
// @name         FTXのWebアプリMod
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  FTXのWebアプリの独自のトレード機能を追加したりします。
// @author       touta44g
// @match        https://ftx.com/trade/*
// @grant        none
// @license      LGPLv3
// @run-at document-start
// @require      https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js
// @require      https://unpkg.com/react@17/umd/react.production.min.js
// @require      https://unpkg.com/react-dom@17/umd/react-dom.production.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/big.min.js
// ==/UserScript==


let isGiriMaker = false;
let isCenterMaker = false;
let currentMarket = null;
let markets = null;

let isScatterMaker = false;
let gPositionBetween = null;
let gPositionCount = null;

let gPositionAmount = null;
let isPositionCount = false;
let isPositionAmount = false;


// 任意の単位値で値を丸める関数
function RoundTo(number, roundto){
    return roundto * Math.round(number/roundto);
}

// 定期的にマーケットデータを取得。
// market価格の区切り単位を取得する為に利用。
const marketsUrl = "https://ftx.com/api/markets"
// 初回
fetch(marketsUrl).then(n => n.json()).then(jsonData => {markets = jsonData.result})
// 定期的に取得して更新
setInterval(
    () => fetch(marketsUrl).then(n => n.json()).then(jsonData => {markets = jsonData.result})
,30 * 1000)

class OrderBook {
    constructor() {
        // ローカル用の板データ保持にSetリストを利用。自動で一意な値に集約してくれて、実装が減る為。
        this.orderbook = new Set();
    }

    updateOrderBook(dataAsksBids) {
        // 空配列はガード
        if (dataAsksBids.length == 0) {
            return;
        }

        // 板データ変更処理
        dataAsksBids.forEach((n) => {
            if (n[1] == 0) {
                this.orderbook.delete(n[0])
                return
            }
            this.orderbook.add(n[0])
        })
    }
}

// 買い板
class BidOrderBook extends OrderBook {
  calcGiriMaker() {
      const higherPrice = _.max([...this.orderbook.values()])
      console.log({higherPrice})
      return higherPrice + markets.find(n => n.name == currentMarket).priceIncrement
  }
}

// 売り板
class AskOrderBook extends OrderBook {
  calcGiriMaker() {
      const lowerPrice = _.min([...this.orderbook.values()])
      console.log({lowerPrice})
      return lowerPrice - markets.find(n => n.name == currentMarket).priceIncrement
  }
}

// class構文だと、calcCenterMaker内のthisの参照先がOrderBooksインスタンスではなく別になってしまい、this.sellがundefinedとなって参照エラーになる。
// コンストラクタ内でbind関数使ってthis束縛も、効果無かった。
// オブジェクトリテラルに変更。
// → this.sellをthis.sell.orderbookに参照変更したら直ったので勘違い。
class OrderBooks {
    constructor() {
     this.buy = new AskOrderBook()
     this.sell = new BidOrderBook()
    }
     calcCenterMaker(side) {
         const higherPrice = _.max([...this.sell.orderbook.values()])
         const lowerPrice = _.min([...this.buy.orderbook.values()])
         // .25や.75は注文が受付られないので、単位値で丸める。
         const avarage = (higherPrice + lowerPrice) / 2
         const { priceIncrement } = markets.find(n => n.name == currentMarket)
         let targetPrice
         if (side == "buy") {
           // スプレットの間隔が0と最狭になっている場合でbuyをすると、ask板の最小値と同額になり、POST注文が失敗する。
           // その為、丸めた値を最小単位で引く。
           targetPrice = RoundTo(avarage, priceIncrement) - priceIncrement
         }
         else {
           targetPrice = RoundTo(avarage, priceIncrement)
         }
         console.log({targetPrice})
         return targetPrice
     }
}

// WebSocketクラスのモンキーパッチ
const originWebSocket = window.WebSocket
window.WebSocket = class extends originWebSocket {
    constructor(...args){
        super(...args)
        console.log("WebSocketモンキーパッチ constructor実行")

        this.orderBooks = new OrderBooks()

        const handleMessage = (e) => {
            // # WSのorderBookメッセージの仕様
            // ## dataオブジェクト配下の構造
            // channel: "orderbook"
            // data:
            // action: "update"
            // asks: Array(3)
            //       0: (2) [19005, 1.343]
            //       1: (2) [19043, 0.002]
            //       2: (2) [18969, 0]
            // bids: Array(4)
            //       0: (2) [18945.5, 11.6304]
            //       1: (2) [18950.5, 0.7109]
            //       2: (2) [18951.5, 4.8141]
            //       3: (2) [18955.5, 0.6507]
            // market: "BTC-0326"
            // type: "update"
            // ## 挙動
            // orderのインデックス1が0(bids: [... ,[18828.5, 0] ,...]) 削除だと思われる。順不同。

            const data = JSON.parse(e.data);
            // console.log("Websocket messageイベント e.data", data)

            // 他のチャネルだと、データ構造相違で後続処理でエラー出るので、ガード
            if (data.channel != "orderbook") {
                return;
            };

            // 単純な実装なので、初回に2回初期化される。
            if (currentMarket != data.market) {
              console.log("ローカルorderBook 初期化")
              this.orderBooks = new OrderBooks()
              currentMarket = data.market
            }

            this.orderBooks["buy"].updateOrderBook(data.data.asks)
            this.orderBooks["sell"].updateOrderBook(data.data.bids)
        }

        this.addEventListener("message",handleMessage)
    }

    send(data) {
      console.log("WebSocket sendメソッド モンキーパッチ",
                   data,
                  {"is string": typeof data == "string",
                   "is ArrayBuffer": data instanceof ArrayBuffer,
                   "is Blob": data instanceof Blob})

      if (!isGiriMaker && !isCenterMaker && !isScatterMaker) {
         super.send(data)
         return
      }

      const json = JSON.parse(data)
      if (json.path != "orders"
          // 注文キャンセルでも通ってしまうので、ガード
         || json.method == "DELETE") {
         super.send(data)
         return
      }

      const bodyJson = JSON.parse(json.body)

      let price;
      if (isGiriMaker) {
          // 最大値を求める処理は、ボタン押した時に算出。
          price = this.orderBooks[bodyJson.side].calcGiriMaker()
      }
      else if (isCenterMaker) {
          price = this.orderBooks.calcCenterMaker(bodyJson.side)
      }
      else if (isScatterMaker) {
          if (isPositionCount) {
              _.times(gPositionCount).reduce((acc, cnt) => {
                  // "Do not send more than 2 orders total per 200ms"とエラーが出た。
                  // なので、直列式の非同期処理にして、スリープ制御を入れる。
                  return acc.then(async () => {
                      // 価格はばら撒きスタート位置に利用。
                      const { price, size, side } = bodyJson
                      const currentPrice = {
                          buy: price - (gPositionBetween * cnt),
                          sell: price + (gPositionBetween * cnt),
                      }[side]
                      const changedBodyJson = {...bodyJson, price: currentPrice, size: size / gPositionCount }
                      const changedJsonString = JSON.stringify({...json, body: JSON.stringify(changedBodyJson)})
                      super.send(changedJsonString)
                      await new Promise((reso) => setTimeout(reso, 200))
                  })
              }, Promise.resolve());
          }
          else if (isPositionAmount) {
              const { price, size, side } = bodyJson
              // 残り可能注文量
              let remainingSize = size
              // 注文回数
              let cnt = 0
              const id = setInterval(() => {
                  const currentPrice = {
                      buy: price - (gPositionBetween * cnt),
                      sell: price + (gPositionBetween * cnt),
                  }[side]
                  const currentSize = gPositionAmount/currentPrice

                  if (remainingSize - currentSize <= 0) {
                      // 量が不足したら、ループ終了
                      clearInterval(id)
                      return
                  }

                  remainingSize = remainingSize - currentSize

                  const changedBodyJson = {...bodyJson, price: currentPrice, size: currentSize }
                  const changedJsonString = JSON.stringify({...json, body: JSON.stringify(changedBodyJson)})
                  super.send(changedJsonString)

                  cnt=cnt+1
              }, 200)
          }
          // 注文済みなので処理終了。
          return
      }

      const changedBodyJson = {...bodyJson, price: price }

      const changedJsonString = JSON.stringify({...json, body: JSON.stringify(changedBodyJson)})

       super.send(changedJsonString)
    }
};


(function() {
    'use strict';

    window.addEventListener("load", () => {
       console.log("loadイベント 発火");

       const e = React.createElement;

       const LikeButton = (props) => {

            const handleClick = (e) => {
                console.log("ボタン押下 handleClickハンドラ実行")
                const {value} = e.target
                if (value == "0") {
                    isGiriMaker = false;
                    isCenterMaker = false;
                    isScatterMaker = false;
                }
                else if (value == "1") {
                    isGiriMaker = true;
                    isCenterMaker = false;
                    isScatterMaker = false;
                }
                else if (value == "2") {
                    isGiriMaker = false;
                    isCenterMaker = true;
                    isScatterMaker = false;
                }
                else if (value == "3") {
                    isGiriMaker = false;
                    isCenterMaker = false;
                    isScatterMaker = true;
                }
            }

            const handleClickRadioPoint = (e) => {
                const {value} = e.target
                if (value == "0") {
                    isPositionCount = true;
                    isPositionAmount = false;
                }
                if (value == "1") {
                    isPositionCount = false;
                    isPositionAmount = true;
                }
            }

            const betweenEl = React.useRef(null)
            const [positionBetween, setPositionBetween] = React.useState(0)
            const handleChangeBetween = (e) => {
                const { value } = e.target
                setPositionBetween(value)
            }
            React.useEffect(() => {
                gPositionBetween = Number(positionBetween)
            },[positionBetween])

            const countEl = React.useRef(null)
            const [positionCount, setPositionCount] = React.useState(0)
            const handleChangeCount = (e) => {
                const { value } = e.target
                setPositionCount(value)
            }
            React.useEffect(() => {
                gPositionCount = Number(positionCount)
            },[positionCount])


            const amountEl = React.useRef(null)
            const [positionAmount, setPositionAmount] = React.useState(0)
            const handleChangeAmount = (e) => {
                const { value } = e.target
                setPositionAmount(value)
            }
            React.useEffect(() => {
                gPositionAmount = Number(positionAmount)
            },[positionAmount])

            const handleClickInput = (el) => (e) => {
                // クリックしても、入力エリアが選択されないので、クリック時にフォーカスするようにした。
                el.current.focus()
            }

            return [
               e("input",
                 {onClick: handleClick, type: "radio", name: "priceAction", value: "0"},
                 null),
               e("span",
                 null,
                 "無し"),
               e("br", null, null),
               e("input",
                 {onClick: handleClick, type: "radio", name: "priceAction", value: "1"},
                 null),
               e("span",
                 null,
                 "Ask最安値-0.5の価格にBUY指値(SELLは逆)"),
               e("br", null, null),
               e("input",
                 {onClick: handleClick, type: "radio", name: "priceAction", value: "2"},
                 null),
               e("span",
                 null,
                 "Ask最安値とBid最高値の中央値にBUY指値(SELLも同様)"),
               e("br", null, null),
               e("input",
                 {onClick: handleClick, type: "radio", name: "priceAction", value: "3"},
                 null),
               e("span",
                 null,
                 "BUY指値を下方向にばら撒き(SELLは上方向)"),
               e("br", null, null),
               e("span",
                 null,
                 "指値配置する間隔"),
               e("input",
                 {onChange: handleChangeBetween, onClick: handleClickInput(betweenEl), ref: betweenEl, type: "tel", value: positionBetween},
                 null),
               e("br", null, null),
               e("input",
                 {onClick: handleClickRadioPoint, type: "radio", name: "pointAction", value: "0"},
                 null),
               e("span",
                 null,
                 "指値配置数"),
               e("input",
                 {onChange: handleChangeCount, onClick: handleClickInput(countEl), ref: countEl, type: "number", value: positionCount},
                 null),
               e("br", null, null),
               e("input",
                 {onClick: handleClickRadioPoint, type: "radio", name: "pointAction", value: "1"},
                 null),
               e("span",
                 null,
                 "1指値の注文量(USD)"),
               e("input",
                 {onChange: handleChangeAmount, onClick: handleClickInput(amountEl), ref: amountEl, type: "tel", value: positionAmount},
                 null),
             ]
        };


        const addonElm = document.createElement("div");
        addonElm.setAttribute("style", `
position: absolute;
cursor: move;
background: rgba(0, 0, 0, 0.8);
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 100000;
font-size: 13px;
text-align: left;
box-sizing: border-box;
contain: size style;
width: 400px;
height: 150px;`);
        addonElm.setAttribute("id", "draggable-div");

        const backOffFn = () => {
            const elm = document.querySelector("#root")
            if (elm == null) {
                // ロードが終わっていなかった場合は、時間を置いて再度関数を実行。
                setTimeout(backOffFn, 5 * 1000)
                return;
            }
            elm.appendChild(addonElm);

            ReactDOM.render(e(LikeButton), addonElm);

        };
        backOffFn();

        dragElement(addonElm);

        // ドラッグ可能コンポーネント実装
        // 参考:https://www.w3schools.com/howto/howto_js_draggable.asp
        function dragElement(elmnt) {
            var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
            if (document.getElementById(elmnt.id + "header")) {
                // if present, the header is where you move the DIV from:
                document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
            } else {
                // otherwise, move the DIV from anywhere inside the DIV:
                elmnt.onmousedown = dragMouseDown;
            }

            function dragMouseDown(e) {
                e = e || window.event;
                e.preventDefault();
                // get the mouse cursor position at startup:
                pos3 = e.clientX;
                pos4 = e.clientY;
                document.onmouseup = closeDragElement;
                // call a function whenever the cursor moves:
                document.onmousemove = elementDrag;
            }

            function elementDrag(e) {
                e = e || window.event;
                e.preventDefault();
                // calculate the new cursor position:
                pos1 = pos3 - e.clientX;
                pos2 = pos4 - e.clientY;
                pos3 = e.clientX;
                pos4 = e.clientY;
                // set the element's new position:
                elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
                elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
            }

            function closeDragElement() {
                // stop moving when mouse button is released:
                document.onmouseup = null;
                document.onmousemove = null;
            }
        }


    })

})();
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

一年攒够 12 元

云驰互联

云驰互联