noteのタイトル画像

[Puppeteer] bitmex資産状況の推移をグラフにしてdiscord通知する[改01]

改01内容:
 プログラムでpngファイルを作成しているが、pltオブジェクトをcloseしてなかったのを修正。画像ファイルで日時の部分が潰れていたのを修正

資産通知Puppet

Puppeteer(傀儡師)の資産状況通知Puppet(傀儡)の紹介です。

bitmexでトレードしていて現在の資産状況をdiscordなどに通知している方は多いと思います。
自分も今までは資産状況を「数値」でdiscordに通知していましたが、数値だけだと増えているのか減っているのか、時間的にどのように推移したのかなどが直感的に分かりづらいと感じていました。

pythonにはmatplotlibという描画ライブラリがあるのでグラフを作成するのは結構簡単にできるのでは?と考えて、さくっと資産状況通知Puppetを作ってみました。
まだまだ改善の余地は有り余るほど有りますが、動作するので公開です。

本Puppetを実行するにはPuppeteerが必要ですので、こちらからDLして設定してください。

プログラムファイル

以下の2つのファイル「balance.py」「balance.json」をPuppeteerのPuppetsフォルダの下に作ります。

- puppets/
  - balance/
    - balance.py
    - balance.json

・balance.py

# -*- coding: utf-8 -*-
# ==========================================
# サンプル・ストラテジ
# ==========================================
import time
import requests
from datetime import datetime
# ログのライブラリ
import logging
from logging import getLogger, StreamHandler, Formatter
from logging.handlers import TimedRotatingFileHandler, RotatingFileHandler
# csv reader として利用
import pandas as pd
# グラフ保存用
import matplotlib as mpl
import matplotlib as plt
import matplotlib.dates as mdates
plt.use('Agg')

from puppeteer import Puppeteer

# ==========================================
# Puppet(傀儡) クラス
#   param:
#       puppeteer: Puppeteerオブジェクト
# ==========================================
class Puppet(Puppeteer):
    # ==========================================================
    # 初期化
    #   param:
    #       puppeteer: Puppeteerオブジェクト
    # ==========================================================
    def __init__(self, Puppeteer):
        self._exchange = Puppeteer._exchange    # 取引所オブジェクト(ccxt.bitmex)
        self._logger = Puppeteer._logger        # logger
        self._config = Puppeteer._config        # 定義ファイル
        self._discord = Puppeteer._discord      # discord

        self._log_name = self._config['LOG_NAME']   # ログ名称

        # balance格納用ログ
        # loggerオブジェクトの宣言
        logger = getLogger("walletBalanceLogger")
        # loggerのログレベル設定(ハンドラに渡すエラーメッセージのレベル)
        logger.setLevel(eval('logging.' + self._config['LOG_LEVEL']))
        # Formatterの生成
        formatter = Formatter(
                fmt='%(message)s'
            )
        # file handlerの生成・追加
        rotating_handler = RotatingFileHandler(
                filename='logs/' + self._log_name + '.log',      # logファイル名
                maxBytes=100*1000*1000,                 # 100MBを指定
                backupCount=7,                          # 7個保持
                encoding='UTF-8'                        # UTF-8
            )
        rotating_handler.setFormatter(formatter)
        logger.addHandler(rotating_handler)
        self._balanceLogger = logger                    # balanceデータ格納ロガー

        # すでにあるwalletBalanceファイルを読み込む
        self._walletBalance = 0
        try:
            # walletBalanceファイルを読み込み
            df = pd.read_csv(
                    filepath_or_buffer='logs/' + self._log_name + '.log',    # logファイル名
                    names=('datetime', 'balance', 'diff'),          # カラム名
                    encoding='UTF-8',                               # エンコーディング
                    sep=','                                         # セパレータ(csv形式なので、「,」)
                )
            # 読み込みに成功したら、最終行のbalanceデータを取り出し、前回値に設定する
            self._walletBalance = df['balance'].values[-1]
        except:
            # ファイルが存在しない
            pass

        # for DEBUG
        # print(self._df.tail())

    # ==========================================================
    # 売買実行
    #   param:
    #       ticker: Tick情報
    #       orderbook: 板情報
    #       position: ポジション情報
    #       balance: 資産情報
    #       candle: ローソク足
    # ==========================================================
    def run(self, ticker, orderbook, position, balance, candle):

        # ------------------------------------------------------
        # ポジションサイズ、参入価格
        # ------------------------------------------------------
        pos_qty = position[0]['currentQty'] if position[0]['currentQty'] is not None else 0
        avg_price = position[0]['avgEntryPrice'] if position[0]['avgEntryPrice'] is not None else 0

        # ------------------------------------------------------
        # 今回walletBalance
        # ------------------------------------------------------
        walletBalance = balance['info'][0]['walletBalance'] * 0.00000001
        
        # ------------------------------------------------------
        # 変化有り
        # ------------------------------------------------------
        if self._walletBalance != walletBalance :
            # --------------------------------------------------
            # 前回との差分
            # --------------------------------------------------
            diff = walletBalance - (self._walletBalance if self._walletBalance != 0 else walletBalance)
            # --------------------------------------------------
            # 次回用に保存
            # --------------------------------------------------
            self._walletBalance = walletBalance
        
            # --------------------------------------------------
            #         time, balance, diff
            # --------------------------------------------------
            message = '{}, {}, {}'.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), round(self._walletBalance,8), round(diff,8))
            # --------------------------------------------------
            # logger通知
            # --------------------------------------------------
            self._logger.info(message)
            # --------------------------------------------------
            # walletBalanceファイルに情報を保存
            # --------------------------------------------------
            self._balanceLogger.info(message)
            time.sleep(1)

            # --------------------------------------------------
            # walletBalanceファイルを読み込み
            # --------------------------------------------------
            df = pd.read_csv(
                    filepath_or_buffer='logs/' + self._log_name + '.log',   # logファイル名
                    header=None,                                            # ヘッダー無し
                    names=('datetime', 'balance', 'diff'),                  # カラム名
                    encoding='UTF-8',                                       # エンコーディング
                    sep=',',                                                # セパレータ(csv形式なので、「,」)
                    index_col='datetime',                                   # 先頭列をインデックスに
                    parse_dates=True                                        # indexをDatetime型に
                )
            # --------------------------------------------------
            # balanceグラフを出力
            # --------------------------------------------------
            #print(df)
            df.plot(y='balance')
            plt.pyplot.gca().xaxis.set_major_formatter(mdates.DateFormatter("%m/%d %H:%M"))
            plt.pyplot.xticks(rotation=45)
            plt.pyplot.savefig('logs/' + self._log_name + '.png')
            # plt.pyplot.show()
            plt.pyplot.close()
            time.sleep(1)

            # --------------------------------------------------
            # discord通知
            #         time, balance, diff, pos_qty, avg_price
            # --------------------------------------------------
            message = '{}: balane={}, diff={}, pos={}, avg={}'.format(datetime.now().strftime("%Y-%m-%d %H:%M:%S"), round(self._walletBalance,8), round(diff,8), pos_qty, round(avg_price,2))
            # --------------------------------------------------
            self._discord.send(message=message, fileName='logs/' + self._log_name + '.png')

・balance.json

{
    "//" : "===============================================",
    "//" : " システムで利用",
    "//" : "===============================================",
    "//" : "取引所のapiKey, secretを設定します",
    "APIKEY" : "YOUR_APIKEY",
    "SECRET" : "YOUR_SECRET",

    "//" : "bitmex取引所で対応する通貨ペア等を記述",
    "SYMBOL" : "BTC/USD",
    "INFO_SYMBOL" : "XBTUSD",
    "COIN_BASE" : "BTC",
    "COIN_QUOTE" : "USD",
    "//" : "bitmex取引所の価格の最小幅(0.5ドル)",
    "PRICE_UNIT" : 0.5,

    "//" : "TestNetを使うか?(使う: true, 使わない: false)",
    "USE_TESTNET" : true,

    "//" : "ticker, orderbook, position, balance, candle のどれを利用するかを指定する。Falseを指定した場合はそのデータは取得しない",
    "USE" : {
        "TICKER" : false,
        "ORDERBOOK" : false,
        "POSITION" : true,
        "BALANCE" : true,
        "CANDLE" : false
    },

    "//" : "ローソク足の収集定義。",
    "CANDLE" : {
        "//" : "ローソク足の足幅を設定する。設定値= 1m, 5m, 1h, 1d",
        "TIMEFRAME" : "1m",
        "//" : "データ取得開始時刻(UNIXTIME:1ミリ秒)、使用しない場合 もしくは自動の場合は null(None) を指定",
        "SINCE" : null,
        "//" : "取得件数(未指定:100、MAX:500)",
        "LIMIT" : null,
        "//" : "True(New->Old)、False(Old->New) 未指定時はFlase",
        "REVERSE" : false,
        "//" : "True(最新の未確定足を含む)、False(含まない) 未指定はTrue",
        "PARTIAL" : false
    },

    "//" : "板情報の収集定義。",
    "ORDERBOOK" : {
        "//" : "取得件数(未指定:25、MAX:取引所による?)",
        "LIMIT" : null
    },

    "//" : "websocketを使用するかどうかを指定",
    "USE_WEBSOCKET" : false,

    "//" : "ログレベルを指定。('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG')",
    "LOG_LEVEL" : "INFO",

    "//" : "インターバルを秒で設定",
    "INTERVAL" :60,

    "//" : "discord通知用URL",
    "DISCORD_WEBHOOK_URL" : "",

    "//" : "===============================================",
    "//" : " ユーザで自由に定義",
    "//" : "===============================================",
    "//" : "売買するサイズ",
    "LOG_NAME" : "balance-01"
}

2つのファイルを保存し終わったら、資産状況を取得したいbitmex取引所のapikey,secretをjsonファイルの所定の場所に記述して、通知先のdiscordのwebhookURLをjsonファイルに指定します。
(上記のJSONファイルはTestNetを指定していますので、本家bitmexサイトを指定する場合は、USE_TESTNETをfalseと変更してください)

コンソールから

python puppeteer.py puppets/balance/balance.py puppets/balance/balance.json

と実行すると、

・現在時刻、現在資産量(btc)、前回との差分

とグラフも一緒にdiscordに通知されます。(今のところ検出周期は60秒にしています)

グラフ通知結果

時間が進んで資産が変化すると上記のような連続したグラフとなって閲覧できるはずです。

最初、balance(資産)データに「BTC total」を使っていましたが、取引が発生していないのにパラパラと値が変化していて、何か違うと思いました。

上記の「ウォレット残高」を取得する方法が分からなかったのですが、balanceデータの中にそのものの値「walletBalance」というデータがあったので、そのデータを出力するようにしました。

現状、Puppeteerとは別のPuppetとして実装していますが、今後はPuppeteer本体機能として実装していきたいと思っています。

楽しいbotライフを!


ソフトウェア・エンジニアを40年以上やってます。 「Botを作りたいけど敷居が高い」と思われている方にも「わかる」「できる」を感じてもらえるように頑張ります。 よろしくお願い致します。