iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

台積電 2024 年 6 月 13 日除息 3.4998 元。除息前收盤 909,交易所公告的除息參考價是 905.5。

那 3.5 元的差額不是下跌,是配息——它從股價裡出來,變成現金進你的帳戶。

有趣的是,那天股價還漲了,收在 919。所以你根本看不到「跳空」這件事:

用原始價算當日報酬     919 / 909 − 1        = +1.10%
把配息加回去           (919 + 3.5) / 909 − 1 = +1.49%

差 0.39 個百分點。一天。

一年四次,二十年。台積電從 2006 年到現在:

原始價   61.0 → 2,400     +3,834%    年化 19.41%
還原價  28.21 → 2,400     +8,408%    年化 23.94%

**不還原股價,台積電二十年的報酬會少算一半以上。**每年 3.8 個百分點的複利差距。

而這件事在單日尺度上幾乎看不出來——那天它甚至是漲的。


三種會製造假跳空的事件

除權息只是其中一種。這 20 年的台股資料裡,總共有三種公司行為會讓價格跳空,而它們跳的方向不一樣:

三種事件的係數分布

              筆數      中位係數    範圍
除權息       30,320     0.962     0.09 ~ 1.18
減資            670     1.363     0.92 ~ 40.04
ETF 分割         11     2.000     0.04 ~ 7.00

除權息:配息配股,價格往下跳。係數幾乎都小於 1。

減資:股數被銷除,你手上的股票變少但每股更值錢,價格往上跳。中位數 1.363 表示典型的減資讓股價漲三成六。極端值 40.04 是易福(5364)在 2013 年 12 月的減資——1.15 元變成 46.05 元。

ETF 分割:一單位拆成多單位,價格等比例下降。反分割則相反。只有 11 筆,但其中一筆是 0050。

三種的處理方式是同一條公式:

調整係數 = 事件後的參考價 ÷ 事件前的收盤價
某一天的還原係數 = 那天之後所有事件係數的連乘積
還原價 = 原始價 × 還原係數

方向由係數自己決定,不需要為每種事件寫不同的邏輯。除權息的係數小於 1,會把歷史價格往下調;減資的係數大於 1,往上調。


為什麼用交易所的參考價,不自己算

配息 3.5 元,把歷史價格減 3.5 不就好了?

不行,因為除權息可能同時配股。台積電只配現金,但很多公司是「配息 2 元 + 配股 1 元」,配股會讓股數增加,這不是減法能處理的。而且遇到現金增資、減資後現金增資這種組合,公式會越長越複雜。

交易所每天會公告除權息參考價——那是它自己算好的、把所有成分都納入的結果。我直接用兩個欄位:

before_price   停止買賣前(或除權息前)的收盤價
ref_price      交易所公告的參考價

減資和分割也是同樣兩個欄位。所以三種事件可以合併成同一串,用同一段程式處理:

_RATIO_BOUNDS = {"dividend": (0.01, 1.5), "reduction": (0.05, 100.0),
                 "split": (0.01, 100.0)}

for kind, src in (("dividend", dividends), ("reduction", reductions),
                  ("split", splits)):
    ev = src[["date", "stock_id", "before_price", "ref_price"]].copy()
    ev["ratio"] = ev["ref_price"] / ev["before_price"]
    lo, hi = _RATIO_BOUNDS[kind]
    ev = ev[ev["ratio"].between(lo, hi)]

那個 _RATIO_BOUNDS 是用來擋髒資料的(參考價為 0 之類),不是業務規則。上界我一開始給減資設 20,結果易福那筆 40.04 直接被丟掉——所以現在被擋下來的事件會印出來,不是無聲跳過。


用已知答案驗證

回測引擎最難的地方是你不知道正確答案是什麼。所以我先做一組答案已知的合成資料。

一檔股票,10 天:前 5 天 10 元,第 6 天減資(10 → 20),維持 3 天,第 9 天除息(20 → 18),最後 2 天 18 元。

這檔股票的真實報酬是——每次跳空都是公司行為造成的,沒有任何真實的漲跌。所以正確的還原序列應該是一條水平線。

合成資料驗證

完全不還原      10 → 20 → 18     憑空多出 100% 的漲幅
只處理除權息     9 → 18 → 18     減資那天還是有 100% 的假漲幅
除權息 + 減資   18 → 18 → 18     水平線,正確

這個測試的價值在於:**如果我哪天改壞了程式,這條線會立刻不是水平的。**它比「拿真實資料跑一跑看起來合理」可靠得多,因為真實資料沒有標準答案。


一個真實案例:0050 的分割

0050 在 2025 年 6 月 18 日做了受益權單位分割,一單位拆成四單位。

0050 原始價 vs 還原價

原始價從 188.65 掉到 47.16。如果不處理,這一天在資料裡就是 −75% 的單日暴跌

我是怎麼發現的?做 Day 3 的實驗時,0050 的最大回落算出來是 −75.98%。台股 2008 年再慘也沒有這麼慘,所以我去查了,才發現分割這件事,也才發現我的還原股價根本沒有分割的資料源。

補上之後:

0050 最大回落   -75.98%  →  -55.75%

−55.75% 是 2008 年金融海嘯造成的,那是真的。

分割的資料在 TWSE 的 split/TWTCAU(ETF 分割/反分割恢復買賣參考價格)。它給的是官方參考價:

恢復買賣日期  ETF代號  名稱        分割    停止買賣前收盤  恢復買賣參考價
114/06/18    0050   元大台灣50   分割      188.65        47.16

188.65 ÷ 47.16 = 4.0002。這比我從價格反推「大概是 1 拆 4」可靠得多——反推會把當天的市場漲跌也算進去。


還原是後復權,所以每次都要重算

我用的是後復權:最新一天等於實際成交價,越往前調整幅度越大。

這代表每次抓到新的除權息事件,整條歷史序列都會變。台積電今天除息,那麼它 2006 年的還原價明天就跟今天不一樣。

所以 transform 不是抓完資料之後的一次性工作,是每天都要跑的:

python3 -m crawler fetch dividend reduction split
python3 -m crawler transform

我把它放進每日排程的最後一步。如果忘了跑,歷史係數就會過期——而且這種過期不會報錯,只會讓你的報酬計算悄悄偏掉一點點。

實作上有一個細節值得提:交易所的減資表會預告未來兩週的事件。如果把未來事件算進去,後復權會把那個係數乘到每一天,連最新一天都被放大,「最新一天等於實際成交價」這個性質就破了。所以 transform 會先濾掉晚於最後一個交易日的事件:

last_px = px["date"].max()
future = ev["date"] > last_px
if future.any():
    log.info("略過 %d 筆尚未生效的事件(晚於最後一個交易日 %s)",
             int(future.sum()), last_px.date())
    ev = ev[~future]

上一篇
Day 7 - 它不會報錯,只會給你錯的東西
系列文
台股日頻量化交易:開發一條從資料、特徵、模型到每日下單清單的產線8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言