PythonでBOTを作ってBitflyerFXで運用していると、以下のようなトラブルにときどき遭遇します。いずれも「対策をしないと気付かないうちに意図しない建玉が残る」という種類のトラブルです。
(1)注文反映の遅延
(2)キャンセルとスレ違いでの約定
(3)注文が消える・約定拒否
(4)二重注文(ダブル約定)
定義
これらの用語はよく聞くものの、それぞれの具体的な定義がなかなか調べてもわからなかったので、以下、私が理解している範囲のそれぞれの定義を記載しておきます。
問題 | 意味 |
---|---|
注文遅延 | API経由で指値注文を出し、それが正常にサーバー側に受け取られたが、その情報が「未約定の注文一覧」「建玉の一覧」のどちらにも反映されず、数十秒~数分間、宙に浮いている状態 |
スレ違い | API経由で指値注文のキャンセルを出し、それが正常にサーバー側に受け取られたが、実際はキャンセルに成功しておらず、スレ違いで注文が約定して建玉に残っている状態 |
注文が消える | API経由で注文を出し、それが正常にサーバー側に受け取られたにも関わらず、出したはずの注文がどこかに消えてしまう状態 |
二重注文 | API経由で注文を出したものの、それがタイムアウト等の通信エラーになり、例外処理でリトライをしたところ、実はさっきの注文が正常に受理されていたため、二重に注文が通ってしまう状態 |
「約定拒否」というのが何かよくわからず、個人的には「注文が消える」と類似の問題と理解しているのですが、もし違ったら教えてください。
問題点
これらの共通の問題は、通信エラーが判定条件にならないことです。
通信エラーが出てくれれば、単に例外処理をすればいいだけですが、通信エラーが出ていないのに注文が反映されない場合(または通信エラーが出たのに注文が執行された場合)、BOTの挙動がおかしくなる原因になります。
遅延の対策
注文の遅延とは、正常に指値注文のリクエストが受理されたのに、「注文一覧」か「建玉一覧」に注文が反映されない状態です。
これはもし「信じて待っていればいつか必ず反映される」という前提であれば、ただひたすら待てばいいことになります。大抵の場合は実際それで問題ありません。基本的な遅延の対策コードは以下に紹介しています。
消えた場合の対策
しかしごく稀に「消えてしまう」という厄介なケースがあります。この場合、信じて待っていると永久に「反映待ち」の状態が続きます。そのため、少し気持ち悪いですが、一定の間隔でカウンターを回してどこかで諦めて「消えた」と判定しなければなりません。
例えば、以下のような感じです。
# サーバーの注文を確認する関数 def bitflyer_check_order( flag ): try: 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" }) except ccxt.BaseError as e: print("BitflyerのAPIで問題発生 : ",e) else: # 注文が約定していた場合 if position: print("注文が約定しました!") flag["order"]["exist"] = False flag["order"]["count"] = 0 flag["order"]["latency"] = 0 # 平均建値やサイズを取得 time.sleep(5) price,size,side = check_bf_positions() flag["position"]["exist"] = True flag["position"]["side"] = side flag["position"]["price"] = price flag["position"]["lot"] = size return flag else: # 注文一覧に残っている場合 if orders: print("まだ未約定の注文があります") for o in orders: print( o["id"] ) flag["order"]["count"] += 1 # 約定しない注文をキャンセルする if flag["order"]["count"] > 12: flag = bitflyer_cancel_order( orders,flag ) else: # 遅延対策 print("注文が遅延しているようです") flag["order"]["latency"] += 1 # ずっと遅延が続いて消えた疑いがある場合 if flag["order"]["latency"] > 12: print("注文が消えてしまったようです") flag["order"]["exist"] = False flag["order"]["count"] = 0 flag["order"]["latency"] = 0 return flag
通常は多くても5~10行、遅延が続いたあとに注文一覧に反映されます。しかし4月後半に注文が消えるという問題が何度か出たことがあったと思います。
記事にしようと思いつつ時間が経ってしまい、少し前のテストなのでうろ覚えですが、たしかこのテストのときは注文が消えたと思います。「注文が消える」ケースはそう頻繁に再現できないので、なかなか対策の方法が難しいです。
▽ 注文が消えた場合
二重注文の対策
二重注文は、タイムアウトやサーバーエラーなどの通信エラーが出たにも関わらず、実際には注文が正常に受理されていたパターンです。それに気づかずに、例外処理をして注文を再送すると、ダブルで約定してしまいます。
これは注文を連打せずに、ある程度、しっかり間隔をあけて間にポジションチェックを入れれば回避できる気もします。しかし前述のように、遅延が酷いときは1~2分待ったくらいでは確信を持って判断できないときもあります。サーバーが混雑していないときまで毎回1~2分待つのも非効率です。
タイムアウトエラーの対策
1つの対策としては、接続のタイムアウトの判定時間を長めに設定しておくという方法があります。
CCXTライブラリ経由でBitflyerに注文を出す場合、初期設定でのタイムアウトの判定は10秒に設定されています。つまり10秒待って応答がなければエラーとして扱われるわけですが、以下のように記述することで、その待機時間を伸ばすことができます。
bitflyer = ccxt.bitflyer() bitflyer.apiKey = '' bitflyer.secret = '' bitflyer.timeout = 30000 # 通信のタイムアウト時間の設定
上記の例では、タイムアウトエラーの判定時間を30秒にしています。抜本的な解決方法ではありませんが、個人的には、上記の設定でかなりエラー頻度が減ったように思います。
定期的に建玉をチェックする
結局のところ、あまりスマートではないですが、定期的に「意図しない建玉」が残っていないかをチェックするしかないかもしれません。現在の建玉を取得する関数の作り方は以下を参考にしてください。
例えば、以下のような関数を全体ループの中に入れることを考えます。
def find_unexpected_pos( flag ): if flag["position"]["exist"] == True: return flag count = 0 while True: price,size,side = bitflyer_check_positions() if size == 0: return flag print("把握していないポジションが見つかりました") print("反映の遅延でないことを確認するため様子を見ています") count += 1 if count > 5: # ポジションの復活 print("把握していないポジションが見つかったためポジションを復活させます") flag["position"]["exist"] = True flag["position"]["side"] = side flag["position"]["lot"] = size flag["position"]["price"] = price return flag time.sleep(30)
※ 例外処理は check_bf_position() の中に入っています。
このような処理をする場合は、今度は逆に「成行注文で決済したはずなのに遅延で建玉が残っている場合」に騙されないように注意しなければなりません。そのため、成行注文を出したときに、確実にすべて執行されたところまで見届けることが重要です。
またすべての成行注文が執行されたとしても、決済後も「建玉一覧」にポジションが解消されたことへの反映が遅延する可能性もあるため、上記のコードでは5回ほどカウンターを回して確認しています。
もっと良い方法も模索中です。