本篇階段:Prj#5 股票分析
使用介面:Claude Code(via VS Code)
前四天在弄個人網站,Day 23 收尾之後那個專案就先告一段落。接下來五天換一個題目:把台股與美股的大盤、ETF 跟一堆總體經濟指標,做成一套每天自己會跑的東西。
會用到的帳號跟金鑰是在Day 12 那天就先去把該辦的辦掉了(現在手上應該會有帳號和金鑰了):永豐金證券開戶拿到帳號密碼、FRED 的金鑰、GitHub 帳號。當時寫「約十天後會用到」,就是現在。
第一天就有一件有一個撞到 AI 邊界的狀況,今年 (2026年) SPACE X 上市 (代號: SPCX),但一開始 Claude Code 很篤定回我:這個代號有問題,SpaceX 沒有上市。實際上 SpaceX 在 2026 年 6 月 12 日已經正式在 Nasdaq 掛牌了。是因為模型的知識截止日,造成在沒有查證的情況下,就直接回答未上市,在它訓練資料裡的世界,SpaceX 確實還沒上市。
救回來的方法是換一種問法:不要罵它「你搞錯了在幹嘛?」(而且有一派研究認為,在訓練這些模型的資料中,大型語言模型也會學到人類的情緒用詞,且它們會發現這些情緒詞出現時,產生犯錯的機率會增加),因此改成直接叫它去查、去抓 SPCX 的日線回來看,它抓回的第一根是 2026-06-12。
這點順帶暴露了另一件事:一年的日線大約是 250 根,這檔只有五分之一,凡是要一整年才算得出來的東西,在它身上都不成立,而這個邊界狀況後面幾天會反覆出現。
所以這件事定調了這個專案所有的做法:任何會被寫進結論的東西,都要去查、去跑一次,不要靠回想。 模型的回想有截止日! 就不得不說自己的回想也有,甚至是動態的(?)
重要說明:本文所有數字與敘述僅為個人技術實作紀錄,不構成投資建議。
前四天才把個人網站重整過,最順手的做法當然是在那個 repo 裡開一個資料夾。我和 Claude 討論了以後,決定要分開,有兩個理由如下。
一是 commit 歷史。這套東西每天產資料、每天 push,一年下來三百多筆「更新今日資料」混在網站的改版紀錄裡,之後想找「我什麼時候改了導覽列」會很痛苦。二是這個 Side Project 不該害到我的個人網站,甚至個人網站的建置流程我三天前才調好,如果這個腳本掛了讓整個個人網站死掉,這個代價完全不成比例。
所以我開了兩個新的 repo:
兩個都是 public 的原因不是想公開,是 GitHub Pages 的免費方案只支援 public repo,而內容目前靠頁面加密擋住。
有一條線在這裡就要畫清楚:**挑戰內容這一側從頭到尾不產生買賣建議。**例如 0050 跟 SPY 是具名的可交易標的,對它們給進出建議落在投顧業務的範圍。所以這條線畫在內容本身,評分數字可以有、分項可以有、機制怎麼做的可以講;把分數翻譯成動作的那一句,不生成。
最後留六個:
| 市場 | 指數(不可交易) | 追蹤它的 ETF |
|---|---|---|
| 台股 | ^TWII 加權指數 |
0050.TW、006208.TW |
| 美股 | ^GSPC S&P 500 |
SPY、VOO |
取「指數 + 兩檔」是為了讓後面幾天有兩組對照可看:大盤指數跟這兩檔 ETF 差在哪,以及兩檔 ETF 彼此又差在哪。這兩個問題個股沒有。
選法的部分,我要觀測的大盤標的就是加權指數 ^TWII,它涵蓋整個上市市場,是這套東西裡「台股今天怎麼樣」的定義。而 0050、006208 追蹤的是臺灣 50 指數,只有五十檔,跟加權指數不是同一個籃子。
所以指數跟 ETF 之間的落差不是誤差,是我本來就要看的東西:一邊是整個市場,一邊是可以真的買到的五十檔,兩者分岔到什麼程度,本身就是一個讀數。這件事每次跑對照分析,第一行就會把「這兩個籃子不一樣」印出來(其實是怕跑久了會忘記)。
順帶釐清一個容易混的後綴。台股代號在 yfinance 有兩種尾巴:.TW 是上市、.TWO 是上櫃。0050 是上市的,所以 0050.TW 抓得到、0050.TWO 回空;拿一檔真正的上櫃股去試(例如環球晶 6488.TWO)就抓得到。
個股那一側的名單不從記憶來,也不問模型(SPCX 那件事就是教訓)。台股的權值股去期交所的問答頁查,那裡有台股期貨成分股的權重排序。美股的則看 TradingView 的大型股清單,按市值排。權值股之所以重要,是因為指數按市值加權,前幾大權值股動一點點,指數就跟著動,這也是為什麼「講 0050 幾乎等於在講台積電」。
先說明,黃金、原油、比特幣看起來都很有存在感,但沒把握它們跟台股大盤的關係穩定到可以編進分數,因此先分開看。
世界層(十一項,進評分)
| 指標 | 頻率 | 來源 |
|---|---|---|
| VIX 恐慌指數 | 每日 | yfinance |
| 美元指數 DXY | 每日 | yfinance |
| 美債 10 年殖利率 | 每日 | yfinance |
| 美債 10Y-2Y 利差 | 每日 | FRED |
| 美國 CPI | 每月 | FRED |
| 美國失業率 | 每月 | FRED |
| 費城 Fed 製造業指數 | 每月 | FRED |
| Fed 利率上限 | 不定期 | FRED |
| 美國初領失業金 | 每週 | FRED |
| 美國非農就業 | 每月 | FRED |
| 美國原油庫存 | 每週 | EIA 每週石油狀況報告 |
台灣層(六項,進評分)
| 指標 | 頻率 | 來源 |
|---|---|---|
| USD/TWD | 每日 | yfinance |
| 費城半導體 SOX | 每日 | yfinance |
| 台灣景氣對策信號 | 每月 | 國發會 |
| 台灣外銷訂單動向指數 | 每月 | 國發會 |
| 台灣經濟成長率 YoY | 每季 | 主計總處 |
| 台灣央行重貼現率 | 不定期 | 央行 |
只顯示不評分(七項):黃金、WTI 原油、S&P 500、EUR/USD、USD/JPY、USD/CNY、比特幣。
合計二十四項,十七項進評分。
收斂名單這件事我交給 Claude 做,它提了四項要刪,理由各自成立:
第四項我同意,也就這一項真的刪掉了。拿大盤指數去解釋大盤,等於把答案抄進題目裡。
前三項我要求留下來,理由分兩種。
初領失業金與非農就業,Claude 那個「共線」的說法確實是一個學派的看法,但不是唯一的。另一派主張三個要配合看,因為它們量的是勞動市場的不同切面:
| 指標 | 量的是 | 頻率 |
|---|---|---|
| 失業率 | 存量 — 現在有多少人沒工作 | 月 |
| 初領失業金 | 邊際流量 — 這週新增多少人請領 | 週 |
| 非農就業 | 淨增減 — 這個月整體多了或少了多少職位 | 月 |
只留存量會漏掉轉折。失業率是落後指標,它動的時候事情已經發生一陣子了;初領是每週更新的,轉折會先出現在那裡。
EIA 原油庫存則是時空背景的問題。2026 年 9 月戰爭正在打,庫存是供給面的直接讀數,跟油價講的不是同一件事:油價會被預期、投機、金融面推動,庫存是實際有多少桶在儲槽裡。
這一段對我來說是個最典型的協作樣貌:Claude 給的四個理由沒有一個是錯的,但其中三個把「一個學派的看法」或「一條路的限制」當成了唯一解。 判斷的品質不在有沒有理由,在那個理由涵蓋了多少種可能。
把美國的費城半導體指數放進台灣層是 Claude 的判斷,理由是台股半導體占比極高。這一項我同意,但它同樣是可以被反駁的:SOX 成分股是美國上市公司,匯率、利率、資金面都跟台灣不同。
順帶一提 FRED 的金鑰。它其實有一個 fredgraph.csv 免金鑰端點,接上了官方 API 後額度會從 30 變成 120 req/min,但程式其實兩條路都留著:找得到金鑰走官方 API,找不到就退回 CSV 端點,沒有金鑰整套照樣跑,只是慢一點。
這一節看起來最瑣碎,卻是需要提防的地方,因為這裡可能遇到一個 bug:標題寫「張」,資料其實是「股」。
台股講張、美股講股,一張一千股。而同一天、同樣來自證交所的兩份盤後資料,單位還不一樣:
三種單位擠在同一套系統裡,而且互不換算:一口台指期不是一千股的任何東西。
Claude 定的規矩是:內部一律存「股」,只在顯示層換算成「張」,而且換算只有一個入口。除此之外,每一個欄位名都自己帶單位字尾:_shares、_lots、_contracts、_pct。這樣看到 margin_lots 就知道它是張。這條命名規矩也有一個測試守著,它會檢查每一個欄位名是不是都帶了單位字尾。
台股的價格同時用兩個來源抓:永豐金的 Shioaji,跟 yfinance。有衝突以 Shioaji 為主,因為它是券商的正式行情源,而 yfinance 抓的是 Yahoo 的前端端點,本來就沒有任何保證。
實際比對下來的結果比我預期乾淨:開高低收四欄完全對得上。
唯一穩定重現的衝突是 006208.TW 的成交量,兩邊每天都差大約 1.1%:Shioaji 報的是整千的數字(例如 1,941,000),yfinance 報的是實際股數(1,962,986)。差異的方向跟幅度每天都一樣,那就不是資料品質問題,是報法不同。
但一開始比出來的不是這樣。第一次跑,衝突有 26 筆,其中 21 筆全都來自 ^TWII。
原因是加權指數的「成交量」。Shioaji 那邊報 9,256,323,000,yfinance 報 4,388,800,差了大約兩千倍,而同一天的價格四欄完全一致。
差的不是資料,是定義。個股的成交量有一個明確的東西在數:這檔股票今天換手了幾股。指數沒有那個東西可以數。加權指數不是一檔可以買賣的標的,它的「成交量」是把成分股的量用某種方式加總出來的,而兩家的加總方式顯然不同。
所以規則改成:指數不參與成交量的跨來源比對,也不套張換股的一千倍換算。 改完之後衝突從 26 筆掉到 5 筆,而那 5 筆是真的。這是指數與個股必須分開處理的第一個地方,後面還會再遇到。
這個修正的意義不在少了 21 筆,而在於:比對機制的用途是「筆數突然變多本身就是警報」,而那個前提是平常的筆數要有意義。天天有 21 筆假衝突,警報就等於沒有。
價格序列還有一個更麻煩的性質:來源會回頭改寫歷史,而且改了不一定會說。
0050.TW 的 Stock Splits 欄位是空的,但三年前的收盤價顯示 31.69,當時實際價位在 126 上下。序列明顯被調整過,沒有任何欄位去說明這件事。
既然來源會偷偷改,就得先把「發現它改了」跟「把它改回來」分成兩件事。三個詞,意思不能混用:
| 詞 | 做什麼 | 何時做 | 會不會改資料 |
|---|---|---|---|
| 比對 | 拿今天抓回來的,跟本機已存的同一批日期逐格比 | 每天,成本零 | 不改,只記旗標 |
| 回看 | 往回抓漏掉或從未抓過的日期 | 固定小窗或手動 | 補進缺的列,不動既有列 |
| 重抓 | 把某一檔的完整歷史整條抓下來覆寫 | 只手動觸發 | 覆寫整條 |
比對是煙霧偵測器、回看是補漏、重抓是修復。自動化只做第一件。
比對是零成本的:yfinance 一次 history() 就是一個請求,抓五天跟抓一天同樣一次。而分割或除權息會把整條歷史乘上同一個常數,所以最新五根對不上就足以知道發生了。判斷的是「有沒有變」,不是「變了幾天」。
自動修復一律不做。:比對函式回傳的型別刻意沒有「修正後的資料」這個欄位,想自動修都沒地方放,而且有測試守著它不准長出來。
價格因此存兩份:一份是當天抓到什麼就存什麼、只增不改的稽核軌跡;另一份是最新完整序列,允許被覆寫,拿來計算。只留一份「最新」的話,就永遠證明不了它改過。
GitHub Actions 有 cron,看起來是免費又不用開機的選擇。還是決定放本機,用 Windows 工作排程器。
主要理由是金鑰全留本機,CI 就完全不需要碰任何祕密。所以分工是:**本機抓、本機算、本機加密,Actions 只負責把推上去的東西部署出去。**這個分工有一個測試在守:掃過每一個 workflow 檔,出現 secrets. 就讓測試失敗。
代價很直接:筆電得開機。台股 18:00、美股 09:00 各排一個任務,兩個都有「錯過時間後盡快啟動」之機制。
例如早上 09:00 那個時間點筆電還在睡,在 17:54 醒來,那可能 17:58:25 美股那個任務自動跑起來,回傳碼 0,執行紀錄多出一筆。而且那一筆抓回來的美股筆數跟前一天一模一樣,因為 9 月 7 日是美國勞動節,NYSE 沒開盤。「資料沒變就不更新」這件事,跟補跑在同一次執行裡一起被驗到了。
節流第一天就寫進資料源那一層,不放在呼叫端。每個來源自己持有一個節流器,任何呼叫路徑都繞不過去,呼叫的人不必記得 sleep,也不該有辦法跳過。
規矩大致是:FRED 固定一秒一次(只用官方額度的一半);yfinance 批次之間 sleep 一秒並禁用多執行緒;證交所跟期交所至少 0.6 秒,以「資料集加日期」為鍵整檔快取。每次跑完把當天各來源打了幾次請求記進執行紀錄。
Shioaji 那一格特別重要,因為它超流量的後果不是報錯,是行情查詢直接回空值,程式會以為「那天沒資料」。不把剩餘流量記下來,就不知道離上限還有多遠。
實測時間:
| 資料 | 大約幾點公布 | 18:00 抓得到嗎 |
|---|---|---|
| 台股日 K(yfinance) | 收盤後很快 | 抓得到 |
| 台指期未平倉、put/call(期交所) | 盤後不久 | 抓得到 |
| 三大法人買賣超(證交所 T86) | 約 17:30 | 抓得到 |
| 融資融券餘額(證交所 MI_MARGN) | 約 21:30,可能更晚 | 抓不到 |
18:00 那次排程確實把當天三檔台股的收盤都抓回來了,T86 也有 17,347 檔,只有融資融券「沒有符合條件的資料」。
所以籌碼面拆成兩班:18:00 抓三大法人跟期交所那兩項,22:30 再補融資融券。18:00 那一班根本不去問融資融券,不是問了失敗。少打一次註定落空的請求,執行紀錄也不會每天多一筆看起來像故障的失敗。
這件事還逼出頁面上一個小設計。同一天、同一個交易所的兩份盤後資料,公布時間就差了四個小時,總經序列更是散落在七八個不同的日期。所以頁面上的時間分成兩層:標頭放一個「最後一次抓取」,明寫它是管線執行的時間;每一列的資料日期各自標在該列。
寫成一行「資料更新時間」最省事,但那等於宣稱所有數字都是那一刻的,而 CPI 停在七月、央行停在 2024 年。省下來的那一行,代價是整頁的可信度。
今天大部分的時間不是花在寫抓資料的程式上,是花在把幾個「之後一定會混淆的東西」先釘死:張跟股跟口誰是誰、比對跟回看跟重抓各做什麼、.TW 跟 .TWO 差在哪。
釘的方式盡量不是註解,是測試:畫面層不准 import 儲存層、欄位名一定要帶單位、比對函式不准長出修改資料的能力。
而今天最值得記下來的還是開場那件事。模型很有自信地告訴我 SpaceX 沒上市,那個自信跟它的正確性完全無關,它只是誠實地回報了一個截止在過去的世界。去查、去跑一次,比回想可靠。
資料來源
其他參考
註一:文中的資料差異、公布時間、排程補跑都是 2026 年 9 月 6 日至 7 日這兩天的單次實測,屬於定性觀察,不是統計證據;外部資料源的行為會隨時間改變。
註二:排程補跑那一次是實際發生的觀察,不是受控實驗,沒有調整系統時間、也沒有刻意關機,只是剛好機器在排定時間睡著了。
註三:各項盤後資料的公布時間(三大法人約 17:30、融資融券約 21:30)是 9 月 7 日單日觀察加上作者既有經驗,證交所沒有公布保證時間,實際可能逐日浮動。
重要說明:本文所有數字與敘述僅為個人技術實作紀錄,不構成投資建議。