• #sqlite
  • #nuxt
  • #financial-data
  • #api
  • #architecture
未分類

SQLiteから財務データを読み込む実装方針

背景・目的

現在/financial-quiz/proportional-animation ページでは、各企業の財務データをTypeScriptのcomposableファイル(useMicrosoftData.tsなど)から読み込んでいる。

今回apps/web/data/koyfin.db に格納されたSQLiteデータを活用して、100社〜300社規模のデータを効率的に管理したい。

結論

ビルド時変換を採用する。

SQLite (koyfin.db) からcomposableファイル(use{Ticker}Data.ts)を自動生成するスクリプトを作成する。

koyfin.db
    ↓ Node.jsスクリプト(変換ロジック)
apps/web/app/composables/
├── useAAPLData.ts  ← 自動生成
├── useAMZNData.ts  ← 自動生成
├── useGOOGLData.ts ← 自動生成
└── ... (100社分)

理由:

  • 既存の仕組み(composable + Vueページ)をそのまま活用できる
  • Cloudflare D1への依存なし、デプロイがシンプル
  • 100社〜300社程度ならバンドルサイズは許容範囲
  • 実装が早い

将来の展望:

  • 300社を超えてバンドルサイズが問題になった場合は、別プロジェクトとしてサーバーAPI経由の実装を一から作る
  • 中途半端に混ぜると複雑になるため、明確に分離する

現在のデータフロー

Koyfin (Web)
    ↓ ダウンロード
TSV/CSV ファイル
    ↓ Gemini API で変換
TypeScript composables (useMicrosoftData.ts)
    ↓ import
Vue ページ (proportional-animation.vue)

新しいデータフロー(候補)

方法A: サーバーAPI経由(推奨)

SQLite (koyfin.db)
    ↓ better-sqlite3 / sql.js
Nuxt Server API (/api/financial/[ticker].ts)
    ↓ JSON レスポンス
Vue ページ (useFetch / useAsyncData)

メリット:

  • データ変換ロジックがサーバー側に集約
  • クライアントのバンドルサイズに影響なし
  • キャッシュ制御が容易
  • プロダクション環境で Cloudflare D1 への移行が容易

デメリット:

  • API エンドポイントの実装が必要
  • 開発環境とプロダクション環境で SQLite 接続方法が異なる

方法B: ビルド時変換

SQLite (koyfin.db)
    ↓ ビルドスクリプト (Node.js)
TypeScript composables (自動生成)
    ↓ import
Vue ページ

メリット:

  • 既存の仕組みをそのまま活用
  • ランタイムのDB接続不要
  • パフォーマンス最高(静的データ)

デメリット:

  • データ更新のたびにビルドが必要
  • composable ファイルが大量に生成される

方法C: クライアント直接(非推奨)

SQLite (koyfin.db)
    ↓ sql.js (WASM)
Vue ページ (クライアント側で変換)

メリット:

  • サーバー実装不要

デメリット:

  • sql.js (WASM) のバンドルサイズが大きい(〜1MB)
  • 初期ロード時間が長い
  • SQLite ファイル全体をダウンロードする必要あり

SQLite スキーマ構造

apps/web/data/schema.sql より抜粋:

-- 企業マスタ
CREATE TABLE companies (
    id INTEGER PRIMARY KEY,
    ticker TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL
);

-- 期間マスタ
CREATE TABLE financial_periods (
    id INTEGER PRIMARY KEY,
    period_type TEXT NOT NULL,  -- 'annual', 'quarterly', 'ltm'
    period_label TEXT NOT NULL  -- 'FY 2024', 'Q1 FY2025'
);

-- 財務データ本体(Annual)
CREATE TABLE financial_data_annual (
    company_id INTEGER,
    period_id INTEGER,
    section TEXT NOT NULL,      -- 'highlights', 'income_statement'
    metric_name TEXT NOT NULL,  -- 'Total Revenues', 'Gross Profit'
    metric_value TEXT
);

データ変換の課題

SQLite のデータは metric_name / metric_value の key-value 形式で格納されている。

metric_name: "Total Revenues"
metric_value: "93580"

これを Vue コンポーネントが期待する CompanyData[] 形式に変換する必要がある:

{
  name: 'Microsoft',
  periods: [
    {
      label: '2015',
      data: {
        bs: {
          currentAssets: [
            { label: '現預金・短期投資', value: 96391 },
            ...
          ],
          ...
        },
        pl: {
          revenue: 93580,
          grossProfit: 60542,
          ...
        },
        ...
      }
    }
  ]
}

変換に必要なマッピング

SQLite metric_nameCompanyData パス
Total Revenuespl.revenue
Gross Profitpl.grossProfit
Operating Incomepl.operatingIncome
Net Incomepl.profit
Total Cash And Short Term Investmentsbs.currentAssets[0].value
Total Receivablesbs.currentAssets[1].value
Operating Cash FlowcashFlow.operatingCF
Capital ExpenditurecashFlow.capex
EPS DilutedperShare.eps
......

推奨アプローチ: 方法A(サーバーAPI経由)

実装計画

  1. Nuxt Server API の作成
    • server/api/financial/companies.get.ts - 企業一覧取得
    • server/api/financial/[ticker].get.ts - 企業別財務データ取得
  2. データ変換ユーティリティの作成
    • server/utils/financial-data-transformer.ts
    • SQLite の行データを CompanyData[] 形式に変換
  3. SQLite 接続設定
    • 開発環境: better-sqlite3 で直接接続
    • プロダクション: Cloudflare D1 バインディング
  4. Vue ページの作成
    • pages/financial-quiz/proportional-animation-db.vue
    • useFetch('/api/financial/MSFT') でデータ取得

ディレクトリ構造

apps/web/
├── data/
│   └── koyfin.db              # SQLite データベース
├── server/
│   ├── api/
│   │   └── financial/
│   │       ├── companies.get.ts
│   │       └── [ticker].get.ts
│   └── utils/
│       └── financial-data-transformer.ts
└── app/
    └── pages/
        └── financial-quiz/
            └── proportional-animation-db.vue

SQLite データの実際の構造

SQLiteには以下のsectionごとにデータが格納されている:

  • balance_sheet - 貸借対照表
  • income_statement - 損益計算書
  • cash_flow - キャッシュフロー計算書
  • highlights - ハイライト(主要指標)
  • profitability - 収益性指標
  • enterprise_value - 企業価値
  • multiples - バリュエーション指標
  • roic - 投下資本利益率
  • solvency - 財務健全性

データ値の形式

値は文字列で格納されている(例: "391,035.0M", "(9,447.0)M", "46.21%")。

  • 負の値はカッコで囲まれている: (9,447.0)M
  • 単位はM(百万ドル)
  • パーセントは%付き
  • 倍率はx付き

詳細マッピングテーブル

BS(貸借対照表)へのマッピング

SQLite (section=balance_sheet)CompanyData パス備考
Total Assets検証用
Total Current Assets検証用
Total Cash And Short Term Investmentsbs.currentAssets[].現預金・短期投資
Total Receivablesbs.currentAssets[].売掛金
Inventorybs.currentAssets[].棚卸資産
(差額計算)bs.currentAssets[].その他流動資産Total Current Assets - 上記3項目
Net Property Plant And Equipmentbs.fixedAssets[].有形固定資産
Long-term Investmentsbs.fixedAssets[].長期投資
Goodwillbs.fixedAssets[].のれん
Other Intangiblesbs.fixedAssets[].無形資産
Deferred Tax Assets Long-Termbs.fixedAssets[].繰延税金資産
Other Long-Term Assetsbs.fixedAssets[].その他長期資産調整項目
Total Current Liabilities検証用
Accounts Payablebs.currentLiabilities[].買掛金
Current Portion of Long-Term Debtbs.currentLiabilities[].短期借入金
Current Portion of Leasesbs.currentLiabilities[].短期リース
Unearned Revenue Current, Totalbs.currentLiabilities[].前受収益(流動)
(差額計算)bs.currentLiabilities[].その他流動負債
Total Liabilities検証用
Long-Term Debtbs.fixedLiabilities[].長期借入金
Long-Term Leasesbs.fixedLiabilities[].長期リース
Unearned Revenue Non Currentbs.fixedLiabilities[].長期前受収益
(差額計算)bs.fixedLiabilities[].その他固定負債
Total Equity検証用
Common Stockbs.equity[].資本金
Additional Paid In Capitalbs.equity[].資本剰余金
Retained Earningsbs.equity[].利益剰余金
Treasury Stockbs.equity[].自己株式負の値
Comprehensive Income and Otherbs.equity[].その他包括利益

PL(損益計算書)へのマッピング

SQLite (section=income_statement)CompanyData パス備考
Total Revenuespl.revenue
Gross Profit (Loss)pl.grossProfit
Operating Incomepl.operatingIncome
Net Incomepl.profit
Cost Of Revenuespl.expenses の一部
Selling General & Admin Expensesexpenses.sga
R&D Expensesexpenses.rd

キャッシュフローへのマッピング

SQLite (section=cash_flow)CompanyData パス備考
Cash from OperationscashFlow.operatingCF
Capital ExpenditurecashFlow.capex負の値
Free Cash FlowcashFlow.fcf

1株当たり指標へのマッピング

SQLite (section=income_statement)CompanyData パス備考
Diluted EPS - Continuing Operations または Net EPS - DilutedperShare.eps

値のパース処理

SQLiteの文字列値をnumber型に変換する処理が必要:

function parseValue(value: string | null): number {
  if (!value || value === 'None') return 0;

  // カッコ付きの負の値を処理: "(9,447.0)M" -> -9447
  const isNegative = value.startsWith('(') && value.includes(')');

  // 単位とカッコを除去
  let cleaned = value
    .replace(/[()MBK%x,]/g, '')
    .trim();

  const num = parseFloat(cleaned);
  return isNegative ? -num : num;
}

次のステップ

  1. SQLite に格納されている metric_name の一覧を確認
  2. マッピングテーブルを作成
  3. サーバーAPIの実装
  4. Vue ページの実装
  5. 動作確認

スケーラビリティの検討(100社〜300社対応)

ビルド時変換の現実性

現状の composable ファイルサイズ

useMicrosoftData.ts: 約740行(12期間分のデータ)

100社 × 740行 = 約74,000行のTypeScriptファイルが生成される

ビルド時変換のメリット

  1. パフォーマンス最高 - 静的データなのでランタイムのDB接続なし
  2. Cloudflare D1への依存なし - デプロイが単純
  3. 既存の仕組みをそのまま活用 - コンポーネント側の変更不要
  4. オフラインでも動作 - 全データがバンドルに含まれる

ビルド時変換のデメリット

  1. バンドルサイズ増加 - 100社で推定 2〜3MB(gzip後 300〜500KB)
  2. データ更新のたびにビルド&デプロイが必要
  3. 初期ロード時間がやや増加(ただしコード分割で緩和可能)

サーバーAPI経由のメリット/デメリット

メリット

  1. バンドルサイズに影響なし
  2. データ更新がリアルタイム(DBを更新するだけ)
  3. ページネーション・フィルタリングが容易

デメリット

  1. Cloudflare D1へのデータ移行が必要
  2. APIレイテンシが発生
  3. 実装が複雑(変換ロジック、エラーハンドリング等)

推奨: 段階的アプローチ

フェーズ企業数推奨方法理由
現状15社手動composable既に動作中
短期100社ビルド時変換スクリプトで自動生成、実装が早い
中長期300社〜サーバーAPI経由バンドルサイズ問題を回避

100社程度ならビルド時変換でスクリプトを作る方が圧倒的に早く完成する。 300社を超えてバンドルサイズが問題になったらその時点でサーバーAPI経由に移行すればよい。

バンドルサイズの試算

企業数推定サイズ(未圧縮)推定サイズ(gzip)評価
15社300KB50KB✅ 問題なし
100社2〜3MB300〜500KB✅ 許容範囲
300社6〜9MB1〜1.5MB⚠️ 要検討
500社10〜15MB2〜3MB❌ API経由推奨

※ コード分割(dynamic import)を使えば、初期ロードには影響しない設計も可能

質問事項

実装を進める前に確認したいこと:

  • 開発環境のみで動けば OK か、プロダクション(Cloudflare Pages + D1)でも動かしたいか
  • 全企業のデータを一度に取得するか、企業ごとに API を叩くか
  • 既存ページ(TypeScript版)との共存か、置き換えか