單一題材量價分析 API
POST /api/theme-agent/analyze 依題材載入最近 40 個官方交易日的原始價格與成交股數,並由服務設定選擇兩種彼此獨立的輸出。預設 mechanical 由程式以原始 JSON 計算並組成文字,全程不呼叫模型;只有 mas.agents.theme.llm_enabled 明確設為布林 true 時,才將同一份原始 JSON 交給模型計算並直接撰寫正文。任何已登入且狀態正常的註冊會員都可使用;每個題材與實際分析交易日只生成並保存第一份成功報告,後續請求直接讀取,不再載入完整個股資料、呼叫模型或執行機械計算。
請求與回應
POST /api/theme-agent/analyze
Authorization: Bearer <registered_member_access_token>
Content-Type: application/json
{"theme_name":"強韌電網/重電設備"}
| 請求欄位 | 規則 |
|---|---|
theme_name |
必填,去除首尾空白後 1–200 字元;使用 public.theme 的完整名稱。 |
date |
選填 YYYY-MM-DD 或 null;省略時使用台北今日。一般會員可傳入任何會解析到最新實際交易日的日期;解析到更早交易日的歷史查詢僅管理員可用。 |
不接受額外欄位;request 不能覆寫輸出模式。日期是指定的分析截止日期,不會加入送給 LLM 的 JSON。
200 回應的五個欄位皆必填、非 null:
{
"theme_name": "強韌電網/重電設備",
"as_of_date": "2026-10-29",
"generated_at": "2026-10-29T08:10:00Z",
"report_version": "theme-mechanical-v1",
"report": "此欄為程式直接組成的分析正文。"
}
以上為回應格式示意。LLM 啟用時,report 是模型直接輸出的字串,後端僅去除首尾空白後封裝,不改寫正文;預設機械模式的 report 完全由程式產生。as_of_date 是實際採用的最後交易日,generated_at 是第一次成功生成的 UTC 時間,report_version 分別為 theme-v1 或 theme-mechanical-v1。同一題材與 as_of_date 的快取命中會原樣回傳這三個欄位,不更新時間或依目前 config 重算。
前端直接呈現 report,並可顯示 as_of_date。本次新增獨立端點,既有題材摘要與其他 API 不變;不需要在前端計算量價或解析正文分類。
每日報告重用
報告保存於 public.theme_analysis_cache,唯一鍵為 (theme_name, as_of_date)。省略日期、明確指定最新交易日,以及週末/休市日期,只要解析到相同官方交易日就共用同一份報告;一般會員只有解析到更早交易日才會被拒絕。最新實際交易日會先確認 OHLC 最後批次完成,再讀取 cache,避免尚未定版的資料或既有錯誤 cache 被回傳。第一次 cache miss 以題材與交易日的 PostgreSQL advisory lock 串行化;每個等待者使用一條獨立 NullPool 連線輪詢,不占用 API connection pool,也不會每次輪詢重新建立連線。取得鎖後會再次查詢 cache,因此併發請求只有一個會載入 40 日資料並生成,其餘等待後直接讀取同一筆。若首位生成者失敗,同一批等待者直接收到資料不可用,不會逐一重跑模型;之後的新請求仍可重試。
Cache key 不含輸出模式或 report_version。同一交易日第一份成功報告即為當日固定結果;當日切換 mas.agents.theme.llm_enabled 不會覆寫,下一個實際交易日才依新設定生成。
原始資料邊界
- 名單取自
taiwan.theme_member的全部當前成員;管理員查詢歷史截止日時也使用當前名單,不宣稱是當時成分股回測。同一股票可屬於其他題材。 - 每檔以
stock_code與listing_type對應taiwan.stock_ohlc;只接受tw、two。 - 由官方台股交易日曆選取不晚於截止日的最近 40 個交易日。若最新請求發生於台北當日 15:30 前,使用前一交易日;無論是交易日當晚、隔日早盤前或休市日,都必須確認實際
as_of_date的 OHLC 排程最後一批 17:45 已成功,否則回 503。批次完成後允許個別停牌或無成交成員保留當日null。假日回到最近交易日。 - 每檔保留相同的 40 個日期。缺少資料的日期保留
null,不以其他日期補位。價格必須為有限正數;成交量必須為有限非負整數,零成交量不改成缺值。全題材最後交易日都沒有有效還原收盤價時回錯誤。 - 題材、名單與價格在同一 SQL statement 的 PostgreSQL snapshot 讀取;沒有行情寫入或資料修補。
- LLM 資料 JSON 只有
theme_name、stocks;每檔只有code、name、listing_type、daily_prices,每筆只有date、close、adjusted_close、volume。不傳預算報酬、量增率、強弱標籤或額外交易日欄位。完整 JSON 後附精簡交稿指令,仍以單次生成直接取得正文,不做第二次改寫或後處理。
兩種模式都直接使用上述同一份原始 JSON。LLM 使用 repo 內的 core/mas/theme_prompt.txt 自行計算與撰寫;程式版使用十進位運算計算還原價報酬、總成交股數、中位數、MAD、涵蓋率及強弱分類,再組成文字。程式版的計算結果不會加入 LLM 輸入,LLM 正文也不會成為程式版輸入。
程式版的「典型表現」刻意採取嚴格規則:短中期都有明確多數方向、個股兩期間都同方向,且同一檔同時是 5 日與 20 日最接近中位數者,才會列為代表;否則寧可不選,不另造加權分數或硬湊名額。
失敗回應
端點的業務錯誤沿用全域格式:
{"error":{"code":"THEME_NOT_FOUND","message":"Theme not found","details":{"code":"THEME_NOT_FOUND","message":"Theme not found"}}}
| HTTP | code / 條件 |
|---|---|
| 401 / 403 | 沿用現有會員認證與帳號狀態錯誤;一般會員查詢歷史日期時回 HISTORICAL_THEME_ANALYSIS_FORBIDDEN,最新日期不要求管理員。 |
| 404 | THEME_NOT_FOUND:題材不存在或已封鎖。 |
| 422 | INVALID_ANALYSIS_DATE:未來日期、超出官方日曆涵蓋或不足 40 個交易日。請求格式驗證失敗則沿用 FastAPI 的 {"detail":[...]};OpenAPI 同時描述兩種格式。 |
| 413 | THEME_INPUT_TOO_LARGE:完整資料與 prompt 超出設定的字元上限;不截斷成員。 |
| 503 | THEME_DATA_UNAVAILABLE:日曆來源不可用、當日最後資料批次尚未完成、名單為空、交易日資料重複或 listing 不支援。THEME_LLM_UNAVAILABLE:LLM 啟用時模型配置不合法或必要的共用 router 不可用。 |
| 502 | THEME_INVALID_PROSE:空回應、非文字、JSON 或程式碼區塊;THEME_GENERATION_FAILED:其他模型生成失敗。 |
| 504 | THEME_LLM_TIMEOUT:生成逾時。 |
此高成本端點另套用 api.rate_limit.theme_analysis,有效 token 以會員 id 分桶,無法驗證 token 時退回可信 client IP;預設為每分鐘 10 次。429 與其他中介層回應請參考 API 通用錯誤處理。失敗不回傳模板報告,不把缺資料或 provider 錯誤偽裝成成功。
配置與驗證
配置集中於 config/core.yaml 的 mas.agents.theme。llm_enabled 預設 false,只有布林 true 會呼叫模型;缺少設定、型別不符或讀取失敗均 fail closed 使用機械輸出,修改設定後須重啟服務。設定只影響尚未生成的題材/交易日,既有每日報告不重算。其餘設定包含 provider/model override、temperature、max_tokens、timeout_ms、max_input_chars,以及首次生成鎖的 lock_poll_ms、lock_wait_buffer_ms。鎖的等待上限由 timeout_ms + lock_wait_buffer_ms 推導,輪詢間隔預設 500 毫秒;設定缺漏、非數值或非正數時拒絕生成。目前使用 90 秒生成上限、30 秒鎖等待緩衝、16,384 output tokens、1,000,000 輸入字元上限。LLM 啟用時經共用 quota router 的 theme service route 選模,順序以 gemini-3.6-flash 為首選並保留其他 Flash 備援,不繞過共用額度管理。
自動化測試包含六份完整真實 40 日資料、缺值與零成交量、官方日期邊界、權限、錯誤回應、原始輸入及正文原樣回傳。fixture 來源見 repo 內的 investment-agent/backend/tests/fixtures/theme_agent/PROVENANCE.md。資料庫整合測試在獨立 PostgreSQL 執行,使用真實資料快照與實際 SQL;provider mock 用於確認呼叫契約,不能當成模型分析品質的證明。
cd investment-agent/backend
PYTHONPATH=../..:. python -m pytest tests/test_theme_agent.py tests/test_theme_mechanical.py tests/test_theme_agent_service.py tests/test_theme_agent_routes.py -q
資料庫整合測試使用完整 migration 後、名為 parity 的一次性 PostgreSQL;設定 CORTEX_INTEGRATION_TESTS=1 與指向該庫的 DATABASE_URL,再執行 tests/integration/test_theme_agent_service_pg.py 與 tests/integration/test_migration_175_theme_analysis_cache_pg.py。測試會建立自己的暫存資料庫並於結束時刪除。