跳轉到

ETF Date Contract — Frontend Integration Guide (PR #372)

Backend branch merged: fixed/etf-date-contractcortex/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/dailyetfs[].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 200effective_date=nullis_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_dateas_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.md lines 5732+ / 5817+ / 5984+
  • OpenAPI 會在下次 version tag 部署時自動更新(deploy-docs.yml