#!/usr/bin/env python3 """ 查詢 TWSE 外資持股比例統計 用法: python3 skills/stocks-query/scripts/twse-foreign-holdings.py 2330 python3 skills/stocks-query/scripts/twse-foreign-holdings.py 3481 資料來源:證交所 TWSE 公開 API(外資及陸資持股統計) """ from __future__ import annotations import json import sys import urllib.request from datetime import datetime, timedelta def fetch_foreign_holdings(stock_no: str, date_str: str | None = None) -> dict: """從 TWSE 抓外資持股資料,自動往前回溯最多 10 天找到有資料的日期。""" if date_str: dates_to_try = [date_str] else: today = datetime.now() dates_to_try = [(today - timedelta(days=i)).strftime("%Y%m%d") for i in range(10)] for d in dates_to_try: url = ( f"https://www.twse.com.tw/rwd/zh/fund/MI_QFIIS" f"?date={d}&selectType=ALL&response=json" ) req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) try: with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode("utf-8", errors="replace")) except Exception: continue if data.get("stat") != "OK": continue tables = data.get("tables", []) if not tables: fields = data.get("fields", []) rows = data.get("data", []) if fields and rows: tables = [{"fields": fields, "data": rows}] else: continue # 找含「證券代號」或「代號」欄位的表 target_table = None for tbl in tables: fields = tbl.get("fields", []) field_str = "".join(fields) if "代號" in field_str: target_table = tbl break if not target_table: if tables: target_table = tables[0] else: continue fields = target_table["fields"] # 找代號欄位 index code_idx = None for i, f in enumerate(fields): if "代號" in f: code_idx = i break if code_idx is None: continue for row in target_table.get("data", []): code = row[code_idx].strip() if code == stock_no: result = {"date": d} for i, field in enumerate(fields): result[field.strip()] = row[i].strip().replace(",", "") if i < len(row) else "-" return result raise RuntimeError(f"找不到 {stock_no} 的外資持股資料(已回溯 10 天)") def print_summary(stock_no: str, d: dict) -> None: date_fmt = f"{d['date'][:4]}/{d['date'][4:6]}/{d['date'][6:]}" name = d.get("證券名稱", d.get("名稱", stock_no)) print(f"{name}({stock_no})|外資持股摘要") print(f"• 日期:{date_fmt}") # 外資持股 shares = d.get("外資及陸資持股股數", d.get("外陸資持股股數", "-")) pct = d.get("外資及陸資持股比率", d.get("外陸資持股比率", d.get("持股比率", "-"))) available = d.get("外資及陸資尚可投資股數", d.get("尚可投資股數", "-")) upper_limit = d.get("外資及陸資共用法令投資上限比率", d.get("投資上限比率", "-")) total_issued = d.get("發行股數", "-") # 外資買賣超相關 foreign_buy = d.get("外資及陸資買進股數", d.get("買進股數", "-")) foreign_sell = d.get("外資及陸資賣出股數", d.get("賣出股數", "-")) print(f"• 外資持股股數:{shares}") print(f"• 外資持股比率:{pct}%") if pct != "-" and "%" not in str(pct) else print(f"• 外資持股比率:{pct}") if total_issued != "-": print(f"• 發行股數:{total_issued}") if available != "-": print(f"• 尚可投資股數:{available}") if upper_limit != "-": print(f"• 投資上限比率:{upper_limit}") if foreign_buy != "-": print(f"• 外資買進股數:{foreign_buy}") if foreign_sell != "-": print(f"• 外資賣出股數:{foreign_sell}") if __name__ == "__main__": stock_no = sys.argv[1] if len(sys.argv) > 1 else "2330" try: data = fetch_foreign_holdings(stock_no) print_summary(stock_no, data) except Exception as e: print(f"取得失敗:{e}", file=sys.stderr) sys.exit(1)