ARK · 金融機器學習CHAPTER 06 / 12

CHAPTER 06 / 12 · PART 2 · 時序學習

金融時間序列建模基礎

Sequence Modeling

順序攜帶資訊:平穩性、ARMA、GARCH 與 walk-forward,把時序工具箱一次鋪好。

§01學習重點

§02課程內容

一、時間給資料裝上了方向

課程第一部處理的都是橫斷面資料:一批房貸戶、一籃股票的財務比率,每筆觀測是一個獨立個體,洗牌重排不損失任何資訊。第二部開場,我們把箭頭轉向另一種資料:同一個變數沿著時間反覆觀測——大盤指數的日收盤、公債殖利率的每月快照、一檔 ETF 的逐筆成交。這種資料叫時間序列,它與橫斷面資料的差異不是格式,而是本質:順序本身攜帶資訊。把一位學生六年來的每月段考成績洗牌重排,你就毀掉了「他最近在進步還是退步」這件最有價值的事;把全班同學同一次段考的成績洗牌,什麼都沒有損失。

順序攜帶資訊的統計說法,是觀測之間存在自相關(autocorrelation):今天的值與昨天、上週、上個月的值相關。第一部的工具幾乎都建立在「觀測獨立同分布」的假設上——訓練與驗證資料隨便切、梯度隨便打散批次——這些操作一旦搬到時序資料上,輕則失效,重則說謊(本章第八節會量給你看謊有多大)。因此在第 8 章把神經網路開上時間軸之前,本章先把計量經濟學打磨了半個多世紀的時序工具箱鋪開:平穩性、AR、MA、GARCH、指數平滑、Box–Jenkins 流程、時序交叉驗證與 PCA。這些工具本身就有實戰價值,而且它們是後面所有花俏架構的「素顏版」——第 8 章的每一種神經網路架構,都會被證明是本章某個模型的推廣。

二、平穩性:不平穩的資料會說謊

時序建模第一個要回答的問題不是「用什麼模型」,而是「這條序列可不可以學」。可學的前提叫平穩性(stationarity)。白話版:一條序列是(弱)平穩的,代表它的統計性格不隨時間改變——平均水準不漂移、震盪幅度不漂移、「今天與 j 天前的相關程度」只看間隔 j、不看是哪一天。寫成條件就是:對所有時點 t,

$$ \mathbb{E}[y_t] = \mu, \qquad \mathrm{Cov}(y_t,\, y_{t-j}) = \gamma_j \quad \forall j $$

逐項拆解:第一條說均值 \(\mu\) 是常數,不掛時間下標——序列沒有趨勢、不會越走越高;第二條說 t 期與 t−j 期的共變異數 \(\gamma_j\) 只依賴間隔 j——「記憶的形狀」在整條序列上是同一套。把 \(\gamma_j\) 除以變異數 \(\gamma_0\) 就得到第 j 階自相關係數 \(\tau_j = \gamma_j / \gamma_0\),它介於 −1 與 1 之間,是後面選模型的主要線索。

為什麼要在意?因為所有統計學習的底層邏輯都是「從歷史樣本估計規律、假設規律延續到未來」。平穩性就是「規律延續」的最低保證:如果序列的均值和變異數本身在漂移,你從前半段估出來的任何數字,對後半段都不作數。

不平穩最惡名昭彰的後果是虛假迴歸(spurious regression):兩條毫無關係的序列,只因為各自都在漂移,迴歸起來卻顯著得不得了。直接做給你看——模擬兩條完全獨立的隨機漫步(\(y_t = y_{t-1} + \epsilon_t\),每步是純雜訊的累加),互相迴歸:

PYTHON
import numpy as np

rng = np.random.default_rng(66)
n = 500

# 兩條完全獨立的隨機漫步:想像兩檔毫無關係的資產價格
x = np.cumsum(rng.normal(0.0, 1.0, n))
y = np.cumsum(rng.normal(0.0, 1.0, n))

def ols_r2(u, v):
    """v 對 u 做最小平方迴歸,回傳斜率與 R 平方"""
    U = np.column_stack([np.ones(len(u)), u])
    beta, *_ = np.linalg.lstsq(U, v, rcond=None)
    resid = v - U @ beta
    return beta[1], 1.0 - resid.var() / v.var()

slope_lv, r2_lv = ols_r2(x, y)
slope_df, r2_df = ols_r2(np.diff(x), np.diff(y))
print(f"價位對價位:斜率 {slope_lv:.3f},R^2 = {r2_lv:.3f}")
print(f"差分對差分:斜率 {slope_df:.3f},R^2 = {r2_df:.4f}")
# 輸出:
# 價位對價位:斜率 -0.664,R^2 = 0.437
# 差分對差分:斜率 0.008,R^2 = 0.0001

print(f"x 前半均值 {x[:250].mean():.2f},後半均值 {x[250:].mean():.2f}")
# 輸出:x 前半均值 -5.01,後半均值 7.98

兩條由亂數獨立生成、理論相關為零的序列,價位對價位迴歸的 R 平方高達 0.437——如果這是一篇研究報告,作者大概已經開始講故事了。同樣的資料差分(取相鄰兩期之差 \(\Delta y_t = y_t - y_{t-1}\))之後再迴歸,R 平方掉到 0.0001:真相是零關係。漂移是騙局的來源:x 的前半段平均在 −5 附近、後半段在 +8 附近——均值本身在走路,任何同樣在走路的序列都能跟它「相關」。

比喻: 百貨公司裡兩台互不相干的手扶梯,各載各的客人往上送。如果你每分鐘記錄「兩台手扶梯上乘客的平均高度」,會得到兩條高度相關的曲線——因為兩台都在把人往上送,高度都在漲。但這個相關是樓層給的,不是乘客之間有什麼默契。想知道兩台手扶梯有沒有真正的關係,要比的是「每一階的瞬間動作」——差分——而不是「目前的高度」——價位。金融資料裡價格幾乎都是手扶梯,報酬才是階梯動作,這就是為什麼實務上永遠對報酬建模、不對價格建模。

處理不平穩的標準手段有兩類。差分是通用解:對價格取一次差(或對數差,即報酬),絕大多數金融序列就落地平穩;差過 d 次的 ARMA 模型就叫 ARIMA(p, d, q)。去趨勢適用於「趨勢是確定性函數」的少數情況:把時間趨勢迴歸掉、留下殘差。兩者不可互換——第九節的作業會讓你看到,對一條只是「掛在確定趨勢上」的序列硬做差分,反而會把雜訊結構弄壞。另外要記住:差分是有代價的,它丟掉了水準資訊,差過頭(過度差分)同樣傷模型。真正頑固的不平穩——制度切換、結構斷裂——差分也救不了,那是第 7 章隱馬可夫模型與濾波方法的戰場。

三、AR(p):用自己的過去解釋自己

平穩性鋪好地基,第一個正式模型登場。自迴歸模型 AR(p)(autoregressive)的想法極簡:一個變數的現在,是它自己過去 p 期的線性組合,加上一點新雜訊:

$$ y_t = \mu + \phi_1 y_{t-1} + \phi_2 y_{t-2} + \cdots + \phi_p y_{t-p} + \epsilon_t $$

逐項拆解:\(\mu\) 是漂移項(水準的錨);\(\phi_i\) 是第 i 期滯後的權重——「i 天前的自己對今天的影響力」;p 是模型的階數,決定記憶往回看幾期;\(\epsilon_t\) 是白噪音——均值為零、變異數固定、彼此獨立的新衝擊,計量文獻裡也叫「創新」(innovation),因為它是 t 期唯一無法由歷史預測的新資訊。整個模型就是「迴歸」一詞的字面意思,只是解釋變數換成了被解釋變數自己的過去——這正是「自」迴歸的由來。

穩定性:回饋增益必須小於一。 AR 模型是一個回饋系統:今天的輸出明天變成輸入。回饋系統的第一個問題永遠是——會不會爆炸?看最簡單的 AR(1):\(y_t = \phi y_{t-1} + \epsilon_t\)。把它往回代入,今天的值可以展開成歷史所有衝擊的加權和:

$$ y_t = \epsilon_t + \phi\,\epsilon_{t-1} + \phi^2 \epsilon_{t-2} + \phi^3 \epsilon_{t-3} + \cdots $$

逐項拆解:j 天前的衝擊 \(\epsilon_{t-j}\) 留在今天體內的份量是 \(\phi^j\)。當 \(|\phi| < 1\),權重幾何衰減——越久遠的衝擊影響越小,這符合直覺(上個月的新聞對今天股價的影響,理應小於今天早上的新聞),無窮級數收斂,序列平穩。當 \(|\phi| \ge 1\),衝擊的影響不衰減甚至放大,序列爆炸或漂移。一般的 AR(p) 把條件寫在特徵方程 \(1 - \phi_1 z - \cdots - \phi_p z^p = 0\) 的根上:所有根的模長都要大於 1(「根在單位圓外」)。兩種說法是等價的——根取倒數就是系統的回饋增益,「根在單位圓外」等於「所有增益都在單位圓內」,任何一個增益碰到 1 就出事。臨界情況 \(\phi = 1\) 正是上一節的隨機漫步:根恰好落在單位圓上(「單位根」),衝擊永不衰減、全部累積——這就是它不平穩的機制根源。檢定一條序列有沒有單位根,標準工具是 ADF 檢定(第七節)。

比喻: 把麥克風對著自己的喇叭。你說一句話,聲音進喇叭、回進麥克風、再進喇叭——這就是自迴歸的回饋迴路,\(\phi\) 是音量旋鈕。旋鈕小於 1:每繞一圈聲音衰減一點,回音一層層淡出,房間安靜下來——平穩。旋鈕大於等於 1:每繞一圈不衰減,瞬間變成刺耳的嘯叫——爆炸。金融序列建模的第一個檢查,就是確認你面對的是「會淡出的回音」還是「正在嘯叫的音響」;後者(單位根)要先差分,把嘯叫降回回音再建模。

選階:偏自相關函數。 實務上 p 不是拍腦袋決定的,資料自己會招供。工具是偏自相關函數(PACF):lag-h 的偏自相關,是「控制中間 h−1 期之後,\(y_t\) 與 \(y_{t-h}\) 還剩多少直接相關」。關鍵性質:AR(p) 過程的 PACF 在 lag p 之後截尾——因為模型裡 \(y_{t-p-1}\) 對今天沒有直接通道,它的影響全部透過中間期轉手,控制住中間期就一刀切斷。所以把樣本 PACF 畫出來,最後一根顯著的柱子在哪裡,p 就是多少。計算上有個好用的事實:lag-h 偏自相關恰好等於「配適 AR(h) 迴歸後,第 h 個滯後的係數」。模擬一條 AR(2) 驗證:

PYTHON
import numpy as np

rng = np.random.default_rng(6)
phi1, phi2 = 0.5, 0.3
T = 2000

eps = rng.normal(0.0, 1.0, T + 100)
y = np.zeros(T + 100)
for t in range(2, T + 100):
    y[t] = phi1 * y[t-1] + phi2 * y[t-2] + eps[t]
y = y[100:]                     # 丟掉暖機段,讓序列進入平穩分布

# 平穩性檢查:特徵方程 1 - 0.5z - 0.3z^2 = 0 的根
roots = np.roots([-phi2, -phi1, 1.0])
print("根的模長:", np.round(np.abs(roots), 3))
# 輸出:根的模長: [2.84  1.174] —— 全部 > 1,平穩

def pacf(series, h):
    """lag-h 偏自相關:配適 AR(h),取最後一個滯後的係數"""
    Y = series[h:]
    X = np.column_stack([series[h-j:len(series)-j] for j in range(1, h+1)])
    X = np.column_stack([np.ones(len(Y)), X])
    return np.linalg.lstsq(X, Y, rcond=None)[0][-1]

print(f"95% 信賴帶約 ±{1.96/np.sqrt(T):.3f}")     # ±0.044
for h in range(1, 7):
    print(f"lag {h}: PACF = {pacf(y, h):+.4f}")
# 輸出:
# lag 1: PACF = +0.7248
# lag 2: PACF = +0.2770
# lag 3: PACF = +0.0078
# lag 4: PACF = +0.0023
# lag 5: PACF = -0.0145
# lag 6: PACF = +0.0036

真實階數是 2,樣本 PACF 也乾脆利落:lag 1、lag 2 遠超信賴帶(±0.044),lag 3 起全部縮回帶內——資料招了,p = 2。信賴帶的來源:若真實偏自相關為零,樣本估計值近似以 \(1/\sqrt{T}\) 為標準差的常態分布,乘上 1.96 就是 95% 的判界。

這裡埋一條線:AR(p) 是「往回看固定 p 期、權重各自估計」的記憶。第 8 章會證明,線性激活的循環神經網路(RNN)就是一個權重幾何衰減的 AR(p)——RNN 沒有丟掉計量的骨架,它做的是把「固定線性記憶」升級成「可學習的非線性記憶」。你現在打的每一寸地基,到時都用得上。

四、MA(q) 與 ARMA:衝擊的餘波

AR 用「過去的」解釋今天,還有另一種描述:用「過去的衝擊」。移動平均模型 MA(q)(moving average)寫成:

$$ y_t = \mu + \epsilon_t + \theta_1 \epsilon_{t-1} + \theta_2 \epsilon_{t-2} + \cdots + \theta_q \epsilon_{t-q} $$

逐項拆解:今天的值由今天的新衝擊 \(\epsilon_t\)、加上過去 q 期衝擊的餘波 \(\theta_i \epsilon_{t-i}\) 組成;\(\theta_i\) 是「i 天前的意外今天還殘留多少」。與 AR 的差別在記憶的壽命:MA(q) 的衝擊活滿 q 期就徹底出清——它的自相關函數在 lag q 之後截尾(對照:AR 的自相關幾何衰減、永不歸零,是 PACF 截尾);兩張相函數圖一起看,AR 與 MA 的簽名恰好互補,這是識別模型的指紋。順帶一提,兩者在數學上是親戚:上一節 AR(1) 的無窮展開,正是把 AR(1) 寫成了 MA(∞)——一個有限參數的自迴歸,等於一個無窮長但權重幾何衰減的移動平均。

兩者合體就是 ARMA(p, q):p 期值記憶加 q 期衝擊記憶,用最少的參數逼近豐富的相關結構。參數估計靠最大概似:假設衝擊服從常態分布,寫出觀測資料的聯合機率,找讓它最大的參數——對線性模型而言,條件版的最大概似跟最小平方法給出幾乎同樣的答案,所以你可以放心把它理解成「時序版的迴歸配適」,細節此處不展開(原書有完整推導,見原書對照)。真正要記住的是結構性的一點:ARMA 是對均值建模——它預測「下一期的期望值」,並假設圍繞期望值的雜訊幅度恆定。金融資料偏偏在這一點上反叛,這就是下一節的主題。

五、波動叢聚與 GARCH:變異數會記仇

拿任何一條股票日報酬序列來看,會看到兩個並存的現象。第一,報酬的方向幾乎不可預測——今天漲跌與明天漲跌的自相關趨近零,這是市場有效性的表現。第二,報酬的量級高度可預測——大漲大跌的日子擠在一起(危機期),風平浪靜的日子也擠在一起(牛市中段)。這叫波動叢聚(volatility clustering):方向像擲硬幣,力道卻有記憶。用一句話總結金融報酬的性格:均值不記仇,變異數會記仇

ARMA 對此無能為力,因為它假設雜訊變異數恆定。解法是把「變異數」本身當成一條會演化的序列來建模。GARCH(1,1)(廣義自迴歸條件異變異數)是這個思路的招牌模型:

$$ \sigma_t^2 = \alpha_0 + \alpha_1\, \epsilon_{t-1}^2 + \beta_1\, \sigma_{t-1}^2 $$

逐項拆解:\(\sigma_t^2\) 是 t 期的條件變異數——站在 t−1 期末看明天的預期波動平方;\(\alpha_0\) 是波動的地板(長期基準的來源);\(\alpha_1 \epsilon_{t-1}^2\) 是驚嚇項——昨天實際衝擊的平方,昨天暴漲暴跌,今天預期波動就抬高,\(\alpha_1\) 控制市場對新消息的敏感度;\(\beta_1 \sigma_{t-1}^2\) 是慣性項——昨天的預期波動延續到今天,\(\beta_1\) 控制波動狀態的黏性。三個參數各司其職:地板、驚嚇、慣性。模型平穩(波動不會永久發散)的條件是 \(\alpha_1 + \beta_1 < 1\),此時長期平均變異數收斂到 \(\bar{\sigma}^2 = \alpha_0 / (1 - \alpha_1 - \beta_1)\)——把地板除以「一減記憶總量」。

\(\alpha_1 + \beta_1\) 這個和有清楚的操作意義:它是波動偏離長期水準後的衰減速率。往前 l 步的變異數預測有個漂亮的閉式:

$$ \hat{\sigma}^2_{t+l} = \bar{\sigma}^2 + (\alpha_1 + \beta_1)^l \left( \sigma_t^2 - \bar{\sigma}^2 \right) $$

逐項拆解:未來的預期變異數 = 長期水準 + 今天的偏離乘上衰減因子的 l 次方。今天處在風暴中(\(\sigma_t^2\) 遠高於 \(\bar{\sigma}^2\)),預測會沿幾何路徑滑回長期水準;滑到一半所需的天數——半衰期——是 \(\ln(0.5)/\ln(\alpha_1+\beta_1)\)。日資料估出來的 \(\alpha_1 + \beta_1\) 常在 0.95 到 0.99 之間:風暴要一到三個月才消化一半,這正是「危機盤整期特別漫長」的量化說法。模擬驗證:

PYTHON
import numpy as np

rng = np.random.default_rng(11)
a0, a1, b1 = 2e-6, 0.08, 0.90      # 地板、驚嚇、慣性
T = 1500

sig2 = np.zeros(T); r = np.zeros(T)
sig2[0] = a0 / (1 - a1 - b1)        # 從長期變異數起步
for t in range(1, T):
    sig2[t] = a0 + a1 * r[t-1]**2 + b1 * sig2[t-1]
    r[t] = np.sqrt(sig2[t]) * rng.normal()

def acf1(s):
    s = s - s.mean()
    return (s[1:] * s[:-1]).sum() / (s**2).sum()

print(f"長期日波動 {np.sqrt(a0/(1-a1-b1))*100:.2f}%,樣本變異數 {r.var():.2e}")
print(f"報酬的一階自相關     = {acf1(r):+.4f}")
print(f"報酬平方的一階自相關 = {acf1(r**2):+.4f}")
print(f"條件波動度範圍 {np.sqrt(sig2).min()*100:.2f}% ~ {np.sqrt(sig2).max()*100:.2f}%")
print(f"半衰期 = {np.log(0.5)/np.log(a1+b1):.1f} 天")
# 輸出:
# 長期日波動 1.00%,樣本變異數 1.03e-04
# 報酬的一階自相關     = +0.0007
# 報酬平方的一階自相關 = +0.1775
# 條件波動度範圍 0.60% ~ 1.88%;半衰期 = 34.3 天

數字把兩個現象同時抓住了:報酬本身的一階自相關 +0.0007——方向無記憶;報酬平方的一階自相關 +0.1775——量級有記憶。同一條序列裡,條件波動度在 0.60% 與 1.88% 之間游走三倍——有風暴期、有平靜期,而且各自成片。這就是為什麼波動預測是金融機器學習裡少數「真的做得準」的任務:選擇權定價、風險值計算、部位規模控制,吃的都是這口飯。

比喻: 波動叢聚像地震。主震(大衝擊)之後,餘震在幾週內明顯變多,然後頻率逐日衰減、回到背景水準;平靜期就大致持續平靜。你無法預測下一次主震的方向與時點(報酬不可測),但「剛震完的地方短期內容易再震」高度可測(波動可測)。GARCH 的三個參數就是這套地質學:\(\alpha_0\) 是背景地震活動度,\(\alpha_1\) 是主震觸發餘震的強度,\(\beta_1\) 是餘震序列自身的延續性,\(\alpha_1+\beta_1\) 決定餘震多久消停。

六、指數平滑:帶折扣的記憶

在 ARMA 這類「結構派」模型之外,實務界還有一支輕武器:指數平滑(exponential smoothing)。它不假設資料生成過程,只維護一個「平滑後的水準估計」\(\tilde{y}_t\),每來一筆新觀測就修正一次:

$$ \tilde{y}_{t+1} = \alpha\, y_t + (1 - \alpha)\, \tilde{y}_t $$

逐項拆解:新的估計是「最新觀測」與「舊估計」的加權平均;\(\alpha\) 介於 0 與 1,是新資訊的權重——α 大,緊跟最新資料、反應快但毛躁;α 小,倚重歷史、平滑但遲鈍。把這條遞迴展開,會發現 \(\tilde{y}_{t+1}\) 其實是全部歷史觀測的加權和,權重 \(\alpha(1-\alpha)^j\) 隨距今天數 j 幾何衰減——所以叫「指數」平滑:記憶無限長,但打折打得飛快,折扣率就是 \(1-\alpha\)。它與 AR 的分工很清楚:AR 只用固定 p 期、每期權重自由估計;指數平滑用全部歷史、但權重形狀鎖死成幾何衰減、只留一個參數。參數少到只剩一個,於是穩健、便宜、難過擬合——這讓它成為業界濾波與短期預測的常青樹,從庫存管理到高頻訊號的預處理都有它。

比喻: 指數平滑是店長對員工的印象分數。每天打烊,店長把「今天的表現」以權重 α 揉進舊印象:印象新 = α × 今天 + (1−α) × 印象舊。三個月前打破一個盤子的事還在印象裡,但已經被 \((1-\alpha)^{90}\) 折到幾乎為零。α 是店長的性格:α 大是「只看最近」的急性子,員工一天好一天壞印象跟著劇烈搖擺;α 小是「日久見人心」的慢性子,印象穩定但對員工真的變好變壞反應遲鈍。

這裡埋本章第二條通往第 8 章的線,而且是最直接的一條:把指數平滑的更新式與 RNN 的隱狀態更新式並排放——RNN 也是「新狀態 = 函數(新輸入, 舊狀態)」的遞迴。指數平滑就是一個線性、單一狀態、係數固定的迷你 RNN;第 8 章會見到的 α-RNN 直接以它命名——把平滑係數 α 變成可以從資料學出來的參數,再進一步讓 α 隨情境變動,就得到 GRU 與 LSTM 裡「遺忘門」的雛形。從店長的印象分數到 LSTM,中間隔的不是鴻溝,是三次升級。

七、Box–Jenkins:計量學派的三步舞

工具攤了一桌,怎麼組成一套可重複的建模流程?計量經濟學的答案定型於 1970 年代,以兩位統計學家命名為 Box–Jenkins 方法,三步循環:

識別(Identification)——先驗平穩:對序列做 ADF 單位根檢定,不平穩就差分再檢,直到落地;然後看兩張相函數圖選階——PACF 截尾處定 AR 的 p,ACF 截尾處定 MA 的 q。不想依賴目測,可以用資訊準則:AIC 把「配適誤差」與「參數個數的罰款」加總,逐一試各種 (p, q) 取總分最低者——跟第 1 章講過的偏差–方差取捨一脈相承:多一個參數要多繳一份罰金,貼合資料的收益必須付得起這筆錢。

估計(Estimation)——對選定的 ARMA(p, q) 跑最大概似,得到參數與標準誤。

診斷(Diagnostics)——檢查模型「吃乾淨了沒有」:配適後的殘差應該是白噪音;若殘差還有自相關,代表結構沒抓完,模型欠擬合。標準工具是 Ljung–Box 檢定:把殘差前 m 階自相關的平方加權加總成一個統計量,太大就拒絕「殘差是白噪音」的虛無假設——退回第一步,加階重來。

這套流程與機器學習的模型選擇是很好的對照組。共同點:兩邊都在對抗過擬合,AIC 的罰款項與正則化的懲罰項精神相通。差異點有二。其一,罰款的時機:AIC 是先無約束地估完、再事後加罰款排名;正則化是把懲罰直接寫進損失函數、在最佳化過程中即時生效。其二,裁判的身分:Box–Jenkins 的診斷都在樣本內進行(殘差檢定用的還是訓練資料),對樣本外表現沒有直接保證;機器學習則把樣本外表現本身當裁判——這就逼出了下一節的問題:時序資料的「樣本外」,到底該怎麼切?

八、預測的裁判:walk-forward

模型配好了,h 步預測本身很直接:對 AR 模型,把預測值遞迴地餵回模型——一步預測用真實歷史,兩步預測用一步的預測值,依此類推;由於每一步都乘上小於一的係數,AR 的多步預測會幾何式地收斂回無條件均值,「模型的膽子隨預測距離變小」。若目標是二元事件(明天漲或跌、會不會違約),慣用做法是對事件機率的 log-odds 建 ARMA,再用門檻切成類別——相當於時序版的 logistic 迴歸,評估就回到第 1 章的混淆矩陣、ROC 與 F1 那一套。

真正的火藥都在評估這一步。第 1 章講過,機器學習的裁判是樣本外資料,而交叉驗證的橫斷面標準做法,是把資料隨機打散成 K 份輪流當驗證集;第 4 章則立過金融資料的切分紀律——一律沿時間切。這個「隨機打散」在時序資料上是一顆地雷:打散之後,訓練集裡混著時間上晚於驗證點的觀測——模型偷看了未來。由於時序資料前後高度相關,「偷看未來」不是小瑕疵,而是系統性的作弊,術語叫前視偏差(look-ahead bias)。正確做法是 walk-forward 驗證(走動式向前驗證):按時間切成連續數段,每一輪都「用某時點之前的資料訓練、對緊接其後的一段測試」,然後把時點往前推、重複——訓練永遠在測試之前,就像真實世界裡你只能用今天以前的資料做明天的決定。量一下兩種切法差多少:

PYTHON
import numpy as np

rng = np.random.default_rng(3)
T = 500
z = np.cumsum(rng.normal(0.0, 1.0, T))     # 高度持續的序列(隨機漫步)
t_idx = np.arange(T) / T

def poly_mse(idx_tr, idx_te, deg=6):
    """用 6 次多項式趨勢模型配適訓練段,回傳測試段的 MSE"""
    coef = np.polyfit(t_idx[idx_tr], z[idx_tr], deg)
    return np.mean((np.polyval(coef, t_idx[idx_te]) - z[idx_te])**2)

# 錯誤做法:隨機打散 5 折——未來混進訓練集
perm = rng.permutation(T)
folds = np.array_split(perm, 5)
mse_shuffle = np.mean([poly_mse(np.setdiff1d(perm, f), f) for f in folds])

# 正確做法:walk-forward——訓練永遠在測試之前
edges = np.linspace(T // 3, T, 6, dtype=int)
mse_wf = np.mean([poly_mse(np.arange(0, edges[i]),
                           np.arange(edges[i], edges[i+1])) for i in range(5)])

# 真相:再走 100 步當「真的未來」,用全部歷史配適後外推
zf = z[-1] + np.cumsum(rng.normal(0.0, 1.0, 100))
coef = np.polyfit(t_idx, z, 6)
mse_future = np.mean((np.polyval(coef, np.arange(T, T + 100) / T) - zf)**2)

print(f"隨機打散 CV 估的 MSE  = {mse_shuffle:.1f}")
print(f"walk-forward 估的 MSE = {mse_wf:.1f}")
print(f"真實未來的 MSE        = {mse_future:.1f}")
# 輸出:
# 隨機打散 CV 估的 MSE  = 11.3
# walk-forward 估的 MSE = 9787.0
# 真實未來的 MSE        = 9772.6

差距不是幾成,是近千倍:隨機打散告訴你誤差是 11.3,真實未來的誤差是 9772.6,而 walk-forward 估出 9787.0——幾乎貼著真相。機制看穿了很簡單:隨機打散讓每個驗證點的前後都有訓練點,模型做的是內插——在已知點之間連線;真實預測是外推——走出所有已知點之外。時序預測在本質上是外推問題,而內插的成績單對外推能力隻字未提。這個實驗刻意用了一個外推很爛的模型(高次多項式趨勢),讓兩種切法的分歧最大化;但別因此以為這是人造病例——動能因子挖掘、波動模型調參、任何含超參數搜尋的策略回測,只要切分方式錯了,都在系統性地產出這種「11.3 vs 9772」的幻覺。實務上的 walk-forward 還有一個選擇題:訓練窗要「固定長度滑動」還是「錨定起點越長越好」——前者對制度變遷更敏捷,後者樣本更足,沒有免費午餐,視資料的平穩程度取捨。

九、PCA:把整條殖利率曲線壓成三個數字

本章最後一個工具處理另一個維度的問題:不是「一條序列往前看」,而是「一把序列一起看」。金融資料常常是高維時序:一條公債殖利率曲線同時有十幾個期限(3 個月、2 年、10 年、30 年……),每天全體一起跳動;一個投資組合有幾百檔持股同時產生報酬。維度一高,共變異數矩陣的參數數量平方成長,風險管理與建模都吃不消。但金融高維資料有個福音:變數之間高度相關——整條殖利率曲線通常一起升降,個股大面積跟著大盤走。高度相關代表有效維度遠低於表面維度,壓縮有利可圖。

主成分分析(PCA)是最經典的壓縮法。白話機制:在資料雲裡找一根方向軸,讓所有觀測投影上去之後的變異數最大——這是第一主成分,資料最主要的「集體運動模式」;然後在與它垂直的方向裡再找變異最大的軸,得到第二主成分;依此類推。數學上,這些軸就是資料共變異數矩陣的特徵向量,各軸攜帶的變異數就是對應的特徵值——把特徵值由大到小排,前幾名的累積佔比就是「壓縮保真率」。只保留前 m 個主成分,就把 n 維資料壓成 m 維,而且保證這是「丟掉的變異數最少」的線性壓縮。附贈一個性質:各主成分之間互不相關——PCA 同時是一台去相關機器,這對下游建模(各因子可以分開建一條 AR)非常友善。

殖利率曲線是 PCA 在金融裡的成名作。對曲線的逐期變動做 PCA,幾乎總是得到三個形狀穩定的因子:水平(level)——所有期限同向載荷,整條曲線一起平移,通常吃掉八九成變異;斜率(slope)——短端與長端反向載荷,曲線變陡或變平,對應央行升降息週期;曲率(curvature)——兩端與中段反向載荷,曲線的彎腰程度。本章作業三會讓你用模擬資料親手跑出這三張臉:十個期限的曲線,前三個主成分解釋 99.1% 的變異——三十維的資產配置問題,實際上是一個三維問題。這對利率風險管理是革命性的簡化:對沖「水平、斜率、曲率」三個因子,就對沖掉了幾乎全部的曲線風險。

比喻: 一條掛滿衣服的晾衣繩。看起來每個點都能自由上下,自由度無限;實際上它的動作幾乎全由三種模式組成——整條一起升降(兩端的桿子同步調高低)、一端翹起一端下沉(單邊桿子調整)、中間下垂兩端翹(衣服掛太多)。記下這三個「模式的用力程度」,你就能八九不離十地重建整條繩子的形狀。PCA 做的就是從資料裡自動找出這幾種主導模式——殖利率曲線的水平、斜率、曲率,就是債券市場那條晾衣繩的三種晃法。

十、鋪給第 8 章的路

收攏本章,把埋好的線一次拉出來。本章的每個模型都是一句「可完成的句子」,第 8 章負責接下半句:AR(p) 是固定線性記憶——線性激活的 RNN 就是它的推廣,把記憶做成可學習的非線性狀態;指數平滑是係數固定的單狀態遞迴——α-RNN 把 α 變成參數,遺忘門再把 α 變成情境函數;GARCH 說「條件變異數有自己的動態」——神經網路版的波動模型把這個動態做成非線性;PCA 是線性壓縮——第 8 章的自編碼器把投影換成非線性編碼器,而且線性激活的自編碼器可以證明就落回 PCA。一句話總結第二部的路線圖:計量模型是神經網路的特例,神經網路是計量模型的鬆綁。鬆綁不是免費的——每鬆一個假設,多一批參數要餵、多一種過擬合要防,而本章的 walk-forward 就是那道不隨模型升級而失效的裁判制度。下一章(第 7 章)先處理另一種鬆綁:當序列背後存在「看不見的狀態」——牛市熊市、平靜恐慌——隱馬可夫模型與粒子濾波怎麼把狀態從資料裡撈出來。

§03原書對照

本課以白話重組了原書第六章的骨架,以下內容原書有、但本課未展開,供進階讀者按頁碼深入。其一,章首對資料頻率的討論:觀測頻率如何決定模型的頻率,以及預測週先行目標時「以日資料建模再推五步」勝過「只用週資料」的設計理由(pp.191–192)。其二,完整的定義鏈:隨機過程、時間序列、自共變異數、弱平穩、自相關、白噪音的正式定義依序給出,並附「衝擊、擾動、創新」的術語對照(pp.193–194)。其三,滯後算子 L 與滯後多項式的緊湊記法、AR(1) 以算子逆展開的推導,以及脈衝響應函數 IRF 為幾何衰減的說明(pp.194–195)。其四,以 Cayley–Hamilton 定理把特徵方程求根化為伴隨矩陣求特徵值的完整構造(pp.196–197,式 6.9–6.13)。其五,偏自相關的正式定義——以正交投影控制中間滯後——與 Yule–Walker 方程的行列式解法,並用兩條不同路徑證明 AR(1) 的 lag-2 偏自相關恰為零(pp.197–199,式 6.14–6.22)。其六,精確概似與條件概似的區分:AR(1) 首項的邊際分布如何進入概似、以及線性模型下條件最大概似等價於 OLS 的論證(pp.199–200,式 6.23–6.24)。其七,異變異數 AR 模型的兩步估計程序與對角共變異數矩陣形式的概似函數(pp.200–201,式 6.25)。其八,Wold 分解定理與 AR(1) 改寫為 MA(∞) 後均值與變異數的推導(pp.201–202,式 6.27)。其九,GARCH(p,q) 一般式、l 步波動預測的逐步遞推,及以係數和 0.97 對應約 23 天半衰期的數值例(pp.202–204,式 6.28–6.34)。其十,指數平滑權重展開至首筆觀測的完整式與半衰期公式(p.204,式 6.35–6.38)。其十一,ADF 檢定、AIC 的定義與「AIC 是事後估計、機器學習罰項是直接最小化」的方法論對比,以及 Ljung–Box 診斷的統計量、自由度與判讀圖例(pp.205–209,式 6.44–6.46)。其十二,二元事件預測完整例:對 log-odds 建 ARMA、混淆矩陣、以卡方統計量檢定分類器是否優於白噪音,及真陽性率、偽陽性率、精確率的逐項定義與 ROC、F1 的數值計算(pp.210–213,例 6.1)。其十三,PCA 的變分推導:載荷向量為共變異數矩陣特徵向量的證明、截斷重建誤差的 Frobenius 範數刻畫與解的非唯一性,及 PCA 作為去相關轉換用於去噪的註記(pp.213–217,式 6.50–6.52)。其十四,章末總結提及演算法交易中預測要「遠到足以實現」的實務考量、業界常以移動平均為輸入的線性迴歸取代 GARCH(pp.217–218)、習題 6.1–6.6(pp.218–219),與附錄的診斷檢定總表(p.220,表 6.3)。原書第六章對應印刷頁 pp.189–220。

§04作業和解答

作業一:模擬 AR(2),用 Yule–Walker 反推係數

Yule–Walker 方程把 AR(p) 的係數與自相關函數連在一起。對 AR(2),前兩階自相關 \(\tau_1, \tau_2\) 滿足線性方程組 \(\tau_1 = \phi_1 + \phi_2 \tau_1\)、\(\tau_2 = \phi_1 \tau_1 + \phi_2\)。請:(1) 模擬一條 \(\phi_1 = 0.5, \phi_2 = 0.3\) 的 AR(2)(沿用第三節的設定);(2) 由真實係數推出理論自相關 \(\tau_1, \tau_2\);(3) 從模擬資料估計樣本自相關,解 Yule–Walker 方程組反推 \(\hat\phi_1, \hat\phi_2\),與真值比較。

解答 SOLUTION
PYTHON
import numpy as np

rng = np.random.default_rng(6)
phi1, phi2 = 0.5, 0.3
T = 2000

eps = rng.normal(0.0, 1.0, T + 100)
y = np.zeros(T + 100)
for t in range(2, T + 100):
    y[t] = phi1 * y[t-1] + phi2 * y[t-2] + eps[t]
y = y[100:]

# (2) 理論自相關:tau1 = phi1/(1-phi2);tau2 = phi2 + phi1*tau1
tau1_theo = phi1 / (1 - phi2)
tau2_theo = phi2 + phi1 * tau1_theo
print(f"理論 tau1 = {tau1_theo:.4f}, tau2 = {tau2_theo:.4f}")
# 輸出:理論 tau1 = 0.7143, tau2 = 0.6571

# (3) 樣本自相關 -> 解 Yule-Walker
def acf_at(s, j):
    s = s - s.mean()
    return (s[j:] * s[:-j]).sum() / (s**2).sum()

t1, t2 = acf_at(y, 1), acf_at(y, 2)
R = np.array([[1.0, t1], [t1, 1.0]])      # 自相關矩陣
phi_hat = np.linalg.solve(R, np.array([t1, t2]))
print(f"樣本 tau1 = {t1:.4f}, tau2 = {t2:.4f}")
print(f"反推 phi1_hat = {phi_hat[0]:.4f}, phi2_hat = {phi_hat[1]:.4f}")
# 輸出:
# 樣本 tau1 = 0.7248, tau2 = 0.6567
# 反推 phi1_hat = 0.5242, phi2_hat = 0.2768(真值 0.5 與 0.3)

原理:把 AR(2) 兩邊同乘 \(y_{t-1}\)(或 \(y_{t-2}\))再取期望,因為 \(\epsilon_t\) 與過去無關,期望項全部化成自共變異數,除以 \(\gamma_0\) 就得到題目給的兩條方程——這說明 AR 係數與自相關結構互相決定:知道係數能寫出整條自相關函數,量到自相關就能反解係數。樣本反推值 (0.5242, 0.2768) 與真值 (0.5, 0.3) 的落差來自樣本自相關的估計誤差(T = 2000 下 \(\tau\) 的標準誤約 0.02),樣本放大十倍會看到它繼續收攏。第三節的 PACF 演算法(逐階配適迴歸)本質上就是在逐階解 Yule–Walker,兩題互為表裡。

作業二:GARCH(1,1) 的波動預測與半衰期

沿用第五節的模擬(\(\alpha_0 = 2 \times 10^{-6}, \alpha_1 = 0.08, \beta_1 = 0.90\))。請:(1) 找出模擬路徑上條件變異數最高的時點,作為「風暴日」;(2) 從風暴日出發,用閉式 \(\hat\sigma^2_{t+l} = \bar\sigma^2 + (\alpha_1+\beta_1)^l(\sigma_t^2 - \bar\sigma^2)\) 算 l = 1, 10, 34, 68, 200 的波動預測;(3) 驗證「偏離長期水準的剩餘比例」在 l ≈ 半衰期時約為一半,並解釋這條曲線對風險管理的意義。

解答 SOLUTION
PYTHON
import numpy as np

rng = np.random.default_rng(11)
a0, a1, b1 = 2e-6, 0.08, 0.90
T = 1500

sig2 = np.zeros(T); r = np.zeros(T)
sig2[0] = a0 / (1 - a1 - b1)
for t in range(1, T):
    sig2[t] = a0 + a1 * r[t-1]**2 + b1 * sig2[t-1]
    r[t] = np.sqrt(sig2[t]) * rng.normal()

uncond = a0 / (1 - a1 - b1)                   # 長期變異數 1.00e-04
s0 = sig2.max()                               # (1) 風暴日
print(f"風暴日條件變異數 = {s0:.2e}(長期水準 {uncond:.2e})")
# 輸出:風暴日條件變異數 = 3.55e-04(長期水準 1.00e-04)

for l in (1, 10, 34, 68, 200):                # (2)(3) l 步預測
    fc = uncond + (a1 + b1)**l * (s0 - uncond)
    pct = (fc - uncond) / (s0 - uncond) * 100
    print(f"l = {l:>3d}: 預測變異數 {fc:.2e},偏離剩餘 {pct:.1f}%")
# 輸出:
# l =   1: 預測變異數 3.50e-04,偏離剩餘 98.0%
# l =  10: 預測變異數 3.08e-04,偏離剩餘 81.7%
# l =  34: 預測變異數 2.28e-04,偏離剩餘 50.3%
# l =  68: 預測變異數 1.64e-04,偏離剩餘 25.3%
# l = 200: 預測變異數 1.04e-04,偏離剩餘 1.8%

驗證:\(\ln(0.5)/\ln(0.98) \approx 34.3\),而 l = 34 時偏離剩餘 50.3%——半衰期公式與逐步預測完全吻合;到 l = 68(兩個半衰期)剩 25.3%,幾何衰減的簽名清清楚楚。對風險管理的意義:風暴日當天的變異數是長期水準的 3.5 倍,若據此設定部位上限,這個限制不該一天就解除、也不該永遠維持——GARCH 給出的衰減曲線就是「戒嚴逐步解除的時間表」:約一個半月後波動預期回到長期水準的一半路上,兩百天後風暴的影響基本出清。任何用固定視窗算歷史波動的做法(例如等權 60 日)都畫不出這條平滑的收斂路徑,這是條件變異數建模的實務價值所在。

作業三:模擬殖利率曲線,用 PCA 拆出三因子

模擬一組殖利率曲線的月變動資料:十個期限(0.25 到 30 年),每月的曲線變動由三個潛在因子驅動——水平(所有期限等幅)、斜率(隨期限由負轉正)、曲率(中段與兩端反向)——再加小量雜訊。請對 750 個月的模擬資料做 PCA:(1) 報告前幾個主成分的解釋變異比例;(2) 畫出(或印出)前三個主成分的載荷向量,對照「水平、斜率、曲率」的形狀;(3) 說明為什麼 PCA 能從「只有雜訊疊加的觀測」還原出三張臉。

解答 SOLUTION
PYTHON
import numpy as np

rng = np.random.default_rng(20)
mats = np.array([0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30])   # 期限(年)
lam = 0.5
load_slope = (1 - np.exp(-lam * mats)) / (lam * mats)      # 斜率因子載荷
load_curv = load_slope - np.exp(-lam * mats)               # 曲率因子載荷
T = 750

dL = rng.normal(0.0, 0.06, T)      # 水平因子月變動
dS = rng.normal(0.0, 0.10, T)      # 斜率因子月變動
dC = rng.normal(0.0, 0.08, T)      # 曲率因子月變動
dY = (dL[:, None] * np.ones(len(mats))
      + dS[:, None] * load_slope
      + dC[:, None] * load_curv
      + rng.normal(0.0, 0.01, (T, len(mats))))             # 觀測雜訊

Y0 = dY - dY.mean(axis=0)                  # 去均值
cov = Y0.T @ Y0 / T                        # 共變異數矩陣
w, V = np.linalg.eigh(cov)                 # 特徵分解
order = np.argsort(w)[::-1]
w, V = w[order], V[:, order]

expl = w / w.sum()
print("解釋比例:", np.round(expl[:4] * 100, 1), "%")
print("前三累積:", round(expl[:3].sum() * 100, 1), "%")
# 輸出:解釋比例: [91.   7.5  0.7  0.2] %;前三累積: 99.1 %

for k in range(3):
    v = V[:, k] * np.sign(V[:, k].sum() or V[0, k])
    print(f"PC{k+1} 載荷:", np.round(v, 2))
# 輸出:
# PC1 載荷: [0.42 0.41 0.39 0.35 0.32 0.28 0.25 0.23 0.2  0.19]
# PC2 載荷: [-0.41 -0.34 -0.23 -0.05  0.08  0.22  0.3   0.35  0.43  0.45]
# PC3 載荷: [ 0.39  0.21 -0.05 -0.33 -0.42 -0.35 -0.2   0.02  0.33  0.49]

判讀:(1) 第一主成分吃掉 91.0% 的變異,前三個合計 99.1%——十維的曲線變動,實際上活在三維裡。(2) 三張臉都認得出來:PC1 載荷全為正——所有期限同向移動,是水平;PC2 載荷從 −0.41 單調爬到 +0.45、在 2 至 3 年期附近換號——短端與長端反向,是斜率;PC3 兩端為正、中段(2 到 7 年)為負——是曲率。注意 PC1 載荷並非完全等權(短端 0.42 大於長端 0.19),因為模擬裡斜率、曲率因子與水平因子在共變異數上互相混色,PCA 找的是「變異最大的正交方向」,不保證與生成因子一一對齊——真實市場資料的 PCA 也有同樣性質,因子是統計建構,經濟命名是人給的。(3) 能還原的原因:三個潛在因子的載荷向量張出一個三維子空間,觀測資料的變異幾乎全部落在這個子空間內,雜訊只貢獻微小的等向膨脹;PCA 按變異數大小排軸,自然先把這個子空間的三根軸挑出來,雜訊被留在後面七個特徵值裡(合計不到 1%)。

作業四:三條序列的平穩性判讀

有三條序列,生成機制分別是:(a) \(y_t = 0.2 + 0.95\,y_{t-1} + \epsilon_t\);(b) \(y_t = y_{t-1} + 0.1 + \epsilon_t\);(c) \(y_t = 0.05\,t + \epsilon_t\),其中 \(\epsilon_t\) 都是白噪音。請判斷:(1) 哪些平穩、哪些不平穩,各屬於哪一型?(2) 對不平穩者,正確的轉換各是什麼?(3) 若對 (c) 誤用差分會發生什麼事?

解答 SOLUTION

(1) (a) 平穩:這是 AR(1),\(\phi = 0.95 < 1\),特徵根 \(1/0.95 \approx 1.053\) 在單位圓外,長期均值收斂到 \(0.2/(1-0.95) = 4\)。但要注意它是「近單位根」:0.95 離 1 很近,短樣本裡它走起來跟隨機漫步幾乎難以分辨(半衰期 \(\ln 0.5/\ln 0.95 \approx 13.5\) 期,均值回歸很慢),ADF 檢定在小樣本常常拒絕不了單位根——平穩性在理論上是零一問題,在有限樣本裡是程度問題。(b) 不平穩,隨機漫步型(隨機趨勢):單位根加上每期 0.1 的漂移,均值隨 t 線性成長、變異數也隨 t 線性成長,衝擊永久累積。(c) 不平穩,趨勢平穩型(確定趨勢):均值 \(0.05t\) 隨時間走,但圍繞趨勢的偏離就是白噪音本身,衝擊完全不累積——它的「不平穩」只是掛在一條確定的斜線上。

(2) (b) 的解是差分:\(\Delta y_t = 0.1 + \epsilon_t\),一刀落地成「常數加白噪音」,完全平穩。(c) 的解是去趨勢:把 \(y_t\) 對 t 迴歸、取殘差,殘差就是白噪音。口訣:隨機趨勢用差分,確定趨勢用去趨勢。

(3) 對 (c) 差分得 \(\Delta y_t = 0.05 + \epsilon_t - \epsilon_{t-1}\):均值沒問題,但雜訊部分變成了係數為 −1 的 MA(1)——一階自相關被人為壓到 −0.5,而且這個 MA 的根落在單位圓上(不可逆),最大概似估計會在參數邊界上打轉、標準誤失真,模型變得更難估而不是更好估。這叫過度差分:差分不是安全的萬靈丹,它專治「衝擊會累積」的隨機趨勢;對衝擊本來就不累積的序列動刀,等於把不存在的病開了刀,還留下新的疤。實務判準:先用 ADF 檢定判斷單位根是否存在,存在才差分;差分後若發現殘差出現強烈的負一階自相關,回頭懷疑自己差過頭了。

§05參考資料