前回の記事の続きです。
エントリーの条件・手仕舞いの条件などの売買ロジックが完成したので、ここに実際にBitflyerにオーダーを出すコードを入れてみましょう。なお、ついでに今回の記事から「売りシグナルの判定」と「売りからのエントリー」にも対応できるようコードを改良します。(こちらの記事の答え合わせです)
暫定的な完成版のpythonコードが以下になります。
Pythonコード
import requests from datetime import datetime import time import ccxt bitflyer = ccxt.bitflyer() bitflyer.apiKey = '**********' bitflyer.secret = '**********' # Cryptowatchから価格を取得する関数 def get_price(min,i): response = requests.get("https://api.cryptowat.ch/markets/bitflyer/btcfxjpy/ohlc", params = { "periods" : 60 }) 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"]) ) # 各ローソク足が陽線・陰線の基準を満たしているか確認する関数 def check_candle( data,side ): 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"] < data["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 # ローソク足が連続で上昇しているか確認する関数 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 # ローソク足が連続で下落しているか確認する関数 def check_descend( data,last_data ): if data["open_price"] < last_data["open_price"] and data["close_price"] < last_data["close_price"]: return True else: return False # 買いシグナルが出たら指値で買い注文を出す関数 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["order"]["exist"] = True flag["order"]["side"] = "BUY" else: flag["buy_signal"] = 0 return flag # 売りシグナルが出たら指値で売り注文を出す関数 def sell_signal( data,last_data,flag ): if flag["sell_signal"] == 0 and check_candle( data,"sell" ): flag["sell_signal"] = 1 elif flag["sell_signal"] == 1 and check_candle( data,"sell" ) and check_descend( data,last_data ): flag["sell_signal"] = 2 elif flag["sell_signal"] == 2 and check_candle( data,"sell" ) and check_descend( data,last_data ): print("3本連続で陰線 なので" + str(data["close_price"]) + "で売り指値を入れます") flag["sell_signal"] = 3 order = bitflyer.create_order( symbol = 'BTC/JPY', type='limit', side='sell', price= data["close_price"], amount='0.01', params = { "product_code" : "FX_BTC_JPY" }) flag["order"]["exist"] = True flag["order"]["side"] = "SELL" else: flag["sell_signal"] = 0 return flag # 手仕舞いのシグナルが出たら決済の成行注文を出す関数 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 # サーバーに出した注文が約定したかどうかチェックする関数 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( orders,flag ) else: print("注文が遅延しているようです") return flag # 注文をキャンセルする関数 def cancel_order( orders,flag ): for o in orders: bitflyer.cancel_order( symbol = "BTC/JPY", id = o["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 # ここからメイン last_data = get_price(60,-2) print_price( last_data ) time.sleep(10) flag = { "buy_signal":0, "sell_signal":0, "order":{ "exist" : False, "side" : "", "count" : 0 }, "position":{ "exist" : False, "side" : "" } } 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)
こちらのコードをそのまま書き写せば、とりあえず動きます。
ただし安定的に(エラーで止まったりせずに)何日も連続で動かし続けるためには、あともう一工夫だけ必要です。これについては、この次の記事で解説します。
コードの解説
前回の記事からの変更点で、重要な部分について解説しておきます。
注文状況を管理する変数 flag
前回の記事でも、「買いシグナルの状況」「サーバーに出した注文状況」「ポジションの保有有無」などの情報を管理するために、flagという変数を作りました。今回は、売りシグナル・売りエントリーも考えないといけないので、以下のように変数flagを改造しましょう。
flag = { "buy_signal":0, "sell_signal":0, "order":{ "exist" : False, "side" : "", "count" : 0 }, "position":{ "exist" : False, "side" : "" } }
buy_signalとsell_signalは、それぞれ「何本連続で陽線が続いているか?」「何本連続で陰線が続いているか?」を管理するフラッグです。この値が「3」になったらその方向(BUY/SELL)でエントリーします。
orderは「現在、サーバーに未約定の注文があるか?」を管理するためのフラッグです。今回は、ロング・ショートの両側を考えるため、orderというフラッグの中に、さらにexistとsideという2つの要素を作ります。existが注文の有無、sideが買いか売りか、を管理するための要素です。
countは「一定時間で注文が約定しなければキャンセルする」というロジックのために使います。メイン処理では全体のループを10秒間隔で回すため、例えば、「1分間(60秒)約定しなければキャンセルする」というロジックを組むために、このcountをループごとに1ずつ増やし、6を超えたらオーダーキャンセルを出すようにします。
positionは、「現在、ポジション(建玉)があるかどうか?」を管理するためのフラッグです。こちらも「買いポジション」「売りポジション」の両方があることを考慮して、「side」という要素を持つ入れ子構造に修正しておきます。
ローソク足の条件を判定する関数
def check_candle( data,side ): 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"] < data["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
買いシグナルと売りシグナルの両方に対応できるように、「side」という引数を受けとり、それによってシグナルを出し分けるように改良しました。
買い注文を出す関数
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["order"]["exist"] = True flag["order"]["side"] = "BUY"
前回の記事で「#ここに買い注文のコードを入れる」と書いておいた部分に上記のコードを入れます。
bitflyer.create_order()と書けば、CCXT経由で簡単にbitflyerに注文を出すことができます。このコードの書き方は、「CCXTでBitflyerに注文を出す方法」の記事ですでに解説しましたので、そちらを参考にしてください。
同じように売りシグナルを判断して売り注文を出す関数も作っています。こちらは買い注文を単に反対にしただけなので、解説は省略します。
未約定の注文を管理する関数
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( orders,flag ) else: print("注文が遅延しているようです") return flag
前回の記事で、「# 注文状況を確認して通っていたら以下を実行」「# 一定時間で注文が通っていなければキャンセルする」と書いておいた部分に、上記のコードを入れていきます。
まず get_getpositions()で建玉の一覧を取得し、さらにfetch_open_orders()で未約定の注文一覧を取得します。
もし既に建玉があれば、指値注文が通ったと判断できますので「注文が約定しました!」と表示します。また、flag["order"]["exist"](未約定の注文の有無を管理するflag)をFalseに更新し、flag["orders"]["exist"](ポジションの有無を管理するflag)をTrueに更新します。
flag["order"]["exist"] = False
flag["position"]["exist"] = True
flag["position"]["side"] = flag["order"]["side"]
もしまだ建玉がなければ、orders(未約定の注文一覧)を確認します。
未約定の注文があれば、「まだ未約定の注文があります」と表示します。そして先ほど説明したように、flag["order"]["count"]というフラッグにカウント1を追加します。全体の処理はWhile文で10秒おきに回っているので、このフラッグのカウントが「6」以上になれば、「1分間約定しなかった」と判断して注文をキャンセルします。
cancel_order() というのは、今回の記事で新たに作成した注文キャンセルのための関数です。ここにキャンセル処理をそのまま書いてもいいのですが、少しだけ長い処理になるので、読みやすいように別の関数として外に出すことにしました。内容は後ほど説明します。
Bitflyerの遅延対策
最後のelse:の部分は、エントリー注文を出したものの、「建玉の一覧」にも「未約定注文の一覧」にも、どちらにも情報が存在しないパターンを捕まえています。
else: print("注文が遅延しているようです")
これは注文のリクエスト(通信)は正常に受理されたものの、サーバー側への注文の反映が「遅延」しているときにおこる現象です。Bitflyerでは1分以上注文の反映が遅延することもあるので、今回のように「建玉一覧」と「未約定注文一覧」の両方を取得しなければ、現在がどういう状態にあるのか判別することができません。
▽ 「遅延」と「未約定」の違いの例
※ 「遅延」はまだサーバー側に注文が認識(反映)されていない状態。「未約定」は注文が反映されてまだ約定していない状態。
注文をキャンセルする関数
# 注文をキャンセルする関数 def cancel_order( orders,flag ): for o in orders: bitflyer.cancel_order( symbol = "BTC/JPY", id = o["id"], params = { "product_code" : "FX_BTC_JPY" }) print("約定していない注文をキャンセルしました") flag["order"]["count"] = 0 flag["order"]["exist"] = False
さきほどの続きで、「未約定の注文一覧」をキャンセルするための関数です。CCXTライブラリを使えば、bitflyer.cancel_order()で未約定の注文をキャンセルすることができます。
注文のキャンセルを出した時点で、「約定しない注文をキャンセルしました」と表示し、flag["order"]["exist"](未約定の注文を管理するフラッグ)をFalseに戻します。またカウント用のフラッグも0に戻しておきます。
ただしBitflyerの注文キャンセルのAPIを使った場合でも、通信のすれ違いで注文が約定してしまうことがあります。そのため、未約定の注文をキャンセルした後に、20秒間待機し、続けて「ポジションが残ってないかどうか?」を念のために確認します。それが以下の部分です。
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
bitflyer.private_get_getpositions()で、現在のポジション(建玉)の状況を確認します。もし空のデータが返ってきたら、未約定の注文のキャンセルに成功したということです。この場合は、「現在、未決済の建玉はありません」と表示します。
一方、1つでもデータが返ってきた場合は、「未約定の注文をキャンセルするつもり」だったものの、「スレ違いで注文が約定してしまいポジションを持ってしまった」という状況になります。そのため、flag["position"]["exist"](ポジションの有無を管理するフラッグ)をTrueに更新します。
▽ 注文キャンセルしたもののスレ違いで約定して建玉が残ってる例
ここでポジション用のフラッグをTrueに更新しないと、BOTを動かしているうちに、どんどん意図しない建玉が溜まっていくので注意!
手仕舞いのための関数
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
flag["position"](ポジションの保有の有無を管理するフラッグ)を使って、買いポジションであれば売り決済、売りポジションであれば買い決済の成行注文を出します。
成行注文を出した後は、flag["position"]["exist"]をFlaseに更新しておきます。これでまたエントリーができるようになります。
メインのループ処理
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 flag["order"]["exist"]: flag = check_order( flag )
まず最初に最優先で「未約定の注文がないか?」をフラッグで確認します。もしあれば、先ほどの「未約定の注文を管理する関数」を呼びます。
メイン処理のWhile文は10秒間隔で回しているので、10秒おきに「注文が実行されたかどうか?」を確認することになります。
data = get_price(60,-2) if data["close_time"] != last_data["close_time"]: print_price( data )
次にCryptowatchから1分足(60秒足)のデータを取得し、最新のローソク足のOHLC(日時・始値・高値・安値・終値)を取り出します。
データを取得するのは10秒おきですが、ローソク足に更新があるのは(1分足ベースであれば)1分おきです。なので、日時データに更新があった場合のみ、その始値・終値などのデータを表示し、次の処理に進むようにします。
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 )
ポジション(建玉)の有無を確認し、もしポジションがあれば、さきほどのclose_position()関数を呼んで、手仕舞いの条件を満たしているかどうかを確認します。
ポジションを持っていない場合は、買いシグナル(buy_signal)、売りシグナル(sell_signal)を順番に確認します。今回の自動売買ロジックでは、売りシグナルと買いシグナルが両方点灯することはありえないので、単に順番に実行するだけで構いません。
実行結果
こちらを実行すると以下のような感じになります。
20時14分頃に3本連続で陰線のシグナルが出たのでショートでエントリーし、次の足ですぐに終値が上がってしまったので損切の決済をしたようです。実際にBitflyerの管理画面にも売買の履歴が入っています。
チャート上では以下のタイミングでエントリーしています。
今回は、挙動を確認するためのテストなので、実体率などの追加条件(increase_rate や realbody_rate の条件)は緩和し、単純に陰線/陽線が3本続いたらそれを売り/買いのシグナルとして動かしました。
左の矢印で3本連続の陰線による売りシグナルが点灯し「売り注文」、右の矢印で前回の終値を上回って引けたので「決済」しています。とりあえず、意図した通りの挙動はしているようです。
例外処理について
今回の記事で、ようやく自動で売買判断をしながら動き続けるBOTを作ることができました!しかし実はあと1つだけ学ばなければならないことがあります。それが例外処理です。
上記の自動売買BOTは、そのままでも動くことは動きます。pythonのプログラムコードや構文には問題はありません。しかし、このままだと、1度でもBitflyerやCryptowatchのサーバーからエラーが返ってきたら、そこで止まってしまいます。
特にBitflyerのサーバーは、混雑時などに頻繁に「500 Internal Server Error」を返します。これに対する処理を入れておかないと、そこでBOTがエラーを吐いて中断してしまいます。このような対策のことを「例外処理」といいます。
次の記事では、相手のサーバーが応答しなくてもエラーを吐いて止まることなく、何日でも安定して動き続けるための「例外処理」の書き方について学びます!