回測不會告訴你它哪裡錯了。它只給你一條曲線。
曲線很漂亮的時候,你會相信它;曲線很難看的時候,你會回去調參數。這兩種反應都不會讓你發現「這條曲線根本不該長這樣」。
所以動工之前先立規矩。四條。
每一條我都用真實台股資料量過它值多少——因為「這樣做比較嚴謹」說服不了任何人,包括我自己。
台股的月營收,M 月的數字必須在 M+1 月的 10 日以前公布。
資料表裡的欄位叫 month。以台積電為例,我們抓下來的 month = 2024-01 那一列是 2,157.9 億元——跟台積電 2024 年 1 月的實際營收一致。所以這個欄位存的是營收所屬月份,不是公布月份。
那麼,month = 2024-01 這筆資料,最早什麼時候拿得到?2024 年 2 月 10 日。
如果你直接拿它去對齊 1 月底的股價,你就提早了十天。
十天聽起來不多。我量了一下。
訊號:月營收 yoy 排名前 30 名,等權持有一個月,只從成交金額前 300 名的普通股裡挑,扣台股來回成本 0.4710%。兩個版本唯一的差別是下單時點:
時間軸(以 M 月營收為例)
M 月底 ← 版本 B 在這裡下單。此時營收還沒公布
M+1 月 10 日 ← 交易所規定的公布期限,全市場同時知道
M+1 月底 ← 版本 A 在這裡下單。資訊已經公開約 20 天

年化報酬 年化波動 夏普 最大回落
A 次月底下單(正確) +5.23% 22.02% 0.34 -39.07%
B 營收月底下單(提早 10 天) +24.33% 23.62% 1.05 -31.58%
年化 5.23% 變成 24.33%,夏普 0.34 變成 1.05,累積淨值 2.7 倍變成 68.6 倍。
十天。
而且請注意版本 B 的最大回落:**−31.58%,比正確版本的 −39.07% 還小。**它在每一個指標上都比較好看。你不會從績效表上看出任何異常——它就只是一個很棒的策略。
這就是為什麼 point-in-time 不能靠「寫程式時記得」。人會忘,而且忘記之後回測不會抗議。它必須寫進資料的結構裡:每一筆除了「這是哪一期的數字」,還要有「這一天之後才拿得到」。回測只讀 available_date <= 決策日 的資料,讓偷看變成寫不出來,而不是靠自律。
財報的落差更大:Q1、Q2、Q3 財報的申報期限是季後 45 天,年報是 75 天。用「季末」對齊財報,偷看的是一個半月到兩個半月。
第二條是量化裡最經典、也最容易犯的洩漏。
你有一個特徵,尺度不一致,於是做 Min-Max 壓到 0 到 1。問題是:max 和 min 從哪裡算出來的?
如果是從整個資料集算的,那麼 2010 年那一格的標準化數值裡就含有 2024 年的資訊——因為分母是「全期間的最大減最小」,而全期間包含未來。
我把台股月營收 yoy 用兩種方式標準化,比較同一格資料的差:

同一格資料,兩種算法的差(尺度 0~1)
中位數 0.0185
第 95 百分位 0.3469
最大值 1.0000
差距 > 0.1 的比例 23.7%
**將近四分之一的資料格,兩種算法的差超過 0.1。**在一個 0 到 1 的尺度上,那不是捨入誤差,是實質不同的輸入。
我原本想順便量「洩漏讓報酬虛增多少」。結果是:洩漏版反而比較差(年化 5.62% vs 10.82%)。
原因是我的策略是排名型的。全樣本 Min-Max 對每一檔股票是單調轉換,不改變它自己的時間序列排序,所以那個報酬差異其實不是洩漏造成的——是「用自己的歷史區間標準化」本身變成了一個不同(而且更好)的訊號,概念上接近 SUE。
我把這個失敗的量測留著,因為它本身就是重點:
洩漏不保證讓結果變好。你不能靠「結果好得不像話」來偵測它。有些洩漏讓績效變差,有些讓它變好,有些在這個模型上有影響、換個模型就沒有。唯一可靠的做法是在建構特徵的當下就不要讓未來進來,而不是事後從結果反推。
這一條在訓練模型時影響最大,Day 14 會用實際的模型再量一次。
第三條是關於怎麼選參數。
最常見的做法:資料切成前後兩段,前段挑參數,後段驗證。聽起來合理,但那個「後段」你會不知不覺地反覆使用——調完一輪覺得不好,回去改,再測一次。測到第三次,那個樣本外已經不是樣本外了。
我拿同一個月營收訊號,唯一的自由參數是持股檔數(10 / 20 / 30 / 50 / 100 檔),比較三種做法:

用 2006-01 ~ 2015-11 挑參數 → 最佳是 20 檔(樣本內夏普 0.30)
樣本外(2015-12 起)表現:
年化報酬 年化波動 夏普 最大回落
一次切分(固定 20 檔) +6.01% 27.14% 0.35 -41.52%
walk-forward(每年重選) +8.67% 24.37% 0.46 -35.93%
完全不挑(固定 30 檔) +7.14% 26.14% 0.39 -39.07%
用前十年挑出來的「最佳參數」,樣本外輸給完全不挑。
20 檔在 2006–2015 確實是最好的,2016 之後就不是了。那個「最佳」只存在於它被挑出來的那段資料裡。
walk-forward 每年只用當時已知的歷史重選一次,它選出來的參數一直在變:
2016: 20 檔 2017: 50 2018: 50 2019: 100 2020: 100
2021: 100 2022: 50 2023: 50 2024: 50 2025: 50
參數會變不是缺點,是誠實——市場結構本來就在變。
但要講清楚:這不代表 walk-forward 一定比較好。它這次贏了,換個訊號、換個期間可能就不會。它真正的價值不在績效,在於你的樣本外測試只用一次:每一年的決定都只用當時的資訊做,沒有回頭修改的餘地。
(實作細節:這個實驗第一版我把樣本內切在 2015-12,但績效序列在索引 t 存的是 t→t+1 的報酬,所以那樣會把 2015 年 12 月到 2016 年 1 月那筆算進挑參數的依據裡——那在當下還不知道。改成切在 2015-11 之後結論沒變,但這種一格的錯位就是規矩一在講的事。)
第四條沒有實驗,是態度問題。
前面三個實驗出現過的年化報酬有:24.33%、10.82%、8.67%、5.23%。這些數字沒有一個可以拿去宣稱,因為它們全都站在一串假設上:
**成本假設。**手續費用 6 折計。你的券商可能是 5 折,也可能沒折。而且我假設每次都成交在月末收盤價。
**可交易性假設。**完全沒處理漲跌停、流動性衝擊、一張 1,000 股的整數限制。訊號叫我買前 30 名,我就假設 30 檔都買得到、買得滿——這在小型股上不成立。
**樣本外期間。**19 年聽起來很長,但月頻換股只有 233 個決策點。而一個完全沒用的隨機訊號,300 個組合裡有 11.3% 純靠運氣就能贏過 0050。
**公司行為。**還原股價目前缺了股票分割這一塊,所以樣本切在 2025 年 5 月。
一個報酬率數字如果沒有附上這些,它就不是結論,是廣告。
所以這 30 天我會一直給數字,但我不會說「這個策略年化 X%」。我會說:在這些假設下,這個做法比那個做法好 Y 個百分點,而 Y 需要大於成本門檻 Z 才有意義。