Composable自動生成リファクタリング - 引き継ぎドキュメント
データ再生成コマンド
cd apps/web
node scripts/generate-financial-data.mjs
重要: SQLiteにデータを追加した後は、上記コマンドでfinancial-data.tsを再生成してください。
作成したファイル
| ファイル | 説明 |
|---|---|
app/types/financial.ts | 型定義ファイル |
scripts/generate-financial-data.mjs | SQLite → TypeScript変換スクリプト |
app/composables/financial-data.ts | 自動生成された財務データ(7社分) |
app/pages/financial-quiz/proportional-animation-qqq.vue | 新しいVueページ |
更新したファイル
| ファイル | 変更内容 |
|---|---|
ProportionalFinancialStatementsAnimated.vue | 型定義を~/types/financialからimport |
目的
SQLite (koyfin.db) から composable ファイルを自動生成するスクリプトを作成する。
その際、以下の問題を解決する。
現状の問題点
1. データ構造の不一致
現在のcomposable(useMicrosoftData.tsなど)の構造:
// 現在の BSData(粒度が粗い)
interface BSData {
currentAssets: BalanceSheetItem[] // { label, value }[]
fixedAssets: BalanceSheetItem[]
currentLiabilities: BalanceSheetItem[]
fixedLiabilities: BalanceSheetItem[]
equity: BalanceSheetItem[]
}
// 現在の PLData(数値のみ)
interface PLData {
revenue: number
grossProfit?: number
operatingIncome?: number
profit: number
expenses: number
}
プロンプトで定義されている最新の構造(より詳細):
// プロンプトの BSData(検証用Total値を含む)
bs: {
currentAssets: [
{ label: '【総資産】', value: 174472 }, // Total Assets(検証用)
{ label: '【流動資産合計】', value: 122797 }, // Total Current Assets(検証用)
{ label: '現預金・短期投資', value: 96391 },
{ label: '売掛金', value: 17908 },
{ label: '棚卸資産', value: 2902 },
{ label: 'その他流動資産', value: 5596 },
],
// ... 他のセクションも同様に検証用Total値を含む
}
// プロンプトの PLData(配列形式でより詳細)
pl: {
revenue: [
{ label: '【売上高】', value: 93580 },
{ label: '【売上総利益】', value: 60542 },
{ label: '売上原価', value: 33038 },
],
operatingExpenses: [
{ label: '【営業利益】', value: 28172 },
{ label: '販管費', value: 20324 },
{ label: '研究開発費', value: 12046 },
],
nonOperating: [...],
netIncome: [...],
}
問題:
- 現在のVueコンポーネントは粗い粒度の構造を期待している
- プロンプトの最新構造はより詳細で検証用データを含む
- 両者が一致していない
2. Vueページの冗長なimport
現在の proportional-animation.vue:
import { useMicrosoftData } from '~/composables/useMicrosoftData'
import { useNvidiaData } from '~/composables/useNvidiaData'
import { useAlphabetData } from '~/composables/useGoogleData'
import { useBroadcomData } from '~/composables/useBroadcomData'
import { useMetaPlatformsData } from '~/composables/useMetaData'
// ... 15個のimport
const microsoftData = useMicrosoftData()
const nvidiaData = useNvidiaData()
// ... 15個の呼び出し
問題:
- 企業が増えるたびにimport文を手動で追加する必要がある
- 100社になると100個のimportが必要
- メンテナンス性が悪い
3. SQLiteデータの集約が必要
SQLite(koyfin.db)のデータ形式:
section: balance_sheet
metric_name: Total Assets
metric_value: "364,980.0M"
section: balance_sheet
metric_name: Total Cash And Short Term Investments
metric_value: "65,171.0M"
section: balance_sheet
metric_name: Total Receivables
metric_value: "66,243.0M"
... (50項目以上)
必要な処理:
- 文字列値を数値にパース(
"364,980.0M"→364980) - 複数の項目を適切なカテゴリに集約
- 差額計算(例:
その他流動資産 = Total Current Assets - 現預金 - 売掛金 - 棚卸資産) - BSバランスの検証・調整
決定事項
以下の方針で進める:
| 項目 | 決定 |
|---|---|
| データ形式 | TypeScriptオブジェクト(型チェックが効くため) |
| 型定義の場所 | types/financial.ts に分離 |
| composableファイル構成 | 1ファイル (composables/financial-data.ts) に全企業を格納 |
| 既存composable(15ファイル) | 残しておく(新旧並行) |
| 新ページパス | /financial-quiz/proportional-animation-qqq |
| Vueコンポーネント | 変更OK(expensesはplの中に移動) |
ファイルサイズ試算
| 企業数 | TypeScript | JSON | gzip後(TS) |
|---|---|---|---|
| 1社 | 30KB | 20KB | 4KB |
| 100社 | 3MB | 2MB | 400KB |
| 300社 | 9MB | 6MB | 1.2MB |
→ 100社程度なら1ファイルで問題なし。ページネーションで一度に全社表示しなければOK。
解決方針
新しいデータ構造(expensesをplに統合)
// types/financial.ts に定義
export interface BalanceSheetItem {
label: string
value: number
color?: string
}
export interface BSData {
currentAssets: BalanceSheetItem[]
fixedAssets: BalanceSheetItem[]
currentLiabilities: BalanceSheetItem[]
fixedLiabilities: BalanceSheetItem[]
equity: BalanceSheetItem[]
}
export interface PLData {
revenue: number
costOfRevenue: number // 売上原価
grossProfit: number // 売上総利益
sga: number // 販管費
rd: number // 研究開発費
operatingIncome: number // 営業利益
interestExpense?: number // 支払利息
interestIncome?: number // 受取利息
ebt: number // 税引前利益
incomeTax: number // 法人税
netIncome: number // 純利益
}
export interface CashFlowData {
operatingCF: number
capex: number
fcf: number
}
export interface PerShareData {
eps: number
}
export interface FinancialData {
bs: BSData
pl: PLData
cashFlow: CashFlowData
perShare: PerShareData
}
export interface PeriodData {
label: string // '2015', '2024', 'LTM'
data: FinancialData
}
export interface CompanyData {
ticker: string
name: string
sector?: string
industry?: string
periods: PeriodData[]
}
// 全企業データの型
export type FinancialDataStore = Record<string, CompanyData>
composableの形式
// composables/financial-data.ts
import type { FinancialDataStore } from '~/types/financial'
export const financialData: FinancialDataStore = {
AAPL: {
ticker: 'AAPL',
name: 'Apple Inc.',
sector: 'Information Technology',
periods: [
{
label: '2015',
data: {
bs: { ... },
pl: { ... },
cashFlow: { ... },
perShare: { ... }
}
},
// ... 他の期間
]
},
MSFT: { ... },
// ... 100社分
}
// ヘルパー関数
export const getCompanyData = (ticker: string): CompanyData | undefined => {
return financialData[ticker]
}
export const getAllTickers = (): string[] => {
return Object.keys(financialData)
}
export const getCompaniesBySector = (sector: string): CompanyData[] => {
return Object.values(financialData).filter(c => c.sector === sector)
}
SQLiteデータベース構造
テーブル構造
-- 使用するビュー
CREATE VIEW v_annual_data AS
SELECT
c.ticker,
c.name,
fp.period_label, -- 'FY 2015', 'FY 2024', 'Current/LTM'
fp.fiscal_year,
fda.section, -- 'balance_sheet', 'income_statement', 'cash_flow'
fda.metric_name, -- 項目名(下記参照)
fda.metric_value, -- '364,980.0M', '(9,447.0)M' など
fda.fetched_at
FROM financial_data_annual fda
JOIN companies c ON fda.company_id = c.id
JOIN financial_periods fp ON fda.period_id = fp.id;
利用可能なticker
AAPL, AMZN, AVGO, GOOGL, META, MSFT, NVDA
利用可能な期間(period_label)
| period_type | period_label | 用途 |
|---|---|---|
| annual | FY 2015 ~ FY 2025 | 年次データ |
| ltm | Current/LTM | 直近12ヶ月 |
取得対象期間の仕様
- 取得対象:
FY 2015~FY 2025+Current/LTM - 変換後のlabel:
'2015','2016', ...,'2025','LTM'
SQLite → CompanyData 変換ロジック
値のパース関数
function parseValue(value: string | null): number {
if (!value || value === 'None' || value === '') 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
}
BS metric_name 完全リスト(section: balance_sheet)
Accounts Payable | 買掛金
Accounts Receivable | 売掛金
Accrued Expenses | 未払費用
Accumulated Depreciation | 減価償却累計額
Additional Paid In Capital | 資本剰余金
Book Value / Share | 1株当たり純資産
Cash And Equivalents | 現預金
Common Equity | 普通株式資本
Common Stock | 普通株式
Comprehensive Income and Other | その他包括利益
Current Income Taxes Payable | 未払法人税
Current Portion of Leases | 短期リース
Current Portion of Long-Term Debt | 短期借入金
Deferred Charges Long-Term | 長期繰延費用
Deferred Tax Assets Long-Term | 繰延税金資産
Deferred Tax Liability Non Current | 繰延税金負債
ECS Total Common Shares Outstanding | 発行済株式数
ECS Total Shares Outstanding on Filing Date | 届出日発行済株式数
Equity Method Investments | 持分法投資
Goodwill | のれん
Gross Property Plant And Equipment | 有形固定資産(総額)
Inventory | 棚卸資産
Loans Receivable Long-Term | 長期貸付金
Long-Term Debt | 長期借入金
Long-Term Leases | 長期リース
Long-term Investments | 長期投資
Net Debt | 純有利子負債
Net Property Plant And Equipment | 有形固定資産(純額)
Other Current Assets | その他流動資産
Other Current Liabilities | その他流動負債
Other Intangibles | その他無形資産
Other Long-Term Assets | その他固定資産
Other Non Current Liabilities | その他固定負債
Other Receivables | その他売掛金
Prepaid Expenses | 前払費用
Restricted Cash | 拘束性預金
Retained Earnings | 利益剰余金
Short Term Investments | 短期投資
Tangible Book Value | 有形純資産
Tangible Book Value Per Share | 1株当たり有形純資産
Total Assets | 総資産(検証用)
Total Cash And Short Term Investments | 現預金・短期投資
Total Current Assets | 流動資産合計(検証用)
Total Current Liabilities | 流動負債合計(検証用)
Total Debt | 有利子負債合計
Total Equity | 純資産合計(検証用)
Total Liabilities | 負債合計(検証用)
Total Liabilities And Equity | 負債純資産合計(検証用)
Total Receivables | 売掛金合計
Trading Asset Securities | 売買目的有価証券
Treasury Stock | 自己株式
Unearned Revenue Current, Total | 前受収益(流動)
Unearned Revenue Non Current | 前受収益(固定)
BSマッピング定義
const bsMapping = {
currentAssets: [
{ sqlite: 'Total Cash And Short Term Investments', label: '現預金・短期投資' },
{ sqlite: 'Total Receivables', label: '売掛金' },
{ sqlite: 'Inventory', label: '棚卸資産' },
// 'その他流動資産' は差額計算: Total Current Assets - 上記合計
],
fixedAssets: [
{ sqlite: 'Net Property Plant And Equipment', label: '有形固定資産' },
{ sqlite: 'Long-term Investments', label: '長期投資' },
{ sqlite: 'Goodwill', label: 'のれん' },
{ sqlite: 'Other Intangibles', label: '無形資産' },
// 'その他固定資産' は差額計算: Total Assets - Total Current Assets - 上記合計
],
currentLiabilities: [
{ sqlite: 'Accounts Payable', label: '買掛金' },
{ sqlite: 'Current Portion of Long-Term Debt', label: '短期借入金' },
{ sqlite: 'Current Portion of Leases', label: '短期リース' },
{ sqlite: 'Unearned Revenue Current, Total', label: '前受収益(流動)' },
// 'その他流動負債' は差額計算: Total Current Liabilities - 上記合計
],
fixedLiabilities: [
{ sqlite: 'Long-Term Debt', label: '長期借入金' },
{ sqlite: 'Long-Term Leases', label: '長期リース' },
{ sqlite: 'Unearned Revenue Non Current', label: '長期前受収益' },
// 'その他固定負債' は差額計算: Total Liabilities - Total Current Liabilities - 上記合計
],
equity: [
{ sqlite: 'Common Stock', label: '資本金' },
{ sqlite: 'Additional Paid In Capital', label: '資本剰余金' },
{ sqlite: 'Retained Earnings', label: '利益剰余金' },
{ sqlite: 'Treasury Stock', label: '自己株式' },
{ sqlite: 'Comprehensive Income and Other', label: 'その他包括利益' },
],
}
PL metric_name 完全リスト(section: income_statement)
Basic EPS - Continuing Operations | 基本EPS(継続事業)
Basic Weighted Average Shares Outstanding | 基本加重平均株式数
Cost Of Revenues | 売上原価
D&A for EBITDA | EBITDA用D&A
Depreciation & Amortization | 減価償却費
Diluted EPS - Continuing Operations | 希薄化後EPS(継続事業)
Diluted Weighted Average Shares Outstanding | 希薄化後加重平均株式数
Dividend Per Share | 1株当たり配当
EBIT | EBIT
EBITA | EBITA
EBITDA | EBITDA
EBT, Excl. Unusual Items | 税引前利益(特別項目除く)
EBT, Incl. Unusual Items | 税引前利益
Earnings From Continuing Operations | 継続事業利益
Effective Tax Rate - (Ratio) | 実効税率
Gain (Loss) On Sale Of Assets | 資産売却損益
Gain (Loss) On Sale Of Investments | 投資売却損益
General and Administrative Expenses | 一般管理費
Gross Profit (Loss) | 売上総利益
Impairment of Goodwill | のれん減損
Income (Loss) On Equity Affiliates | 持分法損益
Income Tax Expense | 法人税等
Interest And Investment Income | 受取利息・投資収益
Interest Expense | 支払利息
Minority Interest | 少数株主損益
Net EPS - Basic | 基本EPS
Net EPS - Diluted | 希薄化後EPS
Net Income | 純利益
Net Income to Common Excl. Extra Items | 普通株主帰属純利益(特別項目除く)
Net Income to Common Incl Extra Items | 普通株主帰属純利益
Net Interest Expenses | 純支払利息
Normalized Basic EPS | 調整後基本EPS
Normalized Diluted EPS | 調整後希薄化後EPS
Normalized Net Income | 調整後純利益
Operating Income | 営業利益
Other Non Operating Income (Expenses) | 営業外損益
Other Operating Expenses | その他営業費用
Other Revenues | その他収益
Other Unusual Items, Total | 特別損益
Payout Ratio | 配当性向
R&D Expenses | 研究開発費
Restructuring Charges | リストラ費用
Revenues | 売上高
Selling General & Admin Expenses | 販管費
Selling and Marketing Expenses | 販売費
Stock-Based Comp., Other (Total) | 株式報酬(その他)
Total Revenues | 売上高合計
Total Stock-Based Compensation | 株式報酬合計
YoY Growth % | 前年比成長率
PLマッピング定義
const plMapping = {
revenue: { sqlite: 'Revenues', label: '売上高' },
costOfRevenue: { sqlite: 'Cost Of Revenues', label: '売上原価' },
grossProfit: { sqlite: 'Gross Profit (Loss)', label: '売上総利益' },
sga: { sqlite: 'Selling General & Admin Expenses', label: '販管費' },
rd: { sqlite: 'R&D Expenses', label: '研究開発費' },
operatingIncome: { sqlite: 'Operating Income', label: '営業利益' },
interestExpense: { sqlite: 'Interest Expense', label: '支払利息' },
interestIncome: { sqlite: 'Interest And Investment Income', label: '受取利息' },
ebt: { sqlite: 'EBT, Incl. Unusual Items', label: '税引前利益' },
incomeTax: { sqlite: 'Income Tax Expense', label: '法人税等' },
netIncome: { sqlite: 'Net Income', label: '純利益' },
}
// expenses計算: revenue - netIncome
// profit = netIncome
CashFlow metric_name 完全リスト(section: cash_flow)
(Gain) Loss From Sale Of Asset | 資産売却(損益)
(Gain) Loss on Sale of Investments | 投資売却(損益)
Amortization of Deferred Charges, Total | 繰延費用償却
Amortization of Goodwill and Intangible Assets | のれん・無形資産償却
Asset Writedown & Restructuring Costs | 減損・リストラ費用
Capital Expenditure | 設備投資
Cash Acquisitions | 現金買収
Cash Income Tax Paid (Refund) | 法人税支払(還付)
Cash Interest Paid | 利息支払
Cash from Financing | 財務CF
Cash from Investing | 投資CF
Cash from Operations | 営業CF
Change In Accounts Payable | 買掛金増減
Change In Accounts Receivable | 売掛金増減
Change In Income Taxes | 法人税増減
Change In Inventories | 棚卸資産増減
Change In Net Working Capital | 運転資本増減
Change in Other Net Operating Assets | その他営業資産増減
Change in Unearned Revenues | 前受収益増減
Common & Preferred Stock Dividends Paid | 配当金支払
Common Dividends Paid | 普通株配当金支払
Depreciation & Amortization | 減価償却費
Depreciation & Amortization, Total | 減価償却費合計
Divestitures | 事業売却
Foreign Exchange Rate Adjustments | 為替換算調整
Free Cash Flow | フリーキャッシュフロー
Free Cash Flow per Share | 1株当たりFCF
Investment in Mkt and Equity Securities | 有価証券投資
Issuance of Common Stock | 株式発行
Long-Term Debt Issued, Total | 長期借入
Long-Term Debt Repaid, Total | 長期借入返済
Miscellaneous Cash Flow Adjustments | その他CF調整
Net Change in Cash | 現金純増減
Net Debt Issued / Repaid | 純借入/返済
Net Income | 純利益
Other Financing Activities | その他財務活動
Other Investing Activities, Total | その他投資活動
Other Operating Activities, Total | その他営業活動
Preferred Dividends Paid | 優先株配当金支払
Repurchase of Common Stock | 自社株買い
Sale of Property, Plant, and Equipment | 固定資産売却
Short Term Debt Issued, Total | 短期借入
Short Term Debt Repaid, Total | 短期借入返済
Special Dividends Paid | 特別配当支払
Stock-Based Compensation | 株式報酬
Total Debt Issued | 借入合計
Total Debt Repaid | 借入返済合計
CashFlowマッピング定義
const cashFlowMapping = {
operatingCF: { sqlite: 'Cash from Operations', label: '営業CF' },
capex: { sqlite: 'Capital Expenditure', label: '設備投資' }, // 負の値
fcf: { sqlite: 'Free Cash Flow', label: 'FCF' },
}
PerShareマッピング定義
const perShareMapping = {
eps: { sqlite: 'Diluted EPS - Continuing Operations', label: 'EPS' },
// または 'Net EPS - Diluted'
}
差額計算
// その他流動資産 = Total Current Assets - (現預金 + 売掛金 + 棚卸資産)
const otherCurrentAssets = totalCurrentAssets - (cash + receivables + inventory)
// その他流動負債 = Total Current Liabilities - (買掛金 + 短期借入金 + 短期リース + 前受収益)
const otherCurrentLiabilities = totalCurrentLiabilities - (ap + shortTermDebt + currentLeases + unearnedRevenue)
// その他固定負債 = (Total Liabilities - Total Current Liabilities) - (長期借入金 + 長期リース + 長期前受収益)
const totalFixedLiabilities = totalLiabilities - totalCurrentLiabilities
const otherFixedLiabilities = totalFixedLiabilities - (longTermDebt + longTermLeases + longTermUnearned)
BSバランス調整
// 資産合計
const totalAssets = sumCurrentAssets + sumFixedAssets
// 負債・純資産合計
const totalLiabilitiesAndEquity = sumCurrentLiabilities + sumFixedLiabilities + sumEquity
// 差額があれば「その他長期資産」または「その他固定負債」で調整
if (totalAssets !== totalLiabilitiesAndEquity) {
const diff = totalLiabilitiesAndEquity - totalAssets
// 資産側を調整
otherLongTermAssets += diff
}
// 許容誤差: ±1M以内
// 検証失敗時: 警告ログを出力
Vueコンポーネントの変更箇所
現在の ProportionalFinancialStatementsAnimated.vue の型定義(該当箇所)
// 現在の PLData(248-254行目)
export interface PLData {
revenue: number
grossProfit?: number
operatingIncome?: number
profit: number // 純利益(Net Income)
expenses: number // ← これが使われている
}
// 現在の ExpenseData(257-260行目)
export interface ExpenseData {
sga: number // SG&A
rd: number // R&D
}
// 現在の FinancialData(274-280行目)
export interface FinancialData {
bs: BSData
pl: PLData
expenses?: ExpenseData // ← 別プロパティ
cashFlow?: CashFlowData
perShare?: PerShareData
}
PL検証ロジック(426-429行目)
const isPlBalanced = (data: FinancialData): boolean => {
// 現在: expenses + profit === revenue を検証
return Math.abs((data.pl.expenses + data.pl.profit) - data.pl.revenue) < 0.1
}
変更が必要な箇所
変更不要: 現在のコンポーネントはそのまま使える。
pl.expensesとpl.profitを使っており、これは生成スクリプトで計算して設定すればよいexpenses?: ExpenseDataは別プロパティとして存在し、表示には使用されていない(データ参照用)
生成スクリプトで設定する値:
// PLData
pl: {
revenue: parseValue(metrics['Revenues']),
grossProfit: parseValue(metrics['Gross Profit (Loss)']),
operatingIncome: parseValue(metrics['Operating Income']),
profit: parseValue(metrics['Net Income']),
expenses: parseValue(metrics['Revenues']) - parseValue(metrics['Net Income']), // 計算
}
// ExpenseData(オプション)
expenses: {
sga: parseValue(metrics['Selling General & Admin Expenses']),
rd: parseValue(metrics['R&D Expenses']),
}
実装タスク
必須タスク
- 型定義ファイルの作成
app/types/financial.ts- 現在のコンポーネントの型定義をそのまま移動(変更なし)
- 変換スクリプトの作成
scripts/generate-financial-data.mjs- SQLiteからデータを読み込み、CompanyData形式に変換
composables/financial-data.tsを自動生成pl.expenses = revenue - netIncomeで計算
- Vueコンポーネントの更新
- 型定義のimport元を
~/types/financialに変更するのみ - ロジックの変更は不要
- 型定義のimport元を
- 新しいVueページの作成
pages/financial-quiz/proportional-animation-qqq.vuefinancial-data.tsからデータを読み込み
- 生成されたファイルの検証
- BSバランスのチェック
- 画面表示の確認
ファイル構成(予定)
apps/web/
├── scripts/
│ └── generate-financial-data.mjs ← 新規作成(変換スクリプト)
├── data/
│ └── koyfin.db ← 既存(SQLiteデータ)
└── app/
├── types/
│ └── financial.ts ← 新規作成(型定義)
├── composables/
│ ├── financial-data.ts ← 新規作成(全企業データ)
│ ├── useMicrosoftData.ts ← 既存(残す)
│ ├── useNvidiaData.ts ← 既存(残す)
│ └── ... ← 既存(残す)
├── components/
│ └── financial-quiz/
│ └── ProportionalFinancialStatementsAnimated.vue ← 更新
└── pages/
└── financial-quiz/
├── proportional-animation.vue ← 既存(残す)
└── proportional-animation-qqq.vue ← 新規作成
実装時の変更点と経緯
1. SQLiteライブラリの変更
当初の計画: better-sqlite3 を使用
変更後: sql.js を使用
変更理由:
better-sqlite3はネイティブモジュールであり、Windows環境でnode-gypによるビルドが必要- ビルドには Visual Studio の C++ ツールセット(Desktop development with C++)が必要
- 環境に C++ ツールセットがインストールされていなかったため、ビルドに失敗
sql.jsは純粋JavaScript実装のため、ネイティブビルド不要
影響:
- APIが同期から非同期に変更(
db.prepare().all()→db.exec()) - 結果の取得方法が変更(
rows→result[0]?.values)
2. 型定義の分離方針
当初の計画: 型定義を完全に新しい構造に変更
変更後: 既存の型定義を維持し、~/types/financial.ts に移動
変更理由:
- 既存の
ProportionalFinancialStatementsAnimated.vueがpl.expensesとpl.profitを使用 - コンポーネントの変更を最小限に抑えるため、データ構造は既存に合わせる
- 新しい詳細なPL構造(
costOfRevenue,sga,rd等を直接持つ)は採用せず、expensesオプションプロパティとして保持
3. PLDataの構造
当初の計画:
interface PLData {
revenue: number
costOfRevenue: number
grossProfit: number
sga: number
rd: number
operatingIncome: number
netIncome: number
}
実装後:
interface PLData {
revenue: number
grossProfit?: number
operatingIncome?: number
profit: number // = netIncome
expenses: number // = revenue - profit(計算値)
}
interface ExpenseData {
sga: number
rd: number
}
変更理由:
- コンポーネントの検証ロジック
expenses + profit === revenueを変更せずに済む expensesはrevenue - netIncomeで計算して設定- SG&A、R&D は別途
ExpenseDataとして保持(チャート表示用)
4. BSの純資産(equity)の集約
当初の計画: Common Stock, Additional Paid In Capital, Retained Earnings, Treasury Stock, Comprehensive Income を個別に表示
実装後: 2項目に集約
資本金等= Common Stock + Additional Paid In Capital剰余金= Retained Earnings + Treasury Stock + Comprehensive Income(内訳をbreakdownで保持)
変更理由:
- 表示がシンプルになる
- ホバーで内訳を確認できる(breakdown機能)
- Appleのように Retained Earnings がマイナス(累積赤字)の場合も適切に表示
5. 期間ラベルの変換
SQLiteのperiod_label: 'FY 2015', 'FY 2024', 'Current/LTM'
変換後のlabel: '2015', '2024', 'LTM'
実装:
function convertPeriodLabel(periodLabel) {
if (periodLabel === 'Current/LTM') return 'LTM'
const match = periodLabel.match(/FY (\d{4})/)
if (match) return match[1]
return periodLabel
}
6. バランス検証と自動調整
実装した検証:
- BSバランス: 資産合計 = 負債合計 + 純資産合計(差額10M以上で警告)
- PLバランス: expenses + profit = revenue(差額1M以上で警告)
検証結果: 全19社・全期間でバランスOK
7. 貸借バランス調整ロジック(2025-12-12追加)
問題: SQLiteから取得した財務データで、個別項目の合計と公表されたTotal値に差異が生じることがある。 これは以下の原因による:
- 小数点の丸め誤差(SQLiteデータは小数点第1位まで保持:
45,097.1M) - カテゴリ分類の違い(SQLiteの項目分類と会計基準の差異)
調整ロジックの実装場所: apps/web/scripts/generate-financial-data.mjs
調整の流れ:
1. カテゴリ別「その他」調整(lines 143-195)
├── その他流動資産 = Total Current Assets - (現預金 + 売掛金 + 棚卸資産)
├── その他固定資産 = Total Fixed Assets - (有形固定 + 長期投資 + のれん + 無形)
├── その他流動負債 = Total Current Liab - (買掛金 + 短期借入 + ...)
└── その他固定負債 = Total Fixed Liab - (長期借入 + 長期リース + ...)
2. 純資産の剰余金調整(lines 211-218)
└── 剰余金 = Total Equity - 資本金等(差額がある場合)
3. 最終貸借バランス調整(lines 245-267)
├── assetsSum = 全資産項目の合計
├── liabAndEquitySum = 全負債・純資産項目の合計
├── balanceAdjustment = assetsSum - liabAndEquitySum
└── 差額を「その他固定負債」で調整(小数点第1位で丸め)
コード(最終貸借バランス調整部分):
// ========== 最終貸借バランス調整 ==========
// 資産合計と(負債+純資産)合計を計算
const assetsSum = currentAssets.reduce((s, i) => s + i.value, 0) +
fixedAssets.reduce((s, i) => s + i.value, 0)
const liabilitiesSum = currentLiabilities.reduce((s, i) => s + i.value, 0) +
fixedLiabilities.reduce((s, i) => s + i.value, 0)
const equitySum = equity.reduce((s, i) => s + i.value, 0)
const liabAndEquitySum = liabilitiesSum + equitySum
// 差額があれば「その他固定負債」で調整(小数点を含む差額にも対応)
const balanceAdjustment = assetsSum - liabAndEquitySum
// 小数点第1位で丸めて比較(浮動小数点誤差対策)
if (Math.abs(balanceAdjustment) > 0.01) {
// 既存の「その他固定負債」を探す
const otherFixedLiabIndex = fixedLiabilities.findIndex(i => i.label === 'その他固定負債')
if (otherFixedLiabIndex >= 0) {
// 既存の項目に加算(小数点第1位で丸め)
fixedLiabilities[otherFixedLiabIndex].value =
Math.round((fixedLiabilities[otherFixedLiabIndex].value + balanceAdjustment) * 10) / 10
} else {
// 新規に作成(小数点第1位で丸め)
fixedLiabilities.push({
label: 'その他固定負債',
value: Math.round(balanceAdjustment * 10) / 10
})
}
}
なぜ「その他固定負債」で調整するのか:
- 「その他」項目は元々差額計算で算出されるため、多少の調整は会計的に許容される
- 固定負債は流動負債より目立ちにくく、ユーザーへの影響が小さい
- 純資産の調整は剰余金で既に行われているため、負債側で調整するのが妥当
対象企業例(調整が適用された):
- ASML: 0.1Mの調整
- NFLX: 0.4Mの調整
- PLTR: 0.3Mの調整
参考ファイル
- 既存composable例:
apps/web/app/composables/useMicrosoftData.ts - Vueコンポーネント:
apps/web/app/components/financial-quiz/ProportionalFinancialStatementsAnimated.vue - SQLiteスキーマ:
apps/web/data/schema.sql - データ変換プロンプト:
apps/web/content/2025-12-11/qqq-financial-data-pipeline.md
今後の作業
- 企業追加: SQLiteにデータを追加後、
node scripts/generate-financial-data.mjsを実行 - 既存composable削除: 新システムが安定したら、個別のcomposable(
useMicrosoftData.ts等)を削除可能 - 既存ページ統合:
proportional-animation.vueをproportional-animation-qqq.vueに統合することも検討可能