Put the two TEJ files in the same folder as this Rmd (or write the full path).
stock_file: monthly return and market cap of all listed
stocks (2017/11 - 2025/12)factor_file: Fama-French 3 factor series from TEJ (the
.zip or the .csv inside it)# install.packages(c("dplyr", "tidyr", "knitr", "rmarkdown"))
library(dplyr)
library(tidyr)
library(knitr)
stock_file <- "20261112020248.csv"
factor_file <- "TEJ20261112021223.zip"
TEJ Pro exports tab-separated text in UTF-16, so I use a small function to read it.
read_tej <- function(path) {
# if it is a zip, unzip and use the csv inside
if (grepl("\\.zip$", path, ignore.case = TRUE)) {
exdir <- tempfile("tej_")
dir.create(exdir)
unzip(path, exdir = exdir)
path <- list.files(exdir, pattern = "\\.csv$", full.names = TRUE)[1]
}
con <- file(path, encoding = "UTF-16LE")
on.exit(close(con))
lines <- readLines(con, warn = FALSE)
out <- read.delim(text = lines, sep = "\t", quote = "", comment.char = "",
check.names = FALSE, stringsAsFactors = FALSE,
colClasses = "character",
na.strings = c("N.A.", "NA", ""))
names(out)[1] <- "CO_ID" # removes the hidden marker at the start of the file
out
}
stocks <- read_tej(stock_file) %>%
transmute(
code = sub(" .*", "", trimws(CO_ID)),
name = sub("^[^ ]+ +", "", trimws(CO_ID)),
month = as.Date(paste0(trimws(Date), "01"), format = "%Y%m%d"),
ret = as.numeric(ROI_Month), # monthly return, in %
mktcap = as.numeric(`Market Cap.`) # market cap, NTD million
) %>%
# keep normal stocks (4-digit codes); drops TDRs such as 910322
filter(grepl("^[1-9][0-9]{3}$", code)) %>%
arrange(month, code)
cat("Rows:", nrow(stocks), "| Stocks:", n_distinct(stocks$code),
"| Months:", n_distinct(stocks$month), "\n")
## Rows: 98095 | Stocks: 1091 | Months: 98
cat("Range:", format(min(stocks$month), "%Y/%m"), "to",
format(max(stocks$month), "%Y/%m"), "\n")
## Range: 2017/11 to 2025/12
head(stocks)
## code name month ret mktcap
## 1 1101 TCC 2017-11-01 0.1493 142470
## 2 1102 ACC 2017-11-01 3.5317 93616
## 3 1103 CHC 2017-11-01 3.7189 9723
## 4 1104 UCC 2017-11-01 -1.9271 14968
## 5 1108 Lucky Cement 2017-11-01 -2.1813 3448
## 6 1109 HSINGTA 2017-11-01 -6.2016 4355
Each month I rank all stocks by market cap and keep the top 50, with their return in that month.
top50 <- stocks %>%
filter(!is.na(mktcap), month >= as.Date("2017-11-01"),
month <= as.Date("2025-12-01")) %>%
group_by(month) %>%
arrange(desc(mktcap), .by_group = TRUE) %>%
mutate(rank = row_number()) %>%
filter(rank <= 50) %>%
ungroup() %>%
select(month, rank, code, name, mktcap, ret)
# check: every month should have exactly 50 stocks
table(table(top50$month))
##
## 50
## 98
write.csv(top50, "top50_by_month.csv", row.names = FALSE)
Example: the 50 largest stocks in 2020/12 (the list used in the next part).
top50 %>%
filter(month == as.Date("2020-12-01")) %>%
transmute(Rank = rank, Code = code, Name = name,
`Market cap (NTD mn)` = mktcap,
`Return 2020/12 (%)` = ret) %>%
kable(format.args = list(big.mark = ","))
| Rank | Code | Name | Market cap (NTD mn) | Return 2020/12 (%) |
|---|---|---|---|---|
| 1 | 2330 | TSMC | 13,743,101 | 10.8445 |
| 2 | 2317 | Hon Hai | 1,275,395 | 11.7862 |
| 3 | 2454 | MediaTek | 1,187,817 | 6.1082 |
| 4 | 6505 | FPCC | 950,691 | 10.2761 |
| 5 | 2412 | CHT | 845,562 | 0.4611 |
| 6 | 2308 | DELTA | 683,154 | 17.1493 |
| 7 | 1301 | FPC | 613,657 | 13.1457 |
| 8 | 2303 | UMC | 585,716 | 16.4196 |
| 9 | 1303 | NPC | 570,226 | 11.9937 |
| 10 | 2882 | CATHAY FHC | 556,399 | 4.5793 |
| 11 | 1326 | FCFC | 496,442 | 10.0000 |
| 12 | 2881 | Fubon Financial | 478,421 | 5.5305 |
| 13 | 3008 | Largan | 428,577 | -0.9301 |
| 14 | 2886 | Mega FHC | 405,275 | 4.7451 |
| 15 | 2002 | CSC | 389,438 | 8.5527 |
| 16 | 2891 | CTBC Holding | 384,091 | 2.8718 |
| 17 | 1216 | Uni-President | 383,536 | 3.8459 |
| 18 | 3711 | ASEH | 353,710 | 6.1357 |
| 19 | 2207 | Hotai Motor | 351,193 | 0.3121 |
| 20 | 3045 | TWM | 347,378 | 1.8538 |
| 21 | 2884 | E.S.F.H | 321,089 | 1.5904 |
| 22 | 2382 | QCI | 312,487 | 5.0651 |
| 23 | 2912 | PCSC | 277,059 | 2.8958 |
| 24 | 2892 | FFHC | 274,044 | 0.2349 |
| 25 | 5880 | TCFHC | 271,351 | 0.2462 |
| 26 | 2395 | Advantech | 270,289 | 14.0062 |
| 27 | 2408 | NTC | 268,633 | 19.8895 |
| 28 | 2327 | Yageo | 257,438 | 17.1944 |
| 29 | 1101 | TCC | 251,003 | 0.6994 |
| 30 | 2885 | Yuanta Group | 249,424 | 7.5916 |
| 31 | 2880 | HNFHC | 234,600 | 0.5508 |
| 32 | 5871 | Chailease | 232,063 | 7.6923 |
| 33 | 3034 | Novatek | 224,541 | 23.4114 |
| 34 | 6415 | Silergy | 223,891 | 8.5586 |
| 35 | 2603 | EMC | 203,130 | 64.7770 |
| 36 | 2379 | RT | 199,422 | 5.2560 |
| 37 | 4904 | Far EasTone | 199,420 | -1.2904 |
| 38 | 2801 | Chang Hwa Bank | 186,406 | 0.8424 |
| 39 | 2357 | Asustek | 186,061 | 0.6026 |
| 40 | 5876 | SCSB | 184,194 | 5.3845 |
| 41 | 4938 | Pegatron | 179,211 | 3.0628 |
| 42 | 2633 | THSRC | 178,417 | 0.7949 |
| 43 | 2345 | Accton | 176,788 | 32.2174 |
| 44 | 9910 | FT | 175,895 | 8.7192 |
| 45 | 1590 | Airtac | 170,122 | 8.4337 |
| 46 | 2474 | Catcher | 156,893 | 9.5747 |
| 47 | 6770 | PSMC | 155,260 | 92.3077 |
| 48 | 1402 | FENC | 154,966 | 5.6569 |
| 49 | 1102 | ACC | 145,215 | -0.2310 |
| 50 | 2887 | TS Holdings | 144,601 | -0.7489 |
Preview of the full 2017/11 - 2025/12 list (also saved as
top50_by_month.csv):
rmarkdown::paged_table(top50 %>% mutate(month = format(month, "%Y-%m")))
TEJ gives the market risk premium, the size factor (SMB), and the book-to-market factor (HML) in % per month. The risk-free rate is an annual %, so I divide it by 12 to get a monthly rate.
ff <- read_tej(factor_file) %>%
transmute(
month = as.Date(paste0(trimws(Date), "01"), format = "%Y%m%d"),
mkt = as.numeric(`Market Risk Premium`),
rf = as.numeric(`Risk-free Interest Rate`) / 12,
smb = as.numeric(`Size Factor (3 Factor)`),
hml = as.numeric(`Book to Market Factor`)
) %>%
arrange(month)
head(ff)
## month mkt rf smb hml
## 1 2017-11-01 -2.2257 0.08708333 2.1437 3.5405
## 2 2017-12-01 0.6676 0.08708333 1.0160 -0.4876
## 3 2018-01-01 4.2437 0.08708333 -2.3603 0.5520
## 4 2018-02-01 -2.6902 0.08708333 0.9889 -0.0220
## 5 2018-03-01 0.8247 0.08708333 2.1831 -0.5813
## 6 2018-04-01 -2.4217 0.08708333 -0.8964 4.3973
Stocks: the 50 largest in 2020/12. Window: 36 months of stock returns and the three factor returns.
formation <- as.Date("2020-12-01")
win_start <- as.Date("2018-01-01")
win_end <- as.Date("2020-12-01")
n_months <- length(seq(win_start, win_end, by = "month")) # 36
members <- top50 %>%
filter(month == formation) %>%
select(rank, code, name, mktcap)
win <- stocks %>%
filter(code %in% members$code, month >= win_start, month <= win_end)
Rows are months, columns are the stock codes ordered by market cap rank.
ret_wide <- win %>%
select(month, code, ret) %>%
pivot_wider(names_from = code, values_from = ret) %>%
arrange(month) %>%
select(month, any_of(members$code)) %>%
mutate(month = format(month, "%Y-%m"))
rmarkdown::paged_table(ret_wide)
write.csv(ret_wide, "window_returns_2018_2020.csv", row.names = FALSE)
ff_win <- ff %>%
filter(month >= win_start, month <= win_end) %>%
mutate(month = format(month, "%Y-%m"))
kable(ff_win, digits = 4,
col.names = c("Month", "Market premium", "Risk-free (monthly)", "SMB", "HML"))
| Month | Market premium | Risk-free (monthly) | SMB | HML |
|---|---|---|---|---|
| 2018-01 | 4.2437 | 0.0871 | -2.3603 | 0.5520 |
| 2018-02 | -2.6902 | 0.0871 | 0.9889 | -0.0220 |
| 2018-03 | 0.8247 | 0.0871 | 2.1831 | -0.5813 |
| 2018-04 | -2.4217 | 0.0871 | -0.8964 | 4.3973 |
| 2018-05 | 2.0594 | 0.0871 | 1.8348 | 0.7161 |
| 2018-06 | 0.5801 | 0.0871 | 2.0703 | -1.2201 |
| 2018-07 | 4.1486 | 0.0871 | -5.7652 | 0.3105 |
| 2018-08 | 0.7661 | 0.0871 | -2.2991 | 2.2655 |
| 2018-09 | -0.4589 | 0.0871 | -1.9985 | 1.1975 |
| 2018-10 | -11.0974 | 0.0871 | -1.6486 | -0.4774 |
| 2018-11 | 0.4839 | 0.0871 | 6.6957 | 1.3314 |
| 2018-12 | -1.6912 | 0.0871 | 1.3346 | -2.1142 |
| 2019-01 | 1.9631 | 0.0871 | 0.0341 | 0.0837 |
| 2019-02 | 4.4682 | 0.0871 | 0.3841 | -2.0120 |
| 2019-03 | 2.3039 | 0.0871 | 0.4998 | -1.4823 |
| 2019-04 | 2.9774 | 0.0871 | -2.0340 | 0.2454 |
| 2019-05 | -4.3307 | 0.0871 | 1.5714 | 2.2665 |
| 2019-06 | 3.1907 | 0.0871 | -1.3933 | -0.7903 |
| 2019-07 | 2.9402 | 0.0871 | 1.4020 | 0.9882 |
| 2019-08 | -1.2756 | 0.0871 | 2.2032 | -1.1659 |
| 2019-09 | 2.1216 | 0.0871 | -1.4567 | -0.9756 |
| 2019-10 | 4.7426 | 0.0871 | -2.2540 | -1.3045 |
| 2019-11 | 1.0799 | 0.0871 | -1.9401 | 3.4893 |
| 2019-12 | 4.4760 | 0.0871 | -0.8082 | -1.5641 |
| 2020-01 | -4.3368 | 0.0871 | -0.8064 | -0.2487 |
| 2020-02 | -1.8544 | 0.0871 | -0.0760 | -0.3514 |
| 2020-03 | -13.8962 | 0.0663 | -2.2179 | -2.3226 |
| 2020-04 | 13.1776 | 0.0663 | 5.9529 | 1.9246 |
| 2020-05 | -0.5231 | 0.0663 | 5.5189 | -0.3267 |
| 2020-06 | 6.7265 | 0.0663 | 0.3759 | 1.6339 |
| 2020-07 | 10.5466 | 0.0663 | -5.4945 | -6.6159 |
| 2020-08 | -0.0977 | 0.0663 | 5.5599 | 7.6317 |
| 2020-09 | -0.3940 | 0.0663 | -0.3002 | 0.6311 |
| 2020-10 | 0.2043 | 0.0663 | 0.4993 | 2.5335 |
| 2020-11 | 9.3275 | 0.0663 | -0.2675 | 0.9808 |
| 2020-12 | 7.4394 | 0.0663 | -3.1343 | 2.5330 |
For each stock I run the 3 factor regression with its excess return (stock return minus the monthly risk-free rate):
\[R_{i,t} - R_{f,t} = \alpha_i + \beta_i (R_{m,t}-R_{f,t}) + s_i\,SMB_t + h_i\,HML_t + \varepsilon_{i,t}\]
Only stocks with all 36 months of returns are used.
reg_data <- win %>%
inner_join(ff, by = "month") %>%
mutate(ex_ret = ret - rf)
# stocks without all 36 months (e.g. listed during the window)
obs <- reg_data %>% count(code, name = "n_obs")
excluded <- obs %>% filter(n_obs < n_months)
full <- obs %>% filter(n_obs == n_months)
fit_one <- function(d) {
m <- lm(ex_ret ~ mkt + smb + hml, data = d)
s <- summary(m)
data.frame(
code = d$code[1],
alpha = unname(coef(m)[1]),
t_alpha = unname(s$coefficients[1, 3]),
b_mkt = unname(coef(m)[2]),
b_smb = unname(coef(m)[3]),
b_hml = unname(coef(m)[4]),
r2 = s$r.squared
)
}
results <- reg_data %>%
filter(code %in% full$code) %>%
split(.$code) %>%
lapply(fit_one) %>%
bind_rows() %>%
right_join(members, by = "code") %>%
filter(!is.na(alpha)) %>%
arrange(rank) %>%
select(Rank = rank, Code = code, Name = name,
Alpha = alpha, `t(Alpha)` = t_alpha,
`Beta (MKT)` = b_mkt, `s (SMB)` = b_smb, `h (HML)` = b_hml,
`R-squared` = r2)
kable(results, digits = 3)
| Rank | Code | Name | Alpha | t(Alpha) | Beta (MKT) | s (SMB) | h (HML) | R-squared |
|---|---|---|---|---|---|---|---|---|
| 1 | 2330 | TSMC | 1.506 | 2.666 | 1.370 | -0.925 | -0.797 | 0.872 |
| 2 | 2317 | Hon Hai | -1.656 | -1.752 | 1.093 | -0.364 | 0.662 | 0.558 |
| 3 | 2454 | MediaTek | 2.314 | 1.602 | 1.127 | 0.907 | -1.485 | 0.427 |
| 4 | 6505 | FPCC | -1.195 | -1.285 | 0.734 | -0.460 | 1.124 | 0.438 |
| 5 | 2412 | CHT | 0.284 | 0.704 | 0.039 | 0.000 | 0.074 | 0.014 |
| 6 | 2308 | DELTA | 0.779 | 0.658 | 1.344 | -0.569 | -0.258 | 0.555 |
| 7 | 1301 | FPC | -0.992 | -1.480 | 0.954 | -0.018 | 0.812 | 0.667 |
| 8 | 2303 | UMC | 2.555 | 1.573 | 1.714 | -0.795 | -0.749 | 0.534 |
| 9 | 1303 | NPC | -1.128 | -1.920 | 0.965 | 0.197 | 0.643 | 0.723 |
| 10 | 2882 | CATHAY FHC | -1.242 | -2.477 | 0.664 | 0.220 | 0.362 | 0.629 |
| 11 | 1326 | FCFC | -1.411 | -2.079 | 0.890 | -0.143 | 0.972 | 0.645 |
| 12 | 2881 | Fubon Financial | -0.947 | -2.035 | 0.755 | 0.197 | 0.473 | 0.719 |
| 13 | 3008 | Largan | -1.120 | -0.702 | 0.946 | 0.080 | -0.453 | 0.247 |
| 14 | 2886 | Mega FHC | 0.451 | 0.830 | 0.455 | -0.192 | 0.119 | 0.395 |
| 15 | 2002 | CSC | -0.790 | -1.567 | 0.693 | -0.272 | 0.703 | 0.662 |
| 16 | 2891 | CTBC Holding | -0.781 | -1.449 | 0.767 | 0.094 | 0.456 | 0.656 |
| 17 | 1216 | Uni-President | -0.127 | -0.188 | 0.405 | 0.003 | 0.207 | 0.248 |
| 19 | 2207 | Hotai Motor | 0.803 | 0.476 | 1.275 | -0.009 | 0.106 | 0.335 |
| 20 | 3045 | TWM | -0.208 | -0.432 | 0.228 | 0.000 | 0.129 | 0.172 |
| 21 | 2884 | E.S.F.H | 0.674 | 0.865 | 0.746 | -0.054 | 0.312 | 0.453 |
| 22 | 2382 | QCI | 0.686 | 1.304 | 0.642 | 0.080 | -0.751 | 0.629 |
| 23 | 2912 | PCSC | -0.187 | -0.247 | 0.193 | -0.048 | 0.501 | 0.113 |
| 24 | 2892 | FFHC | -0.106 | -0.226 | 0.664 | 0.246 | 0.051 | 0.647 |
| 25 | 5880 | TCFHC | 0.349 | 0.926 | 0.572 | 0.160 | 0.119 | 0.676 |
| 26 | 2395 | Advantech | 0.891 | 1.034 | 0.948 | -0.036 | -0.078 | 0.518 |
| 27 | 2408 | NTC | -0.515 | -0.287 | 1.574 | 0.778 | 0.226 | 0.420 |
| 28 | 2327 | Yageo | 1.383 | 0.461 | 2.339 | 2.331 | -0.659 | 0.400 |
| 29 | 1101 | TCC | 0.656 | 0.917 | 0.610 | -0.103 | 0.494 | 0.414 |
| 30 | 2885 | Yuanta Group | 0.261 | 0.451 | 0.837 | -0.223 | 1.061 | 0.701 |
| 31 | 2880 | HNFHC | 0.055 | 0.107 | 0.587 | 0.014 | 0.277 | 0.548 |
| 32 | 5871 | Chailease | 0.554 | 0.629 | 1.428 | 0.410 | 1.035 | 0.724 |
| 33 | 3034 | Novatek | 3.049 | 2.497 | 1.270 | -0.716 | -1.372 | 0.587 |
| 34 | 6415 | Silergy | 3.141 | 1.479 | 1.138 | 2.272 | -0.542 | 0.340 |
| 35 | 2603 | EMC | 0.382 | 0.176 | 1.663 | -0.454 | 4.128 | 0.522 |
| 36 | 2379 | RT | 3.213 | 2.459 | 0.994 | -0.031 | -0.829 | 0.375 |
| 37 | 4904 | Far EasTone | -0.311 | -0.572 | 0.149 | 0.209 | -0.065 | 0.091 |
| 38 | 2801 | Chang Hwa Bank | -0.093 | -0.123 | 0.538 | -0.090 | 0.522 | 0.341 |
| 39 | 2357 | Asustek | -0.523 | -0.533 | 0.402 | -0.256 | 0.986 | 0.232 |
| 40 | 5876 | SCSB | 0.872 | 0.716 | 0.325 | 0.242 | 0.404 | 0.090 |
| 41 | 4938 | Pegatron | -0.698 | -0.749 | 0.802 | -0.589 | 0.598 | 0.437 |
| 42 | 2633 | THSRC | 0.440 | 0.371 | 0.498 | 0.331 | 0.469 | 0.175 |
| 43 | 2345 | Accton | 2.995 | 1.417 | 1.156 | 1.061 | -1.094 | 0.254 |
| 44 | 9910 | FT | 0.741 | 0.524 | 1.091 | 0.518 | 0.952 | 0.396 |
| 45 | 1590 | Airtac | 1.001 | 0.517 | 0.997 | 0.860 | -0.302 | 0.216 |
| 46 | 2474 | Catcher | -1.891 | -1.485 | 0.990 | -0.628 | 0.207 | 0.375 |
| 48 | 1402 | FENC | -0.725 | -0.861 | 0.905 | -0.361 | 1.147 | 0.564 |
| 49 | 1102 | ACC | 0.527 | 0.526 | 0.744 | -0.177 | 1.030 | 0.391 |
| 50 | 2887 | TS Holdings | -0.626 | -1.418 | 0.662 | 0.074 | 0.690 | 0.706 |
write.csv(results, "ff3_results_2018_2020.csv", row.names = FALSE)
Stocks excluded because they do not have 36 months of returns in the window:
members %>%
inner_join(excluded, by = "code") %>%
kable(col.names = c("Rank", "Code", "Name", "Market cap (NTD mn)", "Months available"),
format.args = list(big.mark = ","))
| Rank | Code | Name | Market cap (NTD mn) | Months available |
|---|---|---|---|---|
| 18 | 3711 | ASEH | 353,710 | 33 |
| 47 | 6770 | PSMC | 155,260 | 1 |
results %>%
summarise(across(c(Alpha, `Beta (MKT)`, `s (SMB)`, `h (HML)`, `R-squared`), mean)) %>%
kable(digits = 3, caption = "Cross-sectional average of the regression results")
| Alpha | Beta (MKT) | s (SMB) | h (HML) | R-squared |
|---|---|---|---|---|
| 0.277 | 0.873 | 0.079 | 0.263 | 0.455 |
formation,
win_start and win_end in section 4 (for
example, move all three dates by one month) and rerun the chunks.