2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【J-Quants】 J-Quants API × Googleスプレッドシート (+GoogleAppScript) を用いた日本株の情報抽出

Last updated at Posted at 2023-09-13

目次


はじめに

はじめまして、ねぎっつ(@negitts)と申します。

普段は主にTwitterにて活動しており、技術や日本株についてつぶやいております。
とあるきっかけでJ-Quantsについて知り、自分の株式投資について活かせないかと思い、本記事を書き始めました。
初っ端から予防線を引くようで恐縮なのですが、本業はソフトウェアエンジニアではないため、コーディングや技術資料のお作法のようなものはほとんど存じ上げませんので、お手柔らかにお読みいただけると幸いです。
コーディングは全面的にchatGPTくんの協力を得ているため、Javascriptコードの規則は正直あまり理解できていないです。

J-Quantsとは(引用)

J-Quantsとは、ヒストリカル株価データ・財務データなどの金融データを取得できる、個人投資家向けのAPIデータ配信サービスです。投資にまつわるデータを分析しやすい形式で提供し、個人投資家の皆様がデータを活用して取引できるようになることを目的としております。
個人投資家がデータ分析を用いた投資分析を行う際の大きな障壁は、整形された金融データの取得が難しいことでは?という理由から、2022年7月にベータ版をリリースいたしました。ご好評の声を頂いたこともあり、2023年4月より正式にリリースしております。
J-Quants運営チーム(@j_quants)様より引用 https://qiita.com/j_quants/items/724c7dda5262a1ad835a

image.png

スクレイピングはメンテナンスや法的な課題があったりして、J-Quantsはデータがきちっと整形されている点等に強みがあると理解しています。
正直まだ充分に比較検討できていないのですが、個人的に、JSON形式で取得可能なKABU+(CSVEX)、APIサービスを持つバフェットコードなどが競合サービスになりそうです。
(こちらの方でも色々試してみたいのですが、時間が足らずできていない。。。)

Kabu+
https://kabu.plus/
CSVEX
https://csvex.com/
バフェットコード(APIリファレンス)
https://docs.buffett-code.com/api/

上記KABU+を利用して、かもめ(@kamomejan) 様が財務の深い分析までエクセルテンプレとして公開されてるので、必要に応じて検討してみてください。

投資エクセルテンプレートまとめ - 大河の一滴
https://twitter.com/kamomejan/status/985491229457567745?s=20

各株式情報サービスの比較は、下記の記事が参考になりそうです。

株情報を取得するAPIどれが良い(@passive-radio 様)
https://qiita.com/passive-radio/items/cf3740f9601675b0a8dd
日本株の日足データ取得方法をまとめました(mabui 様)
https://mabui.org/japan-stock-get-daily-data/

必要な環境

① J-Quants API ライトプラン以上の契約
 ⇒フリープランは過去二年分のみまた三か月間の遅延があるため、実応用には実質的に有料プランが必須になると思われます。
② GoogleAppScript
 ⇒いわばスプレッドシートにおけるマクロのようなものです。@kijuky様の記事が大変参考になります。

GoogleスプレッドシートでJ-Quants APIを使ってみる
https://qiita.com/kijuky/items/ad638bc07e59343472d1

この記事でできること

ざっくり「任意のセルにおいて、J-Quants APIから取得した任意の情報を表示する」という事になります。応用例は以下の通りです。

 (例1) 特定銘柄の”上場銘柄一覧(/listed/info)”、”株価四本値(/prices/daily_quotes)”、”財務情報(/fins/statements)”の各項目をセル内に表示してリスト化
 (例2) 特定銘柄の複数年の”財務情報(/fins/statements)”をセル内に表示してリスト化
 (例3) 任意の銘柄群の特定の情報をリスト化し、グラフで比較

◎(例1) 特定銘柄の”上場銘柄一覧(/listed/info)”、”株価四本値(/prices/daily_quotes)”、”財務情報(/fins/statements)”の各項目をセル内に表示してリスト化
 ※各項目でプラン限定だったり、抜けがあったりしますが、やはり全て網羅しているというわけではないのでそこはご注意ください

 〇上場銘柄一覧(/listed/info)
image.png

 〇株価四本値(/prices/daily_quotes)
image.png

 〇財務情報(/fins/statements)
image.png

◎(例2) 特定銘柄の複数年の”財務情報(/fins/statements)”をセル内に表示してリスト化
image.png

◎ (例3) 任意の銘柄群の特定の情報をリスト化し、グラフで比較
[例題]
「とあるシステム受託関連企業の業績を見ると、売上や営業利益率が右肩上がりだった。これは何かこの企業だけの強みがあったのかもしれないし、最近よく聞くデジタルトランスフォーメーション的な需要で業界全体として引き合いが増えて利益改善したのかもしれない。他のシステム受託関連企業の利益率推移を定量的に比較したい。」
⇒スプレッドシート上で、任意の銘柄について2019~2023の間の売上高・営業利益を算出するセルを用意し、それを平均値も含めてグラフ化
⇒2019~2022の間で、AVERAGE(ピンクの破線)は2ポイント程度向上している。やはり業界全体としての質的に改善している可能性が高いので、その要因は何か各社の決算書を確認しに行く。(共通する要因か?個社で異なるか?)

image.png
image.png

実装

GASと独自関数の処理の流れについて

基本的に、GASがどのようなものかについては詳細に書かないので、この理解は他のサイトを参考にしてください。

簡単には、ExcelがGoogleSpreadSheetに対応するとすれば、GoogleAppScriptはいわばマクロ(VBA)のようなものです。Javascriptベースです。@kijuky様の記事が大変参考になります。

GoogleスプレッドシートでJ-Quants APIを使ってみる
https://qiita.com/kijuky/items/ad638bc07e59343472d1

独自関数は一般的に使われる、例えばSUM()のような関数を自分で定義できる機能です。自分で関数名と機能を定義して、引数に対してどのような値を返すか、ということをGoogleAppScriptで実装します。

今回の独自関数の処理の流れは以下のようなイメージです。
各独自関数は、とってくる情報は異なりますが、大まかな処理の流れはほぼ同じです。

セル内で設定した独自関数によりGASが起動
 ⇒ スクリプトプロパティからIDトークン取得
 ⇒ 引数からキャッシュキー生成
 ⇒ 同じ引数のデータがキャッシュにあるか確認
 ⇒ (ない場合)J-Quantsエンドポイントにアクセス (※ある場合 の分岐は省略)
 ⇒ 引数に応じてJ-Quantsサーバ側がまとまったデータをJson形式で返す
 ⇒ GAS上で引数に対応する値をJson内部から抽出
 ⇒ セルに値のみを表示
image.png

ただし上記は後述するコードのうち、"getAccessToken"を除く、"上場銘柄一覧(/listed/info)" "株価四本値(/prices/daily_quotes)" "財務情報(/fins/statements)" についてのフローになります。
また、"財務情報(/fins/statements)"については、フローチャート紫部分のJ-Quantsサーバへの問い合わせの方法に少し工夫が必要でした。(詳細後述)

各機能の解説

1 トークン取得 - "getAccessToken"

1-1 概要説明

APIリファレンスは以下。

リフレッシュトークン取得(/token/auth_user)
https://jpx.gitbook.io/j-quants-ja/api-reference/refreshtoken
IDトークン取得(/token/auth_refresh)
https://jpx.gitbook.io/j-quants-ja/api-reference/idtoken

J-Quants APIでは、呼び出しの際毎回Authorization(認証)が必要で、そのためにidtokenが必要になります。また、その取得のためにはあらかじめRefleshTokenというものを取得しておく必要があります。今回の"getAccessToken"は、それらを取得するためのコードです。
ただしこれは、スプレッドシート内で呼び出すことは基本的になく、時間でトリガーをかけて定期的に実行させます。(後述)

コードの処理の流れとしては、
 スクリプトプロパティから情報メールアドレス、パスワードの取得
 ⇒それらを持ってJ-Quantsエンドポイントにアクセスし、リフレッシュトークンを取得
 ⇒スクリプトプロパティ"REFLESHTOKEN"にサーバから取得したリフレッシュトークンの中身を設定
 ⇒それを持ってJ-Quantsエンドポイントにアクセスし、IDトークンを取得
 ⇒スクリプトプロパティ"IDTOKEN"にサーバから取得したIDトークンの中身を設定

これでスクリプトプロパティ"IDTOKEN"に保存されたIDトークンを、のちの他の独自関数で呼び出します。
idTokenは24時間、Refleshtokenは一週間で期限が切れてしまうので、定期的に実行して保存しておく必要があります。
なぜこのように二重になっているのかというと、セキュリティ上の対策だそうです。寿命の短いIDトークンは流出しても24時間で切れるから被害が比較的少なく済み、これによって利便性も両立できる、という理屈のようですが...詳しいことはググって。

1-2 コード

コードは以下の通りです。

コード
getToken.gs
function getAccessToken() {
  var url = "https://api.jquants.com/v1/token/auth_user";

  // スクリプトプロパティからメールアドレスとパスワードを取得
  var scriptProperties = PropertiesService.getScriptProperties();
  var mailaddress = scriptProperties.getProperty('MAILADDRESS');
  var password = scriptProperties.getProperty('PASSWORD');

  // POSTリクエストのオプションを設定
  var options = {
    method: 'post',
    headers: {
      "Content-Type": "application/json"
    },
  payload: JSON.stringify({
    "mailaddress": mailaddress,
    "password": password
    })
  };

  try {
    var response = UrlFetchApp.fetch(url, options);
    var json = JSON.parse(response.getContentText());
    var refreshToken = json.refreshToken;
    // スクリプトプロパティ "J_QUANTS_REFRESH_TOKEN" に refreshToken を設定
    scriptProperties.setProperty('J_QUANTS_REFRESH_TOKEN', refreshToken);
    Logger.log('Fetched Refresh token: ' + refreshToken);
  } catch (e) {
    Logger.log('Error in fetching refresh token: ' + e.toString());
  }

  url = "https://api.jquants.com/v1/token/auth_refresh?refreshtoken=" + refreshToken;
  try {
    response = UrlFetchApp.fetch(url, options);
    json = JSON.parse(response.getContentText());
    var idToken = json.idToken;
    // スクリプトプロパティ "IDTOKEN" に idToken を設定
    scriptProperties.setProperty('IDTOKEN', idToken);
    Logger.log('Fetched ID token: ' + idToken);
  } catch (e) {
    Logger.log('Error in fetching ID token: ' + e.toString());
  }

  return idToken;
}

1-3 スクリプトプロパティについて

スクリプトプロパティはIDTOKEN,J_QUANTS_REFRESH_TOKEN,MAILADDRESS,PASSWORDを設定していまじ
前述の通り、前者2つはプログラムから自動で更新するので、ユーザーが意識して入力するのは後者2つのみです。

image.png

1-4 トリガー機能による定期実行

次に、前述したとおり、上記のコードを定期的に自動で走らせる方法について説明します。
標準の機能として用意されているので、非常に簡単です。

▼「トリガー」タブを選択
image.png
▼「トリガーを追加」、「時間主導型」にして、実行する周期などを選ぶ
image.png

2 "上場銘柄一覧(/listed/info)"情報の取得 - 独自関数"getListedInfo"

2-1 概要説明

APIリファレンスは以下。

上場銘柄一覧(/listed/info)
https://jpx.gitbook.io/j-quants-ja/api-reference/listed_info

上場銘柄一覧 で取得できる情報の内容・項目については、APIリファレンスの中身を見てください。
この独自関数は、簡単には銘柄コード・年月日・Indexを引数として、前者2つをJ-Quants APIに渡し、その銘柄コードの特定日時での、会社名や市場区分などの情報をJsonとして取得し、Indexに対応する情報をJson内から抽出するものです。
引数はスプレッドシート内で指定し、Indexで指定された種類の値が最終的にセルに表示されます。

処理のフローは前述したとおりです。画像のみ再度貼り付けます。
image.png

2-2 コード

コードは以下の通りです。

コード
getToken.gs
function getListedInfo(stockCode, date, index) {
  // スクリプトプロパティからIDTOKENを取得
  var scriptProperties = PropertiesService.getScriptProperties();
  var idToken = scriptProperties.getProperty('IDTOKEN');

  var cache = CacheService.getScriptCache();
  var cacheKey = 'LISTED_INFO_' + stockCode + '_' + date;
  var cachedData = cache.get(cacheKey);

  var Value;
  
  if (cachedData != null) {
    Logger.log('Data found in cache');
    Logger.log('index: ' + index );
    Logger.log('cachedData: ' + JSON.parse(cachedData));
    console.log(JSON.stringify(cachedData, null, 2))
    var data = JSON.parse(cachedData);
  } else {
    var url = "https://api.jquants.com/v1/listed/info?code=" + stockCode + "&date=" + date;
    var options = {
      method: 'get',
      headers: { 'Authorization': 'Bearer ' + idToken },
      muteHttpExceptions: true
    };
    
    try {
      var response = UrlFetchApp.fetch(url, options);
      var data = JSON.parse(response.getContentText());

      Logger.log('Response status code: ' + response.getResponseCode());
      Logger.log('Fetched data: ' + JSON.stringify(data));

      // のちに使用するためにデータをキャッシュに追加 ただし1 時間 (3600 秒) で期限切れ
      cache.put(cacheKey, JSON.stringify(data), 3600);
    } catch (e) {
      Logger.log('Error in fetching stock price: ' + e.message);
      return 'Error in fetching stock price: ' + e.message;
    }
  }

  switch(index) {
    case 1: 
      Value = data['info'][0]['Date'];
      break;
    case 2: 
      Value = data['info'][0]['Code'];
      break;
    case 3: 
      Value = data['info'][0]['CompanyName'];
      break;
    case 4: 
      Value = data['info'][0]['CompanyNameEnglish'];
      break;
    case 5: 
      Value = data['info'][0]['Sector17Code'];
      break;
    case 6: 
      Value = data['info'][0]['Sector17CodeName'];
      break;
    case 7: 
      Value = data['info'][0]['Sector33Code'];
      break;
    case 8: 
      Value = data['info'][0]['Sector33CodeName'];
      break;
    case 9: 
      Value = data['info'][0]['ScaleCategory'];
      break;
    case 10: 
      Value = data['info'][0]['MarketCode'];
      break;
    case 11: 
      Value = data['info'][0]['MarketCodeName'];
      break;
  }

  Logger.log('index: ' + index );
  Logger.log('listed_info: ' + data['listed_info']);
  Logger.log('Value: ' + Value);
  
  return Value; 
}

2-3 キャッシュ機能について

今回、GAS標準機能であるGoogleCascheServiceを使用しています。
特定の引数に対してサーバ側から返ってきたJson形式のデータをキャッシュとして保存しておくことで、呼び出し回数を削減・高速化しています。
Goggleスプレッドシートで、例えば、同じ銘柄コード・年月日、異なるIndexのセルが多数存在する場合に、キャッシュ機能がなければ何度も同じJsonの塊を呼び出してしまいますが、キャッシュサービスがあれば過去に呼び出して保存された塊から再度Indexに応じたデータを抽出するだけで済みます。

ただし、このGoogleCascheService、どうやら少し厄介な仕様になっているようです。
 ①キャッシュ保存期間が最長1時間
  ⇒どうやらこれは他のキャッシュサービスに比べて短い模様。なので、時間を置くと再度呼び出しをかける必要があります。ただし、期限切れキャッシュは消えるため、前述コードではユーザー側がキャッシュ切れを意識して管理する必要はないようになっています。
 ②キャッシュクリアにキャッシュキーが必要
  ⇒こちらはそれほど大きなデメリットではないですが、一括キャッシュクリアができないため、一度入ったものをクリアするのがかなり面倒です。例えば、何かの不具合で変なデータがキャッシュに入ってしまった場合がややこしいと思います。必要な場合はキャッシュキーを入れてクリアすることもできますが、期限切れ1時間を待つ方が得策だと思うので、キャッシュクリアのコードは本記事に含んでいません。

3 "株価四本値(/prices/daily_quotes)"情報の取得 - function getPricesDailyQuotes

3-1 概要説明

APIリファレンスは以下。

株価四本値(/prices/daily_quotes)
https://jpx.gitbook.io/j-quants-ja/api-reference/daily_quotes

株価四本値 で取得できる情報の内容・項目については、APIリファレンスの中身を見てください。
ただしここについて、2点注意があります。
 ①場中の値動きの詳細な情報はとれない ⇒ 四本値の意味通り、始値/高値/安値/終値です。
 ②前場/後場4本値はプレミアムプランのみ

四本値(よんほんね)|東海東京証券株式会社
https://www.tokaitokyo.co.jp/kantan/term/detail_0094.html#:~:text=%E5%9B%9B%E6%9C%AC%E5%80%A4%E3%81%A8%E3%81%AF,%E5%88%A9%E7%94%A8%E3%81%95%E3%82%8C%E3%82%8B%E3%82%82%E3%81%AE%E3%81%A7%E3%81%99%E3%80%82

情報の内容が異なるだけで、基本的に処理はほぼ一緒なので、[2-1]章を適切に読み替えてください。

3-2 コード

コードは以下の通りです。

コード
PricesDailyQuotes.gs

function getPricesDailyQuotes(stockCode, date, index) {
  // スクリプトプロパティからIDTOKEN を取得する
  var scriptProperties = PropertiesService.getScriptProperties();
  var idToken = scriptProperties.getProperty('IDTOKEN');

  // キャッシュサービスを取得
  var cache = CacheService.getScriptCache();

  // 日付文字列を Date オブジェクトに解析
  var dateObj = new Date(date.toString().replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3"));

  // 入力日の 5 日前をループします(入力した年月日が取引日でない場合の対策)
  for (var i = 0; i < 5; i++) {
    var currentDate = new Date(dateObj.getTime() - i * 24 * 60 * 60 * 1000);  // Subtract i days from the input date
    var currentDateStr = currentDate.toISOString().split('T')[0].replace(/-/g, '');  // Convert the date to string in the format 'yyyymmdd'

    var cacheKey = 'DAILY_QUOTES_' + stockCode + '_' + currentDateStr;
    var cachedData = cache.get(cacheKey);
    var data;

    if (!data) {
      var url = "https://api.jquants.com/v1/prices/daily_quotes?code=" + stockCode + "&date=" + currentDateStr;
      var options = {
        method: 'get',
        headers: { 'Authorization': 'Bearer ' + idToken },
        muteHttpExceptions: true
      };

      try {
        var response = UrlFetchApp.fetch(url, options);
        var responseData = JSON.parse(response.getContentText());

        if (responseData && responseData['daily_quotes'] && responseData['daily_quotes'].length > 0) {
          data = JSON.parse(response.getContentText());
        }

        Logger.log('URL: ' + url);
        Logger.log('Response: ' + response);
        Logger.log('Response status code: ' + response.getResponseCode());
        Logger.log('Fetched data: ' + JSON.stringify(data));

        // 将来使用するためにデータをキャッシュに追加 1 時間 (3600 秒) で期限切れ
        cache.put(cacheKey, JSON.stringify(data), 3600);
      } catch (e) {
        Logger.log('URL: ' + url);
        Logger.log('Error in fetching stock price: ' + e.message);
        return 'Error in fetching stock price: ' + e.message;
      }
    }

    if (cachedData != null) {
      Logger.log('Data found in cache');
      Logger.log('index: ' + index);
      try {
        data = JSON.parse(cachedData);
        Logger.log('parsed cachedData: ' + JSON.stringify(data));
      } catch (e) {
        Logger.log('Error in parsing cachedData: ' + e.message);
        data = null;
      }
    }

    var Value;
    if (data && data['daily_quotes'] && data['daily_quotes'].length > 0) {
    switch(index) {
      case 1: 
        Value = data['daily_quotes'][0]['Date'];
        break;
      case 2: 
        Value = data['daily_quotes'][0]['Code'];
        break;
      case 3: 
        Value = data['daily_quotes'][0]['Open'];
        break;
      case 4: 
        Value = data['daily_quotes'][0]['High'];
        break;
      case 5: 
        Value = data['daily_quotes'][0]['Low'];
        break;
      case 6: 
        Value = data['daily_quotes'][0]['Close'];
        break;
      case 7: 
        Value = data['daily_quotes'][0]['UpperLimit'];
        break;
      case 8: 
        Value = data['daily_quotes'][0]['LowerLimit'];
        break;
      case 9: 
        Value = data['daily_quotes'][0]['Volume'];
        break;
      case 10: 
        Value = data['daily_quotes'][0]['TurnoverValue'];
        break;
      case 11: 
        Value = data['daily_quotes'][0]['AdjustmentFactor'];
        break;
      case 12: 
        Value = data['daily_quotes'][0]['AdjustmentOpen'];
        break;
      case 13: 
        Value = data['daily_quotes'][0]['AdjustmentHigh'];
        break;
      case 14: 
        Value = data['daily_quotes'][0]['AdjustmentLow'];
        break;
      case 15: 
        Value = data['daily_quotes'][0]['AdjustmentClose'];
        break;
      case 16: 
        Value = data['daily_quotes'][0]['AdjustmentVolume'];
        break;
      case 17: 
        Value = data['daily_quotes'][0]['MorningOpen'];
        break;
      case 18: 
        Value = data['daily_quotes'][0]['MorningHigh'];
        break;
      case 19: 
        Value = data['daily_quotes'][0]['MorningLow'];
        break;
      case 20: 
        Value = data['daily_quotes'][0]['MorningClose'];
        break;
      case 21: 
        Value = data['daily_quotes'][0]['MorningUpperLimit'];
        break;
      case 22: 
        Value = data['daily_quotes'][0]['MorningLowerLimit'];
        break;
      case 23: 
        Value = data['daily_quotes'][0]['MorningVolume'];
        break;
      case 24: 
        Value = data['daily_quotes'][0]['MorningTurnoverValue'];
        break;
      case 25: 
        Value = data['daily_quotes'][0]['MorningAdjustmentOpen'];
        break;
      case 26: 
        Value = data['daily_quotes'][0]['MorningAdjustmentHigh'];
        break;
      case 27: 
        Value = data['daily_quotes'][0]['MorningAdjustmentLow'];
        break;
      case 28: 
        Value = data['daily_quotes'][0]['MorningAdjustmentClose'];
        break;
      case 29: 
        Value = data['daily_quotes'][0]['MorningAdjustmentVolume'];
        break;
      case 30: 
        Value = data['daily_quotes'][0]['AfternoonOpen'];
        break;
      case 31: 
        Value = data['daily_quotes'][0]['AfternoonHigh'];
        break;
      case 32: 
        Value = data['daily_quotes'][0]['AfternoonLow'];
        break;
      case 33: 
        Value = data['daily_quotes'][0]['AfternoonClose'];
        break;
      case 34: 
        Value = data['daily_quotes'][0]['AfternoonUpperLimit'];
        break;
      case 35: 
        Value = data['daily_quotes'][0]['AfternoonLowerLimit'];
        break;
      case 36: 
        Value = data['daily_quotes'][0]['AfternoonVolume'];
        break;
      case 37: 
        Value = data['daily_quotes'][0]['AfternoonAdjustmentOpen'];
        break;
      case 38: 
        Value = data['daily_quotes'][0]['AfternoonAdjustmentHigh'];
        break;
      case 39: 
        Value = data['daily_quotes'][0]['AfternoonAdjustmentLow'];
        break;
      case 40: 
        Value = data['daily_quotes'][0]['AfternoonAdjustmentClose'];
        break;
      case 41: 
        Value = data['daily_quotes'][0]['AfternoonAdjustmentVolume'];
        break;
      }
      if (Value !== undefined) {
        // If we found valid data, exit the loop early
        break;
      }

    } else {
      Logger.log('No daily_quotes data in the API response for date: ' + currentDateStr);
    }
  }

  Logger.log('index: ' + index );
  Logger.log('daily_quotes: ' + (data && data['daily_quotes'] ? JSON.stringify(data['daily_quotes']) : 'null'));
  Logger.log('Value: ' + Value);

  return Value; 
}

3-3 取引日以外の年月日入力時の対策

取引日を明確にわかっている場合は良いのですが、「ざっくりこの辺りの株価見たい」という場合もあるのではないかと思います。
そのような場合に適当に入力してもし休場日を入れてしまうと、J-Quantsの仕様上、最後の取引日の終値を返すのではなく、エラーを返してしまうようです。
このため、上記コードでは5日間ループしていますが、そうそうない気もしますが休場日が5日以上続いてしまうとエラーになってしまうので(一応6日間続いたケースはあるようです)、入力時気を付けてください。

PricesDailyQuotes.gs (日付ループの開始部分のみ抜粋)
  // 入力日の 5 日前をループします(入力した年月日が取引日でない場合の対策)
  for (var i = 0; i < 5; i++) {
    var currentDate = new Date(dateObj.getTime() - i * 24 * 60 * 60 * 1000);  // Subtract i days from the input date
    var currentDateStr = currentDate.toISOString().split('T')[0].replace(/-/g, '');  // Convert the date to string in the format 'yyyymmdd'

~(中略)~

        if (responseData && responseData['daily_quotes'] && responseData['daily_quotes'].length > 0) {
          data = JSON.parse(response.getContentText());
        }
 

一応、J-Quants公式から下記のような記事が出ており、こちら参照して取引カレンダーAPIで取引日を確認するという手段もあります。ただ、また別のAPI引っ張ってくるのはコードが複雑になりすぎる気もするので、必要に応じて(正確さがどれだけ要求されるかによって)使い分けるのが良いかと思います。

【J-Quants】取引カレンダーAPIを用いた株価のバッチ取得
https://qiita.com/j_quants/items/724c7dda5262a1ad835a

4 "財務情報(/fins/statements)"情報の取得 - function getFinStatementsSPFY

4-1 概要説明

APIリファレンスは以下。

財務情報(/fins/statements)
https://jpx.gitbook.io/j-quants-ja/api-reference/statements

取得できる情報の内容・項目については、APIリファレンスの中身を見てください。
ただし、J-Quantsの財務情報APIの仕様には、少し厄介なポイントがポイントが一つあります。
これまでの株価4本値と少し似た話ですが、過去の決算開示日を明確に指定しなければ情報をとれない(エラーが返る)という点です。
株価4本値であれば、前述のように数日ループで対応は可能です。しかし決算開示日はワケが違います。年に4日しかないわけですから。また更に、通期決算と各四半期決算の開示日は当然ですが異なります。
財務情報を取得したい場合に、その財務情報の開示日だけを知っているという状況は、なかなか無いように思います。
よって利便性を考えれば、ユーザーは開示日を意識せず、特定の銘柄に対して、決算開示日をこちら側で確認してやる必要があります。

このため、コード内では下記2点で対応しました。
 ①日付のforループによる決算開示日の探索
  ⇒一般的に決算開示日の集中している期間を、日付のループで問い合わせを掛けて、開示日にあたるまで繰り返す
 ②引数に決算月を追加して探索期間の絞り込み
  ⇒会計期間は銘柄によって異なるため、引数に銘柄コードに加えて決算月を要求する
   ※②もできれば不要にしたいところですが、問い合わせ回数が増えてしまうため、現状必須です
 ③通期/四半期決算でプログラムを分ける
  ⇒

これをフローチャートに落とし込むと、下記のようになります。

image.png

4-2 コード

コードは以下の通りです。

コード
FinsStatementsSPFY

function getFinStatementsSPFY(stockCode, SPyear, index, FY_Index,fiscalYearEnd) {
  //変数の宣言
  var scriptProperties = PropertiesService.getScriptProperties();
  var idToken = scriptProperties.getProperty('IDTOKEN');
  var cache = CacheService.getScriptCache();
  
  // 引数から指定の年を取得
  var SpecificYear = SPyear;
  
  // 現在の年と前年を確認
  var years = [SpecificYear - 1, SpecificYear];
  
  //決算期に基づく開始日と終了日の設定
  var startMonth, endMonth;
  if (fiscalYearEnd == 3) {
    startMonth = 3;
    endMonth = 4;
  } else if (fiscalYearEnd == 12) {
    startMonth = 0;  // January
    endMonth = 1;  // February
  } else if (fiscalYearEnd == 9) {
    startMonth = 9;
    endMonth = 10;
  } else if (fiscalYearEnd == 6) {
    startMonth = 6;
    endMonth = 7;
  } else {
    // Handle other fiscal year end cases
  }

  //StatementKeyの数値とKey(文字列)との紐づけ

  const financialStatementKeys = {
    1: 'DisclosedDate',
    2: 'DisclosedTime',
    3: 'LocalCode',
    4: 'DisclosureNumber',
    5: 'TypeOfDocument',
    6: 'TypeOfCurrentPeriod',
    7: 'CurrentPeriodStartDate',
    8: 'CurrentPeriodEndDate',
    9: 'CurrentFiscalYearStartDate',
    10: 'CurrentFiscalYearEndDate',
    11: 'NextFiscalYearStartDate',
    12: 'NextFiscalYearEndDate',
    13: 'NetSales',
    14: 'OperatingProfit',
    15: 'OrdinaryProfit',
    16: 'Profit',
    17: 'EarningsPerShare',
    18: 'DilutedEarningsPerShare',
    19: 'TotalAssets',
    20: 'Equity',
    21: 'EquityToAssetRatio',
    22: 'BookValuePerShare',
    23: 'CashFlowsFromOperatingActivities',
    24: 'CashFlowsFromInvestingActivities',
    25: 'CashFlowsFromFinancingActivities',
    26: 'CashAndEquivalents',
    27: 'ResultDividendPerShare1stQuarter',
    28: 'ResultDividendPerShare2ndQuarter',
    29: 'ResultDividendPerShare3rdQuarter',
    30: 'ResultDividendPerShareFiscalYearEnd',
    31: 'ResultDividendPerShareAnnual',
    32: 'DistributionsPerUnit(REIT)',
    33: 'ResultTotalDividendPaidAnnual',
    34: 'ResultPayoutRatioAnnual',
    35: 'ForecastDividendPerShare1stQuarter',
    36: 'ForecastDividendPerShare2ndQuarter',
    37: 'ForecastDividendPerShare3rdQuarter',
    38: 'ForecastDividendPerShareFiscalYearEnd',
    39: 'ForecastDividendPerShareAnnual',
    40: 'ForecastDistributionsPerUnit(REIT)',
    41: 'ForecastTotalDividendPaidAnnual',
    42: 'ForecastPayoutRatioAnnual',
    43: 'NextYearForecastDividendPerShare1stQuarter',
    44: 'NextYearForecastDividendPerShare2ndQuarter',
    45: 'NextYearForecastDividendPerShare3rdQuarter',
    46: 'NextYearForecastDividendPerShareFiscalYearEnd',
    47: 'NextYearForecastDividendPerShareAnnual',
    48: 'NextYearForecastDistributionsPerUnit(REIT)',
    49: 'NextYearForecastPayoutRatioAnnual',
    50: 'ForecastNetSales2ndQuarter',
    51: 'ForecastOperatingProfit2ndQuarter',
    52: 'ForecastOrdinaryProfit2ndQuarter',
    53: 'ForecastProfit2ndQuarter',
    54: 'ForecastEarningsPerShare2ndQuarter',
    55: 'NextYearForecastNetSales2ndQuarter',
    56: 'NextYearForecastOperatingProfit2ndQuarter',
    57: 'NextYearForecastOrdinaryProfit2ndQuarter',
    58: 'NextYearForecastProfit2ndQuarter',
    59: 'NextYearForecastEarningsPerShare2ndQuarter',
    60: 'ForecastNetSales',
    61: 'ForecastOperatingProfit',
    62: 'ForecastOrdinaryProfit',
    63: 'ForecastProfit',
    64: 'ForecastEarningsPerShare',
    65: 'NextYearForecastNetSales',
    66: 'NextYearForecastOperatingProfit',
    67: 'NextYearForecastOrdinaryProfit',
    68: 'NextYearForecastProfit',
    69: 'NextYearForecastEarningsPerShare',
    70: 'MaterialChangesInSubsidiaries',
    71: 'ChangesBasedOnRevisionsOfAccountingStandard',
    72: 'ChangesOtherThanOnesBasedOnRevisionsOfAccountingStandard',
    73: 'ChangesInAccountingEstimates',
    74: 'RetrospectiveRestatement',
    75: 'NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock',
    76: 'NumberOfTreasuryStockAtTheEndOfFiscalYear',
    77: 'AverageNumberOfShares',
    78: 'NonConsolidatedNetSales',
    79: 'NonConsolidatedOperatingProfit',
    80: 'NonConsolidatedOrdinaryProfit',
    81: 'NonConsolidatedProfit',
    82: 'NonConsolidatedEarningsPerShare',
    83: 'NonConsolidatedTotalAssets',
    84: 'NonConsolidatedEquity',
    85: 'NonConsolidatedEquityToAssetRatio',
    86: 'NonConsolidatedBookValuePerShare',
    87: 'ForecastNonConsolidatedNetSales2ndQuarter',
    88: 'ForecastNonConsolidatedOperatingProfit2ndQuarter',
    89: 'ForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    90: 'ForecastNonConsolidatedProfit2ndQuarter',
    91: 'ForecastNonConsolidatedEarningsPerShare2ndQuarter',
    92: 'NextYearForecastNonConsolidatedNetSales2ndQuarter',
    93: 'NextYearForecastNonConsolidatedOperatingProfit2ndQuarter',
    94: 'NextYearForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    95: 'NextYearForecastNonConsolidatedProfit2ndQuarter',
    96: 'NextYearForecastNonConsolidatedEarningsPerShare2ndQuarter',
    97: 'ForecastNonConsolidatedNetSales',
    98: 'ForecastNonConsolidatedOperatingProfit',
    99: 'ForecastNonConsolidatedOrdinaryProfit',
    100: 'ForecastNonConsolidatedProfit',
    101: 'ForecastNonConsolidatedEarningsPerShare',
    102: 'NextYearForecastNonConsolidatedNetSales',
    103: 'NextYearForecastNonConsolidatedOperatingProfit',
    104: 'NextYearForecastNonConsolidatedOrdinaryProfit',
    105: 'NextYearForecastNonConsolidatedProfit',
    106: 'NextYearForecastNonConsolidatedEarningsPerShare',
  };

  // For each year, check the financial statements. Start from the most recent year.
  for (var i = years.length - 1; i >= 0; i--) {
    var year = years[i];
    var startDate = new Date(year, startMonth, 15);
    var endDate = new Date(year, endMonth, 31);
    var value = null;  // Initialize the value as null

    for (var day = startDate; day <= endDate; day.setDate(day.getDate() + 1)) {
      var dateStr = day.toISOString().split('T')[FY_Index];  // format as 'yyyy-mm-dd'
      var cacheKey = 'FINS_STATEMENTS_SPFY_' + stockCode + '_' + dateStr;
      var cachedData = cache.get(cacheKey);

      var data;

      if (cachedData != null) {
        Logger.log('Data found in cache');
        Logger.log('Date:' + dateStr);
        Logger.log('index: ' + index);
        Logger.log('cachedData: ' + JSON.parse(cachedData));
        data = JSON.parse(cachedData);
      } else {
        var url = "https://api.jquants.com/v1/fins/statements?code=" + stockCode + "&date=" + dateStr;
        var options = {
          method: 'get',
          headers: { 'Authorization': 'Bearer ' + idToken },
          muteHttpExceptions: true
        };
        
        try {
          var response = UrlFetchApp.fetch(url, options);
          data = JSON.parse(response.getContentText());

          Logger.log('Response status code: ' + response.getResponseCode());
          Logger.log('Fetched data: ' + JSON.stringify(data));
          cache.put(cacheKey, JSON.stringify(data), 3600);

        } catch (e) {
          Logger.log('Error in fetching stock price: ' + e.message);
          return 'Error in fetching stock price: ' + e.message;
        }
      }

      // Check if there is a financial statement for this day
      if (data['statements']) {
        if (data['statements'].length > 0) {
          const key = financialStatementKeys[index];
          if (key) {
            var typeOfDocument = data['statements'][FY_Index]['TypeOfDocument'];
            var typeOfCurrentPeriod = data['statements'][FY_Index]['TypeOfCurrentPeriod'];
            if (typeOfCurrentPeriod && !typeOfDocument.includes("Revision") && typeOfCurrentPeriod.includes("FY")) {
              value = data['statements'][FY_Index][key];  // Set the value if it's not a revision and it's for 'FY'
              Logger.log('Value: ' + value);
              break;  // Break the loop if the data is not a revision and it's for 'FY'
            } else {
              Logger.log('Revision detected or not FY, moving to next date');
              value = null;  // Set the value as null if a revision is detected or it's not for 'FY'
              continue; // Skip to the next day if it is a revision or it's not for 'FY'
            }
          } else {
            Logger.log('Invalid index: ' + index);
            return null;
          }
        }
      } else {
        Logger.log('No statements data in the API response for date: ' + day);
      }
    }

    // Return the value at the end
    if (value !== null) {
      return value;
    }
  }

  return null;  // Return null if no non-revision financial statement was found
}

4-3 日付のforループによる開示日の探索

① 一般的に決算開示日の集中している期間を、日付のループで問い合わせを掛けて、開示情報が確認できるまで繰り返す

前述通り、財務情報(/fins/statements)APIは、過去の決算開示日を明確に指定しなければ情報をとれません。よって開示日のわからない銘柄の場合、探索が必要です。
仕組みは簡単で、決算月に合わせ、決算発表が集中する期間を一日ずつ問い合わせします。

FinsStatementsSPFY.gs (日付ループの開始部分のみ抜粋)
  // For each year, check the financial statements. Start from the most recent year.
  for (var i = years.length - 1; i >= 0; i--) {
    var year = years[i];
    var startDate = new Date(year, startMonth, 15);
    var endDate = new Date(year, endMonth, 31);
    var value = null;  // Initialize the value as null

    for (var day = startDate; day <= endDate; day.setDate(day.getDate() + 1)) {
      var dateStr = day.toISOString().split('T')[FY_Index];  // format as 'yyyy-mm-dd'
      var cacheKey = 'FINS_STATEMENTS_SPFY_' + stockCode + '_' + dateStr;
      var cachedData = cache.get(cacheKey);

      var data;

~~~(中略)~~~

// Return the value at the end
    if (value !== null) {
      return value;
    }
  }

  return null;  // Return null if no non-revision financial statement was found
}

4-3 引数に決算月を追加して探索期間の絞り込み

②会計期間は銘柄によって異なるため、引数に銘柄コードに加えて決算月を要求する
銘柄によって決算期間は異なります。ここで、例えば全期間を探索するようにして、ユーザー側で決算月を不要にすることも可能ですが、その場合、4倍の期間の問い合わせをすることになり、スプレッドシート側・サーバー側双方の負担になってしまいます。
また、実際に使ってみてもらうとわかるのですが、スプレッドシート(GAS)の処理はあまり早くないです。財務情報一つの呼び出しに10秒以上かかり、更に大量のセルがある場合は更につらくなってきます。よって、探索期間はできるだけ少なくしたいです。

決算月と探索期間は下記の通り。

決算開示日探索期間
決算月 探索期間
3 4/10~5/15
12 1/10~2/15
9 10/10~11/15
6 7/10~8/15

国税庁が公表している令和3年度の「決算期別の普通法人数」を参照すると、圧倒的に3月決算の法人が多く、次いで9月、12月決算の法人が多いことがわかります。
決算期(決算月)はいつにすべき?決め方や変更手続きについて解説|freee
https://www.freee.co.jp/kb/kb-accounting/accounting-standards/#:~:text=%E6%B1%BA%E7%AE%97%E6%9C%9F%EF%BC%88%E6%B1%BA%E7%AE%97%E6%9C%88%EF%BC%89%E3%81%A8,3%E6%9C%88%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82

FinsStatementsSPFY.gs (関連部分のみ抜粋)
  //決算期に基づく開始日と終了日の設定
  var startMonth, endMonth;
  if (fiscalYearEnd == 3) {
    startMonth = 3;
    endMonth = 4;
  } else if (fiscalYearEnd == 12) {
    startMonth = 0;  // January
    endMonth = 1;  // February
  } else if (fiscalYearEnd == 9) {
    startMonth = 9;
    endMonth = 10;
  } else if (fiscalYearEnd == 6) {
    startMonth = 6;
    endMonth = 7;
  } else {
    // Handle other fiscal year end cases
  }

4-4 修正報告を除外

実はヒットするデータは決算報告だけでなく、修正報告も含みます。何も考えず「内容のあるデータを見つけたらOK」にしておくと、内容に歯抜けのある修正報告の情報を拾ってしまうので注意が必要です。
実装としては簡単で、"Revision"を含むデータを除外するようにするだけです。

PricesDailyQuotes.gs (関連部分のみ抜粋)
            if (typeOfCurrentPeriod && !typeOfDocument.includes("Revision") && typeOfCurrentPeriod.includes("FY")) {
              value = data['statements'][FY_Index][key];  // Set the value if it's not a revision and it's for 'FY'
              Logger.log('Value: ' + value);
              break;  // Break the loop if the data is not a revision and it's for 'FY'

4-5 通期決算(FY)/四半期決算(1Q~3Q)でプログラムを分ける

J-Quantsでは、通期決算報告だけでなく四半期単体の報告にも対応しています。基本的な処理のフローは同じなので、詳しい説明は省きます。
以下に、四半期決算報告の値を抽出するためのコードを載せます

1Q
FinStatementsSPQ1.gs
function getFinStatementsSPQ1(stockCode, SPyear, index, FY_Index,fiscalYearEnd) {
  //変数の宣言
  var scriptProperties = PropertiesService.getScriptProperties();
  var idToken = scriptProperties.getProperty('IDTOKEN');
  var cache = CacheService.getScriptCache();
  
  // 引数から指定の年を取得
  var SpecificYear = SPyear;
  
  // 現在の年と前年を確認
  var years = [SpecificYear - 1, SpecificYear];
  
  //決算期に基づく開始日と終了日の設定
  var startMonth, endMonth;
  if (fiscalYearEnd == 3) {
    startMonth = 6;
    endMonth = 7;
  } else if (fiscalYearEnd == 12) {
    startMonth = 3;
    endMonth = 4;
  } else if (fiscalYearEnd == 9) {
    startMonth = 0;  // January
    endMonth = 1;  // February
  } else if (fiscalYearEnd == 6) {
    startMonth = 9;
    endMonth = 10;
  } else {
    // Handle other fiscal year end cases
  }

  //StatementKeyの数値とKey(文字列)との紐づけ

  const financialStatementKeys = {
    1: 'DisclosedDate',
    2: 'DisclosedTime',
    3: 'LocalCode',
    4: 'DisclosureNumber',
    5: 'TypeOfDocument',
    6: 'TypeOfCurrentPeriod',
    7: 'CurrentPeriodStartDate',
    8: 'CurrentPeriodEndDate',
    9: 'CurrentFiscalYearStartDate',
    10: 'CurrentFiscalYearEndDate',
    11: 'NextFiscalYearStartDate',
    12: 'NextFiscalYearEndDate',
    13: 'NetSales',
    14: 'OperatingProfit',
    15: 'OrdinaryProfit',
    16: 'Profit',
    17: 'EarningsPerShare',
    18: 'DilutedEarningsPerShare',
    19: 'TotalAssets',
    20: 'Equity',
    21: 'EquityToAssetRatio',
    22: 'BookValuePerShare',
    23: 'CashFlowsFromOperatingActivities',
    24: 'CashFlowsFromInvestingActivities',
    25: 'CashFlowsFromFinancingActivities',
    26: 'CashAndEquivalents',
    27: 'ResultDividendPerShare1stQuarter',
    28: 'ResultDividendPerShare2ndQuarter',
    29: 'ResultDividendPerShare3rdQuarter',
    30: 'ResultDividendPerShareFiscalYearEnd',
    31: 'ResultDividendPerShareAnnual',
    32: 'DistributionsPerUnit(REIT)',
    33: 'ResultTotalDividendPaidAnnual',
    34: 'ResultPayoutRatioAnnual',
    35: 'ForecastDividendPerShare1stQuarter',
    36: 'ForecastDividendPerShare2ndQuarter',
    37: 'ForecastDividendPerShare3rdQuarter',
    38: 'ForecastDividendPerShareFiscalYearEnd',
    39: 'ForecastDividendPerShareAnnual',
    40: 'ForecastDistributionsPerUnit(REIT)',
    41: 'ForecastTotalDividendPaidAnnual',
    42: 'ForecastPayoutRatioAnnual',
    43: 'NextYearForecastDividendPerShare1stQuarter',
    44: 'NextYearForecastDividendPerShare2ndQuarter',
    45: 'NextYearForecastDividendPerShare3rdQuarter',
    46: 'NextYearForecastDividendPerShareFiscalYearEnd',
    47: 'NextYearForecastDividendPerShareAnnual',
    48: 'NextYearForecastDistributionsPerUnit(REIT)',
    49: 'NextYearForecastPayoutRatioAnnual',
    50: 'ForecastNetSales2ndQuarter',
    51: 'ForecastOperatingProfit2ndQuarter',
    52: 'ForecastOrdinaryProfit2ndQuarter',
    53: 'ForecastProfit2ndQuarter',
    54: 'ForecastEarningsPerShare2ndQuarter',
    55: 'NextYearForecastNetSales2ndQuarter',
    56: 'NextYearForecastOperatingProfit2ndQuarter',
    57: 'NextYearForecastOrdinaryProfit2ndQuarter',
    58: 'NextYearForecastProfit2ndQuarter',
    59: 'NextYearForecastEarningsPerShare2ndQuarter',
    60: 'ForecastNetSales',
    61: 'ForecastOperatingProfit',
    62: 'ForecastOrdinaryProfit',
    63: 'ForecastProfit',
    64: 'ForecastEarningsPerShare',
    65: 'NextYearForecastNetSales',
    66: 'NextYearForecastOperatingProfit',
    67: 'NextYearForecastOrdinaryProfit',
    68: 'NextYearForecastProfit',
    69: 'NextYearForecastEarningsPerShare',
    70: 'MaterialChangesInSubsidiaries',
    71: 'ChangesBasedOnRevisionsOfAccountingStandard',
    72: 'ChangesOtherThanOnesBasedOnRevisionsOfAccountingStandard',
    73: 'ChangesInAccountingEstimates',
    74: 'RetrospectiveRestatement',
    75: 'NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock',
    76: 'NumberOfTreasuryStockAtTheEndOfFiscalYear',
    77: 'AverageNumberOfShares',
    78: 'NonConsolidatedNetSales',
    79: 'NonConsolidatedOperatingProfit',
    80: 'NonConsolidatedOrdinaryProfit',
    81: 'NonConsolidatedProfit',
    82: 'NonConsolidatedEarningsPerShare',
    83: 'NonConsolidatedTotalAssets',
    84: 'NonConsolidatedEquity',
    85: 'NonConsolidatedEquityToAssetRatio',
    86: 'NonConsolidatedBookValuePerShare',
    87: 'ForecastNonConsolidatedNetSales2ndQuarter',
    88: 'ForecastNonConsolidatedOperatingProfit2ndQuarter',
    89: 'ForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    90: 'ForecastNonConsolidatedProfit2ndQuarter',
    91: 'ForecastNonConsolidatedEarningsPerShare2ndQuarter',
    92: 'NextYearForecastNonConsolidatedNetSales2ndQuarter',
    93: 'NextYearForecastNonConsolidatedOperatingProfit2ndQuarter',
    94: 'NextYearForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    95: 'NextYearForecastNonConsolidatedProfit2ndQuarter',
    96: 'NextYearForecastNonConsolidatedEarningsPerShare2ndQuarter',
    97: 'ForecastNonConsolidatedNetSales',
    98: 'ForecastNonConsolidatedOperatingProfit',
    99: 'ForecastNonConsolidatedOrdinaryProfit',
    100: 'ForecastNonConsolidatedProfit',
    101: 'ForecastNonConsolidatedEarningsPerShare',
    102: 'NextYearForecastNonConsolidatedNetSales',
    103: 'NextYearForecastNonConsolidatedOperatingProfit',
    104: 'NextYearForecastNonConsolidatedOrdinaryProfit',
    105: 'NextYearForecastNonConsolidatedProfit',
    106: 'NextYearForecastNonConsolidatedEarningsPerShare',
  };

  // For each year, check the financial statements. Start from the most recent year.
  for (var i = years.length - 1; i >= 0; i--) {
    var year = years[i];
    var startDate = new Date(year, startMonth, 10);
    var endDate = new Date(year, endMonth, 15);
    var value = null;  // Initialize the value as null

    for (var day = startDate; day <= endDate; day.setDate(day.getDate() + 1)) {
      var dateStr = day.toISOString().split('T')[FY_Index];  // format as 'yyyy-mm-dd'
      var cacheKey = 'FINS_STATEMENTS_SPQ1_' + stockCode + '_' + dateStr;
      var cachedData = cache.get(cacheKey);

      var data;

      if (cachedData != null) {
        Logger.log('Data found in cache');
        Logger.log('Date:' + dateStr);
        Logger.log('index: ' + index);
        Logger.log('cachedData: ' + JSON.parse(cachedData));
        data = JSON.parse(cachedData);
      } else {
        var url = "https://api.jquants.com/v1/fins/statements?code=" + stockCode + "&date=" + dateStr;
        var options = {
          method: 'get',
          headers: { 'Authorization': 'Bearer ' + idToken },
          muteHttpExceptions: true
        };
        
        try {
          var response = UrlFetchApp.fetch(url, options);
          data = JSON.parse(response.getContentText());

          Logger.log('Response status code: ' + response.getResponseCode());
          Logger.log('Fetched data: ' + JSON.stringify(data));
          cache.put(cacheKey, JSON.stringify(data), 3600);

        } catch (e) {
          Logger.log('Error in fetching stock price: ' + e.message);
          return 'Error in fetching stock price: ' + e.message;
        }
      }

      // Check if there is a financial statement for this day
      if (data['statements']) {
        if (data['statements'].length > 0) {
          const key = financialStatementKeys[index];
          if (key) {
            var typeOfDocument = data['statements'][FY_Index]['TypeOfDocument'];
            var typeOfCurrentPeriod = data['statements'][FY_Index]['TypeOfCurrentPeriod'];
            if (typeOfCurrentPeriod && !typeOfDocument.includes("Revision") && typeOfCurrentPeriod.includes("1Q")) {
              value = data['statements'][FY_Index][key];  // Set the value if it's not a revision and it's for 'FY'
              Logger.log('Value: ' + value);
              break;  // Break the loop if the data is not a revision and it's for 'FY'
            } else {
              Logger.log('Revision detected or not 1Q, moving to next date');
              value = null;  // Set the value as null if a revision is detected or it's not for 'FY'
              continue; // Skip to the next day if it is a revision or it's not for 'FY'
            }
          } else {
            Logger.log('Invalid index: ' + index);
            return null;
          }
        }
      } else {
        Logger.log('No statements data in the API response for date: ' + day);
      }
    }

    // Return the value at the end
    if (value !== null) {
      return value;
    }
  }

  return null;  // Return null if no non-revision financial statement was found
}
2Q
FinStatementsSPQ2.gs
function getFinStatementsSPQ2(stockCode, SPyear, index, FY_Index,fiscalYearEnd) {
  //変数の宣言
  var scriptProperties = PropertiesService.getScriptProperties();
  var idToken = scriptProperties.getProperty('IDTOKEN');
  var cache = CacheService.getScriptCache();
  
  // 引数から指定の年を取得
  var SpecificYear = SPyear;
  
  // 現在の年と前年を確認
  var years = [SpecificYear - 1, SpecificYear];
  
  //決算期に基づく開始日と終了日の設定
  var startMonth, endMonth;
  if (fiscalYearEnd == 3) {
    startMonth = 9;
    endMonth = 10;
  } else if (fiscalYearEnd == 12) {
    startMonth = 6;
    endMonth = 7;
  } else if (fiscalYearEnd == 9) {
    startMonth = 3;
    endMonth = 4;
  } else if (fiscalYearEnd == 6) {
    startMonth = 0;  // January
    endMonth = 1;  // February
  } else {
    // Handle other fiscal year end cases
  }

  //StatementKeyの数値とKey(文字列)との紐づけ

  const financialStatementKeys = {
    1: 'DisclosedDate',
    2: 'DisclosedTime',
    3: 'LocalCode',
    4: 'DisclosureNumber',
    5: 'TypeOfDocument',
    6: 'TypeOfCurrentPeriod',
    7: 'CurrentPeriodStartDate',
    8: 'CurrentPeriodEndDate',
    9: 'CurrentFiscalYearStartDate',
    10: 'CurrentFiscalYearEndDate',
    11: 'NextFiscalYearStartDate',
    12: 'NextFiscalYearEndDate',
    13: 'NetSales',
    14: 'OperatingProfit',
    15: 'OrdinaryProfit',
    16: 'Profit',
    17: 'EarningsPerShare',
    18: 'DilutedEarningsPerShare',
    19: 'TotalAssets',
    20: 'Equity',
    21: 'EquityToAssetRatio',
    22: 'BookValuePerShare',
    23: 'CashFlowsFromOperatingActivities',
    24: 'CashFlowsFromInvestingActivities',
    25: 'CashFlowsFromFinancingActivities',
    26: 'CashAndEquivalents',
    27: 'ResultDividendPerShare1stQuarter',
    28: 'ResultDividendPerShare2ndQuarter',
    29: 'ResultDividendPerShare3rdQuarter',
    30: 'ResultDividendPerShareFiscalYearEnd',
    31: 'ResultDividendPerShareAnnual',
    32: 'DistributionsPerUnit(REIT)',
    33: 'ResultTotalDividendPaidAnnual',
    34: 'ResultPayoutRatioAnnual',
    35: 'ForecastDividendPerShare1stQuarter',
    36: 'ForecastDividendPerShare2ndQuarter',
    37: 'ForecastDividendPerShare3rdQuarter',
    38: 'ForecastDividendPerShareFiscalYearEnd',
    39: 'ForecastDividendPerShareAnnual',
    40: 'ForecastDistributionsPerUnit(REIT)',
    41: 'ForecastTotalDividendPaidAnnual',
    42: 'ForecastPayoutRatioAnnual',
    43: 'NextYearForecastDividendPerShare1stQuarter',
    44: 'NextYearForecastDividendPerShare2ndQuarter',
    45: 'NextYearForecastDividendPerShare3rdQuarter',
    46: 'NextYearForecastDividendPerShareFiscalYearEnd',
    47: 'NextYearForecastDividendPerShareAnnual',
    48: 'NextYearForecastDistributionsPerUnit(REIT)',
    49: 'NextYearForecastPayoutRatioAnnual',
    50: 'ForecastNetSales2ndQuarter',
    51: 'ForecastOperatingProfit2ndQuarter',
    52: 'ForecastOrdinaryProfit2ndQuarter',
    53: 'ForecastProfit2ndQuarter',
    54: 'ForecastEarningsPerShare2ndQuarter',
    55: 'NextYearForecastNetSales2ndQuarter',
    56: 'NextYearForecastOperatingProfit2ndQuarter',
    57: 'NextYearForecastOrdinaryProfit2ndQuarter',
    58: 'NextYearForecastProfit2ndQuarter',
    59: 'NextYearForecastEarningsPerShare2ndQuarter',
    60: 'ForecastNetSales',
    61: 'ForecastOperatingProfit',
    62: 'ForecastOrdinaryProfit',
    63: 'ForecastProfit',
    64: 'ForecastEarningsPerShare',
    65: 'NextYearForecastNetSales',
    66: 'NextYearForecastOperatingProfit',
    67: 'NextYearForecastOrdinaryProfit',
    68: 'NextYearForecastProfit',
    69: 'NextYearForecastEarningsPerShare',
    70: 'MaterialChangesInSubsidiaries',
    71: 'ChangesBasedOnRevisionsOfAccountingStandard',
    72: 'ChangesOtherThanOnesBasedOnRevisionsOfAccountingStandard',
    73: 'ChangesInAccountingEstimates',
    74: 'RetrospectiveRestatement',
    75: 'NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock',
    76: 'NumberOfTreasuryStockAtTheEndOfFiscalYear',
    77: 'AverageNumberOfShares',
    78: 'NonConsolidatedNetSales',
    79: 'NonConsolidatedOperatingProfit',
    80: 'NonConsolidatedOrdinaryProfit',
    81: 'NonConsolidatedProfit',
    82: 'NonConsolidatedEarningsPerShare',
    83: 'NonConsolidatedTotalAssets',
    84: 'NonConsolidatedEquity',
    85: 'NonConsolidatedEquityToAssetRatio',
    86: 'NonConsolidatedBookValuePerShare',
    87: 'ForecastNonConsolidatedNetSales2ndQuarter',
    88: 'ForecastNonConsolidatedOperatingProfit2ndQuarter',
    89: 'ForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    90: 'ForecastNonConsolidatedProfit2ndQuarter',
    91: 'ForecastNonConsolidatedEarningsPerShare2ndQuarter',
    92: 'NextYearForecastNonConsolidatedNetSales2ndQuarter',
    93: 'NextYearForecastNonConsolidatedOperatingProfit2ndQuarter',
    94: 'NextYearForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    95: 'NextYearForecastNonConsolidatedProfit2ndQuarter',
    96: 'NextYearForecastNonConsolidatedEarningsPerShare2ndQuarter',
    97: 'ForecastNonConsolidatedNetSales',
    98: 'ForecastNonConsolidatedOperatingProfit',
    99: 'ForecastNonConsolidatedOrdinaryProfit',
    100: 'ForecastNonConsolidatedProfit',
    101: 'ForecastNonConsolidatedEarningsPerShare',
    102: 'NextYearForecastNonConsolidatedNetSales',
    103: 'NextYearForecastNonConsolidatedOperatingProfit',
    104: 'NextYearForecastNonConsolidatedOrdinaryProfit',
    105: 'NextYearForecastNonConsolidatedProfit',
    106: 'NextYearForecastNonConsolidatedEarningsPerShare',
  };

  // For each year, check the financial statements. Start from the most recent year.
  for (var i = years.length - 1; i >= 0; i--) {
    var year = years[i];
    var startDate = new Date(year, startMonth, 10);
    var endDate = new Date(year, endMonth, 15);
    var value = null;  // Initialize the value as null

    for (var day = startDate; day <= endDate; day.setDate(day.getDate() + 1)) {
      var dateStr = day.toISOString().split('T')[FY_Index];  // format as 'yyyy-mm-dd'
      var cacheKey = 'FINS_STATEMENTS_SPQ2_' + stockCode + '_' + dateStr;
      var cachedData = cache.get(cacheKey);

      var data;

      if (cachedData != null) {
        Logger.log('Data found in cache');
        Logger.log('Date:' + dateStr);
        Logger.log('index: ' + index);
        Logger.log('cachedData: ' + JSON.parse(cachedData));
        data = JSON.parse(cachedData);
      } else {
        var url = "https://api.jquants.com/v1/fins/statements?code=" + stockCode + "&date=" + dateStr;
        var options = {
          method: 'get',
          headers: { 'Authorization': 'Bearer ' + idToken },
          muteHttpExceptions: true
        };
        
        try {
          var response = UrlFetchApp.fetch(url, options);
          data = JSON.parse(response.getContentText());

          Logger.log('Response status code: ' + response.getResponseCode());
          Logger.log('Fetched data: ' + JSON.stringify(data));
          cache.put(cacheKey, JSON.stringify(data), 3600);

        } catch (e) {
          Logger.log('Error in fetching stock price: ' + e.message);
          return 'Error in fetching stock price: ' + e.message;
        }
      }

      // Check if there is a financial statement for this day
      if (data['statements']) {
        if (data['statements'].length > 0) {
          const key = financialStatementKeys[index];
          if (key) {
            var typeOfDocument = data['statements'][FY_Index]['TypeOfDocument'];
            var typeOfCurrentPeriod = data['statements'][FY_Index]['TypeOfCurrentPeriod'];
            if (typeOfCurrentPeriod && !typeOfDocument.includes("Revision") && typeOfCurrentPeriod.includes("2Q")) {
              value = data['statements'][FY_Index][key];  // Set the value if it's not a revision and it's for 'FY'
              Logger.log('Value: ' + value);
              break;  // Break the loop if the data is not a revision and it's for 'FY'
            } else {
              Logger.log('Revision detected or not 2Q, moving to next date');
              value = null;  // Set the value as null if a revision is detected or it's not for 'FY'
              continue; // Skip to the next day if it is a revision or it's not for 'FY'
            }
          } else {
            Logger.log('Invalid index: ' + index);
            return null;
          }
        }
      } else {
        Logger.log('No statements data in the API response for date: ' + day);
      }
    }

    // Return the value at the end
    if (value !== null) {
      return value;
    }
  }

  return null;  // Return null if no non-revision financial statement was found
}
3Q
FinStatementsSPQ3.gs
function getFinStatementsSPQ3(stockCode, SPyear, index, FY_Index,fiscalYearEnd) {
  //変数の宣言
  var scriptProperties = PropertiesService.getScriptProperties();
  var idToken = scriptProperties.getProperty('IDTOKEN');
  var cache = CacheService.getScriptCache();
  
  // 引数から指定の年を取得
  var SpecificYear = SPyear;
  
  // 現在の年と前年を確認
  var years = [SpecificYear - 1, SpecificYear];
  
  //決算期に基づく開始日と終了日の設定
  var startMonth, endMonth;
  if (fiscalYearEnd == 3) {
    startMonth = 0;  // January
    endMonth = 1;  // February
  } else if (fiscalYearEnd == 12) {
    startMonth = 9;
    endMonth = 10;
  } else if (fiscalYearEnd == 9) {
    startMonth = 6;
    endMonth = 7;
  } else if (fiscalYearEnd == 6) {
    startMonth = 3;
    endMonth = 4;
  } else {
    // Handle other fiscal year end cases
  }

  //StatementKeyの数値とKey(文字列)との紐づけ

  const financialStatementKeys = {
    1: 'DisclosedDate',
    2: 'DisclosedTime',
    3: 'LocalCode',
    4: 'DisclosureNumber',
    5: 'TypeOfDocument',
    6: 'TypeOfCurrentPeriod',
    7: 'CurrentPeriodStartDate',
    8: 'CurrentPeriodEndDate',
    9: 'CurrentFiscalYearStartDate',
    10: 'CurrentFiscalYearEndDate',
    11: 'NextFiscalYearStartDate',
    12: 'NextFiscalYearEndDate',
    13: 'NetSales',
    14: 'OperatingProfit',
    15: 'OrdinaryProfit',
    16: 'Profit',
    17: 'EarningsPerShare',
    18: 'DilutedEarningsPerShare',
    19: 'TotalAssets',
    20: 'Equity',
    21: 'EquityToAssetRatio',
    22: 'BookValuePerShare',
    23: 'CashFlowsFromOperatingActivities',
    24: 'CashFlowsFromInvestingActivities',
    25: 'CashFlowsFromFinancingActivities',
    26: 'CashAndEquivalents',
    27: 'ResultDividendPerShare1stQuarter',
    28: 'ResultDividendPerShare2ndQuarter',
    29: 'ResultDividendPerShare3rdQuarter',
    30: 'ResultDividendPerShareFiscalYearEnd',
    31: 'ResultDividendPerShareAnnual',
    32: 'DistributionsPerUnit(REIT)',
    33: 'ResultTotalDividendPaidAnnual',
    34: 'ResultPayoutRatioAnnual',
    35: 'ForecastDividendPerShare1stQuarter',
    36: 'ForecastDividendPerShare2ndQuarter',
    37: 'ForecastDividendPerShare3rdQuarter',
    38: 'ForecastDividendPerShareFiscalYearEnd',
    39: 'ForecastDividendPerShareAnnual',
    40: 'ForecastDistributionsPerUnit(REIT)',
    41: 'ForecastTotalDividendPaidAnnual',
    42: 'ForecastPayoutRatioAnnual',
    43: 'NextYearForecastDividendPerShare1stQuarter',
    44: 'NextYearForecastDividendPerShare2ndQuarter',
    45: 'NextYearForecastDividendPerShare3rdQuarter',
    46: 'NextYearForecastDividendPerShareFiscalYearEnd',
    47: 'NextYearForecastDividendPerShareAnnual',
    48: 'NextYearForecastDistributionsPerUnit(REIT)',
    49: 'NextYearForecastPayoutRatioAnnual',
    50: 'ForecastNetSales2ndQuarter',
    51: 'ForecastOperatingProfit2ndQuarter',
    52: 'ForecastOrdinaryProfit2ndQuarter',
    53: 'ForecastProfit2ndQuarter',
    54: 'ForecastEarningsPerShare2ndQuarter',
    55: 'NextYearForecastNetSales2ndQuarter',
    56: 'NextYearForecastOperatingProfit2ndQuarter',
    57: 'NextYearForecastOrdinaryProfit2ndQuarter',
    58: 'NextYearForecastProfit2ndQuarter',
    59: 'NextYearForecastEarningsPerShare2ndQuarter',
    60: 'ForecastNetSales',
    61: 'ForecastOperatingProfit',
    62: 'ForecastOrdinaryProfit',
    63: 'ForecastProfit',
    64: 'ForecastEarningsPerShare',
    65: 'NextYearForecastNetSales',
    66: 'NextYearForecastOperatingProfit',
    67: 'NextYearForecastOrdinaryProfit',
    68: 'NextYearForecastProfit',
    69: 'NextYearForecastEarningsPerShare',
    70: 'MaterialChangesInSubsidiaries',
    71: 'ChangesBasedOnRevisionsOfAccountingStandard',
    72: 'ChangesOtherThanOnesBasedOnRevisionsOfAccountingStandard',
    73: 'ChangesInAccountingEstimates',
    74: 'RetrospectiveRestatement',
    75: 'NumberOfIssuedAndOutstandingSharesAtTheEndOfFiscalYearIncludingTreasuryStock',
    76: 'NumberOfTreasuryStockAtTheEndOfFiscalYear',
    77: 'AverageNumberOfShares',
    78: 'NonConsolidatedNetSales',
    79: 'NonConsolidatedOperatingProfit',
    80: 'NonConsolidatedOrdinaryProfit',
    81: 'NonConsolidatedProfit',
    82: 'NonConsolidatedEarningsPerShare',
    83: 'NonConsolidatedTotalAssets',
    84: 'NonConsolidatedEquity',
    85: 'NonConsolidatedEquityToAssetRatio',
    86: 'NonConsolidatedBookValuePerShare',
    87: 'ForecastNonConsolidatedNetSales2ndQuarter',
    88: 'ForecastNonConsolidatedOperatingProfit2ndQuarter',
    89: 'ForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    90: 'ForecastNonConsolidatedProfit2ndQuarter',
    91: 'ForecastNonConsolidatedEarningsPerShare2ndQuarter',
    92: 'NextYearForecastNonConsolidatedNetSales2ndQuarter',
    93: 'NextYearForecastNonConsolidatedOperatingProfit2ndQuarter',
    94: 'NextYearForecastNonConsolidatedOrdinaryProfit2ndQuarter',
    95: 'NextYearForecastNonConsolidatedProfit2ndQuarter',
    96: 'NextYearForecastNonConsolidatedEarningsPerShare2ndQuarter',
    97: 'ForecastNonConsolidatedNetSales',
    98: 'ForecastNonConsolidatedOperatingProfit',
    99: 'ForecastNonConsolidatedOrdinaryProfit',
    100: 'ForecastNonConsolidatedProfit',
    101: 'ForecastNonConsolidatedEarningsPerShare',
    102: 'NextYearForecastNonConsolidatedNetSales',
    103: 'NextYearForecastNonConsolidatedOperatingProfit',
    104: 'NextYearForecastNonConsolidatedOrdinaryProfit',
    105: 'NextYearForecastNonConsolidatedProfit',
    106: 'NextYearForecastNonConsolidatedEarningsPerShare',
  };

  // For each year, check the financial statements. Start from the most recent year.
  for (var i = years.length - 1; i >= 0; i--) {
    var year = years[i];
    var startDate = new Date(year, startMonth, 10);
    var endDate = new Date(year, endMonth, 15);
    var value = null;  // Initialize the value as null

    for (var day = startDate; day <= endDate; day.setDate(day.getDate() + 1)) {
      var dateStr = day.toISOString().split('T')[FY_Index];  // format as 'yyyy-mm-dd'
      var cacheKey = 'FINS_STATEMENTS_SPQ3_' + stockCode + '_' + dateStr;
      var cachedData = cache.get(cacheKey);

      var data;

      if (cachedData != null) {
        Logger.log('Data found in cache');
        Logger.log('Date:' + dateStr);
        Logger.log('index: ' + index);
        Logger.log('cachedData: ' + JSON.parse(cachedData));
        data = JSON.parse(cachedData);
      } else {
        var url = "https://api.jquants.com/v1/fins/statements?code=" + stockCode + "&date=" + dateStr;
        var options = {
          method: 'get',
          headers: { 'Authorization': 'Bearer ' + idToken },
          muteHttpExceptions: true
        };
        
        try {
          var response = UrlFetchApp.fetch(url, options);
          data = JSON.parse(response.getContentText());

          Logger.log('Response status code: ' + response.getResponseCode());
          Logger.log('Fetched data: ' + JSON.stringify(data));
          cache.put(cacheKey, JSON.stringify(data), 3600);

        } catch (e) {
          Logger.log('Error in fetching stock price: ' + e.message);
          return 'Error in fetching stock price: ' + e.message;
        }
      }

      // Check if there is a financial statement for this day
      if (data['statements']) {
        if (data['statements'].length > 0) {
          const key = financialStatementKeys[index];
          if (key) {
            var typeOfDocument = data['statements'][FY_Index]['TypeOfDocument'];
            var typeOfCurrentPeriod = data['statements'][FY_Index]['TypeOfCurrentPeriod'];
            if (typeOfCurrentPeriod && !typeOfDocument.includes("Revision") && typeOfCurrentPeriod.includes("3Q")) {
              value = data['statements'][FY_Index][key];  // Set the value if it's not a revision and it's for 'FY'
              Logger.log('Value: ' + value);
              break;  // Break the loop if the data is not a revision and it's for 'FY'
            } else {
              Logger.log('Revision detected or not 3Q, moving to next date');
              value = null;  // Set the value as null if a revision is detected or it's not for 'FY'
              continue; // Skip to the next day if it is a revision or it's not for 'FY'
            }
          } else {
            Logger.log('Invalid index: ' + index);
            return null;
          }
        }
      } else {
        Logger.log('No statements data in the API response for date: ' + day);
      }
    }

    // Return the value at the end
    if (value !== null) {
      return value;
    }
  }

  return null;  // Return null if no non-revision financial statement was found
}

5 実際に応用するにはどうすればよいか?

もしかしたらこの記事は、プログラミングに慣れ親しんでいない、個人投資家の方々などもご覧になるかもしれません。よって、なるだけそのまま応用できるようにしたいと思います。
実応用のおおまかな流れは以下の通りです。

⓪あらかじめ、J-Quants APIのスタンダード以上を契約しておく。
https://jpx-jquants.com/

①サンプルのスプレッドシートをダウンロードする。(再配布禁止)
https://docs.google.com/spreadsheets/d/1XPxXj9q2iyT7r8K8QjF2XWQikS_CZ0ek7pawEzKwj-E/edit?usp=sharing

②自分のスプレッドシートにアップロードする。

③GoogleAppScript画面を開き、各プログラムのスクリプトを作成、内容をコピーする。
image.png
image.png

④GAS内でスクリプトプロパティを入力する。
image.png

⑤スプレッドシート上で目的に応じて、独自関数として呼び出す。

さいごに

以上、J-Quants APIとGoogleAppScript(GAS) を用いた日本株の情報抽出でした。
投資家の皆様は、よろしければこれをスムーズな財務分析にお役立てください。

課題としては、やはり大量にセルを並べた際の速度が遅いという点です。
今後何か思いついたらまた追記しようと思います。
大規模言語モデルのAPI組み合わせてなんかできそうだなぁ...とかいうこともふわっと考えています。

お読みいただきありがとうございました!

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?