Files
big-qmt/labs/analysis/etf/screen_etf.py
2026-09-21 12:38:13 +08:00

128 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""抓取全市场 ETF 列表(规模 + 成交额),用于挑选各热门板块的代表性标的。
数据源:东方财富行情接口(公开 JSON无需登录
字段f12=代码 f13=市场(0深/1沪) f14=名称 f2=最新价 f3=涨跌幅 f6=成交额 f20=总市值 f21=流通市值
用法: py -3.14 -B labs/analysis/etf/screen_etf.py [关键词...]
不带关键词时按板块关键词分组输出候选。
"""
import json
import sys
import time
import urllib.parse
import urllib.request
try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
BASE_HOSTS = ("82.push2.eastmoney.com", "push2delay.eastmoney.com", "push2.eastmoney.com")
FS = "b:MK0021,b:MK0022,b:MK0023,b:MK0024" # 沪深 ETF/LOF 集合
FIELDS = "f12,f13,f14,f2,f3,f6,f20,f21"
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
"Referer": "https://fund.eastmoney.com/",
}
# 每个板块的候选关键词(用于在名称里匹配);越靠前优先级越高
SECTORS = {
"AI": ["人工智能", "AIETF", "AIETF", "科创AI", "智能"],
"CPO": ["光通信", "通信ETF", "通信设备", "5G通信", "通信"],
"PCB": ["PCB", "电子ETF", "消费电子", "电子50", "电子"],
"人形机器人": ["机器人", "智能制造", "工业母机"],
"": ["光伏", "新能源", "电池"],
"创新药": ["创新药", "医药", "生物医药", "医疗"],
"半导体": ["半导体", "芯片", "集成电路", "科创芯片"],
"存储": ["存储", "集成电路", "科创芯片"],
"半导体材料": ["半导体材料", "材料ETF", "新材料"],
"玻璃基板": ["玻璃", "建材", "新材料"],
"电力": ["电力", "公用事业", "电网", "绿电"],
"航空航天": ["航空", "航天", "军工", "国防"],
"金属": ["有色", "金属", "黄金", "稀土"],
"金融": ["证券", "银行", "保险", "金融"],
"能源": ["能源", "煤炭", "石油", "油气"],
}
def fetch_page(page: int, size: int = 100, retries: int = 4) -> tuple[list[dict], int]:
query = urllib.parse.urlencode({
"pn": page, "pz": size, "po": 1, "np": 1, "fltt": 2, "invt": 2,
"fid": "f3", "fs": FS, "fields": FIELDS,
"ut": "bd1d9ddb04089700cf9c27f6f7426281",
})
last_error = None
for attempt in range(retries):
# 主站 push2 容易被限流返回 502轮换到镜像域名
host = BASE_HOSTS[attempt % len(BASE_HOSTS)]
request = urllib.request.Request(f"http://{host}/api/qt/clist/get?{query}", headers=HEADERS)
try:
with urllib.request.urlopen(request, timeout=30) as response:
payload = json.load(response)
data = payload.get("data") or {}
return data.get("diff") or [], int(data.get("total") or 0)
except Exception as exc: # 502/超时都换域名重试
last_error = exc
time.sleep(1.0 * (attempt + 1))
raise RuntimeError(f"{page} 页抓取失败:{last_error}")
def fetch_all() -> list[dict]:
rows, total = fetch_page(1)
page = 2
while len(rows) < total:
try:
more, _ = fetch_page(page)
except RuntimeError as exc:
print(f"警告:{exc},已抓 {len(rows)}/{total}", file=sys.stderr)
break
if not more:
break
rows.extend(more)
page += 1
time.sleep(0.6) # 轻量限速,避免被限流
return rows
def code_of(row: dict) -> str:
suffix = "SH" if str(row.get("f13")) == "1" else "SZ"
return f"{row['f12']}.{suffix}"
def size_yi(row: dict) -> float:
"""规模(亿元):接口给的是元。"""
value = row.get("f20") or row.get("f21") or 0
return float(value) / 1e8
def turnover_yi(row: dict) -> float:
"""成交额(亿元)。"""
return float(row.get("f6") or 0) / 1e8
def main() -> int:
keywords = sys.argv[1:]
rows = fetch_all()
print(f"抓取到 {len(rows)} 只 ETF/LOF")
if keywords:
for row in rows:
name = str(row.get("f14") or "")
if any(k in name for k in keywords):
print(f"{code_of(row):12} {name:24} 规模={size_yi(row):8.2f}亿 "
f"成交额={turnover_yi(row):7.3f}亿 涨跌={row.get('f3')}%")
return 0
for sector, words in SECTORS.items():
hits = [r for r in rows if any(w in str(r.get("f14") or "") for w in words)]
hits.sort(key=lambda r: -size_yi(r))
print(f"\n== {sector} 候选 {len(hits)} 只(按规模降序,取前 6==")
for row in hits[:6]:
print(f" {code_of(row):12} {str(row.get('f14')):26} "
f"规模={size_yi(row):8.2f}亿 成交额={turnover_yi(row):7.3f}亿")
return 0
if __name__ == "__main__":
raise SystemExit(main())