feat: 移除 akshare 依赖,全部替换为直连 HTTP API

所有 18 个 akshare 调用替换为东财 datacenter/push2、新浪财经、
同花顺 10jqka、财联社 cls.cn 等直连 HTTP API。
彻底消除 akshare+pandas3.0+pyarrow 的 ArrowInvalid 崩溃,
以及 akshare 与 mootdx 的 httpx 版本冲突。

函数签名和返回格式保持不变,对上层 Agent 透明。
This commit is contained in:
simon
2026-05-17 17:26:39 +08:00
parent 9de2004fb2
commit f3c0fe4cfd
6 changed files with 470 additions and 248 deletions
+28
View File
@@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Breaking changes within the 0.x line are called out explicitly. Breaking changes within the 0.x line are called out explicitly.
## [0.2.5] — 2026-05-17
### Breaking Changes
- **移除 akshare 依赖** — `akshare>=1.18.0``pyproject.toml` 中删除。
所有原 akshare 调用已替换为直接 HTTP API(东财 datacenter、新浪财经、
同花顺 10jqka、财联社 cls.cn、百度股市通)。
### Changed
- `tradingagents/dataflows/a_stock.py` 全面重构数据获取层:
- `get_stock_data()` → 新浪 JSON K线 API + push2.eastmoney 实时行情
- `get_stock_info()` → push2.eastmoney 个股基本信息
- `get_stock_news()` → 东财 np-weblist 滚动新闻(已有,无变化)
- `get_financial_data()` → 新浪财经财报三表 API
- `get_market_news()` → 财联社 cls.cn 快讯 + 东财 np-weblist
- `get_analyst_forecast()` → 同花顺 10jqka EPS 一致预期
- `get_dragon_tiger_board()` → 东财 datacenter RPT_DAILYBILLBOARD
- `get_restricted_release()` → 东财 datacenter RPT_LIFT_STAGE
- `get_industry_overview()` → push2.eastmoney 板块行情
- 新增内部 helper`_eastmoney_datacenter()``_ths_eps_forecast()``_sina_kline_fallback()`
- 所有函数签名和返回格式保持不变,对上层 Agent 透明
### Fixed
- 彻底消除 akshare + pandas 3.0 + pyarrow 的 `ArrowInvalid` 崩溃问题
- 消除 akshare 与 mootdx 的 httpx 版本冲突
## [0.2.4] — 2026-04-25 ## [0.2.4] — 2026-04-25
### Added ### Added
+11 -7
View File
@@ -6,18 +6,22 @@
- **仓库**: https://github.com/simonlin1212/TradingAgents-astock - **仓库**: https://github.com/simonlin1212/TradingAgents-astock
- **协议**: Apache 2.0 - **协议**: Apache 2.0
- **Python**: >=3.10 - **Python**: >=3.10
- **当前版本**: 0.2.4 - **当前版本**: 0.2.5
## 架构 ## 架构
### 数据层 ### 数据层v0.2.5 全部直连 HTTP,零第三方数据库依赖)
| 来源 | 协议 | 数据 | | 来源 | 协议 | 数据 |
|------|------|------| |------|------|------|
| mootdx | TCP 7709 | OHLCV K线、财务快照、F10 文本 | | mootdx | TCP 7709 | OHLCV K线、财务快照、F10 文本 |
| 腾讯财经 | HTTP (qt.gtimg.cn) | PE/PB/市值/换手率 | | 腾讯财经 | HTTP (qt.gtimg.cn) | PE/PB/市值/换手率 |
| 东方财富 | HTTP (直连 API) | 个股新闻 | | 东方财富 datacenter | HTTP (datacenter-web) | 龙虎榜、限售解禁、板块行情 |
| akshare | Python 库 | 财报三表、股票信息、一致预期 EPS | | 东方财富 push2 | HTTP (push2.eastmoney) | 实时行情、个股信息、板块列表 |
| 同花顺 | HTTP | 热股题材归因、北向资金 | | 东方财富 np-weblist | HTTP | 滚动新闻 |
| 新浪财经 | HTTP (money.finance.sina) | K线历史、财报三表 |
| 同花顺 10jqka | HTTP | EPS 一致预期、热股题材 |
| 财联社 cls.cn | HTTP | 全球财经快讯 |
| 百度股市通 | HTTP (gushitong.baidu) | 备用行情 |
### Agent 角色(7 个) ### Agent 角色(7 个)
原版 4 个(市场/情绪/新闻/基本面)+ A 股特化 3 个(政策分析师/游资追踪/解禁监控) 原版 4 个(市场/情绪/新闻/基本面)+ A 股特化 3 个(政策分析师/游资追踪/解禁监控)
@@ -37,8 +41,8 @@
### 依赖冲突 ### 依赖冲突
mootdx 锁死 httpx==0.25.2,与 langchain-google-genai 的 httpx>=0.28.1 冲突。不用 Google 模型时可 `pip install mootdx --no-deps` 绕过。 mootdx 锁死 httpx==0.25.2,与 langchain-google-genai 的 httpx>=0.28.1 冲突。不用 Google 模型时可 `pip install mootdx --no-deps` 绕过。
### akshare 上游 bug ### akshare 已移除(v0.2.5
akshare 在 pandas 3.0 + pyarrow 环境下,`stock_news_em``str.replace(r" ", "", regex=True)` 会崩。用户需升级 akshare 或手动改 `regex=False`。已在项目中增加东方财富直连 API 作为替代方案 v0.2.5 起完全移除 akshare 依赖,所有数据通过直连 HTTP API 获取。彻底消除了 akshare + pandas 3.0 + pyarrow 的 `ArrowInvalid` 崩溃问题,也消除了 akshare 与 mootdx 的 httpx 版本冲突
### 模型兼容性 ### 模型兼容性
deepseek-v4-flash 等模型在 tool call 时可能返回中文股票名而非 6 位代码。`safe_ticker_component` 已加兜底自动转码,但不同模型表现仍有差异。 deepseek-v4-flash 等模型在 tool call 时可能返回中文股票名而非 6 位代码。`safe_ticker_component` 已加兜底自动转码,但不同模型表现仍有差异。
+7 -4
View File
@@ -46,7 +46,7 @@
| 维度 | 原版 | 本 Fork | | 维度 | 原版 | 本 Fork |
|------|------|---------| |------|------|---------|
| **数据源** | Yahoo Finance / Alpha Vantage | mootdx + 腾讯财经 + akshare(全免费) | | **数据源** | Yahoo Finance / Alpha Vantage | mootdx + 东财 + 新浪 + 同花顺(全免费直连 |
| **Analyst 角色** | 4 个(市场/情绪/新闻/基本面) | **7 个**(+政策分析师/游资追踪/解禁监控) | | **Analyst 角色** | 4 个(市场/情绪/新闻/基本面) | **7 个**(+政策分析师/游资追踪/解禁监控) |
| **交易规则** | 美股(T+0、无涨跌停) | A 股(T+1、涨跌停、最小手数、交易时段) | | **交易规则** | 美股(T+0、无涨跌停) | A 股(T+1、涨跌停、最小手数、交易时段) |
| **输出语言** | 英文 | 中文报告(内部辩论保持英文以保证推理质量) | | **输出语言** | 英文 | 中文报告(内部辩论保持英文以保证推理质量) |
@@ -60,7 +60,7 @@
|------|-------------------|-------------| |------|-------------------|-------------|
| 许可证 | Apache 2.0 | **全 Apache 2.0** | | 许可证 | Apache 2.0 | **全 Apache 2.0** |
| 部署依赖 | pip install | **开箱即用** | | 部署依赖 | pip install | **开箱即用** |
| A 股数据 | ❌ | **mootdx + 腾讯 + akshare(免费** | | A 股数据 | ❌ | **mootdx + 东财 + 新浪 + 同花顺(直连 HTTP** |
| A 股特化角色 | ❌ | **政策/游资/解禁 3 个深度角色** | | A 股特化角色 | ❌ | **政策/游资/解禁 3 个深度角色** |
| A 股交易约束 | ❌ | **T+1/涨跌停/手数/ST 全覆盖** | | A 股交易约束 | ❌ | **T+1/涨跌停/手数/ST 全覆盖** |
@@ -130,7 +130,10 @@
|------|------|---------| |------|------|---------|
| **mootdx** | TCP 7709 | OHLCV K 线、财务快照、F10 文本 | | **mootdx** | TCP 7709 | OHLCV K 线、财务快照、F10 文本 |
| **腾讯财经** | HTTP (`qt.gtimg.cn`) | PE / PB / 市值 / 换手率(实时) | | **腾讯财经** | HTTP (`qt.gtimg.cn`) | PE / PB / 市值 / 换手率(实时) |
| **akshare** | Python 库 | 新闻、财报三表(sina 源)、股票信息 | | **东方财富** | HTTP (datacenter / push2) | 龙虎榜、限售解禁、板块行情、个股信息 |
| **新浪财经** | HTTP | K 线历史、财报三表 |
| **同花顺** | HTTP (10jqka) | EPS 一致预期 |
| **财联社** | HTTP (cls.cn) | 全球财经快讯 |
> 完全不依赖 Tushare(积分墙)、Alpha Vantage(海外 API)、Yahoo Finance(不支持 A 股)。 > 完全不依赖 Tushare(积分墙)、Alpha Vantage(海外 API)、Yahoo Finance(不支持 A 股)。
@@ -297,7 +300,7 @@ TradingAgents-Astock/
│ │ ├── trader/ # TraderA 股交易约束) │ │ ├── trader/ # TraderA 股交易约束)
│ │ └── utils/ # 状态定义、工具函数 │ │ └── utils/ # 状态定义、工具函数
│ ├── dataflows/ │ ├── dataflows/
│ │ ├── a_stock.py # A 股数据 vendormootdx + 腾讯 + akshare │ │ ├── a_stock.py # A 股数据 vendor直连 HTTP API,零第三方库
│ │ ├── interface.py # 数据接口抽象层 │ │ ├── interface.py # 数据接口抽象层
│ │ └── ... │ │ └── ...
│ └── graph/ │ └── graph/
+1 -2
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "tradingagents-astock" name = "tradingagents-astock"
version = "0.2.4" version = "0.2.5"
description = "A股多Agent投研框架 — 基于 TradingAgents 深度特化" description = "A股多Agent投研框架 — 基于 TradingAgents 深度特化"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -44,7 +44,6 @@ dependencies = [
"fpdf2>=2.8.0", "fpdf2>=2.8.0",
"python-dotenv>=1.1.0", "python-dotenv>=1.1.0",
"mootdx>=0.10.0", "mootdx>=0.10.0",
"akshare>=1.18.0",
] ]
[project.scripts] [project.scripts]
+2 -2
View File
@@ -15,7 +15,7 @@ config["deep_think_llm"] = "claude-sonnet-4-6" # Kimi maps internally
config["quick_think_llm"] = "claude-sonnet-4-6" config["quick_think_llm"] = "claude-sonnet-4-6"
config["backend_url"] = "https://api.kimi.com/coding/" config["backend_url"] = "https://api.kimi.com/coding/"
# --- Data: A-stock vendor (mootdx + tencent + akshare) --- # --- Data: A-stock vendor (mootdx + tencent + eastmoney + sina) ---
config["data_vendors"] = { config["data_vendors"] = {
"core_stock_apis": "a_stock", "core_stock_apis": "a_stock",
"technical_indicators": "a_stock", "technical_indicators": "a_stock",
@@ -33,7 +33,7 @@ print("TradingAgents-Astock E2E Test")
print("Ticker: 688017") print("Ticker: 688017")
print("Trade date: 2026-04-30") print("Trade date: 2026-04-30")
print("LLM: Kimi 2.6 via Anthropic API") print("LLM: Kimi 2.6 via Anthropic API")
print("Data: a_stock (mootdx + tencent + akshare)") print("Data: a_stock (mootdx + tencent + eastmoney + sina)")
print("=" * 60) print("=" * 60)
ta = TradingAgentsGraph(debug=True, config=config) ta = TradingAgentsGraph(debug=True, config=config)
+421 -233
View File
@@ -1,15 +1,15 @@
"""A-stock (China mainland) data vendor for TradingAgents. """A-stock (China mainland) data vendor for TradingAgents.
Bridges EP11's a-stock-data Skill (mootdx + Tencent Finance + akshare + 同花顺) Zero third-party data dependency (no akshare). All sources are direct HTTP APIs
into TradingAgents' plugin architecture. 12 vendor methods implemented. or mootdx TCP.
Data sources: Data sources:
- mootdx (TCP 7709): OHLCV K-lines, financial snapshots, F10 text - mootdx (TCP 7709): OHLCV K-lines, financial snapshots, F10 text
- Tencent Finance (HTTP GBK): PE/PB/market cap/turnover - Tencent Finance (HTTP GBK): PE/PB/market cap/turnover
- akshare (Python): financial statements, stock info, consensus EPS - 东方财富 push2 / datacenter-web (direct HTTP): stock info, dragon-tiger, lockup
- 东方财富 (direct HTTP): individual stock news - 新浪财经 (direct HTTP): K-line fallback, financial statements
- 新浪财经 (HTTP): stock news (backup) - 同花顺 (direct HTTP): consensus EPS, hot stocks, northbound capital flow
- 同花顺 (HTTP): hot stocks topic attribution, northbound capital flow - 财联社 (direct HTTP): global news wire
""" """
from __future__ import annotations from __future__ import annotations
@@ -197,6 +197,120 @@ def _tencent_quote(codes: list[str]) -> dict[str, dict]:
return result return result
# ---------------------------------------------------------------------------
# Eastmoney Datacenter unified helper (龙虎榜/解禁 etc.)
# ---------------------------------------------------------------------------
_DATACENTER_URL = "https://datacenter-web.eastmoney.com/api/data/v1/get"
_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
def _eastmoney_datacenter(
report_name: str,
columns: str = "ALL",
filter_str: str = "",
page_size: int = 50,
sort_columns: str = "",
sort_types: str = "-1",
) -> list[dict]:
"""东财数据中心统一查询 — 龙虎榜/解禁 共用."""
params = {
"reportName": report_name,
"columns": columns,
"filter": filter_str,
"pageNumber": "1",
"pageSize": str(page_size),
"sortColumns": sort_columns,
"sortTypes": sort_types,
"source": "WEB",
"client": "WEB",
}
r = _requests.get(
_DATACENTER_URL, params=params, headers={"User-Agent": _UA}, timeout=15
)
d = r.json()
if d.get("result") and d["result"].get("data"):
return d["result"]["data"]
return []
# ---------------------------------------------------------------------------
# 同花顺 EPS forecast helper (direct HTTP, no akshare)
# ---------------------------------------------------------------------------
def _ths_eps_forecast(code: str) -> pd.DataFrame:
"""Fetch consensus EPS forecast from 同花顺 (direct HTTP).
Returns DataFrame with columns roughly: 年度, 预测机构数, 最小值, 均值, 最大值.
"""
url = f"https://basic.10jqka.com.cn/new/{code}/worth.html"
headers = {
"User-Agent": _UA,
"Referer": "https://basic.10jqka.com.cn/",
}
r = _requests.get(url, headers=headers, timeout=15)
r.encoding = "gbk"
dfs = pd.read_html(r.text)
# Find the table containing EPS data
for df in dfs:
cols = [str(c) for c in df.columns]
if any("每股收益" in c or "均值" in c for c in cols):
return df
# Fallback: return first table if exists
return dfs[0] if dfs else pd.DataFrame()
# ---------------------------------------------------------------------------
# Sina K-line fallback helper (direct HTTP, no akshare)
# ---------------------------------------------------------------------------
def _sina_kline_fallback(code: str, start_date: str = None, end_date: str = None) -> pd.DataFrame:
"""Fetch daily K-line from Sina HTTP API as mootdx fallback.
Returns DataFrame with columns: Date, Open, High, Low, Close, Volume.
"""
prefix = "sh" if code.startswith("6") else "sz"
url = (
"http://money.finance.sina.com.cn/quotes_service/api/json_v2.php/"
"CN_MarketData.getKLineData"
)
params = {
"symbol": f"{prefix}{code}",
"scale": "240", # daily
"ma": "no",
"datalen": "800",
}
r = _requests.get(url, params=params, timeout=15)
r.raise_for_status()
data = _json.loads(r.text)
if not data:
return pd.DataFrame()
rows = []
for item in data:
rows.append({
"Date": item["day"],
"Open": float(item["open"]),
"High": float(item["high"]),
"Low": float(item["low"]),
"Close": float(item["close"]),
"Volume": int(item["volume"]),
})
df = pd.DataFrame(rows)
df["Date"] = pd.to_datetime(df["Date"])
if start_date:
df = df[df["Date"] >= pd.to_datetime(start_date)]
if end_date:
df = df[df["Date"] <= pd.to_datetime(end_date)]
return df
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# OHLCV loading with cache (mootdx -> CSV) # OHLCV loading with cache (mootdx -> CSV)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -250,32 +364,14 @@ def _load_ohlcv_astock(symbol: str, curr_date: str) -> pd.DataFrame:
df = df[["Date", "Open", "High", "Low", "Close", "Volume"]] df = df[["Date", "Open", "High", "Low", "Close", "Volume"]]
df["Date"] = pd.to_datetime(df["Date"]) df["Date"] = pd.to_datetime(df["Date"])
except Exception as e: except Exception as e:
logger.warning("mootdx OHLCV failed for %s: %s, trying akshare fallback", code, e) logger.warning("mootdx OHLCV failed for %s: %s, trying sina HTTP fallback", code, e)
# Fallback: akshare sina source # Fallback: Sina direct HTTP API
try: try:
import akshare as ak df = _sina_kline_fallback(code)
if df.empty:
prefix = "sh" if code.startswith("6") else "sz" raise ValueError(f"No OHLCV data from sina for {code}")
sina_symbol = f"{prefix}{code}"
# Calculate date range for akshare query (~3 years back)
end_dt = datetime.now()
start_dt = end_dt - relativedelta(years=3)
df_ak = ak.stock_zh_a_daily(
symbol=sina_symbol,
start_date=start_dt.strftime("%Y%m%d"),
end_date=end_dt.strftime("%Y%m%d"),
adjust="qfq",
)
if df_ak is None or df_ak.empty:
raise ValueError(f"No OHLCV data from akshare for {code}")
df_ak = df_ak.rename(columns={
"date": "Date", "open": "Open", "close": "Close",
"high": "High", "low": "Low", "volume": "Volume",
})
df_ak["Date"] = pd.to_datetime(df_ak["Date"])
df = df_ak[["Date", "Open", "High", "Low", "Close", "Volume"]].copy()
except Exception: except Exception:
raise ValueError(f"No OHLCV data from mootdx for {code}") raise ValueError(f"No OHLCV data from mootdx/sina for {code}")
# Cache to disk # Cache to disk
df.to_csv(cache_file, index=False, encoding="utf-8") df.to_csv(cache_file, index=False, encoding="utf-8")
@@ -329,33 +425,15 @@ def get_stock_data(
df["Date"] = pd.to_datetime(df["Date"]) df["Date"] = pd.to_datetime(df["Date"])
except Exception as e: except Exception as e:
logger.warning("mootdx K-line failed for %s: %s, trying akshare fallback", code, e) logger.warning("mootdx K-line failed for %s: %s, trying sina HTTP fallback", code, e)
# Fallback: akshare sina source # Fallback: Sina direct HTTP API
try: try:
import akshare as ak df = _sina_kline_fallback(code, start_date, end_date)
if df.empty:
prefix = "sh" if code.startswith("6") else "sz" return "K线数据获取失败:mootdx和新浪备用源均不可用,请检查网络连接"
sina_symbol = f"{prefix}{code}" data_source = "sina HTTP (fallback)"
start_str = start_date.replace("-", "")
end_str = end_date.replace("-", "")
df_ak = ak.stock_zh_a_daily(
symbol=sina_symbol,
start_date=start_str,
end_date=end_str,
adjust="qfq",
)
if df_ak is not None and not df_ak.empty:
df_ak = df_ak.rename(columns={
"date": "Date", "open": "Open", "close": "Close",
"high": "High", "low": "Low", "volume": "Volume",
})
df_ak["Date"] = pd.to_datetime(df_ak["Date"])
df = df_ak
data_source = "akshare (sina fallback)"
else:
return "K线数据获取失败:mootdx和akshare备用源均不可用,请检查网络连接"
except Exception: except Exception:
return "K线数据获取失败:mootdx和akshare备用源均不可用,请检查网络连接" return "K线数据获取失败:mootdx和新浪备用源均不可用,请检查网络连接"
# Filter by date range # Filter by date range
start_dt = pd.to_datetime(start_date) start_dt = pd.to_datetime(start_date)
@@ -473,7 +551,7 @@ def get_fundamentals(
ticker: Annotated[str, "A-stock code"], ticker: Annotated[str, "A-stock code"],
curr_date: Annotated[str, "current date"] = None, curr_date: Annotated[str, "current date"] = None,
) -> str: ) -> str:
"""Get company fundamentals from Tencent + mootdx + akshare.""" """Get company fundamentals from Tencent + mootdx + Eastmoney + 同花顺."""
code = _normalize_ticker(ticker) code = _normalize_ticker(ticker)
try: try:
@@ -528,42 +606,63 @@ def get_fundamentals(
except Exception as e: except Exception as e:
logger.warning("mootdx finance failed for %s: %s", code, e) logger.warning("mootdx finance failed for %s: %s", code, e)
# --- akshare: basic stock info --- # --- Eastmoney push2: basic stock info (direct HTTP) ---
try: try:
import akshare as ak market_code = 1 if code.startswith("6") else 0
_info_url = "https://push2.eastmoney.com/api/qt/stock/get"
info_df = ak.stock_individual_info_em(symbol=code) _info_params = {
if info_df is not None and not info_df.empty: "fltt": "2",
for _, row in info_df.iterrows(): "invt": "2",
item = str(row.get("item", "")) "fields": "f57,f58,f84,f85,f127,f116,f117,f189,f43",
value = str(row.get("value", "")) "secid": f"{market_code}.{code}",
if item and value and value != "nan": }
lines.append(f"{item}: {value}") r = _requests.get(
except Exception as e: _info_url, params=_info_params,
logger.warning("akshare stock_individual_info_em failed for %s: %s", code, e) headers={"User-Agent": _UA}, timeout=10,
# --- akshare: consensus EPS forecast (同花顺) ---
try:
import akshare as ak
forecast_df = ak.stock_profit_forecast_ths(
symbol=code, indicator="预测年报每股收益"
) )
d = r.json().get("data", {})
if d:
if d.get("f127"):
lines.append(f"行业: {d['f127']}")
if d.get("f84"):
lines.append(f"总股本: {d['f84']}")
if d.get("f85"):
lines.append(f"流通股本: {d['f85']}")
if d.get("f116"):
lines.append(f"总市值: {d['f116']}")
if d.get("f117"):
lines.append(f"流通市值: {d['f117']}")
if d.get("f189"):
lines.append(f"上市日期: {d['f189']}")
except Exception as e:
logger.warning("eastmoney push2 stock info failed for %s: %s", code, e)
# --- 同花顺 direct HTTP: consensus EPS forecast ---
try:
forecast_df = _ths_eps_forecast(code)
if forecast_df is not None and not forecast_df.empty: if forecast_df is not None and not forecast_df.empty:
lines.append("\n--- Consensus EPS Forecast (同花顺) ---") lines.append("\n--- Consensus EPS Forecast (同花顺) ---")
eps_by_year = {} eps_by_year = {}
for _, row in forecast_df.iterrows(): for _, row in forecast_df.iterrows():
year = str(row.get("年度", "")) year = str(row.iloc[0]) if len(row) > 0 else ""
mean_eps = float(row.get("均值", 0)) mean_eps_val = row.iloc[3] if len(row) > 3 else 0
count = int(row.get("预测机构数", 0)) count_val = row.iloc[1] if len(row) > 1 else 0
min_eps = row.get("最小值", "N/A") min_eps_val = row.iloc[2] if len(row) > 2 else "N/A"
max_eps = row.get("最大值", "N/A") max_eps_val = row.iloc[4] if len(row) > 4 else "N/A"
try:
mean_eps = float(mean_eps_val)
except (ValueError, TypeError):
mean_eps = 0
try:
count = int(count_val)
except (ValueError, TypeError):
count = 0
lines.append( lines.append(
f"FY{year}: EPS={mean_eps} " f"FY{year}: EPS={mean_eps} "
f"(range {min_eps}~{max_eps}, {count} analysts)" f"(range {min_eps_val}~{max_eps_val}, {count} analysts)"
) )
if count < 3: if count < 3:
lines.append(f" Warning: low coverage (<3 analysts)") lines.append(" Warning: low coverage (<3 analysts)")
eps_by_year[year] = mean_eps eps_by_year[year] = mean_eps
# Forward PE / PEG / PE digestion # Forward PE / PEG / PE digestion
@@ -635,18 +734,37 @@ def _sina_stock_code(code: str) -> str:
def _get_financial_report_sina( def _get_financial_report_sina(
code: str, report_type: str, freq: str, curr_date: str = None, code: str, report_type: str, freq: str, curr_date: str = None,
) -> pd.DataFrame: ) -> pd.DataFrame:
"""Shared helper: fetch financial report via akshare sina source. """Shared helper: fetch financial report via Sina direct HTTP API.
report_type: '资产负债表' | '利润表' | '现金流量表' report_type: '资产负债表' | '利润表' | '现金流量表'
""" """
import akshare as ak _report_type_map = {
"资产负债表": "fzb",
"利润表": "lrb",
"现金流量表": "llb",
}
source_type = _report_type_map.get(report_type, "lrb")
sina_code = _sina_stock_code(code) prefix = "sh" if code.startswith("6") else "sz"
df = ak.stock_financial_report_sina(stock=sina_code, symbol=report_type) paper_code = f"{prefix}{code}"
url = "https://quotes.sina.cn/cn/api/openapi.php/CompanyFinanceService.getFinanceReport2022"
params = {
"paperCode": paper_code,
"source": source_type,
"type": "0",
"page": "1",
"num": "20",
}
r = _requests.get(url, params=params, headers={"User-Agent": _UA}, timeout=15)
d = r.json()
if df is None or df.empty: result = d.get("result", {}).get("data", {})
items = result.get(source_type, [])
if not isinstance(items, list) or not items:
return pd.DataFrame() return pd.DataFrame()
df = pd.DataFrame(items)
# Filter by curr_date # Filter by curr_date
if curr_date and "报告日" in df.columns: if curr_date and "报告日" in df.columns:
df["报告日"] = pd.to_datetime(df["报告日"], errors="coerce") df["报告日"] = pd.to_datetime(df["报告日"], errors="coerce")
@@ -666,7 +784,7 @@ def get_balance_sheet(
freq: Annotated[str, "frequency: 'annual' or 'quarterly'"] = "quarterly", freq: Annotated[str, "frequency: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None, curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None,
) -> str: ) -> str:
"""Get balance sheet via akshare (sina source).""" """Get balance sheet via Sina direct HTTP API."""
code = _normalize_ticker(ticker) code = _normalize_ticker(ticker)
try: try:
@@ -678,7 +796,7 @@ def get_balance_sheet(
csv_string = df.to_csv(index=False) csv_string = df.to_csv(index=False)
header = f"# Balance Sheet for {code} (A-stock, {freq})\n" header = f"# Balance Sheet for {code} (A-stock, {freq})\n"
header += "# Data source: akshare (sina)\n" header += "# Data source: sina direct HTTP\n"
header += ( header += (
f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
) )
@@ -697,7 +815,7 @@ def get_cashflow(
freq: Annotated[str, "frequency: 'annual' or 'quarterly'"] = "quarterly", freq: Annotated[str, "frequency: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None, curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None,
) -> str: ) -> str:
"""Get cash flow statement via akshare (sina source).""" """Get cash flow statement via Sina direct HTTP API."""
code = _normalize_ticker(ticker) code = _normalize_ticker(ticker)
try: try:
@@ -709,7 +827,7 @@ def get_cashflow(
csv_string = df.to_csv(index=False) csv_string = df.to_csv(index=False)
header = f"# Cash Flow for {code} (A-stock, {freq})\n" header = f"# Cash Flow for {code} (A-stock, {freq})\n"
header += "# Data source: akshare (sina)\n" header += "# Data source: sina direct HTTP\n"
header += ( header += (
f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
) )
@@ -728,7 +846,7 @@ def get_income_statement(
freq: Annotated[str, "frequency: 'annual' or 'quarterly'"] = "quarterly", freq: Annotated[str, "frequency: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None, curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None,
) -> str: ) -> str:
"""Get income statement via akshare (sina source).""" """Get income statement via Sina direct HTTP API."""
code = _normalize_ticker(ticker) code = _normalize_ticker(ticker)
try: try:
@@ -740,7 +858,7 @@ def get_income_statement(
csv_string = df.to_csv(index=False) csv_string = df.to_csv(index=False)
header = f"# Income Statement for {code} (A-stock, {freq})\n" header = f"# Income Statement for {code} (A-stock, {freq})\n"
header += "# Data source: akshare (sina)\n" header += "# Data source: sina direct HTTP\n"
header += ( header += (
f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n" f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
) )
@@ -918,9 +1036,7 @@ def get_global_news(
look_back_days: Annotated[int, "Days to look back"] = 7, look_back_days: Annotated[int, "Days to look back"] = 7,
limit: Annotated[int, "Max articles"] = 10, limit: Annotated[int, "Max articles"] = 10,
) -> str: ) -> str:
"""Get China/global financial news via akshare (CLS + eastmoney).""" """Get China/global financial news via direct HTTP (CLS + Eastmoney)."""
import akshare as ak
start_dt = datetime.strptime(curr_date, "%Y-%m-%d") - relativedelta( start_dt = datetime.strptime(curr_date, "%Y-%m-%d") - relativedelta(
days=look_back_days days=look_back_days
) )
@@ -928,41 +1044,56 @@ def get_global_news(
all_news: list[dict] = [] all_news: list[dict] = []
# Source 1: CLS wire (财联社快讯) # Source 1: CLS wire (财联社快讯) — direct HTTP
try: try:
df_cls = ak.stock_info_global_cls() cls_url = "https://www.cls.cn/nodeapi/telegraphList"
if df_cls is not None and not df_cls.empty: cls_params = {"rn": str(limit), "page": "1"}
for _, row in df_cls.head(limit).iterrows(): cls_headers = {"User-Agent": _UA, "Referer": "https://www.cls.cn/"}
title = str(row.get("标题", row.get("title", ""))) r_cls = _requests.get(cls_url, params=cls_params, headers=cls_headers, timeout=10)
content = str(row.get("内容", row.get("content", ""))) d_cls = r_cls.json()
pub_time = str(row.get("发布时间", row.get("datetime", ""))) for item in d_cls.get("data", {}).get("roll_data", []):
all_news.append( title = item.get("title", "") or item.get("brief", "")
{ content = item.get("content", "") or item.get("brief", "")
"title": title, ctime = item.get("ctime", "")
"content": content, # ctime is unix timestamp
"time": pub_time, pub_time = ""
"source": "CLS Wire", if ctime:
} try:
) pub_time = datetime.fromtimestamp(int(ctime)).strftime("%Y-%m-%d %H:%M")
except (ValueError, TypeError, OSError):
pub_time = str(ctime)
all_news.append({
"title": title,
"content": content,
"time": pub_time,
"source": "CLS Wire",
})
except Exception as e: except Exception as e:
logger.warning("CLS news fetch failed: %s", e) logger.warning("CLS news fetch failed: %s", e)
# Source 2: Eastmoney global (东财全球资讯) # Source 2: Eastmoney global (东财7x24资讯) — direct HTTP
try: try:
df_em = ak.stock_info_global_em() em_url = "https://np-weblist.eastmoney.com/comm/web/getFastNewsList"
if df_em is not None and not df_em.empty: em_params = {
for _, row in df_em.head(limit).iterrows(): "client": "web",
title = str(row.get("标题", row.get("title", ""))) "biz": "web_724",
summary = str(row.get("摘要", row.get("summary", ""))) "fastColumn": "102",
pub_time = str(row.get("发布时间", row.get("datetime", ""))) "sortEnd": "",
all_news.append( "pageSize": str(limit),
{ }
"title": title, em_headers = {"User-Agent": _UA, "Referer": "https://kuaixun.eastmoney.com/"}
"content": summary, r_em = _requests.get(em_url, params=em_params, headers=em_headers, timeout=10)
"time": pub_time, d_em = r_em.json()
"source": "Eastmoney Global", for item in d_em.get("data", {}).get("fastNewsList", []):
} title = item.get("title", "")
) summary = item.get("summary", "")[:200]
pub_time = item.get("showTime", "")
all_news.append({
"title": title,
"content": summary,
"time": pub_time,
"source": "Eastmoney Global",
})
except Exception as e: except Exception as e:
logger.warning("Eastmoney global news fetch failed: %s", e) logger.warning("Eastmoney global news fetch failed: %s", e)
@@ -1051,37 +1182,40 @@ def get_profit_forecast(
ticker: Annotated[str, "A-stock code"], ticker: Annotated[str, "A-stock code"],
curr_date: Annotated[str, "current date (unused, for interface compat)"] = None, curr_date: Annotated[str, "current date (unused, for interface compat)"] = None,
) -> str: ) -> str:
"""Get consensus EPS forecasts with forward valuation (akshare → 同花顺).""" """Get consensus EPS forecasts with forward valuation (同花顺 direct HTTP)."""
import akshare as ak
code = _normalize_ticker(ticker) code = _normalize_ticker(ticker)
try: try:
df = ak.stock_profit_forecast_ths( df = _ths_eps_forecast(code)
symbol=code, indicator="预测年报每股收益"
)
if df is None or df.empty: if df is None or df.empty:
return f"No analyst coverage found for A-stock '{code}'" return f"No analyst coverage found for A-stock '{code}'"
lines = [ lines = [
f"# Consensus EPS Forecast for {code} (A-stock)", f"# Consensus EPS Forecast for {code} (A-stock)",
f"# Source: 同花顺 analyst consensus", f"# Source: 同花顺 analyst consensus (direct HTTP)",
f"# Retrieved: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}", f"# Retrieved: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"", "",
] ]
eps_by_year = {} eps_by_year = {}
for _, row in df.iterrows(): for _, row in df.iterrows():
year = str(row.get("年度", "")) year = str(row.iloc[0]) if len(row) > 0 else ""
count = int(row.get("预测机构数", 0)) count_val = row.iloc[1] if len(row) > 1 else 0
mean_eps = float(row.get("均值", 0)) mean_eps_val = row.iloc[3] if len(row) > 3 else 0
min_eps = row.get("最小值", "N/A") min_eps_val = row.iloc[2] if len(row) > 2 else "N/A"
max_eps = row.get("最大值", "N/A") max_eps_val = row.iloc[4] if len(row) > 4 else "N/A"
industry_avg = row.get("行业平均数", "N/A") try:
count = int(count_val)
except (ValueError, TypeError):
count = 0
try:
mean_eps = float(mean_eps_val)
except (ValueError, TypeError):
mean_eps = 0
lines.append( lines.append(
f"FY{year}: EPS={mean_eps} (range {min_eps}~{max_eps}), " f"FY{year}: EPS={mean_eps} (range {min_eps_val}~{max_eps_val}), "
f"analysts={count}, industry_avg={industry_avg}" f"analysts={count}"
) )
if count < 3: if count < 3:
lines.append(" Warning: low coverage (<3 analysts)") lines.append(" Warning: low coverage (<3 analysts)")
@@ -1599,71 +1733,107 @@ def get_dragon_tiger_board(
Formatted text with LHB appearances, top buyer/seller seats, Formatted text with LHB appearances, top buyer/seller seats,
and institutional activity. and institutional activity.
""" """
import akshare as ak
code = safe_ticker_component(ticker) code = safe_ticker_component(ticker)
end_dt = datetime.strptime(trade_date, "%Y-%m-%d") end_dt = datetime.strptime(trade_date, "%Y-%m-%d")
start_dt = end_dt - pd.Timedelta(days=look_back_days) start_dt = end_dt - pd.Timedelta(days=look_back_days)
start_date = start_dt.strftime("%Y%m%d") start_date_str = start_dt.strftime("%Y-%m-%d")
end_date = end_dt.strftime("%Y%m%d")
lines = [f"# 龙虎榜数据 | {code} | {trade_date} (近{look_back_days}日)"] lines = [f"# 龙虎榜数据 | {code} | {trade_date} (近{look_back_days}日)"]
# 1. 上榜记录 — eastmoney datacenter direct HTTP
try: try:
df = ak.stock_lhb_detail_em(start_date=start_date, end_date=end_date) data = _eastmoney_datacenter(
stock_df = df[df["代码"] == code] "RPT_DAILYBILLBOARD_DETAILSNEW",
if stock_df.empty: filter_str=(
f"(TRADE_DATE>='{start_date_str}')"
f"(TRADE_DATE<='{trade_date}')"
f"(SECURITY_CODE=\"{code}\")"
),
page_size=50,
sort_columns="TRADE_DATE",
sort_types="-1",
)
if not data:
lines.append(f"\n{look_back_days}日未上龙虎榜。") lines.append(f"\n{look_back_days}日未上龙虎榜。")
else: else:
lines.append(f"\n## 上榜记录 ({len(stock_df)} 次)") lines.append(f"\n## 上榜记录 ({len(data)} 次)")
lines.append("日期 | 原因 | 净买入(万) | 成交额(万) | 换手率") lines.append("日期 | 原因 | 净买入(万) | 换手率")
for _, row in stock_df.iterrows(): for row in data:
net_buy = round((row.get("BILLBOARD_NET_AMT") or 0) / 10000, 1)
turnover = round(float(row.get("TURNOVERRATE") or 0), 2)
lines.append( lines.append(
f" {row.get('上榜日', '')} | {row.get('上榜原因', '')} " f" {str(row.get('TRADE_DATE', ''))[:10]} "
f"| {row.get('龙虎榜净买额', 0):.0f} " f"| {row.get('EXPLANATION', '')} "
f"| {row.get('成交额', 0):.0f} " f"| {net_buy:.0f} "
f"| {row.get('换手率', 0):.2f}%" f"| {turnover:.2f}%"
) )
except Exception as e: except Exception as e:
lines.append(f"龙虎榜列表查询失败: {e}") lines.append(f"龙虎榜列表查询失败: {e}")
# 2. 最近上榜的买卖席位 — eastmoney datacenter direct HTTP
try: try:
dates_df = ak.stock_lhb_stock_detail_date_em(symbol=code) if data:
if dates_df is not None and not dates_df.empty: latest_date = str(data[0].get("TRADE_DATE", ""))[:10]
latest_date = str(dates_df.iloc[0]["交易日"]).split(" ")[0]
lines.append(f"\n## 最近上榜席位明细 ({latest_date})") lines.append(f"\n## 最近上榜席位明细 ({latest_date})")
for flag_label in ("买入", "卖出"):
try: # 买入席位
seat_df = ak.stock_lhb_stock_detail_em( buy_data = _eastmoney_datacenter(
symbol=code, date=latest_date, flag=flag_label "RPT_BILLBOARD_DAILYDETAILSBUY",
filter_str=f"(TRADE_DATE='{latest_date}')(SECURITY_CODE=\"{code}\")",
page_size=10,
sort_columns="BUY",
sort_types="-1",
)
if buy_data:
lines.append("\n### 买入席位 TOP5")
lines.append("营业部 | 买入(万) | 卖出(万) | 净额(万)")
for row in buy_data[:5]:
buy_amt = round((row.get("BUY") or 0) / 10000, 1)
sell_amt = round((row.get("SELL") or 0) / 10000, 1)
net = round((row.get("NET") or 0) / 10000, 1)
lines.append(
f" {row.get('OPERATEDEPT_NAME', '')} "
f"| {buy_amt:.0f} | {sell_amt:.0f} | {net:.0f}"
)
# 卖出席位
sell_data = _eastmoney_datacenter(
"RPT_BILLBOARD_DAILYDETAILSSELL",
filter_str=f"(TRADE_DATE='{latest_date}')(SECURITY_CODE=\"{code}\")",
page_size=10,
sort_columns="SELL",
sort_types="-1",
)
if sell_data:
lines.append("\n### 卖出席位 TOP5")
lines.append("营业部 | 买入(万) | 卖出(万) | 净额(万)")
for row in sell_data[:5]:
buy_amt = round((row.get("BUY") or 0) / 10000, 1)
sell_amt = round((row.get("SELL") or 0) / 10000, 1)
net = round((row.get("NET") or 0) / 10000, 1)
lines.append(
f" {row.get('OPERATEDEPT_NAME', '')} "
f"| {buy_amt:.0f} | {sell_amt:.0f} | {net:.0f}"
) )
if seat_df is not None and not seat_df.empty:
lines.append(f"\n### {flag_label}席位 TOP5")
lines.append("营业部 | 买入(万) | 卖出(万) | 净额(万)")
for _, row in seat_df.head(5).iterrows():
lines.append(
f" {row.get('交易营业部名称', '')} "
f"| {row.get('买入金额', 0):.0f} "
f"| {row.get('卖出金额', 0):.0f} "
f"| {row.get('净额', 0):.0f}"
)
except Exception:
pass
except Exception: except Exception:
pass pass
# 3. 机构动向 — eastmoney datacenter direct HTTP
try: try:
inst_df = ak.stock_lhb_jgmmtj_em( inst_data = _eastmoney_datacenter(
start_date=start_date, end_date=end_date "RPT_ORGANIZATION_BUSSINESS",
filter_str=f"(SECURITY_CODE=\"{code}\")",
page_size=1,
sort_columns="TRADE_DATE",
sort_types="-1",
) )
stock_inst = inst_df[inst_df["代码"] == code] if inst_data:
if not stock_inst.empty: row = inst_data[0]
lines.append("\n## 机构动向") lines.append("\n## 机构动向")
for _, row in stock_inst.iterrows(): lines.append(
lines.append( f" 机构买入 {row.get('BUY_TIMES', 0)}"
f" 机构买入 {row.get('买方机构数', 0)}" f"| 卖出 {row.get('SELL_TIMES', 0)}"
f"| 卖出 {row.get('卖方机构数', 0)} " f"| 净额 {round((row.get('NET_BUY_AMT') or 0) / 10000, 1):.0f} "
f"| 净额 {row.get('机构买卖净额', row.get('净额', 0)):.0f}" )
)
except Exception: except Exception:
pass pass
@@ -1690,55 +1860,61 @@ def get_lockup_expiry(
Formatted text with historical unlock records and upcoming Formatted text with historical unlock records and upcoming
expiry calendar with impact metrics. expiry calendar with impact metrics.
""" """
import akshare as ak
code = safe_ticker_component(ticker) code = safe_ticker_component(ticker)
lines = [f"# 限售解禁日历 | {code} | {trade_date}"] lines = [f"# 限售解禁日历 | {code} | {trade_date}"]
# 1. 历史解禁记录 — eastmoney datacenter direct HTTP
try: try:
df = ak.stock_restricted_release_queue_em(symbol=code) history_data = _eastmoney_datacenter(
if df is not None and not df.empty: "RPT_LIFT_STAGE",
lines.append(f"\n## 个股解禁记录 (共 {len(df)})") filter_str=f"(SECURITY_CODE=\"{code}\")",
lines.append( page_size=15,
"解禁时间 | 类型 | 解禁数量 | 占总市值比例 | 解禁前后涨跌幅" sort_columns="FREE_DATE",
) sort_types="-1",
for _, row in df.head(15).iterrows(): )
if history_data:
lines.append(f"\n## 个股解禁记录 (共 {len(history_data)} 批)")
lines.append("解禁时间 | 类型 | 解禁数量 | 占比")
for row in history_data:
lines.append( lines.append(
f" {row.get('解禁时间', '')} " f" {str(row.get('FREE_DATE', ''))[:10]} "
f"| {row.get('限售股类型', '')} " f"| {row.get('LIMITED_STOCK_TYPE', '')} "
f"| {row.get('实际解禁数量', row.get('解禁数量', ''))} " f"| {row.get('FREE_SHARES_NUM', '')} "
f"| {row.get('占总市值比例', '')} " f"| {row.get('FREE_RATIO', '')}"
f"| {row.get('解禁前后20日涨跌幅', '')}"
) )
else: else:
lines.append("\n无历史解禁记录。") lines.append("\n无历史解禁记录。")
except Exception as e: except Exception as e:
lines.append(f"个股解禁查询失败: {e}") lines.append(f"个股解禁查询失败: {e}")
# 2. 未来待解禁 — eastmoney datacenter direct HTTP
try: try:
end_dt = datetime.strptime(trade_date, "%Y-%m-%d") + pd.Timedelta( end_dt = datetime.strptime(trade_date, "%Y-%m-%d") + pd.Timedelta(
days=forward_days days=forward_days
) )
df = ak.stock_restricted_release_detail_em( end_str = end_dt.strftime("%Y-%m-%d")
start_date=trade_date.replace("-", ""), upcoming_data = _eastmoney_datacenter(
end_date=end_dt.strftime("%Y%m%d"), "RPT_LIFT_STAGE",
filter_str=(
f"(SECURITY_CODE=\"{code}\")"
f"(FREE_DATE>='{trade_date}')"
f"(FREE_DATE<='{end_str}')"
),
page_size=20,
sort_columns="FREE_DATE",
sort_types="1",
) )
if df is not None: if upcoming_data:
stock_df = df[df["股票代码"] == code] lines.append(f"\n## 未来 {forward_days} 天待解禁")
if not stock_df.empty: for row in upcoming_data:
lines.append( lines.append(
f"\n## 未来 {forward_days} 天待解禁" f" {str(row.get('FREE_DATE', ''))[:10]} "
f"| {row.get('LIMITED_STOCK_TYPE', '')} "
f"| 数量 {row.get('FREE_SHARES_NUM', '')} "
f"| 占比 {row.get('FREE_RATIO', '')}"
) )
for _, row in stock_df.iterrows(): else:
lines.append( lines.append(f"\n未来 {forward_days} 天无待解禁。")
f" {row.get('解禁时间', '')} "
f"| {row.get('限售股类型', '')} "
f"| 数量 {row.get('实际解禁数量', row.get('解禁数量', ''))} "
f"| 占流通 {row.get('占流通市值比例', '')} "
f"| 解禁前收盘 {row.get('解禁前收盘价', '')}"
)
else:
lines.append(f"\n未来 {forward_days} 天无待解禁。")
except Exception as e: except Exception as e:
lines.append(f"解禁日历查询失败: {e}") lines.append(f"解禁日历查询失败: {e}")
@@ -1765,33 +1941,45 @@ def get_industry_comparison(
Formatted text with sector performance ranking, highlighting Formatted text with sector performance ranking, highlighting
the sector the target stock belongs to. the sector the target stock belongs to.
""" """
import akshare as ak
code = safe_ticker_component(ticker) code = safe_ticker_component(ticker)
lines = [f"# 行业横向对比 | {code} | {trade_date}"] lines = [f"# 行业横向对比 | {code} | {trade_date}"]
# 东财 push2 行业板块排名 (direct HTTP, replaces 同花顺 which has 401)
try: try:
df = ak.stock_board_industry_summary_ths() url = "https://push2.eastmoney.com/api/qt/clist/get"
if df is not None and not df.empty: params = {
"pn": "1",
"pz": "100",
"po": "1",
"np": "1",
"fltt": "2",
"invt": "2",
"fs": "m:90+t:2",
"fields": "f2,f3,f4,f12,f13,f14,f104,f105,f128,f136,f140,f141,f207",
}
r = _requests.get(url, params=params, headers={"User-Agent": _UA}, timeout=15)
d = r.json()
items = d.get("data", {}).get("diff", [])
if items:
lines.append( lines.append(
f"\n## 全行业表现 (同花顺 {len(df)} 个行业)" f"\n## 全行业表现 (东财 {len(items)} 个行业)"
) )
lines.append( lines.append(
"排名 | 行业 | 涨跌幅 | 成交额(亿) | 净流入(亿) " "排名 | 行业 | 涨跌幅 | 上涨 | 下跌 | 领涨股"
"| 上涨 | 下跌 | 领涨股"
) )
for i, (_, row) in enumerate(df.iterrows()): for i, item in enumerate(items):
change = row.get("涨跌幅", 0) name = item.get("f14", "")
turnover = row.get("总成交额", 0) change_pct = item.get("f3", 0)
net_flow = row.get("净流入", 0) up_count = item.get("f104", 0)
down_count = item.get("f105", 0)
leader = item.get("f140", "")
lines.append( lines.append(
f" {i+1}. {row.get('板块', '')} " f" {i+1}. {name} "
f"| {change}% " f"| {change_pct}% "
f"| {turnover:.1f} " f"| {up_count} "
f"| {net_flow:.1f} " f"| {down_count} "
f"| {row.get('上涨家数', '')} " f"| {leader}"
f"| {row.get('下跌家数', '')} "
f"| {row.get('领涨股', '')}"
) )
if i >= top_n * 2 - 1: if i >= top_n * 2 - 1:
lines.append(f" ... (showing top/bottom {top_n})") lines.append(f" ... (showing top/bottom {top_n})")