trading-bot アーキテクチャ解説
— 戦略は薄く、リスクは厚く、監視は読むだけ
📖 TL;DR — 3行でいうと
- 既製エンジン Freqtrade に「薄い戦略アダプタ+Freqtrade 非依存のリスクコア」を載せ、ops が read-only で監視する三役構成。
- リスク閾値・実行時契約・品質ゲート定義はすべて単一供給源(
risk_params.py/ops/contract.py/ci.sh)に集約し、テストが整合を常時検証する。 - 安全は多層で守る:戦略退避 → ATR ソフトストップ → ハード −20% → 日次/通算 損失ガード → キルスイッチ3経路+外部デッドマン。
0全体像 — 三役構成と1サイクルの流れ
このリポジトリは「Bot をゼロから書く」のではなく、既製のトレーディングエンジン Freqtrade の上に、自分の判断が必要な部分だけを薄く載せる構成をとる。
登場するのは三役。取引の実行系はすべて Freqtrade が持ち、リポジトリ側のコードは「何を・いくらで・買ってよいか」の判断と、「壊れていないか」の観測に徹する。
| 役 | 実体 | やること | やらないこと |
|---|---|---|---|
| ⚙️ エンジン | Freqtrade 2026.5(pip 依存) | 取引ループ・発注・約定管理・DB 記録・dry-run シミュレーション・REST API | 戦略判断(コールバックで委譲) |
| 🧠+🛡️ 戦略とリスク | user_data/strategies/ | シグナル生成(薄いアダプタ)とリスク計算(Decimal 厳密・Freqtrade 非依存の risk.py) | 発注 API を直接叩くこと |
| 👀 運用監視 | ops/ + ops-monitor エージェント | デッドマン判定・状態スナップショット・異常の分類と記録・改善の提案 | 取引への介入(read-only / propose-only) |
0.14時間に1回の判定サイクル
取引対象は BTC/JPY 現物・4時間足・最大1建玉(max_open_trades=1)。1サイクルの流れは次のとおり。
「戦略は薄く、リスクは厚く、監視は読むだけ」。シグナル生成のコードは最小限に抑え、資金を守るコード(リスクコア・ガード・キルスイッチ・検証基盤)に厚みを置く。監視は取引に手を出せない構造にする。これは姉妹編・体系ガイドの「Signal Engine と Risk Engine を分離せよ」「破産しない型をつくる」をこのリポジトリで実装に落としたもの。
1ディレクトリと責務 — 4+1 の地区
リポジトリは責務の異なる 4 つのディレクトリ+学習用 docs で構成される。地区をまたぐ知識の共有は「単一供給源ファイル」経由のみに制限されている(§8)。
| パス | 責務 | 代表ファイル |
|---|---|---|
🏭 user_data/strategies/ | 取引の中枢。戦略・リスクコア・閾値定義・取引所パッチ | TrendFollowSpotV1.py / risk.py / risk_params.py / ccxt_bitbank_patch.py |
🗼 ops/ | 運用監視 CLI・実行時契約・手順書・監視エージェントの記憶 | healthcheck.py / snapshot.py / contract.py / RUNBOOK.md / agent/ |
🚚 scripts/ | データ搬入と検問。取得・検証・バックテスト・品質ゲート | fetch_bitbank_ohlcv.py / validate_ohlcv.py / run_backtest.sh / ci.sh |
🔬 tests/ | 不変条件の見張り。ユニットテスト+ tripwire テスト | test_risk.py / test_risk_params.py / test_quality_gates.py ほか(約80ケース) |
📚 docs/ | 学習コンテンツ・品質ゲート設計・設計記録 | quality-gates.md / superpowers/(設計 spec)/ 体系ガイド HTML |
Freqtrade の strategy loader は user_data/strategies/ をそのまま sys.path に追加して戦略をロードする。サブディレクトリに整理することはフレームワーク規約上できないため、risk.py や risk_params.py も同じ階層に「フラット配置」される。README に「変更禁止」と明記されているのはこのため。フレームワークの規約に逆らわず、整理は命名で行う。
2起動と配線 — freqtrade trade から取引ループまで
起動コマンドは 1 行。だがその裏で「設定 → 戦略 → パッチ → ガード」が順番に配線される。配線の要は戦略の bot_start()。
# 起動(dry-run)
freqtrade trade --config user_data/config.json --strategy TrendFollowSpotV1- 設定の解決 —
config.jsonを読み、環境変数(FREQTRADE_*)→ CLI 引数の順で上書き。主要設定:stake_currency: JPY/timeframe: 4h/max_open_trades: 1/dry_run: true(仮想残高 100万円)/stake_amount: "unlimited"(=サイズ決定を戦略へ委譲)。 - 戦略のロード — StrategyResolver が
user_data/strategies/からTrendFollowSpotV1(IStrategy v3)を解決。 - bot_start() での配線 — 戦略の初期化フックで CCXT bitbank パッチを適用(適用済みフラグで冪等化)し、DailyLossGuard を生成(閾値は
risk_params.pyから注入)。キルスイッチのファイルパスはops/contract.pyと同じ規約を使う。 - 取引所接続 — CCXT 経由で bitbank に接続(dry-run でも板・価格は本物を参照)。
- 取引ループ開始 — 約5秒間隔のループで足の確定を監視し、確定足ごとに §0.1 のサイクルを回す。約定・建玉は
tradesv3.dryrun.sqlite(dry-run 用 DB。本番 DB とはファイルから分離)に記録される。
CCXT bitbank パッチ — 「使う側でフレームワークの穴を塞ぐ」
CCXT 4.5.56 の bitbank.fetch_ohlcv には、4時間足のローソク取得で日付フォーマットを誤送信する不具合(年単位 YYYY を要求される API に 8桁 YYYYMMDD を送る)があり、そのままではリアルタイムのデータ取得が失敗する。ccxt_bitbank_patch.py が起動時にモンキーパッチでこれを修正し、年跨ぎにも対応する。CCXT 本体のフォークではなくパッチにすることで、依存の更新を妨げずに修正を局所化している。
API キー等の秘密情報を含みうるため config.json は .gitignore 対象。リポジトリには config.example.json だけを置き、複製して使う。「リポジトリに置くのは形(example)、実体(秘密)はローカルだけ」という分離。
3戦略コア — TrendFollowSpotV1 は薄いアダプタ
戦略クラスは「Freqtrade との通訳」に徹し、計算の実体は risk.py に委譲する。指標は 4 種類だけ(過剰最適化への歯止め)。パラメータは初版固定で hyperopt は使っていない。
3.1指標とエントリー/イグジット条件
| 指標 | 設定 | 役割 |
|---|---|---|
| SMA200(日足) | @informative('1d') で日足から 4h 足へ注入 | 市場フィルター:大きな上昇トレンドの中でだけ買う |
| EMA20 / EMA50 | 4h 足 | モメンタム:短期が中期を上抜けた瞬間=勢いの転換 |
| ATR14 | 4h 足 | ボラティリティ:ストップ距離とポジションサイズの基準 |
| volume_ma | 出来高の50本移動平均 | 流動性フィルター:閑散時のダマシを避ける |
# populate_entry_trend — 4条件の AND(1つでも欠ければ買わない)
conditions = [
dataframe["close"] > dataframe["sma200_1d"], # 日足SMA200より上(市場フィルター)
qtpylib.crossed_above(ema_fast, ema_mid), # EMA20 が EMA50 を上抜け(モメンタム)
dataframe["volume"] > 0,
dataframe["volume"] > dataframe["volume_ma"], # 出来高が平均超え(流動性)
]
# 全て成立した足に enter_long=1, enter_tag="trend_follow"イグジットは「終値が EMA50 を下回ったら退避」(exit_long=1 → 次足成行)。これは利確でも損切りでもなく「トレンドが終わったらキャッシュに戻る」という現物トレンドフォローの基本動作で、これとは独立にリスク層のストップ群(§4)が機能する。
※ startup_candle_count=1400:日足 SMA200 を 4h 足換算で温めるのに必要な本数。感覚値ではなく recursive-analysis(指標の再帰汚染チェック)で実測決定されている。
3.2Freqtrade コールバック一覧 — どの瞬間に何をするか
Freqtrade は決まったタイミングで戦略のメソッドを呼ぶ(Hollywood 原則:「呼ぶのはフレームワーク側」)。実装しているのは次の 8 つ。
| コールバック | タイミング | この戦略での仕事 |
|---|---|---|
bot_start | 起動時に1回 | CCXT パッチ適用・DailyLossGuard 生成(§2) |
bot_loop_start | 毎ループ | 損失ガード判定+キルスイッチ確認 → _blocked 更新。heartbeat 書き込み |
populate_indicators | 足の確定ごと | EMA/ATR/出来高MA 算出(日足 SMA200 は informative で自動合成) |
populate_entry_trend | 足の確定ごと | AND 4条件で enter_long シグナル |
populate_exit_trend | 足の確定ごと | EMA50 下抜けで exit_long シグナル |
custom_stake_amount | エントリー直前 | _blocked なら 0。%リスクでサイズ計算(§4.2) |
confirm_trade_entry | 発注直前(dry/live のみ) | _blocked なら False。板スプレッド ≤0.5% を確認(広すぎれば見送り) |
custom_stoploss | 建玉保有中の毎評価 | ATR×2.0 のソフトストップ価格を返す(§4.1) |
各コールバックの中身は数行〜十数行で、判断ロジックの実体(ストップ価格・サイズ計算・ガード判定・スプレッド判定)はすべて risk.py の純関数を呼ぶだけ。Freqtrade の API が float を要求するのに対し risk.py は内部 Decimal で計算するため、戦略クラスは「float ⇄ Decimal の境界」も担う。おかげで risk.py は Freqtrade なしで単体テストできる(§6)。
confirm_trade_entry の板スプレッド確認はリアルタイム(dry-run / live)でしか呼ばれない。バックテストでは常に通過し、その分の執行コストは §5 の「コスト感度バックテスト」で fee に織り込んで評価する。「BT に現れない現実」を fee の上乗せで対称に扱う設計。
4リスク管理 — 勝つことより破綻しないこと
このリポジトリで最も厚いのがこの層。独立した防御を 5 層重ね、どれか 1 層が破れても次が受け止める。背景には「bitbank はサーバ側ストップ注文に非対応」という取引所制約がある(KI-002、§8)。
4.15層の防御
custom_stoploss)現在値 − ATR×2.0 をストップ価格として返す。Freqtrade のストップ更新は上方向のみのため、実質トレーリング。ボラが膨らんでも既存ストップは緩まない。stoploss=-0.20)ATR 計算が NaN になる等、上の層が機能しない場合の最後の砦。Freqtrade 組込み protections(MaxDrawdown / StoplossGuard)も併用。_blocked=True。新規エントリーは custom_stake_amount(0 を返す)と confirm_trade_entry(False を返す)の二重チェックで遮断される。既存建玉の決済は止めない(損切りは生かす)。touch user_data/KILL_SWITCH(ファイル)② BOT_KILL_SWITCH=1(環境変数)③ Freqtrade REST API(127.0.0.1:8080 の /stopentry 等。手順は ops/RUNBOOK.md)。①②は bot_loop_start が検知して新規を遮断、③はエンジン自体を止める。さらに層の外側に、プロセスが「生きているが止まっている」状態を検知する外部デッドマン監視がある(§7 healthcheck)。
DailyLossGuard が見る equity は実現ベース(含み損益を反映しない)。つまり建玉の含み損には Layer 4 は反応しない。含み損を受け持つのは Layer 2/3(ストップ)という役割分担。またガードの状態はメモリ上のみで再起動するとリセットされる(既知の限界 KI-001、§8)。「万能の 1 層」ではなく「役割の違う層の重なり」として読む。
4.2ポジションサイジング — 1トレードのリスクを equity の 0.5% に固定
「いくら買うか」は risk.py の position_stake() が決める。ストップまでの距離が遠いほどサイズは小さくなり、1 回の負けが equity の約 0.5% に収まるよう逆算する。
- 計算はすべて Decimal(float の丸め誤差で発注額がズレる事故を排除)。丸めは常に切り捨て=保守側。
- bitbank BTC/JPY の最小ロット・数量刻み(0.0001)でクランプし、満たせなければ「見送り」を返す。中途半端なサイズで無理に入らない。
4.3閾値の単一供給源 — risk_params.py
# user_data/strategies/risk_params.py — リスク閾値はこのファイルだけが定義する
RISK_PCT = 0.005 # 1トレードあたり equity の 0.5%
DAILY_LOSS_LIMIT = 0.02 # 当日 −2% で新規ブロック
TOTAL_LOSS_LIMIT = 0.15 # ピーク比 −15% で新規ブロックこの 3 定数を参照するのは、戦略(import してクラス属性へ)、ops/snapshot(importlib で動的ロード——Freqtrade を入れていない環境でも読める)、テスト(test_risk_params.py が両者の整合を CI で検証)の 3 箇所。かつて snapshot は戦略ソースを正規表現で読んでいて「リネームしたら黙って null になる」侵入結合があり、2026-06-12 のリファクタでこの形に解消された(§8)。
5データと検証 — fetch → validate → 三層バックテスト
体系ガイドの教訓「バックテストは静かに嘘をつく」を、このリポジトリは機械化された検証パイプラインで受け止める。データは検査してから使い、結果は 3 方向から疑う。
① 取得 — 自前の年次ローダ(fetch_bitbank_ohlcv.py)
バックテスト用の過去データは CCXT を使わず、bitbank 公式 candlestick API を年単位で直接叩いて取得する(§2 で触れた CCXT の日付フォーマット不具合の回避が設計動機)。未確定足を落とし、Freqtrade が読める feather 形式で user_data/data/bitbank/ に保存。2017年〜現在の 4h/1d を揃える。
② 検査 — validate_ohlcv.py
空データ・時刻順序の逆転・重複・NaN・非正値を検出し、合格したデータだけがバックテストに進める。欠損は致命扱いにしない(Freqtrade 側が出来高 0 で補完する仕様に委ねる)。
③ 三層バックテスト — run_backtest.sh
| 検証 | 内容 | 何を疑っているか |
|---|---|---|
| 💸 コスト感度 | fee 0.0032(基準)/ 0.0044(手数料2倍)/ 0.0052(スリッページ2倍)の3本 | エッジがコストの僅かな悪化で消えないか |
| 🌊 レジーム別 | 2017暴騰・2018暴落・2020-21強気・2022弱気・2023-25 など年代別ウィンドウ | 特定の相場環境だけで勝つ戦略ではないか |
| 🕳️ lookahead / recursive | Freqtrade 公式の lookahead-analysis / recursive-analysis | 未来の値を盗み見ていないか・指標の起動本数は足りているか |
執行価格を細工してコストを表現する方法(custom_entry/exit_price)は、バックテスト経路によって呼ばれたり呼ばれなかったりして監査しづらい。このリポジトリは「コストはすべて --fee に対称に束ねる」方針を取り、検証の再現性・監査性を優先している。
6品質ゲート — ローカルも CI も同じ漏斗
品質チェックの定義は scripts/ci.sh ただ 1 つ。ローカル実行・pre-push hook・GitHub Actions のすべてが同じスクリプトを呼ぶため、「ローカルでは通ったのに」が構造的に起きない。
- 依存整合 —
pip check:壊れた依存関係を最初に検知 - 構文 —
compileall:import すら通らない破壊を検知 - 整形 —
ruff format --check - リント —
ruff check - 型 —
mypy - テスト —
pytest+カバレッジ 75% 以上を強制(対象:scripts/ops/user_data/strategies) - データ品質 — OHLCV ファイルがローカルにある場合のみ
validate_ohlcvを実行(CI は構造品質に専念し、市場データ依存の検証はrun_backtest.sh側が分担)
tripwire テスト — 「壊れたら事故」だけを見張る
test_quality_gates.py は ci.sh の不変条件だけを検証する:実行可能であること、全ゲートを含むこと、カバレッジ閾値、CI が ci.sh を呼んでいること。かつてはツールの呼び出し文字列まで assert していて「ツール表記を変えただけでテストが割れる」状態だったが、リファクタで「触ったら警報が鳴るべき線(tripwire)だけ張る」形に改められた。
テストの import パスも pyproject.toml の pythonpath = [".", "user_data/strategies"] が唯一の定義。以前は sys.path ハックが 4 箇所に散っていたが、一本化により conftest.py 自体が削除された。「同じ知識を 2 箇所に書かない」はコードだけでなくビルド設定にも貫かれている。
7運用監視 — read-only の見張り塔と自己進化エージェント
監視系は「決定的な CLI(Layer 1)→ 永続メモリ(Layer 2)→ AI エージェント(Layer 3)」の三層。エージェントが関与しても安全なよう、観測は read-only、変更は提案のみという境界が敷かれている。
7.1Layer 1: healthcheck / snapshot — 決定的な観測 CLI
| CLI | 何を見るか | 出力 |
|---|---|---|
python -m ops.healthcheck | デッドマン判定。戦略が毎ループ書く last_candle.txt(最後に処理した足の時刻)を読み、足が 1.5 本分(4h 足なら計 10 時間)前進していなければ STALLED と判定 | exit code 0 / 1(cron・外部監視から使える) |
python -m ops.snapshot | 稼働状態の一括観測:sqlite(read-only 接続)から約定・建玉、ログから ERROR/protection 行、risk_params.py から閾値、キルスイッチ状態、heartbeat 経過 | JSON({heartbeat, trades, kill_switch, thresholds, warnings}) |
snapshot は部分観測耐性を持つ:どれかの読み取りに失敗しても全体は落とさず、warnings[] に失敗を記録して残りを返す。「監視ツールが落ちて監視できない」を避ける設計。
ops/contract.py — 戦略と監視の「実行時契約」
heartbeat ファイルのパス、キルスイッチのパス、timeframe→分換算などは、戦略側と ops 側の両方が知っていなければならない知識。これを ops/contract.py に一本化し、「片方だけ変えてもう片方が黙って壊れる」事故を防ぐ。timeframe は config.json から自動導出され、二重定義しない。
7.2Layer 2-3: 記憶を持つ自己進化エージェント(ops-monitor)
/ops-monitor で起動する監視エージェントは、Layer 1 の CLI を実行して観測値を集め、Layer 2 の永続メモリと照合して分類する:
- NORMAL —
ops/agent/baseline.md(正常の定義)に合致 → 記録のみ - KNOWN —
ops/agent/known-issues.md(KI-NNN でカタログ化された既知の問題)に合致 → 既知として記録 - ANOMALY — どちらでもない → 調査し、結果を
ops/agent/journal/に記録
取引挙動(閾値・config・戦略コード)を変えたいときは ops/agent/proposals/ に提案を書くだけで、適用するかは人間が決める。さらに監視手順そのものの改善は自分の SKILL.md を更新する(Layer 3: 自己進化)。
① 実行時は read-only(発注 API・REST API を呼ばない。sqlite も読み取り専用接続)② 書き込み先は ops/agent/** のみ ③ 取引に関わる変更は propose-only。AI を運用ループに入れつつ、資金に触れる経路を構造的に塞ぐ。人間が forceexit / stop を行うときは Freqtrade の REST API(localhost)を使う(手順は RUNBOOK)。
8設計原則と既知の限界
ここまでの各章を貫く原則は 4 つ。そして「できていないこと」も文書として固定されている。
原則 1: 知識の単一供給源(Single Source of Truth)
| 知識 | 唯一の定義場所 | 参照する側 | 整合の見張り |
|---|---|---|---|
| リスク閾値 | user_data/strategies/risk_params.py | 戦略 / ops/snapshot | test_risk_params.py |
| heartbeat・キルスイッチのパス、timeframe 換算 | ops/contract.py | 戦略 / healthcheck / snapshot | test_contract.py |
| 品質ゲート定義 | scripts/ci.sh | ローカル / pre-push / GitHub Actions | test_quality_gates.py |
| テストの import パス | pyproject.toml の pythonpath | すべてのテスト実行環境 | (定義の一本化自体が対策) |
変更時に「どこを触ればよいか」は README の値の対応表がそのまま答えになる。対応表が短く保てるのは、単一供給源が徹底されている証拠でもある。
原則 2: 依存の向きを揃える
リスクコア(risk.py)は Freqtrade を import しない。ops は戦略を import せず、ファイル(heartbeat / sqlite / risk_params.py の動的ロード)越しに観測する。「中心ほど依存が少なく、周辺が中心に依存する」向きが保たれているため、Freqtrade のバージョンアップや監視系の変更が資金計算のコードに波及しない。
原則 3: 安全は多層・独立で
§4 の 5 層は意図的に仕組みが互いに独立している(シグナル・ストップ・定数・状態ガード・人間の手)。1 つのバグが全層を同時に無効化しにくい。
原則 4: できていないことを文書で固定する
DailyLossGuard の「当日開始 equity」「ピーク equity」はメモリ上にのみあり、プロセス再起動で当日値から再初期化される。大きな損失の直後に再起動すると、ガードの基準が緩む可能性がある。補償:Freqtrade 組込み MaxDrawdown protection との二重化+ops-monitor による記録とエスカレーション。恒久対応(ファイル永続化)は v2 課題。
損切りは Bot プロセスが生きている間しか執行できない(体系ガイド 1.1 の「最重要警告」がこのリポジトリで現実になっている箇所)。補償:ハードストップ −20%+ATR ソフトストップに加え、外部デッドマン(healthcheck)でプロセス停止を 10 時間以内に検知して人間が対応する。
歩んできた道 — 設計記録より
- 2026-06-02 — MVP 設計:Freqtrade 採用・トレンドフォロー戦略・リスクコア分離・ops-monitor 三層構想(
docs/superpowers/specs/に設計 spec が残る) - 2026-06-12 — リファクタ(commit 155dee0):risk_params.py 新設による閾値の単一供給源化、contract.py への契約集約、品質ゲート定義の ci.sh 一本化、pythonpath 一本化。「重複した知識」と「侵入結合」の解消がテーマ。
▶次の一歩 — コードリーディングの推奨順
この地図を持って、実際のコードを以下の順で読むと迷子にならない。チェックはこの端末のブラウザに保存される。
📚リポジトリ内の参照先
運用・品質
ops/RUNBOOK.md— 運用手順(キルスイッチ・forceexit・再起動)docs/quality-gates.md— 品質ゲートの設計思想ops/agent/— baseline / known-issues / journal / proposals(監視エージェントの記憶)
設計記録(docs/superpowers/specs/)
- 2026-06-02 Freqtrade × bitbank トレンドフォロー MVP 設計
- 2026-06-02 自己進化 ops-monitor エージェント設計
- 2026-06-12 リポジトリリファクタ設計(単一供給源化・結合バランス)
学習コンテンツ(docs/)
crypto-trading-bot-roadmap-v2.html— 体系ガイド+段階的ロードマップ(理論編・図解版)crypto-trading-bot-handbook.md— ハンドブック
本ページの挙動説明は 2026-06-12 時点(commit 155dee0)のコード調査に基づく。設定値・閾値・依存バージョンは変わりうるため、実装・変更時は必ず現物のコードと README の値の対応表を一次情報とすること。
結論 — このアーキテクチャの核
- 責務の薄さ:戦略は Freqtrade への薄いアダプタに徹し、資金計算は Decimal 厳密・フレームワーク非依存の
risk.pyに隔離されている。 - 知識の重複を許さない:閾値は
risk_params.py、契約はops/contract.py、品質ゲートはci.shが単一供給源で、テストが整合を tripwire として守る。 - 安全は層で守る:単一の防御に頼らず独立した 5 層+外部デッドマンを重ね、既知の限界(KI-001/KI-002)は隠さず文書化して補償策を添える。
題材: trading-bot リポジトリ(2026-06-12 時点、commit 155dee0)。本文はコードベースの並列調査(11 サブエージェント)に基づいて作成。姉妹編: docs/crypto-trading-bot-roadmap-v2.html