计算问题全解析:三个数据源的 TTM 修复实战指南)
TradingAgents-CN 估值指标PS/PE计算问题全解析三个数据源的 TTM 修复实战指南【免费下载链接】TradingAgents-CN基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN导读本文基于 docs/bugfix/2025-10-26-ps-pe-calculation-summary.md 与 docs/bugfix/2025-10-26-ps-calculation-fix.md完整复盘 TradingAgents-CN 中 MongoDBAKShare、Tushare、实时行情三个数据源在 PS/PE 计算上的数据口径错误、影响范围与修复方案。读完你将掌握TTM滚动 12 个月指标的计算原理与降级策略、市值计算对总股本的依赖关系以及如何通过源码级改动和单元测试根治单期数据冒充 TTM这类估值失真问题。一、问题总览三个数据源全部中招2025-10-26 的排查发现三个数据源在 PS/PE 估值指标计算上全部存在问题严重程度被标记为高影响所有股票的估值指标。核心问题可以归纳为两类数据使用错误用单期季报/半年报营业收入、净利润计算 PS/PE而正确的做法是使用 TTM滚动 12 个月数据市值计算错误Tushare 与实时行情数据源用固定 10 亿股本计算市值而不同股票的总股本差异巨大这直接污染了所有依赖市值的指标。各数据源的具体问题如下表所示数据源PS 问题PE 问题市值问题修复状态MongoDB (AKShare)❌ 使用单期营业收入❌ 使用单期净利润✅ 正确实际股本✅ PS 已修复Tushare❌ 使用单期营业收入❌ 使用单期净利润❌ 固定 10 亿股本⚠️ 已标记实时行情❌ 使用单期营业收入❌ 使用单期净利润❌ 固定 10 亿股本⚠️ 已标记在深入每个数据源之前先明确估值指标的定义这是判断问题严重性的基准PS市销率 总市值 / 营业收入TTMPE市盈率 总市值 / 净利润TTMPB市净率 总市值 / 净资产最新期TTMTrailing Twelve Months 最近 12 个月的累计数据TTM 之所以关键是因为它能更准确地反映公司当前的经营状况、避免季节性波动影响并且与随股价实时变化的市值口径相匹配。二、MongoDB 数据源AKSharePS 已修复PE 仍待处理代码位置tradingagents/dataflows/optimized_china_data.py2.1 问题定位在_parse_mongodb_financial_data约 1126-1148 行区域中修复前的逻辑存在两个问题PS使用revenue单期营业收入计算PE使用net_profit单期净利润计算市值✅ 正确money_cap price * total_share且money_cap万元 股价 × 总股本万股随股价实时更新。2.2 PS 修复代码已落地修复的核心思路是优先使用 TTM 营业收入无 TTM 数据时才降级到单期。当前源码optimized_china_data.py中的实际实现# 计算 PS - 市销率使用TTM营业收入 # 优先使用 TTM 营业收入如果没有则使用单期营业收入 revenue_ttm latest_indicators.get(revenue_ttm) revenue latest_indicators.get(revenue) # 选择使用哪个营业收入数据 revenue_for_ps revenue_ttm if revenue_ttm and revenue_ttm 0 else revenue revenue_type TTM if revenue_ttm and revenue_ttm 0 else 单期 if revenue_for_ps and revenue_for_ps 0: try: # 使用市值/营业收入计算PS money_cap latest_indicators.get(money_cap) if money_cap and money_cap 0: ps_calculated money_cap / revenue_for_ps metrics[ps] f{ps_calculated:.2f}倍 logger.debug(f✅ 计算PS({revenue_type}): 市值{money_cap}万元 / 营业收入{revenue_for_ps}万元 {metrics[ps]}) else: metrics[ps] N/A except (ValueError, TypeError, ZeroDivisionError): metrics[ps] N/A else: metrics[ps] N/A从代码可以读出两个关键设计TTM 字段命名约定revenue_ttm与单期revenue并列存储TTM 值有效存在且大于 0时优先降级兼容旧数据没有revenue_ttm字段时会自动降级到revenue单期数据避免指标缺失但精度下降。2.3 PE 未修复的现状在 PE 计算链路约 1152-1218 行中net_profit单期净利润仍是主要数据来源net_profit latest_indicators.get(net_profit) # 关键修复检查净利润是否为正数亏损股不计算PE if net_profit and net_profit 0: ... pe_calculated money_cap / net_profit metrics[pe] f{pe_calculated:.1f}倍这里已加入亏损股保护净利润 ≤ 0 时 PE 直接置为 N/A不再计算但仍使用单期净利润而非 TTM 净利润属于中优先级待办。值得注意的细节是源码还实现了三层降级第一层尝试实时 PE 计算失败后第二层用市值 / 单期净利润第三层直接回落到stock_basic_info中静态pe字段且只接受正数。三、Tushare 数据源三重错误叠加代码位置tradingagents/dataflows/optimized_china_data.py约 1377-1416 行区域3.1 三个问题PS使用income_statement[0][total_revenue]单期营业收入PE使用income_statement[0][n_income]单期净利润市值使用固定的 10 亿股本price_value * 1000000000。3.2 影响评估PS 被高估2-4 倍PE 被高估2-4 倍市值计算完全错误不同股票的总股本差异巨大大盘股动辄上百亿股小盘股可能只有几亿股固定 10 亿股本会系统性扭曲所有依赖市值的指标。3.3 曾经的临时措施修复前代码中以警告日志与 TODO 注释的形式留下了问题标记# ⚠️ 警告Tushare income_statement 的 total_revenue 是单期数据可能是季报/半年报 # 理想情况下应该使用 TTM 数据但 Tushare 数据结构中没有预先计算的 TTM 字段 # TODO: 需要从多期数据中计算 TTM total_revenue latest_income.get(total_revenue, 0) or 0 # ⚠️ 警告市值计算使用固定股本10亿股是不准确的 # 理想情况下应该从 stock_basic_info 或 daily_basic 获取实际总股本 # TODO: 需要获取实际总股本数据 market_cap price_value * 1000000000 # 假设10亿股本不准确3.4 当前源码中的进展TTM 与总股本已落地从当前源码看约 1740-1794 行Tushare 数据源已经取得了阶段性修复进展1TTM 计算已接入遍历income_statement多期数据构建 DataFrame复用scripts/sync_financial_data.py中的_calculate_ttm_metric()计算 TTM 营业收入与净利润# 构建营业收入 DataFrame revenue_data [] for stmt in income_statement: end_date stmt.get(end_date) revenue stmt.get(total_revenue) if end_date and revenue is not None: revenue_data.append({报告期: str(end_date), 营业收入: float(revenue)}) if len(revenue_data) 2: revenue_df pd.DataFrame(revenue_data) from scripts.sync_financial_data import _calculate_ttm_metric ttm_revenue _calculate_ttm_metric(revenue_df, 营业收入) if ttm_revenue: logger.info(f✅ Tushare 计算 TTM 营业收入: {ttm_revenue:.2f} 万元) # 净利润同理字段名 净利润数据来自 n_income # 降级到单期数据 total_revenue ttm_revenue if ttm_revenue else (latest_income.get(total_revenue, 0) or 0) net_income ttm_net_income if ttm_net_income else (latest_income.get(n_income, 0) or 0) revenue_type TTM if ttm_revenue else 单期 profit_type TTM if ttm_net_income else 单期2市值计算已改为实际总股本# 获取实际总股本计算市值 total_share stock_info.get(total_share) if stock_info else None if total_share and total_share 0: # 市值元 股价元× 总股本万股× 10000 market_cap price_value * total_share * 10000 market_cap_yi market_cap / 100000000 # 转换为亿元 metrics[total_mv] f{market_cap_yi:.2f}亿元 logger.info(f✅ [Tushare-总市值计算成功] 总市值{market_cap_yi:.2f}亿元 (股价{price_value}元 × 总股本{total_share}万股)) else: logger.error(f❌ {stock_info.get(code, Unknown)} 无法获取总股本无法计算准确的估值指标) market_cap None metrics[total_mv] N/A关键改进是宁可缺失、不可错误当无法获取总股本时市值被置为None、total_mv置为N/A后续估值指标只在market_cap有效时才计算if market_cap:分支而不是用固定股本硬算一个错误值。从源码结构可以推断该数据源完成修复的前提是stock_info中必须包含total_share总股本万股因此同步/查询链路需要保证stock_basic_info或 Tusharedaily_basic数据完整。四、实时行情数据源建议只做价格不做估值实时行情数据源的问题与 Tushare完全相同同一代码区域因为它本质上复用了同一套财务指标计算逻辑。原文档给出的架构建议非常明确实时行情数据源应该只提供价格数据不应该计算估值指标两个可选的改造方向移除PS/PE/PB 等估值指标的计算或者从 MongoDB 数据源获取财务数据进行计算。从职责划分上看实时行情擅长的是此刻的价格与成交量而估值指标依赖的是财务报告期数据 总股本两者数据时效性和来源完全不同。将估值计算从实时行情链路剥离可以让每个数据源职责单一、口径统一这也是该项目后续重构的方向。五、TTM 计算原理与降级策略TTM 是整个修复方案的技术核心。原文档与配套的 PS 修复文档 docs/bugfix/2025-10-26-ps-calculation-fix.md 共同描述了完整逻辑当前仓库中scripts/sync_financial_data.py的_calculate_ttm_metric()/_calculate_ttm_revenue()就是它的落地实现。5.1 三种计算情形情况 1最新期是年报12 月 31 日TTM 年报营业收入年报本身已覆盖 12 个月直接使用无需换算。情况 2最新期是中报/季报有完整历史数据TTM 最近年报 (本期 - 去年同期)示例2023 年报 1100 万元、2023 中报 500 万元、2024 中报 600 万元最新期则TTM 1100 (600 - 500) 1200 万元。其含义是以最近年报为基准把本报告期相对去年同期的增量叠加进去从而外推出滚动 12 个月的数值。情况 3数据不足时的简单年化降级当无法取得年报与去年同期数据时按报告期长度做线性年化最新报告期年化系数公式中报0630×2TTM 营业收入 × 2一季报0331×4TTM 营业收入 × 4三季报0930×4/3TTM 营业收入 × 4 / 35.2 面向 Tushare 的 TTM 计算参考实现原文档为 Tushare 数据源给出了一份可直接落地的参考实现核心逻辑与 AKShare 版本一致但按 Tushare 利润表结构end_datetotal_revenue/n_income编写def _calculate_ttm_from_tushare(income_statements: List[dict], field: str) - Optional[float]: 从 Tushare 利润表数据计算 TTM Args: income_statements: 利润表数据列表按报告期倒序 field: 字段名total_revenue 或 n_income Returns: TTM 值如果无法计算则返回 None if not income_statements or len(income_statements) 1: return None latest income_statements[0] latest_period latest.get(end_date) latest_value latest.get(field) if not latest_period or latest_value is None: return None # 判断是否是年报 if latest_period.endswith(1231): return latest_value # 非年报需要计算 TTM # 查找最近年报和去年同期 year int(latest_period[:4]) month_day latest_period[4:] last_annual_period f{year-1}1231 last_same_period f{year-1}{month_day} last_annual next((x for x in income_statements if x.get(end_date) last_annual_period), None) last_same next((x for x in income_statements if x.get(end_date) last_same_period), None) if last_annual and last_same: last_annual_value last_annual.get(field) last_same_value last_same.get(field) if last_annual_value is not None and last_same_value is not None: # TTM 最近年报 (本期 - 去年同期) return last_annual_value (latest_value - last_same_value) # 降级简单年化 if month_day 0630: return latest_value * 2 elif month_day 0331: return latest_value * 4 elif month_day 0930: return latest_value * 4 / 3 return None注意end_date的格式约定形如20240630的字符串endswith(1231)判定年报[:4]取年份、[4:]取月日。这与 AKShare 数据源的_calculate_ttm_revenue()函数互为对照两个数据源的 TTM 逻辑最终都收敛到同一套年报 同比增量外推的算法。六、市值计算的修复方案告别固定股本市值是 PS/PE/PB 的共同分子市值错了所有估值指标全错。原文档给出了两个获取实际总股本的方案方案 A从 MongoDB 获取总股本# 从 stock_basic_info 集合获取总股本 stock_info await self.db.stock_basic_info.find_one({code: symbol}) if stock_info and total_share in stock_info: total_share stock_info[total_share] # 万股 market_cap price_value * total_share * 10000 # 转换为元 else: logger.warning(f⚠️ {symbol} 无法获取总股本无法计算市值) market_cap None方案 B从 Tushare API 获取总股本# 从 Tushare daily_basic 获取总股本 daily_basic await asyncio.to_thread( self.api.daily_basic, ts_codets_code, trade_datetrade_date, fieldstotal_share ) if daily_basic is not None and not daily_basic.empty: total_share daily_basic.iloc[0][total_share] # 万股 market_cap price_value * total_share * 10000 # 转换为元单位换算务必注意Tushare 的total_share与 MongoDBstock_basic_info中的total_share均以万股为单位而股价以元为单位因此market_cap price × total_share × 10000才能得到以元计量的市值。MongoDB 数据源中的money_cap万元则等价于price × total_share万 × 万元口径。从当前源码看Tushare 路径已采用方案 A 的变体从stock_info.get(total_share)读取并在缺失时拒绝计算印证了以实际股本为准、数据缺失宁可置 N/A的最终取向。七、测试验证TTM 计算的五个用例修复不是拍脑袋配套的单元测试位于 scripts/test_ttm_calculation.py它直接 import 生产代码scripts.sync_financial_data中的_calculate_ttm_revenue与_safe_float进行断言验证。测试覆盖以下五个场景用例场景输入报告期 / 营业收入预期 TTM1年报数据最新期 20231231营收 12001200直接用年报2中报历史不足2022 年报 1000、2023 中报 500、2024 中报 600简单年化 600 × 2 12003中报完整历史2022 年报 1000、2023 中报 500、2023 年报 1100、2024 中报 6001100 (600 - 500) 12004一季报最新期 20240331营收 300300 × 4 12005三季报最新期 20240930营收 900900 × 4/3 1200运行方式python scripts/test_ttm_calculation.py全部通过时输出 ✅ 所有测试通过 此外测试脚本还内置了一个 PS 计算示例直观演示修复前后的差异某公司股价 10 元、总股本 10 亿股、总市值 100 亿元若半年报营业收入 30 亿元则 PS 100/30 33.33 倍错误改用 TTM 营业收入 60 亿元后 PS 100/60 16.67 倍正确单期数据导致估值被高估整整 2 倍——这还只是半年报的情形若误用一季报高估幅度可达 4 倍。八、修复优先级与部署计划8.1 修复优先级清单高优先级必须修复序号项目状态1MongoDB PS 计算使用 TTM✅ 已修复2Tushare 市值计算使用固定股本完全错误✅ 已改为实际总股本3Tushare PS 计算使用单期数据高估 2-4 倍✅ 已接入 TTM4Tushare PE 计算使用单期数据高估 2-4 倍✅ 已接入 TTM中优先级建议修复序号项目状态5MongoDB PE 计算使用单期净利润❌ 待修复6实时行情估值指标建议移除或重构❌ 待规划低优先级可选序号项目说明7PB 计算净资产使用最新期数据资产负债表为时点数据问题不大但受市值错误牵连8.2 分阶段部署计划阶段 1紧急修复已完成修复 MongoDB PS 计算使用 TTM、添加单元测试验证 TTM 计算、标记 Tushare 与实时行情的问题。阶段 2高优先级修复修复 Tushare 市值计算获取实际总股本、修复 Tushare PS/PE 计算使用 TTM、重新同步所有股票的财务数据。阶段 3中优先级修复修复 MongoDB PE 计算使用 TTM、重构实时行情数据源移除估值指标或改用 MongoDB 数据。8.3 财务数据重新同步TTM 修复依赖数据库中新增的revenue_ttm/net_profit_ttm字段。在 scripts/sync_financial_data.py 中可以看到这两个字段已经写入同步结果revenue_ttm: ttm_revenue, # TTM营业收入最近12个月 net_profit_ttm: ttm_net_profit, # TTM净利润最近12个月同时同步脚本内部约 161-177 行已经在计算 PE/PS 时优先取 TTM 字段、缺失时降级到单期。因此部署时需要对存量数据执行重新同步# 同步单只股票 python scripts/sync_financial_data.py 600036 # 批量同步前 100 只 python scripts/sync_financial_data.py --batch 100 # 同步所有股票 python scripts/sync_financial_data.py --all同步完成后可用如下方式验证revenue_ttm字段是否落库from motor.motor_asyncio import AsyncIOMotorClient client AsyncIOMotorClient(mongodb://localhost:27017) db client[tradingagents] # 查询一只股票的财务数据 doc await db.stock_financial_data.find_one({code: 600036}) print(frevenue: {doc.get(revenue)}) print(frevenue_ttm: {doc.get(revenue_ttm)})注意旧数据没有revenue_ttm/net_profit_ttm字段时会自动降级使用单期数据指标不会缺失但精度下降因此强烈建议重新同步全部股票的财务数据。九、经验总结估值指标计算的三个教训口径必须匹配PS/PE 的分母必须是 TTM滚动 12 个月的营收/净利润任何单期数据季报、半年报都会导致 2-4 倍的系统性高估。原文档明确记录单期数据对应的是某个报告期的累计值而市值是此刻的实时值两者时间窗不匹配估值自然失真。市值 价格 × 真实总股本固定股本假设是估值计算中的重大隐患。正确的做法是从stock_basic_info集合或 Tusharedaily_basicAPI 获取实际总股本注意万股与元的单位换算数据缺失时宁可返回N/A也不要用拍脑袋的常数。数据源职责要单一实时行情数据源只应提供价格估值指标应从专门的财务数据链路MongoDB/AKShare、Tushare 利润表计算避免重复实现导致口径漂移。当前进展小结MongoDB 的 PS 已修复并配套了 TTM 单元测试Tushare 数据源在源码层面已接入 TTM 计算与实际总股本市值MongoDB 的 PETTM 净利润与实时行情数据源重构仍是后续待办。整个排查—修复—测试—再同步的闭环为多数据源量化框架提供了一个可复用的估值指标质量治理范式。延伸阅读PS 计算修复的完整细节文档docs/bugfix/2025-10-26-ps-calculation-fix.mdTTM 计算单元测试scripts/test_ttm_calculation.pyAKShare 财务数据同步脚本TTM 字段写入处scripts/sync_financial_data.py三个数据源的估值指标计算实现tradingagents/dataflows/optimized_china_data.py【免费下载链接】TradingAgents-CN基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考