趣味(hobby)

金利データを使って投資環境を自動評価!スプレッドシートとGASで毎月の環境を自動集計&通知する方法

キーワード: 金利を見れば投資はうまくいく, 投資環境, 自動集計, 通知,
免責事項: 本記事の計算された数値は将来の確実性を示すものではありません。最終的な投資判断は自己責任でお願いいたします。

背景

最近、書籍「金利を見れば投資はうまくいく」を読みました。

本書の最後に紹介されていた、現在(特に米国)の投資環境に対する評価例が面白く、自分でも計算してみようと思いました。

この評価は、基本的にFRBの提供するデータを基に行われており、一度手動で集計してみました。
しかしながら、これを毎月行うのは非常に手間がかかると感じました。

そこで、毎月のデータを自動で集計し、指定した場所へ通知する仕組みを作り、最終的な評価結果だけを確認できるようにしたいと考えました。

今回、Google Apps Script(GAS)とGoogleスプレッドシート、Googleドライブを利用し、特別な追加費用をかけずにこれらの仕組みを実装したので、その手順を紹介します。

なお、私は金融の専門家ではありません。ここに記載している数字や計算結果が妥当かどうかは保証できません。最終的な判断は自己責任でお願いいたします。

想定読者

  • 「金利を見れば投資はうまくいく」を読み、同様の評価と通知を自動化したいと考えている方
  • 現在の投資環境を各種指標に基づいて分析してみたい方

完成イメージ

この仕組みの最終的な成果物としては以下のようなものを目指す。

  • 毎月、最新の指標データをFRBのAPIから取得し、Googleスプレッドシートに追加。評価結果を自動計算する
  • 現在の投資環境に関する分析結果や関連指標の推移をSlackに自動で通知する

概要

本仕組みの大まかな流れは以下の通りです。

  1. FRBのAPIから指標情報を取得
  2. 取得した情報をGoogleスプレッドシートに追加
  3. スプレッドシート内で書籍に記載されている評価基準に基づき分析結果を出力
  4. その分析結果をSlackの指定チャンネルに通知

これにより、手間のかかる集計作業なしで、定期的に分析結果を確認できるようになります。


分析で使用する指標

各指標の意義や分析方法の詳細については、本書に記載されているため、ここでは割愛します。以下に、使用する指標を一覧します。

政策金利 (Effective Federal Funds Rate): FEDFUNDS

前年と比較して水準が高い(利上げ)場合、約1年後に景気減速に作用する可能性があるとされている。本書では、5年周期の「金融政策サイクル」が存在するとされ、そのトレンド判断に有用な指標である。

10年国債利回り (10-Year Treasury Constant Maturity Rate): DGS10

前年度との差を計算することで、長期金利の上昇・低下トレンドを判断できる。長短金利差がマイナスかつ低下トレンドである場合、景気減速のサインとなり得る。本書では、5年周期の「金融政策サイクル」が存在するとされ、そのトレンド判断に有用な指標である。

金利差(政策金利 – 10年国債利回り)

政策金利から10年国債利回りを引いて算出される重要な指標で、逆イールド(長短金利差がマイナス)になると景気減速の示唆とされる。本書では、5年周期の「金融政策サイクル」が存在するとされ、そのトレンド判断に有用な指標である。

社債スプレッド (Moody’s Seasoned Baa Corporate Bond Yield Related to Yield on 10-Year Treasury Constant Maturity): BAA10Y

前年との差分を算出し、スプレッドの変動方向を見る。スプレッドの拡大は景気減速のサインとされている。

米ドル指数 (Nominal Broad U.S. Dollar Index): TWEXBGSMTH

前年からの変化率を計算し、ドル高が新興国経済に与える影響を分析する際に使用する。

環境状況自動集計/通知システムの構築

事前準備

過去データの取得

今回実装するスクリプトでは、過去データの自動取得までは対応していません。
初回の1回のみですが、各指標の過去データを取得し、自分で準備したGoogleSpreadSheetに反映させましょう。

以下のテンプレートを用意させていただきました。
こちらをコピーして利用していただくのがオススメです。
template: https://docs.google.com/spreadsheets/d/1wG4y58GJ9bwh7pwsZimOGoPR5oczP4-YMdTekM6htjw/edit?usp=sharing

テンプレート上、抜けているデータもあるので、そこは各々での補完をお願いいたします。

以下、DGS10の場合の例です。
link: https://fred.stlouisfed.org/series/DGS10#0

まずは"Edit Graph"を選択します。

Monthly GrequencyとAggregation methodを指定可能な場合、これを以下のように指定します。

  • Monthly Grequency: Monthly
  • Aggregation method: End of Period

最後に、CSVでデータをダウンロードしましょう。

以下の"Data"シート上に実データを記入します。

プログラムでデータを取得するためのAPIキーの発行

以下のサイトに飛んだ後に、個人アカウントを作成します。
https://fred.stlouisfed.org/

今回は全体的にGoogleのお世話になっているので、
Google Accountでアカウントも作成してしまうのが楽だと思います。

アカウント作成/ログイン後は、"API key"を選択。

"Request API Key"を選択。

その後、APIの利用目的を記入する必要があります。
ここはChatgptにでも作ってもらいましょう。

順当に進めるとAPI keyが入手出来ます。
後でこのキーを利用しますので、Publicに公開しない形で情報を記録しておきましょう。

実装

それでは、実装パートに移ります。
※ ほぼchatgptに作成してもらっています。このスクリプトの利用も自己責任でお願いします。

最新情報の自動取得

まずは、最新の指標情報を取得して、自分で準備したスプレットシートを更新するスクリプトを作成します。

スプレッドシートから"Apps Script"を選択し、スクリプトの編集画面に飛びます。

以下のスクリプトを貼り付けましょう。

function fetchLatestObservation(seriesId, frequency = 'm', aggregationMethod = 'eop') {
  const apiKey = PropertiesService.getScriptProperties().getProperty('FRB_API_KEY');
  const url = `https://api.stlouisfed.org/fred/series/observations?series_id=${seriesId}&api_key=${apiKey}&file_type=json&frequency=${frequency}&aggregation_method=${aggregationMethod}&sort_order=desc&limit=1`;

  try {
    const response = UrlFetchApp.fetch(url);
    const data = JSON.parse(response.getContentText());

    if (data && data.observations && data.observations.length > 0) {
      const latestObservation = data.observations[0];
      const latestDate = latestObservation.date;
      const latestValue = latestObservation.value;

      if (latestValue === ".") {
        Logger.log(`Series: ${seriesId} | Date: ${latestDate} has missing data.`);
        return { date: latestDate, value: null, message: "Missing data" };
      }

      Logger.log(`Series: ${seriesId} | Latest Date: ${latestDate}, Value: ${latestValue}`);
      return { date: latestDate, value: latestValue };
    } else {
      Logger.log(`No data found for series ID: ${seriesId}`);
      return null;
    }
  } catch (error) {
    Logger.log(`Error fetching data for series ID: ${seriesId} | Error: ${error}`);
    return null;
  }
}

function updateIndicatorCell(indicator, date, value) {
  const sheetName = 'Data'; // Replace with your actual sheet name
  const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = spreadsheet.getSheetByName(sheetName);

  // Define the column indices for each indicator
  const columnIndices = {
    "FEDFUNDS": 2,
    "DGS10": 3,
    "BAA10Y": 4,
    "TWEXBGSMTH": 5,
    "United States ISM Manufacturing PMI": 6
  };

  // Ensure the indicator exists in our column mappings
  if (!(indicator in columnIndices)) {
    Logger.log(`Indicator "${indicator}" is not defined in column mappings.`);
    return;
  }

  // Locate the row with the matching date
  const dateColumnIndex = 1; // Assuming the Date column is the first column
  const dataRange = sheet.getDataRange();
  const values = dataRange.getValues();
  let rowIndex = -1;

  // Compare date directly as a string (assumed format 'YYYY-MM-DD')
  const formattedDate = date; // Assuming date is already in 'YYYY-MM-DD' format

  // Find the row with the matching date
  for (let i = 1; i < values.length; i++) { // Start from 1 to skip the header row
    const cellDate = values[i][dateColumnIndex - 1]; // Get the cell value

    // Convert the cellDate to JST
    const jstDate = new Date(cellDate);
    jstDate.setHours(jstDate.getHours() + 9); // Add 9 hours for JST
    const cellDateStr = jstDate.toISOString().split('T')[0]; // Convert to 'YYYY-MM-DD'

    if (cellDateStr === formattedDate) { // Compare as strings
      rowIndex = i + 1; // Convert to 1-based index for Google Sheets
      break;
    }
  }

  // If the date is not found, log an info message and return without adding a new row
  if (rowIndex === -1) {
    Logger.log(`Date ${formattedDate} not found in the sheet. No update was made.`);
    return;
  }

  // Get the column index for the indicator
  const columnIndex = columnIndices[indicator];

  // Update the cell only if the value is valid (not "." or null)
  if (value !== "." && value !== null) {
    sheet.getRange(rowIndex, columnIndex).setValue(parseFloat(value));
    Logger.log(`Updated ${indicator} on ${formattedDate} with value: ${value}`);
  } else {
    Logger.log(`Skipped updating ${indicator} on ${formattedDate} due to missing or invalid value.`);
  }
}

// Main function to fetch data and update the sheet
function main() {
  const indicators = [
    { seriesId: 'FEDFUNDS' },
    { seriesId: 'DGS10' },
    { seriesId: 'BAA10Y' },
    { seriesId: 'TWEXBGSMTH' }
  ];

  // Iterate over each indicator to fetch the data and update the sheet
  indicators.forEach(indicator => {
    const data = fetchLatestObservation(indicator.seriesId);
    if (data) {
      updateIndicatorCell(indicator.seriesId, data.date, data.value);
    }
  });
}

貼り付け後は以下のようなイメージです。

次に事前に準備したAPIキーを埋め込みます。
秘匿情報なので、コードには直接貼り付けずに設定します。

  1. 歯車マークを選択
  2. "スクリプトプロパティを追加"を選択
  3. プロパティ: FRB_API_KEY, 値: 先程取得した APIの値 をそれぞれ入力
  4. "スクリプトプロパティを保存"を保存

あとは、スクリプトを試しに実行します。
スクリプト実行ボタン押下後に、権限付与が必要だと言われた場合にはこれを許可します。

"main"を選択した上で、"実行"を押下します。

該当月に最新の値が反映されれば、無事に実装完了です。

あとは、このスクリプトが定期実行されるように設定しましょう。

"トリガー"を選択。

"トリガーを追加"を選択。

以下の画像に沿って設定する。時間や頻度やお好みで設定してください。
1週間に1度程度動かしておくで良いと思います。

最後に"Calculation"シートを見てみましょう。
取得したデータを元に、結果の集計を行います。

データを元に、グラフも描写されるようにしています。

集計結果の自動通知

次は集計結果をSlackに通知する方法です。
SlackアカウントやSlackチャンネルへの投稿用のkey(WEB_HUUK_URL)の発行は既に完了している前提で進めます。

分からない方は以下のような記事を参考に、設定をお願いいたします。

まずは新しいAppsScriptを作成します。
https://script.google.com/home

以下のスクリプトを貼り付けます。

function sendAllChartsToSlack() {
  const spreadsheetId = PropertiesService.getScriptProperties().getProperty('SID'); // スプレッドシートのID
  const sheet = SpreadsheetApp.openById(spreadsheetId).getSheetByName('Calculation'); // "Calculation"シート
  const charts = sheet.getCharts(); // シート内のすべての図表を取得

  // SlackのWebhook URL
  const slackWebhookUrl = PropertiesService.getScriptProperties().getProperty('WEB_HOOK_URL'); // SlackのWebhook URL

  const userName = "Fundリス子"; // Slackに通知する時の名前になります
  const icon = ":risu:"; // 表示されるアイコン

  // すべての図表を処理
  charts.forEach(function(chart, index) {
    // 図表を画像としてエクスポート
    const imageBlob = chart.getAs('image/png');

    // Google Driveに一時保存
    const file = DriveApp.createFile(imageBlob);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW); // 公開設定

    // Google Driveの公開URLを取得し、画像URLを作成
    const fileId = file.getId();
    const fileUrl = `https://drive.google.com/uc?export=view&id=${fileId}`; // 画像URL

    Logger.log(`File URL: ${fileUrl}`);  // URLの確認ログ

    // Slackに送信するペイロードを作成
    const payload = {
      "username" : userName,
      "icon_emoji" : icon,
      "text": `Here's the chart #${index + 1} from the Calculation sheet:`,
      "attachments": [
        {
          "fallback": `Chart #${index + 1} from the Calculation sheet`,
          "image_url": fileUrl // Google Driveの画像URLをSlackに送信
        }
      ]
    };

    // Slackに通知を送信
    const options = {
      "method": "post",
      "contentType": "application/json",
      "payload": JSON.stringify(payload)
    };

    try {
      // Slackに画像を送信
      const response = UrlFetchApp.fetch(slackWebhookUrl, options);
      Logger.log(`Sent chart #${index + 1} to Slack.`);
    } catch (e) {
      Logger.log(`Error sending chart #${index + 1} to Slack: ${e}`);
    }

    // Google Driveから画像ファイルを削除(任意)
    file.setTrashed(true); // 送信後にファイルを削除する場合
  });
}

秘匿情報として、SIDとWEB_HOOK_URLを先ほどと同様の手順で設定します。

ここまで出来たら、スクリプトを実行します。

"Calculation"シートにある図表がSlackに通知されます。

あとは、先ほどと同様にトリガーを設定し、定期的にこのスクリプトが実行されるように設定しておきます。

これで、手間をかけずに定期的に指標を確認できます。

まとめ

今回は書籍: 金利を見れば投資はうまくいく で紹介されていた分析手法をプログラム上で自動集計し、通知する方法を紹介しました。

なぜこのような指標を参考にするのか、その意図や考え方はこの書籍の中で詳しく語られています。

興味がある方はそちらもご覧くださいませ。

筆者は結局どう動けばいいんだ?という部分が良く分かっていないので、
以下のような書籍の方が参考になるのかもしれないです…

ここまで見ていただき、ありがとうございました。