mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: 升级 AlphaSift 并兼容热点详情龙头股字段 (#1714)
* fix: update AlphaSift pin and hotspot detail fields * fix(review-feedback-1714): fix still returning no concept stocks despite the leaders being
This commit is contained in:
@@ -31,10 +31,10 @@ ALPHASIFT_ENABLED=false
|
||||
# - ALPHASIFT_ENABLED 只影响 AlphaSift 选股流程,不会改写/迁移/清理现有 LITELLM_*/LLM_* 配置。
|
||||
# - 关闭/回退只需将 ALPHASIFT_ENABLED 置 false;现有 provider/base_url/custom headers 与 fallback 语义保持原状。
|
||||
# - 兼容依据(运维核验):LiteLLM providers 与 OpenAI-compatible 语义说明见下方注释;运行时默认使用 requirements.txt 中固定 litellm 版本。
|
||||
ALPHASIFT_INSTALL_SPEC=git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404
|
||||
ALPHASIFT_INSTALL_SPEC=git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8
|
||||
# 兼容说明:
|
||||
# - ALPHASIFT_INSTALL_SPEC 仅用于显式调用 /api/v1/alphasift/install;不会自动覆盖历史 .env 配置。
|
||||
# - 升级后想使用新 pin,请清理旧 `.env` 中该变量后重建依赖/重启服务;或直接改为 14e74fc... 并重建后端。
|
||||
# - 升级后想使用新 pin,请清理旧 `.env` 中该变量后重建依赖/重启服务;或直接改为 3770498... 并重建后端。
|
||||
# - 仅改 .env 里该字段而不回退 `requirements.txt` 与 `src/config.py` 会触发 allow-list 拒绝(alphasift_install_spec_not_allowed)。
|
||||
# AlphaSift 全市场快照源优先级;未配置时 DSA 会按是否配置 TUSHARE_TOKEN 自动注入:
|
||||
# - 有 TUSHARE_TOKEN:tushare,sina,efinance,akshare_em,em_datacenter
|
||||
@@ -52,6 +52,8 @@ ALPHASIFT_INSTALL_SPEC=git+https://github.com/ZhuLinsen/alphasift.git@14e74fc081
|
||||
# INDUSTRY_PROVIDER_MAX_BOARDS=80
|
||||
# 单次 LLM 请求超时秒数;AlphaSift 选股会复用 DSA 的该配置,超时后降级返回非 LLM 排序结果。
|
||||
# LLM_TIMEOUT_SEC=60
|
||||
# AlphaSift LLM 重排请求输出上限;仅影响 AlphaSift 选股重排,默认 2048。
|
||||
# LLM_MAX_TOKENS=2048
|
||||
|
||||
# Finnhub(可选,美股数据源,免费 tier 60 calls/min)
|
||||
# 获取: https://finnhub.io/register
|
||||
|
||||
@@ -205,6 +205,7 @@ describe('alphasiftApi', () => {
|
||||
summary: '玻璃基板盘中发酵',
|
||||
route: [{ title: '盘中发酵', description: '出现大笔买入' }],
|
||||
stocks: [{ code: '920438', name: '戈碧迦', role: '异动核心' }],
|
||||
leader_stocks: [{ code: '920438', name: '戈碧迦', role: '异动核心' }],
|
||||
stock_count: 1,
|
||||
},
|
||||
});
|
||||
@@ -218,6 +219,7 @@ describe('alphasiftApi', () => {
|
||||
expect(result.topic).toBe('玻璃基板');
|
||||
expect(result.stockCount).toBe(1);
|
||||
expect(result.stocks[0].name).toBe('戈碧迦');
|
||||
expect(result.leaderStocks?.[0].name).toBe('戈碧迦');
|
||||
});
|
||||
|
||||
it('uses a long timeout for LLM-backed screening', async () => {
|
||||
|
||||
@@ -153,6 +153,7 @@ export type AlphaSiftHotspotDetail = {
|
||||
route: AlphaSiftHotspotRouteItem[];
|
||||
timeline?: AlphaSiftHotspotRouteItem[];
|
||||
stocks: AlphaSiftHotspotStock[];
|
||||
leaderStocks?: AlphaSiftHotspotStock[];
|
||||
stockCount: number;
|
||||
sourceErrors?: string[];
|
||||
qualityStatus?: 'available' | 'partial' | 'stale' | 'failed' | string;
|
||||
|
||||
@@ -36,12 +36,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
<!-- 新条目格式:- [类型] 描述(类型取值:新功能/改进/修复/文档/测试/chore)-->
|
||||
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
|
||||
- [修复] 发布说明生成查询 PR 作者失败时保留降级并输出包含 PR 编号和异常类型的 warning,便于排查 token、权限、网络或 GitHub API 异常。
|
||||
- [改进] DSA 数据源链路新增 Tencent 日 K 直连 fetcher、daily source health 短期熔断,并升级 AlphaSift 默认 pin/runtime bridge,默认启用 `DAILY_SOURCE=auto`、Sina snapshot 优先级和候选级 quote context。
|
||||
- [改进] DSA 数据源链路新增 Tencent 日 K 直连 fetcher、daily source health 短期熔断,并升级 AlphaSift 默认 pin/runtime bridge,默认启用 `DAILY_SOURCE=auto`、Sina snapshot 优先级、候选级 quote context 与 LLM ranking timeout/max tokens 边界。
|
||||
- [文档] 补充 AlphaSift 迁移与回退边界:明确 `ALPHASIFT_INSTALL_SPEC` 显式覆盖语义、`requirements.txt + DEFAULT_ALPHASIFT_INSTALL_SPEC` 与运行时兼容边界、以及回滚路径(关闭功能/完整 revert)说明,覆盖旧 pin 用户升级行为。
|
||||
- [新功能] #1707 新增合规 RSS/Atom 资讯源配置、拉取、去重、入库、查询、retention 与基础安全校验 API,作为个股/市场资讯情报池基线。
|
||||
- [文档] 本次 #1707 情报源基线仅新增 `NEWS_INTEL_*` 配置,不变更 LITELLM / ANSPIRE / Base URL / provider/model 清理与回退语义;回退策略为移除或禁用这三项情报源变量后恢复既有 LLM 配置行为。
|
||||
|
||||
- [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。
|
||||
- [修复] AlphaSift 热点详情兼容 `leader_stocks` 与 `stocks` 双字段,避免旧合约或缓存只提供其中一个字段时概念股详情报缺失字段。
|
||||
- [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ AlphaSift 作为独立仓库维护的选股引擎接入 DSA。DSA 默认不启
|
||||
|
||||
- 默认关闭:`ALPHASIFT_ENABLED=false`。
|
||||
- 启用入口:设置页或选股页点击开启,或在 `.env` 中配置 `ALPHASIFT_ENABLED=true`。
|
||||
- 依赖来源:`requirements.txt` 固定到已验证的 AlphaSift 适配层 commit:`git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404#egg=alphasift`(对应提交 `https://github.com/ZhuLinsen/alphasift/commit/14e74fc0819267f7c04c3117a0dd0fe3f9b19404`,来源 PR `https://github.com/ZhuLinsen/alphasift/pull/16`)。该来源覆盖 `alphasift.dsa_adapter` 契约、`screen/list_strategies/get_status` 调用,以及 Tencent 日 K、Sina snapshot、source health、stale daily fallback 与候选级 quote context。
|
||||
- 依赖来源:`requirements.txt` 固定到已验证的 AlphaSift 适配层 commit:`git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8#egg=alphasift`(对应提交 `https://github.com/ZhuLinsen/alphasift/commit/377049857cc04175dc3cca62121ee41adec6cdb8`,来源 PR `https://github.com/ZhuLinsen/alphasift/pull/16` 与 `https://github.com/ZhuLinsen/alphasift/pull/19`)。该来源覆盖 `alphasift.dsa_adapter` 契约、`screen/list_strategies/get_status` 调用、Tencent 日 K、Sina snapshot、source health、stale daily fallback、候选级 quote context,以及 LLM ranking 的 `LLM_MAX_TOKENS` 输出上限和 timeout 后不重复 JSON-mode 重试的边界。
|
||||
- 修复安装来源:`ALPHASIFT_INSTALL_SPEC` 仍保留,默认等于同一个受信任 commit。它不再是策略列表或选股接口的运行时安装主路径,只用于显式调用 `/api/v1/alphasift/install` 时做修复安装和来源校验;未显式配置时才按代码常量 `DEFAULT_ALPHASIFT_INSTALL_SPEC` 回退。
|
||||
- 迁移边界(显式 `.env` 优先):
|
||||
- 若 `.env` 中显式保留旧 pin(如 `...de54ea0da367be85770d9589a5bf7ded4f62d386`),DSA 会把该值当作用户覆盖,不会在运行期自动替换为新 pin;
|
||||
@@ -21,7 +21,7 @@ AlphaSift 作为独立仓库维护的选股引擎接入 DSA。DSA 默认不启
|
||||
|
||||
## 外部契约来源与迁移边界
|
||||
|
||||
- 外部契约依据:本次 AlphaSift 运行契约(含 `schema_version=2` 的热点缓存与题材详情字段)对应 GitHub 提交 `https://github.com/ZhuLinsen/alphasift/commit/14e74fc0819267f7c04c3117a0dd0fe3f9b19404`。
|
||||
- 外部契约依据:本次 AlphaSift 运行契约(含 `schema_version=2` 的热点缓存与题材详情字段)对应 GitHub 提交 `https://github.com/ZhuLinsen/alphasift/commit/377049857cc04175dc3cca62121ee41adec6cdb8`。
|
||||
该 commit 在 DSA 中通过以下链路生效:`requirements.txt` 安装 pin、`src/config.py` 默认 `DEFAULT_ALPHASIFT_INSTALL_SPEC`、`.env.example` 默认示例值。
|
||||
- 升级路径:
|
||||
- 部署侧只需按部署方式 `git pull` + `pip install -r requirements.txt` + 重启服务;
|
||||
@@ -35,9 +35,9 @@ AlphaSift 作为独立仓库维护的选股引擎接入 DSA。DSA 默认不启
|
||||
|
||||
- DSA 增强:AlphaSift 通过 DSA provider context 在 LLM 重排前只补充 Top 候选的轻量实时行情和基本面上下文,不在初筛阶段抓新闻;DSA API 返回阶段会对最终 Top 候选补新闻和辅助摘要,并通过 `dsa_enrichment` 记录复用或补全情况。
|
||||
- 日 K 线补特征:DSA 调用 AlphaSift 时会优先复用 DSA 历史行情加载链路(数据库缓存、Tushare、Efinance、Akshare、Pytdx、Baostock、Yfinance 等 fallback),仅在 DSA 链路无可用数据时回退到 AlphaSift 原始日线数据源,减少单一上游超时拖垮选股。
|
||||
- LLM 环境:DSA 调用 AlphaSift 时会桥接 DSA 已解析的 `LITELLM_MODEL`、`LITELLM_FALLBACK_MODELS`、`LLM_CHANNELS`、`LLM_<NAME>_*`、`LITELLM_CONFIG`、渠道额外请求头和各模型密钥;AlphaSift 独立运行时仍使用自己的 `.env`/环境变量。
|
||||
- LLM 环境:DSA 调用 AlphaSift 时会桥接 DSA 已解析的 `LITELLM_MODEL`、`LITELLM_FALLBACK_MODELS`、`LLM_CHANNELS`、`LLM_<NAME>_*`、`LITELLM_CONFIG`、渠道额外请求头和各模型密钥;AlphaSift 独立运行时仍使用自己的 `.env`/环境变量。`LLM_TIMEOUT_SEC` 与 `LLM_MAX_TOKENS` 可被 AlphaSift 选股 LLM 重排读取,分别限制单次请求耗时和输出 token 上限。
|
||||
- 快照源:DSA 调用 AlphaSift 时,未显式配置 `SNAPSHOT_SOURCE_PRIORITY` 会按 token-aware 顺序注入:有 `TUSHARE_TOKEN` 时为 `tushare,sina,efinance,akshare_em,em_datacenter`,无 token 时为 `sina,efinance,akshare_em,em_datacenter`;同时注入 `DAILY_SOURCE=auto` 和默认候选上下文 `news,fund_flow,announcement,quote`。显式配置的源顺序和候选上下文 provider 会原样保留。
|
||||
- 最新 AlphaSift 能力:锁定 commit `14e74fc0819267f7c04c3117a0dd0fe3f9b19404` 包含选股 pipeline 性能优化、Tencent 日 K、Sina snapshot、source health、stale daily fallback、candidate quote context、last-good snapshot fallback、日线历史缓存、行业/概念 provider cache、热点/行业热度因子、hotspot 热点题材榜单与本地 scorecard/post-analysis 元信息。DSA 调用时会注入隔离缓存默认路径 `data/alphasift`、`data/alphasift/snapshot.last_good.json`、`data/alphasift/daily_history`、`data/alphasift/industry_provider_cache`;Web 选股页提供“热点题材”手动刷新入口,请求 `/api/v1/alphasift/hotspots` 时会显式使用 `akshare` provider 优先拉取具体概念/题材异动(例如钼、铅锌、铜、诊断服务等实时板块异动),行业板块仅作为兜底;默认打开页面时优先读取上一次成功且不少于 3 条的热点题材缓存,点击刷新才实时拉取并覆盖缓存,实时拉取失败时会尽量回退旧缓存;如果 AlphaSift 合约层只返回少量或缺少涨跌幅等关键字段的热点,DSA 会用东方财富板块异动直连榜单替代。点击题材会请求 `/api/v1/alphasift/hotspots/{topic}` 展示发酵路线与概念股;题材详情另有 DSA 侧 30 分钟磁盘缓存,路径为 `data/alphasift/hotspot_details` 或自定义 `ALPHASIFT_DATA_DIR/hotspot_details`,重复点开同一题材会优先返回缓存,实时详情失败时可回退过期缓存;手动刷新热点榜单并保留当前题材时会对详情请求传入 `refresh=true` 绕过详情缓存;不会默认触发 AlphaSift 的 DSA deep-analysis 回调,避免无提示扩大递归调用面。
|
||||
- 最新 AlphaSift 能力:锁定 commit `377049857cc04175dc3cca62121ee41adec6cdb8` 包含选股 pipeline 性能优化、Tencent 日 K、Sina snapshot、source health、stale daily fallback、candidate quote context、LLM ranking timeout/max tokens 边界、last-good snapshot fallback、日线历史缓存、行业/概念 provider cache、热点/行业热度因子、hotspot 热点题材榜单与本地 scorecard/post-analysis 元信息。DSA 调用时会注入隔离缓存默认路径 `data/alphasift`、`data/alphasift/snapshot.last_good.json`、`data/alphasift/daily_history`、`data/alphasift/industry_provider_cache`;Web 选股页提供“热点题材”手动刷新入口,请求 `/api/v1/alphasift/hotspots` 时会显式使用 `akshare` provider 优先拉取具体概念/题材异动(例如钼、铅锌、铜、诊断服务等实时板块异动),行业板块仅作为兜底;默认打开页面时优先读取上一次成功且不少于 3 条的热点题材缓存,点击刷新才实时拉取并覆盖缓存,实时拉取失败时会尽量回退旧缓存;如果 AlphaSift 合约层只返回少量或缺少涨跌幅等关键字段的热点,DSA 会用东方财富板块异动直连榜单替代。点击题材会请求 `/api/v1/alphasift/hotspots/{topic}` 展示发酵路线与概念股;题材详情另有 DSA 侧 30 分钟磁盘缓存,路径为 `data/alphasift/hotspot_details` 或自定义 `ALPHASIFT_DATA_DIR/hotspot_details`,重复点开同一题材会优先返回缓存,实时详情失败时可回退过期缓存;手动刷新热点榜单并保留当前题材时会对详情请求传入 `refresh=true` 绕过详情缓存;不会默认触发 AlphaSift 的 DSA deep-analysis 回调,避免无提示扩大递归调用面。
|
||||
- 热点刷新容错:东方财富热点直连源遇到连接中断、超时或 `Connection aborted` 会做短 backoff 重试;手动刷新失败且没有可用热点缓存时,`/api/v1/alphasift/hotspots` 返回稳定空态 payload、`source_errors=["eastmoney_hotspot_unavailable"]` 和用户可读 `message`,原始异常仅保留在服务端日志或诊断链路中。桌面端更新会保留 `data/alphasift/hotspots.json`、`data/alphasift/hotspot.history.jsonl`、`data/alphasift/hotspot_details` 与 `data/alphasift/snapshot.last_good.json`,避免更新后丢失 last-good 缓存。
|
||||
- 热点数据源补充:DSA provider 使用直连 HTTP 思路,东财板块兜底源使用 `push2.eastmoney.com/api/qt/clist/get` 并保留涨跌幅、领涨股、上涨/下跌家数等字段;题材详情会在一次 provider 生命周期内缓存并合并东方财富成分股、同花顺页面解析和板块异动龙头兜底,优先返回多只概念股,发酵路线按日期聚合展示,避免把同一盘中观察拆成多条同时间节点;事件催化不再使用 DSA 内置静态文案,只展示 AlphaSift 合约时间线、同花顺摘要、已配置新闻搜索源或东财板块异动结构拿到的真实信息;新闻搜索命中的消息会优先通过已配置 LLM 压缩成一句题材催化摘要,LLM 不可用时回退本地短摘要,避免在时间线中展示完整报道。
|
||||
- 风险提示:前端设置页和选股页展示第三方来源与投资风险说明;不会弹窗打断用户。
|
||||
@@ -121,7 +121,7 @@ context = {
|
||||
|
||||
AlphaSift 会在 L1 初筛后、LLM 重排前调用 `context["dsa"]` 中的 provider,为有限 Top 候选补充 DSA 行情和基本面轻量上下文,并把 `dsa_context` 随候选返回。新闻搜索、完整摘要和缺失字段补全由 DSA API 在最终 Top 候选阶段执行;若候选已经携带完整新闻上下文,DSA API 返回阶段会复用这些字段,避免重复请求。
|
||||
|
||||
AlphaSift 侧已在 `ZhuLinsen/alphasift@14e74fc0819267f7c04c3117a0dd0fe3f9b19404` 提供 DSA provider context 支持、DSA adapter contract,并支持复用 DSA 的 `LLM_TIMEOUT_SEC`。
|
||||
AlphaSift 侧已在 `ZhuLinsen/alphasift@377049857cc04175dc3cca62121ee41adec6cdb8` 提供 DSA provider context 支持、DSA adapter contract,并支持复用 DSA 的 `LLM_TIMEOUT_SEC`;同一 pin 还会读取 `LLM_MAX_TOKENS` 限制 LLM 重排输出,且 timeout 后不再盲目重试无 JSON mode 请求。
|
||||
|
||||
## DSA 后端行为
|
||||
|
||||
@@ -135,7 +135,7 @@ AlphaSift 侧已在 `ZhuLinsen/alphasift@14e74fc0819267f7c04c3117a0dd0fe3f9b1940
|
||||
## 配置兼容边界(LLM / LiteLLM / Base URL)
|
||||
|
||||
- 兼容语义与版本证据(可追溯):
|
||||
- 运行依赖约束:`requirements.txt` 中将 LiteLLM 固定到 `litellm>=1.80.10,!=1.82.7,!=1.82.8,<2.0.0`,并通过 `git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404` 安装 AlphaSift 适配层。
|
||||
- 运行依赖约束:`requirements.txt` 中将 LiteLLM 固定到 `litellm>=1.80.10,!=1.82.7,!=1.82.8,<2.0.0`,并通过 `git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8` 安装 AlphaSift 适配层。
|
||||
- 文档依据:
|
||||
- LiteLLM Providers: https://docs.litellm.ai/docs/providers
|
||||
- LiteLLM OpenAI-compatible: https://docs.litellm.ai/docs/providers/openai_compatible
|
||||
@@ -168,7 +168,7 @@ AlphaSift 侧已在 `ZhuLinsen/alphasift@14e74fc0819267f7c04c3117a0dd0fe3f9b1940
|
||||
|
||||
- 依赖与源码约束核验:`requirements.txt` 中的 `litellm` 约束与 `src/config.py`/`requirements.txt` 一致。
|
||||
- Hotspot 契约兼容核验:`docs/alphasift-integration.md` 与 `api/v1/endpoints/alphasift.py`、`src/services/alphasift_service.py` 保持 `hotspots`/`hotspots/{topic}` 字段与 `tests/test_alphasift_api.py` 一致,调用前后默认使用 `snapshot.last_good` 缓存兜底。
|
||||
- 外部版本来源:本次集成依赖来源为 `https://github.com/ZhuLinsen/alphasift/commit/14e74fc0819267f7c04c3117a0dd0fe3f9b19404`,需在复验时按该 commit pin 回放导入与接口契约。
|
||||
- 外部版本来源:本次集成依赖来源为 `https://github.com/ZhuLinsen/alphasift/commit/377049857cc04175dc3cca62121ee41adec6cdb8`,需在复验时按该 commit pin 回放导入与接口契约。
|
||||
- 行为核验:`src/services/alphasift_service.py` 的 `_build_alphasift_runtime_env` 与 `_build_alphasift_context` 仅在调用期写入进程环境;`/api/v1/alphasift/screen`、`strategies`、`status` 在运行期不回写 `.env`。
|
||||
- 回退核验:关闭 `ALPHASIFT_ENABLED` 并重启配置链路后,系统恢复原始 `LITELLM_MODEL/FALLBACK_MODELS`、`LLM_CHANNELS` 与 `LLM_*` 运行语义,不执行迁移清理脚本。
|
||||
- 语义来源核验:LiteLLM 文档(https://docs.litellm.ai/docs/providers)、OpenAI-compatible 文档(https://docs.litellm.ai/docs/providers/openai_compatible)与 LiteLLM 配置文档(https://docs.litellm.ai/docs/proxy/configs)用于核对 provider/model/base_url/extra_headers 映射链路。
|
||||
|
||||
@@ -19,7 +19,7 @@ yfinance>=0.2.0 # Priority 4: Yahoo Finance (Fallback)
|
||||
longbridge>=0.2.77 # Priority 5: Longbridge OpenAPI fallback for US/HK stocks; OAuth capability checked at runtime
|
||||
tickflow>=0.1.0 # TickFlow official SDK (Issue #632, market review enhancement)
|
||||
# Built-in optional AlphaSift screening engine
|
||||
git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404#egg=alphasift
|
||||
git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8#egg=alphasift
|
||||
|
||||
# Feishu
|
||||
lark-oapi>=1.0.0 # Feishu API
|
||||
|
||||
@@ -40,7 +40,7 @@ from src.llm import generation_params as llm_generation_params
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ALPHASIFT_INSTALL_SPEC = (
|
||||
"git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404"
|
||||
"git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ def _load_alphasift_hotspot_detail_cache(
|
||||
if stale and not allow_stale:
|
||||
return None
|
||||
|
||||
cached = dict(payload)
|
||||
cached = _ensure_hotspot_detail_compat_fields(dict(payload))
|
||||
cached.update({
|
||||
"enabled": True,
|
||||
"provider": provider or cached.get("provider") or "akshare",
|
||||
@@ -181,7 +181,7 @@ def _write_alphasift_hotspot_detail_cache(*, provider: str, topic: str, payload:
|
||||
cache_path = _alphasift_hotspot_detail_cache_path(provider=provider, topic=topic)
|
||||
try:
|
||||
cache_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
cleaned = _remove_non_finite_json_values(dict(payload))
|
||||
cleaned = _remove_non_finite_json_values(_ensure_hotspot_detail_compat_fields(dict(payload)))
|
||||
cached_at = _utc_now_iso()
|
||||
cache_path.write_text(
|
||||
json.dumps(
|
||||
@@ -201,6 +201,36 @@ def _write_alphasift_hotspot_detail_cache(*, provider: str, topic: str, payload:
|
||||
logger.warning("Failed to write AlphaSift hotspot detail cache for %s: %s", topic, exc)
|
||||
|
||||
|
||||
def _ensure_hotspot_detail_compat_fields(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Keep old and new AlphaSift hotspot detail consumers on the same shape."""
|
||||
stocks = payload.get("stocks")
|
||||
leader_stocks = payload.get("leader_stocks")
|
||||
if not isinstance(stocks, list):
|
||||
stocks = []
|
||||
if not isinstance(leader_stocks, list) or not leader_stocks:
|
||||
nested_leader_stocks = _extract_nested_hotspot_leader_stocks(payload)
|
||||
leader_stocks = nested_leader_stocks or (leader_stocks if isinstance(leader_stocks, list) else [])
|
||||
if not stocks and leader_stocks:
|
||||
stocks = leader_stocks
|
||||
if not leader_stocks and stocks:
|
||||
leader_stocks = stocks
|
||||
payload["stocks"] = stocks
|
||||
payload["leader_stocks"] = leader_stocks
|
||||
payload["stock_count"] = len(stocks)
|
||||
return payload
|
||||
|
||||
|
||||
def _extract_nested_hotspot_leader_stocks(payload: Dict[str, Any]) -> List[Any]:
|
||||
for key in ("summary_detail", "summary"):
|
||||
summary = payload.get(key)
|
||||
if not isinstance(summary, dict):
|
||||
continue
|
||||
leader_stocks = summary.get("leader_stocks")
|
||||
if isinstance(leader_stocks, list) and leader_stocks:
|
||||
return leader_stocks
|
||||
return []
|
||||
|
||||
|
||||
def _load_alphasift_hotspot_cache(*, provider: str, top: int) -> Optional[Dict[str, Any]]:
|
||||
cache_path = _alphasift_hotspot_cache_path()
|
||||
try:
|
||||
@@ -1028,6 +1058,7 @@ class AlphaSiftService:
|
||||
if search_routes:
|
||||
route = normalized.get("route")
|
||||
normalized["route"] = search_routes + (route if isinstance(route, list) else [])
|
||||
normalized = _ensure_hotspot_detail_compat_fields(normalized)
|
||||
normalized["enabled"] = True
|
||||
normalized["provider"] = provider_name
|
||||
cleaned = _remove_non_finite_json_values(normalized)
|
||||
@@ -1105,7 +1136,9 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
|
||||
summary_value = raw.get("summary")
|
||||
summary: Dict[str, Any] = summary_value if isinstance(summary_value, dict) else {}
|
||||
stocks_value = raw.get("stocks")
|
||||
leader_stocks_value = raw.get("leader_stocks")
|
||||
stocks: List[Any] = stocks_value if isinstance(stocks_value, list) else []
|
||||
leader_stocks: List[Any] = leader_stocks_value if isinstance(leader_stocks_value, list) else []
|
||||
timeline_value = raw.get("timeline")
|
||||
timeline: List[Any] = timeline_value if isinstance(timeline_value, list) else []
|
||||
route_value = raw.get("route")
|
||||
@@ -1122,7 +1155,7 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
|
||||
if isinstance(summary_text_value, str)
|
||||
else _build_alphasift_hotspot_summary_text(summary, topic=topic, canonical_topic=canonical_topic)
|
||||
)
|
||||
return {
|
||||
return _ensure_hotspot_detail_compat_fields({
|
||||
"enabled": True,
|
||||
"provider": provider,
|
||||
"topic": topic,
|
||||
@@ -1134,7 +1167,7 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
|
||||
"route": route,
|
||||
"timeline": timeline,
|
||||
"stocks": stocks,
|
||||
"stock_count": len(stocks),
|
||||
"leader_stocks": leader_stocks,
|
||||
"source_errors": source_errors,
|
||||
"quality_status": quality_status,
|
||||
"missing_fields": missing_fields,
|
||||
@@ -1142,7 +1175,7 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
|
||||
"stale": bool(summary.get("stale") or raw.get("stale") or False),
|
||||
"stale_age_hours": summary.get("stale_age_hours") or raw.get("stale_age_hours"),
|
||||
"resolver_candidates": _list_dict_values(summary.get("resolver_candidates") or raw.get("resolver_candidates")),
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def _list_text_values(value: Any) -> List[str]:
|
||||
@@ -2190,16 +2223,17 @@ class DsaEastMoneyHotspotProvider:
|
||||
"change_pct": None,
|
||||
"hot_stock_score": 60.0,
|
||||
})
|
||||
return {
|
||||
return _ensure_hotspot_detail_compat_fields({
|
||||
"topic": topic,
|
||||
"name": self._display_hotspot_name(topic),
|
||||
"canonical_topic": topic,
|
||||
"summary": self._build_hotspot_summary(topic, summary),
|
||||
"route": route,
|
||||
"stocks": stocks[:30],
|
||||
"leader_stocks": stocks[:30],
|
||||
"stock_count": len(stocks),
|
||||
"source_errors": [],
|
||||
}
|
||||
})
|
||||
|
||||
def _fetch_board_changes(self) -> Any:
|
||||
import pandas as pd
|
||||
|
||||
@@ -860,8 +860,91 @@ class AlphaSiftOpportunitiesApiTestCase(unittest.TestCase):
|
||||
self.assertEqual(payload["missing_fields"], ["live_stocks"])
|
||||
self.assertEqual(payload["source_errors"], ["none: no live detail rows"])
|
||||
self.assertEqual(payload["stocks"][0]["source"], "last_good_cache.leader_stocks")
|
||||
self.assertEqual(payload["leader_stocks"][0]["source"], "last_good_cache.leader_stocks")
|
||||
self.assertEqual(payload["route"][0]["title"], "AI算力催化")
|
||||
|
||||
def test_hotspot_detail_backfills_stocks_from_contract_leader_stocks(self) -> None:
|
||||
config = self._config(enabled=True)
|
||||
|
||||
def get_hotspot_detail(topic: str, **_kwargs: Any) -> Dict[str, Any]:
|
||||
return {
|
||||
"summary": {"topic": topic, "name": "算力"},
|
||||
"leader_stocks": [{
|
||||
"code": "300001",
|
||||
"name": "算力龙头",
|
||||
"role": "核心龙头",
|
||||
"source": "last_good_cache.leader_stocks",
|
||||
}],
|
||||
"route": [{"title": "盘中发酵", "description": "真实新闻催化", "source": "news"}],
|
||||
}
|
||||
|
||||
provider = alphasift_service.DsaEastMoneyHotspotProvider()
|
||||
provider.hotspot_detail = MagicMock(side_effect=AssertionError("provider route fallback should not be used"))
|
||||
with (
|
||||
patch("src.services.alphasift_service._get_alphasift_status_snapshot", return_value=({}, True, {})),
|
||||
patch("src.services.alphasift_service._resolve_hotspot_provider", return_value=("akshare", provider)),
|
||||
patch(
|
||||
"src.services.alphasift_service._import_alphasift_hotspot",
|
||||
return_value=SimpleNamespace(get_hotspot_detail=get_hotspot_detail),
|
||||
),
|
||||
):
|
||||
payload = self._hotspot_detail(config=config, provider="akshare", topic="AI算力")
|
||||
|
||||
self.assertEqual(payload["stocks"][0]["name"], "算力龙头")
|
||||
self.assertEqual(payload["leader_stocks"][0]["name"], "算力龙头")
|
||||
self.assertEqual(payload["stock_count"], 1)
|
||||
provider.hotspot_detail.assert_not_called()
|
||||
|
||||
def test_hotspot_detail_compat_backfills_from_summary_detail_leader_stocks(self) -> None:
|
||||
payload = alphasift_service._ensure_hotspot_detail_compat_fields({
|
||||
"summary_detail": {
|
||||
"leader_stocks": [{
|
||||
"code": "300001",
|
||||
"name": "缓存龙头",
|
||||
"source": "legacy.summary_detail.leader_stocks",
|
||||
}],
|
||||
},
|
||||
})
|
||||
|
||||
self.assertEqual(payload["stocks"][0]["name"], "缓存龙头")
|
||||
self.assertEqual(payload["leader_stocks"][0]["name"], "缓存龙头")
|
||||
self.assertEqual(payload["stock_count"], 1)
|
||||
|
||||
def test_hotspot_detail_backfills_stocks_from_summary_leader_stocks(self) -> None:
|
||||
config = self._config(enabled=True)
|
||||
|
||||
def get_hotspot_detail(topic: str, **_kwargs: Any) -> Dict[str, Any]:
|
||||
return {
|
||||
"summary": {
|
||||
"topic": topic,
|
||||
"name": "算力",
|
||||
"leader_stocks": [{
|
||||
"code": "300001",
|
||||
"name": "嵌套龙头",
|
||||
"role": "缓存龙头",
|
||||
"source": "last_good_cache.summary.leader_stocks",
|
||||
}],
|
||||
},
|
||||
"route": [{"title": "盘中发酵", "description": "真实新闻催化", "source": "news"}],
|
||||
}
|
||||
|
||||
provider = alphasift_service.DsaEastMoneyHotspotProvider()
|
||||
provider.hotspot_detail = MagicMock(side_effect=AssertionError("provider route fallback should not be used"))
|
||||
with (
|
||||
patch("src.services.alphasift_service._get_alphasift_status_snapshot", return_value=({}, True, {})),
|
||||
patch("src.services.alphasift_service._resolve_hotspot_provider", return_value=("akshare", provider)),
|
||||
patch(
|
||||
"src.services.alphasift_service._import_alphasift_hotspot",
|
||||
return_value=SimpleNamespace(get_hotspot_detail=get_hotspot_detail),
|
||||
),
|
||||
):
|
||||
payload = self._hotspot_detail(config=config, provider="akshare", topic="AI算力")
|
||||
|
||||
self.assertEqual(payload["stocks"][0]["name"], "嵌套龙头")
|
||||
self.assertEqual(payload["leader_stocks"][0]["name"], "嵌套龙头")
|
||||
self.assertEqual(payload["stock_count"], 1)
|
||||
provider.hotspot_detail.assert_not_called()
|
||||
|
||||
def test_hotspot_detail_uses_dsa_detail_cache_after_first_fetch(self) -> None:
|
||||
config = self._config(enabled=True)
|
||||
provider = alphasift_service.DsaEastMoneyHotspotProvider()
|
||||
@@ -889,6 +972,7 @@ class AlphaSiftOpportunitiesApiTestCase(unittest.TestCase):
|
||||
self.assertFalse(first.get("cache_used", False))
|
||||
self.assertTrue(second["cache_used"])
|
||||
self.assertEqual(second["stocks"][0]["name"], "盛龙股份")
|
||||
self.assertEqual(second["leader_stocks"][0]["name"], "盛龙股份")
|
||||
|
||||
def test_hotspot_detail_refresh_bypasses_dsa_detail_cache(self) -> None:
|
||||
config = self._config(enabled=True)
|
||||
@@ -1118,6 +1202,7 @@ class AlphaSiftOpportunitiesApiTestCase(unittest.TestCase):
|
||||
self.assertEqual(payload["topic"], "玻璃基板")
|
||||
self.assertEqual(payload["route"][0]["title"], "盘中发酵")
|
||||
self.assertEqual(payload["stocks"][0]["name"], "戈碧迦")
|
||||
self.assertEqual(payload["leader_stocks"][0]["name"], "戈碧迦")
|
||||
|
||||
def test_hotspot_detail_route_accepts_slash_containing_topic(self) -> None:
|
||||
config = self._config(enabled=True)
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_dockerfile_bundles_default_alphasift_adapter() -> None:
|
||||
requirements = (REPO_ROOT / "requirements.txt").read_text(encoding="utf-8")
|
||||
|
||||
assert "git \\" in dockerfile
|
||||
assert "git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404#egg=alphasift" in requirements
|
||||
assert "git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8#egg=alphasift" in requirements
|
||||
assert "pip install --no-cache-dir -r requirements.txt" in dockerfile
|
||||
assert "import alphasift.dsa_adapter" in dockerfile
|
||||
|
||||
|
||||
@@ -748,7 +748,7 @@ class SystemConfigServiceTestCase(unittest.TestCase):
|
||||
"LLM_OPENAI_MODELS=openai/gpt-4o-mini,openai/gpt-4o",
|
||||
"LITELLM_FALLBACK_MODELS=openai/gpt-4o-mini,openai/gpt-4o",
|
||||
"ALPHASIFT_ENABLED=false",
|
||||
"ALPHASIFT_INSTALL_SPEC=git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404",
|
||||
"ALPHASIFT_INSTALL_SPEC=git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8",
|
||||
"LLM_USAGE_HMAC_SECRET=telemetry-secret",
|
||||
"LLM_USAGE_HMAC_KEY_VERSION=test-v1",
|
||||
"GEMINI_API_KEY=legacy-secret",
|
||||
@@ -775,7 +775,7 @@ class SystemConfigServiceTestCase(unittest.TestCase):
|
||||
self.assertEqual(current_map["ALPHASIFT_ENABLED"], "true")
|
||||
self.assertEqual(
|
||||
current_map["ALPHASIFT_INSTALL_SPEC"],
|
||||
"git+https://github.com/ZhuLinsen/alphasift.git@14e74fc0819267f7c04c3117a0dd0fe3f9b19404",
|
||||
"git+https://github.com/ZhuLinsen/alphasift.git@377049857cc04175dc3cca62121ee41adec6cdb8",
|
||||
)
|
||||
self.assertEqual(current_map["LLM_USAGE_HMAC_SECRET"], "telemetry-secret")
|
||||
self.assertEqual(current_map["LLM_USAGE_HMAC_KEY_VERSION"], "test-v1")
|
||||
|
||||
Reference in New Issue
Block a user