Fama-French ファクターを日本の株式市場で作る
20251030_FamaFrenchFactors_in-Japan
20251031 修正: authentification に関する記述と期間指定を追加しました。
Disclosure
この記事は所属組織とは一切関係なく、個別の銘柄・投資術などを推奨するものではありません。
Fama French Factor Model とは
みずほのサイトによくまとまっているので引用します。ただ、後述しますが、サイズの分け方の説明がオリジナルとちょっと違うんですよね。
論文を元にして建てのリターンを使ったファクターは French 教授のサイトで公開されています。
これらのファクターを含め、最近ではこちらのサイトで Yale 大学の教授がファクターリターンを公開しているので、ドル建てでもよければこういったファクターは簡単に取ることができます。
Factor を再現したい
これらのファクターはドル建てなので、日本株ポートフォリオのリスク・リターン分析に使うときはノイズが大きくて使い物になりません。そこで、円建ての日本株情報を使って自分でファクターを再現したいというときがよくあります。
今この記事を書いていて気づきましたが、各サイトからダウンロードしたファクターに為替リターンをかければ円建てのリターンが簡単に手に入りますね。。。とはいえ、それではこの記事の意義がなくなってしまうので、不都合な真実はいったん忘れましょう。
サイズポートフォリオの定義
前述の French 教授のサイトでは、以下の通り正確な定義が記載されています。米国においては中央値でサイズポートフォリオを分けています。
> The size breakpoint for year t is the median NYSE market equity at
the end of June of year t.
これらの論文にて日本株においてファーマフレンチの有効性を検証しているので、今回はこちらにしたがってポートフォリオを作って、ファクターを再現していきます。
今回はコードの紹介記事なので、さっそくダラダラと書いて行きます。かなり記事が長くなりそうなので、分割して投稿していきます。
コード
ライブラリー読み込みとデータ取得
これらの記事を参考に、R で J-Quant からデータを取得する関数を書いていきます。事前に J-Quant にユーザー登録して、Refresh
token を発行するようにしてください。僕は、authinfo.rというファイルに認証情報を以下の形で入れてあります。
jquant <- list(
mailaddress = "xxx@yyy.com",
password = "xxxyyyzzz"
)
slack_url <- "https://hooks.slack.com/services/xxxxxxxx"
J-Quants API の仕様書などはこちらで確認できます。
library(tidyverse)
library(here)
library(arrow)
library(fs)
library(httr)
library(jsonlite)
get_header <- function() {
r_post_refresh <- POST(
url = "https://api.jquants.com/v1/token/auth_user",
body = toJSON(jquant, auto_unbox = TRUE),
encode = "json"
)
REFRESH_TOKEN <- content(r_post_refresh, as = "parsed", type = "application/json")
r_post <- POST(
url = str_c("https://api.jquants.com/v1/token/auth_refresh?refreshtoken=", REFRESH_TOKEN)
)
# Parse the JSON response
idToken <- content(r_post, as = "parsed", type = "application/json")
headers <- add_headers(Authorization = paste("Bearer", idToken))$headers
return(headers)
}
fetch_data <- function(query, data_type, key, headers) {
api_url <- "https://api.jquants.com"
if (key == "") {
path <- c("v1", data_type)
} else {
path <- c("v1", data_type, key)
}
url <- modify_url(api_url, path = path, query = query)
response <- GET(url, add_headers(.headers = headers))
stop_for_status(response)
# Parse the JSON response
parsed <- content(response, as = "text", encoding = "UTF-8") |> fromJSON()
data <- parsed[[1]] |> tibble()
return(data)
}
get_simple_data <- function(query, data_type, key, headers) {
data <- fetch_data(query, data_type, key, headers)
if (nrow(data) > 0) {
data <- data |> mutate_all(~ str_replace_all(., "-", ""))
return(data)
}
}
get_fs_detail <- function(date, headers) {
data <- fetch_data(list(date = date), "fins", "fs_details", headers)
if (nrow(data) != 0) {
data <- data |>
unnest(FinancialStatement) |>
pivot_longer(cols = -c(DisclosedDate, DisclosedTime, LocalCode, DisclosureNumber, TypeOfDocument))
return(data)
}
}
post_to_slack <- function(text, slack_url) {
webhook_url <- slack_url
payload <- list(text = text)
POST(webhook_url, body = payload, encode = "json")
}
save_data <- function(dat, col_name) {
if (!dir_exists(here("data"))) {
dir_create(here("data"))
}
dat |>
select({
col_name
}) |>
unnest({
col_name
}) |>
write_parquet(here(str_c("data/dat_", col_name, ".parquet")))
}
main <- function(flag_output, start_date, end_date) {
start_time <- proc.time()
source(here("data/authinfo.r"))
start_date <- ymd(20241201)
end_date <- ymd(20251031)
headers <- get_header()
dat_calendar <- fetch_data(list(holidaydivision = 1), "markets", "trading_calendar", headers) |>
mutate(date_data = ymd(Date)) |>
filter(date_data |> between(ymd(start_date), ymd(end_date))) |>
select(-date_data)
dat_all <- dat_calendar |>
mutate(
price = map(Date, ~ get_simple_data(list(date = .x), "prices", "daily_quotes", headers)),
financials = map(Date, ~ get_simple_data(list(date = .x), "fins", "statements", headers)),
listedinfo = map(Date, ~ get_simple_data(list(date = .x), "listed", "info", headers))
)
if (flag_output) {
if (!dir_exists(here("data"))) {
dir_create(here("data"))
}
dat_all |>
select(price) |>
unnest(price) |>
write_parquet(here("data/dat_price.parquet"))
dat_all |>
select(financials) |>
unnest(financials) |>
write_parquet(here("data/dat_fin.parquet"))
dat_all |>
select(listedinfo) |>
unnest(listedinfo) |>
write_parquet(here("data/dat_info.parquet"))
dat_all |> saveRDS(here("data/dat_all.rds"))
}
elapsed_minutes <- ((proc.time() - start_time) / 60)[[3]]
message <- as.character(paste0(
"downloading jquants data has done! in ",
round(elapsed_minutes),
" minutes"
))
post_to_slack(message, slack_url)
}
get_premium_data <- function(flag_output) {
# for premium plan
start_time <- proc.time()
source(here("data/authinfo.r"))
headers <- get_header()
dat_calendar <- fetch_data(list(holidaydivision = 1), "markets", "trading_calendar", headers)
dat_all <- dat_calendar |>
mutate(
dividend = map(Date, ~ get_simple_data(list(date = .x), "fins", "dividend", headers)),
options_price = map(Date, ~ get_simple_data(list(date = .x), "derivatives", "options", headers)),
futures_price = map(Date, ~ get_simple_data(list(date = .x), "derivatives", "futures", headers)),
index_value = map(Date, ~ get_simple_data(list(date = .x), "indices", "", headers)),
fs_detail = map(Date, ~ get_fs_detail(.x, headers))
)
if (flag_output) {
dat_all |> save_data("dividend")
dat_all |> save_data("options_price")
dat_all |> save_data("futures_price")
dat_all |> save_data("fs_detail")
}
elapsed_minutes <- ((proc.time() - start_time) / 60)[[3]]
message <- as.character(paste0(
"downloading jquants data has done! in ",
round(elapsed_minutes),
" minutes"
))
post_to_slack(message, slack_url)
}
# main(TRUE, 20250101, 20251031)
なんか中途半端で恐縮ですが、初めての記事ですしむちゃくちゃ長くなってしまったのでいったんここまでにします。
Discussion