☰
TradingAgents-CN 数据源优先级修复实战:根治 ROE、负债率、PS 返回 null 问题
2026/10/6 4:24:11 网站建设 项目流程

TradingAgents-CN 数据源优先级修复实战:根治 ROE、负债率、PS 返回 null 问题

【免费下载链接】TradingAgents-CN基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN

导读

本文围绕 TradingAgents-CN 在 2025-10-30 完成的一次数据源优先级修复展开,深入剖析/api/stocks/000001/fundamentals接口返回roe、debt_ratio、ps全部为null这一典型问题的根因,并完整还原三处核心修复:财务数据查询、股票名称解析与筛选服务聚合查询。读完本文,你将掌握 Tushare / AKShare / BaoStock 多数据源混用场景下的"按优先级查询、不混用数据源"的实现范式,以及如何通过system_configs集合动态调整数据源优先级。

问题背景:为什么接口返回的全是 null

用户反馈调用/api/stocks/000001/fundamentals时,返回的roe(净资产收益率)、debt_ratio(资产负债率)、ps(市销率)三个指标全部为null。经排查,根因可分为三个层面:

数据源混用问题

  • stock_basic_info集合中的基础信息来自 Tushare(source: tushare);
  • stock_financial_data集合中存在两条财务记录:
    • 最新一条(报告期 20251231)来自 AKShare,但所有字段都是 None(解析失败);
    • 之前一条(报告期 20250930)来自 Tushare,携带了有效数据ROE=7.5711、debt_to_assets=91.0187。

接口逻辑问题

原接口按report_period降序查询stock_financial_data中的最新记录,但最新记录(AKShare 20251231)恰好是解析失败的脏数据,所有字段为 None。接口没有按数据源优先级查询,因此直接返回了 None,而不是回退到优先级更高、数据更完整的 Tushare 记录。

系统级问题

不止 fundamentals 接口,项目中多处数据查询均未按数据源优先级执行,存在混用不同数据源数据的隐患——同一只股票的指标可能来自不同数据源,口径不一致。

核心修复一:/api/stocks/{code}/fundamentals接口按数据源优先级查询

文件:app/routers/stocks.py,对应get_fundamentals()路由(约第 215 行起)。

修复思路是:将"按时间戳取最新"改为"按数据源优先级取最新",优先级为tushare > akshare > baostock,并且确保整条链路不混用不同数据源的数据。核心代码如下:

# 🔥 按数据源优先级查询,而不是按时间戳,避免混用不同数据源的数据 financial_data = None try: # 获取数据源优先级配置 from app.core.unified_config import UnifiedConfigManager config = UnifiedConfigManager() data_source_configs = await config.get_data_source_configs_async() # 提取启用的数据源,按优先级排序 enabled_sources = [ ds.type.lower() for ds in data_source_configs if ds.enabled and ds.type.lower() in ['tushare', 'akshare', 'baostock'] ] if not enabled_sources: enabled_sources = ['tushare', 'akshare', 'baostock'] # 按数据源优先级查询财务数据 for data_source in enabled_sources: financial_data = await db["stock_financial_data"].find_one( {"$or": [{"symbol": code6}, {"code": code6}], "data_source": data_source}, {"_id": 0}, sort=[("report_period", -1)] ) if financial_data: logger.info(f"✅ 使用数据源 {data_source} 的财务数据") break

实现细节解读

  • 查询条件双字段兼容:{"$or": [{"symbol": code6}, {"code": code6}]}同时兼容symbol与code两种字段命名,避免因历史数据字段不一致而漏查。
  • 内层按报告期降序:sort=[("report_period", -1)]保证在同一数据源内部取到最新一期的记录,避免旧报告期覆盖新数据。
  • 外层按优先级遍历:一旦某个数据源查到有效记录立即break,保证优先使用 Tushare,只有 Tushare 无数据时才降级到 AKShare、BaoStock。

与基础信息查询的配合

在同一个路由中,基础信息(stock_basic_info)也遵循相同的优先级逻辑(app/routers/stocks.py 第 124-147 行):按enabled_sources顺序查询{"code": code6, "source": src},并在所有数据源都无结果时回退到不带source条件的查询以兼容旧数据。此外,PE/PB 会优先通过 tradingagents/dataflows/realtime_metrics.py 的get_pe_pb_with_fallback()实时计算,实时市值优先于静态市值,进一步保证估值指标口径一致。

财务指标提取与 PS 动态计算

拿到financial_data后,ROE 与负债率的提取路径为(app/routers/stocks.py 第 386-416 行):

if financial_data.get("financial_indicators"): indicators = financial_data["financial_indicators"] data["roe"] = indicators.get("roe") data["debt_ratio"] = indicators.get("debt_to_assets") # 如果 financial_indicators 中没有,尝试从顶层字段获取 if data["roe"] is None: data["roe"] = financial_data.get("roe") if data["debt_ratio"] is None: data["debt_ratio"] = financial_data.get("debt_to_assets")

PS(市销率)采用动态计算策略:优先使用 TTM 营业收入(revenue_ttm),否则使用单期营业收入(revenue),再用市值除以营收(单位换算为亿元)得出:

revenue_for_ps = revenue_ttm if revenue_ttm and revenue_ttm > 0 else revenue if revenue_for_ps and revenue_for_ps > 0 and total_mv and total_mv > 0: revenue_yi = revenue_for_ps / 100000000 data["ps"] = round(total_mv / revenue_yi, 2)

核心修复二:get_stock_name()按数据源优先级解析股票名称

文件:app/routers/reports.py(第 23-85 行)。

get_stock_name()用于在报告模块中根据股票代码解析名称,修复后遵循缓存 -> MongoDB(按数据源优先级) -> 默认返回股票代码的三级降级链路:

  • 缓存优先:模块级字典_stock_name_cache缓存已解析结果,避免重复查库;
  • 按优先级查询:通过UnifiedConfigManager().get_data_source_configs()(同步版本)获取启用且属于tushare / akshare / baostock的数据源列表,逐个查询{"$or": [{"symbol": code6}, {"code": code6}], "source": data_source};
  • 兼容旧数据:若所有数据源都无结果,回退到不带source条件的查询,并打印警告日志;
  • 最终兜底:仍未找到则直接返回股票代码本身。
# 🔥 按数据源优先级查询 config = UnifiedConfigManager() data_source_configs = config.get_data_source_configs() enabled_sources = [ ds.type.lower() for ds in data_source_configs if ds.enabled and ds.type.lower() in ['tushare', 'akshare', 'baostock'] ] if not enabled_sources: enabled_sources = ['tushare', 'akshare', 'baostock'] stock_info = None for data_source in enabled_sources: stock_info = db.stock_basic_info.find_one( {"$or": [{"symbol": code6}, {"code": code6}], "source": data_source} ) if stock_info: logger.debug(f"✅ 使用数据源 {data_source} 获取股票名称 {code6}") break # 如果所有数据源都没有,尝试不带 source 条件查询(兼容旧数据) if not stock_info: stock_info = db.stock_basic_info.find_one( {"$or": [{"symbol": code6}, {"code": code6}]} )

该函数被 app/routers/reports.py 中的报告列表、报告详情等多处调用(约第 180、279、309 行),修复后确保报告展示的股票名称与财务数据出自同一优先级的来源。

核心修复三:筛选服务聚合查询只取最高优先级数据源

文件:app/services/database_screening_service.py(_enrich_with_financial_data(),约第 253-322 行)。

数据库选股服务在批量填充财务指标时,原先的聚合管道未过滤数据源,可能把不同来源的 ROE 混进同一批结果。修复后在聚合管道中只查询优先级最高的数据源:

# 🔥 获取数据源优先级配置 config = UnifiedConfigManager() data_source_configs = await config.get_data_source_configs_async() # 提取启用的数据源,按优先级排序 enabled_sources = [ ds.type.lower() for ds in data_source_configs if ds.enabled and ds.type.lower() in ['tushare', 'akshare', 'baostock'] ] if not enabled_sources: enabled_sources = ['tushare', 'akshare', 'baostock'] # 优先使用优先级最高的数据源 preferred_source = enabled_sources[0] if enabled_sources else 'tushare' # 批量查询最新的财务数据(只查询优先级最高的数据源) pipeline = [ {"$match": {"code": {"$in": codes}, "data_source": preferred_source}}, {"$sort": {"code": 1, "report_period": -1}}, {"$group": { "_id": "$code", "roe": {"$first": "$roe"}, "roa": {"$first": "$roa"}, "netprofit_margin": {"$first": "$netprofit_margin"}, "gross_margin": {"$first": "$gross_margin"}, }} ]

聚合管道的执行语义

  • $match用data_source字段把范围锁定在单一最高优先级数据源,杜绝混源;
  • $sort按code升序、report_period降序排列;
  • $group按code分组后用$first取每个股票最新一期的 ROE、ROA、净利率、毛利率;
  • 填充时只覆盖空值:if result.get("roe") is None: result["roe"] = ...,避免覆盖stock_basic_info已有的有效指标;
  • 异常兜底:填充失败仅记录 warning 日志,不阻断选股主流程。

同样的"只取最高优先级数据源"模式也出现在行业分类查询中(app/routers/screening.py 约第 279-312 行),通过{"source": preferred_source}只查询优先级最高的数据源。

数据源优先级配置的底层实现

数据源优先级配置集中在 app/core/unified_config.py 的UnifiedConfigManager中,提供同步与异步两个版本:

  • get_data_source_configs()(同步版,第 259 行)
  • get_data_source_configs_async()(异步版,第 327 行)

两者的加载策略一致:优先从 MongoDB 的system_configs集合读取is_active: True且版本号最新的配置文档,其中data_source_configs数组被解析为DataSourceConfig对象,并按priority降序排列(数字越大优先级越高);若数据库无配置或解析失败,则回退到硬编码默认配置。

硬编码默认配置的要点(异步版):

  • AKShare:enabled=True,priority=1,默认启用;
  • Tushare:enabled=True,priority=2,仅在系统设置中存在tushare_token时启用;
  • Finnhub:enabled=True,priority=3,仅在存在finnhub_api_key时启用(主要用于美股)。

也就是说,默认情况下 Tushare(priority=2)优先于 AKShare(priority=1),与修复文档中的tushare > akshare > baostock约定一致。而DataSourceConfig模型定义在 app/models/config.py(第 243-261 行),其关键字段包括:

字段类型默认值说明
namestr必填数据源名称
typeDataSourceType必填数据源类型(tushare / akshare / baostock / finnhub 等)
api_key/api_secretOptional[str]NoneAPI 密钥
endpointOptional[str]NoneAPI 端点地址
timeoutint30请求超时时间(秒)
rate_limitint100每分钟请求限制
enabledboolTrue是否启用
priorityint0优先级,数字越大优先级越高
config_paramsDict[str, Any]{}额外配置参数
market_categoriesOptional[List[str]][]所属市场分类列表

DataSourceType枚举定义于 app/models/config.py 第 150-178 行,覆盖 MongoDB、Tushare、AKShare、BaoStock、Finnhub、Yahoo Finance、Alpha Vantage、Wind、Choice 等类型,并且与tradingagents/constants/data_sources.py中的注册保持同步。

如何动态调整优先级

由于配置优先从数据库读取,可以通过修改system_configs集合中激活配置的data_source_configs数组来调整优先级,例如将 AKShare 的priority调高即可让 AKShare 成为首选数据源。调整后无需改动代码,UnifiedConfigManager会在下一次调用时读取最新配置。若数据库读取失败,系统自动回退到硬编码默认值,保证服务可用性。

已按优先级查询、无需修改的实现点

修复总结中明确列出以下位置已具备正确的优先级查询逻辑,可视为同类实现的标准参考:

  1. app/routers/stocks.py ——get_fundamentals()✅
  2. app/routers/stock_data.py —— 已按优先级查询 ✅
  3. app/routers/screening.py —— 已按优先级查询 ✅
  4. app/services/stock_data_service.py —— 已按优先级查询 ✅
  5. app/services/favorites_service.py —— 已按优先级查询 ✅

其中 app/services/stock_data_service.py 在文档字符串中明确说明默认优先级为tushare > multi_source > akshare > baostock,并在未指定source参数时按source_priority = ["tushare", "multi_source", "akshare", "baostock"]顺序遍历查询(约第 42-62 行)——注意这里与财务数据查询的tushare > akshare > baostock略有差异,多了一个multi_source聚合数据源,属于基础信息场景的扩展约定。

测试建议与验证方法

修复完成后,可按以下步骤验证:

  1. 接口回归:调用/api/stocks/000001/fundamentals,确认返回的roe、debt_ratio、ps不再是null;
  2. 来源校验:查看服务端日志中的✅ 使用数据源 {data_source} 的财务数据与✅ 使用数据源: {src} 查询股票 {code6},确认数据来自最高优先级的数据源(Tushare);
  3. 一致性抽检:测试其他股票代码(如 000002、300750 等),确保不同股票、不同报告期下均能取到一致口径的数据;
  4. 降级验证:临时禁用最高优先级数据源(在system_configs中将enabled置为 false),确认接口能自动降级到次优先级数据源,且不混用数据;
  5. 旧数据兼容:对只有无source字段的旧记录,确认回退查询逻辑生效(日志出现⚠️ 使用旧数据(无 source 字段))。

总结

本次修复的核心价值在于确立了一套可复用的多数据源查询范式:外层按数据源优先级遍历、内层按报告期取最新、全程不混用数据源、无结果时兼容旧数据。这套范式贯穿了 fundamentals 接口、股票名称解析、筛选聚合与行业分类等多个数据入口,并借助UnifiedConfigManager将优先级从硬编码升级为数据库可配置,使数据源策略的调整无需改码重启。对于任何需要对接多套金融数据源的工程实践,这一"优先级驱动、降级兜底"的模式都具备直接的参考价值。

【免费下载链接】TradingAgents-CN基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询