【第1回】環境構築とプロジェクト設計
npm install さようなら — FastAPI + htmx でトレーディングシステムを作る

このシリーズについて
このシリーズでは FastAPI + htmx + Plotly を使って、日経225先物・オプションのリアルタイムトレーディングダッシュボードをゼロから作っていきます。
「Pythonで自動売買システムを作りたいけど、何から手をつければいいか分からない」 「FastAPIは触ったことあるけど、本格的なアプリケーション設計の方法を知りたい」
そんな方に向けた、全8回の実践シリーズです。手を動かしながら一緒に作っていきましょう!
シリーズ目次
| 回 | タイトル |
|---|---|
| 1 | 環境構築とプロジェクト設計(本記事) |
| 2 | htmx でSPA風ダッシュボードを作る |
| 3 | ダークテーマUIとコンポーネント設計 |
| 4 | DB設計とAlembicマイグレーション |
| 5 | 証券APIとの接続 |
| 6 | トレーディングエンジン — 司令塔の設計 |
| 7 | 戦略とリスク管理 |
| 8 | バックテストと本番デプロイ |
目次
- uv で Python プロジェクトを立ち上げる
- ディレクトリ構成を設計する
- config.toml + dataclass で設定管理する
- FastAPI の lifespan パターンで起動・停止を管理する
- Docker でコンテナ化する
- まとめ
このシリーズでは、Plotlyチャート、ポジション一覧、リアルタイム損益がひと目で分かるダッシュボードUIを作っていきます。今回(第1回)では、その土台となる 開発環境・プロジェクト構成・設定管理・アプリケーションの起動ライフサイクル を整えていきます。いきなりUIやロジックに入りたい気持ちをグッとこらえて、まずはしっかりした土台を作りましょう。この土台があるからこそ、後半の複雑な機能が綺麗に組み上がります。
1. uv で Python プロジェクトを立ち上げる
1.1 なぜ uv なのか
Python のパッケージマネージャーは pip、Poetry、PDM など選択肢が多いですが、このプロジェクトでは uv を使います。
uv は Rust で書かれた高速なパッケージマネージャーで、pip と比較して 10〜100倍 高速にパッケージをインストールできます。しかも pyproject.toml をネイティブにサポートしており、venv の作成からパッケージの解決・インストール・ロックまで一貫して行えます。
「速いのは嬉しいけど、枯れてないツールは不安...」という方もいるかもしれません。でも uv は Ruff(Python リンター)を作った Astral 社が開発しており、2024年以降のPythonエコシステムで急速にデファクトになりつつあります。Docker との相性も抜群なので、安心して使ってください。
1.2 uv をインストールする
まだインストールしていない方は、以下のコマンドで一発です。
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Homebrew が好きな方はこちらでもOK
brew install uv
インストール後にバージョンを確認しておきましょう。
uv --version
# uv 0.6.x
1.3 プロジェクトの初期化
それでは、プロジェクトを作っていきます。
mkdir systrade && cd systrade
uv init
uv init を実行すると pyproject.toml が自動生成されます。これを以下のように編集してください。
[project]
name = "systrade"
version = "0.1.0"
description = "日経225先物・オプション自動売買システム"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
"jinja2>=3.1.0",
"httpx>=0.27.0",
"websockets>=13.0",
"pandas>=2.2.0",
"numpy>=1.26.0",
"plotly>=5.22.0",
"python-multipart>=0.0.9",
"sqlalchemy>=2.0.0",
"aiosqlite>=0.20.0",
"alembic>=1.13.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
各パッケージの役割を簡単に整理しておきます。
| パッケージ | 用途 |
|---|---|
fastapi |
WebフレームワークのコアでAPIサーバーを構築する |
uvicorn[standard] |
ASGI サーバー。FastAPI を動かすランタイム |
jinja2 |
HTML テンプレートエンジン。htmx と組み合わせる |
httpx |
非同期 HTTP クライアント。証券API呼び出しに使う |
websockets |
WebSocket 通信。リアルタイムの板情報取得用 |
pandas / numpy |
データ分析。テクニカル指標の計算に必須 |
plotly |
インタラクティブなチャート描画 |
sqlalchemy + aiosqlite |
非同期ORM + SQLite。注文・ポジション履歴を保存 |
alembic |
DBマイグレーション管理 |
:::message
requires-python = ">=3.12" としています。Python 3.12 で追加された tomllib(標準ライブラリ)を設定読み込みで使うためです。3.11 以前の場合は tomli パッケージが別途必要になるので、pyproject.toml に "tomli>=2.0.0;python_version<'3.11'" というマーカー付き依存を追加する手もあります。
:::
1.4 依存パッケージのインストール
以下のコマンドで、仮想環境の作成と依存パッケージのインストールを一気に行えます。
uv sync
これだけです。.venv ディレクトリが作成され、uv.lock にロックファイルが生成されます。pip なら python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt と何ステップも踏むところを、たった1コマンドで済むのは本当に快適ですね。パッケージの解決からインストールまで数秒で完了します。
2. ディレクトリ構成を設計する
2.1 全体構成
プロジェクトのディレクトリ構成を先に決めておきましょう。構成をしっかり考えずにコードを書き始めると、後から「あのモジュールどこだっけ...」と迷子になりがちです。
systrade/
├── src/
│ └── systrade/
│ ├── __init__.py
│ ├── main.py # FastAPI エントリーポイント
│ ├── config.py # 設定管理
│ ├── api/
│ │ ├── __init__.py
│ │ ├── auth.py # Basic認証ミドルウェア
│ │ └── routes.py # APIルーティング
│ ├── broker/
│ │ ├── __init__.py
│ │ ├── base.py # ブローカー抽象基底クラス
│ │ ├── kabu_api.py # kabuステーションAPI実装
│ │ └── kabu_ws.py # WebSocket接続
│ ├── db/
│ │ ├── __init__.py
│ │ ├── engine.py # DB接続管理
│ │ ├── tables.py # テーブル定義
│ │ └── repositories/ # リポジトリパターンでDB操作を分離
│ ├── models/
│ │ ├── __init__.py
│ │ ├── market.py # 市場データ型
│ │ ├── order.py # 注文型
│ │ └── position.py # ポジション型
│ ├── services/
│ │ ├── __init__.py
│ │ ├── trading_engine.py # トレーディングエンジン(司令塔)
│ │ ├── market_data.py # 市場データ管理
│ │ ├── risk_manager.py # リスク管理
│ │ ├── order_manager.py # 注文管理
│ │ └── notifier.py # 通知(Slack等)
│ ├── strategy/
│ │ ├── __init__.py
│ │ ├── base.py # 戦略基底クラス
│ │ ├── futures_trend.py # 先物トレンド戦略
│ │ └── options_seller.py # オプション売り戦略
│ └── utils/
│ └── visualization.py # チャート描画ユーティリティ
├── templates/ # Jinja2 HTMLテンプレート
├── static/ # CSS, JS, 画像
├── alembic/ # DBマイグレーション
├── data/ # SQLite DBファイル等
├── config.toml # アプリケーション設定
├── pyproject.toml
├── uv.lock
├── Dockerfile
├── docker-compose.yml
└── start.sh # 開発用起動スクリプト
2.2 設計の方針
この構成にはいくつかの意図があります。
src/systrade/ レイアウト(src レイアウト)を採用している理由
Pythonプロジェクトの定番パターンとして、ソースコードを src/ ディレクトリに格納する方法があります。こうすることで、テスト実行時に「ローカルのソースを意図せず直接 import してしまう」問題を防げます。パッケージとしてインストールされたもの"だけ"が import されることが保証されるわけですね。
レイヤー構成の意図
api/ → 外との窓口(HTTPエンドポイント)
services/ → ビジネスロジック(エンジン・リスク管理)
broker/ → 外部API連携
strategy/ → 売買戦略
models/ → データ型定義
db/ → 永続化層
この構成は、いわゆる レイヤードアーキテクチャ を意識しています。上位レイヤー(api)が下位レイヤー(services, broker)を呼び出す一方向の依存関係を保つことで、変更の影響範囲を限定できます。
例えば、証券会社のAPIが変わっても broker/ だけ修正すればよく、戦略ロジックや UI には影響しません。トレーディングシステムでは証券会社の仕様変更が頻繁にあるので、この分離は特に重要です。
3. config.toml + dataclass で設定管理する
3.1 なぜ TOML なのか
設定ファイルのフォーマットには JSON、YAML、.env、TOML などの選択肢があります。このプロジェクトでは TOML を選びました。
理由はシンプルです。
- コメントが書ける: JSON はコメント不可。設定にはコメントが欲しい
- Python 3.11+ で標準ライブラリに入った:
tomllibで外部依存なしに読める - ネストが自然: YAML のインデント地獄にならない
- 型が明確: 文字列、整数、浮動小数点、ブーリアンが区別される
特にトレーディングシステムでは「この値は何のためのパラメータか」をコメントで残せることが非常に大切です。3ヶ月後の自分が設定値を見たとき、コメントがあるかないかで理解速度が段違いです。
3.2 config.toml を書く
まずは設定ファイルを作りましょう。プロジェクトルートに config.toml を配置します。
[trading]
capital = 3000000 # 軍資金(円) — プラン算出の基準
risk_plan = "balanced" # conservative / balanced / aggressive
[broker]
base_url = "http://localhost:18080/kabusapi"
password = ""
sandbox = false
[risk]
max_position_futures = 5
max_position_options = 10
max_loss_per_day = 50000 # 日次損失上限 5万円 → 超えたら全停止
stop_loss_pct = 3.0 # 損切り 3%(最優先)
take_profit_pct = 5.0 # 利確 5%
trailing_stop_pct = 1.5 # トレーリングストップ(高値から1.5%下落で利確)
use_trailing_stop = true
daily_profit_target = 100000 # 日次目標利益 10万円 → 達成したらその日は取引停止
[strategy.futures_trend]
enabled = true
product = "mini"
sma_short = 5
sma_long = 25
rsi_period = 14
rsi_oversold = 30
rsi_overbought = 70
[strategy.futures_trend.position_sizer]
enabled = true
min_qty = 1
max_qty = 2
confidence_threshold = 0.6
[strategy.options_seller]
enabled = true
style = "strangle"
target_delta = 0.20
min_iv = 15.0
days_to_exit = 3
[strategy.delta_hedge]
enabled = true
rebalance_threshold = 0.3
hedge_product = "mini"
[options]
chain_refresh_interval = 300 # オプションチェーン更新間隔(秒)
risk_free_rate = 0.005
deriv_month = 0 # 0=期近
各セクションの役割を簡単に説明します。
| セクション | 内容 |
|---|---|
[trading] |
資金量やリスクプランなど、システム全体の基本設定 |
[broker] |
証券会社APIの接続先。パスワードは後述の環境変数で上書きする |
[risk] |
リスク管理パラメータ。損切り・利確・日次上限など |
[strategy.*] |
各売買戦略のパラメータ。戦略ごとにサブテーブルで分離 |
[options] |
オプション固有の設定(チェーン更新間隔、無リスク金利など) |
:::message alert
[broker] セクションの password は空文字にしています。パスワードを config.toml に直書きしてはいけません。Git にコミットされてしまうリスクがあるためです。後述する環境変数オーバーライドの仕組みで、実行時に安全に注入します。
:::
3.3 dataclass で型安全に読み込む
次に、この TOML ファイルを Python の dataclass にマッピングするコードを書きます。ファイルは src/systrade/config.py です。
ここで大事な設計ポイントがあります。「なぜ Pydantic ではなく dataclass なのか」。
Pydantic は素晴らしいバリデーションライブラリですが、設定読み込みにはやや大げさです。dataclass なら標準ライブラリだけで済み、デフォルト値の指定も簡潔に書けます。「外部依存を最小限にする」というのは、長期運用するシステムでは重要な判断です。
まずは、設定ファイルの各セクションに対応する dataclass を定義していきましょう。段階的に見ていきます。
ステップ1: 基本の設定クラスを定義する
"""設定読み込み"""
import os
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
CONFIG_PATH = Path(__file__).resolve().parent.parent.parent / "config.toml"
# DB デフォルトパス(プロジェクトルート/data/systrade.db)
DEFAULT_DB_PATH = str(
Path(__file__).resolve().parent.parent.parent / "data" / "systrade.db"
)
@dataclass
class TradingConfig:
capital: float = 3_000_000
risk_plan: str = "balanced"
@dataclass
class BrokerConfig:
base_url: str = "http://localhost:18080/kabusapi"
password: str = ""
sandbox: bool = False
TradingConfig と BrokerConfig、それぞれ TOML の [trading] と [broker] セクションに対応しています。dataclass のフィールドにデフォルト値を設定しておくことで、config.toml にキーが存在しなくても安全にフォールバックできます。
CONFIG_PATH は __file__(このスクリプト自身のパス)から相対的にプロジェクトルートを辿って config.toml を見つけています。こうすることで、どこからスクリプトを実行しても正しいパスが解決されます。
ステップ2: リスク管理と戦略の設定クラスを追加する
@dataclass
class RiskConfig:
max_position_futures: int = 5
max_position_options: int = 10
max_loss_per_day: float = 50_000
stop_loss_pct: float = 3.0
take_profit_pct: float = 5.0
trailing_stop_pct: float = 1.5
use_trailing_stop: bool = True
daily_profit_target: float = 100_000
@dataclass
class PositionSizerConfig:
enabled: bool = True
min_qty: int = 1
max_qty: int = 3
weight_trend: float = 0.3
weight_confluence: float = 0.3
weight_daily_pnl: float = 0.2
weight_volatility: float = 0.2
confidence_threshold: float = 0.6
@dataclass
class FuturesTrendConfig:
enabled: bool = True
product: str = "mini"
sma_short: int = 5
sma_long: int = 25
rsi_period: int = 14
rsi_oversold: int = 30
rsi_overbought: int = 70
position_sizer: PositionSizerConfig = field(
default_factory=PositionSizerConfig
)
@dataclass
class OptionSellerConfig:
enabled: bool = True
style: str = "strangle"
target_delta: float = 0.20
min_iv: float = 15.0
days_to_exit: int = 3
@dataclass
class DeltaHedgeConfig:
enabled: bool = True
rebalance_threshold: float = 0.3
hedge_product: str = "mini"
@dataclass
class OptionsConfig:
chain_refresh_interval: int = 300
risk_free_rate: float = 0.005
deriv_month: int = 0
ここで注目してほしいのが FuturesTrendConfig の中にある position_sizer フィールドです。field(default_factory=PositionSizerConfig) とすることで、dataclass のネスト を実現しています。
TOML 側の [strategy.futures_trend.position_sizer] がこのネストされた dataclass に対応するわけです。設定のネストが深くなっても、Python 側のオブジェクトツリーとして自然にアクセスできます。
# こんな感じでアクセスできる
config.strategy.futures_trend.position_sizer.max_qty # => 2
ステップ3: トップレベルの AppConfig と読み込み関数
@dataclass
class StrategyConfig:
futures_trend: FuturesTrendConfig = field(
default_factory=FuturesTrendConfig
)
options_seller: OptionSellerConfig = field(
default_factory=OptionSellerConfig
)
delta_hedge: DeltaHedgeConfig = field(
default_factory=DeltaHedgeConfig
)
@dataclass
class AppConfig:
trading: TradingConfig = field(default_factory=TradingConfig)
broker: BrokerConfig = field(default_factory=BrokerConfig)
risk: RiskConfig = field(default_factory=RiskConfig)
strategy: StrategyConfig = field(default_factory=StrategyConfig)
options: OptionsConfig = field(default_factory=OptionsConfig)
def load_config(path: Path | None = None) -> AppConfig:
"""config.toml を読み込んで AppConfig を返す"""
path = path or CONFIG_PATH
if not path.exists():
return AppConfig()
with open(path, "rb") as f:
raw = tomllib.load(f)
trading = TradingConfig(**raw.get("trading", {}))
broker = BrokerConfig(**raw.get("broker", {}))
risk = RiskConfig(**raw.get("risk", {}))
strat_raw = raw.get("strategy", {})
ft_raw = dict(strat_raw.get("futures_trend", {}))
ps_raw = ft_raw.pop("position_sizer", {})
strategy = StrategyConfig(
futures_trend=FuturesTrendConfig(
**ft_raw,
position_sizer=PositionSizerConfig(**ps_raw),
),
options_seller=OptionSellerConfig(
**strat_raw.get("options_seller", {})
),
delta_hedge=DeltaHedgeConfig(
**strat_raw.get("delta_hedge", {})
),
)
options = OptionsConfig(**raw.get("options", {}))
cfg = AppConfig(
trading=trading,
broker=broker,
risk=risk,
strategy=strategy,
options=options,
)
# 環境変数オーバーライド
if env_pw := os.environ.get("KABU_API_PASSWORD"):
cfg.broker.password = env_pw
if env_url := os.environ.get("KABU_API_BASE_URL"):
cfg.broker.base_url = env_url
return cfg
load_config() 関数のポイントを整理します。
ファイルが無ければデフォルト値で返す:
if not path.exists(): return AppConfig()により、config.toml が存在しなくても起動できます。開発初期やテスト時に便利です。ネストされた TOML の展開:
[strategy.futures_trend.position_sizer]のようなネストされたテーブルは、tomllibが辞書のネストとして返します。ft_raw.pop("position_sizer", {})で position_sizer 部分を分離してから、それぞれの dataclass に展開しています。環境変数オーバーライド: ファイルから読み込んだ後、環境変数
KABU_API_PASSWORDとKABU_API_BASE_URLがあればそちらで上書きします。これが先ほど「パスワードは config.toml に書くな」と言った理由の受け皿です。
[設定の優先順位]
環境変数 > config.toml > dataclass のデフォルト値
この3段階のフォールバック構成は、12-Factor App の原則にも沿っています。本番環境では環境変数で注入し、開発環境では config.toml で手軽に試す、という使い分けができます。
4. FastAPI の lifespan パターンで起動・停止を管理する
4.1 lifespan とは
FastAPI(正確には Starlette)には lifespan というライフサイクル管理の仕組みがあります。アプリケーションの起動時と停止時に実行したい処理を、asynccontextmanager でひとまとめに書けるパターンです。
以前は @app.on_event("startup") / @app.on_event("shutdown") というデコレータが使われていましたが、これは 非推奨(deprecated) になっています。新しいプロジェクトでは必ず lifespan パターンを使いましょう。
4.2 ミニマルな main.py から始める
まずは最小構成の main.py を書いてみます。
"""FastAPIアプリケーション — 最小構成"""
import logging
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from src.systrade.config import load_config
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
logger = logging.getLogger(__name__)
BASE_DIR = Path(__file__).resolve().parent.parent.parent
config = load_config()
@asynccontextmanager
async def lifespan(app: FastAPI):
"""アプリケーション起動・停止処理"""
logger.info("システムトレード起動")
logger.info("ブローカーURL: %s", config.broker.base_url)
# 設定をアプリケーション状態に保存
app.state.config = config
yield # ← ここでアプリが稼働する
logger.info("システムトレード停止")
app = FastAPI(
title="日経225先物・オプション自動売買",
lifespan=lifespan,
)
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
templates = Jinja2Templates(directory=BASE_DIR / "templates")
app.state.templates = templates
@app.get("/health")
async def health(request: Request):
return JSONResponse({"status": "ok"})
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse(
"dashboard.html", {"request": request, "config": config}
)
これだけでも動くサーバーになります。ここから機能を追加していきましょう。
4.3 lifespan にDB初期化とクリーンアップを追加する
実際のプロジェクトでは、起動時にDBの初期化、停止時にリソースのクリーンアップが必要です。完全版の lifespan は以下のようになっています。
@asynccontextmanager
async def lifespan(app: FastAPI):
"""アプリケーション起動・停止処理"""
logger.info("システムトレード起動")
logger.info("ブローカーURL: %s", config.broker.base_url)
# DB初期化
db_path = os.environ.get("DATABASE_PATH", DEFAULT_DB_PATH)
await init_db(db_path)
# Notifier初期化
notifier = Notifier()
app.state.notifier = notifier
# 戦略・サービスの初期化(ブローカー接続はダッシュボードから手動)
app.state.config = config
app.state.recent_signals = []
app.state.strategies = []
yield
# クリーンアップ: エンジン稼働中なら安全に停止
if hasattr(app.state, "trading_engine") and app.state.trading_engine:
logger.info("エンジン停止処理開始(lifespan shutdown)")
await app.state.trading_engine.stop(
cancel_orders=True, close_positions=False
)
else:
# エンジン未使用時の個別クリーンアップ
if hasattr(app.state, "broker"):
await app.state.broker.close()
if hasattr(app.state, "market_data"):
await app.state.market_data.stop()
# Notifierクローズ
await notifier.close()
# DB接続クローズ
await close_db()
logger.info("システムトレード停止")
yield の前が 起動処理、後が 停止処理 です。この構造が asynccontextmanager の美しいところですね。try/finally を意識しなくても、確実にクリーンアップが走ります。
停止処理で特に重要なのは以下の部分です。
# クリーンアップ: エンジン稼働中なら安全に停止
if hasattr(app.state, "trading_engine") and app.state.trading_engine:
await app.state.trading_engine.stop(
cancel_orders=True, close_positions=False
)
トレーディングシステムでは「未約定の注文をキャンセルする」「でもポジションは勝手に閉じない」という停止時の振る舞いが超重要です。誤ってポジションをクローズしてしまったら大損する可能性がありますからね。cancel_orders=True, close_positions=False というパラメータで、この挙動を明示的にコントロールしています。
4.4 app.state をハブにする設計
コード中で app.state.config = config や app.state.notifier = notifier のように、app.state にオブジェクトを格納しています。
これは FastAPI / Starlette が提供するアプリケーションスコープの状態管理機構で、リクエストハンドラから request.app.state.config のようにアクセスできます。グローバル変数を使うよりもスコープが明確で、テスト時にモックを差し込みやすいメリットがあります。
# ルートハンドラからのアクセス例
@app.get("/health")
async def health(request: Request):
engine = getattr(request.app.state, "trading_engine", None)
if engine and engine.state.value == "error":
return JSONResponse(
{"status": "error", "error": engine._error_message},
status_code=503,
)
return JSONResponse({"status": "ok"})
ヘルスチェックエンドポイントでは、トレーディングエンジンの状態を確認し、エラー状態なら 503 を返しています。Docker の HEALTHCHECK やロードバランサーがこのエンドポイントを叩いて、アプリケーションの生死を監視できるわけです。
4.5 開発サーバーの起動
ここまで書いたら、実際に動かしてみましょう。起動スクリプト start.sh を作ります。
#!/bin/bash
cd "$(dirname "$0")"
exec uv run uvicorn src.systrade.main:app --host 0.0.0.0 --port 8001 --reload
chmod +x start.sh
./start.sh
uv run を使うことで、uv sync で作った仮想環境を自動的に使ってくれます。source .venv/bin/activate を手動で実行する必要はありません。--reload フラグを付けているので、コードを変更するとサーバーが自動的にリスタートします。開発中はこれが非常に便利です。
ブラウザで http://localhost:8001/health にアクセスすると、以下のレスポンスが返ってくるはずです。
{"status": "ok"}
ブラウザまたは curl http://localhost:8001/health で動作確認できます。
5. Docker でコンテナ化する
5.1 なぜ Docker を使うのか
「ローカルで動くならそれでいいのでは?」と思うかもしれません。でもトレーディングシステムは 本番環境での安定稼働 が何より大切です。「自分のマシンでは動くけど、サーバーでは動かない」は絶対に避けたい。
Docker でコンテナ化することで、開発環境と本番環境の差異をなくし、再現性のあるデプロイが実現できます。
5.2 マルチステージビルドの Dockerfile
FROM python:3.12-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
COPY src/ src/
COPY templates/ templates/
COPY static/ static/
COPY alembic.ini alembic.ini
COPY alembic/ alembic/
RUN mkdir -p /app/data
EXPOSE 8001
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8001/health')"
CMD ["python", "-m", "uvicorn", "src.systrade.main:app", \
"--host", "0.0.0.0", "--port", "8001", "--workers", "1"]
この Dockerfile にはいくつかのこだわりがあります。順番に見ていきましょう。
マルチステージビルド
FROM python:3.12-slim AS builder # ステージ1: ビルド用
...
FROM python:3.12-slim # ステージ2: 実行用
COPY --from=builder /app/.venv /app/.venv
ビルド用のステージで依存パッケージをインストールし、実行用のステージには .venv ディレクトリだけをコピーしています。ビルドツール(uv、コンパイラ等)は実行用イメージに含まれないため、最終イメージのサイズを小さく保てます。
uv を Docker で使うテクニック
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
uv の公式 Docker イメージからバイナリだけをコピーしてくる方法です。pip install uv するより高速で、ビルドキャッシュとの相性も良いです。
--frozen フラグ
RUN uv sync --frozen --no-dev --no-install-project
--frozen は「ロックファイル(uv.lock)を一切更新せず、完全に一致する依存関係をインストールする」というオプションです。CI/CD やDockerビルドでは、意図しないバージョン変更を防ぐために必ず付けましょう。--no-dev で開発用パッケージを除外し、イメージを軽量化しています。
HEALTHCHECK
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8001/health')"
30秒間隔でヘルスチェックを行い、5秒以内に応答がなければ異常とみなします。--start-period=30s は起動猶予時間で、アプリケーションの初期化が完了するまでチェックを待ちます。先ほど main.py に書いた /health エンドポイントがここで活きてきますね。
workers=1 の理由
CMD ["python", "-m", "uvicorn", "src.systrade.main:app", \
"--host", "0.0.0.0", "--port", "8001", "--workers", "1"]
ワーカー数を1にしているのは、トレーディングシステムの特性上の判断です。複数ワーカーにすると、アプリケーション状態(app.state)がワーカー間で共有されず、ポジション管理やエンジンの状態が矛盾する恐れがあります。シングルワーカーで十分な処理能力がありますし、状態管理の安全性を優先しています。
5.3 docker-compose.yml
services:
systrade:
build: .
container_name: systrade
ports:
- "8001:8001"
volumes:
- ./data:/app/data
- ./config.toml:/app/config.toml:ro
environment:
- KABU_API_PASSWORD=${KABU_API_PASSWORD}
- KABU_API_BASE_URL=${KABU_API_BASE_URL:-http://host.docker.internal:18080/kabusapi}
- DATABASE_PATH=/app/data/systrade.db
- SLACK_WEBHOOK_URL=${SLACK_WEBHOOK_URL:-}
- AUTH_USERNAME=${AUTH_USERNAME:-}
- AUTH_PASSWORD=${AUTH_PASSWORD:-}
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"
docker-compose.yml のポイントを解説します。
volumes(データ永続化)
volumes:
- ./data:/app/data # DBファイルをホストに永続化
- ./config.toml:/app/config.toml:ro # 設定ファイルを読み取り専用でマウント
./data:/app/data で SQLite のDBファイルをホスト側に永続化しています。コンテナを再起動してもデータが消えません。config.toml は :ro(read-only)でマウントし、コンテナ内から誤って書き換えられないようにしています。
environment(環境変数の注入)
environment:
- KABU_API_PASSWORD=${KABU_API_PASSWORD}
- KABU_API_BASE_URL=${KABU_API_BASE_URL:-http://host.docker.internal:18080/kabusapi}
ホストマシンの環境変数(または .env ファイル)から値を注入します。${VAR:-default} 構文でデフォルト値も指定できます。
ここで一点注意が必要なのは KABU_API_BASE_URL のデフォルト値です。http://host.docker.internal:18080/kabusapi となっていますね。
kabuステーション(証券会社のAPI)はローカルマシン上で動作するデスクトップアプリです。Docker コンテナからホストマシンの localhost にアクセスするには host.docker.internal という特殊なホスト名を使う必要があります。この辺りは extra_hosts の設定とセットで機能します。
extra_hosts:
- "host.docker.internal:host-gateway"
Linux 環境では host.docker.internal がデフォルトで解決されないため、host-gateway へのマッピングを明示的に追加しています。macOS では不要ですが、書いておいても害はありません。
restart: unless-stopped
手動で停止しない限り、コンテナがクラッシュしても自動的に再起動します。トレーディングシステムはマーケットが開いている間、常に稼働し続ける必要があるので、この設定は必須です。
5.4 ビルドと起動
# .env ファイルを作成(実際のパスワードを設定)
cat <<EOF > .env
KABU_API_PASSWORD=your_password_here
AUTH_USERNAME=admin
AUTH_PASSWORD=your_dashboard_password
EOF
# ビルド & 起動
docker compose up --build -d
# ログを確認
docker compose logs -f
docker compose logs で起動ログを確認し、「システムトレード起動」が表示されればOKです。
ブラウザで http://localhost:8001/health にアクセスして {"status": "ok"} が返ってくれば、Docker 環境のセットアップは完了です!
6. まとめ
第1回では、トレーディングダッシュボードの土台となる環境構築とプロジェクト設計を行いました。
- uv を使うことで、Python の依存管理が高速かつシンプルになる。
uv sync一発で仮想環境の作成からパッケージインストールまで完了する - ディレクトリ構成 は
src/レイアウト + レイヤードアーキテクチャを採用。api / services / broker / strategy / models / dbの6層でコードを整理する - config.toml + dataclass による設定管理で、型安全かつコメント付きの設定を実現。環境変数によるオーバーライドでセキュリティも確保する
- FastAPI の lifespan パターン で起動・停止処理を一元管理する。特にトレーディングシステムでは停止時の安全なクリーンアップ(注文キャンセル等)が重要
- Docker のマルチステージビルド で、uv との連携・軽量なイメージ・ヘルスチェックまで含めた本番対応のコンテナを構築する
- docker-compose.yml では
host.docker.internalを使った証券APIへの接続、環境変数による秘密情報の注入、データの永続化を設定する
次回予告
第2回では、いよいよ htmx を導入して SPA風のダッシュボード を構築していきます。
htmx を使えば、JavaScript をほとんど書かずに、サーバーサイドレンダリングの HTML だけでリッチなインタラクションを実現できます。ページ遷移なしのタブ切り替え、部分更新、フォーム送信後のリアルタイム反映など、まるでSPAのような操作感をシンプルなアーキテクチャで作っていく予定です。
最後まで読んでくださりありがとうございました! この記事がお役に立ったら、いいねやフォローで応援していただけると嬉しいです。質問やフィードバックもコメント欄でお待ちしています。