跳轉到

API 規格文檔

概述

Investment Agent API 提供 RESTful 端點(含 SSE 串流回應),支援股票查詢、分析和即時對話。

籌碼日資料改為各來源獨立提供,缺值回傳 null,指定交易日不再回退;個股籌碼總覽缺省日期使用該股已有任一日來源的最新日期,所有日維度只讀該日,未到來源維持 null,絕不以舊日補值。完整日期、來源狀態、分析門檻與前端契約見 籌碼資料逐來源提供。

會員管理後端提供 admin-only 的 investment overview/detail、時間型 membership、tier permission matrix 與既有 access-log 會員過濾。完整 request/response schema 以 api-reference.md 與 OpenAPI 為準。

技術棧: - FastAPI - Pydantic (資料驗證) - SSE (串流回應)

Base URL: http://localhost:8000


端點清單

1. 健康檢查

GET /health

回應:

rows 與 results 內容相同,都是去重後的事件物件。

{
  "status": "ok",
  "timestamp": "2025-02-01T10:30:00Z",
  "database": "connected",
  "services": {
    "llm": "ok",
    "vector_store": "ok"
  }
}

2. 股票查詢

2.1 搜尋股票

GET /api/stocks/search?q={query}

參數: - q: 搜尋關鍵字(股票代碼或名稱)

回應:

{
  "results": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "market": "taiwan",
      "industry": "半導體",
      "surveillance_status": "normal"
    },
    {
      "ticker": "2317.TW",
      "name": "鴻海",
      "market": "taiwan",
      "industry": "電子製造",
      "surveillance_status": "attention"
    }
  ],
  "total": 2
}

surveillance_status 可為 normal、attention、disposition。

2.2 獲取股票資訊

GET /api/stocks/{ticker}

回應:

{
  "ticker": "2330.TW",
  "name": "台積電",
  "market": "taiwan",
  "industry": "半導體",
  "current_price": 625.0,
  "price_change": 5.0,
  "price_change_pct": 0.81,
  "volume": 25000000,
  "last_updated": "2025-02-01T13:30:00Z",
  "surveillance_status": "normal"
}

2.2.1 查詢台股注意/處置事件

GET /api/market-surveillance/events?ticker=3026.TW&status=disposition

參數: - ticker: 股票代號,可選 - status: attention 或 disposition,可選 - exchange: twse 或 tpex,可選 - start_date, end_date: 公告日期區間 (YYYY-MM-DD,可選) - include_unmapped: 是否包含權證、可轉債等無法映射 ticker 的資料列 - limit, offset: 分頁

回應:

{
  "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",
      "disposition_measure": "第二次處置",
      "matching_interval_minutes": 20
    }
  ],
  "total": 1
}

matching_interval_minutes 為處置撮合頻率分鐘數;例如 約每五分鐘撮合一次 回傳 5,約每二十分鐘撮合一次 回傳 20。注意事件或無法解析時為 null。

2.2.2 查詢目前處置中股票名單

前端處置名單請使用此 endpoint;預設每個 ticker 最多一筆,避免同一檔股票因多筆重疊有效處置事件而重複顯示。若傳 status=attention 或 status_type=attention,則回傳 as_of 當日或之前最新可用公告日的注意資料;非公告日不會因為 as_of 當天沒有 row 而直接回空清單。

GET /api/market-surveillance/active-dispositions?as_of=2026-05-29&limit=500

注意清單範例:

GET /api/market-surveillance/active-dispositions?as_of=2026-06-03&status=attention&limit=500

注意模式下 summary.requested_as_of 是前端要求日期,summary.data_as_of 是實際回傳的注意公告日期,summary.date_mode 固定為 latest_available。

回應:

{
  "results": [
    {
      "ticker": "6806.TW",
      "security_name": "森崴能源",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "matching_interval_minutes": 60
    },
    {
      "ticker": "4916.TW",
      "security_name": "事欣科",
      "status_type": "disposition",
      "announce_date": "2026-05-25",
      "effective_start_date": "2026-05-26",
      "effective_end_date": "2026-06-08",
      "matching_interval_minutes": 5
    }
  ],
  "rows": [
    {
      "ticker": "6806.TW",
      "security_name": "森崴能源",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "matching_interval_minutes": 60
    },
    {
      "ticker": "4916.TW",
      "security_name": "事欣科",
      "status_type": "disposition",
      "announce_date": "2026-05-25",
      "effective_start_date": "2026-05-26",
      "effective_end_date": "2026-06-08",
      "matching_interval_minutes": 5
    }
  ],
  "total": 2,
  "summary": {
    "as_of": "2026-05-29",
    "dedupe_by": "ticker"
  }
}

注意清單回應中的 summary 範例:

{
  "total": 36,
  "summary": {
    "as_of": "2026-06-02",
    "requested_as_of": "2026-06-03",
    "data_as_of": "2026-06-02",
    "date_mode": "latest_available",
    "dedupe_by": "none"
  }
}

2.2.3 查詢今日離開處置名單

查詢 as_of 當天已經不在處置中的股票。API 會使用前一個交易日作為 effective_end_date 比對日;若無法解析交易日,fallback 為 as_of - 1 day。

GET /api/market-surveillance/exited-dispositions?as_of=2026-06-12&limit=500

回應:

{
  "results": [
    {
      "ticker": "6806.TW",
      "security_name": "森崴能源",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "matching_interval_minutes": 60
    }
  ],
  "rows": [
    {
      "ticker": "6806.TW",
      "security_name": "森崴能源",
      "status_type": "disposition",
      "announce_date": "2026-05-28",
      "effective_start_date": "2026-05-29",
      "effective_end_date": "2026-06-11",
      "matching_interval_minutes": 60
    }
  ],
  "total": 1,
  "summary": {
    "as_of": "2026-06-12",
    "dedupe_by": "ticker",
    "exit_date_field": "effective_end_date",
    "exit_effective_end_date": "2026-06-11"
  }
}

2.2.4 查詢單檔目前監管狀態

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

回應:

{
  "ticker": "3026.TW",
  "status": "disposition",
  "as_of": "2026-05-29",
  "near_disposition_threshold": null,
  "active_events": [
    {
      "ticker": "3026.TW",
      "status_type": "disposition",
      "matching_interval_minutes": 20
    }
  ]
}

near_disposition_threshold 僅在單檔距離處置條件只差一次可計算的注意事件時回傳;否則為 null。前端只需顯示 upper_close、lower_close、min_volume,不需也不會收到內部累計規則文字。

回傳物件範例:

{
  "as_of": "2026-05-29",
  "trading_date": "2026-05-29",
  "source_exchange": "twse",
  "upper_close": 612.0,
  "lower_close": null,
  "min_volume": 2500000
}

2.3 獲取歷史資料

GET /api/stocks/{ticker}/ohlc?start_date={start}&end_date={end}&limit={limit}

參數: - start_date: 開始日期 (YYYY-MM-DD, 可選) - end_date: 結束日期 (YYYY-MM-DD, 可選) - limit: 限制筆數 (可選)

回應:

{
  "ticker": "2330.TW",
  "data": [
    {
      "date": "2025-02-01",
      "open": 620.0,
      "high": 628.0,
      "low": 618.0,
      "close": 625.0,
      "volume": 25000000
    },
    // ...
  ],
  "total": 90
}

2.4 獲取技術指標

GET /api/stocks/{ticker}/indicators?latest=true

參數: - latest: 只取最新 (預設 true) - days: 僅 latest=false 時生效;最近 N 個完整交易日資料列(可選,預設 66、範圍 1-200,非日曆日 cutoff)

歷史模式先從 data_quality='complete' snapshots 取日期最新的 N 筆,再以日期升冪回傳。 input_basis='adjusted' 的價格型指標依各自交易日 factor 投影到 raw OHLC 顯示尺度, input_basis='raw' 的列不轉換;basis 為 null、未知值,或 adjusted 列缺 factor 時,只將價格型 leaf 設為 null。

最新快照回應 (latest=true):

{
  "ticker": "2330.TW",
  "timestamp": "2025-02-01",
  "trading_date": "2025-02-01",
  "indicators": {
    "ma": {
      "5": 622.5,
      "20": 615.3,
      "50": 598.7
    },
    "rsi": 65.4,
    "macd": {
      "value": 2.5,
      "signal": 1.8,
      "histogram": 0.7
    },
    "kd": {
      "k": 72.3,
      "d": 68.9
    },
    "bollinger": {
      "upper": 635.0,
      "middle": 615.0,
      "lower": 595.0
    }
  }
}

歷史序列回應 (latest=false):

{
  "ticker": "2330.TW",
  "days": 2,
  "total": 2,
  "data": [
    {
      "timestamp": "2025-01-31T00:00:00",
      "indicators": {
        "ma": {"5": 620.0, "20": 614.8},
        "rsi": 63.9
      }
    },
    {
      "timestamp": "2025-02-01T00:00:00",
      "indicators": {
        "ma": {"5": 622.5, "20": 615.3},
        "rsi": 65.4
      }
    }
  ]
}

2.5 三大法人區間估算金額

這兩個唯讀 endpoint 以個股每日法人股數乘上同日收盤價,提供區間排行與逐日行為。金額皆為新台幣估算值,不是交易所提供的個股實際成交金額:

daily estimated amount = daily shares × same-day close
period estimated net amount = Σ(daily net shares × same-day close)

回應固定帶 currency="TWD" 與 amount_basis="net_shares_x_daily_close"。days 表示交易日,不是日曆日。

2.5.1 區間估算金額排行
GET /api/institutional/amount-ranking?days=5&limit=20&investor_type=all
Query Type Required Default Validation / Notes
days integer no 1 1..30;全市場法人資料最新日起往回的 distinct 交易日數
limit integer no 20 1..100;買超榜與賣超榜各自上限
investor_type all/foreign/trust/dealer no all all 固定回傳外資、投信、自營商、三大法人合計;不開放直接傳 total

Response 以 rankings[] 分法人回傳 buy[] 與 sell[]。all 時順序固定為 foreign、trust、dealer、total;total 是前三者逐日淨股數合計,不是散戶金額。buy 只含正估算淨額並由大到小;sell 只含負估算淨額並由小到大。相同金額以 ticker 升冪穩定排序。某 ticker 在視窗內有非零 actor net_shares、但缺任一所需同日 close 時,不以不完整金額參與該 actor 排行,並計入 excluded_unpriced_tickers。完全沒有可排行資料時仍回 HTTP 200 與空榜。

{
  "requested_days": 5,
  "actual_days": 5,
  "start_date": "2026-07-17",
  "end_date": "2026-07-23",
  "investor_type": "all",
  "limit": 20,
  "currency": "TWD",
  "amount_basis": "net_shares_x_daily_close",
  "rankings": [
    {
      "investor_type": "foreign",
      "investor_name": "外資",
      "excluded_unpriced_tickers": 0,
      "buy": [
        {
          "rank": 1,
          "ticker": "2330.TW",
          "name": "台積電",
          "direction": "buy",
          "net_shares": 1000000,
          "estimated_net_amount": 2405000000.0
        }
      ],
      "sell": []
    },
    {
      "investor_type": "trust",
      "investor_name": "投信",
      "excluded_unpriced_tickers": 0,
      "buy": [],
      "sell": []
    },
    {
      "investor_type": "dealer",
      "investor_name": "自營商",
      "excluded_unpriced_tickers": 0,
      "buy": [],
      "sell": []
    },
    {
      "investor_type": "total",
      "investor_name": "三大法人合計",
      "excluded_unpriced_tickers": 0,
      "buy": [
        {
          "rank": 1,
          "ticker": "2330.TW",
          "name": "台積電",
          "direction": "buy",
          "net_shares": 1000000,
          "estimated_net_amount": 2405000000.0
        }
      ],
      "sell": []
    }
  ]
}
2.5.2 個股逐日估算金額行為
GET /api/institutional/stock/2330/amount-flows?days=1&investor_type=all
Parameter In Type Required Default Validation / Notes
ticker path string yes — 台灣 4–6 位數字,可帶一個大寫字母;可加 .TW / .TWO
days query integer no 1 1..30;resolved ticker 自己最近 N 筆法人交易日
investor_type query all/foreign/trust/dealer no all all 固定依外資、投信、自營商、三大法人合計分組;不開放直接傳 total

裸代號會解析為 canonical .TW 或 .TWO ticker;若兩個市場同時存在相同裸代號,回 HTTP 409 並要求明確 suffix。actors[].items[] 依 trade_date 新到舊排列。direction 為 buy、sell 或 neutral;amount_status 為:

  • available:有同日 close,可計算估算金額;close 為 0 仍屬 available。
  • missing_close:有非零股數但缺同日 close,三個 estimated_*_amount 均為 null。
  • zero_flow:buy_shares、sell_shares、net_shares 全為 0,不需價格即可確定三個估算金額均為 0。
{
  "ticker": "2330.TW",
  "name": "台積電",
  "requested_days": 1,
  "actual_days": 1,
  "start_date": "2026-07-23",
  "end_date": "2026-07-23",
  "investor_type": "all",
  "currency": "TWD",
  "amount_basis": "net_shares_x_daily_close",
  "actors": [
    {
      "investor_type": "foreign",
      "investor_name": "外資",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "sell",
          "buy_shares": 10889394,
          "sell_shares": 15503824,
          "net_shares": -4614430,
          "close_price": 2405.0,
          "estimated_buy_amount": 26188992570.0,
          "estimated_sell_amount": 37286696720.0,
          "estimated_net_amount": -11097704150.0,
          "amount_status": "available"
        }
      ]
    },
    {
      "investor_type": "trust",
      "investor_name": "投信",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "sell",
          "buy_shares": 322751,
          "sell_shares": 1266009,
          "net_shares": -943258,
          "close_price": 2405.0,
          "estimated_buy_amount": 776216155.0,
          "estimated_sell_amount": 3044751645.0,
          "estimated_net_amount": -2268535490.0,
          "amount_status": "available"
        }
      ]
    },
    {
      "investor_type": "dealer",
      "investor_name": "自營商",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "buy",
          "buy_shares": 657629,
          "sell_shares": 619173,
          "net_shares": 38456,
          "close_price": 2405.0,
          "estimated_buy_amount": 1581597745.0,
          "estimated_sell_amount": 1489111065.0,
          "estimated_net_amount": 92486680.0,
          "amount_status": "available"
        }
      ]
    },
    {
      "investor_type": "total",
      "investor_name": "三大法人合計",
      "items": [
        {
          "trade_date": "2026-07-23",
          "direction": "sell",
          "buy_shares": 11869774,
          "sell_shares": 17389006,
          "net_shares": -5519232,
          "close_price": 2405.0,
          "estimated_buy_amount": 28546806470.0,
          "estimated_sell_amount": 41820559430.0,
          "estimated_net_amount": -13273752960.0,
          "amount_status": "available"
        }
      ]
    }
  ]
}

個股不存在或完全沒有法人資料時回 HTTP 404;query/path 驗證失敗時回 FastAPI HTTP 422 detail array。這兩個 endpoint 是 additive,不改變任何既有 institutional API。

2.6 主動式 ETF 個股推估持有成本

GET /api/etf-holdings/{etf_ticker}/estimated-costs

輸入完整主動式 ETF ticker(例如 00993A.TW),回傳該 ETF 目前持有個股的推估成本,以及各股目前成本週期內的買賣成本紀錄。endpoint 會從全部可用快照重建狀態,不提供 days query,也不受 60 天限制。

估算使用完整成功快照之間的股數差與後一快照日收盤價:

estimated trade amount = abs(current quantity - previous quantity)
                         × same-day close
  • 第一個可觀察持股以首筆完整快照日 close 建立 opening estimate。
  • 首筆完整快照後才第一次買進的股票標記為 estimated_from_first_buy;只有曾清倉後再次買進才標記為 estimated_from_reentry。
  • 加碼採移動加權平均;部分賣出維持平均成本並按剩餘股數減少總成本。
  • 股票在下一個完整成功快照缺席或 quantity 歸零即視為清倉;清倉後不回傳該股票,清倉後再買則從再買日重新起算,且不回傳清倉前的成本或 action。
  • 只納入 stock / shares 且可 canonicalize 為 .TW / .TWO 的持股;讀取時沿用既有 ETF ticker normalization 與 dedupe。
  • fetched_count 與該 ETF/date 實際持久化筆數不一致的污染快照會被排除,排除數量由 excluded_snapshot_count 揭露。
  • 買入日缺 close 或股數缺失時,不虛構成本;相關金額與 basis 回傳 null,並由 amount_status / basis_status 說明原因。
  • 每筆 delta 代表兩個已發布完整快照之間的淨變化,並以後一快照日 close 估算;不代表基金真實成交日、成交價或日內交易。

成功回傳 ETFEstimatedCostsResponse,主要欄位如下:

欄位 說明
first_snapshot_date / last_snapshot_date 實際納入成本重播的完整歷史範圍
snapshot_count 納入的完整快照數,可大於 60
excluded_snapshot_count 因完整性檢查失敗而未納入的成功非空快照數
stocks[].estimated_cost_basis 目前部位推估總成本
stocks[].estimated_average_cost 目前部位推估每股平均成本
stocks[].cost_basis_start_date 目前成本週期起日;清倉再買後為再買日
stocks[].actions[] 依日期升冪排列的目前成本週期 opening/買入/部分賣出/資料缺失紀錄

存在但非 enabled 主動式 ETF 回 HTTP 400;ETF 不存在或沒有可用完整快照回 HTTP 404;path 格式錯誤回 HTTP 422。完整 schema、enum 與範例見 api-reference.md。

2.6.1 AETF Agent 單一基金報告

POST /api/etf-holdings/{etf_ticker}/aetf-agent/analyze?date=YYYY-MM-DD&force=false
GET  /api/etf-holdings/{etf_ticker}/aetf-agent/latest?date=YYYY-MM-DD

POST 是管理員生成入口;GET 是公開 cache-only 讀取,不讀 source 也不呼叫 LLM。analysis.report 是持股動向、跨期比較及績效三段純正文;analysis.sections 保留操作總結、今日、近 5 日、近 20 日與近期績效五個機械詳情欄位,並保留完整 facts。aetf-v14 的 anchor_date 是 requested date 當日或以前最近的合格官方交易日;只有 requested date 等於台北今天且現在早於共用 DAILY_STOCK_UPDATE_TIME(15:30)時排除今天,15:30 起納入。歷史請求不套今天的時間限制,週末/休市不額外退一個 session。holdings_as_of 是最後一個同步成功、非空且列數相符的 snapshot。持股窗口獨立選取結束於 holdings_as_of 的 2/6/21 個官方 session;raw-close 績效窗口選取結束於 official anchor_date 的 2/6/21 個 session。兩者各自標示 complete|partial|unavailable,缺少中間 snapshot 不會跨日拼接。

analysis.facts 及 report_data 含 holdings_freshness={status,anchor_date,holdings_as_of,lag_session_count};lag 是 (holdings_as_of, anchor_date] 之間的官方交易日數,不是日曆天數,也不因週末 requested date 增加。狀態為 stale 時正文必須揭露實際持股日與 lag,不套任意天數門檻。report_data performance.today|five_day|twenty_day 各自保留 baseline_date、end_date;exact-date 價格缺失時保留 null,不回退舊價。

股數變化只採一致 shares 單位,權重-only 不推估股數;估算金額為逐日股數差乘同日未還原收盤價,不代表成交額或申贖流量。報告專用 serializer 輸出 current_holdings、daily_changes、stock_summary;每日與逐股合計使用同一份標準化變動,保留無異動日及已確認零值,未知不補零。scope.covers_entire_fund 與範圍內 periods.*.data_status 分開;排除非股票/海外資產不單獨使範圍內完整個股降級,但存在未識別或海外股票時,正文必須用「就台股部位而言」限制結論範圍;只有現金等非股票排除項時不強制加入此句。共通 semantics 定義單位、口徑、期間邊界、重疊與缺值,interval_count 為官方交易區間數;缺價金額維持 null,公司行動旗標為 true/false/null。詳見 report_data 契約。

若 requested date(含)以前沒有任何有效完整 snapshot,或官方 calendar 在有效 anchor(含)以前本身不足 21 個 session,POST 回 404。ETF 的 snapshot 筆數與官方 session 數分開判斷:新上市基金即使只有 1 個有效 snapshot,只要 calendar 仍可選滿 21 個官方 session,就回 200,歷史持股缺漏由 partial|unavailable 揭露;不會把缺少 snapshot 誤判為缺少官方 session。

mas.agents.aetf.llm_enabled 預設為 false,僅布林 true 啟用模型;缺省或無效型別使用機械模板並回 fallback_reason=llm_disabled。啟用時模型採使用者指定 prompt 回傳三段、約 250~400 字純正文,不要求 JSON 或引用 ID。服務端將受支援的期間、股票、數值、方向、績效與必要警語 claim 綁回 facts;任何格式或驗證檢查未通過的輸出都 fail closed 回機械模板,不把 null/缺列當成零。這仍是有界的自然語言防線,不宣稱能證明任意語句的完整語意。未設定 provider、逾時或失敗時也回模板與既有 fallback_reason。生成模式納入 cache input hash,所以下一次 POST 不沿用相反模式的舊報告;GET 仍只回已存 cache,不讀設定或重生。正文消費者應讀 analysis.report;五個 sections 是保留的機械化期間詳情。request/response 欄位不變,fallback_reason enum 為 additive 擴充。

完整 request/response/error、計算、快取與前端接法見 AETF Agent 契約。此功能為 additive,不改變既有綜合日報、ETF holdings 或 MAS 個股契約。

2.7 台股逐檔當沖統計

兩個 endpoint 都是公開、唯讀 DB API;request time 不呼叫 TWSE/TPEx。

GET /api/day-trading/stocks/{ticker}?days=30&final_only=false
GET /api/day-trading/rankings/latest?market=all&metric=day_trade_volume&limit=10&security_type=common&final_only=false
  • {ticker} 使用 canonical suffix(例如 2330.TW、5483.TWO)。不存在回 404;存在但沒有資料回 200 與空 items。
  • 個股歷史先取最近 days 筆,再依 trade_date ASC 回傳,供趨勢圖使用。
  • 首頁榜 metric 可為 day_trade_volume(預設,向下相容)或 day_trade_ratio;兩者皆為降冪、同值時固定 ticker ASC。ratio 榜只納入同日 total_volume > 0 的標的。market=all 取 TWSE 與 TPEx 都存在的最近兩個共同日期;指定市場則取該市場最近兩日。最新日為 trade_date,共同前一日為 required nullable previous_trade_date;沒有符合日期時兩者皆為 null、items=[]。
  • day_trade_ratio = day_trade_volume / total_volume * 100,其中 total_volume 必須來自同 ticker、同交易日 stock_ohlc;缺值或 0 時 ratio 為 null。
  • 每筆榜單另回 required nullable day_trade_volume_change_pct = (當日股數 - 前日股數) / 前日股數 * 100 與 day_trade_ratio_change_pp = 當日 ratio - 前日 ratio。兩者 round 2;前日個股缺列或前日股數為 0 時股數差為 null,任一日 OHLC volume 缺值/0 時比率差為 null,且不為個股回溯更舊日期。
  • revision_stage=0/1/2 分別表示 T/T+1/T+2;只有 stage 2 為 is_final=true。預設可回傳未 final 資料;final_only=true 同時套用最新日與前一日。
  • security_type=common 為首頁預設;傳 all 才包含 ETF、ETN 等其他證券類型。

完整 response wrapper 與欄位見 api-reference.md。


3. 股票分析

3.1 執行分析 (同步)

POST /api/analyze

請求:

{
  "ticker": "2330.TW",
  "analysis_type": "comprehensive",  // comprehensive, technical, trend
  "timeframe": "3_months"  // 1_month, 3_months, 6_months, 1_year
}

回應:

{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "ticker": "2330.TW",
  "timestamp": "2025-02-01T10:30:45Z",
  "analysis": {
    "summary": "台積電目前處於中性偏多區間...",
    "trend": {
      "overall": "bullish",
      "short_term": "neutral",
      "medium_term": "bullish"
    },
    "key_levels": {
      "support": [600, 590, 580],
      "resistance": [630, 640, 650]
    },
    "indicators_interpretation": {
      "rsi": "RSI 65.4,處於中性偏多區間,尚未超買",
      "macd": "MACD 金叉形成,短期趨勢偏多",
      "ma": "價格站上 20MA,中期趨勢向上"
    },
    "risks": [
      "短期 RSI 接近超買區,注意回調風險",
      "成交量較前期萎縮,需觀察後續量能"
    ],
    "recommendations": [
      "觀察 630 壓力位能否突破",
      "若回調至 600 支撐位可考慮進場",
      "設定停損於 590 以下"
    ]
  },
  "metadata": {
    "confidence": 0.75,
    "verification_passed": true,
    "iterations": 1,
    "execution_time_ms": 3250
  }
}

5. 系統管理

5.1 觸發資料更新

POST /api/admin/update-data

請求:

{
  "markets": ["taiwan", "us"],  // 可選,預設全部
  "force": false  // 強制更新
}

回應:

{
  "status": "started",
  "job_id": "update-20250201-103045",
  "estimated_time_minutes": 15
}

5.2 查詢任務狀態

GET /api/admin/jobs/{job_id}

回應:

{
  "job_id": "update-20250201-103045",
  "status": "running",  // pending, running, completed, failed
  "progress": {
    "current": 500,
    "total": 1700,
    "percentage": 29.4
  },
  "started_at": "2025-02-01T10:30:45Z",
  "estimated_completion": "2025-02-01T10:45:00Z"
}


6. 題材 (Themes)

題材(概念股)來自 coverage 知識庫的 coverage_theme_membership,也就是 data/my-tw-coverage/themes/*.md 匯入後的主題成員資料。這份資料比策略用的 stock_group concept 群組更完整。API 只回傳最新 knowledge_as_of 快照的題材資料。

6.1 題材清單

GET /api/themes

回應:

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

6.2 查詢題材對應個股列表

GET /api/themes/{theme_id}/stocks?limit={limit}&offset={offset}

參數: - theme_id: coverage 題材名稱(例如 AI 伺服器、CoWoS、電動車;URL path 需做 URL encoding) - limit: 可選,每頁筆數,預設 themes.default_limit(200),上限 themes.max_limit(1000,超過自動截到上限) - offset: 可選,分頁位移,預設 0

回應:

{
  "theme_id": "AI 伺服器",
  "theme_name": "AI 伺服器",
  "found": true,
  "member_count": 148,
  "stocks": [
    {
      "ticker": "2330.TW",
      "name": "台積電",
      "english_name": "TSMC",
      "industry": "半導體",
      "sector": "科技"
    }
  ]
}

題材不存在時,回傳 found: false 與空 stocks 陣列(HTTP 200)。


FastAPI 實現

main.py

檔案: backend/api/main.py

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
import logging

# 初始化日誌
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@asynccontextmanager
async def lifespan(app: FastAPI):
    """應用生命週期管理"""
    # 啟動時
    logger.info("初始化資料庫連接...")
    await init_database()

    logger.info("載入 LLM 和 Agents...")
    await init_agents()

    logger.info("API 服務啟動完成")

    yield

    # 關閉時
    logger.info("關閉資料庫連接...")
    await close_database()

# 建立 FastAPI app
app = FastAPI(
    title="Cortex Investment Agent API",
    version="1.0.0",
    lifespan=lifespan
)

# CORS 設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # Production 應限制來源
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# 載入路由
from api.routes import stocks, analyze, chat, admin

app.include_router(stocks.router, prefix="/api/stocks", tags=["stocks"])
app.include_router(analyze.router, prefix="/api", tags=["analyze"])
app.include_router(chat.router, prefix="/ws", tags=["chat"])
app.include_router(admin.router, prefix="/api/admin", tags=["admin"])

@app.get("/health")
async def health_check():
    """健康檢查"""
    return {
        "status": "ok",
        "timestamp": datetime.now().isoformat(),
        "database": "connected",  # 實際檢查
        "services": {
            "llm": "ok",
            "vector_store": "ok"
        }
    }

路由範例: analyze.py

檔案: backend/api/routes/analyze.py

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Literal
from agents.coordinator import StockAnalysisWorkflow
from database.crud import get_db

router = APIRouter()

class AnalyzeRequest(BaseModel):
    ticker: str
    analysis_type: Literal["comprehensive", "technical", "trend"] = "comprehensive"
    timeframe: Literal["1_month", "3_months", "6_months", "1_year"] = "3_months"

class AnalyzeResponse(BaseModel):
    request_id: str
    ticker: str
    timestamp: str
    analysis: dict
    metadata: dict

@router.post("/analyze", response_model=AnalyzeResponse)
async def analyze_stock(
    request: AnalyzeRequest,
    # 一律 function scope:session 在回應序列化後、送出前歸還(#2098,
    # tests/test_get_db_scope_pin.py 釘住 api/ 與 auth/ 的每個呼叫點)
    db = Depends(get_db, scope="function")
):
    """執行股票分析"""

    # 驗證股票存在
    stock = await StockCRUD.get_stock(db, request.ticker)
    if not stock:
        raise HTTPException(status_code=404, detail="股票不存在")

    # 執行分析
    workflow = get_analysis_workflow()  # 從 app state 獲取

    result = await workflow.run(
        ticker=request.ticker,
        analysis_type=request.analysis_type,
        timeframe=request.timeframe
    )

    return AnalyzeResponse(
        request_id=str(uuid.uuid4()),
        ticker=request.ticker,
        timestamp=datetime.now().isoformat(),
        analysis=result.to_dict(),
        metadata={
            "confidence": result.confidence,
            "verification_passed": result.verification_passed,
            "iterations": result.iterations
        }
    )

投資組合 Portfolio

交易列表

GET /api/portfolio/transactions

列出目前登入會員的交易紀錄。此端點需登入,沿用 portfolio API 的 JWT 驗證。每筆 row 維持既有交易列表 response shape,包含交易基本欄位、broker 欄位、當沖配對欄位與 warning/matched/unmatched 欄位。

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 查詢。

Request modes:

  • GET /api/portfolio/transactions:全交易列表,既有 response shape 不變。
  • GET /api/portfolio/transactions?ticker=2330.TW:單一個股交易列表,既有 response shape 不變。
  • 指定 ticker 且無交易時回傳 []。

類股配置彙總

GET /api/portfolio/allocation

依目前仍持有的正部位(position_qty > 0)彙總股票分類、投資總額與配置比例。此端點需登入,沿用 portfolio API 的 JWT 驗證。

Query Parameters:

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

分類規則:

  • type=Group:優先使用 stocks.industry,缺失時使用 stocks.sector,仍缺失時歸入 Unclassified。
  • type=ETF:使用 etf_funds.etf_ticker 判斷 ETF,回傳 etf 與 stock 兩種 bucket。Response format 不變。

回應:

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

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

market_value 以 market_price * position_qty 計算;若市價缺失,使用 cost_basis_total 作為 fallback。allocation_pct 以市場價值為分母,四捨五入到小數第 4 位。空投資組合回傳 total_market_value=0.0、total_cost_basis=0.0、groups=[]。

投資組合期間績效

GET /api/portfolio/performance

取得目前使用者持股的未實現績效彙總與 day/month/quarter/year 期間變化;quarter 為日曆季至今,baseline 為上一季最後一個交易日 close。此端點需登入,沿用 portfolio API 的 JWT 驗證。

Query Parameters:

Field Type Required Default Notes
ticker string no null 單一 ticker 期間績效;不帶時為整體投資組合。
type Group | ETF no null 分組期間績效;Group 依產業分類,ETF 依 ETF/個股分類。不可與 ticker 同時使用。

Request modes:

  • GET /api/portfolio/performance:整體投資組合期間績效,既有 response shape 不變。
  • GET /api/portfolio/performance?ticker=2330.TW:單一 ticker 期間績效,既有 response shape 不變。
  • GET /api/portfolio/performance?type=Group:產業分組期間績效,分類口徑比照 GET /api/portfolio/allocation?type=Group。
  • GET /api/portfolio/performance?type=ETF:ETF / 個股分組期間績效,分類口徑比照 GET /api/portfolio/allocation?type=ETF。
  • ticker 與 type 同時提供回 HTTP 422。

既有整體 / 單一 ticker 回應:

{
  "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.8264
    },
    "month": {
      "label": "month",
      "baseline_date": "2026-05-29",
      "baseline_value": 2200.0,
      "unrealized_pnl": 200.0,
      "unrealized_pnl_pct": 9.0909
    },
    "quarter": {
      "label": "quarter",
      "baseline_date": "2026-03-31",
      "baseline_value": 1950.0,
      "unrealized_pnl": 450.0,
      "unrealized_pnl_pct": 23.0769
    },
    "year": {
      "label": "year",
      "baseline_date": "2025-12-31",
      "baseline_value": 1700.0,
      "unrealized_pnl": 700.0,
      "unrealized_pnl_pct": 41.1765
    }
  }
}

分組回應(type=Group / type=ETF):

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

type=Group 的 category_source 為 industry / sector / unclassified;type=ETF 的 category_source 為 asset_type。若某期間任一 group holding 缺少 current 或 baseline close,該 group 的對應 period 欄位回傳 null。空投資組合回傳 zero totals 與 groups=[]。

投資操作績效(全時/指定期間)

GET /api/portfolio/investment-performance

此 endpoint 與既有 /api/portfolio/performance 不同:既有 endpoint 只看「目前持倉市場變化」;本 endpoint 計入有效 closure 的一般/當沖已實現、依除息日 entitlement 計算的現金股利、期末未實現與資金投入報酬。

Query Type Required Default Notes
period all/day/week/month/quarter/year no all 操作 lot 開倉期間
value string period 非 all 時 yes null day: YYYYMMDD/YYYY-MM-DD; week: YYYY-Www; month: YYYYMM/YYYY-MM; quarter: YYYYQn/YYYY-Qn; year: YYYY
ticker string no null 可查已清倉 ticker;與 type 互斥
type Group/ETF no null Group=industry→sector→Unclassified; ETF=etf/stock

非 all 的操作 cohort 只計入該期間開倉 lots;realized regular P&L 以 sell-date 實際 allocated original cost 計算,realized dividend 則依 ex-date 的完整 qualifying holdings,非期間 cohort。expected dividend 以 period-end holdings 計算,不受 stale quote 阻擋。ex_dividend_date <= end_date(timeseries 為 point period_end)即依除息權利認列已實現股利;更晚 event 只進 expected_cash_dividend,不進 total P&L。歷史股數使用除息日前 transactions/closures 重建;缺價時 unrealized_pnl/total_pnl/return_pct/equity_multiple 為 null。非 all 的 U/K/total/return composition 仍未定案,尚非 merge-ready。

未來除息事件單純現金金額缺值或公告金額衝突,不產生 unquantified_cash_dividend/conflicting_dividend_event 或目前績效不完整警示;到查詢有效截止日已除息且當時有資格、仍缺必要金額或有衝突,才在已實現股利相關指標揭露。可量化的 expected_cash_dividend 及當前交易未配對等真實問題保留。overview/details/groups/timeseries 共用此規則。

主要 response 欄位:period/value/start_date/end_date/valuation_date/is_partial/ticker、opened_lots_count/closed_lots_count、realized.{regular_pnl,day_trade_pnl,cash_dividend,total}、expected_cash_dividend、unrealized_pnl/total_pnl/capital_employed/return_pct/equity_multiple、data_quality。Grouped response 另含 type/classification_basis/overall/groups[]。

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

Overview 的 is_complete 由本次回應真正相關的 calculation issues 決定;沒有相關 計算問題時為 true。Overview 沒有 reconciliation 欄位。Details 另以同一次 audit 的 reconciliation.status 驗證 aggregate 與完整明細:matched 才表示對帳成功, incomplete/mismatch 均使 details 相關範圍不完整。未知 issue code 不得被忽略; deprecated coverage 欄位不參與任何判定。每個 data_quality object 另有必填、非 null 的 calculation_issues;空陣列表示本次 audit 未偵測到已知計算問題,不表示外部來源完整。 每筆 issue 必含 code:string、count:integer、tickers:string[]、 affected_metrics:PerformanceDetailMetric[]。初始 code 為 unmatched_sell、 missing_price、unattributed_cash_dividend;coverage checkpoint 不進此清單, consumer 必須容忍未知 code。Details 會按所選 metric 過濾 issues 及其 count/tickers; 無關問題不得污染該 metric(尤其 day_trade_pnl 不受無關股利或價格問題影響)。 後端固定提供這四個相容欄位的既有 shape,足以支援原 frontend consumer;本次不要求 任何 Web/Management/Mobile frontend change。

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

投資操作績效逐筆明細

GET /api/portfolio/investment-performance/details?period=month&value=2026-07&metric=cash_dividend&offset=0&limit=100

必填 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 沿用單期 endpoint;分類 drill-down 必須同時傳 type/category/category_source,由 backend 驗證 current metadata scope,且與 ticker 互斥。offset 預設 0,limit 預設 100、最大 1000。

Response 為 window/scope/metric/unit/value/reconciliation/pagination/items/data_quality。前端必須顯示 backend value,不可自行 sum 當頁 items;reconciliation.detail_value 在分頁前以全量 items 計算。capital_employed 取 capital_required 最大值。所有 items 依主要日期由新到舊;capital-flow cumulative/peak 仍按正向時間計算,只反轉 response 排列。Items 以 item_type=closure/dividend/lot/capital_flow 判別,欄位與完整 example 見 docs/frontend-integration/investment-performance.md。

reconciliation 只比較同一次 audit 的 aggregate 與 details;matched 不會清除 calculation_issues。Details 的 data_quality 已按所選 metric 過濾。 mismatch/incomplete 亦使後端回 is_complete=false,即使 issue 陣列為空。 完整性描述所選數值及全量對帳,不保證 items 的附帶欄位:例如開倉筆數可正確而 lot 估值為 null,或未配對賣出下剩餘量/status 暫定;估值品質須看未實現/總損益。

投資操作績效序列

GET /api/portfolio/investment-performance/timeseries?start_date=2026-01-01&end_date=2026-07-10&interval=month
Query Type Required Default Notes
start_date date yes - inclusive
end_date date yes - inclusive,不得晚於今日
interval day/week/month/quarter/year no month chart bucket
ticker string no null 與 type 互斥
type Group/ETF no null current metadata classification

series[].points[] 同時回 period(該 bucket 的 sell-date/ex-date 已實現與仍待定案的其他指標)及 cumulative(inception 至 point period_end)。日期點取實際報價日、非現金股利交易日、來源除權息日,以及核心分類為真正 standalone orphan 的付款日聯集;不為已歸屬或 ambiguous 的付款日建立股利日期點。point valuation_date 為最後活動日,價格只取該日以前最新已知 close,不產生虛構報價。歷史 point 不倒灌未來賣出或 expected dividend;已歸屬的晚記錄付款仍只作為原 ex-date 的金額證據,不改歸期。總點數超過 2000 回 HTTP 422。

Portfolio summary 股利修正

GET /api/portfolio/summary 的 request/response shape 不變(OpenAPI metadata 亦不變)。股利 摘要使用以 canonical asset identity 與除權息日識別事件的共用股利引擎:先在完整同資產事件 時間線建立現金入帳的 ownership,再依年度篩選事件;不得以「下一個 cash ex-date」截斷舊付款 範圍,因較早事件可能在較晚 ex-date 後才入帳。在依期間篩選之前,先處理相同 ex-date 的 有效 payment bundle;其他 payment 只有在恰有一個較早、尚未 claim 的 eligible cash event, 且該事件恰有一個未配對 candidate payment 時才可歸屬(不以金額相等或 nearest guess 推定)。 所有較早且仍可能相關的未 claim event 都保留為候選;已證明 stock-only 的事件排除,unknown announcement、conflicting event 或 uncertain holdings 會阻止 false unique claim。

唯一歸屬的 payment 可一次 override 回原 ex-date(不改 payment 原始日期,也不在支付月份重列)。 同 ticker/ex-date 的入帳仍優先,且不與 derived event 重複。event、明細與 issue 使用一致的 display ticker;raw payment evidence 保留原始 ticker,配對仍使用 canonical asset identity。 received event 的股數依 ex-date 前歷史 entitlement;已知有效的 partial closure 先扣除已證明 股數,未知殘量不被假裝已解決。pending event 則維持以台北今日 transactions/closures 計算的 current holdings。例:兩個較早事件 entitlement 為 200 與 100、晚到 payment 為 200 時,不得 任意選一事件;兩個事件維持 inferred 300、manual_received_amount=0,payment 留在 raw unattributed_payments 並產生相應 issue。

同一天的 payment bundle 以整體核對:只要其中任一筆的 net amount <= 0,整個 bundle 保留於 raw invalid_payments evidence,不能 override 有效 event,也不能新增 manual income;有效的 event entitlement 仍維持推算值。invalid_payments 與 unattributed_payments 互斥;同一 raw row 只計一次 invalid issue,不重複放入 unattributed。audit.unattributed_payments 是所有其餘 未消費 payment evidence 的原始集合;其中 audit.standalone_payments 是可進 manual bucket 的子集。與已知 cash event 相關、但因多筆候選而無法唯一歸屬的 payments 只留在 unattributed,且不會進 realized performance 或在 inferred event 之外再算一筆 manual income;沒有未占用候選(含事件已被 同日款占用)的有效 orphan/manual payment,以及 zero-entitlement 的 orphan,則在 shared audit 的 realized_dividends 保留一次,attribution=standalone、event_key=null、 explicit_override=false。此 standalone item 的既有 ex_dividend_date 欄位僅表示 payment date,不是已確認的 ex-date;其金額同時納入既有 summary manual_received_amount bucket。 歧義可為多個事件候選、單一事件對多筆付款或資格/身份未確定,保留 unattributed issue; invalid payment 僅帶 invalid issue,兩者均不另算 manual income。summary manual bucket 仍依 payment date 篩選且沒有 quality 欄位,不宣稱確認了 ex-date。上述不新增 public wire 欄位。

年度仍是 Asia/Taipei 今日所屬年度,沒有新的 year query parameter。跨年付款仍按 ex-date 歸屬:例如 ex-date 在 2025、payment 在 2026 時,2026 summary 的 received/total 為 0, 原 2025 event 才包含實際入帳(例如 173),不按 payment year 重列。


八大行庫/券商分點 Participant Flows

Prefix: /api/participant-flows,全部需登入(Authorization: Bearer <access_token>;無效 token 401,未帶 header 422)。

鏡射 Cortex Data Hub 的八大行庫(government-bank)、券商分點(broker-branch)買賣明細;每日 01:00 TW 排程同步。金額/股數欄位在 wire 上一律為十進位字串或 null;ticker 為外部格式(2330.TW/.TWO)。ticker 無法解析或非台股回 422;不在 taiwan.stocks 的 ticker 回 404;ticker 存在但區間/窗口無列回 200 + 空陣列(例外:/api/participant-flows/broker-branch/branch/{participant_id} 在窗口內無列回 404,/api/participant-flows/broker-branch/stock/{ticker}/branch/{participant_id} 在窗口內查無該分點交易此股的任一列亦回 404,見下)。

個股八大行庫買賣

GET /api/participant-flows/government-bank/stock/2330.TW?days=30
Query Type Required Default Validation / Notes
days integer no 30 1..3650;以今日(Asia/Taipei)往回抓的日曆天數

total(participant_kind=government_bank_total,市場合計)與 banks(participant_kind=government_bank,各行庫明細,每日 1..8 家、不補滿)分開回傳。

{
  "ticker": "2330.TW",
  "total": [
    {
      "trade_date": "2026-09-01",
      "buy_shares": "1000000",
      "sell_shares": "400000",
      "net_shares": "600000",
      "buy_value": "50000000.50000000",
      "sell_value": "20000000.25000000",
      "net_value": "30000000.25000000"
    }
  ],
  "banks": [
    {
      "trade_date": "2026-09-01",
      "participant_id": "BANK1",
      "participant_name": "第一銀行",
      "buy_shares": "600000",
      "sell_shares": "100000",
      "net_shares": "500000",
      "buy_value": "30000000.00000000",
      "sell_value": "5000000.00000000",
      "net_value": "25000000.00000000"
    }
  ]
}

八大行庫市場排行

GET /api/participant-flows/government-bank/ranking?side=buy&limit=20
Query Type Required Default Validation / Notes
date date no 最新可得日 YYYY-MM-DD
side buy/sell no buy buy=只取 net_value > 0 依 DESC;sell=只取 net_value < 0 依 ASC
limit integer no 20 1..100

INNER JOIN taiwan.stocks:不在 stocks 的 ETF/權證代碼不進榜;limit 為「可 JOIN 到的前 N 名」。item 的 name/listing_type 皆可為 null(taiwan.stocks.name 可為空;指數等代碼無上市別)。買超榜與賣超榜互斥:同一檔不會同時進兩榜,net_value 為 0 或 null 的個股兩榜都不出現。net_value 相同時以 stock_code 升冪決定名次(排序次鍵),同分個股的排序在多次呼叫間穩定;listing_type 為 null 的代碼 ticker 退回裸代碼。若該裸代碼是純數字的個股代碼(防禦情境,現行資料不存在),它不能回填 /api/participant-flows/government-bank/stock/{ticker}——該端點要求 .TW/.TWO 後綴,會回 422;現行資料中唯一 listing_type 為 NULL 的是指數 ^TWII,指數代碼不需後綴,可直接查詢。

{
  "trade_date": "2026-09-01",
  "side": "buy",
  "items": [
    {
      "rank": 1,
      "stock_code": "2330",
      "ticker": "2330.TW",
      "name": "台積電",
      "listing_type": "tw",
      "net_shares": "600000",
      "net_value": "30000000.25000000"
    }
  ]
}

個股券商分點窗口彙總

GET /api/participant-flows/broker-branch/stock/2330.TW?window=5
Query Type Required Default Validation / Notes
window integer yes 無 只接受 1/5/20/60;其他值或未帶回 422
limit integer no 5000 1..5000;全市場約 820 個分點,一頁即取全量;截斷發生在 ORDER BY net_value DESC NULLS LAST 之後,limit 設小時只會留下淨買超最多的前幾名

窗口 = 整個券商分點 dataset 最近 N 個 distinct trade_date(dataset-wide,非該股自己的最近 N 天);某股在最新市場日無列時 window=1 回 200 + items: []。start_date/end_date/actual_days 描述該窗口(資料不足時 actual_days < window;無交易日時 start_date/end_date 為 null、actual_days 為 0)。每個分點一列(GROUP BY participant_id)、ORDER BY net_value DESC NULLS LAST, participant_id;buy_avg_price/sell_avg_price 是 SUM(value)/NULLIF(SUM(shares),0) 的窗口 VWAP(股數為 0 → null)。窗口內任一日股數非零但金額為 null 時,該側 *_value 與 *_avg_price 回 null(股數仍加總),買/賣兩側各自判斷。淨額則是該日買或賣任一側股數非零且 net_value 為 null 時回 null(淨股數為 0 不代表無交易,例如同日買 100 股賣 100 股);只有買賣兩側股數皆為 0 的日子不影響。

{
  "ticker": "2330.TW",
  "window": 5,
  "start_date": "1902-01-06",
  "end_date": "1902-01-07",
  "actual_days": 2,
  "items": [
    {
      "participant_id": "9A00-9A01",
      "participant_name": "永豐金-台北",
      "buy_shares": "2400000",
      "sell_shares": "800000",
      "net_shares": "1600000",
      "buy_value": "144000000.00000000",
      "sell_value": "47200000.00000000",
      "net_value": "96800000.00000000",
      "buy_avg_price": "60.0000000000000000",
      "sell_avg_price": "59.0000000000000000"
    }
  ]
}

單一分點的個股窗口彙總

GET /api/participant-flows/broker-branch/branch/9A00-9A01?window=5
Query Type Required Default Validation / Notes
window integer yes 無 同上
limit integer no 5000 1..5000;全市場約 2,001 檔個股;截斷發生在 ORDER BY net_value DESC NULLS LAST 之後,limit 設小時只會留下淨買超最多的前幾名

participant_id 為 path 字串(VARCHAR(80),超長回 422)。每檔個股一列(GROUP BY stock_code)、ORDER BY net_value DESC NULLS LAST, stock_code;name/listing_type 來自 LEFT JOIN taiwan.stocks,hub-only 的 ETF/權證代碼兩者為 null、ticker 退回裸代碼。該分點在窗口內無列回 404(與個股 endpoint 的 200 + [] 不對稱:個股有 taiwan.stocks 主檔可判存在,分點沒有名冊主檔)。

{
  "participant_id": "9A00-9A01",
  "participant_name": "永豐金-台北",
  "window": 5,
  "start_date": "1902-01-06",
  "end_date": "1902-01-07",
  "actual_days": 2,
  "items": [
    {
      "stock_code": "2330",
      "ticker": "2330.TW",
      "name": "台積電",
      "listing_type": "tw",
      "buy_shares": "2400000",
      "sell_shares": "800000",
      "net_shares": "1600000",
      "buy_value": "144000000.00000000",
      "sell_value": "47200000.00000000",
      "net_value": "96800000.00000000",
      "buy_avg_price": "60.0000000000000000",
      "sell_avg_price": "59.0000000000000000"
    },
    {
      "stock_code": "00940",
      "ticker": "00940",
      "name": null,
      "listing_type": null,
      "buy_shares": "200000",
      "sell_shares": "0",
      "net_shares": "200000",
      "buy_value": "1960000.00000000",
      "sell_value": "0E-8",
      "net_value": "1960000.00000000",
      "buy_avg_price": "9.8000000000000000",
      "sell_avg_price": null
    }
  ]
}

單一分點的個股逐日明細

GET /api/participant-flows/broker-branch/stock/2330.TW/branch/1480?days=5
Query Type Required Default Validation / Notes
days integer yes 無 1..116(MAX_BROKER_BRANCH_DAILY_DAYS = BROKER_BRANCH_RETENTION_DAYS);⚠️ 與同一個 router 的 government-bank/stock 的 days(日曆天回溯、選填、預設 30、上限 3650)語意與範圍都不同——本參數是交易日筆數、必填、無預設值,也不像 A/B 的 window 限定 1/5/20/60

days 是整個券商分點 dataset 最近 N 個 distinct trade_date(與 A/B 的 window 相同建構,dataset-wide、非該股或該分點自己的最近 N 天),只是這裡可以帶任意整數,不限 1/5/20/60(上限 116,見下)。實際能回的天數受券商分點保留期限制:taiwan.hub_participant_flows 每日由排程 purge 掉超過 BROKER_BRANCH_RETENTION_DAYS(116 個日曆天)的分點列,上限參數 MAX_BROKER_BRANCH_DAILY_DAYS 即由該保留期天數導出(單一定義,保留期變動時一起變動);因此 actual_days 實際上限落在 116 個日曆天內的交易日數(約 84 個交易日),start_date 不會早於約 116 天前——days 帶 85..116 仍是合法請求(只是回得比要求的少),帶 117 以上則回 422(例如 days=120 是參數錯誤,不是回一份截短的資料)。

窗口內每個交易日各回一列,ORDER BY trade_date DESC;該分點當天沒有交易此股時,八個數量欄位一律回 null——判斷「當天有無交易」看 buy_shares 是否為 null(底層欄位為 BIGINT NOT NULL,真實列的 buy_shares 必為非 null),本 endpoint 刻意不提供額外的 traded boolean。buy_avg_price/sell_avg_price 是 value / NULLIF(shares, 0) 的單日均價,公式與 A/B 窗口 VWAP 相同,但這裡是逐日各自計算、不先加總,因此能與上層卡片(A/B)的彙總數字對得起來。當天股數非零但金額為 null(unpriced day)時,該側 *_value/*_avg_price 回 null、*_shares 仍保留真實股數;因為每列的 SQL NULL 已隨計算自然傳遞,這裡不需要 A/B 窗口加總時用的 bool_or 防護。無 limit 參數,列數自然是 min(days, 窗口內可得交易日數)。

該分點在窗口內查無此股的任一列時回 404(與 B 的 404 判斷方式相同:分點沒有名冊主檔,「有無列」是唯一存在性訊號;範圍比 B 更窄,這裡是 ticker + participant_id 兩者同時對得上才算有資料)。

{
  "ticker": "2330.TW",
  "participant_id": "1480",
  "participant_name": "美商高盛",
  "days": 5,
  "start_date": "2026-09-03",
  "end_date": "2026-09-09",
  "actual_days": 5,
  "items": [
    {
      "trade_date": "2026-09-09",
      "buy_shares": "1200000",
      "sell_shares": "300000",
      "net_shares": "900000",
      "buy_value": "72000000.00000000",
      "sell_value": "18150000.00000000",
      "net_value": "53850000.00000000",
      "buy_avg_price": "60.0000000000000000",
      "sell_avg_price": "60.5000000000000000"
    },
    {
      "trade_date": "2026-09-08",
      "buy_shares": null,
      "sell_shares": null,
      "net_shares": null,
      "buy_value": null,
      "sell_value": null,
      "net_value": null,
      "buy_avg_price": null,
      "sell_avg_price": null
    },
    {
      "trade_date": "2026-09-07",
      "buy_shares": "800000",
      "sell_shares": "950000",
      "net_shares": "-150000",
      "buy_value": "48400000.00000000",
      "sell_value": "58900000.00000000",
      "net_value": "-10500000.00000000",
      "buy_avg_price": "60.5000000000000000",
      "sell_avg_price": "62.0000000000000000"
    },
    {
      "trade_date": "2026-09-04",
      "buy_shares": "500000",
      "sell_shares": "200000",
      "net_shares": "300000",
      "buy_value": null,
      "sell_value": "12000000.00000000",
      "net_value": null,
      "buy_avg_price": null,
      "sell_avg_price": "60.0000000000000000"
    },
    {
      "trade_date": "2026-09-03",
      "buy_shares": "600000",
      "sell_shares": "600000",
      "net_shares": "0",
      "buy_value": "36600000.00000000",
      "sell_value": "36300000.00000000",
      "net_value": "300000.00000000",
      "buy_avg_price": "61.0000000000000000",
      "sell_avg_price": "60.5000000000000000"
    }
  ]
}

2026-09-08 是「該分點當天沒有交易此股」的示例(八個數量欄位皆 null);2026-09-04 是「unpriced day」示例(buy_shares 非 null 但當日買方金額不可得,buy_value/buy_avg_price/net_value 隨之為 null,sell 側不受影響)。


錯誤處理

標準錯誤格式

{
  "error": {
    "code": "STOCK_NOT_FOUND",
    "message": "股票代碼不存在",
    "details": {
      "ticker": "INVALID"
    }
  }
}

HTTP 狀態碼

  • 200: 成功
  • 400: 請求錯誤
  • 404: 資源不存在
  • 429: 請求過於頻繁
  • 500: 伺服器錯誤

API 測試

Pytest 範例

import pytest
from httpx import AsyncClient
from api.main import app

@pytest.mark.asyncio
async def test_analyze_stock():
    async with AsyncClient(app=app, base_url="http://test") as ac:
        response = await ac.post(
            "/api/analyze",
            json={
                "ticker": "2330.TW",
                "analysis_type": "comprehensive",
                "timeframe": "3_months"
            }
        )

    assert response.status_code == 200
    data = response.json()
    assert "analysis" in data
    assert data["ticker"] == "2330.TW"

文檔版本: 1.0
最後更新: 2025-02-01