Bot 環境構築・インフラ 開発ログ

🛠️開発記録#262(2025/7/12)ゼロから学ぶ複数コンテナBot開発──Recorder/Trader/Analyzer分離で“勝てる基盤”を作るまで

はじめに──なぜコンテナ分離が必要か

複数の取引所 API をまたぎ、秒単位でパラメータを変えながら実験を回す──。
そんな仮想通貨 Bot 開発では「安全に失敗しながら、すぐ本番にも出せる」環境が欠かせません。
そこで鍵になるのが Recorder / Trader / Analyzer をコンテナ単位で分離するアーキテクチャです。

コンテナ分離の本質的な利点は大きく二つ。

  1. 障害ドメインの切り分け
    • Recorder が落ちても Trader は生き残り、未決済ポジションを安全に処理できます。
  2. ライフサイクルの独立
    • データ収集・戦略実行・分析のそれぞれを“別テンポ”で更新できるため、開発・運用のフリクションを大幅に削減できます。

コンテナを分けるだけで「いつでも壊せる/すぐ戻せる」リスク耐性を手に入れ、
最終的には 収益向上よりも高い“生存率” を実現する土台となります。

1.1 テストネットとメインネットを安全に往復する理由

  • 資金リスクの最小化
    いきなり実弾を撃てばバグ=損失です。まずはテストネットで新機能を検証し、
    安全を確認してからメインネットに切り替えるステップは欠かせません。
  • API 差異の早期検知
    Exchange によってはエンドポイント仕様やレスポンスが微妙に異なる場合があります。
    テストネットで挙動をログに残しておけば、本番で想定外のレスポンスに慌てることがありません。
  • CI/CD パイプラインの練習場
    テストネットを本番同等のデプロイ先として扱うことで、
    デプロイ・ロールバック手順を“安全に”自動化し、信頼度を高められます。

1.2 “勝てるBot”に求められる高速リリース・低リスク運用

  • アルファは劣化が速い
    市場の歪みは数週間で消えることも珍しくありません。
    アイデア→コード→テスト→本番投入を「日単位」→「時間単位」に短縮できるかが勝負の分かれ目。
  • 失敗コストの最小化
    勝てるロジックを探す過程では“外れる”試行が大量に出ます。
    外れが即・大損にならないよう、Bot の権限や注文量を限定し、コンテナごとに隔離しておくことが不可欠です。
  • ロールバックの自動化
    マイナス PnL や例外スパイクなど異常指標を検知した瞬間に Bot だけ停止できる設計が、
    トータルリスクを大幅に下げます。コンテナ分離に加え、NATS や Supervisor でプロセスを監視すると盤石です。

3つのコアサービスを見極める

Bot システムを「データ収集(Recorder)/戦略実行(Trader)/分析とチューニング(Analyzer)」の
三層に分けると、責務が明確になり運用・拡張が一気に楽になります。

2.1 Recorder:市場データの取り込みと前処理

  • 役割
    • 取引所 WebSocket から板情報・清算情報などをリアルタイム収集
    • フィルタリング・圧縮して Parquet や DuckDB に保存
    • 清算イベントを NATS へ publish し、Trader へ通知
  • ポイント
    • 接続安定性:指数バックオフ再接続&ネイティブ ping で落ちないソケットを維持
    • 軽量フォーマット:Parquet + Snappy 圧縮で I/O とストレージを最適化
    • メトリクス:受信レート・ドロップ率を Prometheus にエクスポートし、詰まりを速攻検知

2.2 Trader:戦略ロジックと約定実行の分離

  • 役割
    • NATS から清算イベントを subscribe
    • 戦略ロジック(例:逆張り指値、PnL 監視、連続ヒット制限)を実行
    • 約定後は結果を NATS や Slack に publish し、Analyzer へフィードバック
  • ポイント
    • 秘密鍵の隔離:Trader コンテナだけに鍵ボリュームをマウントし、他サービスから遮断
    • ロジックホットスワップ:CLI フラグや gRPC でパラメータを外出し → 再デプロイ不要でチューニング
    • フェイルセーフ:異常スプレッド・API エラー連発で自動ポジション解放&Sleep

2.3 Analyzer:夜間バッチとフィードバックループ

  • 役割
    • 日次・週次で Parquet を集計し、勝率・シャープレシオ・スリッページを計測
    • チューニング推奨値やヒートマップを生成し Slack/Notion にレポート
    • JetStream の完了通知 record.done.* をトリガに自動実行
  • ポイント
    • バッチ実行 = CronJob:本番 Bot を止めずにリソースを確保しやすい
    • モデルチューニング連携:Ray Tune や Optuna を呼び出し、最適パラメータを生成→Trader へプッシュ
    • 可観測性:処理時間・I/O 帯域を計測し、将来の水平スケール判断材料に

これらの基礎を押さえるだけで、
いつでもテストネット⇄メインネットを往復しながら高速に試行錯誤し、しかも事故らない
Bot 開発フローが完成します。


次章から、具体的な開発テクニックやワークフロー最適化の実例に踏み込みます。

3. 開発フローを劇的にラクにする4つのテクニック

3.1 docker compose exec でプロセスのみ再起動する

「コンテナごと落とす」から「中のプロセスだけ落として上げ直す」へ発想転換すると、開発サイクルが数分→数秒に短縮できます。

# Bot を止める(プロセスだけ kill)
docker compose exec trader pkill -f trade

# コンテナはそのまま、Bot だけ再起動
docker compose exec trader \
  python -m hyperliquid_sniper.cli trade --network testnet
  • 依存パッケージ再インストール0回:イメージを焼き直さないので I/O 待ちが消える
  • ログ・DB が残る:ウォームスタートでバックテストと実運用を継ぎ目なく比較

3.2 --network フラグとシングルソース ENV で“誤ネット接続”を防ぐ

testnetmainnet1 つのイメージで共存させつつ、ENV でガードを掛けます。

# entrypoint.sh 冒頭で
: "${NETWORK:?Need to set NETWORK=testnet|mainnet}"

if [ "$NETWORK" = "testnet" ]; then
  export SECRET_DIR=/secrets/testnet
else
  export SECRET_DIR=/secrets/mainnet
fi
  • ENV が無ければ即 exit 1:ヒューマンエラーをゼロに
  • CLI 側は --network 引数 → ENV に上書き、BOOTING IN TESTNET MODE を必ず標準出力へ

3.3 nats-box/Makefile でストリーム管理をワンライナー化

JetStream ストリームの追加・確認を 毎回ググる時間が惜しい。
Makefile のレシピにして 1 行コマンドで済ませます。

stream-add:
	docker compose run --rm nats-box \
	  nats stream add LIQUIDATION \
	  --subjects="liquidation.events" \
	  --storage=file --ack \
	  --max-age=48h --max-bytes=2GB
# ストリーム新設
make stream-add
  • バージョン差分の吸収:nats-box のタグを固定すれば CI / 本番どこでも同じ挙動
  • 実行ログが履歴に残る:誰がストリームをいつ作ったかを Git でトラッキング

3.4 ログと Prometheus に “BOOTING IN XXXX MODE” を必ず出す

「どのネットで動いている?」をコードが宣言すれば、人も AI も迷わない。

# ストリーム新設
make stream-add
  • Cursor や Copilot も誤診断しにくい:ログの定型文で判定できる
  • Grafana で ネット別起動回数を一目で可視化 → 異常リスタートを即発見

4. 環境切替を瞬時にするワークフロー比較

方式切替速度破壊的?メリット向き不向き
4.1 常駐コンテナ+exec★★★ 秒単位イメージ再ビルドなし・I/Oほぼゼロ開発・デバッグ最速反復
4.2 docker compose run --rm★★☆ 10秒前後“使い捨て”で環境クリーン、キャッシュは再利用テストごとに真っさらで比べたい
4.3 Supervisor/SIGHUP リロード★★★ ミリ秒〜秒極低プロセスを Graceful Reload、約定中でも止まらない本番24/7・ダウンタイム許容ゼロ

4.1 常駐コンテナ+exec方式

  • 起動コマンドは 1 行docker compose up -d infra
  • Bot の起動・停止は exec で完結。
  • メモリリークに注意:長時間回すなら週次で docker restart trader を cron へ。

4.2 docker compose run --rm 方式

  • コンテナは 起動するたび新規 → 終了で自動消滅
  • キャッシュ層は Docker レイヤで共有されるので、ビルドは高速。
  • 完全クリーン環境なので「テストで動いたのに本番で動かない」を減らせる。

4.3 Supervisor/SIGHUP リロード方式

  • Trader コンテナに supervisord を仕込み、supervisorctl restart trader で数百 ms リロード。
  • 約定中でも TCP が切れないため、本番のポジション維持率が上がる。
  • 代償として Dockerfile がやや肥大化(proctitle, pidfile 管理が必要)。

まとめ

  • 開発フェーズなら 4.1 常駐+exec が圧倒的ラク。
  • 検証フェーズで差分を明確にしたいなら 4.2 run --rm へ切替。
  • 本番 24/7 で本気運用する頃には 4.3 SIGHUP リロード を検討すると、
    レイテンシーもダウンタイムもほぼゼロで回し続けられます。

これらのテクニックを組み合わせれば、
起動のたび 5 分待ち → コンテナ常駐・秒で切替」へ劇的に改善し、
“試す→学ぶ→改良” のループが 一晩で数十ラウンドこなせる開発速度に到達できます。

5. フェイルセーフ設計──“間違ってメインネット”を 0 にするガード

5.1 エントリポイントで ENV と Secrets をクロスチェック

# entrypoint.sh
set -euo pipefail
: "${NETWORK:?NETWORK (testnet|mainnet) must be set}"

SECRET_DIR="/secrets/${NETWORK}"
KEY_FILE="${SECRET_DIR}/api_key.txt"

if [[ ! -f "$KEY_FILE" ]]; then
  >&2 echo "❌  secret ${KEY_FILE} is missing — aborting"
  exit 11
fi
echo "BOOTING IN ${NETWORK^^} MODE"
python -m hyperliquid_sniper.cli "$@"
  • シングルソース化NETWORK が空なら即死。
  • Secrets パス = ネット名 – testnet で mainnet 鍵を誤ロードできない。
  • 起動バナー – ログと Prometheus でモードを後追い確認できる。

5.2 起動後 3 秒で API Ping しチェーン ID を検証

async def verify_network(expected: str) -> None:
    url = "https://api.hyperliquid.xyz/info"
    async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=2)) as s:
        chain_id = (await (await s.get(url)).json())["chain_id"]
    ok = (expected == "testnet" and chain_id == 999) or \
         (expected == "mainnet" and chain_id == 1)
    if not ok:
        raise RuntimeError(f"Connected to WRONG network! expected={expected}, id={chain_id}")
  • fail-fast – 3 s 以内に不一致ならプロセス即終了。
  • CI で再現可 – pytest の起動テストに組み込み、PR 時点で事故を検出。

5.3 GitHub Actions で CI 自動ブロックを入れる

- name: Validate NETWORK/Secrets
  run: |
    docker compose -f docker-compose.testnet.yml up -d trader
    sleep 4
    docker compose logs trader | grep -q "BOOTING IN TESTNET MODE"
  • テストネット用 compose で起動 → バナーが出なければ workflow ×
  • secrets マウントや ENV が抜け落ちた PR は main ブランチに入らない。

6. 計測と可観測性:Bot が本当に動いているかを数値で見る

6.1 publish→subscribe 往復遅延を Prometheus で可視化

latency_s = Histogram("liq_event_latency_seconds",
                      "Recorder→Trader round-trip",
                      buckets=(.001, .002, .005, .01, .02, .05))

# Recorder
start = time.time() ; js.publish("liq.events", payload)

# Trader
delta = time.time() - start
latency_s.observe(delta)
  • Grafana パネル – 99 パーセンタイルが 5 ms を超えたら赤信号。

6.2 Recorder Dropped Events とバックプレッシャー監視

dropped = Counter("recorder_events_dropped_total",
                  "Events discarded due to full queue")
queue_size = Gauge("recorder_queue_fill_ratio",
                   "Internal queue fill level",)

while True:
    if q.full():
        dropped.inc()
        continue
    q.put(item)
    queue_size.set(q.qsize() / q.maxsize)
  • キュー充満率 0.7 超えで Alertmanager が Slack @here。
  • ドロップ増加→Collector CPU/帯域のスケール判断。

6.3 成功約定率と PnL グラフを Slack 自動送信

success = Counter("order_success_total",   labels=["pair"])
fail    = Counter("order_fail_total",      labels=["pair"])
pnl     = Gauge("pnl_total_usd",           labels=["strategy"])

async def send_daily_report():
    text = f"""
*Daily Report*
‣ Success Rate: {success_total / attempt_total:.2%}
‣ PnL: {pnl_total:.2f} USD
"""
    await slack.chat_postMessage(channel="#trading-bot", text=text)
  • メトリクス→Crontab Job→Slack Webhook の 3 行パイプ。
  • “稼働していないのに静かなまま”を防ぎ、毎日ワンクリックで状態確認。

まとめ

  1. フェイルセーフ
    • ENV×Secrets クロスチェック → 起動 0 秒でヒューマンエラー排除
    • API Ping → 実ネット mismap を 3 秒で感知
    • CI ブロック → 誤 PR が main に落ちない
  2. 可観測性
    • 往復遅延・ドロップ率・成功約定率を 三大 KPI に固定
    • Grafana & Slack で“見える化” + アラート自動化

これで **「動いているつもりで実は止まっていた/逆にメインネットで暴発」**という最悪パターンをほぼ排除しつつ、数字一本で健全性を追える体制が完成します。

7. さらに先へ──モノリシック Rust や Serverless への拡張アイデア

7.1 レイテンシーを極める Rust Collector

  • 狙い: Recorder のホットパスだけを GC レス言語に置き換え、WebSocket → bus の往復を 100 µs 未満へ。
  • 実装メモ
    1. Tokio + nats.rs で非同期ストリームを実装。
    2. FlatBuffers or Protobuf でバイト列を生成し、Python 側はゼロコピー参照。
    3. Dockerfile を multi-stage にし、cargo build --release → 15 MB の極小イメージを生成。
  • 効果: Python Collector 比で CPU 50 %↓・レイテンシー 75 %↓。Trader 側の約定ヒット率が体感で数%伸びる。

7.2 Strategy / Execution 分離で AB テスト自動化

  • 分割イメージ
    Strategy Service(FastAPI + Pydantic)
    ↳ gRPC でスコア/方向性/サイズのみ返す
    Execution Engine(Rust or Python)
    ↳ 鍵・レート制御・ idempotency 管理
  • AB テスト手順
    1. GitHub Actions でブランチごとに Strategy コンテナをビルド。
    2. Argo Rollouts / Flagger で PnL, Sharpe, 負け回数 を自動指標に Canary。
    3. 24 h で勝率が閾値を超えたバージョンを “Production” タグに自動昇格。
  • ポイント: Execution は常に安定版。失敗戦略を即ロールバックしてもポジション管理は安全。

7.3 Serverless デプロイで 1 クリック新戦略投入

  • 構成
    • 常駐: NATS + Execution Engine
    • On-demand: AWS Lambda / Cloud Run イメージとして戦略ロジックをデプロイ
  • ワークフロー
    1. make deploy-strategy NAME=mean_reversion → 60 秒で新 Lambda が作成。
    2. Execution Engine が HTTP Keep-Alive で戦略 API を叩く。
    3. 無ヒット 30 分で自動スケール = 0、コストもゼロへ。
  • メリット:
    • コードを書いて Commit → GitHub Actions → 本番投入が 数分
    • 戦略ごとに IAM、タイムアウト、メモリをきめ細かく設定できる。

8. まとめと次の一歩

8.1 今日から取り入れられる最小セット

やること工数期待効果
Rust Collector PoC半日レイテンシー計測で現状との差を把握
Strategy/Execution gRPC Stub 生成1 日後から複数言語戦略を差し替え可
Flask → Lambda 移行スクリプト雛形2 hServerless 版デプロイの土台が完成

8.2 来月の改善案を逃さない “可換アーキテクチャ” 思考

  1. インターフェース先決 – Collector → bus、Strategy → Execution を「メッセージ仕様」で固定。
  2. 実装はホットスワップ – “Rust” も “Lambda” も同じインターフェースで接続し、性能実験を並列化。
  3. 計測→決定を自動ループ – PnL / レイテンシー / 失敗率メトリクスを基準に、CI が勝手に最適構成へ収束。

こうして “構造を可換にしておく” ことで、来月ひらめいたアイデアも 大規模な作り直しゼロで即投入できます。
最速で学び、最小リスクで試行を回し、α が見つかれば自動で本番採用──それが“勝てる Bot 基盤”の最終形です。

付録:Dockerのビルド

1. 「再ビルドが必要になる」典型パターン

こんな変更をしたときなぜ再ビルド?代替策があるか
requirements.txt / pyproject.toml を更新イメージに焼き込んだパッケージが古いままになるほぼ必須(pip install をコンテナ起動時に毎回やる手もあるが遅い)
② ソースコードを COPY 層に含めているCOPY . /app のキャッシュが無効化されていないと古いコードで起動するボリュームマウントで済む開発フェーズなら rebuild 不要
③ Dockerfile を書き換えた
(RUN apt-get 追加など)
レイヤが変わる=ハッシュ不一致でキャッシュ再利用不可当然再ビルド
④ ベースイメージの脆弱性修正
(python:3.12-slim の更新)
docker pull だけではランタイムは変わらない--pull 付きでビルド or 定期自動ビルド
⑤ マルチアーキテクチャ対応
(arm64 / amd64)
マニフェストを再生成しないと他アーキで動かないCI で docker buildx bake などを回す
⑥ OS/コンパイル系の依存
(poetry install --only main → dev 追加)
イメージ内のビルドキャッシュが古いbuildkit のキャッシュ共有で高速化可

開発中はコードを -v $(pwd):/app でマウント → 再ビルド頻度を最小化。
本番イメージは「コード + 依存 + OS」を一体化するため、変更時にフルビルドが必要。


2. 「コンテナをビルドする」って実際に何をしているの?

  1. ビルドコンテキストを送る
    docker build . を実行すると、カレントディレクトリのファイルを tar してデーモンに送信
  2. Dockerfile の命令をレイヤ単位で実行
    • FROM python:3.12-slim → ベースイメージを取得
    • RUN apt-get update && apt-get install gcc → 一時コンテナでコマンド実行、結果をコミット
    • COPY . /app → ホストのコードをレイヤに追加
  3. 各レイヤをキャッシュ保存
    次回同じ命令・同じ入力ならスキップして高速化
  4. メタデータ付与
    ENTRYPOINTENV を最終イメージに書き込む
  5. イメージ ID(ハッシュ)で保存
    内容が 1 bit でも変わればハッシュも変わる=再ビルド判断に使える

結果として “すぐ起動できるスナップショット” が出来上がり、
docker run イメージIDファイルシステムと実行環境が即再現されるわけです。


3. 再ビルドを減らす小技

フェーズコツ効果
開発volumes: でコードだけホットリロード再ビルド不要、秒単位で変更反映
CIBuildKit の キャッシュエクスポート (--cache-to/--cache-from)依存インストール層を再利用しビルド 10x 速く
本番マルチステージビルドで不要ファイル削除軽量化&セキュリティ向上、更新差分も小さく
運用docker build --pull を週次ジョブベースイメージ更新を自動取り込み、脆弱性回避

4. TL;DR

  • 再ビルドが必要なのは「イメージに焼いた層」が変わる時(依存・Dockerfile・ベース OS)。
  • ビルド工程は「命令を一時コンテナで実行→結果をレイヤ保存→ハッシュ化」しているだけ。
  • 開発では ボリュームマウント、CI では キャッシュ再利用で“ビルド連打地獄”を避けよう。

-Bot, 環境構築・インフラ, 開発ログ