期末報告子組題(E)

研究動機

  • 希望藉此觀察台積電在不同市場環境下的行為模式變化,為投資者提供更即時的風險關聯性洞察,輔助其進行風險調整後的投資決策。
  • 透過計算2330,也就是台積電相對於台灣加權股價指數 (^TWII) 的移動 Beta 值,來動態評估其市場系統性風險。

方法

  • 資料獲取:
    • 使用 quantmod 套件從Yahoo Finance下載台積電(2330.TW)及台灣加權股價指數 (^TWII)近五年的每日調整後收盤價資料。
    • 為確保資料完整性與時序一致性,僅保留兩者共同的交易日。
  • 報酬率計算:
    • 將調整後收盤價轉換為每日對數報酬率,以確保報酬率的可加性,符合金融時間序列分析的標準做法。
  • 移動 Beta 值計算:
    • 設定 252 個交易日(約一年)作為移動視窗。
    • 利用 for 迴圈 逐日滾動此視窗。
    • 在每個視窗內,計算台積電報酬率與加權股價指數報酬率之間的協方差 (Covariance),以及加權股價指數報酬率的方差 (Variance)。
    • 依據 CAPM 定義,將 Beta 值計算為 Beta=Variance(加權指數報酬率)將計算出的移動 Beta 值依時間序列儲存為 xts 物件。
    • 視覺化呈現:運用 dygraphs套件,將計算所得的移動Beta值繪製成互動式走勢圖,提供縮放、平移、時間範圍選擇等功能,方便觀察 Beta 值的動態變化與趨勢。

程式說明

  • 安裝或載入必要的套件
if (!requireNamespace("quantmod", quietly = TRUE)) install.packages("quantmod")
if (!requireNamespace("xts", quietly = TRUE)) install.packages("xts")
if (!requireNamespace("dygraphs", quietly = TRUE)) install.packages("dygraphs")
if (!requireNamespace("lubridate", quietly = TRUE)) install.packages("lubridate")
library(quantmod) # 用於下載股價資料
library(xts)      # 用於處理時間序列資料
library(dygraphs) # 用於繪製互動式圖表
library(lubridate)#  用於日期時間處理函數
  • 取得歷史股價資料
# 設定股票代號和時間範圍
tsmc_ticker <- "2330.TW"
taiex_ticker <- "^TWII" # 加權指數代號包含特殊符號 ^

# 設定資料起始和結束日期 (過去五年)
end_date <- Sys.Date()
start_date <- as.Date(end_date - years(5)) # 這裡需要 lubridate 套件

cat(paste0("正在下載 ", format(start_date, "%Y-%m-%d"), " 到 ", format(end_date, "%Y-%m-%d"), " 的資料...\n"))

tryCatch({
  # *** 修正點 1: auto.assign = FALSE,並手動賦值給有效變數名 ***
  # 下載台積電資料,並將其結果直接賦值給 tsmc_data
  tsmc_data <- getSymbols(tsmc_ticker, src = "yahoo", from = start_date, to = end_date, auto.assign = FALSE)
  # 下載加權指數資料,並將其結果直接賦值給 taiex_data
  taiex_data <- getSymbols(taiex_ticker, src = "yahoo", from = start_date, to = end_date, auto.assign = FALSE)
  
}, error = function(e) {
  message(paste0("資料下載錯誤: ", e$message))
  message("請檢查您的網路連線或股票代號是否正確。程式終止。")
  q(save = "no") # 終止 R 會話
})

# 檢查下載的資料是否為空
if (NROW(tsmc_data) == 0 || NROW(taiex_data) == 0) {
  message("下載的資料為空。程式終止。")
  q(save = "no")
}

# 使用調整後的收盤價 (Adj Close)
# *** 修正點 2: 因為已經直接賦值,所以不需要 get() 了 ***
tsmc_prices <- Ad(tsmc_data)
taiex_prices <- Ad(taiex_data)

# 合併股價資料,並移除任何包含 NA 的列
combined_prices <- merge(tsmc_prices, taiex_prices, all = FALSE)
colnames(combined_prices) <- c("TSMC", "TAIEX")

cat("\n合併後的股價資料範例 (前幾筆):\n")
print(head(combined_prices))
  • 計算日對數報酬率
# 使用 log return: log(今日收盤價 / 昨日收盤價)
tsmc_returns <- diff(log(combined_prices$TSMC))
taiex_returns <- diff(log(combined_prices$TAIEX))

# 合併報酬率資料,並移除任何包含 NA 的列 (第一筆會是 NA)
returns_df <- merge(tsmc_returns, taiex_returns, all = FALSE)
colnames(returns_df) <- c("TSMC_Returns", "TAIEX_Returns")
returns_df <- na.omit(returns_df) # 移除第一筆的 NA

cat("\n日報酬率資料範例 (前幾筆):\n")
print(head(returns_df))
  • 定義移動視窗
# 您可以調整這個視窗大小。常見的選擇有 20 (月)、60 (季)、252 (年)
rolling_window <- 252 # 大約一年的交易日
  • 利用 array + for 迴圈計算移動 Beta
# Beta = 協方差(資產報酬率, 市場報酬率) / 方差(市場報酬率)

# 初始化一個空的數組來儲存移動 Beta 值
# 預先分配空間可以提高效率
rolling_betas_array <- array(NA, dim = NROW(returns_df) - rolling_window + 1)
dates_for_beta <- array(NA, dim = NROW(returns_df) - rolling_window + 1)

# 檢查是否有足夠的資料來計算 Beta
if (NROW(returns_df) < rolling_window) {
  message(paste0("資料不足以計算滾動 Beta (視窗大小為 ", rolling_window, ")。"))
  message(paste0("目前可用的資料點數: ", NROW(returns_df)))
  message("請減少滾動視窗大小或獲取更多歷史資料。程式終止。")
  q(save = "no")
}

# 迴圈計算 Beta
for (i in 1:(NROW(returns_df) - rolling_window + 1)) {
  # 取得當前視窗內的報酬率資料
  window_returns <- returns_df[i:(i + rolling_window - 1), ]
  
  # 將 xts 物件轉換為數值向量,以便進行計算
  tsmc_window_returns <- as.numeric(window_returns$TSMC_Returns)
  taiex_window_returns <- as.numeric(window_returns$TAIEX_Returns)
  
  # 計算協方差和方差
  # R 的 cov() 函式已經考慮了自由度
  covariance_tsmc_taiex <- cov(tsmc_window_returns, taiex_window_returns)
  variance_taiex <- var(taiex_window_returns) # 預設 ddof=1
  
  # 避免除以零
  if (variance_taiex == 0) {
    beta <- NA
  } else {
    beta <- covariance_tsmc_taiex / variance_taiex
  }
  
  rolling_betas_array[i] <- beta
  # 與此 Beta 值相關聯的日期是該視窗的最後一天
  dates_for_beta[i] <- index(window_returns)[rolling_window]
}

# 將結果轉換為 xts 物件,方便 dygraphs 處理
rolling_beta_xts <- xts(rolling_betas_array, order.by = as.Date(dates_for_beta))
colnames(rolling_beta_xts) <- "Rolling Beta"

cat(paste0("\n已計算 ", NROW(rolling_beta_xts), " 個移動 Beta 值 (視窗大小 = ", rolling_window, " 天)。\n"))
cat("\n移動 Beta 值範例 (後幾筆):\n")
print(tail(rolling_beta_xts))
  • 繪製走勢圖 (使用 dygraphs)
cat("\n正在產生 dygraphs 圖表,請在 Viewer 或瀏覽器中查看。\n")

dygraph(rolling_beta_xts,
        main = paste0("台積電 (2330.TW) 相對於加權指數 (^TWII) 的移動 Beta 值 (視窗: ", rolling_window, " 天)")) %>%
  dySeries("Rolling Beta", label = "移動 Beta") %>%
  dyAxis("y", label = "Beta 值", # <-- Y 軸的標籤設定在這裡
         valueFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }"),
         axisLabelFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }")
  ) %>%
  dyOptions(fillGraph = FALSE, drawPoints = FALSE, colors = c("#007BFF"), strokeWidth = 1.5
            # 移除 'axes' 參數,因為它應該在 dyAxis() 中設定
            # axes = list(
            #   y = list(valueFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }"),
            #            axisLabelFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }"))
            # )
  ) %>%
  dyRangeSelector() %>% # 允許使用者選擇日期範圍
  dyCrosshair(direction = "vertical") %>% # 滑鼠懸停時顯示垂直線
  dyHighlight(highlightSeriesOpts = list(strokeWidth = 3, strokeBorderWidth = 1, highlightCircleSize = 5)) %>%
  dyLegend(show = "always", width = 200) # 永遠顯示圖例

程式執行結果

if (!requireNamespace("quantmod", quietly = TRUE)) install.packages("quantmod")
if (!requireNamespace("xts", quietly = TRUE)) install.packages("xts")
if (!requireNamespace("dygraphs", quietly = TRUE)) install.packages("dygraphs")
if (!requireNamespace("lubridate", quietly = TRUE)) install.packages("lubridate")
library(quantmod) # 用於下載股價資料
library(xts)      # 用於處理時間序列資料
library(dygraphs) # 用於繪製互動式圖表
library(lubridate)#  用於日期時間處理函數

# 1. 取得歷史股價資料
# 設定股票代號和時間範圍
tsmc_ticker <- "2330.TW"
taiex_ticker <- "^TWII" # 加權指數代號包含特殊符號 ^

# 設定資料起始和結束日期 (過去五年)
end_date <- Sys.Date()
start_date <- as.Date(end_date - years(5)) # 這裡需要 lubridate 套件

cat(paste0("正在下載 ", format(start_date, "%Y-%m-%d"), " 到 ", format(end_date, "%Y-%m-%d"), " 的資料...\n"))
## 正在下載 2020-06-05 到 2025-06-05 的資料...
tryCatch({
  # *** 修正點 1: auto.assign = FALSE,並手動賦值給有效變數名 ***
  # 下載台積電資料,並將其結果直接賦值給 tsmc_data
  tsmc_data <- getSymbols(tsmc_ticker, src = "yahoo", from = start_date, to = end_date, auto.assign = FALSE)
  # 下載加權指數資料,並將其結果直接賦值給 taiex_data
  taiex_data <- getSymbols(taiex_ticker, src = "yahoo", from = start_date, to = end_date, auto.assign = FALSE)
  
}, error = function(e) {
  message(paste0("資料下載錯誤: ", e$message))
  message("請檢查您的網路連線或股票代號是否正確。程式終止。")
  q(save = "no") # 終止 R 會話
})

# 檢查下載的資料是否為空
if (NROW(tsmc_data) == 0 || NROW(taiex_data) == 0) {
  message("下載的資料為空。程式終止。")
  q(save = "no")
}

# 使用調整後的收盤價 (Adj Close)
# *** 修正點 2: 因為已經直接賦值,所以不需要 get() 了 ***
tsmc_prices <- Ad(tsmc_data)
taiex_prices <- Ad(taiex_data)

# 合併股價資料,並移除任何包含 NA 的列
combined_prices <- merge(tsmc_prices, taiex_prices, all = FALSE)
colnames(combined_prices) <- c("TSMC", "TAIEX")

cat("\n合併後的股價資料範例 (前幾筆):\n")
## 
## 合併後的股價資料範例 (前幾筆):
print(head(combined_prices))
##                TSMC    TAIEX
## 2020-06-05 281.9039 11479.40
## 2020-06-08 287.7863 11610.32
## 2020-06-09 288.6913 11637.11
## 2020-06-10 291.8588 11720.16
## 2020-06-11 290.0488 11535.77
## 2020-06-12 285.9763 11429.94
# 2. 計算日對數報酬率
# 使用 log return: log(今日收盤價 / 昨日收盤價)
tsmc_returns <- diff(log(combined_prices$TSMC))
taiex_returns <- diff(log(combined_prices$TAIEX))

# 合併報酬率資料,並移除任何包含 NA 的列 (第一筆會是 NA)
returns_df <- merge(tsmc_returns, taiex_returns, all = FALSE)
colnames(returns_df) <- c("TSMC_Returns", "TAIEX_Returns")
returns_df <- na.omit(returns_df) # 移除第一筆的 NA

cat("\n日報酬率資料範例 (前幾筆):\n")
## 
## 日報酬率資料範例 (前幾筆):
print(head(returns_df))
##            TSMC_Returns TAIEX_Returns
## 2020-06-08  0.020652015   0.011340226
## 2020-06-09  0.003139862   0.002304775
## 2020-06-10  0.010912001   0.007111289
## 2020-06-11 -0.006220931  -0.015857847
## 2020-06-12 -0.014140067  -0.009216336
## 2020-06-15 -0.020783982  -0.010879732
# 3. 定義移動視窗
# 您可以調整這個視窗大小。常見的選擇有 20 (月)、60 (季)、252 (年)
rolling_window <- 252 # 大約一年的交易日

# 4. 利用 array + for 迴圈計算移動 Beta
# Beta = 協方差(資產報酬率, 市場報酬率) / 方差(市場報酬率)

# 初始化一個空的數組來儲存移動 Beta 值
# 預先分配空間可以提高效率
rolling_betas_array <- array(NA, dim = NROW(returns_df) - rolling_window + 1)
dates_for_beta <- array(NA, dim = NROW(returns_df) - rolling_window + 1)

# 檢查是否有足夠的資料來計算 Beta
if (NROW(returns_df) < rolling_window) {
  message(paste0("資料不足以計算滾動 Beta (視窗大小為 ", rolling_window, ")。"))
  message(paste0("目前可用的資料點數: ", NROW(returns_df)))
  message("請減少滾動視窗大小或獲取更多歷史資料。程式終止。")
  q(save = "no")
}

# 迴圈計算 Beta
for (i in 1:(NROW(returns_df) - rolling_window + 1)) {
  # 取得當前視窗內的報酬率資料
  window_returns <- returns_df[i:(i + rolling_window - 1), ]
  
  # 將 xts 物件轉換為數值向量,以便進行計算
  tsmc_window_returns <- as.numeric(window_returns$TSMC_Returns)
  taiex_window_returns <- as.numeric(window_returns$TAIEX_Returns)
  
  # 計算協方差和方差
  # R 的 cov() 函式已經考慮了自由度
  covariance_tsmc_taiex <- cov(tsmc_window_returns, taiex_window_returns)
  variance_taiex <- var(taiex_window_returns) # 預設 ddof=1
  
  # 避免除以零
  if (variance_taiex == 0) {
    beta <- NA
  } else {
    beta <- covariance_tsmc_taiex / variance_taiex
  }
  
  rolling_betas_array[i] <- beta
  # 與此 Beta 值相關聯的日期是該視窗的最後一天
  dates_for_beta[i] <- index(window_returns)[rolling_window]
}

# 將結果轉換為 xts 物件,方便 dygraphs 處理
rolling_beta_xts <- xts(rolling_betas_array, order.by = as.Date(dates_for_beta))
colnames(rolling_beta_xts) <- "Rolling Beta"

cat(paste0("\n已計算 ", NROW(rolling_beta_xts), " 個移動 Beta 值 (視窗大小 = ", rolling_window, " 天)。\n"))
## 
## 已計算 961 個移動 Beta 值 (視窗大小 = 252 天)。
cat("\n移動 Beta 值範例 (後幾筆):\n")
## 
## 移動 Beta 值範例 (後幾筆):
print(tail(rolling_beta_xts))
##            Rolling Beta
## 2025-05-27     1.287902
## 2025-05-28     1.286842
## 2025-05-29     1.286939
## 2025-06-02     1.286635
## 2025-06-03     1.287094
## 2025-06-04     1.290347
# 5. 繪製走勢圖 (使用 dygraphs)
cat("\n正在產生 dygraphs 圖表,請在 Viewer 或瀏覽器中查看。\n")
## 
## 正在產生 dygraphs 圖表,請在 Viewer 或瀏覽器中查看。
dygraph(rolling_beta_xts,
        main = paste0("台積電 (2330.TW) 相對於加權指數 (^TWII) 的移動 Beta 值 (視窗: ", rolling_window, " 天)")) %>%
  dySeries("Rolling Beta", label = "移動 Beta") %>%
  dyAxis("y", label = "Beta 值", # <-- Y 軸的標籤設定在這裡
         valueFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }"),
         axisLabelFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }")
  ) %>%
  dyOptions(fillGraph = FALSE, drawPoints = FALSE, colors = c("#007BFF"), strokeWidth = 1.5
            # 移除 'axes' 參數,因為它應該在 dyAxis() 中設定
            # axes = list(
            #   y = list(valueFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }"),
            #            axisLabelFormatter = htmlwidgets::JS("function(y) { return y.toFixed(2); }"))
            # )
  ) %>%
  dyRangeSelector() %>% # 允許使用者選擇日期範圍
  dyCrosshair(direction = "vertical") %>% # 滑鼠懸停時顯示垂直線
  dyHighlight(highlightSeriesOpts = list(strokeWidth = 3, strokeBorderWidth = 1, highlightCircleSize = 5)) %>%
  dyLegend(show = "always", width = 200) # 永遠顯示圖例

結論

  • Beta值
    • 呈現出明顯的波動性
  • dygraphs讓投資者可以直觀地看到台積電的 Beta 值在不同市場階段的表現
    • 若Beta值呈現上升趨勢,則台積電對市場整體波動的反應更為劇烈
    • 若Beta值下降,則其與市場的聯動性可能減弱。
  • 證實了採用移動Beta而非靜態Beta值對於風險評估更適合評估市場