J-Quants APIを使用したPER、PBRの算出検証

日本取引所グループ（JPX）が出している[J-Quants API](https://jpx-jquants.com/)という株関連の情報を取得できるAPIがあります。
データ範囲に制限がありますが無料でも使えますので今回はJ-Quants APIを使ってPER(株価収益率)、PBR（株価純資産倍率）を出したいと思います

# J-Quants API とその他の株関連API

各種APIについて個人的な理解をまとめておきます

## J-Quants API
- ファンダメンタル分析に向いている
- JPXが出しているので安心安全
- 株価はデイリーのみなのでデイトレには向いていない
- 無料だとお遊びにしか使えない（株価が三ヶ月前までのものしか取得できない）
- 株分割されたあとの過去株価の調整データがあるのでかなりこれは良い
- 地獄のEDINETタクソノミXBRLに比べたらかなり良い

## Stooq 
- Python pandas_datareaderから無料で株価がデイリーで取得できる
- デイリーなのでデイトレには向いていない
- 無料！アカウント登録なし！のしびれる仕様
- 株価取得だけなのでファンダメンタル分析には向いていない
- テクニカルチャートをバンバン出力するのに向いている
- 株分割の考慮されたデータがたぶんないので時前で用意する必要がある、なので株分割があると地獄

## EDINET
- 金融庁産
- 有価証券報告書が入手できる
- APIがあるが中身はZIP😅、XBRL😅、タクソノミ😅　難解
- 株価でなくファンダメンタル分析的なデータを正確に取得できる


# PERとPBRの算出方法

J-Quants APIのstatements（財務情報取得API）でESPとBSPが取得できますのでそれを使います

|用語|日本語|
|:--|:--|
|EPS|1株当たり当期純利益| 
|BPS|1株当たり純資産|

計算方法はこうなります

|用語|算出方法|
|:--|:--|
|PER| (ある時点の)株価 ÷ EPS |
|PBR| (ある時点の)株価 ÷ BPS |


株価は/prices/daily_quotesで取得できます。無料だと遅延日数がありますが今回のような使用では問題にならないでしょう。


# 実装

ソースコードはこちら https://github.com/AKB428/pbr_per_poc1

あらかじめアカウント登録を行い環境変数にリフレッシュトークンを格納しておきます

```
export REFRESH_TOKEN=""
```

## 1 EPS BPSを取得

まずEPS、BPSを取得します

```python
import requests
import os
import json
import pandas as pd
import sys

def format_number(value):
    try:
        return f"{int(value):,}"
    except (ValueError, TypeError):
        return value

# 銘柄コードを第一引数から取得
if len(sys.argv) < 2:
    raise ValueError("銘柄コードを指定してください。")

code = sys.argv[1]

# 環境変数からREFRESH_TOKENを取得
REFRESH_TOKEN = os.getenv("REFRESH_TOKEN")

# REFRESH_TOKENが取得できなかった場合のエラーハンドリング
if not REFRESH_TOKEN:
    raise ValueError("環境変数 'REFRESH_TOKEN' が設定されていません。")

# POSTリクエストを送信
r_post = requests.post(f"https://api.jquants.com/v1/token/auth_refresh?refreshtoken={REFRESH_TOKEN}")

# レスポンスからJSONデータを取得
response_data = r_post.json()

# idTokenを取得
idToken = response_data.get("idToken")

# idTokenが取得できなかった場合のエラーハンドリング
if not idToken:
    raise ValueError("レスポンスに 'idToken' が含まれていません。")

# APIエンドポイントへのGETリクエスト
headers = {'Authorization': f'Bearer {idToken}'}
params = {"code": code}
r_get = requests.get("https://api.jquants.com/v1/fins/statements", headers=headers, params=params)

# レスポンスを取得
response_json = r_get.json()

# 指定された項目を抽出して整形
statements = response_json.get('statements', [])
data = []

for statement in statements:
    data.append({
        "DisclosedDate": statement.get("DisclosedDate"),
        "NetSales": format_number(statement.get("NetSales")),
        "EarningsPerShare": statement.get("EarningsPerShare"),
        "BookValuePerShare": statement.get("BookValuePerShare")
    })

# DataFrameを作成して表示
df = pd.DataFrame(data)
print(df.to_string(index=False, justify='left'))

```

出力結果

任天堂(7832)で試してみます

```
python3 getfinStatements.py 7974

```

```
DisclosedDate NetSales          EarningsPerShare BookValuePerShare
2022-05-10    1,695,344,000,000 4046.69          17635.6
2022-08-03      307,460,000,000 1018.55
2022-11-08      656,974,000,000  197.61
2023-02-07    1,295,178,000,000  297.05
2023-05-09    1,601,677,000,000  371.41          1946.55
2023-08-03      461,341,000,000  155.48
2023-11-07      796,237,000,000  233.03
2024-02-06    1,394,796,000,000  350.48
```

うまくEPS、BPSが取得できています。
すべてのレコードにBPSがあるわけではないようです。


## EPS、BPSがあるレコードに対してPER、PBRを算出

DisclosedDate（開示日）の株価を取得しPER、BSPを取得します。

```python
import requests
import os
import json
import pandas as pd
import sys
from tokyo_stock_exchange import tse

def format_number(value):
    try:
        return f"{int(value):,}"
    except (ValueError, TypeError):
        return value

def get_closing_price(idToken, code, date):
    headers = {'Authorization': f'Bearer {idToken}'}
    params = {"code": code, "date": date}
    r_get = requests.get("https://api.jquants.com/v1/prices/daily_quotes", headers=headers, params=params)
    response_json = r_get.json()
    daily_quotes = response_json.get('daily_quotes', [])
    if daily_quotes:
        return daily_quotes[0].get('Close')
    return None

# 銘柄名またはコードを第一引数から取得
stock_name_or_code = sys.argv[1]

# 銘柄コードを取得
stock_info = tse.get_stock_info(stock_name_or_code)
if not stock_info:
    raise ValueError(f"銘柄 '{stock_name_or_code}' の情報が見つかりません。")
code = stock_info[0]

# 環境変数からREFRESH_TOKENを取得
REFRESH_TOKEN = os.getenv("REFRESH_TOKEN")

# REFRESH_TOKENが取得できなかった場合のエラーハンドリング
if not REFRESH_TOKEN:
    raise ValueError("環境変数 'REFRESH_TOKEN' が設定されていません。")

# POSTリクエストを送信
r_post = requests.post(f"https://api.jquants.com/v1/token/auth_refresh?refreshtoken={REFRESH_TOKEN}")

# レスポンスからJSONデータを取得
response_data = r_post.json()

# idTokenを取得
idToken = response_data.get("idToken")

# idTokenが取得できなかった場合のエラーハンドリング
if not idToken:
    raise ValueError("レスポンスに 'idToken' が含まれていません。")

# APIエンドポイントへのGETリクエスト
headers = {'Authorization': f'Bearer {idToken}'}
params = {"code": code}
r_get = requests.get("https://api.jquants.com/v1/fins/statements", headers=headers, params=params)

# レスポンスを取得
response_json = r_get.json()

# 指定された項目を抽出して整形
statements = response_json.get('statements', [])
data = []

for statement in statements:
    closing_price = get_closing_price(idToken, code, statement.get("DisclosedDate"))
    earnings_per_share = statement.get("EarningsPerShare")
    book_value_per_share = statement.get("BookValuePerShare")

    if closing_price and earnings_per_share:
        per = closing_price / float(earnings_per_share)
    else:
        per = None

    if closing_price and book_value_per_share:
        pbr = closing_price / float(book_value_per_share)
    else:
        pbr = None

    data.append({
        "DisclosedDate": statement.get("DisclosedDate"),
        "EarningsPerShare": earnings_per_share,
        "BookValuePerShare": book_value_per_share,
        "ClosingPrice": format_number(closing_price),
        "PER": f"{per:.1f}" if per is not None else None,
        "PBR": f"{pbr:.2f}" if pbr is not None else None
    })

# DataFrameを作成して表示
df = pd.DataFrame(data)
print(df.to_string(index=False, justify='left'))

```

実行
```
python3 getPerPbr.py 任天堂
```

結果
```
DisclosedDate EarningsPerShare BookValuePerShare ClosingPrice PER  PBR
2022-05-10    4046.69          17635.6           56,360       13.9 3.20
2022-08-03    1018.55                            56,700       55.7 None
2022-11-08     197.61                             6,225       31.5 None
2023-02-07     297.05                             5,624       18.9 None
2023-05-09     371.41          1946.55            5,730       15.4 2.94
2023-08-03     155.48                             6,357       40.9 None
2023-11-07     233.03                             6,385       27.4 None
2024-02-06     350.48                             8,376       23.9 None
```

PER、PBRが算出できました😁！


# 検証　果たしてこれはあってるのか

## 四季報を見てみる

四季報を見てみると
- 実績PER　高値
- 実績PER　安値
- PBR
- 予想PER

といくつかわかれていました。
PER、PBRの算出方法もかいてあり、予想PERは直近株価、実績PERは過去3決算期を使用。
PBRは直近株価を使用と書かれてありました。
四季報のPER、PBRとはずれていましたが（というかそもそも過去の四季報を持っていない）算出方法自体はずれてなさそうです。


## 四季報にならって高値、安値株価からのPERも算出してみる

```python
import requests
import os
import json
import pandas as pd
import sys
from tokyo_stock_exchange import tse

def format_number(value):
    try:
        return f"{int(value):,}"
    except (ValueError, TypeError):
        return value

def get_closing_price(idToken, code, date):
    headers = {'Authorization': f'Bearer {idToken}'}
    params = {"code": code, "date": date}
    r_get = requests.get("https://api.jquants.com/v1/prices/daily_quotes", headers=headers, params=params)
    response_json = r_get.json()
    daily_quotes = response_json.get('daily_quotes', [])
    if daily_quotes:
        return daily_quotes[0].get('Close')
    return None

def get_price_range(idToken, code, start_date, end_date):
    headers = {'Authorization': f'Bearer {idToken}'}
    params = {"code": code, "from": start_date, "to": end_date}
    r_get = requests.get("https://api.jquants.com/v1/prices/daily_quotes", headers=headers, params=params)
    response_json = r_get.json()
    daily_quotes = response_json.get('daily_quotes', [])
    if daily_quotes:
        high_prices = [quote.get('High') for quote in daily_quotes]
        low_prices = [quote.get('Low') for quote in daily_quotes]
        return max(high_prices), min(low_prices)
    return None, None

# 銘柄名またはコードを第一引数から取得
stock_name_or_code = sys.argv[1]

# 銘柄コードを取得
stock_info = tse.get_stock_info(stock_name_or_code)
if not stock_info:
    raise ValueError(f"銘柄 '{stock_name_or_code}' の情報が見つかりません。")
print("Stock Info:", stock_info)  # 取得したstock_infoを出力
code = stock_info[0]

# 環境変数からREFRESH_TOKENを取得
REFRESH_TOKEN = os.getenv("REFRESH_TOKEN")

# REFRESH_TOKENが取得できなかった場合のエラーハンドリング
if not REFRESH_TOKEN:
    raise ValueError("環境変数 'REFRESH_TOKEN' が設定されていません。")

# POSTリクエストを送信
r_post = requests.post(f"https://api.jquants.com/v1/token/auth_refresh?refreshtoken={REFRESH_TOKEN}")

# レスポンスからJSONデータを取得
response_data = r_post.json()

# idTokenを取得
idToken = response_data.get("idToken")

# idTokenが取得できなかった場合のエラーハンドリング
if not idToken:
    raise ValueError("レスポンスに 'idToken' が含まれていません。")

# APIエンドポイントへのGETリクエスト
headers = {'Authorization': f'Bearer {idToken}'}
params = {"code": code}
r_get = requests.get("https://api.jquants.com/v1/fins/statements", headers=headers, params=params)

# レスポンスを取得
response_json = r_get.json()

# 指定された項目を抽出して整形
statements = response_json.get('statements', [])
data = []
high_per = None
low_per = None

for statement in statements:
    closing_price = get_closing_price(idToken, code, statement.get("DisclosedDate"))
    earnings_per_share = statement.get("EarningsPerShare")
    book_value_per_share = statement.get("BookValuePerShare")

    if closing_price and earnings_per_share:
        per = closing_price / float(earnings_per_share)
    else:
        per = None

    if closing_price and book_value_per_share:
        pbr = closing_price / float(book_value_per_share)
    else:
        pbr = None

    data.append({
        "DisclosedDate": statement.get("DisclosedDate"),
        "NetSales": format_number(statement.get("NetSales")),
        "EarningsPerShare": earnings_per_share,
        "BookValuePerShare": book_value_per_share,
        "ClosingPrice": format_number(closing_price),
        "PER": f"{per:.1f}" if per is not None else None,
        "PBR": f"{pbr:.2f}" if pbr is not None else None
    })

# 最後のstatementから期間を取得
last_statement = statements[-1]
start_date = last_statement.get("CurrentPeriodStartDate")
end_date = last_statement.get("CurrentPeriodEndDate")
high_price, low_price = get_price_range(idToken, code, start_date, end_date)

if high_price and last_statement.get("EarningsPerShare"):
    high_per = high_price / float(last_statement.get("EarningsPerShare"))

if low_price and last_statement.get("EarningsPerShare"):
    low_per = low_price / float(last_statement.get("EarningsPerShare"))

# DataFrameを作成して表示
df = pd.DataFrame(data)
print(df.to_string(index=False, justify='left'))

print(f"\n対象期間: {start_date} ～ {end_date}")
print(f"High Price: {format_number(high_price)}")
print(f"Low Price: {format_number(low_price)}")
print(f"High PER: {high_per:.1f}" if high_per is not None else "High PER: N/A")
print(f"Low PER: {low_per:.1f}" if low_per is not None else "Low PER: N/A")

```

```
python3 getPerPbr2.py 任天堂
```

```
Stock Info: ('7974', '任天堂')
DisclosedDate NetSales          EarningsPerShare BookValuePerShare ClosingPrice PER  PBR
2022-05-10    1,695,344,000,000 4046.69          17635.6           56,360       13.9 3.20
2022-08-03      307,460,000,000 1018.55                            56,700       55.7 None
2022-11-08      656,974,000,000  197.61                             6,225       31.5 None
2023-02-07    1,295,178,000,000  297.05                             5,624       18.9 None
2023-05-09    1,601,677,000,000  371.41          1946.55            5,730       15.4 2.94
2023-08-03      461,341,000,000  155.48                             6,357       40.9 None
2023-11-07      796,237,000,000  233.03                             6,385       27.4 None
2024-02-06    1,394,796,000,000  350.48                             8,376       23.9 None

対象期間: 2023-04-01 ～ 2023-12-31
High Price: 7,395
Low Price: 5,150
High PER: 21.1
Low PER: 14.7
```


## EDINETから有価証券報告書と比べる

EDINETから有価証券報告書の四半期のPDFをダウンロードして値を比べます

![スクリーンショット 2024-07-06 180127.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/36931/411fd471-6145-2a62-c631-ad507bb17b9c.png)

84期第3四半期のEPSは有価証券報告書でも350.48となっており

J-Quants APIから取得したEPSともあっているので少なくともEPSは正確な値のため
取得する株価を適切にすればPERはそこまで変なことにはならなそうです。

↓先ほどの結果
```
DisclosedDate EarningsPerShare BookValuePerShare ClosingPrice PER  PBR
～～
2024-02-06     350.48                             8,376       23.9 None
```



# まとめ、得られたこと
巷でよく聞くようになったPER、PBRですが元となる値はどこから来ているのか？、対象とする株価はどの時点のものなのか？をしっかり理解できるいい機会となりました。株関連の理解を深めるためにも自分でプログラムを作って様々な指標を算出していくのはためになると思いました。

単純にPERを調べたい場合は有価証券報告書に記載があるので難解EDINETを使えばできます。
簡単にとりあえず算出したいみたいなときに無料のJ-Quants APIは役立ちそうです。