Bot

仮想通貨botの開発を本格的に始めてみる#20(2023/9/23)

前回の内容に引き続き、今回も仮想通貨botの開発状況をまとめていきます。

今回から「バックテストで勝率の検証をする」ことをテーマに学習を進めていきます。

参考にしたのはこちらの記事です。

勝率とは

勝率を考える上で重要な指標は「トレードの回数を増やすことができるか」「勝ち負けの差し引きで期待値を算出する」「1回のトレードで使った金額に対して平均して何%のリターンを得られるか」「トレードの結果、軍資金が何倍になったか」「最大でどれくらいまで軍資金を減らしたか」の5つです。

bot運用において、少なくともこれら5つの指標を考慮していないと思わぬところで損失を出すことになってしまいます。

上記5つの項目を数値に落とし込んでbotを開発することが基本です。

注文遅延とスリッページ

取引所での注文には「遅延」が発生することがあります。

バックテストの際には、過去のデータでbotの挙動を検証するため遅延が考慮されません。

しかし、リアルタイムの取引では頻繁に遅延が発生し、設定した通りの価格で注文が通らないこともあります。

また、注文を通す以前に板に並ぶタイミングが遅れてしまうということも起こり得ます。

そのため、遅延を加味していないと、損失が一方的に貯まる可能性もあります。

さらに成り行き注文での手仕舞いを設定している場合は、理想の価格での手仕舞いが実行できないこともあるため損失を出してしまうこともあります。

これらの対策としては「テスト段階からスリップ損失を考慮して手数料としてあらかじめ差し引いておく」「実際の運用では1分足は使わない。15分足や1時間足で運用する」「エントリー注文は指値にする」などが挙げられます。

過去の価格データを集める

ここからは、こちらの記事を参考にして過去の価格データを集めていきます。

実際に過去のデータを取得するためには、Cryptowatchを使います。

本家のサイトに掲載されていたコードを書いて実装したところ、なぜか1件のデータしか取得できずChatGPTに質問。

その結果、returnの位置がずれていたことが原因だということが分かりました。

実行して正しく動作したコードは以下の通り。

import requests
from datetime import datetime
import time

def get_price(min, before=0, after=0):
    price = []
    params = {"periods" : min }
    if before != 0:
        params["before"] = before
    if after != 0:
        params["after"] = after
        
    response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params)
    data = response.json()
    
    if data["result"][str(min)] is not None:
        for i in data["result"][str(min)]:
            price.append({"close_time" : i[0],
                          "close_time_dt" : datetime.fromtimestamp(i[0]).strftime('%Y/%m/%d %H:%M'),
                          "open_price" : i[1],
                          "high_price" : i[2],
                          "low_price" : i[3],
                          "close_price" : i[4] })
        return price#このreturnはforにかかる
        
    else:
        print("データが存在しません")
        return None
    
    
    
price = get_price(60)

if price is not None:
    print("先頭データ : " + price[0]["close_time_dt"] + " UNIX時間 : " + str(price[0]["close_time"]))
    print("末尾データ : " + price[-1]["close_time_dt"] + " UNIX時間 : " + str(price[-1]["close_time"]))
    print("合計 : " + str(len(price)) + "件のローソク足データを取得")
    print("---------------------")
    print("---------------------")

このコードの中から

price = get_price(60)

を修正することで、データを取得する時間の間隔を変更することができます。

【60=1分足の場合】↓約11時間分のデータを取ることができました。

先頭データ : 2023/09/23 10:14 UNIX時間 : 1695431640
末尾データ : 2023/09/24 11:35 UNIX時間 : 1695522900
合計 : 1000件のローソク足データを取得

【300=5分足の場合】↓約4日分のデータを取ることができました。

先頭データ : 2023/09/20 23:25 UNIX時間 : 1695219900
末尾データ : 2023/09/24 11:40 UNIX時間 : 1695523200
合計 : 1000件のローソク足データを取得

【3600=1時間足の場合】↓約40日分のデータを取ることができました。

先頭データ : 2023/08/13 20:00 UNIX時間 : 1691924400
末尾データ : 2023/09/24 12:00 UNIX時間 : 1695524400
合計 : 1000件のローソク足データを取得

デフォルト設定では1,000件までのデータを取得することが可能なようです。

さらに多くのデータを取得してみる

どれだけ過去に遡ってデータを取得できるのか試してみます。

まずは、1分足で10日前まで遡ってデータを取得するように指定してみました。

本日は9月24日なので、9月14日の0時0分をタイムスタンプツールで変換して入力。

price = get_price(60,after=1694617200)

このコードを実行して得られた結果は以下の通り。

先頭データ : 2023/09/19 19:29 UNIX時間 : 1695119340
末尾データ : 2023/09/24 14:41 UNIX時間 : 1695534060
合計 : 6000件のローソク足データを取得

6,000件のデータを取得することができました。1分足では約5日分のデータを獲得することができるようです。

【5分足では3週間分】

続いて、5分足でも検証。

6,000件が上限だと仮定して、3週間分のデータを取得するために9月1日までさかのぼってデータを取得するように指定。

price = get_price(300,after=1693494000)

得られた結果は以下の通り。

先頭データ : 2023/09/03 15:00 UNIX時間 : 1693720800
末尾データ : 2023/09/24 14:50 UNIX時間 : 1695534600
合計 : 6000件のローソク足データを取得

予想通り3週間分のデータを取得しました。

【1時間足の場合】

6,000件のデータを取得すると仮定すると、約8ヶ月分のデータが獲得できるはずです。

とりあえず、テキトーに一年前の日付(2022/9/24)を指定して、コードを実行してみます。

price = get_price(3600,after=1663945200)

得られた結果は以下の通り。

先頭データ : 2023/01/17 14:00 UNIX時間 : 1673931600
末尾データ : 2023/09/24 15:00 UNIX時間 : 1695535200
合計 : 6000件のローソク足データを取得

予想通り約8ヶ月分のデータを取得することができました。

データの保存と読み込み

取得したデータをファイルに書き出すためのコードも実装しました。

import requests
from datetime import datetime
import time
import json

def get_price(min, before=0, after=0):
    price = []
    params = {"periods" : min }
    if before != 0:
        params["before"] = before
    if after != 0:
        params["after"] = after
        
    response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc",params)
    data = response.json()
    
    if data["result"][str(min)] is not None:
        for i in data["result"][str(min)]:
            price.append({"close_time" : i[0],
                          "close_time_dt" : datetime.fromtimestamp(i[0]).strftime('%Y/%m/%d %H:%M'),
                          "open_price" : i[1],
                          "high_price" : i[2],
                          "low_price" : i[3],
                          "close_price" : i[4] })
        return price
        
    else:
        print("データが存在しません")
        return None

def get_price_from_file(path):
    file = open(path,"r",encoding="utf-8")
    price = json.load(file)
    return price    
    
#メインの処理
#cryntwatchのAPIから価格データを読み込む時    
price = get_price(60,after=1663945200)

#ファイルから読み込む時
#price = get_price_from_file("./1673935200-price.json")#ファイル名を入力

if price is not None:
    print("先頭データ : " + price[0]["close_time_dt"] + " UNIX時間 : " + str(price[0]["close_time"]))
    print("末尾データ : " + price[-1]["close_time_dt"] + " UNIX時間 : " + str(price[-1]["close_time"]))
    print("合計 : " + str(len(price)) + "件のローソク足データを取得")
    print("---------------------")
    print("---------------------")
    
    #ファイルに書き込む時
    #file = open("./{0}-{1}-price.json".format(price[0]["close_time"],price[-1]["close_time"]),"w",encoding="utf-8")
    #json.dump(price, file, indent=4)

以下の部分(#file~~と#json.~~)で#を消すと、コードが実行されて取得したデータをファイルに書き出すことができます。

#ファイルに書き込む時
    #file = open("./{0}-{1}-price.json".format(price[0]["close_time"],price[-1]["close_time"]),"w",encoding="utf-8")
    #json.dump(price, file, indent=4)

また、以下の部分(#price~~)で#を消して、メイン処理price = get_price(60,after=1663945200)の前に#をつけてコードを実行すると、ファイルのデータを読み込んでデータを出力することができます。

#price = get_price_from_file("./1673935200-price.json")#ファイル名を入力

しかし、ファイル内のデータの一部が””(ダブルクォート)で囲まれていないとエラーが出てしまうため、別方面で対策が必要です。

ChatGPTにコードの修正をさせてみましたが、根本的な解決には至らなかったため、一旦保留にします。

解決方法は以下の2通り。

①ファイル内部のデータで""で囲まれていない部分を修正する←該当箇所を探し出すのが大変
②ファイルを出力するときに全てのプロパティが""で囲まれるように指示する←指示コードに不足があり、望んだ通りの出力ができないため、改善の必要あり

一応修正版のコードも掲載しておきます。以下のコードであれば、読み込んだファイルから不具合を起こした箇所を特定できるようになっています。

不正なデータをスキップしつつデコードエラーにも対応していますが、エラーを完全に取り除けるわけではないため改善が必要です。

import requests
from datetime import datetime
import time
import json

def get_price(min, before=0, after=0):
    price = []
    params = {"periods": min}
    if before != 0:
        params["before"] = before
    if after != 0:
        params["after"] = after

    response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc", params)

    try:
        data = response.json()
    except json.JSONDecodeError as e:
        print(f"JSONデコードエラー: {e}")
        return None

    if data.get("result") and data["result"].get(str(min)):
        for i in data["result"][str(min)]:
            if len(i) == 6:
                try:
                    price.append({"close_time": i[0],
                                  "close_time_dt": datetime.fromtimestamp(i[0]).strftime('%Y/%m/%d %H:%M'),
                                  "open_price": i[1],
                                  "high_price": i[2],
                                  "low_price": i[3],
                                  "close_price": i[4]})
                except Exception as e:
                    print(f"不正なデータをスキップしました: {e}")
            else:
                print("不正なデータをスキップしました")
        return price
    else:
        print("データが存在しません")
        return None

def get_price_from_file(path):
    try:
        with open(path, "r", encoding="utf-8") as file:
            try:
                price = json.load(file)
                return price
            except json.JSONDecodeError as e:
                print(f"JSONデコードエラー: {e}")
                return None
    except FileNotFoundError as e:
        print(f"ファイル読み込みエラー: {e}")
        return None

# メインの処理
# cryntwatchのAPIから価格データを読み込む時
# price = get_price(60, after=1663945200)

# ファイルから読み込む時
price = get_price_from_file("./1693722000-price.json")

if price is not None:
    print("先頭データ : " + price[0]["close_time_dt"] + " UNIX時間 : " + str(price[0]["close_time"]))
    print("末尾データ : " + price[-1]["close_time_dt"] + " UNIX時間 : " + str(price[-1]["close_time"]))
    print("合計 : " + str(len(price)) + "件のローソク足データを取得")
    print("---------------------")
    print("---------------------")

    # ファイルに書き込む時
    # with open("./{0}-{1}-price.json".format(price[0]["close_time"],price[-1]["close_time"]), "w", encoding="utf-8") as file:
    #     json.dump(price, file, ensure_ascii=False, indent=4)

まとめ

今回は、バックテストの下準備でした。

CryptowatchのAPIから過去6,000件分の価格データを取得する方法を知ることができました。

バックテストにおいては、過去の情報は多ければ多いほど検証のデータに信頼性が出ます。

解決したいことは、json形式のファイルの読み込みエラーです。

""で囲まれていない形式で一部のデータが出力されてしまうことを避けたいので、この点は引き続きリサーチです。

ファイルに書き込むコード自体を修正するのも一つの手段として検討します。

また、1分足や5分足のデータにおいてはエラーが見られましたが、1時間足のデータは正しく読み込むことができました。

また、こちらの記事を参考にして、新たに追加されていくデータの更新もできるようにしていきます。

これを行うことで6,000件以上のデータを利用することができるようになるはずです。

次回は、取得したデータを利用して「botの勝率・平均リターン・総利益を計算する」ことがテーマです。

-Bot