跳轉到

投資操作績效 Frontend API Integration Summary

此文件是 Cortex Web、Cortex Mobile、Cortex Management 三個 consumer agents 的共同 API SSOT;不規定各專案 UI/UX。

Endpoint matrix

Method/path Purpose
GET /api/portfolio/performance 既有「目前持倉市場變化」,contract 不變
GET /api/portfolio/investment-performance 全時或指定日/週/月/季/年的操作績效
GET /api/portfolio/investment-performance/details 單一績效數字的可對帳逐筆明細
GET /api/portfolio/investment-performance/timeseries chart 用 period + cumulative 序列
GET /api/portfolio/summary 既有總覽;股利 entitlement 修正但 shape 不變

所有 endpoint 需要 Bearer access token。

單期 request

GET /api/portfolio/investment-performance?period=month&value=202607
Authorization: Bearer <token>
Location Field Type Required Nullable Default Notes
query period enum no no all all/day/week/month/quarter/year
query value string non-all yes yes null normalized response value uses hyphen form
query ticker string no yes null uppercase echo;closed ticker allowed
query type Group/ETF no yes null mutually exclusive with ticker

目前 month compact 202607 與 2026-07 都接受;day 接受 compact/ISO;quarter 接受 compact/hyphen。

單期 response example

{
  "period": "month",
  "value": "2026-07",
  "start_date": "2026-07-01",
  "end_date": "2026-07-10",
  "valuation_date": "2026-07-09",
  "is_partial": true,
  "ticker": null,
  "opened_lots_count": 4,
  "closed_lots_count": 0,
  "realized": {
    "regular_pnl": 0.0,
    "day_trade_pnl": 0.0,
    "cash_dividend": 0.0,
    "total": 0.0
  },
  "expected_cash_dividend": 3000.0,
  "unrealized_pnl": -12525.5,
  "total_pnl": -12525.5,
  "capital_employed": 225916.0,
  "return_pct": -5.5443173569,
  "equity_multiple": 0.9445568264,
  "data_quality": {
    "is_complete": true,
    "unmatched_sell_count": 0,
    "unmatched_sell_tickers": [],
    "missing_price_tickers": [],
    "dividend_basis": "ex_dividend_entitlement",
    "dividend_history_complete": true,
    "dividend_missing_coverage": [],
    "dividend_schedule_complete": true,
    "dividend_schedule_missing_coverage": [],
    "standalone_cash_dividend_count": 0,
    "unattributed_cash_dividend_count": 0,
    "calculation_issues": [],
    "classification_basis": null
  }
}

數字來自 2026-07-10 task runtime 的去識別化 contract capture;consumer fixture 應固定 shape,不應固定真實金額。

dividend_history_complete、dividend_missing_coverage、dividend_schedule_complete、 dividend_schedule_missing_coverage 是 deprecated compatibility fields,固定回傳 true/[]。它們不再是績效查詢的依賴,也不代表外部來源完整;來源採集/漏載須由 來源層另行稽核。缺少或過期的月度 historical coverage、每日 schedule 或 checkpoint, 在沒有實際計算問題時不得使績效回應不完整。

Overview 的 is_complete 由本次回應真正相關的 calculation_issues 決定;沒有相關 計算問題時為 true,overview 沒有 reconciliation 欄位。Details 另以同一次 audit 的 reconciliation.status 驗證 aggregate 與完整明細:matched 才表示對帳成功, incomplete/mismatch 均使 details 相關範圍不完整。未知 issue code 不得被忽略; deprecated coverage 欄位不參與判定。calculation_issues=[] 表示本次 engine audit 未偵測到已知計算問題, 不可解讀為已驗證外部來源完整。Overview、details、group 與 timeseries 共用同一 public audit snapshot;各 scope 以今日作為 payment_evidence_date,因此不需新增欄位或前端 action。

每個 calculation issue 的 shape 為:

{
  "code": "missing_price",
  "count": 1,
  "tickers": ["2330.TW"],
  "affected_metrics": ["unrealized_pnl", "total_pnl"]
}

四個欄位都必填;count 非負、tickers 非 null、affected_metrics 使用 details metric enum 且非空。初始 code 為 unmatched_sell、missing_price、 unattributed_cash_dividend;consumer 對未知 code 必須顯示通用計算問題。Coverage checkpoint 不進此清單。Details 依所選 metric 過濾 issues 及其 count/tickers;無關問題 不得污染該 metric,尤其 day_trade_pnl 不受無關股利或價格問題影響。後端固定提供 這四個相容欄位的既有 shape,足以支援原 frontend consumer;本次不要求任何 Web/Management/Mobile frontend change。

count 的單位依 code 定義:missing_price 為缺價的不同標的檔數, 恆等於 len(tickers);unmatched_sell 為該 affected-metrics bucket 的賣出交易筆數; unattributed_cash_dividend 為無法歸屬的現金股利交易筆數。不同 code 的 count 不可直接相加當作總交易筆數,同一標的也可能有多筆交易問題。

code affected_metrics
missing_price unrealized_pnl, total_pnl
unattributed_cash_dividend cash_dividend, realized_total, total_pnl, capital_employed
unmatched_sell(有 day_trade_pair_id) day_trade_pnl 加下列共通欄位
unmatched_sell(缺分類證據) regular_pnl、day_trade_pnl 加下列共通欄位

一般賣出問題的共通欄位為 realized_total、unrealized_pnl、total_pnl、 capital_employed、closed_lots_count。只有同一賣出證據確實影響除息資格時, 才再加入 cash_dividend 或 expected_cash_dividend;opened_lots_count 不受影響。

unmatched_sell 不再 blanket-taint cash_dividend 或其他無關 metric;每筆 issue 的 affected_metrics 是同一 exact evidence ID 的 metric union,consumer 必須依目前 metric 篩選,且不得把無關問題複製到 details。Overview 的 return_pct/equity_multiple 只有在 其 total_pnl/capital_employed evidence 受影響時才受到 issue 保護。

前端不得以 deprecated coverage 欄位或 schedule freshness 顯示績效錯誤 banner;只有 相關 calculation_issues 與 reconciliation 狀態可觸發計算問題提示。

Grouped response

type=Group 或 type=ETF top-level 改為:

{
  "period": "all",
  "value": null,
  "start_date": "2021-01-29",
  "end_date": "2026-07-10",
  "valuation_date": "2026-07-09",
  "is_partial": false,
  "type": "Group",
  "classification_basis": "current_metadata",
  "overall": { "...metric fields": "same as ungrouped" },
  "groups": [
    {
      "category": "半導體業",
      "category_source": "industry",
      "tickers": ["2330.TW"],
      "...metric fields": "same as overall"
    }
  ],
  "data_quality": { "...": "overall quality" }
}

Group fallback 固定為 industry → sector → Unclassified;ETF 分類為 etf/stock。歷史分類採 current metadata。每個 group 的 capital_employed/return_pct 已由 backend 重算,不可在 frontend 平均百分比。

groups[].tickers 為本次有明細或未配對賣出的標的:期間內賣出、期間內除息、當前預計股利, 以及期間內開倉 lots。賣出/股利可以來自舊持股,不受開倉 cohort 限制;不是該分類的全歷史成員。 沒有任何上述明細或問題的分類仍回傳 group(tickers=[]、數字為 0)。period=all 時等同全部成員。

Timeseries

GET /api/portfolio/investment-performance/timeseries?start_date=2026-01-01&end_date=2026-07-10&interval=month

Response:series[] { key, label, category_source, tickers, points[] };每個 point:

  • period_start/period_end/valuation_date/is_partial
  • period: 該 bucket 的 sell-date/ex-date 已實現,與仍待定案的未實現/資本/衍生指標
  • cumulative: inception 至 point period_end 的績效
  • data_quality: point cumulative quality

日期點取實際報價日、非現金股利交易日、來源除權息日,以及核心分類為真正 standalone orphan 的付款日聯集;已歸屬或 ambiguous 的付款日不建立股利日期點,也不只回有市場報價的日子。 point valuation_date 維持 date 型別,代表最後活動日; 實際價格只使用該日以前最新已知 close,不產生虛構報價。Overview/window 的同名欄位仍為 最後真實報價日或 null。過去點 expected_cash_dividend=0,只有 current terminal point 可以 顯示已公告預計股利;晚記錄的付款可作為過去 ex-date 金額證據,不改歸期。超過 2000 points 回 422。

逐筆明細

GET /api/portfolio/investment-performance/details?period=month&value=2026-07&metric=cash_dividend&offset=0&limit=100
Authorization: Bearer <token>

metric 必填:

  • regular_pnl
  • day_trade_pnl
  • cash_dividend
  • expected_cash_dividend
  • realized_total
  • unrealized_pnl
  • capital_employed
  • opened_lots_count
  • closed_lots_count
  • total_pnl

period/value/ticker 與單期 API 相同。分類 drill-down 必須同時傳 type/category/category_source,例如:

GET /api/portfolio/investment-performance/details?metric=total_pnl&type=Group&category=半導體業&category_source=industry

三個分類欄位缺任一欄、與 ticker 同時出現、分類不存在或 source 不匹配均回 422。分類 ticker 清單由 backend current metadata 解析,client 不傳 ticker 清單。offset 預設 0;limit 預設 100、最大 1000。

共用 response

{
  "window": {
    "period": "month",
    "value": "2026-07",
    "start_date": "2026-07-01",
    "end_date": "2026-07-11",
    "valuation_date": "2026-07-10",
    "is_partial": true
  },
  "scope": {
    "kind": "overall",
    "ticker": null,
    "type": null,
    "category": null,
    "category_source": null,
    "tickers": ["2330.TW"]
  },
  "metric": "cash_dividend",
  "unit": "TWD",
  "value": 5000.0,
  "reconciliation": {
    "basis": "sum",
    "detail_value": 5000.0,
    "status": "matched"
  },
  "pagination": {
    "total_items": 2,
    "offset": 0,
    "limit": 100,
    "has_more": false
  },
  "items": [],
  "data_quality": {
    "is_complete": true,
    "unmatched_sell_count": 0,
    "unmatched_sell_tickers": [],
    "missing_price_tickers": [],
    "standalone_cash_dividend_count": 0,
    "unattributed_cash_dividend_count": 0,
    "dividend_history_complete": true,
    "dividend_missing_coverage": [],
    "dividend_schedule_complete": true,
    "dividend_schedule_missing_coverage": [],
    "calculation_issues": []
  }
}

重要:畫面數字一律使用 top-level value。detail_value 是 backend 對全部未分頁 items 的對帳結果;不得 sum 當頁 items。capital_employed 的 basis=max_capital_required,不是 sum。count metrics 的 unit 為 count,其餘為 TWD。

reconciliation.status:

  • matched:全量明細與 aggregate 相等。
  • incomplete:缺 valuation price 等資料,value/detail_value 可為 null;不得轉 0。
  • mismatch:非預期計算不一致,frontend 必須顯示 data-quality warning。

Reconciliation 只比較同一次 audit 的 aggregate 與全量 details,不是獨立來源驗證; matched 不會覆蓋 calculation_issues。Details 的 data_quality 已按所選 metric 過濾,不是 overview 的全域副本;mismatch/incomplete 亦使後端回 is_complete=false,即使 issue 陣列為空。完整性涵蓋所選數值及全量對帳,不保證 每個附帶 item 欄位:開倉筆數可正確而 lot 估值為 null,或未配對賣出下剩餘量/status 仍暫定;估值品質須看 unrealized_pnl/total_pnl,不能由開倉筆數完整推論。

Closure item

{
  "item_type": "closure",
  "closure_id": "uuid",
  "classification": "regular",
  "ticker": "2330.TW",
  "quantity": 100,
  "buy_transaction_id": "uuid",
  "buy_date": "2026-01-02",
  "buy_price": 1000.0,
  "sell_transaction_id": "uuid",
  "sell_date": "2026-07-03",
  "sell_price": 1100.0,
  "day_trade_pair_id": null,
  "allocated_buy_gross": 100000.0,
  "allocated_buy_fee": 142.0,
  "allocated_buy_tax": 0.0,
  "allocated_cost": 100142.0,
  "allocated_sell_gross": 110000.0,
  "allocated_sell_fee": 156.0,
  "allocated_sell_tax": 330.0,
  "allocated_proceeds": 109514.0,
  "pnl": 9372.0
}

fee/tax 依 closure quantity 比例配置。當沖分類採 closure.is_day_trade OR sell.day_trade_pair_id,因此 legacy pair id null 仍可能是 day trade。

Dividend item

{
  "item_type": "dividend",
  "status": "realized",
  "attribution": "event",
  "event_key": {
    "ticker": "2330.TW",
    "ex_dividend_date": "2026-07-01"
  },
  "ticker": "2330.TW",
  "ex_dividend_date": "2026-07-01",
  "position_quantity": 1000,
  "per_share_dividend": 5.0,
  "amount": 5000.0,
  "source": "TWSE",
  "explicit_override": false,
  "transaction_ids": [],
  "selected_entitlement_quantity": 1000,
  "total_entitlement_quantity": 1000
}

attribution=standalone 時 event_key=null、explicit_override=false,且 transaction IDs 會包含實際 cash-dividend transaction;此時既有 ex_dividend_date 欄位僅表示 payment date,不是已確認的 ex-date。 有效的 no-candidate orphan/manual receipt 在 shared audit 的 realized_dividends 保留一次,並以相同金額 納入 summary 的既有 manual bucket;因此例如 event 600 加 standalone 20,realized cash dividend 為 620。 event explicit override、event 推算與 standalone 不會重複;已實現股利的 entitlement 依 ex-date 的完整 qualifying holdings(非開倉 cohort)計算;未來除息事件只出現在 expected metric。

未來事件單純現金金額缺值或公告金額衝突,在有效截止日前不會產生 unquantified_cash_dividend/conflicting_dividend_event 或績效不完整警示。 可量化的預估金額仍保留;到除息日且當時有資格、仍缺必要金額或有衝突才揭露已實現股利計算問題。 overview/details/groups/timeseries 共用核心日期規則,前端無需另行隱藏警示;當前交易的真實配對問題仍保留。

多個事件候選、單一事件对多筆款或資格/身份未確定的 payment,仍留在 unattributed 並保留 unattributed_cash_dividend issue,不另算 manual income;invalid payment 只保留 invalid issue。 沒有未占用候選(包含唯一事件已被同日款占用)或 zero-entitlement 的有效 orphan 保留一次 standalone 金額及 unattributed issue,不因無法確認 ex-date 而從績效靜默排除。

Lot item

{
  "item_type": "lot",
  "transaction_id": "uuid",
  "ticker": "2330.TW",
  "action": "buy",
  "opened_date": "2026-07-01",
  "original_quantity": 100,
  "closed_quantity": 40,
  "remaining_quantity": 60,
  "status": "partial",
  "unit_cost": 1001.42,
  "remaining_cost_basis": 60085.2,
  "valuation_date": "2026-07-10",
  "valuation_price": 1050.0,
  "market_value": 63000.0,
  "unrealized_pnl": 2914.8
}

opened_lots_count 回 cohort 全部 lots;closed_lots_count 只回 fully closed;unrealized/total 只回 remaining lots。缺價格時 valuation price/value/unrealized 都是 null。

Capital-flow item

{
  "item_type": "capital_flow",
  "date": "2026-07-01",
  "transaction_ids": ["uuid"],
  "buy_outflow": 100142.0,
  "sell_proceeds": 0.0,
  "realized_dividend_inflow": 0.0,
  "net_cash_flow": -100142.0,
  "cumulative_cash_flow": -100142.0,
  "day_trade_open_cost_floor": 0.0,
  "capital_required": 100142.0,
  "is_peak": true
}

所有 items 均依主要日期由新到舊:closure 用 sell_date、dividend 用 ex_dividend_date、lot 用 opened_date、capital flow 用 date。closure 的 realized regular P&L 以 sell-date 實際 allocated original cost 計算;dividend realized 以 ex-date 的完整 qualifying holdings 計算;expected dividend 以 period-end holdings 計算,不受 stale quote 阻擋。realized_total 會混合 closure + realized dividend;total_pnl 再加入 remaining lot,請用 item_type 判斷 component。Capital flow 的 cumulative/peak 仍由 backend 按日期正向計算,只有 response items 的顯示順序反轉。

Null、empty、error

  • 必要 close 缺失:unrealized_pnl/total_pnl/return_pct/equity_multiple=null,ticker 出現在 missing_price_tickers;不可轉成 0。
  • 無交易/找不到 ticker:HTTP 200、counts/realized/capital 為 0,無分母欄位 null。
  • 無效 value、future-only period、ticker+type、timeseries future end、超過 2000 points:HTTP 422, body 為 error envelope {"error": {"code", "message", "details"}}。這五種都是路由自己 raise HTTPException(422)(api/routes/investment_performance.py),走全域 handler; 與 FastAPI 參數驗證的 {"detail": [...]} 陣列形狀不同,見 api-reference §通用 Response 格式。
  • data_quality.is_complete=false 時仍可顯示已知值,但 Web 的計算錯誤 banner 應以 calculation_issues(details 再依 affected_metrics 篩選)及 reconciliation 為準。 Deprecated coverage 欄位固定為 true/[],不得被當成來源完整證明或計算錯誤; metric-scoped quality 必須避免無關問題污染當沖明細。

Product semantics

  • all 是所有歷史操作;非 all 的已實現賣出按 sell-date、股利按 ex-date,舊 lot 也納入。
  • 草稿中的未實現/lot counts 仍使用期間內開倉 lots;這不是已選定的完整期間模型,不能把整份回應稱為純日曆 P&L。
  • ex_date <= end_date(圖表點為 period_end)已實現,即使最新股價仍停在前一交易日也不延後;更晚 event 是 expected,不進 realized/total。
  • return_pct = total_pnl / capital_employed * 100;equity_multiple = 1 + total_pnl / capital_employed;非 all 範圍的 U/K/total/return composition 目前仍未定案,以下不可視為已驗證公式。
  • 非 all 的 cohort buy costs 與新的 old-lot sell-date R/ex-date D flows 如何組成 U/K/total/return 尚未解決;在模型選定並完成驗證前,這條線不是 merge-ready,不得自行選定模型或把示例比例宣稱為正確。

Consumer action items

  1. 先從 running backend capture status 200 fixture,不以手寫 shape 開始。
  2. Web/Mobile/Management 分別依自己的 workflow 提供 rendered UI/UX approval artifact。
  3. 所有 money/percentage formatter 必須保留 null、負數與 incomplete warning。
  4. transaction mutation 後 invalidate single-period、timeseries、summary queries。
  5. 三專案共享 OpenAPI 與本文件,避免各自重新推導公式。