Files
auv/backend/services/market_extra.py
T

211 lines
7.8 KiB
Python

"""市场补充数据源(供 AI 分析与看板扩展)
- fetch_news: 新浪财经 7x24 快讯
- fetch_sector_fund_flow: 东方财富板块主力资金流排行(行业/概念)
- fetch_margin_summary: 东方财富两融余额汇总(T+1 数据)
均为公开接口,失败时返回 []/None,不阻塞主流程。
"""
import asyncio
import httpx
_TIMEOUT = 10.0
_UA = ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36")
async def fetch_news(limit: int = 30) -> list[dict]:
"""新浪财经 7x24 快讯,返回 [{time: 'MM-DD HH:MM', content}];失败返回 []"""
try:
limit = max(1, min(int(limit or 30), 50))
except (TypeError, ValueError):
limit = 30
try:
async with httpx.AsyncClient(timeout=_TIMEOUT, headers={"User-Agent": _UA}) as client:
resp = await client.get(
"https://zhibo.sina.com.cn/api/zhibo/feed",
params={"page": 1, "page_size": limit, "zhibo_id": 152, "tag_id": 0},
)
resp.raise_for_status()
items = resp.json()["result"]["data"]["feed"]["list"]
news = []
for it in items or []:
text = (it.get("rich_text") or "").strip()
if not text:
continue
news.append({
"time": (it.get("create_time") or "")[5:16], # 'MM-DD HH:MM'
"content": text[:300],
})
return news
except Exception:
return []
_FLOW_HOSTS = (
# push2 对部分客户端有 TLS 指纹拦截(peer closed),delay 镜像同接口且稳定
"https://push2delay.eastmoney.com",
"https://push2.eastmoney.com",
)
async def _fetch_flow_boards(client: httpx.AsyncClient, fs: str, po: int, pz: int) -> list[dict]:
"""拉取一类板块的主力净流入排行。po=1 降序(净流入最多),po=0 升序(净流出最多)
查询串保持字面量 + 号(与东财网页请求一致),逐 host 尝试。
"""
qs = (f"/api/qt/clist/get?fid=f62&po={po}&pz={pz}&pn=1&np=1"
f"&fltt=2&invt=2&fs={fs}&fields=f12,f14,f62,f184")
last_err: Exception | None = None
for host in _FLOW_HOSTS:
try:
resp = await client.get(host + qs)
resp.raise_for_status()
diff = (resp.json().get("data") or {}).get("diff") or []
break
except Exception as e:
last_err = e
diff = []
else:
raise ConnectionError(f"板块资金流全部数据源失败: {last_err}")
if isinstance(diff, dict): # 兼容旧版 {index: item} 结构
diff = list(diff.values())
boards = []
for d in diff:
amt = d.get("f62")
if not isinstance(amt, (int, float)):
continue
boards.append({
"code": d.get("f12", ""),
"name": d.get("f14", ""),
"mainNet": round(amt / 1e8, 1), # 亿元
"mainNetPct": d.get("f184"), # 主力净占比 %
})
return boards
_GLOBAL_INDICES = "100.NDX,100.DJIA,100.SPX,100.HSI,100.N225,100.FTSE"
_FUT_MARKETS = ("m:8", "m:113", "m:142", "m:114", "m:115") # 中金所/上期所/上期能源/大商所/郑商所
async def fetch_global_markets() -> dict | None:
"""海外主要指数 + 国内期货主力合约;失败返回 None"""
try:
async with httpx.AsyncClient(timeout=_TIMEOUT, headers={"User-Agent": _UA}) as client:
overseas, futures = await asyncio.gather(
_fetch_overseas_indices(client),
_fetch_futures_main(client),
)
if not overseas and not futures:
return None
return {"overseas": overseas, "futures": futures}
except Exception:
return None
async def _fetch_overseas_indices(client: httpx.AsyncClient) -> list[dict]:
resp = await client.get(
"https://push2delay.eastmoney.com/api/qt/ulist.np/get",
params={"secids": _GLOBAL_INDICES, "fields": "f12,f14,f2,f3", "fltt": 2, "invt": 2},
)
resp.raise_for_status()
diff = (resp.json().get("data") or {}).get("diff") or []
return [
{"name": d.get("f14", ""), "price": d.get("f2"), "changePct": d.get("f3")}
for d in diff
]
async def _fetch_futures_main(client: httpx.AsyncClient) -> list[dict]:
"""国内期货主力合约(名称含"主连/主力合约"),按成交额降序取前12"""
import re
results: list[dict] = []
for fs in _FUT_MARKETS:
try:
resp = await client.get(
"https://push2delay.eastmoney.com/api/qt/clist/get",
params={"fid": "f6", "po": 1, "pz": 200, "pn": 1, "np": 1,
"fltt": 2, "invt": 2, "fs": fs, "fields": "f12,f14,f2,f3,f6"},
)
resp.raise_for_status()
diff = (resp.json().get("data") or {}).get("diff") or []
except Exception:
continue
for d in diff:
name = d.get("f14") or ""
if "主连" not in name and "主力合约" not in name:
continue
if "次主连" in name: # 次主力合约,排除
continue
amount = d.get("f6")
if not isinstance(amount, (int, float)):
continue
results.append({
"name": re.sub(r"(主连|主力合约)$", "", name),
"price": d.get("f2"),
"changePct": d.get("f3"),
"amountYi": round(amount / 1e8, 1),
})
results.sort(key=lambda x: -x["amountYi"])
return results[:12]
async def fetch_sector_fund_flow() -> dict | None:
"""板块主力资金流排行:行业净流入/净流出 TOP6 + 概念净流入 TOP6;失败返回 None"""
try:
async with httpx.AsyncClient(timeout=_TIMEOUT, headers={"User-Agent": _UA}) as client:
industry_in, industry_out, concept_in = await asyncio.gather(
_fetch_flow_boards(client, "m:90+t:2", 1, 6),
_fetch_flow_boards(client, "m:90+t:2", 0, 6),
_fetch_flow_boards(client, "m:90+t:3", 1, 6),
)
return {
"industryInflow": industry_in, # 主力净流入降序
"industryOutflow": industry_out, # 升序(净流出最多在前)
"conceptInflow": concept_in,
"unit": "亿元",
}
except Exception:
return None
async def fetch_margin_summary() -> dict | None:
"""沪深北两融余额汇总(交易所 T+1 披露);失败返回 None"""
try:
async with httpx.AsyncClient(timeout=_TIMEOUT, headers={"User-Agent": _UA}) as client:
resp = await client.get(
"https://datacenter-web.eastmoney.com/api/data/v1/get",
params={
"reportName": "RPTA_RZRQ_LSHJ",
"columns": "ALL",
"source": "WEB",
"sortColumns": "dim_date",
"sortTypes": "-1",
"pageSize": 2,
"pageNumber": 1,
},
)
resp.raise_for_status()
rows = ((resp.json().get("result") or {}).get("data")) or []
if not rows:
return None
def _balance(row: dict) -> float:
return float(row.get("RZYE") or 0) + float(row.get("RQYE") or 0)
latest = rows[0]
prev = rows[1] if len(rows) > 1 else None
balance = _balance(latest)
change = (balance - _balance(prev)) if prev else None
return {
"date": (latest.get("DIM_DATE") or "")[:10],
"balanceYi": round(balance / 1e8), # 亿元
"changeYi": round(change / 1e8) if change is not None else None,
"rzjmeYi": round(float(latest.get("RZJME") or 0) / 1e8, 1), # 融资净买入
}
except Exception:
return None