股票API分時轉日線的時區陷阱:我們在跨境投資數據處理中的經驗筆記
最近我們在整理一批美股的分鐘級行情資料時,遇到一個有點惱人的小插曲。用股票API拉回來的分鐘K線,經過我們自己寫的聚合程式轉成日線之後,有少數幾個交易日的開盤價和收盤價,跟券商軟體上的數據硬是差了一小截。起初我們還以為是K線計算模組哪裡出了狀況,把價格公式、資料格式、高低點判斷全都翻出來檢查一遍,程式碼本身卻乾淨得很,一點毛病也挑不出來。
後來我們靜下心來,把焦點從「怎麼算」移到「什麼時候算」,才赫然發現,真正作怪的並非價格計算本身,而是每筆分鐘資料背後所承載的時間規則。不同市場的交易時間、不同API回傳的時間格式、以及時區轉換的處理方式,全都會影響最終生成的K線。尤其當你仰賴股票API獲取行情資料時,如果沒有在一開始就把時間問題處理妥當,分鐘線拼成日線的過程裡,就很容易出現資料錯位,而且往往錯得不知不覺。
為什麼我們選擇自己合成日線,而不是直接買現成資料
對我們這種同時關注多個市場的跨境投資者來說,資料成本一直是個需要精打細算的項目。如果每一個市場都去訂閱官方日線服務,一整年下來的費用其實相當可觀,對小團隊或個人研究者來說,性價比不見得划算。所以我們後來採取了一個相對務實的做法:透過股票API取得原始的分鐘級數據,再依照自己的需求,在內部系統中合成日線。
這樣做的好處很明顯:
成本大幅下降,只需要負擔API的存取費用
可以完全掌控K線的組成邏輯,例如要納入哪些交易時段、如何處理盤前盤後資料
能依據不同策略需求,彈性產生不同時間區段的合成K線
但代價是,我們必須自己扛起資料品質的責任。時間一旦處理得不夠精準,後面所有的回測、策略分析、即時圖表,全都會建立在一個歪斜的基礎上。
我們在時間處理上最在意的幾件事
在開始動手寫轉換程式之前,我們先釐清了對這條資料管線的基本要求:
日線的開高低收,必須與交易所正式公佈的數值一致
歷史資料與即時資料,必須使用完全相同的時間轉換規則
遇到美國夏令時間或冬令時間切換,系統要能自動適應
不同資料來源合併時,時間格式和時區基準要能統一
只要其中任何一項沒有做好,回測出來的信號就可能跟實際市場脫鉤,而且這種脫鉤往往非常隱蔽,不容易在短時間內被發現。
讓我們頭痛的那個時區偏移
最初我們的日線聚合邏輯非常直覺:直接依照API回傳的日期欄位來分組,然後把每一組裡的第一筆當開盤、最後一筆當收盤、最高最低也照算。這套邏輯在單一時區的市場裡,大致上不會出什麼差錯。但美股市場的交易時間是以美東時間為準,而不少行情API回傳的卻是UTC時間。於是,問題就來了。
我們用一個簡單的例子來呈現這個落差。美國夏令時間期間,美東市場在早上九點半開盤,換算成UTC是下午一點半。大盤在美東下午四點收盤,換算成UTC是晚上八點。請看下表:
如果我們完全不進行時區轉換,直接用UTC的日期來切割交易日,那麼美東下午時段(大約UTC晚上七點到八點之間)的分鐘線,就可能因為跨過了UTC的午夜分界,而被錯誤地歸到隔天。結果就是,原本屬於同一個交易日的資料硬生生被拆成兩半,當天的收盤價少了最後一個小時,隔天的開盤價又混入了前一天的尾巴。這種狀況平時不容易察覺,但只要一跑歷史回測,累積下來的誤差就足以讓策略的績效曲線出現明顯的偏移。
解決方案:把時間轉換拉到所有計算之前
意識到問題的根源之後,我們重新調整了資料處理的順序。現在我們的原則很簡單:任何資料進入系統,第一件事就是先搞定時間,再來談K線計算。 具體的流程是這樣:
取得API回傳的原始時間戳記
判斷該標的所屬的交易所時區
利用 Python 的 zoneinfo 模組轉換成交易所當地時間
依照正式交易時段進行切割,最後才做日線聚合
以美股為例,我們會先把時間統一轉成 America/New_York,接下來的所有分組與計算,都只認這個時區。下面這段程式碼是我們目前資料管線中穩定運行的轉換邏輯:
from datetime import datetime
from zoneinfo import ZoneInfo
utc_time = datetime.strptime(
"2026-07-01 13:30:00",
"%Y-%m-%d %H:%M:%S"
)
utc_time = utc_time.replace(
tzinfo=ZoneInfo("UTC")
)
market_time = utc_time.astimezone(
ZoneInfo("America/New_York")
)
print(market_time)這樣處理之後,系統會自動根據時區規則套用夏令時間或冬令時間,我們再也不用自己手動判斷到底要加幾個小時、減幾個小時。光是這一點,就省掉了過去很多不必要的錯誤。
交易日不等於自然日:那些盤前盤後的雜訊
時區問題解決了,但我們很快又撞上另一個容易忽略的細節:交易日跟自然日並不完全相同。很多行情API除了回傳正式交易時間的資料之外,也會一併把盤前和盤後的成交紀錄塞進來。如果我們只看日期欄位就全部丟進日線計算,那些非正式時段的少量成交,就可能反過來干擾開盤價或收盤價的判斷。
所以我們現在會在生成日線之前,先明確劃定資料範圍:
只採用正式交易時段(美股為美東時間 09:30 至 16:00)的分鐘線
盤前與盤後資料另外存放,若有需要再單獨調用
搭配交易日曆,處理半日市、提早收盤、節假日等特殊情況
這樣一來,日線的組成乾淨許多,也比較能反映真實的市場流動性。
即時行情也需要一致的時間處理
如果說處理歷史分鐘資料時,時間錯誤還可以在事後從結果中看出端倪,那麼即時行情就完全不同了。即時資料是一筆一筆持續進來的,任何一筆的時間處理出錯,都會直接污染正在生成的即時K線,而且事後幾乎不可能回頭修正。
我們在串接即時行情時,就是把時間轉換模組直接放在資料入口的最前端。收到的每一筆 tick,都會先解析時間欄位、轉成交易所當地時間,然後才送進分鐘K線的合成器。後來我們選用了一個成本相對合理的即時行情來源 AllTick 來做 WebSocket 串接測試,也因為前面已經把時間模組獨立出來了,整個整合過程比想像中順利不少。
下面這段是我們處理即時行情資料的基本架構:
import websocket
import json
from datetime import datetime
from zoneinfo import ZoneInfo
def on_message(ws, message):
data = json.loads(message)
trade_time = data["tradeTime"]
dt = datetime.strptime(
trade_time,
"%Y-%m-%d %H:%M:%S"
)
market_time = dt.replace(
tzinfo=ZoneInfo("America/New_York")
)
print(
data["symbol"],
data["price"],
market_time
)
ws = websocket.WebSocketApp(
"wss://api.alltick.co/stock/websocket",
on_message=on_message
)
ws.run_forever()在實際專案中,我們會把時間轉換單獨封裝成一個共用模組。不管是歷史資料的批次匯入,還是即時的 WebSocket 推播,都走同一套規則,這樣就能避免不同入口各自為政,產生難以追蹤的時間差異。
我們在資料處理過程中特別留意的幾點
分分鐘線拼日線這件事情,看似簡單,但其實埋了不少小細節。以下幾點是我們自己踩過坑之後,特別會留意的:
不同資料來源的時間格式可能完全不一樣,有些給UTC,有些給交易所當地時間。多來源合併前,一定要先做格式與時區的正規化。
原始時間戳記一定要保留下來。日後若發現某一根K線異常,原始時間往往是最快定位問題的線索。
不要預設每個交易日都有固定數量的分鐘線。遇到節假日、提前收盤或臨時停牌,當天的資料筆數都會變動。
時間轉換邏輯最好集中管理,不要散落在不同的腳本裡面,否則改了一個地方,另一個地方又忘了同步。
寫在最後:時間才是行情系統最隱形的根基
經過這段時間的折騰,我們越來越深刻地體會到,很多看似K線計算的異常,其實都不是數學公式出了問題,而是最基礎的時間資料沒有被妥善對待。分鐘K線轉日線,表面上只是一個資料聚合的動作,但背後牽涉到交易時間、時區轉換、交易日規則,甚至即時與歷史的一致性。
對於像我們這樣,希望用股票API自建行情系統的跨境投資者來說,把時間處理的流程固定下來,絕對是一筆很值得的投資。時間軸一旦穩定,不管是回測、策略分析,還是即時看盤,所有結果都會更貼近真實市場的樣貌。我們也還在持續優化這條資料管線,但至少現在看著不再跳空的日線,心裡踏實多了。