0. Packages and file names

Put the two TEJ files in the same folder as this Rmd (or write the full path).

# install.packages(c("dplyr", "tidyr", "knitr", "rmarkdown"))
library(dplyr)
library(tidyr)
library(knitr)

stock_file  <- "20261112020248.csv"
factor_file <- "TEJ20261112021223.zip"

1. Read the TEJ files

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

2. Task 1 and 2: 50 largest stocks each month and their monthly returns

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")))

3. Task 3: Fama-French 3 factor series from TEJ

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

4. First window: 2018/01 - 2020/12 (36 months)

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)

4.1 36-month stock returns (%)

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)

4.2 Fama-French 3 factor returns (%)

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

4.3 Regression results

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

4.4 Average of the 50-stock results

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")
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

5. Notes