This report presents the daily stock price data of listed companies in the Taiwan stock market over the most recent two-year period. The data were obtained from the Taiwan Economic Journal (TEJ) database using the TSE/OTC Adjusted Price (Daily) dataset.
The objective of this assignment is to download, organize, inspect, validate, and summarize daily stock price data for Taiwan-listed companies using R and R Markdown. Beyond basic data cleaning, the report also computes daily returns and a simple volatility ranking to show that the dataset is ready for financial analysis.
The dataset was obtained from TEJ and covers common stocks listed on the Taiwan Stock Exchange (TWSE) and the GreTai/Taipei Exchange (TPEx). The observation period is approximately two years, from October 2024 to October 2026.
Important: the prices are adjusted prices (adjusted for corporate actions such as dividends and stock splits), which is why values carry more decimals than the quoted tick size. Adjusted prices are appropriate for return calculations but differ from the raw prices at which the shares actually traded.
tibble(
Variable = c("coid", "coid_name", "date", "open_ntd", "high_ntd",
"low_ntd", "close_ntd", "volume_1000s"),
Description = c("Company ID (stock code)", "Company name", "Trading date",
"Adjusted opening price", "Adjusted highest price",
"Adjusted lowest price", "Adjusted closing price",
"Trading volume"),
Unit = c("-", "-", "YYYY-MM-DD", "NTD", "NTD", "NTD", "NTD",
"Thousand shares")
) |>
kable(caption = "Table 1. Variables used in this report (after renaming)")
| Variable | Description | Unit |
|---|---|---|
| coid | Company ID (stock code) | - |
| coid_name | Company name | - |
| date | Trading date | YYYY-MM-DD |
| open_ntd | Adjusted opening price | NTD |
| high_ntd | Adjusted highest price | NTD |
| low_ntd | Adjusted lowest price | NTD |
| close_ntd | Adjusted closing price | NTD |
| volume_1000s | Trading volume | Thousand shares |
The combined TEJ dataset is imported from a CSV file. Column names
are cleaned (e.g., Open(NTD) becomes open_ntd)
so they can be used without backticks, and the date column is converted
to R’s Date format.
stock_data <- read_csv(
"Taiwan_Stocks_2024_2026_FINAL.csv",
show_col_types = FALSE
) |>
clean_names() |>
mutate(
date = ymd(date), # handles "2024/10/07" style dates
coid = as.character(coid)
)
# Make sure the date conversion did not silently fail
stopifnot(sum(is.na(stock_data$date)) == 0)
# Key numbers computed once and reused in the text below
n_obs <- nrow(stock_data)
n_vars <- ncol(stock_data)
n_companies <- n_distinct(stock_data$coid)
n_days <- n_distinct(stock_data$date)
start_date <- min(stock_data$date)
end_date <- max(stock_data$date)
daily_company_count <- stock_data |>
count(date, name = "n_companies")
fmt <- function(x) format(x, big.mark = ",")
The dataset has 957,924 company-day observations and 8 variables, covering 1,987 unique company IDs and 485 trading days from October 07, 2024 to October 05, 2026.
Although the requested period begins around October 5, 2024, stock markets only operate on trading days, so the first available observation is October 07, 2024.
glimpse(stock_data)
## Rows: 957,924
## Columns: 8
## $ coid <chr> "1101", "1101", "1101", "1101", "1101", "1101", "1101", "…
## $ coid_name <chr> "TCC", "TCC", "TCC", "TCC", "TCC", "TCC", "TCC", "TCC", "…
## $ date <date> 2024-10-07, 2024-10-08, 2024-10-09, 2024-10-11, 2024-10-…
## $ open_ntd <dbl> 31.4261, 30.9148, 30.5893, 30.4499, 30.2639, 30.3104, 30.…
## $ high_ntd <dbl> 31.4726, 31.1007, 30.6358, 30.6823, 30.4034, 30.4034, 30.…
## $ low_ntd <dbl> 30.9612, 30.5893, 30.1709, 30.1245, 30.1709, 30.1709, 29.…
## $ close_ntd <dbl> 31.1472, 30.7288, 30.1709, 30.2174, 30.2639, 30.2174, 29.…
## $ volume_1000s <dbl> 13191, 14236, 11396, 9445, 6059, 8102, 10435, 7485, 10399…
stock_data |>
head(10) |>
kable(digits = 2, caption = "Table 2. First 10 observations")
| coid | coid_name | date | open_ntd | high_ntd | low_ntd | close_ntd | volume_1000s |
|---|---|---|---|---|---|---|---|
| 1101 | TCC | 2024-10-07 | 31.43 | 31.47 | 30.96 | 31.15 | 13191 |
| 1101 | TCC | 2024-10-08 | 30.91 | 31.10 | 30.59 | 30.73 | 14236 |
| 1101 | TCC | 2024-10-09 | 30.59 | 30.64 | 30.17 | 30.17 | 11396 |
| 1101 | TCC | 2024-10-11 | 30.45 | 30.68 | 30.12 | 30.22 | 9445 |
| 1101 | TCC | 2024-10-14 | 30.26 | 30.40 | 30.17 | 30.26 | 6059 |
| 1101 | TCC | 2024-10-15 | 30.31 | 30.40 | 30.17 | 30.22 | 8102 |
| 1101 | TCC | 2024-10-16 | 30.22 | 30.22 | 29.89 | 29.89 | 10435 |
| 1101 | TCC | 2024-10-17 | 29.94 | 30.31 | 29.94 | 30.22 | 7485 |
| 1101 | TCC | 2024-10-18 | 30.31 | 30.45 | 29.94 | 30.45 | 10399 |
| 1101 | TCC | 2024-10-21 | 30.59 | 30.59 | 30.03 | 30.03 | 9212 |
stock_data |>
summarise(across(everything(), ~ sum(is.na(.x)))) |>
pivot_longer(everything(), names_to = "Variable", values_to = "Missing") |>
kable(caption = "Table 3. Number of missing values per variable")
| Variable | Missing |
|---|---|
| coid | 0 |
| coid_name | 0 |
| date | 0 |
| open_ntd | 0 |
| high_ntd | 0 |
| low_ntd | 0 |
| close_ntd | 0 |
| volume_1000s | 0 |
Each company should have at most one observation per trading date.
duplicate_check <- stock_data |>
count(coid, date) |>
filter(n > 1)
nrow(duplicate_check)
## [1] 0
A result of 0 means there are no duplicated company-date observations.
Missing values and duplicates are not the only possible problems. The checks below look for logically impossible prices (e.g., a high price below the low price), non-positive prices, and days with zero volume (which may indicate suspended trading).
consistency_check <- stock_data |>
summarise(
`High < Low` = sum(high_ntd < low_ntd),
`Close outside High-Low range` = sum(close_ntd > high_ntd | close_ntd < low_ntd),
`Open outside High-Low range` = sum(open_ntd > high_ntd | open_ntd < low_ntd),
`Non-positive price` = sum(pmin(open_ntd, high_ntd, low_ntd, close_ntd) <= 0),
`Zero volume` = sum(volume_1000s == 0)
) |>
pivot_longer(everything(), names_to = "Check", values_to = "Observations")
kable(consistency_check,
format.args = list(big.mark = ","),
caption = "Table 4. Observations failing each consistency check")
| Check | Observations |
|---|---|
| High < Low | 0 |
| Close outside High-Low range | 0 |
| Open outside High-Low range | 5,448 |
| Non-positive price | 0 |
| Zero volume | 10,442 |
Any non-zero count above deserves a closer look before using the data for modeling. Zero-volume days, in particular, can be legitimate (trading halts) and are usually handled explicitly rather than deleted.
The number of companies with observations is not constant: it differs from the total of 1,987 unique IDs because companies list (IPO) and delist during the sample period.
daily_company_count |>
summarise(
Minimum = min(n_companies),
Median = median(n_companies),
Maximum = max(n_companies)
) |>
kable(caption = "Table 5. Companies with data per trading day")
| Minimum | Median | Maximum |
|---|---|---|
| 1935 | 1980 | 1987 |
ggplot(daily_company_count, aes(x = date, y = n_companies)) +
geom_line(color = "#2C3E50") +
scale_y_continuous(limits = c(0, NA), labels = label_comma()) +
labs(
title = "Number of Taiwan Listed Companies with Daily Price Data",
x = "Trading date",
y = "Number of companies"
) +
theme_minimal()
Number of Taiwan-listed companies with daily price data. The y-axis starts at zero so that small changes are not visually exaggerated.
Companies that do not appear on every trading day are newly listed, delisted, or suspended during the period. The distribution below shows how many trading days each company has.
stock_data |>
count(coid, name = "n_days_company") |>
ggplot(aes(x = n_days_company)) +
geom_histogram(bins = 40, fill = "#2C3E50") +
labs(
title = "Trading Days Available per Company",
x = "Number of trading days",
y = "Number of companies"
) +
theme_minimal()
Distribution of the number of trading days per company.
Pooled means of price across all companies are not very informative, because the price level differs enormously between stocks (e.g., TSMC trades near NT$1,000 while many stocks trade below NT$20) and trading volume is highly right-skewed. The table below therefore reports the median, quartiles, standard deviation, and extremes of closing price and volume instead.
stock_data |>
select(close_ntd, volume_1000s) |>
pivot_longer(everything(), names_to = "Variable", values_to = "value") |>
group_by(Variable) |>
summarise(
Median = median(value),
Q1 = quantile(value, 0.25),
Q3 = quantile(value, 0.75),
SD = sd(value),
Min = min(value),
Max = max(value),
.groups = "drop"
) |>
kable(digits = 1, format.args = list(big.mark = ","),
caption = "Table 6. Summary statistics of closing price (NTD) and volume (thousand shares)")
| Variable | Median | Q1 | Q3 | SD | Min | Max |
|---|---|---|---|---|---|---|
| close_ntd | 41.7 | 22.8 | 84.9 | 374.1 | 1.1 | 20,905 |
| volume_1000s | 251.0 | 54.0 | 1,191.0 | 15,946.9 | 0.0 | 1,690,701 |
stock_data |>
group_by(coid, coid_name) |>
summarise(
`Total volume (million shares)` = sum(volume_1000s) / 1000,
`Median close (NTD)` = median(close_ntd),
.groups = "drop"
) |>
arrange(desc(`Total volume (million shares)`)) |>
slice_head(n = 10) |>
rename(`Company ID` = coid, `Company` = coid_name) |>
kable(digits = 1, format.args = list(big.mark = ","),
caption = "Table 7. Top 10 stocks by total trading volume")
| Company ID | Company | Total volume (million shares) | Median close (NTD) |
|---|---|---|---|
| 3481 | INX | 102,008.0 | 14.0 |
| 2409 | AUO | 74,199.4 | 14.4 |
| 2344 | WEC | 68,875.0 | 36.3 |
| 6770 | PSMC | 63,193.2 | 27.2 |
| 2303 | UMC | 51,851.9 | 44.6 |
| 2408 | NTC | 43,422.8 | 80.7 |
| 2887 | TS Holdings | 36,571.4 | 17.5 |
| 2337 | Macronix | 35,383.2 | 26.4 |
| 1815 | FFG | 31,818.9 | 63.6 |
| 1303 | NPC | 30,518.6 | 41.9 |
Since the prices are adjusted, daily log returns can be computed directly from closing prices. Returns are calculated within each company so that the first day of one stock is never compared with the last day of another.
stock_returns <- stock_data |>
arrange(coid, date) |>
group_by(coid) |>
mutate(log_return = log(close_ntd / lag(close_ntd))) |>
ungroup() |>
filter(!is.na(log_return), is.finite(log_return))
stock_returns |>
filter(between(log_return, -0.15, 0.15)) |>
ggplot(aes(x = log_return)) +
geom_histogram(bins = 120, fill = "#2C3E50") +
scale_x_continuous(labels = label_percent()) +
labs(
title = "Distribution of Daily Log Returns",
x = "Daily log return",
y = "Number of observations"
) +
theme_minimal()
Distribution of daily log returns across all stocks (clipped to +/-15% for readability).
The distribution is centered near zero with heavy tails, as is typical for equity returns. The table below ranks the most volatile stocks by annualized volatility, using only companies with at least one year (250 days) of return data so the estimate is stable.
stock_returns |>
group_by(coid, coid_name) |>
summarise(
n_days = n(),
`Annualized volatility` = sd(log_return) * sqrt(250),
.groups = "drop"
) |>
filter(n_days >= 250) |>
arrange(desc(`Annualized volatility`)) |>
slice_head(n = 10) |>
mutate(`Annualized volatility` = percent(`Annualized volatility`, accuracy = 0.1)) |>
rename(`Company ID` = coid, Company = coid_name, `Return days` = n_days) |>
kable(caption = "Table 8. Top 10 most volatile stocks (annualized, at least 250 return days)")
| Company ID | Company | Return days | Annualized volatility |
|---|---|---|---|
| 7772 | MOE | 311 | 100.4% |
| 7825 | NSTC | 361 | 90.6% |
| 4991 | GCS | 484 | 84.4% |
| 8291 | SMT | 484 | 84.2% |
| 7822 | V5 Technologies | 372 | 82.6% |
| 7856 | HTSI | 252 | 81.7% |
| 4971 | IET | 484 | 81.6% |
| 7828 | Innostar | 345 | 81.4% |
| 3081 | LandMark | 484 | 81.1% |
| 5475 | Glotech | 484 | 79.4% |
As an example, the figure below shows the adjusted closing price and trading volume of TSMC (company code 2330).
stock_data |>
filter(coid == "2330") |>
head(10) |>
kable(digits = 1, caption = "Table 9. First 10 observations for company 2330 (TSMC)")
| coid | coid_name | date | open_ntd | high_ntd | low_ntd | close_ntd | volume_1000s |
|---|---|---|---|---|---|---|---|
| 2330 | TSMC | 2024-10-07 | 964.7 | 981.2 | 960.8 | 976.3 | 43850 |
| 2330 | TSMC | 2024-10-08 | 971.5 | 981.2 | 968.6 | 981.2 | 35344 |
| 2330 | TSMC | 2024-10-09 | 1000.6 | 1005.5 | 990.9 | 990.9 | 53208 |
| 2330 | TSMC | 2024-10-11 | 995.8 | 1020.0 | 990.9 | 1015.2 | 47776 |
| 2330 | TSMC | 2024-10-14 | 1015.2 | 1024.9 | 1005.5 | 1015.2 | 39906 |
| 2330 | TSMC | 2024-10-15 | 1020.0 | 1044.3 | 1020.0 | 1039.5 | 52066 |
| 2330 | TSMC | 2024-10-16 | 1010.3 | 1039.5 | 1005.5 | 1015.2 | 60312 |
| 2330 | TSMC | 2024-10-17 | 1020.0 | 1024.9 | 1000.6 | 1005.5 | 56618 |
| 2330 | TSMC | 2024-10-18 | 1063.8 | 1068.6 | 1044.3 | 1054.0 | 91036 |
| 2330 | TSMC | 2024-10-21 | 1058.9 | 1063.8 | 1049.2 | 1054.0 | 42094 |
stock_data |>
filter(coid == "2330") |>
ggplot(aes(x = date, y = close_ntd)) +
geom_line(color = "#C0392B") +
labs(title = "TSMC (2330) Adjusted Closing Price", x = NULL, y = "NTD") +
theme_minimal()
TSMC (2330) adjusted closing price.
stock_data |>
filter(coid == "2330") |>
ggplot(aes(x = date, y = volume_1000s)) +
geom_col(fill = "#7F8C8D") +
scale_y_continuous(labels = label_comma()) +
labs(title = "TSMC (2330) Daily Trading Volume", x = NULL,
y = "Volume (thousand shares)") +
theme_minimal()
TSMC (2330) daily trading volume.
This report imported, organized, and validated daily stock price data for Taiwan-listed companies. The final dataset contains 957,924 observations, 1,987 unique company IDs, and 485 trading days from October 07, 2024 to October 05, 2026. It includes daily adjusted open, high, low, and close prices together with trading volume.
Validation showed no missing values and no duplicated company-date observations, and the consistency checks (Table 4) flagged the observations that should be reviewed before modeling. The number of companies per day changes over time because of listings and delistings, so analyses that need a balanced panel should restrict the sample to companies with complete coverage.
Limitations.
The data are ready for further financial and time-series analysis, such as return modeling, volatility estimation, and cross-sectional comparisons between stocks.
sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: Asia/Jakarta
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] scales_1.4.0 knitr_1.52 janitor_2.2.1 lubridate_1.9.5
## [5] forcats_1.0.1 stringr_1.6.0 dplyr_1.2.1 purrr_1.2.2
## [9] readr_2.2.0 tidyr_1.3.2 tibble_3.3.1 ggplot2_4.0.3
## [13] tidyverse_2.0.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 snakecase_0.11.1
## [9] jquerylib_0.1.4 yaml_2.3.12 fastmap_1.2.0 R6_2.6.1
## [13] labeling_0.4.3 generics_0.1.4 bslib_0.12.0 pillar_1.11.1
## [17] RColorBrewer_1.1-3 tzdb_0.5.0 rlang_1.3.0 stringi_1.8.9
## [21] cachem_1.1.0 xfun_0.60 sass_0.4.10 S7_0.2.2
## [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