Django+bitflyerapiでBitCoinの価格データを取得する
bitflyer api を使って、BitCoinの価格データを取得し、蓄積する方法を解説します。
アプリ自体は作り終わっていて、githubで公開しています。
https://github.com/msamunetogetoge/AutoTrader
この記事のコードは、元のコードの一部を使ったものです。
また、この記事のコードもgithubにあります。
大体の構想
どんな順序で、何をするのか、初めに書いておきます。
1. Djangoアプリを作る。
2. bitflyerのAPI keyを取得する。
3. APIでTickerを取得する。
4. データを保存するモデルを作る。
5. 得られたデータを加工する。
6. データの蓄積
わざわざDjangoを使うのは、この後の記事でも今回作ったアプリを流用する為です。
Djangoアプリを作る
適当な名前でDjangoアプリを作っておきます。
django-admin startproject autotrader_1
cd autotrader_1
python manage.py startapp technical
アプリを作ったら、autotrader_1/autotrader_1/settings.pyやautotrader_1/autotrader_1/urls.pyを編集して、autotrader_1/technical/urls.pyを作ります。
autotrader_1/technical/urls.py
from pathlib import Path
from django.urls import path
from . import views
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent
urlpatterns = [
path("", views.index, name="index"),
]
ここまで準備したら、一旦Djangoを離れて、bitflyerのapiを使ってみます。
bitflyer のApi keyを取得する
後々の為に、bitflyer のApi key を取得しましょう。
この記事を試す分には、必要ありません。
やり方は、bitflyer のサイトで解説されています。
https://lightning.bitflyer.com/docs?lang=ja
データの取得
bitflyer apiでビットコインの価格を取得します。
自分で行うと面倒なので、pybitflyer というライブラリを使います。
以下のようなコードで試す事が出来ます。
autotrader_1/src/bitflyer.py
import pybitflyer
import key
product_code = "BTC_JPY"
api_key = key.api_key
api_secret = key.api_secret
api = pybitflyer.API()
ticker= api.ticker()
print(ticker)
"""
print結果
{'product_code': 'BTC_JPY', 'state': 'RUNNING',
'timestamp': '2021-02-18T00:20:25.387', 'tick_id': 1322408,
'best_bid': 5541445.0, 'best_ask': 5544432.0, 'best_bid_size': 0.05,
'best_ask_size': 0.05726689, 'total_bid_depth': 1397.72424647,
'total_ask_depth': 315.92527907, 'market_bid_size': 0.0,
'market_ask_size': 0.0, 'ltp': 5541523.0, 'volume': 57970.33344175,
'volume_by_product': 9441.53401808}
"""
api.ticker()で,現時刻の価格やボリュームなどの取得が出来、辞書形式で値が返ってきます。
今回は、timestamp と、best_bid, best_askの値を使います。1分間にリクエストを行って良い回数が決められているので注意してください。
https://lightning.bitflyer.com/docs?lang=ja#http-private-api
のAPI制限の項目を見てください。
例えば、同じIPアドレスからのAPI呼び出しは5分で500回までです。
データを保存するモデルを作る
データの取得は簡単ですが、たくさん集まらないと使い物になりません。
そこで、データベースにデータを保存するスクリプトを作りましょう。
Djangoを使うので、modelsを使ってsqlite3にデータを保存できます。
キャンドルスティックグラフ等を描くために、価格以外も保存しておけるようにモデルを作ります。
autotrader/technical/models.py
from django.db import models
# Create your models here.
# 売買を記録するモデル
class SignalEvents(models.Model):
time = models.DateTimeField(null=False, primary_key=True)
product_code = models.CharField(default="BTC_JPY", max_length=15)
side = models.CharField(max_length=10)
price = models.FloatField()
size = models.FloatField()
def __str__(self):
return f"{self.time}:{self.product_code}:{self.side}:price={self.price},size={self.size}"
# tickerを記録するモデル
class Candle_1hBTC_JPY(models.Model):
time = models.DateTimeField(primary_key=True)
product_code = models.CharField(default="BTC_JPY", max_length=15)
open = models.FloatField()
close = models.FloatField()
high = models.FloatField()
low = models.FloatField()
volume = models.FloatField()
def __str__(self):
return f"duration=h,{self.time} {self.product_code} open={self.open} close={self.close} volume={self.volume}"
SignalEventsに売買データを蓄積し、Candle_1hBTC_JPYにビットコインの価格情報を保存します。
Candle_1hBTC_JPYに関して、timeには成形した時刻データをいれます。
価格の情報は、open,close, high, lowです。
openには、データの保存が始まった時の値段、closeには、データの保存が終わった時の値段、high,lowは、1時間での最大/最小の値段を入力します。
また、volumeには、累計の出来高をいれます。
時間を主キーとしておくことで、1時間毎、や1日毎でのデータの管理を楽にしています。(SignalEventsは時間でなくて、bitflyerから発行される売買のidを主キーにした方が良いかもしれませんが。)
管理が楽になる理由は以下の通りです。
主キーはデータに対して一意な値でなくてはなりません。また、djangoは、主キーが同じデータを保存しようとすると、sqlでいうinsertでなくupdateを行うようになっています。(https://docs.djangoproject.com/en/3.1/ref/models/instances/#the-pk-property に書いてあります。)
そういうDjangoの特性があるので、tickerデータのtimestamp部分を"%Y-%m-%dT%H" のように、'分(minutes)'以下を切り捨てるように成形して保存すれば、1時間に1回しかinsertは起こらないのです。
得られたデータを加工する
そういう訳で、得られたデータを成形する為のpythonファイルを用意します。pythonにはdatetimeという時間を扱うクラスがあるので、それに合わせます。
/autotrader/technical/calc/get_data.py
class Ticker():
"""Ticker を取得するクラス。
"""
def __init__(self, api_key=None, api_secret=None, code="BTC_JPY"):
self.api = pybitflyer.API(api_key, api_secret)
self.product_code = code
self.ticker = self.api.ticker(product_code=self.product_code)
def DateTime(self, time=None):
"""[summary] create datetime.datetime object from ticker or str object
Args:
time ([type], optional): [description]. str or datetime.datetime object
Returns:
[type]datetime.datetime: [description]format='%Y-%m-%dT%H:%M:%S'
"""
timeformat = "%Y-%m-%dT%H:%M:%S"
if time is None:
date = self.ticker["timestamp"][:19]
date = datetime.datetime.strptime(date, timeformat)
return date
elif isinstance(time, str):
date = time[:19]
date = datetime.datetime.strptime(date, timeformat)
return date
elif isinstance(time, datetime.datetime):
timeformat = "%Y-%m-%dT%H:%M:%S"
date = datetime.datetime.strptime(
time.strftime(timeformat), timeformat)
# date = time
else:
print("time type is must be str or datetime.datetime ")
return date
def TruncateDateTime(self, duration="h", time=None):
"""[summary] create trucated datetime like datetime.datetime(year,month,hour)
Args:
duration ([type]): [description]
time ([type], optional): [description]. Defaults to None.
Returns:
[type]datetime.datetime: [description] duration='h'→datetime.datetime(year,month,hour)
"""
if time is None:
date = self.DateTime()
else:
date = self.DateTime(time)
timeformat = "%Y-%m-%dT%H"
date = datetime.datetime.strptime(
date.strftime(timeformat), timeformat)
return date
def GetMidPrice(self):
"""GetMidPrice from ticker
Returns:
float: (BestBid +BestAsk) /2
"""
return (self.ticker["best_bid"] + self.ticker["best_ask"]) / 2
具体的にどんな処理を行っているかは読めば分かると思うので、何をする関数かだけ書いておきます。
Datetime()、datetime.datetimeやstrをdatetime.datetime に変換する関数です。
TruncateDateTime() は、datetime.datetime(years, months, hours)の形に自国のデータを成形する関数です。
GetMidPriceは、ビットコインの価格を大体で計算する関数です。
Ticker classを呼び出した段階で、bitflyer apiでticker データを取得している事に注意してください。
これらの関数を使って、modelsにデータを保存する関数を作ります。
/autotrader/technical/calc/get_data.py
class Candle(Ticker):
"""[summary]tickerからcandleを作ったり、モデルに保存したりする。
Args:
Ticker ([type]): [description] ticker class
"""
def __init__(self, api_key, api_secret, code="BTC_JPY"):
super().__init__(api_key=api_key, api_secret=api_secret, code=code)
def GetCandle(self, duration="h", time=None):
"""[summary] Get or create django.db.models objects.
Filtering time=now(time=None) or given time, then, if 'ticker' was exits in models(Candle_1s, etc...), get it.
If objects was not exists, create time=given time(or now) prices = self.MidPrice().
Args:
duration ([type]str in ['s, 'm', 'h']) ): [description] s, m or h decide which models choosen.
Returns:
[type]chart.models.Candle_1s, 1m, 1h: [description] data which is time = self.ticker['timestamp']
"""
model = "Candle_1" + duration + self.product_code
candle = eval(model)
date = self.TruncateDateTime(duration=duration, time=time)
if not candle.objects.filter(time=date, product_code=self.product_code).exists():
price = self.GetMidPrice()
candle = candle(
time=date,
product_code=self.product_code,
open=price,
close=price,
high=price,
low=price,
volume=self.ticker["volume"])
candle.save()
return candle
else:
try:
c = candle.objects.get(time=date, product_code=self.product_code)
except Exception as e:
print(e)
print("models.get method is failed.")
c = candle.objects.filter(time=date, product_code=self.product_code).first()
return c
def CreateCandleWithDuration(self, duration="h", time=None):
"""[summary] get candle from self.GetCandle. then update candle params and save.
Args:
duration ([type]): [description]
"""
current_candle = self.GetCandle(duration=duration, time=time)
price = self.GetMidPrice()
current_candle.open = current_candle.open
current_candle.close = price
if current_candle.high <= price:
current_candle.high = price
elif current_candle.low >= price:
current_candle.low = price
current_candle.volume += self.ticker["volume"]
current_candle.save()
def GetAllCandle(self, duration="h"):
"""[summary]Get django.db.models object from duration. if duration='h', this funcution returns chart.models.Candle_1hBTC_JPY.
Args:
duration ([type] str ): [description] h means Candle_1hBTC_JPY
Returns:
[type]: [description] django.db.models from chart.models
"""
model = "Candle_1" + duration + self.product_code
model = eval(model)
return model
Candleクラスは、Tickerクラスを継承してモデルにデータを保存するクラスです。
興味のある人だけ中身を真面目に見てもらうとして、関数達が何をしているか簡単に説明します。
GetCandle()は、modelsに現在の時刻のデータがあるか問い合わせ、もしもあればそのデータを返します。もしもデータが無ければ、取得したtickerデータを元にmodelsの要求する形のデータを作り、保存してからmodels.object の形で返します。
CreateCandleWithDuration()は、データの更新をする関数です。
具体的には、新たに得られたtickerデータの値段をGetMidPriceに通し、GetCandleで取ってきたデータの値段を比べて、high,low,closeを更新します。
GetAllCandle()は、モデルのデータ全てを取得する関数です。
これは後でグラフを描いたり、関数が上手く動いているか確認するのに使います。
データを蓄積する
今まで作った関数を使って、データの蓄積を始める事が出来ます。
djangoでは、app/management/commands/commandname.pyのようにディレクトリを作る事で、python manage.py のコマンドを自作する事が出来ます。
詳しくは、公式ドキュメントを読んでください。
https://docs.djangoproject.com/en/3.1/howto/custom-management-commands/
その機能を使って、データをbitflyerから取得するコマンドを作ります。
以下のように、ディレクトリとファイルを作ります。
technical
|
└─management
│ init.py
│
└─commands
init.py
startstream.py
init.py は、空のファイルです。
startstream.pyは以下のように記述します。
startstream.py
from django.core.management.base import BaseCommand
import time
from chart.models import *
from chart.controllers import get_data
from src import key
class Command(BaseCommand):
"""[summary] get candles ftom bitflyer api and save to DB, and save().
Args:
BaseCommand ([type]): [description]
"""
def handle(self, *args, **options):
print("Start GetCandles")
api_key = key.api_key
api_secret = key.api_secret
duration = "h"
while True:
product_code = "BTC_JPY"
cdl = get_data.Candle(api_key=api_key, api_secret=api_secret, code=product_code)
cdl.CreateCandleWithDuration(duration=duration)
model = eval("Candle_1" + duration + product_code)
ticker = model.objects.filter(product_code=product_code).last()
print(f"Create Model Data:{ticker}")
time.sleep(10)
上で作った関数を使って、10秒ごとにtickerデータを取得します。
manage.pyのあるディレクトリで、以下のように入力すると、作ったコマンドを呼び出す事が出来ます。
python manage.py startstream
startstream.pyの中身を見ると分かるように、この関数は端末を閉じたり、強制的に止めるまで動き続けます。
sqlite3 を使っていると、30Mbデータを貯めこむと動きがおかしくなってくるので、注意してください。
1日放っておけば、24個のデータが貯まるはずです。24個のデータだと何も起こらないのは想像に難くないと思います。
最後辺りの記事で、私がため込んだ数か月分の時間足のデータを公開する予定なので、それを待っても良いかもしれません。
まとめ
まとめると、今回の記事では、以下の事を行いました。
・bitflyer のapi keyの取得
・Djangoアプリの作成
・tickerデータをの取得
・データの加工
・データの保存用のコマンド作成