ETF Date Contract — Frontend Integration Guide (PR #372)
Backend branch merged: fixed/etf-date-contract → cortex/dev @ 99ddc94
API base: http://localhost:8000(dev)/ production base 同既有
生效時間: cortex-api 已 restart,即時生效
1. 欄位改名對照表(Breaking Change)
讀舊欄位會拿到 undefined,必須改名。
| Endpoint | 舊欄位 | 新欄位 | 型別 |
|---|---|---|---|
GET /api/etf-holdings/performance |
price_end_date |
effective_date |
string \| null(date YYYY-MM-DD) |
GET /api/etf-holdings/stock-actions/ranking |
date |
effective_date |
string \| null |
GET /api/etf-holdings/{ticker}/changes |
as_of_date |
effective_date |
string \| null |
⚠
GET /api/etf-holdings/recap/daily的etfs[].as_of_date不變(不同 endpoint,不在本次 scope)。
2. 新欄位(additive,三支 API 都有)
| 欄位 | 型別 | 語意 |
|---|---|---|
requested_date |
string \| null |
前端傳入的 ?date=YYYY-MM-DD;不傳則 null |
is_fallback |
boolean |
後端是否回退到較早資料日 |
effective_date |
string \| null |
「資料時間」= 後端實際使用的資料日。前端顯示「資料時間:YYYY-MM-DD」就讀這欄 |
is_fallback 計算規則(後端統一):
is_fallback = requested_date != null && effective_date != requested_date
語意:
false:前端要的日期就是後端用的日期(或前端沒指定日期)→ UI 顯示「本日」true:前端要 6/4 但後端只能給 6/3 → UI 顯示「最新資料日:6/3(您查詢的 6/4 尚無資料)」
3. 404 行為改變(Breaking Change)
舊:DB 無資料時三支 API 行為各異(/performance 回 200 + 空 list、/ranking 與 /changes 回 404)。
新:三支統一規則
| 情境 | 回應 |
|---|---|
| 完全無 snapshot(DB 空 或 該 ETF 從未抓到資料) | HTTP 200,effective_date=null、is_fallback=true、list 欄位皆空 |
ETF ticker 不存在於 etf_funds |
HTTP 404(保留,僅限 /{ticker}/changes) |
| 其他錯誤(無效 period、limit 超界) | HTTP 422(保留) |
→ 前端不要再用 404 判斷「無資料」,改檢查 effective_date === null 或 list .length === 0。
4. 五情境完整 response 對照
4-1 不帶 date(最新資料日)
GET /api/etf-holdings/performance?isActive=true
{
"period": "day",
"effective_date": "2026-06-03",
"requested_date": null,
"is_fallback": false,
"...": "..."
}
UI 顯示:「資料時間:2026-06-03」
4-2 帶有資料的日期(精確匹配)
GET /api/etf-holdings/performance?date=2026-06-03
{
"effective_date": "2026-06-03",
"requested_date": "2026-06-03",
"is_fallback": false
}
UI 顯示:「2026-06-03」
4-3 今天但尚無交易資料
GET /api/etf-holdings/performance?date=2026-06-04
{
"effective_date": "2026-06-03",
"requested_date": "2026-06-04",
"is_fallback": true
}
UI 顯示:「資料時間:2026-06-03(2026-06-04 尚無資料)」
4-4 假日,回退到最近交易日
GET /api/etf-holdings/performance?date=2026-05-31
{
"effective_date": "2026-05-29",
"requested_date": "2026-05-31",
"is_fallback": true
}
UI 顯示:「資料時間:2026-05-29(2026-05-31 為假日)」
4-5 完全無資料
GET /api/etf-holdings/00NEWETF.TW/changes?date=2026-06-04
{
"etf_ticker": "00NEWETF.TW",
"effective_date": null,
"previous_date": null,
"window_start": null,
"window_end": null,
"requested_date": "2026-06-04",
"is_fallback": true,
"added": [],
"removed": [],
"quantity_changes": []
}
UI 顯示:「目前無資料」(empty state)
5. 建議 helper(共用組件)
type EtfDateContract = {
effective_date: string | null
requested_date: string | null
is_fallback: boolean
}
function formatDataDateLabel(r: EtfDateContract): string {
if (r.effective_date === null) return "目前無資料"
if (!r.is_fallback) return `資料時間:${r.effective_date}`
return `資料時間:${r.effective_date}(您查詢的 ${r.requested_date} 尚無資料)`
}
function hasData(r: EtfDateContract): boolean {
return r.effective_date !== null
}
6. TypeScript 完整 type diff
ETFPerformanceResponse
type ETFPerformanceResponse = {
period: 'day' | 'week' | 'month' | 'q' | 'year'
is_active_filter: boolean
window_start: string // unchanged
window_end: string // unchanged
price_start_date: string | null
- price_end_date: string | null
+ effective_date: string | null
+ requested_date: string | null
+ is_fallback: boolean
total: number
etfs: ETFPerformanceItem[]
}
ETFStockActionRankingResponse
type ETFStockActionRankingResponse = {
period: 'day' | 'week' | 'month' | 'q' | 'year'
- window_start: string
- window_end: string
- date: string
+ window_start: string | null // 無資料時 null
+ window_end: string | null
+ effective_date: string | null
previous_date: string | null
requested_date: string | null // (已存在)
is_fallback: boolean // (已存在)
is_active_filter: boolean
limit: number
summary: ETFStockActionRankingSummary
buy_rankings: ETFStockActionRankingItem[]
sell_rankings: ETFStockActionRankingItem[]
}
ETFChangesResponse
type ETFChangesResponse = {
etf_ticker: string
etf_name: string | null
is_active_etf: boolean
- as_of_date: string | null
+ effective_date: string | null
previous_date: string | null
current_close_price: number | null
baseline_close_price: number | null
price_delta: number | null
price_change_pct: number | null
added: HoldingChange[]
removed: HoldingChange[]
quantity_changes: HoldingChange[]
period: 'day' | 'week' | 'month' | 'q' | 'year'
- window_start: string
- window_end: string
+ window_start: string | null
+ window_end: string | null
+ requested_date: string | null
+ is_fallback: boolean
}
7. 前端遷移 checklist
- [ ] grep 整個 frontend repo:
price_end_date、as_of_date(限 ETF 相關 caller,/recap/daily不動)、ranking 的\.date屬性存取,全部改effective_date - [ ] 任何
catch (404)或if (status === 404)後顯示「無資料」的 ETF caller,改檢查effective_date === null/list.length === 0 - [ ] 首頁「本日 ETF 買賣焦點股」改用
is_fallback控制顯示文字(本日 / 最新資料日) - [ ] TypeScript types 同步更新(如上 §6);型別放寬處(如
window_start變 nullable)要處理 null safety - [ ] 加 helper(如 §5)統一處理三支 API 的日期顯示邏輯
- [ ] 加 regression test:5 情境(§4)至少各一條 component / e2e test
8. 參考
- PR: https://github.com/rdaoshiken/Cortex/pull/372
- Merged commit:
99ddc94 - 後端規格:
docs/api-reference.mdlines 5732+ / 5817+ / 5984+ - OpenAPI 會在下次 version tag 部署時自動更新(
deploy-docs.yml)