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:
mumu
2026-06-19 09:28:32 +08:00
committed by GitHub
parent cd37608506
commit 0ed601022f
11 changed files with 147 additions and 22 deletions

View File

@@ -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_TOKENtushare,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

View File

@@ -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 () => {

View File

@@ -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;

View File

@@ -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` 双字段,避免旧合约或缓存只提供其中一个字段时概念股详情报缺失字段。
- [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。

View File

@@ -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 映射链路。

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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")