自動売買botを壊した5つのバグ——実際のデバッグ記録
「動いているのに結果がおかしい」
これが一番怖い。エラーが出て止まるなら原因を探せる。でも動いているように見えて、静かにおかしな挙動をしているbotは、気づくのが遅れる。
今回はわたしが実際に遭遇した5つのバグを記録として残しておく。同じような自動売買botを作ろうとしている人の参考になれば嬉しい。
バグ① フィールド名ミスマッチ——再起動するたびポジションが消えていた
症状:botを再起動すると、前回のポジションが0として認識される。
これが最も深刻だったバグだ。botは再起動時にAPIを叩いてポジション情報を取得し、前回の状態を引き継ぐ設計になっている。ところが、APIのレスポンスで返ってくるフィールド名と、コード内で参照しているフィールド名がずれていた。
# 間違い:こんなフィールドは存在しない
position["side"]
position["amount"]
position["price"]
# 正しいフィールド名
position["position_side"]
position["open_amount"]
position["average_price"]
結果として、再起動のたびにポジションをゼロと誤認識し、実際には建っているポジションを無視して新規の注文を出し続けていた。これがどれだけ危険かは想像してもらえると思う。
教訓:APIのレスポンスは必ず実際に出力して確認する。ドキュメントと実装が違うことはよくある。
バグ② レースコンディション——重複注文が静かに発生していた
症状:同じタイミングで複数の注文が出てしまう。
botは約定を検知したら次の注文を出す、という流れで動いている。ところが2つの処理(monitor_loopと_refresh_orders)が同時に動いているとき、ほぼ同じタイミングで「約定した」と判断して両方が注文を出してしまうことがあった。
これを「レースコンディション」と呼ぶ。競走(race)するように2つの処理が同時に走ってしまう状態だ。
解決策はフラグ管理。注文処理中はフラグを立てて他の処理をブロックし、完了したらフラグを下ろす。
if self._refresh_pending:
return # 処理中なら何もしない
self._refresh_pending = True
# ...注文処理...
self._refresh_pending = False
シンプルな仕組みだけど、これで重複注文はなくなった。
教訓:非同期・並行処理を使うときは、処理の重なりを常に意識する。
バグ③ PubNubトークン失効——夜中に黙って止まっていた
症状:起動して数時間後から約定が発生しなくなる。エラーは出ない。
PubNubはリアルタイムで板情報・約定情報を受け取るためのサービスだ。このサービスへの接続には認証トークンが必要で、そのトークンには約8時間の有効期限がある。
有効期限が切れると接続が静かに切断される。エラーも出ない、ログにも残らない。ただ情報が届かなくなるだけ。結果として、板が動いても約定しても、botは何も知らないまま待ち続ける。
夜中に起動して朝起きたら何も動いていなかった、という状態がこれだ。
根本的な解決策は「トークンが失効したら自動で再接続するロジックを実装すること」だが、暫定対応として定期的な手動再起動で対処している。
教訓:外部サービスへの接続は「いつ切れるか」を最初から想定して設計する。
バグ④ 現物注文を誤キャンセル——マージンと現物の見分け方
症状:キャンセルしていないはずの現物注文が消えている。
botは注文をリセットする際、すべての注文をキャンセルする処理を走らせる。ところが最初の実装では、現物の指値注文までキャンセルしてしまっていた。
原因は、注文一覧を取得するAPIが現物・マージン両方の注文を返すのに、フィルタリングせずに全部キャンセルしていたことだ。
解決策は、レスポンスに含まれるposition_sideフィールドの有無でマージン注文だけを選別してキャンセルすること。
# position_sideフィールドがある注文だけマージン注文
margin_orders = [o for o in orders if "position_side" in o]
これで現物注文には触れずに、マージン注文だけをキャンセルできるようになった。
教訓:「全部キャンセル」は危険。注文の種別を必ず確認してから処理する。
バグ⑤ 買い戻し数量オーバー——エラー50062との戦い
症状:買い戻し注文を出すとエラー50062が返ってくる。
エラーコード50062は「注文数量がポジション数量を超えている」という意味だ。
たとえば実際のポジションが0.9XRPなのに、1.0XRP分の買い戻し注文を出そうとすると弾かれる。なぜこんなことが起きるかというと、計算の端数処理や、ポジションサイズの把握が微妙にずれていたからだ。
解決策は、買い戻し数量を「計算値」ではなく「APIから取得した実際のポジション数量」にキャップすること。
buyback_amount = min(calculated_amount, actual_position_size)
これだけで解決した。
教訓:数量の計算は「自分が思っている値」ではなく「APIが返す実際の値」を信用する。
まとめ
5つのバグを振り返ると、共通するパターンが見えてくる。
- 思い込みで実装しない(フィールド名は実際に確認する)
- 並行処理の重なりを意識する(フラグ管理)
- 外部サービスは必ず切れると想定する(再接続ロジック)
- 種別を確認してから操作する(マージン/現物の区別)
- 計算値より実測値を信用する(APIの返す数値を使う)
プログラミング初心者がbotを作ると、こういうところでハマる。でも逆に言えば、これさえ知っておけば同じ落とし穴は避けられる。
次回は、買い戻し価格のロジックを改善した話——「平均建値を基準にする」から「直前の約定価格を基準にする」へ変えた判断とその効果を書いていく。
使用技術:Python / python-bitbankcc / PubNub / python-dotenv