floviq logo floviq
← 筆記 · 2026-05-31 · mechanics

TWSE MIS 即時報價 API 的漲停跌停價:u / w 欄位早就幫你算好了

串台股即時報價時,一個常見的直覺是:漲停價 / 跌停價要自己算。拿前一日收盤價乘以 1.1 是漲停、乘以 0.9 是跌停,看起來再單純不過。但 TWSE 的即時行情 API (MIS · mis.twse.com.twgetStockInfo) 回傳裡,漲停價跟跌停價已經是兩個現成欄位 — uw — 交易所早就幫你算好放進去了。自己重算不只是造輪子,還會直接踩進「無漲跌幅例外股」的坑。本文整理 MIS 的欄位語意、自己算的錯誤後果,以及怎麼正確接這個端點。


① 以為:漲停跌停要自己拿前收盤算

台股上市股票每日漲跌幅限制為 ±10%,以「當市開盤競價基準」(通常是前一日收盤價) 為基準。寫成程式很直覺:

// 直覺但不完整的做法
const limitUp   = prevClose * 1.1;
const limitDown = prevClose * 0.9;

於是很多人串即時報價時,只抓「現價」跟「昨收」,漲停跌停自己乘出來。表面上跑得動,數字也接近,問題往往要等到某一天數字對不上才浮現。

② 其實:自己算會錯,而且是靜默地錯

自己拿前收盤乘 1.1 / 0.9 至少有三個層次的坑:

坑 1 · 沒有套升降單位 (tick) 取整

±10% 算出來的價格不會直接是合法委託價。漲停 / 跌停價必須符合 TWSE 的升降單位規則 — 漲停向下取最近合法 tick、跌停向上取最近合法 tick,不可逾越 10% 上限。升降單位隨股價分級 (例如 50–100 元區間 tick = 0.10 元、150–500 元區間 tick = 1.00 元)。

也就是說,漲停板的實際漲幅常常不是精確的 10%,而是 9.9X% 或剛好卡在合法 tick 上。自己乘 1.1 得到的浮點數,跟交易所掛出的真正漲停價會差一個 tick。

坑 2 · 漏掉「無漲跌幅例外」股

更嚴重的是,有一整類股票根本沒有 ±10% 限制:

  • 新上市股票前 5 個交易日 (IPO 首日至第 5 日)
  • 處置股票 (依處置條件可能調整或無一般限制)
  • 政府公債、外國有價證券、部分 ETF

對這些股票套 ×1.1 / ×0.9,算出來的「漲停價」是一個現實中不存在的約束。如果你的偵測邏輯是「現價 ≥ 昨收 ×1.1 視為漲停」,新股前 5 日的成交價可能遠超 +10%,但因為沒有實際漲停板,偵測邏輯永遠不會 fire,呼叫端誤以為「沒漲停」。反過來若邏輯是「漲幅未達 ±10% 就排除」,正常波動的新股反而被篩掉。(這個誤判機制在另一篇關於新上市無漲跌幅窗口的文章有完整拆解,見文末 See also。)

坑 3 · 這些錯都不會 throw error

最麻煩的地方在於:乘法不會出錯。prevClose * 1.1 永遠回傳一個 number,不會 null、不會 NaN、不會 throw。整條自動化 pipeline graceful 跑完,推播照送、警報照觸發 — 只是觸發的判斷基準是錯的。這是典型的 silent failure:stack trace 救不了,linter 救不了,只有人工拿去跟官方數字對一次才會發現。

③ MIS getStockInfo 的真實運作:u / w 已經是現成欄位

TWSE 的即時行情走 MIS (Market Information System) 端點:

GET https://mis.twse.com.tw/stock/api/getStockInfo.jsp?ex_ch=tse_{code}.tw&json=1&delay=0

回傳是 JSON,主資料在 msgArray[] 陣列裡,每檔股票一個 object。這個 object 的 key 是高度縮寫的單字母 / 符號 (MIS 為了壓 payload 體積),語意如下表:

key語意範例值形式
c證券代號4 碼數字字串
n證券簡稱中文股名
nf證券全名完整公司名
z最近成交價字串數字 (含小數 4 位)
y昨日收盤價字串數字
o開盤價字串數字
h / l當日最高 / 最低字串數字
u漲停價 (upper limit)字串數字
w跌停價 (lower limit)字串數字
a五檔賣出價 (ask)5 個價位以 _ 串接
b五檔買進價 (bid)5 個價位以 _ 串接
tv / v當盤成交量 / 累計成交量字串數字
t最新揭示時間HH:MM:SS

重點:u (漲停價) 跟 w (跌停價) 是交易所端算好的成交基準上下限,已經套過升降單位取整、也已經對無漲跌幅股做了對應處理 — 你直接讀這兩個欄位就是當日合法的漲停 / 跌停價,不需要自己乘。

字串細節 (容易踩的點)

  • 所有數值都是字串:z / u / w 都是 string,要運算前先 parseFloat,不能直接拿來比大小 (字串比較會出錯)。
  • 價格帶 4 位小數:形式像 "275.0000",不是整數。
  • a / b 是底線串接的五檔:例如賣價五檔是 "261.0000_261.5000_262.0000_262.5000_263.0000_" — 用 _ split,注意尾端有個空字串要過濾;a 由最佳賣價往外遞增 (asks[0] 為最佳賣價)。
  • 回傳外層有 rtcode:正常是 "0000",非此值代表查詢有問題,要先檢查再讀 msgArray
  • 「查無資料」有兩種形態,要分開擋:
    • 非交易時段 / 真空查詢:msgArray 可能是空陣列 [],直接 index [0] 會 undefined,要先判長度。
    • 代號錯誤:MIS 不會回空陣列,而是回一個佔位 object — 形如 {"c":"", "z":"-", ...},rtcode 仍是 "0000"。此時 msgArray[0] 存在但 c 為空字串、價格欄是 "-",parseFloat("-") 會得到 NaN。所以光判 length === 0 擋不掉打錯代號的情況,要再判 stock.c(空字串)或關鍵價格欄是否 NaN,否則就是把 ① 開頭講的 silent failure 自己種回去。

④ 知道後怎麼接這個 API

做法 1 · 直接讀 u / w,不要自己乘

// 對齊 MIS ground truth · u = 漲停價 · w = 跌停價
const stock = resp.msgArray?.[0];
// 同時擋「空陣列」與「代號錯誤的佔位 object」(c 為空字串 / 價格欄是 '-')
if (!stock || !stock.c) {
  throw new Error('MIS 查無有效資料 · 代號錯誤或非交易時段');
}

const limitUp   = parseFloat(stock.u);   // 漲停價 (交易所已算好)
const limitDown = parseFloat(stock.w);   // 跌停價 (交易所已算好)
const last      = parseFloat(stock.z);   // 最近成交價

// parseFloat 失敗 (價格欄為 '-' 等) 會是 NaN,先擋掉再比較
if ([limitUp, limitDown, last].some(Number.isNaN)) {
  throw new Error('MIS 價格欄缺值 · 可能非交易時段或佔位回傳');
}

// 判斷是否觸及漲停 / 跌停,直接跟 u / w 比,不用 ±10%
const atLimitUp   = last >= limitUp;
const atLimitDown = last <= limitDown;

這樣寫的好處:升降單位取整、無漲跌幅例外,全部交給交易所處理。你的程式不需要維護 tick 分級表,也不需要另外查哪些股票今天無漲跌幅 — u / w 已經反映了。

做法 2 · 解析五檔 (a / b)

// 五檔以底線串接,尾端含空字串需過濾
const asks = (stock.a ?? '').split('_').filter(Boolean).map(parseFloat);
const bids = (stock.b ?? '').split('_').filter(Boolean).map(parseFloat);
// asks[0] = 最佳賣價 · bids[0] = 最佳買價

做法 3 · 防呆:rtcode + 空陣列 + 佔位 object 先擋

if (resp.rtcode !== '0000') {
  throw new Error(`MIS rtcode 非 0000: ${resp.rtcode}`);
}
// (a) 空陣列:非交易時段 / 真空查詢
if (!Array.isArray(resp.msgArray) || resp.msgArray.length === 0) {
  return null; // graceful skip
}
const stock = resp.msgArray[0];
// (b) 佔位 object:代號錯誤時 msgArray 非空,但 c 為空字串 / 價格欄為 '-'
if (!stock.c || Number.isNaN(parseFloat(stock.z))) {
  return null; // 查無有效資料 · graceful skip
}

⑤ Edge case 已知

  • 盤後 / 非交易時段:z (成交價) 可能停在最後一筆,或某些欄位缺值,但 u / w / y 通常仍在 — 漲停跌停價是當日盤前就確定的,不隨盤中變動。
  • 代號錯誤:MIS 不回空陣列,而是回 c 為空字串、價格欄是 "-" 的佔位 object,且 rtcode 仍為 "0000" — 必須額外判 c / NaN,不能只靠 rtcode 或陣列長度。
  • 無漲跌幅股:新上市前 5 日、處置股等,u / w 的處理由交易所端決定,直接讀即可,不要再套 ±10% 邏輯覆蓋。
  • 非交易日:msgArray 可能為空,務必先判長度再 index。
  • MIS 是即時端點,有 referer / rate 限制:高頻輪詢需控制節奏並帶合理 User-Agent,避免被擋。它跟盤後的 Open Data API (如收盤行情 MI_INDEX) 是不同系統,用途也不同 — MIS 給盤中即時、Open Data 給盤後結算。
  • u / w 仍是字串:再強調一次,運算前 parseFloat

⑥ 為什麼這個設計值得知道

MIS 把漲停 / 跌停價當成現成欄位給出來,等於把「升降單位取整」跟「無漲跌幅例外判斷」這兩件麻煩事在 server 端一次處理掉。自己拿前收盤乘 1.1 / 0.9 看似省一個 API 欄位,實際上是把交易所維護的規則表重新實作一遍 — 而且實作得不完整 (漏 tick、漏例外股),錯誤還是靜默的。對寫 n8n 或自建報價 workflow 的人來說,讀 u / w 不只是少寫幾行,而是把判斷基準對齊到交易所這個唯一正確來源。


See also

  • 新上市股票前 5 日無漲跌幅窗口的靜默誤判機制 — 為什麼 ±10% 邏輯會漏接新股 (twse-new-listing-no-price-limit)
  • 台股漲跌停機制與升降單位完整規則 (twse-limit-up-down-mechanism)
  • TWSE T86 欄位命名實況與 silent failure 風險 (twse-field-name-reality)

如果想直接看一套已經把這些邊界 (漲跌停取值、無漲跌幅例外、欄位防呆) 處理好的台股工具,可以參考 floviq 的台股投資工具集 — 裡面的盤前 / 盤中模板在取漲停跌停價時就是直接對齊交易所欄位,而不是自己乘。


※ 本文只是在講 TWSE MIS 這個即時報價 API 的欄位怎麼用、怎麼避免自己算錯 · 屬資訊與工程工具範疇,不是投資建議,買賣請自己評估風險。

資料來源:TWSE MIS 即時行情端點 mis.twse.com.tw/stock/api/getStockInfo.jsp · 漲跌停與升降單位規則參考 TWSE 官方交易制度說明 (https://www.twse.com.tw/zh/products/system/trading.html)