跳轉到

Cortex Investment Agent API — 前端串接技術文件

Base URL: http://<host>:<port> API Version: v1 Protocol: REST (JSON) + SSE Authentication: Bearer Token (JWT)


目錄

  1. 通用說明
  2. 認證 Auth
  3. 會員 Members 3B. 管理端會員 Admin Members
  4. 訂閱 Subscriptions
  5. 股票資料 Stocks
  6. 技術指標 Indicators
  7. 三大法人 Institutional
  8. 融資融券 Margin
  9. 股權分散表 Ownership
  10. 基本面 Fundamentals
  11. 策略訊號 Signals
  12. 每日分析 Daily Analysis
  13. 訊號回測 Signal Backtest
  14. MAS 多代理分析
  15. Agent Payloads
  16. 自選股 Watchlist
  17. 投資組合 Portfolio
  18. 族群 Groups 18B. 統一個股清單 Stock Lists
  19. 策略規則 Rules
  20. 個股規則 Stock Rules
  21. 黑天鵝階段 Black Swan
  22. 供應鏈分析 Supply Chain
  23. 更新紀錄 Updates
  24. Health Check
  25. 錯誤處理
  26. Observability — Access Log 26B. Server Metrics
  27. 統一策略 Strategy
  28. 原物料 Commodities
  29. MAS Execution Trace
  30. Data Status 30B. Sync Status
  31. 籌碼面 Recap
  32. 技術面 Recap 32B. 首頁 Market Pulse 32C. 統一 Daily Market 32D. 全市場多面向日報分析
  33. 用戶反饋 Feedback
  34. 管理端反饋 Admin Feedback
  35. 使用者群組 User Groups

1. 通用說明

CORS

Fail-closed allow-list(非全開放)。後端只允許 config/core.yamlapi.cors.allow_origins 明確列出的 origin(本機預設 http://localhost:1688 管理後台、8501/18501),並帶 allow_credentials=true絕不使用 ** + credentials 不安全)。未列入的 origin 其 preflight 會被回 400 Disallowed CORS origin

正式部署以環境變數 CORS_ALLOW_ORIGINS(逗號分隔)覆寫為真實前端網域,例如:

CORS_ALLOW_ORIGINS=https://app.example.com,https://admin.example.com

認證方式

需要認證的 endpoint 在 header 帶入:

Authorization: Bearer <access_token>

認證相關 endpoints 回傳的 access_token 為 JWT。

速率限制 Rate Limiting

所有 endpoint 皆有每 IP 速率限制(slowapi)。超過限制回 429 Too Many Requests。 兩層限制值可在 config/core.yamlapi.rate_limit 調整:

  • default(預設 60/minute):套用於所有路由。
  • strict(預設 10/minute):套用於未認證的一次性濫用面 —— POST /api/auth/login/{provider}/api/auth/exchange/api/auth/google/verify-token/api/auth/refresh 與 OAuth callback 維持 default 層,避免多裝置/共用 NAT 使用者被誤鎖。

安全標頭 Security Headers

所有回應皆帶 Strict-Transport-SecurityX-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrer

通用 Response 格式

成功:HTTP 200,body 為各 endpoint 定義的 JSON schema。 失敗:HTTP 4xx/5xx,body 為:

{
  "detail": "error message string"
}

日期格式

  • 日期欄位統一使用 YYYY-MM-DD(例如 2024-03-15
  • 時間戳記使用 ISO 8601(例如 2024-03-15T10:30:00
  • 黑天鵝階段使用 yyyymmdd 格式
  • 所有 date 參數均由 api/helpers.py 統一驗證,格式錯誤一律回傳 HTTP 422

source_window 查詢參數(v12 — UI hint only)

從 v12(migration 046)起,source_window 不再是 SQL filter,而是 前端 UI 偏好提示

說明
2y 預設值,回傳值寫進 filters_applied.source_window 供前端讀回
all 同上

API 行為: - response 永遠同時回 nested metrics_2y + metrics_all 兩組 dict(每組 8 fields),不論 query param 為何。 - ?source_window=2y|all 只會寫進 response 的 filters_applied不影響 SQL filter / column select / sort。 - 排序預設用 *_2y,由 config/core.yaml › strategy.api.match_default_window(預設 "2y")決定。query param 不會改變排序。 - 傳入 2yall 以外的值 → HTTP 422(FastAPI Pydantic 自動驗證)。

Per Contract D rule shape(每筆 rule / match response):

{
  "rule_id": "...",
  "strategy_key": "long_d1",
  "scope": "global",
  "metrics_2y": {
    "hit_rate": 0.65, "mean_return": 0.018, "expectancy": 0.012,
    "avg_win": 0.025, "avg_loss": 0.013,
    "sample_count": 87, "trigger_rate": 0.05, "profit_factor": 1.8
  },
  "metrics_all": {
    "hit_rate": 0.61, "mean_return": 0.015, "expectancy": 0.010,
    ...
  }
}

任一窗口可能為 null(AD13: 該窗 0 trigger 或 gate 沒過時,整個 dict 為 null)。 Top-level 不再出現 flat hit_rate / expected_value / mean_return 等欄位;DB 雖是 flat 16 column,API serialize 階段必 pack 成 nested。

下列章節的 endpoint 全部遵循此 contract: §15 Agent Payloads — chip§18 Groups§19 Rules§27 Strategy(不含 Story-first 子節)§35 User Groups (rules / matches)

例外:§27 的 Story-first 策略 子節有自己的統計結構。它的 response 雖同樣出現 metrics_2y / metrics_all 鍵名,但 schema 為 StoryMetricsevent_count / average_return / up_count / …),與此處的 Contract D(hit_rate / expectancy / profit_factor 等 8 欄)無關,且不接受 source_window。詳見該子節。


2. 認證 Auth

Prefix: /api/auth

POST /api/auth/login/{provider}

發起 OAuth 登入流程。後端自行從 config 構建 callback URL,前端不需傳 redirect_uri

Path Params:

名稱 類型 說明
provider string OAuth 提供者 allow-list:googleapple;其他值(含 Facebook)回傳 400 INVALID_PROVIDER

Request Body(optional):

{
  "platform": "web"  // "web" | "android" | "ios", default "web"
}

不傳 body 時預設 platform = "web"platform 決定 OAuth callback 完成後 redirect 到哪個前端 URL。

Response:

{
  "auth_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
  "flow_id": "c37e66d8-9acf-4832-bae9-cbab53f503cf",
  "flow_verifier": "browser_held_random_secret"
}

前端收到後將使用者導向 auth_url,並只在該次登入流程的 client session 暫存 flow_verifier。後端 PostgreSQL 僅保存 state / verifier 的 SHA-256 hash;raw value 不會寫入 DB 或 access log。flow_id 僅供前端關聯 callback,不可取代 verifier。


GET|POST /api/auth/callback/{provider}

OAuth callback — 由 OAuth provider 瀏覽器導向觸發,非前端 API 呼叫。

  • GET:Google 使用 query params(?code=xxx&state=yyy
  • POST:Apple Sign In 使用 form_post(code + state 在 form body)

Redirect 目標由 login 時指定的 platform 決定:

platform redirect URL
web FRONTEND_URL_WEB
android FRONTEND_URL_ANDROID
ios FRONTEND_URL_IOS

成功時302 redirect 到對應平台 URL,帶 ?auth_code=xxx&auth_flow=<flow_id>auth_code 是一次性授權碼;前端應確認 auth_flow 與 login response 的 flow_id 相同,再連同原本保存的 verifier 交換 token。

錯誤時302 redirect 到對應平台 URL,帶 ?error=xxx&message=yyy。新 identity 若沒有 provider 驗證的 email,會以 VERIFIED_EMAIL_REQUIRED fail closed,不會發行 pending token。Facebook callback 亦只會 redirect error。Pre-decode errors(缺少參數、state 無效)redirect 到 FRONTEND_URL_WEB

⚠️ 此 endpoint 不回傳 JSON,而是 302 redirect。


POST /api/auth/exchange

用一次性授權碼交換 token。前端從 callback redirect 的 URL 取得 auth_code,在此交換。

Request Body:

{
  "code": "one_time_auth_code_from_callback_redirect",
  "flow_verifier": "browser_held_random_secret_from_login"
}

Response:

{
  "status": "authenticated",
  "access_token": "jwt_token",
  "refresh_token": "refresh_token_string",
  "token_type": "bearer",
  "expires_in": 3600,
  "member": { ... }
}

授權碼為一次性使用,預設 30 秒過期,且必須與 login response 的 flow_verifier 配對。錯誤 verifier 不會消耗授權碼;成功交換、重複使用或過期均以 原子 DB 操作處理,無法跨 worker replay。無效輸入回傳 400 INVALID_CODE

/api/auth/* response 均帶 Cache-Control: no-store,不得由瀏覽器、proxy 或 CDN 快取。


POST /api/auth/google/verify-token

Mobile app Google 登入。App 透過 Google Sign-In SDK 取得 id_token(Google 簽發的 JWT),後端驗證後回傳 app token。

Request Body:

{
  "id_token": "google_signed_jwt_from_sign_in_sdk"
}

Response (成功):

{
  "status": "authenticated",
  "access_token": "jwt_token",
  "refresh_token": "refresh_token_string",
  "token_type": "bearer",
  "expires_in": 3600,
  "member": { ... }
}

Error Responses:

狀態碼 error code 說明
401 INVALID_ID_TOKEN id_token 無效、過期、或 audience 不符
403 VERIFIED_EMAIL_REQUIRED 新 identity 缺少 Google 驗證的 email
403 MEMBER_SUSPENDED 帳號已停權
422 Validation Error 缺少 id_token 欄位
500 Internal Server Error 伺服器端設定異常(如 JWT_SECRET_KEY 未設定)

此端點與 web OAuth 流程(login → callback → exchange)回傳相同的 response 結構,mobile app 和 web 可共用 token 管理邏輯。


POST /api/auth/complete-registration

相容性保留端點;pending registration 已停用。通過既有 request schema 驗證後固定回傳 410 REGISTRATION_COMPLETION_DISABLED;缺欄位或格式錯誤仍為 422。此端點不解析舊 pending token,也不會建立或連結會員、建立 refresh token或寫入 DB。

Request Body:

{
  "pending_token": "legacy_pending_registration_token",
  "email": "user@example.com"
}

410 Response:

{"error": {"code": "REGISTRATION_COMPLETION_DISABLED", "message": "Registration requires a provider-verified email"}}

Email OTP auth — removed

Email + password / OTP authentication (/api/auth/email/*) and the development OTP-lookup endpoint (/api/dev/email-otp/latest) have been removed. Authentication is OAuth-only (Google / Apple — see /api/auth/login/{provider} above). These routes now return 404.


POST /api/auth/refresh

刷新 access token。

Request Body:

{
  "refresh_token": "current_refresh_token"
}

Response:

{
  "access_token": "new_jwt_token",
  "refresh_token": "new_refresh_token",
  "token_type": "bearer"
}

POST /api/auth/logout

單一裝置登出;撤銷 request 中 refresh token 所屬的整個 rotation family,避免並行 refresh 產生的後繼 token 逃逸。

Request Body:

{
  "refresh_token": "refresh_token_to_revoke"
}

POST /api/auth/logout-all

登出所有裝置(撤銷該用戶所有 refresh token)。需認證。


GET /api/auth/me

取得當前用戶資料。需認證。


3. 會員 Members

Prefix: /api/members — 需認證

GET /api/members/me

取得會員個人資料。


PUT /api/members/me

更新會員資料。

Request Body:

{
  "display_name": "新名稱",          // optional, string
  "avatar_url": "https://..."        // optional, string
}

GET /api/members/me/providers

列出已綁定的 OAuth 提供者。既有資料若含 legacy Facebook identity,仍可能出現在清單中。


POST /api/members/me/providers/{provider}

綁定新的 OAuth 提供者。新綁定 allow-list 僅接受 Google、Apple;Facebook 或其他 provider 回 400 INVALID_PROVIDER

Request Body:

{
  "code": "authorization_code",
  "redirect_uri": "https://your-app.com/callback"
}

DELETE /api/members/me/providers/{provider}

解除綁定 OAuth 提供者;可用於移除既有 legacy Facebook identity。


3B. 管理端會員 Admin Members

Prefix: /api/admin/members — 需 Bearer token 且 members.is_admin = true

GET /api/admin/members

列出會員,包含 OAuth provider、email login 狀態、目前 tier 與 private relay email 訊號。

Query Params:

欄位 類型 必填 預設 說明
page int no 1 頁碼,最小 1
page_size int no 20 每頁筆數,1-100
q string no null 搜尋 email、display name、member id
status string no null active / suspended / deleted
provider string no null google / apple / email
is_admin bool no null 過濾 admin flag
email_verified bool no null 過濾 email credential 是否已驗證

Response 200:

{
  "total": 1,
  "page": 1,
  "page_size": 20,
  "items": [
    {
      "id": "member-id",
      "email": "user@example.com",
      "display_name": "User",
      "avatar_url": null,
      "status": "active",
      "is_admin": false,
      "created_at": "2026-06-11T00:00:00Z",
      "updated_at": "2026-06-11T00:00:00Z",
      "last_login_at": null,
      "tier": {"name": "free", "display_name": "Free", "level": 0},
      "providers": ["google", "email"],
      "email_login": {
        "enabled": true,
        "email_verified": true,
        "otp_required": true,
        "password_updated_at": "2026-06-11T00:00:00Z",
        "locked_until": null
      },
      "has_private_relay_email": false
    }
  ]
}

GET /api/admin/members/identity-lookup

用 provider identity 或 email credential 查找會員。

Query Params:

欄位 類型 必填 說明
provider string yes google / apple / email
provider_user_id string conditional provider=google/apple 時必填
email string conditional provider=email 時必填

Response 200: 單筆 AdminMemberSummary,欄位同 list item。


GET /api/admin/members/{member_id}

取得單一會員管理詳情。

Path Params: member_id 必須是 UUID;格式錯誤會回 422。

Response 200: list item 欄位加上:

欄位 類型 說明
provider_records array OAuth provider 詳細紀錄
current_subscription object/null active/trialing subscription
subscription_history array 最近 20 筆 subscription
recent_auth_events array 最近 20 筆 auth audit logs

PATCH /api/admin/members/{member_id}/status

更新會員狀態。member_id 必須是 UUID;格式錯誤會回 422。設為 suspendeddeleted 時會撤銷該會員未撤銷的 refresh tokens。後端會阻擋管理員停用/刪除自己的帳號,也會阻擋停用/刪除最後一位 active admin。

Request Body:

欄位 類型 必填 說明
status string yes active / suspended / deleted
reason string no 管理原因,寫入 audit log

Response 200:

{
  "id": "member-id",
  "status": "suspended",
  "revoked_refresh_token_count": 2
}

PATCH /api/admin/members/{member_id}/admin-flag

授予或移除會員 admin flag。member_id 必須是 UUID;格式錯誤會回 422。後端會阻擋移除自己的 admin 權限與移除最後一位 admin。

Request Body:

欄位 類型 必填 說明
is_admin bool yes 目標 admin flag
reason string no 管理原因,寫入 audit log

Response 200:

{
  "id": "member-id",
  "is_admin": true
}

PATCH /api/admin/members/{member_id}/tier

相容性 endpoint:由管理員立即設定會員的無期限 manual entitlement。member_id 必須是 UUID;格式錯誤會回 422。目標 tier 必須存在且 is_active = true,保留的 admin permission set 不可指派。操作會同步 members.current_tier_id 相容 cache 並寫入 audit log,但不會建立或修改 subscription/付款紀錄;目前有效 tier 相同、沒有到期日且沒有未來排程時回 changed=false 且不重複寫 audit。若目前 tier 相同但有到期或未來排程,仍會建立新的無期限指派並取代後續排程。

Request Body:

欄位 類型 必填 Nullable 預設 說明
tier string yes no active tier 名稱;去空白並轉小寫,例如 freeadvancepremium
reason string yes no 管理原因;去除前後空白後 1-500 字元,寫入 audit log

Response 200:

{
  "id": "member-id",
  "previous_tier": "free",
  "tier": "advance",
  "changed": true
}

Errors:

HTTP Code 說明
400 INVALID_MEMBERSHIP_TIER tier 不存在或已停用
401 auth error Bearer token 無效或過期
403 ADMIN_REQUIRED 呼叫者不是 admin
404 MEMBER_NOT_FOUND 會員不存在
422 FastAPI validation UUID、必填欄位或欄位長度不合法

GET /api/admin/members/investment-overview

分頁列出會員身份、最相關的 membership 排程與投資資料筆數。只計算 watchlist、持股與交易數量,不在 list endpoint 執行完整市場估值。holding_count 與 portfolio API 共用 closure-valid 語意;未完整配對的 sell 不會扣減持股。

Query Params: pagepage_sizeqstatustiermembership_statefree / active / scheduled / expired)。

{
  "total": 1,
  "page": 1,
  "page_size": 20,
  "items": [{
    "member_id": "member-id",
    "email": "user@example.com",
    "display_name": "User",
    "status": "active",
    "membership": {
      "tier": "premium",
      "state": "scheduled",
      "source": "manual",
      "set_at": "2026-07-22T00:00:00Z",
      "starts_at": "2026-08-01T00:00:00+08:00",
      "ends_at": "2027-08-01T00:00:00+08:00"
    },
    "investment_summary": {
      "watchlist_group_count": 3,
      "watchlist_item_count": 20,
      "holding_count": 5,
      "transaction_count": 120,
      "last_transaction_at": "2026-07-18T10:00:00Z"
    }
  }]
}

GET /api/admin/members/{member_id}/investment-detail

一次查詢一個 sectionwatchlistsholdingstransactions。共同 response 為 {member_id, section, total, page, page_size, items}page_size 預設 50、最大 200。

  • watchlists 是 group/item flat rows;空 group 仍回一列且 item_id=null
  • holdings 欄位與 GET /api/portfolio/holdings 相同。
  • transactions 欄位與 GET /api/portfolio/transactions 相同,包含 day-trade 與 unmatched-sell warning。
  • 會員不存在回 404 MEMBER_NOT_FOUND;非法 UUID、section 或分頁回 422。

PUT /api/admin/members/{member_id}/membership

starts_at 起取代該會員的 manual/legacy 排程。時間區間採左閉右開;未來指派在開始前維持原 tier,到期後若沒有其他有效 entitlement 立即回退 free,不需 scheduler。

{
  "tier": "premium",
  "starts_at": "2026-08-01T00:00:00+08:00",
  "ends_at": "2027-08-01T00:00:00+08:00",
  "reason": "年度 VIP 方案"
}

starts_at 必填且必須含 timezone;ends_at 可為 null,若存在必須晚於起始時間。相同 member/tier/starts/ends 的重複 PUT 回 changed=false,不新增 entitlement 或 audit。Response 同時回 previous_tiereffective_tierscheduled_tierstate、日期與 entitlement_id

GET /api/admin/memberships

列出每位會員的有效與已指派 tier。沒有 entitlement 的會員仍會出現,effective_tier=freeassigned_tier=nullstate=free

Query Params: pagepage_sizeqstatustiermembership_stateexpires_before(timezone-aware datetime)。每筆包含 member identity、effective_tierassigned_tierstatesourceset_atstarts_atends_atcreated_byreason

GET /api/admin/membership-tiers

列出可管理的 tier permission sets。預設只回 active tiers;include_inactive=true 會包含保留的 inactive admin permission set。每筆包含 featuresrate_limitsupdated_atreserved

PUT /api/admin/membership-tiers/{tier_name}/permissions

完整取代 tier 的 permission set,並以 expected_updated_at 做 optimistic concurrency。未知/缺少 key、bool 冒充 int 或非法負數回 422;stale version 回 409 MEMBERSHIP_TIER_VERSION_CONFLICT;相同內容回 changed=false 且不重複 audit。

{
  "features": {
    "ai_analysis": true,
    "portfolio_export": true,
    "max_watchlists": 10,
    "max_watchlist_items": 100
  },
  "rate_limits": {
    "requests_per_minute": 120,
    "daily_analyses": 50
  },
  "expected_updated_at": "2026-07-22T00:00:00Z",
  "reason": "調整 Advance 權限"
}

max_watchlistsmax_watchlist_items-1 表示無限制;其他值需 >= 0。max_watchlist_items 是單一會員跨所有 watchlist groups 的項目總上限,不是每個 group 各自的上限。requests_per_minute-1 或 >= 1;daily_analyses-1 或 >= 0。本版實際 enforce ai_analysis 與兩個 watchlist limits;動態 rate-limit counter 與 portfolio export endpoint 不在本版範圍。

會員管理部署順序(migration 110)

會員授權讀取採 fail-closed。正式部署必須先在 live PostgreSQL 套用 backend/database/migrations/110_member_entitlements.sql,確認 member_entitlements 與相關 indexes 存在後,才部署/重啟 API 與 scheduler。若程式部署需要回滾,只回滾 application image;一旦 entitlement 已開始寫入,不可向下刪除 migration 110 的資料表或 indexes。

GET /api/admin/access-logs?member_id={member_id}

會員 API 呼叫紀錄沿用既有 access-log endpoint,不另建重複 API。可搭配時間、method、path、status、request ID、limit/offset;request/response body 仍遵循既有遮罩規則。


4. 訂閱 Subscriptions

Prefix: /api/subscriptions

GET /api/subscriptions/tiers

列出所有會員等級。不需認證。


GET /api/subscriptions/plans

列出所有訂閱方案。不需認證。


GET /api/subscriptions/current

取得當前訂閱狀態。需認證。


GET /api/subscriptions/history

取得訂閱歷史。需認證。


POST /api/subscriptions

相容性保留端點。需認證;已登入且通過既有 request schema 驗證後固定回傳 403 SUBSCRIPTION_ACTIVATION_DISABLED,缺失或格式錯誤的 plan_id 仍為 422。目前沒有付款提供者可驗證交易,因此此端點不查詢方案、不建立訂閱、不更新會員等級,也不 commit DB transaction。

Request Body:

{
  "plan_id": 1
}

403 Response:

{"error": {"code": "SUBSCRIPTION_ACTIVATION_DISABLED", "message": "Direct subscription activation is disabled"}}

PUT /api/subscriptions/{subscription_id}/cancel

取消訂閱。需認證。


5. 股票資料 Stocks

Prefix: /api/stocks

GET /api/stocks/search

搜尋股票。

Query Params:

名稱 類型 必填 預設 說明
q string 搜尋關鍵字(代號或名稱)
market string 市場篩選(taiwan, us, japan
limit int 20 回傳筆數(1-100)

比對 tickernameenglish_name 三欄(子字串、不分大小寫)。只回有名稱(name IS NOT NULL)的標的——無名稱的列是資料匯入殘留的佔位列,其代號與真實代號有子字串重疊,曾使整個查詢回空結果(#1199)。已下市標的(is_active = false)若有名稱仍會出現在結果中。

Response (422): market 不是有效值時回傳 validation error。

Response (200):

rowsresults 內容相同,都是去重後的 MarketSurveillanceEventResponse objects。

{
  "results": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "english_name": "TSMC",
      "market": "taiwan",
      "industry": "半導體業",
      "security_type": "common",
      "surveillance_status": "normal"
    }
  ],
  "total": 1
}

surveillance_status 代表台股官方監管狀態,值為 normalattentiondisposition。非台股或查無監管事件時回傳 normal

security_type(issue #1282)為證券型別單欄分類,15 值平面枚舉:commonpreferredtdrindexetnreitetfetf_activeetf_balancedetf_bondetf_active_bondetf_leveragedetf_inverseetf_futuresetf_foreign_ccy。查主動型 ETF:security_type IN ('etf_active','etf_active_bond');查債券型 ETF: security_type IN ('etf_bond','etf_active_bond');查全部 ETF:security_type LIKE 'etf%'。非台股標的預設 commonetn 為指數投資證券(Exchange Traded Note,發行人無擔保債券,追蹤指數但非基金),02 開頭六碼/五碼+字母尾碼(如 02001L 槓桿型 ETN)與 etf_leveragedetf_inverse 共用尾碼字母但非同一類,故獨立分類值(2026-08-08 live 盤查發現)。 reit 為不動產投資信託受益證券(Real Estate Investment Trust),01 開頭三碼+字母尾碼(如 01001T 土銀富邦R1), 與 etf_balanced 共用尾碼字母 T 但非同一類,亦非普通股(2026-08-08 使用者核可加入)。


GET /api/stocks/{ticker}

取得單一股票資訊。

Response — StockInfo:

{
  "ticker": "2330.TW",
  "name": "台積電",
  "english_name": "TSMC",
  "market": "taiwan",
  "industry": "半導體業",
  "sector": "資訊科技",
  "security_type": "common",
  "current_price": 850.0,
  "price_change": 15.0,
  "price_change_pct": 1.8,
  "volume": 25000000,
  "last_updated": "2024-03-15",
  "etfs": [
    { "etf_ticker": "0050.TW", "etf_name": "元大台灣50", "weight_pct": 45.2 }
  ],
  "themes": [
    { "theme": "AI 伺服器", "ticker": "2330", "company_name": "台積電", "role": "upstream" },
    { "theme": "CoWoS", "ticker": "2330", "company_name": "台積電", "role": "midstream" }
  ],
  "surveillance_status": "normal"
}

GET /api/market-surveillance/events

查詢台股注意/處置歷史事件。資料來源為 TWSE 與 TPEx 官方公告;包含一般股票與不可映射到股票代號的權證/可轉債等原始列。

Query Params:

名稱 類型 必填 預設 說明
ticker string 股票代號,如 3026.TW
status string attentiondisposition
status_type string status 的別名
exchange string twsetpex
source_exchange string exchange 的別名
start_date date 公告起始日期 YYYY-MM-DD
date_from date start_date 的別名
end_date date 公告結束日期 YYYY-MM-DD
date_to date end_date 的別名
active_on date 查指定日期有效資料;處置用 effective period,注意用 announcement date
include_unmapped bool false 是否包含無法映射成股票 ticker 的公告列
limit int 100 回傳筆數(1-500)
offset int 0 分頁 offset

Response (200):

{
  "results": [
    {
      "id": 7,
      "ticker": "3026.TW",
      "security_code": "3026",
      "security_name": "禾伸堂",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-25",
      "effective_start_date": "2026-05-26",
      "effective_end_date": "2026-06-08",
      "cumulative_count": 3,
      "trading_information": null,
      "disposition_condition": "連續三次",
      "disposition_measure": "第二次處置",
      "disposition_content": "以人工管制之撮合終端機執行撮合作業(約每二十分鐘撮合一次)。",
      "matching_interval_minutes": 20,
      "close_price": null,
      "pe_ratio": null,
      "source_url": "https://www.twse.com.tw/announcement/punish?response=json&startDate=20260525&endDate=20260527"
    }
  ],
  "rows": [
    { "id": 7, "ticker": "3026.TW", "status_type": "disposition" }
  ],
  "total": 1,
  "summary": {
    "total": 1,
    "returned": 1,
    "by_status": { "disposition": 1 },
    "by_exchange": { "twse": 1 }
  }
}

rowsresults 內容相同;保留兩者是為了支援較口語的 API 名稱與原計畫中的 rows 命名。

matching_interval_minutes 代表處置撮合頻率分鐘數;例如官方公告文字 約每五分鐘撮合一次 會回傳 5約每二十分鐘撮合一次 會回傳 20。注意事件或無法解析處置文字時為 null


GET /api/market-surveillance/active-dispositions

查詢指定日期的市場監管清單。預設回傳仍在處置中的股票名單;若傳 status=attention,則回傳 as_of 當日或之前最新可用公告日的注意資料,避免非公告日回傳空清單。原始事件查詢仍使用 /api/market-surveillance/events

Query Params:

名稱 類型 必填 預設 說明
as_of date 今天(台北時間) 處置模式為有效處置判斷日期;注意模式為查詢基準日,API 會找 <= as_of 的最新可用注意公告日
status string disposition dispositionattention
status_type string status 的別名;若兩者同時存在,以 status 優先
exchange string twsetpex
source_exchange string exchange 的別名
limit int 500 回傳筆數(1-1000)
offset int 0 分頁 offset

status=disposition 或未傳 status/status_type 時,每個 ticker 最多回傳一筆;若同一檔股票有多筆重疊有效處置事件,取最新公告日與最新 id 的有效處置事件。status=attention 時不做 ticker dedupe,dedupe_by 會回傳 none,且 summary.requested_as_of 代表前端要求日期、summary.data_as_of 代表實際回傳的注意公告日期、summary.date_mode 固定為 latest_available

Response (200):

{
  "results": [
    {
      "id": 628,
      "ticker": "6806.TW",
      "security_code": "6806",
      "security_name": "森崴能源",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "disposition_measure": "人工管制撮合",
      "matching_interval_minutes": 60
    },
    {
      "id": 193,
      "ticker": "4916.TW",
      "security_code": "4916",
      "security_name": "事欣科",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-25",
      "effective_start_date": "2026-05-26",
      "effective_end_date": "2026-06-08",
      "disposition_measure": "第一次處置",
      "matching_interval_minutes": 5
    }
  ],
  "rows": [
    {
      "id": 628,
      "ticker": "6806.TW",
      "security_code": "6806",
      "security_name": "森崴能源",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "disposition_measure": "人工管制撮合",
      "matching_interval_minutes": 60
    },
    {
      "id": 193,
      "ticker": "4916.TW",
      "security_code": "4916",
      "security_name": "事欣科",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-25",
      "effective_start_date": "2026-05-26",
      "effective_end_date": "2026-06-08",
      "disposition_measure": "第一次處置",
      "matching_interval_minutes": 5
    }
  ],
  "total": 2,
  "summary": {
    "total": 2,
    "returned": 2,
    "as_of": "2026-05-29",
    "by_status": { "disposition": 2 },
    "by_exchange": { "twse": 2 },
    "dedupe_by": "ticker"
  }
}

Attention mode date fallback example:

GET /api/market-surveillance/active-dispositions?as_of=2026-06-03&status=attention&limit=500
{
  "results": [
    {
      "id": 8001,
      "ticker": "9110.TW",
      "security_code": "9110",
      "security_name": "越南控-DR",
      "source_exchange": "twse",
      "status_type": "attention",
      "announce_date": "2026-06-02",
      "effective_start_date": null,
      "effective_end_date": null,
      "matching_interval_minutes": null
    }
  ],
  "rows": [
    {
      "id": 8001,
      "ticker": "9110.TW",
      "security_code": "9110",
      "security_name": "越南控-DR",
      "source_exchange": "twse",
      "status_type": "attention",
      "announce_date": "2026-06-02",
      "effective_start_date": null,
      "effective_end_date": null,
      "matching_interval_minutes": null
    }
  ],
  "total": 36,
  "summary": {
    "total": 36,
    "returned": 1,
    "as_of": "2026-06-02",
    "requested_as_of": "2026-06-03",
    "data_as_of": "2026-06-02",
    "date_mode": "latest_available",
    "by_status": { "attention": 1 },
    "by_exchange": { "twse": 1 },
    "dedupe_by": "none"
  }
}

GET /api/market-surveillance/exited-dispositions

查詢指定日期「已經離開處置」的股票。API 會先解析 as_of 前一個交易日,並查詢 effective_end_date 等於該日期的處置事件;若無法取得交易日,會以 as_of - 1 day 作為 fallback。回傳的 summary.exit_effective_end_date 會標示實際用來比對的處置結束日。

Query Params:

名稱 類型 必填 預設 說明
as_of date 今天(台北時間) 查詢哪一天已經離開處置
exchange string twsetpex
source_exchange string exchange 的別名
limit int 500 回傳筆數(1-1000)
offset int 0 分頁 offset

Response (200):

{
  "results": [
    {
      "id": 628,
      "ticker": "6806.TW",
      "security_code": "6806",
      "security_name": "森崴能源",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "disposition_measure": "人工管制撮合",
      "matching_interval_minutes": 60
    }
  ],
  "rows": [
    {
      "id": 628,
      "ticker": "6806.TW",
      "security_code": "6806",
      "security_name": "森崴能源",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "disposition_measure": "人工管制撮合",
      "matching_interval_minutes": 60
    }
  ],
  "total": 1,
  "summary": {
    "total": 1,
    "returned": 1,
    "as_of": "2026-06-12",
    "by_status": { "disposition": 1 },
    "by_exchange": { "twse": 1 },
    "dedupe_by": "ticker",
    "exit_date_field": "effective_end_date",
    "exit_effective_end_date": "2026-06-11"
  }
}

GET /api/market-surveillance/stocks/{ticker}/status

查詢單一股票目前監管狀態與近期相關事件。處置中的股票優先回傳 disposition;若沒有有效處置,但近期有注意公告,回傳 attention;否則為 normal

Query Params:

名稱 類型 必填 預設 說明
as_of date 今天(台北時間) 指定狀態判斷日期
lookback_days int config 預設 注意股回看天數

Response (200):

{
  "ticker": "3026.TW",
  "status": "disposition",
  "as_of": "2026-05-29",
  "near_disposition_threshold": null,
  "is_attention": true,
  "is_disposition": true,
  "latest_attention": {
    "id": 9,
    "ticker": "3026.TW",
    "security_code": "3026",
    "security_name": "禾伸堂",
    "source_exchange": "twse",
    "status_type": "attention",
    "announce_date": "2026-05-27",
    "effective_start_date": null,
    "effective_end_date": null
  },
  "active_disposition": {
    "id": 7,
    "ticker": "3026.TW",
    "security_code": "3026",
    "security_name": "禾伸堂",
    "source_exchange": "twse",
    "status_type": "disposition",
    "announce_date": "2026-05-25",
    "effective_start_date": "2026-05-26",
    "effective_end_date": "2026-06-08",
    "matching_interval_minutes": 20
  },
  "active_events": [
    {
      "id": 7,
      "ticker": "3026.TW",
      "security_code": "3026",
      "security_name": "禾伸堂",
      "source_exchange": "twse",
      "status_type": "disposition",
      "announce_date": "2026-05-25",
      "effective_start_date": "2026-05-26",
      "effective_end_date": "2026-06-08",
      "cumulative_count": 3,
      "trading_information": null,
      "disposition_condition": "連續三次",
      "disposition_measure": "第二次處置",
      "disposition_content": "以人工管制之撮合終端機執行撮合作業(約每二十分鐘撮合一次)。",
      "matching_interval_minutes": 20,
      "close_price": null,
      "pe_ratio": null,
      "source_url": "https://www.twse.com.tw/announcement/punish?response=json&startDate=20260525&endDate=20260527"
    }
  ],
  "recent_events": [
    { "id": 7, "ticker": "3026.TW", "status_type": "disposition" }
  ]
}

near_disposition_threshold 為顯示用的近處置價量門檻;只有當該股票距離處置條件只差一次可計算的注意事件時才回傳物件,否則為 null。此物件只包含價量門檻欄位,不回傳「連續幾日」或「幾日內幾次」等內部累計規則文字。

回傳物件範例:

{
  "as_of": "2026-05-29",
  "trading_date": "2026-05-29",
  "source_exchange": "twse",
  "upper_close": 612.0,
  "lower_close": null,
  "min_volume": 2500000
}
欄位 型別 說明
as_of date 本次判斷日期
trading_date date 門檻適用交易日
source_exchange string twsetpex
upper_close number/null 收盤價大於等於此值時可能觸發可計算的上漲方向注意門檻
lower_close number/null 收盤價小於等於此值時可能觸發可計算的下跌方向注意門檻
min_volume integer/null 成交量大於等於此股數時可能觸發可計算的量能門檻

GET /api/stocks/{ticker}/ohlc

取得 OHLC 歷史資料。

Query Params:

名稱 類型 必填 預設 說明
start_date string 起始日期 YYYY-MM-DD
end_date string 結束日期 YYYY-MM-DD
limit int 100 回傳筆數(1-5000)
basis string raw 價格基準:raw = 實際成交價(未還原),adj = 還原價(分割/除權息調整)。其他值回 422

basis=adjstock_ohlc.adj_open/adj_high/adj_low/adj_close,欄位名稱與 raw 相同(open/high/low/close),前端不需改 parser。volume 兩種基準都是實際成交股數(成交量無還原概念)。還原價缺漏的列會被略過、不會用 raw 補(混基準序列比缺列更有害,issue #1253);因此 adj 的筆數可能少於 raw。要把 K 線與還原基準的指標歷史疊在同一張圖時用 adj,只看實際成交價用預設 raw

Response:

{
  "ticker": "2330.TW",
  "data": [
    {
      "date": "2024-03-15",
      "open": 840.0,
      "high": 855.0,
      "low": 838.0,
      "close": 850.0,
      "volume": 25000000
    }
  ],
  "total": 100
}

GET /api/stocks/markets/list

列出可用市場。


6. 技術指標 Indicators

GET /api/stocks/{ticker}/indicators

取得技術指標數據。支援兩種模式:

  • 快照模式(預設):latest=true,回傳最新一筆指標快照。
  • 歷史模式latest=false,回傳過去 N 天的指標時間序列,適用於繪製技術指標圖。

Query Params:

名稱 類型 必填 預設 說明
latest bool true true 回傳最新快照;false 回傳歷史序列
days int 66 取近 N 天(1-120,僅 latest=false 時生效)
fields string null 逗號分隔的指標群組名稱,如 rsi,macd,ma,僅回傳指定指標

days 上限由 config/core.yamlindicators.series_max_days 控制(預設 120)。 當 latest=true 時,days 參數會被忽略。

Response(快照模式 latest=true):

{
  "ticker": "2330.TW",
  "timestamp": "2024-03-15T15:00:00",
  "indicators": {
    "ma": { "5": 848.0, "10": 845.0, "20": 840.0, "50": 830.0, "60": 828.0, "100": 815.0, "120": 812.0, "200": 800.0, "240": 795.0 },
    "ema": { "5": 849.0, "10": 844.0, "20": 839.0, "50": 832.0, "60": 830.5, "100": 818.0, "120": 815.0, "200": 805.0, "240": 800.0 },
    "rsi": 65.3,
    "macd": { "value": 5.2, "signal": 3.1, "histogram": 2.1 },
    "kd": { "k": 72.5, "d": 68.1 },
    "bollinger": { "upper": 870.0, "middle": 845.0, "lower": 820.0 },
    "adx": 25.0,
    "atr": 15.0,
    "obv": 1234567,
    "willr": -32.5,
    "volume_ma": { "5": 25000000, "20": 22000000 },
    "vwap": 847.5,
    "ichimoku": { "tenkan": 845.0, "kijun": 838.0 },
    "cci": 85.3,
    "mfi": 62.1,
    "chop": 45.8,
    "wavetrend": { "wt1": 12.5, "wt2": 10.3 },
    "aroon": { "up": 85.7, "down": 14.3, "oscillator": 71.4 },
    "keltner": { "upper": 865.0, "middle": 845.0, "lower": 825.0 },
    "supertrend": { "value": 830.0, "direction": "bullish" },
    "donchian": { "high_20": 870.0, "low_20": 820.0, "high_55": 890.0, "low_55": 790.0 },
    "tii": 65.2,
    "wvixf": { "value": 8.5, "upper": 15.0, "panic": false },
    "elder_ray": { "bull_power": 12.5, "bear_power": -3.2 },
    "weis_wave": { "volume": 5000000, "direction": 1.0, "price_delta": 5.5 },
    "qqe": { "rsi_smooth": 62.3, "line": 58.1, "above_line": true },
    "szo": 35.2,
    "vsa": { "no_supply_detected": false, "no_supply_confirmed": false }
  }
}

indicators 共包含 28 個指標群組(62 個 leaf 值)。完整群組清單見 api/routes/stocks.py 中的 DB_COLUMN_TO_API_GROUP 映射表。

Response(歷史模式 latest=false):

{
  "ticker": "2330.TW",
  "days": 66,
  "total": 66,
  "data": [
    {
      "timestamp": "2024-01-10T00:00:00",
      "indicators": {
        "rsi": 58.2,
        "macd": { "value": 3.1, "signal": 2.8, "histogram": 0.3 }
      }
    }
  ]
}

indicators 結構與快照模式相同(28 群組 / 62 leaf 值)。使用 fields 可篩選回傳的指標群組,如 fields=rsi,macd,aroon


7. 三大法人 Institutional

Prefix: /api/institutional

個股金額估算基礎

以下兩個 additive endpoint 以現有法人股數與同日收盤價估算個股金額:

estimated_buy_amount = buy_shares × same-day close
estimated_sell_amount = sell_shares × same-day close
estimated_net_amount = net_shares × same-day close
period estimated_net_amount = Σ(daily net_shares × same-day close)

這些欄位是以每日收盤價估算的新台幣金額,不是交易所提供的個股實際成交金額。呼叫方必須以 response 的 amount_basis="net_shares_x_daily_close" 判斷口徑,不得將估算值標示成官方成交金額。多日金額先逐日計算再加總,不使用「期間淨股數 × 最新收盤價」。

days 在這兩個 endpoint 都表示交易日,不是日曆日:

  • 排行 API:以全市場法人資料最新日起,精確取最近 N 個 distinct trade_date
  • 個股 API:以 canonical ticker 自己的法人資料,精確取最近 N 筆 trade_date
  • 歷史少於 N 日時回 HTTP 200,actual_days 為實際可得日數。

GET /api/institutional/amount-ranking

回傳最近 N 個交易日內,各法人按「逐日估算淨買賣金額加總」計算的買超榜與賣超榜。這是公開、唯讀 endpoint。

Query Params:

名稱 類型 Required Nullable 預設 驗證 / 說明
days integer no no 1 1..30;最近交易日數
limit integer no no 20 1..100;買超榜與賣超榜各自最多筆數
investor_type enum no no all allforeigntrustdealerall 回前三組及第四組 total,不開放直接傳 total

Response 200 — top-level:

欄位 類型 Nullable 說明
requested_days integer no 呼叫方要求的交易日數
actual_days integer no 實際納入視窗的 distinct 交易日數
start_date date yes 實際視窗最早交易日;無資料時為 null
end_date date yes 實際視窗最新交易日;無資料時為 null
investor_type string no 套用的法人篩選
limit integer no 每側排行上限
currency string no 固定 TWD,base unit
amount_basis string no 固定 net_shares_x_daily_close
rankings InstitutionalAmountRankingGroup[] no 法人分組排行;無資料時仍保留 requested actor 分組,其買賣榜為空

InstitutionalAmountRankingGroup

欄位 類型 Nullable 說明
investor_type string no foreigntrustdealertotal
investor_name string no 外資投信自營商三大法人合計
excluded_unpriced_tickers integer no 視窗內有非零 actor net_shares、但至少一個必要同日 close 缺漏,因而未參與此 actor 排行的 ticker 數
buy InstitutionalAmountRankingItem[] no estimated_net_amount > 0;金額由大到小
sell InstitutionalAmountRankingItem[] no estimated_net_amount < 0;金額由小到大,賣超絕對值最大者在前

InstitutionalAmountRankingItem

欄位 類型 Nullable 說明
rank integer no 從 1 起算的穩定 ROW_NUMBER
ticker string no canonical ticker,例如 2330.TW
name string yes 股票名稱
direction string no buysell
net_shares integer no 視窗內每日淨股數加總
estimated_net_amount number no Σ(daily net_shares × same-day close),TWD

排序規則:

  • buyestimated_net_amount DESC, ticker ASC
  • sellestimated_net_amount ASC, ticker ASC
  • 零淨額不進任一榜。
  • investor_type=all 時,rankings 固定依 foreigntrustdealertotal 排序;total 是前三者逐日淨股數合計,不是散戶金額。指定單一法人時只有一組。
  • 若 ticker 在該 actor 視窗內任何非零 net_shares 日期缺少同日 close,整個 ticker 從該 actor 的買、賣排行排除,避免以不完整金額低估後仍參與名次。其他 actor 不受影響。

Request example:

GET /api/institutional/amount-ranking?days=5&limit=20&investor_type=all

Response example:

{
  "requested_days": 5,
  "actual_days": 5,
  "start_date": "2026-07-17",
  "end_date": "2026-07-23",
  "investor_type": "all",
  "limit": 20,
  "currency": "TWD",
  "amount_basis": "net_shares_x_daily_close",
  "rankings": [
    {
      "investor_type": "foreign",
      "investor_name": "外資",
      "excluded_unpriced_tickers": 1,
      "buy": [
        {
          "rank": 1,
          "ticker": "3231.TW",
          "name": "緯創",
          "direction": "buy",
          "net_shares": 122234059,
          "estimated_net_amount": 20457525118.5
        }
      ],
      "sell": [
        {
          "rank": 1,
          "ticker": "2330.TW",
          "name": "台積電",
          "direction": "sell",
          "net_shares": -49033372,
          "estimated_net_amount": -112659401380.0
        }
      ]
    },
    {
      "investor_type": "trust",
      "investor_name": "投信",
      "excluded_unpriced_tickers": 0,
      "buy": [],
      "sell": []
    },
    {
      "investor_type": "dealer",
      "investor_name": "自營商",
      "excluded_unpriced_tickers": 12,
      "buy": [],
      "sell": []
    },
    {
      "investor_type": "total",
      "investor_name": "三大法人合計",
      "excluded_unpriced_tickers": 1,
      "buy": [
        {
          "rank": 1,
          "ticker": "3231.TW",
          "name": "緯創",
          "direction": "buy",
          "net_shares": 122234059,
          "estimated_net_amount": 20457525118.5
        }
      ],
      "sell": [
        {
          "rank": 1,
          "ticker": "2330.TW",
          "name": "台積電",
          "direction": "sell",
          "net_shares": -49033372,
          "estimated_net_amount": -112659401380.0
        }
      ]
    }
  ]
}

完全沒有交易日資料時仍回 HTTP 200,保留 request metadata,actual_days=0start_date=nullend_date=null,並保留 requested actor 分組,其 buy=[]sell=[]。若有交易日視窗但指定 actor 全為零淨額,行為相同。

GET /api/institutional/stock/{ticker}/amount-flows

回傳指定個股最近 N 個法人資料交易日中,各法人的買賣股數、方向、同日收盤價與估算金額。這是公開、唯讀 endpoint。

Path Params:

名稱 類型 Required Nullable 說明
ticker string yes no 台灣 4–6 位數字,可帶一個大寫字母;可加 .TW / .TWO

Ticker resolution:

  • 已帶 .TW / .TWO 時只查明確 ticker。
  • 裸代號依 .TW.TWO 候選解析;只有一個可用候選時回 canonical ticker。
  • 候選只計有名稱(name IS NOT NULL)的 ticker。 無名稱的列是資料匯入殘留的佔位列,不視為可解析標的(#1206)。
  • .TW.TWO 同時存在相同裸代號且兩者皆有名稱時回 HTTP 409,呼叫方必須指定 suffix。
  • ticker 不存在,或存在但完全沒有法人資料時回 HTTP 404。

Query Params:

名稱 類型 Required Nullable 預設 驗證 / 說明
days integer no no 1 1..30;resolved ticker 自己最近 N 筆法人交易日
investor_type enum no no all allforeigntrustdealerall 回前三組及第四組 total,不開放直接傳 total

Response 200 — top-level:

欄位 類型 Nullable 說明
ticker string no resolved canonical ticker
name string yes 股票名稱
requested_days integer no 呼叫方要求的交易日數
actual_days integer no 此 ticker 實際回傳的法人資料日數
start_date date no 實際最早資料日
end_date date no 實際最新資料日
investor_type string no 套用的法人篩選
currency string no 固定 TWD,base unit
amount_basis string no 固定 net_shares_x_daily_close
actors InstitutionalAmountFlowActor[] no 法人分組;all 時固定為 foreign、trust、dealer、total

InstitutionalAmountFlowActor

欄位 類型 Nullable 說明
investor_type string no foreigntrustdealertotal
investor_name string no 外資投信自營商三大法人合計
items InstitutionalAmountFlowItem[] no trade_date DESC 排列

InstitutionalAmountFlowItem

欄位 類型 Nullable 說明
trade_date date no 法人資料交易日
direction string no buy(net > 0)、sell(net < 0)、neutral(net = 0)
buy_shares integer no 當日買進股數
sell_shares integer no 當日賣出股數
net_shares integer no 當日淨股數
close_price number yes 同日收盤價;缺價時為 null;數值 0 是有效價格
estimated_buy_amount number yes buy_shares × close_price;缺價且非零流量時為 null
estimated_sell_amount number yes sell_shares × close_price;缺價且非零流量時為 null
estimated_net_amount number yes net_shares × close_price;缺價且非零流量時為 null
amount_status string no availablemissing_closezero_flow

amount_status 語意:

條件 金額欄位
available 有同日 close;close=0 仍屬 available 按公式計算,可能為 0
missing_close buy/sell/net 任一股數非 0,但缺同日 close 三個 estimated_*_amount 均為 null
zero_flow buy_sharessell_sharesnet_shares 全為 0 三個 estimated_*_amount 均為 0;close 可為 null

Request example:

GET /api/institutional/stock/2330/amount-flows?days=1&investor_type=all

Response example:

{
  "ticker": "2330.TW",
  "name": "台積電",
  "requested_days": 1,
  "actual_days": 1,
  "start_date": "2026-07-23",
  "end_date": "2026-07-23",
  "investor_type": "all",
  "currency": "TWD",
  "amount_basis": "net_shares_x_daily_close",
  "actors": [
    {
      "investor_type": "foreign",
      "investor_name": "外資",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "sell",
          "buy_shares": 10889394,
          "sell_shares": 15503824,
          "net_shares": -4614430,
          "close_price": 2405.0,
          "estimated_buy_amount": 26188992570.0,
          "estimated_sell_amount": 37286696720.0,
          "estimated_net_amount": -11097704150.0,
          "amount_status": "available"
        }
      ]
    },
    {
      "investor_type": "trust",
      "investor_name": "投信",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "sell",
          "buy_shares": 322751,
          "sell_shares": 1266009,
          "net_shares": -943258,
          "close_price": 2405.0,
          "estimated_buy_amount": 776216155.0,
          "estimated_sell_amount": 3044751645.0,
          "estimated_net_amount": -2268535490.0,
          "amount_status": "available"
        }
      ]
    },
    {
      "investor_type": "dealer",
      "investor_name": "自營商",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "buy",
          "buy_shares": 657629,
          "sell_shares": 619173,
          "net_shares": 38456,
          "close_price": 2405.0,
          "estimated_buy_amount": 1581597745.0,
          "estimated_sell_amount": 1489111065.0,
          "estimated_net_amount": 92486680.0,
          "amount_status": "available"
        }
      ]
    },
    {
      "investor_type": "total",
      "investor_name": "三大法人合計",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "sell",
          "buy_shares": 11869774,
          "sell_shares": 17389006,
          "net_shares": -5519232,
          "close_price": 2405.0,
          "estimated_buy_amount": 28546806470.0,
          "estimated_sell_amount": 41820559430.0,
          "estimated_net_amount": -13273752960.0,
          "amount_status": "available"
        }
      ]
    }
  ]
}

缺價日期不會從個股行為清單移除;方向仍由 net_shares 決定,金額依上述 amount_status 規則回傳。

新增估算金額 endpoints 的錯誤回應

HTTP 條件 Body
404 ticker 不存在(Unknown ticker …),或股票存在但沒有任何法人資料(No institutional data found … 全域 error envelope
409 裸代號同時命中 .TW.TWO 全域 error envelope
422 ticker 格式錯誤;dayslimitinvestor_type 驗證失敗 FastAPI detail array
500 未預期服務錯誤 全域 error envelope

不存在 ticker 的 404 範例(股票存在但無法人資料時 code/message 為 No institutional data found for …;409/500 使用相同全域 envelope):

{
  "error": {
    "code": "Unknown ticker 9999.TW",
    "message": "Unknown ticker 9999.TW",
    "details": {}
  }
}

422:

{
  "detail": [
    {
      "type": "greater_than_equal",
      "loc": ["query", "days"],
      "msg": "Input should be greater than or equal to 1",
      "input": "0",
      "ctx": {
        "ge": 1
      }
    }
  ]
}

這兩個 endpoint 不修改既有 /api/institutional/ranking/*/api/institutional/stock/{ticker} 或其他 institutional response schema/status code。

GET /api/institutional/foreign/daily/{ticker}

外資每日買賣超。

Query Params:

名稱 類型 預設 說明
days int 30 天數
sort string 排序欄位
limit int 回傳筆數

GET /api/institutional/ranking/{investor_type}

個股層級三大法人買賣超排行。

Path Params:

名稱 類型 說明
investor_type string foreigntrustdealertotal

Query Params:

名稱 類型 預設 說明
days int 1 統計天數,1-30
order string desc desc 買超、asc 賣超
limit int 20 回傳筆數,1-100

Response fields:

欄位 類型 說明
ticker string 股票代號
name string null
net_shares int 期間買賣超股數
close_price number null
net_value number null

GET /api/institutional/trust/daily/{ticker}

投信每日買賣超。參數同 foreign。

GET /api/institutional/trust/ranking

投信買賣超排行。參數同 foreign ranking。

GET /api/institutional/dealer/daily/{ticker}

自營商每日買賣超。參數同 foreign。

GET /api/institutional/dealer/ranking

自營商買賣超排行。參數同 foreign ranking。


GET /api/institutional/consensus/{ticker}

三大法人共識(看個股三大法人方向是否一致)。

Query Params:

名稱 類型 預設 說明
days int 20 統計天數

GET /api/institutional/consensus/all

全市場法人共識。

Query Params:

名稱 類型 預設 說明
consensus_type string 共識類型
limit int 50 回傳筆數

GET /api/institutional/breadth-indicator

市場寬度指標。

Query Params:

名稱 類型 預設 說明
indicators string 指標類型(逗號分隔)
days int 60 天數

8. 融資融券 Margin

Prefix: /api/margin

GET /api/margin/stock/{ticker}

取得融資融券餘額資料。

Query Params:

名稱 類型 預設 說明
days int 30 天數(1-365)

Response — MarginData[]:

[
  {
    "ticker": "2330.TW",
    "trade_date": "2024-03-15",
    "margin_balance": 15000,
    "short_balance": 2000,
    "margin_buy": 500,
    "margin_sell": 300,
    "short_buy": 100,
    "short_sell": 150,
    "margin_previous": 14800,
    "short_previous": 1950,
    "source_market": "twse"
  }
]

GET /api/margin/ranking

融資融券標的排行榜。取最新可用交易日,依指定指標排序回傳前 N 檔。

Query Params:

名稱 類型 預設 說明
metric string margin_change 排序維度,四選一:margin_change(融資增減)/ short_change(融券增減)/ margin_balance(融資餘額)/ short_balance(融券餘額)
order string desc desc=增加/餘額最多在前;asc=相反
limit int 20 回傳筆數(1-100)

*_change 為日對日差(餘額 − 前日餘額)。前日資料缺漏者該指標為 null 並排在最後。無任何融資融券資料時回傳空陣列 []

Response — MarginRankingStock[]:

[
  {
    "ticker": "00631L.TW",
    "name": "元大台灣50正2",
    "trade_date": "2026-07-17",
    "margin_balance": 275683,
    "margin_previous": 244400,
    "margin_change": 31283,
    "short_balance": 13864,
    "short_previous": 16884,
    "short_change": -3020,
    "close_price": 32.17
  }
]

Errors: metric / order 非白名單值或 limit 越界 → 422


9. 股權分散表 Ownership

Prefix: /api/ownership

內部儲存重構(2026-06,zero API change):股權寬欄位(15 級距 ratio/change) 自每日重複寫入 daily_chip_snapshot 改為一週一列的 ownership_weekly_snapshot 內部表,相關 endpoint(本節、/api/chip/summary/{ticker}/api/recaps/chip/daily/{requested_date}/api/signals/stock/{ticker})的 request/response 完全不變

GET /api/ownership/stock/{ticker}

取得股權分散表。

Query Params:

名稱 類型 預設 說明
trade_date string 指定日期 YYYY-MM-DD
weeks int 近 N 週(1-104)

Response — OwnershipDistribution[]:

[
  {
    "ticker": "2330.TW",
    "trade_date": "2024-03-15",
    "holder_level": "1-999",
    "holder_count": 350000,
    "shares": 500000000,
    "share_ratio": 5.2
  }
]

10. 基本面 Fundamentals

Prefix: /api/fundamentals

GET /api/fundamentals/monthly-revenue/{ticker}

月營收。

Query Params:

名稱 類型 預設 說明
months int 12 月數(1-120)

Response — MonthlyRevenue[]:

[
  {
    "ticker": "2330.TW",
    "data_year_month": 202403,
    "report_date": "2024-04-10",
    "revenue_current": 2100000000,
    "revenue_prev_month": 1950000000,
    "revenue_prev_year_month": 1800000000,
    "mom_change_pct": 7.69,
    "yoy_change_pct": 16.67,
    "ytd_revenue": 6000000000,
    "ytd_revenue_prev_year": 5200000000,
    "ytd_yoy_change_pct": 15.38,
    "source_market": "twse"
  }
]

GET /api/fundamentals/valuation/{ticker}

估值指標(PE/PB/殖利率)。

Query Params:

名稱 類型 預設 說明
days int 60 天數(1-365)

Response — ValuationMetric[]:

[
  {
    "ticker": "2330.TW",
    "trade_date": "2024-03-15",
    "pe_ratio": 25.3,
    "pb_ratio": 6.8,
    "dividend_yield": 1.8,
    "dividend_per_share": 15.0,
    "source_market": "twse"
  }
]

GET /api/fundamentals/quarterly/{ticker}

季報數據。

Query Params:

名稱 類型 預設 說明
years int 5 年數(1-10)

Response — QuarterlyReport[]:

[
  {
    "ticker": "2330.TW",
    "report_year": 2024,
    "report_quarter": 1,
    "report_date": "2024-05-15",
    "eps_basic": 9.56,
    "eps_diluted": 9.52,
    "revenue": 5926000000,
    "net_income": 2254000000,
    "source": "mops"
  }
]

GET /api/fundamentals/dividend-events/{ticker}

個股除權息時間軸(歷史 + 即將到來),除息(現金)與除權(配股)各自包裹、各有填息/填權狀態與推導現金殖利率。 資料來源:TWSE TWT48U_ALL + TPEx tpex_exright_prepost 除權除息預告表(whole-market,免費)。

Query Params:

名稱 類型 預設 說明
days int config lookback_days(730) 回溯天數(1-3650);未來已排定事件一律納入

Response — DividendEvent[](API 值皆為英文 enum;繁中顯示由前端 mapping):

typecash(除息)/ stock(除權)/ both(除權息)。 fill_statusex_dividendex_rights 各一):not_due(未到)/ filled(已填,最新收盤 ≥ 除權息前收盤)/ partial(部分填,介於參考價與前收盤)/ discount(貼,低於參考價)/ unknown(無可比價)。 reference_price(除權息參考價)、各 fill_statuscash_yield 皆由本地 OHLC 即時推導(預告表來源無價格欄位)。ex_dividend 於純除權時為 nullex_rights 於純除息時為 null

reference_price 採 TWSE 官方全公式 (前收 - 現金股利 + 認股比例 × 認購價) / (1 + 配股率 + 認股比例)

現金增資(rights issue): 預告表的「除權/除權息」同時涵蓋配股與現金增資。ex_rights 額外帶 subscription_ratio(認股比例)與 subscription_price(認購價格);subscription_ratio > 0 即代表含現金增資成分。純現金增資指紋type="stock"stock_dividend=0subscription_ratio>0(前端應顯示「現金增資」而非配股)。當認購價尚未公告時,無法計算參考價,reference_pricenull

[
  {
    "ex_date": "2023-06-15",
    "type": "both",
    "reference_price": 97.0,
    "ex_dividend": { "cash_dividend": 0.5, "cash_yield": 0.52, "fill_status": "filled" },
    "ex_rights": { "stock_dividend": 0.11, "subscription_ratio": null, "subscription_price": null, "fill_status": "filled" },
    "source": "TWSE"
  },
  {
    "ex_date": "2026-06-18",
    "type": "stock",
    "reference_price": 19.45,
    "ex_dividend": null,
    "ex_rights": { "stock_dividend": 0.0, "subscription_ratio": 0.2468, "subscription_price": 17.2, "fill_status": "not_due" },
    "source": "TPEx"
  }
]

回傳 404 當該 ticker 無除權息資料。


GET /api/fundamentals/dividend-calendar

全市場除權息行事曆:列出 [on, on + window] 區間內即將除權息的個股。

Query Params:

名稱 類型 預設 說明
on date 今日 區間起始日(YYYY-MM-DD)
window int config calendar_window_days(7) 往後天數(1-90)

Response — DividendCalendarEntry[](v2 enum shape,同 DividendEvent 外加 ticker/name;無事件時回傳空陣列):

[
  {
    "ticker": "2330.TW",
    "name": "台積電",
    "ex_date": "2024-06-13",
    "type": "cash",
    "reference_price": 796.0,
    "ex_dividend": { "cash_dividend": 4.0, "cash_yield": 0.5, "fill_status": "filled" },
    "ex_rights": null,
    "source": "TWSE"
  }
]

11. 策略訊號 Signals

Prefix: /api/signals

GET /api/signals/latest

取得最新策略訊號。

Query Params:

名稱 類型 必填 說明
direction string 依 primary signal 的實際 state 篩選:bullish / bearish
date string 指定日期 YYYY-MM-DD
ticker string 指定個股
view string summary(預設)/ full
limit int 排序(confidence 降冪)後最多回傳 N 筆;1 ≤ N ≤ signals.latest.max_limit(core.yaml,預設 3000)

⚠️ 預設為 view=summary:巢狀欄位(structured_signals / signal_summary / ta_index_groups / signals / events / stories / relations)一律以空值([] / {})回傳,全市場回應約 2-3MB。需要完整巢狀 payload 必須顯式view=full(全市場約 240MB,慎用)。此預設自 issue #568 起生效——舊呼叫方若依賴巢狀欄位,會拿到空值而非錯誤。

方向語意:頂層 directiondirection query filter,以及 /api/signals/chip-highlights/api/signals/financial-highlights 的 頂層 bias 都使用 primary signal 的實際 state。巢狀 signals[*].direction 則是 signal definition 支援的方向集合,可能是 bullish/bearish/neutral 等複合值;該訊號在本筆資料的實際狀態請讀 signals[*].state

支撐族雙基準(issue #1253,migration 125):支撐/壓力欄位同時提供兩種價格 基準,兩者都正確、回答的是不同問題,呼叫方依用途擇一,不可混用:

基準 欄位形態 語意 適用
還原(adjusted) 既有欄名(major_supportlow_20d_mindistance_to_support_*structure_stop…) 以還原權息序列量測;除息後隨價格一併下修 停損/決策——不會把已配發的股利算進支撐
名目(raw) _raw 後綴孿生欄(major_support_raw…) 以實際成交價序列量測 市場記憶——「買方上次進場的價位」

窗口定義與還原欄完全相同(近 20 根收盤 min/max、20/60 根盤中低點 min),距離 欄的分子分母必為同一基準。atr_stop_distance / invalidation_level 沒有 _raw 版:ATR 屬指標、指標恆為還原基準。_raw 欄在一次性全史回填跑過之前為 null

Response — SignalResponse[]:

[
  {
    "ticker": "2330.TW",
    "name": "台積電",
    "signal_date": "2024-03-15",
    "signal_type": "ta_ma_break_event",
    "strategy_type": "short_term_long",
    "direction": "bullish",
    "description": "RSI 超賣反彈訊號",
    "strength": 0.85,
    "price": 850.0,
    "change_pct": 1.8,
    "bias": "bullish",
    "confidence": 0.72,
    "rsi_value": 28.5,
    "rsi_status": "oversold",
    "macd_cross_signal": "golden_cross",
    "macd_divergence": null,
    "kdj_signal": "low_passivization",
    "kdj_cross_signal": "golden_cross",
    "volume_ratio": 1.35,
    "volume_status": "above_average",
    "ma_alignment": "bullish",
    "cti_score": 0.65,
    "bb_status": "near_lower",
    "major_support": 830.0,
    "major_resistance": 870.0,
    "major_support_raw": 812.0,
    "major_resistance_raw": 851.0,
    "patterns": ["hammer", "morning_star"],
    "pattern_reliability": "high",
    "atr_stop_distance": 25.0,
    "invalidation_level": 825.0,
    "signals": [
      {
        "id": "technical.signal.ta_ma_break_event",
        "signal_code": "ta_ma_break_event",
        "domain": "technical",
        "state": "bullish",
        "direction": "bullish/bearish",
        "score": 0.72,
        "strength": 0.72,
        "name": "均線穿越事件",
        "description": "",
        "display": { "title": "均線穿越事件" },
        "readiness": { "status": "ready", "missing_indexes": [] },
        "index_evidence": {
          "primary": {
            "break_above_ma20": {
              "value": true,
              "present": true,
              "name": "站上月線(20MA)",
              "description": "",
              "display": { "title": "站上月線(20MA)" }
            }
          },
          "supporting": {},
          "risk": {},
          "gate": {}
        }
      }
    ],
    "events": [
      {
        "id": "technical.event.ma_month_reclaim",
        "event_code": "ma_month_reclaim",
        "signal_code": "ta_ma_break_event",
        "domain": "technical",
        "state": "active",
        "direction": "bullish",
        "score": 0.76,
        "strength": 0.76,
        "confidence": 0.76,
        "name": "站回月線(MA20)",
        "description": "",
        "display": { "title": "站回月線(MA20)" },
        "readiness": { "status": "ready", "source_signal_status": "ready", "blocked_reason": null },
        "evidence": { "used_paths": ["primary.break_above_ma20"] }
      }
    ],
    "stories": [],
    "relations": [
      {
        "from": { "type": "index", "code": "break_above_ma20", "name": "站上月線(20MA)", "display": { "title": "站上月線(20MA)" } },
        "to": { "type": "signal", "id": "technical.signal.ta_ma_break_event", "name": "均線穿越事件" },
        "relation": "supports",
        "role": "primary_index",
        "condition": "break_above_ma20 == true",
        "required": true,
        "weight": 0.5
      },
      {
        "from": { "type": "signal", "id": "technical.signal.ta_ma_break_event", "name": "均線穿越事件" },
        "to": { "type": "event", "id": "technical.event.ma_month_reclaim", "name": "站回月線(MA20)" },
        "relation": "triggers",
        "role": "source_signal",
        "condition": "ta_ma_break_event is ready",
        "required": true,
        "weight": 1.0
      }
    ],
    "structured_signals": [
      {
        "signal_code": "ta_ma_break_event",
        "event_code": "ma_month_reclaim",
        "direction": "bullish",
        "confidence": 0.76,
        "source": "signals_events_projection"
      }
    ],
    "signal_summary": {
      "count": 1,
      "source": "signals_events_projection"
    },
    "ta_index_groups": {
      "moving_average_events": {
        "ma20": {
          "distance_pct": -1.2,
          "break_direction": "break_below",
          "break_above": false,
          "break_below": true,
          "days_above": 0,
          "days_below": 1
        }
      },
      "price_acceptance": {
        "price_vs_vwap_pct": -0.8,
        "vwap_position": "below"
      },
      "gap_limit": {
        "gap_pct": -1.5,
        "gap_type": "gap_down",
        "limit_up_hit": false,
        "limit_down_hit": false
      },
      "pattern_signals": {
        "bearish_reversal_pattern_detected": true,
        "top_pattern_name": "DOUBLE_TOP",
        "top_pattern_score": 0.82
      }
    }
  }
]

正式資料流為 indexes -> signals -> events -> stories,並以 relations 表達因果鏈。structured_signals / signal_summary 是 deprecated compatibility alias,內容由 signals / events projection 產生。正式 API response 不提供 canonical 欄位。

Display note: replacement signals[*], events[*], signals[*].index_evidence.<section>.<index_code>, and relations[*].from (when from.type == "index") provide localized Traditional Chinese display.title values for direct frontend rendering.

Name field: every signals[*], events[*], stories[*], and signals[*].index_evidence.<section>.<index_code> entry carries a flat name (Traditional Chinese display name), and every relations[*].from / relations[*].to node carries a name too, so a graph UI can label nodes without joining back to the arrays. Names come from a single registry (config/sesi_names.yaml); name mirrors display.title where both exist. This is an additive, non-breaking field.

Description field: the same signals[*], events[*], stories[*], and index-evidence entries also carry a flat description (Traditional Chinese long-form note) from the same registry. It is authored incrementally, so it is "" for most codes today. This nested SESI description is a distinct field from the legacy top-level SignalResponse.description (the free-text field on the flat signal object) — do not conflate them. description is not added to relations[*] graph nodes (nodes carry name only; look up the entity in the arrays for its description). Additive, non-breaking.


GET /api/signals/stories/latest

首頁用:把當日 ses_story_daily 的 story 命中按 (domain, story_code) 分組,回傳每個 story 及其命中個股清單。唯讀,涵蓋 technical / chip / financial / cross_domain 四域。

Query 參數:

參數 型別 預設 說明
domain technical\|chip\|financial\|cross_domain 全部 限定單一 domain;不給回四域
date YYYY-MM-DD 各 domain 各自最新 每個 domain 獨立取 MAX(trading_date) ≤ date
direction string 全部 依 story direction 篩選(bullish/bearish/risk/support…)

行為說明:

  • as_of 逐 domain 列出實際採用的交易日(各域獨立取最新,可能不同)。
  • stocks[] 僅含 ticker + name,按 ticker 排序。不回傳 score(未經前瞻報酬校準的 heuristic,避免被誤讀為機率)與 suppressed_by(本端點不呈現衝突組主/從區別;個股頁端點則會帶出該欄位,見下方 /api/signals/ticker/{ticker} 說明)。
  • 同一檔若當日觸發同組多個 story,會分別出現在各該 story group。
  • 每個 group 的 stocks[] 上限為 signals.stories.max_stocks_per_group(config,預設 500);超限只回前 N 檔並標 truncated: true
  • conflict_group 為該 story 所屬衝突組;cross_domain story 無衝突組,一律 null
  • title 為 story 中文名;description 為 story 的繁中敘述(來自 SESI registry sesi_names.yaml,未收錄時為 "")。
  • 錯誤:domain 非四值之一或 date 格式錯 → 422;當日無 story → 200 + groups: []

Response — StoriesLatestResponse:

{
  "as_of": {
    "technical": "2026-07-06",
    "chip": "2026-07-06",
    "financial": "2026-07-06",
    "cross_domain": "2026-07-06"
  },
  "groups": [
    {
      "domain": "technical",
      "story_code": "new_high_breakout",
      "title": "創新高突破",
      "description": "當股價突破52週新高或波段高點任一時,構成多方創新高突破",
      "direction": "bullish",
      "conflict_group": "technical_breakout_discovery",
      "count": 12,
      "truncated": false,
      "stocks": [
        {"ticker": "2330.TW", "name": "台積電"},
        {"ticker": "2454.TW", "name": "聯發科"}
      ]
    }
  ]
}

GET /api/signals/market-regime-today

取得今日大盤環境研判。

Response — MarketRegimeTodayResponse:

{
  "status": "ok",
  "trading_date": "2024-03-15",
  "ticker": "^TWII",
  "close": 20150.5,
  "confirmed_regime": "bull_trend",
  "stress_confirmed": false,
  "regime_label": "多頭趨勢",
  "ma60": 19800.0,
  "ma120": 19200.0,
  "ma200": 18500.0,
  "ma120_slope_pct": 0.15,
  "atr_ratio": 0.012,
  "drawdown_pct": -2.5,
  "stress_by_atr": false,
  "stress_by_drawdown": false,
  "stress_by_return_3d": false,
  "ma_structure": "bullish_aligned",
  "interpretation": "大盤處於多頭格局...",
  "current_segment": {
    "regime": "bull_trend",
    "start_date": "2024-01-15",
    "end_date": "2024-03-15",
    "duration_days": 60,
    "total_return_pct": 8.5,
    "max_drawdown_pct": -3.2,
    "stress_day_ratio": 0.05
  },
  "acute_shock_alert": null,
  "acute_shock_score": null,
  "acute_shock_reasons": null,
  "risk_mode": null
}

status 可能值: "ok" | "no_data" | "warmup"


GET /api/signals/market-regime-range

取得大盤趨勢區間資料(每日 regime + 跨區段 segments)。

Query Parameters:

參數 類型 預設 說明
start_date string (YYYY-MM-DD) config default_lookback_days 天前 起始日
end_date string (YYYY-MM-DD) today 結束日

驗證規則: - start_date 必須 ≤ end_date,否則 422 - 區間不得超過 config max_range_days(預設 365 天),否則 422

Response — MarketRegimeRangeResponse:

{
  "ticker": "^TWII",
  "start_date": "2025-01-01",
  "end_date": "2025-03-20",
  "count": 55,
  "daily": [
    {
      "trading_date": "2025-01-02",
      "close": 18500.0,
      "confirmed_regime": "bull",
      "stress_confirmed": false,
      "regime_label": "bull_normal",
      "ma120_slope_pct": 0.5,
      "atr_ratio": 1.1,
      "drawdown_pct": -2.0,
      "acute_shock_alert": "none",
      "acute_shock_score": 0,
      "risk_mode": "risk_on"
    }
  ],
  "segments": [
    {
      "regime": "bull",
      "start_date": "2024-12-01",
      "end_date": "2025-03-20",
      "duration_days": 80,
      "total_return_pct": 5.0,
      "max_drawdown_pct": -3.0,
      "stress_day_ratio": 0.1
    }
  ]
}

GET /api/signals/market-pulse

市場脈動總覽。

Response — MarketPulseResponse:

{
  "signal_date": "2024-03-15",
  "total_tickers": 500,
  "bullish_count": 280,
  "bearish_count": 150,
  "neutral_count": 70,
  "avg_confidence": 0.62,
  "high_confidence_bullish": 85,
  "high_confidence_bearish": 45,
  "overbought_count": 30,
  "oversold_count": 25,
  "aggressive_buying_count": 40,
  "aggressive_selling_count": 20,
  "golden_cross_count": 15,
  "death_cross_count": 8,
  "kdj_golden_cross_count": 21,
  "kdj_death_cross_count": 12,
  "ma_month_reclaim_count": 42,
  "ma_month_breakdown_count": 31,
  "ma_quarter_reclaim_count": 18,
  "ma_quarter_breakdown_count": 22,
  "ma_half_year_reclaim_count": 7,
  "ma_half_year_breakdown_count": 10,
  "ma_year_reclaim_count": 4,
  "ma_year_breakdown_count": 6,
  "vwap_above_count": 260,
  "vwap_below_count": 240,
  "gap_up_count": 12,
  "gap_down_count": 9,
  "limit_up_count": 3,
  "limit_down_count": 1,
  "channel_breakout_count": 20,
  "channel_breakdown_count": 16,
  "bullish_reversal_pattern_count": 11,
  "bearish_reversal_pattern_count": 8
}

GET /api/signals/market-pulse/tickers

/market-pulse 統計計數的個股名單下鑽(Cortex#222)——給定一個 belt 計數 metric,回傳該日符合條件的個股清單。

Query Params:

名稱 類型 必填 預設 說明
metric string MarketPulseResponse 26 個 belt 計數 key 之一(如 ma_month_reclaim_countoverbought_count);未知值回 422
limit int 50 1-200

目標交易日與 base WHERE 與 /market-pulse 完全一致(MAX(trading_date) probe、interval='1d'data_quality='complete'ticker LIKE '%.TW' OR '%.TWO');metric→SQL 條件逐字取自 /market-pulseFILTER 子句。

Response — MarketPulseTickersResponse:

{
  "signal_date": "2024-03-15",
  "metric": "ma_month_breakdown_count",
  "total": 240,
  "items": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "close": 1080.0,
      "change_percent": -6.5,
      "rsi_value": 28.0,
      "volume_ratio": 2.1,
      "gap_pct": 5.0
    }
  ]
}

total 為該 metric 條件下的全數個股數(同 /market-pulse 對應 FILTER count);itemslimit 截斷,排序 ABS(change_percent) DESC NULLS LAST(純呈現排序,非篩選條件)。

跳空定義(2026-07-29 修正)gap_up_count/gap_down_count教科書跳空缺口——開盤越過昨日最高/最低open_above_prev_high/open_below_prev_low),非「開盤 vs 昨收 ±1%」的開高開低(該寬鬆版仍存於 snapshot 的 gap_type 欄位,statistics 不再使用)。gap_pct 為開盤相對昨日收盤的幅度,供名單對照;change_percent 是收盤結果,跳空開出後收盤仍可反向(開高走低)。


GET /api/signals/ticker/{ticker}

個股訊號歷史。

Query Params:

名稱 類型 預設 說明
days int 30 天數(1-365)

Response: SignalResponse[](同 /latest schema)


12. 每日分析 Daily Analysis

Prefix: /api/daily-analysis

GET /api/daily-analysis/{ticker}/latest

取得最新每日分析。

meta 識別欄位(issue #571):新增 meta.trading_date(ISO 日期,標識快照所屬交易日);移除 meta.schema_version / meta.timestamp / meta.exchange_tz(原為每列常數,時區定義在 config/markets.yaml)。

回傳 build_agent_payload 的完整技術 payload。TA payload 新增: - ta_index_groups: MA line break、EMA distance、VWAP、normalized volatility、channel position、gap/limit、extra oscillators、pattern signals。 - signals / events / stories / relations: 正式 replacement arrays。 - indexes / context / risk / quality: index、情境、風險與資料品質區塊。 - structured_signals / signal_summary: deprecated compatibility alias,來源為 signals / events projection。

Storage note: relations 是 API read-time derived field,不是 DB historical truth。DB snapshot rows persist index/source fields and may keep compact signals_json / events_json / stories_json cache; dense index -> signal relation graphs must not be full-history materialized.


GET /api/daily-analysis/{ticker}

取得日期範圍內的每日分析。

Query Params:

名稱 類型 必填 說明
from string 起始日期 YYYY-MM-DD
to string 結束日期 YYYY-MM-DD

舊的三層 sidecar include flag 已移除;若舊 client 仍送此 query param,API 會忽略它。Range item 一律回傳 replacement signals / events / stories / relations

Range responses derive replacement arrays from persisted index/source snapshot fields when compact cache is absent. Clients should treat relations as trace output, not as separately persisted domain data.


POST /api/daily-analysis/run

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

手動觸發每日分析。

Request Body:

{
  "date": "2024-03-15",       // optional
  "ticker": "2330.TW",        // optional, 空=全市場
  "market": "taiwan",         // default "taiwan"
  "backfill_days": 5          // optional, 補回填天數
}

12B. 個股分析與管理端 Analysis & Admin

POST /api/analyze

執行單檔個股分析(技術面趨勢、支撐/壓力、VRR/LLM 敘事)。

🔒 Auth: 需登入(Authorization: Bearer <access_token>)。無效 token 回 401;未帶 Authorization header 回 422。

Request Body: { "ticker": "2330.TW", ... }


POST /api/admin/update-data

觸發背景資料更新工作(依市場更新股價/籌碼等)。

🔒 Auth:admin 權限。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

Request Body: { "markets": ["taiwan"], "force": false }markets 選填,省略=全部市場)


GET /api/admin/jobs

列出所有背景工作狀態。

🔒 Auth:admin 權限。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。


GET /api/admin/jobs/{job_id}

查詢單一背景工作狀態。

🔒 Auth:admin 權限。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。


13. 訊號回測 Signal Backtest

Prefix: /api/signals/backtest

POST /api/signals/backtest/run

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

執行訊號回測。

Request Body:

{
  "domain": "technical",        // "technical" | "chip" | "financial"
  "test_days": 180,             // optional, 回測天數
  "start_date": "2024-01-01",   // optional
  "end_date": "2024-06-30",     // optional
  "tickers": ["2330.TW"],       // optional, 空=全部
  "cooldown_days": 3            // optional, 冷卻天數(0=不冷卻)
}

GET /api/signals/backtest/export/csv

匯出回測結果為 CSV。


GET /api/signals/backtest/accuracy/signal-type

依訊號類型的回測準確度。


GET /api/signals/backtest/accuracy/ticker/{ticker}

個股回測準確度。


POST /api/signals/backtest/accuracy/tickers

批次查詢多檔個股準確度。

Request Body:

{
  "tickers": ["2330.TW", "2317.TW"],
  "run_id": 1,              // optional, 預設最新一次
  "domain": "technical"      // "technical" | "chip"
}

GET /api/signals/backtest/accuracy

整體回測準確度矩陣。


14. MAS 多代理分析

Prefix: /api

POST /api/v1/mas/analyze ⭐ 主要使用

🔒 Paid: 需登入且 DB 即時會員等級為 advancepremium。JWT tier claim 不作授權依據;free、null、unknown 或 inactive tier 回 403 TIER_REQUIRED。admin 帳號(DB is_admin=true)不受 tier 限制。無效 token 回 401;未帶 Authorization header 回 422。

V1 MAS 分析(含快取)。

Request Body:

{
  "ticker": "2330.TW",
  "trading_date": "2024-03-15",    // optional, 預設最新
  "response_mode": "stream",       // "stream" (SSE) | "json"
  "target": "orchestrator",        // "orchestrator" | "technical" | "chip" | "financial"
  "options": {}                    // optional
}

response_mode: "stream" — 回傳 Server-Sent Events (SSE):

Content-Type: text/event-stream

data: {"type": "agent_start", "agent": "technical", ...}
data: {"type": "agent_result", "agent": "technical", "content": "..."}
data: {"type": "agent_start", "agent": "chip", ...}
data: {"type": "agent_result", "agent": "chip", "content": "..."}
data: {"type": "final", "content": "..."}

response_mode: "json" — 回傳完整 JSON。

多空對照欄位(target: "orchestrator"): 綜合結果(orchestrator_output / SSE done 事件的 result)除既有 key_insights / risks / recommendations 外,另含 bull_pointsbear_points(各 ≤3 條,{text, evidence} 結構,evidence 帶 [技術面]/[籌碼面]/[財務面] 來源前綴)——三個 agent 已提供事實依多空方向分桶的彙整視圖。舊快取列無此欄位時反序列化為空陣列。

SSE 串流實際以 message 事件逐行送出各區塊,meta.section 標記段落;多空對照對應新增的 section 值 bull_point / bear_point,例如:

data: {"v": 1, "seq": 42, "request_id": "…", "ticker": "3231.TW", "trading_date": "2026-07-28", "ts": "…", "agent": "orchestrator", "type": "message", "message": "🟢 法人資金動能延續,外資與投信呈現連續買超態勢。  📎 [籌碼面] foreign_streak_days=7", "meta": {"section": "bull_point"}}
data: {"v": 1, "seq": 43, "request_id": "…", "ticker": "3231.TW", "trading_date": "2026-07-28", "ts": "…", "agent": "orchestrator", "type": "message", "message": "🔴 短線動能指標出現死亡交叉,波動率處於偏高水平。  📎 [技術面] kdj_cross_signal=death_cross", "meta": {"section": "bear_point"}}

(本節上方的 agent_start/agent_result/final 示意為簡化描述,與實際 event schema 的整體對齊列於 follow-up。)

target 說明:

target 說明
orchestrator 完整分析(技術+籌碼+財務→綜合判斷)
technical 僅技術面 agent
chip 僅籌碼面 agent
financial 僅財務面 agent

POST /api/mas/analyze (Legacy)

🔒 Paid: 需登入且 DB 即時會員等級為 advancepremium。JWT tier claim 不作授權依據;free、null、unknown 或 inactive tier 回 403 TIER_REQUIRED。admin 帳號(DB is_admin=true)不受 tier 限制。無效 token 回 401;未帶 Authorization header 回 422。

舊版 MAS 分析(需自行提供 agent JSON)。

{
  "ticker": "2330.TW",
  "technical_json": { ... },
  "chip_json": { ... },
  "financial_json": { ... }
}

POST /api/mas/agent/{domain} (Legacy)

🔒 Paid: 需登入且 DB 即時會員等級為 advancepremium。JWT tier claim 不作授權依據;free、null、unknown 或 inactive tier 回 403 TIER_REQUIRED。admin 帳號(DB is_admin=true)不受 tier 限制。無效 token 回 401;未帶 Authorization header 回 422。

舊版單一 agent 分析。

{
  "ticker": "2330.TW",
  "domain_json": { ... }
}

14.5 Stock Signals (Consolidated)

GET /api/signals/stock/{ticker}

三面向(技術/籌碼/財務)個股訊號整合端點。固定使用 interval='1d' 的 daily snapshots。

Storage note: this endpoint returns formal signals / events / stories / relations, but frontends must not assume relations_json is stored in DB. Relations are deterministic trace projections from signal/event/story definitions and current domain objects.

Frontend note: relations includes trace edges such as index -> signal, signal -> event, signal -> story, and event -> story. Factor joins should use from.type == "index" / to.type == "signal" relations, not DB relations_json. Replacement signals[*], events[*], stories[*], index evidence entries, and index relation nodes include display.title when a display label can be derived. Additionally, every signals[*], events[*], stories[*], and index-evidence entry carries a flat name (Traditional Chinese), and every relations[*] node (from/to, all types) carries a name, so a graph UI can label nodes without joining back to the arrays. Names come from a single registry (config/sesi_names.yaml, the authoritative source); name mirrors display.title where both exist. Additive, non-breaking. Those same entity entries (but not relation nodes) also carry a flat description (Traditional Chinese, from the same registry, "" until authored) — distinct from the legacy top-level SignalResponse.description.

Cross-domain note: the response also includes a cross_domain DomainPayload (same shape as the three per-domain fields) synthesizing the three domains into cross-domain stories (e.g. high_conviction_long). Its relations are cross-linkage onlysignal -> event, signal -> story, event -> story — and deliberately omit index -> signal edges (those are already provided per-domain, so they are not repeated here). cross_domain is null when any of the three domains has no data. When the three domains resolve to different trading dates (e.g. chip lagging over a weekend), cross_domain.stories is empty and cross_domain.quality.diagnostic carries the reason ({state, reason: "cross_domain_source_mismatch", ...}) — this is expected, not an error. Note cross_domain.quality therefore has a different shape from the per-domain quality ({data_quality, warnings}). cross_domain only populates signals/events/stories/relations (+ quality on misalignment); its indexes/context/risk/ses_cache_status are always empty/null (those are per-domain-only concepts).

Chip cost-zone note (v1.5.0): chip.indexes.cost_zones is the V3 institutional cost-zone object sourced from chip.context.chip_structure. Cost windows default to 60 trading days and can be changed with cost_window_days on /api/agent/chip/{ticker}. Each actor zone (foreign/trust/dealer/composite) keeps the legacy net-view band / position, and additionally exposes buy_cost_zone, sell_cost_zone, and net_cost_zone. The net view uses signed Net Cash Flow VWAP: avg_cost = Σ(close × net_capped) / Σ(net_capped) where net_capped is the symmetric ±median(|net|)×5 clip. Directional buy/sell views use only buy-flow or absolute sell-flow weights, so net_cost_zone.status=distorted can still coexist with available buy/sell costs. Each band exposes avg_cost, median_cost, and poc_price; each meta carries diagnostics such as window_days, net_cumulative_shares, total_abs_flow, and churn_ratio. v1.6.0: each buy_cost_zone / sell_cost_zone additionally carries histogram — either null (no data: no flow in that direction, or an all-flat price window) or { "prices": number[20], "shares": int[20] }, a volume-profile-style distribution of the clipped directional flow. prices are bin midpoints on one shared price grid (min→max of the window's closes split into 20 equal bins), so histograms from different actors/directions are bin-aligned and the composite histogram is the element-wise sum of its component actors' histograms; sum(shares) == meta.total_shares. The histogram key is always present in newly computed payloads (stored evidence lacking the key predates this field and is recomputed at request time). The old V2 chip_price_zones / margin_crowded_zone payload is no longer used for this field.

Support dual-basis note (issue #1253, migration 125): technical.context 在既有 support / resistance(還原基準)旁額外提供 support_raw / resistance_raw / structure_stop_raw(名目基準,來源為 major_support_raw / major_resistance_raw / structure_stop_raw)。既有欄位語意與值不變;兩種基準的差異與適用場景見 §11 支撐族雙基準

Query Parameters:

參數 類型 預設 說明
qryDate string latest YYYY-MM-DD,嚴格同日查詢,不 fallback

Response: StockSignalsResponse

{
  "ticker": "2330.TW",
  "trading_date": "2026-03-28",
  "technical": {
    "signals": [{ "id": "technical.signal.ta_ma_stack_position", "signal_code": "ta_ma_stack_position", "name": "均線排列位階", "display": { "title": "均線排列位階" } }],
    "events": [{ "id": "technical.event.ma_bullish_stack", "event_code": "ma_bullish_stack", "signal_code": "ta_ma_stack_position", "name": "均線多頭排列", "display": { "title": "均線多頭排列" } }],
    "stories": [],
    "relations": [
      {
        "from": { "type": "index", "code": "ma_alignment", "name": "均線排列", "display": { "title": "均線排列" } },
        "to": { "type": "signal", "id": "technical.signal.ta_ma_stack_position", "name": "均線排列位階" },
        "relation": "supports",
        "role": "primary_index",
        "condition": "ma_alignment present",
        "required": true,
        "weight": 0.5
      },
      {
        "from": { "type": "signal", "id": "technical.signal.ta_ma_stack_position", "name": "均線排列位階" },
        "to": { "type": "event", "id": "technical.event.ma_bullish_stack", "name": "均線多頭排列" },
        "relation": "triggers",
        "role": "source_signal",
        "condition": "ta_ma_stack_position is ready",
        "required": true,
        "weight": 1.0
      }
    ],
    "indexes": {
      "ta_index_groups": { "moving_average_events": { "...": "..." } },
      "technical_indicators": {
        "trend": { "ma_alignment": "bullish_stack", "cti_score": 0.72 },
        "momentum": { "macd_histogram": 2.5 },
        "volatility": { "bb_status": "normal" },
        "volume": { "volume_ratio": 1.35 }
      },
      "indicator_settings": {
        "moving_averages": { "type": "SMA", "periods": [5, 20, 60, 120, 240], "source": "close" }
      }
    },
    "context": {
      "market_summary": { "bias": "bullish", "confidence": 0.75, "ma_alignment": "bullish_stack" },
      "patterns": [
        {
          "name": "DOUBLE_BOTTOM",
          "score": 0.70,
          "reliability": "medium",
          "trigger_price": 110.0,
          "invalidation_price": 95.0,
          "stage": "watch"
        }
      ],
      "support": 875.0,
      "resistance": 925.0,
      "support_raw": 858.0,
      "resistance_raw": 908.0,
      "structure_stop_raw": 858.0
    },
    "risk": {},
    "quality": { "data_quality": "complete", "warnings": [] }
  },
  "chip": {
    "signals": [{ "id": "chip.signal.chip_institutional_flow_intensity", "signal_code": "chip_institutional_flow_intensity", "name": "三大法人流向強度", "display": { "title": "三大法人流向強度" } }],
    "events": [{ "id": "chip.event.flow_volume_aligned_buy", "event_code": "flow_volume_aligned_buy", "signal_code": "chip_institutional_flow_intensity", "name": "量價買盤一致", "display": { "title": "量價買盤一致" } }],
    "stories": [{ "id": "chip.story.accumulation", "story_code": "accumulation", "source_signal_codes": ["chip_inst_flow"], "source_event_codes": ["flow_volume_aligned_buy"], "name": "法人吸籌", "display": { "title": "法人吸籌" } }],
    "relations": [
      {
        "from": { "type": "event", "id": "chip.event.flow_volume_aligned_buy", "name": "量價買盤一致" },
        "to": { "type": "story", "id": "chip.story.accumulation", "name": "法人吸籌" },
        "relation": "supports",
        "role": "source_event",
        "condition": "flow_volume_aligned_buy active",
        "required": true,
        "weight": 1.0
      }
    ],
    "indexes": {
      "raw_chip_summary": { "foreign_flow_zscore": 1.85, "inst_consensus": "bullish" },
      "institutional_flow": { "foreign_net_shares": 1000, "foreign_flow_zscore": 1.85, "inst_consensus": "bullish" },
      "margin": { "margin_balance": 12000, "margin_change_pct": 1.69 },
      "short": { "short_balance": 800, "short_change_pct": -5.88 },
      "ownership": {},
      "cost_zones": {
        "foreign_cost_zone": {
          "exists": true,
          "band": {
            "lower": 2250.94,
            "upper": 2319.49,
            "avg_cost": 2285.21,
            "median_cost": 2282.50,
            "poc_price": 2280.00
          },
          "position": "above",
          "distance_pct": 4.16,
          "penetration_pct": null,
          "confidence": 0.82,
          "flags": [],
          "meta": {
            "flow_sign": "net",
            "positive_flow_days": 8,
            "effective_window_days": 60,
            "net_cumulative_shares": 1840000,
            "total_abs_flow": 3210000,
            "churn_ratio": 0.5732,
            "extreme_day_capped": false,
            "concentration_ratio": 0.18
          }
        },
        "trust_cost_zone": { "exists": true, "band": { "avg_cost": 2239.90, "median_cost": 2240.10, "poc_price": 2238.00 }, "position": "above" },
        "dealer_cost_zone": { "exists": true, "band": { "avg_cost": 2310.98, "median_cost": 2309.75, "poc_price": 2308.50 }, "position": "above" },
        "composite_cost_zone": {
          "exists": true,
          "band": {
            "lower": 2241.56,
            "upper": 2309.83,
            "avg_cost": 2275.70,
            "median_cost": 2273.40,
            "poc_price": 2272.00
          },
          "position": "above",
          "components": ["foreign", "trust", "dealer"],
          "weights": { "foreign": 0.6, "trust": 0.25, "dealer": 0.15 }
        },
        "chip_support_resistance": {
          "active_support": null,
          "secondary_support_candidates": [],
          "primary_sell_pressure_zone": null,
          "primary_buy_pressure_zone": null
        }
      },
      "support_resistance": {
        "active_support": null,
        "secondary_support_candidates": []
      }
    },
    "context": {
      "chip_structure": { "...": "same V3 object as chip.indexes.cost_zones" },
      "chip_leverage": {},
      "chip_state": {},
      "triggered_rules": []
    },
    "risk": { "veto_triggered": false, "veto_reason": null },
    "quality": { "data_quality": "complete", "warnings": [] }
  },
  "financial": {
    "signals": [{ "id": "financial.signal.fin_revenue_growth_momentum", "signal_code": "fin_revenue_growth_momentum", "name": "營收成長動能", "display": { "title": "營收成長動能" } }],
    "events": [{ "id": "financial.event.revenue_acceleration", "event_code": "revenue_acceleration", "signal_code": "fin_revenue_growth_momentum", "name": "營收成長加速", "display": { "title": "營收成長加速" } }],
    "stories": [{ "id": "financial.story.earnings_acceleration", "story_code": "earnings_acceleration", "source_signal_codes": ["fin_growth_quality"], "source_event_codes": ["revenue_acceleration"], "name": "獲利加速", "display": { "title": "獲利加速" } }],
    "relations": [
      {
        "from": { "type": "event", "id": "financial.event.revenue_acceleration", "name": "營收成長加速" },
        "to": { "type": "story", "id": "financial.story.earnings_acceleration", "name": "獲利加速" },
        "relation": "supports",
        "role": "source_event",
        "condition": "revenue_acceleration active",
        "required": true,
        "weight": 1.0
      }
    ],
    "indexes": {
      "growth_score": 0.65,
      "valuation_score": 0.55,
      "valuation_regime": "fair",
      "revenue": { "revenue_yoy_pct": 18.5, "revenue_mom_pct": 4.2 },
      "profitability": { "eps_latest": 8.2, "roe_annualized": 28.0 },
      "valuation": { "pe_ratio": 18.2, "pb_ratio": 4.8, "valuation_regime": "fair" },
      "industry": {}
    },
    "context": {
      "financial_matrix": { "position": "bullish", "final_score": 0.62 },
      "growth_dimension": { "score": 0.65, "signal_type": "growth_moderate" },
      "valuation_dimension": { "score": 0.55, "signal_type": "fair" },
      "industry_comparison": {},
      "price_context": { "closing_price": 900.0 }
    },
    "risk": { "veto_triggered": false, "veto_reason": null },
    "quality": { "data_quality": { "status": "complete" }, "warnings": [] }
  },
  "technical_signals": {
    "trading_date": "2026-03-28",
    "signal_type": "ta_ma_stack_position",
    "bias": "bullish",
    "confidence": 0.75,
    "patterns": [
      {
        "name": "DOUBLE_BOTTOM",
        "score": 0.70,
        "reliability": "medium",
        "trigger_price": 110.0,
        "invalidation_price": 95.0,
        "stage": "watch"
      }
    ],
    "support": 875.0,
    "resistance": 925.0,
    "support_raw": 858.0,
    "resistance_raw": 908.0,
    "structure_stop_raw": 858.0,
    "structured_signals": [
      {
        "signal_code": "ta_ma_stack_position",
        "event_code": "ma_bullish_stack",
        "direction": "bullish",
        "confidence": 0.75,
        "source": "signals_events_projection"
      }
    ],
    "signal_summary": {
      "count": 1,
      "source": "signals_events_projection"
    },
    "ta_index_groups": { "moving_average_events": { "...": "..." } },
    "signals": [{ "id": "technical.signal.ta_ma_stack_position", "signal_code": "ta_ma_stack_position" }],
    "events": [{ "id": "technical.event.ma_bullish_stack", "event_code": "ma_bullish_stack", "signal_code": "ta_ma_stack_position" }],
    "stories": [],
    "relations": [
      {
        "from": { "type": "signal", "id": "technical.signal.ta_ma_stack_position" },
        "to": { "type": "event", "id": "technical.event.ma_bullish_stack" },
        "relation": "triggers",
        "role": "source_signal",
        "condition": "ta_ma_stack_position is ready",
        "required": true,
        "weight": 1.0
      }
    ],
    "reasons": ["MA_BULLISH_STACK"],
    "warnings": []
  },
  "chip_signals": {
    "trading_date": "2026-03-28",
    "chip_signal_type": "chip_accumulation",
    "bias": "bullish",
    "confidence": 0.72,
    "story_type": "accumulation",
    "role": "leading",
    "story_reasons": ["foreign_sustained_buy"],
    "action_hints": ["hold_or_add"],
    "foreign_flow_zscore": 1.85,
    "inst_consensus": "bullish",
    "signals": [{ "id": "chip.signal.chip_inst_flow", "signal_code": "chip_inst_flow" }],
    "events": [{ "id": "chip.event.flow_volume_aligned_buy", "event_code": "flow_volume_aligned_buy", "signal_code": "chip_inst_flow" }],
    "stories": [{ "id": "chip.story.accumulation", "story_code": "accumulation", "source_signal_codes": ["chip_inst_flow"], "source_event_codes": ["flow_volume_aligned_buy"] }],
    "relations": [
      {
        "from": { "type": "event", "id": "chip.event.flow_volume_aligned_buy" },
        "to": { "type": "story", "id": "chip.story.accumulation" },
        "relation": "supports",
        "role": "source_event",
        "condition": "flow_volume_aligned_buy active",
        "required": true,
        "weight": 1.0
      }
    ],
    "reasons": ["FOREIGN_NET_BUY"],
    "warnings": []
  },
  "financial_signals": {
    "trading_date": "2026-03-28",
    "bias": "bullish",
    "confidence": 0.68,
    "growth_signal": "growth_moderate",
    "valuation_regime": "fair",
    "matrix_position": "bullish",
    "story_type": "earnings_acceleration",
    "role": "supporting",
    "story_reasons": ["revenue_acceleration"],
    "action_hints": ["growth_intact"],
    "signals": [{ "id": "financial.signal.fin_growth_quality", "signal_code": "fin_growth_quality" }],
    "events": [{ "id": "financial.event.revenue_acceleration", "event_code": "revenue_acceleration", "signal_code": "fin_growth_quality" }],
    "stories": [{ "id": "financial.story.earnings_acceleration", "story_code": "earnings_acceleration", "source_signal_codes": ["fin_growth_quality"], "source_event_codes": ["revenue_acceleration"] }],
    "relations": [
      {
        "from": { "type": "event", "id": "financial.event.revenue_acceleration" },
        "to": { "type": "story", "id": "financial.story.earnings_acceleration" },
        "relation": "supports",
        "role": "source_event",
        "condition": "revenue_acceleration active",
        "required": true,
        "weight": 1.0
      }
    ],
    "reasons": ["REVENUE_YOY_STRONG"],
    "warnings": []
  }
}

Example note: some signal_code / story_code values above (e.g. accumulation, chip_inst_flow, fin_growth_quality) are illustrative placeholders and are not guaranteed to resolve via the config/sesi_names.yaml registry; the name shown for them is representative only. In real responses each code is a live SESI code whose name comes from the registry.

qryDate 語義: - 未帶:各面向查各自最新 trading_date(可能不同日) - 有帶:嚴格同日查詢,不 fallback 到其他日期 - 三面向全無資料:404 - 指定日某面向無資料:該面向回 null - 每個面向物件都使用 replacement signals / events / stories / relations。Deprecated aliases(如 signal_typestory_typestructured_signals)只由 replacement arrays projection 產生。 - technical / chip / financial 是正式 domain payload。technical_signals / chip_signals / financial_signals 只保留為 deprecated compatibility aliases,內容必須由正式 payload projection 產生。 - stories[] 含衝突組的敗方。 每個 domain 的 stories[] 依引擎排序(priority 優先,同 priority 再比 score)取前 N 筆(ses_display.story_top_n_per_domain,technical/chip/financial 各 3、cross_domain 2),不再依 suppressed_by 剔除。同一衝突組的主/從 story 因此可能同時出現。 - suppressed_by 保留在 story 物件上,值為壓過它的 story code(未被壓則為 null)。要不要在 UI 收合由前端決定。 - 此行為與 GET /api/signals/stories/latest 一致——該端點本就顯示同組全部 story。先前兩條路徑對同一筆 ledger 資料的呈現不同(首頁顯示、個股頁隱藏)。

PatternItem.stage: - "watch" — 型態形成中(如 DOUBLE_BOTTOM) - "confirmed" — 型態已確認(如 DOUBLE_TOP、HEAD_SHOULDERS、w_bottom_breakout) - null — 無階段區分

Error Responses: - 404: {"detail": "No signal data for {ticker}"}{"detail": "No signal data for {ticker} on {qryDate}"} - 422: {"detail": "Invalid date format. Expected YYYY-MM-DD."}


15. Agent Payloads

這些 endpoint 回傳原始 agent 計算結果,供 MAS 系統或前端進階展示使用。

GET /api/agent/technical/{ticker}

技術面 agent payload。回傳含 scoring、quality flags、penalties、evidence 的結構化資料。

meta 識別欄位同 /api/daily-analysis/{ticker}/latest(issue #571):新增 meta.trading_date,移除 meta.schema_version / meta.timestamp / meta.exchange_tz

technical_evidence 會同步投影 daily-analysis payload 的 replacement evidence: - ta_index_groups: 同 /api/daily-analysis/{ticker}/latest 的 grouped TA index payload。 - signals / events / stories / relations: 正式 replacement arrays。 - structured_signals / signal_summary: deprecated compatibility alias,來源為 signals / events projection。

支撐族雙基準(issue #1253,migration 125):語意見 §11 支撐族雙基準。本 endpoint 在既有還原基準欄位旁額外 提供 7 個名目基準欄位,既有欄位值與名稱不變:

位置 新增欄位 對應快照欄
context resistance_price_raw major_resistance_raw
context support_20d_price_raw / support_60d_price_raw low_20d_min_raw / low_60d_min_raw
context distance_to_support_20d_pct_raw / _60d_pct_raw / _20d_price_raw / _60d_price_raw 同名 _raw
technical_evidence.price_action distance_to_support_20d_pct_raw / _60d_pct_raw 同名 _raw

scoring、quality flags、penalties 一律沿用還原基準,不受這些欄位影響。

GET /api/agent/chip/{ticker}

籌碼面 agent payload。回傳法人流向、融資券壓力、籌碼證據。

Query Params:

名稱 類型 說明
source_window string 2y(預設)/ all,控制 triggered_rules 回傳對應回測窗口的策略匹配;見 §1 通用說明
cost_window_days integer 法人成本區計算視窗,單位為交易日;預設 60,允許 20252

Replacement 變更: - 新增 triggered_rules: 當天觸發的回測驗證規則,每筆含 metrics_2y + metrics_all 8 項指標(hit_rate, mean_return, expectancy, profit_factor 等)+ support_count - 新增 raw_chip_summary: 原始籌碼資料摘要(z-score, streak, consensus, margin 等事實數據) - meta.cost_window_days: 回傳本次成本區實際採用的交易日視窗。 - context.chip_structure.*_cost_zone: 保留既有外層 exists / band / position(net 視角)並新增: - buy_cost_zone: 只用買超日計算的買入成本區。 - sell_cost_zone: 只用賣超日絕對值計算的賣出成本區。 - net_cost_zone: 舊版 signed net cost zone 的明確巢狀版;若 status=distortedreason=invalid_net_cost,應改讀 buy/sell 兩側成本,不再把外層 position=unknown 解讀成無資料。 - buy_cost_zone.histogram / sell_cost_zone.histogram(v1.6.0): { "prices": number[20], "shares": int[20] }null(無該方向流量、或視窗內收盤全平價)。prices 為視窗收盤 min→max 等分 20 桶的桶中點(所有 actor / 方向共用同一 grid,composite histogram = 各 actor histogram 逐桶相加);shares 為 clip 後的方向性流量股數,sum(shares) == meta.total_shares。新產出的 payload 一律帶此 key;儲存的舊 evidence 缺 key 時會於 request-time 重算。 - signals / events / stories / relations: 正式 replacement arrays。 - story_typestory_reasonschip_story 是 deprecated compatibility alias,來源為 stories projection。 - 移除 top-level schema_version / exchange_tz / timestamp(issue #571,原為每列常數);快照所屬交易日請讀 trading_date

GET /api/agent/financial/{ticker}

財務面 agent payload。回傳成長維度、估值維度、矩陣位置、故事。

valuation_dimension.components.valuation 內含 dividend 區塊(與 /api/fundamentals/dividend-events 同一 v2 enum shape): { "events": DividendEvent[], "next_ex_dividend_date": date|null, "next_ex_rights_date": date|null }。 個股頁一次 fetch 即取得除權息時間軸;無資料時 events 為空陣列。

Top-level replacement fields: - signals: array<object> - events: array<object> - stories: array<object> - relations: array<object>

financial_story 是 deprecated compatibility alias,來源為 stories projection。

移除 top-level schema_version(issue #571,原為每列常數);快照所屬交易日請讀 trading_date

GET /api/agent/fundamental/{ticker}

基本面 agent(尚未實作,回傳 "not_ready")。


16. 自選股 Watchlist

Prefix: /api/watchlists — 全部需認證

GET /api/watchlists/groups

列出所有自選股群組。

Response:

[
  {
    "id": "uuid-string",
    "name": "我的觀察清單",
    "display_order": 0
  }
]

POST /api/watchlists/groups

建立自選股群組。

Request Body:

{
  "name": "科技股",            // 1-100 字元
  "display_order": 0           // optional, default 0
}

GET /api/watchlists/groups/{group_id}/items

取得群組內的股票列表。

Query Params:

名稱 類型 預設 說明
market string taiwan OHLC 行情市場
trade_date date latest OHLC 行情查詢日期上限
include_price bool true 是否回傳 items[].pricefalse 時省略

Response:

[
  {
    "id": "uuid-string",
    "group_id": "uuid-string",
    "ticker": "2330.TW",
    "display_order": 0,
    "note": "主力持股",
    "added_at": "2024-03-15T10:00:00",
    "latest_ohlc": {
      "date": "2026-05-14",
      "open": 850.0,
      "high": 860.0,
      "low": 845.0,
      "close": 855.0,
      "volume": 25000000
    },
    "price": {
      "as_of_date": "2026-05-14",
      "close": 855.0,
      "change": 15.0,
      "change_pct": 1.7857,
      "five_day_close": 800.0,
      "five_day_change": 55.0,
      "five_day_change_pct": 6.875,
      "twenty_day_close": 780.0,
      "twenty_day_change": 75.0,
      "twenty_day_change_pct": 9.6154
    }
  }
]

latest_ohlc 若無完整 OHLC 資料則為 nullprice 若無最新 close 資料則為 null;若只有部分回看資料不足,price 內對應欄位為 null


POST /api/watchlists/groups/{group_id}/items

新增股票到群組。

Request Body:

{
  "ticker": "2330.TW",        // 1-20 字元
  "display_order": 0,          // optional
  "note": "觀察中"             // optional
}

DELETE /api/watchlists/groups/{group_id}

刪除群組。

DELETE /api/watchlists/groups/{group_id}/items/{ticker}

從群組移除股票。

GET /api/watchlists/membership/{ticker}

查詢某股票屬於哪些群組。

GET /api/watchlists/groups/{group_id}/advice

觀察清單版每日操作建議(不使用成本資訊)。對群組內每檔個股回傳 consider_buy(考慮買進)或 watch(持續觀察),由規則引擎依技術/籌碼/財務三維快照計算 composite 分數,疊加主動式 ETF 動向與大盤 regime 守門(bear_stress / range_stress 時禁止 consider_buy)。權重與門檻externalized 於 config/core.yaml advice: 區塊。

  • 群組不屬於當前 member → 404
  • 非台股 ticker(無 .TW / .TWO 後綴)→ 固定 watch,evidence unsupported_market(v1 僅支援台股)。
  • 任一維度缺快照 → 權重重新正規化並在 evidence 標註 missing_data,不回 500。

Response

{
  "advices": [
    {
      "ticker": "2330.TW",
      "action": "consider_buy",
      "composite": 0.72,
      "scores": {"technical": 0.8, "chip": 0.6, "financial": 0.5},
      "evidence": [{"rule": "composite_add", "value": {"composite": 0.72}}],
      "strategies": ["trend_follow"],
      "close": 1085.0,
      "as_of": {"technical": "2026-07-15", "chip": "2026-07-15", "financial": "2026-07-15"}
    }
  ],
  "market": {"regime_label": "bull_normal", "index_close": 23500.5, "evidence": []},
  "as_of": {"market": "2026-07-15", "market_chip": "2026-07-15", "market_regime": "2026-07-15", "etf": "2026-07-15"},
  "disclaimer": "僅供參考,不構成投資建議"
}

17. 投資組合 Portfolio

Prefix: /api/portfolio — 除 GET /api/portfolio/config 外全部需認證

GET /api/portfolio/open-lots

查詢某 ticker 的未關閉買入批次,給賣出表單的批次選擇器使用。依 FIFO 順序 (transaction_date ASC, created_at ASC, id ASC) 回傳。

Query Parameters: - ticker (必填): 股票代碼,會自動轉大寫。

Response: List[OpenLot]

[
  {
    "buy_transaction_id": "uuid",
    "transaction_date": "2026-04-01",
    "buy_price_per_share": 800.0,        // 原始買入價
    "effective_cost_per_share": 800.1,   // (price*qty + fee + tax) / qty
    "original_qty": 1000,
    "remaining_qty": 700                 // original_qty - 已被 closures 扣除
  }
]

Semantics: - 只回傳 remaining_qty > 0 的買入;已全關的 lot 不出現。 - 前端用 remaining_qty 判斷可賣量,用 effective_cost_per_share 顯示成本基準。 - 沒有任何開放 lot 時回傳 [],不是 404。

GET /api/portfolio/config

回傳前端渲染交易表單時需要的系統設定(目前僅包含台股證交稅率)。無需認證。值由 config/core.yamlportfolio.tax_rates 決定,前端不得 hardcode 稅率。

Response:

{
  "standard_sell_rate": 0.003,    // 一般現股賣出證交稅 0.3%
  "day_trade_sell_rate": 0.0015   // 當沖賣出優惠證交稅 0.15%
}

Invariants: - 0 < day_trade_sell_rate < standard_sell_rate < 1 - 兩個值型別皆為 float

POST /api/portfolio/day-trades

建立一筆當沖交易(day-trade pair)。Backend 原子地寫入 2 筆 transactions (buy leg + sell leg) 與 1 筆 transaction_closures 建立兩者的關聯,全部共用同一個 day_trade_pair_id

注意: 當沖是產品層的簡化型別,不是原始成交流水。每一組買賣為一筆配對紀錄,不支援分批成交或券商匯入對帳。券商匯入走 POST /api/portfolio/transactions 的一般買/賣路徑。

Request Body:

{
  "ticker": "2330.TW",
  "transaction_date": "2026-04-10",     // YYYY-MM-DD 或 YYYYMMDD
  "direction": "long_first",            // long_first (先買後賣現股) | short_first (先賣後買融券)
  "buy_price": 800.0,                   // > 0
  "sell_price": 810.0,                  // > 0
  "quantity": 1000,                     // > 0
  "fee_buy": 0,                         // >= 0, default 0
  "fee_sell": 0,                        // >= 0, default 0
  "tax_buy": 0,                         // >= 0, default 0
  "tax_sell": null,                     // null → 自動套用 day_trade_sell_rate;0 視為使用者明確設定不覆寫
  "note": "optional",
  "broker_id": "optional-uuid"
}

Response:

{
  "pair_id": "uuid",
  "buy_transaction_id": "uuid",
  "sell_transaction_id": "uuid",
  "closure_id": "uuid",
  "realized_pnl": 9835.0,               // (sell - buy) * qty - fee_buy - fee_sell - tax_buy - tax_sell
  "tax_sell_applied": 165.0             // 實際套用的賣出證交稅(自動或使用者指定)
}

Semantics: - direction = long_first → backend 先 INSERT buy leg,再 INSERT sell leg(best-effort,兩筆 INSERT 可能落在同一個 CURRENT_TIMESTAMP tick 上)。Leg 身份以 day_trade_direction 欄位為準,不應依賴 created_at 排序。 - direction = short_first → 先 INSERT sell leg,再 INSERT buy leg(同上 best-effort 說明)。 - tax_sell 省略或設為 null → 自動計算 sell_price * quantity * day_trade_sell_rate(見 GET /api/portfolio/config)。 - tax_sell 明確設為 0 → 以使用者設定為準,不自動覆寫。 - 當沖的 sell_type 固定為 現股(融券當沖尚未支援)。 - 三筆 DB INSERT 在單一 transaction 中執行,全部成功才 commit。任何一步失敗整組 rollback。

PUT /api/portfolio/day-trades/{pair_id}

部分更新一組當沖配對的兩筆 transactions 與 closure。所有欄位都是 optional;未提供的欄位保留原值。

Request Body (all optional, extra fields rejected):

{
  "buy_price": 810.0,
  "sell_price": 820.0,
  "quantity": 500,
  "transaction_date": "2026-04-10",
  "fee_buy": 0,
  "fee_sell": 0,
  "tax_buy": 0,
  "tax_sell": null,
  "note": "更正",
  "broker_id": "uuid"
}

Response:

{
  "pair_id": "uuid",
  "buy_transaction_id": "uuid",
  "sell_transaction_id": "uuid",
  "realized_pnl": 4910.0,
  "tax_sell_applied": 615.0
}

Semantics: - 若 buy_price / sell_price / quantity 任一變動且 tax_sell 未提供 → 依新值與 day_trade_sell_rate 自動重算。 - 若 tax_sell 明確傳入(含 0)→ 以使用者為準。 - 若都沒有變動 price/qty 相關欄位且 tax_sell 未傳 → 保留原 tax_sell。 - quantity 變動會同步更新 transaction_closures.qty。 - 不允許修改 direction:想改方向請 delete 再 create。送 direction 欄位會觸發 422。 - pair 不存在 → 404。pair 結構異常(只找到一 leg)→ 500(資料不一致,需人工介入)。

DELETE /api/portfolio/day-trades/{pair_id}

刪除一組當沖配對。端到端在單一 transaction 中執行:先刪 closure,再刪兩筆 transactions,最後 commit。

Response: {"ok": true}

Errors: - 404 → pair 不存在或不屬於當前使用者。

GET /api/portfolio/brokers

列出券商。

POST /api/portfolio/brokers

新增券商。

Request Body:

{
  "name": "元大證券",           // 1-100 字元
  "fee_rate": 0.001425,        // 手續費率 (≥0)
  "fee_discount": 0.6,         // 折扣 (≥0), default 1.0
  "is_default": false,          // default false
  "note": "網路下單 6 折"       // optional
}

PUT /api/portfolio/brokers/{broker_id}

更新券商(同 POST schema)。


POST /api/portfolio/transactions

建立交易紀錄。

Request Body:

{
  "ticker": "2330.TW",
  "action": "buy",              // "buy" | "sell"
  "sell_type": null,            // optional, required when action="sell"
  "price_per_share": 850.0,     // > 0
  "quantity": 1000,             // > 0
  "fee": 121,                   // default 0
  "tax": 0,                     // default 0
  "transaction_date": "2024-03-15",
  "note": "定期買入",            // optional
  "broker_id": "uuid-string",   // optional
  "closes_lots": [              // optional for sell (see below)
    {"buy_transaction_id": "uuid", "qty": 500}
  ]
}

Response:

{
  "id": "uuid",
  "warning": {
    "transaction_id": "uuid",
    "code": "unmatched_sell_lot",
    "severity": "warning",
    "message": "Insufficient open lots: need 100, available 0",
    "item": {
      "ticker": "3324.TWO",
      "action": "sell",
      "transaction_date": "2026-04-29",
      "quantity": 100
    }
  }
}

warning 只會在 sell 未指定 closes_lots 且 FIFO open lots 不足時出現;一般成功建立仍只回 { "id": "uuid" }

closes_lots 規範: - closes_lots 只能出現在 action="sell" 的請求,buy + closes_lots → 400。 - Sell 可以省略 closes_lots,backend 會依 FIFO(transaction_date, created_at, id ASC)自動配對當前使用者在同一 ticker 的未關閉買入批次。 - 若顯式指定 closes_lots,每筆 lot 的 buy_transaction_id 必須指向當前使用者、同一 ticker、action='buy' 的未關閉 lot。 - 禁止同一個 buy_transaction_id 在 closes_lots 裡出現兩次(400 duplicate)。 - 顯式 closes_lotssum(closes_lots[*].qty) 必須等於 request 的 quantity,否則 400 sum mismatch。 - 每筆 lot 的 qty 不能超過該 lot 的 remaining_qty(原始 quantity - 已被關閉的 qty)。 - 若未指定 closes_lots 且使用者沒有足夠的 open lot(總可用 < quantity),仍會建立 sell transaction,但不建立 closure,並回傳 warning.code = "unmatched_sell_lot" 供前端提示使用者。該 sell 不會產生成本、realized PnL 或 unrealized PnL;portfolio 的成本與績效只從可配對的 open lots / positive holdings 計算。 - Validate-first: 顯式 closes_lots 的檢查全部在任何 INSERT 之前進行;任一失敗 → DB 完全不變。 - 所有 INSERT(transaction + N × closure)在單一 DB session,單次 commit。

POST /api/portfolio/transactions/batch

批次新增交易紀錄。此 endpoint 供匯入流程一次送多筆交易,缺少 open lots 的 sell 與單筆建立採相同 warning/anomaly 規則。

Request Body:

{
  "transactionList": [
    {
      "ticker": "2330.TW",
      "action": "buy",
      "sell_type": null,
      "price_per_share": 850.0,
      "quantity": 1000,
      "fee": 121,
      "tax": 0,
      "transaction_date": "2024-03-15",
      "note": "券商匯入",
      "broker_id": null
    }
  ]
}

Response:

{
  "created_count": 1,
  "transactions": [{"index": 0, "id": "uuid"}],
  "skipped_count": 0,
  "skipped_duplicates": [],
  "anomaly_count": 0,
  "anomalies": []
}

批次新增規範: - transactionList 至少 1 筆、最多 100 筆;server 會先依 transaction_date 排序,同日 buy 先於 sell,response index 仍指向原 request index。 - 每筆 item 欄位與單筆 POST /api/portfolio/transactions 相同。 - 非重複資料以 request item 的值寫入;DB 既有資料只作為 duplicate / validation 依據,不會覆蓋 request。 - Duplicate 判斷:同會員、同 transaction_date 只是候選範圍;還必須 tickeractionsell_typeprice_per_sharequantityfeetax 都在合理 tolerance 內才是 duplicate 候選。price_per_share 允許極小 numeric tolerance(<= 0.000001);fee / tax 允許 1 元內券商匯入 rounding drift。broker_id 相同者優先 match;若沒有相同 broker_id、但忽略 broker_id 後只有一筆候選,也會視為 retry duplicate 並 skip。 - note 不納入 duplicate signature,避免匯入註記文字不同造成重複漏排。 - 同一個 request 內後續 item 與前面已新增或已 match 的 signature 相同,也會 skip。 - Duplicate skip 不是錯誤;response 以 skipped_count / skipped_duplicates 回報。 - 匯入檔可能不是完整成本基礎。Batch item 是 sell、未提供 closes_lots、且 FIFO open lots 不足時,endpoint 仍會建立 sell transaction,但不建立 closure,並以 anomaly_count / anomalies 回報 unmatched_sell_lot。該 sell 不會產生成本、realized PnL 或 unrealized PnL;portfolio 的成本與績效只從可配對的 open lots / positive holdings 計算。 - anomalies[] 欄位包含 indextransaction_idcodeseveritymessageitemitem 會回傳原 request index、ticker、action、transaction_date、quantity,供前端告知使用者。 - 明確提供 closes_lots 時仍採 validate-first。任一 validation / explicit closure 錯誤時,整批 rollback。


GET /api/portfolio/transactions

列出交易紀錄。每筆 row 回傳的欄位包含 day_trade_pair_idday_trade_direction(若 row 屬於某組當沖配對),前端可依此渲染配對標記並禁用單筆編輯(backend 的 pair guard 亦會拒絕這類 PUT)。

Query Parameters

Field Type Required Default Notes
limit integer no portfolio.transaction_list.default_limit 回傳筆數;backend clamp 到設定的最大值。
ticker string no null 篩選單一個股交易列表;backend 會正規化為大寫,例如 2330.tw2330.TW 查詢。

範例:

GET /api/portfolio/transactions?ticker=2330.TW&limit=100

不帶 ticker 時維持既有全交易列表 response shape;指定 ticker 且無交易時回傳空陣列 []

Response (per row):

{
  "id": "sell-warning",
  "ticker": "2330.TW",
  "action": "sell",
  "sell_type": "現股",
  "price_per_share": 800.0,
  "quantity": 1000,
  "fee": 0.0,
  "tax": 0.0,
  "transaction_date": "2026-04-01",
  "note": null,
  "broker_id": null,
  "broker_name": null,
  "day_trade_pair_id": null,
  "day_trade_direction": null,
  "is_warning": true,
  "warning_code": "unmatched_sell_lot",
  "warning_reason": "Sell quantity does not have full matching buy lots; this warning transaction is excluded from holdings and PnL.",
  "matched_quantity": 0,
  "unmatched_quantity": 1000,
  "warning_details": {
    "sell_quantity": 1000,
    "matched_quantity": 0,
    "unmatched_quantity": 1000
  }
}

Semantics(is_warning / warning_code / warning_reason / matched_quantity / unmatched_quantity / warning_details): - 這六個欄位只對 action="sell" 的 row 有意義;action="buy" 的 row 全部固定回傳 is_warning=false、其餘五個欄位皆為 null。 - matched_quantity:該筆 sell 在 transaction_closures 中被關閉的總量(COALESCE(SUM(qty), 0));buy row 為 null。 - unmatched_quantitymax(quantity - matched_quantity, 0);buy row 為 null。 - is_warningaction="sell"unmatched_quantity != 0(完全關閉的 sell、或 buy,皆為 false)。 - warning_code / warning_reasonis_warning=true 時固定為 "unmatched_sell_lot" 與上例的固定文字;否則皆為 null。此 warning 意義與 POST /api/portfolio/transactions 回傳的巢狀 warning(見上方,同樣使用 code="unmatched_sell_lot")相同,但這裡是攤平在 row 上的獨立表示法,非 schema 不一致。 - warning_detailsis_warning=true 時回傳 {sell_quantity, matched_quantity, unmatched_quantity} 快照;否則為 null。 - Warning row(is_warning=true)不產生 cost basis、realized PnL 或 unrealized PnL,會被排除在 holdings 與 PnL 計算之外(呼應 warning_reason 內文)。

PUT /api/portfolio/transactions/{transaction_id}

更新交易紀錄(同 POST schema,除 ticker 外)。支援 closes_lots 重新配對(validate-first-replace):

  • 若 request 帶 closes_lots,backend 先驗證新 closures(以「排除本 sell 自己既有 closures」後的 open_lots 視角),通過後在單一 session 內:DELETE FROM transaction_closures WHERE sell_transaction_id=idINSERT 新 closures → UPDATE transactions → 單次 commit。
  • 若 request 不帶 closes_lots,沿用舊行為只更新 transaction 欄位,closures 不動。
  • closes_lots 的 400 錯誤條件同 POST。

注意: 若目標 transaction 屬於某個當沖配對(day_trade_pair_id 不為 NULL),回傳 400,要求呼叫 PUT /api/portfolio/day-trades/{pair_id} 進行配對編輯。此 pair guard 避免半配對狀態(只改其中一 leg 會讓 closure 失去一致性)。

PUT /api/portfolio/transactions/batch

批次更新交易紀錄。每筆 item 需要帶 id 與完整 update 欄位;語意與單筆 PUT /api/portfolio/transactions/{transaction_id} 相同。

Request Body:

{
  "transactionList": [
    {
      "id": "uuid",
      "action": "buy",
      "sell_type": null,
      "price_per_share": 855.0,
      "quantity": 1000,
      "fee": 121,
      "tax": 0,
      "transaction_date": "2024-03-15",
      "note": "券商匯入修正",
      "broker_id": null
    }
  ]
}

Response:

{
  "updated_count": 1,
  "transactions": [{"index": 0, "id": "uuid"}]
}

批次更新規範: - transactionList 至少 1 筆、最多 100 筆;同一 id 重複出現 → 400。 - Update item 使用全量欄位,不是 partial patch;未帶 required 欄位會回 422。 - 欄位值以 request item 為準;DB 既有 row 只用於存在性、member ownership、pair guard 與 closure validation。 - closes_lots 行為同單筆 PUT:帶入時 validate-first-replace。 - 任一 item 不存在、不屬於目前 member、pair guard 失敗、或 validation 失敗時,整批 rollback。

DELETE /api/portfolio/transactions/{transaction_id}

刪除交易紀錄。

注意: 若目標 transaction 屬於某個當沖配對(day_trade_pair_id 不為 NULL),回傳 400,要求呼叫 DELETE /api/portfolio/day-trades/{pair_id} 進行配對刪除。此 pair guard 避免單腳刪除導致另一 leg 與 closure 關聯孤立。

DELETE /api/portfolio/transactions/batch

批次刪除交易紀錄。

Request Body:

{
  "transactionIds": ["uuid-1", "uuid-2"]
}

Response:

{
  "deleted_count": 2,
  "transaction_ids": ["uuid-1", "uuid-2"]
}

批次刪除規範: - transactionIds 至少 1 筆、最多 100 筆;同一 id 重複出現 → 400。 - 刪除只接受 transaction id,不用日期/價格/數量推測,以避免同日多筆交易時誤刪。 - 任一 id 不存在、不屬於目前 member、或 pair guard 失敗時,整批 rollback。 - 若目標 transaction 屬於當沖配對,回 400,要求呼叫 DELETE /api/portfolio/day-trades/{pair_id}


GET /api/portfolio/positions/{ticker}

取得個股持倉。

GET /api/portfolio/positions

取得所有持倉。

GET /api/portfolio/holdings

取得持股明細(含成本資訊)。每筆 row 包含 realized_pnlday_trade_realized_pnltotal_realized_pnlunrealized_pnlavg_buy_pricecost_basis_total 等欄位。語意參見下方 Metrics Naming Specification。

GET /api/portfolio/advice

持股版每日操作建議(使用真實成本)。對每檔未平倉持股(position_qty > 0,已平倉部位排除)回傳一個 action + 逐條 evidence:

action 觸發條件(first-match 順序)
stop_loss 高點回檔 ≥ 30%(drawdown_stop_loss_pct),或(use_invalidation_stop 開啟時)收盤 < invalidation_level(缺值 fallback structure_stop
take_profit 未實現獲利 ≥ 20%(take_profit_pnl_pct composite ≤ 轉弱門檻
reduce 高點回檔 ≥ 15%(drawdown_reduce_pct
add composite ≥ 加碼門檻 且 大盤 regime 放行 且 距 major_resistance 有足夠空間
hold 其餘
  • 高點定義:自進場(最早未平倉 lot 的 transaction_date)以來 stock_ohlc.adj_close還原價)最高收盤價,與最新收盤取 max;回檔 = (peak − close) / peak
  • 還原價基準(issue #1253):高點自 #1253 起改讀 adj_close。原本的分段峰值(公司行動修正) workaround 已退役——該 workaround 是為了原始價序列在分割/大額除權當日的斷崖(會把獲利部位誤判成 stop_loss,issue #941);還原序列本身沒有斷崖,且在還原基準上僅存的單日暴跌都是真實崩跌,再做分段只會把該報的回撤藏起來。連帶移除:evidence 規則 corporate_action_adjusted(含 peak_basis_datecliff_day_drop_ratio)不再出現;drawdown_cliff_ratio / drawdown_cliff_max_gap_days 不再影響高點。無還原價的 K 棒直接略過、不以原始價替代(混基準的高點比沒有高點更糟);個股若尚無任何還原價資料則無高點,回 missing_data
  • 優先序為刻意設計:composite 強多但回檔落在 [15%, 30%) 區間時 action 為 reduceadd(保護獲利優先於訊號強度),evidence 同時附 drawdown 與 composite 值。
  • composite 計算、ETF 動向疊加、大盤守門、非台股與缺資料降級行為同觀察清單版(見 §16)。
  • 所有權重/門檻在 config/core.yaml advice: 區塊。

Response:結構同觀察清單版,每筆 advice 額外含 position_qtyavg_buy_price(含手續費成本)、unrealized_pnl_pctdrawdown_pct

GET /api/portfolio/allocation

依股票分類彙總目前仍持有的部位(position_qty > 0),用於呈現類股投資總額與配置比例。

Query Parameters:

Field Type Required Default Notes
type Group | ETF no Group Group 依產業分類;ETF 依 ETF/個股分類。

分類規則: - type=Group:優先使用 stocks.industry,與既有族群/產業分析的 industry-first 慣例一致。 - type=Groupindustry 缺失時,改用 stocks.sector。 - type=Groupstocks 查無資料或 industry / sector 都是空值,歸入 Unclassified。 - type=ETF:使用 etf_funds.etf_ticker 判斷 ETF,回傳 etfstock 兩種 bucket。Response format 不變。

Response:

{
  "total_market_value": 11500.0,
  "total_cost_basis": 11000.0,
  "groups": [
    {
      "category": "Semiconductors",
      "category_source": "industry",
      "market_value": 10500.0,
      "cost_basis_total": 10000.0,
      "allocation_pct": 91.3043,
      "holdings_count": 2,
      "tickers": ["2330.TW", "8299.TWO"]
    },
    {
      "category": "Insurance",
      "category_source": "industry",
      "market_value": 1000.0,
      "cost_basis_total": 1000.0,
      "allocation_pct": 8.6957,
      "holdings_count": 1,
      "tickers": ["2881.TW"]
    }
  ]
}

type=ETF 範例:

{
  "total_market_value": 4500.0,
  "total_cost_basis": 4100.0,
  "groups": [
    {
      "category": "stock",
      "category_source": "asset_type",
      "market_value": 2700.0,
      "cost_basis_total": 2500.0,
      "allocation_pct": 60.0,
      "holdings_count": 2,
      "tickers": ["2330.TW", "8299.TWO"]
    },
    {
      "category": "etf",
      "category_source": "asset_type",
      "market_value": 1800.0,
      "cost_basis_total": 1600.0,
      "allocation_pct": 40.0,
      "holdings_count": 1,
      "tickers": ["0050.TW"]
    }
  ]
}

Semantics: - market_value = market_price * position_qty;若該 ticker 沒有可用市價,使用 cost_basis_total 作為 fallback,避免缺價持股從配置中消失。 - allocation_pctmarket_value / total_market_value * 100 計算,四捨五入到小數第 4 位。 - total_cost_basis 為所有 active holdings 的成本基礎加總,可作為「投入成本」口徑。 - groupsmarket_value DESC, category ASC 排序;每個 group 的 tickers 依 ticker ASC 排序。 - 空投資組合回傳 {"total_market_value": 0.0, "total_cost_basis": 0.0, "groups": []}

GET /api/portfolio/performance

取得目前使用者持股的未實現績效彙總。總未實現損益沿用 open lots 成本基準;day / month / year 一律以目前 close 對該期間開始前最後一個可用交易日 close——分別是前一個交易日、上月最後一個交易日、去年最後一個交易日。若某檔在期間開始之後才上市、期間開始前沒有任何收盤,則退回該期間內最早一個可用交易日 close。*_pnl_pct 單位為百分點(例如 9.09 代表 9.09%)。

Query Parameters:

Field Type Required Default Notes
ticker string no null 股票代碼;未提供時彙總所有 open holdings,提供時只計算該個股。會自動轉大寫。
type Group | ETF no null 新增分組期間績效;Group 依產業分類,ETF 依 ETF/個股分類。不可與 ticker 同時使用。

Request modes:

  • 不帶 ticker / type:回傳整體投資組合期間績效,response shape 維持既有格式。
  • ticker:回傳單一 ticker 期間績效,response shape 維持既有格式。
  • type=Group:回傳產業分組期間績效;分類口徑比照 GET /api/portfolio/allocation?type=Group,優先 stocks.industry,缺失時用 stocks.sector,仍缺失時為 Unclassified
  • type=ETF:回傳 ETF / 個股分組期間績效;分類口徑比照 GET /api/portfolio/allocation?type=ETF,分類為 etf / stock
  • tickertype 同時提供會回 HTTP 422

若某期間任一 open holding 缺少 current 或 baseline close,該期間的 baseline_datebaseline_valueunrealized_pnlunrealized_pnl_pct 皆回傳 null;總未實現欄位仍會依既有 portfolio metrics 行為回傳。

Response:

{
  "as_of_date": "2026-06-22",
  "ticker": null,
  "positions_count": 2,
  "market_value": 2400.0,
  "cost_basis_total": 2000.0,
  "total_unrealized_pnl": 400.0,
  "total_unrealized_pnl_pct": 20.0,
  "periods": {
    "day": {
      "label": "day",
      "baseline_date": "2026-06-19",
      "baseline_value": 2420.0,
      "unrealized_pnl": -20.0,
      "unrealized_pnl_pct": -0.826446
    },
    "month": {
      "label": "month",
      "baseline_date": "2026-05-29",
      "baseline_value": 2200.0,
      "unrealized_pnl": 200.0,
      "unrealized_pnl_pct": 9.090909
    },
    "year": {
      "label": "year",
      "baseline_date": "2025-12-31",
      "baseline_value": 1700.0,
      "unrealized_pnl": 700.0,
      "unrealized_pnl_pct": 41.176471
    }
  }
}

單一個股範例:

GET /api/portfolio/performance?ticker=2330.TW
{
  "as_of_date": "2026-06-22",
  "ticker": "2330.TW",
  "positions_count": 1,
  "market_value": 1200.0,
  "cost_basis_total": 1000.0,
  "total_unrealized_pnl": 200.0,
  "total_unrealized_pnl_pct": 20.0,
  "periods": {
    "day": {
      "label": "day",
      "baseline_date": "2026-06-19",
      "baseline_value": 1180.0,
      "unrealized_pnl": 20.0,
      "unrealized_pnl_pct": 1.694915
    },
    "month": {
      "label": "month",
      "baseline_date": "2026-05-29",
      "baseline_value": 1100.0,
      "unrealized_pnl": 100.0,
      "unrealized_pnl_pct": 9.090909
    },
    "year": {
      "label": "year",
      "baseline_date": "2025-12-31",
      "baseline_value": 900.0,
      "unrealized_pnl": 300.0,
      "unrealized_pnl_pct": 33.333333
    }
  }
}

分組績效範例(type=Group):

GET /api/portfolio/performance?type=Group
{
  "type": "Group",
  "as_of_date": "2026-06-22",
  "positions_count": 3,
  "market_value": 2750.0,
  "cost_basis_total": 2250.0,
  "total_unrealized_pnl": 500.0,
  "total_unrealized_pnl_pct": 22.2222,
  "groups": [
    {
      "category": "Semiconductors",
      "category_source": "industry",
      "positions_count": 1,
      "market_value": 1200.0,
      "cost_basis_total": 1000.0,
      "total_unrealized_pnl": 200.0,
      "total_unrealized_pnl_pct": 20.0,
      "periods": {
        "day": {
          "label": "day",
          "baseline_date": "2026-06-19",
          "baseline_value": 1180.0,
          "unrealized_pnl": 20.0,
          "unrealized_pnl_pct": 1.6949
        },
        "month": {
          "label": "month",
          "baseline_date": "2026-05-29",
          "baseline_value": 1100.0,
          "unrealized_pnl": 100.0,
          "unrealized_pnl_pct": 9.0909
        },
        "year": {
          "label": "year",
          "baseline_date": "2025-12-31",
          "baseline_value": 900.0,
          "unrealized_pnl": 300.0,
          "unrealized_pnl_pct": 33.3333
        }
      },
      "tickers": ["2330.TW"]
    }
  ]
}

分組績效範例(type=ETF):

GET /api/portfolio/performance?type=ETF
{
  "type": "ETF",
  "as_of_date": "2026-06-22",
  "positions_count": 2,
  "market_value": 2200.0,
  "cost_basis_total": 1800.0,
  "total_unrealized_pnl": 400.0,
  "total_unrealized_pnl_pct": 22.2222,
  "groups": [
    {
      "category": "stock",
      "category_source": "asset_type",
      "positions_count": 1,
      "market_value": 1200.0,
      "cost_basis_total": 1000.0,
      "total_unrealized_pnl": 200.0,
      "total_unrealized_pnl_pct": 20.0,
      "periods": {
        "day": {
          "label": "day",
          "baseline_date": "2026-06-19",
          "baseline_value": 1180.0,
          "unrealized_pnl": 20.0,
          "unrealized_pnl_pct": 1.6949
        },
        "month": {
          "label": "month",
          "baseline_date": "2026-05-29",
          "baseline_value": 1100.0,
          "unrealized_pnl": 100.0,
          "unrealized_pnl_pct": 9.0909
        },
        "year": {
          "label": "year",
          "baseline_date": "2025-12-31",
          "baseline_value": 900.0,
          "unrealized_pnl": 300.0,
          "unrealized_pnl_pct": 33.3333
        }
      },
      "tickers": ["2330.TW"]
    },
    {
      "category": "etf",
      "category_source": "asset_type",
      "positions_count": 1,
      "market_value": 1000.0,
      "cost_basis_total": 800.0,
      "total_unrealized_pnl": 200.0,
      "total_unrealized_pnl_pct": 25.0,
      "periods": {
        "day": {
          "label": "day",
          "baseline_date": "2026-06-19",
          "baseline_value": 980.0,
          "unrealized_pnl": 20.0,
          "unrealized_pnl_pct": 2.0408
        },
        "month": {
          "label": "month",
          "baseline_date": "2026-05-29",
          "baseline_value": 950.0,
          "unrealized_pnl": 50.0,
          "unrealized_pnl_pct": 5.2632
        },
        "year": {
          "label": "year",
          "baseline_date": "2025-12-31",
          "baseline_value": 800.0,
          "unrealized_pnl": 200.0,
          "unrealized_pnl_pct": 25.0
        }
      },
      "tickers": ["0050.TW"]
    }
  ]
}

Grouped Response Semantics:

  • groups[].periods 的 day/month/year 計算口徑與既有整體/單 ticker performance 相同。
  • grouped top-level market_valuecost_basis_totaltotal_unrealized_pnlpositions_count 為各 group 加總。
  • grouped top-level as_of_date 為 groups 中最大 as_of_date;若無 active holdings 則為 null
  • groupsmarket_value DESC, category ASC 排序;每個 group 的 tickers 依 ticker ASC 排序。
  • 空投資組合回傳 type、zero totals 與 groups=[]

GET /api/portfolio/investment-performance

完整投資操作績效。與既有 /api/portfolio/performance(只看目前持倉市場變化)分開。

Query Parameters:

參數 類型 必填 預設 說明
period all/day/week/month/quarter/year all lot 開倉 cohort
value string non-all 是 null day YYYYMMDD/ISO;week YYYY-Www;month YYYYMM/YYYY-MM;quarter compact/hyphen;year YYYY
ticker string null 可查已清倉 ticker;與 type 互斥
type Group/ETF null current metadata classification

Response metrics:

  • realized.regular_pnl/day_trade_pnl/cash_dividend/total
  • expected_cash_dividend(不進 realized/total)
  • unrealized_pnl/total_pnl/capital_employed/return_pct/equity_multiple
  • opened_lots_count/closed_lots_count
  • data_quality: unmatched sells、missing prices、已完成月份的 historical dividend provider/ticker/month coverage、當月估值日的 daily schedule provider/date freshness、standalone/unattributed dividends

股利完整性分成兩種不可互換的證據:dividend_history_completedividend_missing_coverage 只檢查已完成月份的 historical coverage; dividend_schedule_completedividend_schedule_missing_coverage 則檢查當月估值日 當天或之後、且不晚於查詢當日的 TWSE/TPEx current/upcoming schedule 是否成功, 估值日前 snapshot 仍視為 stale;缺口格式例如 TWSE:2026-07-23。Daily schedule 不會被視為 historical provider coverage。 估值日等於查詢當日時,此檢查只在當日每日更新的重試截止時點(15:30 起算 6 小時,即 21:30 台北)之後才判缺:該時點前缺當日 snapshot 代表每日 job 尚未跑完(今日 OHLC 落地會讓估值日提前數小時推進),並非資料缺口。

非 all 只計入期間內開倉 lots;舊 lots 的本期漲跌與股利只進 all/cumulative。ex_date <= end_date(圖表點為 period_end)即依除息權利認列 realized,不受最新股價日期落後影響;歷史 entitlement 使用 ex-date 前 transactions/closures。

Grouped response 為 type/classification_basis/overall/groups/data_qualitygroups[] 另有 category/category_source/tickersgroups[].tickers 為該期間內實際有操作的個股子集(cohort 明細 closures/dividends/lots 標的+區間內未配對賣出),非全歷史成員;period=all 等同全部成員。完整 schema 與 example 請參考 Frontend integration SSOT

GET /api/portfolio/investment-performance/details

回傳單一操作績效 metric 的可對帳逐筆明細。必填 metricregular_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,且與 ticker 互斥;backend 以 current metadata 驗證分類並解析 tickers。offset 預設 0;limit 預設 100、最大 1000。

Response 包含 window/scope/metric/unit/value/reconciliation/pagination/items/data_qualityvalue 是權威 aggregate;detail_value 由未分頁的完整明細計算,前端不可 sum 當頁 items。capital_employed 對帳 basis 為 max_capital_required,不是 sum。所有 items 均依主要日期由新到舊;capital-flow cumulative/peak 仍按正向時間計算,只反轉輸出排列。Items 以 item_type=closure/dividend/lot/capital_flow 判別;mixed realized_total/total_pnl 會包含多種 item。完整欄位、範例、null 與錯誤處理見 Frontend integration SSOT

GET /api/portfolio/investment-performance/timeseries

Query: start_dateend_date(不可晚於今日)、interval=day|week|month|quarter|year,以及互斥的 ticker / type

series[].points[] 同時包含 period(bucket 開倉 cohort)與 cumulative(inception 至 point)。歷史點不倒灌 future sell 或今日 future-dividend 公告。總點數超過 2000 回 HTTP 422。

GET /api/portfolio/summary

投資組合總覽(含總市值、損益等)。

股利欄位 shape 不變;ex_date <= 台北今日 即列 received,其 position_qty 改由除息日前歷史 entitlement 重建,pending event 才使用今日 transactions/closures 的 current holdings。同 ticker/ex-date 的 manual cash-dividend transaction 優先且不重複加總。

Response:

{
  "positions_count": 3,
  "net_shares": 1500,
  "total_buy_amount": 150000.0,
  "total_sell_amount": 80000.0,
  "net_cash_flow": -70000.0,
  "realized_pnl": 3200.0,              // 不含當沖
  "day_trade_realized_pnl": 1800.0,    // 只含當沖
  "total_realized_pnl": 5000.0,        // realized_pnl + day_trade_realized_pnl
  "unrealized_pnl": 12000.0,
  "dividends": {
    "year": 2026,
    "currency": "TWD",
    "total_amount": 3300.0,
    "received_amount": 3000.0,
    "pending_amount": 300.0,
    "total_stock_dividend_shares": 20.0,
    "received_stock_dividend_shares": 0.0,
    "pending_stock_dividend_shares": 20.0,
    "events": [
      {
        "ticker": "2330.TW",
        "ex_date": "2026-06-12",
        "type": "息",
        "status": "received",
        "position_qty": 1000.0,
        "cash_dividend_per_share": 3.0,
        "cash_amount": 3000.0,
        "stock_dividend_ratio": null,
        "stock_dividend_shares": 0.0,
        "source": "TWSE"
      },
      {
        "ticker": "2317.TW",
        "ex_date": "2026-08-01",
        "type": "息",
        "status": "pending",
        "position_qty": 200.0,
        "cash_dividend_per_share": 1.5,
        "cash_amount": 300.0,
        "stock_dividend_ratio": null,
        "stock_dividend_shares": 0.0,
        "source": "TWSE"
      }
    ]
  }
}

dividends 以目前 active holdings(position_qty > 0)與今年 dividend_events 計算;年度採 Asia/Taipei 今日所屬年度。現金股利金額為 position_qty * cash_dividendstatus="received" 代表 ex_date <= todaystatus="pending" 代表未來除息/除權日;目前資料表沒有實際發放日,因此不以發放日判斷。除權配股另以 position_qty * stock_dividend 估算股數,不換算進現金 *_amount


Metrics Naming Specification

所有 portfolio endpoints 的損益欄位命名與語意統一如下。這些規則保證「非重疊加總」—— realized_pnlday_trade_realized_pnl 互斥,相加得到 total_realized_pnl,符合直覺。

欄位 語意 是否含當沖
position_qty 當前持股數(所有 open buy lots 的 remaining_qty 加總)
avg_buy_price open lots 的 quantity-weighted effective cost per share (含 fee/tax 分攤)
cost_basis_total open lots 剩餘 qty × effective_cost 的加總
unrealized_pnl (market_price - avg_buy_price) × position_qty(當沖結束後不會留下 open lot,因此自然不影響)
realized_pnl closures 所產生的已實現損益,只含 day_trade_pair_id IS NULL 的 sell leg ❌ 不含
day_trade_realized_pnl closures 所產生的已實現損益,只含 day_trade_pair_id IS NOT NULL 的 sell leg ✅ 只含
total_realized_pnl realized_pnl + day_trade_realized_pnl ✅ 兩者相加

計算細節: - 每筆 buy 產生一個 open lot,effective_cost_per_share = (price × qty + fee + tax) / qty。 - 每筆 sell 透過 transaction_closures 指向一筆或多筆 buy lot,依 closure.qty 扣減 lot 的 remaining_qty。 - 賣出 fee/tax 依 closure.qty / total_sell_qty 比例分攤到每筆 closure。 - short_first 當沖配對允許中間 walk 階段 position_qty 短暫變負(sell leg 比 buy leg 早被處理),最終狀態必定回到非負的淨部位。 - 遇到未知的 action 會 raise ValueError,避免未來 schema 新增 action type 時靜默漂移。


18. 族群 Groups

Prefix: /api/groups

GET /api/groups

列出股票族群。active_count = 族群內有至少一個 is_primary=TRUE 策略匹配的成員數。

Query Params:

名稱 類型 說明
group_type string industry / sector / concept
source_window string 2y(預設)/ all,見 §1 通用說明

GET /api/groups/{group_id}/members

族群成員列表,附帶每個成員的主要策略(is_primary=TRUE)。

Path Params: group_id 可使用 /api/groups 回傳的 canonical group_id(建議),也可使用 exact group_name;若名稱重複,後端會以 group_type, group_id 排序後取第一筆匹配。

Query Params:

名稱 類型 說明
sort string score(預設)/ name
direction string long / short(篩選主策略方向)
source_window string 2y(預設)/ all,見 §1 通用說明
market string OHLC 行情市場,預設 taiwan
trade_date date 行情查詢日期上限;未提供時使用該 market 最新 OHLC 日期
include_price bool 是否回傳 members[].price,預設 true
limit int 回傳筆數(預設 200)
offset int 分頁偏移

Response — member item:

欄位 類型 說明
ticker string 股票代碼
name string 股票名稱
industry string 產業
strategy_key string? 主策略鍵(long_d1, short_d5, ...)
score float? 綜合分數
metrics_2y object? 2y 窗口 8 項指標(hit_rate, expectancy, ...);見 §1
metrics_all object? 全期窗口 8 項指標;見 §1
scope string? global / group / stock / supply_chain / user_group
rule_id string? 規則 ID
trade_date string? 交易日
price object | null 個股行情與漲跌資訊;無最新 close 資料時為 nullinclude_price=false 時省略

members[].price 欄位:

欄位 類型 說明
as_of_date string | null 實際使用的最新 OHLC 交易日
close number | null as_of_date 收盤價
change number | null 相對前一交易列的價差
change_pct number | null 相對前一交易列的漲跌幅百分比
five_day_close number | null 第 5 個回看交易列收盤價
five_day_change number | null 相對第 5 個回看交易列的價差
five_day_change_pct number | null 相對第 5 個回看交易列的漲跌幅百分比
twenty_day_close number | null 第 20 個回看交易列收盤價
twenty_day_change number | null 相對第 20 個回看交易列的價差
twenty_day_change_pct number | null 相對第 20 個回看交易列的漲跌幅百分比

Response 範例:

{
  "group_id": "semiconductor",
  "group_type": "industry",
  "group_name": "半導體",
  "member_count": 120,
  "found": true,
  "members": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "industry": "半導體",
      "strategy_key": "long_d5",
      "score": 0.82,
      "metrics_2y": {},
      "metrics_all": {},
      "scope": "group",
      "rule_id": "R001",
      "domain": "TA",
      "regime_slice": "all",
      "trade_date": "2026-05-17",
      "price": {
        "as_of_date": "2026-05-17",
        "close": 110.0,
        "change": 10.0,
        "change_pct": 10.0,
        "five_day_close": 90.0,
        "five_day_change": 20.0,
        "five_day_change_pct": 22.2222,
        "twenty_day_close": 80.0,
        "twenty_day_change": 30.0,
        "twenty_day_change_pct": 37.5
      }
    }
  ]
}

成員沒有最新 close 資料時,pricenull


GET /api/groups/{group_id}/signals

族群策略訊號 — 回傳 scope='group' 的策略匹配結果。

Path Params:/api/groups/{group_id}/members,可使用 canonical group_id 或 exact group_name

Query Params:

名稱 類型 說明
strategy_key string 策略鍵篩選(long_d1, short_d5, ...)
direction string long / short
source_window string 2y(預設)/ all,見 §1 通用說明

18B. 統一個股清單 Stock Lists

Prefix: /api/stock-lists

給前端用同一支 API 查「同題材 / 同族群 / 同觀察清單」內的個股與漲跌資訊。這支 API 只回傳 stockprice,不包含 legacy endpoint 裡的 industrysector、策略欄位、watchlist metadata 或 latest_ohlc

GET /api/stock-lists/items

Query Params:

名稱 類型 預設 說明
searchType string required theme / group / watchlist
searchID string required 查詢 ID;見下方對應表
market string taiwan OHLC 行情市場
tradeDate date latest OHLC 行情查詢日期上限
includePrice bool true 是否回傳 items[].pricefalseprice 固定為 null 且不查 price window
limit int 200 每頁筆數,上限 1000
offset int 0 分頁位移

searchType / searchID 對應:

searchType searchID 來源 認證
theme /api/themestheme_id,對應 coverage_theme_membership.theme 不需要
group /api/groupsgroup_id(建議)或 exact group_name,對應 stock_group.group_id / stock_group.group_name 不需要
watchlist /api/watchlists/groupsid,必須是 UUID,對應 watchlist_groups.id 需要 Bearer token,且只能查自己的 group

Response (200):

{
  "searchType": "theme",
  "searchID": "AI 伺服器",
  "found": true,
  "total": 148,
  "items": [
    {
      "stock": {
        "ticker": "2330.TW",
        "name": "台積電",
        "englishName": "Taiwan Semiconductor"
      },
      "price": {
        "asOfDate": "2026-06-18",
        "close": 2410.0,
        "change": 10.0,
        "changePct": 0.4167,
        "fiveDayClose": 2300.0,
        "fiveDayChange": 110.0,
        "fiveDayChangePct": 4.7826,
        "twentyDayClose": 2230.0,
        "twentyDayChange": 180.0,
        "twentyDayChangePct": 8.0717
      }
    }
  ]
}

Response 欄位:

欄位 類型 說明
searchType string 回傳本次查詢類型
searchID string 回傳本次查詢 ID
found bool source 是否存在;不存在時 total=0items=[]
total int 分頁前 source 內個股總數
items[].stock.ticker string 股票代碼
items[].stock.name string | null 股票名稱
items[].stock.englishName string | null 英文名稱
items[].price object | null 個股行情與漲跌資訊;沒有 OHLC close 或 includePrice=false 時為 null

items[].price 欄位:

欄位 類型 說明
asOfDate string | null 實際使用的最新 OHLC 交易日
close number | null asOfDate 收盤價
change number | null 相對前一交易列的價差
changePct number | null 相對前一交易列的漲跌幅百分比
fiveDayClose number | null 第 5 個回看交易列收盤價
fiveDayChange number | null 相對第 5 個回看交易列的價差
fiveDayChangePct number | null 相對第 5 個回看交易列的漲跌幅百分比
twentyDayClose number | null 第 20 個回看交易列收盤價
twentyDayChange number | null 相對第 20 個回看交易列的價差
twentyDayChangePct number | null 相對第 20 個回看交易列的漲跌幅百分比

Error / empty behavior:

情境 HTTP Response
searchType 不是 theme/group/watchlist 422 FastAPI validation error
searchType=watchlistsearchID 不是 UUID 422 searchID must be a valid UUID when searchType=watchlist
searchType=watchlist 但未帶 Bearer token 401 Auth error
source 不存在或 watchlist group 不屬於目前 member 200 {"found": false, "total": 0, "items": []}

searchType=group 若使用 group_name,後端會先嘗試 exact group_id,再用 exact group_name 解析到 canonical group_id;若名稱在不同分類重複,會以 group_type, group_id 排序後取第一筆。前端仍建議優先使用 /api/groups 回傳的 group_id。 | source 存在但分頁超出範圍 | 200 | {"found": true, "total": <source total>, "items": []} |


19. 策略規則 Rules

Prefix: /api/rules

共用篩選參數

以下參數可用於 catalogthreshold-summarythreshold-rules 三個端點:

名稱 類型 說明
ticker string 篩選 ticker 匹配的規則(不限 scope)
scope string global / group / stock / supply_chain / user_group
group_id string 篩選特定族群
supply_chain_id string 篩選特定供應鏈
commodity_code string 篩選特定商品(domain=COM)
family_id string 篩選特定規則家族(domain=IR)
domain string TA / CT / IR / COM
strategy_key string long_d1, short_d60 等 12 種策略
regime_slice string all / bull / bear / range
min_hit_rate float 最低勝率門檻
min_sample_count int 最低觸發次數
min_ev float 最低期望值(EV = hit_rate × mean_return)
min_pf float 最低獲利因子
include_below_floor bool 是否包含 below_floor 品質規則(預設 true)
source_window string 2y(預設)/ all,見 §1 通用說明

GET /api/rules/catalog/threshold-summary

依門檻篩選後,按 strategy_key 分組統計規則數量。前端 slider 互動用。

額外參數:

名稱 類型 說明
group_by string 額外分組維度(逗號分隔)。允許:domain, scope, regime_slice, quality_tier

Response:

{
  "filters_applied": {"min_hit_rate": 0.6},
  "group_by": ["strategy_key", "domain"],
  "total_rules": 1234,
  "items": [
    {"strategy_key": "long_d1", "domain": "TA", "rule_count": 45, "avg_hit_rate": 0.72, "avg_ev": 0.008, "avg_pf": 2.31}
  ]
}

GET /api/rules/catalog/threshold-rules

依門檻篩選後,回傳通過的規則明細(分頁)。

額外參數:

名稱 類型 預設 說明
limit int 200 每頁筆數(1-1000)
offset int 0 偏移

Response:

{
  "filters_applied": {"min_hit_rate": 0.6},
  "total": 1234,
  "limit": 200,
  "offset": 0,
  "rules": [
    {
      "catalog_key": 123, "rule_id": "G_TA_...", "strategy_key": "long_d1",
      "scope": "global", "domain": "TA",
      "metrics_2y": {"hit_rate": 0.75, "expectancy": 0.012, "sample_count": 28, "profit_factor": 3.2, "...": "..."},
      "metrics_all": {"hit_rate": 0.71, "expectancy": 0.009, "sample_count": 92, "profit_factor": 2.8, "...": "..."},
      "conditions": {}
    }
  ]
}

GET /api/rules/catalog

列出策略規則目錄(支援共用篩選參數 + 分頁)。

額外參數: limit (int, 預設 200), offset (int, 預設 0)

Response:

{
  "filters_applied": {"domain": "TA"},
  "total": 1234,
  "limit": 200,
  "offset": 0,
  "rules": [
    {
      "catalog_key": 123, "rule_id": "G_TA_...", "strategy_key": "long_d1",
      "scope": "global", "domain": "TA",
      "metrics_2y": {"hit_rate": 0.75, "expectancy": 0.012, "sample_count": 28, "profit_factor": 3.2, "...": "..."},
      "metrics_all": {"hit_rate": 0.71, "expectancy": 0.009, "sample_count": 92, "profit_factor": 2.8, "...": "..."},
      "conditions": {}
    }
  ]
}

GET /api/rules/catalog/{catalog_key}

依 catalog_key(PK)取得單一規則詳情。

Response:

{"found": true, "catalog_key": 42, "rule": {"catalog_key": 42, "rule_id": "...", ...}}

GET /api/rules/bucket-summary

策略分組統計摘要(按 strategy_key + domain 分組)。

Query Params:

名稱 類型 預設 說明
include_below_floor bool true 是否包含 below_floor 品質規則
source_window string 2y 2y / all,見 §1 通用說明

20. 個股規則 Stock Rules

Prefix: /api/stock-rules

GET /api/stock-rules/{ticker}

取得個股所有啟用中的規則設定。

Response — StockRulesResponse:

{
  "ticker": "2330.TW",
  "sources": {
    "default": {
      "version": 3,
      "computed_at": "2024-03-15T10:00:00",
      "data_range": { "start": "2023-01-01", "end": "2024-03-15" },
      "sample_count": 500,
      "quality_tier": "gold",
      "warnings": [],
      "n_rules": 25,
      "channels": {
        "short_term_long": { ... },
        "short_term_short": { ... }
      }
    }
  }
}

GET /api/stock-rules/{ticker}/summary

取得規則摘要(不含 channels 細節)。


GET /api/stock-rules/{ticker}/history

取得規則版本歷史。

Query Params:

名稱 類型 說明
source string 來源篩選
include_channels bool 是否含 channel 細節

GET /api/stock-rules/{ticker}/{source}

取得特定來源的規則。


21. 黑天鵝階段 Black Swan

Prefix: /api/black-swan

GET /api/black-swan/phase

黑天鵝事件階段估算。

Query Params:

名稱 類型 預設 說明
date string 日期,格式 yyyymmdd
subject string 0050.TW 觀測標的

Response — BlackSwanPhaseResponse:

{
  "date": "20240315",
  "resolved_trading_date": "20240315",
  "mode": "realtime",
  "subject": "0050.TW",
  "proxy_source": null,
  "main_type": "normal",
  "dominant_phase": "recovery",
  "phase_source": "composite",
  "overlap_flag": false,
  "black_swan": {
    "shock": { "pct": 0.1, "matched_rules": ["rule_1"] },
    "panic": { "pct": 0.05, "matched_rules": [] },
    "recovery": { "pct": 0.6, "matched_rules": ["rule_3", "rule_5"] },
    "rebuild": { "pct": 0.25, "matched_rules": ["rule_7"] }
  }
}

GET /api/black-swan/phase-range

黑天鵝事件階段區間查詢 — 回傳日期範圍內每個交易日的階段估算。

Query Params:

名稱 類型 預設 說明
start_date string (YYYY-MM-DD) config default_lookback_days 天前 起始日
end_date string (YYYY-MM-DD) today 結束日
subject string 0050.TW 觀測標的

驗證規則: - start_date 必須 ≤ end_date,否則 422 - 區間不得超過 config max_range_days(預設 180 天),否則 422

Response — BlackSwanPhaseRangeResponse:

{
  "subject": "0050.TW",
  "start_date": "2025-02-18",
  "end_date": "2025-03-20",
  "count": 22,
  "daily": [
    {
      "trading_date": "2025-02-18",
      "dominant_phase": "pre_shock",
      "phase_source": "estimated",
      "overlap_flag": false,
      "warmup": false,
      "black_swan": {
        "pre_shock": { "pct": 0.70, "matched_rules": ["rule_a"] },
        "shock_start": { "pct": 0.10, "matched_rules": [] },
        "panic_active": { "pct": 0.10, "matched_rules": [] },
        "recovery": { "pct": 0.10, "matched_rules": ["rule_b"] }
      }
    }
  ]
}

warmup: true 表示該日的 trailing window 不足完整天數,估算結果可能不夠穩定。


22. 供應鏈分析 Supply Chain

Prefix: /api/v1/supply-chain

供應鏈知識圖譜分析。基於 My-TW-Coverage 1,735 家台股研究報告的 RAG 語義搜尋與結構化圖譜。

POST /api/v1/supply-chain/analyze

🔒 Auth: 需登入(Authorization: Bearer <access_token>)。無效 token 回 401;未帶 Authorization header 回 422。

LLM 供應鏈分析(需等待 AI 回應,約 5-15 秒)。

Request Body:

{
  "ticker": "2330",
  "trading_date": "2026-03-31",
  "response_mode": "json"
}
欄位 型別 必填 說明
ticker string 台股代碼
trading_date string 交易日期 (YYYY-MM-DD)
response_mode string "json" (預設) 或 "stream"

Response (200):

{
  "agent_name": "supply_chain",
  "ticker": "2330",
  "summary": "供應鏈分析摘要...",
  "signal": "bullish",
  "confidence": 0.72,
  "engine_signal": null,
  "engine_confidence": null,
  "enabled_dimensions": ["coverage_profile", "supply_chain_relations", "entity_mentions"],
  "skipped_dimensions": ["theme_membership"],
  "key_points": [
    { "text": "台積電為 Apple、NVIDIA 核心供應商", "evidence": "rel_type=customer; target=Apple; bidirectional=true" }
  ],
  "risks": [
    { "text": "上游設備依賴 ASML 獨家 EUV 供應", "evidence": "rel_type=upstream; target=ASML; mention_count=5" }
  ],
  "recommendations": [
    { "text": "關注 NVIDIA AI GPU 需求帶動先進製程營收", "evidence": "rel_type=downstream; target=NVIDIA; theme=AI伺服器" }
  ],
  "parser_warnings": []
}

GET /api/v1/supply-chain/profile/{ticker}

取得個股 Coverage 研究報告 Profile(不呼叫 LLM,即時回應)。 ticker 可帶 raw code 或交易所尾碼;response 會優先回傳 stocks.ticker 中的 canonical 代號(direct match → .TW.TWO)。

Response (200):

{
  "ticker": "2330.TW",
  "company_name": "台積電",
  "sector": "Semiconductors",
  "sector_en": "Technology",
  "industry_en": "Semiconductors",
  "market_cap_m": 47845508,
  "ev_m": 45886629,
  "description": "台積電成立於 1987 年,總部位於台灣新竹..."
}
狀態碼 說明
404 無此 ticker 的 coverage 資料

GET /api/themes

列出 coverage 知識庫中的完整題材清單。資料來源是 coverage_theme_membership,對應 data/my-tw-coverage/themes/*.md 匯入後的主題成員資料。API 只回傳最新 knowledge_as_of 快照。 coverage 原始資料若只有 raw code,member count 會以 canonical ticker 去重: direct match → .TW.TWO → raw code。

Response (200):

[
  {
    "theme_id": "AI 伺服器",
    "theme_name": "AI 伺服器",
    "member_count": 148,
    "is_eligible": true
  },
  {
    "theme_id": "CoWoS",
    "theme_name": "CoWoS",
    "member_count": 39,
    "is_eligible": true
  }
]

GET /api/themes/{theme_id}/stocks

取得 coverage 題材的成員清單。theme_id 是題材名稱,例如 AI 伺服器CoWoS電動車;放在 URL path 時需做 URL encoding。 成員 ticker 會用 stocks 表 canonicalize:先比對 direct ticker,再依序嘗試 .TW.TWO,仍查不到才回傳 coverage raw code。

Query Params:

名稱 類型 預設 說明
limit int themes.default_limit 每頁筆數,超過 themes.max_limit 時自動截到上限
offset int 0 分頁位移
market string taiwan OHLC 行情市場
trade_date date latest OHLC 行情查詢日期上限
include_price bool true 是否回傳 stocks[].pricefalse 時省略

Response (200):

{
  "theme_id": "AI 伺服器",
  "theme_name": "AI 伺服器",
  "found": true,
  "member_count": 148,
  "stocks": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "english_name": "TSMC",
      "industry": "半導體業",
      "sector": "資訊科技",
      "security_type": "common",
      "price": {
        "as_of_date": "2026-06-18",
        "close": 144.0,
        "change": -1.5,
        "change_pct": -1.0309,
        "five_day_close": 145.0,
        "five_day_change": -1.0,
        "five_day_change_pct": -0.6897,
        "twenty_day_close": 137.5,
        "twenty_day_change": 6.5,
        "twenty_day_change_pct": 4.7273
      }
    }
  ]
}

題材不存在時回傳 found: false 與空 stocks 陣列,HTTP status 仍為 200。 成員沒有最新 close 資料時,pricenullsecurity_type(issue #1282,見 §3.x stocks 端點說明的 15 值枚舉) 於成員可 canonicalize 到 stocks 表時回傳實際分類值;退回 coverage raw code(查無對應 stocks 列)時為 null


POST /api/admin/themes/generate

啟動 admin-only 題材生成 job。這支 API 會用 coverage RAG / web search / LLM 產生題材草稿,不會直接把 LLM 結果寫成正式題材;正式套用需呼叫 publish。 job 由 API process 內的 background task 執行;若 process 重啟,執行中的 job 不會續跑,startup recovery 會把超過 theme_generation.jobs.stale_running_minutesrunning / pending job 標記為 failedweb / coverage_rag_then_web 會先建立 job;如果 web search provider 未設定, 錯誤會記錄在 job status 的 error 欄位,而不是讓啟動 API 回 422。

Auth: Bearer token,且會員需為 admin(members.is_admin = true)。

Request Body:

欄位 類型 必填 預設 說明
theme string yes - 題材關鍵字,例如 液冷散熱
market string no taiwan 研究市場
source enum no theme_generation.default_source coverage_ragwebcoverage_rag_then_web
max_members int no theme_generation.default_max_members 候選成員上限
publish bool no false 是否生成完成後自動 publish
rebuild_after_publish bool no config publish 後是否接 rebuild
sync_db_after_rebuild bool no true rebuild 後是否同步 DB

Response (202):

{
  "job_id": "theme-generate-20260521-031500-a1b2c3d4",
  "status": "pending",
  "kind": "generate",
  "theme": "液冷散熱",
  "created_at": "2026-05-21T03:15:00"
}

POST /api/admin/themes/rebuild

啟動 admin-only 題材重建 job。這支 API 不做新研究,只讀取目前既有 coverage / wikilinks / themes/*.md,重建題材並可同步到 coverage_theme_membership

Auth: Bearer token,且會員需為 admin(members.is_admin = true)。

Request Body:

欄位 類型 必填 預設 說明
theme string/null no null 指定單一題材;null 代表重建全部
sync_db bool no true 是否執行 CoverageETL 同步 DB
knowledge_as_of date/null no server date 同步快照日期
skip_embed bool/null no theme_generation.sync.skip_embed true 時不重算 embeddings

Response (202):

{
  "job_id": "theme-rebuild-20260521-031530-e5f6a7b8",
  "status": "pending",
  "kind": "rebuild",
  "theme": "AI 伺服器",
  "created_at": "2026-05-21T03:15:30"
}

GET /api/admin/themes/jobs/{job_id}

查詢題材生成或重建 job 狀態與結果。

Auth: Bearer token,且會員需為 admin(members.is_admin = true)。

Status: pendingrunningcompletedfailedpublishedcanceled

candidates[].confidence 會以 JSON number 或 null 回傳,與 result.members[].confidence 一致。

result.coverage_knowledge_as_of(#774):本次生成所依據的語料世代日期(ISO date)。 僅 coverage 來源(coverage_rag / coverage_rag_then_web)會帶值;web 來源或語料 查詢失敗時為 null。同一日期也會標注於發布後的題材頁 (**語料版本 (knowledge_as_of):** ... 行)與 LLM prompt(語料知識截止日聲明)。

Response (200):

{
  "job_id": "theme-generate-20260521-031500-a1b2c3d4",
  "kind": "generate",
  "status": "completed",
  "theme": "液冷散熱",
  "progress": {"current": 3, "total": 3, "percentage": 100},
  "result": {
    "theme": "液冷散熱",
    "theme_name": "液冷散熱",
    "description": "AI 資料中心高功耗伺服器帶動液冷散熱需求。",
    "coverage_knowledge_as_of": "2026-06-18",
    "members": [
      {
        "ticker": "3017",
        "company_name": "奇鋐",
        "role": "midstream",
        "reason": "伺服器散熱模組供應商。",
        "confidence": 0.91,
        "source": "coverage_rag_then_web",
        "evidence": ["coverage 提到 AI 伺服器散熱模組"]
      }
    ]
  },
  "candidates": [
    {
      "id": 12,
      "theme": "液冷散熱",
      "ticker": "3017",
      "company_name": "奇鋐",
      "role": "midstream",
      "reason": "伺服器散熱模組供應商。",
      "confidence": 0.91,
      "source": "coverage_rag_then_web",
      "evidence": ["coverage 提到 AI 伺服器散熱模組"],
      "suggested_update": {},
      "approved": true,
      "created_at": "2026-05-21T03:15:20"
    }
  ],
  "error": null,
  "created_at": "2026-05-21T03:15:00",
  "started_at": "2026-05-21T03:15:01",
  "completed_at": "2026-05-21T03:15:20",
  "published_at": null
}

未知 job 回傳 404。


PATCH /api/admin/themes/jobs/{job_id}/candidates

批次更新已完成 generate job 的候選股審核狀態。前端可一次送出多筆 candidate_id / approved,讓使用者剔除不相干個股後再 publish。

Auth: Bearer token,且會員需為 admin(members.is_admin = true)。

Request Body:

{
  "updates": [
    {"candidate_id": 12, "approved": false},
    {"candidate_id": 13, "approved": true}
  ]
}

Response (200):

{
  "job_id": "theme-generate-20260521-031500-a1b2c3d4",
  "updated_count": 2,
  "candidates": [
    {
      "id": 12,
      "ticker": "3017",
      "company_name": "奇鋐",
      "role": "midstream",
      "reason": "伺服器散熱模組供應商。",
      "confidence": 0.91,
      "source": "coverage_rag_then_web",
      "evidence": ["coverage 提到 AI 伺服器散熱模組"],
      "approved": false
    }
  ]
}

未知 job 或 candidate 回傳 404;非 completed generate job 回傳 409。


POST /api/admin/themes/jobs/{job_id}/candidates

在 review 階段手動注入 LLM 漏掉的候選股。候選以 source="manual"approved=true 寫入,後續 publish 既有流程(讀 approved candidates)會自然 帶入,無需額外操作。發布時該成員行會標記 (人工)

Auth: Bearer token,且會員需為 admin(members.is_admin = true)。

Request Body:

欄位 類型 必填 預設 說明
ticker string yes 個股代號(4-6 位數字,其他格式回傳 422)
company_name string yes 公司名稱(成員行需具名才可被 ETL 解析,Gemini r3)
role string no related upstream / midstream / downstream / related
reason string no null 加入理由

Response (200): candidates 為該 job 的完整候選清單(含剛注入的一筆)。

{
  "job_id": "theme-generate-20260521-031500-a1b2c3d4",
  "candidate": {
    "id": 14,
    "ticker": "6435",
    "company_name": "台勝科",
    "role": "upstream",
    "reason": "漏掉的候選",
    "confidence": null,
    "source": "manual",
    "evidence": [],
    "approved": true
  },
  "candidates": [
    {
      "id": 12,
      "ticker": "3017",
      "company_name": "奇鋐",
      "role": "midstream",
      "confidence": 0.91,
      "source": "coverage_rag_then_web",
      "approved": true
    },
    {
      "id": 14,
      "ticker": "6435",
      "company_name": "台勝科",
      "role": "upstream",
      "confidence": null,
      "source": "manual",
      "approved": true
    }
  ],
  "warnings": []
}

ticker 存在性為軟驗證:若 ticker 不在 stocks 表,候選仍會加入,但 warnings 會帶出 "ticker {t} 不在 stocks 表(仍已加入)" 提示。

未知 job 回傳 404;非 completed generate job、或同 job 同 ticker 已存在回傳 409。


POST /api/admin/themes/jobs/{job_id}/cancel

取消尚未完成的題材生成或重建 job。可取消 pending / running job;若 job 正在背景執行,執行流程會在下一個階段檢查到 canceled 後停止更新結果。 Cancel 是 cooperative:它會標記 job,但正在進行中的 HTTP / LLM / subprocess 呼叫會先完成或 timeout,worker 才會在下一個 checkpoint 退出。

Auth: Bearer token,且會員需為 admin(members.is_admin = true)。

Response (200):

{
  "job_id": "theme-rebuild-20260521-031530-e5f6a7b8",
  "status": "canceled",
  "error": "Cancelled by admin",
  "completed_at": "2026-05-21T03:15:45"
}

未知 job 回傳 404;completed / published job 回傳 409。


POST /api/admin/themes/jobs/{job_id}/publish

將已完成的 generate draft 套用成正式題材資料。publish 只會使用 approved=true 的 candidates;使用者透過批次 candidates API 設為 approved=false 的個股不會加入題材。publish 會把 approved draft 寫成 generated themes/*.md 頁面;當 apply_coverage_updates=true 時,也會把 published theme wikilink 寫入核准成員對應的 Pilot_Reports/**/*.md 報告。 最後再視參數觸發 rebuild / CoverageETL sync。若所有 candidates 都被 reject, publish 回傳 409。

Auth: Bearer token,且會員需為 admin(members.is_admin = true)。

Request Body:

欄位 類型 必填 預設 說明
apply_coverage_updates bool no true 是否更新核准成員 Pilot_Reports 報告內的 published theme wikilink
apply_theme_definition bool no true 保留給 THEME_DEFINITIONS 更新;目前會回傳 warning
rebuild_after_publish bool no true publish 後是否啟動 rebuild job
sync_db_after_rebuild bool no true rebuild 後是否同步 DB

Response (200):

{
  "job_id": "theme-generate-20260521-031500-a1b2c3d4",
  "status": "published",
  "published_at": "2026-05-21T03:16:00",
  "rebuild_job_id": "theme-rebuild-20260521-031600-c9d0e1f2",
  "warnings": [
    "目前尚未自動更新 THEME_DEFINITIONS。"
  ]
}

題材頁成員編輯

以下四個端點把「手動編輯 Obsidian 題材頁」包成 API:題材頁(themes/<theme>.md) 即事實來源,API 做 line-surgery(只動目標成員行,其餘描述、相關主題行、他人 成員行含既有標記逐字保留),人工新增行帶 (人工) 標記,並同步更新各分節 ## X (N) 計數與 **涵蓋公司數:**

所有編輯端點都會回傳一則 warnings 提示:若題材屬白名單頁,下次 build_themes rebuild 可能覆蓋 (人工) 標記的成員行(build_themes 保留語義的 PR 落地前)。

sync_db=true 時,寫檔成功後會觸發 CoverageETL sync 讓 DB 跟上頁面。寫檔 成功即編輯成功:sync 失敗不會回滾、不回 500,而是以 warnings 帶出並把 sync_report 設為 null(避免 client 誤判編輯失敗而重試造成重複行)。

Auth(四端點皆同): Bearer token,且會員需為 admin(members.is_admin = true)。

GET /api/admin/themes/{theme}/page-members

回傳題材頁解析出的成員(供編輯 UI 顯示頁面實態,與 DB 版成員清單區隔)。

Response (200):

{
  "theme": "功率半導體",
  "members": [
    {"ticker": "1234", "company_name": "甲公司", "role": "upstream", "manual": false},
    {"ticker": "6435", "company_name": "台勝科", "role": "upstream", "manual": true}
  ]
}

題材頁不存在回傳 404。

POST /api/admin/themes/{theme}/members

在指定 role 分節新增一筆成員行 - **{ticker} {company_name}** (人工);無該分節 時依 上游→中游→下游→相關 順序建立新節。

Request Body:

欄位 類型 必填 預設 說明
ticker string yes 個股代號(4-6 位數字,其他格式回傳 422)
company_name string yes 公司名稱(成員行需具名才可被 ETL 解析)
role string yes upstream / midstream / downstream / related
sync_db bool no false 是否於寫檔後同步 DB

ticker 存在性為軟驗證:若 ticker 不在 stocks 表,成員仍會加入,但 warnings 會多帶一則 "ticker {t} 不在 stocks 表(仍已加入)" 提示。

Response (200):

{
  "theme": "功率半導體",
  "members": [
    {"ticker": "1234", "company_name": "甲公司", "role": "upstream", "manual": false},
    {"ticker": "6435", "company_name": "台勝科", "role": "upstream", "manual": true}
  ],
  "warnings": ["此編輯已直接寫入題材頁。若該題材屬白名單頁,下次 build_themes rebuild 可能覆蓋 (人工) 標記的成員行。"],
  "sync_report": null,
  "excluded_for_rebuild": false
}

excluded_for_rebuild(bool,所有編輯端點皆回傳)表示本次編輯是否往頁面 **人工排除:** 行寫入了 ticker。僅 DELETE 衍生成員時可能為 true(見下方 DELETE 說明);POST/PATCH 一律為 false。POST 重新加回先前被排除的 ticker 時, 會把它從 **人工排除:** 行移除(add-beats-exclude),該行清空則整行刪除。

題材頁不存在回傳 404;ticker 已在頁上回傳 409(改用 PATCH 調整 role)。

PATCH /api/admin/themes/{theme}/members/{ticker}

把既有成員行逐字搬移至新的 role 分節(保留其 (sector) / (LLM 生成) / (人工) 等註記),兩側分節計數同步更新,空分節整節移除。

Request Body:

欄位 類型 必填 預設 說明
role string yes 目標 role:upstream / midstream / downstream / related
sync_db bool no false 是否於寫檔後同步 DB

Response (200):POST .../membersThemePageEditResponse

{ticker} path 參數僅接受 4-6 位數字,其他格式回傳 422。 題材頁不存在、或 ticker 不在頁上回傳 404。

DELETE /api/admin/themes/{theme}/members/{ticker}

刪除指定成員行,更新計數,空分節整節移除。

刪除衍生成員(無 (人工) 標記,即 build_themes 會在 rebuild 時重新推導出 的成員)時,除了移除該行,還會把 ticker 追加到頁面的 **人工排除:** [[DDDD]] ... 行(無此行則建立;位置緊接 **相關主題:** 行之後, 無相關主題行時緊接 **涵蓋公司數:** 區塊之後),使刪除在下次 build_themes rebuild 後仍生效;此時 excluded_for_rebuildtrue。刪除手動成員(人工) 標記,屬 admin 新增、非衍生)僅移除該行、不寫排除行, excluded_for_rebuildfalse

**人工排除:** 行以裸 ticker wikilink 表示,語法對齊語料 build_themes;ETL _parse_themes 顯式跳過該行、不將其 wikilink 當成成員回灌(#840)。

Query Parameters:

參數 類型 必填 預設 說明
sync_db bool no false 是否於寫檔後同步 DB

Response (200):POST .../membersThemePageEditResponse(衍生成員刪除時 excluded_for_rebuildtrue)。

{ticker} path 參數僅接受 4-6 位數字,其他格式回傳 422。 題材頁不存在、或 ticker 不在頁上回傳 404。


GET /api/theme-recaps/latest

取得 Theme Recap 頁面的初載 read model。資料來源以最新 coverage_theme_membership.knowledge_as_of 的題材成員為主,再把成員股票 join 到 strategy_matchdaily_chip_stock_eventdaily_technical_stock_event 聚合成卡片與選中題材 detail。 coverage raw ticker 會先 canonicalize 成 stocks.ticker(direct → .TW.TWO), 再用 canonical ticker join 策略、籌碼、技術事件。 active_member_count 代表成員股在來源交易日有 primary/recommended strategy、 籌碼事件或技術事件任一訊號;signal_distribution 以成員股層級彙總這些當日訊號的 dominant bullish/neutral/bearish 方向。avg_win_rateexpected_return 仍來自 strategy_match 的策略績效欄位;策略資料缺漏時可為 null

這支 API 不使用 daily_technical_group_summary.group_iddaily_chip_group_flow.group_id 當主要來源,因為 coverage theme 不等於 stock group aggregation。

Query Params:

名稱 類型 預設 說明
market string taiwan 市場範圍
trade_date date latest available 要求的 recap 日期;無資料時 fallback 到最近可用資料日
selected_theme string top ranked theme 選中題材名稱,URL query 需 encode
limit int 20 回傳 theme card 數量,上限 100
window_days int 30 趨勢線 lookback window,上限 120

Response (200):

{
  "market": "taiwan",
  "knowledge_as_of": "2026-05-10",
  "requested_trade_date": "2026-05-18",
  "source_trade_date": "2026-05-17",
  "is_fallback": true,
  "themes": [
    {
      "theme_id": "AI 伺服器",
      "theme_name": "AI 伺服器",
      "member_count": 42,
      "active_member_count": 18,
      "active_ratio": 0.4286,
      "recap_score": 61.25,
      "technical_score": 64.0,
      "chip_score": 55.0,
      "sparkline": [
        {"date": "2026-05-15", "value": 58.0},
        {"date": "2026-05-17", "value": 61.0}
      ]
    }
  ],
  "selected_theme": {
    "theme_id": "AI 伺服器",
    "theme_name": "AI 伺服器",
    "found": true,
    "member_count": 42,
    "active_member_count": 18,
    "summary": {
      "active_ratio": 0.4286,
      "active_day_change": 0.0312,
      "avg_win_rate": 0.62,
      "expected_return": 0.018,
      "recap_score": 61.25
    },
    "chip": {
      "foreign_net_value": 1280000000.0,
      "trust_net_value": 360000000.0,
      "dealer_net_value": -120000000.0,
      "margin": {
        "as_of_date": "2026-05-17",
        "covered_member_count": 38,
        "margin_balance": 320000,
        "margin_previous": 315000,
        "margin_change": 5000,
        "margin_change_pct": 1.59,
        "margin_buy": 18000,
        "margin_sell": 13000,
        "short_balance": 12500,
        "short_previous": 11000,
        "short_change": 1500,
        "short_change_pct": 13.64,
        "short_buy": 900,
        "short_sell": 2400
      },
      "event_count": 12,
      "chip_score": 55.0,
      "top_net_buy_stocks": [
        {"ticker": "2382.TW", "name": "廣達", "net_value": 520000000.0, "event_score": 91.0}
      ]
    },
    "technical": {
      "technical_score": 64.0,
      "pct_above_sma_20": 57.14,
      "rsi_neutral_ratio": 0.62,
      "macd_bullish_ratio": 0.48,
      "breakout_count": 9,
      "breakdown_count": 3,
      "trend_series": [],
      "badges": ["multi_bullish", "technical_strength", "high_activity"]
    },
    "signal_distribution": {"bullish": 11, "neutral": 5, "bearish": 2},
    "member_ranking": [
      {
        "ticker": "2330.TW",
        "name": "台積電",
        "industry": "半導體",
        "sector": "科技",
        "strategy_key": "long_d5",
        "score": 85.0,
        "win_rate": 0.68,
        "expected_return": 0.0235,
        "technical_score": 86.0,
        "chip_score": 82.0,
        "price": {
          "as_of_date": "2026-05-17",
          "close": 110.0,
          "change": 10.0,
          "change_pct": 10.0,
          "five_day_close": 90.0,
          "five_day_change": 20.0,
          "five_day_change_pct": 22.22,
          "twenty_day_close": 80.0,
          "twenty_day_change": 30.0,
          "twenty_day_change_pct": 37.5
        },
        "margin": {
          "as_of_date": "2026-05-17",
          "margin_balance": 1000,
          "margin_previous": 900,
          "margin_change": 100,
          "margin_change_pct": 11.11,
          "margin_buy": 200,
          "margin_sell": 100,
          "short_balance": 50,
          "short_previous": 25,
          "short_change": 25,
          "short_change_pct": 100.0,
          "short_buy": 10,
          "short_sell": 35,
          "source_market": "TWSE"
        },
        "is_favorite": false
      }
    ],
    "latest_signals": []
  }
}

active_day_change 是本次 active_ratio 減前一個可用 recap 交易日的 active_ratioforeign_net_valuetrust_net_valuedealer_net_value 來自成員股籌碼事件 payload 的法人買賣超金額加總;來源資料缺漏時仍可能為 nullpct_above_sma_20 是 0-100 的百分比;rsi_neutral_ratiomacd_bullish_ratio 是 0-1 比例。member_ranking[].price 使用 stock_ohlcsource_trade_date 之前最近交易列、前一交易列、第 5 個回看 交易列、第 20 個回看交易列計算收盤價、價差與百分比漲跌幅。chip.marginmember_ranking[].margin 使用 margin_balance 取每檔成員股 trade_date <= source_trade_date 的最新一筆資料;題材層的 margin_change_pctshort_change_pct 以合計 change 除以合計 previous balance 計算。若沒有 融資融券資料、previous balance 缺漏或為 0,對應欄位回傳 null

chip.margin 欄位說明:

欄位 型別 說明
as_of_date string | null 題材成員使用的最新融資融券資料日
covered_member_count int 有融資融券資料的成員股數
margin_balance number | null 題材成員融資餘額合計
member_margin_balance_sum number | null 題材成員融資餘額合計;與 margin_balance alias 相同
member_margin_previous_sum number | null 題材成員前一交易日融資餘額合計;與 margin_previous alias 相同
margin_scope string 固定為 theme_members_aggregate,表示成分股聚合,不是官方大盤融資餘額
is_official_market_margin_balance boolean 固定為 false
margin_previous number | null 題材成員前一交易日融資餘額合計
margin_change number | null margin_balance - margin_previous
margin_change_pct number | null 融資餘額合計變動百分比;合計 previous 為 0 或缺漏時為 null
margin_buy number | null 題材成員融資買進合計
margin_sell number | null 題材成員融資賣出合計
short_balance number | null 題材成員融券餘額合計
short_previous number | null 題材成員前一交易日融券餘額合計
short_change number | null short_balance - short_previous
short_change_pct number | null 融券餘額合計變動百分比;合計 previous 為 0 或缺漏時為 null
short_buy number | null 題材成員融券買進合計
short_sell number | null 題材成員融券賣出合計

GET /api/theme-recaps/{theme_id}

取得單一 coverage theme 的 recap detail。theme_id 是題材名稱,例如 AI 伺服器CoWoS電動車;放在 URL path 時需做 URL encoding。

Query Params:

名稱 類型 預設 說明
market string taiwan 市場範圍
trade_date date latest available 要求的 recap 日期
window_days int 30 趨勢線 lookback window
member_limit int 5 member_ranking preview 數量
signal_limit int 3 latest_signals preview 數量

題材不存在時回傳 found: false、空排行與空訊號陣列,HTTP status 仍為 200。

GET /api/theme-recaps/search

搜尋 coverage theme 或其成員股票。

Query Params:

名稱 類型 預設 說明
q string 必填 Theme name、ticker 或公司名稱片段
market string taiwan 市場範圍
limit int 10 結果數量,上限 50

Response (200):

[
  {"kind": "theme", "theme_id": "AI 伺服器", "ticker": null, "name": "AI 伺服器", "member_count": 42},
  {"kind": "stock", "theme_id": "AI 伺服器", "ticker": "2382.TW", "name": "廣達", "member_count": null}
]

GET /api/theme-recaps/{theme_id}/members

取得 Theme Recap 的成員排行 tab。

Query Params:

名稱 類型 預設 說明
market string taiwan 市場範圍
trade_date date latest available 要求的 recap 日期
sort string composite_score composite_score / technical_score / chip_score / win_rate / ticker
limit int 50 Page size,上限 200
offset int 0 Pagination offset

Response (200):

{
  "theme_id": "AI 伺服器",
  "total": 42,
  "limit": 50,
  "offset": 0,
  "requested_trade_date": "2026-05-18",
  "source_trade_date": "2026-05-17",
  "is_fallback": true,
  "members": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "industry": "半導體",
      "sector": "科技",
      "strategy_key": "long_d5",
      "score": 85.0,
      "win_rate": 0.68,
      "expected_return": 0.0235,
      "technical_score": 86.0,
      "chip_score": 82.0,
      "price": {
        "as_of_date": "2026-05-17",
        "close": 110.0,
        "change": 10.0,
        "change_pct": 10.0,
        "five_day_close": 90.0,
        "five_day_change": 20.0,
        "five_day_change_pct": 22.22,
        "twenty_day_close": 80.0,
        "twenty_day_change": 30.0,
        "twenty_day_change_pct": 37.5
      },
      "margin": {
        "as_of_date": "2026-05-17",
        "margin_balance": 1000,
        "margin_previous": 900,
        "margin_change": 100,
        "margin_change_pct": 11.11,
        "margin_buy": 200,
        "margin_sell": 100,
        "short_balance": 50,
        "short_previous": 25,
        "short_change": 25,
        "short_change_pct": 100.0,
        "short_buy": 10,
        "short_sell": 35,
        "source_market": "TWSE"
      },
      "is_favorite": false
    }
  ]
}

members[].price 欄位說明:

欄位 型別 說明
as_of_date string | null 實際使用的最新 OHLC 交易日
close number | null as_of_date 收盤價
change number | null 相對前一交易列的價差
change_pct number | null 相對前一交易列的漲跌幅百分比
five_day_close number | null 第 5 個回看交易列收盤價
five_day_change number | null 相對第 5 個回看交易列的價差
five_day_change_pct number | null 相對第 5 個回看交易列的漲跌幅百分比
twenty_day_close number | null 第 20 個回看交易列收盤價
twenty_day_change number | null 相對第 20 個回看交易列的價差
twenty_day_change_pct number | null 相對第 20 個回看交易列的漲跌幅百分比

members[].margin 欄位說明:

欄位 型別 說明
as_of_date string | null 實際使用的最新融資融券資料日,為 source_trade_date 之前或當日的最新列
margin_balance number | null 融資餘額
margin_previous number | null 前一交易日融資餘額
margin_change number | null margin_balance - margin_previous
margin_change_pct number | null 融資餘額變動百分比;margin_previous 為 0 或缺漏時為 null
margin_buy number | null 融資買進
margin_sell number | null 融資賣出
short_balance number | null 融券餘額
short_previous number | null 前一交易日融券餘額
short_change number | null short_balance - short_previous
short_change_pct number | null 融券餘額變動百分比;short_previous 為 0 或缺漏時為 null
short_buy number | null 融券買進
short_sell number | null 融券賣出
source_market string | null 資料來源市場,例如 TWSE / TPEX

GET /api/theme-recaps/{theme_id}/signals

取得 Theme Recap 的最新訊號 tab,來源包含成員股票的 strategy match、 chip events、technical events。

Query Params:

名稱 類型 預設 說明
market string taiwan 市場範圍
trade_date date latest available 要求的 recap 日期
kind string null 可選:strategy / chip / technical
limit int 20 Page size,上限 100
offset int 0 Pagination offset

Response (200):

{
  "theme_id": "AI 伺服器",
  "total": 3,
  "limit": 20,
  "offset": 0,
  "requested_trade_date": "2026-05-18",
  "source_trade_date": "2026-05-17",
  "is_fallback": true,
  "signals": [
    {
      "kind": "strategy",
      "label": "bullish",
      "ticker": "2330.TW",
      "name": "台積電",
      "title": "long_d5",
      "description": "2330.TW long_d5",
      "strength": 86.0,
      "occurred_at": "2026-05-17"
    }
  ]
}

GET /api/v1/supply-chain/relations/{ticker}

取得個股的供應鏈關係(上下游、客戶、供應商)。 ticker 可帶 raw code 或 .TW / .TWO;response 的 source_ticker、 台股 target_ticker 會回傳 canonical ticker。

Query Params:

名稱 類型 預設 說明
rel_type string null 篩選:upstream / downstream / customer / supplier / midstream
direction string outgoing outgoing(以該 ticker 為起點)或 incoming(以該 ticker 為終點)

Response (200):

[
  {
    "source_ticker": "2330.TW",
    "target_name": "Apple",
    "target_ticker": null,
    "target_is_tw": false,
    "rel_type": "customer",
    "source_section": "customer_supplier",
    "is_bidirectional": true,
    "mention_count": 3
  },
  {
    "source_ticker": "2330.TW",
    "target_name": "ASML",
    "target_ticker": null,
    "target_is_tw": false,
    "rel_type": "upstream",
    "source_section": "supply_chain",
    "is_bidirectional": false,
    "mention_count": 5
  }
]

GET /api/v1/supply-chain/entities/{ticker}

取得個股相關的技術/材料/應用 entity。

Query Params:

名稱 類型 預設 說明
entity_type string null 篩選:technology / material / application

Response (200):

[
  { "entity_name": "CoWoS", "entity_type": "technology", "rel_type": "upstream", "source_section": "supply_chain", "mention_count": 3 },
  { "entity_name": "EUV", "entity_type": "technology", "rel_type": "upstream", "source_section": "supply_chain", "mention_count": 5 },
  { "entity_name": "矽晶圓", "entity_type": "material", "rel_type": null, "source_section": "supply_chain", "mention_count": 2 },
  { "entity_name": "AI伺服器", "entity_type": "application", "rel_type": null, "source_section": "description", "mention_count": 1 }
]

GET /api/v1/supply-chain/themes/{ticker}

取得個股所屬的主題族群。 ticker 可帶 raw code 或交易所尾碼;response ticker 會回傳 canonical ticker (例如 raw 3152 會回傳 3152.TWO)。

Response (200):

[
  { "theme": "5G", "ticker": "3152.TWO", "company_name": "璟德", "role": "related" },
  { "theme": "手機零組件", "ticker": "3152.TWO", "company_name": "璟德", "role": "upstream" }
]

GET /api/v1/supply-chain/theme-members/{theme}

取得主題族群的成員清單。 response ticker 會用同一套 canonicalization(direct → .TW.TWO → raw code)。

Path Params: theme — 主題名稱(例:AI 伺服器CoWoS電動車

Query Params:

名稱 類型 預設 說明
role string null 篩選:upstream / midstream / downstream / related

Response (200):

[
  { "theme": "CoWoS", "ticker": "3167.TWO", "company_name": "大量", "role": "upstream" },
  { "theme": "CoWoS", "ticker": "3535.TWO", "company_name": "晶彩科", "role": "midstream" },
  { "theme": "CoWoS", "ticker": "2330.TW", "company_name": "台積電", "role": "downstream" }
]

GET /api/v1/supply-chain/path

查找兩個 ticker 之間的最短供應鏈路徑(BFS)。 查找時會用 coverage raw ticker 比對,但回傳 path edge 會轉成 canonical ticker。

Query Params:

名稱 類型 必填 說明
ticker_a string 起點 ticker
ticker_b string 終點 ticker
max_depth int 最大搜尋深度(預設 4)

Response (200):

{
  "ticker_a": "2330.TW",
  "ticker_b": "2454.TW",
  "found": true,
  "path": [
    { "source_ticker": "2330.TW", "target_name": "聯發科", "target_ticker": "2454.TW", "rel_type": "downstream", "is_bidirectional": true }
  ],
  "depth": 1
}
{
  "ticker_a": "2330",
  "ticker_b": "9999",
  "found": false,
  "path": [],
  "depth": 0
}

GET /api/v1/supply-chain/search

RAG 語義搜尋(Coverage 研究報告全文檢索)。

🔒 Auth: 需登入(Authorization: Bearer <access_token>)。每次查詢會呼叫付費 OpenAI embedding,故需驗證。無效 token 回 401;未帶 Authorization header 回 422。

Query Params:

名稱 類型 必填 說明
q string 搜尋文字(例:液冷散熱CoWoS 上游Apple 代工
chunk_type string document / section_desc / section_sc / section_cs
sector string 篩選產業
top_k int 回傳數量(預設:document=3, section=5)

Response (200):

[
  {
    "doc_id": "sec_2330_section_sc",
    "ticker": "2330",
    "chunk_type": "section_sc",
    "section_name": "供應鏈位置",
    "content": "上游 (設備/原料): ASML (獨家 EUV 供應商)...",
    "score": 0.92
  }
]

共通錯誤碼:

狀態碼 說明
404 該 ticker 無 coverage 資料
422 參數格式錯誤
500 服務內部錯誤

23. 更新紀錄 Updates

Prefix: /api/updates

GET /api/updates/recent

取得近期資料更新紀錄。

Query Params:

名稱 類型 預設 說明
limit int 50 筆數(1-200)

Response — UpdateRun[]:

[
  {
    "id": 1,
    "job_name": "daily_price_update",
    "status": "completed",
    "reason": null,
    "message": "Updated 1500 tickers",
    "started_at": "2024-03-15T06:00:00",
    "ended_at": "2024-03-15T06:15:00"
  }
]

GET /api/updates/schedule-summary

取得某一天(台北日曆日)排程執行彙整:當天每個排程 job 與其逐步(每隻 API)的開始/結束時間與成功失敗狀態。供 admin 每日彙整信與前端 dashboard 共用同一份資料(issue #439)。

Query Params:

名稱 類型 預設 說明
date date (YYYY-MM-DD) 今天(Asia/Taipei) 要彙整的台北日曆日

overall_statussuccess(全部成功)/ partial(部分失敗或仍進行中)/ failed(全部失敗)/ empty(當天無排程)。每個 task 的 in_progress=true 表示 summary 產生時該步驟仍在執行(ended_at 為 null)。

Response — ScheduleSummaryOut:

{
  "day": "2026-06-16",
  "overall_status": "partial",
  "jobs": [
    {
      "job_name": "daily_update",
      "status": "success",
      "run_id": 525,
      "started_at": "2026-06-16T07:30:00+00:00",
      "ended_at": "2026-06-16T07:53:00+00:00",
      "duration_seconds": 1380.0,
      "reason": "scheduled:daily:retry=0",
      "message": "Daily update complete",
      "tasks": [
        {
          "task_name": "ohlc",
          "status": "success",
          "started_at": "2026-06-16T07:30:00+00:00",
          "ended_at": "2026-06-16T07:35:00+00:00",
          "duration_seconds": 300.0,
          "in_progress": false,
          "error_message": null
        },
        {
          "task_name": "dividend_events",
          "status": "failed",
          "started_at": "2026-06-16T07:35:00+00:00",
          "ended_at": "2026-06-16T07:35:02+00:00",
          "duration_seconds": 2.0,
          "in_progress": false,
          "error_message": "twse 500"
        }
      ]
    }
  ]
}

24. Health Check

GET /health

系統健康檢查。會實際對資料庫執行一次 SELECT 1 存活查詢,逾時上限由 config/core.yamlapi.health.db_probe_timeout_seconds 控制。

此端點是 Docker healthcheck、CD 部署閘門(含自動 rollback 判準)與 SIT DB 切換驗證的共同判準,故資料庫不可用時回 503 而非 200

Response 200 OK — 存活查詢成功:

{
  "status": "ok",
  "timestamp": "2026-07-23T02:31:45.746607",
  "database": "connected",
  "services": {
    "llm": "ok",
    "agents": "ok"
  }
}

Response 503 Service Unavailable — 存活查詢失敗或逾時:

{
  "status": "degraded",
  "timestamp": "2026-07-23T02:31:45.746607",
  "database": "disconnected",
  "services": {
    "llm": "ok",
    "agents": "limited"
  }
}
欄位 型別 說明
status string ok(健康)/ degraded(資料庫不可用)
timestamp string ISO 8601
database string connected / disconnected
services.llm string ok / unavailable
services.agents string ok / limited

GET /

根路徑,回傳 API metadata。


25. 錯誤處理

HTTP Status Codes

Code 說明
200 成功
400 請求參數錯誤
401 未認證或 token 過期
403 權限不足
404 資源不存在
422 請求 body 驗證失敗
500 伺服器錯誤

Error Response Format

{
  "detail": "描述錯誤的訊息"
}

認證失敗時(AuthError)可能包含額外欄位:

{
  "detail": "Token expired",
  "error_code": "TOKEN_EXPIRED"
}

附錄:Enum 值快速查表

direction

"bullish" | "bearish"

strategy_key (unified)

"long_d1" | "short_d1" | "long_d5" | "short_d5" | "long_d10" | "short_d10" | "long_d20" | "short_d20" | "long_d40" | "short_d40" | "long_d60" | "short_d60"

analysis_type

"comprehensive" | "technical" | "trend"

timeframe

"1_month" | "3_months" | "6_months" | "1_year"

response_mode

"stream" (SSE) | "json"

MAS target

"orchestrator" | "technical" | "chip" | "financial"

backtest domain

"technical" | "chip" | "financial"

portfolio action

"buy" | "sell"

group_type

"industry" | "sector" | "concept"

market_regime

"bull_trend" | "bear_trend" | "range_bound" | "recovery" | ...(依模型輸出)

job_status

"pending" | "running" | "completed" | "failed"


27. 統一策略 Strategy

統一策略引擎(Unified Strategy Engine)取代 V3/V4 雙軌,提供單一 API。

未指定 trade_date 時,matches、recommendations、summary 與個股 overview 一律使用同一個 authoritative snapshot:MAX(strategy_match.trade_date)。 recommendations 不會因最新日沒有推薦列而退回前一日。 若 authoritative snapshot 本身不存在,預設 recommendations 與個股 overview 回傳 503;若 snapshot 存在但沒有推薦列,recommendations 則正常回傳空陣列。

GET /api/strategy/matches/{ticker}

個股策略匹配結果。

參數 類型 說明
ticker path 股票代碼 (e.g. 2330.TW)
trade_date query 日期 YYYY-MM-DD,預設最新
source_window query 2y(預設)/ all,見 §1 通用說明
include_below_floor query true 回傳所有品質層級(預設 false,僅回傳 qualified 及舊版資料)

Response: StrategyMatchResponse[]

欄位 類型 說明
ticker string 股票代碼
trade_date string 交易日
strategy_key string 策略鍵 (long_d1, short_d5, ...)
rule_id string? 規則 ID
scope string? global / group / stock / supply_chain / user_group
score float 綜合分數 (HR + EV bonus)
ev_bonus float? EV 加成項
expected_value float? 期望值(後相容;新介接請從 metrics_2y.expectancy 取得)
metrics_2y object? 2y 窗口 8 項指標(hit_rate, expectancy, ...);見 §1
metrics_all object? 全期窗口 8 項指標;見 §1
support_count int? 樣本數
rank int? 策略內排名 (1=最佳)
is_primary bool 是否為該策略最佳規則
is_recommended bool 是否為推薦
domain string? 規則來源 (TA / CT / IR / COM)
regime_slice string? 產出時的 regime 切片 (all / bull / bear / range)
family_id string? IR family ID(僅 IR domain)
commodity_code string? 原物料代碼(僅 COM domain,如 COPPER
user_group_id string? 使用者群組 ID(僅 scope=user_group)
visibility string? public / private(user_group 預設 private)

GET /api/strategy/recommendations

首頁總覽推薦(is_recommended = TRUE 的 primary rules)。

參數 類型 說明
trade_date query 日期,預設最新
source_window query 2y(預設)/ all,見 §1 通用說明
include_below_floor query true 回傳所有品質層級(預設 false,僅回傳 qualified 及舊版資料)

Response: RecommendationResponse[]

GET /api/strategy/summary

策略統計摘要(每個 strategy_key 的 match 數量、平均 HR、平均 score)。

參數 類型 說明
trade_date query 日期,預設最新

Response: StrategySummaryResponse[]

strategy_key 對照表

strategy_key 說明
long_d1 日線做多
short_d1 日線做空
long_d5 週線做多
short_d5 週線做空
long_d10 雙週線做多
short_d10 雙週線做空
long_d20 月線做多
short_d20 月線做空
long_d40 雙月線做多
short_d40 雙月線做空
long_d60 季線做多
short_d60 季線做空

GET /api/strategy/overview/{ticker}

個股十二策略總覽,固定回傳 12 個策略狀態(含未觸發),附帶 primary rule + full supporting rules + conditions。每筆 rule 以 active 區分是否在指定交易日觸發。

參數 類型 說明
ticker path 股票代碼 (e.g. 2330.TW)
trade_date query 日期 YYYY-MM-DD,預設最新
source_window query 2y(預設)/ all,見 §1 通用說明
include_below_floor query true 回傳所有品質層級(預設 false,僅回傳 qualified 及舊版資料)
include_inactive_rules query true 額外回傳「對應此 ticker」的未觸發 catalog rules;預設 true。範圍包含 c.ticker = ticker 的規則、ticker 所屬 eligible group 的 scope=group 規則、ticker 所屬 validated supply chain 的 scope=supply_chain 規則;未命中的 generic global 規則不展開

Response: StrategyOverviewResponse

欄位 類型 說明
ticker string 股票代碼
trade_date string 交易日
strategies StrategyStatus[] 12 個策略狀態

StrategyStatus:

欄位 類型 說明
strategy_key string 策略鍵
label string 中文標籤
direction string long / short
triggered bool 是否觸發
primary_rule SupportingRule? 最佳規則(null = 未觸發)
supporting_rules SupportingRule[] primary_rule 以外的完整規則清單;未觸發規則會以 active=false 回傳

SupportingRule:

欄位 類型 說明
rule_id string? 規則 ID
catalog_key int? 目錄主鍵;legacy match 可能為 null
scope string? global / group / stock / supply_chain / user_group
active bool 是否在本次 ticker + trade_date 命中
score float 綜合分數;active=false 的 catalog-only 規則為 0.0
expected_value float? 期望值(後相容欄位;新介接請從 metrics_2y.expectancy 取得)
metrics_2y object? 2y 窗口 8 項指標(hit_rate, expectancy, profit_factor, ...);見 §1
metrics_all object? 全期窗口 8 項指標;見 §1
conditions object 觸發條件 {condition_name: true}

Story-first 策略(Story Strategy)

以「當日 Story Set」為單位、用歷史事件報酬統計驅動的策略檢視,資料來自已發布(status=published)的 Story Strategy 快照。端點會檢查 published as_of_date、同 rule version 最新的 ses_story_daily.trading_date 與 authoritative strategy_match.trade_date 三者完全相同;source 缺失或日期不一致時回傳 503,不提供過期結果。個股 overview 只讀 published 當日的 exact Story Set,不回退到 ticker 的舊日期。與上述 rule-based 策略並行,前綴 /api/strategy/story

不適用 §1 的 Contract D。 本子節 response 的 metrics_2y / metrics_allStoryMetricsevent_countaverage_return?up_countaverage_up_return?down_countaverage_down_return?),與 rule-based 策略的 8 欄指標(hit_rate / expectancy / profit_factor …)是完全不同的結構;且這些端點不接受 source_window 參數。

GET /api/strategy/story/overview/{ticker}

個股 Story-first 統計總覽:回傳該個股當日 Story Set 內每個 story 的歷史報酬統計,以及「精確 Story Set」(整組 story 同時命中)的統計。每項統計橫跨多個 horizon 與 stock / market 兩個對照基準、2y / all 兩個窗口。

參數 類型 說明
ticker path 股票代碼 (e.g. 2330.TW)
trade_date query 僅提供已發布快照日;不給則用該日,帶入其他日期回 404,不回退 ticker 舊日
rule_version query SES rule version,預設為當前發布版本

錯誤: 404 帶入的日期非已發布服務日,或該日期無此 ticker 的 Story Set;503 已發布快照不可用、該世代資料不完整,或 Story source revision 尚未收斂(發布世代/source/derived 三個 revision 不相等,或存在 historical dirty 標記)。

Response: StoryStrategyOverviewResponse

欄位 類型 說明
ticker string 股票代碼
trade_date string Story Set 交易日
stats_as_of_date string 統計世代的 as-of 日期
rule_version string SES rule version
stories StoryOverviewItem[] 當日 Story Set 內各單一 story 的統計
exact_story_set ExactStorySetOverview 整組 story 同時命中的精確統計

StoryOverviewItem: domainstory_codestory_name?direction?horizons: StoryHorizonMetrics[] ExactStorySetOverview: story_set_hashstory_countstory_keys[]horizons: StoryHorizonMetrics[] StoryHorizonMetrics: days(horizon 天數,如 1/5/10/20/40/60)、stock: {metrics_2y, metrics_all}market: {metrics_2y, metrics_all} StoryMetrics: event_countaverage_return?up_countaverage_up_return?down_countaverage_down_return?

GET /api/strategy/story/recommendations

依 2 年 evidence 排序的 Story-first 策略推薦,結果按 12 個 strategy_keylong/short × d1/d5/d10/d20/d40/d60)分桶,每桶內依 score 排名。

參數 類型 說明
trade_date query V1 僅提供已發布快照日;帶入其他日期回 404
rule_version query SES rule version,預設為當前發布版本
strategy_key query 限定單一策略鍵(12 個 enum 之一);不給則回全部
limit_per_strategy query 每個策略回傳上限,1100,預設 30

錯誤: 404 帶入的日期非已發布服務日;503 已發布快照不可用。

Response: StoryRecommendationsResponse

欄位 類型 說明
trade_date string 服務日(已發布快照日)
stats_as_of_date string 統計世代的 as-of 日期
rule_version string SES rule version
score_version string 排序 score 版本
score_window string 固定 2y
filters_applied object {strategy_key?, limit_per_strategy}
strategies StoryRecommendationBucket[] 各策略鍵分桶

StoryRecommendationBucket: strategy_keydirectionlong/short)、horizontotalitems: StoryRecommendationItem[] StoryRecommendationItem: ranktickerstock_name?scorecandidate_countcurrent_story_setselected_evidence SelectedStoryEvidence: typesingle_story/exact_story_set)、domain?story_code?story_set_hash?story_keys?event_counthit_counthit_ratedirectional_average_returnstock_metrics_2ymarket_metrics_2y


28. 原物料 Commodities

跨資產原物料及石油價格追蹤。提供 WTI、Brent、天然氣、黃金、白銀、銅的歷史價格及技術指標。

27.1 GET /api/commodities/list

列出所有追蹤的商品。

Query Parameters:

參數 類型 預設 說明
active_only bool true 只顯示啟用的商品

Response: CommodityInfo[]

欄位 類型 說明
symbol string 商品代號(WTI, GOLD, COPPER 等)
name string 商品名稱(中文)
english_name string? 英文名稱
category string 分類:energy, precious_metals, industrial_metals
series_type string 資料來源類型:fred_spot, yfinance_front_month
unit string? 單位(USD/barrel, USD/troy_oz, USD/lb)
is_active bool 是否啟用

27.2 GET /api/commodities/{symbol}/ohlc

取得商品 OHLC 歷史價格。

Path Parameters:

參數 類型 說明
symbol string 商品代號(例如 WTI, GOLD

Query Parameters:

參數 類型 預設 範圍 說明
days int 60 1-2000 回傳最近 N 天

Response: CommodityOHLC[]

欄位 類型 說明
symbol string 商品代號
trade_date date 交易日期
open float? 開盤價(FRED 來源為 null)
high float? 最高價(FRED 來源為 null)
low float? 最低價(FRED 來源為 null)
close float? 收盤 / 現貨價
volume int? 成交量(FRED 來源為 null)
data_source string 資料來源:fredyfinance

27.3 GET /api/commodities/{symbol}/snapshot

取得商品技術指標快照。

Path Parameters:

參數 類型 說明
symbol string 商品代號

Query Parameters:

參數 類型 預設 範圍 說明
days int 30 1-500 回傳最近 N 天

Response: CommoditySnapshot[]

欄位 類型 說明
symbol string 商品代號
trading_date date 交易日期
close float? 收盤價
sma_5 float? 5 日均線
sma_10 float? 10 日均線
sma_20 float? 20 日均線
sma_60 float? 60 日均線
rsi float? 14 日 RSI
atr float? 14 日 ATR(FRED 來源為 null)
pct_change_1d float? 1 日漲跌幅
pct_change_5d float? 5 日漲跌幅
pct_change_20d float? 20 日漲跌幅
pct_change_60d float? 60 日漲跌幅
trend_20d string? 20 日趨勢:up, down, flat
trend_60d string? 60 日趨勢:up, down, flat
above_sma_20 bool? 收盤 > SMA20
above_sma_60 bool? 收盤 > SMA60

27.4 GET /api/commodities/snapshot/latest

取得所有商品最新一日的技術指標快照。

Response: CommoditySnapshot[](每個商品一筆,最新日期)

與 27.3 相同的 response schema,但不需要指定 symbol,回傳所有商品的最新狀態。適合用於跨資產訊號總覽面板。


28. 規則說明 Rule Explanation

rule_idstrategy_rule_catalog 取得 conditions JSON,翻譯為可讀的中文條件說明。 V12 起 rule_id 為 32 字元 MD5 hash(V11 path-string 格式已淘汰)。

28.1 GET /api/strategy/rules/{rule_id}

取得單一規則的條件說明。

Path Parameters: - rule_id (string, required) — 規則 ID(MD5 hash),如 2e99c2e78db812ae9d1bb593403a5a3f

Response: RuleExplanationResponse

{
  "rule_id": "2e99c2e78db812ae9d1bb593403a5a3f",
  "conditions": ["cond__ma_bullish"],
  "conditions_zh": ["均線多頭排列"],
  "description": "當 均線多頭排列 時觸發"
}

Error: - 404 — rule_id 不在 strategy_rule_catalog

28.2 POST /api/strategy/rules/explain

批次查詢多筆規則說明(單一 ANY(:rule_ids) 查詢,無 N+1)。

Request Body:

{
  "rule_ids": [
    "2e99c2e78db812ae9d1bb593403a5a3f",
    "1ed44d0ab8c1c8029eeca7d2b52c525b"
  ]
}
  • rule_ids (string[], required) — 最少 1 筆,上限由 strategy.rule_explanation.batch_max 設定(預設 100)

Response: RuleExplanationBatchResponse

{
  "items": [
    {
      "rule_id": "2e99c2e78db812ae9d1bb593403a5a3f",
      "conditions": ["cond__ma_bullish"],
      "conditions_zh": ["均線多頭排列"],
      "description": "當 均線多頭排列 時觸發"
    },
    {
      "rule_id": "1ed44d0ab8c1c8029eeca7d2b52c525b",
      "conditions": ["cond__rsi_lt_30"],
      "conditions_zh": ["RSI < 30(超賣)"],
      "description": "當 RSI < 30(超賣) 時觸發"
    }
  ]
}

行為說明: - 輸出順序與輸入一致 - 不去重(重複 rule_id 會回傳重複結果) - 未知的 condition token 不報錯,conditions_zh 回傳原 token - 不在 catalog 的 rule_id 在批次中回傳空 conditions + 空 conditions_zh + 空 description(不報錯,方便前端對映)

Error: - 422 — request body 不合法(空陣列、超過 strategy.rule_explanation.batch_max 上限)


29. MAS Execution Trace

MAS 執行追蹤 API。所有 endpoints 為 read-only,且僅限 active admin 查詢 MAS 分析執行的完整 LLM call trace。 若資料庫尚未套用 members.is_admin migration,授權會 fail closed 並回傳 503 ADMIN_SCHEMA_MISSING

Prefix: /api/mas/traces

29.1 GET /api/mas/traces/executions

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。active non-admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

列出 MAS 分析執行紀錄。

參數 型別 預設 說明
ticker string? 依股票代號篩選
status string? 依狀態篩選(running / success / failed
started_after datetime? 起始時間下限
started_before datetime? 起始時間上限
limit int 20 每頁筆數(1-100)
offset int 0 分頁偏移

Response:

{
  "items": [
    {
      "execution_id": "uuid-string",
      "ticker": "2330.TW",
      "started_at": "2026-03-26T14:30:00Z",
      "ended_at": "2026-03-26T14:30:10Z",
      "status": "success",
      "activated_agents": ["technical", "chip", "financial", "orchestrator"],
      "total_tokens": 5200,
      "total_input_tokens": 3500,
      "total_output_tokens": 1700,
      "llm_call_count": 9,
      "agent_count": 4
    }
  ],
  "total": 42
}

29.2 GET /api/mas/traces/executions/{execution_id}

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。active non-admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

查詢單次執行的完整詳情,包含所有 LLM call trace。

參數 型別 預設 說明
sort string timeline timeline:依 started_at 排序;agent:依 agent_name + call_sequence 排序

Response:

{
  "execution": { "...ExecutionSummary..." },
  "agent_summaries": [
    {
      "agent_name": "technical",
      "call_count": 3,
      "input_tokens": 1200,
      "output_tokens": 600,
      "total_tokens": 1800
    }
  ],
  "calls": [
    {
      "id": 1,
      "execution_id": "uuid-string",
      "ticker": "2330.TW",
      "agent_name": "technical",
      "stage": "main_analysis",
      "phase": null,
      "vrr_iteration": null,
      "call_sequence": 1,
      "provider": "gemini",
      "model": "gemini-2.5-flash",
      "system_prompt": "...",
      "user_prompt": "...",
      "raw_response_text": "...",
      "input_tokens": 500,
      "output_tokens": 200,
      "total_tokens": 700,
      "latency_ms": 2340,
      "success": true
    }
  ]
}

29.3 GET /api/mas/traces/executions/{execution_id}/calls

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。active non-admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

查詢單次執行的 LLM call list,可篩選。

參數 型別 預設 說明
agent_name string? 篩選特定 agent
stage string? 篩選特定 stage(main_analysis / vrr_review / vrr_revise / orchestrate 等)
sort string timeline 排序模式(指定 agent_name 時自動切 agent

Response: LlmCallResponse[]

29.4 GET /api/mas/traces/tickers/{ticker}

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。active non-admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

查詢某支股票的歷史執行紀錄。

參數 型別 預設 說明
started_after datetime? 起始時間下限
started_before datetime? 起始時間上限
limit int 20 每頁筆數(1-100)
offset int 0 分頁偏移

Response: 同 29.1

29.5 GET /api/mas/traces/executions/{execution_id}/summary

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。active non-admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

Token 用量明細,含 per-agent、per-stage、per-VRR-iteration breakdown。

Response:

{
  "execution_total": {
    "total_input_tokens": 3500,
    "total_output_tokens": 1700,
    "total_tokens": 5200,
    "llm_call_count": 9,
    "agent_count": 4
  },
  "per_agent": [
    { "agent_name": "technical", "call_count": 3, "input_tokens": 1200, "output_tokens": 600, "total_tokens": 1800 }
  ],
  "per_stage": [
    { "stage": "main_analysis", "call_count": 4, "input_tokens": 2000, "output_tokens": 800, "total_tokens": 2800 }
  ],
  "per_iteration": [
    { "agent_name": "technical", "vrr_iteration": 1, "call_count": 2, "input_tokens": 600, "output_tokens": 300, "total_tokens": 900 }
  ]
}

29.6 Stage 標準值

stage 說明
main_analysis Agent 主分析
vrr_review VRR 驗證
vrr_revise VRR 修正
vrr_finalize VRR 最終確認
orchestrate Orchestrator 綜合分析
verify 驗證步驟
fallback 退回路徑

30. Data Status(資料更新狀態)

30.1 GET /api/data-status

回報今日所有 pipeline 資料表的更新狀態。

Query Parameters:

參數 類型 必填 說明
date string (YYYY-MM-DD) 查詢日期,預設今日(Asia/Taipei)

Response:

{
  "report_date": "2026-03-31",
  "timestamp": "2026-03-31T17:30:00+08:00",
  "pipeline": {
    "job_name": "daily_update",
    "status": "success",
    "started_at": "2026-03-31 15:30:08",
    "ended_at": "2026-03-31 15:46:09",
    "message": "Daily update complete"
  },
  "tables": [
    {
      "name": "stock_ohlc",
      "category": "price",
      "latest_date": "2026-03-31",
      "row_count": 2103,
      "prev_date": "2026-03-30",
      "prev_count": 2130,
      "status": "fresh"
    }
  ],
  "summary": {
    "total_tables": 10,
    "fresh": 7,
    "stale": 3,
    "empty": 0
  }
}

Status 判定邏輯:

Status 條件
fresh latest_date >= report_date
stale latest_date < report_date
empty 近 7 天內無任何資料

涵蓋資料表:

Category Table Description
price stock_ohlc 股價 OHLC
price technical_indicators 技術指標
chip institutional_investors 三大法人
chip margin_balance 融資融券
chip daily_chip_snapshot 籌碼快照
fundamental valuation_metrics 估值指標
fundamental daily_financial_snapshot 財務快照
technical daily_technical_snapshot 技術快照
strategy strategy_match 策略配對
commodity commodity_ohlc 商品價格

30B. Sync Status(market × capability 觀測)

/api/data-status 的差異: - /api/data-status:job 層級 + table freshness(粗顆粒)。回答「今天 daily pipeline 跑完了沒,哪些表 stale」。 - /api/sync-status:market × capability × run_date 細顆粒。回答「taiwan 今天 chip_snapshot 跑成功嗎?us 過去 30 天哪幾天 OHLC 失敗?」

兩者並存,前端依需求挑選。

30B.1 GET /api/sync-status

回傳某一天所有 (market, capability) 的 pipeline 狀態。

Query Parameters:

參數 類型 必填 說明
date string (YYYY-MM-DD) 查詢日期,預設今日(Asia/Taipei)

Response:

{
  "report_date": "2026-05-11",
  "rows": [
    {
      "market": "taiwan",
      "capability": "ohlc",
      "ticker": null,
      "run_date": "2026-05-11",
      "status": "success",
      "error_message": null,
      "retry_count": 0,
      "updated_at": "2026-05-11T08:30:42+08:00"
    }
  ],
  "summary": {
    "total": 12,
    "success": 11,
    "failed": 1,
    "skipped": 0,
    "skipped_upstream_failure": 0
  }
}

30B.2 GET /api/sync-status/history

過濾條件全部 optional。未帶 date_from/date_to 時,預設覆蓋最近 default_history_days 天(由 config/core.yamlsync_status.default_history_days 控制,預設 30)。

Query Parameters:

參數 類型 必填 說明
market string 市場名稱(taiwan / us / japan
capability string 能力名稱(ohlc / chip_snapshot / financial_snapshot / commodity / etf_holdings
ticker string 指定 ticker;scheduler 目前只寫 market-level row(ticker=NULL),保留作為 forward-compat
date_from string (YYYY-MM-DD) 起始日(含),預設 date_to - default_history_days
date_to string (YYYY-MM-DD) 結束日(含),預設今日(Asia/Taipei)
status string (可重複) success / failed / skipped / skipped_upstream_failure。重複帶可組合:?status=failed&status=skipped

Constraints: - date_from <= date_to,否則回 400。 - (date_to - date_from) <= max_history_days(預設 365),超過回 400。

常見用法: - 單格 30 天 trend(B 模式):?market=taiwan&capability=chip_snapshot - 最近 30 天所有失敗:?status=failed - 跨市場 4 月所有未成功:?date_from=2026-04-01&date_to=2026-04-30&status=failed&status=skipped

Response:

{
  "rows": [
    {
      "market": "taiwan",
      "capability": "chip_snapshot",
      "ticker": null,
      "run_date": "2026-05-11",
      "status": "success",
      "error_message": null,
      "retry_count": 0,
      "updated_at": "2026-05-11T08:30:42+08:00"
    }
  ],
  "summary": {
    "total": 30,
    "success": 28,
    "failed": 2,
    "skipped": 0,
    "skipped_upstream_failure": 0
  }
}

Rows ORDER BY (market, capability, COALESCE(ticker, ''), run_date DESC) — same key as the matrix endpoint so per-cell rows stay contiguous when per-ticker tracking is added.


31. 籌碼面 Recap (Chip Recap)

每日籌碼面市場總覽與族群資金流向。Pipeline 每日 15:30 後產出。

31.1 GET /api/chip-recap/latest

取得最新一日的籌碼面市場摘要。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response: 同 31.2,自動查詢最新可用日期。

31.2 GET /api/chip-recap/{trade_date}

取得指定日期的籌碼面市場摘要。若該日無資料,自動 fallback 至最近可用日。

Path Parameters:

參數 類型 說明
trade_date date (YYYY-MM-DD) 交易日

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response:

{
  "trade_date": "2026-04-07",
  "requested_trade_date": "2026-04-07",
  "source_trade_date": "2026-04-07",
  "is_fallback": false,
  "market": "taiwan",
  "n_stocks": 1850,
  "total_foreign_net_value": 12345678.0,
  "total_trust_net_value": -2345678.0,
  "total_dealer_net_value": 567890.0,
  "total_market_net_value": 10567890.0,
  "total_margin_change_value": 1968695000,
  "total_short_change_value": null,
  "market_context": {
    "official_market_margin_balance_amount": 329543807000,
    "official_market_margin_change_amount": 1968695000,
    "official_market_margin_balance_lots": 8738210,
    "official_market_margin_change_lots": 23928,
    "official_market_margin_source": "FINMIND_TaiwanStockTotalMarginPurchaseShortSale",
    "official_market_margin_date": "2025-01-02",
    "official_market_margin_status": "available",
    "foreign_tx_futures_long_oi": 9023,
    "foreign_tx_futures_short_oi": 52279,
    "foreign_tx_futures_net_oi": -43256,
    "foreign_tx_futures_net_short_oi": 43256,
    "foreign_tx_futures_source": "FINMIND_TaiwanFuturesInstitutionalInvestors",
    "foreign_tx_futures_date": "2025-01-02",
    "foreign_tx_futures_status": "available"
  },
  "top_bullish_stocks": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "group_name": "AI 伺服器",
      "total_net_value": 11000000.0,
      "foreign_net_value": 10000000.0,
      "trust_net_value": 2000000.0,
      "dealer_net_value": -1000000.0,
      "flow_score": 11000000.0,
      "reasons": ["foreign_net_buy", "trust_continuous_buy"],
      "dominant_actor": "foreign"
    }
  ],
  "top_bearish_stocks": [
    {
      "ticker": "2454.TW",
      "name": "聯發科",
      "group_name": "5G",
      "total_net_value": -8250000.0,
      "foreign_net_value": -7500000.0,
      "trust_net_value": -1500000.0,
      "dealer_net_value": 750000.0,
      "flow_score": -8250000.0,
      "reasons": ["foreign_net_sell", "trust_continuous_sell"],
      "dominant_actor": "foreign"
    }
  ],
  "top_big_holder_1000_increases": [
    {
      "ticker": "9911.TW",
      "name": "千張增",
      "current_ratio": 11.0,
      "change": 1.2,
      "current_shares": 120000000,
      "shares_change": 30000000,
      "ownership_data_date": "2026-04-05",
      "ownership_data_age_days": 1,
      "levels": ["15"]
    }
  ],
  "top_big_holder_1000_decreases": [],
  "top_retail_50_increases": [],
  "top_retail_50_decreases": [
    {
      "ticker": "9914.TW",
      "name": "散戶減",
      "current_ratio": 6.0,
      "change": -0.7,
      "current_shares": 9000000,
      "shares_change": -3000000,
      "ownership_data_date": "2026-04-05",
      "ownership_data_age_days": 1,
      "levels": ["1", "2", "3", "4", "5", "6", "7", "8"]
    }
  ]
}

is_fallback=true 表示 source_trade_daterequested_trade_date 不同(使用了 fallback 日期)。

市場層級籌碼欄位:

total_margin_change_value 只代表官方/市場層級融資金額變動;沒有 summary_json.market_context.official_market_margin_status="available" 時必須是 null。後端不得由個股 margin_balancedaily_chip_snapshot.margin_change 自行加總推導大盤融資。外資台指期口數來自 FinMind TaiwanFuturesInstitutionalInvestorsdata_id=TX

欄位 類型 說明
total_margin_change_value number | null 官方/市場層級融資金額變動;pending/error/no_data 時為 null
total_short_change_value number | null 官方/市場層級融券金額變動;目前沒有官方 TWD source 時為 null
summary_json.market_context.official_market_margin_balance_amount number | null 大盤融資餘額金額,TWD
summary_json.market_context.official_market_margin_change_amount number | null 大盤融資餘額金額變動,TWD
summary_json.market_context.official_market_margin_balance_lots number | 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

融資融券口徑日期欄位:

個股融資融券來源(margin_balance)每交易日 21:30 才落地,晚於盤後其他籌碼資料。 排程在該日資料落地前不會產生籌碼快照,因此正常情況下這兩個欄位的 age 為 0 (即當日資料)。它們留在 payload 供除錯與可觀測性使用。

注意:槓桿風險引擎「資料過舊就整段跳過」的判斷(MARGIN_STALE_SKIP_AGE)發生在 寫入快照之前,讀的是 loader 當下算出的 age,不是這裡的 payload 欄位。 payload 這份是純下游副本,沒有任何流程消費它。

只有槓桿類項目帶這兩個欄位,而且兩處的存在性語意不同:

位置 帶欄位的項目 存在性
summary_json.stock_event_summary margin_surgemargin_flushshort_surge 三個分桶 無口徑資訊時整組 key 不出現
summary_json.risk_signals[] margin_up_without_institutional_supportshort_build_upinstitutional_sell_with_margin_rise key 一律存在,無值時為 null
欄位 類型 說明
margin_data_trade_date string (YYYY-MM-DD) | null 該筆融資融券數字實際所屬的交易日
margin_data_age_days integer | null 與 recap trade_date 相差的日曆天數;正常為 0

消費端一律用「key 是否存在」判斷,不可假設 key 一定在。 risk_signals[] 另外兩種訊號 —— group_inflow_but_leverage_heatingbuy_to_sell_in_leading_group —— 兩個 key 都沒有,直接 signal["margin_data_age_days"]KeyError。特別注意 group_inflow_but_leverage_heating 是第一個被加入的,常常就是 risk_signals[0]。 非槓桿分桶(continuous_buy / continuous_sell / sell_to_buy / buy_to_sell)同樣不帶這兩個欄位。

法人大買 / 法人大賣欄位:

欄位 類型 說明
top_bullish_stocks InstitutionalStockFlowItem[] 當日三大法人合計淨買超前 10 大股票,僅包含 total_net_value > 0,依金額由大到小排序;金額相同時依 ticker 穩定排序
top_bearish_stocks InstitutionalStockFlowItem[] 當日三大法人合計淨賣超前 10 大股票,僅包含 total_net_value < 0,依金額由小到大排序(賣壓最大在前);金額相同時依 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 排序

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 排序分數;目前等於 signed total_net_value
reasons string[] 後端整理的原因標籤,例如 foreign_net_buy, trust_continuous_buy, foreign_buy_to_sell
dominant_actor "foreign" \| "trust" \| "dealer" \| "mixed" 以絕對金額判定的主導法人

OwnershipChangeRankingItem

欄位 類型 說明
ticker string 股票代碼
name string | null 股票名稱
current_ratio number | null 該 bucket 最新持股比例加總;千張以上為 level 15,50 張以下為 levels 1-8
change number 該 bucket 持股比例變動加總,單位為百分點;增加榜為正,減少榜為負
current_shares integer | null 該 bucket 在 ownership_data_dateownership_distribution.shares 加總;snapshot 存在但 bucket 無對應 level rows 時為 0;整個 current snapshot 缺失時為 null
shares_change integer | null 該 bucket 股數變化,計算方式為 current ownership shares 減去同 ticker 前一個 ownership snapshot 的 shares;current 或 previous snapshot 缺失時為 null;snapshot 存在但 bucket 無對應 level rows 時以 0 計算
ownership_data_date string | null TDCC 持股分布資料日期
ownership_data_age_days number | null ownership 資料相對 chip snapshot 的新鮮度
levels string[] 實際納入計算的 TDCC levels;千張以上為 ["15"],50 張以下為 ["1", ..., "8"]

法人大買 / 法人大賣欄位存放於 daily_chip_market_summary.summary_json。Ownership 排行於讀取 daily chip payload 時從同日 daily_chip_snapshot 的 ownership 比例欄位即時計算排名,並從 ownership_distribution.shares 補上 current_shares / shares_change;若整個 current 或 previous ownership snapshot 缺失,對應股數欄位為 null;若 snapshot 存在但該 bucket 無對應 level rows,股數以 0 計算。若該日 snapshot 沒有 ownership 變動資料,四個排行欄位會回傳空陣列。

31.3 POST /api/chip-recap/{trade_date}/analyze

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

使用 LLM 對指定日期的籌碼面摘要進行分析。結果以 payload hash 快取。

Path Parameters / Query Parameters: 同 31.2

Response: MAS LLM 分析結果(含 requested_trade_date, source_trade_date, is_fallback)。

風險欄位 schemarisks 為結構化陣列(新 client 請使用此欄位),risk_summary 為衍生字串(deprecated,未來主版移除)。Legacy 快取行讀取時會自動補上 risks(單一 EvidenceItem,evidence=legacy=true),保證 response 永遠同時含兩欄。

{
  "headline": "外資主導回補",
  "market_summary": "市場偏多。",
  "group_summary": "半導體流入。",
  "stock_summary": "2330.TW 連買。",
  "etf_summary": "主動式 ETF 00981A 新增緯穎,增碼台積電 20%。被動式 ETF 無顯著指數調整。",
  "risk_summary": "留意槓桿升溫。 / 融資異常擴張。",
  "risks": [
    {"text": "留意槓桿升溫。", "evidence": "margin_change_pct=6.4"},
    {"text": "融資異常擴張。", "evidence": "margin_balance_pct=95"}
  ]
}
欄位 型別 預設值 說明
etf_summary string "" ETF 持股變動分析。主動式 ETF 調倉行為 + 被動式 ETF 指數調整。無 ETF 資料時為空字串。

31.4 GET /api/chip-recap/{trade_date}/groups

取得指定日期各族群的資金流向。新產生的 recap 以 coverage theme membership 作為後端族群來源;歷史資料若尚未重算,可能仍保留舊的 industry / sector / concept / custom 來源。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan
direction string inflow / outflow / neutral / all(預設)
group_type string 篩選族群類型
limit int 回傳筆數,預設 20,上限 200

Response:

[
  {
    "group_id": "AI伺服器",
    "group_type": "theme",
    "group_name": "AI伺服器",
    "member_count": 120,
    "active_member_count": 95,
    "foreign_net_value": 5678901.0,
    "trust_net_value": -234567.0,
    "dealer_net_value": 123456.0,
    "total_net_value": 5567790.0,
    "margin_change_value": -12345.0,
    "short_change_value": 6789.0,
    "flow_direction": "inflow",
    "flow_score": 85.5,
    "dominant_actor": "foreign",
    "representative_tickers": ["2330.TW", "2303.TW"],
    "meta_json": {}
  }
]

flow_directionflow_score 的語意(issue #1121):

  • flow_directioninflow 需同時滿足 total_net_value > 0flow_score >= mas.chip_recap.inflow_zscoreoutflow 需同時滿足 total_net_value < 0flow_score <= mas.chip_recap.outflow_zscore。 只滿足其一 → neutral
  • flow_score當日所有族群 total_net_value 的橫斷面 z-score,已減去 當日平均,因此是相對排名而非絕對方向:全市場普遍賣超的日子,賣超較少的 族群仍可能拿到正的 flow_score。判斷資金方向請看 flow_directiontotal_net_value 正負,不要單看 flow_score 或用它排序當成流入強度。

31.5 GET /api/chip-recap/{trade_date}/stocks

取得指定日期的個股籌碼事件。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan
event_type string 篩選事件類型
limit int 回傳筆數,預設 50,上限 200

Response:

[
  {
    "ticker": "2330.TW",
    "event_type": "foreign_surge",
    "event_direction": "inflow",
    "event_score": 92.0,
    "group_id": "semiconductor",
    "payload_json": {}
  }
]

31.6 GET /api/chip-recap/pulse/latest

首頁籌碼面快讀卡片。從既有 daily chip recap 壓縮出三大法人合計買超/合計賣超、主導法人、族群流向、連買連賣與風險提醒。Headline 為規則式產生,不呼叫 LLM。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response: 同 31.7,自動查詢最新可用日期。

31.7 GET /api/chip-recap/pulse/{trade_date}

取得指定日期的首頁籌碼面快讀卡片。若該日無資料,自動 fallback 至最近可用日。

Path Parameters / Query Parameters: 同 31.2。

Response:

{
  "pulse_type": "chip",
  "headline": "三大法人合計買超,外資主導,外資、投信買超,自營商賣超,資金流向半導體",
  "headline_parts": {
    "market": "三大法人合計買超",
    "actor": "外資主導",
    "participation": "外資、投信買超,自營商賣超",
    "group": "資金流向半導體"
  },
  "market_bias": "bullish",
  "market_flow": {
    "direction": "net_buy",
    "total_net_value": 18000000000.0,
    "dominant_actor": "foreign",
    "buy_breadth": 612,
    "sell_breadth": 388
  },
  "actor_flows": [
    {"actor": "foreign", "label": "外資", "direction": "buy", "net_value": 15600000000.0, "rank": 1},
    {"actor": "trust", "label": "投信", "direction": "buy", "net_value": 3200000000.0, "rank": 2},
    {"actor": "dealer", "label": "自營商", "direction": "sell", "net_value": -800000000.0, "rank": 3}
  ],
  "group_pulse": {
    "top_inflows": [],
    "top_outflows": []
  },
  "stock_pulse": {
    "continuous_buy": [],
    "continuous_sell": [],
    "sell_to_buy": [],
    "buy_to_sell": []
  },
  "risk_signals": [],
  "data_status": {
    "trade_date": "2026-05-15",
    "requested_trade_date": "2026-05-18",
    "source_trade_date": "2026-05-15",
    "is_fallback": true,
    "market": "taiwan",
    "n_stocks": 1000
  }
}

31A. 個股籌碼面 Summary

單一個股的七維籌碼面總覽(融資/融券/外資/投信/自營/400張大戶/散戶),每維輸出 direction + magnitude + comparison_period 以同一 FlowSignal schema 呈現, 方便前端直接繪製一致的視覺元件。資料來源為 daily_chip_snapshot,read-only。

31A.1 GET /api/chip/summary/{ticker}

Path parameters

參數 類型 必填 說明
ticker string 股票代號,例如 2330.TW

Query parameters

參數 類型 必填 說明
trade_date date (ISO) 若指定,回傳該日(含)前最近一筆 snapshot;預設為最新可得

Response

每個 FlowSignal 欄位固定為:

欄位 類型 說明
current number | null 當期數值
previous number | null 前一期數值
change number | null current - previous
change_pct number | null 僅 margin / short 有;其餘為 null
direction "increase" \| "decrease" \| "flat" \| "unknown" 方向判定
magnitude "large" \| "medium" \| "small" \| "none" 相對閾值分級
flow_zscore number | null 原始 clipped ±3 z-score(僅 foreign / trust / dealer 有值;其餘維度為 null)。欄位名對齊上游 daily_chip_snapshot.{actor}_flow_zscore(migration 035)
comparison_period "daily_balance" \| "daily_flow" \| "weekly" 揭露比較週期

各維度語義:

維度 comparison_period current 定義 magnitude 指標
margin / short daily_balance T 日餘額(張) |change_pct|
foreign / trust / dealer daily_flow T 日 net_shares |flow_zscore|(20d)
big_holders_400 weekly TDCC levels 12–15 ratio 加總 |change| (pct 點)
retail weekly TDCC levels 1–3 ratio 加總 |change| (pct 點)

Response example

{
  "ticker": "2330.TW",
  "trading_date": "2026-04-16",
  "margin": {
    "current": 26878, "previous": 27298, "change": -420, "change_pct": -1.5386,
    "direction": "decrease", "magnitude": "small", "flow_zscore": null, "comparison_period": "daily_balance"
  },
  "short": {
    "current": 96, "previous": 71, "change": 25, "change_pct": 35.2113,
    "direction": "increase", "magnitude": "large", "flow_zscore": null, "comparison_period": "daily_balance"
  },
  "foreign": {
    "current": -2389835, "previous": 4825960, "change": -7215795, "change_pct": null,
    "direction": "decrease", "magnitude": "small", "flow_zscore": -0.42, "comparison_period": "daily_flow"
  },
  "trust":  { "...": "..." },
  "dealer": { "...": "..." },
  "big_holders_400": {
    "current": 88.22, "previous": 88.4, "change": -0.18, "change_pct": null,
    "direction": "decrease", "magnitude": "small", "flow_zscore": null, "comparison_period": "weekly"
  },
  "retail": { "...": "..." },
  "data_freshness": {
    "ownership_age_days": 6,
    "ownership_data_date": "2026-04-10"
  },
  "meta": { "bias": "bullish", "confidence": 0.75 }
}

Error responses

  • 404ticker 無 chip snapshot
  • 422trade_date 格式非 ISO

設定來源

參數 來源 預設值
大戶門檻 config/chip_scoring.py::DEFAULT_BIG_HOLDER_THRESHOLD_LOTS 400 (張以上 = levels 12-15)
散戶門檻 config/chip_scoring.py::DEFAULT_RETAIL_THRESHOLD_LOTS 10 (< 10 張 = levels 1-3)
Magnitude 閾值 config/chip_scoring.py::DEFAULT_MAGNITUDE_THRESHOLDS 見檔案

32. 技術面 Recap (Technical Recap)

每日技術面市場總覽與族群強弱。Pipeline 每日 15:30 後產出。

32.1 GET /api/technical-recap/latest

取得最新一日的技術面市場摘要。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response: 同 32.2,自動查詢最新可用日期。

32.2 GET /api/technical-recap/{trade_date}

取得指定日期的技術面市場摘要。若該日無資料,自動 fallback 至最近可用日。

Path Parameters:

參數 類型 說明
trade_date date (YYYY-MM-DD) 交易日

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response:

{
  "trade_date": "2026-04-07",
  "requested_trade_date": "2026-04-07",
  "source_trade_date": "2026-04-07",
  "is_fallback": false,
  "market": "taiwan",
  "n_stocks": 1850,
  "advancers": 920,
  "decliners": 780,
  "breakout_count": 45,
  "breakdown_count": 32
}

summary_json.market_context(大盤 ^TWII 技術狀態,additive)新增欄位:

欄位 類型 說明
above_sma_120 bool 收盤 > 120 日均線
above_sma_240 bool 收盤 > 240 日均線
distance_to_sma_120_pct number | null 收盤與 120 日均線乖離百分比
distance_to_sma_240_pct number | null 收盤與 240 日均線乖離百分比

32.3 POST /api/technical-recap/{trade_date}/analyze

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

使用 LLM 對指定日期的技術面摘要進行分析。結果以 payload hash 快取。

Path Parameters / Query Parameters: 同 32.2

Response: MAS LLM 分析結果(含 requested_trade_date, source_trade_date, is_fallback)。

風險欄位 schema:同 31.3 — risks 為結構化陣列(新 client 請使用此欄位),risk_summary 為衍生字串(deprecated,未來主版移除)。

{
  "headline": "半導體推升收紅",
  "market_summary": "技術偏多延續。",
  "breadth_summary": "廣度支撐突破。",
  "group_summary": "半導體、金融領漲。",
  "stock_summary": "2330.TW 突破 20 日。",
  "risk_summary": "廣度偏窄,留意追高風險。",
  "risks": [
    {"text": "廣度偏窄,留意追高風險。", "evidence": "pct_above_sma_20=42; breakout_count=8"}
  ]
}

32.4 GET /api/technical-recap/{trade_date}/groups

取得指定日期各族群的技術面強弱。新產生的 recap 以 coverage theme membership 作為後端族群來源;歷史資料若尚未重算,可能仍保留舊的 industry / sector / concept / custom 來源。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan
trend_state string 篩選趨勢狀態(如 uptrend, rebound, breakout 等)
bucket string strong / weak / improving / deteriorating / all(預設)
limit int 回傳筆數,預設 20,上限 200

Response:

[
  {
    "group_id": "矽光子",
    "group_type": "theme",
    "group_name": "矽光子",
    "member_count": 120,
    "active_member_count": 95,
    "strength_score": 72.5,
    "trend_state": "uptrend",
    "pct_above_sma_20": 0.68,
    "pct_above_sma_60": 0.55,
    "avg_rsi": 58.3,
    "breakout_count": 8,
    "breakdown_count": 2,
    "representative_tickers": ["2330.TW", "2303.TW"],
    "meta_json": {}
  }
]

32.5 GET /api/technical-recap/{trade_date}/stocks

取得指定日期的個股技術事件。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan
event_type string 篩選事件類型
limit int 回傳筆數,預設 50,上限 200

Response:

[
  {
    "ticker": "2330.TW",
    "event_type": "breakout",
    "event_direction": "bullish",
    "event_score": 88.0,
    "group_id": "semiconductor",
    "payload_json": {}
  }
]

32.6 GET /api/technical-recap/pulse/latest

首頁技術面快讀卡片。從既有 daily technical recap 壓縮出市場強弱、廣度、突破/跌破、強弱族群與風險提醒。Headline 為規則式產生,不呼叫 LLM。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response: 同 32.7,自動查詢最新可用日期。

32.7 GET /api/technical-recap/pulse/{trade_date}

取得指定日期的首頁技術面快讀卡片。若該日無資料,自動 fallback 至最近可用日。

Path Parameters / Query Parameters: 同 32.2。

Response:

{
  "pulse_type": "technical",
  "headline": "技術面偏強,上漲家數占優,突破家數較多,半導體族群領漲",
  "headline_parts": {
    "market": "技術面偏強",
    "breadth": "上漲家數占優",
    "breakout": "突破家數較多",
    "group": "半導體族群領漲"
  },
  "market_bias": "bullish",
  "market_breadth": {
    "advancers": 612,
    "decliners": 388,
    "breadth_direction": "positive"
  },
  "breakout_pulse": {
    "breakout_count": 42,
    "breakdown_count": 12,
    "top_breakouts": [],
    "top_breakdowns": []
  },
  "group_pulse": {
    "strong_groups": [],
    "weak_groups": []
  },
  "risk_signals": [
    {
      "signal_type": "high_rsi_overheat",
      "scope": "stock",
      "subject": "5701.TWO",
      "subject_name": "凱崴",
      "severity": "medium",
      "message": "突破同時 RSI 偏高,短線過熱需留意。"
    }
  ],
  "data_status": {
    "trade_date": "2026-05-15",
    "requested_trade_date": "2026-05-18",
    "source_trade_date": "2026-05-15",
    "is_fallback": true,
    "market": "taiwan",
    "n_stocks": 1000
  }
}

risk_signals[] 欄位說明:

欄位 類型 說明
signal_type string 風險類型 ID(如 high_rsi_overheatfailed_breakout_riskindex_up_breadth_weak
scope string stock / group / market
subject string 對應的 ticker(stock)、族群名(group)或指數代碼(market)
subject_name string 對應的顯示名稱:個股為中文名(如「凱崴」),無對照時 fallback 為 subject
severity string low / medium / high
message string 風險敘述。不含 subject 前綴,前端可獨立用 subject_name 渲染 Link

回溯相容: 舊持久化資料可能仍以 ticker 開頭的訊息儲存且缺少 subject_name,pulse 端會自動補欄並去除前綴,前端可一律依新 schema 解析。

32.8 GET /api/technical-recap/range

多日廣度(breadth)/ 大盤 market_context 精簡時間序列,供前端畫「寬度疊大盤」sparkline。每日僅回傳精簡欄位(非整包 summary_json),以控制 payload 大小。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan
start_date date (YYYY-MM-DD) 起始日期,預設為 end_date 往前 default_lookback_days(預設 90 天,可透過 recap.technical.range.default_lookback_days 調整)
end_date date (YYYY-MM-DD) 結束日期,預設今日

錯誤:

情境 HTTP Status 說明
start_date 晚於 end_date 422 start_date must be on or before end_date.
區間超過上限 422 超過 max_range_days(預設 365 天,可透過 recap.technical.range.max_range_days 調整):Date range exceeds maximum of {max_range_days} days.
區間內無資料 200 days 回傳空陣列(非 404)

Response:

{
  "market": "taiwan",
  "start_date": "2026-07-01",
  "end_date": "2026-07-28",
  "days": [
    {
      "trade_date": "2026-07-28",
      "n_stocks": 2344,
      "advancers": 225,
      "decliners": 2015,
      "breakout_count": 18,
      "breakdown_count": 756,
      "unchanged": 104,
      "new_highs_20d": 18,
      "new_lows_20d": 756,
      "pct_above_sma_20": 15.61,
      "pct_above_sma_60": 22.78,
      "pct_rsi_above_50": 15.91,
      "breadth_state": "broad_weakness",
      "index_name": "^TWII",
      "index_close": 41603.36,
      "index_change_pct": -4.65,
      "market_regime": "risk_off"
    }
  ]
}

days[] 欄位說明:

欄位 類型 說明
trade_date date 交易日
n_stocks int 個股數
advancers / decliners int 上漲/下跌家數
breakout_count / breakdown_count int 突破/跌破家數
unchanged int | null 平盤家數(取自 breadth_summary.unchanged
new_highs_20d / new_lows_20d int | null 20 日新高/新低家數
pct_above_sma_20 / pct_above_sma_60 number | null 站上 20/60 日均線家數占比
pct_rsi_above_50 number | null RSI ≥ 50 家數占比
breadth_state string | null 廣度狀態
index_name string | null 基準指數名(取自 market_context.index_name;歷史列可能非 ^TWII,如 2026-06-12 前為 0050.TW,混用會跨尺度)
index_close number | null 大盤收盤(取自 market_context.close
index_change_pct number | null 大盤漲跌幅(取自 market_context.change_pct
market_regime string | null 大盤 regime

32.9 GET /api/technical-recap/{trade_date}/breadth/tickers

取得市場寬度卡片上某個數字背後的構成個股,讓畫面上每個數字都能展開查證。

母體: 使用 recap 自己的母體(stocks.market + is_active),不是 /api/signals/market-pulse/tickers 的(data_quality='complete' + ticker 後綴)。2026-08-04 實測兩者為 2,345 vs 2,318 列,同一條件會得到不同答案(new_highs_20d 58 vs 53),所以名單與數字必須共用母體,故另立端點。

Query Parameters:

參數 類型 必填 說明
metric string 寬度指標,見下表;未知值回 422
market string 市場,預設 taiwan
limit int 回傳筆數,預設 50,上限 200(total 不受 limit 影響)

metric 可用值: advancersdeclinersunchangednew_highs_20dnew_lows_20dbreakout_countbreakdown_countpct_above_sma_20pct_above_sma_60pct_rsi_above_50

Response:

{
  "trade_date": "2026-08-04",
  "market": "taiwan",
  "metric": "new_highs_20d",
  "total": 57,
  "items": [
    {
      "ticker": "2059.TW",
      "name": "川湖",
      "close": 9495.0,
      "change_percent": 9.9595,
      "rsi_value": 66.9156,
      "volume_ratio": 1.276684
    }
  ]
}

total 是套用 limit 前的總數,items 依當日漲跌幅絕對值排序。呼叫端若同時顯示日報快照的數字,兩者可能不同:日報是產出當時的統計,此端點為即時查詢。

32.10 GET /api/technical-recap/{trade_date}/events/summary

取得當日各技術事件類型的個股檔數,供前端畫「今天哪類事件在主導」的分布圖。

為什麼不能用日報的 summary_json.stock_event_summary 那份 payload 每個事件類型只留 recap.technical.top_event_size(預設 10)檔,用途是餵 LLM 寫日報。數它的長度等於把上限當統計值 —— 2026-08-05 實測 breakout 實際 91 檔、gap_up_breakout 超過 1,100 檔,兩者在該 payload 裡都是 10。

也不能用 /{trade_date}/stocks 自己數: 該端點 limit 上限為 200,2026-08-05 有五個事件類型打滿,真值無法從該端點取得。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

limit 參數:事件類型共 18 種(見 _bucket_events),結果集由值域本身封頂,加上限反而會漏掉尾端類型。

Response:

{
  "trade_date": "2026-08-05",
  "market": "taiwan",
  "total_events": 4340,
  "items": [
    { "event_type": "bullish_reversal_pattern", "count": 1203 },
    { "event_type": "breakout", "count": 91 },
    { "event_type": "breakdown", "count": 18 }
  ]
}
欄位 類型 說明
total_events int 各類型 count 加總,即相異的(個股, 事件類型)配對數。不是個股數 —— 一檔同時觸發五種事件會被計五次。2026-08-05 實測加總 4,340,但當日有事件的相異個股只有 2,191 檔,兩者差近一倍,因此欄名帶單位以免被當成「N 檔」渲染
items[].event_type string 事件類型
items[].count int 該類型當日個股檔數COUNT(DISTINCT ticker);此表無 (trade_date, market, ticker, event_type) 唯一鍵,用 COUNT(*) 只是碰巧成立)

itemscount 由大到小排序(同數則依 event_type),排序在後端做,避免各前端各自實作而不一致。

當日沒有任何個股觸發的事件類型不會出現在 items —— 回傳長度為當日實際有事件的類型數(2026-08-05 為 17,非 18),呼叫端不可假設固定 18 筆。當日完全無事件(如休市)回 200 且 items 為空陣列、total_events 為 0。


32A. 統一 Recaps(Unified Recaps)

認證: 無需認證(與現有 daily recap endpoint 一致)

📱 前端 / APP 整合: 另見 Recap API 前端整合指南(含 TypeScript / Swift / Kotlin code sample、UI/UX 建議、錯誤處理、空狀態設計)

統一入口,覆蓋三個 dimension × 三個 period_type 共九種組合:

dimension period_type 資料來源
technical daily daily_technical_market_summary(支援 fallback)
technical weekly / monthly periodic_technical_market_summary(精確匹配)
chip daily daily_chip_market_summary(支援 fallback)
chip weekly / monthly periodic_chip_market_summary(精確匹配)
orchestrator daily / weekly / monthly orchestrator_recap

既有 /api/technical-recap/*/api/chip-recap/* 路由保留(backward-compat),內部讀取相同的 daily 資料表。下一個主要版本會 deprecate。

GET /api/recaps/{dimension}/{period_type}/latest

回傳該 dimension + period_type 最新一筆 recap 資料。Daily 對應 MAX(trade_date);weekly/monthly/orchestrator 對應 MAX(period_end_date)(後者並以 period_type 篩選)。

Path Parameters: | 參數 | 類型 | 說明 | |------|------|------| | dimension | "technical" \| "chip" \| "orchestrator" | 維度 | | period_type | "daily" \| "weekly" \| "monthly" | 時間粒度 |

Query Parameters: | 參數 | 類型 | 必填 | 預設 | 說明 | |------|------|------|------|------| | market | string | 否 | taiwan | 市場代碼 |

Response:GET /api/recaps/{dimension}/{period_type}/{requested_date},see below。

Errors: - 400 unsupported dimension / period_type - 404 該 dimension + period_type 尚無任何資料

GET /api/recaps/{dimension}/{period_type}/{requested_date}

回傳指定日期的 recap。

  • Daily 路徑:若 requested_date 當日無資料(非交易日或 pipeline 尚未產出),自動 fallback 到 ≤ requested_date 的最新 trade_date,並在 response 中標示 is_fallback=true
  • Weekly/Monthly 路徑requested_date 必須精確等於 period_end_date;不 fallback(週期是離散的),找不到回 404。
  • Orchestrator 路徑requested_date 必須精確等於該 period_typeperiod_end_date

Path Parameters: | 參數 | 類型 | 說明 | |------|------|------| | dimension | "technical" \| "chip" \| "orchestrator" | 維度 | | period_type | "daily" \| "weekly" \| "monthly" | 時間粒度 | | requested_date | YYYY-MM-DD | 查詢日期(daily)或 period_end_date(weekly/monthly/orchestrator) |

Query Parameters: | 參數 | 類型 | 必填 | 預設 | 說明 | |------|------|------|------|------| | market | string | 否 | taiwan | 市場代碼 |

Response (technical / chip): 對應 market summary 欄位加上:

{
  "period_type": "daily|weekly|monthly",
  "market": "taiwan",
  "period_start_date": "2025-11-17",        // periodic only
  "period_end_date": "2025-11-21",          // periodic only
  "trade_date": "2025-11-21",               // daily only
  "requested_trade_date": "2025-11-25",     // daily only
  "source_trade_date": "2025-11-21",        // daily only
  "is_fallback": true,                      // daily only
  "n_sessions": 5,                          // periodic only
  "n_stocks": 1000,
  "advancers": 3000,
  "decliners": 1500,
  "breakout_count": 40,
  "breakdown_count": 15,
  "summary_json": { ... }
}

dimension=chip&period_type=dailysummary_json 另包含前端可直接渲染的法人買賣超個股清單:

欄位 類型 說明
summary_json.top_bullish_stocks InstitutionalStockFlowItem[] 當日三大法人合計淨買超前 10 大股票,僅包含 total_net_value > 0,依金額由大到小排序;金額相同時依 ticker 穩定排序
summary_json.top_bearish_stocks InstitutionalStockFlowItem[] 當日三大法人合計淨賣超前 10 大股票,僅包含 total_net_value < 0,依金額由小到大排序;金額相同時依 ticker 穩定排序

InstitutionalStockFlowItem 欄位說明請參考上方 GET /api/chip-recap/{trade_date} 章節的表格。

dimension=chip&period_type=daily 另在 top-level additive 回傳 ownership 排行欄位;既有 summary_json 不會被攤平。每個 OwnershipChangeRankingItem 也包含 current_sharesshares_change;snapshot 存在但 bucket 無對應 level rows 時以 0 計算,current 或 previous snapshot 缺失時為 null

欄位 類型 說明
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 排序

reasons 由後端產生,例如 foreign_net_buy, trust_continuous_buy, foreign_buy_to_sell,前端不需要從 stock_event_summary 自行合併推論。

Response (orchestrator): 頂層欄位為最常用的 synthesis / narrative 投影,原始 JSONB 仍可從 synthesis_json / narrative_json 取得:

{
  "period_type": "weekly",
  "period_start_date": "2025-11-17",
  "period_end_date": "2025-11-21",
  "market": "taiwan",
  "headline": "本週 AI 擴散",
  "market_synthesis": "技術突破搭配法人買超。",
  "divergence_narrative": "本週未見顯著背離。",
  "dual_confirmation_narrative": "本週技術面與籌碼面同向確認的強勢個股:\n- 台積電 (2330)(技術面 breakout|籌碼面 continuous_buy)",
  "dual_confirmation_stocks": [ { "ticker": "2330", "direction": "bullish", ... } ],
  "dual_confirmation_groups": [],
  "divergence_stocks": [],
  "risks": [ { "text": "...", "evidence": "..." } ],
  "parser_warnings": [],
  "synthesis_json": { ... },
  "narrative_json": { "dual_confirmation_narrative": "..." },
  "technical_recap_json": { ... },
  "chip_recap_json": { ... }
}

dual_confirmation_* 為程式模板輸出,逐一點名同向訊號個股/族群,無 LLM 幻覺風險。headline / market_synthesis / divergence_narrative / risks 由 LLM 生成;失敗或解析不成功時 degrade 為空字串/空陣列並記錄於 parser_warnings

Daily technical recap technical_recap_json.stock_events[*].payload_json 會帶 structured_signalsta_index_groups。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 periodic technical event payload 另提供 period_ma_events,用於辨識該週/月 K 的 MA20/MA60 站上或跌破。

Errors: - 400 unsupported dimension / period_type - 404 找不到對應 row(weekly/monthly/orchestrator)或 daily 無可 fallback 的資料

POST /api/recaps/{period_type}/{requested_date}/analyze ⭐ 單次呼叫取得三種 LLM 敘事

🔒 Auth:admin 權限(Authorization: Bearer <access_token>)。非 admin 回 403,無效 token 回 401;未帶 Authorization header 回 422。

一次回傳 technical + chip + orchestrator 三個 dimension 的 LLM 敘事,覆蓋 daily / weekly / monthly 三種週期。前端不需要再做三個串接呼叫 + orchestrator GET 拼裝。

Path Parameters: | 參數 | 類型 | 說明 | |------|------|------| | period_type | "daily" \| "weekly" \| "monthly" | 時間粒度 | | requested_date | YYYY-MM-DD | Daily 為查詢日期;weekly/monthly 為 period_end_date |

Query Parameters: | 參數 | 類型 | 必填 | 預設 | 說明 | |------|------|------|------|------| | market | string | 否 | taiwan | 市場代碼 | | response_mode | "json" \| "stream" | 否 | json | stream 改為 SSE 逐維度串流,僅支援 period_type=daily(見下方「SSE 串流模式」) |

Response (200):

{
  "period_type": "weekly",
  "period_start_date": "2025-11-17",
  "period_end_date": "2025-11-21",
  "market": "taiwan",
  "requested_date": "2025-11-21",
  "source_date": "2025-11-21",
  "is_fallback": false,
  "technical": {
    "trade_date": "2025-11-21",
    "headline": "台股技術結構多空分歧...",
    "market_summary": "...",
    "breadth_summary": "...",
    "group_summary": "...",
    "stock_summary": "...",
    "risks": [{"text": "...", "evidence": "..."}],
    "risk_summary": "...",
    "period_type": "weekly",
    "period_start_date": "2025-11-17",
    "period_end_date": "2025-11-21",
    "period_summary": "..."
  },
  "technical_error": null,
  "chip": { /* 同 technical 結構,但 chip 面向;若該 domain LLM 失敗則為 null */ },
  "chip_error": null,
  "orchestrator": {
    "period_type": "weekly",
    "period_start_date": "2025-11-17",
    "period_end_date": "2025-11-21",
    "market": "taiwan",
    "headline": "...",
    "market_synthesis": "...",
    "dual_confirmation_narrative": "本週技術面與籌碼面同向確認的強勢個股:...",
    "divergence_narrative": "...",
    "dual_confirmation_stocks": [
      {"ticker": "2330.TW", "direction": "bullish", "technical_event_type": "breakout", "chip_event_type": "continuous_buy", "combined_score": 1.95, /* ... */}
    ],
    "dual_confirmation_groups": [ /* ... */ ],
    "divergence_stocks": [ /* ... */ ],
    "risks": [ /* ... */ ],
    "parser_warnings": []
  },
  "orchestrator_error": null
}

Domain / Orchestrator isolation: 若 technical 或 chip 單一 domain LLM 失敗(例如 rate-limit),endpoint 會回傳 200 partial response:成功的 domain 正常填充,失敗的 domain 為 null,並以 technical_error / chip_error 帶公開分類錯誤(例如 rate limit exceededrequest timed outanalysis failed);若兩個 domain 都失敗則回傳 503。若 orchestrator 階段失敗(LLM 超時、rate-limit 等),endpoint 仍回傳 200,technical + chip 欄位正常填充,orchestratornull 或帶 parser warning 的 deterministic 結果,且 orchestrator_error / orchestrator.parser_warnings 帶錯誤訊息 — 前端可選擇 degrade 只顯示可用面向。原始 exception 只寫入 server log,不回傳給 client。

Side effect: Orchestrator 成功時會 UPSERT 到 orchestrator_recap table — 相當於 on-demand backfill。同一 period 之後呼叫 GET /api/recaps/orchestrator/{period_type}/{date} 可直接取得結果。

Cache: Technical / chip 走 recap_analysis_cache(同日同 period 第二次呼叫 ~150ms)。Orchestrator 每次重新 synthesize(結果 UPSERT 覆寫 — 最後一次 wins)。

Errors: - 400 unsupported period_type / response_mode,或 response_mode=stream 搭配非 daily period - 409 weekly/monthly 該期尚未 aggregated — 請先跑 scripts/run_periodic_recap.py --period-type {weekly|monthly} --period-end-date {date} - 503 MAS service 尚未初始化,或 technical + chip 兩個 domain LLM 都失敗

Example:

curl -X POST "http://localhost:8000/api/recaps/daily/2026-04-22/analyze?market=taiwan"
curl -X POST "http://localhost:8000/api/recaps/weekly/2025-11-21/analyze"

SSE 串流模式(response_mode=stream,daily only)

JSON 模式一次跑完三段 LLM 才回應,實測 40-60 秒(真實環境量測:technical 0.7s → chip 24.6s → orchestrator 47.6s),超過一般 client 的 request timeout。串流模式讓每個 dimension 算完就送出,前端可逐張卡片渲染。

Framing 與心跳與 /api/v1/mas/analyze 共用同一支 api/sse_helpers.py:事件為 event: msg\ndata: {json},靜默期每 sse_heartbeat_interval_secondsconfig/core.yaml,目前 15s)送出一則 : keep-alive comment。Response headers 帶 Cache-Control: no-cache, no-transformX-Accel-Buffering: no,避免中間 proxy 緩衝整條串流。

事件 envelope:

{
  "v": 1,
  "seq": 4,
  "request_id": "req_ab12cd34ef56",
  "period_type": "daily",
  "market": "taiwan",
  "ts": "2026-07-29T02:14:12+00:00",
  "dimension": "technical",
  "type": "result",
  "message": "",
  "payload": { }
}

type dimension payload 說明
meta system {period_start_date, period_end_date, requested_date, source_date, is_fallback} 第一則事件,供前端顯示資料日期/fallback 提示
status technical / chip / orchestrator {} 該維度開始分析
result technical / chip / orchestrator 該維度完整結構化結果(等同 JSON 模式的 .technical / .chip / .orchestrator 算完即送
error technical / chip / orchestrator {} message 為公開分類錯誤,與 JSON 模式的 *_error 相同字串
error system {} technical + chip 皆失敗
done system {} 串流結束

seq 由 1 開始嚴格遞增,request_id 全串流一致。

與 JSON 模式的行為差異(唯一一項): technical + chip 皆失敗時,JSON 模式回 503;串流模式此時 header 已送出,改以 system / error 事件表達,HTTP status 仍是 200。所有開串流之前的失敗(400 / 409 / 503 MAS service 未初始化 / 503 orchestrator LLM unavailable)都維持一般 HTTP 回應。

Example:

curl -N -X POST "http://localhost:8000/api/recaps/daily/2026-07-28/analyze?response_mode=stream&market=taiwan" \
  -H "Authorization: Bearer <access_token>"


32B. 首頁 Market Pulse

首頁聚合快讀 API。一次回傳技術面 Pulse 與籌碼面 Pulse,讓首頁不用分別解析完整 recap response。所有 headline 均為規則式產生,不呼叫 LLM。

32B.1 GET /api/market-pulse/latest

取得最新首頁市場快讀。系統會先解析技術面與籌碼面都有資料的最新共同 daily recap 日期,再用同一個 source_trade_date 讀取技術面與籌碼面,避免首頁卡片出現不同資料日期。前端顯示資料日期時應使用 top-level data_status.source_trade_date;當其中一邊資料較晚入庫時,這個日期可能早於單獨技術面或籌碼面 Pulse 的 latest 日期。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response: 同 32B.2,自動查詢最新可用日期。

32B.2 GET /api/market-pulse/{trade_date}

取得指定日期首頁市場快讀。若該日無完整聚合資料,自動 fallback 至技術面與籌碼面都有資料的最近可用日,且技術面與籌碼面共用同一個 source_trade_date。前端應顯示 top-level data_status.source_trade_date,並可用 data_status.is_fallback 判斷是否為回補日期。

Path Parameters:

參數 類型 說明
trade_date date (YYYY-MM-DD) 交易日

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response:

{
  "headline": "技術面偏強,籌碼面三大法人合計買超,外資、投信買超,自營商賣超",
  "headline_parts": {
    "technical": "技術面偏強",
    "chip": "籌碼面三大法人合計買超,外資、投信買超,自營商賣超"
  },
  "market_bias": "bullish",
  "technical_pulse": {
    "pulse_type": "technical",
    "headline": "技術面偏強,上漲家數占優,突破家數較多,半導體族群領漲"
  },
  "chip_pulse": {
    "pulse_type": "chip",
    "headline": "三大法人合計買超,外資主導,外資、投信買超,自營商賣超,資金流向半導體"
  },
  "data_status": {
    "trade_date": "2026-05-15",
    "requested_trade_date": "2026-05-18",
    "source_trade_date": "2026-05-15",
    "is_fallback": true,
    "market": "taiwan"
  }
}

32C. 統一 Daily Market

每日大盤 dashboard API。一次回傳 TAIEX 指數、官方大盤成交統計、TWSE 漲跌家數、技術 breadth、三大法人買賣超、大盤融資融券、外資台指期未平倉、market regime 與 leadership 摘要。

資料來源規則:

  • TAIEX / 成交量 / 成交值 / 漲跌家數:TWSE primary,FinMind 僅作已驗證欄位 fallback。
  • 大盤融資、融券、借券賣出、外資台指期:讀既有官方 market-chip context。
  • margin.twse_listed_maintenance_ratio上市融資維持率,百分比):value = Σ(上市個股融資張數 × 收盤價 × 1000) ÷ official_market_margin_balance_amount × 100,分子由 margin_balancesource_market='TWSE')JOIN stock_ohlc 即時聚合,分母為官方上市融資金額。scope 僅含上市(TWSE),不含上櫃(TPEx),因官方融資金額來源(FinMind)僅涵蓋上市;勿與坊間含上櫃的「大盤融資維持率」混用。自帶 statusavailable/pending_source/no_data),非台股市場回 no_data。並含 previous_day_change 子物件(previous_valuevalue_change=與前一可用交易日的百分點差(pp)directionprevious_datestatus);今日維持率不可用 / 無前一可用日 / gap>10 日 / 前日不可算時回 no_data
  • 大盤均線、關鍵均線狀態與量能結構:讀 daily_technical_snapshot^TWII benchmark row;缺資料時回 no_data,不得 fallback ETF proxy。
  • 大盤前日差異:讀 market_daily_metrics 前一個可用交易日的官方指數收盤價,回傳 direction=up|down|flat
  • 大盤成交值前日差異:讀 market_daily_metrics 前一個可用交易日的官方成交值,回傳 turnover.previous_day_change;不得用個股成交值或 volume proxy 自算。
  • 三大法人連續買賣超:讀 daily_chip_market_summary 往前計算同方向 streak,不從個股籌碼資料聚合。
  • 外資期貨部位變化:讀 market_chip_metrics 前一個可用市場層級期貨資料日。
  • 前一日/前一筆變化若資料日期間隔超過 10 個 calendar days,視為 stale gap,change 欄位回 null 並標 status: "no_data"
  • 欄位無官方/市場層級資料時回 null,並以 status 標示 pending_source / no_data / source_error;不得使用個股 rows、ETF proxy 或 stock_ohlc 的 0 volume 自算成官方資料。

32C.1 GET /api/daily-market/latest

取得最新 unified daily market payload。系統會先找 technical recap 與 chip recap 都存在的最新共同日期,再讀同日 official daily market metrics 與 regime。

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response: 同 32C.2,自動查詢最新可用日期。

32C.2 GET /api/daily-market/{trade_date}

取得指定日期 unified daily market payload。若 requested date 無完整 technical + chip recap,會 fallback 到最近共同 recap 日期,並設定 is_fallback=true

Path Parameters:

參數 類型 說明
trade_date date (YYYY-MM-DD) 查詢日期

Query Parameters:

參數 類型 必填 說明
market string 市場,預設 taiwan

Response 200 主要欄位:

欄位 類型 nullable 說明
trade_date string no 實際使用的 source date
requested_trade_date string no Client requested date
source_trade_date string no trade_date,供前端一致顯示
is_fallback boolean no 是否 fallback 至較早共同 recap 日期
index object no TAIEX OHLC/漲跌、5/20/60 日均線、關鍵均線狀態與前日差異,缺官方資料時數值為 null
turnover object no 官方大盤成交股數、成交金額、成交筆數、成交值前日差異,並附 benchmark 量能結構
market_breadth object no TWSE 整體市場/股票漲跌與漲跌停家數
technical_breadth object no 技術 recap breadth 摘要
institutional object no 官方三大法人買賣超金額與連續買賣超天數
margin object no 官方大盤融資餘額/變動,含 twse_listed_maintenance_ratio上市融資維持率)子物件
short object no 官方融券與借券賣出餘額
futures object no 外資台指期未平倉多單/空單/淨部位與淨空單變化
regime object no market_regime_daily 趨勢狀態
leadership object no 強弱族群與資金流向摘要
data_status object no 各來源可用狀態

Example:

{
  "trade_date": "2026-06-12",
  "requested_trade_date": "2026-06-12",
  "source_trade_date": "2026-06-12",
  "is_fallback": false,
  "market": "taiwan",
  "headline": "技術面偏強,籌碼面三大法人合計買超,外資、投信買超,自營商賣超",
  "market_bias": "bullish",
  "data_status": {
    "technical_recap": "available",
    "chip_recap": "available",
    "official_market_daily": "available",
    "index_moving_averages": "available",
    "index_previous_day_change": "available",
    "turnover_previous_day_change": "available",
    "turnover_volume_structure": "available",
    "institutional_streaks": "available",
    "foreign_tx_futures_position_change": "available",
    "twse_listed_margin_maintenance_ratio": "available",
    "twse_listed_margin_maintenance_ratio_previous_day_change": "available",
    "market_regime": "available"
  },
  "index": {
    "ticker": "^TWII",
    "name": "TAIEX",
    "open": 43587.63,
    "high": 44798.88,
    "low": 43587.63,
    "close": 44169.04,
    "change_points": 1019.58,
    "change_percent": 2.36,
    "source": "TWSE_MI_INDEX_IND",
    "date": "2026-06-12",
    "status": "available",
    "moving_averages": {
      "ma5": 43750.252,
      "ma20": 43568.7185,
      "ma60": 39157.4413,
      "distance_to_ma5_pct": 0.9572,
      "distance_to_ma20_pct": 1.3779,
      "distance_to_ma60_pct": 12.7986,
      "bias_rate_ma5_pct": 0.9572,
      "bias_rate_ma20_pct": 1.3779,
      "bias_rate_ma60_pct": 12.7986,
      "benchmark_ticker": "^TWII",
      "source": "daily_technical_snapshot",
      "date": "2026-06-12",
      "status": "available"
    },
    "key_ma_state": {
      "ma_alignment": "bullish",
      "price_position": "above_all",
      "above_ma5": true,
      "above_ma20": true,
      "above_ma60": true,
      "days_above_ma5": 1,
      "days_above_ma20": 1,
      "days_above_ma60": 50,
      "ma5_break_direction": "break_above",
      "ma20_break_direction": "break_above",
      "ma60_break_direction": "above",
      "benchmark_ticker": "^TWII",
      "source": "daily_technical_snapshot",
      "date": "2026-06-12",
      "status": "available"
    },
    "previous_day_change": {
      "previous_close": 43149.46,
      "close_change": 1019.58,
      "close_change_pct": 2.363,
      "direction": "up",
      "previous_date": "2026-06-11",
      "source": "TWSE_MI_INDEX_IND",
      "date": "2026-06-12",
      "status": "available"
    }
  },
  "turnover": {
    "trade_volume_shares": 12336471343,
    "trade_value_amount": 1169186958350,
    "transaction_count": 5353143,
    "source": "TWSE_MI_INDEX_MS",
    "date": "2026-06-12",
    "status": "available",
    "previous_day_change": {
      "previous_trade_value_amount": 1338516691099,
      "trade_value_change_amount": -169329732749,
      "trade_value_change_pct": -12.6505507085,
      "direction": "down",
      "previous_date": "2026-06-11",
      "source": "TWSE_MI_INDEX_MS",
      "date": "2026-06-12",
      "status": "available"
    },
    "volume_structure": {
      "current_volume": 12336471343,
      "volume_ma_5": 14258199219,
      "avg_volume_20": 15227862957,
      "volume_ratio": 0.810125,
      "volume_status": "normal",
      "benchmark_ticker": "^TWII",
      "source": "daily_technical_snapshot",
      "date": "2026-06-12",
      "status": "available"
    }
  },
  "market_breadth": {
    "market_up_count": 9333,
    "market_down_count": 2154,
    "market_unchanged_count": 399,
    "market_limit_up_count": 454,
    "market_limit_down_count": 121,
    "stock_up_count": 808,
    "stock_down_count": 203,
    "stock_unchanged_count": 57,
    "stock_limit_up_count": 42,
    "stock_limit_down_count": 4,
    "source": "TWSE_MI_INDEX_MS",
    "date": "2026-06-12",
    "status": "available"
  },
  "margin": {
    "official_market_margin_balance_amount": 557027949000,
    "official_market_margin_change_amount": 7850967000,
    "official_market_margin_balance_lots": 9088829,
    "official_market_margin_change_lots": null,
    "source": "FINMIND_TaiwanStockTotalMarginPurchaseShortSale",
    "date": "2026-06-12",
    "status": "available",
    "twse_listed_maintenance_ratio": {
      "label": "上市融資維持率",
      "scope": "twse_listed",
      "value": 193.2,
      "collateral_value": 1076153798610,
      "margin_loan_amount": 557027949000,
      "matched_stocks": 1205,
      "source": "margin_balance×stock_ohlc ÷ official_market_margin_balance_amount",
      "date": "2026-06-12",
      "status": "available",
      "previous_day_change": {
        "previous_value": 188.87,
        "value_change": 4.33,
        "direction": "up",
        "previous_date": "2026-06-11",
        "status": "available"
      }
    }
  },
  "institutional": {
    "foreign_net_amount": 28690268352,
    "trust_net_amount": 14233248854,
    "dealer_net_amount": 8942532337,
    "total_net_amount": 51866049543,
    "source": "TWSE_BFI82U",
    "date": "2026-06-12",
    "status": "available",
    "streaks": {
      "foreign": {
        "direction": "buy",
        "days": 1,
        "latest_net_amount": 28690268352,
        "start_date": "2026-06-12",
        "end_date": "2026-06-12",
        "source": "daily_chip_market_summary",
        "status": "available"
      },
      "trust": {
        "direction": "buy",
        "days": 3,
        "latest_net_amount": 14233248854,
        "start_date": "2026-06-10",
        "end_date": "2026-06-12",
        "source": "daily_chip_market_summary",
        "status": "available"
      },
      "dealer": {
        "direction": "buy",
        "days": 3,
        "latest_net_amount": 8942532337,
        "start_date": "2026-06-10",
        "end_date": "2026-06-12",
        "source": "daily_chip_market_summary",
        "status": "available"
      },
      "total": {
        "direction": "buy",
        "days": 1,
        "latest_net_amount": 51866049543,
        "start_date": "2026-06-12",
        "end_date": "2026-06-12",
        "source": "daily_chip_market_summary",
        "status": "available"
      },
      "source": "daily_chip_market_summary",
      "status": "available"
    }
  },
  "futures": {
    "foreign_tx_futures_long_oi": 9318,
    "foreign_tx_futures_short_oi": 74357,
    "foreign_tx_futures_net_oi": -65039,
    "foreign_tx_futures_net_short_oi": 65039,
    "source": "FINMIND_TaiwanFuturesInstitutionalInvestors",
    "date": "2026-06-12",
    "status": "available",
    "foreign_tx_futures_position_change": {
      "foreign_tx_futures_net_oi_change": -5039,
      "foreign_tx_futures_net_short_oi_change": 5039,
      "previous_foreign_tx_futures_net_oi": -60000,
      "previous_foreign_tx_futures_net_short_oi": 60000,
      "previous_date": "2026-06-11",
      "source": "FINMIND_TaiwanFuturesInstitutionalInvestors",
      "date": "2026-06-12",
      "status": "available"
    }
  }
}

缺資料範例:

{
  "index": {
    "ticker": "^TWII",
    "name": "TAIEX",
    "close": null,
    "source": null,
    "date": null,
    "status": "pending_source"
  },
  "turnover": {
    "trade_value_amount": null,
    "status": "pending_source",
    "previous_day_change": {
      "previous_trade_value_amount": null,
      "trade_value_change_amount": null,
      "trade_value_change_pct": null,
      "direction": null,
      "previous_date": null,
      "source": null,
      "date": null,
      "status": "pending_source"
    }
  }
}

32C.3 GET /api/daily-market/range

取得指定日期區間的大盤每日籌碼指標時間序列(精簡版)。以 market_chip_metrics 的交易日列為基礎,並排除所有輸出欄位皆為 null 的 tombstone 列;逐交易日回傳:融資餘額、融券餘額、外資台指期多單/空單/淨額、上市融資維持率、大盤成交值與大盤均線乖離率。

維持率分子(融資擔保品現值 = Σ 個股融資張數 × 收盤價 × 1000,僅上市 TWSE)以單一 set-based 聚合查詢一次計算整段,故不設區間上限

Query Parameters:

參數 類型 必填 說明
start_date date (YYYY-MM-DD) 起始日。省略時 = end_date − default_lookback_daysconfig/core.yamldaily_market.range_query.default_lookback_days,預設 30)
end_date date (YYYY-MM-DD) 結束日。省略時 = 今天
market string 市場,預設 taiwan

start_date > end_date422

Response 200 欄位:

欄位 類型 nullable 說明
market string no 市場
start_date string no 實際採用的起始日
end_date string no 實際採用的結束日
count integer no daily 筆數
daily array no 每交易日一筆,依日期升冪
daily[].trade_date string no 交易日
daily[].margin_balance_amount integer yes 融資餘額(元)
daily[].short_balance_lots integer yes 融券餘額(張)
daily[].foreign_tx_futures_long_oi integer yes 外資台指期多單(口)
daily[].foreign_tx_futures_short_oi integer yes 外資台指期空單(口)
daily[].foreign_tx_futures_net_oi integer yes 外資台指期淨額(多−空,口)
daily[].twse_listed_maintenance_ratio number yes 上市融資維持率(%)
daily[].market_index_close number yes 大盤指數(TAIEX/^TWII)收盤
daily[].market_trade_value_amount integer yes 大盤成交值(元)
daily[].bias_rate_ma5_pct number yes 大盤收盤相對 MA5 的乖離率(%),來源為 daily_technical_snapshot^TWII
daily[].bias_rate_ma20_pct number yes 大盤收盤相對 MA20 的乖離率(%),來源為 daily_technical_snapshot^TWII
daily[].bias_rate_ma60_pct number yes 大盤收盤相對 MA60 的乖離率(%),來源為 daily_technical_snapshot^TWII

各欄位僅在其來源 status == "available" 時回實值,否則回 null;外資期貨與成交值不放行 derived_available,但大盤指數收盤允許 derived_available(皆與單日端點同日同值);維持率在融資金額缺、擔保品現值為 0、或無對應個股時回 null;乖離率在同日 ^TWII 技術快照缺資料時回 null

Example: GET /api/daily-market/range?start_date=2026-06-11&end_date=2026-06-12

{
  "market": "taiwan",
  "start_date": "2026-06-11",
  "end_date": "2026-06-12",
  "count": 2,
  "daily": [
    {
      "trade_date": "2026-06-11",
      "margin_balance_amount": 549176982000,
      "short_balance_lots": 220000,
      "foreign_tx_futures_long_oi": 9000,
      "foreign_tx_futures_short_oi": 70000,
      "foreign_tx_futures_net_oi": -61000,
      "twse_listed_maintenance_ratio": 188.9,
      "market_index_close": 23149.46,
      "market_trade_value_amount": 980000000000,
      "bias_rate_ma5_pct": -0.15,
      "bias_rate_ma20_pct": 2.36,
      "bias_rate_ma60_pct": 7.11
    },
    {
      "trade_date": "2026-06-12",
      "margin_balance_amount": 557027949000,
      "short_balance_lots": 224946,
      "foreign_tx_futures_long_oi": 9318,
      "foreign_tx_futures_short_oi": 74357,
      "foreign_tx_futures_net_oi": -65039,
      "twse_listed_maintenance_ratio": 193.19,
      "market_index_close": 24169.04,
      "market_trade_value_amount": 1169186958350,
      "bias_rate_ma5_pct": 0.95,
      "bias_rate_ma20_pct": 1.38,
      "bias_rate_ma60_pct": 12.8
    }
  ]
}

32D. 全市場多面向日報分析

GET /api/recap-analysis/latest

讀取最新已預熱的全市場日報。此端點為公開、唯讀、cache-only; request 不會呼叫 LLM,也不會寫入資料庫。

回傳的日報不保證與當下原始資料完全同步。原始資料在日報產生後仍可能 被上游修正(例如隔日補抓的 ETF 持股),此端點不因此拒絕回傳;新舊改由 trade_date(日報對應的交易日)與 generated_at(產生時間)表達, 由呼叫端自行判讀。

Query Parameters:

參數 類型 必填 說明
analysis_type technical \| chip \| financial \| integrated 日報面向
market string 市場,預設 taiwan

四種面向各自回傳該面向最新一筆 period_type='daily' 的快取列 (依 period_end_date 由新到舊取第一筆)。integrated 讀自己的快取列, 不要求與 technical / chip / financial 同一交易日 —— 各面向的 trade_date 可能不同,以各自回應中的 trade_date 為準。

共同 Response envelope:

{
  "analysis_type": "financial",
  "market": "taiwan",
  "trade_date": "2026-07-28",
  "period_type": "daily",
  "generated_at": "2026-07-28T10:00:00Z",
  "analysis": {
    "trade_date": "2026-07-28",
    "headline": "全市場財務摘要",
    "market_summary": "市場整體說明",
    "growth_summary": "成長分布說明",
    "valuation_summary": "估值分布說明",
    "industry_summary": "產業強弱說明",
    "stock_summary": "代表個股說明",
    "risk_summary": "資料覆蓋與風險限制",
    "risks": []
  }
}

analysisanalysis_type 使用不同 typed schema:

類型 專屬摘要欄位
technical breadth_summary, group_summary, stock_summary
chip group_summary, stock_summary, etf_summary
financial growth_summary, valuation_summary, industry_summary, stock_summary
integrated technical_summary, chip_summary, financial_summary, alignment_summary

四種類型均包含 trade_dateheadlinemarket_summaryrisk_summaryrisksperiod_typeperiod_start_dateperiod_end_dateperiod_summary

錯誤:

  • 404 {"detail":"latest market recap analysis not available"}:該面向沒有任何 daily 快取列,或該列的 analysis_json 不符 schema、缺少時間戳。 原始資料變動不再構成 404
  • 422:缺少 analysis_type 或值不在允許集合。

33. 用戶反饋 Feedback

認證: Bearer Token(一般用戶)

POST /api/feedback

提交反饋。

Request Body:

{
  "title": "圖表載入很慢",          // string, 1-200 chars, required
  "description": "首頁技術分析圖表要等 10 秒以上"  // string, min 1 char, required
}

Response 201:

{
  "id": "uuid",
  "title": "圖表載入很慢",
  "status": "pending"
}

Error Codes: 422 validation error, 401 unauthenticated


GET /api/feedback/me

查看自己的反饋歷史(不含已封存)。

Query Parameters:

參數 類型 預設 說明
page int 1 頁碼(≥1)
page_size int 20 每頁筆數(1-100)

Response 200:

{
  "total": 5,
  "page": 1,
  "page_size": 20,
  "items": [
    {
      "id": "uuid",
      "title": "圖表載入很慢",
      "description": "...",
      "status": "pending",
      "created_at": "2026-04-01T10:00:00"
    }
  ]
}

不回傳 admin_notenotion_*reporter_emailarchived_at


34. 管理端反饋 Admin Feedback

認證: Bearer Token + Admin 權限(is_admin = true非 Admin 存取任何 endpoint → 403 ADMIN_REQUIRED

GET /api/admin/feedback

列出所有反饋(分頁、可篩選)。

Query Parameters:

參數 類型 預設 說明
page int 1 頁碼
page_size int 20 每頁筆數(1-100)
status string null 篩選 status(pending/acknowledged/resolved/ignored)
include_archived bool false 是否包含已封存反饋

Response 200: 同用戶端格式,但含 member_idreporter_emailadmin_notenotion_page_idnotion_category


GET /api/admin/feedback/{id}

查看單筆完整詳情(含 Notion export state、admin_note、archived_at)。

Response 200: 全欄位 | 404 不存在


PATCH /api/admin/feedback/{id}/status

更新 domain status。

Request Body:

{ "status": "acknowledged" }   // pending | acknowledged | resolved | ignored

Response 200: { "id": "uuid", "status": "acknowledged" } | 404 | 422 invalid status


PATCH /api/admin/feedback/{id}/note

新增/更新管理備註。

Request Body:

{ "admin_note": "已確認:iPhone 15, iOS 18.2, 4G 環境下重現" }

Response 200: { "id": "uuid", "admin_note": "..." } | 404


POST /api/admin/feedback/{id}/export-notion

匯出反饋至 Notion(3-phase race condition protection)。

Request Body:

{ "category": "bug" }    // bug | feature_request | general

Response 200: { "id": "uuid", "notion_page_id": "notion-page-abc" }

Error Codes: - 409 已匯出或匯出進行中 - 422 invalid category - 502 Notion API 失敗(已自動 rollback PENDING 狀態)


POST /api/admin/feedback/{id}/archive

Soft delete(設定 archived_at)。

Response 200: { "id": "uuid", "archived": true } | 404 不存在或已封存


35. 使用者群組 User Groups

使用者自建股票群組,可獨立生成策略規則與 match。所有 endpoint 需要 JWT 認證,僅 group owner 可存取。

POST /api/user-groups

建立新群組。

參數 類型 說明
group_name string 群組名稱(1-100 字元)
description string? 群組描述

Response 201: UserGroupResponse

GET /api/user-groups

列出認證用戶擁有的所有群組。

Response: UserGroupResponse[]

PATCH /api/user-groups/{group_id}

更新群組名稱、描述或啟用狀態。

參數 類型 說明
group_name string? 新名稱
description string? 新描述
is_active bool? 啟用/停用

Response: UserGroupResponse | 403 非 owner | 404 不存在

DELETE /api/user-groups/{group_id}

刪除群組(cascade 刪除成員關係)。

Response 204 | 403 非 owner | 404 不存在


GET /api/user-groups/{group_id}/members

列出群組中的活躍成員。

Response: UserGroupMemberResponse[]

POST /api/user-groups/{group_id}/members

新增成員。若先前已移除,新增一筆新的 membership 紀錄(保留歷史;不更新舊紀錄的 removed_at)。

參數 類型 說明
ticker string 股票代碼

Response 201: UserGroupMemberResponse | 409 已在群組中

DELETE /api/user-groups/{group_id}/members/{ticker}

軟移除成員(設定 removed_at,保留歷史紀錄)。

Response 204 | 404 不在群組中


GET /api/user-groups/{group_id}/rules

查詢群組的策略規則(scope=user_group, visibility=private)。

參數 類型 說明
source_window query 2y(預設)/ all,見 §1 通用說明
include_below_floor query true 回傳所有品質層級(預設 false,僅回傳 qualified 及舊版資料)

Response: UserGroupRuleResponse[]

欄位 類型 說明
rule_id string? 規則 ID
strategy_key string? 策略鍵
domain string? TA / CT / IR / COM
regime_slice string? all / bull / bear / range
metrics_2y object? 2y 窗口 8 項指標(hit_rate, mean_return, expectancy, sample_count, profit_factor, ...);見 §1
metrics_all object? 全期窗口 8 項指標;見 §1

GET /api/user-groups/{group_id}/matches

查詢群組的策略 match(預設 limit 200)。

參數 類型 說明
trade_date query? 日期 YYYY-MM-DD,不指定則回傳最近
source_window query 2y(預設)/ all,見 §1 通用說明
include_below_floor query true 回傳所有品質層級(預設 false,僅回傳 qualified 及舊版資料)

Response: UserGroupMatchResponse[]

欄位 類型 說明
ticker string 股票代碼
trade_date string 交易日
strategy_key string? 策略鍵
rule_id string? 規則 ID
score float? 綜合分數
metrics_2y object? 2y 窗口 8 項指標;見 §1
metrics_all object? 全期窗口 8 項指標;見 §1
rank int? 排名
is_primary bool? 是否主要規則
domain string? TA / CT / IR / COM
regime_slice string? all / bull / bear / range

UserGroupResponse

欄位 類型 說明
group_id string 群組 UUID
group_name string 群組名稱
description string? 描述
is_active bool 是否啟用
member_count int 活躍成員數
created_at string? 建立時間
updated_at string? 更新時間

UserGroupMemberResponse

欄位 類型 說明
ticker string 股票代碼
added_at string? 加入日期
removed_at string? 移除日期(null=活躍)

ETF Holdings

台股 ETF 持股查詢、歷史快照、Daily Recap 與個股反查。資料來源涵蓋 13 家投信、約 274 檔 ETF(國泰、元大、富邦、中信、群益、台新/新光、復華、野村、第一金、兆豐、統一、安聯、永豐)。

GET /api/etf-holdings/performance

查詢 ETF 在指定本期區間內的價格績效。此 endpoint 比較 ETF 自身在 stock_ohlc 的收盤價,不比較持股成分。

參數 位置 類型 預設 說明
isActive query bool false true 僅回傳主動式 A 類 ETF;false 回傳所有 enabled ETF
period query string day day 前一交易日收盤至當日/最新可得收盤、week 上週最後交易日收盤至本週至今、month 上月底/本月前最後交易日收盤至本月至今、q 上季末/本季前最後交易日收盤至本季至今、year 去年底/今年前最後交易日收盤至本年至今。其他值回傳 422
date query string 今日 anchor date,格式 YYYY-MM-DD。指定後可查該日期所屬的日/週/月/季/年

期間以台灣本地日期為準。未傳 date 時使用今日;傳入 date 時,day/week/month/q/year 都以該日期作為 anchor。period=daycurrent_close_price 為 anchor date 當日或之前最新一筆有效 stock_ohlc.closebaseline_close_pricecurrent_date 前一個有效收盤價,用來呈現單日漲跌。若 anchor date 尚無價格,current_date 會回退到最新可得價格日,並透過 effective_date(top-level)與 per-item current_date 表明實際價格日期,同時 is_fallback=truerequested_date 保留使用者指定值。period=week/month/q/yearcurrent_close_price 為 period end 當日或之前最新一筆有效 stock_ohlc.closebaseline_close_price 為 period start 之前最後一筆有效 stock_ohlc.close,因此 period=week 在週二會使用上週最後交易日作 baseline,而不是週一;period=month/q/year 也會分別使用本月/本季/今年開始前最後一個有效收盤價。有效價格會排除 NULL 與 PostgreSQL NUMERIC NaN。若某 ETF 找不到 period start 前 baseline(例如期間內才上市),則改用 period 內最早一筆有效收盤價作 baseline,呈現其上市以來於該期間的漲跌;若該 ETF 在期間內僅有單一有效價格(此時 baseline_date 等於 current_date),baseline/current 相同且報酬為 0;若沒有有效價格,該 ETF 不會出現在 etfs;若全部 ETF 都沒有有效價格,回傳 200total=0etfs=[]

Response 200

{
  "period": "week",
  "is_active_filter": true,
  "window_start": "2026-05-11",
  "window_end": "2026-05-17",
  "price_start_date": "2026-05-08",
  "effective_date": "2026-05-15",
  "requested_date": "2026-05-17",
  "is_fallback": true,
  "total": 2,
  "etfs": [
    {
      "etf_ticker": "00981A.TW",
      "etf_name": "主動式 ETF 名稱",
      "is_active_etf": true,
      "market": "taiwan",
      "baseline_date": "2026-05-08",
      "baseline_close_price": 10.15,
      "current_date": "2026-05-15",
      "current_close_price": 10.72,
      "price_delta": 0.57,
      "price_change_pct": 5.615763546798029
    }
  ]
}

Response 200(無有效價格)

{
  "period": "week",
  "is_active_filter": true,
  "window_start": "2026-05-11",
  "window_end": "2026-05-17",
  "price_start_date": null,
  "effective_date": null,
  "requested_date": "2026-05-17",
  "is_fallback": true,
  "total": 0,
  "etfs": []
}

Response Model: ETFPerformanceResponse

欄位 類型 說明
period string day / week / month / q / year
is_active_filter bool 是否只回傳主動式 ETF
window_start date 查詢區間起日
window_end date 查詢區間迄日
price_start_date date? response 內 ETF 實際最早 baseline/current 價格日;可能早於 window_start;無資料為 null
effective_date date? response 內 ETF 實際最新價格日(即「資料時間」);無資料為 null。重命名自 price_end_date
requested_date date? 使用者透過 date 參數指定的日期;未指定為 null
is_fallback bool requested_date 有帶且 effective_date != requested_date(含 effective_date 為 null)時為 true。effective_date 在所有 path 下都 <= requested_date,故 !=< 等價
total int etfs 筆數
etfs ETFPerformanceItem[] ETF 績效列表,依 price_change_pct 降序、etf_ticker 升序

Response Model: ETFPerformanceItem

欄位 類型 說明
etf_ticker string ETF 代號
etf_name string? ETF 中文名稱
is_active_etf bool 是否主動式 ETF
market string ETF market
baseline_date date period=day 為 current 前一個有效價格日;period=week/month/q/year 為 period start 前最後一個有效價格日;若無(期間內才上市)則為 period 內最早一筆有效價格日。僅當該 ETF 在期間內只有單一價格點時,baseline_date 才會等於 current_date(報酬為 0)
baseline_close_price float baseline_date 收盤價
current_date date 期間內最新有效價格日
current_close_price float current_date 收盤價
price_delta float current_close_price - baseline_close_price
price_change_pct float price_delta / baseline_close_price * 100;baseline 為 0 時回傳 0

GET /api/etf-holdings/stock-actions/ranking

跨 ETF 彙總持股異動,依個股買入/賣出估算金額排行。金額使用「ETF 持股異動股數 × 該成分股在 source date 的個股收盤價」;不是 ETF 本身成交量或 ETF 收盤價。多檔 ETF 對同一個股同方向異動會合併為同一排名列,並在 actions 保留 ETF-level 明細。

參數 位置 類型 預設 說明
date query string? 最新成功快照日 YYYY-MM-DD;有帶時使用該日或之前最新成功 ETF 持股快照,並以 is_fallback 標示是否回退。完全無 snapshot 時回 200 空 payload,不回 404
isActive query bool false true 僅回傳主動式 ETF (is_active_etf=true) 的持股異動;false 回傳所有 enabled ETF
period query string day day 保留既有「當日快照 vs 前一成功快照」;week / month / q / year 使用與 ETF 績效排行相同的 anchor window,改以 period baseline holdings vs current holdings 計算區間持股異動。非法值回 422
limit query int config etf_holdings.stock_action_ranking.default_limit 每個方向最多回傳幾筆,需介於 1 與 config max_limit

period 沒帶時預設 dayperiod=week|month|q|year 時,date 為 anchor date;未傳 date 時以最新成功 ETF 持股快照日作 anchor。window_start / window_endGET /api/etf-holdings/performance 相同:週期起日到 anchor。

逐檔基準日解析(per-ETF anchor resolution):baseline/current 日期逐檔各自解析,不使用全域單一日期的精確比對——每檔 ETF 各自的 current snapshot 為該檔在 window 內的最新成功快照,baseline snapshot 優先使用該檔在 window_start 前最新成功快照,若不存在則 fallback 到該檔在 window 內最早的成功快照(僅當該最早快照早於該檔 current 時才視為可比較,否則該檔被排除)。此設計避免任一檔在某個精確錨點日缺漏 sync 就導致整批 ETF 被排除。total_etfs 為可解析出 (baseline, current) 兩者的 ETF 數;previous_date 為這些 ETF 各自 baseline 中的最早值,effective_date 為各自 current 中的最晚值(無任何可解析 ETF 時 effective_date 退回 window 內的全域最新快照日)。個股排行 amount/current_close_price 使用該檔自己 current 日的收盤價,而非全域最新 window 日期的收盤價——兩檔 ETF 的 current 日不同時,排行計算仍各自正確。

以上為 period≠day 的規則。period=day 的基準日語意不同:baseline 必須是 day baseline(納入 ETF 中各自前一筆成功快照日的最晚值)。前一筆快照早於 day baseline 的 ETF 只能算出跨多日的累積變動,會被排除出排行、改列 stale_etfs——詳見 GET /api/etf-holdings/recap/dailyperiod=day 說明。

day 只納入「前一筆成功快照 == day baseline」的 ETF;漏掉 day baseline 的檔案無法計算單日變動,改列 stale_etfs(見下)。首次快照的 ETF 不受此限,維持全部持股列為 added

addedquantity_increased 會進入 buy_rankingsremovedquantity_decreased 會進入 sell_rankingsquantity_delta 在買入為正、賣出為負;amount 使用絕對股數計算。若成分不是 asset_type='stock'、缺少可計算股數、或找不到有效個股收盤價,該 action 不會進入排行,並在 summary 的 excluded counts 中揭露。

Response 200

{
  "period": "day",
  "window_start": "2026-04-25",
  "window_end": "2026-04-25",
  "effective_date": "2026-04-25",
  "previous_date": "2026-04-24",
  "requested_date": "2026-04-25",
  "is_fallback": false,
  "is_active_filter": false,
  "limit": 10,
  "summary": {
    "total_etfs": 2,
    "etfs_with_changes": 2,
    "buy_action_count": 3,
    "sell_action_count": 2,
    "buy_ranked_count": 2,
    "sell_ranked_count": 2,
    "excluded_missing_quantity_count": 0,
    "excluded_missing_price_count": 0,
    "excluded_non_stock_count": 0
  },
  "buy_rankings": [
    {
      "rank": 1,
      "side": "buy",
      "raw_code": "2330",
      "constituent_ticker": "2330.TW",
      "constituent_name": "台積電",
      "asset_type": "stock",
      "amount": 135000.0,
      "quantity_delta": 150.0,
      "current_close_price": 900.0,
      "etf_count": 2,
      "actions": [
        {
          "etf_ticker": "00981A.TW",
          "etf_name": "主動統一台股增長",
          "is_active_etf": true,
          "change_type": "added",
          "prev_quantity": null,
          "curr_quantity": 100.0,
          "quantity_delta": 100.0,
          "prev_weight_pct": null,
          "curr_weight_pct": 8.5,
          "weight_delta_pct": null,
          "current_close_price": 900.0,
          "amount": 90000.0
        }
      ]
    }
  ],
  "sell_rankings": []
}

Response 200(無 snapshot)

無任何成功快照(DB 完全無資料 或 period 內無資料)時,回 200 空 payload,回 404:

{
  "period": "day",
  "window_start": null,
  "window_end": null,
  "effective_date": null,
  "previous_date": null,
  "requested_date": "2099-01-01",
  "is_fallback": true,
  "is_active_filter": false,
  "limit": 10,
  "summary": {
    "total_etfs": 0,
    "etfs_with_changes": 0,
    "buy_action_count": 0,
    "sell_action_count": 0,
    "buy_ranked_count": 0,
    "sell_ranked_count": 0,
    "excluded_missing_quantity_count": 0,
    "excluded_missing_price_count": 0,
    "excluded_non_stock_count": 0
  },
  "buy_rankings": [],
  "sell_rankings": []
}

Response Model: ETFStockActionRankingResponse

欄位 類型 說明
period string day / week / month / q / year,未傳 query 時為 day
window_start date? period 查詢區間起日;day 時等於 effective_date;無 snapshot 時 null
window_end date? period 查詢區間迄日;day 時等於 effective_date;無 snapshot 時 null
effective_date date? 實際使用的 ETF 持股快照日(即「資料時間」)。逐檔 current 中的最晚值(無可解析 ETF 時退回 window 內全域最新快照日);無 snapshot 時 null。重命名自 date
previous_date date? ETF holdings diff 使用的 baseline 快照日。day 為逐檔前一成功快照中的最晚值(day baseline;baseline 更早的 ETF 不列入排行,改列 stale_etfs);period≠day 為逐檔 baseline 中的最早值;無可解析 ETF 時 null
stale_etfs StaleETF[] 無法計算單日變動而被排除出排行的 ETF(period≠day 恆為 []
requested_date date? 使用者指定的日期;未指定為 null
is_fallback bool requested_date 有帶且 effective_date != requested_date(含 effective_date 為 null)時為 true
is_active_filter bool 是否套用主動式 ETF filter
limit int 每個方向實際套用的筆數上限
summary ETFStockActionRankingSummary ETF/action/ranking/exclusion 計數;無 snapshot 時全部 0
buy_rankings ETFStockActionRankingItem[] 個股買入估算金額排行,依 amount 降序;無 snapshot 時空陣列
sell_rankings ETFStockActionRankingItem[] 個股賣出估算金額排行,依 amount 降序;無 snapshot 時空陣列

Response Model: ETFStockActionRankingSummary

欄位 類型 說明
total_etfs int 目標日有成功快照的 ETF 數。period=day 時包含被列入 stale_etfs、未實際參與比較的 ETF(即 total_etfs ≥ 參與 diff 的 ETF 數 + stale_etfs 長度);period≠day 為逐檔可解析出 baseline/current 兩者的 ETF 數
etfs_with_changes int 有持股異動的 ETF 數
buy_action_count int 買入候選 action 數,含非個股或缺資料排除者;不含不可解析為台股價格 ticker 的海外股票
sell_action_count int 賣出候選 action 數,含非個股或缺資料排除者;不含不可解析為台股價格 ticker 的海外股票
buy_ranked_count int limit 前的買入個股排行列數
sell_ranked_count int limit 前的賣出個股排行列數
excluded_missing_quantity_count int 因缺少可計算股數而排除的 action 數
excluded_missing_price_count int 可定價台股候選因缺少有效個股收盤價而排除的 action 數
excluded_non_stock_count int 因非個股資產而排除的 action 數

Response Model: ETFStockActionRankingItem

欄位 類型 說明
rank int 1-based 排名
side string buy / sell
raw_code string 成分股 raw code
constituent_ticker string? 正規化個股 ticker
constituent_name string? 成分股名稱
asset_type string 排行列預期為 stock
amount float 同一個股同方向 action 的加總估算金額
quantity_delta float 同一個股同方向 action 的加總異動股數;買入為正、賣出為負
current_close_price float? 用於金額計算的個股收盤價
etf_count int 貢獻此個股排行列的 ETF 數
actions ETFStockActionDetail[] ETF-level 買賣明細

Response Model: ETFStockActionDetail

欄位 類型 說明
etf_ticker string ETF 代號
etf_name string? ETF 名稱
is_active_etf bool 該 ETF 是否主動式
change_type string added / removed / quantity_increased / quantity_decreased
prev_quantity float? 前次持股股數
curr_quantity float? 本次持股股數
quantity_delta float 本 action 異動股數;買入為正、賣出為負
prev_weight_pct float? 前次持股權重
curr_weight_pct float? 本次持股權重
weight_delta_pct float? curr_weight_pct - prev_weight_pct,無法計算時為 null
current_close_price float 用於金額計算的個股收盤價
amount float abs(quantity_delta) * current_close_price

GET /api/etf-holdings/stock-actions/{raw_code}

個股為主體,回傳該個股在區間內買方與賣方所有 ETF 的操作明細,每檔 ETF 另附區間內的逐日操作。供「點選個股 → 展開對應 ETF 買賣明細」使用。

/stock-actions/ranking 的差異:

  • 不排名、不截斷ranking 每個方向最多回 limit(上限 200)筆,實測 period=weekbuy_ranked_count=246sell_ranked_count=281,代表榜單確實會被截斷;本 endpoint 已鎖定單一個股,不受該限制,「另一側沒有資料」在此可安全解讀為「該側無操作」。
  • 附逐日rankingprev_*/curr_* 是 window 兩端的兩點比較;本 endpoint 的 daily_actions 把該區間拆成相鄰快照之間的移動。
  • 窗語意完全相同,共用同一個 window/fallback 解析器,故兩者可由同一個期間控制驅動。
參數 位置 類型 預設 說明
raw_code path string 成分股 raw code,取自 /stock-actions/ranking 回應的 raw_code(已正規化為台股 4 碼,如 3037)。長度上限 60,對應 etf_holdings.raw_constituent_code 欄寬
date query string? 最新成功快照日 /stock-actions/ranking
isActive query bool false /stock-actions/ranking
period query string day day / week / month / q / year,語意與 window 同 /stock-actions/ranking

raw_code 的正規化daily_actions 比對 holdings 時套用與持股 loader 同一份正規化規則(RAW_CODE_NORMALIZATION_CASE_SQL)——Bloomberg 2330 TT(7 碼)與台股 ISIN TW0002330008(12 碼)都會收斂為 2330。請一律傳排行回應給的 raw_code,不要傳 provider 原始碼。

逐日的認定:一筆 daily_actions 代表該 ETF 對該個股的持有在兩個相鄰成功快照之間真的動了——持有與否改變(added / removed),或股數改變(quantity_increased / quantity_decreased)。只有 weight_pct 變動而股數不變者不列入:那是同一檔 ETF 內其他成分股變動造成的權重漂移,不是對本個股的買賣。持有與否以 holdings 列是否存在判定,不以 quantity 是否為 null 判定(只報權重的 provider 會有一列但 quantity 為 null)。

/changes/recent 的門檻差異(刻意為之,2026-08-08 定案):本 endpoint 採「股數只要不同就是變動」;/{etf_ticker}/changes/recentdetect_holdings_changes,有 QUANTITY_CHANGE_THRESHOLD_RATIO(0.1%)門檻,quantity 缺值時另以 WEIGHT_CHANGE_THRESHOLD_PCT(0.05pp)判定。

因此 0.1% 以下的微調在本 endpoint 會列出、在 changes/recent 不會。實例:00927.TW 持有 2330 於 2026-07-20 → 07-21 由 3,005,000 股增至 3,008,000 股(+0.0998%),本 endpoint 列為一筆 quantity_increasedchanges/recent 的 action 日期序列則自 07-20 直接跳至 07-22。

保留全量而不套門檻,是為了維持「daily_actions 是區間彙總的完整分解」這項性質——逐日 quantity_delta 加總恆等於該檔的區間 quantity_delta。若套上門檻,被濾掉的微調仍計入彙總,明細就會加不回總數。呼叫端若只想呈現「有意義的調倉」,請自行以 abs(quantity_delta) / prev_quantity 過濾。

逐日與彙總的對帳關係:同一檔 ETF 的 daily_actions[].quantity_delta 加總,等於該檔的區間 quantity_deltaadded 計為 +curr_quantityremoved 計為 -prev_quantity(與排行彙總共用同一份實作),故區間內進出場也對得起來。

窗內首筆的前一個快照落在 window_start 之前(即區間 baseline),與彙總的逐檔基準日解析一致;若該檔在 window 前無任何成功快照,其窗內最早一筆沒有可比較的前一筆,不產生對應的 daily_actions

Response 200

{
  "raw_code": "3037",
  "constituent_ticker": "3037.TW",
  "constituent_name": "欣興",
  "asset_type": "stock",
  "period": "week",
  "window_start": "2026-08-03",
  "window_end": "2026-08-07",
  "effective_date": "2026-08-07",
  "previous_date": "2026-07-31",
  "requested_date": null,
  "is_fallback": false,
  "is_active_filter": false,
  "buy_amount": 4910988000.0,
  "sell_amount": 143825865.0,
  "buy_etf_count": 14,
  "sell_etf_count": 2,
  "excluded_missing_quantity_count": 0,
  "excluded_missing_price_count": 0,
  "stale_etfs": [],
  "buy_actions": [
    {
      "etf_ticker": "00981A.TW",
      "etf_name": "主動統一台股增長",
      "is_active_etf": true,
      "change_type": "quantity_increased",
      "prev_quantity": 17833000.0,
      "curr_quantity": 20210000.0,
      "quantity_delta": 2377000.0,
      "prev_weight_pct": 4.87,
      "curr_weight_pct": 6.46,
      "weight_delta_pct": 1.59,
      "current_close_price": 955.0,
      "amount": 2270035000.0,
      "daily_actions": [
        {
          "as_of_date": "2026-08-03",
          "previous_date": "2026-07-31",
          "change_type": "quantity_increased",
          "prev_quantity": 17833000.0,
          "curr_quantity": 17992000.0,
          "quantity_delta": 159000.0,
          "prev_weight_pct": 4.87,
          "curr_weight_pct": 5.16,
          "weight_delta_pct": 0.29
        },
        {
          "as_of_date": "2026-08-04",
          "previous_date": "2026-08-03",
          "change_type": "quantity_increased",
          "prev_quantity": 17992000.0,
          "curr_quantity": 20210000.0,
          "quantity_delta": 2218000.0,
          "prev_weight_pct": 5.16,
          "curr_weight_pct": 6.26,
          "weight_delta_pct": 1.1
        }
      ]
    }
  ],
  "sell_actions": []
}
欄位 類型 說明
raw_code string 查詢的成分股 raw code,原樣回傳
constituent_ticker / constituent_name / asset_type string? 該個股識別資訊;區間內無任何操作時為 null
period / window_start / window_end / effective_date / previous_date / requested_date / is_fallback / is_active_filter 語意同 /stock-actions/ranking
buy_amount / sell_amount float 該方向所有 ETF 的估算金額合計
buy_etf_count / sell_etf_count int 該方向的 ETF 檔數
excluded_missing_quantity_count int 本個股因缺股數而未納入的 action 數
excluded_missing_price_count int 本個股因缺收盤價而未納入的 action 數
stale_etfs array /recap/dailyperiod≠day 恆為 []
buy_actions / sell_actions array 欄位同 rankingactions[],另加 daily_actions
buy_actions[].daily_actions array 該 ETF 在區間內的逐日操作,依 as_of_date 遞增;欄位同 /{etf_ticker}/changes/recentStockDailyAction

個股在區間內完全沒有 ETF 操作時回 200 空 payload(buy_actions / sell_actions 皆為 [],識別欄位為 null),不回 404。

GET /api/etf-holdings/{etf_ticker}

查詢單一 ETF 最新成功的持股快照。

參數 位置 類型 說明
etf_ticker path string ETF 代號(如 0050.TW00400A.TW
date query string? YYYY-MM-DD,省略為最新成功快照日。指定日期無快照時回傳 404

回傳 ETFHoldingsResponse404 表示 ETF 不存在、尚無成功快照,或指定日期無快照。

holdings[] 會對 asset_type='stock' 的成分股補上價格摘要:

  • current_close_priceas_of_date 當日或之前最近一筆有效 stock_ohlc.close
  • baseline_close_pricecurrent_close_price 前一筆有效 stock_ohlc.close
  • price_deltacurrent_close_price - baseline_close_price
  • price_change_pctprice_delta / baseline_close_price * 100

若標準化 ticker 查不到價格,會在台股上市/上櫃尾碼間 fallback(例如 5347.TW 可 fallback 查 5347.TWO)。非股票或無有效 OHLC 時價格欄位為 null

部分投信來源若提供 Bloomberg-style 台股代號(例如 2368 TT),API 會正規化為 raw_code=2368,並回填 2368.TW2368.TWO

GET /api/etf-holdings/{etf_ticker}/history

查詢歷史持股快照。

參數 位置 類型 說明
etf_ticker path string ETF 代號
start_date query string? YYYY-MM-DD,預設 end_date 前 30 天
end_date query string? YYYY-MM-DD,省略為該 ETF 最近成功快照日。無快照時回傳 404

回傳 ETFHistoryResponse

GET /api/etf-holdings/{etf_ticker}/changes

查詢單一 ETF 在某日或指定 period 相對 baseline 快照的變動明細。

參數 位置 類型 說明
etf_ticker path string ETF 代號
date query string? YYYY-MM-DD,省略為該 ETF 最近成功快照日。day period 若該日無 snapshot,會 fallback 到 ≤ requested_date 之最新 snapshot 並以 is_fallback=true 標示;該 ETF 完全無 snapshot 時回 200 空 payload,不回 404。404 僅在 etf_ticker 不存在於 etf_funds 時回傳
period query string 預設 dayday 保留既有「指定/最新快照 vs 前一成功快照」;week / month / q / year 使用與 ETF 績效排行相同的 anchor window,改以 period baseline holdings vs current holdings 計算區間持股異動。非法值回 422

period 沒帶時預設 dayperiod=week|month|q|year 時,date 為 anchor date;未傳 date 時以該 ETF 最新成功快照日作 anchor。window_start / window_endGET /api/etf-holdings/performance 相同。current snapshot 為該 ETF 在 window_end 當日或之前、且落在 window 內的最新成功快照;baseline snapshot 優先使用 window_start 前最新成功快照,若不存在則使用 window 內最早且可比較的成功快照。沒有可比較 baseline 時不會把第一筆持股全部算成買入,而是以既有 200 response shape 回傳空異動。

回傳 ETFChangesResponse。response top-level 會包含 periodwindow_startwindow_endeffective_daterequested_dateis_fallback,以及 ETF 本身在 effective_date 的價格 summary:current_close_pricebaseline_close_priceprice_deltaprice_change_pctcurrent_close_priceeffective_date 當日或之前最近一筆可用 OHLC 收盤價,baseline_close_price 為該 current 價格日期之前最近一筆可用 OHLC 收盤價。可用代表 close 不是 NULL / NaN

NoteETFChangesResponse 不再 inherit ETFRecapItemETFRecapItem.as_of_date 仍由 GET /api/etf-holdings/recap/dailyetfs[] 使用,不受本 endpoint 改名影響。

addedremovedquantity_changes 的每一筆 HoldingChange 也會包含該成分股在 effective_date 的同格式價格 summary,並使用同一套可用 OHLC 規則。成分股價格查詢會先使用 constituent_ticker exact match;若該筆是台股股票且 exact ticker 查不到,會用同一個 raw_code 嘗試 .TW / .TWO exchange suffix fallback。若 ETF 或成分股無法對應到 stock_ohlc、缺少 current 可用收盤價、缺少基準可用收盤價,或基準收盤價為 0,無法取得或無法計算的欄位會回傳 null

addedremovedquantity_changes 各自回傳穩定排序。排序先依 change type 分組;同一 group 內,有可用代表持股權重的項目會排在沒有權重的項目前面,再依代表持股權重降序:added 使用 curr_weight_pctremoved 使用 prev_weight_pctquantity_changes 使用 max(curr_weight_pct, prev_weight_pct)。接著,有可計算 weight_delta 的項目會排在沒有 delta 的項目前面,再依 |weight_delta||quantity_delta| 降序,最後依 raw_code 升序。

Response 200(正常)

{
  "etf_ticker": "0050.TW",
  "etf_name": "元大台灣50",
  "is_active_etf": false,
  "period": "day",
  "window_start": "2026-04-25",
  "window_end": "2026-04-25",
  "effective_date": "2026-04-25",
  "previous_date": "2026-04-24",
  "requested_date": "2026-04-25",
  "is_fallback": false,
  "current_close_price": 128.0,
  "baseline_close_price": 120.0,
  "price_delta": 8.0,
  "price_change_pct": 6.666666666666667,
  "added": [
    {
      "raw_code": "2317",
      "constituent_ticker": "2317.TW",
      "constituent_name": "鴻海",
      "asset_type": "stock",
      "change_type": "added",
      "prev_quantity": null,
      "curr_quantity": 200.0,
      "prev_weight_pct": null,
      "curr_weight_pct": 5.0,
      "current_close_price": 168.5,
      "baseline_close_price": 166.0,
      "price_delta": 2.5,
      "price_change_pct": 1.5060240963855422
    }
  ],
  "removed": [],
  "quantity_changes": [
    {
      "raw_code": "2330",
      "constituent_ticker": "2330.TW",
      "constituent_name": "台積電",
      "asset_type": "stock",
      "change_type": "quantity_increased",
      "prev_quantity": 900.0,
      "curr_quantity": 1000.0,
      "prev_weight_pct": 46.0,
      "curr_weight_pct": 46.5,
      "current_close_price": 872.0,
      "baseline_close_price": 864.0,
      "price_delta": 8.0,
      "price_change_pct": 0.9259259259259258
    }
  ]
}

Response 200(無 snapshot)

ETF 存在但完全無成功 snapshot(例如剛新增的 ETF 還沒抓資料),或 explicit date 之前完全無 snapshot 時,回 200 空 payload:

{
  "etf_ticker": "0050.TW",
  "etf_name": "元大台灣50",
  "is_active_etf": false,
  "period": "day",
  "window_start": null,
  "window_end": null,
  "effective_date": null,
  "previous_date": null,
  "requested_date": "2099-01-01",
  "is_fallback": true,
  "current_close_price": null,
  "baseline_close_price": null,
  "price_delta": null,
  "price_change_pct": null,
  "added": [],
  "removed": [],
  "quantity_changes": []
}

Response Model: ETFChangesResponse

欄位 類型 說明
etf_ticker string ETF 代號
etf_name string? ETF 中文名稱
is_active_etf bool 是否主動式 ETF
period string day / week / month / q / year
window_start date? period 查詢區間起日;無 snapshot 時 null
window_end date? period 查詢區間迄日;無 snapshot 時 null
effective_date date? 實際使用的持股 snapshot 日(即「資料時間」)。重命名自 as_of_date;無 snapshot 時 null
previous_date date? baseline snapshot 日
requested_date date? 使用者指定的日期;未指定為 null
is_fallback bool requested_date 有帶且 effective_date != requested_date(含 effective_date 為 null)時為 true
current_close_price / baseline_close_price / price_delta / price_change_pct float? ETF 自身 OHLC 價格 summary;無 snapshot 時 null
added / removed / quantity_changes HoldingChange[] 持股異動明細;無 snapshot 時空陣列

價格欄位

欄位 類型 說明
current_close_price float? top-level 為 etf_ticker 在 response as_of_date 當日或之前最近一筆可用 stock_ohlc.closeHoldingChange 內優先為 constituent_ticker 的同規則價格,台股股票 exact miss 時可用同 raw_code.TW / .TWO fallback
baseline_close_price float? top-level 為 etf_ticker 在 current 價格日期之前最近一筆可用 stock_ohlc.closeHoldingChange 內優先為 constituent_ticker 的同規則價格,台股股票 exact miss 時可用同 raw_code.TW / .TWO fallback
price_delta float? current_close_price - baseline_close_price
price_change_pct float? price_delta / baseline_close_price * 100

GET /api/etf-holdings/{etf_ticker}/changes/recent

查詢單一 ETF 最近 N 個成功快照交易日內,每一檔有變動個股的逐日交易動作時間軸(stock-major)。

參數 位置 類型 說明
etf_ticker path string ETF 代號(不存在於 etf_funds → 404)
days query int 欲回溯的交易日數量,範圍 1 ≤ days ≤ etf_holdings.max_recent_changes_days(預設 fallback 30)。超界 → 422

語義說明

  • 取該 ETF etf_holdings_syncstatus='success'fetched_count > 0 的最新 N 個 as_of_date。每個入選日 D_i 與其「前一個成功快照日 D_prev」(可能在視窗外更早)比對。
  • days_returned 反映實際可比 diff 筆數(== 視窗內能配到 previous_date 的快照日數量),入選快照數。例如:ETF 僅有 1 個成功快照 → days_returned=0stocks=[];首日無 prev 自動跳過。
  • 整個 N 天視窗內無 trading action(既未被增刪、quantity / weight 也未跨 threshold)的個股列入 stocks;如需完整持股清單請使用 GET /{etf_ticker}/history
  • stocks 排序:依累計 |weight_delta_pct| 降序、次依 len(actions) 降序、末依 raw_code 升序保證穩定。
  • start_date / end_date 對應 daily_diffs 首末日,無 diff 時為 null
  • response top-level 會包含 ETF 本身與 /changes 相同格式的價格 summary:current_close_price 對應 response start_dateend_date 之間最新可取得的 OHLC 收盤價,baseline_close_price 對應同一區間內最早可取得的 OHLC 收盤價。成分股價格查詢與 /changes 一樣,優先使用 constituent_ticker exact match;台股股票 exact miss 時可用同 raw_code.TW / .TWO fallback。可取得代表 close 不是 NULL / NaNprice_delta = current_close_price - baseline_close_priceprice_change_pct = price_delta / baseline_close_price * 100
  • 每檔 stocks[] 也會包含同格式的成分股價格 summary,並使用同一個 response start_dateend_date 價格區間與同一套 NULL / NaN 過濾規則。這是整個 N 日變化的價格 summary,不是逐個 actions[] 的單日變化。

Response 200(正常)

{
  "etf_ticker": "0050.TW",
  "etf_name": "元大台灣50",
  "is_active_etf": false,
  "days_requested": 5,
  "days_returned": 5,
  "start_date": "2026-05-07",
  "end_date": "2026-05-13",
  "current_close_price": 128.0,
  "baseline_close_price": 120.0,
  "price_delta": 8.0,
  "price_change_pct": 6.666666666666667,
  "stocks": [
    {
      "raw_code": "2330",
      "constituent_ticker": "2330.TW",
      "constituent_name": "台積電",
      "asset_type": "stock",
      "current_close_price": 100.0,
      "baseline_close_price": 80.0,
      "price_delta": 20.0,
      "price_change_pct": 25.0,
      "actions": [
        {
          "as_of_date": "2026-05-09",
          "previous_date": "2026-05-08",
          "change_type": "quantity_increased",
          "prev_quantity": 12000000.0,
          "curr_quantity": 12500000.0,
          "quantity_delta": 500000.0,
          "prev_weight_pct": 48.10,
          "curr_weight_pct": 48.55,
          "weight_delta_pct": 0.45
        }
      ]
    }
  ]
}

Response 200(ETF 存在但無快照)

{
  "etf_ticker": "00999.TW",
  "etf_name": "某新上市 ETF",
  "is_active_etf": false,
  "days_requested": 5,
  "days_returned": 0,
  "start_date": null,
  "end_date": null,
  "current_close_price": null,
  "baseline_close_price": null,
  "price_delta": null,
  "price_change_pct": null,
  "stocks": []
}

Response 404(ETF 不存在)

{ "detail": "ETF 'XXX.TW' not found" }

Response 422(days 越界)

{
  "detail": [
    {
      "type": "greater_than_equal",
      "loc": ["query", "days"],
      "msg": "Input should be greater than or equal to 1",
      "input": "0"
    }
  ]
}

回傳 ETFRecentChangesResponse

Response Model: ETFRecentChangesResponse
欄位 類型 說明
etf_ticker string 請求路徑帶入的 ETF 代號
etf_name string? 來自 etf_funds.etf_name
is_active_etf bool 是否為主動式 ETF
days_requested int client 傳入的 days
days_returned int 實際可比 diff 筆數(0 ≤ days_returned ≤ days_requested
start_date date? 最舊有 diff 的 as_of_date;無 diff → null
end_date date? 最新有 diff 的 as_of_date;無 diff → null
current_close_price float? ETF 在 response start_dateend_date 之間最新可取得的 OHLC 收盤價
baseline_close_price float? ETF 在 response start_dateend_date 之間最早可取得的 OHLC 收盤價
price_delta float? current_close_price - baseline_close_price
price_change_pct float? price_delta / baseline_close_price * 100
stocks StockActionTimeline[] 每檔有動作個股的 timeline
Response Model: StockActionTimeline
欄位 類型 說明
raw_code string 投信原始代碼
constituent_ticker string? 標準化 ticker
constituent_name string? 個股名稱
asset_type string stock / bond / future / ...
current_close_price float? 成分股在 response start_dateend_date 之間最新可取得的 OHLC 收盤價;台股股票 exact miss 時可用同 raw_code.TW / .TWO fallback
baseline_close_price float? 成分股在 response start_dateend_date 之間最早可取得的 OHLC 收盤價;台股股票 exact miss 時可用同 raw_code.TW / .TWO fallback
price_delta float? current_close_price - baseline_close_price
price_change_pct float? price_delta / baseline_close_price * 100
actions StockDailyAction[] as_of_date 升序
Response Model: StockDailyAction
欄位 類型 說明
as_of_date date 該動作發生日
previous_date date? 對比基準日(可能在視窗外更早)
change_type string added / removed / quantity_increased / quantity_decreased
prev_quantity float? added 時為 null
curr_quantity float? removed 時為 null
quantity_delta float? curr - prev;任一 null → null
prev_weight_pct float? added 時為 null
curr_weight_pct float? removed 時為 null
weight_delta_pct float? curr - prev;任一 null → null

GET /api/etf-holdings/{etf_ticker}/estimated-costs

查詢單一 enabled 主動式 ETF 目前持有個股的推估成本,以及各股目前成本週期內的推估買賣成本紀錄。後端會讀取全部可用歷史以找出最近一次清倉並重建目前週期,但不回傳已結束的舊週期。這是公開、唯讀 endpoint。

估算限制:本 endpoint 只有每日持股快照,沒有 ETF 實際成交價或日內交易明細。所有金額均以「日終持股數量差 × 該快照日收盤價」估算,不代表基金真實成交成本。

Request

名稱 位置 類型 Required Nullable 預設 說明
etf_ticker path string yes no N/A 完整 ETF public ticker,例如 00993A.TW;只接受 enabled 主動式 ETF
  • 不提供 days query,也沒有 60 天上限。
  • 固定讀取該 ETF 全部 status='success' AND fetched_count > 0,且 fetched_count 等於該 ETF/date 實際持久化總筆數的完整快照;筆數不一致的重抓殘留快照會被排除,排除數量由 excluded_snapshot_count 揭露。
  • 僅納入 asset_type='stock'quantity_unit='shares' 且有 .TW / .TWO canonical ticker 的成分。
  • 持股讀取沿用既有 ETF read-time canonicalization 與 dedupe,避免 ISIN、2330 TT、plain code 等歷史格式變化把同一股票拆成不同成本週期。

成本規則

  • 第一個完整成功快照已經持有的股票,以該日 exact same-day close 建立 opening_estimatebasis_status=estimated_from_first_observation。這是可觀察歷史起點的估算,不是已驗證實際買入。
  • 股票在第一個完整快照後才首次出現,且先前從未清倉時,以首次買進日 close 建立成本,basis_status=estimated_from_first_buy
  • 持股增加:新總成本 = 舊總成本 + 增加股數 × 當日 close,平均成本採移動加權平均。
  • 部分賣出:平均成本不變,總成本依剩餘股數等比例下降。
  • 持股在下一個完整成功快照缺席或明確歸零:立即視為清倉,成本、週期起日與該週期 actions 全部丟棄;若最新狀態仍為清倉,該股票不列入 stocks
  • 清倉後再買:只從再買日建立新成本週期,basis_status=estimated_from_reentry,不承接或回傳清倉前成本與 actions。
  • 買入日缺 close:該週期的總成本與平均成本保持 null,直到清倉後才可由新週期恢復。
  • 賣出日缺 close:該筆賣出金額為 null;若先前成本已知,剩餘總成本與平均成本仍可更新。
  • 同日 close 優先使用 constituent_ticker;exact ticker 缺價時,可使用同 raw_code.TW / .TWO suffix fallback。不得以前一交易日 close 補價。
  • 所有計算處理每一筆非零股數差,不套用既有持股異動畫面的 0.1% 顯示 threshold。
  • quantity 從已知變為 null 或從 null 恢復時,會寫入 direction=unknown 的資料品質 action,且 basis 維持 null,不將未知股數誤報為買賣。
  • 每筆 delta 是兩個納入成本重播的完整快照間的淨變化,金額歸於後一快照日 close;若中間有未發布或被完整性檢查排除的快照,不代表所有交易實際發生於該日。previous_date 指前一個納入的快照日,consumer 應搭配 excluded_snapshot_count 判斷歷史是否存在排除缺口。
  • 股票分割、股票股利等公司行動也可能造成 quantity 變化;目前沒有可靠事件因子可逐筆拆分,因此 action 與成本仍屬推估,不應視為基金揭露的真實成交成本。

Response 200

Top-level ETFEstimatedCostsResponse

欄位 類型 Nullable 說明
etf_ticker string no ETF public ticker
etf_name string yes ETF 名稱
is_active_etf boolean no 成功時固定為 true
first_snapshot_date date no 全歷史第一個通過完整性檢查的成功非空快照日
last_snapshot_date date no 全歷史最新通過完整性檢查的成功非空快照日
snapshot_count integer no 實際使用的完整快照數,可大於 60
excluded_snapshot_count integer no status='success'fetched_count > 0,但因持久化筆數不一致而排除的快照數
currency string no 固定 TWD
amount_basis string no 固定 holdings_quantity_delta_x_same_day_close
cost_method string no 固定 moving_weighted_average_same_day_close
stocks ETFEstimatedCostStock[] no 最新完整快照仍持有的股票;已清倉且尚未再買者不回傳
total integer no stocks 數量

ETFEstimatedCostStock

欄位 類型 Nullable 說明
raw_code string no provider 原始成分代碼
constituent_ticker string no canonical 台股 ticker
constituent_name string yes 個股名稱
asset_type string no 固定 stock
quantity_unit string no 固定 shares
is_current_holding boolean no 回傳項目固定為 true
current_quantity number yes 最新快照股數;缺量時 null
current_weight_pct number yes 最新快照權重
current_close_price number yes 最新快照日 exact close
estimated_cost_basis number yes 目前部位推估總成本;basis 不完整時 null
estimated_average_cost number yes 目前部位推估每股平均成本
basis_status string no estimated_from_first_observationestimated_from_first_buyestimated_from_reentrymissing_buy_closemissing_quantity
cost_basis_start_date date yes 目前成本週期起日
actions ETFEstimatedCostAction[] no as_of_date 升序的目前成本週期紀錄;不含最近一次清倉以前的 actions

ETFEstimatedCostAction

欄位 類型 Nullable 說明
as_of_date date no action 所屬快照日
previous_date date yes 前一成功快照日;opening estimate 為 null
record_type string no opening_estimateobserved_deltaunknown_observation
direction string no buysell 或股數資料不足時的 unknown
change_type string no initial_observationaddedincreaseddecreasedquantity_unavailablequantity_restored
previous_quantity number yes 前一快照股數
current_quantity number yes 目前快照股數
quantity_delta number yes current - previous;缺股數時 null
close_price number yes action 日 exact same-day close
estimated_trade_amount number yes abs(quantity_delta) × close_price
amount_status string no availablemissing_closemissing_quantity
estimated_position_cost_after number yes action 後的推估總成本
estimated_average_cost_after number yes action 後的推估平均成本

範例:

GET /api/etf-holdings/00993A.TW/estimated-costs
{
  "etf_ticker": "00993A.TW",
  "etf_name": "主動安聯台灣",
  "is_active_etf": true,
  "first_snapshot_date": "2026-05-19",
  "last_snapshot_date": "2026-07-23",
  "snapshot_count": 39,
  "excluded_snapshot_count": 0,
  "currency": "TWD",
  "amount_basis": "holdings_quantity_delta_x_same_day_close",
  "cost_method": "moving_weighted_average_same_day_close",
  "stocks": [
    {
      "raw_code": "3443",
      "constituent_ticker": "3443.TW",
      "constituent_name": "創意",
      "asset_type": "stock",
      "quantity_unit": "shares",
      "is_current_holding": true,
      "current_quantity": 33000,
      "current_weight_pct": 1.44,
      "current_close_price": 4380,
      "estimated_cost_basis": 144230000,
      "estimated_average_cost": 4370.606060606061,
      "basis_status": "estimated_from_reentry",
      "cost_basis_start_date": "2026-07-22",
      "actions": [
        {
          "as_of_date": "2026-07-22",
          "previous_date": "2026-07-21",
          "record_type": "observed_delta",
          "direction": "buy",
          "change_type": "added",
          "previous_quantity": 0,
          "current_quantity": 31000,
          "quantity_delta": 31000,
          "close_price": 4370,
          "estimated_trade_amount": 135470000,
          "amount_status": "available",
          "estimated_position_cost_after": 135470000,
          "estimated_average_cost_after": 4370
        }
      ]
    }
  ],
  "total": 1
}

Status/Error

  • 200:成功;active ETF 只有債券/非支援資產時 stocks=[]
  • 400:ETF 存在,但不是 enabled 主動式 ETF。
  • 404:ETF 不存在,或沒有任何通過筆數完整性檢查的成功非空持股快照。
  • 422etf_ticker path 格式不合法。
  • 500:沿用全域 error envelope,不回傳 SQL 或內部 exception。

GET /api/etf-holdings/recap/daily

所有 ETF 持股變動彙總(預設為最近成功快照日)。

參數 位置 類型 說明
date query string? YYYY-MM-DD,省略為最近成功快照日(非當日)。提供日期時回傳該日(含)以前最近成功快照;若該日前無快照則回傳 404
active_only query bool True 僅列主動式 ETF(代號 A 結尾)
period query enum day(預設)/ week / month / q / year。與 /stock-actions/rankingperiod 同語意、同 window,單一期間控制可同時驅動兩者

回傳 DailyRecapResponse

period=day(單日視圖)previous_dateday baseline —— 納入 ETF 中各自前一筆成功快照日的最晚值,亦即多數 ETF 實際比對的那一天。只有 baseline 等於 day baseline 的 ETF 會被 diff。某檔 ETF 的前一筆成功快照早於 day baseline 時(其 provider 未在前一交易日發布快照,例如 allianz 的 NavDate 停滯數日),它只能產生跨多日的累積變動,混入單日視圖會灌水,因此不列入 etfs,改由 stale_etfs 回報(見下方 StaleETF)。該檔完全沒有更早成功快照時(即該檔在 date 是首次快照),不視為 stale,全部持股視為 added(維持既有行為)。沒有任何 ETF 有 baseline 時 previous_datenull

period≠day(區間視圖):走與 /stock-actions/ranking 相同的 window 解析與逐檔基準日解析——每檔 ETF 的 current 為該檔在 window 內的最新成功快照,baseline 為該檔在 window_start 前的最新成功快照。此路徑必定能為每檔解析出 baseline,故不會有 stale ETF,stale_etfs 恆為 []previous_date 為納入 ETF 中各自 baseline 的最早值(與 day path 相反:區間比的是「window 起點之前」的存量,逐檔 baseline 不需一致)。每筆 ETFRecapItemas_of_date / previous_date該檔自己的 current / baseline 日期。持股是存量非流量,window 中間缺任何一天的快照都不影響區間淨變動。

每一筆 ETFRecapItem 的 top-level 會包含該 ETF 自身的價格 summary(current_close_pricebaseline_close_priceprice_deltaprice_change_pct),其 addedremovedquantity_changes 內每一筆 HoldingChange 也會包含該成分股的同格式價格 summary。價格 target 為 response date(recap 目標快照日):current_close_price 為該日當日或之前最近一筆可用 stock_ohlc.closebaseline_close_price 為該 current 價格日期之前最近一筆可用收盤價,可用代表 close 不是 NULL / NaN。成分股查詢先用 constituent_ticker exact match;台股股票 exact miss 時可用同 raw_code.TW / .TWO exchange suffix fallback。無法對應 stock_ohlc、缺 current、缺 baseline、或 baseline 為 0 時,無法取得或計算的欄位回傳 null。此價格規則與 GET /{etf_ticker}/changes 完全一致。

排序差異(注意):與 GET /{etf_ticker}/changes 不同,recap/dailyaddedremovedquantity_changes 不套用 impact 排序,維持 build_daily_recap 原始輸出順序。此為刻意保留既有行為,前端如需排序請自行於 client 端處理。

Response Models

ETFHoldingItem

欄位 類型 說明
raw_code string 投信原始代碼(保證金 / 現金統一為 CASH:CCY / MARGIN:CCY
constituent_ticker string? 標準化 ticker,僅股票型標的填入
constituent_name string? 持股名稱
asset_type string stock / bond / future / cash / etf / reit / other
weight_pct float? 權重 %
quantity float? 股數 / 口數 / 面額
quantity_unit string? shares / contracts / amount / units
current_close_price float? 成分股在快照日或之前最新有效 stock_ohlc.close
baseline_close_price float? 成分股在 current_close_price 前一筆有效 stock_ohlc.close
price_delta float? current_close_price - baseline_close_price
price_change_pct float? price_delta / baseline_close_price * 100

ETFHoldingsResponse

欄位 類型 說明
etf_ticker string ETF 代號
etf_name string? ETF 中文名稱
is_active_etf bool 是否主動式 ETF
as_of_date date 快照日期
holdings ETFHoldingItem[] 持股明細,依權重降序
total int 持股檔數

HoldingChange

欄位 類型 說明
raw_code string 投信原始代碼
constituent_ticker string? 標準化 ticker
constituent_name string? 持股名稱
asset_type string 資產類型
change_type string added / removed / quantity_increased / quantity_decreased
prev_quantity float? 前一日股數
curr_quantity float? 當日股數
prev_weight_pct float? 前一日權重
curr_weight_pct float? 當日權重
current_close_price float? 成分股在 response as_of_date 當日或之前最近一筆有效 stock_ohlc.close
baseline_close_price float? 成分股在前一個 OHLC 交易日的 stock_ohlc.close
price_delta float? current_close_price - baseline_close_price
price_change_pct float? price_delta / baseline_close_price * 100

ETFRecapItem

欄位 類型 說明
etf_ticker string ETF 代號
etf_name string? ETF 中文名稱
is_active_etf bool 是否主動式 ETF
as_of_date date? 快照日期(實際資料日,不一定是當日)
previous_date date? 該檔自己的比對基準日。period=day 時等於 response 的 day baseline;period≠day 時為該檔在 window_start 前的最新成功快照日
current_close_price float? ETF 在 response as_of_date 當日或之前最近一筆有效 stock_ohlc.close
baseline_close_price float? ETF 在前一個 OHLC 交易日的 stock_ohlc.close
price_delta float? current_close_price - baseline_close_price
price_change_pct float? price_delta / baseline_close_price * 100
added HoldingChange[] 新增的持股
removed HoldingChange[] 移除的持股
quantity_changes HoldingChange[] 股數變動超過 0.1% 的持股

DailyRecapResponse

欄位 類型 說明
date date 目標日期
previous_date date? 比對基準日。period=day 為納入 ETF 中各自前一筆成功快照日的最晚值(day baseline,多數 ETF 實際比對的那一天;baseline 更早的 ETF 改列 stale_etfs);period≠day 為納入 ETF 中各自 baseline 的最早值(逐檔基準日解析,見上方 recap/daily 說明)
active_only bool 是否僅列主動式 ETF
period enum 本次 recap 的期間(day / week / month / q / year
window_start date? 期間 window 起日(day 時等於 date
window_end date? 期間 window 迄日(day 時等於 date
etfs ETFRecapItem[] 有變動的 ETF
stale_etfs StaleETF[] 資料未更新、無法計算單日變動而被排除的 ETF(period≠day 恆為 []
summary DailyRecapSummary 統計摘要

StaleETF

僅出現在 period=day。該 ETF 在 missing_date(day baseline)沒有成功快照——其 provider 未在該日發布持股 (例如 allianz 的 NavDate 停滯),且該 provider 只提供「當前」快照、無歷史查詢,故此缺口無法回補。 它最新的成功快照停在 previous_date,只能算出跨多日的累積變動,因此不列入 etfs 與排行。

欄位 類型 說明
etf_ticker string ETF 代號
etf_name string? ETF 中文名稱
previous_date date 該檔最新一筆成功快照日(早於 day baseline)
missing_date date 缺少快照的那一天,即 day baseline

DailyRecapSummary

欄位 類型 說明
total_etfs int 當日有成功快照的 ETF 總數。stale_etfs 內未參與比較的 ETF——「有變動」與「資料未更新」之外的差額才是「無變動」
etfs_with_changes int 其中有變動的 ETF 數

StockInfo 新增欄位

GET /api/stocks/{ticker} 回傳的 StockInfo 新增 etfs 陣列:個股被哪些 ETF 持有(取每檔 ETF 最新成功快照),元素為 StockETFInfo { etf_ticker, etf_name?, weight_pct? }。空陣列代表該股票不在任何 ETF 持股中。


26. Observability — Access Log

每一個進入 cortex-api 的 HTTP / SSE 請求都會被 AccessLogMiddleware 攔截,產生一筆完整紀錄寫入 PostgreSQL api_access_log 表。由 scheduler 每日 03:15 (Asia/Taipei) 清理;retention / cron 設定都在 config/core.yamllogging.access_log 區段。

保留天數依部署環境而異,由 scheduler 容器的 DEPLOY_ENV 環境變數對應 logging.access_log.retention_days_by_env

DEPLOY_ENV 保留天數
dev 7
sit 14
uat 14
prod 180

DEPLOY_ENV 未設定、無法辨識或設定值有誤時,退回 logging.access_log.retention_days(180,即 prod 值)並記一筆 WARNING——清理是不可逆的 DELETE,因此失敗方向一律取「保留較多」。此變數與 auth 硬化用的 ENVIRONMENT 是兩個獨立變數,不可混用。

X-Request-ID Header

  • 每一個 response 都會帶 X-Request-ID(16 字元 hex string)。
  • Client 可在 request 端自行提供 X-Request-ID,server 會沿用同一個值;缺則自動產生。
  • 前端在錯誤回報時建議把這個 ID 一併帶上 → 後端 grep / DB 查詢都能對齊單筆紀錄。

敏感資料遮蔽

  • request body / response body / request headers 中,凡是 key 名稱含 password, token, authorization, access_token, refresh_token, client_secret, api_key, jwt, secret, session_id, cookie(不分大小寫、忽略 _-)一律 mask 為 ***
  • 非 JSON 文字 body 中若含 Bearer xxx,會被替換為 Bearer ***
  • mask 清單由 config/core.yamllogging.access_log.mask_keys 控制。

GET /api/admin/access-logs

管理員專用查詢端點。需要管理員權限(members.is_admin = true),非管理員回 403

Query parameters

參數 類型 說明
from_ts datetime 起始時間(含),ISO 8601
to_ts datetime 結束時間(不含)
status int 精確比對 HTTP status code
status_gte int HTTP status code 下界(含),範圍 100..599
status_lte int HTTP status code 上界(含),範圍 100..599
min_duration_ms float 最短耗時(含),單位毫秒且必須是有限值、>= 0duration_ms IS NULL 不符合此條件
order_by enum timestamp_desc(預設)或 duration_desc
method string GET / POST / ...(大寫)
path string 子字串比對 request path(LIKE %<path>%
member_id UUID 過濾特定會員
request_id string 精確查 X-Request-ID
limit int 預設 50,上限 200
offset int 分頁 offset,預設 0

所有參數都可組合,條件間採 AND。status 仍是精確比對;若同時帶級距參數,也必須同時符合。status_gte > status_lte 或任一參數超出規則時回 422

排序規則:

  • timestamp_desctimestamp DESC, id DESC,維持既有行為。
  • duration_descduration_ms DESC NULLS LAST, timestamp DESC, id DESC。相同耗時以較新的紀錄優先,確保分頁穩定。

常用 drill-down:

GET /api/admin/access-logs?from_ts=2026-07-01T00:00:00%2B08:00&to_ts=2026-08-01T00:00:00%2B08:00&status_gte=500&status_lte=599&order_by=timestamp_desc&limit=50&offset=0
GET /api/admin/access-logs?from_ts=2026-07-01T00:00:00%2B08:00&to_ts=2026-08-01T00:00:00%2B08:00&min_duration_ms=842.7&order_by=duration_desc&limit=50&offset=0

Response

{
  "total": 12345,
  "items": [
    {
      "id": 9001,
      "request_id": "a3f2c8d1b4e5f6a7",
      "timestamp": "2026-05-12T03:00:00.020000+00:00",
      "request_started_at": "2026-05-12T02:59:59.990000+00:00",
      "response_completed_at": "2026-05-12T03:00:00.010000+00:00",
      "method": "GET",
      "path": "/api/stocks/2330.TW",
      "query_string": null,
      "status_code": 200,
      "duration_ms": 12.4,
      "client_ip": "127.0.0.1",
      "transport": "http",
      "member_id": "uuid-of-member-or-null",
      "member_email": "caller@example.com-or-null",
      "member_display_name": "Caller Name-or-null",
      "request_body": "",
      "response_body": "...full masked body...",
      "response_bytes": 1234,
      "request_headers": {"Authorization": "***", "Accept": "application/json"},
      "error_class": null,
      "error_message": null
    }
  ]
}

timestamp 是 access-log 背景 writer 建立/持久化 row 的時間,發生在 response 完成之後,且可能受 queue 延遲影響;它不是請求開始或回應完成時間。請使用 request_started_atresponse_completed_at 顯示真實的「請求時間/回應時間」,不可由 timestamp ± duration_ms 當成真實值。這兩欄自 migration 117 起才收集,因此歷史 row 為 null;未正常送出 final response body 的例外紀錄,其 response_completed_at 也可能為 null

Response wrapper 維持 { "total", "items" }total 是套用全部過濾條件後的筆數,即使 offset 已超出最後一頁仍會回正確 total。

transport 欄位值:http(一般 HTTP 與 SSE/CSV streaming — middleware 自動 stream-tap 抓取完整 body)。(歷史上曾有 ws 值供 WebSocket session 使用;/ws/* 端點已於 #630 移除。)

member_email / member_display_namemember_id 左連接 members 取得,供 API monitor 顯示可讀使用者。下列情況會是 null:未帶 Bearer token、token 無效或過期、CORS preflight OPTIONS、公開 endpoint、或歷史 log 指到已刪除/不存在的 member。

退化情境

  • 若 DB 寫入失敗或佇列已滿(預設 10000),row 會被寫入 logs/access_log_fallback.jsonl(jsonline 格式),事後可手動 ingest 回 DB。Request 不會因為 access log 失敗而 block。
  • 非 JSON 的 binary chunk 若無法 utf-8 decode 會被寫成 <binary, N bytes> 占位字串。

26B. Server Metrics

GET /api/admin/server-metrics

管理員專用的 server 管理摘要。需要有效的管理員 Bearer access token;非管理員回 403

Query parameters

參數 類型 預設 說明
from_date YYYY-MM-DD 今日 Asia/Taipei 起始日(含)
to_date YYYY-MM-DD 今日 Asia/Taipei 結束日(含)
endpoint_limit int 10 熱門 endpoint 筆數,範圍 1..20
  • 未提供日期時查詢台北今日。
  • 只提供其中一個日期時,該日期同時作為起訖日。
  • 日期範圍最多 31 個日曆日;起日晚於訖日或超過上限時回 422
  • 查詢以 [from_date 00:00, to_date + 1 day 00:00) 計算,因此跨月與月底不需特殊處理。

目前上線定義

users.online_now 完全以 token session 判定,不使用「最近五分鐘有請求」等活動推估。符合下列條件的 distinct member 才列入:

  • refresh_tokens.revoked_at IS NULL
  • access_token_expires_at > generated_at
  • refresh token 的 expires_at > generated_at
  • token session 已建立,且會員狀態為 active

valid_access_token_sessions 是符合相同條件的 session 數,因此同一會員多裝置登入時可能大於 online_now。後端只保存 refresh token hash 與 access token 到期時間,不保存 access bearer token 明文。

目前 token policy 為:

  • access token:60 分鐘
  • refresh token:30 天

logged_in_todaylogged_in_this_weeklogged_in_this_monthlogged_in_in_range 是期間內曾簽發 token 的 distinct member 數;為保留登入歷史,已輪替或撤銷的 session 仍會納入期間統計。

連線人數定義

上面所有欄位都源自 refresh_tokens,因此只看得到登入者——未登入的呼叫端根本沒有 token 紀錄。connected_*_in_range 改以 api_access_log 為來源,涵蓋兩者,並以查詢區間(同 query_range,上界會截到 generated_at)為範圍,不做「現在是否在線」判定:

欄位 定義
connected_members_in_range 區間內 member_id 非 NULL 的 distinct member 數
connected_anonymous_ips_in_range 區間內 member_id IS NULL 的 distinct client_ip
connected_total_in_range 上述兩者相加

這三個數字是人數的上界,不是精確人數。 兩半可能重疊:public 端點(test_endpoint_auth_gating.py::PUBLIC_ROUTE_PATHS)無 token 即可呼叫,已登入會員的呼叫端若在這些請求上省略 Authorization header,同一人會同時出現在兩半。

早期版本曾用 anti-join 扣除「同區間內也出現在已登入請求上的 IP」試圖去重,該做法已於 #1242 移除:anti-join 的作用域是查詢區間本身,導致聚合非單調——某 IP 若在區間頭幾天匿名、後幾天登入,放大區間反而會讓總數下降,逐日圖表與週月彙總互相矛盾。且該去重只在同一 IP 成立,同一人換網路仍算兩個。以一個壞掉的聚合換取部分去重不划算,故改為保留重疊並如實記載。

現行保證:放大查詢區間,三個數字都不會下降test_widening_the_range_never_lowers_a_count 釘住)。但仍不可加總——COUNT(DISTINCT) 本質不可加,逐日數字相加不低於該期間的實際區間查詢結果(各日呼叫端完全不重疊時才相等)。

已知限制:

  • 匿名端以 IP 為單位,不是人:同一 NAT/公司網路後方的多人會被算成 1;同一人切換 4G/Wi-Fi 會被算成 2。系統沒有匿名 session id,IP 是請求唯一攜帶的穩定識別。
  • connected_total_in_range 混合了兩種單位:distinct member(人)加 distinct IP(位址)。一位會員用三台裝置貢獻 1,一位匿名者換三個網路貢獻 3,因此登入比例改變時,總數的移動方向取決於呼叫端組成:同一人原本用 N 個位址、登入後收斂成 1,總數下降;但 NAT 後方多人共用一個位址時,其中一人登入會把原本合併的 1 拆成「1 位會員 + 該位址其餘的人」而使總數上升。不可當趨勢指標。當標題數字使用前請理解此性質。
  • 不檢查會員目前狀態,但也不是不變量online_nowJOIN members 並要求 status = 'active'connected_members_in_range 則否——帳號被停權仍計入。但刪除會員會改變歷史數字:api_access_log.member_idON DELETE SET NULL(migration 051),該員過去的請求會脫離 member 半邊:client_ip 非 NULL 的列改由匿名半邊計(若該位址尚未被計過),client_ip IS NULL 的列則兩半皆不計,總數可能淨減。同一組查詢參數在刪除前後會得到不同結果。
  • logging.access_log.exclude_paths(預設 /health//docs/openapi.json/redoc)不寫入 access log,只造訪這些路徑的呼叫端不會被計入。
  • 保留期限依環境而異api_access_logaccess_log_cleanup 排程清理,天數取自 logging.access_log.retention_days_by_env,以 DEPLOY_ENV 選定——dev 7 天、SIT/UAT 14 天、prod 180 天。超出保留期的區間查不到連線紀錄;在 SIT 上這代表只能回溯兩週。
  • client_ip 的真實性取決於部署:SIT 由 FORWARDED_ALLOW_IPS 讓 uvicorn 解析 Cloudflare Tunnel 的 X-Forwarded-For,記到的是真實客戶端 IP;本機 dev compose 未設該變數,所有請求會塌成同一個 docker gateway 位址,匿名計數在 dev 無參考價值。

Response 範例

{
  "generated_at": "2026-07-30T08:30:00Z",
  "timezone": "Asia/Taipei",
  "windows": {
    "day_start": "2026-07-30T00:00:00+08:00",
    "week_start": "2026-07-27T00:00:00+08:00",
    "month_start": "2026-07-01T00:00:00+08:00"
  },
  "query_range": {
    "from_date": "2026-07-29",
    "to_date": "2026-07-30",
    "from_ts": "2026-07-29T00:00:00+08:00",
    "to_ts_exclusive": "2026-07-31T00:00:00+08:00",
    "day_count": 2
  },
  "users": {
    "online_now": 42,
    "valid_access_token_sessions": 51,
    "logged_in_today": 80,
    "logged_in_this_week": 240,
    "logged_in_this_month": 620,
    "logged_in_in_range": 135,
    "connected_members_in_range": 128,
    "connected_anonymous_ips_in_range": 940,
    "connected_total_in_range": 1068
  },
  "token_policy": {
    "access_token_ttl_minutes": 60,
    "refresh_token_ttl_days": 30,
    "online_source": "persisted_access_token_expiry"
  },
  "api_quality": {
    "request_count": 12000,
    "success_count": 11700,
    "client_error_count": 220,
    "server_error_count": 60,
    "incomplete_count": 20,
    "success_rate_percent": 97.5,
    "client_error_rate_percent": 1.83,
    "server_error_rate_percent": 0.5,
    "error_rate_percent": 2.5,
    "requests_per_minute": 4.1667,
    "response_bytes": 9876543,
    "latency_sample_count": 11980,
    "avg_latency_ms": 42.1,
    "p50_latency_ms": 18.0,
    "p95_latency_ms": 110.0,
    "p99_latency_ms": 280.0
  },
  "top_endpoints": [
    {
      "method": "GET",
      "path": "/api/stocks/2330.TW",
      "request_count": 1600,
      "server_error_count": 3,
      "incomplete_count": 0,
      "server_error_rate_percent": 0.19,
      "avg_latency_ms": 35.2,
      "p95_latency_ms": 92.0
    }
  ],
  "telemetry": {
    "latest_access_log_at": "2026-07-30T08:29:59Z",
    "ingestion_lag_seconds": 1.0,
    "access_log_writer_running": true,
    "access_log_queue_depth": 2,
    "access_log_queue_capacity": 10000
  }
}

API 品質只統計所選日期範圍內、且不晚於 generated_atapi_access_log

  • 2xx / 3xx → success
  • 4xx → client error
  • 5xx → server error
  • status_code IS NULL → incomplete
  • 沒有 request 時 rate 與 RPM 為 0,latency percentile 與 average 為 null
  • error_rate_percent 包含 client error、server error 與 incomplete

telemetry.latest_access_log_at 是全表截至 generated_at 的最新 log,用來計算 ingestion lag,不受日期範圍限制。writer 未啟動時 access_log_writer_running=false,queue 數值為 0