1 Introduction

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.

2 Data Source

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

3 Import and Prepare Data

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.

3.1 Structure

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…

3.2 Preview

stock_data |>
  head(10) |>
  kable(digits = 2, caption = "Table 2. First 10 observations")
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

4 Data Quality Checks

4.1 Missing values

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

4.2 Duplicates

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.

4.3 Price and volume consistency

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

5 Coverage: How Many Companies per Day?

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

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.

Distribution of the number of trading days per company.

6 Descriptive Statistics

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

6.1 Most actively traded stocks

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

7 Daily Returns and Volatility

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

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

8 Sample Company: TSMC (2330)

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

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.

TSMC (2330) daily trading volume.

9 Conclusion

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.

  • Prices are adjusted, so they are suitable for returns but not for the actual traded price levels.
  • The sample may suffer from survivorship bias if delisted companies are not fully included.
  • The most recent trading day may be incomplete, depending on when the data were downloaded.
  • The raw CSV is not published with this report, so results depend on the TEJ download.

The data are ready for further financial and time-series analysis, such as return modeling, volatility estimation, and cross-sectional comparisons between stocks.

10 Reproducibility

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