股票觀測站台指期 · 題材 · 美股 · 論文

MARKET RESEARCH

預測紀律

為什麼會出錯、根因是什麼、怎麼改、改了沒有

已修並驗證19fixed(登記簿 2026-09-01 版,43 天前)
修了一半2partial —— 剩下的寫在改善方法(登記簿 2026-09-01 版,43 天前)
已查證未修0open(登記簿 2026-09-01 版,43 天前)
量出來的事實1measured —— 不是缺陷(登記簿 2026-09-01 版,43 天前)

🕐 這四個計數是上游登記簿那一版的盤點,本站唯讀不重算。登記簿已 43 天沒有更新(2026-09-01)——所以「已查證未修 0」的意思是那一天沒有未修項,不是今天沒有問題。

這一頁的規矩最後更新 2026-09-01

每一項都必須有 原因(不是現象)與 改善方法(不是抱怨)。

只寫「prob_boom 不準」沒有用——那會讓人想去調參數。要寫 「它是 min(催化劑×10, 95),分子只跟題材有關, 所以同題材下每檔股票拿到一模一樣的分數,在建構上就不可能有個股 鑑別力」,才會讓人知道調參數沒用、要換做法。

🔴 缺「原因」或「改善方法」的登記不會被擋下——本站唯讀上游的 登記簿,這種項目照樣完整顯示在下面的個別項目表裡,只是會在下方亮一則 警告點名是哪一項缺什麼。看到的每一項不代表都通過了完整性檢查。

共通病因(這一段比個別項目重要)同一個坑踩很多次,代表問題不在那幾行程式,在寫法的習慣。⚠️ 兩個數字口徑不同,不相等是正常的:「全專案盤點 N 次」是上游寫登記那天(2026-09-01)對整個專案手寫的次數;「本登記簿收錄 M 項」是本站算的——這份登記簿裡掛在這個病因底下的個別項目數。一個病因可以踩很多次而只登記一項,也可以拆成好幾項登記。⚠️ 前者是那一天的快照,之後沒有人重數過

病因說明例子根治方法
不可重建的資料用就地覆寫
歷史盤點 9 次 本登記簿收錄 1 項
read → 改 → 寫回同一路徑,沒有 tmp+rename、沒有備份。 外層 cron 的 timeout 隨時可能在寫入中途送 SIGTERM。 ・ML 特徵 CSV 11,225 列
・prediction_log
・outcome_log
・tech_indicators.json
・latest.json
・backfill_horizons.py 的 outcome_log 寫回(2026-08-27 新寫的,漏網第 9 處)
scripts/atomic_io.py,9 處全部已改。另加 guard_shrink 擋縮水覆寫。 ⚠️ 「8 處全改完」之後,兩天後新寫的程式又犯了一次。 backfill_horizons.py(2026-08-27)用 df.to_csv 就地覆寫 outcome_log, 而 atomic_io 在那時候已經存在了。2026-09-01 補修。 這說明修完既有的 N 處不等於這個坑關上了——它只清掉了存量。 新程式要寫任何不可重建的資料時,得先問一句:這個檔案被砍在寫入中途 還救得回來嗎?救不回來就走 atomic_io,不要自己 to_csv / open(w)。
故障偽裝成空狀態
歷史盤點 5 次 本登記簿收錄 6 項
抓不到資料時回傳空的、或填一個看起來正常的預設值,而不是報錯。 下游看到的是「今天沒有」,而不是「壞了」——而「今天沒有」看起來 完全正常,沒有人會去查。 ・法人籌碼爬蟲回空 DataFrame
・RSI 填 50
・結算價寫 0.0
・主題熱度預設 5.0
・抓價全滅時靜默改用歷史舊價
抓不到就拋錯或明確標記缺漏,絕不用中性預設值頂替。 中性值最危險,因為它落在正常範圍內。
寫死的清單決定了系統的上限
歷史盤點 3 次 本登記簿收錄 3 項
題材、股票、名稱全部是手填清單。清單外的東西在結構上不可能被看見, 而且看不見的時候沒有任何跡象。補清單是治標——補到 30 個,第 31 個 照樣看不見。 ・26 條 RSS 查詢的關鍵字就是題材名
・THEME_MAP 12 個
・yfinance 只抓手填的 23 檔
改成從沒有預設立場的資料讓答案浮出來。已實作 theme_discovery.py (廣掃新聞 ∩ 全市場價量 ∩ 共動)與 stock_names_official.py(交易所全表)。
沒有任何東西會跟它對帳
歷史盤點 3 次 本登記簿收錄 6 項
一個數字算出來就直接印,沒有第二條路徑驗算。錯了也不會有人發現—— 期望值公式把虧損加成正的,活了好幾個月。 ・期望值 +5.22% vs 實現 −0.56%
・prob_boom 校準表用被污染的欄位
・log 標 ✅ 但 69% 解析失敗
關鍵數字旁邊一定要有一個獨立算法的對照值,差太多就出警告。 已在 print_accuracy_report 加「實現平均報酬」與期望值對帳。
用一個在訊號存在時買不到的價格
歷史盤點 2 次 本登記簿收錄 2 項
績效計算取的價格,在報告產出的當下已經不存在了。台股 13:30 收盤、 而所有 bot 都是 15:40 之後才跑——任何「當日收盤」或更早的價格, 訊號出現時都已經買不到。 這不是資料洩漏(資訊上合法,收盤後才用收盤價),是不可實現的假設: 回測數字對,但那筆交易做不出來。它不會拋錯、不會有缺值, 只會讓整套績效安靜地建立在一個不存在的成交上。 ・run_theme_hunter 的 entry_price = 當日收盤(2026-08-25 修)
・發現層結算用 trade_date 而非進榜日(2026-09-01 修)
進場價一律取訊號產出之後第一個可成交時點的價格(T+1 開盤或收盤), 抓不到就保留 pending。絕不退回用當日收盤頂替——那正是這個坑的原型。 寫任何新的結算路徑時先問一句:「這個價格,在訊號出現的那一刻買得到嗎?」 兩次都是新管線各自犯一次,所以規則要寫在 pattern 這一層,不是個別修完就算。

個別項目(22 項)「原因」要指到檔案:行號或機制,不能只說「怪怪的」

項目/狀態現象原因影響改善方法
prob-boom-not-a-probability
修了一半
沒有任何東西會跟它對帳
prediction_log 最近幾筆的 prob_boom 全都是 95;報告印成「勝率 95%」theme_simulator_v3.py:117 是 min(scenario.catalyst_strength * 10, 95)。 分子只跟題材有關,同一題材底下每一檔股票拿到完全相同的分數; 而 catalyst_strength 在 catalyst_monitor.py:770 已被 clamp 在 10.0, 所以熱度 ≥9.5 的題材必然輸出 95。 它在建構上就不可能有個股鑑別力——這不是「校準不好」, 是它根本不是機率,只是催化劑分數換了個單位再撞上天花板。 註解還寫著「× (美股Boost+1)」,但程式碼裡沒有 boost 這一項。 使用者看到「暴漲機率 95%」,而實現的勝利+達標率是 6.5%(31 筆)① 短期:停止把它標示為「機率」或「勝率」,改稱「催化劑強度(0-95)」。 ② 中期:若要有真正的機率,必須是個股層級的模型且經過校準檢定 (像台指期那邊的 ECE 閘門)。在那之前不應該有任何數字叫「機率」。 ③ 已做:網站的可靠度表已標明「這張表測不出東西」並說明兩個理由。
pred-id-collision
修了一半
pred_id=46 在 outcome_log 出現兩次,對應不同股票與不同日期prediction_tracker.py:62-77 的 get_next_pred_id():舊格式時回傳 len(df)+1,新格式時回傳 max(int_ids)+1。兩套編號器交替使用 必然撞號。 主鍵失效,任何 join 出來的分析都可能對錯行改用全域單調序號或 UUID。theme_simulator_v3 已改用 sim-{日期}-{代號}。
discovery-no-edge
量出來的事實
發現引擎找得到清單外的題材,但那些題材沒有超額報酬不是缺陷,是量出來的事實。10 個交易日、47 筆發現的歷史回測: 5 日 題材 +3.60% 市場等權 +3.50% 超額 +0.10% t=+0.08 10 日 題材 +3.48% 市場等權 +4.10% 超額 −0.62% t=−0.39 兩個都與 0 無法區分。勝率 70.2% 不代表準——那段期間全市場 5 日平均 漲 3.5%,什麼都在漲。 ⚠️ 這個回測還帶著無法消除的樂觀偏誤:Google News 的 after:/before: 回的是今天的索引與排序,後來證明重要的文章更可能還留著。 所以判讀是不對稱的:不賺錢 → 結論成立;賺錢 → 只能說值得繼續累積。 現在落在第一格。 「看得見清單外的題材」與「那些題材會賺錢」是兩件事,目前只有前者成立。 13 個發現的題材裡 10 個是寫死清單看不到的(31/47 筆紀錄)。 ① 已做:theme_discovery_track.py 每天存全市場收盤,累積乾淨樣本, 三個月後可用不帶偏誤的資料重測。 ② 待做(不可現在做):想試「只取 🆕 全新題材」或「只取共動 ≥3 檔」 之類的子集。但 n=10 天且語料偏樂觀,任何子集分析保證找得到假訊號。 要做必須先寫死判準、等乾淨樣本。這是台指期那邊 prereg 制度的同一條規矩。
loop-variable-leak
已修並驗證
沒有任何東西會跟它對帳
outcome_log 同一個結算批次內,所有股票的 scenario 與 prob_boom 完全相同prediction_tracker.py 寫 outcome_log 時,for r in resolved: 迴圈裡取的是 row['scenario'] 與 row['prob_boom']——row 是上面 for _, row in pending.iterrows() 殘留的最後一筆。 根因是 resolved 這個 dict 本來就沒帶這兩個欄位,所以下游只好去抓 迴圈外的變數。 13 個多筆批次裡 12 個的 prob_boom 是常數;分箱表 50 筆中 46 筆(92%) 落在這種批次。依 prob_boom 分箱 = 依批次分箱 = 依時間分箱, 量到的是不同時期的行情差異。上游那個 corr −0.273、以及 v1.3 據此 「廢除 prob_boom 排序」的決策,資料基礎都不可信。 在 resolved.append 時就帶好 scenario/prob_boom/name,下游一律用 r。已修。
expectancy-formula
已修並驗證
沒有任何東西會跟它對帳
報告印「期望值 +5.22%」,但同一份資料的實現平均報酬是 −0.56%prediction_tracker.py 的 (1 - win_rate)/100,而 win_rate 已經是百分數 (30.0)。(1 - 30.0)/100 = −0.29,負負得正,虧損被「加」進期望值。 只要 win_rate > 1 就必然高估。 使用者最直接看到的「這套策略賺不賺」的數字,報的是反的,差 5.78 個百分點改成兩邊同一個分母:p_win × avg_win − p_loss × avg_loss。 並在旁邊加印實現平均報酬做對帳,差 >3pp 就出警告—— 這個 bug 能活這麼久,就是因為沒有任何東西會跟它對帳。 修正後 −0.52% vs 實現 −0.56%,差 0.04pp。
rsi-default-50
已修並驗證
故障偽裝成空狀態
抓不到技術指標的個股,仍出現在推薦名單上並標「✅進場區」run_theme_hunter.py 的 rsi = ... if tech.get('RSI_14') is not None else 50。 get_latest_tech() 在下載失敗或資料不足 30 根時直接 continue,該股根本 不在 tech_data 裡 → RSI 填 50。 而 50 正好落在報告的「中線 RSI 40-60 整理正常區」—— 「沒資料」因此偽裝成一筆有目標價、有停損、標著 ✅進場區 的具體建議。 無法量化(不知道歷史上有幾檔中招),但這是最危險的一種: 它不是變成空的,是變成一筆看起來完全正常的買進建議。 AGENTS.md:41 記著 2026-08-19「結算抓不到價寫 0.0 造成 28 筆假結算」, 結算路徑當時修了,推薦路徑的同型 bug 沒修。 缺指標或缺報價一律排除,並在報告列出「今日缺資料 N 檔」。 排除方式是把項目從 r['results'] 裡拿掉,不是在每個消費點各加判斷 ——下游有 5 處消費點,漏掉一處就等於沒修。已修。
csv-column-shift
已修並驗證
故障偽裝成空狀態
prediction_log.csv 有 70/108 列(65%)的欄位語意整排位移一格theme_simulator_v3.py 用手寫字串 append,只給 14 欄,而檔頭是 21 欄 且第一欄是 pred_id。整列往左移:日期跑進 pred_id、代號跑進 pred_date、 股名跑進 stock_id、題材跑進 name。 每一列的欄數看起來都對,所以 pandas 與 csv.reader 都不報錯。 pred_id 這個閉環主鍵壞了:outcome_log 有 38 個 pred_id 在 prediction_log 裡找不到來源,且 pred_id=46 出現兩次(不同股票、不同日期)。 「全部來自真實到期結算的閉環」這個說法,在 38/70 的紀錄上追不回去。 改用 csv.DictWriter + 從檔頭讀回來的 fieldnames,欄位對齊由結構保證。 另修:相對路徑改絕對路徑(這支自己沒 chdir,從別的目錄執行會生一份 新的空 log);status 標 'simulated' 而非留空或 'pending'。已修。 ⚠️ 已寫進去的 70 列仍是舊格式(依使用者指示不動資料)。 tx_forecast 的讀取層有還原邏輯(依「第一欄是不是日期」判斷 schema)。
theme-funnel
已修並驗證
寫死的清單決定了系統的上限
題材永遠只有 ABF、AI GPU、記憶體那幾個三層寫死的清單,每層都在縮小: 26 條 RSS 查詢 → 12 個 THEME_MAP → 7 個 SCENARIOS(唯一能產生推薦的) 而且每一條查詢的關鍵字就是題材自己的名字(q=ABF+載板+欣興), 所以它只能查回它已經相信的東西——這是確認機不是發現機。 18/26 條查詢的結果抓進來、算了分數、然後丟掉:CPO、AI 散熱、AI 電源、 化合物半導體、機器人、軍工無人機、重電綠能、生技…全部結構上不可能 變成一筆建議。 再加上 theme_bridge 用子字串比對傳分數,而「記憶體超級循環」與 「記憶體封測」互不包含——12 個題材只有 1 個對得上,其餘 11 個永遠 拿寫死的預設 5.0。頁面上「除了 ABF 是 10.0 其他全是 5.0」不是評分結果, 是預設值。 系統結構上看不見清單外的任何題材,而且看不見時沒有任何跡象沒有去補那三張清單——補清單只是把 12 個變成 30 個,第 31 個照樣 看不見,那是治標。 改成新增一條不經過這個漏斗的路徑:theme_discovery.py。 查詢只含市場結構詞(盤後/類股/漲停/買超),題材從 「廣掃新聞詞頻 ∩ 全市場價量異常 ∩ 個股共動」的交集浮出來。 並用 _assert_queries_are_theme_free() 結構性擋住有人往查詢裡加題材名。 實測第一天就撈到「矽晶圓」(現有 12 個 THEME_MAP 完全沒有), 當天 2 檔同向 +8.63%。
duplicate-outcomes
已修並驗證
網站顯示的勝率 42.0%,去重後只有 24.2%outcome_log 70 列裡有 19 列是逐字重複(2026-05-03 那一批,同一筆 結算被寫進去 3–8 次),而且重複集中在贏的那幾筆——24 列重複裡 16 列是勝利/達標,全樣本只有 21/70。 寫入端為什麼會重複尚未查清(可能是同一天重跑多次且沒有去重)。 勝率 42.0% → 24.2%;平均 5 日報酬 −0.56% → −2.88%已修(2026-08-25),三個層次都做了。 ① 查清楚成因。 重複列的 pred_id 各不相同(145–168),其他欄位 逐字相同——也就是說同一筆預測被寫進 prediction_log 3–8 次, 然後每一份都各自結算。那些 pred_id 在 prediction_log 裡找不到來源 (是那 38 個孤兒之一),寫入者不在 repo 裡。 ② 寫入端加去重鍵。 用 (pred_date, stock_id),不是 pred_id ——後者本身就會撞號(兩套編號器交替使用,見 pred-id-collision)。 比對前兩邊都正規化成字串:prediction_log 混著舊格式列,同一欄可能被 pandas 讀成 str/int/float,型別不同時 == 恆為 False,去重會靜默失效。 實測 '2382' / 2382 / 2382.0 三種寫法現在都擋得住。 log_prediction() 的既有去重有同一個型別問題,一併修。 擋下時會印「🛡️ 去重擋下 N 筆」——靜靜跳過與靜靜寫入一樣糟, 前者會讓人以為結算數變少是市場沒訊號。 ③ 清掉既有的 19 列(使用者 2026-08-25 核准後才動)。 先寫 outcome_log.csv.bak-20260825-dedup,被刪的每一列另存 removed_duplicates_20260825.csv(含刪除理由與時間),git 留可回退的 commit。 刪除前 73 列 勝率 40.4% 平均 −0.79% 刪除後 54 列 勝率 22.9% 平均 −3.05% 19 列全部來自 2026-05-01,是同 5 筆預測的複本;而重複集中在贏的那幾筆 (24 列裡 16 列是勝利/達標,全樣本只有 21/70),所以它是單向把勝率灌高。 讀取層的去重保留著防上游再出問題,現在回報「再去重 0 列」。
institutional-crawler
已修並驗證
故障偽裝成空狀態
報告顯示「法人淨買超=0」,讀起來像查到了只是法人賣超market_data.py:77 用 股票代號,而證交所 T86 的實際欄位是 證券代號 → KeyError → 被 except Exception 吞成空 DataFrame → 上層 if df.empty: return 靜默結束 → data/institutional.db 檔案根本不存在 → 查詢一律回全 0 → check_beta_tech 的 total_buy > 0 永遠是 False。 整條鏈上沒有任何一個環節會報錯。專案其他地方用的都是 證券代號, 只有 market_data.py 用錯。 Beta 策略永遠進不了場。而且目前 market_data.py 因為 import pandas_ta (套件沒裝)整支 import 不進來,連帶 router.py 也是—— 所以現在是死路徑,沒有實際傷害,但也代表少了一個真正的訊號源。 已修(2026-08-25)。實際壞的比原本查到的多: ① 欄位名錯的有三個不是一個:股票代號→證券代號、 外陸資買賣超股數→外陸資買賣超股數(不含外資自營商)、 總買賣超→三大法人買賣超股數。而 row.get("總買賣超", 0) or 0 讓最後一個恆為 0 且看起來像真的查到 0。 ② 查詢參數也錯:strType 應為 selectType。 ③ pandas_ta 改成函式內延遲載入——它只用在技術指標, 卻寫在模組頂層把整支 market_data.py 一起拖死,法人籌碼是被連坐的。 ④ save 前先 init_institutional_db(),並改用 INSERT OR REPLACE (PRIMARY KEY (ticker,date)),同一天重跑覆寫而非累積。 ⑤ 補上櫃:T86 只有上市,而 6488 環球晶、5483 中美晶都在櫃買。 欄序用三條恆等式驗證而不是用猜的——初版索引寫錯時, 912 列裡有 781 列剛好通過、131 列沒過,部分失敗比全部失敗難發現得多。 ⑥ 改成 backfill_institutional(7):櫃買 SSL 間歇性壞掉時不無限重試, 而是每次執行掃最近 7 個交易日補洞——暫時性失敗會自動癒合。 (法人資料與價格歷史不同,交易所歷史端點吃日期參數,補得回來。)
entry-price-unobtainable
已修並驗證
用一個在訊號存在時買不到的價格
所有績效統計都建立在一個現實中買不到的進場價上run_theme_hunter.py 的 entry_price = 當日收盤價,而 bot 是 15:40 才跑, 台股 13:30 就收盤了。這個價格在報告產出時已經不存在。 這不是資料洩漏(資訊上合法,收盤後才用收盤價),是不可實現的假設。 整套勝率與期望值都偏樂觀,幅度未量化已修(2026-08-25)。結算改用 fetch_open_price() 抓預測日之後第一個 交易日的開盤價,抓不到就保留 pending——絕不退回用收盤價頂替, 那正是這個 bug 的原型。同時保留舊口徑的報酬做對照,差 >0.5pp 會印出來。 ⚠️ 實測結果與預期相反,值得記下來。 14 筆歷史紀錄的滑價 平均 −0.19%、中位數 −0.72%:T+1 開盤通常低於前一日收盤, 所以舊口徑其實略為保守而不是樂觀。 但修正的理由不受影響:那個價格買不到,這是正確性問題不是偏誤問題。 而且單筆差異高達 ±5%(最大 −4.87%),對個別交易的損益影響很大, 只是正負相抵後平均接近零。 (該樣本含重複列,見 duplicate-outcomes,所以有效樣本更少。)
resolve-calendar-days
已修並驗證
「5 日報酬」欄位裡混了 1、2、3、4 個交易日的報酬prediction_tracker.py 的 date_5d = pred_date + timedelta(days=5) 是 日曆日不是交易日,再用 abs(diff).idxmin() 貼到最近的交易日。 13 個結算批次裡有 4 個 resolve_date 落在週末,導致不同 pred_date 的 預測被結算在同一根 K 棒上。最極端的是 gap=1 天和 gap=2 天的兩批, 卻都寫在 gain_5d 欄位裡。 不同持有期的結果被放進同一個勝率分母;報酬幅度不可比已修(2026-08-25)。新增 nth_trading_day_after(),直接數該檔股票自己的 交易日——不必維護交易日曆,而且自動處理停牌(停牌日不會出現在 history 裡, 本來就不該算)。還沒滿 N 個交易日就保留 pending,不用「最近的一天」湊。 實測:2026-05-11 的 5 日結算從 05-16(週六)改成 05-18; 2026-08-18 從 08-23(週日)改成 08-25。
fake-columns
已修並驗證
故障偽裝成空狀態
outcome_log 的 actual_close_10d / 20d / gain_10d / gain_20d 全部是 0.0prediction_tracker.py:350-357 硬編 0.0,從來沒有真的算過。 best_gain 也不是窗口內最佳報酬,就是 gain_5d 複製一份。 諷刺的是 notes 寫著「20天持有」、HOLD_TARGETS 設定 20 天、 目標價按 +12%/20天 訂,但結算只在第 5 個日曆日做一次。 目標價與結算窗口在設計上就對不起來。 58 筆已結算的列,10d/20d 欄位 100% 是 0.0,任何用到它們的分析都是錯的已修(2026-08-25)。真的算:用同一個 nth_trading_day_after() 取第 10、20 個交易日的收盤,算出 gain_10d / gain_20d / stop_hit_10d/20d。 未滿期寫 None(CSV 裡是空白)而不是 0.0——「還沒到」與「報酬是 0」 必須分得出來。best_gain 也不再是 gain_5d 複製一份,改成三個持有期裡 最好的那個(有算到的才算)。
stop-loss-optimizer-circular
已修並驗證
沒有任何東西會跟它對帳
v1.4 宣稱「網格搜尋實證 ATR×2.0 最優(PF 最高 2.44)」三個問題疊在一起: ① 循環論證:stop_loss_optimizer.py:46-47 把 actual_close=0 的列 補成「剛好在停損價出場」,於是虧損 = 停損距離,而停損距離正是 這個網格在優化的變數。而 2026-08-19 修 bug 之前,所有列都是 actual_close=0。「PF 2.44」極可能是這條 imputation 的產物。 ② 搜尋空間 325 組、樣本 ~20-31 筆,且 if len(trades) < 3: continue ——3 筆交易的組合就有資格當「最優」。沒有 holdout。 ③ 結論寫死在程式裡:第 302-303 行的「結論:寬停損對本系統幾乎無效」 是字串常數,不管資料算出什麼都會印。 而且這支程式現在根本跑不起來——第 20 行 BASE = '{_PC_STR}/...' 漏了 f 前綴,所以 v1.4 的「實證」無法重現。 停損參數在 4 個月內 2.0 → 1.5 → 2.0 繞了一圈回原點,每次都以「實證」包裝已修(2026-08-25),而且移除循環論證之後結論完全變了: 舊版宣稱 「網格搜尋實證 ATR×2.0 最優,PF 最高 2.44」 實際重跑後 2x ATR 勝率 21%/均報酬 −3.8% 3x ATR 勝率 21%/均報酬 −4.2% 最佳組合也只有 勝率 18.8%/均報酬 −3.1% 沒有任何一組是賺錢的。 那個 PF 2.44 確實是補值的產物。 修的五處: ① 移除 stop_price imputation(改成排除 actual_close 無效的列並回報筆數) ② 移除兩行寫死的結論字串——它們不管資料算出什麼都會印 ③ 修 BASE = '{_PC_STR}/...' 漏掉的 f 前綴(所以這支程式本來根本跑不起來, v1.4 的「實證」無法重現) ④ 最少交易筆數 3 → 20(3 筆樣本在 175 組裡挑最大值必然找得到好看的數字), 並在輸出頂端標明「樣本內、無 holdout、搜尋 N 組」 ⑤ 分母寫死的 /20 改成實際筆數(resolved 已有 34 筆,會印出 >100% 的勝率) 順帶:row.get('rsi', 50) ——RSI 預設 50 在這個專案出現第三次 (推薦路徑、結算路徑、這裡)。50 落在「正常區」,缺資料的紀錄會安靜地 通過每一個 RSI 濾網,等於用一批「不知道 RSI 是多少」的樣本去調 RSI 門檻。 已改成 RSI 缺值一律排除。並在載入時統一轉型,不再逐處撞 TypeError。
llm-parse-failure
已修並驗證
沒有任何東西會跟它對帳
log 標「✅ 有效:5/16」,且每個主題的 α均、β均都是 0.0⚠️ 2026-08-25 查清楚了,而且根本不是解析問題。 原本的判讀是「LLM 抽取的解析失敗率最高 69%」。查下去發現真相是 根本沒有 API 金鑰,一次模型請求都沒發出過。三個缺陷串成一條: ① catalyst_intel_center.py 有兩份同名的 _load_openrouter_key (129 行與 160 行),後者覆蓋前者。 ② 存活的那一份是用正則去 grep catalyst_monitor.py 的原始碼, 找 (憑證細節不公開) = "字面字串"。但那邊早就改成 (憑證細節不公開) = _load_minimax_key()(函式呼叫)→ 正則永遠不匹配 → None。 ③ 就算走到 _load_minimax_key() 也拿不到:(憑證細節不公開) 的 providers 只有 zai、沒有 minimax-oauth;(憑證細節不公開).minimax 只有指紋 ((憑證細節不公開))不含 (憑證細節不公開),而它宣告的來源是 env:(憑證細節不公開)——而那個環境變數沒設。 於是掉到最後的後備 return "sk-cp-...lOAo", 那是一個帶刪節號的佔位字串,長度 13,不是金鑰。 於是 call_llm 每次都回 {"error": "No API key found"}, 而 .get('alpha_score', 0.0) 把它靜默轉成 0.0。 α/β 全部是 0.0 不是「模型判斷為中性」,是根本沒問過模型。 (憑證細節不公開).minimax 的 request_count 是 0——從來沒用過。 那份 log 從頭到尾沒有一個 ❌,結尾是「✅ bot 全鏈路完成」 ——✅ 在這個系統裡不代表成功。 題材排名建立在大部分沒抽取成功的 LLM 輸出上程式面已全部修好(2026-08-25),剩下的只有一件事需要你提供。 已修: ① 移除重複的 _load_openrouter_key,改成直接呼叫 catalyst_monitor._load_minimax_key()。 用正則讀另一個模組的原始碼取金鑰,本來就會在對方改寫法時無聲失效。 ② _load_minimax_key() 補上讀環境變數 (憑證細節不公開) (那正是 (憑證細節不公開) 宣告的來源,先前程式碼根本沒讀)。 ③ 拿掉後備的佔位字串 "sk-cp-...lOAo",改回 None—— 讓上游知道「沒有金鑰」,而不是拿一個假金鑰去撞 401。 ④ AsyncOpenAI(...) 從模組層改成延遲建構。沒有金鑰時舊版會 在 import 時就拋 OpenAIError,整支 catalyst_monitor import 不進來 ——與 pandas_ta 那次是同一種病:模組層的失敗會傳染。 ⑤ 報告與哨兵把「沒有金鑰」與「有金鑰但解析失敗」分開報。 混在一起會讓人去查 prompt,而實際上根本沒發出過請求。 網站 /theme-sources 現在直接寫「根本沒問過模型」。 ⑥ 改用 GLM(Z.ai coding plan)取代 MiniMax(2026-08-25,使用者指示)。 MiniMax 沒有可用金鑰((憑證細節不公開).minimax 只有 (憑證細節不公開) 不含 (憑證細節不公開),request_count 是 0——從來沒成功過), 而 GLM 的金鑰本來就已經在 (憑證細節不公開) 的 (憑證細節不公開), (憑證細節不公開) 也已經記著端點 `(憑證細節不公開)。 所以不必新增任何金鑰,只要讓程式去讀既有的那把。 ⑦ 新增 llm_client.py——全專案唯一的金鑰載入與呼叫路徑。 金鑰載入散在三個模組正是這次出事的根源;集中之後「金鑰從哪來」 只有一個答案,而且它會回報「有沒有、長度多少」(不印金鑰本身)。 模型預設 glm-4.5:新聞評分是結構化小任務,不需要推理模型。 glm-4.6 可用但 113 個 completion token 裡有 105 個是 reasoning, 一次要評上百則新聞時成本差很多。max_tokens 提到 1500—— 實測 glm-4.6 用 200 會被推理吃光而 content 為空字串。 實測單則新聞:「矽晶圓三年首見漲價潮,環球晶、台勝科迎接循環轉折」 → alpha 8.3 / beta 8.7 / ticker 6488(環球晶)/ 推理正確。
no-venv
已修並驗證
故障偽裝成空狀態
market_data.py 現在 import 不進來(No module named 'pandas_ta')不是 venv,是 pip install --user 裝在 ~/.local/lib/python3.14/, 與 apt 套件混用,零版本鎖定,專案內沒有 requirements.txt。 路徑綁死小版本:Ubuntu 一旦把 /usr/bin/python3 換成 3.15, yfinance、pandas、feedparser、curl_cffi 會同時從 sys.path 消失。 而 pandas(pip) 建在 numpy(apt) 的 ABI 上,apt upgrade python3-numpy 可能直接 ABI 破壞 pandas。 已經有一支模組壞掉沒人發現;下次系統更新可能整條鏈路同時掛掉已修(2026-08-25)。四件事: ① 建 .venv + requirements.txt,10 個套件全部 pin 版本。 刻意不列 pandas_ta(與 pandas 3.x 不相容)與 streamlit/plotly (只有互動式儀表板需要,不在 daily_bot 鏈路上),理由寫在檔案裡。 ② cron 兩條都改走 .venv/bin/python。 ③ daily_bot 找不到 venv 時會出聲再退回系統 python,不靜默降級。 ④ 新增 scripts/smoke_imports.py 並排進 daily_bot 第 0 步。 冒煙測試第一次跑就抓到兩個沒人知道的壞模組: loop_health_dashboard 缺 streamlit(合理,已列為選配) theme_report_generator from stock_theme_map import STOCK_NAMES ——而 stock_theme_map 裡根本沒有 STOCK_NAMES 已改用交易所權威股名表(1,980 檔),對不到再退回 THEME_MAP。 順帶發現 j4_select_core 在 import 時就執行主流程並寫檔 (寫死的 Windows 路徑),冒煙測試已排除它以免變成一次真的執行—— 但那本身是缺陷,尚未修。
no-alerting
已修並驗證
故障偽裝成空狀態
系統不知道「今天沒跑成功」,也沒有任何告警路徑AGENTS.md:32-34 宣稱有三條 cron(21:00 健康報告、16:05 hard_case_mining、 22:05 prob_boom_calibration)。 ⚠️ 這一條先前記錯了,更正如下。 原本寫「三條全部不存在」, 但 2026-08-25 12:50 實際檢查時,21:00 的 daily_status_report 是存在的 (16:05 與 22:05 確實不存在)。無法確定是先前查錯還是之後才加上, 如實標明而不是默默改掉。 實測 daily_status_report.py 跑得起來,而且它自己就在標 「prob_boom 公式嚴重失準」——所以問題不是「沒有健康報告」, 是沒有人看它,而且它的輸出只進 log 不會通知任何人。 全 scripts/ grep smtp|sendmail|telegram|webhook|notify|alert → 零實際 發送程式碼。系統連 MTA 都沒有(sendmail/mail/postfix 全部 not found), 所以 cron 的失敗郵件也寄不出去。 任何失敗都要人主動去翻 log 才會發現① ~~裝上 21:00 的健康報告 cron~~ ——它已經存在(見上方更正)。 真正缺的是通知路徑:它的輸出只附加進 daily_bot.log, 而系統連 MTA 都沒有,沒有人會看到。 ② 紅燈條件用「latest.json 的 date 是否等於今天」「tech_indicators.json 的 date 是否等於最近交易日」,而不是以 24 小時牆鐘判斷。 ③ ⚠️ theme_bridge.py:198-213 的 _load_latest() 在 age ≥24h 時靜默重產, 而重產用的是同一份可能已過期的 catalyst_snapshot——等於把舊資料 洗成新的 generated_at。每個週一讀到週五的檔案必然觸發一次。
non-atomic-writes
已修並驗證
不可重建的資料用就地覆寫
全專案 grep os.replace / .tmp / fsync 是零命中所有寫檔都是 read → 改 → to_csv(同一路徑)。而外層 cron 是 timeout 1800,到點送 SIGTERM——如果那一刻正好在寫入中途, 留下的就是截斷檔,而且沒有備份。 受害最重的是 data/ml_data/tw_stocks_2y_features.csv:11,225 列、 兩年的 ML 特徵。交易所的當日端點只給今天,漏一天永久補不回來。 8 處不可重建的資料處於「隨時可能被截斷且無法還原」的狀態新增 scripts/atomic_io.py:寫 <name>.tmp → fsync → os.replace()。 POSIX 保證同檔案系統的 rename 是原子的。8 處全改,ML 特徵另加 .bak。 ⚠️ 原子寫入防不了「算錯了但完整地寫回去」——2026-08-19 那次 28 筆假結算就是後者:檔案完好,內容是錯的。所以另加 guard_shrink(), 要寫回的筆數少於現有 90% 就拋錯(上游抓不到資料時最常見的失敗形態)。 ⚠️ 跨檔案一致性它也保證不了。theme_bridge 連寫兩個檔案,中間掛掉會 「快照新、latest 舊」。改成固定先寫每日快照、後寫 latest—— 「latest 較新」永遠成立,反過來不成立。
stock-name-contradictions
已修並驗證
寫死的清單決定了系統的上限
9 張手填的「代號→名稱」對照互相矛盾,10 個代號有爭議每個模組各自維護一份手打的名稱表,沒有共同來源。而交易所自己 每天都在公布——這件事不該靠投票或記憶決定。 最明顯的是 2881 在 8 張表裡一致是「富邦金」,而 2882 在 6 張表裡 也是「富邦金」。兩個代號不可能都是富邦金,所以那 6 張一定錯。 outcome_log 裡「英業達」同時掛在 2356 與 2357 上,而兩者進場價中位數 是 64 與 581(差 9 倍)。價格是照代號抓的所以績效沒錯, 但任何依名稱做的比對、去重、人工複核都會出錯(17 筆,佔已結算 24%)。 連帶問題:題材抽取會把個股名當題材(「聯發科概念股」→「聯發科」)。 新增 stock_names_official.py:交易所全市場 1,980 檔,7 天快取,原子寫入。 ① 仲裁:比對後修正 40 處指向不同公司的名稱,散落 10 個檔案。 分類是關鍵——88 筆裡 18 筆只是縮寫(「京元電 vs 京元電子」), 把它們跟「建準 vs 興勤」混報,整份清單會被當雜訊沒人看。 ② 排除清單:題材抽取用它結構性排掉個股名,不必手工加停用詞 (手工清單一定會漏,而且漏掉時沒有跡象)。 只比完全相等,不做子字串——「光學」是題材、「大立光」是個股。 ③ 每日檢查已排進 daily_bot 第 6 步,再有人手打錯名稱會立刻被抓到。
discovery-settlement-duplicates
已修並驗證
沒有任何東西會跟它對帳
發現層首批結算即將把 3 個題材寫成 25 筆watchlist.jsonl 是每跑一次 append 一列,不是每天一列。上線日 2026-08-25 重跑多次,同一個 (進榜日, 題材) 最多重複 9 筆 (AI×9、記憶體×9、矽晶圓×7、金融×4;54 列實際只有 26 個進榜事件)。 theme_graph.dedup_last() 與 web/data.py::theme_discovery() 早就按 (日期, 題材) 去重了,但結算路徑 theme_discovery_track.py 沒有 ——同一個去重規則在三個消費點各寫一次,漏掉第三個。 抓到的時機是首批 5 日結算落地前一天(outcomes 當時仍為空)。 再晚一天就會寫進 append-only、且交易所補不回來的檔案。 未實際發生(趕在寫入前修掉)。若發生:同一個發現在成效統計裡被算 9 次, 而發現層本來就只有幾十筆樣本,等於整層的超額報酬與勝率全部失真, 且無法事後分離——outcomes 沒有記「這筆來自哪一列 watchlist」。 已修(2026-09-01)。結算前先按 (date, term) 去重,同組取 trade_date 最新那筆(市場資料最新鮮),watchlist 本身不動(append-only)。 兩個數字都留著顯示(沿用 AGENTS.md 既有慣例): 「watchlist 54 列 → 26 個 (進榜日, 題材);28 列是同日重跑」。 只報去重後的,「一天跑了 9 次」這件事就從畫面上消失了。 回歸測試 test_discovery_pipeline.py::test_dedup_takes_latest_trade_date 與 ::test_dedup_keeps_both_numbers_visible 守著這兩件事。
discovery-entry-day-unobtainable
已修並驗證
用一個在訊號存在時買不到的價格
發現層的進場價用了訊號發布前就已收盤的價格theme_discovery_track.py 取進場日時用 trade_date(市場資料日)而不是 date(進榜日)。2026-08-25 那批的 trade_date 是 08-24——交易所當日 端點還沒更新,發現引擎記下的是前一天的收盤。 照 trade_date 推算,進場日會落在 08-25 收盤;但發現引擎 15:40 才跑完, 台股 13:30 就收盤了。那個價格在訊號存在的時候已經買不到。 這是 entry-price-unobtainable 的第二次,換了一條管線再犯一次: 上次是 run_theme_hunter 用當日收盤,這次是發現層用前一日收盤。 模組開頭自己寫的判準(「進場價用進榜隔一個交易日的收盤」)是對的, 是程式沒跟上文件。 未實際發生(同樣趕在首批結算前修掉)。若發生:所有發現層績效建立在 買不到的價格上,而發現層的成效正是晉升門檻 P1~P3 的唯一依據—— 等於用假數字決定哪些題材進推薦。 已修(2026-09-01)。進場日一律以進榜日 date 為準,買在其後第一個 交易日的收盤,不足期就 pending。首批 5 日結算因此順延到 2026-09-02。 回歸測試 ::test_entry_day_uses_listing_date_not_market_date 守著它: 餵一筆 date 晚於 trade_date 的紀錄,斷言它不會被結算。
satcom-half-promotion
已修並驗證
寫死的清單決定了系統的上限
SCENARIOS 裡有一個情境,每天跑、每天被整個剔除,從未出現在推薦裡SATCOM 低軌衛星/太空通訊爆發(催化 9.0,過閥值)的 5 檔成分股 (3491/2313/6285/2367/3105)沒有一檔在 rebuild_tech_indicators.py 的 23 檔抓取名單裡。run_theme_hunter 逐檔以「缺技術指標」排除 → 情境剩 0 檔(需 ≥2)→「整個情境不列入報告」。 晉升一個題材要動 6 個檔案(THEME_MAP、SCENARIOS、技術指標名單、 catalyst_key_map、RSS 查詢、分類樹),當初只做了 SCENARIOS 一步。 沒有任何錯誤訊息——它在程式碼裡看起來已經上線了。 它也從未通過任何驗證:催化 9.0 與先例文字都是手寫的。 零產出但每天佔用一次情境迴圈;更嚴重的是它讓「已上線」這件事看起來是真的已移除(2026-09-01)。評估後選擇移除而非補完:補完等於讓一個零驗證的 題材,在晉升門檻剛定好的第一天就從側門插隊。移除的功能損失是零 (它的實際產出本來就是「被剔除」)。 回歸路徑走正門:發現層已抓得到低軌衛星(lift ×1.8~×3.4), 過 PROMOTION_CRITERIA.md 門檻後照 PROMOTION_CHECKLIST.md 完整晉升。 那份 checklist 就是為了不再出現半套晉升而寫的,SATCOM 是它的頭號案例。

與 回測結果 的改善提案登記簿是同一套紀律,對象不同:那邊管台指期的研究提案(要 prereg、要算驗證折),這邊管題材獵人的缺陷(要根因、要修法)。兩邊共通的規矩是不准只寫現象。

版本 5f92ccf(第 163 次提交,2026-10-14) | 資料建置於 2026-10-14 10:30 UTC | 行情與研究結果供查看;會員可管理觀察清單並要求更新個股分析