fix: support JP/KR web autocomplete (#1721)

This commit is contained in:
mumu
2026-06-19 13:34:21 +08:00
committed by GitHub
parent 9ea2dab7ad
commit ec6c8fbbcf
12 changed files with 278 additions and 16 deletions

View File

@@ -31616,5 +31616,25 @@
["ZZHGF","ZZHGF","ZHONGAN ONL.P&C IN (OTC)","ZHONGAN ONL.P&C IN (OTC)","ZHONGAN ONL.P&C IN (OTC)",[],"US","stock",true,100], ["ZZHGF","ZZHGF","ZHONGAN ONL.P&C IN (OTC)","ZHONGAN ONL.P&C IN (OTC)","ZHONGAN ONL.P&C IN (OTC)",[],"US","stock",true,100],
["ZZHGY","ZZHGY","ZHONGAN ONL.P & C UNSPONSORED ADR","ZHONGAN ONL.P & C UNSPONSORED ADR","ZHONGAN ONL.P & C UNSPONSORED ADR",[],"US","stock",true,100], ["ZZHGY","ZZHGY","ZHONGAN ONL.P & C UNSPONSORED ADR","ZHONGAN ONL.P & C UNSPONSORED ADR","ZHONGAN ONL.P & C UNSPONSORED ADR",[],"US","stock",true,100],
["ZZLL","ZZLL","ZZLL INFO.TECH.","ZZLL INFO.TECH.","ZZLL INFO.TECH.",[],"US","stock",true,100], ["ZZLL","ZZLL","ZZLL INFO.TECH.","ZZLL INFO.TECH.","ZZLL INFO.TECH.",[],"US","stock",true,100],
["ZZZOF","ZZZOF","ZINC ONE RESOURCES (OTC)","ZINC ONE RESOURCES (OTC)","ZINC ONE RESOURCES (OTC)",[],"US","stock",true,100] ["ZZZOF","ZZZOF","ZINC ONE RESOURCES (OTC)","ZINC ONE RESOURCES (OTC)","ZINC ONE RESOURCES (OTC)",[],"US","stock",true,100],
["000660.KS","000660.KS","SK海力士","SKhailishi","SKhls",["SK Hynix","海力士","하이닉스"],"KR","stock",true,100],
["005380.KS","005380.KS","现代汽车","xiandaiqiche","xdqc",["Hyundai","Hyundai Motor","现代","현대차"],"KR","stock",true,100],
["005930.KS","005930.KS","三星电子","sanxingdianzi","sxdz",["Samsung","Samsung Electronics","三星","삼성전자"],"KR","stock",true,100],
["006400.KS","006400.KS","三星SDI","sanxingSDI","sxSDI",["Samsung SDI"],"KR","stock",true,100],
["035420.KS","035420.KS","NAVER","AVER","AVER",["Naver","네이버"],"KR","stock",true,100],
["035720.KQ","035720.KQ","Kakao","Kakao","Kakao",["可可","카카오"],"KR","stock",true,100],
["051910.KS","051910.KS","LG化学","LGhuaxue","LGhx",["LG Chem"],"KR","stock",true,100],
["091990.KQ","091990.KQ","赛尔群医疗","saierqunyiliao","seqyl",["Celltrion Healthcare","Celltrion","셀트리온헬스케어"],"KR","stock",true,100],
["247540.KQ","247540.KQ","Ecopro BM","Ecopro BM","Ecopro BM",["ECOPRO BM","에코프로비엠"],"KR","stock",true,100],
["373220.KS","373220.KS","LG新能源","LGxinnengyuan","LGxny",["LG Energy Solution","LGES"],"KR","stock",true,100],
["6501.T","6501.T","日立","rili","rl",["Hitachi","日立制作所","日立製作所"],"JP","stock",true,100],
["6758.T","6758.T","索尼集团","suonijituan","snjt",["Sony","Sony Group","索尼","ソニー"],"JP","stock",true,100],
["6861.T","6861.T","基恩士","jienshi","jes",["Keyence","キーエンス"],"JP","stock",true,100],
["7203.T","7203.T","丰田汽车","fengtianqiche","ftqc",["Toyota","Toyota Motor","丰田","トヨタ"],"JP","stock",true,100],
["7974.T","7974.T","任天堂","rentiantang","rtt",["Nintendo"],"JP","stock",true,100],
["8035.T","8035.T","东京电子","dongjingdianzi","djdz",["Tokyo Electron","東京エレクトロン"],"JP","stock",true,100],
["8306.T","8306.T","三菱日联金融集团","sanlingrilianjinrongjituan","slrljrjt",["MUFG","Mitsubishi UFJ","三菱日联","三菱UFJ"],"JP","stock",true,100],
["9432.T","9432.T","日本电信电话","ribendianxindianhua","rbdxdh",["NTT","Nippon Telegraph and Telephone"],"JP","stock",true,100],
["9983.T","9983.T","迅销","xunxiao","xx",["Fast Retailing","Uniqlo","优衣库"],"JP","stock",true,100],
["9984.T","9984.T","软银集团","ruanyinjituan","ryjt",["SoftBank","SoftBank Group","软银","ソフトバンク"],"JP","stock",true,100]
] ]

View File

@@ -4,7 +4,7 @@
* Stock data index for autocomplete functionality * Stock data index for autocomplete functionality
*/ */
export type Market = 'CN' | 'HK' | 'US' | 'INDEX' | 'ETF' | 'BSE'; export type Market = 'CN' | 'HK' | 'US' | 'JP' | 'KR' | 'INDEX' | 'ETF' | 'BSE';
export type AssetType = 'stock' | 'index' | 'etf'; export type AssetType = 'stock' | 'index' | 'etf';
/** /**

View File

@@ -67,6 +67,42 @@ const mockIndex: StockIndexItem[] = [
active: true, active: true,
popularity: 98, popularity: 98,
}, },
{
canonicalCode: "7203.T",
displayCode: "7203.T",
nameZh: "丰田汽车",
pinyinFull: "fengtianqiche",
pinyinAbbr: "ftqc",
aliases: ["Toyota", "Toyota Motor", "丰田"],
market: "JP",
assetType: "stock",
active: true,
popularity: 97,
},
{
canonicalCode: "005930.KS",
displayCode: "005930.KS",
nameZh: "三星电子",
pinyinFull: "sanxingdianzi",
pinyinAbbr: "sxdz",
aliases: ["Samsung", "Samsung Electronics", "三星"],
market: "KR",
assetType: "stock",
active: true,
popularity: 97,
},
{
canonicalCode: "035720.KQ",
displayCode: "035720.KQ",
nameZh: "Kakao",
pinyinFull: "Kakao",
pinyinAbbr: "Kakao",
aliases: ["Kakao", "可可"],
market: "KR",
assetType: "stock",
active: true,
popularity: 92,
},
{ {
canonicalCode: "600000.SH", canonicalCode: "600000.SH",
displayCode: "600000", displayCode: "600000",
@@ -213,6 +249,40 @@ describe('searchStocks', () => {
expect(results[0].market).toBe('HK'); expect(results[0].market).toBe('HK');
}); });
test('日股 Yahoo 后缀代码匹配', () => {
const results = searchStocks('7203.T', mockIndex);
expect(results).toHaveLength(1);
expect(results[0].canonicalCode).toBe('7203.T');
expect(results[0].market).toBe('JP');
});
test('日股英文别名匹配', () => {
const results = searchStocks('Toyota', mockIndex);
expect(results).toHaveLength(1);
expect(results[0].canonicalCode).toBe('7203.T');
expect(results[0].matchField).toBe('alias');
});
test('韩股 KOSPI Yahoo 后缀代码匹配', () => {
const results = searchStocks('005930.KS', mockIndex);
expect(results).toHaveLength(1);
expect(results[0].canonicalCode).toBe('005930.KS');
expect(results[0].market).toBe('KR');
});
test('韩股 KOSDAQ Yahoo 后缀代码匹配', () => {
const results = searchStocks('035720.KQ', mockIndex);
expect(results).toHaveLength(1);
expect(results[0].canonicalCode).toBe('035720.KQ');
expect(results[0].market).toBe('KR');
});
test('韩股中文别名匹配', () => {
const results = searchStocks('三星', mockIndex);
expect(results).toHaveLength(1);
expect(results[0].canonicalCode).toBe('005930.KS');
});
describe('Edge case tests', () => { describe('Edge case tests', () => {
test('special character query', () => { test('special character query', () => {
const results = searchStocks('@#$%', mockIndex); const results = searchStocks('@#$%', mockIndex);

View File

@@ -0,0 +1,30 @@
import { describe, expect, test } from 'vitest';
import {
isObviouslyInvalidStockQuery,
looksLikeStockCode,
validateStockCode,
} from '../validation';
describe('stock code validation', () => {
test.each([
['7203.T', '7203.T'],
['6758.t', '6758.T'],
['005930.KS', '005930.KS'],
['035720.kq', '035720.KQ'],
])('accepts JP/KR Yahoo suffix code %s', (input, normalized) => {
expect(looksLikeStockCode(input)).toBe(true);
expect(validateStockCode(input)).toEqual({
valid: true,
normalized,
});
expect(isObviouslyInvalidStockQuery(input)).toBe(false);
});
test.each(['7203', '005930.K', '035720.KRX'])(
'does not treat ambiguous JP/KR-like query %s as a valid suffix code',
(input) => {
const result = validateStockCode(input);
expect(result.valid).toBe(false);
}
);
});

View File

@@ -13,6 +13,8 @@ const STOCK_CODE_PATTERNS = [
/^\d{5}$/, // HK code without prefix /^\d{5}$/, // HK code without prefix
/^HK\d{1,5}$/, // HK-prefixed code, for example HK00700 /^HK\d{1,5}$/, // HK-prefixed code, for example HK00700
/^\d{1,5}\.HK$/, // HK suffix format, for example 00700.HK /^\d{1,5}\.HK$/, // HK suffix format, for example 00700.HK
/^\d{4,5}\.T$/, // Japan Yahoo suffix format, for example 7203.T
/^\d{6}\.(KS|KQ)$/, // Korea Yahoo suffix format, for example 005930.KS or 035720.KQ
/^[A-Z]{1,5}(?:\.(?:US|[A-Z]))?$/, // Common US ticker format /^[A-Z]{1,5}(?:\.(?:US|[A-Z]))?$/, // Common US ticker format
]; ];
@@ -25,7 +27,7 @@ export const looksLikeStockCode = (value: string): boolean => {
}; };
/** /**
* Validate common A-share, HK, and US stock code formats. * Validate common A-share, HK, US, JP, and KR stock code formats.
*/ */
export const validateStockCode = (value: string): ValidationResult => { export const validateStockCode = (value: string): ValidationResult => {
const normalized = value.trim().toUpperCase(); const normalized = value.trim().toUpperCase();

View File

@@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。 - [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。
- [修复] AlphaSift 热点详情兼容 `leader_stocks``stocks` 双字段,避免旧合约或缓存只提供其中一个字段时概念股详情报缺失字段。 - [修复] AlphaSift 热点详情兼容 `leader_stocks``stocks` 双字段,避免旧合约或缓存只提供其中一个字段时概念股详情报缺失字段。
- [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。 - [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。
- [修复] #1718 Web 股票输入与自动补全补齐日韩 Yahoo 后缀代码校验和常用日韩股票索引,手输或搜索 `7203.T``005930.KS``035720.KQ` 可正常提交分析。
## [3.22.0] - 2026-06-13 ## [3.22.0] - 2026-06-13

View File

@@ -2,7 +2,7 @@
## 日本/韩国个股 suffix-only MVPIssue #1718 ## 日本/韩国个股 suffix-only MVPIssue #1718
当前阶段支持手动输入日本、韩国股票的 Yahoo Finance 后缀代码,进入既有个股分析、历史保存和基础报告展示链路。 当前阶段支持手动输入日本、韩国股票的 Yahoo Finance 后缀代码,进入既有个股分析、历史保存和基础报告展示链路。Web 自动补全内置一批常用日股/韩股种子索引,支持按 suffix 代码、中英文名称或常用别名搜索。
支持格式: 支持格式:
@@ -22,10 +22,10 @@
- 不承诺实时行情Yahoo Finance 数据可能延迟或字段缺失。 - 不承诺实时行情Yahoo Finance 数据可能延迟或字段缺失。
- 不承诺完整基本面、行业/板块、市场宽度、涨跌家数或日韩大盘复盘。 - 不承诺完整基本面、行业/板块、市场宽度、涨跌家数或日韩大盘复盘。
-包含日韩股票列表自动补全;没有索引前仅支持手动输入 suffix 代码。 -承诺完整日韩全市场股票列表Web 自动补全当前仅覆盖仓内种子索引中的常用标的,未命中时仍可手动输入 suffix 代码。
- 不补齐 Portfolio 的 JPY/KRW 汇率、成本、市值完整口径;相关字段仅放开市场类型以避免前后端校验拒绝。 - 不补齐 Portfolio 的 JPY/KRW 汇率、成本、市值完整口径;相关字段仅放开市场类型以避免前后端校验拒绝。
回滚方式:移除 `jp/kr` 市场识别、交易日历注册、YFinance 路由扩展Web/API 类型放行,并删除本文档中的能力声明。 回滚方式:移除 `jp/kr` 市场识别、交易日历注册、YFinance 路由扩展Web/API 类型放行`scripts/stock_index_seeds/` 日韩种子索引,并删除本文档中的能力声明。
配置兼容性: 配置兼容性:

View File

@@ -5,6 +5,7 @@ Generate Stock Index from CSV File
Input: Input:
- Tushare format: data/stock_list_{a,hk,us}.csv - Tushare format: data/stock_list_{a,hk,us}.csv
- Seed format: scripts/stock_index_seeds/stock_list_{jp,kr}.csv
- AkShare format: logs/stock_basic_*.csv - AkShare format: logs/stock_basic_*.csv
Output: apps/dsa-web/public/stocks.index.json Output: apps/dsa-web/public/stocks.index.json
@@ -84,7 +85,7 @@ def load_csv_data(csv_path: Path) -> List[Dict[str, Any]]:
def load_tushare_data(data_dir: Path) -> List[Dict[str, Any]]: def load_tushare_data(data_dir: Path) -> List[Dict[str, Any]]:
""" """
从 Tushare CSV 文件加载三个市场股票数据 从 Tushare CSV 文件加载市场股票数据
Args: Args:
data_dir: 数据目录路径 data_dir: 数据目录路径
@@ -93,10 +94,22 @@ def load_tushare_data(data_dir: Path) -> List[Dict[str, Any]]:
合并后的股票列表 合并后的股票列表
""" """
all_stocks = [] all_stocks = []
seed_dir = Path(__file__).parent / 'stock_index_seeds'
default_data_dir = Path(__file__).parent.parent / 'data'
use_seed_fallback = data_dir.resolve() == default_data_dir.resolve()
def _csv_path(file_name: str) -> Path:
data_path = data_dir / file_name
if data_path.exists() or not use_seed_fallback:
return data_path
return seed_dir / file_name
market_files = { market_files = {
'CN': data_dir / 'stock_list_a.csv', 'CN': data_dir / 'stock_list_a.csv',
'HK': data_dir / 'stock_list_hk.csv', 'HK': data_dir / 'stock_list_hk.csv',
'US': data_dir / 'stock_list_us.csv', 'US': data_dir / 'stock_list_us.csv',
'JP': _csv_path('stock_list_jp.csv'),
'KR': _csv_path('stock_list_kr.csv'),
} }
for market_name, csv_file in market_files.items(): for market_name, csv_file in market_files.items():
@@ -290,6 +303,7 @@ def extract_symbol_from_ts_code(ts_code: str, market: str) -> Optional[str]:
- A股000001.SZ → 000001 - A股000001.SZ → 000001
- 港股00700.HK → 00700 - 港股00700.HK → 00700
- 美股AAPL → AAPL - 美股AAPL → AAPL
- 日股/韩股7203.T / 005930.KS → 保留后缀,避免与其他市场裸代码冲突
Args: Args:
ts_code: TS代码 ts_code: TS代码
@@ -301,8 +315,8 @@ def extract_symbol_from_ts_code(ts_code: str, market: str) -> Optional[str]:
if not ts_code: if not ts_code:
return None return None
if market == 'US': if market in {'US', 'JP', 'KR'}:
# 美股无后缀,直接返回 # 美股常见 class/share 后缀、日韩 Yahoo 后缀都是代码身份的一部分。
return ts_code return ts_code
if '.' in ts_code: if '.' in ts_code:
@@ -316,7 +330,7 @@ def get_stock_name(row: Dict[str, str], market: str) -> Optional[str]:
""" """
获取股票名称 获取股票名称
- A股/港股:使用 name 字段 - A股/港股/日股/韩股:使用 name 字段
- 美股:使用 enname 字段(英文名称) - 美股:使用 enname 字段(英文名称)
Args: Args:
@@ -337,6 +351,20 @@ def get_stock_name(row: Dict[str, str], market: str) -> Optional[str]:
return name if name else None return name if name else None
def parse_aliases(row: Dict[str, str]) -> List[str]:
"""Parse optional seed aliases from a CSV row."""
raw_aliases = (row.get('aliases') or row.get('alias') or '').strip()
if not raw_aliases:
return []
aliases: List[str] = []
for alias in re.split(r'[|;,,、]+', raw_aliases):
normalized = unicodedata.normalize('NFKC', alias).strip()
if normalized and normalized not in aliases:
aliases.append(normalized)
return aliases
def parse_stock_row(row: Dict[str, str], preferred_market: Optional[str] = None) -> Optional[Dict[str, Any]]: def parse_stock_row(row: Dict[str, str], preferred_market: Optional[str] = None) -> Optional[Dict[str, Any]]:
""" """
解析单行股票数据 解析单行股票数据
@@ -387,6 +415,7 @@ def parse_stock_row(row: Dict[str, str], preferred_market: Optional[str] = None)
'symbol': display_code, 'symbol': display_code,
'name': name, 'name': name,
'market': market, 'market': market,
'aliases': parse_aliases(row),
} }
@@ -395,10 +424,10 @@ def determine_market(ts_code: str) -> str:
Determine market based on code Determine market based on code
Args: Args:
ts_code: Trading code (e.g., 000001.SZ, AAPL, BRK.B, GOOG.A) ts_code: Trading code (e.g., 000001.SZ, AAPL, BRK.B, 7203.T, 005930.KS)
Returns: Returns:
Market code (CN, HK, US, BSE) Market code (CN, HK, US, BSE, JP, KR)
""" """
if '.' in ts_code: if '.' in ts_code:
# 有后缀的情况 # 有后缀的情况
@@ -410,6 +439,10 @@ def determine_market(ts_code: str) -> str:
return 'HK' return 'HK'
elif suffix == 'BJ': elif suffix == 'BJ':
return 'BSE' return 'BSE'
elif suffix == 'T':
return 'JP'
elif suffix in ['KS', 'KQ']:
return 'KR'
# 有后缀但不是中国市场后缀,检查是否为美股 # 有后缀但不是中国市场后缀,检查是否为美股
# 美股可能有点号后缀(如 BRK.B, GOOG.A, AAPL.U # 美股可能有点号后缀(如 BRK.B, GOOG.A, AAPL.U
prefix = ts_code.split('.')[0] prefix = ts_code.split('.')[0]
@@ -536,6 +569,9 @@ def build_stock_index(stocks: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
# Generate aliases. # Generate aliases.
aliases = generate_aliases(name, market) aliases = generate_aliases(name, market)
for alias in stock.get('aliases', []):
if alias != name and alias not in aliases:
aliases.append(alias)
index.append({ index.append({
"canonicalCode": ts_code, # Example: 000001.SZ, AAPL "canonicalCode": ts_code, # Example: 000001.SZ, AAPL

View File

@@ -4,7 +4,7 @@
Default flow: Default flow:
1. Fetch Tushare stock lists into ``data/`` with ``--a-rk`` for A-share name correction. 1. Fetch Tushare stock lists into ``data/`` with ``--a-rk`` for A-share name correction.
2. Generate ``apps/dsa-web/public/stocks.index.json`` from CSV. 2. Generate ``apps/dsa-web/public/stocks.index.json`` from CSV plus JP/KR seed rows.
3. Copy the generated index to ``static/stocks.index.json`` for backend use. 3. Copy the generated index to ``static/stocks.index.json`` for backend use.
""" """

View File

@@ -0,0 +1,11 @@
ts_code,symbol,name,enname,aliases
7203.T,7203.T,丰田汽车,Toyota Motor Corporation,Toyota|Toyota Motor|丰田|トヨタ
6758.T,6758.T,索尼集团,Sony Group Corporation,Sony|Sony Group|索尼|ソニー
9984.T,9984.T,软银集团,SoftBank Group Corp.,SoftBank|SoftBank Group|软银|ソフトバンク
8306.T,8306.T,三菱日联金融集团,Mitsubishi UFJ Financial Group,MUFG|Mitsubishi UFJ|三菱日联|三菱UFJ
6861.T,6861.T,基恩士,Keyence Corporation,Keyence|キーエンス
6501.T,6501.T,日立,Hitachi Ltd.,Hitachi|日立制作所|日立製作所
7974.T,7974.T,任天堂,Nintendo Co. Ltd.,Nintendo|任天堂
8035.T,8035.T,东京电子,Tokyo Electron Limited,Tokyo Electron|東京エレクトロン
9432.T,9432.T,日本电信电话,Nippon Telegraph and Telephone Corporation,NTT|Nippon Telegraph and Telephone|日本电信电话
9983.T,9983.T,迅销,Fast Retailing Co. Ltd.,Fast Retailing|Uniqlo|优衣库|迅销
1 ts_code symbol name enname aliases
2 7203.T 7203.T 丰田汽车 Toyota Motor Corporation Toyota|Toyota Motor|丰田|トヨタ
3 6758.T 6758.T 索尼集团 Sony Group Corporation Sony|Sony Group|索尼|ソニー
4 9984.T 9984.T 软银集团 SoftBank Group Corp. SoftBank|SoftBank Group|软银|ソフトバンク
5 8306.T 8306.T 三菱日联金融集团 Mitsubishi UFJ Financial Group MUFG|Mitsubishi UFJ|三菱日联|三菱UFJ
6 6861.T 6861.T 基恩士 Keyence Corporation Keyence|キーエンス
7 6501.T 6501.T 日立 Hitachi Ltd. Hitachi|日立制作所|日立製作所
8 7974.T 7974.T 任天堂 Nintendo Co. Ltd. Nintendo|任天堂
9 8035.T 8035.T 东京电子 Tokyo Electron Limited Tokyo Electron|東京エレクトロン
10 9432.T 9432.T 日本电信电话 Nippon Telegraph and Telephone Corporation NTT|Nippon Telegraph and Telephone|日本电信电话
11 9983.T 9983.T 迅销 Fast Retailing Co. Ltd. Fast Retailing|Uniqlo|优衣库|迅销

View File

@@ -0,0 +1,11 @@
ts_code,symbol,name,enname,aliases
005930.KS,005930.KS,三星电子,Samsung Electronics Co. Ltd.,Samsung|Samsung Electronics|三星|삼성전자
000660.KS,000660.KS,SK海力士,SK hynix Inc.,SK Hynix|海力士|하이닉스
373220.KS,373220.KS,LG新能源,LG Energy Solution Ltd.,LG Energy Solution|LGES|LG新能源
005380.KS,005380.KS,现代汽车,Hyundai Motor Company,Hyundai|Hyundai Motor|现代|현대차
035420.KS,035420.KS,NAVER,NAVER Corporation,Naver|NAVER|네이버
051910.KS,051910.KS,LG化学,LG Chem Ltd.,LG Chem|LG化学
006400.KS,006400.KS,三星SDI,Samsung SDI Co. Ltd.,Samsung SDI|三星SDI
035720.KQ,035720.KQ,Kakao,Kakao Corp.,Kakao|可可|카카오
247540.KQ,247540.KQ,Ecopro BM,Ecopro BM Co. Ltd.,Ecopro BM|ECOPRO BM|에코프로비엠
091990.KQ,091990.KQ,赛尔群医疗,Celltrion Healthcare Co. Ltd.,Celltrion Healthcare|Celltrion|셀트리온헬스케어
1 ts_code symbol name enname aliases
2 005930.KS 005930.KS 三星电子 Samsung Electronics Co. Ltd. Samsung|Samsung Electronics|三星|삼성전자
3 000660.KS 000660.KS SK海力士 SK hynix Inc. SK Hynix|海力士|하이닉스
4 373220.KS 373220.KS LG新能源 LG Energy Solution Ltd. LG Energy Solution|LGES|LG新能源
5 005380.KS 005380.KS 现代汽车 Hyundai Motor Company Hyundai|Hyundai Motor|现代|현대차
6 035420.KS 035420.KS NAVER NAVER Corporation Naver|NAVER|네이버
7 051910.KS 051910.KS LG化学 LG Chem Ltd. LG Chem|LG化学
8 006400.KS 006400.KS 三星SDI Samsung SDI Co. Ltd. Samsung SDI|三星SDI
9 035720.KQ 035720.KQ Kakao Kakao Corp. Kakao|可可|카카오
10 247540.KQ 247540.KQ Ecopro BM Ecopro BM Co. Ltd. Ecopro BM|ECOPRO BM|에코프로비엠
11 091990.KQ 091990.KQ 赛尔群医疗 Celltrion Healthcare Co. Ltd. Celltrion Healthcare|Celltrion|셀트리온헬스케어

View File

@@ -55,6 +55,16 @@ class TestExtractSymbol:
result = extract_symbol_from_ts_code("AAPL", "US") result = extract_symbol_from_ts_code("AAPL", "US")
assert result == "AAPL" assert result == "AAPL"
def test_jp_stock_preserves_suffix(self):
"""测试日股保留 Yahoo 后缀以避免裸代码冲突"""
result = extract_symbol_from_ts_code("7203.T", "JP")
assert result == "7203.T"
def test_kr_stock_preserves_suffix(self):
"""测试韩股保留 Yahoo 后缀以避免裸代码冲突"""
result = extract_symbol_from_ts_code("005930.KS", "KR")
assert result == "005930.KS"
def test_empty_ts_code(self): def test_empty_ts_code(self):
"""测试空 ts_code""" """测试空 ts_code"""
result = extract_symbol_from_ts_code("", "CN") result = extract_symbol_from_ts_code("", "CN")
@@ -114,6 +124,21 @@ class TestDetermineMarket:
result = determine_market("AAPL.U") result = determine_market("AAPL.U")
assert result == "US" assert result == "US"
def test_jp_stock_with_yahoo_suffix(self):
"""测试日股 Yahoo 后缀"""
result = determine_market("7203.T")
assert result == "JP"
def test_kr_kospi_stock_with_yahoo_suffix(self):
"""测试韩股 KOSPI Yahoo 后缀"""
result = determine_market("005930.KS")
assert result == "KR"
def test_kr_kosdaq_stock_with_yahoo_suffix(self):
"""测试韩股 KOSDAQ Yahoo 后缀"""
result = determine_market("035720.KQ")
assert result == "KR"
class TestGetStockName: class TestGetStockName:
"""测试股票名称获取函数""" """测试股票名称获取函数"""
@@ -214,6 +239,38 @@ class TestDataCleaning:
assert result['name'] == "BERKSHIRE HATHAWAY 'B'" assert result['name'] == "BERKSHIRE HATHAWAY 'B'"
assert result['market'] == 'US' assert result['market'] == 'US'
def test_valid_jp_stock_with_seed_aliases(self):
"""测试有效的日股种子记录"""
row = {
'ts_code': '7203.T',
'name': '丰田汽车',
'enname': 'Toyota Motor Corporation',
'aliases': 'Toyota|Toyota Motor|丰田'
}
result = parse_stock_row(row, 'JP')
assert result is not None
assert result['ts_code'] == '7203.T'
assert result['symbol'] == '7203.T'
assert result['name'] == '丰田汽车'
assert result['market'] == 'JP'
assert result['aliases'] == ['Toyota', 'Toyota Motor', '丰田']
def test_valid_kr_stock_with_seed_aliases(self):
"""测试有效的韩股种子记录"""
row = {
'ts_code': '005930.KS',
'name': '三星电子',
'enname': 'Samsung Electronics',
'aliases': 'Samsung|Samsung Electronics|三星'
}
result = parse_stock_row(row, 'KR')
assert result is not None
assert result['ts_code'] == '005930.KS'
assert result['symbol'] == '005930.KS'
assert result['name'] == '三星电子'
assert result['market'] == 'KR'
assert result['aliases'] == ['Samsung', 'Samsung Electronics', '三星']
def test_us_dummy_filtered(self): def test_us_dummy_filtered(self):
"""测试美股 DUMMY 记录被过滤""" """测试美股 DUMMY 记录被过滤"""
row = { row = {
@@ -430,23 +487,47 @@ class TestIntegration:
'enname': 'Apple Inc.' 'enname': 'Apple Inc.'
}) })
jp_csv = tmp_path / 'stock_list_jp.csv'
with open(jp_csv, 'w', encoding='utf-8-sig', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['ts_code', 'name', 'enname', 'aliases'])
writer.writeheader()
writer.writerow({
'ts_code': '7203.T',
'name': '丰田汽车',
'enname': 'Toyota Motor Corporation',
'aliases': 'Toyota|丰田'
})
kr_csv = tmp_path / 'stock_list_kr.csv'
with open(kr_csv, 'w', encoding='utf-8-sig', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['ts_code', 'name', 'enname', 'aliases'])
writer.writeheader()
writer.writerow({
'ts_code': '005930.KS',
'name': '三星电子',
'enname': 'Samsung Electronics',
'aliases': 'Samsung|三星'
})
# 加载数据 # 加载数据
stocks = load_tushare_data(tmp_path) stocks = load_tushare_data(tmp_path)
# 验证数据 # 验证数据
assert len(stocks) == 3 assert len(stocks) == 5
# 构建索引 # 构建索引
index = build_stock_index(stocks) index = build_stock_index(stocks)
# 验证索引 # 验证索引
assert len(index) == 3 assert len(index) == 5
assert next(item for item in index if item['canonicalCode'] == '7203.T')['aliases'] == ['Toyota', '丰田']
assert next(item for item in index if item['canonicalCode'] == '005930.KS')['aliases'] == ['Samsung', '三星']
# 压缩索引 # 压缩索引
compressed = compress_index(index) compressed = compress_index(index)
# 验证压缩 # 验证压缩
assert len(compressed) == 3 assert len(compressed) == 5
# 验证字段数量 # 验证字段数量
for item in compressed: for item in compressed: