【第3回】ダークテーマUIとコンポーネント設計
npm install さようなら — FastAPI + htmx でトレーディングシステムを作る

こんにちは、今回もシリーズ「npm install さようなら — FastAPI + htmx でトレーディングシステムを作る」にお越しいただきありがとうございます。
第3回となる今回は、ダークテーマUIとコンポーネント設計をテーマにお届けします。トレーディングシステムの「見た目」を本格的に作り込んでいきましょう。
CSSカスタムプロパティによるテーマ設計から始めて、カードやテーブル、損益の色分け、レスポンシブ対応、そしてPlotlyチャートとのダークテーマ連携まで、一気に仕上げていきます。
このシリーズについて
本シリーズは全8回で、FastAPI + htmx を使ったリアルタイムトレーディングダッシュボードをゼロから構築していきます。
| # | タイトル |
|---|---|
| 1 | 環境構築とプロジェクト設計 |
| 2 | htmx でSPA風ダッシュボードを作る |
| 3 | ダークテーマUIとコンポーネント設計(本記事) |
| 4 | DB設計とAlembicマイグレーション |
| 5 | 証券APIとの接続 |
| 6 | トレーディングエンジン -- 司令塔の設計 |
| 7 | 戦略とリスク管理 |
| 8 | バックテストと本番デプロイ |
目次
- なぜダークテーマなのか
- CSSカスタムプロパティでテーマを定義する
- ベースレイアウトとヘッダー
- UIコンポーネントを段階的に構築する
- 損益の色分けとバッジシステム
- Plotlyチャートのダークテーマ連携
- 証拠金利用率プログレスバー
- レスポンシブデザイン対応
- まとめ
1. なぜダークテーマなのか
トレーディングダッシュボードにダークテーマを採用する理由は、単なる見た目のかっこよさだけではありません。
長時間モニタリングでの目の疲労軽減 が最大の理由です。トレーダーはマーケットが開いている間、何時間も画面を見続けます。白背景に黒文字の画面を長時間凝視すると、目への負担がかなり大きくなってしまいます。
また、数値データの視認性 という観点でも、ダークテーマは優秀です。暗い背景の上に、利益は緑、損失は赤といった色分けを施すと、明るい背景よりもコントラストが際立ち、瞬時に状況を把握しやすくなります。
実際に、TradingView、Bloomberg Terminal、thinkorswim といったプロ向けトレーディングツールのほとんどがダークテーマをデフォルトで採用しています。私たちのダッシュボードも、この「業界標準」に合わせていきましょう。
2. CSSカスタムプロパティでテーマを定義する
まず最初に、テーマの「土台」となるCSSカスタムプロパティ(CSS変数)を定義します。ここで定義した変数を全コンポーネントで使い回すことで、一貫性のあるデザインを保てます。
2.1 リセットとルート変数
/* ===== リセット & ベース ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f172a;
--surface: #1e293b;
--border: #334155;
--text: #e2e8f0;
--text-muted: #94a3b8;
--primary: #3b82f6;
--primary-hover: #2563eb;
--green: #22c55e;
--red: #ef4444;
--yellow: #eab308;
--radius: 8px;
}
各変数の役割を整理しておきましょう。
| 変数名 | 色コード | 用途 |
|---|---|---|
--bg |
#0f172a |
ページ全体の背景色(最も暗い色) |
--surface |
#1e293b |
カードやサイドバーなど、一段浮かせた要素の背景色 |
--border |
#334155 |
ボーダーやセパレーターの色 |
--text |
#e2e8f0 |
メインのテキスト色(明るいグレー) |
--text-muted |
#94a3b8 |
ラベルや補足テキストの色(抑えめのグレー) |
--primary |
#3b82f6 |
アクセントカラー(ボタン、リンク、アクティブ要素) |
--green |
#22c55e |
利益・買いを表す色 |
--red |
#ef4444 |
損失・売りを表す色 |
--yellow |
#eab308 |
警告・キャンセル状態を表す色 |
--radius |
8px |
全コンポーネント共通の角丸サイズ |
ここで使っている色はTailwind CSSのカラーパレット(Slate系)をベースにしています。ダークテーマでは、背景色に純粋な #000000 を使うよりも、少し青みがかったダークネイビー系の色を使うと、画面全体がやわらかい印象になります。
ポイント: --bg と --surface の2段階の背景色を用意しているのがミソです。これにより、カードやパネルを「浮いた」ように見せることができます。
2.2 ボディの基本スタイル
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Hiragino Sans", "Noto Sans JP", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
フォントスタックは、macOS/Windows/Linux それぞれのシステムフォントを優先的に使い、日本語フォントとして Hiragino Sans(macOS)と Noto Sans JP(Linux/Android)を指定しています。システムフォントを使うことで、Webフォントの読み込みを待つ必要がなくなり、初回描画が高速になります。
3. ベースレイアウトとヘッダー
3.1 HTMLテンプレート(base.html)
Jinja2のテンプレート継承を使った base.html の全体像はこうなっています。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>日経225 システムトレード</title>
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
<script src="https://unpkg.com/htmx-ext-json-enc@2.0.1/json-enc.js"></script>
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<header>
<h1>日経225 システムトレード</h1>
<p class="subtitle">先物デイトレード + オプション売り + デルタヘッジ</p>
</header>
<div class="app-layout">
{% block sidebar %}{% endblock %}
<main class="content">
{% block content %}{% endblock %}
</main>
</div>
<script>
function renderChart(elementId, chartJson) {
if (!chartJson || chartJson === '{}') return;
try {
const data = JSON.parse(chartJson);
Plotly.newPlot(elementId, data.data, data.layout, {responsive: true});
} catch(e) {
console.error('Chart render error:', e);
}
}
</script>
</body>
</html>
ここで注目してほしい点がいくつかあります。
- htmx と json-enc 拡張: htmx本体に加えて、JSONエンコーディング拡張を読み込んでいます。これはフォーム送信時にJSONでPOSTするためです(第2回で解説済み)。
- Plotly CDN: チャート描画にPlotlyを使います。テーマ連携は後ほど詳しく解説します。
renderChartヘルパー関数: サーバー側でJSON化されたPlotlyチャートデータを受け取り、指定したDOM要素にレンダリングする共通関数です。{responsive: true}オプションにより、ブラウザのリサイズに自動追従します。app-layout: サイドバーとメインコンテンツをFlexboxで横並びにするレイアウトコンテナです。
3.2 ヘッダーのCSS
header {
background: linear-gradient(135deg, #1e3a5f, #0f172a);
border-bottom: 1px solid var(--border);
color: #fff;
padding: 1.2rem 2rem;
text-align: center;
}
header h1 { font-size: 1.5rem; font-weight: 700; }
header .subtitle { font-size: 0.85rem; opacity: 0.7; margin-top: 0.2rem; }
ヘッダーには linear-gradient で微妙なグラデーションをかけています。単色の背景よりも深みが出て、高級感のある印象になりますね。サブタイトルは opacity: 0.7 で控えめに表示し、主従関係を明確にしています。
3.3 アプリレイアウト
.app-layout {
display: flex;
min-height: calc(100vh - 80px);
}
.content {
flex: 1;
max-width: 1200px;
margin: 1.5rem auto;
padding: 0 1rem;
min-width: 0;
}
min-height: calc(100vh - 80px) でヘッダーの高さを差し引いた残り全体をレイアウト領域にしています。min-width: 0 は、Flexboxの子要素がコンテンツに引っ張られて親コンテナを突き破るのを防ぐ定番テクニックです。テーブルやチャートのような「幅を取りがちな要素」を含む場合は、これを忘れると横スクロールが発生してしまいます。
このレイアウトにより、左側にサイドバー、右側にメインコンテンツが横並びになるFlexbox構成が完成します。
4. UIコンポーネントを段階的に構築する
ここからが本記事の核心部分です。ダッシュボードを構成する各コンポーネントのCSSとHTMLテンプレートを、一つずつ見ていきましょう。
4.1 カードコンポーネント
ダッシュボード上のほぼすべてのセクションは「カード」の中に収まります。カードは情報のグルーピングに使う、最も基本的なコンポーネントです。
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1.2rem 1.5rem;
margin-bottom: 1.2rem;
}
.card h2 {
font-size: 1rem;
margin-bottom: 0.8rem;
color: var(--primary);
border-bottom: 1px solid var(--border);
padding-bottom: 0.5rem;
}
デザイン意図:
- 背景に
--surface(#1e293b)を使い、ページ背景--bg(#0f172a)より一段明るくして「浮いた」感じを出す - カードの見出し
h2はアクセントカラー(--primary)にして、セクションの区切りを視覚的に明確にする border-bottomで見出しとコンテンツの間に区切り線を入れる
4.2 サイドバーナビゲーション
サイドバーは画面左に固定(position: sticky)で配置されます。htmxのタブ切り替えと連動して、アクティブなナビゲーション項目をハイライトします。
.sidebar {
width: 220px;
flex-shrink: 0;
background: var(--surface);
border-right: 1px solid var(--border);
position: sticky;
top: 0;
height: calc(100vh - 80px);
overflow-y: auto;
display: flex;
flex-direction: column;
}
.nav-item {
display: block;
width: 100%;
padding: 0.7rem 1.2rem;
border: none;
background: none;
font-size: 0.9rem;
cursor: pointer;
color: var(--text-muted);
text-align: left;
border-left: 3px solid transparent;
transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.nav-item:hover {
color: var(--text);
background: rgba(59, 130, 246, 0.05);
}
.nav-item.active {
color: var(--primary);
border-left-color: var(--primary);
background: rgba(59, 130, 246, 0.1);
font-weight: 600;
}
デザイン意図:
border-left: 3px solidでアクティブ状態を左端のラインで表現する。これはVS Codeなどのエディタでもよく見るパターンですね- ホバー時にはごく薄い青い背景色(
rgba(59, 130, 246, 0.05))で反応を返す。微妙すぎるくらいがちょうどいいです transitionでアニメーションを付けることで、ぬるっとした切り替え体験になります
4.3 テーブルコンポーネント
ポジション一覧や注文一覧など、テーブルはトレーディングダッシュボードの主役です。
table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
}
th, td {
padding: 0.5rem 0.6rem;
text-align: right;
border-bottom: 1px solid var(--border);
}
th {
background: var(--bg);
font-weight: 600;
color: var(--text-muted);
font-size: 0.8rem;
position: sticky;
top: 0;
}
td:first-child, th:first-child { text-align: left; }
デザイン意図:
- 数値データは
text-align: rightで右寄せ。金額や数量は桁を揃えて表示するのがトレーディングUIの鉄則です - ただし最初のカラム(銘柄名など)は
text-align: leftで左寄せ。人間の目は左から右に読むので、「何の」データかをまず認識してから数値を読むという自然な流れになります thにposition: sticky; top: 0を付けることで、テーブルが長くなってもヘッダーが追従します
ポジションテーブルのJinja2テンプレートはこうなっています。
{% if positions %}
<table>
<thead>
<tr>
<th>銘柄</th>
<th>売買</th>
<th>数量</th>
<th>平均価格</th>
<th>現在価格</th>
<th>含み損益</th>
</tr>
</thead>
<tbody>
{% for pos in positions %}
<tr>
<td>{{ pos.symbol }}</td>
<td class="{{ 'buy' if pos.side.value == 'buy' else 'sell' }}">
{{ '買' if pos.side.value == 'buy' else '売' }}
</td>
<td>{{ pos.qty }}</td>
<td>{{ "{:,.0f}".format(pos.avg_price) }}</td>
<td>{{ "{:,.0f}".format(pos.current_price) }}</td>
<td class="{{ 'profit' if pos.unrealized_pnl >= 0 else 'loss' }}">
{{ "{:+,.0f}".format(pos.unrealized_pnl) }}円
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="empty">ポジションなし</p>
{% endif %}
Jinja2の "{:+,.0f}".format() を使って、プラスの値には + 記号を付け、3桁区切りのカンマも挿入 しています。これにより +1,250,000円 のような見やすいフォーマットになります。
ここで一点注意が必要なのは、pos.side.value のようにPythonのEnumの .value を参照している点です。サーバー側でデータモデルを設計する際に、side を文字列リテラルではなく Enum にしておくと、テンプレート側で明示的に .value を取り出す必要があります。
4.4 ボタンコンポーネント
ボタンは用途に応じて複数のバリエーションを用意しています。
.btn {
display: inline-block;
padding: 0.5rem 1.5rem;
border: none;
border-radius: var(--radius);
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary { background: var(--primary); color: #fff; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-secondary { background: #475569; color: #fff; }
.btn-secondary:hover { background: #64748b; }
.btn-sm { padding: 0.25rem 0.75rem; font-size: 0.8rem; }
.btn-on { background: var(--green); color: #fff; }
.btn-off { background: var(--red); color: #fff; }
さらに、エンジン制御用のボタンも定義しています。
.btn-start { background: var(--green); color: #fff; }
.btn-start:hover { background: #16a34a; }
.btn-stop { background: #475569; color: #fff; }
.btn-stop:hover { background: #64748b; }
.btn-danger { background: var(--red); color: #fff; }
.btn-danger:hover { background: #dc2626; }
デザイン意図:
btn-primaryは青(通常のアクション)、btn-on/btn-startは緑(開始・有効化)、btn-off/btn-dangerは赤(停止・危険な操作)- この色分けはトレーディングの文脈で直感的に意味が伝わります。トレーダーにとって「緑 = Go / 赤 = Stop」は世界共通です
btn-smはテーブル行内に収まる小さなボタン用
4.5 フォームコンポーネント
バックテストのパラメータ入力などに使うフォームは、グリッドレイアウトで整列させています。
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 0.8rem;
margin-bottom: 1rem;
}
.form-group { display: flex; flex-direction: column; gap: 0.3rem; }
.form-group label {
font-size: 0.82rem;
font-weight: 600;
color: var(--text-muted);
}
input, select {
padding: 0.45rem 0.6rem;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 0.9rem;
background: var(--bg);
color: var(--text);
transition: border-color 0.15s;
}
input:focus, select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
デザイン意図:
repeat(auto-fill, minmax(180px, 1fr))で、画面幅に応じてフォーム項目が自動的に折り返します。パラメータが多い場合でもきれいに並びます- フォーカス時には
box-shadowで青い光彩(グロー)を付けて、今どの入力欄にフォーカスがあるかを明確にします - 入力欄の背景は
--bg(最も暗い色)にして、カードの--surfaceよりさらに沈ませています。この「3段階の深度」(bg < surface < text)がダークテーマの奥行き感を生みます
4.6 ローディングインジケーター
htmxのリクエスト中に表示するスピナーです。
.htmx-indicator {
display: none;
align-items: center;
}
.htmx-indicator.htmx-request { display: inline-flex; }
.spinner {
width: 20px;
height: 20px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
htmxは、リクエスト中の要素に自動的に htmx-request クラスを付与してくれます。このクラスを利用して display: none から display: inline-flex に切り替えるだけで、ローディング表示が実現できます。CSSだけで完結するシンプルなパターンですね。
5. 損益の色分けとバッジシステム
トレーディングダッシュボードで最も重要な視覚表現が、「利益は緑、損失は赤」の色分けです。
5.1 損益カラークラス
/* ===== 色 ===== */
.profit, .buy { color: var(--green); }
.loss, .sell { color: var(--red); }
.profit-row { background: rgba(34, 197, 94, 0.05); }
.loss-row { background: rgba(239, 68, 68, 0.05); }
これらのクラスはテンプレート内で動的に付与されます。例えば損益(PnL)カードのテンプレートを見てみましょう。
<div class="pnl-cards">
<div class="pnl-card">
<span class="pnl-label">本日実現損益</span>
<span class="pnl-value {{ 'profit' if pnl.realized >= 0 else 'loss' }}">
{{ "{:+,.0f}".format(pnl.realized) }}円
</span>
</div>
<div class="pnl-card">
<span class="pnl-label">含み損益</span>
<span class="pnl-value {{ 'profit' if pnl.unrealized >= 0 else 'loss' }}">
{{ "{:+,.0f}".format(pnl.unrealized) }}円
</span>
</div>
<div class="pnl-card">
<span class="pnl-label">合計</span>
<span class="pnl-value {{ 'profit' if (pnl.realized + pnl.unrealized) >= 0 else 'loss' }}">
{{ "{:+,.0f}".format(pnl.realized + pnl.unrealized) }}円
</span>
</div>
</div>
Jinja2の三項演算子 {{ 'profit' if pnl.realized >= 0 else 'loss' }} で、値に応じてCSSクラスを切り替えています。これにより、プラスなら緑、マイナスなら赤でリアルタイムに色が変わります。
損益カードのCSSも見ておきましょう。
.pnl-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.pnl-card {
text-align: center;
padding: 0.8rem;
background: var(--bg);
border-radius: var(--radius);
}
.pnl-label {
display: block;
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 0.3rem;
}
.pnl-value {
display: block;
font-size: 1.4rem;
font-weight: 700;
}
pnl-value は 1.4rem と大きめのフォントサイズに font-weight: 700 のボールドを組み合わせています。損益は「ダッシュボードで最も重要な数字」なので、パッと目に入るサイズ感にしておくのがポイントです。実際の表示では、利益は緑、損失は赤で色分けされ、一目で状況を把握できます。
5.2 行単位の色分け
バックテスト結果のテーブルでは、行全体を薄く色付けする profit-row / loss-row クラスも使っています。
{% for entry in optimization %}
<tr class="{{ 'profit-row' if entry.result.total_return_pct > 0 else 'loss-row' }}">
<td>{{ loop.index }}</td>
<!-- ... -->
<td class="{{ 'profit' if entry.result.total_return_pct > 0 else 'loss' }}">
{{ "{:+.1f}".format(entry.result.total_return_pct) }}%
</td>
</tr>
{% endfor %}
rgba(34, 197, 94, 0.05) のように アルファ値 0.05 というごく薄い色を使っているのがコツです。行の色分けは「うっすら分かる」くらいが適切で、これ以上濃くするとテーブルが見づらくなってしまいます。
5.3 注文ステータスバッジ
注文一覧テーブルでは、注文の状態を色付きバッジで表示しています。
.badge {
display: inline-block;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-submitted { background: #1e3a5f; color: var(--primary); }
.badge-filled { background: #14532d; color: var(--green); }
.badge-cancelled { background: #451a03; color: var(--yellow); }
.badge-pending { background: #1c1917; color: var(--text-muted); }
.badge-rejected { background: #450a0a; color: var(--red); }
対応するテンプレートは以下のとおりです。
<td>
<span class="badge badge-{{ order.status.value }}">{{ order.status.value }}</span>
</td>
各バッジの配色パターンに注目してください。テキスト色とほぼ同系統の、極めて暗い背景色 を組み合わせています。例えば badge-filled(約定済み)は、テキストが --green(#22c55e)に対して、背景は極めて暗い緑(#14532d)です。こうすることで、ダークテーマの中でもバッジが浮きすぎず、かつ状態が一目でわかる絶妙なバランスが生まれます。
6. Plotlyチャートのダークテーマ連携
チャートライブラリ Plotly にもダークテーマがビルトインで用意されています。CSSのテーマとPlotlyのテーマを合わせることで、統一感のあるダッシュボードになります。
6.1 Pythonサーバー側のチャート生成
src/systrade/utils/visualization.py に実装されたチャート生成関数を見てみましょう。
"""Plotlyチャート生成"""
import json
import plotly.graph_objects as go
from plotly.subplots import make_subplots
def create_equity_chart(dates: list[str], equity: list[float]) -> str:
"""資産推移チャート"""
fig = make_subplots(rows=1, cols=1)
fig.add_trace(go.Scatter(
x=dates,
y=equity,
mode="lines",
name="資産推移",
line=dict(color="#2196F3", width=2),
fill="tozeroy",
fillcolor="rgba(33, 150, 243, 0.1)",
))
fig.update_layout(
title="資産推移",
xaxis_title="日付",
yaxis_title="資産額 (円)",
template="plotly_dark",
height=400,
margin=dict(l=60, r=20, t=40, b=40),
)
return json.dumps(fig.to_dict())
最も重要な行は template="plotly_dark" です。この1行を指定するだけで、Plotlyのチャート全体がダークテーマになります。具体的には以下が自動的に変わります。
- チャートの背景色がダーク系に
- 軸のテキスト色やグリッド線の色が明るいグレーに
- ツールチップ(ホバーラベル)もダーク系に
6.2 トレード損益チャートの色分け
def create_trade_chart(trades: list[dict]) -> str:
"""トレード損益チャート"""
if not trades:
return "{}"
dates = [t["exit_date"] for t in trades]
pnls = [t["pnl"] for t in trades]
colors = ["#4CAF50" if p > 0 else "#F44336" for p in pnls]
fig = go.Figure(data=[go.Bar(
x=dates,
y=pnls,
marker_color=colors,
name="損益",
)])
fig.update_layout(
title="トレード別損益",
xaxis_title="決済日",
yaxis_title="損益 (円)",
template="plotly_dark",
height=300,
margin=dict(l=60, r=20, t=40, b=40),
)
return json.dumps(fig.to_dict())
ここでは marker_color にリスト内包表記で 各棒の色を動的に設定 しています。プラスの損益は #4CAF50(緑)、マイナスは #F44336(赤)。CSSで使っている --green / --red と同系統の色を使うことで、CSS側のテーマとチャート側のテーマに統一感が生まれます。
6.3 グリークスチャート(オプション用)
オプション取引のグリークス(Delta、Gamma、Theta、Vega)を4分割で表示するチャートもあります。
def create_greeks_chart(
strikes: list[float],
deltas: list[float],
gammas: list[float],
thetas: list[float],
vegas: list[float],
) -> str:
"""グリークスチャート(権利行使価格別)"""
fig = make_subplots(
rows=2, cols=2,
subplot_titles=("Delta", "Gamma", "Theta", "Vega"),
)
fig.add_trace(go.Scatter(x=strikes, y=deltas, mode="lines+markers", name="Delta"), row=1, col=1)
fig.add_trace(go.Scatter(x=strikes, y=gammas, mode="lines+markers", name="Gamma"), row=1, col=2)
fig.add_trace(go.Scatter(x=strikes, y=thetas, mode="lines+markers", name="Theta"), row=2, col=1)
fig.add_trace(go.Scatter(x=strikes, y=vegas, mode="lines+markers", name="Vega"), row=2, col=2)
fig.update_layout(
title="オプション・グリークス",
template="plotly_dark",
height=500,
showlegend=False,
margin=dict(l=60, r=20, t=60, b=40),
)
return json.dumps(fig.to_dict())
make_subplots(rows=2, cols=2) でサブプロットを作り、各グリーク指標を1枚のチャートにまとめています。showlegend=False はサブプロットのそれぞれに subplot_titles があるので、凡例は不要です。
6.4 フロントエンドでのレンダリング
サーバーからJSON文字列として返されたチャートデータは、base.html で定義した renderChart 関数で描画します。
<!-- バックテスト結果テンプレート内 -->
<div id="equity-chart" class="chart-container"></div>
<div id="trade-chart" class="chart-container"></div>
<script>
renderChart('equity-chart', '{{ equity_chart|safe }}');
renderChart('trade-chart', '{{ trade_chart|safe }}');
</script>
チャートのコンテナCSSはシンプルです。
.chart-container {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 0.5rem;
margin-bottom: 1rem;
}
Plotlyのダークテーマと、コンテナの --surface 背景色はほぼ同じトーンなので、チャートが自然にカード内に溶け込みます。
7. 証拠金利用率プログレスバー
ウォレット情報のセクションでは、証拠金の利用率をプログレスバーで視覚的に表示しています。これはトレーダーにとって「今どれだけ余力があるか」を一目で把握できる重要なUIパーツです。
7.1 プログレスバーのCSS
/* 証拠金プログレスバー */
.margin-bar-container {
height: 8px;
background: var(--bg);
border-radius: 4px;
overflow: hidden;
margin: 0.3rem 0 0.15rem;
}
.margin-bar {
height: 100%;
border-radius: 4px;
transition: width 0.4s ease;
}
.margin-bar.bar-safe { background: var(--green); }
.margin-bar.bar-warn { background: var(--yellow); }
.margin-bar.bar-danger { background: var(--red); }
.margin-bar-label {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.78rem;
margin-bottom: 0.15rem;
}
.bar-safe-text { color: var(--green); font-weight: 700; }
.bar-warn-text { color: var(--yellow); font-weight: 700; }
.bar-danger-text { color: var(--red); font-weight: 700; }
デザイン意図:
- 3段階の色分け: 利用率が低ければ緑(安全)、中程度なら黄色(注意)、高ければ赤(危険)。信号機と同じ直感的な色分けです
transition: width 0.4s easeにより、htmxで値が更新されるたびにバーがスムーズにアニメーションします。htmxは部分的なHTML差し替えを行うため、Barのwidthが変わったときにCSSトランジションが自然に発火しますheight: 8pxと薄型にすることで、情報を圧迫せずにコンパクトに収まります
7.2 ウォレット情報テンプレート
先物口座の余力表示テンプレートを見てみましょう。
<!-- 先物口座余力 -->
{% if wallet %}
<div class="wallet-items">
{% if wallet.FutureTradeLimit is not none %}
<div class="wallet-item">
<span class="label">新規建玉可能額</span>
<span class="value">{{ "{:,.0f}".format(wallet.FutureTradeLimit) }}円</span>
</div>
{% endif %}
{% if wallet.MarginRequirement is not none %}
<div class="wallet-item">
<span class="label">必要証拠金</span>
<span class="value">{{ "{:,.0f}".format(wallet.MarginRequirement) }}円</span>
</div>
{% endif %}
{% if wallet.Deposit is not none %}
<div class="wallet-item">
<span class="label">預託金</span>
<span class="value">{{ "{:,.0f}".format(wallet.Deposit) }}円</span>
</div>
{% endif %}
{% if wallet.ProfitLoss is not none %}
<div class="wallet-item">
<span class="label">評価損益</span>
<span class="value {% if wallet.ProfitLoss > 0 %}profit{% elif wallet.ProfitLoss < 0 %}loss{% endif %}">
{{ "{:+,.0f}".format(wallet.ProfitLoss) }}円
</span>
</div>
{% endif %}
</div>
{% else %}
<p class="empty">ブローカー未接続(エンジン起動後に表示されます)</p>
{% endif %}
各ウォレット項目は {% if ... is not none %} で存在チェックをしてから表示しています。APIから返ってくるデータはフィールドが欠けている場合があるため、こうした防御的なテンプレートにしておくと安全です。
ウォレット項目のCSSはグリッドで並べています。
.wallet-items {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.6rem;
}
.wallet-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.7rem;
background: var(--bg);
border-radius: var(--radius);
font-size: 0.88rem;
}
.wallet-item .label {
color: var(--text-muted);
font-size: 0.82rem;
}
.wallet-item .value {
font-weight: 700;
font-size: 0.95rem;
}
ラベルと値を justify-content: space-between で左右に配置し、コンパクトなキーバリュー形式で表示しています。
8. レスポンシブデザイン対応
トレーダーの中にはデスクの複数モニターで表示する人もいれば、外出先でスマートフォンから確認する人もいます。レスポンシブ対応は必須です。
8.1 ブレークポイント: 768px(タブレット以下)
@media (max-width: 768px) {
.app-layout { flex-direction: column; }
.sidebar {
width: 100%;
height: auto;
position: static;
border-right: none;
border-bottom: 1px solid var(--border);
flex-direction: row;
align-items: center;
}
.sidebar-nav {
flex-direction: row;
overflow-x: auto;
padding: 0;
gap: 0;
}
.nav-item {
white-space: nowrap;
padding: 0.6rem 1rem;
border-left: none;
border-bottom: 3px solid transparent;
font-size: 0.82rem;
}
.nav-item.active {
border-left-color: transparent;
border-bottom-color: var(--primary);
}
.sidebar-footer {
border-top: none;
border-left: 1px solid var(--border);
padding: 0.6rem 0.8rem;
display: flex;
align-items: center;
}
}
768px以下では、大きな変化が起きます。
- サイドバーが上部に移動:
flex-direction: columnでレイアウトが縦積みに変わり、サイドバーがメインコンテンツの上に配置されます - ナビゲーションが横スクロールに:
flex-direction: row+overflow-x: autoで、ナビ項目が横にスクロールできるタブバーに変身します - アクティブ表示が左ボーダーから下ボーダーに: デスクトップでは
border-leftだったアクティブインジケーターが、モバイルではborder-bottomに切り替わります。これはiOSのタブバーに近い操作感です
8.2 ブレークポイント: 640px(スマートフォン)
@media (max-width: 640px) {
.pnl-cards { grid-template-columns: 1fr; }
.form-grid { grid-template-columns: 1fr; }
.summary-cards { grid-template-columns: repeat(2, 1fr); }
.plan-params { grid-template-columns: repeat(2, 1fr); }
}
640px以下では、グリッドレイアウトの列数をさらに減らしています。
- 損益カード: 3列 → 1列(縦積み)。狭い画面で3つ横並びは窮屈すぎるので
- フォーム入力: 自動列 → 1列。入力欄を縦に並べて、タップしやすくします
- サマリカード: 自動列 → 2列。完全な1列にするとスカスカになるので、2列に
ポイント: このプロジェクトでは、auto-fill と minmax() を多用しているため、ブレークポイントで書き換える必要がある箇所は意外と少ないです。CSS Gridの repeat(auto-fill, minmax(200px, 1fr)) パターンは、メディアクエリなしでもかなりの範囲をカバーしてくれます。これは「暗黙的なレスポンシブ」と呼ばれることがあり、現代CSSの強力な武器です。
デスクトップ表示では左サイドバー+右メインコンテンツの横並びですが、モバイル表示ではサイドバーが画面上部の横スクロールタブバーに変わり、メインコンテンツがその下に配置されます。
9. まとめ
本記事で構築したダークテーマUIのポイントを振り返りましょう。
- CSSカスタムプロパティ(CSS変数) で全テーマカラーを
:rootに集約。色の変更が1箇所で済むため、将来のテーマ変更やライトテーマ追加も容易 - 3段階の背景色(
--bg<--surface<--text) でダークテーマに奥行きを持たせる。フラットなダークテーマは単調になりがちなので、この「深度」が大切 - 損益の色分け(緑/赤) は Jinja2テンプレートの三項演算子で動的にCSSクラスを付与。htmxの部分更新と組み合わせることで、リアルタイムな色変化が実現できる
- 注文ステータスバッジ は、テキスト色と同系統の暗い背景色を組み合わせて、ダークテーマでも視認性を確保
- Plotlyのダークテーマ は
template="plotly_dark"の1行で有効化でき、CSS側のカラーパレットと揃えることで統一感が生まれる - 証拠金利用率プログレスバー は緑/黄/赤の3段階で、
transitionアニメーション付き。htmxの部分更新で値が変わるたびにスムーズに伸縮する - レスポンシブ対応 は
auto-fill + minmax()による暗黙的なレスポンシブと、768px/640pxの2つのブレークポイントで対応。サイドバーはモバイルで横スクロールタブに変身する
次回予告
第4回は 「DB設計とAlembicマイグレーション」 です。
ここまで3回にわたって、FastAPIのプロジェクト構成、htmxによるSPA風のインタラクション、そしてダークテーマのUIを構築してきました。次回はいよいよデータの永続化に取り組みます。
- SQLAlchemy 2.0 のモデル定義
- Alembicによるマイグレーション管理
- 注文・ポジション・損益履歴のテーブル設計
- 非同期DBセッション管理
ダッシュボードに「記憶」を持たせる回です。お楽しみに。
最後まで読んでくださりありがとうございました。質問や感想があればコメント欄でぜひお聞かせください!