TWSE API 非交易日行為分層 — daily-snapshot 與 monthly-aggregate 的 HTTP 200 陷阱完整解析
TWSE(台灣證券交易所)開放資料 API 在非交易日的回應行為,是建構台股自動化流程時容易踩到的靜默錯誤來源。問題的核心在於:多數端點在假日查詢仍回傳 HTTP 200,與交易日的 200 無從區分。然而不同端點的 200 包含截然不同的語意——有的是「今天沒資料」的明確訊號,有的則是「成功」地把舊資料當成今日資料回傳。本文整理 TWSE API 的兩類端點設計邏輯、週末實測的回應差異、工程上的防禦驗證策略,以及排程設計建議。
① 兩類端點設計:daily-snapshot 與 monthly-aggregate
TWSE 的開放資料 API 按資料更新邏輯,可分為兩個截然不同的類型:
Daily-snapshot 類(逐日快照)
這類端點的資料以單一交易日為單位更新。查詢時需傳入 date=YYYYMMDD 參數,服務端只回傳該日的交易資料。非交易日本身沒有資料產生,因此查詢結果明確區分「有交易日資料」與「無資料」兩種狀態。
代表性端點(www.twse.com.tw/rwd/ 介面):
| 端點路徑 | 功能 | 更新單位 |
|---|---|---|
/rwd/zh/fund/T86 | 三大法人買賣超(含外陸資 / 投信 / 自營) | 逐日 |
/rwd/zh/afterTrading/MI_INDEX | 當日大盤指數收盤行情 | 逐日 |
Monthly-aggregate 類(月彙總)
這類端點的資料以月份為單位組織。查詢時雖然也傳 date=YYYYMMDD,但系統實際上讀取的是該日期所在月份的全部交易日資料,組合成一個陣列回傳。週末或假日查詢時,查詢日期本身不需要有交易,系統直接回傳「截至當月最新交易日」的資料集合。
代表性端點:
| 端點路徑 | 功能 | 回傳範圍 |
|---|---|---|
/rwd/zh/afterTrading/STOCK_DAY | 單一個股當月逐日 OHLC | 當月已有交易日全部 |
/rwd/zh/afterTrading/STOCK_DAY_ALL | 全體上市股票當日 OHLC(全市場) | 靜默回前一交易日全市場資料(見說明) |
openapi.twse.com.tw/v1/exchangeReport/FMTQIK | 大盤月成交量彙總 | 當月已有交易日全部 |
STOCK_DAY_ALL 實測說明(2026-06-06 週六):查詢
?date=20260606回傳 HTTP 200 +stat="OK"+date="20260605"(週五)+ 完整全市場 OHLC 資料陣列。行為與 monthly-aggregate 類一致:stat="OK"不代表今日有新資料,而是靜默回傳前一交易日全市場數據。排程若只看stat=="OK"判斷成功,週末會拿到週五全市場資料誤以為是今日,靜默失敗風險等級同 §③ 表中「高」分類。
② 非交易日實測:兩類端點的 HTTP 200 差異
以下為 2026-06-06(週六,非交易日)的實際 curl 結果。
Daily-snapshot 類:HTTP 200 + stat 訊號明確
T86(三大法人)非交易日查詢
GET https://www.twse.com.tw/rwd/zh/fund/T86?date=20260606&selectType=ALLBUT0999&response=json
實際回應:
{"stat":"很抱歉,沒有符合條件的資料!","total":0}
HTTP 狀態碼為 200,但 stat 欄位明確回傳中文錯誤字串。data 欄位不存在,total 為 0。排程邏輯只需判斷 stat !== "OK" 即可確實攔截:今日無有效資料。
MI_INDEX(大盤行情)非交易日查詢
GET https://www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX?date=20260606&type=ALLBUT0999&response=json
實際回應(節錄):
{"stat":"很抱歉,沒有符合條件的資料!"}
行為與 T86 一致:HTTP 200 + stat 非 "OK",data / tables 不存在。
小結:daily-snapshot 類端點在非交易日的回應設計,是「HTTP 200 本身不代表有資料,stat 欄位才是真正的成功/失敗訊號」。
Monthly-aggregate 類:HTTP 200 + stat=“OK” + 舊資料
STOCK_DAY(個股月成交)非交易日查詢
GET https://www.twse.com.tw/rwd/zh/afterTrading/STOCK_DAY?stockNo=0050&date=20260606&response=json
實際回應(截錄):
{
"stat": "OK",
"date": "20260606",
"title": "115年06月 ○○○○ 各日成交資訊",
"fields": ["日期","成交股數","成交金額","開盤價","最高價","最低價","收盤價","漲跌價差","成交筆數","註記"],
"data": [
["115/06/01", "...", "...", "..."],
["115/06/02", "...", "...", "..."],
["115/06/03", "...", "...", "..."],
["115/06/04", "...", "...", "..."],
["115/06/05", "...", "...", "..."]
],
"total": 5
}
關鍵點:
stat="OK"(看起來完全成功)date="20260606"(週六)data有 5 筆實際交易資料(6/1~6/5,即當週五個交易日)- 個股名稱以佔位符號呈現;上述數值均為測試用 schema 示意,非任何個股真實市場數字
這個回應與平日正常查詢在結構上完全相同:stat="OK"、data 非空、欄位齊全。排程若只看 stat 是否為 "OK",週六的查詢會被視為「成功取得今日資料」,但實際拿到的是截至上週五的月累積資料。
FMTQIK(大盤月彙總)openapi 介面
GET https://openapi.twse.com.tw/v1/exchangeReport/FMTQIK
此端點採用 OpenAPI 介面(無需 date 參數),回傳為純陣列格式:
[
{"Date":"1150601","TradeVolume":"17539429740","TradeValue":"1611133989740","Transaction":"7653162","TAIEX":"45337.91","Change":"604.97"},
{"Date":"1150602", ...},
{"Date":"1150603", ...},
{"Date":"1150604", ...},
{"Date":"1150605", ...}
]
HTTP 200,陣列有 5 筆(6/1~6/5),不存在 stat 欄位。沒有任何程式可見的失敗訊號——這類 openapi 介面連 stat 都不提供,進一步增加了誤判難度。
③ 端點行為差異總表
以下為 2026-06-06(週六)實測彙整:
| 端點 | 介面類型 | 非交易日 stat | data 欄位 | 靜默失敗風險 |
|---|---|---|---|---|
| T86(三大法人) | daily-snapshot / rwd | "很抱歉,沒有符合條件的資料!" | 不存在 | 低(stat 可攔截) |
| MI_INDEX(大盤行情) | daily-snapshot / rwd | "很抱歉,沒有符合條件的資料!" | 不存在 | 低(stat 可攔截) |
| STOCK_DAY(個股月 OHLC) | monthly-aggregate / rwd | "OK" | 存在(當月截至上個交易日) | 高(stat 無法區分) |
| STOCK_DAY_ALL(全市場 OHLC) | silent-stale / rwd | "OK" + date 為前一交易日 | 存在(前一交易日全市場資料) | 高(stat=“OK” 不代表今日) |
| FMTQIK(大盤月彙總) | monthly-aggregate / openapi | 不存在(陣列格式) | 存在(當月截至上個交易日) | 最高(無 stat 欄位) |
| BWIBBU_d(估值端點) | openapi cached | 不存在 | 存在(上個交易日資料,Date 為西元 8 碼 YYYYMMDD) | 高(Date 欄位需主動比對) |
實測日期:2026-06-06(週六)。stat 字串與 HTTP 狀態碼直接取自實際 curl response,未經修改。
④ 靜默失敗為何容易發生
monthly-aggregate 類端點的靜默失敗,根源在於「stat="OK" 代表端點服務正常,不代表查詢日期有今日資料」這個設計語意的分層。
daily-snapshot 類端點裡,stat="OK" 與「今日有資料」是同義詞:這類端點本來就只回傳該日的資料,沒有當日資料自然回傳錯誤字串。
monthly-aggregate 類端點裡,stat="OK" 只代表「本月份的資料成功組裝並回傳」,而「本月份」包含的是截至最後一個交易日為止的所有資料。週六查詢時,服務端確實正確回傳了本月所有交易日的資料——從服務端的角度沒有任何問題,stat="OK" 是正確的。問題是呼叫端若假設「stat="OK" 代表今日有新資料產生」,就會拿到舊的月累積資料卻誤以為是今日資料。
此外,FMTQIK 的 openapi 介面更進一步:連 stat 欄位都不存在,只有 Date 欄位在 data 項目內。程式若不主動比對最新 Date 是否等於今日,完全沒有外在訊號可以判斷是否為非交易日。
⑤ 工程防禦:三層驗證策略
針對上述兩類端點的不同失敗模式,建議採用三層驗證:
策略 1 · stat 欄位強制驗證(daily-snapshot 類)
// daily-snapshot 類端點(T86 / MI_INDEX 等)
// stat = "OK" 才繼續處理(對齊官方端點實測行為)
const response = await fetch(url).then(r => r.json());
if (!response || response.stat !== 'OK') {
// 非交易日、颱風假日、或其他服務異常
// stat 可能為「很抱歉,沒有符合條件的資料!」或其他字串
console.log('今日無交易資料,跳過本次執行。stat:', response?.stat);
return null;
}
// stat === 'OK' 才繼續
const data = response.data ?? response.tables ?? [];
stat 欄位的已知值:
"OK"— 正常交易日,資料存在"很抱歉,沒有符合條件的資料!"— 非交易日(假日 / 颱風停市)- 查詢未來日期:各端點回傳字串不同(T86 實測為
"查詢日期大於可查詢最大日期,請重新查詢!",STOCK_DAY 實測為"查詢日期大於今日,請重新查詢!")。防禦邏輯應一律用stat !== "OK"判斷,不可硬比對特定錯誤字串,以免跨端點複用時比對失誤。
策略 2 · Date 欄位比對今日(monthly-aggregate 類)
// monthly-aggregate 類端點(STOCK_DAY / FMTQIK 等)
// stat="OK" 不代表今日有新資料,需主動比對最新一筆的日期
const response = await fetch(url).then(r => r.json());
// STOCK_DAY 舊介面有 stat
if (response.stat && response.stat !== 'OK') {
return null;
}
const dataRows = response.data ?? response; // openapi 直接是陣列
if (!dataRows || dataRows.length === 0) {
return null;
}
// 取得最新一筆的日期,比對是否為今日(或前一交易日)
// STOCK_DAY: data[last][0] 格式為 "115/06/05"(民國年斜線格式)
// FMTQIK openapi: item.Date 格式為 "1150605"(民國年 7 碼)
const latestEntry = dataRows[dataRows.length - 1];
const latestDateStr = latestEntry[0] ?? latestEntry.Date; // 依端點結構調整
// 轉換民國年為西元日期以便比對
// 注意:僅適用民國年格式(STOCK_DAY "115/06/05" 或 FMTQIK "1150605")
// BWIBBU_d 的 Date 欄位為西元 8 碼("20260605"),不可套用此函式
function parseROCDate(str) {
// 支援 "115/06/05" 與 "1150605" 兩種格式
str = str.replace(/\//g, ''); // 移除斜線
const year = parseInt(str.slice(0, 3)) + 1911;
const month = parseInt(str.slice(3, 5));
const day = parseInt(str.slice(5, 7));
return new Date(year, month - 1, day);
}
const latestDate = parseROCDate(latestDateStr);
const today = new Date(); today.setHours(0, 0, 0, 0);
const isStale = latestDate < today && latestDate.getDay() !== 0 && latestDate.getDay() !== 6;
// 若最新資料非今日且非正常交易延遲(收盤後 17:30 才更新),視為舊月資料
if (latestDate.toDateString() !== today.toDateString()) {
console.log('月彙總端點最新資料為', latestDateStr, '非今日,可能為非交易日查詢。');
// 依業務邏輯決定是否繼續處理或 skip
}
策略 3 · 交易日曆前置查詢(最根本防禦)
排程執行前,先從交易日曆判斷今日是否為交易日,若否則整個流程跳過,不發出 API 查詢。台股行政假日(含連假 / 颱風臨時停市)可透過政府公開資料查詢,或維護本地快取(詳見相關文章)。
// 排程最前端:先判交易日
async function isTradingDay(dateStr) {
// dateStr: "20260606"
// 方法 A:查政府開放資料行政假日 API
// 方法 B:本地快取 + T86 stat 當二次確認
// 週六週日直接排除(台股週一~週五)
const d = new Date(dateStr.slice(0,4), dateStr.slice(4,6)-1, dateStr.slice(6,8));
if (d.getDay() === 0 || d.getDay() === 6) return false;
// 其餘假日需對照公告清單
return true;
}
if (!(await isTradingDay(todayStr))) {
console.log('非交易日,終止流程。');
process.exit(0);
}
⑥ 排程設計建議
建議 A · daily-snapshot 類:依賴 stat 欄位,不額外跳過
對 T86 / MI_INDEX 等 daily-snapshot 端點,stat 欄位已提供足夠的訊號。排程可以在假日正常觸發、查詢 API,程式看到 stat !== "OK" 後 graceful skip,不需要在排程層做假日前置判斷。
優點:邏輯簡單,且能自動處理颱風臨時停市(不需要維護假日清單)。
缺點:假日每次仍會觸發 API 呼叫,若有呼叫次數成本考量需額外控制。
注意:STOCK_DAY_ALL 雖路徑結構類似 daily-snapshot,但實測行為屬 silent-stale(見 §① 說明)。對 STOCK_DAY_ALL 不可套用本建議,必須改用建議 B(主動比對
date或data內日期)。
建議 B · monthly-aggregate 類:必須主動驗證日期
對 STOCK_DAY / FMTQIK 類端點,stat="OK" 無法區分非交易日。必須在程式邏輯中加上「最新一筆資料的日期是否為預期日期」的比對,否則假日查詢會靜默地帶入舊資料進行後續處理。
建議 C · 混合使用 T86 stat 作為交易日確認器
若流程同時呼叫多個端點,可以將 T86 的 stat 作為「今日是否為交易日」的快速確認器:先查詢 T86,若 stat !== "OK" 則整條流程中止,其他端點(包含 monthly-aggregate 類)不再繼續查詢。這樣可以避免在非交易日從 STOCK_DAY 拿到 stat="OK" 的舊月資料,同時節省 API 呼叫。
// T86 作為交易日 gate
const t86 = await fetch(
`https://www.twse.com.tw/rwd/zh/fund/T86?date=${todayStr}&selectType=ALLBUT0999&response=json`
).then(r => r.json());
if (t86.stat !== 'OK') {
// 今日為非交易日或尚未收盤
console.log('交易日 gate: 非交易日,流程終止。');
return;
}
// 繼續查詢 STOCK_DAY 等其他端點
⑦ Edge case 已知
-
颱風臨時停市:T86 / MI_INDEX 回傳
stat="很抱歉,沒有符合條件的資料!",與週末行為相同。STOCK_DAY 同樣回傳stat="OK"加當月截至前一交易日資料,靜默失敗風險不降低。 -
連假跨月:若查詢日期在連假中且月份已切換,STOCK_DAY 的
title欄位會顯示新月份(如「115年06月」),但data可能為空陣列(當月尚無交易日),或僅有極少筆數。此時stat仍為"OK",需額外判斷data.length === 0。 -
收盤後延遲更新(17:30 前):T86 / MI_INDEX 在交易日盤後 17:30 前查詢,可能回傳
stat非"OK"或回傳前一日資料。排程若在下午盤後立即觸發,建議設定 18:00 以後才執行(或加入重試機制)。 -
openapi 介面的 Date 格式:同一 host、三種格式:FMTQIK openapi 回傳
Date: "1150605"(民國年 7 碼,無斜線);STOCK_DAY 舊介面data[i][0]為"115/06/05"(民國年有斜線);同屬 openapi host 的 BWIBBU_d 則回傳西元 8 碼Date: "20260605"(YYYYMMDD),為第三種格式。三種格式跨端點不一致,解析前必須依端點區分,不可共用同一個 parse 函式。parseROCDate只適用民國年兩種格式,套到 BWIBBU_d 會算錯年份(8 碼前三位解為 202,加 1911 = 3921)。日期格式差異的完整分析見 TWSE OpenAPI 日期格式陷阱。 -
openapi.twse.com.tw 不提供 T86 端點:
openapi.twse.com.tw/v1/exchangeReport/T86對任何請求均回傳 HTTP 302 導向/404.html(content-type: text/html),並非 JSON。這不是假日專屬行為,而是 openapi 介面根本未提供 T86 此端點。若程式未處理非 JSON 回應,嘗試.json()解析會拋出例外。T86 僅可透過舊介面(www.twse.com.tw/rwd/)存取。
⑧ See also
- TWSE OpenAPI 日期格式陷阱 — 五個端點的日期格式差異完整分析,與本文 Date 欄位解析互補
- TWSE T86 欄位命名實況 — T86 欄位字串完整解析,
stat欄位行為在此文附錄 - TWSE MIS 即時報價 API 的漲停跌停價:u / w 欄位早就幫你算好了 — 同為靜默失敗類型,MIS 端點的非交易時段行為
- TWSE OpenAPI Swagger:https://openapi.twse.com.tw/
- 已把非交易日 / 假日防禦完整納入的台股自動化工作流,可參考 floviq 台股投資工具集
※ 本文描述 TWSE 開放資料 API 在非交易日的 HTTP 回應行為差異(daily-snapshot 類與 monthly-aggregate 類的 stat 欄位設計),屬工程工具參考範疇,不構成投資建議,投資請自行評估風險。
資料來源:
www.twse.com.tw/rwd/zh/fund/T86(T86 三大法人,2026-06-06 週六實測)www.twse.com.tw/rwd/zh/afterTrading/MI_INDEX(大盤行情,2026-06-06 週六實測)www.twse.com.tw/rwd/zh/afterTrading/STOCK_DAY(個股月成交,2026-06-06 週六實測)openapi.twse.com.tw/v1/exchangeReport/FMTQIK(大盤月彙總,2026-06-06 週六實測)- TWSE T86 端點 stat 欄位規格:
www.twse.com.tw/rwd/zh/fund/T86(官方端點,stat 已知值為 ground truth)
如果這篇對你有用: