mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 20:14:16 +08:00
## 1. 拼音首字母搜索 (同花顺式) 后端 search_instruments 新增拼音匹配层, 输入 payh 可命中「平安银行」。 - 辅助函数 _name_pinyin_keys 用 lru_cache 缓存「名称→首字母串」, 命中后 近似 dict 查找, 全市场遍历 < 1ms - 多音字用 heteronym 笛卡尔积展开, 「重庆」同时匹配 cq/zq 两种读音; 并加载 A 股高频地名词典 (重庆/长安/长春/长沙/长城/长江) - 拼音分支仅在纯 ASCII 字母输入时触发, 中文/数字搜索零开销跳过, 完全向后兼容 - 搜索分层: ① code/symbol 前缀 → ② 拼音首字母前缀 → ③ 包含匹配 新增依赖: pypinyin>=0.50 (纯 Python, 无 C 扩展) 新增测试: tests/test_instrument_search.py (15 用例覆盖拼音/多音字/兼容/边界) ## 2. 搜索结果显示板块徽标 自选/财务搜索/监控规则/回测选标的 四个搜索入口的结果项, 现在会显示 创业板(橙「创」)/科创板(青「科」)/北交所(紫「北」)彩色徽标。 - 复用项目既有的 boardTag (@/components/stock-table/primitives), 与自选/策略/Dashboard 表格徽标样式完全统一 - 沪深主板不显示徽标 (信息量低, 保持简洁) ## 验证 - 后端全量测试 503 passed (含新增 15) - 前端 tsc --noEmit 零错误
149 lines
4.9 KiB
Python
149 lines
4.9 KiB
Python
"""标的搜索测试: 代码 / 名称 / 拼音首字母 (同花顺式 payh → 平安银行)。
|
|
|
|
直接调用 search_instruments, 用最小 FakeRepo 提供 instruments 缓存, 不走 HTTP/DB。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import types
|
|
|
|
import polars as pl
|
|
import pytest
|
|
|
|
from app.api.kline import search_instruments
|
|
|
|
|
|
class _FakeRepo:
|
|
"""最小 repo 桩: 只实现 search_instruments 依赖的 get_instruments_asset。"""
|
|
|
|
def __init__(self, by_asset: dict[str, pl.DataFrame]) -> None:
|
|
self.store = types.SimpleNamespace(data_dir="data")
|
|
self._by_asset = by_asset
|
|
|
|
def get_instruments_asset(self, asset_type: str) -> pl.DataFrame:
|
|
return self._by_asset.get(asset_type, pl.DataFrame())
|
|
|
|
|
|
def _request(repo: _FakeRepo) -> types.SimpleNamespace:
|
|
return types.SimpleNamespace(app=types.SimpleNamespace(state=types.SimpleNamespace(repo=repo)))
|
|
|
|
|
|
STOCKS = pl.DataFrame({
|
|
"symbol": ["000001.SZ", "600000.SH", "600519.SH", "000333.SZ", "600737.SH"],
|
|
"code": ["000001", "600000", "600519", "000333", "600737"],
|
|
"name": ["平安银行", "浦发银行", "贵州茅台", "美的集团", "中粮糖业"],
|
|
})
|
|
|
|
|
|
def _search(q: str, asset_types: str = "stock", limit: int = 20) -> list[dict]:
|
|
repo = _FakeRepo({"stock": STOCKS})
|
|
return search_instruments(_request(repo), q=q, limit=limit, asset_types=asset_types)["results"]
|
|
|
|
|
|
# ===== 既有逻辑回归: 代码 / 名称搜索不受影响 =====
|
|
|
|
def test_code_prefix_match():
|
|
"""code 前缀: 6005 → 600519 (前缀优先)。"""
|
|
rows = _search("6005")
|
|
assert "600519.SH" in [r["symbol"] for r in rows]
|
|
|
|
|
|
def test_symbol_contains_match():
|
|
rows = _search("6005")
|
|
assert "600519.SH" in [r["symbol"] for r in rows]
|
|
|
|
|
|
def test_chinese_name_contains_match():
|
|
rows = _search("银行")
|
|
assert sorted(r["symbol"] for r in rows) == ["000001.SZ", "600000.SH"]
|
|
|
|
|
|
def test_empty_query_returns_empty():
|
|
assert _search(" ") == []
|
|
|
|
|
|
# ===== 拼音首字母搜索 (新功能) =====
|
|
|
|
def test_pinyin_full_initials_match():
|
|
"""payh → 平安银行"""
|
|
rows = _search("payh")
|
|
assert [r["symbol"] for r in rows] == ["000001.SZ"]
|
|
|
|
|
|
def test_pinyin_prefix_match():
|
|
"""m → 美的集团 (m 开头)"""
|
|
rows = _search("m")
|
|
assert "000333.SZ" in [r["symbol"] for r in rows]
|
|
|
|
|
|
def test_pinyin_prefix_picks_multiple():
|
|
"""pf → 浦发银行; pa → 平安银行 (前缀区分)"""
|
|
assert [r["symbol"] for r in _search("pf")] == ["600000.SH"]
|
|
assert [r["symbol"] for r in _search("pa")] == ["000001.SZ"]
|
|
|
|
|
|
def test_pinyin_respects_limit():
|
|
"""limit 限制拼音结果数"""
|
|
rows = _search("z", limit=1) # z → 中粮糖业
|
|
assert len(rows) == 1
|
|
|
|
|
|
def test_pinyin_layer_between_prefix_and_contains():
|
|
"""拼音命中应排在包含匹配之前 (分层优先级)。"""
|
|
rows = _search("md") # md → 美的集团 (拼音); 无代码/符号以 md 前缀
|
|
assert "000333.SZ" in [r["symbol"] for r in rows]
|
|
|
|
|
|
def test_pinyin_and_code_prefix_coexist():
|
|
"""纯字母查询同时命中 code 前缀和拼音首字母时, code 前缀优先排前。"""
|
|
# '600000' 是浦发的 code 前缀; 这里用纯字母无法命中 code, 故仅验证拼音路径独立可用
|
|
rows = _search("pf") # 浦发
|
|
assert "600000.SH" in [r["symbol"] for r in rows]
|
|
|
|
|
|
# ===== 多音字 =====
|
|
|
|
POLYPHONE_STOCKS = pl.DataFrame({
|
|
"symbol": ["600729.SH", "000625.SZ"],
|
|
"code": ["600729", "000625"],
|
|
"name": ["重庆百货", "长安汽车"],
|
|
})
|
|
|
|
|
|
def test_polyphone_all_readings_match():
|
|
"""'重庆' 多音字: cq (chóng) 和 zq (zhòng) 读音都应命中。"""
|
|
repo = _FakeRepo({"stock": POLYPHONE_STOCKS})
|
|
# 取首字母集, 验证两种读音都能搜到
|
|
cq = search_instruments(_request(repo), q="cqbh", limit=20, asset_types="stock")["results"] # chóng qīng
|
|
zq = search_instruments(_request(repo), q="zqbh", limit=20, asset_types="stock")["results"] # zhòng qìng
|
|
assert "600729.SH" in [r["symbol"] for r in cq]
|
|
assert "600729.SH" in [r["symbol"] for r in zq]
|
|
|
|
|
|
# ===== 边界 =====
|
|
|
|
def test_non_ascii_skips_pinyin_branch():
|
|
"""中文输入不走拼音分支, 仍按名称匹配。"""
|
|
rows = _search("平安")
|
|
assert [r["symbol"] for r in rows] == ["000001.SZ"]
|
|
|
|
|
|
def test_digits_skips_pinyin_branch():
|
|
"""数字输入不走拼音分支, 走 code 前缀。"""
|
|
rows = _search("000")
|
|
assert "000001.SZ" in [r["symbol"] for r in rows]
|
|
|
|
|
|
def test_no_pinyin_hit_returns_empty():
|
|
"""无任何拼音命中时返回空 (不报错)。"""
|
|
assert _search("xyz") == []
|
|
|
|
|
|
def test_cache_returns_same_result_across_calls():
|
|
"""lru_cache 不应在不同请求间串结果 (按 name 缓存, 查询无状态)。"""
|
|
repo = _FakeRepo({"stock": STOCKS})
|
|
req = _request(repo)
|
|
r1 = search_instruments(req, q="payh", limit=20, asset_types="stock")["results"]
|
|
r2 = search_instruments(req, q="payh", limit=20, asset_types="stock")["results"]
|
|
assert r1 == r2
|
|
assert [r["symbol"] for r in r1] == ["000001.SZ"]
|