0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIの思考プロセスとモデル世代交代を3Dリアルタイム監視する「運用コクピット」の設計と実装

0
Posted at

はじめに

バックグラウンドで並行稼働する複数の機械学習モデルが、市場データを読み込み、判断を下し、日々モデル同士で戦って世代交代していく様子――。「映画のミッションコントロール(管制室)」のように3Dでリアルタイムに眺められたら、日々の開発と運用のモチベーションはどうなるでしょうか。

日本株のAI銘柄スクリーニング&継続学習端末「ALPHA FORGE(アルファフォージ)」を個人で開発・運用しています。バックエンド(FastAPI / Celery)では4種類のMLモデル(XGBoost, Random Forest, LSTM, Transformer)が日々推論と学習を回し、Obsidianには4,800本以上の企業分析ノートが蓄積されています。

普段は「未来データのリークを構造で防ぐ設計」や「プロンプトインジェクションの防御」といった泥臭いパイプライン構築と戦っていますが、今回はちょっと息抜きを兼ねて、暗い部屋のサブモニターに常時映しておける専用コクピット「Ops Console(オプス・コンソール)」を開発しました。

本記事では、本体に負荷をかけない完全分離アーキテクチャから、Three.jsによる推論判断・モデル世代交代の3Dパーティクル演出、SVGによるシームレス心電図(ECG)モニタ、4,800ファイルのグラフ走査高速化(45秒 → 1.6秒)まで、フロントエンドとバックエンドの両面から実装の裏側を共有します。

【要約】

  1. 並行稼働する4種のMLモデル(XGB/RF/LSTM/Transformer)の推論プロセスと世代交代(Challenger ➔ Champion昇格)をThree.jsで可視化する独立コクピット「Ops Console」を構築した
  2. 本体の推論や監視タスクを絶対に重くしないため、Next.js 3003 / FastAPI 8003 の完全分離プロセスにし、SQLiteは file:...?mode=ro(read-only URI)で接続するゼロ副作用アーキテクチャを採用した
  3. 80px周期のシームレススクロール心電図(ECG)モニタ、4,800本の企業分析ノートを結ぶ3Dナレッジ星雲、全幅日付セパレータ付きログなど、監視端末としての機能美とUXを徹底追求した

※本記事はnote連載[ALPHA FORGE 開発秘話]の特別編を、エンジニア向けに再構成したものです。

環境と技術スタック

本サブシステムは、本体のトレーディング支援システムから独立したプロセスとして構成しています。

レイヤー 技術スタック 役割
フロントエンド Next.js 16 (App Router) + TypeScript (strict) 監視UIコンポーネント、ポーリング制御、Three.jsキャンバス
スタイリング Vanilla CSS(CSS Variables + デザイントークン) ダークネイビー基調のプロ端末デザイン、軽量キーフレームアニメーション
3Dグラフィックス Three.js モデル訓練・推論判断アリーナ、ナレッジグラフ3D星雲
バックエンド FastAPI + Python 3.11 集約APIエンドポイント、バックグラウンド死活監視
データアクセス aiosqlite(mode=ro) 本体のSQLiteデータベースへの完全読み取り専用接続
外部連携 Redis, Celery worker/beat, Qdrant プロセス状態取得、ベクトル検索基盤のヘルスチェック

全体画面とアーキテクチャ設計:本体を絶対に重くしない「完全分離」

01_ops_console_overview.png

完成したOps Console全景。ダークネイビー(#060a13)をベースに、シアン・アンバー・エメラルドグリーンの光が浮かぶUI

開発にあたって最優先にした設計原則は、「監視画面側の負荷やクラッシュが、絶対に本体のトレード推論・定期バッチに波及してはならない」 という点です。

これを担保するため、以下の2つの隔離レイヤーを設けました。

1. プロセスとポートの物理分離

本体のWebコンソール(Next.js: 3001 / FastAPI: 8002)に対し、Ops Consoleは専用のポート(Next.js: 3003 / FastAPI: 8003)で立ち上げ、依存関係やNode/Pythonプロセス自体を完全に分けました。

2. SQLite read-only URI による書き込み遮断

Ops Consoleのバックエンドは、本体の予測台帳(prediction_ledger)や決着テーブルから直近の推移を読み取りますが、URIパラメータに mode=ro を付与してオープンすることで、ドライバレベルで書き込みクエリを物理的に遮断しています。

subsystems/ops-console/backend/ops_console/sources/ledger_db.py(抜粋)
@asynccontextmanager
async def readonly_connection(db_path: Path) -> AsyncIterator[aiosqlite.Connection]:
    """本体のSQLiteを読み取り専用で開く非同期コンテキストマネージャ.
    
    `file:...?mode=ro` で開くため、Ops Consoleから本体のDBへ誤って
    書き込みやロック競合を発生させるリスクをゼロにする。
    """
    if not db_path.exists():
        raise FileNotFoundError(f"Database not found: {db_path}")

    # Windowsの絶対パスをURI形式に整形
    uri = f"file:{db_path.as_posix()}?mode=ro"
    async with aiosqlite.connect(uri, uri=True) as db:
        db.row_factory = aiosqlite.Row
        yield db

万が一Ops Console側で巨大な集計クエリを実行してロック待ちが発生しても、本体側の書き込み(予測発行や注文判定)をブロックしない設計にしています。

実装1:SVG心電図(ECG)モニタのシームレス・スクロール

01_ops_console_top.gif
最上段HUD。各コンポーネントの呼吸ランプ(左)と、滑らかにスクロールするECG心電図モニタ(右)

画面最上段の右側に配置した 『BEAT HEARTBEAT』 は、Celery beatが定期的に発行した心拍(ハートビート)をCelery workerがDBに書き込んだタイムスタンプを監視するモニタです。

映画の生命維持装置やICUのモニタのように、緑色のネオン波形が滑らかに右から左へ流れ続けます。

シームレス・ループの数学的設計

一般的なSVGパスのアニメーションでは stroke-dashoffset を使うことが多いですが、それだと線が途中で伸び縮みして不自然に見えがちです。

そこで、波形の1周期(PQRST波)をぴったり 80px で生成し、ビューポート幅320pxに対して余裕を持たせた 480px(6周期分) のパスを描画。CSS transformで -80px 平行移動させてループさせることで、目に見える繋ぎ目(カクつき)が一切ない完全シームレスな連続スクロールを実現しました。

subsystems/ops-console/frontend/src/components/HeartbeatPanel.tsx(抜粋)
export const HeartbeatPanel: React.FC<HeartbeatPanelProps> = ({
  tasksStatus = "ok",
  detail = "5 分おき ・ 最終確認中",
}) => {
  const isOk = tasksStatus === "ok";
  // 80px × 6周期 = 480px で生成。-80px 移動でシームレスに一周する
  const ekgPath = generateEkgPath(480, 90);
  const strokeColor = isOk ? "var(--color-loss)" : "var(--color-gain)";

  return (
    <section className="mc-cell" style={{ gridColumn: "span 4" }}>
      <div style={{ display: "flex", justifyContent: "space-between" }}>
        <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
          <h2>BEAT HEARTBEAT</h2>
          {isOk && <span className="pulse-badge">PULSE 60 BPM</span>}
        </div>
        <span style={{ color: strokeColor }}>{detail}</span>
      </div>

      <svg viewBox="0 0 320 90" style={{ width: "100%", height: "100px" }}>
        <defs>
          {/* 医療用モニタ風の極細方眼グリッド */}
          <pattern id="hb-grid" width="16" height="15" patternUnits="userSpaceOnUse">
            <path d="M 16 0 L 0 0 0 15" fill="none" stroke="#13203a" strokeWidth="0.7" strokeDasharray="1 2" />
          </pattern>
        </defs>

        <rect width="320" height="90" fill="url(#hb-grid)" opacity="0.8" />
        <path d="M0 45 H320" stroke="var(--border-muted)" strokeWidth="1" />

        {isOk ? (
          <g style={{
            animation: "mc-ekg-scroll 1.2s linear infinite",
            filter: "drop-shadow(0 0 4px var(--color-loss))", // ネオングロー
          }}>
            <path d={ekgPath} fill="none" stroke={strokeColor} strokeWidth="2.2" />
          </g>
        ) : (
          /* 異常停止時は映画のように警告音のフラットライン(赤の直線)に変化 */
          <path d="M0 45 H320" fill="none" stroke={strokeColor} strokeWidth="2.5" />
        )}
      </svg>
    </section>
  );
};
subsystems/ops-console/frontend/src/app/globals.css(抜粋)
/* 80px 移動した瞬間に次の周期の波形と完全一致するため、繋ぎ目が消える */
@keyframes mc-ekg-scroll {
  from {
    transform: translateX(0);
  }
  to {
    transform: translateX(-80px);
  }
}

定期タスクが死ぬと、波形がピキーンと平らな直線(フラットライン)になり、色が赤(--color-gain)に反転して警告を発します。

実装2:3Dモデルアリーナでの推論判断と世代交代ワープ

02_model_training_3d.gif
外周からデータ粒子が吸い込まれ、4モデルから中心コアへ判断ビームが走る。強いモデルが内周へワープして昇格する瞬間

画面中央右の 『MODEL TRAINING 3D』 は、裏で学習・推論を行っている4種類のMLモデル(XGBoost, Random Forest, LSTM, Transformer)の挙動をThree.jsで可視化したアリーナです。

1. 推論判断フロー(ビーム照射 & ショックウェーブ)

  • データ流入: 外周の空間から、四本値や財務特徴量を模した白いデータ粒子群が、4方位に浮かぶ各モデルクラスタへ向かって吸い込まれます。
  • 判断ビーム: 各モデルがスコアリングを完了すると、中心の「意思決定コア」へ向かって判断レーザービームが照射されます。
  • パルス放出: 中心コアが4モデルの一致度(コンコーダンス)を統合して最終判断を下すと、中心コアが強くパルス発光し、宇宙空間のようにリング状の衝撃波(ショックウェーブ)が広がります。

2. 世代交代(モデルスワップ・アセンド)

継続学習ループにおいて、外周でシャドウ推論を走らせていた挑戦者(Challenger)モデルが既存の現行チャンピオンの検証精度を上回ったとき、「モデル昇格(Promotion)」 が発生します。

このイベントを検知すると、黄金に輝くオーブが螺旋軌道を描きながら内周のチャンピオン座へとワープ突入し、HUDに「👑 MODEL PROMOTION」がフラッシュします。

subsystems/ops-console/frontend/src/components/Training3D.tsx(抜粋)
// モデル入れ替え(スワップ)時の螺旋アセンド軌道計算
if (swapAnimationRef.current.active) {
  const anim = swapAnimationRef.current;
  anim.progress += delta * 0.8;
  const p = Math.min(1.0, anim.progress);
  
  // 外周軌道(radius=42)から内周チャンピオン軌道(radius=18)へ螺旋突入
  const currentRadius = 42 - 24 * (1 - Math.cos(p * Math.PI)) * 0.5;
  const currentAngle = anim.startAngle + p * Math.PI * 4; // 2回転スパイラル
  const currentY = Math.sin(p * Math.PI) * 12; // 放物線を描いて浮上
  
  swapOrb.position.set(
    Math.cos(currentAngle) * currentRadius,
    currentY,
    Math.sin(currentAngle) * currentRadius
  );
  
  // 突入完了時に衝撃波リングを炸裂させる
  if (p >= 1.0) {
    anim.active = false;
    triggerPromotionShockwave();
  }
}

実装3:4,800ノートの「3Dナレッジ星雲」と走査高速化(45秒 → 1.6秒)

knowledge_graph_3d.gif
Obsidianに蓄積された4,841本の企業分析ノートと内部リンクが星雲のように自転する

画面右下の 『KNOWLEDGE GRAPH 3D』 は、ローカルMarkdown(Obsidian)に書き溜めてきた4,800本以上の企業分析ノートや四季報メモを3D空間にマッピングした星雲です。

各ノートをノード、ノート間の内部リンク([[企業名]] やサプライチェーン関係)をエッジとして立体配置し、作成日順にノードがパッと光を灯しながら自転します。

NTFS走査の壁:45秒のタイムアウト

この機能を実装する際、最大のボトルネックとなったのが 「ローカルファイルシステムのI/O速度」 でした。

Obsidianの保管庫内にある4,800本以上のMarkdownファイルを起動時に直列で os.walk / Path.read_text() してリンク解析を行っていたところ、Windowsのファイルシステム(NTFS)のメタデータアクセス待ちで初回スキャンに 45秒以上 かかり、APIがタイムアウトする事態に陥りました。

数千〜数万のローカルファイルを都度スキャンしてグラフ構造を構築すると、I/Oが支配的になりフロントエンドの初期描画を激しくブロッキングします。

解決策:インデックスメタデータキャッシュ

この問題を解決するため、バックエンドのグラフ構築サービスに「軽量インデックスキャッシュ」を導入しました。

  1. 更新日時チェック: 保管庫全体の最新コミット/ディレクトリ更新時刻を1回の os.stat で確認
  2. インデックス保存: 初回走査時に抽出した {node_id, title, tag, created_at, links} の構造化辞書を .graph_cache.json にシリアライズ
  3. 差分更新: 変更があったファイルのみパースしてキャッシュを更新

これにより、2回目以降のAPIレスポンスは 45秒 ➔ 1.6秒(約28倍高速化) に短縮され、UIが瞬時に立ち上がるようになりました。

実装4:時系列ログの視認性を高める全幅日付セパレータ

picks.gif

日付セパレータによって、過去5日間の検証ログが上詰めで整然と並ぶ(※銘柄情報はモザイク処理済)

画面左下の 『PICKS』 パネルは、直近5営業日にアルゴリズムが抽出した検証対象銘柄の追跡ログです。

中長期(20日/60日ホライズン)と短期(当日〜3日ホライズン)の2カラム構成ですが、テーブルヘッダーではなく 行全体を横断するセパレータ(──── YYYY/MM/DD ────) を挿入し、各日付の中で銘柄を上詰めで描画するようにしました。

subsystems/ops-console/frontend/src/components/RecentPicks.tsx(抜粋)
{/* 中長期と短期の2列をまたぐ全幅の日付セパレータ行 */}
<div style={{
  gridColumn: "1 / -1",
  display: "flex",
  alignItems: "center",
  justifyContent: "center",
  padding: "4px 0",
  fontSize: "11px",
  fontFamily: "'JetBrains Mono', monospace",
  color: "var(--text-muted)",
  letterSpacing: ".15em",
}}>
  <span style={{ flex: 1, height: "1px", background: "var(--border-muted)" }} />
  <span style={{ padding: "0 12px", color: "var(--text-cyan)" }}>
    ──── {dateStr} ────
  </span>
  <span style={{ flex: 1, height: "1px", background: "var(--border-muted)" }} />
</div>

起点価格からのリアルタイム騰落率、ミニスパークライン(折れ線)、出来高バーが一目で確認できるよう設計しています。

まとめ:個人開発だからこそ「ロマンと実用性」を両立する

「バックグラウンドで動いているAIの思考を3Dで見たい」という、一見するとお遊びのような動機から作り始めたコクピットでしたが、実際に運用してみると以下のような実利がありました。

  1. システムの異常が一瞬でわかる:
    心電図がフラットラインになったり、ステータスランプの色が変わるだけで、ログを掘る前に「Celery workerが落ちたな」「Qdrantの接続が切れたな」と直感的に把握できます。
  2. モデルの進化が肌感で伝わる:
    数値のログファイル(val_auc: 0.6288 など)を眺めるよりも、3Dアリーナで新しいモデルがワープしてチャンピオン座を奪取する演出を見るほうが、継続学習パイプラインが正常に機能している実感が湧きます。
  3. 何より開発が楽しくなる:
    サブモニターにこの画面を表示させておくだけで、日々の機能追加やバックテストが「宇宙船のシステムを拡張している」かのようなワクワク感に変わります。

個人開発の醍醐味は、誰の許可も要らず、実用性と自分のロマンをとことん追求できる点にあります。皆様のプロジェクトでも、ぜひ「自分専用の運用コクピット」を作ってみてはいかがでしょうか。


【システム検証記録に関する免責事項・注意事項】

  • 本記事の目的: 本記事は個人開発におけるフロントエンド(Next.js / Three.js)およびバックエンド(FastAPI)の設計・実装ノウハウの共有を目的としており、特定の有価証券の売買推奨や投資助言を目的としたものではありません。
  • 検証データの性質: 記事中に登場するシミュレーション起点価格、判定スコア、グラフ等の数値は、アルゴリズムの動作検証および機能テスト用に記録されたシステム内部ログであり、将来の運用成果を示唆・保証するものではありません。
  • 投資判断の自己責任: 株式等の投資にかかる最終判断は、ご自身の責任と判断において行っていただきますようお願い申し上げます。
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?