Recap API — 前端 / APP 整合指南
本文件面向前端 / iOS / Android 整合工程師。API 的權威文件在 api-reference.md §32A,本頁提供整合視角:範例 payload、error 處理、edge case、以及 mock/stub 建議。
1. 快速說明
Cortex 的盤後 recap 涵蓋三個維度 × 三個時間粒度,共 9 種組合,全部透過統一的 /api/recaps 路由存取。
維度 dimension |
意義 | 典型 UI 位置 |
|---|---|---|
technical |
技術面(價量結構、突破、均線、族群輪動) | 技術分析 tab |
chip |
籌碼面(三大法人、融資融券、資金輪動) | 籌碼分析 tab |
orchestrator |
跨維度整合(雙重確認個股 + 背離) | 首頁總覽 / 推播 |
時間粒度 period_type |
觸發時機 | 典型 UI |
|---|---|---|
daily |
每日盤後產出 | 每日快訊 |
weekly |
當週最後交易日產出 | 週報 |
monthly |
當月最後交易日產出 | 月報 |
首頁建議入口:Market Pulse
首頁若只需要快速告訴使用者「今天技術面與籌碼面狀況」,優先使用 Pulse API,而不是完整 recap:
GET /api/market-pulse/latest?market=taiwan
GET /api/market-pulse/{trade_date}?market=taiwan
Pulse response 會同時包含 technical_pulse 與 chip_pulse,並用同一個 source_trade_date,避免首頁兩張卡片資料日期不同。前端顯示資料日期時請使用 top-level data_status.source_trade_date;/latest 會回傳技術面與籌碼面都有資料的最新共同日期,可能早於單獨 technical/chip Pulse 的 latest 日期。headline 與 headline_parts 均為規則式產生,不依賴 LLM;前端可直接顯示 headline,或用 headline_parts 拆成 chips。
每日大盤資訊入口:Daily Market
如果首頁或 dashboard 需要顯示 TAIEX、成交量、成交值、大盤融資餘額、外資台指期空單、三大法人買賣超、漲跌家數、均線狀態、量能結構與 regime,請使用統一 Daily Market API:
GET /api/daily-market/latest?market=taiwan
GET /api/daily-market/{trade_date}?market=taiwan
/api/daily-market 會保留每個市場層級欄位的 source、date、status。沒有官方或市場層級資料時,數值會是 null,狀態會是 pending_source / no_data / source_error,前端不可把 null 當成 0 顯示。大盤融資餘額與外資期貨空單來自 official market-chip source,不是由個股 margin_balance 聚合。
前端常用欄位:
index.close、index.change_points、index.change_percent:官方大盤指數漲跌。index.previous_day_change.direction:up/down/flat,用來直接判斷相較前一個可用交易日是漲或跌。index.moving_averages.ma5/ma20/ma60:benchmark 5 / 20 / 60 日均線。index.key_ma_state:關鍵均線狀態,包含ma_alignment、price_position、above_ma5、above_ma20、above_ma60與站上均線天數。turnover.trade_value_amount:官方大盤成交值。turnover.previous_day_change.trade_value_change_amount:官方大盤成交值相較前一個可用交易日的差異。turnover.previous_day_change.direction:up/down/flat,用來直接判斷成交值放大或收斂。turnover.volume_structure:benchmark 量能結構,包含current_volume、volume_ma_5、avg_volume_20、volume_ratio、volume_status。institutional.streaks.foreign|trust|dealer|total:外資、投信、自營與三大法人合計的連續買賣超方向與天數。futures.foreign_tx_futures_position_change.foreign_tx_futures_net_short_oi_change:外資台指期淨空單相較前一個可用市場期貨資料日的變化。
均線與量能目前使用 daily_technical_snapshot 的 ^TWII benchmark row。若缺資料會回 status: "no_data",前端不可 fallback ETF proxy 或把 ETF proxy 誤標成官方加權指數技術線。
前一日指數差異、成交值差異與外資期貨部位變化有 stale gap 保護:若前一筆資料距離目前日期超過 10 個 calendar days,change 欄位會是 null 並標 status: "no_data",前端不可拿舊資料自行補差值。
認證
目前無需認證(與既有 /api/technical-recap、/api/chip-recap 一致)。
Base URL
- Local dev:
http://localhost:8000 - Production:TBD(請向後端團隊索取)
2. 兩個主要 endpoint
2.1 GET /api/recaps/{dimension}/{period_type}/latest
取該 dimension × period_type 的最新一筆 recap。前端常見用途:首頁開啟時載入最新週 / 月報。
Path params
| 欄位 | 類型 | 說明 |
|---|---|---|
| dimension | "technical" \| "chip" \| "orchestrator" | 維度 |
| period_type | "daily" \| "weekly" \| "monthly" | 時間粒度 |
Query params
| 欄位 | 必填 | 預設 | 說明 |
|---|---|---|---|
| market | 否 | "taiwan" | 市場代碼 |
Response:回傳單一 recap 物件(結構依 dimension 不同,見 §3)
Errors
| Code | 觸發條件 |
|---|---|
| 400 | dimension / period_type 不在允許清單 |
| 404 | 該組合目前尚無任何資料(pipeline 尚未產出 / 回填未完成) |
2.2 GET /api/recaps/{dimension}/{period_type}/{requested_date}
取指定日期的 recap。
Path params 同上,新增:
| 欄位 | 類型 | 說明 |
|---|---|---|
| requested_date | YYYY-MM-DD | Daily:查詢日;Weekly/Monthly:period_end_date |
語意差異(重要!)
| dimension / period_type | requested_date 語意 |
找不到會怎樣 |
|---|---|---|
technical/daily、chip/daily |
查詢日;若當日無資料(非交易日或 pipeline 未跑)→ 自動 fallback 到 ≤ requested_date 的最新交易日 | 仍找不到才回 404 |
technical/weekly、chip/weekly、*/monthly |
必須精準等於 period_end_date(該週/月最後交易日) |
直接 404 |
orchestrator/* |
必須精準等於該 period_type 的 period_end_date |
直接 404 |
實作提醒:前端不需要自己算「哪天是週最後交易日」—— 用
/latestendpoint 取,response 裡的period_end_date就是該週 key,下次帶回這個值即可。
Errors:同 /latest,再加 422(requested_date 格式錯誤)。
3. 各 dimension 的 Response Schema
3.1 Technical Daily Response
GET /api/recaps/technical/daily/{date}
{
"period_type": "daily",
"trade_date": "2026-04-17",
"market": "taiwan",
"n_stocks": 1000,
"advancers": 612,
"decliners": 358,
"breakout_count": 42,
"breakdown_count": 18,
"summary_json": {
"headline": "AI 類股續強,大盤收紅 0.8%",
"market_summary": "...",
"breadth_summary": "...",
"group_summary": "...",
"stock_summary": "...",
"risks": [
{"text": "外資買超收斂", "evidence": "foreign_net_value=10B; prev=35B"}
],
"risk_summary": "外資買超收斂"
},
"requested_trade_date": "2026-04-18",
"source_trade_date": "2026-04-17",
"is_fallback": true,
"created_at": "...",
"updated_at": "..."
}
| 欄位 | 類型 | 說明 |
|---|---|---|
n_stocks |
number | 當日有效樣本數 |
advancers / decliners |
number | 上漲 / 下跌家數 |
breakout_count / breakdown_count |
number | 技術突破 / 跌破家數 |
summary_json.headline |
string | ≤25 字標題(UI 卡片標題適用) |
summary_json.market_summary |
string | 市場結構敘述(1 段) |
summary_json.breadth_summary |
string | 廣度與內部強弱 |
summary_json.group_summary |
string | 族群輪動 |
summary_json.stock_summary |
string | 個股事件 |
summary_json.risks |
Array<{text, evidence}> |
結構化風險清單(可能為空陣列) |
summary_json.risk_summary |
string | (Legacy)risks 文字串接,供未升級的 client 使用 |
is_fallback |
boolean | true = requested_trade_date ≠ source_trade_date(回退到最近交易日) |
3.2 Technical Weekly / Monthly Response
GET /api/recaps/technical/weekly/{period_end_date}
GET /api/recaps/technical/monthly/{period_end_date}
{
"period_type": "weekly",
"period_start_date": "2026-04-13",
"period_end_date": "2026-04-17",
"market": "taiwan",
"n_sessions": 5,
"n_stocks": 1000,
"advancers": 2800,
"decliners": 1650,
"breakout_count": 120,
"breakdown_count": 58,
"summary_json": {
"headline": "本週 AI 供應鏈同步擴散",
"market_summary": "...",
"breadth_summary": "...",
"group_summary": "...",
"stock_summary": "...",
"period_summary": "週內連續三日創新高...",
"risks": [...],
"risk_summary": "..."
},
"created_at": "...",
"updated_at": "..."
}
與 daily 的差異:
- 欄位 trade_date → period_start_date + period_end_date
- 新增 n_sessions(週/月內的交易日數,週通常 5,月通常 ~21)
- 新增 summary_json.period_summary(跨日延續性觀察,daily 沒有)
- 沒有 is_fallback / source_trade_date / requested_trade_date(weekly/monthly 精準匹配)
3.3 Chip Daily Response
GET /api/recaps/chip/daily/{date}
{
"period_type": "daily",
"trade_date": "2026-04-17",
"market": "taiwan",
"n_stocks": 1000,
"total_foreign_net_value": 15600000000,
"total_trust_net_value": 3200000000,
"total_dealer_net_value": -800000000,
"total_market_net_value": 18000000000,
"total_margin_change_value": 1968695000,
"total_short_change_value": null,
"summary_json": {
"market_context": {
"official_market_margin_balance_amount": 329543807000,
"official_market_margin_change_amount": 1968695000,
"official_market_margin_status": "available",
"foreign_tx_futures_short_oi": 52279,
"foreign_tx_futures_net_short_oi": 43256,
"foreign_tx_futures_status": "available"
},
"group_flow_summary": { "...": "..." },
"stock_event_summary": { "...": "..." },
"top_bullish_stocks": [
{
"ticker": "2330.TW",
"name": "台積電",
"group_name": "AI 伺服器",
"total_net_value": 11000000,
"foreign_net_value": 10000000,
"trust_net_value": 2000000,
"dealer_net_value": -1000000,
"flow_score": 11000000,
"reasons": ["foreign_net_buy", "trust_continuous_buy"],
"dominant_actor": "foreign"
}
],
"top_bearish_stocks": [
{
"ticker": "2454.TW",
"name": "聯發科",
"group_name": "5G",
"total_net_value": -8250000,
"foreign_net_value": -7500000,
"trust_net_value": -1500000,
"dealer_net_value": 750000,
"flow_score": -8250000,
"reasons": ["foreign_net_sell", "trust_continuous_sell"],
"dominant_actor": "foreign"
}
],
"risk_signals": []
},
"requested_trade_date": "2026-04-17",
"source_trade_date": "2026-04-17",
"is_fallback": false,
"created_at": "...",
"updated_at": "..."
}
| 欄位 | 類型 | 說明 |
|---|---|---|
total_foreign_net_value |
number (TWD) | 外資當日淨買賣超金額(正=買超) |
total_trust_net_value |
number (TWD) | 投信淨額 |
total_dealer_net_value |
number (TWD) | 自營商淨額 |
total_market_net_value |
number (TWD) | 三大法人合計 |
total_margin_change_value |
number (TWD) | null | 官方/市場層級融資金額變動;沒有 summary_json.market_context.official_market_margin_status="available" 時為 null,不得視為 0 |
total_short_change_value |
number (TWD) | null | 官方/市場層級融券金額變動;目前沒有官方 TWD source 時為 null |
summary_json.market_context.official_market_margin_balance_amount |
number (TWD) | null | 大盤融資餘額金額 |
summary_json.market_context.official_market_margin_change_amount |
number (TWD) | null | 大盤融資餘額金額變動 |
summary_json.market_context.official_market_margin_status |
string | available / pending_source / source_error / no_data |
summary_json.market_context.foreign_tx_futures_short_oi |
number | null | 外資台指期未平倉空單口數 |
summary_json.market_context.foreign_tx_futures_net_short_oi |
number | null | 外資台指期淨空單口數,空單減多單 |
summary_json.market_context.foreign_tx_futures_status |
string | available / pending_source / source_error / no_data |
summary_json.top_bullish_stocks |
InstitutionalStockFlowItem[] |
法人合計淨買超前 10 大股票,已依 total_net_value 由大到小排序;金額相同時依 ticker 穩定排序 |
summary_json.top_bearish_stocks |
InstitutionalStockFlowItem[] |
法人合計淨賣超前 10 大股票,已依 total_net_value 由小到大排序;金額相同時依 ticker 穩定排序 |
top_big_holder_1000_increases |
OwnershipChangeRankingItem[] |
千張以上大戶(TDCC level 15)持股比例增加前 10 名;change > 0,依 change DESC, ticker ASC 排序 |
top_big_holder_1000_decreases |
OwnershipChangeRankingItem[] |
千張以上大戶(TDCC level 15)持股比例減少前 10 名;change < 0,依 change ASC, ticker ASC 排序 |
top_retail_50_increases |
OwnershipChangeRankingItem[] |
50 張以下散戶(TDCC levels 1-8)持股比例增加前 10 名;change > 0,依 change DESC, ticker ASC 排序 |
top_retail_50_decreases |
OwnershipChangeRankingItem[] |
50 張以下散戶(TDCC levels 1-8)持股比例減少前 10 名;change < 0,依 change ASC, ticker ASC 排序 |
數字規模:原始單位為 TWD,常見量級為百億 ~ 千億。UI 顯示建議轉換為億元或 B/M 縮寫。
InstitutionalStockFlowItem:
type InstitutionalStockFlowItem = {
ticker: string
name: string | null
group_name: string | null
total_net_value: number
foreign_net_value: number
trust_net_value: number
dealer_net_value: number
flow_score: number
reasons: string[]
dominant_actor: "foreign" | "trust" | "dealer" | "mixed"
}
OwnershipChangeRankingItem:
type OwnershipChangeRankingItem = {
ticker: string
name: string | null
current_ratio: number | null
change: number
ownership_data_date: string | null
ownership_data_age_days: number | null
levels: string[]
}
Ownership 排行是從同日 daily_chip_snapshot 的 TDCC ownership 欄位即時計算,不需要前端自行合併 levels。_increases 清單只含正 change,_decreases 清單只含負 change,item 內不再重複提供 direction。
前端顯示「法人大買標的」請直接讀 summary_json.top_bullish_stocks;
顯示「法人大賣標的」請直接讀 summary_json.top_bearish_stocks。不要再從
stock_event_summary 的連買、連賣、買轉賣 buckets 自行合併與排序。
3.4 Chip Weekly / Monthly Response
與 daily 相同欄位結構,改為 period_start_date + period_end_date + n_sessions,不含 fallback 欄位。所有 total_*_net_value 為該期間的累計淨額。
3.5 Orchestrator Response(所有 period_type 通用)
GET /api/recaps/orchestrator/{period_type}/{date} — 這是最有產品價值的 endpoint。
{
"period_type": "weekly",
"period_start_date": "2026-04-13",
"period_end_date": "2026-04-17",
"market": "taiwan",
"headline": "本週 AI 擴散,2330.TW+2454.TW 雙重確認看多",
"market_synthesis": "技術面突破搭配法人持續買超,AI 供應鏈呈現結構性強勢...",
"dual_confirmation_narrative": "本週技術面與籌碼面同向確認的強勢個股:\n- 台積電 (2330.TW)(技術面 breakout|籌碼面 continuous_buy)\n- 聯發科 (2454.TW)(技術面 breakout|籌碼面 continuous_buy)\n\n族群層面雙重確認看多:\n- 半導體 (uptrend × inflow,代表股:2330.TW、2454.TW、3034.TW)",
"dual_confirmation_stocks": [
{
"ticker": "2330.TW",
"name": "台積電",
"group_name": "半導體",
"direction": "bullish",
"technical_event_type": "breakout",
"technical_score": 0.92,
"chip_event_type": "continuous_buy",
"chip_score": 580000000,
"combined_score": 2.0
},
{
"ticker": "3008.TW",
"name": "大立光",
"group_name": "光學元件",
"direction": "bearish",
"technical_event_type": "breakdown",
"technical_score": 0.78,
"chip_event_type": "buy_to_sell",
"chip_score": 120000000,
"combined_score": 1.73
}
],
"dual_confirmation_groups": [
{
"group_id": "semiconductor",
"group_name": "半導體",
"group_type": "industry",
"direction": "bullish",
"technical_trend_state": "uptrend",
"technical_strength_score": 0.82,
"chip_flow_direction": "inflow",
"chip_flow_score": 12500000000,
"representative_stocks": ["2330.TW", "2454.TW", "3034.TW"],
"combined_score": 2.0
}
],
"divergence_stocks": [
{
"ticker": "2317.TW",
"name": "鴻海",
"group_name": "電子代工",
"technical_direction": "bullish",
"chip_direction": "bearish",
"note": "技術 breakout 對 籌碼 continuous_sell"
}
],
"divergence_narrative": "本週鴻海 (2317.TW) 呈現價漲量縮搭配法人賣壓...",
"risks": [
{"text": "成交量收斂", "evidence": "week_volume=9.2T; prev=11.5T"},
{"text": "融資擁擠", "evidence": "margin_change=+12%"}
],
"parser_warnings": [],
"synthesis_json": { "headline": "...", "market_synthesis": "...", "divergence_narrative": "...", "risks": [...], "parser_warnings": [] },
"narrative_json": { "dual_confirmation_narrative": "..." },
"technical_recap_json": { "stock_events": [...], "groups": [...] },
"chip_recap_json": { "stock_events": [...], "groups": [...] },
"model_name": "gemini-2.0-flash-exp",
"provider_name": "gemini",
"run_id": "01HKX...",
"created_at": "...",
"updated_at": "..."
}
technical_recap_json.stock_events[*].payload_json 現在會帶:
- structured_signals: TA taxonomy,例如 ma_month_breakdown、vwap_loss、gap_down_breakdown、bearish_reversal_pattern。
- ta_index_groups: MA line break、VWAP、normalized volatility、channel、gap/limit、oscillator、pattern grouped indexes。
Daily event buckets 可能新增 ma_month_reclaim、ma_month_breakdown、gap_up_breakout、gap_down_breakdown、limit_up_strength、limit_down_risk、bullish_reversal_pattern、bearish_reversal_pattern。Weekly/monthly technical events 的 payload_json.period_ma_events 會提供該週/月 K 的 MA20/MA60 站上/跌破資訊。
🔑 核心產品邏輯:雙重確認(Dual Confirmation)
這是整個 recap 系統最高價值的輸出。個股同時在技術面與籌碼面出現同向訊號 = 高勝率訊號。
| 欄位 | 資料來源 | 是否可能為空 |
|---|---|---|
dual_confirmation_stocks |
程式模板(deterministic, 不經 LLM) | 可能空陣列 |
dual_confirmation_groups |
同上 | 可能空陣列 |
dual_confirmation_narrative |
程式模板(逐一點名、零幻覺) | 空陣列時回「本週無明顯雙重確認訊號」 |
divergence_stocks |
程式模板(技術/籌碼方向相反) | 可能空陣列 |
headline / market_synthesis / divergence_narrative / risks |
LLM 合成 | LLM 失敗時為空字串,parser_warnings 會記錄 |
3.5.1 DualConfirmationStock 欄位
| 欄位 | 類型 | 說明 |
|---|---|---|
ticker |
string | 股票代碼(含市場後綴,如 2330.TW、006201.TWO) |
name |
string | null | 股票中文名稱 |
group_name |
string | null | 所屬族群(如「半導體」) |
direction |
"bullish" \| "bearish" |
共識方向 |
technical_event_type |
string | 技術面事件類型(breakout / breakdown / trend_continuation_up / ...) |
technical_score |
number | 技術面事件原始分數(0~1) |
chip_event_type |
string | 籌碼面事件類型(continuous_buy / buy_to_sell / ...) |
chip_score |
number | 籌碼面事件原始金額 |
combined_score |
number (0~2) | rank-normalized 綜合分數(chip TWD 量級無法壓倒 technical);UI 排序用這個 |
3.5.2 DualConfirmationGroup 欄位
| 欄位 | 類型 | 說明 |
|---|---|---|
group_id |
string | 族群 ID |
group_name |
string | 族群名(顯示用) |
group_type |
"industry" \| "sector" \| "concept" |
族群分類 |
direction |
"bullish" \| "bearish" |
共識方向 |
technical_trend_state |
"uptrend" \| "breakout" \| "rebound" \| "downtrend" \| "breakdown" \| "range" |
族群技術趨勢 |
technical_strength_score |
number | 族群技術強度 |
chip_flow_direction |
"inflow" \| "outflow" \| "neutral" |
資金流向 |
chip_flow_score |
number | 資金流量金額 |
representative_stocks |
string[] |
代表股 ticker(最多 5 支) |
combined_score |
number (0~2) | 綜合分數 |
3.5.3 DivergenceStock 欄位
| 欄位 | 類型 | 說明 |
|---|---|---|
ticker / name / group_name |
string | 同 DualConfirmationStock |
technical_direction |
"bullish" \| "bearish" |
技術面方向 |
chip_direction |
"bullish" \| "bearish" |
籌碼面方向(與 technical 相反才進入此陣列) |
note |
string | 背離描述(如「技術 breakout 對 籌碼 continuous_sell」) |
4. 整合範例
4.1 cURL
# 最新週報(orchestrator)
curl -s "http://localhost:8000/api/recaps/orchestrator/weekly/latest?market=taiwan" | jq
# 指定週末(如 2026-04-17 週五)
curl -s "http://localhost:8000/api/recaps/orchestrator/weekly/2026-04-17?market=taiwan" | jq '.headline, .dual_confirmation_stocks[].ticker'
# 今日技術面(daily 自動 fallback)
curl -s "http://localhost:8000/api/recaps/technical/daily/2026-04-19" | jq '.is_fallback, .source_trade_date'
4.2 TypeScript / React (fetch + SWR)
import useSWR from 'swr';
type Direction = 'bullish' | 'bearish';
type DualConfirmationStock = {
ticker: string;
name: string | null;
group_name: string | null;
direction: Direction;
technical_event_type: string;
technical_score: number;
chip_event_type: string;
chip_score: number;
combined_score: number;
};
type DualConfirmationGroup = {
group_id: string;
group_name: string;
group_type: 'industry' | 'sector' | 'concept';
direction: Direction;
technical_trend_state:
| 'uptrend' | 'breakout' | 'rebound'
| 'downtrend' | 'breakdown' | 'range';
technical_strength_score: number;
chip_flow_direction: 'inflow' | 'outflow' | 'neutral';
chip_flow_score: number;
representative_stocks: string[];
combined_score: number;
};
type DivergenceStock = {
ticker: string;
name: string | null;
group_name: string | null;
technical_direction: Direction;
chip_direction: Direction;
note: string;
};
type RiskItem = { text: string; evidence: string };
type OrchestratorRecap = {
period_type: 'daily' | 'weekly' | 'monthly';
period_start_date: string;
period_end_date: string;
market: string;
headline: string;
market_synthesis: string;
dual_confirmation_narrative: string;
dual_confirmation_stocks: DualConfirmationStock[];
dual_confirmation_groups: DualConfirmationGroup[];
divergence_stocks: DivergenceStock[];
divergence_narrative: string;
risks: RiskItem[];
parser_warnings: string[];
};
const fetcher = (url: string) => fetch(url).then(r => {
if (!r.ok) throw new Error(`${r.status}`);
return r.json();
});
export function WeeklyOrchestratorCard() {
const { data, error, isLoading } = useSWR<OrchestratorRecap>(
'/api/recaps/orchestrator/weekly/latest',
fetcher,
);
if (isLoading) return <Skeleton />;
if (error?.message === '404') return <EmptyState message="本週 recap 尚未產出" />;
if (error) return <ErrorState />;
const bullish = data!.dual_confirmation_stocks.filter(s => s.direction === 'bullish');
const bearish = data!.dual_confirmation_stocks.filter(s => s.direction === 'bearish');
return (
<Card>
<Headline>{data!.headline}</Headline>
<Narrative>{data!.dual_confirmation_narrative}</Narrative>
<StockList title="同向看多" stocks={bullish} />
<StockList title="同向看空" stocks={bearish} />
</Card>
);
}
4.3 Swift (iOS, URLSession + Codable)
struct RiskItem: Codable {
let text: String
let evidence: String
}
struct DualConfirmationStock: Codable {
let ticker: String
let name: String?
let groupName: String?
let direction: String // "bullish" | "bearish"
let technicalEventType: String
let technicalScore: Double
let chipEventType: String
let chipScore: Double
let combinedScore: Double
enum CodingKeys: String, CodingKey {
case ticker, name, direction
case groupName = "group_name"
case technicalEventType = "technical_event_type"
case technicalScore = "technical_score"
case chipEventType = "chip_event_type"
case chipScore = "chip_score"
case combinedScore = "combined_score"
}
}
struct DualConfirmationGroup: Codable {
let groupId: String
let groupName: String
let groupType: String // "industry" | "sector" | "concept"
let direction: String // "bullish" | "bearish"
let technicalTrendState: String // "uptrend" | "breakout" | "rebound" | "downtrend" | "breakdown" | "range"
let technicalStrengthScore: Double
let chipFlowDirection: String // "inflow" | "outflow" | "neutral"
let chipFlowScore: Double
let representativeStocks: [String]
let combinedScore: Double
enum CodingKeys: String, CodingKey {
case direction
case groupId = "group_id"
case groupName = "group_name"
case groupType = "group_type"
case technicalTrendState = "technical_trend_state"
case technicalStrengthScore = "technical_strength_score"
case chipFlowDirection = "chip_flow_direction"
case chipFlowScore = "chip_flow_score"
case representativeStocks = "representative_stocks"
case combinedScore = "combined_score"
}
}
struct DivergenceStock: Codable {
let ticker: String
let name: String?
let groupName: String?
let technicalDirection: String // "bullish" | "bearish"
let chipDirection: String
let note: String
enum CodingKeys: String, CodingKey {
case ticker, name, note
case groupName = "group_name"
case technicalDirection = "technical_direction"
case chipDirection = "chip_direction"
}
}
struct OrchestratorRecap: Codable {
let periodType: String
let periodStartDate: String
let periodEndDate: String
let headline: String
let marketSynthesis: String
let dualConfirmationNarrative: String
let dualConfirmationStocks: [DualConfirmationStock]
let dualConfirmationGroups: [DualConfirmationGroup]
let divergenceStocks: [DivergenceStock]
let divergenceNarrative: String
let risks: [RiskItem]
let parserWarnings: [String]
enum CodingKeys: String, CodingKey {
case headline, risks
case periodType = "period_type"
case periodStartDate = "period_start_date"
case periodEndDate = "period_end_date"
case marketSynthesis = "market_synthesis"
case dualConfirmationNarrative = "dual_confirmation_narrative"
case dualConfirmationStocks = "dual_confirmation_stocks"
case dualConfirmationGroups = "dual_confirmation_groups"
case divergenceStocks = "divergence_stocks"
case divergenceNarrative = "divergence_narrative"
case parserWarnings = "parser_warnings"
}
}
func fetchLatestWeeklyOrchestrator() async throws -> OrchestratorRecap {
let url = URL(string: "\(baseURL)/api/recaps/orchestrator/weekly/latest")!
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResp = response as? HTTPURLResponse else { throw APIError.invalidResponse }
if httpResp.statusCode == 404 { throw APIError.notYetAvailable }
if httpResp.statusCode >= 400 { throw APIError.serverError(httpResp.statusCode) }
return try JSONDecoder().decode(OrchestratorRecap.self, from: data)
}
4.4 Kotlin (Android, Retrofit + kotlinx.serialization)
@Serializable
data class RiskItem(
val text: String,
val evidence: String,
)
@Serializable
data class DualConfirmationStock(
val ticker: String,
val name: String?,
@SerialName("group_name") val groupName: String?,
val direction: String, // "bullish" | "bearish"
@SerialName("technical_event_type") val technicalEventType: String,
@SerialName("technical_score") val technicalScore: Double,
@SerialName("chip_event_type") val chipEventType: String,
@SerialName("chip_score") val chipScore: Double,
@SerialName("combined_score") val combinedScore: Double,
)
@Serializable
data class DualConfirmationGroup(
@SerialName("group_id") val groupId: String,
@SerialName("group_name") val groupName: String,
@SerialName("group_type") val groupType: String, // "industry" | "sector" | "concept"
val direction: String,
@SerialName("technical_trend_state") val technicalTrendState: String, // "uptrend" | "breakout" | "rebound" | "downtrend" | "breakdown" | "range"
@SerialName("technical_strength_score") val technicalStrengthScore: Double,
@SerialName("chip_flow_direction") val chipFlowDirection: String, // "inflow" | "outflow" | "neutral"
@SerialName("chip_flow_score") val chipFlowScore: Double,
@SerialName("representative_stocks") val representativeStocks: List<String>,
@SerialName("combined_score") val combinedScore: Double,
)
@Serializable
data class DivergenceStock(
val ticker: String,
val name: String?,
@SerialName("group_name") val groupName: String?,
@SerialName("technical_direction") val technicalDirection: String, // "bullish" | "bearish"
@SerialName("chip_direction") val chipDirection: String,
val note: String,
)
@Serializable
data class OrchestratorRecap(
@SerialName("period_type") val periodType: String,
@SerialName("period_start_date") val periodStartDate: String,
@SerialName("period_end_date") val periodEndDate: String,
val headline: String,
@SerialName("market_synthesis") val marketSynthesis: String,
@SerialName("dual_confirmation_narrative") val dualConfirmationNarrative: String,
@SerialName("dual_confirmation_stocks") val dualConfirmationStocks: List<DualConfirmationStock>,
@SerialName("dual_confirmation_groups") val dualConfirmationGroups: List<DualConfirmationGroup>,
@SerialName("divergence_stocks") val divergenceStocks: List<DivergenceStock>,
@SerialName("divergence_narrative") val divergenceNarrative: String,
val risks: List<RiskItem>,
@SerialName("parser_warnings") val parserWarnings: List<String>,
)
// Note: different dimensions return different shapes. Pick dimension-specific
// methods for compile-time type safety. TechnicalRecap / ChipRecap data
// classes follow the same pattern as OrchestratorRecap above — define per
// §3.1–§3.4 in this doc.
interface RecapApi {
// Orchestrator (any period_type) → OrchestratorRecap
@GET("api/recaps/orchestrator/{periodType}/latest")
suspend fun getOrchestratorLatest(
@Path("periodType") periodType: String,
@Query("market") market: String = "taiwan",
): OrchestratorRecap
@GET("api/recaps/orchestrator/{periodType}/{date}")
suspend fun getOrchestratorByDate(
@Path("periodType") periodType: String,
@Path("date") date: String, // YYYY-MM-DD
@Query("market") market: String = "taiwan",
): OrchestratorRecap
// Technical and chip return their own shapes; add parallel methods:
// suspend fun getTechnicalLatest(...): TechnicalRecap
// suspend fun getChipLatest(...): ChipRecap
// If you prefer a single untyped method, return JsonElement and
// branch on dimension in the caller.
}
5. 錯誤處理建議
| HTTP | 前端應該 |
|---|---|
200 |
正常渲染 |
400 |
當作 bug(參數組合非法,不應在 production 出現) — log 並 fallback 到預設頁 |
404 |
顯示空狀態「本週 recap 尚未產出」—— 不是錯誤,只是時機未到或尚未 backfill |
422 |
requested_date 格式錯誤 —— 前端應在 client-side 驗證 YYYY-MM-DD 格式 |
5xx |
伺服器錯誤 —— 顯示通用錯誤 + retry 按鈕 |
空狀態的三種情境
- 尚未到產出時機 — 例:週三呼叫
/api/recaps/orchestrator/weekly/latest取到上週資料(正常),但若是新週首日還沒有新一週 →latest仍然是上週(要提示用戶這是上週資料,看period_end_date) - LLM 部分失敗 —
headline/market_synthesis為空字串,parser_warnings非空 → 仍然可以顯示dual_confirmation_narrative(這部分不經 LLM,不會失敗) - 完全 404 — 該日期 pipeline 尚未跑,呼叫
/{date}回 404 → 顯示「尚未產生」並建議切到latest
6. UI / UX 設計建議
6.1 Orchestrator Weekly 首屏
┌─────────────────────────────────────────────┐
│ 本週 AI 擴散 2330.TW+2454.TW 雙重確認看多 [週] │ ← headline + period badge
├─────────────────────────────────────────────┤
│ 技術面突破搭配法人持續買超...(2-3 行) │ ← market_synthesis 節錄
├─────────────────────────────────────────────┤
│ 🟢 同向確認看多 (2) │
│ ▸ 台積電 (2330.TW) breakout + continuous_buy│ ← dual_confirmation_stocks
│ ▸ 聯發科 (2454.TW) breakout + continuous_buy│ (direction=bullish)
├─────────────────────────────────────────────┤
│ 🔴 同向確認看空 (1) │
│ ▸ 大立光 (3008.TW) breakdown + buy_to_sell │ (direction=bearish)
├─────────────────────────────────────────────┤
│ ⚠️ 技術/籌碼背離 (1) │
│ ▸ 鴻海 (2317.TW) 技術看多 vs 籌碼看空 │ ← divergence_stocks
├─────────────────────────────────────────────┤
│ 風險: │
│ · 成交量收斂 │
│ · 融資擁擠 │
└─────────────────────────────────────────────┘
6.2 排序建議
dual_confirmation_stocks / dual_confirmation_groups 已依 combined_score DESC 排序,前端不需再排序。若要限制顯示數量,取前 N 個即可。
6.3 Ticker 點擊導頁
所有 ticker 字串包含市場後綴(上市 .TW、上櫃 .TWO,如 2330.TW、006201.TWO)。若 UI 內部路由使用不帶後綴的形式,用 split('.')[0] 去除即可。
6.4 Direction 視覺對應
direction |
色系 | 圖示建議 |
|---|---|---|
bullish |
紅色系(台股慣例) | ↑ / 🔺 |
bearish |
綠色系(台股慣例) | ↓ / 🔻 |
6.5 週/月粒度切換
前端常見需求是「週報 ↔ 月報 ↔ 每日推播」切換。三者 URL pattern 一致,只換 period_type path 段即可,response shape 幾乎相同(只差 period_summary 欄位月 vs 週內容不同,前端不需特判)。
7. 資料產出時機
| period_type | 觸發時機 | 延遲 |
|---|---|---|
daily |
每日盤後 daily pipeline 跑完(約 15:30 TW 後) + orchestrator 合成 LLM | 盤後 30 分鐘內 |
weekly |
每週最後交易日(通常週五)盤後,跟 daily 同一輪 | 同上 |
monthly |
每月最後交易日盤後 | 同上 |
Scheduler 會在對應條件達成時自動 dispatch,前端無需主動觸發。若要強制重算,需由後端 CLI:
python scripts/run_periodic_recap.py --period-type weekly --period-end-date 2026-04-17 --dimensions technical,chip,orchestrator
8. ⚠️ 目前已知限制(整合前必讀)
8.1 技術面週/月聚合正在重寫(PR-4)
目前版本(dev 最新)的技術面週/月 recap 實作是從已事件化的 daily_technical_* 彙整表聚合,這個做法存在聚合失真問題:
- 週 breakout_count = SUM(日 breakout_count)(應為週線 breakout 事件數)
- 週 trend_state = mode(5 個日 trend_state)(應為週 K 線上的 trend)
- 族群 strength_score = mean(5 個日分數)(應為週 bar 重算)
前端影響:response schema 不變,欄位型別與意義不變;只是數值在 PR-4 合併後會有差異。前端可以按本文件 schema 開發,PR-4 合併後換到正確數字即可,不需改前端程式碼。
8.2 籌碼面週/月大致正確
淨額 SUM 是籌碼資料的正確聚合方式,唯一會微調的是 streak_days(目前是日連續,PR-4 會改為週連續),對前端 UI 影響極小。
8.3 Orchestrator 雙重確認不受影響
dual_confirmation_stocks / dual_confirmation_groups / divergence_stocks 是在既有 periodic_*_stock_event 基礎上比對,邏輯正確;PR-4 只會讓上游事件更精準,但比對演算法不變。
8.4 /groups 與 /stocks 子路由
本次 API 尚未提供 /api/recaps/{dim}/{period}/{date}/groups 和 /stocks 子路由。若前端需要族群或個股清單,暫時從 orchestrator response 的 dual_confirmation_groups / dual_confirmation_stocks / divergence_stocks 取用,或等後續 PR 補上對稱路由。
8.5 歷史 backfill
Production DB 目前只有 2026-04-01 起的 daily recap 資料(週/月表是空的)。PR-4 完成後將進行 8 年 raw data backfill —— 屆時 latest endpoint 就會有穩定數據。
9. Legacy / 相容 endpoints
以下舊路由仍保留且可用(與新路由讀同一張 daily_* 表),但標為 deprecated,建議新專案直接用 /api/recaps/*:
| Legacy | 對應新路由 |
|---|---|
GET /api/technical-recap/latest |
GET /api/recaps/technical/daily/latest |
GET /api/technical-recap/{date} |
GET /api/recaps/technical/daily/{date} |
GET /api/technical-recap/{date}/groups |
(暫無對應 unified 路由) |
GET /api/technical-recap/{date}/stocks |
(暫無對應 unified 路由) |
POST /api/technical-recap/{date}/analyze |
(暫無對應 unified 路由) |
GET /api/chip-recap/* |
對稱同上 |
舊路由 response schema 與 /api/recaps/{dim}/daily/* 完全一致,可無痛遷移。
10. 問題回報
- API schema 疑問 → 後端團隊
- UI / UX 討論 → Product 團隊
- Bug / 資料錯誤 → GitHub issue 附 PR 連結 + 實際 response payload
最後更新:2026-04-20(配合 PR #133 Recap 上線 + PR-4 技術面聚合修正規劃)