前回の記事に引き続き、今回も仮想通貨botの開発状況をまとめていきます。
倒れても倒れても立ち上がるゾンビbotになっていた。損失極大化する前に通知で気づいたから良かったけど、怖すぎ。
— よだか(夜鷹/yodaka) (@yodakablog) May 14, 2025
§0. はじめに
この記事は、ひとことで言えば、**「DDガードを実装したのにBotが止まらなかった話」**です。
自分でも、最初は信じられませんでした。Botは異常を検知して exit()
していた。SlackにはDDガード発動の通知が飛んでいた。なのに、Botは止まっていなかったのです。
繰り返される通知、再起動されるBot、蓄積していく損失。
「DDガードさえあれば安心」という考えは、甘かった。
この体験を通じて私が学んだのは、表面的なロジック実装と、システム全体としての設計との間には、決定的な“ズレ”が生じ得るということでした。
Bot単体での正しさでは不十分で、そのBotが動いている“環境全体”の設計と整合していなければ、安全な運用は成り立たないのです。
📌 この記事は、次のような方に向けて書いています。
- 仮想通貨Botを本番環境で運用している方
- DockerやSlack通知など、外部サービスと連携してBotを構成している方
- 自作Botの挙動が「なぜか想定どおりに動かない」と悩んだことのある方
- ロジックは書けるけど、設計上のミスに気づきづらいと感じている方
この失敗は、私自身にとって非常に大きな学びとなりました。そして、これからBot開発や運用に取り組もうとしている人にとっても、確実に役立つ実例になるはずです。
どうかこの記録が、あなたのBotが「止まれる」ようにする一助となりますように。
§1. 現象とその背景
違和感のきっかけは、Slackの通知を眺めていた時でした。
いつものようにMMBotを稼働させ、本番ネットでの挙動をモニターしていた。すでに最大ドローダウン(DD)ガードも実装済み。実弾も極小ロットに抑えており、「何か起きても止まる構造」はできあがっている——はずだったのに。
ところがSlackには、次のようなメッセージが1分間に何十通も並んでいました。
:fire: MMBot DD guard hit Realised +0.0000 Unreal -6.7988 :fire: MMBot DD guard hit Realised +0.0000 Unreal -8.5126 :fire: MMBot DD guard hit Realised +0.0000 Unreal -9.3982 ...
まるでBotが自分から崖に突っ込んでいくような有り様。
——止まらない。なぜ?
私は、即座にターミナルでコンテナを強制停止。残っていたポジションも手動で成行精算。
そしてログを遡った私は、こうつぶやいた。
「おい……ちゃんと exit()
してるじゃん……」
だがこのとき、私は重大な勘違いをしていたのです。
§2. 原因の分析と設計の落とし穴
MMBotのコードでは、最大ドローダウン(unreal + realised)が閾値を超えると exit()
が呼ばれるようにしていました。
if dd <= -max_loss_usd: logger.error("max_loss hit → exit") sys.exit(0)
Botは「終わるべきときにちゃんと終わっていた」。それ自体は間違っていなかった。
だが、「exit(0)」だった。
この 0
は、システム的には「正常終了」を意味する。問題なく処理を終えた、という意思表示です。
しかし、私は exit(0)
を「異常停止(DD guard)」のつもりで書いていた。そこに、**Botとシステム(Docker)の“解釈のズレ”**があったのです。
Docker の docker-compose.yml
には、こう書かれていた。
restart: unless-stopped
これは、「自分で止めない限り、Botが終了しても自動で再起動してよい」という指示です。
そしてその再起動のトリガーは、「異常終了ではない」という条件——つまり exit(0)
のような正常終了だったのです。
結果としてこうなった:
- Bot:DD guard検知 →
exit(0)
- Docker:「おっ、正常に終わったね。じゃあ再起動するね」
- 再起動されたBot:またDD guardに引っかかって
exit(0)
- Docker:「また正常に終わった!再起動だ!」
……地獄の無限ループの完成である。
このとき私は初めて、「exit()
は設計の一部なんだ」と理解しました。
§3. 実施した修正と対策
原因が分かれば、対処方法はシンプル。
設計の勘違いに気づいた私は、即座に再発防止のための修正を開始。以下が、今回実施した修正とそれぞれの狙いです。
✅ exit(0)
→ exit(1)
に修正
sys.exit(1) # 異常終了を明示
これで Docker は「このBot、エラーで落ちたな」と判断し、自動再起動を行わなくなります。
一見小さな変更だが、これが最重要ポイント。
✅ .env
パラメータの見直し
パラメータ | Before | After | 理由 |
---|---|---|---|
LOSS | 6.0 | 3.0 | DD guard発動の敷居を下げ、安全性を高める |
S_ENTRY | 8e-7 | 1e-6 | 成立しやすさと安定性のバランスを取る |
MAX_NOTIONAL | 250 | 200 | 最大建玉を制限しリスク縮小 |
INTERVAL | 4 | 6 | 過剰発注を抑えて冷却時間を増やす |
✅ Slack通知の過剰発火を抑える設計
Slack通知を発火させる際、再起動ループで何度も送らないように warn_sent
/ summary_sent
のフラグ管理を強化。
また、今後はSlack接続エラーが起きてもBotが落ちないように notify_slack()
に try/except
を導入予定。
✅ Docker 再ビルド&テスト再開
修正後、Dockerコンテナをビルドし直し、docker compose logs
で正常な起動・DD guardの発動・停止を確認済み。
この時点で、ようやくBotが再起動せず、想定どおりに止まる構造が完成しました。
§4. 得られた知見と教訓
🧠 1. exit()
は「Botの終了」ではなく「システムへのメッセージ」
コード的には「終わった」つもりでも、その終了の“意味”が外部からどう見えるかが肝要でした。
Bot単体の完結性ではなく、ホスト側(Dockerや監視システム)との設計整合性が必要です。
⚠️ 2. 「止めたつもり」は通用しない
止めたのに動いてる──これはよくあるパターン。
- OSが再起動してた
- supervisorやpm2が勝手に復活させてた
- exit codeが0で監視ツールが「異常なし」と判断してた
いずれにせよ、Bot自身に“止まれる構造”がないと、止まらない。
🔄 3. 設計は「外部との相互理解」で成り立つ
Dockerの restart
設定、Slackの疎通状態、env → config → CLI
の引き継ぎ――
各レイヤーでの振る舞いを想定して設計することが、Bot開発の本質でした。
アルゴリズムの工夫だけでは不十分で、周囲の挙動を踏まえた構造設計こそが、実運用の勝敗を分ける。
§5. 今後の改善計画
- ✅ Slack通知に
try/except
を入れて通信エラーでも落ちないようにする - ✅ ターミナル用のリアルタイムPNL表示ツールを導入する
- ✅
adaptive_s_entry
を実装し、市場状況に応じて自動でスプレッド条件を最適化 - ✅ PnLをサイクル単位で集計・可視化して「勝てるパターン/負けるパターン」を分析
これらを通して、「事故が起きても止まるBot」から、「勝ち筋を自ら学習していくBot」への進化を目指します。
§6. おわりに
Bot開発者にとって、「DD guardの実装」はひとつの安心材料です。
けれども、それが実際に稼働環境でどう振る舞うかまで想像できていなければ、安心は幻想に過ぎません。
今回の経験から、私は**「止め方」を知らないBotは、どれだけ高性能でも危険だ**という事実を痛感しました。
もしあなたのBotが同じように「止まれる構造」を持っていないなら――
ぜひ一度、exit()
の意味と、再起動ポリシーの設計を見直してみてください。
「止まるべきときに止まる」
その一点を確保するだけで、Bot開発は一段階、次のフェーズに進めます。
📎(おまけ)この記事のポイントまとめ
exit(0)
は「正常終了」 → Dockerにとっては再起動対象restart: unless-stopped
は、exit(0)
でも容赦なくBotを再起動- DD guardは「止める条件」だけでなく、「どう止まるか」の設計が必須
- 本当に安全なBotは、「自分で止まれる構造」を持っている