fix: exclude ambiguous profile history aliases

This commit is contained in:
ZhuLinsen
2026-08-29 21:28:13 +08:00
parent 1169f4e45a
commit e468429bd0
3 changed files with 22 additions and 2 deletions

View File

@@ -28,7 +28,7 @@
## 数据来源与边界
- quote/history复用 `StockService`,不新增数据获取器。
- research复用 `HistoryService`#2291`ResearchArtifact` builder;本 PR 因此堆叠在 #2291。报告查询把档案入口已经解析出的 market hint 传到 `HistoryService` 的候选生成层A 股 canonical code 只展开对应 `SH` / `SZ` / `BJ` 代码,日韩台查询保留交易所后缀及其他无歧义别名但不展开可能命中其他市场历史记录的裸数字代码市场限定后候选为空时直接返回空分页不会退化成无过滤查询。artifact 同时复用历史详情的上下文、raw result 和独立基本面快照 fallback保留财报、分红与市场结构专项证据及其 source count。
- research复用 `HistoryService`#2291`ResearchArtifact` builder。报告查询把档案入口已经解析出的 market hint 传到 `HistoryService` 的候选生成层A 股 canonical code 只保留能在无提示解析时仍确认属于 A 股的裸数字别名;例如 `600519` 可继续兼容旧记录,而与韩股同形的 `SZ000660` 不会查询裸 `000660`日韩台查询保留交易所后缀及其他无歧义别名但不展开可能命中其他市场历史记录的裸数字代码市场限定后候选为空时直接返回空分页不会退化成无过滤查询。artifact 同时复用历史详情的上下文、raw result 和独立基本面快照 fallback保留财报、分红与市场结构专项证据及其 source count。
- intelligence复用 `IntelligenceService` 的 symbol scope 查询,并兼容 canonical、交易所前后缀、港股前后缀、混合大小写及日韩台市场限定的裸数字历史别名无法无提示解析回当前市场的歧义裸码不会进入 `global` 查询。查询别名按 casefold identity 去重,由 repository 的 symbol-only 不区分大小写过滤兼容历史大小写,避免重复 count/select。任一别名/市场查询失败时块保持 partial limitation即使其余查询成功但为空也不误报为已确认无资讯。
- portfolio只读 `PortfolioRepository.list_cached_position_identities()`,并使用每条缓存持仓自己的 market 解析旧裸代码;只有 market 与档案身份一致时才算持有。不为了打开个股页触发实时估值或写 snapshot所以状态固定为 `partial` 并包含 `cached_positions_only`
- monitors复用 `AlertService.list_rules()`,以不区分大小写的 symbol 过滤分页汇总并去重 canonical code 及等价历史别名下的 `single_symbol` 规则。由于现有告警目标没有独立 market 字段,日韩台档案不会查询可能与 A/HK 同形的裸数字别名,避免跨市场规则误归属。
@@ -37,4 +37,4 @@
## 回滚
Revert 本 PR 即可移除 profile schema、service、endpoint、测试和文档。没有数据库迁移、配置变更或数据清理步骤。#2291 尚未合入,本 PR 需与其一起保持堆叠或在合入后把 base 改回 `main`
Revert 本 PR 即可移除 profile schema、service、endpoint、测试和文档。没有数据库迁移、配置变更或数据清理步骤。

View File

@@ -127,6 +127,11 @@ class HistoryService:
if identity is None or identity.market != trusted_market:
return []
for candidate in identity.code_candidates:
candidate_text = str(candidate or "").strip()
if candidate_text.isdigit():
unhinted_identity = resolve_daily_stock_identity(candidate_text)
if unhinted_identity is None or unhinted_identity.market != trusted_market:
continue
add(candidate)
return candidates

View File

@@ -109,8 +109,23 @@ class TestHistoryCsiCandidateConvergence(unittest.TestCase):
queried_codes = db.get_analysis_history_paginated.call_args.kwargs["code"]
self.assertIn("SZ000660", queried_codes)
self.assertIn("000660.SZ", queried_codes)
self.assertNotIn("000660", queried_codes)
self.assertNotIn("000660.KS", queried_codes)
def test_market_hint_keeps_unambiguous_same_market_bare_numeric_alias(self):
db = MagicMock()
db.get_analysis_history_paginated.return_value = ([], 0)
HistoryService(db).get_history_list(
stock_code="600519",
page=1,
limit=5,
market_hint="cn",
)
queried_codes = db.get_analysis_history_paginated.call_args.kwargs["code"]
self.assertIn("600519", queried_codes)
def test_empty_market_qualified_candidate_set_fails_closed(self):
db = MagicMock()