【Python】bitflyer の API を使って約定一覧を取得してみる

2018年3月16日Python,開発

おはようございます。

注文をする予定だったのですが、
その前に約定や建玉、注文の一覧が必要だろうということで、
今回は約定一覧を取得して表示してみます。

プログラムは前回のものを流用します。

【Python】bitflyer の Private API を使って資産情報を取得してみる

スポンサーリンク

プロジェクトのリファクタリング

少しごちゃってきたので整理します。

BfTool
│ BfApi.py
│ BfTool.py(Sampl.py)
├─common
│ Constants.py
├─static
│ ├─css
│ │ style.css
│ └─js
│ script.js
└─templates
Main.html(index.html)

画面の修正

Main.html

<!DOCTYPE html>
<html>
   <head>
      <title>{{ title }}</title>
      <link rel="stylesheet" href="{{ static_url('css/style.css') }}"/>
      <script type="text/javascript" src="{{ static_url('js/script.js') }}"></script>
      <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
   </head>
   <body>
      <div id="container">
         <div style="clear:both; padding-top:10px;">
            <div class="entry_title">
               <div class="pull_left">資産情報</div>
               <div class="pull_right"><input type="button" value="更新" /></div>
            </div>
            <table id="balanceTable">
               <tr><th>円</th><td id="jpy"></td><th>イーサクラシック</th><td id="etc"></td></tr>
               <tr><th>ビットコイン</th><td id="btc"></td><th>ライトコイン</th><td id="ltc"></td></tr>
               <tr><th>ビットコインキャッシュ</th><td id="bch"></td><th>モナコイン</th><td id="mona"></td></tr>
               <tr><th>イーサ</th><td id="eth"></td><th>リスク</th><td id="lsk"></td></tr>
            </table>
         </div>
         <div style="clear:both; padding-top:10px;">
            <div class="entry_title">ティッカー情報</div>
            <table id="tickerTable">
               <tr class="header">
                  <th style="width:5%">種別</th>
                  <th style="width:10%">時刻</th>
                  <th style="width:5%">ID</th>
                  <th style="width:5%">売値</th>
                  <th style="width:5%">買値</th>
                  <th style="width:10%">売り数量</th>
                  <th style="width:10%">買い数量</th>
                  <th style="width:10%">売り注文総数</th>
                  <th style="width:10%">買い注文総数</th>
                  <th style="width:10%">最終取引価格</th>
                  <th style="width:10%">出来高</th>
                  <th style="width:10%">価格単位出来高</th>
               </tr>
            </table>
         </div>
         <div style="clear:both; padding-top:10px;">
            <div class="entry_title">
               <div class="pull_left">約定一覧</div>
               <div class="pull_right"><input type="button" value="更新" /></div>
            </div>
            <div class="table_container">
               <table id="executionTable">
                  <tr class="header">
                     <th style="width:10%">ID</th>
                     <th style="width:10%">売買</th>
                     <th style="width:10%">値段</th>
                     <th style="width:10%">数量</th>
                     <th style="width:10%">約定日時</th>
                     <th style="width:20%">注文ID</th>
                     <th style="width:10%">委任</th>
                     <th style="width:20%">受付ID</th>
                  </tr>
               </table>
            </div>
         </div>
      </div>
   </body>
</html>

style.css

body {
   font-family:"MS Pゴシック","MS PGothic",sans-serif;
   width: 100%;
   margin: 0 auto;
}
div {
   margin:20px;
}
div#container{
   width: 100%;
}
table {
   width:95%;
   border: 1px solid #ccc;
   border-collapse:collapse;
}
th {
   text-align:center;
   background-color:#404040;
   color:#ffffff;
   width: 100px;
   height: 25px;
   border: 1px solid #ccc;
}
td {
   padding-left:5px;
   width: 200px;
   height: 20px;
   border: 1px solid #ccc;
}
#balanceTable th {
   padding-left:5px;
    text-align: left;
}

div.table_container {
    margin: 0px;
    width:95%;
    height: 280px;
    overflow-y: scroll;
}
#executionTable {
    width: 100%;
}

div.entry_title {
    font-size: 18px;
    width: 93%;
    height: 25px;
    padding: 10px;
    margin-left: 0px;
    border-bottom: 1px solid #316d9c;
    border-left: 15px solid #316d9c;
}

div.pull_left {
    float:left;
    margin: 0px;
}
div.pull_right {
    float:right;
    margin: 0px;
}

プログラムの修正

定数クラスの作成

Constants.py

Pythonでは、名前付きタプル(namedtuple)というモジュールを使うことによって擬似的にクラスのような使い方ができるようです。

# -*- coding: utf-8 -*-
"""
Created on 2018/03/14
@author: doraxdora
"""
from collections import namedtuple

# 名前付きタプルの定義
HTTP_TUPLE = namedtuple("HTTP", ("GET", "POST"))


class Constants:

    HTTP = HTTP_TUPLE(GET="GET", POST="POST")

APIクラスの作成

Bitflyer の API を利用する処理を別クラスに抜き出して整理しました。

bitApi.py

# -*- coding: utf-8 -*-
"""
Created on 2018/03/14
@author: doraxdora
"""

import hashlib
import hmac
import json
import logging
import requests
import time
import urllib

from common.Constants import Constants
from pubnub.pnconfiguration import PNConfiguration
from pubnub.pubnub import PubNub, SubscribeListener


class BfApi:
    """
    Bitflyer API を利用するためのツールクラス
    """

    def __init__(self, access_key=None, secret_key=None):
        self.access_key = access_key
        self.secret_key = secret_key
        self.api_url = "https://api.bitflyer.jp"
        self.pb_config = PNConfiguration()
        self.pb_config.subscribe_key = "sub-c-52a9ab50-291b-11e5-baaa-0619f8945a4f"
        self.pb_config.ssl = False

    def call_pub_nub(self, channels):
        """
        pubnub を利用して指定したチャネルからデータを取得

        :param channels: 接続チャネル
        :return: リアルタイム配信データ
        """

        # pubnubの生成
        pub_nub = PubNub(self.pb_config)
        listener = SubscribeListener()
        pub_nub.add_listener(listener)

        # チャンネルへ接続要求し接続を待機する
        pub_nub.subscribe().channels(channels).execute()
        listener.wait_for_connect()

        # リアルタイム配信からデータを取得
        result = listener.wait_for_message_on(channels)
        data = result.message

        # チャンネルの接続解除を要求し切断を待機する
        pub_nub.unsubscribe().channels(channels).execute()
        listener.wait_for_disconnect()

        return data

    def send_req(self, api_path, http_method="GET", timeout=None, **send_params):
        """
        Bitflyer Private API を利用してリクエストを送信し
        取得したデータを JSON 形式で返却します

        :param api_path: 呼び出す Private API のパス
        :param http_method: GET/POST
        :param timeout: 接続タイムアウト時間
        :param send_params: APIに送信するパラメータ
        :return: 取得したデータのJSON
        """

        url = self.api_url + api_path
        body = ""
        auth_header = None

        if http_method == Constants.HTTP.POST:
            body = json.dumps(send_params)
        else:
            if send_params:
                body = "?" + urllib.parse.urlencode(send_params)

        if self.access_key and self.secret_key:
            access_time = str(time.time())
            encode_secret_key = str.encode(self.secret_key)
            encode_text = str.encode(access_time + http_method + api_path + body)
            access_sign = hmac.new(encode_secret_key, encode_text, hashlib.sha256).hexdigest()

            auth_header = {
                'ACCESS-KEY': self.access_key,
                'ACCESS-TIMESTAMP': access_time,
                'ACCESS-SIGN': access_sign,
                'Content-Type': 'application/json'
            }

        try:
            with requests.Session() as s:
                if auth_header:
                    s.headers.update(auth_header)

                if http_method == Constants.HTTP.GET:
                    response = s.get(url, params=send_params, timeout=timeout)
                else:
                    response = s.post(url, data=json.dumps(send_params), timeout=timeout)
        except requests.RequestException as e:
            logging.error(e)
            raise e

        content = ""
        if len(response.content) > 0:
            content = json.loads(response.content.decode("utf-8"))

        return content

リクエスト処理クラス

メインとなる、画面からのリクエストを受付け、処理をハンドリングするクラス

BfTool.py

# -*- coding: utf-8 -*-
"""
Created on 2018/03/14
@author: doraxdora
"""

import json
import logging
import os
import time
import tornado.ioloop

from tornado.web import RequestHandler
from tornado.websocket import WebSocketHandler
from tornado.options import options
from BfApi import BfApi


class MainHandler(RequestHandler):
    """
    メイン処理
    初期画面を表示します.
    """

    def initialize(self):
        logging.info("MainHandler [initialize]")

    def get(self):
        logging.info("MainHandler [get]")

        self.render("Main.html", title="BfTool")


class SendWebSocket(WebSocketHandler):
    """
    WEBソケット通信
    1秒毎にティッカー情報を画面に配信します
    """

    def open(self):
        logging.info('SendWebSocket [open] IP :' + self.request.remote_ip)
        self.ioloop = tornado.ioloop.IOLoop.instance()
        self.send_ticker()

    def on_close(self):
        logging.info("SendWebSocket [on_closed]")

    def check_origin(self, origin):
        logging.info("SendWebSocket [check_origin]")

        return True

    def send_ticker(self):

        self.ioloop.add_timeout(time.time() + 1, self.send_ticker)
        if self.ws_connection:
            api = BfApi(access_key="APIキー", secret_key="Private API キー")
            data = api.call_pub_nub('lightning_ticker_FX_BTC_JPY')
            message = json.dumps(data)
            self.write_message(message)


class GetBalanceHandler(RequestHandler):
    """
    資産情報を取得
    """

    def initialize(self):
        logging.info("GetBalanceHandler [initialize]")

    def post(self):
        logging.info("GetBalanceHandler [post]")

        api = BfApi(access_key="APIキー", secret_key="Private API キー")
        data = api.send_req(api_path="/v1/me/getbalance")
        self.write(json.dumps(data, ensure_ascii=False))


class GetExecutionHandler(RequestHandler):
    """
    約定一覧を取得
    """

    def initialize(self):
        logging.info("GetExecutionHandler [initialize]")

    def post(self):
        logging.info("GetExecutionHandler [post]")

        api = BfApi()
        data = api.send_req(api_path="/v1/me/getexecutions", product_code="FX_BTC_JPY")
        self.write(json.dumps(data, ensure_ascii=False))

app = tornado.web.Application([
    (r"/", MainHandler),
    (r"/ticker", SendWebSocket),
    (r"/balance", GetBalanceHandler),
    (r"/execution", GetExecutionHandler)
    ],
    template_path=os.path.join(os.getcwd(), "templates"),
    static_path=os.path.join(os.getcwd(), "static"),
    js_path=os.path.join(os.getcwd(), "js"),
)

if __name__ == "__main__":
    options.parse_command_line()
    app.listen(8888)
    logging.info("server started")
    tornado.ioloop.IOLoop.instance().start()

画面処理スクリプト

script.js

/**
 * 画面読み込み時の処理
 */
function initialize() {

    // 各テーブルに空行を追加しておく
    addEmptyRow("tickerTable", 10, 12);
    addEmptyRow("executionTable", 10, 8);

    // 初期表示時に一度データを取得
    updateBalance();
    updateExecution();

    var connection = new WebSocket('ws://127.0.0.1:8888/ticker');
    connection.onmessage = function (e) {
        var data = JSON.parse(e.data.replace( /\\/g , "" ));

        var table = $("#tickerTable");

        // 日付け変換
        var date = new Date(data.timestamp);
        data.timestamp = date.toLocaleString();

        // テーブルに追加
        var tr = document.createElement("tr");
        $.each(data, function(i, cell){
            var td = document.createElement("td");
            td.innerHTML = cell;
            tr.appendChild(td);
        });
        var rows = table.find("tr");
        if (rows.length > 10) {
            $("#tickerTable tr:last").remove();
        }
        $(tr).insertAfter("#tickerTable tr.header");

    };
}

/**
 * 空行をテーブルに追加します
 */
function addEmptyRow(tableId, rowCount, colCount) {

    for (i = 0; i < rowCount; i++) {
        var tr = document.createElement("tr");
        for (j = 0; j < colCount; j++) {
            var td = document.createElement("td");
            tr.appendChild(td);
        }
        $(tr).insertAfter("#" + tableId + " tr.header");
    }
}

/**
 * 資産情報を更新します.
 */
function updateBalance() {

   $.ajax({
      url: "http://localhost:8888/balance",
      type: "POST",
      success: function(jsonResponse) {
         jsonResponse = jsonResponse.replace( /\\/g , "" );
         var data = JSON.parse(jsonResponse);
         for(row in data) {
             switch (data[row].currency_code) {
                 case "JPY":
                     $("#jpy").text(data[row].amount);
                     break;
                 case "BTC":
                     $("#btc").text(data[row].amount);
                     break;
                 case "BCH":
                     $("#bch").text(data[row].amount);
                     break;
                 case "ETH":
                     $("#eth").text(data[row].amount);
                     break;
                 case "ETC":
                     $("#etc").text(data[row].amount);
                     break;
                 case "LTC":
                     $("#ltc").text(data[row].amount);
                     break;
                 case "MONA":
                     $("#mona").text(data[row].amount);
                     break;
                 case "LSK":
                     $("#lsk").text(data[row].amount);
                     break;
                 default:
                     console.log("該当なし");
                     break;
             }
         }
      },
      error: function() {
      }
   });
}

/**
 * 約定一覧を更新します.
 */
function updateExecution() {
   $.ajax({
      url: "http://localhost:8888/execution",
      type: "POST",
      success: function(jsonResponse) {
         jsonResponse = jsonResponse.replace( /\\/g , "" );
         var data = JSON.parse(jsonResponse);

            var table = $("#executionTable");
            var rows = table.find("tr");
            for (index in rows) {
                if (rows[index].className == ""){
                    rows[index].remove();
                }
            }

            // テーブルに追加
            $.each(data, function(i, row){
                var tr = document.createElement("tr");
                $.each(row, function(i, cell){
                    var td = document.createElement("td");
                    td.innerHTML = cell;
                    tr.appendChild(td);
                });

                table.append(tr);
            });

      },
      error: function() {
      }
   });
}

 

起動してみる

起動後の画面

結構すっきりしたんじゃないでしょうか。

まとめ

次回は注文の一覧を取得し、Tornadoのテンプレートで JSON形式のデータからうまいことテーブルに変換したいと思います。

ではでは。

スポンサーリンク


関連するコンテンツ

2018年3月16日Python,開発Python,Tornado,プログラミング

Posted by doradora