feat: AI分析第二档——get_news消息面、板块主力资金流TOPS、两融余额、截断续写机制

This commit is contained in:
Sakurasan
2026-09-02 03:50:43 +08:00
parent 0fb2f8d3a5
commit 311b9f0d7e
5 changed files with 250 additions and 19 deletions
+143
View File
@@ -0,0 +1,143 @@
"""市场补充数据源(供 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
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