TWSE MIS 即時報價 API 的漲停跌停價:u / w 欄位早就幫你算好了
串台股即時報價時,一個常見的直覺是:漲停價 / 跌停價要自己算。拿前一日收盤價乘以 1.1 是漲停、乘以 0.9 是跌停,看起來再單純不過。但 TWSE 的即時行情 API (MIS · mis.twse.com.tw 的 getStockInfo) 回傳裡,漲停價跟跌停價已經是兩個現成欄位 — u 與 w — 交易所早就幫你算好放進去了。自己重算不只是造輪子,還會直接踩進「無漲跌幅例外股」的坑。本文整理 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)
如果這篇對你有用: