Cortex Investment Agent API — 前端串接技術文件
Base URL:
http://<host>:<port>API Version: v1 Protocol: REST (JSON) + SSE Authentication: Bearer Token (JWT)
目錄
- 通用說明
- 認證 Auth
- 會員 Members 3B. 管理端會員 Admin Members
- 訂閱 Subscriptions
- 股票資料 Stocks
- 技術指標 Indicators
- 三大法人 Institutional
- 融資融券 Margin
- 股權分散表 Ownership
- 基本面 Fundamentals
- 策略訊號 Signals
- 每日分析 Daily Analysis
- 訊號回測 Signal Backtest
- MAS 多代理分析
- Agent Payloads
- 自選股 Watchlist
- 投資組合 Portfolio
- 族群 Groups 18B. 統一個股清單 Stock Lists
- 策略規則 Rules
- 個股規則 Stock Rules
- 黑天鵝階段 Black Swan
- 供應鏈分析 Supply Chain
- 更新紀錄 Updates
- Health Check
- 錯誤處理
- 統一策略 Strategy
- 原物料 Commodities
- MAS Execution Trace
- Data Status 30B. Sync Status
- 籌碼面 Recap
- 技術面 Recap 32B. 首頁 Market Pulse 32C. 統一 Daily Market
- 用戶反饋 Feedback
- 管理端反饋 Admin Feedback
- 使用者群組 User Groups
1. 通用說明
CORS
Fail-closed allow-list(非全開放)。後端只允許 config/core.yaml 的
api.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.yaml 的 api.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-Security、X-Content-Type-Options: nosniff、
X-Frame-Options: DENY、Referrer-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 不會改變排序。
- 傳入 2y 或 all 以外的值 → 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 為StoryMetrics(event_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:google、apple;其他值(含 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。設為 suspended 或 deleted 時會撤銷該會員未撤銷的 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 名稱;去空白並轉小寫,例如 free、advance、premium |
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):
rows 與 results 內容相同,都是去重後的 MarketSurveillanceEventResponse objects。
{
"results": [
{
"ticker": "2330.TW",
"name": "台積電",
"english_name": "TSMC",
"market": "taiwan",
"industry": "半導體業",
"surveillance_status": "normal"
}
],
"total": 1
}
surveillance_status 代表台股官方監管狀態,值為 normal、attention、disposition。非台股或查無監管事件時回傳 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 | 否 | — | attention 或 disposition |
| status_type | string | 否 | — | status 的別名 |
| exchange | string | 否 | — | twse 或 tpex |
| 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 }
}
}
rows 與 results 內容相同;保留兩者是為了支援較口語的 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 |
disposition 或 attention |
| status_type | string | 否 | — | status 的別名;若兩者同時存在,以 status 優先 |
| exchange | string | 否 | — | twse 或 tpex |
| 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 | 否 | — | twse 或 tpex |
| 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 | twse 或 tpex |
| 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.yaml的indicators.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 | foreign、trust、dealer、total |
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):
type:cash(除息)/ stock(除權)/ both(除權息)。
fill_status(ex_dividend 與 ex_rights 各一):not_due(未到)/ filled(已填,最新收盤 ≥ 除權息前收盤)/ partial(部分填,介於參考價與前收盤)/ discount(貼,低於參考價)/ unknown(無可比價)。
reference_price(除權息參考價)、各 fill_status、cash_yield 皆由本地 OHLC 即時推導(預告表來源無價格欄位)。ex_dividend 於純除權時為 null、ex_rights 於純除息時為 null。
reference_price 採 TWSE 官方全公式 (前收 - 現金股利 + 認股比例 × 認購價) / (1 + 配股率 + 認股比例)。
現金增資(rights issue): 預告表的「除權/除權息」同時涵蓋配股與現金增資。ex_rights 額外帶 subscription_ratio(認股比例)與 subscription_price(認購價格);subscription_ratio > 0 即代表含現金增資成分。純現金增資指紋為 type="stock" 且 stock_dividend=0 且 subscription_ratio>0(前端應顯示「現金增資」而非配股)。當認購價尚未公告時,無法計算參考價,reference_price 為 null。
[
{
"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 registrysesi_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 即時會員等級為 advance 或 premium。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 即時會員等級為 advance 或 premium。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 即時會員等級為 advance 或 premium。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 only — signal -> 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_codevalues above (e.g.accumulation,chip_inst_flow,fin_growth_quality) are illustrative placeholders and are not guaranteed to resolve via theconfig/sesi_names.yamlregistry; thenameshown for them is representative only. In real responses each code is a live SESI code whosenamecomes from the registry.
qryDate 語義:
- 未帶:各面向查各自最新 trading_date(可能不同日)
- 有帶:嚴格同日查詢,不 fallback 到其他日期
- 三面向全無資料:404
- 指定日某面向無資料:該面向回 null
- 每個面向物件都使用 replacement signals / events / stories / relations。Deprecated aliases(如 signal_type、story_type、structured_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,允許 20 到 252 |
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=distorted 或 reason=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_type、story_reasons、chip_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[].price;false 時省略 |
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 資料則為 null。price 若無最新 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.yaml 的 portfolio.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_lots 時 sum(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 只是候選範圍;還必須 ticker、action、sell_type、price_per_share、quantity、fee、tax 都在合理 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[] 欄位包含 index、transaction_id、code、severity、message、item;item 會回傳原 request index、ticker、action、transaction_date、quantity,供前端告知使用者。
- 明確提供 closes_lots 時仍採 validate-first。任一 validation / explicit closure 錯誤時,整批 rollback。
GET /api/portfolio/transactions
列出交易紀錄。每筆 row 回傳的欄位包含 day_trade_pair_id 與 day_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.tw 以 2330.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_quantity:max(quantity - matched_quantity, 0);buy row 為 null。
- is_warning:action="sell" 且 unmatched_quantity != 0(完全關閉的 sell、或 buy,皆為 false)。
- warning_code / warning_reason:is_warning=true 時固定為 "unmatched_sell_lot" 與上例的固定文字;否則皆為 null。此 warning 意義與 POST /api/portfolio/transactions 回傳的巢狀 warning(見上方,同樣使用 code="unmatched_sell_lot")相同,但這裡是攤平在 row 上的獨立表示法,非 schema 不一致。
- warning_details:is_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=id→INSERT新 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_pnl、day_trade_realized_pnl、total_realized_pnl、unrealized_pnl、avg_buy_price、cost_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=Group 且 industry 缺失時,改用 stocks.sector。
- type=Group 且 stocks 查無資料或 industry / sector 都是空值,歸入 Unclassified。
- type=ETF:使用 etf_funds.etf_ticker 判斷 ETF,回傳 etf 與 stock 兩種 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_pct 以 market_value / total_market_value * 100 計算,四捨五入到小數第 4 位。
- total_cost_basis 為所有 active holdings 的成本基礎加總,可作為「投入成本」口徑。
- groups 依 market_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。ticker與type同時提供會回 HTTP 422。
若某期間任一 open holding 缺少 current 或 baseline close,該期間的 baseline_date、baseline_value、unrealized_pnl、unrealized_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_value、cost_basis_total、total_unrealized_pnl、positions_count為各 group 加總。 - grouped top-level
as_of_date為 groups 中最大as_of_date;若無 active holdings 則為null。 groups依market_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/totalexpected_cash_dividend(不進 realized/total)unrealized_pnl/total_pnl/capital_employed/return_pct/equity_multipleopened_lots_count/closed_lots_countdata_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_quality;groups[] 另有 category/category_source/tickers。groups[].tickers 為該期間內實際有操作的個股子集(cohort 明細 closures/dividends/lots 標的+區間內未配對賣出),非全歷史成員;period=all 等同全部成員。完整 schema 與 example 請參考 Frontend integration SSOT。
GET /api/portfolio/investment-performance/details
回傳單一操作績效 metric 的可對帳逐筆明細。必填 metric:regular_pnl/day_trade_pnl/cash_dividend/expected_cash_dividend/realized_total/unrealized_pnl/capital_employed/opened_lots_count/closed_lots_count/total_pnl。
period/value/ticker 沿用單期 API。分類 drill-down 必須同時傳 type/category/category_source,且與 ticker 互斥;backend 以 current metadata 驗證分類並解析 tickers。offset 預設 0;limit 預設 100、最大 1000。
Response 包含 window/scope/metric/unit/value/reconciliation/pagination/items/data_quality。value 是權威 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_date、end_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_dividend。status="received" 代表 ex_date <= today,status="pending" 代表未來除息/除權日;目前資料表沒有實際發放日,因此不以發放日判斷。除權配股另以 position_qty * stock_dividend 估算股數,不換算進現金 *_amount。
Metrics Naming Specification
所有 portfolio endpoints 的損益欄位命名與語意統一如下。這些規則保證「非重疊加總」—— realized_pnl 與 day_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 資料時為 null,include_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 資料時,price 為 null。
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 只回傳 stock 與 price,不包含 legacy endpoint 裡的 industry、sector、策略欄位、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[].price;false 時 price 固定為 null 且不查 price window |
limit |
int | 200 | 每頁筆數,上限 1000 |
offset |
int | 0 | 分頁位移 |
searchType / searchID 對應:
| searchType | searchID 來源 | 認證 |
|---|---|---|
theme |
/api/themes 的 theme_id,對應 coverage_theme_membership.theme |
不需要 |
group |
/api/groups 的 group_id(建議)或 exact group_name,對應 stock_group.group_id / stock_group.group_name |
不需要 |
watchlist |
/api/watchlists/groups 的 id,必須是 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=0、items=[] |
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=watchlist 但 searchID 不是 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
共用篩選參數
以下參數可用於 catalog、threshold-summary、threshold-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[].price;false 時省略 |
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 資料時,price 為 null。
POST /api/admin/themes/generate
啟動 admin-only 題材生成 job。這支 API 會用 coverage RAG / web search / LLM
產生題材草稿,不會直接把 LLM 結果寫成正式題材;正式套用需呼叫 publish。
job 由 API process 內的 background task 執行;若 process 重啟,執行中的 job
不會續跑,startup recovery 會把超過 theme_generation.jobs.stale_running_minutes
的 running / pending job 標記為 failed。
web / 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_rag、web、coverage_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: pending、running、completed、failed、published、canceled。
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 .../members 的 ThemePageEditResponse。
{ticker} path 參數僅接受 4-6 位數字,其他格式回傳 422。
題材頁不存在、或 ticker 不在頁上回傳 404。
DELETE /api/admin/themes/{theme}/members/{ticker}
刪除指定成員行,更新計數,空分節整節移除。
刪除衍生成員(無 (人工) 標記,即 build_themes 會在 rebuild 時重新推導出
的成員)時,除了移除該行,還會把 ticker 追加到頁面的
**人工排除:** [[DDDD]] ... 行(無此行則建立;位置緊接 **相關主題:** 行之後,
無相關主題行時緊接 **涵蓋公司數:** 區塊之後),使刪除在下次 build_themes
rebuild 後仍生效;此時 excluded_for_rebuild 回 true。刪除手動成員
((人工) 標記,屬 admin 新增、非衍生)僅移除該行、不寫排除行,
excluded_for_rebuild 回 false。
**人工排除:** 行以裸 ticker wikilink 表示,語法對齊語料 build_themes;ETL
_parse_themes 顯式跳過該行、不將其 wikilink 當成成員回灌(#840)。
Query Parameters:
| 參數 | 類型 | 必填 | 預設 | 說明 |
|---|---|---|---|---|
sync_db |
bool | no | false | 是否於寫檔後同步 DB |
Response (200): 同 POST .../members 的 ThemePageEditResponse(衍生成員刪除時
excluded_for_rebuild 為 true)。
{ticker} path 參數僅接受 4-6 位數字,其他格式回傳 422。
題材頁不存在、或 ticker 不在頁上回傳 404。
GET /api/theme-recaps/latest
取得 Theme Recap 頁面的初載 read model。資料來源以最新
coverage_theme_membership.knowledge_as_of 的題材成員為主,再把成員股票
join 到 strategy_match、daily_chip_stock_event、
daily_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_rate 與 expected_return
仍來自 strategy_match 的策略績效欄位;策略資料缺漏時可為 null。
這支 API 不使用 daily_technical_group_summary.group_id 或
daily_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_ratio。foreign_net_value、trust_net_value、dealer_net_value
來自成員股籌碼事件 payload 的法人買賣超金額加總;來源資料缺漏時仍可能為
null。pct_above_sma_20 是 0-100 的百分比;rsi_neutral_ratio 與
macd_bullish_ratio 是 0-1 比例。member_ranking[].price 使用
stock_ohlc 取 source_trade_date 之前最近交易列、前一交易列、第 5 個回看
交易列、第 20 個回看交易列計算收盤價、價差與百分比漲跌幅。chip.margin
與 member_ranking[].margin 使用 margin_balance 取每檔成員股
trade_date <= source_trade_date 的最新一筆資料;題材層的 margin_change_pct
與 short_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_status:success(全部成功)/ 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_all為StoryMetrics(event_count、average_return?、up_count、average_up_return?、down_count、average_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: domain、story_code、story_name?、direction?、horizons: StoryHorizonMetrics[]
ExactStorySetOverview: story_set_hash、story_count、story_keys[]、horizons: StoryHorizonMetrics[]
StoryHorizonMetrics: days(horizon 天數,如 1/5/10/20/40/60)、stock: {metrics_2y, metrics_all}、market: {metrics_2y, metrics_all}
StoryMetrics: event_count、average_return?、up_count、average_up_return?、down_count、average_down_return?
GET /api/strategy/story/recommendations
依 2 年 evidence 排序的 Story-first 策略推薦,結果按 12 個 strategy_key(long/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 | 每個策略回傳上限,1–100,預設 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_key、direction(long/short)、horizon、total、items: StoryRecommendationItem[]
StoryRecommendationItem: rank、ticker、stock_name?、score、candidate_count、current_story_set、selected_evidence
SelectedStoryEvidence: type(single_story/exact_story_set)、domain?、story_code?、story_set_hash?、story_keys?、event_count、hit_count、hit_rate、directional_average_return、stock_metrics_2y、market_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 | 資料來源:fred 或 yfinance |
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_id 查 strategy_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.yaml 之 sync_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_date與requested_trade_date不同(使用了 fallback 日期)。
市場層級籌碼欄位:
total_margin_change_value 只代表官方/市場層級融資金額變動;沒有
summary_json.market_context.official_market_margin_status="available" 時必須是
null。後端不得由個股 margin_balance 或 daily_chip_snapshot.margin_change
自行加總推導大盤融資。外資台指期口數來自 FinMind
TaiwanFuturesInstitutionalInvestors 的 data_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_date 的 ownership_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)。
風險欄位 schema:risks 為結構化陣列(新 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
404—ticker無 chip snapshot422—trade_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_overheat、failed_breakout_risk、index_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_type的period_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=daily 的 summary_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_shares 與 shares_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_signals 與 ta_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 exceeded、request timed out、analysis failed);若兩個 domain 都失敗則回傳 503。若 orchestrator 階段失敗(LLM 超時、rate-limit 等),endpoint 仍回傳 200,technical + chip 欄位正常填充,orchestrator 為 null 或帶 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_balance(source_market='TWSE')JOINstock_ohlc即時聚合,分母為官方上市融資金額。scope 僅含上市(TWSE),不含上櫃(TPEx),因官方融資金額來源(FinMind)僅涵蓋上市;勿與坊間含上櫃的「大盤融資維持率」混用。自帶status(available/pending_source/no_data),非台股市場回no_data。並含previous_day_change子物件(previous_value、value_change=與前一可用交易日的百分點差(pp)、direction、previous_date、status);今日維持率不可用 / 無前一可用日 / gap>10 日 / 前日不可算時回no_data。- 大盤均線、關鍵均線狀態與量能結構:讀
daily_technical_snapshot的^TWIIbenchmark 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_days(config/core.yaml 的 daily_market.range_query.default_lookback_days,預設 30) |
end_date |
date (YYYY-MM-DD) | 否 | 結束日。省略時 = 今天 |
market |
string | 否 | 市場,預設 taiwan |
start_date > end_date → 422。
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_note、notion_*、reporter_email、archived_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_id、reporter_email、admin_note、notion_page_id、notion_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=day 的 current_close_price 為 anchor date 當日或之前最新一筆有效 stock_ohlc.close,baseline_close_price 為 current_date 前一個有效收盤價,用來呈現單日漲跌。若 anchor date 尚無價格,current_date 會回退到最新可得價格日,並透過 effective_date(top-level)與 per-item current_date 表明實際價格日期,同時 is_fallback=true、requested_date 保留使用者指定值。period=week/month/q/year 的 current_close_price 為 period end 當日或之前最新一筆有效 stock_ohlc.close,baseline_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 都沒有有效價格,回傳 200、total=0、etfs=[]。
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 沒帶時預設 day。period=week|month|q|year 時,date 為 anchor date;未傳 date 時以最新成功 ETF 持股快照日作 anchor。window_start / window_end 與 GET /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/daily 的 period=day 說明。
day 只納入「前一筆成功快照 == day baseline」的 ETF;漏掉 day baseline 的檔案無法計算單日變動,改列 stale_etfs(見下)。首次快照的 ETF 不受此限,維持全部持股列為 added。
added 與 quantity_increased 會進入 buy_rankings;removed 與 quantity_decreased 會進入 sell_rankings。quantity_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.TW、00400A.TW) |
| date | query | string? | YYYY-MM-DD,省略為最新成功快照日。指定日期無快照時回傳 404 |
回傳 ETFHoldingsResponse。404 表示 ETF 不存在、尚無成功快照,或指定日期無快照。
holdings[] 會對 asset_type='stock' 的成分股補上價格摘要:
current_close_price:as_of_date當日或之前最近一筆有效stock_ohlc.closebaseline_close_price:current_close_price前一筆有效stock_ohlc.closeprice_delta:current_close_price - baseline_close_priceprice_change_pct:price_delta / baseline_close_price * 100
若標準化 ticker 查不到價格,會在台股上市/上櫃尾碼間 fallback(例如 5347.TW 可 fallback 查 5347.TWO)。非股票或無有效 OHLC 時價格欄位為 null。
部分投信來源若提供 Bloomberg-style 台股代號(例如 2368 TT),API 會正規化為 raw_code=2368,並回填 2368.TW 或 2368.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 | 預設 day。day 保留既有「指定/最新快照 vs 前一成功快照」;week / month / q / year 使用與 ETF 績效排行相同的 anchor window,改以 period baseline holdings vs current holdings 計算區間持股異動。非法值回 422 |
period 沒帶時預設 day。period=week|month|q|year 時,date 為 anchor date;未傳 date 時以該 ETF 最新成功快照日作 anchor。window_start / window_end 與 GET /api/etf-holdings/performance 相同。current snapshot 為該 ETF 在 window_end 當日或之前、且落在 window 內的最新成功快照;baseline snapshot 優先使用 window_start 前最新成功快照,若不存在則使用 window 內最早且可比較的成功快照。沒有可比較 baseline 時不會把第一筆持股全部算成買入,而是以既有 200 response shape 回傳空異動。
回傳 ETFChangesResponse。response top-level 會包含 period、window_start、window_end、effective_date、requested_date、is_fallback,以及 ETF 本身在 effective_date 的價格 summary:current_close_price、baseline_close_price、price_delta、price_change_pct。current_close_price 為 effective_date 當日或之前最近一筆可用 OHLC 收盤價,baseline_close_price 為該 current 價格日期之前最近一筆可用 OHLC 收盤價。可用代表 close 不是 NULL / NaN。
Note:
ETFChangesResponse不再 inheritETFRecapItem。ETFRecapItem.as_of_date仍由GET /api/etf-holdings/recap/daily的etfs[]使用,不受本 endpoint 改名影響。
added、removed、quantity_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。
added、removed、quantity_changes 各自回傳穩定排序。排序先依 change type 分組;同一 group 內,有可用代表持股權重的項目會排在沒有權重的項目前面,再依代表持股權重降序:added 使用 curr_weight_pct,removed 使用 prev_weight_pct,quantity_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.close;HoldingChange 內優先為 constituent_ticker 的同規則價格,台股股票 exact miss 時可用同 raw_code 的 .TW / .TWO fallback |
| baseline_close_price | float? | top-level 為 etf_ticker 在 current 價格日期之前最近一筆可用 stock_ohlc.close;HoldingChange 內優先為 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_sync中status='success'且fetched_count > 0的最新 N 個as_of_date。每個入選日D_i與其「前一個成功快照日D_prev」(可能在視窗外更早)比對。 days_returned反映實際可比 diff 筆數(== 視窗內能配到previous_date的快照日數量),非入選快照數。例如:ETF 僅有 1 個成功快照 →days_returned=0、stocks=[];首日無 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對應 responsestart_date到end_date之間最新可取得的 OHLC 收盤價,baseline_close_price對應同一區間內最早可取得的 OHLC 收盤價。成分股價格查詢與/changes一樣,優先使用constituent_tickerexact match;台股股票 exact miss 時可用同raw_code的.TW/.TWOfallback。可取得代表close不是NULL/NaN。price_delta = current_close_price - baseline_close_price,price_change_pct = price_delta / baseline_close_price * 100。 - 每檔
stocks[]也會包含同格式的成分股價格 summary,並使用同一個 responsestart_date到end_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_date 到 end_date 之間最新可取得的 OHLC 收盤價 |
| baseline_close_price | float? | ETF 在 response start_date 到 end_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_date 到 end_date 之間最新可取得的 OHLC 收盤價;台股股票 exact miss 時可用同 raw_code 的 .TW / .TWO fallback |
| baseline_close_price | float? | 成分股在 response start_date 到 end_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/ranking 的 period 同語意、同 window,單一期間控制可同時驅動兩者 |
回傳 DailyRecapResponse。
period=day(單日視圖):previous_date 為 day baseline —— 納入 ETF 中各自前一筆成功快照日的最晚值,亦即多數 ETF 實際比對的那一天。只有 baseline 等於 day baseline 的 ETF 會被 diff。某檔 ETF 的前一筆成功快照早於 day baseline 時(其 provider 未在前一交易日發布快照,例如 allianz 的 NavDate 停滯數日),它只能產生跨多日的累積變動,混入單日視圖會灌水,因此不列入 etfs,改由 stale_etfs 回報(見下方 StaleETF)。該檔完全沒有更早成功快照時(即該檔在 date 是首次快照),不視為 stale,全部持股視為 added(維持既有行為)。沒有任何 ETF 有 baseline 時 previous_date 為 null。
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 不需一致)。每筆 ETFRecapItem 的 as_of_date / previous_date 為該檔自己的 current / baseline 日期。持股是存量非流量,window 中間缺任何一天的快照都不影響區間淨變動。
每一筆 ETFRecapItem 的 top-level 會包含該 ETF 自身的價格 summary(current_close_price、baseline_close_price、price_delta、price_change_pct),其 added、removed、quantity_changes 內每一筆 HoldingChange 也會包含該成分股的同格式價格 summary。價格 target 為 response date(recap 目標快照日):current_close_price 為該日當日或之前最近一筆可用 stock_ohlc.close,baseline_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/daily 的 added、removed、quantity_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.yaml 的 logging.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.yaml的logging.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_name 由 member_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>占位字串。