
自動売買ボットのログ設計とは?障害調査で困らないログの残し方まとめ
※本記事にはアフィリエイト広告(プロモーション)が含まれています。
ボットが止まっていた、想定外の注文を出していた――そんなとき、ログを見返しても「何が起きたのか」が分からず途方に暮れた経験はないでしょうか。ログは、動いているときには気にされませんが、トラブルが起きた瞬間に唯一の手がかりになります。この記事では、自動売買ボットのログをどう設計すればよいか、一般的な考え方として整理します。
なぜログ設計が重要なのか
ボットは基本的に無人で動き続けます。開発者がその場で異常に気づけるとは限らず、後から振り返って「あのとき何が起きていたか」を再現できるかどうかは、ログの質にかかっています。
ボット開発でよくある失敗トップ10の記事でも触れているとおり、「ログ設計が甘く障害調査ができない」ことは典型的な失敗の一つです。また、休眠バグに気づけなかった話のように、ログ自体は正常に出力されていても「何を記録するか」の設計が甘いと、異常そのものに気づけないケースもあります。ログは「エラーが出たら残す」だけでなく、「正常に動いている」ことも含めて設計する必要があります。
ログレベルの使い分け
多くのロギングライブラリには、重要度に応じたレベルが用意されています。Pythonの標準ライブラリ logging を例にすると、以下のような使い分けが一般的です。
| レベル | 用途の例 |
|---|---|
| DEBUG | APIリクエストの詳細、計算途中の値など、通常は見ない詳細情報 |
| INFO | 注文の発注・約定、ボットの起動・停止など、正常な動作の記録 |
| WARNING | リトライが発生した、想定外だが致命的ではない状況 |
| ERROR | 注文が失敗した、想定していない例外が発生したなど |
| CRITICAL | ボットが停止せざるを得ない致命的な状況 |
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
)
logger = logging.getLogger('trading_bot')
logger.info('bot started: symbol=BTC/USDT strategy=grid')
logger.warning('order retry: attempt=2 reason=timeout')
logger.error('order failed: symbol=BTC/USDT side=buy error=%s', str(exc))
本番運用では INFO 以上を常時出力し、調査が必要なときだけ DEBUG に切り替えられるようにしておくと、平常時のログ量を抑えつつ詳細な調査もできる構成になります。
何を記録すべきか
「エラーメッセージだけ」では、後から状況を再現できないことが多々あります。最低限、次の情報をセットで残すことが望ましいとされています。
- タイムスタンプ: いつ起きたか(タイムゾーンも明記する)
- 銘柄・戦略名: 複数銘柄・複数戦略を動かしている場合は必須
- 注文の詳細: 発注価格・数量・注文ID・注文タイプ
- APIのレスポンス: 取引所から返ってきたエラーコードやメッセージ
- 判断に使った値: シグナルの根拠となった指標値やポジション状態
たとえば発注ロジックでは、次のように「なぜその判断をしたか」まで含めてログに残すと、後から見返したときの再現性が大きく変わります。
logger.info(
'entry signal: symbol=%s rsi=%.2f ma_short=%.2f ma_long=%.2f decision=%s',
symbol, rsi_value, ma_short, ma_long, decision,
)
エントリーの根拠となったテクニカル指標の計算方法については、テクニカル指標をPythonで計算する記事でまとめています。
「取引がない」ことも記録する
ログ設計で見落とされがちなのが、「何も起きなかったこと」の記録です。エラーは出ていないのに取引が発生していない、という状態は正常にも異常にも見えます。定期的に「ヘルスチェック」的なログ(生存確認・現在のポジション状態など)を出力しておくと、「ログはあるのに何も記録されていない」という空白期間を防げます。
# 1時間ごとなど、定期的に状態をログに残す
logger.info(
'heartbeat: position=%s balance=%.2f last_trade=%s',
position_state, balance, last_trade_time,
)
異常を通知の形でリアルタイムに検知したい場合は、Discord通知ボットの作り方も参考にしてください。ログとして残すことと、即座に気づけるようにすることは別の仕組みとして両方用意しておくと安心です。
ログの出力先とローテーション
ログをファイルに出力する場合、放置すると際限なく肥大化し、ディスク容量を圧迫します。Pythonでは RotatingFileHandler や TimedRotatingFileHandler を使うと、サイズや日付を基準に自動でファイルを分割・削除できます。
from logging.handlers import TimedRotatingFileHandler
handler = TimedRotatingFileHandler(
'bot.log', when='midnight', backupCount=14,
)
handler.setFormatter(
logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')
)
logger.addHandler(handler)
上記の例では、日次でログファイルを切り替え、直近14日分だけを保持します。何日分残すかは、調査に必要な期間とディスク容量のバランスで決めるとよいでしょう。VPSで24時間運用している場合は特にディスク容量への配慮が必要です。運用環境についてはVPSでの24時間運用の記事やVPS徹底比較の記事も参考になります。
構造化ログという選択肢
文字列を並べたログは人間には読みやすい一方、後から集計・検索するには不向きです。JSON形式などで構造化しておくと、特定の銘柄・エラーコードだけを抽出する、といった機械的な処理がしやすくなります。
import json
import logging
def log_json(logger, level, event, **fields):
logger.log(level, json.dumps({'event': event, **fields}, default=str))
log_json(logger, logging.INFO, 'order_filled',
symbol='BTC/USDT', price=6000000, amount=0.001)
小規模なボットであれば通常のテキストログで十分なことも多く、構造化ログは「ログの量が増えて目視での確認が追いつかなくなってきた」段階で検討すれば十分です。最初から過剰に作り込む必要はありません。
APIエラーとログの関係
取引所APIのエラーは、ネットワーク起因のものからレートリミット超過まで多岐にわたります。エラーの種類ごとにログの粒度を変える、たとえば一時的なネットワークエラーはWARNINGで、注文拒否のような致命的なエラーはERRORで分ける、といった設計にしておくと、ログを見返したときに優先度の高い問題をすぐに見つけられます。ccxtで発生しやすい代表的なエラーとその対処法はccxtのエラー対処法の記事にまとめています。
まとめ
- ログは平常時には見られないが、障害発生時には唯一の手がかりになる
- ログレベル(DEBUG/INFO/WARNING/ERROR/CRITICAL)を用途に応じて使い分ける
- タイムスタンプ・銘柄・注文詳細・判断根拠をセットで記録し、再現性を高める
- 「取引がないこと」も定期的なハートビートログとして残しておくと、異常の見逃しを防げる
- ファイルへの出力はローテーションを設定し、ディスク容量を圧迫しないようにする
- ログの量が増えてきたら、構造化ログへの移行を検討する
ログ設計は地味な作業に見えますが、いざというときの調査時間を大きく左右します。まずは「発注」「エラー」「定期的な生存確認」の3点だけでも、レベル分けと必要な項目をそろえてログに残すところから始めてみてください。