Bot

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

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

今回参考にした記事はこちら。

これまでの学習で「売買ロジックの定義」と「テスト検証」が完了しています。

いよいよ、「自動売買をするbotの試作版」を作っていきます。

余談:ChatGPTでコード改良

ChatGPTを使って、エラー修正を試みました。

上記↑の指示を打ち込むと、以下↓のように修正版のコードを書いてくれました。

無事に機能するコードができたため、今後どうしても理解できないことは、AIの力を借りることも選択肢に入れておきます。

以下は、前回のコードの修正版です。

import requests
from datetime import datetime
import time

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

if response.status_code != 200:
    print("APIからデータを正しく取得できませんでした。")
    exit()

def get_price(min, i):
    data = response.json()
    return {
        "close_time": data["result"][str(min)][i][0],
        "open_price": data["result"][str(min)][i][1],
        "high_price": data["result"][str(min)][i][2],
        "low_price": data["result"][str(min)][i][3],
        "close_price": data["result"][str(min)][i][4]
    }

def print_price(data):
    print("時間: " + datetime.fromtimestamp(data["close_time"]).strftime('%Y/%m/%d %H:%M') + " 始値: " + str(
        data["open_price"]) + " 終値: " + str(data["close_price"]))

# check_candle 関数の定義を追加
def check_candle(data):
    if data["high_price"] == data["low_price"]:
        return False
    realbody_rate = abs(data["close_price"] - data["open_price"]) / (data["high_price"] - data["low_price"])
    increase_rate = data["close_price"] / data["open_price"] - 1

    if data["close_price"] < data["open_price"]:
        return False
    elif increase_rate < 0.0005:
        return False
    elif realbody_rate < 0.5:
        return False
    else:
        return True

# check_ascend 関数の定義を追加
def check_ascend(data, last_data):
    if data["open_price"] > last_data["open_price"] and data["close_price"] > last_data["close_price"]:
        return True
    else:
        return False

# close_position 関数の定義を追加
def close_position(data, last_data, flag):
    if data["close_price"] < last_data["close_price"]:
        print("前回の終値を下回ったので" + str(data["close_price"]) + "で決済")
        flag["position"] = False
    return flag

# buy_signal 関数の定義を追加
def buy_signal(data, last_data, flag):
    if flag["buy_signal"] == 0 and check_candle(data):
        flag["buy_signal"] = 1
    elif flag["buy_signal"] == 1 and check_candle(data) and check_ascend(data, last_data):
        flag["buy_signal"] = 2
    elif flag["buy_signal"] == 2 and check_candle(data) and check_ascend(data, last_data):
        print("3本連続で陽線 なので" + str(data["close_price"]) + "で買い指値")
        # ここに買い注文のコードを入れる
        flag["buy_signal"] = 3
        flag["order"] = True
    else:
        flag["buy_signal"] = 0
    return flag

# 以下、関数の定義はそのままです

# ここからメイン処理
last_data = None
i = 0

flag = {
    "buy_signal": 0,
    "sell_signal": 0,
    "order": False,
    "position": False
}

while i < 500:
    data = get_price(60, i)
    
    if last_data is not None:
        if data["close_time"] == last_data["close_time"]:
            time.sleep(1)  # 同じデータの場合は1秒待って再度取得
            continue

        print_price(data)

        if flag["position"]:
            flag = close_position(data, last_data, flag)
        else:
            flag = buy_signal(data, last_data, flag)

    last_data = data
    i += 1

    time.sleep(0)  # 0秒待つ

コードを実行してみたところ、正常に機能しています。

変数flagを作る

まずは、注文状態の全体を管理する基準点を作ります。

flag = {
        "buy_signal":0,
        "sell_singnal":0,
        "order":{
            "exist" : False,
            "side" : "",
            "count" : 0
            },
        "position":{
            "exist" : False,
            "side" : ""
            }
        }

buy_signalとsell_signalは、それぞれ「陽線が何本連続しているか」「陰線が何本連続しているか」を管理します。

この値が「3」になったら「買い」か「売り」でエントリーするように設定します。

orderは「未約定の注文があるかどうか」を管理します。

orderの中にはexist、side、countという3つの要素を準備します。

existは「注文の有無」を、sideは「買いか売りか」を管理します。(sideは「買い/売り」を判別するために使います。別の箇所で指定する"BUY"か"SELL"が入ります)

countは、一定時間経過したら注文をキャンセルするロジックのために使います。

ここでは、1分間で約定しなければ注文をキャンセルするように設定します。

今回のコードのように10秒ごとにループ処理をする設定にしてある場合は、このカウントが6になった時(60秒が経過した時)に注文をキャンセルするように指示を出しておきます。

positionは「現在、ポジション(買い・売り)を持っているかどうか」を管理するためのものです。

ローソク足の条件判定

ローソク足の条件判定のコードです。

今回は、買いと売りの両方を指定するため、buyとsellの2つを設定しています。

def check_candle( data,side ):
    if (data["high_price"]-data["low_price"]) == 0 : return False
    realbody_rate = abs(data["close_price"] - data["open_price"]) / (data["high_price"]-data["low_price"])
    increase_rate = data["close_price"] / data["open_price"] - 1
    
    if side  == "buy":
        if data["close_price"] < ["open_price"] : return False
        elif increase_rate < 0.0003 : return False
        elif realbody_rate < 0.5 : return False
        else : return True
        
    if side == "sell":
        if data["close_price"] > data["open_price"] : return False
        elif increase_rate > -0.0003 : return False
        elif realbody_rate < 0.5 : return False
        else : return True

まず、2行目の

if (data["high_price"]-data["low_price"]) == 0 : return False

でゼロ除算の対策をしています。直後に続く式で高値と安値の差が0になった時に、計算をすることを避けるように指示しています。0除算を避けるためのコードは、一番最初におく必要があります。(以前に自分で修正した時は、計算式の後に組み込んでいたためエラーを止めることができなかった)

realbody_rateは「価格の実体の割合(ヒゲの大きさに対するローソク部分の大きさ)」を算出しています。これは、高値と安値の差(=ヒゲの大きさ)に対して始値と終値の差(ローソクの大きさ)が占める割合を求めることで求められます。つまり、「ローソクの大きさ÷ヒゲの大きさ」で求められます。一般的には、ローソク足が長いほどその市場での取引が安定している証とされます。反対に、ローソク足が短いとその市場での取引に不安定さがみられると考えられます。以上のことから「価格の実体の割合」が大きいほど、その局面(上昇・下降)への力が働いていると分析できるのです。

increase_rateは「価格の増加率(減少率)」を算出しています。終値に対して始値が占める割合を求めることで、価格が○%の増加(減少)したかを算出することができます。

これらの値を大きく設定するほど、エントリーに慎重なbotを作ることができます。一方、この値を小さくするほど、わずかな値動きにも反応してエントリーを頻繁に行うbotになります。

buy(=買い)で設定している条件分岐は「終値が始値よりも小さい時は処理を実行するな」「価格の上昇率が3%よりも小さい時は処理を実行するな」「価格の実体が50%を下回る時は処理を実行するな」「それ以外の時は処理を実行しろ」という指示を出しています。

sell(=売り)で設定している条件分岐は「終値が始値よりも大きいときは処理を実行するな」「価格の上昇率が-3%よりも大きい時は処理を実行するな」「価格の実体が50%を下回る時は処理を実行するな」「それ以外の時は処理を実行しろ」という指示を出しています。

買い注文・売り注文を出す関数

買い注文を出す関数は以下の通り。

order = bitflyer.create_order(
        symbol = 'BTC/JPY',
        type='limit',
        side='buy',
        price= data["close_price"],
        amount='0.01',
        params = {"product_code" : "FX_BTC_JPY"})

過去に学習したコードの一部を書き換えています。

赤字の部分で「注文時の価格を終値(close_price)にすること」を指定しています。

売り注文を出す関数は以下の通り。

order = bitflyer.create_order(
        symbol = 'BTC/JPY',
        type='limit',
        side='sell',
        price= data["close_price"],
        amount='0.01',
        params = {"product_code" : "FX_BTC_JPY"})

こちらも赤字部分で「終値」での指値注文を指示しています。

買いシグナルが出たら買い注文を出す関数

def buy_signal( data,last_data,flag ):
	if flag["buy_signal"] == 0 and check_candle( data,"buy" ):
		flag["buy_signal"] = 1

	elif flag["buy_signal"] == 1 and check_candle( data,"buy" )  and check_ascend( data,last_data ):
		flag["buy_signal"] = 2

	elif flag["buy_signal"] == 2 and check_candle( data,"buy" )  and check_ascend( data,last_data ):
		print("3本連続で陽線 なので" + str(data["close_price"]) + "で買い指値を入れます")
		flag["buy_signal"] = 3
		
		order = bitflyer.create_order(
			symbol = 'BTC/JPY',
			type='limit',
			side='buy',
			price= data["close_price"],
			amount='0.01',
			params = { "product_code" : "FX_BTC_JPY" })
		flag["buy_signal"]  = True
		flag["order"]["side"] = "BUY"
			
	else:
		flag["buy_signal"] = 0
	return flag

elif flag["buy_signal"] == 2 and check_candle( data,"buy" ) and check_ascend( data,last_data )で3本連続で陽線を確認した時に、「flag["buy_signal"] 3に更新しなさい」「終値で指し値注文を出せ」「flag["buy_signal"] Trueに更新しなさい(実行しなさい)」「flag["order"]["side"] ”BUY”に更新しなさい」という指示を出しています。

売りシグナルでは、この逆になるため説明は省きます。

未約定の注文を管理する関数

前回の学習で省略していた部分のコードです。

現在出している注文の状況を確認して、その注文が通っているかどうかを判別すること」と「その後の処理」について書かれています。

def check_order( flag :
                position = bitflyer.private_get_getpositions( params = { "product_code" : "FX_BTC_JPY" })
                orders = bitflyer.fetch_open_orders(
                    symbol = "BTC/JPY",
                    params = { "product_code" : "FX_BTC_JPY"})
                
            if position:
                print("注文が約定しました!")
                flag["order"]["exist"] = False
                flag["order"]["count"]) = 0
                flag["position"]["exist"] = True
                flag["position"]["side"] = flag["order"]["side"]
            else:
                if orders:
                    print("まだ未約定の注文があります")
                    for o in orders:
                        print( o["id"])
                    flag["order"]["count"] += 1
                    if flag["order"]["count"] > 6:
                        flag = cancel_order( order,flag )
                else:
                    print("注文が遅延しているようです")
            return flag

まず、position = ~~で「建玉の有無(ポジションの現状)のデータ」を取得します。つまり「買いか売りのポジションを持っているかどうか」を調べているということです。

続いて、orders = ~~で「未約定の注文の状況データ」を取得します。これは「出した注文が通っているかどうか」を確認するためのものです。

if position~~では、ポジションを持っていれば「"注文が約定しました!"と書き出しなさい」「flag["order"]["exist"]をFalseに更新しなさい」「flag["order"]["count"])を0に更新しなさい」「flag["position"]["exist"]をTrueに更新しなさい」「flag["position"]["side"]をflag["order"]["side"]に更新しなさい」という指示を出すように設定しています。

つまり「買い/売りのエントリーを初期状態に戻しなさい」ということです。

一方、else:~~では「未約定の注文が60秒以上経過しても残っている場合は、注文をキャンセルしなさい」という指示を出しています。

今回のコードでは、10秒ごとにコードの処理を実行するように設定しているため、ループ処理が行われるたびにカウントを1追加して、それが6を上回った場合は、注文をキャンセルしなさいという指示にしてあります。

最後のeles:~~では「サーバー側への注文の遅延が発生している場合」の処理です。

注文をキャンセルする関数

注文をキャンセルする関数の構造は、工夫する必要があります。

def cancel_order( orders,flag ):
    for o in orders:
        bitflyer.cancel_order(
            symbol = "BTC/JPY",
            id = 0["id"],
            params = { "product_code" : "FX_BTC_JPY "})
    print("約定していない注文をキャンセルしました")
    flag["order"]["count"] = 0
    flag["order"]["exist"] = False
    
    time.sleep(20)
    position = bitflyer.private_get_getpositions( params = { "product_code" : "FX_BTC_JPY"})
    if not position:
        print("現在、未決済の建玉はありません")
    else:
        print("現在、まだ未決済の建玉があります")
        flag["position"]["exist"] = True
        flag["position"]["side"] = position[0]["side"]
    return flag

for o in orders:~~の部分は、注文をキャンセルした時に「"約定した注文をキャンセルしました"と書き出しなさい」「flag["order"]["count"]を0に更新しなさい」「flag["order"]["exist"]をFalseに更新しなさい」という指示を出しています。

これも、コードの処理状態を初期設定に戻すための指示です。

time.sleep(20):~~は「注文をキャンセルしたものの、すれ違いで約定してしまっていないかどうか」を確認するための処理です。キャンセルを出したタイミングで誰かが別の注文を出していた場合も、サーバー(注文板)に注文が残っていない状態になります。そのため、注文板に注文が残っていない状態を確認すると同時にposition~~で「ポジションを持っているかどうか」を確認する処理を挟んでいるのです。

手仕舞いのための関数

手仕舞いのための関数は以下の通りです。

def close_position( data,last_data,flag ):
    if flag["position"]["side"]  == "BUY":
        if data["close_price"] < last_data["close_price"]:
            print("前回の終値を下回ったので" + str(data["close_price"]) + "あたりで成行で決済します")
            order = bitflyer.create_order(
                symbol = 'BTC/JPY',
                type = 'market',
                side = 'sell',
                amount = '0,01',
                params = { "product_code" : "FX_BTC_JPY"})
        flag["position"]["exist"] = False
        
    if flag["position"]["side"] == "SELL":
        if data["close_price"] > last_data["close_price"]:
            print("前回の終値を上回ったので" + str(data["close_price"]) + "あたりで成行で決済します")
            order = bitflyer.create_order(
                symbol = 'BTC/JPY',
                type = 'market',
                side = 'buy',
                amount = '0,01',
                params = { "product_code" : "FX_BTC_JPY"})
        flag["position"]["exist"] = False
    return flag

ここで行っている処理は、ポジションを持っている場合「成り行き注文で決済しなさい」「処理の状態を初期設定に戻しなさい」という指示です。

具体的には「買いポジションを持っている時、現在の終値が前回の終値を下回ったら決済しなさい」「売りポジションを持っている時、現在の終値が前回の終値を上回ったら決済しなさい」という指示を出しています。

メインの処理

メインの処理は以下の通りです。

while True:
    if flag["order"]["exist"]:
        flag = check_order( flag )
    data = get_price(60,-2)
    if data["close_time"] != last_data["close_time"]:
        print_price( data )
        if flag["position"]["exist"]:
            flag = close_position( data,last_data,flag )
        else:
            flag = buy_signal( data,last_data,flag )
            flag = sell_signal( data,last_data,flag )
        last_data["close_time"] = data["close_time"]
        last_data["open_price"] = data["open_price"]
        last_data["close_price"] = data["close_price"]
        
    time.sleep(10)

まずは未約定の注文の有無を確認して、その後に処理に関する関数を呼び出すように設定しています。

次に、最新のローソク足のデータ(1分足)を取得します。

if文を使った条件分岐で、時間が更新されていた場合(今回は1分ごとに更新)は次の処理に進むように設定してあります。

さらに、ポジションを持っている場合と持っていない場合とでやるべきことを分けています。

ポジションを持っている場合は「手仕舞い処理の関数」を呼び出します。

ポジションを持っていない場合は「買いシグナル」と「売りシグナル」を順番に実行していきます。

最後にこれらの処理を10秒ごとに繰り返すように設定してあります。

まとめ

今回は、コードを部分ごとに解説しました。

全体のループ処理をシンプルにするために関数の構造はやや複雑になりましたが、修正を行いたい時にはこういう形式をとった方が便利そうです。

やっていることはできる限りシンプルにすることで、結果的に修正が楽になるはずです。

変数flagを使うことのメリットや関数にまとめることのメリットも少しずつ分かってきました。

また、if文の条件分岐を活用した構造も抵抗なく読めるようになってきていると感じます。

現在書かれているコードの意味や「=」 を使った代入の意味などもなんとなく理解できるようになってきました。

次回は、サーバーエラーへの処理を追加して実際に動くプログラムを完成させます。

-Bot