Recover 7 existing scripts from remote repo and rebuild 5 missing ones: - twse-margin-trading.py (融資融券) - twse-day-trading.py (當沖交易) - twse-foreign-holdings.py (外資持股) - yahoo-dividend-browserless.py (股利資訊) - yahoo-broker-trading-browserless.py (主力券商進出) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
129 lines
4.4 KiB
Python
129 lines
4.4 KiB
Python
#!/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)
|