EN 中文
← 回文章列表
Design · Data freshness

不用假日表的到價警報:兩道閘門,不是一本黃曆

家庭 bot 怎麼盯股價又做到閒置零成本、假日零誤報——盤中閘門管成本、新鮮度閘門管正確性,外加一個上線隔天就被推翻的界線決策。

2026-07-23 line-bot 系列 · 6
2
道閘門,職責分開
0
本要維護的假日表
0
次閒置時的 API 呼叫

背景

LINE 家庭助手系列第六篇。需求來自一位有持股、有手機的家人:「2328 跌破 50 通知我。」不是投資組合分析——一支公開股票、一個門檻、一則通知。功能是到價警報:bot 在背景輪詢報價,價格穿越門檻的那一刻推一則真 @ 通知。

機制上它是提醒系統的手足——同一副「背景 loop+推回來源對話」的骨架、同樣落 SQLite、同樣一次性語意。差別在觸發條件:提醒在時間戳到達時開火;到價警報在價格穿越門檻時開火。就這一個差別,扛走了所有設計重量——因為時間會準時抵達,價格不會。

先講那個被推翻的決策

第一篇寫過動工前畫下的分工界線:單人私密歸個人 agent 那套、全家共享進 bot。這條原則下的第一個裁決,把到價警報判給了私密那邊——它有投資味、我的投資組合自動化本來就在那、結案。

一天後裁決被推翻。破綻就在真實需求裡:家人想盯自己的股票、用自己的手機、在共享的群組裡。把這條路由到我私人的 Telegram 是荒謬的。原則沒錯——是它的判準錯了。「私密」是資料的性質,不是主題的性質。整份投資組合(淨值、持股、目標進度)是私密的;一支公開股票的價格是任何人 Google 得到的數字,家人想盯它,正是這隻 bot 存在的那種共享場景。我的組合自動化留在原地;單一標的警報成了 bot 的功能。

設計原則要在被真實案例磨利的那天才算掙到飯錢——一條從來不需要修訂定義的原則,是一條沒人在用的原則。

Loop 本體,以及沒人在看時它花多少錢

背景任務每 PRICE_ALERT_TICK 秒醒來一次(預設 180),載入活躍警報、查報價——兩個性質讓它當個好公民:

  • 零警報 → 零輪詢。 沒有活躍的列,loop 不碰網路直接回去睡。閒置的功能不花錢——跟買斷制 LLM 那個論證同一種紀律,套用在一個非官方報價端點上。
  • 依標的去重。 同一支股票上掛五個警報,只查一次報價、分發給五個門檻檢查。

門檻被穿越時,警報推播一次、@ 設定它的人、標記完成。一次性是刻意的:價格在門檻附近震盪,不然每三分鐘對群組掃射一輪——警報的工作是「發生時叫醒我」,不是「實況轉播穿越過程」。

難的部分:週末、假日、颱風天

先講讓天真版到價警報出糗的那種失敗。報價 API 不會在休市時停止回答——它回答最後一筆成交。台股在週六的「現價」是週五的收盤。如果週五收在使用者的門檻之下,天真的 loop 會在週六早上開心地「偵測到」穿越,對一個根本沒在動的市場發出警報。

直覺的修法是交易日曆:檢查星期、檢查盤中時段、檢查國定假日。陷阱就在這——對台股來說,那不只是週末,還有農曆年(每年日期不同)、國定假日、補班補市日、以及前一晚才公布的颱風假。假日表是一份你要維護到永遠的負債,而它第一次漏登記,就是一次誤報。

這隻 bot 不維護任何這種表。它用兩道職責嚴格分開的閘門:

def should_poll(symbol: str, now_ms: int) -> bool:
  """這個標的現在該不該查?(成本閘門。)

  台股:只在台北交易時段查。其餘(美股/加密/指數)
  一律查——正確性是新鮮度閘門的工作,不是這裡的。
  """
  if not is_tw_symbol(symbol):
      return True
  now = datetime.fromtimestamp(now_ms / 1000, _TPE)
  if now.weekday() > 4:  # 週六日
      return False
  return _TW_OPEN <= now.time() <= _TW_CLOSE
      tools/price_alert.py — 閘門一:盤中時段,純管成本
    
def is_fresh(as_of: str | None, now_ms: int) -> bool:
  """報價的時間戳夠不夠新、能不能拿來行動?

  沒有時間戳、或超過 _FRESH_SECS → 市場沒在交易
  (週末、假日、盤後)。不觸發。
  """
  if not as_of:
      return False
  try:
      dt = datetime.strptime(as_of, "%Y-%m-%d %H:%M").replace(tzinfo=_TPE)
  except ValueError:
      return False
  age = now_ms / 1000 - dt.timestamp()
  return -60 <= age <= _FRESH_SECS  # 容許輕微時鐘超前
      tools/price_alert.py — 閘門二:新鮮度,管正確性
    

新鮮度閘門才是正確性機制,而它成立的原因是:報價自己會告訴你市場有沒有在交易。假日的報價帶著昨天的時間戳;閘門看到過期的 as_of,拒絕觸發。農曆年、颱風假、臨時的半日市——同樣三行程式碼,自動、永久、全部處理,因為資料源就是自身活性的權威。相較之下,盤中閘門為了一件事存在:別為一個一天只開四個半小時的市場,整天轟一個非官方端點——所以它只管台股。美股和加密貨幣沒有單一乾淨的交易窗口,就每一輪都查、讓新鮮度做決定。

收穫

關於時間戳勝過黃曆

任何需要知道「世界現在有沒有在做那件事」的系統,可以維護一本世界的日曆,或檢查世界自己吐出的資料新不新鮮。日曆是你做到永遠、每年錯一次的工;時間戳是把權威下放給源頭。選源頭。

關於成本閘門與正確性閘門分開

兩道閘門長得像——都在決定「做或跳過」——但一道保護的是流量預算,另一道保護的是真相。讓它們保持兩個具名函式、兩份 docstring,任何一道都能被調整(或刪除)而不動搖另一道的保證。

關於原則與現實的碰撞

「私密歸私密那套」撐過真實需求的檢驗,靠的是把判準從「主題」磨利成「資料」。推翻一個上線一天的裁決不是搖擺——是原則獲得一個精確到可以把未來決策託付給它的定義。