Skip to content

Commit cb172ad

Browse files
committed
fix: handle AlphaSift hotspot leader stocks field
1 parent 689a69d commit cb172ad

5 files changed

Lines changed: 72 additions & 6 deletions

File tree

apps/dsa-web/src/api/__tests__/alphasift.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ describe('alphasiftApi', () => {
205205
summary: '玻璃基板盘中发酵',
206206
route: [{ title: '盘中发酵', description: '出现大笔买入' }],
207207
stocks: [{ code: '920438', name: '戈碧迦', role: '异动核心' }],
208+
leader_stocks: [{ code: '920438', name: '戈碧迦', role: '异动核心' }],
208209
stock_count: 1,
209210
},
210211
});
@@ -218,6 +219,7 @@ describe('alphasiftApi', () => {
218219
expect(result.topic).toBe('玻璃基板');
219220
expect(result.stockCount).toBe(1);
220221
expect(result.stocks[0].name).toBe('戈碧迦');
222+
expect(result.leaderStocks?.[0].name).toBe('戈碧迦');
221223
});
222224

223225
it('uses a long timeout for LLM-backed screening', async () => {

apps/dsa-web/src/api/alphasift.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export type AlphaSiftHotspotDetail = {
153153
route: AlphaSiftHotspotRouteItem[];
154154
timeline?: AlphaSiftHotspotRouteItem[];
155155
stocks: AlphaSiftHotspotStock[];
156+
leaderStocks?: AlphaSiftHotspotStock[];
156157
stockCount: number;
157158
sourceErrors?: string[];
158159
qualityStatus?: 'available' | 'partial' | 'stale' | 'failed' | string;

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
4040
- [文档] 补充 AlphaSift 迁移与回退边界:明确 `ALPHASIFT_INSTALL_SPEC` 显式覆盖语义、`requirements.txt + DEFAULT_ALPHASIFT_INSTALL_SPEC` 与运行时兼容边界、以及回滚路径(关闭功能/完整 revert)说明,覆盖旧 pin 用户升级行为。
4141

4242
- [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。
43+
- [修复] AlphaSift 热点详情兼容 `leader_stocks``stocks` 双字段,避免旧合约或缓存只提供其中一个字段时概念股详情报缺失字段。
4344
- [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。
4445

4546

src/services/alphasift_service.py

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ def _load_alphasift_hotspot_detail_cache(
163163
if stale and not allow_stale:
164164
return None
165165

166-
cached = dict(payload)
166+
cached = _ensure_hotspot_detail_compat_fields(dict(payload))
167167
cached.update({
168168
"enabled": True,
169169
"provider": provider or cached.get("provider") or "akshare",
@@ -181,7 +181,7 @@ def _write_alphasift_hotspot_detail_cache(*, provider: str, topic: str, payload:
181181
cache_path = _alphasift_hotspot_detail_cache_path(provider=provider, topic=topic)
182182
try:
183183
cache_path.parent.mkdir(parents=True, exist_ok=True)
184-
cleaned = _remove_non_finite_json_values(dict(payload))
184+
cleaned = _remove_non_finite_json_values(_ensure_hotspot_detail_compat_fields(dict(payload)))
185185
cached_at = _utc_now_iso()
186186
cache_path.write_text(
187187
json.dumps(
@@ -201,6 +201,25 @@ def _write_alphasift_hotspot_detail_cache(*, provider: str, topic: str, payload:
201201
logger.warning("Failed to write AlphaSift hotspot detail cache for %s: %s", topic, exc)
202202

203203

204+
def _ensure_hotspot_detail_compat_fields(payload: Dict[str, Any]) -> Dict[str, Any]:
205+
"""Keep old and new AlphaSift hotspot detail consumers on the same shape."""
206+
stocks = payload.get("stocks")
207+
leader_stocks = payload.get("leader_stocks")
208+
if not isinstance(stocks, list):
209+
stocks = []
210+
if not isinstance(leader_stocks, list):
211+
leader_stocks = []
212+
if not stocks and leader_stocks:
213+
stocks = leader_stocks
214+
if not leader_stocks and stocks:
215+
leader_stocks = stocks
216+
payload["stocks"] = stocks
217+
payload["leader_stocks"] = leader_stocks
218+
if not isinstance(payload.get("stock_count"), int):
219+
payload["stock_count"] = len(stocks)
220+
return payload
221+
222+
204223
def _load_alphasift_hotspot_cache(*, provider: str, top: int) -> Optional[Dict[str, Any]]:
205224
cache_path = _alphasift_hotspot_cache_path()
206225
try:
@@ -1028,6 +1047,7 @@ def hotspot_detail(self, *, topic: str, provider: str = "", refresh: bool = Fals
10281047
if search_routes:
10291048
route = normalized.get("route")
10301049
normalized["route"] = search_routes + (route if isinstance(route, list) else [])
1050+
normalized = _ensure_hotspot_detail_compat_fields(normalized)
10311051
normalized["enabled"] = True
10321052
normalized["provider"] = provider_name
10331053
cleaned = _remove_non_finite_json_values(normalized)
@@ -1105,7 +1125,13 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
11051125
summary_value = raw.get("summary")
11061126
summary: Dict[str, Any] = summary_value if isinstance(summary_value, dict) else {}
11071127
stocks_value = raw.get("stocks")
1128+
leader_stocks_value = raw.get("leader_stocks")
11081129
stocks: List[Any] = stocks_value if isinstance(stocks_value, list) else []
1130+
leader_stocks: List[Any] = leader_stocks_value if isinstance(leader_stocks_value, list) else []
1131+
if not stocks and leader_stocks:
1132+
stocks = leader_stocks
1133+
if not leader_stocks and stocks:
1134+
leader_stocks = stocks
11091135
timeline_value = raw.get("timeline")
11101136
timeline: List[Any] = timeline_value if isinstance(timeline_value, list) else []
11111137
route_value = raw.get("route")
@@ -1122,7 +1148,7 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
11221148
if isinstance(summary_text_value, str)
11231149
else _build_alphasift_hotspot_summary_text(summary, topic=topic, canonical_topic=canonical_topic)
11241150
)
1125-
return {
1151+
return _ensure_hotspot_detail_compat_fields({
11261152
"enabled": True,
11271153
"provider": provider,
11281154
"topic": topic,
@@ -1134,6 +1160,7 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
11341160
"route": route,
11351161
"timeline": timeline,
11361162
"stocks": stocks,
1163+
"leader_stocks": leader_stocks,
11371164
"stock_count": len(stocks),
11381165
"source_errors": source_errors,
11391166
"quality_status": quality_status,
@@ -1142,7 +1169,7 @@ def _normalize_alphasift_hotspot_detail(detail: Any, *, provider: str, requested
11421169
"stale": bool(summary.get("stale") or raw.get("stale") or False),
11431170
"stale_age_hours": summary.get("stale_age_hours") or raw.get("stale_age_hours"),
11441171
"resolver_candidates": _list_dict_values(summary.get("resolver_candidates") or raw.get("resolver_candidates")),
1145-
}
1172+
})
11461173

11471174

11481175
def _list_text_values(value: Any) -> List[str]:
@@ -2190,16 +2217,17 @@ def hotspot_detail(self, topic: str) -> Dict[str, Any]:
21902217
"change_pct": None,
21912218
"hot_stock_score": 60.0,
21922219
})
2193-
return {
2220+
return _ensure_hotspot_detail_compat_fields({
21942221
"topic": topic,
21952222
"name": self._display_hotspot_name(topic),
21962223
"canonical_topic": topic,
21972224
"summary": self._build_hotspot_summary(topic, summary),
21982225
"route": route,
21992226
"stocks": stocks[:30],
2227+
"leader_stocks": stocks[:30],
22002228
"stock_count": len(stocks),
22012229
"source_errors": [],
2202-
}
2230+
})
22032231

22042232
def _fetch_board_changes(self) -> Any:
22052233
import pandas as pd

tests/test_alphasift_api.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,8 +860,40 @@ def get_hotspot_detail(topic: str, **kwargs: Any) -> Dict[str, Any]:
860860
self.assertEqual(payload["missing_fields"], ["live_stocks"])
861861
self.assertEqual(payload["source_errors"], ["none: no live detail rows"])
862862
self.assertEqual(payload["stocks"][0]["source"], "last_good_cache.leader_stocks")
863+
self.assertEqual(payload["leader_stocks"][0]["source"], "last_good_cache.leader_stocks")
863864
self.assertEqual(payload["route"][0]["title"], "AI算力催化")
864865

866+
def test_hotspot_detail_backfills_stocks_from_contract_leader_stocks(self) -> None:
867+
config = self._config(enabled=True)
868+
869+
def get_hotspot_detail(topic: str, **_kwargs: Any) -> Dict[str, Any]:
870+
return {
871+
"summary": {"topic": topic, "name": "算力"},
872+
"leader_stocks": [{
873+
"code": "300001",
874+
"name": "算力龙头",
875+
"role": "核心龙头",
876+
"source": "last_good_cache.leader_stocks",
877+
}],
878+
"route": [{"title": "盘中发酵", "description": "真实新闻催化", "source": "news"}],
879+
}
880+
881+
provider = alphasift_service.DsaEastMoneyHotspotProvider()
882+
provider.hotspot_detail = MagicMock(side_effect=AssertionError("provider route fallback should not be used"))
883+
with (
884+
patch("src.services.alphasift_service._get_alphasift_status_snapshot", return_value=({}, True, {})),
885+
patch("src.services.alphasift_service._resolve_hotspot_provider", return_value=("akshare", provider)),
886+
patch(
887+
"src.services.alphasift_service._import_alphasift_hotspot",
888+
return_value=SimpleNamespace(get_hotspot_detail=get_hotspot_detail),
889+
),
890+
):
891+
payload = self._hotspot_detail(config=config, provider="akshare", topic="AI算力")
892+
893+
self.assertEqual(payload["stocks"][0]["name"], "算力龙头")
894+
self.assertEqual(payload["leader_stocks"][0]["name"], "算力龙头")
895+
provider.hotspot_detail.assert_not_called()
896+
865897
def test_hotspot_detail_uses_dsa_detail_cache_after_first_fetch(self) -> None:
866898
config = self._config(enabled=True)
867899
provider = alphasift_service.DsaEastMoneyHotspotProvider()
@@ -889,6 +921,7 @@ def test_hotspot_detail_uses_dsa_detail_cache_after_first_fetch(self) -> None:
889921
self.assertFalse(first.get("cache_used", False))
890922
self.assertTrue(second["cache_used"])
891923
self.assertEqual(second["stocks"][0]["name"], "盛龙股份")
924+
self.assertEqual(second["leader_stocks"][0]["name"], "盛龙股份")
892925

893926
def test_hotspot_detail_refresh_bypasses_dsa_detail_cache(self) -> None:
894927
config = self._config(enabled=True)
@@ -1118,6 +1151,7 @@ def hotspot_detail(self, topic: str) -> Dict[str, Any]:
11181151
self.assertEqual(payload["topic"], "玻璃基板")
11191152
self.assertEqual(payload["route"][0]["title"], "盘中发酵")
11201153
self.assertEqual(payload["stocks"][0]["name"], "戈碧迦")
1154+
self.assertEqual(payload["leader_stocks"][0]["name"], "戈碧迦")
11211155

11221156
def test_hotspot_detail_route_accepts_slash_containing_topic(self) -> None:
11231157
config = self._config(enabled=True)

0 commit comments

Comments
 (0)