Collect the daily prices of all listed companies in the Taiwan stock market for the last two years, check the data quality, summarize it, and publish this report to RPubs.
The data come from one file,
Taiwan_Stocks_2024_2026_FINAL.csv, which already combines
the TWSE and GreTai (TPEx) markets from 2024-10-07 to 2026-10-05.
csv_file <- file.path("data", "Taiwan_Stocks_2024_2026_FINAL.csv")
out_dir <- "output" # folder for the cleaned data set
if (!file.exists(csv_file)) {
stop("File not found: '", csv_file, "'. Put the CSV in a folder named 'data' ",
"next to this .Rmd file.")
}
dir.create(out_dir, showWarnings = FALSE)
Important details:
COID is read as text so stock codes
keep their exact form.Date is stored as text (2024/10/07), so it
is read as text first and converted to a real date afterwards.Open(NTD). The file has a UTF-8 BOM, which
read_csv() handles automatically.col_names <- c("coid", "name", "date", "open", "high", "low",
"close", "volume_1000s")
raw <- read_csv(
csv_file,
skip = 1,
col_names = col_names,
col_types = cols(
coid = col_character(), name = col_character(), date = col_character(),
open = col_double(), high = col_double(), low = col_double(),
close = col_double(), volume_1000s = col_double()
)
)
nrow(raw)
## [1] 957924
Steps:
date from text to Date.coid + date rows (a
safety check; the file should already be free of duplicates).91xxxx codes are
Taiwan Depositary Receipts, TDRs) and an approximate daily turnover in
NTD.n_raw <- nrow(raw)
n_dup <- n_raw - nrow(distinct(raw, coid, date))
stocks <- raw %>%
mutate(date = as.Date(date, format = "%Y/%m/%d"))
n_bad_date <- sum(is.na(stocks$date))
stocks <- stocks %>%
filter(!is.na(date)) %>%
distinct(coid, date, .keep_all = TRUE) %>%
filter(date > max(date) %m-% years(2)) %>%
mutate(
security_type = if_else(str_detect(coid, "^91\\d{4}$"), "TDR", "Stock/other"),
turnover_ntd = close * volume_1000s * 1000 # approximate NTD traded
) %>%
arrange(coid, date) %>%
select(coid, name, date, open, high, low, close, volume_1000s,
turnover_ntd, security_type)
tibble(
Check = c("Rows in CSV",
"Duplicate rows removed",
"Rows with unreadable dates",
"Rows in final data",
"Number of companies / securities",
"Number of trading days",
"First date",
"Last date",
"Rows with High < Low",
"Rows with missing prices",
"Rows with zero volume"),
Value = c(comma(n_raw),
comma(n_dup),
comma(n_bad_date),
comma(nrow(stocks)),
comma(n_distinct(stocks$coid)),
comma(n_distinct(stocks$date)),
as.character(min(stocks$date)),
as.character(max(stocks$date)),
comma(sum(stocks$high < stocks$low, na.rm = TRUE)),
comma(sum(!complete.cases(stocks[, c("open", "high", "low", "close")]))),
comma(sum(stocks$volume_1000s == 0, na.rm = TRUE)))
) %>%
kable(caption = "Summary of the final data set")
| Check | Value |
|---|---|
| Rows in CSV | 957,924 |
| Duplicate rows removed | 0 |
| Rows with unreadable dates | 0 |
| Rows in final data | 957,924 |
| Number of companies / securities | 1,987 |
| Number of trading days | 485 |
| First date | 2024-10-07 |
| Last date | 2026-10-05 |
| Rows with High < Low | 0 |
| Rows with missing prices | 0 |
| Rows with zero volume | 10,442 |
Preview of the data (TSMC, 2330, most recent 10 days):
stocks %>%
filter(coid == "2330") %>%
arrange(desc(date)) %>%
head(10) %>%
kable(digits = 2)
| coid | name | date | open | high | low | close | volume_1000s | turnover_ntd | security_type |
|---|---|---|---|---|---|---|---|---|---|
| 2330 | TSMC | 2026-10-05 | 2550 | 2580 | 2545 | 2575 | 26404 | 67990300000 | Stock/other |
| 2330 | TSMC | 2026-10-02 | 2505 | 2515 | 2495 | 2500 | 15792 | 39480000000 | Stock/other |
| 2330 | TSMC | 2026-10-01 | 2490 | 2510 | 2485 | 2510 | 20666 | 51871660000 | Stock/other |
| 2330 | TSMC | 2026-09-30 | 2495 | 2510 | 2480 | 2480 | 34282 | 85019360000 | Stock/other |
| 2330 | TSMC | 2026-09-29 | 2475 | 2495 | 2475 | 2475 | 26893 | 66560175000 | Stock/other |
| 2330 | TSMC | 2026-09-24 | 2480 | 2490 | 2470 | 2475 | 14557 | 36028575000 | Stock/other |
| 2330 | TSMC | 2026-09-23 | 2475 | 2505 | 2475 | 2500 | 22817 | 57042500000 | Stock/other |
| 2330 | TSMC | 2026-09-22 | 2505 | 2510 | 2460 | 2460 | 22009 | 54142140000 | Stock/other |
| 2330 | TSMC | 2026-09-21 | 2445 | 2485 | 2445 | 2480 | 16086 | 39893280000 | Stock/other |
| 2330 | TSMC | 2026-09-18 | 2460 | 2460 | 2435 | 2460 | 40892 | 100594320000 | Stock/other |
stocks %>%
filter(coid == "2330") %>%
ggplot(aes(date, close)) +
geom_line(linewidth = 0.6) +
scale_y_continuous(labels = comma) +
labs(title = "TSMC (2330) daily closing price",
x = NULL, y = "Close (NTD)") +
theme_minimal()
top10 <- stocks %>%
group_by(coid, name) %>%
summarise(total_turnover_bn = sum(turnover_ntd, na.rm = TRUE) / 1e9,
.groups = "drop") %>%
slice_max(total_turnover_bn, n = 10) %>%
mutate(label = paste(coid, name))
ggplot(top10, aes(reorder(label, total_turnover_bn), total_turnover_bn)) +
geom_col() +
coord_flip() +
scale_y_continuous(labels = comma) +
labs(title = "Top 10 by total traded value (last two years)",
x = NULL, y = "Total turnover (NTD billion, approximate)") +
theme_minimal()
stocks %>%
mutate(month = floor_date(date, "month")) %>%
group_by(month) %>%
summarise(securities = n_distinct(coid), .groups = "drop") %>%
ggplot(aes(month, securities)) +
geom_col() +
labs(title = "Distinct securities with a price record, by month",
x = NULL, y = "Securities") +
theme_minimal()
saveRDS(stocks, file.path(out_dir, "taiwan_stocks_daily_2yr.rds"))
write_csv(stocks, file.path(out_dir, "taiwan_stocks_daily_2yr.csv.gz"))
Saved to the output/ folder:
taiwan_stocks_daily_2yr.rds (open in R with
readRDS())taiwan_stocks_daily_2yr.csv.gz (compressed CSV; open in
R with read_csv())31.4261), so they will not match the
raw exchange quotes exactly. Use them for return and trend
analysis.0050 does not appear, so the data cover listed companies
plus a small number of TDRs (flagged in
security_type).sessionInfo()
## R version 4.6.1 (2026-06-24)
## Platform: aarch64-apple-darwin23
## Running under: macOS Sequoia 15.7.4
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.6/Resources/lib/libRlapack.dylib; LAPACK version 3.12.1
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: Asia/Taipei
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] knitr_1.52 scales_1.4.0 ggplot2_4.0.3 lubridate_1.9.5
## [5] stringr_1.6.0 dplyr_1.2.1 readr_2.2.0
##
## loaded via a namespace (and not attached):
## [1] bit_4.6.0 gtable_0.3.6 jsonlite_2.0.0 crayon_1.5.3
## [5] compiler_4.6.1 tidyselect_1.2.1 parallel_4.6.1 jquerylib_0.1.4
## [9] yaml_2.3.12 fastmap_1.2.0 R6_2.6.1 labeling_0.4.3
## [13] generics_0.1.4 tibble_3.3.1 RColorBrewer_1.1-3 bslib_0.12.0
## [17] pillar_1.11.1 tzdb_0.5.0 rlang_1.3.0 cachem_1.1.0
## [21] stringi_1.8.9 xfun_0.60 S7_0.2.2 sass_0.4.10
## [25] bit64_4.8.6 otel_0.2.0 timechange_0.4.0 cli_3.6.6
## [29] withr_3.0.3 magrittr_2.0.5 digest_0.6.39 grid_4.6.1
## [33] vroom_1.7.1 rstudioapi_0.19.0 hms_1.1.4 lifecycle_1.0.5
## [37] vctrs_0.7.3 evaluate_1.0.5 glue_1.8.1 farver_2.1.2
## [41] rmarkdown_2.32 tools_4.6.1 pkgconfig_2.0.3 htmltools_0.5.9