chundev
日期:2026-03-18 環境:台灣期貨市場(個股期貨),自建回測引擎
回測期貨結算日轉倉行為時,夜盤資料的日期歸屬是最容易踩的陷阱 — 台灣期貨夜盤(個股期 17:25 / 台指期 15:00 開盤)在資料源中歸屬到下一個交易日,如果用結算日當天的 dateString 去查夜盤開盤價,拿到的其實是前一天晚上的資料。修正這個 bug 後,原本「夜盤進場最便宜」的結論完全翻轉。三種結算日重入方式(結算日次月 open / 結算日夜盤 / T+1 open)的價格差異不大,沒有哪一種有穩定優勢。
在台灣期貨市場,個股期貨每月第三個星期三結算。如果策略持有的部位被結算日強制平倉(P0 出場),但進場條件仍然成立,有三種方式可以重新建倉:
回測引擎的目標是比較這三種進場方式的價格差異,找出哪種方式成本最低。
初版分析腳本中,用以下方式取得結算日夜盤開盤價:
const nightOpen = nightSessionMap.get(exitDate) ?? null;
// exitDate = 結算日(例如 2024-05-15)
結果顯示「夜盤 open 在 4/5 次中是最便宜的進場點」,看起來夜盤進場有明顯優勢。但這個結論是錯的。
❌ 初版結果:夜盤看起來最便宜
| 出場日 | 次月open | 夜盤open(錯) | T+1 open |
|---|---|---|---|
| 2024-05-15 | 840 | 826 | 857 |
| 2024-06-19 | 961 | 951 | 986 |
→ 夜盤 4/5 最低。結論:夜盤進場最好。
⚠️ 發現異常:夜盤 open 比日盤收盤(出場價)還低?結算日收盤 838,夜盤 826?不太合理。
✅ 根本原因:dateString 歸屬日期
台灣期貨夜盤交易時段是當天 15:00 ~ 隔天 05:00。在資料源(FinMind / 期交所)中,夜盤的 dateString 歸屬到下一個交易日:
實際時間 → dateString(資料歸屬日)
2024-05-15 17:25 夜盤開盤 → 2024-05-16
2024-05-14 17:25 夜盤開盤 → 2024-05-15
所以 nightSessionMap.get("2024-05-15") 拿到的是 5/14 晚上的夜盤 open,不是 5/15 結算日當天的夜盤。
正確寫法:
// 結算日當天的夜盤 open → dateString 歸屬到 T+1
const t1Date = uniqueDates[exitDateIdx + 1];
const nightOpen = nightSessionMap.get(t1Date) ?? null;
✅ 修正後結果:夜盤不再有優勢
| 出場日 | 次月open | 夜盤open(正確) | T+1 open | 最便宜 |
|---|---|---|---|---|
| 2024-05-15 | 840 | 848 | 857 | ①次月 |
| 2024-06-19 | 961 | 988 | 986 | ①次月 |
| 2025-09-17 | 1275 | 1270 | 1280 | ②夜盤 |
| 2026-01-21 | 1745 | 1760 | 1775 | ①次月 |
| 2026-02-23 | 1945 | 1925 | 1930 | ②夜盤 |
修正後:次月 open 最低 3 次、夜盤最低 2 次、T+1 最低 0 次。沒有任何一種方式有穩定優勢。
台灣期交所的夜盤(盤後交易)日期歸屬規則:
| 商品類型 | 日盤 | 夜盤開盤 | dateString 歸屬 |
|---|---|---|---|
| 台指期/選擇權 | 08:45 ~ 13:45 | 15:00 | 下一個交易日 |
| 個股期貨(如台積電期) | 08:45 ~ 13:45 | 17:25 | 下一個交易日 |
注意:不同商品的夜盤開盤時間不同(台指期 15:00、個股期 17:25),但 dateString 歸屬規則一致 — 都歸屬到下一個交易日。這個規則在期交所官方查詢系統和 FinMind API 中一致。
夜盤的交易結算歸屬到下一個交易日的結算。例如週一晚上的夜盤交易,其保證金、損益、未平倉都算在週二的帳上。這是期交所的制度設計,不是資料源的 bug。
如果你的回測引擎用 Map<dateString, nightOpen> 存夜盤資料,查詢邏輯要注意:
// ❌ 錯誤:拿到的是「前一天晚上」的夜盤
const nightOpen = nightMap.get(settlementDate);
// ✅ 正確:結算日當天的夜盤歸屬到 T+1
const nightOpen = nightMap.get(nextTradingDay);
這個 bug 之所以難抓,是因為:
| 進場方式 | 價格最低次數 |
|---|---|
| ① 結算日次月 open | 5 次 |
| ② 結算日夜盤 open | 2 次 |
| ③ T+1 open | 5 次 |
平均價差(T+1 − 次月 open)= +5.5 點,約 0.3~0.5%。
直覺上「結算日收盤後立刻進場」應該最接近出場價,但實際上:
相比之下,次月合約在結算日當天的日盤 open 是在還未結算時的價格,有時反而比結算後的夜盤便宜。
在自建回測引擎中,結算日出場(P0)和持有天數到期(P4)的轉倉邏輯是完全分開的:
exit 檢查 →
P0(結算日)→ 無條件出場,不可攔截
P4(持有天數到期)→ 可被 rollover 或 holdOnEntrySignal 攔截
如果要支援「P0 結算日轉倉」,需要在引擎中新增專門的邏輯,不能直接複用 P4 的 rollover。這是因為: