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

🛠️開発記録#243(2025/6/4)構造化と実装の両輪で前進した一日

1. はじめに:プロジェクトの再編からスタート

仮想通貨Botの運用に向けて、今回はプロジェクト構造の再編から着手した。
その理由は単純で、「機能が増えてきた分、構成が煩雑になってきた」からだ。

特に次のような課題が浮かび上がっていた:

  • スクリプト間の依存関係がコードを追わないと分からない
  • 各種ファイル(設定・ログ・DBなど)の保存場所が散らかっている
  • Dockerコンテナとホストマシンの間でファイルパスが不安定

こうした状態では、本番運用への移行やCI/CDの自動化、監視体制の構築が足を引っ張られるのは目に見えていた。


構造が崩れていた例

たとえば、起動フローを見てもその断片性は明らかだった。

entrypoint.sh → envsubst で config を生成 → mmbot.cli が config.json を読む

一見シンプルに見えるが、

  • config のテンプレートがどこにあるか
  • JSON の生成先がどこか
  • cli 側でどこを参照しているか

がすべて ハードコードされており、実行環境ごとに都度確認が必要だった


再編のスタート地点:構造の「見える化」

そこでまず私が行ったのは、プロジェクトの構造を手書きで可視化することだった。
以下は、当初の再編着手時に書いた構造メモを再現したものである。

MMBot/
├─ compose/               ← Compose YAML、Dockerfile、entrypoint.sh
├─ env/                   ← .env.testnet / .env.production
├─ src/
│   └─ mmbot/
│       ├─ cli.py         ← メインロジック
│       ├─ logging_setup.py
│       └─ guard.py / execution.py / notify.py …
├─ config/
│   └─ mmbot/
│       ├─ testnet/mmbot.tpl
│       └─ mainnet/mmbot.tpl
└─ data_host/             ← ホスト側でマウント予定のログ・DB保存先

この構造を描きながら「どのスクリプトがどの環境変数を必要としているのか?」「ファイルの出力先はどこにしたいのか?」を洗い出した。

結果として、以下のような狙いが明確になった:

  • CONFIG_DIR / DATA_DIR を環境変数化し、ホスト⇄コンテナ間で柔軟に切り替える
  • 起動フローの中でどこで何が生成されるかを固定化し、安全にトレースできる構造にする
  • .env の役割を明確に分離し、「Compose用」「Bot内用」を整理する

こうして構造を「見える化」したことで、
このあと進めていく修正・リファクタ・テストが驚くほどスムーズに進行していった。
思い返せば、この手書き構造図こそが開発加速のトリガーだったとさえ思う。


このあとのセクションでは、実際にどのように環境変数化やDocker構成の調整を進めていったかを段階的に振り返っていく。

2. CONFIG_DIR / DATA_DIR の環境変数化

構造を見える化したことでまず最初に明確になったのは、多くのパスがハードコードされていたという事実だった。
典型的なのは以下のようなコード:

# entrypoint.sh(修正前)
envsubst < /app/config/mmbot/testnet/mmbot.tpl > /app/config_testnet.json
# cli.py(修正前)
cfg_path = Path("/app/config_testnet.json")
db_file = Path(f"mmtrades_{mode}_{date}.db")

このようなパスは、ローカル開発・テストネット・本番環境で全て異なる要件があるのに、書き換える術がない。
また、出力ファイルの保存先(ログ、DB)も同様に /app/data に固定されており、ホスト側から直接見たり、ログを解析したりするには不便だった。


対応方針:「すべては環境変数で切り替える」

そこで取った方針はシンプルかつ強力なものである。

CONFIG_DIR / DATA_DIR を環境変数にし、全てのパスをこれに従って動的に決定する。

この設計によって、以下のような状況に柔軟に対応できるようになる:

ケース
テスト環境のconfigを読みたい固定パスを書き換えるMMBOT_MODE=testnet + CONFIG_DIR=/app/config
ホスト側にログ/DBを書きたいdocker cp で毎回コピーDATA_DIR=$PWD/data_host でマウント可能
本番とtestnetでパスを分けたいentrypointを分岐させる必要あり.env.production / .env.testnet に値を書くだけ

実装内容(修正例)

🔧 entrypoint.sh

CONFIG_DIR=${CONFIG_DIR:-/app/config}
DATA_DIR=${DATA_DIR:-/app/data}

CFG_TPL="${CONFIG_DIR}/mmbot/${MODE}/mmbot.tpl"
CFG_JSON="/app/config_${MODE}.json"

envsubst < "${CFG_TPL}" > "${CFG_JSON}"

🔧 cli.py

CONFIG_DIR = Path(os.getenv("CONFIG_DIR", "/app/config")).resolve()
DATA_DIR = Path(os.getenv("DATA_DIR", "/app/data")).resolve()

# 出力先例
db_file = DATA_DIR / "db" / f"mmtrades_{mode}_{today}.db"
log_path = DATA_DIR / "logs" / "MMBot.log"

🔧 docker-compose.yml

environment:
  CONFIG_DIR: ${CONFIG_DIR:-/app/config}
  DATA_DIR: ${DATA_DIR:-/app/data}
volumes:
  - ${DATA_DIR}:${DATA_DIR}

成果:自由に切り替えられる開発環境

これにより、私の開発フローは次のようにシンプルかつ柔軟になった。

# ローカルホストのログ出力先を切り替え
export DATA_DIR=$PWD/data_host
docker compose -f compose/compose.yml up -d

コンテナ内から見れば /app/data
ホストから見れば ./data_host/ にログ・DB が記録されるようになった。


🧠 学び:固定パスは“使い捨て”になる

構造が固まっていない段階では、ハードコードされたパスは一見便利だ。
だが環境が増えた瞬間に切り替えが破綻し、構成の見直しコストが跳ね上がる。

パスを変数にし、構造を変えずに環境を切り替える仕組みを持つことこそが、長期的にメンテ可能なプロジェクトの第一歩だと強く実感した。

3. .env管理とDocker Composeの整備

構造の可視化と CONFIG_DIR / DATA_DIR の環境変数化を進める中で、次に直面したのが .envファイルの整理だった。

これまでは .env.testnet.env.production がプロジェクトルート直下に雑多に置かれており、
「どれをどこで使っているのか?」 がわかりにくい状態だった。
また、docker-compose.yml との接続もあいまいで、Compose実行時に正しい環境が読み込まれているかを確認する手段も乏しかった。


目標:役割別に.envを整理する

そこで次のように 役割ごとに .env を分類する運用ルールを整備した。

ファイル目的内容
.env(プロジェクト直下)Composeパーサ用DATA_DIR, MMBOT_RESTART など、ホストマシンの環境依存値のみ
env/.env.testnetBotに渡す環境変数(テストネット)APIキー、SlackWebhook、MMBOT_MODE、戦略パラメータなど
env/.env.productionBotに渡す環境変数(本番用)構成は testnet と同じだが、値が異なる

この分離によって、どの変数がどこで効いているかが明確になり、管理・切り替えが非常に容易になった。


Docker Compose 側の変更点

# compose/compose.yml
services:
  mmbot:
    env_file:
      - ../env/.env.testnet     # ← 本番に切り替えるときは env/.env.production
    environment:
      CONFIG_DIR: ${CONFIG_DIR:-/app/config}
      DATA_DIR:   ${DATA_DIR:-/app/data}
    volumes:
      - ${DATA_DIR}:${DATA_DIR}
    restart: "${MMBOT_RESTART:-unless-stopped}"

このように env_file: では Bot内部に渡したい環境変数 を、
environment:volumes: では Compose実行時に展開したい環境変数 を使い分けることで、
柔軟かつ予測可能な構成が実現した。


.env.testnet の中身(抜粋)

# MMBot テストネット用設定
MMBOT_MODE=testnet
CONFIG_DIR=/app/config
DATA_DIR=/app/data

BYBIT_APIKEY=xxx
BYBIT_SECRET=xxx

SLACK_WEBHOOK_URL=https://hooks.slack.com/services/...
SYMBOL=BTCUSDT
S_ENTRY=5e-5
LOT=0.002
MAX_NOTIONAL_USD=1200
LOSS=10

.env(プロジェクト直下)での最低限の定義

# Compose 展開専用
DATA_DIR=./data_host
MMBOT_RESTART=unless-stopped

この .env はコンテナには渡らないが、docker-compose.yml 内の ${VAR} を展開する際に参照される。


運用時の実行例

# ローカル開発
export DATA_DIR=$PWD/data_host
docker compose -f compose/compose.yml up -d
# testnet を起動
docker compose -f compose/compose.yml --env-file env/.env.testnet up -d

学び:envファイルは「読み手別」に分けると混乱が減る

これまで .env を1枚に集約しようとして、ComposeパーサとBot内部の変数が混在していた。
だが、「誰がこの値を読むのか?」 という観点で切り分けることで、運用の明瞭さと安全性が一気に向上した。

特にDocker Composeは .env の解決範囲が特殊(env_file:とは別扱い)なため、それを前提に設計するだけでトラブルを予防できる

4. entrypoint.shcli.py のつなぎ直し

構造を明確にし、環境変数を整備したことで、次に見直すべきは
**「コンテナ起動時のエントリーポイントから Python ロジックへの橋渡し部分」**だった。

従来のコードは最小限の構成ではあるが、

  • JSON の生成場所や読み込み先がハードコードされていた
  • 起動時のモード(testnet / mainnet)の判定が曖昧だった
  • DATA_DIR に出力されるログやDBのパスが固定的で変更がきかない

といった問題を抱えていた。
これらを整理し直すことで、コンテナ起動フローの“見通し”と“安全性”が一気に改善した。


改修後の起動フロー構造

以下が、今回構築した起動処理の構造図である。

               ┌────────────────────┐
               │ docker-compose.yml│
               └────────┬───────────┘
                        │
                        ▼
              ┌────────────────────┐
              │   entrypoint.sh    │
              └────────┬───────────┘
     (環境変数読み込み CONFIG_DIR, DATA_DIR, MMBOT_MODE)
                        │
                        ▼
       JSONテンプレートからconfigを生成(envsubst)
                        │
                        ▼
         JSON構文チェック(jq emptyでvalidate)
                        │
                        ▼
          Python CLIへ移行(exec python -m mmbot.cli)
                        │
                        ▼
     cli.pyがconfigを読み込み、Botのループ処理へ

entrypoint.sh の改善ポイント

CONFIG_DIR=${CONFIG_DIR:-/app/config}
DATA_DIR=${DATA_DIR:-/app/data}
MODE=${MMBOT_MODE:-mainnet}

CFG_TPL="${CONFIG_DIR}/mmbot/${MODE}/mmbot.tpl"
CFG_JSON="/app/config_${MODE}.json"

envsubst < "${CFG_TPL}" > "${CFG_JSON}"
jq empty "${CFG_JSON}"

exec python -m mmbot.cli "$@"

改善点:

  • CONFIG_DIR / DATA_DIR を外部から指定可能に
  • MODE が未指定でもデフォルトを mainnet に固定
  • jq を使った構文チェックで テンプレート記述ミスを早期検出
  • exec によって Python プロセスを PID 1 に置換し、シグナル伝播を正しく処理

cli.py 側の受け口(抜粋)

CONFIG_DIR = Path(os.getenv("CONFIG_DIR", "/app/config")).resolve()
DATA_DIR = Path(os.getenv("DATA_DIR", "/app/data")).resolve()

cfg_root = Path(os.getenv("CONFIG_ROOT", "/app"))
cfg_path = cfg_root / f"config_{mode}.json"

db_dir = DATA_DIR / "db"
db_dir.mkdir(parents=True, exist_ok=True)
db_file = db_dir / f"mmtrades_{cfg['mode']}_{today}.db"

改善点:

  • CONFIG_DIR/ROOT・DATA_DIR を受け取りつつ、Pathlib で安全に解決
  • DB やログの生成先を DATA_DIR 配下の db/, logs/ に集約
  • "mmtrades_testnet_20240604.db" など日付付きファイルで蓄積・分析しやすく

実際の動作ログ例(起動時)

[entrypoint] CONFIG_DIR=/app/config
[entrypoint] DATA_DIR=/Users/xxx/MMBot/data_host
▶ generating /app/config_testnet.json from /app/config/mmbot/testnet/mmbot.tpl
▶ validating JSON syntax
▶ starting MMBot (testnet)
📌 NEW SESSION: mode=testnet, LOSS=10.0

このログが安定して出ることによって、「エラーのトレース」と「成功の再現」が圧倒的に楽になった。


学び:起点と終点を明示するとシステムは安定する

entrypoint.sh の責務は「起動パラメータを整えて、CLIに橋渡しすること」。
cli.py の責務は「設定を読み取り、Bot本体のロジックを駆動すること」。

この責務分離がはっきりしたことで、どこを修正すれば何が変わるかが明確になり、安心して開発が進められた。

5. テストネット環境の正常起動確認

ここまでの構造整理とエントリーポイント修正を経て、いよいよ 本番に近い形でBotを実行するステップに進んだ。

対象は Bybit テストネット
Slack通知やログ出力、ポジション建てと決済など、実際に「動く」Botとしての機能検証を一通り行った。


起動コマンド(testnet)

export DATA_DIR=$PWD/data_host
docker compose -f compose/compose.yml up -d --build
  • DATA_DIR はホストの ./data_host に設定
  • compose.ymlenv/.env.testnet を読み込む
  • 起動後、ログ確認:
docker compose -f compose/compose.yml logs -f --tail 50 mmbot

起動ログに現れた「成功」の兆候

[entrypoint] CONFIG_DIR=/app/config
[entrypoint] DATA_DIR=/Users/xxx/MMBot/data_host
▶ generating /app/config_testnet.json from /app/config/mmbot/testnet/mmbot.tpl
▶ validating JSON syntax
▶ starting MMBot (testnet)
📌 NEW SESSION: mode=testnet, LOSS=10.0

このログが示すのは:

  • テンプレート読み込みと変数展開が成功
  • 生成された JSON に構文エラーなし
  • main loop に入る前の初期化が完了
  • モードは testnet として正しく起動

Slack通知も正常

  • 起動時に "📌 NEW SESSION" のSlack通知を受信
  • 以降のサイクルごとに、スプレッド条件・注文結果・PnLが報告される

通知例(抜粋):

📈 *MMBot* cycle 3
spread `0.00045`
Buy @xxx → Fill
Sell@xxx → Fill
*Cycle PnL* `+3.22`
*Total PnL* `+6.78`

これにより、Botの健全な稼働とパフォーマンスをリアルタイムで可視化できている。


実際にBybit テストネットで注文も確認

テストネットの UI 上でも、Botが出した注文が反映され、約定が発生していた。

これにより、CLI → WebSocket → REST API 呼び出し → 注文処理の全パスが通ったことを意味する。


ホスト側の出力も問題なし

ls -R data_host/

出力例:

data_host/
├── db
│   └── mmtrades_testnet_20240604.db
├── logs
│   └── MMBot.log
  • SQLite DB は日付単位でファイル出力され、run_loop() のPnLロジックと連動
  • ログファイルも loguru を通じて logs/ に書き込まれており、追跡が可能

学び:構造の整理は「運用の可視化」に直結する

このフェーズで最も感じたのは、構造が整理されていれば「本当に動いているか?」の確認も簡単になるという点だ。

  • 通知が来る
  • ログが伸びる
  • DBができる
  • UIで注文が見える

という 「多方面からの確認」がすべて可視化された状態は、
今後の本番稼働・異常検知・改善ループを支える非常に強固な基盤になる。

6. 成功の鍵は「構造の可視化」だった

今回の環境構築とリファクタは、わずか半日でtestnet稼働まで到達するという、想定以上のスピードで進行した。
その背景には間違いなく、「構造を自分の手で可視化していたこと」がある。


構造の“見える化”がもたらしたもの

私が最初に行ったのは、プロジェクト全体の構成を手書きで整理することだった。
これは単に「どこに何があるか」を書き出すだけではなく、

  • どのスクリプトがどのファイルに依存しているか
  • 起動フローがどう進み、どこで何が生成・参照されるか
  • 外部との接続点(Slack、Bybit API、ログ/DB出力)がどこにあるか

といった“動線と接点のマッピング”を行う作業だった。


結果:コード提案の背景が“読める”ようになった

この構造を把握していたことで、AIから提案された変更にも 「何を、どこで、なぜ変えようとしているか」 が直感的に理解できた。

たとえば、

environment:
  CONFIG_DIR: ${CONFIG_DIR:-/app/config}

といった提案を受けたときも、
「それを変更すると entrypoint.sh の JSON 出力先が変わり、それを cli.py が読む構造になっているから、env_file で変数を渡す必要があるな」とすぐに頭の中でつながった


「つながりの見える」開発は、迷いを減らす

このような構造を把握した開発では、次のような変化が起こる:

BeforeAfter
「これを書き換えていいのかわからない」「これは〇〇に影響するから書き換える必要がある」
「提案されたコードを試すのが怖い」「その変更が何を起こすかが事前にイメージできる」
「バグが起きたとき、原因を探すのに時間がかかる」「構造的に見ると、おそらくこのあたりが怪しい」と当たりがつけられる

構造 × ロジックの“二層思考”へ

この体験を通じて、私の中に新たな視点が形成された。

コードはロジックだけでなく、構造の中で意味を持つ

だからこそ、

  • 「動くコード」を作るだけでなく
  • 「運用に耐える構造」を作ることが、長期的には効率と自信をもたらす

という開発観が強くなった。

7. まとめ:今日の達成と今後の一手

今回の開発では、プロジェクトの構造整理からtestnet稼働までを半日で完了させることができた。
これは単なる作業の積み上げではなく、「構造を見通す視点」を持って取り組んだ結果だと感じている。


✅ 今日の達成まとめ

項目内容
構造の見直しパス・スクリプト・設定ファイルの依存関係を整理し、構造図を描いた
CONFIG_DIR / DATA_DIR の環境変数化ハードコードを撤廃し、運用環境ごとの柔軟な切り替えが可能に
.env の分離設計Compose向けとBot内部向けで役割を分けたenvファイル管理を実現
entrypoint.sh ⇄ cli.py の接続整理JSON生成、構文検証、PID引き継ぎ、ログ出力のルートを明確化
テストネットでの安定稼働Slack通知、実注文、ログ出力、PnL蓄積がすべて正常に動作することを確認

🧠 獲得した学び

  • 構造を可視化すると、迷わずに速く動ける
  • “どのスクリプトが何を担っているか”がわかっていれば、提案や修正も自信を持って進められる
  • AIの提案は、こちらの構造把握力に比例して理解・活用しやすくなる

🔜 次に進むべき一手

タスク内容
watchdog.sh の cron登録擬似障害通知 → Slack → restart をテストネットで試す
CIへの dry-run 組み込みpython -m mmbot.cli --dry-run を含むビルドテストで本番安全性担保
.env.production の整備本番APIキーとパラメータを反映。実環境で稼働準備へ
PRの作成とCI通過feat/config-data-env ブランチをGitHubにpushし、コードレビュー&マージへ
mainnet初回稼働テスト小ロットでの本番動作検証を開始予定

✍️ 最後にひとこと

構造を見直すことは、時間がかかるようでいて実は一番の近道だった。
その構造さえ見えていれば、複雑なスクリプトでも恐れることなく変更できるし、
外部と連携していく土台にもなる。

この日をきっかけに、私は単にBotを動かす開発者から、
構造をコントロールする運用者へ一歩踏み出せたような気がする。

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