跳至主要內容
台股研究 約 10 分鐘閱讀

Python 時間序列教學:pandas 整理台股股價,
從 resample 到動能回測

Python 時間序列完整教學:用 pandas 把台股股價整理成以日期為索引、股票代號為欄位的時間序列寬表,逐步示範 resample、rolling、shift、pct_change 四個核心工具,並用台股全市場真實資料實際回測時間序列動能策略:2018 至 2026 年化報酬 20.53%、最大回撤 -30.8%,同窗口 0050 含息年化報酬 25.05%,輸贏的原因與回測方法限制文中逐項交代,完整程式碼與每日淨值數據都可下載重現。

從 pandas 寬表到 TSMOM 動能回測,一篇學完。 CAGR 20.5% Sharpe 1.08

把零散的每日股價整理成「以日期為索引、股票代號為欄位」的時間序列寬表,是所有量化分析的起點;移動平均、波動度、動能訊號、回測,全部建立在這張表上。這篇教學用台股全市場的真實資料,從 pandas 的四個核心工具教起,一路做到一個可重現的時間序列動能(TSMOM)回測,訊號、權重、成本都有程式碼與數據對應,整段流程可以下載重跑。

Python 時間序列動能 TSMOM 策略與 0050 含息的權益曲線對照圖

關鍵數字

回測區間 2018-01-01 至 2026-06-09,策略含 finlab 預設交易成本,0050 為含息買進持有(不含成本,口徑見「回測方法與限制」):

指標 時間序列動能 TSMOM 12-1 0050 含息
總報酬 375.8% 558.5%
年化報酬(CAGR) 20.53% 25.05%
日夏普 1.08 1.22
月索提諾 1.72 1.92
最大回撤(MDD) -30.8% -33.96%
平均持股 179.7 檔 1 檔

三件事先說清楚:

  • 這段期間 TSMOM 沒有贏過 0050:報酬輸、風險調整後指標也輸,只有最大回撤略淺。本文把它定位成「時間序列工具的完整應用示範」,讓你看懂每一行 pandas 在策略裡的角色,而非推薦你照抄的策略。
  • 波動度倒數加權有實際效果:同樣的訊號,從等權改成波動度倒數加權,日夏普從 0.96 升到 1.08,最大回撤從 -37.61% 縮到 -30.8%。
  • 月頻換股的成本負擔可控:手續費加倍的壓力測試下,年化報酬從 20.53% 降到 19.22%,結論不變。

時間序列是什麼?為什麼量化分析都從它開始

時間序列(time series)是按時間順序排列的一連串觀測值。對股票來說,最常用的形式是一張二維表:每一列是一個交易日,每一欄是一檔股票,格子裡是當天的收盤價。這張「寬表」之所以重要,是因為 pandas 對它的每一種操作都是向量化的:算 60 日均線是一行程式,算全市場兩千多檔股票的 60 日均線,還是同一行程式。

回測的本質就是在這張表上做時間平移與聚合:用過去的資料算訊號,套到未來的報酬上驗證。所以在寫任何策略之前,先把資料整理成乾淨的時間序列寬表,並熟悉幾個核心操作,後面的事情都會變簡單。想看這套流程在完整交易系統裡的位置,可以先讀程式交易完整指南

報酬率與波動度:你會反覆用到的三條公式

簡單報酬是兩期價格的變化率:

rt=PtPt11r_t = \frac{P_t}{P_{t-1}} - 1

對數報酬取自然對數:

r~t=ln ⁣(PtPt1)\tilde{r}_t = \ln\!\left(\frac{P_t}{P_{t-1}}\right)

兩者的關鍵差異在「跨期累積」的方式:簡單報酬要連乘,對數報酬可以相加,

t=1Tr~t=ln ⁣(PTP0)\sum_{t=1}^{T} \tilde{r}_t = \ln\!\left(\frac{P_T}{P_0}\right)

這讓對數報酬在統計分析(加總、平均、迴歸)時特別好用;要報給人看的績效,再轉回簡單報酬。

年化波動度把日報酬標準差按一年約 252 個交易日縮放:

σann=σdaily×252\sigma_{\text{ann}} = \sigma_{\text{daily}} \times \sqrt{252}

用一個虛構的計算示例走一遍(以下數字純屬示範,非任何真實股票):某檔「示範股」連續三天的收盤價是 100、110、99 元。兩天的簡單報酬是 +10% 與 -10%,連乘 1.1×0.9=0.991.1 \times 0.9 = 0.99,累積報酬是 -1%,而非直覺的 0%。對數報酬則是 ln(1.1)=+9.53%\ln(1.1) = +9.53\%ln(0.9)=10.54%\ln(0.9) = -10.54\%,兩者相加 -1.01%,正好等於 ln(0.99)\ln(0.99)。再看波動度:若某股日報酬標準差是 2%,年化波動度就是 2%×25231.7%2\% \times \sqrt{252} \approx 31.7\%。對照真實資料,台積電(2330)截至 2026-06-09 的 60 日年化波動度是 34.5%,量級相近。

用 finlab 取得台股時間序列寬表

2020 年這篇文章的第一版,教的是自己寫 TWSE 爬蟲再用 transpose() 拼表(原始做法保留在文末附錄)。現在更穩的主路徑是用 finlab 套件,兩行就拿到整理好的寬表:

顯示程式碼
# pip install finlab
from finlab import data
 
# 收盤價:date x 股票代號 的時間序列寬表
close = data.get("price:收盤價")
 
# 還原股價:已調整除權息,適合算報酬與回測
adj = data.get("etl:adj_close")

第一次執行時 finlab 會自動引導登入,照指示操作即可。兩張表都是「日期 × 股票代號」的 DataFrame,索引是 DatetimeIndex,可以套用 pandas 所有時間序列功能。

要特別記住 price:收盤價etl:adj_close 的分工:收盤價是市場上看到的原始價格,但除權息當天會跳空,用它算報酬會把配股配息誤判成下跌;還原股價把這些調整補回去,算報酬、算動能、跑回測一律用它。本文後面所有報酬計算都基於 etl:adj_close

pandas 時間序列四大工具:resample、rolling、shift、pct_change

pct_change:一行算出全市場日報酬

顯示程式碼
# 全市場每日簡單報酬
returns = adj.pct_change()
 
# 過去 60 個交易日的累積報酬
returns_60d = adj.pct_change(60)

pct_change(n) 算的是相隔 n 列的變化率,正是上面簡單報酬公式的向量化版本。

resample:改變時間頻率

顯示程式碼
# 月底取樣:每個月留最後一個交易日的價格
monthly_close = adj.resample("ME").last()
 
# 月報酬
monthly_returns = monthly_close.pct_change()

resample 是「換頻率再聚合」:日資料變月資料、變週資料,聚合方式可以是 last()mean()max() 等。月頻再平衡的回測,就是靠它把訊號對齊到每月一次。

rolling:移動視窗統計

顯示程式碼
# 60 日移動平均
ma60 = adj.rolling(60).mean()
 
# 60 日年化波動度
vol_60d = adj.pct_change().rolling(60).std() * (252 ** 0.5)

rolling(n) 在每個時點往回看 n 列,名稱裡的「移動」就是視窗隨日期往前滑。移動平均公式

MAn(t)=1ni=0n1Pti\mathrm{MA}_n(t) = \frac{1}{n}\sum_{i=0}^{n-1} P_{t-i}

對應的程式就是 rolling(n).mean()。下圖用台積電示範 resamplerolling 的差別:月底取樣是「抽稀」,每月只留一點;60 日均線是「平滑」,每天都有值但把短期雜訊磨掉。

pandas resample 與 rolling 在台積電 2330 還原股價上的差異示意圖

shift:把資料往後搬,避免用到未來

顯示程式碼
# 12-1 動能:過去 240 個交易日的報酬,剔除最近 20 個交易日
momentum_12_1 = adj.shift(20) / adj.shift(240) - 1

shift(n) 把整張表往後搬 n 列,是回測防前視偏差的關鍵:今天能用的訊號,只能由昨天以前的價格組成。上面這行同時用了兩個 shift,分子是 20 天前的價格、分母是 240 天前的價格,算出「過去 12 個月剔除最近 1 個月」的報酬,這正是動能研究的標準定義。

真實資料長什麼樣子:厚尾與波動聚集

工具會用之後,看兩個台股時間序列的真實性質。第一個是厚尾:把 2018 至 2026 年流動性池內所有股票的日報酬畫成分佈,超額峰度達 2.26(常態分佈為 0),極端漲跌出現的頻率遠高於常態分佈的預測;分佈兩端 ±10% 附近的突起,是台股漲跌幅限制造成的獨特印記。Cont (2001) 整理了跨市場金融時間序列的典型化事實,厚尾與波動聚集名列前茅,台股完全不例外。

台股日報酬分佈與常態分佈對照,超額峰度 2.26 顯示厚尾

第二個是波動聚集。用 autocorr() 算台積電日報酬的自相關:報酬本身的 lag-1 自相關只有 -0.021,幾乎是零,但「報酬絕對值」的 lag-1 自相關高達 0.148,而且往後二十天都顯著為正。意思是:明天漲跌的方向很難從今天預測,但明天的「波動大小」可以;大波動後面跟著大波動。

台積電日報酬與報酬絕對值的自相關對照,呈現波動聚集

這兩個性質有學術根據也有實作意義。Lo & MacKinlay (1988) 用變異數比檢定拒絕了「股價是隨機漫步」的假設,發現指數與投資組合的週報酬存在顯著正自相關,個股層級反而偏弱,與我們算出的 2330 個股自相關接近零一致。實作上,「波動度可預測」正是下一節策略用波動度倒數加權的理由:波動度是少數真的能用昨天預測明天的量。

時間序列動能(TSMOM):把四個工具串成一個策略

動能策略有兩種問法。橫斷面動能問「誰比別人強」:把全市場按過去報酬排名,買前段班;我們在動能投資 18 年實測裡測的就是這種,搭配基本面因子後年化 32.27%。時間序列動能(TSMOM)問的是另一個問題:「這檔股票比自己的過去強嗎?」每檔股票只跟自己比,過去 12 個月(剔除最近 1 個月)報酬為正就持有,為負就空手,不需要橫向排名。

這個機制的出處是 Moskowitz, Ooi & Pedersen (2012):他們檢驗 58 個期貨與外匯市場,發現資產自身過去 12 個月的超額報酬正負號,對未來 1 到 12 個月的報酬有預測力,且部位用波動度縮放後,風險調整績效更好。本文把這個機制搬到台股現股、做純多頭版本:

wi,t=1 ⁣{ri,t(12-1)>0}σi,t1j1 ⁣{rj,t(12-1)>0}σj,t1w_{i,t} = \frac{\mathbf{1}\!\left\{ r^{(12\text{-}1)}_{i,t} > 0 \right\} \cdot \sigma_{i,t}^{-1}}{\sum_{j} \mathbf{1}\!\left\{ r^{(12\text{-}1)}_{j,t} > 0 \right\} \cdot \sigma_{j,t}^{-1}}

其中 1{}\mathbf{1}\{\cdot\} 是訊號指示函數(12-1 自身報酬為正取 1,否則 0),σi,t\sigma_{i,t} 是個股 60 日年化波動度。波動度越大的股票拿越小的權重,全部權重正規化後總和為 1。下圖把訊號畫在台積電的股價上:綠色底色是 12-1 自身報酬為正的持有區間,可以看到訊號在 2022 年的長空頭裡大段退出,又在趨勢恢復後重新進場,但每次轉折都有遲到的代價。

台積電 2330 股價與時間序列動能 12-1 訊號持有區間視覺化

策略核心程式碼如下,每一行都對應前面教過的工具:

顯示程式碼
import numpy as np
from finlab import data
from finlab.backtest import sim
 
# 載入還原股價與成交金額
adj = data.get("etl:adj_close")
amount = data.get("price:成交金額")
 
# 流動性池:近 20 日成交金額移動平均,排名前 250 檔
liquidity = amount.rolling(20).mean()
pool = liquidity.rank(axis=1, ascending=False) <= 250
 
# 訊號:12-1 自身報酬 > 0(shift 防前視)
momentum_12_1 = adj.shift(20) / adj.shift(240) - 1
signal = (momentum_12_1 > 0) & pool
 
# 權重:60 日年化波動度的倒數(rolling 算波動)
daily_returns = adj.pct_change()
vol_60d = daily_returns.rolling(60).std() * np.sqrt(252)
 
weights = (1.0 / vol_60d).where(signal)
position = weights.div(weights.sum(axis=1), axis=0).fillna(0)
 
# 回測:每月初再平衡,finlab 台股預設成本
report = sim(position, resample="M", upload=False)

回測設定與結果

設定整理如下:

項目 設定
回測區間 2018-01-01 至 2026-06-09(資料快照日)
股票池 近 20 日成交金額移動平均排名前 250
訊號 還原股價 12-1 自身報酬 > 0
權重 60 日年化波動度倒數,逐月正規化
再平衡 每月初(sim(resample="M")
交易成本 finlab sim() 台股預設:手續費 0.1425% + 賣出證交稅 0.3%
基準 0050 還原價買進持有,純指數算術、不含成本

互動式回測報告可以操作看每一期持股與績效:

完整結果,含參數變化與成本壓力測試:

版本 CAGR 日夏普 日索提諾 月索提諾 MDD 總報酬 平均持股
TSMOM 12-1 波動度倒數加權(主) 20.53% 1.08 1.27 1.72 -30.8% 375.8% 179.7 檔
TSMOM 12-1 等權 21.64% 0.96 1.16 1.55 -37.61% 413.7% 181.8 檔
TSMOM 6-1 波動度倒數加權 18.48% 0.99 1.16 1.53 -31.5% 312.4% 168.9 檔
TSMOM 12-1 流動性前 150 檔 21.9% 1.08 1.31 1.82 -33.71% 422.8% 112.4 檔
TSMOM 12-1 手續費加倍 19.22% 1.02 1.2 1.62 -31.32% 334.4% 179.7 檔
0050 含息買進持有 25.05% 1.22 1.67 1.92 -33.96% 558.5% 1 檔

怎麼解讀這個結果

在本文設定下、2018 至 2026 這段期間,每一個 TSMOM 版本的 CAGR 與夏普都輸給 0050。 主策略年化 20.53% 對 0050 的 25.05%,日夏普 1.08 對 1.22。逐年拆開看更清楚輸在哪:

時間序列動能策略與 0050 含息的逐年報酬對照長條圖

策略贏的年份是 2021(+31.66% 對 +21.92%)、2022(-18.73% 對 -21.37%)與 2023(+42.26% 對 +27.52%);輸的年份集中在 2024(+11.33% 對 +48.64%)與 2025(+18.77% 對 +36.83%),2019 與 2020 也小輸,圖中 2026 年為截至 6 月 9 日的部分年度(+61.98% 對 +60.0%,策略暫時小幅領先,半年數字不足以下結論)。原因不難理解:這兩年台股的報酬高度集中在台積電與 AI 供應鏈權值股,0050 的市值加權結構天然重押它們,而 TSMOM 平均分散在約 180 檔股票上,波動度倒數加權還會壓低高波動飆股的權重,多頭集中行情裡注定跟不上。2018 年策略也輸(-8.42% 對 -5.46%),因為 12-1 訊號對趨勢反轉的反應有約一個月的延遲,下跌初期來不及退場。

MOP 2012 的核心主張「波動度縮放改善風險調整報酬」在台股是成立的:等權版日夏普 0.96、MDD -37.61%;改成波動度倒數加權後日夏普 1.08、MDD -30.8%,代價是 CAGR 從 21.64% 降到 20.53%。用約一個百分點的年化報酬,換到淺了近 7 個百分點的回撤,這個交換對多數人是划算的。

最後講明:主策略日夏普 1.08,未達本站策略文慣用的 1.5 風險調整門檻,所以本文不把它包裝成可上線的策略,而是當成時間序列機制的完整示範。想看靠大盤訊號控制回撤的另一條路,可以接著讀大盤濾網降低回撤

穩健性檢查:換參數會不會改變結論

上表已經內建了四個方向的敏感度測試,結論對參數不敏感:

  • 回看視窗:12-1 換成 6-1,年化從 20.53% 降到 18.48%,夏普從 1.08 降到 0.99;短視窗訊號更吵、換股更頻繁,沒有更好。
  • 加權方式:等權對波動度倒數,上一節已拆解,方向一致。
  • 流動性門檻:股票池從前 250 檔縮到前 150 檔,年化 21.9%、夏普 1.08,結果略好但量級不變,結論不靠特定門檻。
  • 成本壓力:手續費加倍(模擬滑價與較差的成交品質),年化掉 1.3 個百分點到 19.22%,夏普 1.02,策略沒有被成本擊穿,但也沒有因此翻盤。

四個方向都指向同一件事:台股 2018 至 2026 的長多結構裡,純多頭 TSMOM 跑不贏市值加權的 0050,這不是某組參數挑壞了,是機制本身在這個市場環境的特性。 把它當成防守型的機制元件(空頭退場、波動控管)來理解,比把它當報酬引擎合理。

回測方法與限制

項目 本文做法
交易成本 finlab sim() 預設已內扣手續費 0.1425% 與賣出證交稅 0.3%;另測手續費加倍的壓力組
滑價 未另外假設滑價;手續費加倍組可視為粗略的滑價代理
股票池 全上市櫃中近 20 日成交金額排名前 250;排名用當時點資料,不挑事後存活的股票
流動性過濾 即上述成交金額排名;平均持股約 180 檔,單檔部位小,但本文未估算策略容量
排除類別 未排除金融股、ETF 與 KY 股;高成交金額的 ETF 可能進入股票池
前視偏差 訊號全部由 shift 後的價格組成;僅用價量資料,無財報公布日對齊問題
權重 波動度倒數加權,未另設單檔上限;因持股分散,集中度風險天然有限
周轉率 未統計具體 turnover 數值;月頻再平衡且訊號為慢速動能,換手率屬中等
樣本內外 全段 in-sample,未做樣本外測試;以參數敏感度與成本壓力測試替代部分穩健性檢查

附錄:把台股每日爬蟲資料整理成 time series(2020 年原始做法)

這篇文章 2020 年的第一版,示範自己從 TWSE 抓資料再拼成時間序列。以下程式碼保留作教學示意(TWSE 回應格式可能已變動,未持續維護),想走自建爬蟲路線的話,先讀台股每日爬蟲教學確認最新寫法:

顯示程式碼
import requests
from io import StringIO
import pandas as pd
 
def crawl_price(date):
    # 抓 TWSE 單日全市場行情,回傳以證券代號為索引的 DataFrame
    datestr = str(date).split(" ")[0].replace("-", "")
    r = requests.post(
        "https://www.twse.com.tw/exchangeReport/MI_INDEX"
        "?response=csv&date=" + datestr + "&type=ALL"
    )
    lines = [
        line.translate({ord(c): None for c in " "})
        for line in r.text.split("\n")
        if len(line.split('",')) == 17 and line[0] != "="
    ]
    ret = pd.read_csv(StringIO("\n".join(lines)), header=0)
    ret = ret.set_index("證券代號")
    return ret

逐日呼叫存進字典 data 之後,當年最受歡迎的就是這兩行轉換:把每天一張的「股票 × 欄位」表,拼成「日期 × 股票」的收盤價寬表:

顯示程式碼
close = pd.DataFrame({k: d["收盤價"] for k, d in data.items()}).transpose()
close.index = pd.to_datetime(close.index)

transpose() 把列與欄對調,to_datetime 讓索引變成可以 resamplerollingDatetimeIndex。原理到今天都沒變,只是 data.get("price:收盤價") 把抓資料、清理、拼表全部做完了,你可以把時間花在策略本身。

常見問題 FAQ

Python 做時間序列分析要用哪些套件?

核心是 pandas(資料整理、resample、rolling)與 numpy(數學運算),畫圖用 matplotlib。台股資料來源用 finlab 的 data.get() 最省事;績效統計另有現成的 ffn 套件,drawdown、年化報酬一行算完。

pandas 的 resample 和 rolling 有什麼差別?

resample 改變資料頻率:日資料變月資料,輸出的列數變少。rolling 保持頻率不變,在每個時點往回看固定長度的視窗做統計,輸出列數不變。月頻換股用 resample,算均線與波動度用 rolling

計算報酬率該用簡單報酬還是對數報酬?

做統計分析(平均、加總、迴歸)用對數報酬,因為跨期可加;對人報告績效用簡單報酬,因為符合直覺。單日層級兩者差異很小,報酬越大差異越大。

回測為什麼要用還原股價而非收盤價?

除權息當天收盤價會跳空下調,原始收盤價會把配股配息誤算成虧損。還原股價(etl:adj_close)把股利調整回價格序列,算出來的報酬才是含息總報酬。本文所有報酬與回測都用還原股價。

時間序列動能與橫斷面動能差在哪?

時間序列動能是每檔股票跟自己的過去比,報酬為正就持有,市場全弱時可以全數退場;橫斷面動能是股票之間互相排名,永遠持有相對最強的一批,市場全跌時照樣持股。兩者可以共存,動能投資 18 年實測測的是橫斷面版本。

TSMOM 在台股有效嗎?

以本文設定與 2018 至 2026 的窗口,純多頭 TSMOM 年化 20.53%、日夏普 1.08,輸給 0050 含息的 25.05% 與 1.22,僅最大回撤略淺(-30.8% 對 -33.96%)。波動度縮放改善風險調整報酬的效果有重現,但整體跑不贏市值加權大盤;把它當風險控管元件比當報酬引擎合理。

沒寫過 Python 也能跑這個回測嗎?

可以從複製開始。文末的 strategy.py 是完整可執行版本,安裝 finlab 後執行即可重現本文數字;想系統性入門,從量化交易完整指南的學習路徑開始,再回頭逐行理解本文程式碼會快很多。

延伸閱讀

免費註冊 FinLab 即可用 pip install finlab 取得本文使用的全部資料表,跑通第一個回測。

下載資源

檔案 說明
strategy.py TSMOM 12-1 完整策略程式碼,可重現本文主策略數字
equity.csv 主策略、等權版與 0050 的每日淨值序列

免責聲明:本文所有策略與回測結果僅供教學與研究用途,不構成投資建議。過去績效不代表未來表現。投資一定有風險,請審慎評估自身風險承受能力。

最後更新:2026-06|回測區間:2018-01-01 至 2026-06-09(資料快照 2026-06-09)|作者:FinLab 量化研究團隊(經量化研究員審閱)

FinLab AI

想建立自己的策略?

用自然語言描述你的選股想法,AI 自動驗證、回測、給你答案

免費開始

更多技術指標研究

查看全部