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_name | CompanyData パス |
|---|---|
| Total Revenues | pl.revenue |
| Gross Profit | pl.grossProfit |
| Operating Income | pl.operatingIncome |
| Net Income | pl.profit |
| Total Cash And Short Term Investments | bs.currentAssets[0].value |
| Total Receivables | bs.currentAssets[1].value |
| Operating Cash Flow | cashFlow.operatingCF |
| Capital Expenditure | cashFlow.capex |
| EPS Diluted | perShare.eps |
| ... | ... |
推奨アプローチ: 方法A(サーバーAPI経由)
実装計画
- Nuxt Server API の作成
server/api/financial/companies.get.ts- 企業一覧取得server/api/financial/[ticker].get.ts- 企業別財務データ取得
- データ変換ユーティリティの作成
server/utils/financial-data-transformer.ts- SQLite の行データを
CompanyData[]形式に変換
- SQLite 接続設定
- 開発環境:
better-sqlite3で直接接続 - プロダクション: Cloudflare D1 バインディング
- 開発環境:
- Vue ページの作成
pages/financial-quiz/proportional-animation-db.vueuseFetch('/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 Investments | bs.currentAssets[].現預金・短期投資 | |
Total Receivables | bs.currentAssets[].売掛金 | |
Inventory | bs.currentAssets[].棚卸資産 | |
| (差額計算) | bs.currentAssets[].その他流動資産 | Total Current Assets - 上記3項目 |
Net Property Plant And Equipment | bs.fixedAssets[].有形固定資産 | |
Long-term Investments | bs.fixedAssets[].長期投資 | |
Goodwill | bs.fixedAssets[].のれん | |
Other Intangibles | bs.fixedAssets[].無形資産 | |
Deferred Tax Assets Long-Term | bs.fixedAssets[].繰延税金資産 | |
Other Long-Term Assets | bs.fixedAssets[].その他長期資産 | 調整項目 |
Total Current Liabilities | 検証用 | |
Accounts Payable | bs.currentLiabilities[].買掛金 | |
Current Portion of Long-Term Debt | bs.currentLiabilities[].短期借入金 | |
Current Portion of Leases | bs.currentLiabilities[].短期リース | |
Unearned Revenue Current, Total | bs.currentLiabilities[].前受収益(流動) | |
| (差額計算) | bs.currentLiabilities[].その他流動負債 | |
Total Liabilities | 検証用 | |
Long-Term Debt | bs.fixedLiabilities[].長期借入金 | |
Long-Term Leases | bs.fixedLiabilities[].長期リース | |
Unearned Revenue Non Current | bs.fixedLiabilities[].長期前受収益 | |
| (差額計算) | bs.fixedLiabilities[].その他固定負債 | |
Total Equity | 検証用 | |
Common Stock | bs.equity[].資本金 | |
Additional Paid In Capital | bs.equity[].資本剰余金 | |
Retained Earnings | bs.equity[].利益剰余金 | |
Treasury Stock | bs.equity[].自己株式 | 負の値 |
Comprehensive Income and Other | bs.equity[].その他包括利益 |
PL(損益計算書)へのマッピング
| SQLite (section=income_statement) | CompanyData パス | 備考 |
|---|---|---|
Total Revenues | pl.revenue | |
Gross Profit (Loss) | pl.grossProfit | |
Operating Income | pl.operatingIncome | |
Net Income | pl.profit | |
Cost Of Revenues | pl.expenses の一部 | |
Selling General & Admin Expenses | expenses.sga | |
R&D Expenses | expenses.rd |
キャッシュフローへのマッピング
| SQLite (section=cash_flow) | CompanyData パス | 備考 |
|---|---|---|
Cash from Operations | cashFlow.operatingCF | |
Capital Expenditure | cashFlow.capex | 負の値 |
Free Cash Flow | cashFlow.fcf |
1株当たり指標へのマッピング
| SQLite (section=income_statement) | CompanyData パス | 備考 |
|---|---|---|
Diluted EPS - Continuing Operations または Net EPS - Diluted | perShare.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;
}
次のステップ
SQLite に格納されている✅metric_nameの一覧を確認マッピングテーブルを作成✅- サーバーAPIの実装
- Vue ページの実装
- 動作確認
スケーラビリティの検討(100社〜300社対応)
ビルド時変換の現実性
現状の composable ファイルサイズ
useMicrosoftData.ts: 約740行(12期間分のデータ)
100社 × 740行 = 約74,000行のTypeScriptファイルが生成される
ビルド時変換のメリット
- パフォーマンス最高 - 静的データなのでランタイムのDB接続なし
- Cloudflare D1への依存なし - デプロイが単純
- 既存の仕組みをそのまま活用 - コンポーネント側の変更不要
- オフラインでも動作 - 全データがバンドルに含まれる
ビルド時変換のデメリット
- バンドルサイズ増加 - 100社で推定 2〜3MB(gzip後 300〜500KB)
- データ更新のたびにビルド&デプロイが必要
- 初期ロード時間がやや増加(ただしコード分割で緩和可能)
サーバーAPI経由のメリット/デメリット
メリット
- バンドルサイズに影響なし
- データ更新がリアルタイム(DBを更新するだけ)
- ページネーション・フィルタリングが容易
デメリット
- Cloudflare D1へのデータ移行が必要
- APIレイテンシが発生
- 実装が複雑(変換ロジック、エラーハンドリング等)
推奨: 段階的アプローチ
| フェーズ | 企業数 | 推奨方法 | 理由 |
|---|---|---|---|
| 現状 | 15社 | 手動composable | 既に動作中 |
| 短期 | 100社 | ビルド時変換 | スクリプトで自動生成、実装が早い |
| 中長期 | 300社〜 | サーバーAPI経由 | バンドルサイズ問題を回避 |
100社程度ならビルド時変換でスクリプトを作る方が圧倒的に早く完成する。 300社を超えてバンドルサイズが問題になったらその時点でサーバーAPI経由に移行すればよい。
バンドルサイズの試算
| 企業数 | 推定サイズ(未圧縮) | 推定サイズ(gzip) | 評価 |
|---|---|---|---|
| 15社 | 300KB | 50KB | ✅ 問題なし |
| 100社 | 2〜3MB | 300〜500KB | ✅ 許容範囲 |
| 300社 | 6〜9MB | 1〜1.5MB | ⚠️ 要検討 |
| 500社 | 10〜15MB | 2〜3MB | ❌ API経由推奨 |
※ コード分割(dynamic import)を使えば、初期ロードには影響しない設計も可能
質問事項
実装を進める前に確認したいこと:
- 開発環境のみで動けば OK か、プロダクション(Cloudflare Pages + D1)でも動かしたいか
- 全企業のデータを一度に取得するか、企業ごとに API を叩くか
- 既存ページ(TypeScript版)との共存か、置き換えか