Files
daily_stock_analysis/docs/data-source-stability.md
青玉案 d0e66a1dc3 feat: 新闻检索为空时在报告中如实标注 (#2229)
* feat: 新闻检索为空时在报告中如实标注

消息面章节此前是「有内容才渲染」,检索一条没拿到时整段直接消失,
读报告的人无从判断是确实没新闻,还是检索静默失败了(搜索源限流、
未配置可用渠道等)。这把「抓取失败」呈现成了「确实没有新闻」。

- src/analyzer.py: AnalysisResult 新增 news_result_count,默认 None
- src/core/pipeline.py: 把 Step 4 已算好的计数交给结果对象
  (此前只进了 diagnostic context snapshot,报告层拿不到)
- src/notification.py: news_lines 为空且计数为 0 时,渲染明确提示,
  并说明结论未纳入新闻维度证据
- tests: 新增 5 条用例,含两条负例——计数为 None 时不得报警
  (那是未配置搜索渠道,不是失败)、拿到新闻时行为与改动前一致

不触碰任何检索路径,纯展示层增量。

* fix: 把新闻缺失提示放进真实渲染路径,并独立于模型输出判定

按 review 三条意见修正:

P1-1 提示只存在于 generate_daily_report,而正常流程从不调用它——
_send_single_stock_notification 与聚合报告走的是 dashboard / brief /
single_stock。原实现对所有标准 REPORT_TYPE 都不生效。
改为抽出共享判定 _empty_news_disclosure,四个渲染器统一接入。

P2 检索零命中但模型按 schema 写出了 market_sentiment / hot_topics 时,
原 elif 分支被跳过,报告会展示模型生成的情绪判断却隐瞒无新闻证据。
改为独立判定 news_result_count == 0,与模型是否产出文字无关。

P1-2 补 docs/CHANGELOG.md [Unreleased] 条目,并在
docs/data-source-stability.md 的「用户可见提示建议」一节记录该行为,
含 None / 0 / >0 三态语义表。

测试从 5 条增至 10 条,新增覆盖 dashboard、brief、single_stock 三个真实
渲染器,以及「模型有输出但检索为空」这一最糟组合。39 passed

* fix: 把新闻零命中披露覆盖到模板链路与企业微信入口

按 review 指出的 blocker 修正。此前只接了字符串拼接分支,遗漏两类活路径:

1. REPORT_RENDERER_ENABLED=true 时,generate_dashboard_report /
   generate_brief_report / generate_wechat_dashboard 会先 return render(...),
   模板链路一路不渲染披露;
2. generate_wechat_dashboard 的非模板 fallback 从未接入,而 pipeline 在
   企业微信非 brief 场景会直接调用它。

后果是同一份分析结果在部分渠道披露、在另一些渠道沉默。

改法不再逐点打补丁,而是抽出单一事实来源:

- 新增 src/services/empty_news.py 持有判定与中英文案
- src/notification.py 的 _empty_news_disclosure 改为委托该模块
- src/services/report_renderer.py 为每条结果预计算 empty_news_disclosure,
  三个平台模板共用
- templates/report_markdown.j2 / report_brief.j2 / report_wechat.j2 各加渲染分支
- generate_wechat_dashboard 的 fallback 正文接入披露

新增 6 条回归测试:模板链路三个平台各一条、企业微信入口一条,
外加两条负例(未执行检索时模板与企业微信均不得提示)。

本文件测试 10 → 16 全过;全量 5824 passed,9 个既有失败与本 PR 无关
(干净 main 上同样失败,属测试顺序依赖)。

* fix: 修正计数源头的两处缺口(自查发现)

按 review 的 merge-base..HEAD 方法自查全链路,发现此前几轮都只盯着渲染出口,
从未核对计数源头,而源头本身在两条路径上是错的:

1. src/core/pipeline.py: news_result_count 只在 intel_results 非空时赋值,
   搜索服务整体失败(正是所有搜索源限流全挂的场景)时停留在 None,
   语义为「未执行检索」,于是本 PR 想解决的头号场景反而不提示。
   改为检索一发起即置 0。

2. _analyze_with_agent: Agent 模式自行调用 search_stock_news 完成检索,
   却从不回写计数,该路径下零命中永远静默。改为按检索结果回写 0 或实际条数。

渲染层再周全,源头数据不对则全部落空。

新增 2 条测试锁住这两处语义(18 passed,此前 16)。
全量 5826 passed,9 个既有失败与本 PR 无关。

* fix: disclose missing news search configuration

* chore: remove unrelated agent guidance

* test: run all empty news tests directly

* fix: preserve empty news disclosure across reports

* fix: 让 Agent 模式的新闻披露跟随实际消费的证据

原问题:agent_arch=multi 等受支持的 Agent 配置下,报告可能声称「未纳入新闻
面证据」而分析其实用了新闻,或反过来该提示而不提示。

根因:news_result_count 取自 executor.run() 结束后为持久化情报补打的一次
search_stock_news()。真实情报由 IntelAgent 通过 search_comprehensive_intel
取得,两者不等价,因此披露与真实证据链可能相反。

修复点:新增 src/agent/news_evidence.py,以运行期证据作用域收集 Agent 搜索
工具的真实返回条数;搜索渠道不可用为 None(未执行检索),可用则从 0 起步、
拿到多少算多少。pipeline 在 executor.run() 前后开启并读取该作用域,事后的
持久化补查不再回写计数。

回归风险:工具在 ThreadPoolExecutor 中执行,runner.py 以
contextvars.copy_context() 提交,故作用域中必须是可变累加器对象,换成不可变
值会让父线程读不到;已加回归测试锁住该机制。原 test_agent_path_records_count
断言的正是被修复的错误行为,已替换为反向断言。

Refs #2225

* fix: 让新闻披露以实际证据为准而非搜索命中数

原问题:本地已落库的资讯池或社交情绪进入 news_context 参与分析后,报告仍可能
声称「未配置搜索渠道,本次分析未纳入新闻面证据」或「零命中」。

根因:news_context 由三路来源拼成——实时检索、社交情绪(美股)、本地资讯池,
但只有实时检索会更新 news_result_count。披露断言的是「结论有没有用到新闻面
证据」,而计数只是「搜索命中了几条」,两者是不同命题,后两路参与时必然失真。

修复点:AnalysisResult 新增 news_evidence_present,由 news_context 是否非空
得出,pipeline 两条路径共用 src/services/empty_news.news_evidence_present()
这一个判定函数。披露改为先看有无证据;确无证据时才用计数解释原因
(None=未配置渠道,0=检索零命中)。历史重建同步恢复该字段。

回归风险:旧记录没有该字段,按计数回退推断,与该记录当时的报告表现一致,不会
追溯改变旧报告;已有用例锁住。review 只点名了本地资讯池,社交情绪属同一缺陷类,
本次一并修复并加测试。另加源码断言:任一 pipeline 路径改回只传计数即失败。

Refs #2225

* fix: 按来源登记新闻证据,不让零命中占位文本冒充证据

原问题:普通分析链路在「搜索已执行但一条证据都没拿到」时,报告不再显示零命中
披露——正是本 PR 要修的核心场景,反而比改动前更差。

根因:src/search_service.py 的 format_intel_report() 即使所有维度失败或为空,
也会输出「【XX 情报搜索结果】」标题和每个维度的「未找到相关信息」占位文本,
整段永远非空。上一版把拼好的 news_context 整段交给 news_evidence_present()
判定,于是 news_result_count == 0 时 evidence 被翻成 true,披露被吞掉,错误
状态还会经 to_dict() 持久化,继续影响历史、详情 API 与 Web。

修复点:判定改为按来源逐个登记——实时检索的真实命中数、社交情绪内容、本地
资讯池内容,任一为真才算有证据;两条 pipeline 路径都不再传拼好的整段。
news_evidence_present() 的契约随之改为接收各来源,并在文档串里写明为什么不能
传整段。

回归风险:新增反例用真实的 format_intel_report() 产出占位文本(不用 mock),
断言其不得被判成证据、且报告必须出现零命中披露。另有源码断言:谁把整段
news_context 交回判定函数即失败。上一版两条测试实际在保护该缺陷(一条名为
「任何非空 context 都算证据」,一条要求必须传入 news_context),已一并纠正。

Refs #2225

---------

Co-authored-by: Mach-Chan <zz-b240@zz-b240deMacBook-Air.local>
2026-08-22 21:27:04 +08:00

11 KiB
Raw Blame History

数据源稳定性与故障处理图示

本文面向用户、部署者和维护者,说明 DSA 已接入的数据源如何参与分析、选股和大盘复盘,以及当数据源失败时系统会怎么降级。

核心原则:先用项目已经接入并验证过的数据源,把失败路径讲清楚;新增外部数据源应放在第二阶段,避免先扩大维护面。

一句话答复用户

如果遇到“数据源失败”,通常不是系统只能用一个源,而是免费源被限流、上游接口临时变更、网络抖动或当前市场/标的不支持。DSA 已经内置多数据源 fallback会按场景自动尝试下一个源如果你希望更稳定建议至少配置一个 token 型稳定源:

  • A 股个股与选股:优先配置 TUSHARE_TOKEN,并保留 AkShare / Efinance / Tencent / Baostock / YFinance 兜底。
  • A 股大盘复盘:配置 TICKFLOW_API_KEY 后,指数和市场宽度会优先尝试 TickFlow失败后回退现有免费源。
  • 港股 / 美股:配置 LONGBRIDGE_* 后优先使用 LongbridgeYFinance、Finnhub、AlphaVantage 继续兜底。
  • 热点题材:选股的热点实现参考 AlphaSift默认走 EastMoney provider并使用本地 last-good cache 降低实时接口失败影响。

已接入数据源矩阵

场景 已接入源 默认使用方式 失败处理
A 股日线 / 技术面 Efinance、Tencent、AkShare、Tushare、Pytdx、Baostock、YFinance DataFetcherManager 按优先级尝试;配置 TUSHARE_TOKEN 后 Tushare 自动进入候选源 单源失败后尝试下一个源;连续失败会短期熔断该源
A 股实时行情 Tencent、AkShare Sina、Efinance、AkShare EM、Tushare REALTIME_SOURCE_PRIORITY 控制顺序,默认偏向 Tencent / Sina 这类轻量源 失败源记录 fallback_from,成功源继续返回
A 股大盘复盘 TickFlow、AkShare、Tushare、Efinance 配置 TICKFLOW_API_KEY 后,主指数和市场宽度优先尝试 TickFlow TickFlow 权限不足或失败时回退 AkShare / Tushare / Efinance 链路
选股快照 Tushare、Sina、Efinance、AkShare EM、EastMoney Datacenter TUSHARE_TOKEN 时自动把 tushare 放入快照优先级;否则使用免费源链路 选股引擎维护 source health状态接口透出 snapshot/daily health
选股日线补特征 DataFetcherManager 选股引擎优先复用现有日线与缓存链路 现有链路失败后才回到引擎自身的日线源
选股热点题材 EastMoney provider、参考 AlphaSift 的 hotspot 实现、last-good cache 未指定 provider 时默认使用 EastMoney provider 实时失败时回退热点缓存;无缓存时返回稳定空态和可读错误
港股 / 美股 Longbridge、YFinance、AkShare、Tushare、Finnhub、AlphaVantage、Stooq 配置 Longbridge 凭证后参与港美股日线/实时兜底YFinance 保持基础兜底 Longbridge 冷却或失败时回退 YFinance / 其他可用源

总体链路图

flowchart TD
    Q[用户触发分析/选股/大盘复盘] --> S{场景}

    S --> D[个股日线与技术面]
    S --> R[实时行情]
    S --> A[选股/热点]
    S --> M[大盘复盘]

    D --> C[本地 stock_daily 缓存]
    C -->|命中且新鲜| COK[复用缓存]
    C -->|缺失或过期| DM{市场}
    DM -->|A 股| CN[Tushare if token -> Efinance/Tencent -> AkShare -> Pytdx -> Baostock -> YFinance]
    DM -->|港股| HK[Longbridge if configured -> AkShare/Tushare -> YFinance]
    DM -->|美股| US[Longbridge/YFinance -> Finnhub/AlphaVantage -> Stooq]

    R --> RP[REALTIME_SOURCE_PRIORITY]
    RP --> RS[Tencent -> AkShare Sina -> Efinance -> AkShare EM]
    RP --> RT[Tushare can be placed first when token/points are available]

    A --> AS[Snapshot: Tushare/Sina/Efinance/AkShare EM/EM Datacenter]
    A --> AD[Daily features: DSA DataFetcherManager]
    A --> AH[Hotspots: DSA EastMoney provider]
    AH --> AC[hotspots.json / hotspot_details last-good cache]

    M --> TF{TICKFLOW_API_KEY configured?}
    TF -->|yes| TFM[TickFlow indices and market breadth]
    TF -->|no or failed| MF[AkShare/Tushare/Efinance fallback]

    CN --> QL[质量标记: source/fallback/stale/fetch_failed]
    HK --> QL
    US --> QL
    RS --> QL
    RT --> QL
    AS --> QL
    AD --> QL
    AC --> QL
    TFM --> QL
    MF --> QL

失败与降级图

flowchart LR
    A[请求某个数据块] --> B{当前源成功且数据有效?}
    B -->|是| OK[返回数据并记录 source]
    B -->|否| E[记录失败原因]
    E --> F{还有下一个可用源?}
    F -->|有| N[切换到下一源]
    N --> B
    F -->|没有| C{有 last-good cache?}
    C -->|有| STALE[返回 stale/fallback 数据并提示降级]
    C -->|没有| FAIL[返回 fetch_failed/稳定空态]

    E --> H{同源连续失败达到阈值?}
    H -->|是| CB[短期熔断该源]
    H -->|否| KEEP[保留在候选链中]
    CB --> SKIP[后续请求先跳过该源]
    SKIP --> RECOVER[冷却后半开探测恢复]

当前日线源熔断策略为连续失败 3 次后短期冷却约 5 分钟。它的目的不是永久禁用数据源,而是避免一个短时间不可用的源拖慢整批分析。

选股与热点链路

flowchart TD
    UI[Web 选股/热点入口] --> API[/api/v1/screening/]

    API --> SCREEN{screen}
    SCREEN --> ENV[注入 DSA LLM 与数据源运行环境]
    ENV --> CACHE{5 分钟内有成功快照?}
    CACHE -->|yes| RESULT
    CACHE -->|no| SNAP[内建 snapshot 源优先级]
    SNAP --> TS{TUSHARE_TOKEN?}
    TS -->|yes| SP1[tushare -> sina -> efinance -> akshare_em -> em_datacenter]
    TS -->|no| SP2[sina -> efinance -> akshare_em -> em_datacenter]
    ENV --> DAILY[DSA provider context]
    DAILY --> DFM[DataFetcherManager: Tushare/Efinance/Tencent/AkShare/Pytdx/Baostock/YFinance]
    DFM --> RESULT[候选股 + source_errors/warnings/llm_parse_errors]

    API --> HOT{hotspots与 screen 并行}
    HOT --> HP{provider specified?}
    HP -->|no| EM[DSA EastMoney provider]
    HP -->|yes| CUSTOM[指定 provider/env provider]
    EM --> LIVE[实时热点榜单,详情按需加载]
    LIVE -->|成功| HCACHE[写入热点 last-good cache]
    LIVE -->|失败| OLD[读取 hotspots.json / hotspot_details]
    OLD -->|无缓存| EMPTY[稳定空态 + eastmoney_hotspot_unavailable]

推荐配置档

免费模式

适合个人试用,依赖免费源自动 fallback。优点是不需要 token缺点是更容易遇到上游限流或临时接口变化。

REALTIME_SOURCE_PRIORITY=tencent,akshare_sina,efinance,akshare_em
ENABLE_EASTMONEY_PATCH=true

A 股稳定模式

适合经常跑选股、批量分析或对外服务。Tushare 用于增强 A 股日线与快照稳定性TickFlow 可增强 A 股日 K、实时行情和大盘复盘实时行情需显式加入 REALTIME_SOURCE_PRIORITY);免费源继续作为兜底。

TUSHARE_TOKEN=your_tushare_token
TICKFLOW_API_KEY=your_tickflow_key

REALTIME_SOURCE_PRIORITY=tickflow,tushare,tencent,akshare_sina,efinance,akshare_em
SNAPSHOT_SOURCE_PRIORITY=tushare,sina,efinance,akshare_em,em_datacenter

# 选股运行期默认值;显式配置时会保留你的值
DAILY_FETCH_RETRIES=3
DAILY_FETCH_MAX_WORKERS=1

注意TickFlow 能力按套餐权限分层;权限不足或请求失败时会 fail-open 回退到现有免费源,不建议把它当成所有市场行情的唯一来源。

港股 / 美股稳定模式

适合港美股组合、持仓和个股分析。Longbridge 配置后优先参与港美股链路YFinance、Finnhub、AlphaVantage 作为兜底。

LONGBRIDGE_OAUTH_CLIENT_ID=your_client_id
LONGBRIDGE_OAUTH_TOKEN_CACHE_B64=your_token_cache_base64

FINNHUB_API_KEY=your_finnhub_key
ALPHAVANTAGE_API_KEY=your_alphavantage_key

如果仍使用 Legacy Longbridge 凭证,也可以继续配置:

LONGBRIDGE_APP_KEY=your_app_key
LONGBRIDGE_APP_SECRET=your_app_secret
LONGBRIDGE_ACCESS_TOKEN=your_access_token

用户可见提示建议

对外沟通时建议区分三类情况:

情况 建议提示
单个源失败但 fallback 成功 本次使用了降级数据源,分析仍可继续;报告中会标记实际成功源。
多个源失败但有缓存 实时源不可用,本次使用上一次成功缓存;结论会降低置信度。
全部源失败且无缓存 当前数据不可用,请稍后重试,或配置 Tushare / TickFlow / Longbridge 等 token 型数据源。

新闻面证据缺失的报告标注(已实现)

报告会区分新闻检索未执行执行后零命中,分别渲染对应提示:

⚠️ 未配置搜索渠道,本次分析未纳入新闻面证据。

⚠️ 本次未获取到可用的新闻面数据,以下结论未纳入新闻维度证据。

覆盖 dashboard、brief、个股与日报四个渲染路径以及历史 Markdown、分享图片和 Web 报告详情;中文、英文、韩文报告使用各自的披露文案。该提示的判定独立于模型输出: 即使 LLM 按 schema 写出了 market_sentiment / hot_topics,只要检索零命中就照常提示, 避免出现「展示模型生成的情绪判断、却隐瞒无新闻证据」这一组合。

判定依据是 AnalysisResult.news_result_count

取值 含义 是否提示
None 未执行检索(未配置搜索渠道) ——明确说明本次分析没有新闻面证据
0 执行了检索但零命中(搜索源限流、全部失败等)
> 0 正常拿到新闻

新记录会把该三态值随分析结果持久化,保证实时报告和历史报告一致。旧记录若没有保存 news_result_count,其新闻检索状态只能视为未知,历史展示保持原样,不会倒推为“未配置搜索渠道”。

后续可做的产品化增强

  1. 数据源 Doctor 页面:展示每个源最近成功时间、失败原因、熔断状态和下一次恢复探测时间。
  2. 一键推荐配置:根据市场选择生成 .env 片段例如“A 股稳定模式”“港美股稳定模式”“免费模式”。
  3. 选股状态面板:直接展示 snapshot/daily source health让用户知道是 Sina、Efinance、AkShare 还是 Tushare 出问题。
  4. 批量任务限速策略:对免费源自动降低并发,优先复用本地日线缓存,减少触发上游限流。
  5. 可选商业源接入:只有在现有 Tushare / TickFlow / Longbridge / Finnhub / AlphaVantage 仍不能覆盖需求时,再考虑新增 Twelve Data、Massive/Polygon、Nasdaq Data Link 等源。

官方资料