拆解台股盤後報告:讓 AI 當分析師,但不讓它說了算
台股盤後自動分析報告在作品集裡介紹的是「它能做什麼」:強弱族群、RRG 資金輪動、訊號判讀。這篇要講的是另一面——它是怎麼被做出來的,以及裡面有哪些我覺得值得借鏡的設計。
先講結論:這個專案最厲害的地方不是用了 AI,而是很清楚哪些事該交給 AI、哪些事絕對不能交給 AI。整個 repo 有 40 多支腳本、162 個測試,但每日主流程裡真正需要 AI 的角色只有三個,其餘全部是確定性的程式。
一、三層分工:大模型切題、小模型平行研究、大模型收斂
每天收盤後要處理漲跌幅前後各 100 檔股票。如果把 200 檔丟給一個 AI 叫它「寫一份報告」,結果會又慢又貴又不穩。這個專案把工作拆成三種角色:
- Controller(大模型,1 次):把 200 檔股票歸成 20~30 個族群,每個族群寫成一個獨立的 task 檔
- Worker(小模型,平行 N 個):一個族群一個子行程,各自上網查近 3 天新聞,寫出族群故事
- Finalizer(大模型,1 次):讀取所有結果,寫總結、長線策略與歷史記憶
INTERACTIVE · 模擬示意
三層分工:切題 → 平行研究 → 收斂
分工的重點在於每一層拿到的任務都很窄。Worker 的 prompt 明確規定:
- 只處理自己那一個 task,不准去讀其他族群的結果
- 最多搜尋 2 輪,找不到就停,改用產業知識補寫
- 不要輸出新聞來源與流程說明
「給搜尋型 agent 明確的搜尋預算與停止條件」這一條,後來還被寫進了專案的 AGENTS.md 當通用原則。沒有停止條件的 agent 會一直搜下去,時間跟費用都會失控。
研究階段的併發數預設是 10:族群研究的瓶頸在等 API,幾乎不吃本機 CPU,20~30 個族群大約兩輪就跑完。
二、能用程式做的,就不要叫 AI 做
這是整個專案最值得學的一點。
Finalizer 一開始實測要跑 688 秒,佔全流程 34%。拆開一看,大部分時間都花在「讀 150 個檔案、依規則搬字串、算分數加總」——這些工作根本不需要模型,而每讀一個檔都是一次 API 來回。
於是多了一支 build-analysis-skeleton.ts,把可以機械化的部分全部先做好:
- task 與 worker 結果依族群配對,故事缺漏時走三層 fallback
- 弱勢族群只留前 3 大與「昨強今弱」的族群,其餘併成「其他弱勢」
- 族群階段(連 1 日、連 2 日、連 3 日+、回歸)依歷史快照機械判定
- 進場分數四軸中的時機、籌碼、風險三軸直接查表算出
- 含有禁用模板句的故事標成
needsRewrite
Finalizer 現在只要讀 1 個檔,只做真正需要判斷的事:趨勢評分、進場理由、總結、改寫被標記的故事。沒被標記的故事,prompt 直接要求「原樣照抄,不要潤飾」——這句話本身就省下大量時間。
改完那次的 commit 紀錄是:finalizer 從 957 秒降到 284 秒(每天的族群數不同,耗時本來就會浮動,所以跟上面的 688 秒不是同一次量測)。
同樣的思路也出現在族群分類上。AI 分類常犯固定的錯,例如把華通(2313)放進一般 PCB,而不是低軌衛星;或把記憶體模組和矽晶圓混成一組。與其每天期待 AI 自己想通,不如在 controller 之後跑一支 refine-group-tasks.ts,用確定性的規則把已知的誤分拆回來。族群名稱也有一份 taxonomy.json(目前 99 個標準名稱與別名),確保「CPO 矽光子」和「CPO 矽光子(分化)」在歷史比對時會被算成同一個族群。
三、AI 的幻覺,用程式擋
check-story-facts.ts 是我最喜歡的一支檔案,因為它的註解記錄了一次真實事故。
台灣上市櫃公司的月營收依規定在次月 10 日前公布,所以 8/28 的盤後報告裡不可能出現「8 月營收」。但某天的報告寫了:
京元電作為測試代工龍頭,8 月營收衝破 31 億元,年增 31.9%,連七月 39.91 億元年增 36.75% 的佳績後再創新高。
七月那半句是真的,八月整句是編的,而且自相矛盾(31 億比 39.91 億少,怎麼會是「再創新高」)。
註解裡說得很直白:prompt 已經寫了「不要硬湊新聞」,模型還是會在找不到資料時填一個看起來合理的數字。這種錯讀起來完全正常——有公司、有數字、有年增率——人工很難每天逐段抓,只有機械規則擋得住。
// N 月營收最晚在 (N+1) 月 10 日公布;跨年也要對
export function revenueMonthPublished(month: number, asOf: Date): boolean {
const y = asOf.getFullYear();
const m = asOf.getMonth() + 1;
const year = month <= m ? y : y - 1;
const deadline = new Date(year + (month === 12 ? 1 : 0), month === 12 ? 0 : month, 10);
return deadline <= asOf;
}
處理方式也很講究:整句刪除,不改寫。改寫等於讓 AI 再編一次;少一句話的故事,遠好過一個假的營收數字。
下面這個小工具用的是同一套規則。把報告日期從 8/28 往後拖,看八月、九月的營收句子分別在哪一天變成「可以寫」:
INTERACTIVE · 同一套規則
拖動報告日期,看哪些句子會被刪掉
四、每天自動跑的流程,要能自己爬起來
這份報告是排程在平日晚上自動跑的,沒人盯著。所以流程韌性被設計得很仔細:
- 斷點續跑:流程分成 fetch → classify → research → finalize → send → publish,用
CODEX_REPORT_START_STAGE=finalize就能從任一階段接回,不用整條重跑 - 快照保護:controller 切完 task 就先備份一份,worker 和 finalizer 都以快照為準,避免中途被覆寫
- 看結果不看 exit code:AI CLI 偶爾非零退出,但檔案其實已經產出了。這時以「檔案在不在」決定要不要繼續,而不是整串失敗
- 逾時就砍:每個 worker 有逾時與重試上限,卡住的子行程會被清掉,不會拖垮整條流程
- 引擎自動切換:有 Codex CLI 就走 Codex 版,沒有就自動退回 Claude Code 版,兩條路跑的是同一套 5 階段流程
- 補跑與通知:Mac 在排程時間睡著了,喚醒後會自動補跑;網路沒恢復會等最多 15 分鐘,失敗就跳系統通知
連 shell 腳本裡都有踩過坑的痕跡:變數後面接全形括號時,bash 會把 $rc) 連同中文位元組一起當成變數名,所以一律寫成 ${rc}。這種註解對下一個接手的人(或 AI)非常有價值。
五、效能優化:先量測,不要憑感覺
主流程的每個步驟都包在一個 timed 函式裡,跑完會印出一張「最慢的在最上面」的耗時表,並存成 JSON 供跨日比較。腳本註解寫得很清楚:要優化先看這張表,不要憑感覺猜。
看了表之後發現幾件事:
- RRG 要抓 245 檔成分股兩年的價格,原本一檔接一檔抓,還每檔 sleep 250ms。改成有限併發的請求池(同時 6 條),不用無上限的
Promise.all,是怕一次打幾百個請求被 Yahoo 擋 - RRG、信用利差這些輔助資料只依賴市場資料、只有最後寄信時才用到,所以抓完市場資料就丟到背景跑,整段藏在研究階段底下,不佔關鍵路徑(最慢的 RRG 實測約 6 分半)
- 前面提到的 finalizer 瘦身,也是從這張表看出來的
六、懂金融資料的「假訊號」
寫投資工具最怕的不是程式錯,而是資料看起來對、其實是錯的。這個專案在好幾個地方都預先想到了:
- RRG 族群籃子刻意固定:成分股跟每天的 AI 分類分開維護。籃子一變,軌跡就失去跨日比較的意義
- 籃子代號對帳:代號打錯時,如果剛好是另一檔存在的股票,程式不會報錯,只會靜靜地算進錯的族群。所以有一支
verify-baskets.ts專門拿代號回查公司名做比對 - 集保大戶用累計門檻:一個 900 張的大戶加碼到 1100 張會跨級,只看單一級距會誤判方向(實測兩個級距的變動相關係數是 -0.43)。改用「≥400 張以上全部加總」這種累計門檻,跨級移動就會在門檻內相消
- 除權息與現增的稀釋:持股比例上升但大戶人數沒動的,標記為可能是稀釋而非買進,並降低排序權重
- 指數貢獻校準:用公開股數算出的個股貢獻點數,加總會跟交易所公布值差幾個百分點。用一個整體係數對齊,個股之間的相對比重不變,總數精確等於官方數字
- 信用風險用 OAS 取代 CDS:真正的 CDS 報價是付費商品,改用 FRED 免費提供的高收益債、投等債、AAA 利差,走勢跟 CDX 高度同向
每一條都寫在該檔案開頭的註解裡,連「為什麼不用另一種做法」都交代了。
七、讓系統替自己打分數
AI 每天說某個族群「正在擴散」,那它說得準嗎?
score-report.ts 每天把前一天的分析結果跟之後的實際股價對照,算出每個族群、每種階段判斷在 T+1、T+5 的平均報酬與勝率,存成 scorecard.json。Finalizer 會讀這份記分板當作自我校準的參考——例如過去被標成「擴散」的族群,五天後平均表現如何。
另一支 build-stock-picks.ts 則走相反路線:純規則、零 AI。它整合族群輪動、法人籌碼、集保大戶、公司派作價等訊號,而且規定進榜必須至少兩個獨立資料源同時給正訊號——只有法人在買、但大戶在倒或族群在弱化的,一律擋在門外。每天的結果都存成歷史快照,權重之後可以回測調整。
一個用 AI 寫敘事、一個用規則選股,兩者互相對照。
八、寫給 AI 看的專案文件
這個專案本身也是用 Claude Code、Codex 這類 AI 工具開發與維護的,所以文件有一大部分是寫給 AI 看的:
AGENTS.md開頭就是一張「任務速查表」:使用者說「跑今天的報告」就只執行npm run report;說「改分類」就去看哪幾個檔案、用哪個 skill- 明確寫出「不要做什麼」:不要動
data/底下的產出檔、不要改使用者已安裝的排程設定、不要做大範圍重構 - 維護類工作包成 Claude Code Skill,例如
stock-report-maintenance,AI 接手時不用每次重新摸索
再加上前面提到的,幾乎每支腳本開頭都有一段「為什麼這樣做、踩過什麼坑」的註解。這讓 AI 在修改時比較不會把一個刻意的設計當成 bug 修掉。
小結:可以帶走的幾個原則
如果你也在用 AI 做自動化工具,這個專案有幾條原則很值得借用:
- AI 負責判斷與敘事,程式負責其他所有事。配對、計分、排序、格式轉換都不該交給模型
- 把任務切窄再平行化,並給每個 agent 明確的預算與停止條件
- 幻覺用規則擋,而且擋下來就刪掉,不要叫 AI 再改一次
- 無人值守的流程要能斷點續跑,並以實際產出判斷成敗
- 先量測再優化,把慢但不相依的步驟移出關鍵路徑
- 讓系統記錄自己的準確度,才知道 AI 的判斷到底值不值得信
- 註解寫「為什麼」,給下一個接手的人,也給下一個接手的 AI