floviq logo floviq
← 筆記 · 2026-06-06 · mechanics

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(週六)實測彙整:

端點介面類型非交易日 statdata 欄位靜默失敗風險
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(主動比對 datedata 內日期)。

建議 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 開放資料 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)