投資操作績效 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_partialperiod: 該 bucket 的 sell-date/ex-date 已實現,與仍待定案的未實現/資本/衍生指標cumulative: inception 至 pointperiod_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_pnlday_trade_pnlcash_dividendexpected_cash_dividendrealized_totalunrealized_pnlcapital_employedopened_lots_countclosed_lots_counttotal_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
- 先從 running backend capture status 200 fixture,不以手寫 shape 開始。
- Web/Mobile/Management 分別依自己的 workflow 提供 rendered UI/UX approval artifact。
- 所有 money/percentage formatter 必須保留 null、負數與 incomplete warning。
- transaction mutation 後 invalidate single-period、timeseries、summary queries。
- 三專案共享 OpenAPI 與本文件,避免各自重新推導公式。