• #prompt-engineering
  • #financial-data
  • #typescript

なぜスクリプト自動化ではなくAIを使うのか

背景

KoyfinからエクスポートされるTSVファイルは一定のフォーマットを持っているため、スクリプトによる自動変換を試みた。しかし、実験の結果、AIによる変換の方が圧倒的に優れていることが判明した。

スクリプト自動化の限界

  1. 勘定科目のマッピングが厳密すぎる
    • スクリプトは完全一致でしか項目をマッピングできない
    • 例: Short-Term DebtShort Term Debt(ハイフンの有無)で別項目として認識される
    • 企業によって勘定科目名が微妙に異なるケースに対応できない
  2. 「その他」への集約が困難
    • マッピングに該当しない項目を「その他流動資産」「その他固定負債・税」などに集約する必要がある
    • スクリプトでは、マッピング定義にない項目を適切なカテゴリに振り分けることができない
    • AIは文脈を理解して、どの「その他」に振り分けるべきか判断できる
  3. BSバランス調整の柔軟性
    • 元データの端数処理により、資産合計と負債・純資産合計が一致しないことがある
    • スクリプトは固定的なロジック(例: 常に「投資・その他」を調整)しか持てない
    • AIは状況に応じて最適な調整先を選択できる
  4. 数値の丸め処理
    • 浮動小数点の計算誤差により、合計値が微妙にずれることがある
    • AIは「合理的な丸め」を判断できるが、スクリプトは厳密な計算しかできない

AIの強み

観点スクリプトAI
勘定科目の認識完全一致のみ意味的理解(セマンティック)
新しい項目への対応マッピング追加が必要自動で適切に分類
BSバランス調整固定ロジック状況に応じた柔軟な調整
エラーハンドリング失敗するかサイレントに無視問題を認識して対処
メンテナンスコスト高い(企業ごとに例外処理)低い(プロンプトのみ)

結論

財務データの変換にはAI(Gemini等)を使い続けるべき。 スクリプト自動化は:

  • 初期開発コストがかかる
  • 企業ごとの差異に対応するためのメンテナンスコストが高い
  • 結果的にAIより品質が低い出力になる

スクリプトが向いているのは、フォーマットが完全に固定されており、例外が一切ないデータの変換のみ。財務データは企業ごとに勘定科目の構成が異なるため、この条件に当てはまらない。


ティッカーシンボル一覧

Koyfinで検索する際に使用するティッカーシンボル:

カテゴリ企業名ティッカーComposable
ハイパースケーラーMicrosoftMSFTuseMicrosoftData.ts
Google (Alphabet)GOOGLuseGoogleData.ts
AmazonAMZNuseAmazonData.ts
半導体NVIDIANVDAuseNvidiaData.ts
BroadcomAVGOuseBroadcomData.ts
TSMCTSMuseTsmcData.ts
ASMLASMLuseAsmlData.ts
Big TechMetaMETAuseMetaData.ts
AppleAAPLuseAppleData.ts
バフェット銘柄Coca-ColaKOuseCocaColaData.ts
American ExpressAXPuseAmexData.ts
Bank of AmericaBACuseBofaData.ts
Moody'sMCOuseMoodysData.ts
決済ネットワークMastercardMAuseMastercardData.ts
VisaVuseVisaData.ts

コピペ用(カンマ区切り):

MSFT, GOOGL, AMZN, NVDA, AVGO, TSM, ASML, META, AAPL, KO, AXP, BAC, MCO, MA, V

ワークフロー概要

財務データを可視化コンポーネントで表示するまでの流れ:

1. Koyfinでデータ取得
   ├── BS(貸借対照表)をテキストファイルにコピー
   ├── PL(損益計算書)をテキストファイルにコピー
   └── CF(キャッシュフロー)をテキストファイルにコピー

1.5. 生データを保存(バックアップ用)
   └── apps/web/data/financial-raw/{企業名}/ に保存
   └── 例: apps/web/data/financial-raw/microsoft/bs.txt
   └── ※ このディレクトリはCloudflareにはデプロイされない

2. Geminiにデータを投げる
   └── 下記のシステムプロンプトを使用
   └── 3つのテキストファイルの内容を貼り付け

3. Geminiの出力をcomposableとして保存
   └── apps/web/app/composables/use{企業名}Data.ts として保存
   └── 例: useMicrosoftData.ts, useAppleData.ts

4. ページコンポーネントで使用
   └── composableをimportして表示コンポーネントに渡す

Gemini用システムプロンプト

Role: あなたは、財務データを解析し、Webアプリケーション(React/TypeScript)用の可視化データ構造に変換するシニアデータエンジニアです。

Objective: ユーザーから提供される「ExcelやWebからコピーされた生の財務データ(テキスト形式)」を読み取り、指定されたTypeScriptの CompanyData[] 形式に変換してください。

Data Structure Constraints: 以下のTypeScriptインターフェースに従ってデータを作成してください。

interface CompanyData {
  name: string;
  periods: {
    label: string; // 例: '2015', '2025 (Est)', 'LTM'
    data: {
      // === 貸借対照表(BS) ===
      bs: {
        currentAssets: { label: string; value: number }[];
        fixedAssets: { label: string; value: number }[];
        currentLiabilities: { label: string; value: number }[];
        fixedLiabilities: { label: string; value: number }[];
        equity: { label: string; value: number }[];
      };

      // === 損益計算書(PL) ===
      pl: {
        revenue: number;          // 売上高
        grossProfit: number;      // 売上総利益
        operatingIncome: number;  // 営業利益
        profit: number;           // 純利益(Net Income)
        expenses: number;         // 費用合計(revenue - profit)
      };

      // === 経費内訳 ===
      expenses: {
        sga: number;              // SG&A(販売費及び一般管理費)
        rd: number;               // R&D(研究開発費)
      };

      // === キャッシュフロー ===
      cashFlow: {
        operatingCF: number;      // 営業キャッシュフロー
        capex: number;            // 設備投資(負の値)
        fcf: number;              // フリーキャッシュフロー
      };

      // === 1株当たり指標 ===
      perShare: {
        eps: number;              // EPS(1株当たり利益、Diluted)
      };
    };
  }[];
}

Processing Logic & Rules:

  1. 項目のマッピングと集約(英語 -> 日本語): 入力データの勘定科目を以下のように分類・集約し、ラベル付けしてください。単位は元のデータのまま(百万ドル等)扱ってください。
* **BS - 流動資産 (`currentAssets`):**
    * "現預金・短期投資": *Total Cash, Short Term Investments*
    * "売掛金": *Total Receivables, Accounts Receivable*
    * "棚卸資産": *Inventory*
    * "その他流動資産": *Prepaid Expenses, Other Current Assets*
* **BS - 固定資産 (`fixedAssets`):**
    * "有形固定資産": *Net Property Plant And Equipment*
    * "のれん・無形資産": *Goodwill, Intangibles*
    * "投資・その他": *Long-term Investments, Deferred Tax Assets, Other Long-Term Assets* (**※調整項目**)
* **BS - 流動負債 (`currentLiabilities`):**
    * "買掛金": *Accounts Payable*
    * "短期借入・リース": *Short-Term Debt, Current Portion of Leases/Debt*
    * "前受収益(流動)": *Unearned Revenue (Current)*
    * "その他流動負債": *Accrued Expenses, Other Current Liabilities*
* **BS - 固定負債 (`fixedLiabilities`):**
    * "長期借入金": *Long-Term Debt*
    * "長期リース": *Long-Term Leases* (**※必ず独立項目として抽出すること**)
    * "長期前受収益": *Unearned Revenue (Non-Current)*
    * "その他固定負債・税": *Deferred Tax Liab, Other Non-Current Liabilities* (**※調整項目**)
* **BS - 純資産 (`equity`):**
    * "資本金等": *Common Stock, Paid In Capital*
    * "利益剰余金": *Retained Earnings*
    * "その他包括利益": *Accumulated Other Comprehensive Income*

2. PLデータの計算: * revenue: Total Revenues の値を使用。 * grossProfit: Gross Profit の値を使用。 * operatingIncome: Operating Income の値を使用。 * profit: Net Income の値を使用。 * expenses: revenue - profit で算出。

  1. 経費内訳データ (expenses):
    • sga: SG&A (Selling General & Admin Expenses) の値を使用。
    • rd: R&D (Research & Development) の値を使用。
  2. キャッシュフローデータ (cashFlow):
    • operatingCF: Operating Cash Flow の値を使用。
    • capex: Capital Expenditure の値を使用(負の値として格納すること)。
    • fcf: Free Cash Flow の値を使用。または operatingCF + capex で算出。
  3. 1株当たり指標 (perShare):
    • eps: EPS Diluted (Diluted Earnings Per Share) の値を使用。
  4. BSバランスの整合性チェックと自動調整(最重要): 入力データは端数処理や項目の欠落により、資産合計と負債・純資産合計が一致しない場合があります。以下の手順で必ず一致させてください。
* **Step A:** 入力データの *Total Assets* の値を正(ターゲット値)とします。
* **Step B (資産側):** 集計した流動資産と固定資産の合計がターゲット値と一致しない場合、その差額を**固定資産の「投資・その他」**に加減算して調整してください。
* **Step C (負債側):** 集計した負債と純資産の合計がターゲット値と一致しない場合、その差額を**固定負債の「その他固定負債・税」**に加減算して調整してください。
* 結果として、出力データの資産合計と負債・純資産合計は完全に一致しなければなりません。

7. 期間の取り扱い: * 過去の実績データだけでなく、入力に含まれる**未来の予測(FY2025など)LTM(直近12ヶ月)**の列も全て出力に含めてください。

Koyfinで取得すべきデータ項目:

カテゴリKoyfin項目名出力先
損益計算書Total Revenuespl.revenue
損益計算書Gross Profitpl.grossProfit
損益計算書Operating Incomepl.operatingIncome
損益計算書Net Incomepl.profit
損益計算書SG&Aexpenses.sga
損益計算書R&Dexpenses.rd
キャッシュフローOperating Cash FlowcashFlow.operatingCF
キャッシュフローCapital ExpenditurecashFlow.capex
キャッシュフローFree Cash FlowcashFlow.fcf
1株指標EPS DilutedperShare.eps
貸借対照表各項目bs.*

Output Format:

出力はVue Composable形式で生成してください。そのまま apps/web/app/composables/use{企業名}Data.ts として保存できる形式にします。

ファイル命名規則:

  • use{企業名}Data.ts 形式(例: useMicrosoftData.ts, useAppleData.ts
  • 関数名も同様に use{企業名}Data とする

出力例(useMicrosoftData.ts):

import type { CompanyData } from '~/components/financial-quiz/ProportionalFinancialStatementsAnimated.vue'

export const useMicrosoftData = (): CompanyData[] => [
  {
    name: 'Microsoft (FY2015-2025 + LTM)',
    periods: [
      // FY 2015
      {
        label: '2015',
        data: {
          bs: {
            currentAssets: [
              { label: '現預金・短期投資', value: 96391 },
              { label: '売掛金', value: 17908 },
              { label: '棚卸資産', value: 2902 },
              { label: 'その他流動資産', value: 5596 },
            ],
            fixedAssets: [
              { label: '有形固定資産', value: 14731 },
              { label: 'のれん・無形資産', value: 21774 },
              { label: '投資・その他', value: 15170 },
            ],
            currentLiabilities: [
              { label: '買掛金', value: 6591 },
              { label: '短期借入・リース', value: 2499 },
              { label: '前受収益(流動)', value: 23223 },
              { label: 'その他流動負債', value: 17334 },
            ],
            fixedLiabilities: [
              { label: '長期借入金', value: 27808 },
              { label: '長期リース', value: 0 },
              { label: '長期前受収益', value: 2095 },
              { label: 'その他固定負債・税', value: 14839 },
            ],
            equity: [
              { label: '資本金等', value: 68465 },
              { label: '剰余金', value: 11618 },
            ],
          },
          pl: {
            revenue: 93580,
            grossProfit: 60542,
            operatingIncome: 28172,
            profit: 12193,
            expenses: 81387,
          },
          expenses: {
            sga: 20324,
            rd: 12046,
          },
          cashFlow: {
            operatingCF: 29668,
            capex: -5944,
            fcf: 23724,
          },
          perShare: {
            eps: 1.48,
          },
        },
      },
      // FY 2016
      {
        label: '2016',
        // ... 同様の構造
      },
      // ... 他の期間も全て含める
    ],
  },
]

重要な注意事項:

  • 各期間には // FY 20XX のコメントを付けて可読性を高める
  • 末尾のカンマ(trailing comma)を付ける
  • BSバランスが一致しない場合は調整を行い、コメントで調整内容を記載する

ページコンポーネントでの使用例

Geminiが出力したcomposableを保存後、ページで以下のように使用します。

<template>
  <ProportionalFinancialStatementsAnimated
    v-model="periodIndex"
    :companies="companyData"
    :max-height="600"
    :play-interval="1500"
  />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import ProportionalFinancialStatementsAnimated from '~/components/financial-quiz/ProportionalFinancialStatementsAnimated.vue'
import { useMicrosoftData } from '~/composables/useMicrosoftData'

// composableからデータを取得
const companyData = useMicrosoftData()
const periodIndex = ref(0)
</script>

チャートコンポーネント用のデータも同様にcomposableから取得できます:

// 利益率データ
const marginData = computed(() => {
  const company = companyData[0]
  return company.periods.map(period => ({
    label: period.label,
    grossMargin: (period.data.pl.grossProfit / period.data.pl.revenue) * 100,
    operatingMargin: (period.data.pl.operatingIncome / period.data.pl.revenue) * 100,
    margin: (period.data.pl.profit / period.data.pl.revenue) * 100,
  }))
})

// EPSデータ
const epsData = computed(() => {
  const company = companyData[0]
  return company.periods.map(period => ({
    label: period.label,
    eps: period.data.perShare?.eps || 0,
  }))
})

// キャッシュフローデータ
const cashFlowData = computed(() => {
  const company = companyData[0]
  return company.periods.map(period => ({
    label: period.label,
    operatingCF: period.data.cashFlow?.operatingCF || 0,
    capex: period.data.cashFlow?.capex || 0,
    fcf: period.data.cashFlow?.fcf || 0,
  }))
})