Blow Up by Black Swan

Python-仮想通貨取引所のAPIからデータを取得し、CSVデータに書き込みを行うプログラム

今回は前からやってみたかった、仮想通貨取引所のAPIからデータを取り、それをグラフにするプログラムを書いてみました。プログラミング初心者としては、本やサイトを写経してただインプットしていくだけよりも、自分で考えながらアウトプットする方が勉強になるなと改めて感じました。今回も分量が長くなるので2部構成にしており、(1)APIからデータを取り、csvデータに保存するまでと、(2)そのcsvファイルからデータを読み込んでグラフにするという部分で分けています。今回はAPIからデータを取得してをそれをCSVに保存するまでです。

APIはbitflyerのものを利用しており、pythonは3系です。たった一人でも、同じようなところでつまづいている人の参考になれば幸いです。

1. 全体の構成

コード全体の構成や流れは以下のようになります。

構成
  ・今後新たな機能の追加や重層的なコードにすることも考え、クラスを利用
  ・柔軟な条件設定ができ、かつテストや不具合時に都度コード自体に書き込み、コードを荒らすことがないよう、設定をJsonファイルから読み取る形式を選択
  ・bitflyerの全ての約定履歴を取得し、その後リアルタイムデータを逐次追加する処理に移行する流れ

また、全体の流れは下記のようになっています。

全体の流れ

  ①過去のデータを取得するフロー
    1. bitflyerのAPIから情報を取得
    2. 取得したJsonデータを解析
    3. データを整形
    4. csvに書き込み
    5. 1~4を繰り返しデータを取得へ

  ②リアルタイムにAPIを取得するフロー
    1. bitflyerのAPIから情報を取得
    2. 取得したJsonデータを解析
    3. データを整形
    4. csvに書き込み
    5. 一定間隔ごとに1~4のループを回し、リアルタイム情報を取得し続ける
  ③チャート表示フロー(次の記事の対象)

①と②の1〜4がほとんど重複しているので、そこを整理することで、簡単にできることがわかると思います。実際には、ボトムアップで試行錯誤しながら、最終的に上記のような整理した形になったということです。また、今回会員登録が不要なbitflyerのPublic APIの中で、約定履歴を取得できるTickerというAPIを利用しています。ここでは、約定履歴を取得する際に、1回の呼び出しで500データ、1分間に200回のAPI呼び出し制限などがあります。他にも様々なAPIが用意されていますので、詳しくは下記をご覧ください。

2. コード

ここから、私が具体的に作ったコードの説明です。

2-1. 全体のコード

まずは、全体のコードです。他の取引所でも利用できるような汎用性を持たせようとはしましたが、若干の仕様の違いがあり、完全な汎用性は持たせられていません。これは、後々挑戦してみたいと考えています。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#bitflyer_api.py

import requests, csv, pytz, time, json, sys

class BF_api:
 def __init__(self, json_obj):
 self.URL = json_obj['URL']
 self.count = json_obj['count']
 self.after = json_obj['after']
 self.before = json_obj['before']
 self.csvfile = json_obj['csvfile']


    #過去データ取得メソッド
    def past_data(self):
        params = {'count':self.count, 'after':self.after, 'before':self.before}
        while True:
        #for num in range(10):   #実験用のforループ
            api_data_json = requests.get(self.URL, params)
            if api_data_json.status_code != 200:
                print('Error occured: ' + str(api_data_json.status_code))
                sys.exit()
            api_data = api_data_json.json()
            if api_data == []:
                break
            api_data.sort(key=lambda x: x['id'])
            data_list = []
            for i in range(len(api_data)):
                data_list.append([api_data[i]['id'],
                                api_data[i]['price'],
                                api_data[i]['exec_date']])
            last_id = api_data[-1]['id']
            with open(self.csvfile, 'a', newline="") as csv_file:
                csv_writer = csv.writer(csv_file, lineterminator='\n')
                csv_writer.writerows(data_list)
            params['after'] = last_id
            params['before'] = last_id + 500
            time.sleep(0.2)
        return last_id

    #リアルタイムデータ取得メソッド
    def real_data(self, last_id, csvfile=None):
        params = {'count':self.count, 'after':last_id, 'before':last_id + 500}
        while True:
        #for number in range(10):  #実験用のforループ
            api_data_json = requests.get(self.URL, params)
            if api_data_json.status_code != 200:
                print('Error occured: ' + str(api_data_json.status_code))
                sys.exit()
            api_data = api_data_json.json()
            if api_data == []:
                time.sleep(30)
                continue
            api_data.sort(key=lambda x: x['id'])
            data_list = []
            for i in range(len(api_data)):
                data_list.append([api_data[i]['id'],
                                  api_data[i]['price'],
                                  api_data[i]['exec_date']])
            last_id = api_data[-1]['id']
            with open(self.csvfile, 'a', newline='') as csv_file:
                csv_writer = csv.writer(csv_file, lineterminator='\n')
                csv_writer.writerows(date_list)
            params['after'] = last_id
            time.sleep(30)

            
            
#実行文
if __name__ == "__main__":
    with open('test.json', 'r') as config_file:
        json_obj = json.load(config_file)
    BF = BF_api(json_obj)
    with open(BF.csvfile, 'w', newline='') as csv_file:
        csv_writer = csv.writer(ccsv_file, lineterminator='\n')
        csv_writer.writerow(['id', 'price', 'date_time'])
    last_id = BF.past_data()  #過去データ
    BF.real_data(last_id)  #リアルタイムデータ

・設定用のJsonデータ

{
  "URL": "https://api.bitflyer.jp/v1/getexecutions",
  "count": 499,
  "after": 318068520,
  "before": 318069020,
  "csvfile": "/Users/nero/Desktop/test.csv"
}

クラスのインスタンス変数を5つ、クラスメソッドが2つという設定です。

2-2. APIからのデータの取得からcsvファイルへの保存まで

まずは根幹となるapiからデータを取得し、csvファイルに保存するまでです。コードだと以下の部分です。

api_data_json = requests.get(self.URL, params)
if api_data_json.status_code != 200:
    print('Error occured: ' + str(api_data_json.status_code))
    sys.exit()
api_data = api_data_json.json()
if api_data == []:
    break
api_data.sort(key=lambda x: x['id'])
data_list = []
for i in range(len(api_data)):
    data_list.append([api_data[i]['id'],
                      api_data[i]['price'],
                      api_data[i]['exec_date']])
last_id = api_data[-1]['id']
with open(self.csvfile, 'a', newline="") as csv_file:
    csv_writer = csv.writer(csv_file, lineterminator='\n')
    csv_writer.writerows(data_list)

まずはAPIからの情報の取得です。

●API呼び出しとレスポンスデータの取得
ここでは、「人間のためのHTTP」と題するRequestsライブラリを利用しています。requests.getメソッドでは、引数に渡したURLを呼び出し、そのレスポンスデータを格納してくれます。普通のURLの場合、htmlが格納されますが、今回はAPIを利用しているので、Json形式のデータが返されます。また、今回複数のクエリパラメータを指定しているので、pythonの辞書型でクエリパラメータを指定しています。クエリパラーメタの指定が1つであれば、クエスチョンマークで繋ぐこともできます。
(例:”https://api.bitflyer.jp/v1/getexecutions?BTC_JPY”)

次にif文が入っていますが、ここは2-4.の過去の約定履歴の取得のところで説明します。

●Jsonデータの解析
レスポンスされたデータがJson形式となるため、このJsonデータを解析します。ここでは、Pythonの標準モジュール、Jsonライブラリを利用します。Jsonデータの解析は非常に簡単で、「Jsonオブジェクト.json()」で解析され、pythonのオブジェクトに変換されます。この下にもまた、if文が登場しますが、ここも2-4で説明します。ちなみにレスポンスデータは、最近だとJsonが主流のようですが、このレスポンスデータがどのファイル形式で返されているかを確認するには、Responseオブジェクトのheaders属性で確認することができます。

import requests
re = requests.get("https://api.bitflyer.jp/v1/getexecutions")  #Responseオブジェクト
re.headers

戻り値

{'Date': 'Mon, 23 Jul 2018 07:21:57 GMT', 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': '2049', 'Connection': 'keep-alive', 'Set-Cookie': '__cfduid=dcbc3b2fbfa760003bdc812100e5ab1221532330517; expires=Tue, 23-Jul-19 07:21:57 GMT; path=/; domain=.bitflyer.jp; HttpOnly; Secure', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache', 'Content-Encoding': 'gzip', 'Expires': '-1', 'Request-Context': 'appId=cid-v1:e4fbc941-a2df-48ac-bbac-f2180e904002', 'X-Content-Type-Options': 'nosniff', 'X-XSS-Protection': '1; mode=block', 'X-Frame-Options': 'sameorigin', 'Content-Security-Policy': "default-src http: https: ws: wss: data: 'unsafe-inline' 'unsafe-eval'", 'Strict-Transport-Security': 'max-age=31536000', 'Expect-CT': 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', 'Server': 'cloudflare', 'CF-RAY': '43ec64a63a20949f-NRT'}

‘Content-Type’の「application/json」という記載から確認できます。

●情報の整形
APIから情報を取得し、Jsonを解析すると以下のようなデータが返ってきます。

#データ量が多いので、取得データを2つに限定している
import requests
re = requests.get("https://api.bitflyer.jp/v1/getexecutions", {"count":2})
re.json()

戻り値

[{'id': 319747260,
  'side': 'SELL',
  'price': 858011.0,
  'size': 0.01,
  'exec_date': '2018-07-23T08:13:11.007',
  'buy_child_order_acceptance_id': 'JRF20180723-081229-301270',
  'sell_child_order_acceptance_id': 'JRF20180723-081310-280089'},
 {'id': 319747253,
  'side': 'SELL',
  'price': 858104.0,
  'size': 0.01,
  'exec_date': '2018-07-23T08:13:09.583',
  'buy_child_order_acceptance_id': 'JRF20180723-081308-564415',
  'sell_child_order_acceptance_id': 'JRF20180723-081309-340787'}]

価格以外にも様々なデータが入っていますが、全データを取得し保存する場合、データ量がかなりの量になってしまうため、この中から、チャート作りなどで必要と思われる’id’、’price’、’exec_date’を抽出し、リストに格納していきます。レスポンスデータを色々と取得してみると時折、データの並びがおかしいことがわかったので、一旦データの並びをidを軸に昇順で並び替える文を入れています。lambda関数は無名関数と言われているもので、今回の引数の条件指定に非常に有用でした。

そして、取得したレスポンスデータがpythonの辞書データを格納するリストという構造になっているため、forループで上記の必要なデータを取得し、新しいデータを入れる空リストにひとまとまりのリストデータとして格納していきます(リストの中にリストという構造)。次列にlast_idを設定していますが、ここも2-4で取り扱います。これでデータの取得から必要なデータを抽出しリストを作成するところまで来ました。

2-3. csvファイルへの書き込み

次は、csvファイルへのデータの書き込みです。pythonでcsvファイルを取り扱う場合、標準モジュールのcsvを利用します。また、csvファイルの取り扱いは一般的なテキストファイルなどの取り扱いと同様でopen関数で開き、readかwriteオブジェクトを作り、作業が終わったらclose関数を呼ぶという流れです。今回は、with文を利用しているので、close関数を呼び出さずとも処理が終わるとファイルが自動で閉じられます。さらに書き込み時には「lineterminator=’\n’」と指定することで行と列が綺麗に揃った状態でデータが格納されるようにしています。そして、最後に「writerows」メソッドで保有するリストデータを全て書き込むようにしています。これが、APIからデータを取得し、csvファイルへ書き込むまでの一連のフローです。

2-4, 過去の約定履歴の取得

次は、2-3を基礎に過去のデータを回す流れを作ります。構成としては、APIに渡すパラメーターを都度いじっていくことで1~4のループを回していくイメージです。具体的には最初{"count":500, "after":0, "before":501}からはじめ、次の試行で{"count":500,"after":last_id, "before":last_id+501}と順次推移していきます。afterを501にしている理由は、bitflyerAPIではbeforeとafterに指定された数字はレスポンスされるデータに含まれないからです。また、beforeをしている理由は、指定しない場合直近のデータをとってきてしまうためです。私が色々と試してみて見つけたbitflyerAPIの特徴は下記になります。

【bitflyerAPIの特徴】
 ・beforeとafterはともにidを指す
 ・beforeとafterに幅がある場合新しいデータから返される
 (例:{"count":500, "after":0, "before":1000}と指定された場合、id番号が999から降順に数えて500個のデータが返される。)
 ・beforeとafterにはint型とstr型どちらでも良い
 ・countの指定がない場合、100個のデータが返ってくる
 ・countを500個にしていると、500サーバーエラーが返ってくることがある(色々検証するも理由は不明)
 ・対象の通貨ペアを表すprodact_codeというクエリもあるが、指定しないとBTC_JPYの組み合わせになる

paramsのafterとbeforeを指定しているのは、このループのためです。もれなくだぶりなくデータを取得するために、取得できたデータの最後のid番号利用しています。また、このループはwhile Trueにすることで永遠に回り続けるようになっていますが、リアルタイムに追いつき、新たな約定も起こっていない場合、レスポンスデータは空リストになります。この時、過去データの取得ができたことを意味するため、そこで抜け出せるようにif文を設定しています。

最後に、時折bitflyerAPIからのデータ取得がエラーになることがあります。私が一番見たのはサーバーエラーを表す500ですが、他のエラーの発生も考えられるため、エラー用の記述も入れておきます。requests.getの場合、レスポンスがステータスエラーの場合でもpythonエラーが起こるわけではないため、Responseオブジェクトのstatus_code属性を利用しています。

ここでは、一旦print関数とsys.endメソッドで終了という形をとっていますが、LINEやSlackのAPIを利用し、メッセージがくるように設定するのも面白いのかなと思います。ここら辺も改めて挑戦して見たいと思います。このような流れで考え、過去データを取得するクラスメソッドを構築しました。

2-5. 現在のリアルタイムの約定履歴の取得

最後は、リアルタイムデータの取得です。全体のコードの流れとして、このpythonファイルを実行したら、エラーコードが返ってこない限り、プログラムが回り続けるという作りになっています。そのやり方が良いのかどうかは今の私にはわかりませんが、その流れだと過去データ取得用のメソッドが終了後、リアルタイムデータ取得メソッドが始まるという風にコードを書けば、実行されます。

そこで、過去データ取得メソッドを回す場合と同様に、last_idを利用し、もれなくだぶりなくデータをとっていくのが最善だと思うので、過去データ取得メソッドではメソッドの戻り値として最後のlast_idを返すようにしています。そして、リアルタイムデータはそれを引数として受け取って、クエリパラメータ設定を行い、30秒間隔程度でループを回していくという流れを取るようにしています。

3. まとめ

以上が今回作成したコードです。より使い勝手の良いコードにするには、他の仮想通貨取引所でも利用できるような汎用性を加える、1~4のところを別のクラスメソッドとして設計し、過去データ取得メソッドとリアルタイムデータ取得メソッドをより簡潔にする(パラメータの設定が思うようにいかず設計できていません)、サーバでエラーとなった場合の連絡が自動で怒るようなシステムの導入、せっかくの設定をJsonで導入する方式を使用しているので、より条件設定の自由度をあげられるような設計、などまだまだ多くの欠点とトライがあると思います。この点は、引き続きトライしていきたいと思います。

読んでくださった方、ありがとうございます。

4. 参考サイト等

主要参考サイトは以下になります。標準モジュールIやサードパーティーモジュールの公式ドキュメントはすぐ探せると思うので、記載はしていません。