跳轉到

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. 統一策略 Strategy
  27. 原物料 Commodities
  28. MAS Execution Trace
  29. Data Status 30B. Sync Status
  30. 籌碼面 Recap
  31. 技術面 Recap 32B. 首頁 Market Pulse 32C. 統一 Daily Market
  32. 用戶反饋 Feedback
  33. 管理端反饋 Admin Feedback
  34. 使用者群組 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

由管理員直接設定會員的有效 tier。member_id 必須是 UUID;格式錯誤會回 422。目標 tier 必須存在且 is_active = true。此操作只更新 members.current_tier_id 並寫入 audit log,不會建立或修改 subscription/付款紀錄;相同 tier 的重複 request 回 changed=false 且不重複寫 audit。

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、必填欄位或欄位長度不合法

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)

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

Response (200):

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

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

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


GET /api/stocks/{ticker}

取得單一股票資訊。

Response — StockInfo:

{
  "ticker": "2330.TW",
  "name": "台積電",
  "english_name": "TSMC",
  "market": "taiwan",
  "industry": "半導體業",
  "sector": "資訊科技",
  "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)

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

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"
  }
]

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 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 起生效——舊呼叫方若依賴巢狀欄位,會拿到空值而非錯誤。

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,
    "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",
        "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(本端點不呈現衝突組主/從區別)。
  • 同一檔若當日觸發同組多個 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/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。無效 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 說明:

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。無效 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。無效 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.

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
    },
    "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,
    "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 產生。

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。

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}

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


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/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 以目前 close 對前一個可用交易日 close,month / year 以目前 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-06-01",
      "baseline_value": 2200.0,
      "unrealized_pnl": 200.0,
      "unrealized_pnl_pct": 9.090909
    },
    "year": {
      "label": "year",
      "baseline_date": "2026-01-02",
      "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-06-01",
      "baseline_value": 1100.0,
      "unrealized_pnl": 100.0,
      "unrealized_pnl_pct": 9.090909
    },
    "year": {
      "label": "year",
      "baseline_date": "2026-01-02",
      "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-06-01",
          "baseline_value": 1100.0,
          "unrealized_pnl": 100.0,
          "unrealized_pnl_pct": 9.0909
        },
        "year": {
          "label": "year",
          "baseline_date": "2026-01-02",
          "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-06-01",
          "baseline_value": 1100.0,
          "unrealized_pnl": 100.0,
          "unrealized_pnl_pct": 9.0909
        },
        "year": {
          "label": "year",
          "baseline_date": "2026-01-02",
          "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-06-01",
          "baseline_value": 950.0,
          "unrealized_pnl": 50.0,
          "unrealized_pnl_pct": 5.2632
        },
        "year": {
          "label": "year",
          "baseline_date": "2026-01-02",
          "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、standalone/unattributed dividends

非 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": "資訊科技",
      "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 資料時,pricenull


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

系統健康檢查。

Response:

{
  "status": "healthy",
  "database": true,
  "llm": true,
  "agents": true
}

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。

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 快照。與上述 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 Story Set 日期;預設為該 ticker 在已發布快照內的最新日
rule_version query SES rule version,預設為當前發布版本

錯誤: 404 Story Set 不存在;503 已發布快照不可用或該世代資料不完整。

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

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

欄位 類型 說明
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": {}
  }
]

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
}

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 解析。


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 (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 - 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"


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
    }
  ]
}

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/{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/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 表。預設保留 180 天,由 scheduler 每日 03:15 (Asia/Taipei) 清理;retention / cron 設定都在 config/core.yamllogging.access_log 區段。

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
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

Response

{
  "total": 12345,
  "items": [
    {
      "id": 9001,
      "request_id": "a3f2c8d1b4e5f6a7",
      "timestamp": "2026-05-12T03:00:00+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
    }
  ]
}

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> 占位字串。