想自己重算,可以從頁首下載 R Markdown 與執行結果。所有實證例都使用隨書提供的固定資料; 檔案來源、變數單位與已知限制請見資料來源與重現說明

《財務時間序列分析》線上附錄

R05:AAPL 的 ARMA 估計、診斷與預測

本附錄對應第 6–7 章,要完整走過一次實際的預測流程:只看 AAPL 的早期日對數報酬時,應如何選擇低階 ARMA 模型、檢查根與殘差,並在模型選定後評估未見期間?固定資料源自原課程 S&P 500 價格檔;每一列代表一個交易日,有效報酬樣本為 2019-01-03 至 2022-06-22,共 874 筆,單位是小數日對數報酬。資料建置方式見 data/DATA_SOURCES.md

第 $t$ 日對數報酬要等當日調整收盤價可得後才能完整計算,所以本頁假設在訓練期最後一日收盤後形成預測。ARMA 模型描述的是 AAPL 報酬的條件平均動態;係數、診斷與預測誤差都沒有識別市場事件的因果效果,也不構成投資建議。

knitr::opts_chunk$set(
  echo = TRUE, message = FALSE, warning = FALSE,
  fig.width = 8, fig.height = 4.8,
  dev = "ragg_png", dpi = 144,
  dev.args = list(background = "white")
)

root_candidates <- c(".", "..")
is_root <- vapply(root_candidates, function(x) {
  file.exists(file.path(x, "main.tex"))
}, logical(1))
stopifnot(any(is_root))
project_root <- root_candidates[which(is_root)[1]]
project_path <- function(...) file.path(project_root, ...)

stopifnot(
  requireNamespace("ragg", quietly = TRUE),
  requireNamespace("systemfonts", quietly = TRUE)
)
cwtex_file <- project_path("assets", "fonts", "cwTeXQKai-Medium.ttf")
stopifnot(file.exists(cwtex_file))
if (!"cwTeX Online" %in% systemfonts::registry_fonts()$family) {
  systemfonts::register_font("cwTeX Online", cwtex_file)
}
plot_family <- "cwTeX Online"

固定資料與時間切分#

前 80% 報酬是訓練期,負責比較候選模型、計算 BIC 與估計所有參數;最後 20% 是一次性測試期,只負責回答選定模型在未見資料上的預測誤差。本例沒有另外設驗證期,因為調校工作都限於訓練期內的預先指定候選集合。測試期一旦被拿來改階數或改規格,就不再是未見測試資料。

aapl <- read.csv(project_path(
  "data", "processed", "aapl_adjusted_daily_2019_2022.csv"
))
aapl$date <- as.Date(aapl$date)
# 報酬的落後關係依日期而定,先排序再切分,不能隨機打散。
aapl <- aapl[order(aapl$date), ]
aapl <- aapl[is.finite(aapl$log_return), ]
row.names(aapl) <- NULL

stopifnot(
  !anyNA(aapl$date), !anyNA(aapl$log_return),
  all(diff(aapl$date) > 0)
)

y <- aapl$log_return
dates <- aapl$date
n <- length(y)
train_end <- floor(0.80 * n)
# 切點只由樣本長度決定,不參考後面的預測分數。
y_train <- y[seq_len(train_end)]
y_test <- y[(train_end + 1L):n]

split_table <- data.frame(
  區段 = c("訓練期", "測試期"),
  起日 = dates[c(1L, train_end + 1L)],
  迄日 = dates[c(train_end, n)],
  觀察值 = c(train_end, n - train_end),
  資料來源 = "原課程 S&P 500 價格檔的 AAPL 固定版本",
  單位 = "日對數報酬,小數",
  check.names = FALSE
)
knitr::kable(split_table)
區段 起日 迄日 觀察值 資料來源 單位
訓練期 2019-01-03 2021-10-11 699 原課程 S&P 500 價格檔的 AAPL 固定版本 日對數報酬,小數
測試期 2021-10-12 2022-06-22 175 原課程 S&P 500 價格檔的 AAPL 固定版本 日對數報酬,小數

表中訓練期為 2019-01-03 至 2021-10-11,共 699 筆;測試期為 2021-10-12 至 2022-06-22,共 175 筆。這裡評估的是「在 2021-10-11 收盤後一次形成,之後不重新估計」的多步預測。若希望每個交易日都加入新資訊並重新估計,應改用 R06 的擴展或滾動起點設計。

只在訓練期比較低階候選模型#

候選集合在讀取測試結果以前固定,只包含 $p,q\leq 2$ 的幾個低階模型。每個模型都用相同的 699 筆訓練資料與最大概似法估計,才可以公平比較 BIC。要注意,stats::arima() 對定態 ARMA 所報的 intercept 是序列平均數,而不是 $Y_t=c+\phi Y_{t-1}+a_t$ 中的 $c$。

candidate_orders <- list(
  ARMA00 = c(0, 0, 0),
  AR10 = c(1, 0, 0),
  MA01 = c(0, 0, 1),
  ARMA11 = c(1, 0, 1),
  AR20 = c(2, 0, 0),
  MA02 = c(0, 0, 2),
  ARMA21 = c(2, 0, 1),
  ARMA12 = c(1, 0, 2)
)

# 對每一個事前列出的階數,用同一估計法配適同一訓練期。
fits <- lapply(candidate_orders, function(ord) {
  arima(
    y_train,
    order = ord,
    include.mean = TRUE,
    method = "ML"
  )
})
stopifnot(all(vapply(fits, function(z) z$code == 0, logical(1))))

model_table <- data.frame(
  模型 = names(fits),
  p = vapply(candidate_orders, function(z) z[1], numeric(1)),
  q = vapply(candidate_orders, function(z) z[3], numeric(1)),
  對數概似 = vapply(fits, function(z) as.numeric(logLik(z)), numeric(1)),
  AIC = vapply(fits, AIC, numeric(1)),
  BIC = vapply(fits, BIC, numeric(1)),
  check.names = FALSE
)
model_table <- model_table[order(model_table$BIC), ]
row.names(model_table) <- NULL
knitr::kable(model_table, digits = 3)
模型 p q 對數概似 AIC BIC
AR10 1 0 1692.238 -3378.477 -3364.828
MA01 0 1 1691.150 -3376.300 -3362.651
ARMA11 1 1 1692.452 -3376.904 -3358.705
AR20 2 0 1692.444 -3376.887 -3358.689
MA02 0 2 1692.226 -3376.451 -3358.253
ARMA12 1 2 1692.452 -3374.905 -3352.156
ARMA21 2 1 1692.452 -3374.903 -3352.155
ARMA00 0 0 1678.042 -3352.084 -3342.985
selected_name <- model_table$模型[1]
selected_fit <- fits[[selected_name]]
selected_order <- candidate_orders[[selected_name]]
selected_name
## [1] "AR10"

表格由 BIC 小到大排列,這份訓練資料選出 AR(1)。BIC 只是在既定候選集合中的訓練期相對比較,不是測試期成績,也沒有保證候選集合包含真實資料生成過程。若所有候選模型的殘差都不理想,仍應降低結論強度或重新設計候選集合。

套件作法:用 auto.arima() 比較候選階數#

原課程的 slides/L04_ARMA/W1L4_R_template_for_estimating_ARMA.Rforecast::auto.arima() 選階、checkresiduals() 診斷,再以 forecast() 形成預測。 slides/L05_Forecasting_and_CV/W1L5_R_prediction_cv.R 另示範了不使用逐步搜尋的 AIC 選階。下列兩個套件版本都只讀訓練期:第一個把階數範圍與資訊準則限制成上一節的八種 $(p,d,q)$ 組合,但 auto.arima() 另會容許零平均規格,因此模型候選集合仍比手動版稍廣;第二個沿用原課程較自動的 AIC 搜尋。若要和手動版使用完全相同的候選集合,應逐一對 candidate_orders 呼叫 forecast::Arima(..., include.mean = TRUE, method = "ML"),再比較 BIC。auto.arima() 會代為估計與排序候選模型,卻不會替我們決定測試期何時開始、該採 AIC 或 BIC,或哪些階數具有實質上的合理性。

stopifnot(requireNamespace("forecast", quietly = TRUE))

# 階數範圍與手動版相同;auto.arima() 另會比較零平均規格。
fit_auto_matched <- forecast::auto.arima(
  y_train,
  d = 0,
  stationary = TRUE,
  seasonal = FALSE,
  max.p = 2,
  max.q = 2,
  max.order = 3,
  ic = "bic",
  stepwise = FALSE,
  approximation = FALSE,
  allowmean = TRUE
)

# 再沿用原課程 AIC 搜尋;階數仍只由訓練期決定。
fit_auto_course <- forecast::auto.arima(
  y_train,
  seasonal = FALSE,
  ic = "aic",
  stepwise = FALSE,
  approximation = FALSE
)

order_text <- function(fit) {
  order <- forecast::arimaorder(fit)[c("p", "d", "q")]
  sprintf("ARIMA(%d,%d,%d)", order["p"], order["d"], order["q"])
}

selection_comparison <- data.frame(
  方法 = c(
    "手動候選集/BIC",
    "forecast 同階數範圍/BIC",
    "forecast 原課程搜尋/AIC"
  ),
  選定模型 = c(
    sprintf(
      "ARIMA(%d,%d,%d)",
      selected_order[1], selected_order[2], selected_order[3]
    ),
    order_text(fit_auto_matched),
    order_text(fit_auto_course)
  ),
  對數概似 = c(
    as.numeric(logLik(selected_fit)),
    as.numeric(logLik(fit_auto_matched)),
    as.numeric(logLik(fit_auto_course))
  ),
  AIC = c(
    AIC(selected_fit), AIC(fit_auto_matched), AIC(fit_auto_course)
  ),
  BIC = c(
    BIC(selected_fit), BIC(fit_auto_matched), BIC(fit_auto_course)
  ),
  check.names = FALSE
)
knitr::kable(selection_comparison, digits = 3)
方法 選定模型 對數概似 AIC BIC
手動候選集/BIC ARIMA(1,0,0) 1692.238 -3378.477 -3364.828
forecast 同階數範圍/BIC ARIMA(1,0,0) 1692.238 -3378.477 -3364.828
forecast 原課程搜尋/AIC ARIMA(2,0,3) 1708.172 -3402.344 -3370.497

同階數範圍搜尋與手動表若選到相同結果,只能說兩者在這份資料上的相對勝者一致;由於前者另容許零平均,不能據此宣稱候選集合完全相同。若兩者結果不同,應先檢查平均數規格;若要逐模型核對,則應使用前述 forecast::Arima() 寫法固定每個階數與 include.mean = TRUE。原課程 AIC 版本的搜尋範圍與懲罰也不同,選到另一個階數時,差異來自研究設定而非套件失靈。報告「自動選階」結果時,仍要一併寫清楚資訊準則、搜尋範圍、平均數規格與訓練期間。

係數、定態根與可逆根#

選定階數後,先讀係數的方向與不確定性,再檢查 AR 與 MA 多項式的根。係數表的每一列是一個參數,估計值標準誤 使用日對數報酬的小數尺度;根表則列出複數根的實部、虛部與模。

coefficient_table <- data.frame(
  參數 = names(coef(selected_fit)),
  估計值 = as.numeric(coef(selected_fit)),
  標準誤 = sqrt(diag(selected_fit$var.coef)),
  check.names = FALSE
)
knitr::kable(coefficient_table, digits = 6)
參數 估計值 標準誤
ar1 ar1 -0.202870 0.037686
intercept intercept 0.001906 0.000677
extract_roots <- function(fit) {
  b <- coef(fit)
  ar_coef <- b[grep("^ar[0-9]+$", names(b))]
  ma_coef <- b[grep("^ma[0-9]+$", names(b))]

  # AR 與 MA 多項式的符號不同,分開建立可避免根的方向寫反。
  ar_roots <- if (length(ar_coef)) {
    polyroot(c(1, -ar_coef))
  } else {
    complex()
  }
  ma_roots <- if (length(ma_coef)) {
    polyroot(c(1, ma_coef))
  } else {
    complex()
  }

  rbind(
    if (length(ar_roots)) data.frame(
      部分 = "AR", 實部 = Re(ar_roots), 虛部 = Im(ar_roots), 模 = Mod(ar_roots)
    ),
    if (length(ma_roots)) data.frame(
      部分 = "MA", 實部 = Re(ma_roots), 虛部 = Im(ma_roots), 模 = Mod(ma_roots)
    )
  )
}

root_table <- extract_roots(selected_fit)
if (nrow(root_table)) {
  knitr::kable(root_table, digits = 5)
  stopifnot(all(root_table$模 > 1))
} else {
  cat("所選模型沒有 AR 或 MA 根需要檢查。\n")
}

所選 AR(1) 的 AR 係數約為 -0.2029,標準誤約為 0.0377;intercept 約為 0.001906,這裡代表訓練期估計的序列平均數。負的 AR 係數表示報酬偏離平均數後,下一日的條件平均有短期反向調整,但它只是樣本內的縮減式動態,不能解讀成可交易策略或因果反應。

AR 根在單位圓外時,估計模型具有因果定態表示;若模型含 MA 項,MA 根在單位圓外則表示可逆。根通過條件是採用模型的必要檢查,還不是充分診斷;根接近 1 時,有限樣本推論與遠期預測仍可能不穩定。

訓練期殘差診斷#

資訊準則選出相對勝者後,要問的是它是否已把可預測的平均動態留在殘差之外。第一列 Ljung–Box 檢查殘差前 20 階的線性相依,並扣除已估的 AR 與 MA 參數自由度;第二列檢查平方殘差,尋找尚未處理的波動群聚。

selected_residual <- as.numeric(residuals(selected_fit))
selected_residual <- selected_residual[is.finite(selected_residual)]
p_plus_q <- selected_order[1] + selected_order[3]

# 平均方程的檢定扣除 p+q;平方殘差列用來診斷波動,不沿用同一 fitdf。
q_mean <- Box.test(
  selected_residual, lag = 20, type = "Ljung-Box",
  fitdf = p_plus_q
)
q_square <- Box.test(
  selected_residual^2, lag = 20, type = "Ljung-Box"
)

diagnostic_table <- data.frame(
  檢查對象 = c("殘差", "平方殘差"),
  Q20 = c(unname(q_mean$statistic), unname(q_square$statistic)),
  自由度 = c(unname(q_mean$parameter), unname(q_square$parameter)),
  p值 = c(q_mean$p.value, q_square$p.value),
  check.names = FALSE
)
knitr::kable(diagnostic_table, digits = 6)
檢查對象 Q20 自由度 p值
殘差 54.18846 19 3.1e-05
平方殘差 355.02037 20 0.0e+00

本次 AR(1) 殘差的 Ljung–Box $p$ 值約為 $3.1\times10^{-5}$,平方殘差的 $p$ 值更接近零。這是明確的診斷警訊:BIC 所選模型只是候選集合內的相對勝者,並未清除全部平均與波動相依。平均殘差的結果提示可以檢查其他低階規格;平方殘差則提示把 ARCH/GARCH 類模型納入後續分析。

matched_order <- forecast::arimaorder(fit_auto_matched)[c("p", "d", "q")]
matched_residual <- as.numeric(residuals(fit_auto_matched))
matched_residual <- matched_residual[is.finite(matched_residual)]
matched_df <- matched_order["p"] + matched_order["q"]

matched_q_mean <- Box.test(
  matched_residual,
  lag = 20,
  type = "Ljung-Box",
  fitdf = matched_df
)
matched_q_square <- Box.test(
  matched_residual^2,
  lag = 20,
  type = "Ljung-Box"
)

diagnostic_comparison <- data.frame(
  方法 = rep(c("手動候選集", "forecast 同階數範圍搜尋"), each = 2),
  檢查對象 = rep(c("殘差", "平方殘差"), 2),
  Q20 = c(
    unname(q_mean$statistic), unname(q_square$statistic),
    unname(matched_q_mean$statistic),
    unname(matched_q_square$statistic)
  ),
  p值 = c(
    q_mean$p.value, q_square$p.value,
    matched_q_mean$p.value, matched_q_square$p.value
  ),
  check.names = FALSE
)
knitr::kable(diagnostic_comparison, digits = 7)
方法 檢查對象 Q20 p值
手動候選集 殘差 54.18846 3.09e-05
手動候選集 平方殘差 355.02037 0.00e+00
forecast 同階數範圍搜尋 殘差 54.19123 3.09e-05
forecast 同階數範圍搜尋 平方殘差 355.02619 0.00e+00
# 原課程使用的一行診斷,同時呈現殘差路徑、ACF 與聯合檢定。
forecast::checkresiduals(fit_auto_matched, lag = 20)

forecast::checkresiduals() 對同階數範圍 BIC 自動選階模型的殘差診斷。

## 
##  Ljung-Box test
## 
## data:  Residuals from ARIMA(1,0,0) with non-zero mean
## Q* = 54.191, df = 19, p-value = 3.089e-05
## 
## Model df: 1.   Total lags used: 20

若同階數範圍搜尋與手動程式不只選到相同階數,也採用相同的平均數規格,上表的兩組診斷才應幾乎重合。小數點差異可能來自初始化與數值容差;若模型階數、平均數規格或自由度不同,就要先統一這些設定、估計法與 fitdf,才能把 Ljung–Box 數值放在同一基準上比較。checkresiduals() 替我們排好圖與檢定,仍需由讀者根據殘差或平方殘差的警訊決定下一步。

old_par <- par(
  mfrow = c(1, 3), mar = c(4.5, 3.5, 4, 1),
  family = plot_family, cex.main = 0.88
)
plot(
  dates[seq_along(selected_residual)], 100 * selected_residual,
  type = "l", col = "#173B57",
  xlab = "日期", ylab = "殘差(%)", main = selected_name
)
acf(selected_residual, lag.max = 30, main = "殘差 ACF")
acf(selected_residual^2, lag.max = 30, main = "平方殘差 ACF")

BIC 所選模型的訓練期殘差、殘差 ACF 與平方殘差 ACF。

par(old_par)

圖形補上檢定數字沒有呈現的資訊:殘差時間圖可以看出警訊集中在哪些日期,兩張 ACF 則顯示相依落在何種期距。由於殘差仍有線性相依,所選低階平均數模型只能視為候選集合中的相對勝者;平方殘差的明顯相依則是第 11–12 章 ARCH/GARCH 模型的直接動機。

選定模型後的一次多步預測#

現在才讓模型面對保留的 175 筆測試資料。predict() 在訓練期末使用已估的 AR(1) 係數,一次產生整段多步預測;測試期內即使已經觀察到新的實際報酬,也不把它加入模型。這個設計回答「若在 2021-10-11 只估計一次,遠期預測會如何」,與每日更新的一步預測是不同問題。

h <- length(y_test)
# 模型、參數與預測起點都在讀取測試答案前選定。
forecast_object <- predict(selected_fit, n.ahead = h)

forecast_table <- data.frame(
  日期 = dates[(train_end + 1L):n],
  實際值 = y_test,
  ARMA預測 = as.numeric(forecast_object$pred),
  預測標準誤 = as.numeric(forecast_object$se),
  check.names = FALSE
)
forecast_table$下界95 <- forecast_table$ARMA預測 -
  1.96 * forecast_table$預測標準誤
forecast_table$上界95 <- forecast_table$ARMA預測 +
  1.96 * forecast_table$預測標準誤
forecast_table$ARMA誤差 <- forecast_table$實際值 -
  forecast_table$ARMA預測

knitr::kable(head(forecast_table, 8), digits = 6)
日期 實際值 ARMA預測 預測標準誤 下界95 上界95 ARMA誤差
2021-10-12 -0.009145 0.002420 0.021496 -0.039711 0.044552 -0.011565
2021-10-13 -0.004249 0.001802 0.021933 -0.041188 0.044791 -0.006051
2021-10-14 0.020024 0.001927 0.021951 -0.041097 0.044952 0.018097
2021-10-15 0.007485 0.001902 0.021952 -0.041124 0.044928 0.005583
2021-10-18 0.011737 0.001907 0.021952 -0.041119 0.044933 0.009830
2021-10-19 0.014968 0.001906 0.021952 -0.041120 0.044932 0.013062
2021-10-20 0.003356 0.001906 0.021952 -0.041120 0.044932 0.001450
2021-10-21 0.001473 0.001906 0.021952 -0.041120 0.044932 -0.000433

表中每一列是一個測試交易日:ARMA預測 是訓練期末可形成的條件平均預測,預測標準誤 隨期距反映未來創新累積,實際值 只在事後計分時使用。任何一列的實際值都沒有回流到後面日期的預測。

score_row <- function(actual, forecast, label) {
  error <- actual - forecast
  data.frame(
    模型 = label,
    RMSE = sqrt(mean(error^2)),
    MAE = mean(abs(error)),
    平均誤差 = mean(error),
    check.names = FALSE
  )
}

score_table <- rbind(
  score_row(y_test, forecast_table$ARMA預測, selected_name),
  score_row(y_test, rep(0, h), "零報酬"),
  score_row(y_test, rep(mean(y_train), h), "訓練期平均數")
)
score_table$常態區間涵蓋率 <- c(
  mean(
    y_test >= forecast_table$下界95 &
      y_test <= forecast_table$上界95
  ),
  NA_real_, NA_real_
)
knitr::kable(score_table, digits = 6)
模型 RMSE MAE 平均誤差 常態區間涵蓋率
AR10 0.020809 0.016036 -0.002191 0.977143
零報酬 0.020694 0.015951 -0.000283 NA
訓練期平均數 0.020805 0.016031 -0.002162 NA

RMSE 對較大的預測錯誤給予更高權重,MAE 則以絕對誤差計分,平均誤差用來觀察預測是否整體偏高或偏低。在這 175 筆固定測試資料中,零報酬基準的 RMSE 與 MAE 都略低於訓練期 BIC 所選的 AR(1)。這個結果很有教學價值:樣本內資訊準則較佳,不保證樣本外預測勝過簡單基準。既然測試答案已經看過,就不應再為了追上零報酬基準而回頭改階數。

套件作法:用 forecast() 形成預測並以 accuracy() 計分#

下列程式對前面兩個 auto.arima() 模型執行原課程採用的 forecast()accuracy() 工作流程。forecast() 會整理點預測與區間,accuracy() 會依實際值計算常用誤差指標;兩者都不會替我們維持樣本外紀律。因此,兩個模型的階數仍在訓練期選定,測試期只用來計分。

course_forecast_matched <- forecast::forecast(
  fit_auto_matched,
  h = h,
  level = 95
)
course_forecast_aic <- forecast::forecast(
  fit_auto_course,
  h = h,
  level = 95
)

accuracy_row <- function(object, actual, label) {
  # accuracy() 可能同時回報訓練與測試列;此處明確取最後一列測試結果。
  package_accuracy <- forecast::accuracy(object, actual)
  test_accuracy <- package_accuracy[nrow(package_accuracy), , drop = FALSE]
  data.frame(
    模型 = label,
    RMSE = unname(test_accuracy[1, "RMSE"]),
    MAE = unname(test_accuracy[1, "MAE"]),
    平均誤差 = unname(test_accuracy[1, "ME"]),
    常態區間涵蓋率 = mean(
      actual >= as.numeric(object$lower[, 1]) &
        actual <= as.numeric(object$upper[, 1])
    ),
    check.names = FALSE
  )
}

manual_accuracy <- score_row(
  y_test,
  forecast_table$ARMA預測,
  paste0("手動候選:", selected_name)
)
manual_accuracy$常態區間涵蓋率 <- mean(
  y_test >= forecast_table$下界95 &
    y_test <= forecast_table$上界95
)

zero_accuracy <- score_row(y_test, rep(0, h), "零報酬")
zero_accuracy$常態區間涵蓋率 <- NA_real_

package_forecast_comparison <- rbind(
  manual_accuracy,
  accuracy_row(
    course_forecast_matched,
    y_test,
    paste0("forecast 同階數範圍BIC:", order_text(fit_auto_matched))
  ),
  accuracy_row(
    course_forecast_aic,
    y_test,
    paste0("forecast 原課程AIC:", order_text(fit_auto_course))
  ),
  zero_accuracy
)
row.names(package_forecast_comparison) <- NULL
knitr::kable(package_forecast_comparison, digits = 7)
模型 RMSE MAE 平均誤差 常態區間涵蓋率
手動候選:AR10 0.0208091 0.0160361 -0.0021910 0.9771429
forecast 同階數範圍BIC:ARIMA(1,0,0) 0.0208091 0.0160361 -0.0021907 0.9771429
forecast 原課程AIC:ARIMA(2,0,3) 0.0208093 0.0160548 -0.0022301 0.9771429
零報酬 0.0206943 0.0159514 -0.0002826 NA

若手動 BIC 與同階數範圍的 auto.arima() 不只選到同一階數,也採用相同平均數規格,兩者點預測與評分才應幾乎相同。只看階數相同,還不足以宣稱兩套程式估的是同一個模型。原課程 AIC 版本可能選到不同階數,因而產生不同預測。比較表要連同模型標籤一起讀,不能只挑 RMSE 最小的一列,事後把它說成事前選定的模型。

old_par <- par(family = plot_family)
plot(
  forecast_table$日期, 100 * forecast_table$實際值,
  type = "l", col = "gray35",
  xlab = "日期", ylab = "日對數報酬(%)"
)
polygon(
  c(forecast_table$日期, rev(forecast_table$日期)),
  100 * c(forecast_table$下界95, rev(forecast_table$上界95)),
  border = NA, col = adjustcolor("#9FC2D4", alpha.f = 0.45)
)
lines(
  forecast_table$日期, 100 * forecast_table$ARMA預測,
  col = "#A34045", lwd = 2
)
lines(
  forecast_table$日期, 100 * forecast_table$實際值,
  col = "gray35"
)
legend(
  "topright", c("實際值", "ARMA 點預測", "95% 區間"),
  col = c("gray35", "#A34045", "#9FC2D4"),
  lty = c(1, 1, NA), pch = c(NA, NA, 15), bty = "n"
)

訓練期末一次形成的 AAPL 多步預測與 95% 常態近似區間。

par(old_par)

圖中帶狀區域是以所選 ARMA 模型與常態近似形成的 95% 區間,主要反映未來創新的不確定性。它沒有完整納入參數估計、階數選擇、厚尾與條件異質變異造成的不確定性。表中的固定測試期涵蓋率只能描述這 175 筆觀察值;若涵蓋不足,下一步應檢查創新分配與波動模型,而不是任意加寬區間到看起來理想。

用已知真值確認符號與估計流程#

最後另生成一條 5,000 期的 ARMA(1,1) 模擬序列。因為 AR 與 MA 真值已知,我們可以確認 arima() 的係數符號與名稱是否和課文一致。這條模擬序列只檢查程式流程,不參與 AAPL 的模型選擇、預測評分或實證結論。

set.seed(20260721)
truth <- c(ar1 = 0.55, ma1 = -0.35)
simulated_check <- as.numeric(arima.sim(
  model = list(ar = truth["ar1"], ma = truth["ma1"]),
  n = 5000, sd = 0.01
))
fit_check <- arima(
  simulated_check, order = c(1, 0, 1),
  include.mean = TRUE, method = "ML"
)
check_table <- data.frame(
  參數 = names(truth),
  真值 = truth,
  估計值 = coef(fit_check)[names(truth)],
  check.names = FALSE
)
knitr::kable(check_table, digits = 4)
參數 真值 估計值
ar1 ar1 0.55 0.5673
ma1 ma1 -0.35 -0.3769
stopifnot(max(abs(check_table$估計值 - check_table$真值)) < 0.10)

兩個估計值與真值的差距都應小於事前設定的 0.10 容許範圍。若沒有通過,應先檢查 MA 符號慣例、係數名稱與模擬長度,而不能據此修改 AAPL 的實證模型。

如何延伸這次結果#

這份 AAPL 固定樣本顯示,訓練期 BIC 在既定候選集合中選出 AR(1),但殘差與平方殘差仍有明顯相依,而且一次形成的多步預測沒有勝過零報酬基準。可以得到的結論是:這個低階條件平均模型尚不足以完整描述資料,也沒有展現穩健的樣本外優勢;我們還不能把負 AR 係數解讀成可交易規律。

下一步可在新的訓練/驗證設計中納入其他平均規格與 ARCH/GARCH 波動模型,再保留另一段未見期間評估。報告時應保存固定資料版本、訓練截止日、候選集合、估計方法、截距參數化、資訊準則、根、殘差診斷、預測期距與區間假設。若看過這 175 筆測試結果後再換模型,這段期間就只能當作驗證資料;新的主張需要另一段真正未見的測試期。

© 陳釗而 《財務時間序列分析》