Files
stock/backend/app/data/etf_provider.py
2026-09-07 18:07:31 +08:00

110 lines
4.0 KiB
Python
Raw Permalink 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 快照(免 token 直连 HTTP仅用于 etf_basic 列表与规模字段)。
K 线数据不走这里:统一走 Tushare fund_daily见 etf_sync / fetcher
东财在链路里只承担一件 Tushare quicksync 镜像做不到的事——
全市场 ETF 名单 + 总市值/流通市值/换手率(镜像上 fund_etf_basic 不存在)。
接口注意clist 实测 pz 上限 100传 50000 也只回 100必须按 pn 翻页
拿全 ~1600 只push2 主站短连发几次会直接断连push2delay 镜像稳,
列表/市值用延迟值无妨(价格另有 K 线)。
"""
from __future__ import annotations
import asyncio
import httpx
from .symbols import is_etf_symbol
_HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
}
# 东财 ETF 板块akshare fund_etf_spot_em 同款集合:股票/混合/债券/货币/商品/跨境等)
_SPOT_HOSTS = [
"https://push2delay.eastmoney.com",
"https://push2.eastmoney.com",
]
_SPOT_PARAMS = {
"pn": "1",
"pz": "100",
"po": "1",
"np": "1",
"ut": "bd1d9ddb04089700cf9c27f6f7426281",
"fltt": "2",
"invt": "2",
"fid": "f12",
"fs": "b:MK0021,b:MK0022,b:MK0023,b:MK0024,b:MK0026,b:MK0027,b:MK0028,b:MK0029",
# f12 代码 / f13 市场(1沪0深) / f14 名称 / f8 换手% / f20 总市值 / f21 流通市值(元)
"fields": "f12,f13,f14,f8,f20,f21",
}
def _num(v) -> float | None:
"""东财 fltt=2 下停牌/缺数据的字段是 '-' 字符串。"""
if v is None or isinstance(v, str):
return None
f = float(v)
return None if f != f else f
def new_client() -> httpx.AsyncClient:
"""统一构造(超时/UA同步工厂`async with new_client() as c` 使用。"""
return httpx.AsyncClient(timeout=httpx.Timeout(15.0), headers=_HEADERS)
async def _fetch_spot_page(client: httpx.AsyncClient, host: str, pn: int):
params = {**_SPOT_PARAMS, "pn": str(pn)}
resp = await client.get(f"{host}/api/qt/clist/get", params=params)
resp.raise_for_status()
data = resp.json().get("data") or {}
return data.get("total") or 0, data.get("diff") or []
async def fetch_etf_spot(client: httpx.AsyncClient) -> list[dict]:
"""全市场场内 ETF 快照(翻页拿全 ~1600 只)-> [{ts_code, symbol, name, exchange,
turnover_rate, total_mv, circ_mv}]。空结果视为异常(接口改版/被拦截时宁可不覆盖表)。"""
diff: list[dict] = []
for host in _SPOT_HOSTS:
try:
total, first = await _fetch_spot_page(client, host, 1)
diff = first
pn = 2
while total and len(diff) < total:
await asyncio.sleep(0.15) # 翻页间隔,礼貌控频
_, page = await _fetch_spot_page(client, host, pn)
if not page:
break
diff.extend(page)
pn += 1
break # 首个可用 host 拿完即止
except Exception: # noqa: BLE001 —— 主镜像抖动换备用镜像整重来
diff = []
continue
rows: list[dict] = []
for d in diff:
code = str(d.get("f12") or "").strip()
name = str(d.get("f14") or "").strip()
market = d.get("f13")
if not code or not name or market is None:
continue
if not is_etf_symbol(code):
continue # 板块返回里混进的 LOF/封基16/50/57 开头)不进 ETF 表
exchange = "SH" if int(market) == 1 else "SZ"
rows.append({
"ts_code": f"{code}.{exchange}",
"symbol": code,
"name": name,
"exchange": exchange,
"turnover_rate": _num(d.get("f8")),
"total_mv": _num(d.get("f20")),
"circ_mv": _num(d.get("f21")),
})
if not rows:
raise RuntimeError("东财 ETF 快照为空(接口可能改版或被限流)")
return rows
__all__ = ["fetch_etf_spot", "new_client"]