diff --git a/apps/dsa-web/public/stocks.index.json b/apps/dsa-web/public/stocks.index.json index 4c5ce56c4..589d92f94 100644 --- a/apps/dsa-web/public/stocks.index.json +++ b/apps/dsa-web/public/stocks.index.json @@ -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], ["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], -["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] ] diff --git a/apps/dsa-web/src/types/stockIndex.ts b/apps/dsa-web/src/types/stockIndex.ts index f978dbe56..9ab915e72 100644 --- a/apps/dsa-web/src/types/stockIndex.ts +++ b/apps/dsa-web/src/types/stockIndex.ts @@ -4,7 +4,7 @@ * 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'; /** diff --git a/apps/dsa-web/src/utils/__tests__/searchStocks.test.ts b/apps/dsa-web/src/utils/__tests__/searchStocks.test.ts index b7f31dfd7..7883b5779 100644 --- a/apps/dsa-web/src/utils/__tests__/searchStocks.test.ts +++ b/apps/dsa-web/src/utils/__tests__/searchStocks.test.ts @@ -67,6 +67,42 @@ const mockIndex: StockIndexItem[] = [ active: true, 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", displayCode: "600000", @@ -213,6 +249,40 @@ describe('searchStocks', () => { 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', () => { test('special character query', () => { const results = searchStocks('@#$%', mockIndex); diff --git a/apps/dsa-web/src/utils/__tests__/validation.test.ts b/apps/dsa-web/src/utils/__tests__/validation.test.ts new file mode 100644 index 000000000..f6034a928 --- /dev/null +++ b/apps/dsa-web/src/utils/__tests__/validation.test.ts @@ -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); + } + ); +}); diff --git a/apps/dsa-web/src/utils/validation.ts b/apps/dsa-web/src/utils/validation.ts index fb2a268d2..cc94ea310 100644 --- a/apps/dsa-web/src/utils/validation.ts +++ b/apps/dsa-web/src/utils/validation.ts @@ -13,6 +13,8 @@ const STOCK_CODE_PATTERNS = [ /^\d{5}$/, // HK code without prefix /^HK\d{1,5}$/, // HK-prefixed code, for example HK00700 /^\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 ]; @@ -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 => { const normalized = value.trim().toUpperCase(); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index cd95802fb..dc232a1d9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -56,6 +56,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/). - [新功能] 个股分析历史成功保存后会从最终报告 best-effort 提取 `DecisionSignal` 决策信号,复用现有信号去重、计划质量计算和脱敏契约。 - [修复] AlphaSift 热点详情兼容 `leader_stocks` 与 `stocks` 双字段,避免旧合约或缓存只提供其中一个字段时概念股详情报缺失字段。 - [改进] 问股页移动端策略选择改为默认收起的按钮入口,展开后仍可多选策略并在发送后自动收起,减少对对话内容的遮挡。 +- [修复] #1718 Web 股票输入与自动补全补齐日韩 Yahoo 后缀代码校验和常用日韩股票索引,手输或搜索 `7203.T`、`005930.KS`、`035720.KQ` 可正常提交分析。 ## [3.22.0] - 2026-06-13 diff --git a/docs/market-support.md b/docs/market-support.md index 968a3917a..19128ed42 100644 --- a/docs/market-support.md +++ b/docs/market-support.md @@ -2,7 +2,7 @@ ## 日本/韩国个股 suffix-only MVP(Issue #1718) -当前阶段支持手动输入日本、韩国股票的 Yahoo Finance 后缀代码,进入既有个股分析、历史保存和基础报告展示链路。 +当前阶段支持手动输入日本、韩国股票的 Yahoo Finance 后缀代码,进入既有个股分析、历史保存和基础报告展示链路。Web 自动补全内置一批常用日股/韩股种子索引,支持按 suffix 代码、中英文名称或常用别名搜索。 支持格式: @@ -22,10 +22,10 @@ - 不承诺实时行情;Yahoo Finance 数据可能延迟或字段缺失。 - 不承诺完整基本面、行业/板块、市场宽度、涨跌家数或日韩大盘复盘。 -- 不包含日韩股票列表自动补全;没有索引前仅支持手动输入 suffix 代码。 +- 不承诺完整日韩全市场股票列表;Web 自动补全当前仅覆盖仓内种子索引中的常用标的,未命中时仍可手动输入 suffix 代码。 - 不补齐 Portfolio 的 JPY/KRW 汇率、成本、市值完整口径;相关字段仅放开市场类型以避免前后端校验拒绝。 -回滚方式:移除 `jp/kr` 市场识别、交易日历注册、YFinance 路由扩展和 Web/API 类型放行,并删除本文档中的能力声明。 +回滚方式:移除 `jp/kr` 市场识别、交易日历注册、YFinance 路由扩展、Web/API 类型放行、`scripts/stock_index_seeds/` 日韩种子索引,并删除本文档中的能力声明。 配置兼容性: diff --git a/scripts/generate_index_from_csv.py b/scripts/generate_index_from_csv.py index 473b4b8ce..809b57918 100644 --- a/scripts/generate_index_from_csv.py +++ b/scripts/generate_index_from_csv.py @@ -5,6 +5,7 @@ Generate Stock Index from CSV File Input: - 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 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]]: """ - 从 Tushare CSV 文件加载三个市场的股票数据 + 从 Tushare CSV 文件加载多市场股票数据 Args: data_dir: 数据目录路径 @@ -93,10 +94,22 @@ def load_tushare_data(data_dir: Path) -> List[Dict[str, Any]]: 合并后的股票列表 """ 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 = { 'CN': data_dir / 'stock_list_a.csv', 'HK': data_dir / 'stock_list_hk.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(): @@ -290,6 +303,7 @@ def extract_symbol_from_ts_code(ts_code: str, market: str) -> Optional[str]: - A股:000001.SZ → 000001 - 港股:00700.HK → 00700 - 美股:AAPL → AAPL + - 日股/韩股:7203.T / 005930.KS → 保留后缀,避免与其他市场裸代码冲突 Args: ts_code: TS代码 @@ -301,8 +315,8 @@ def extract_symbol_from_ts_code(ts_code: str, market: str) -> Optional[str]: if not ts_code: return None - if market == 'US': - # 美股无后缀,直接返回 + if market in {'US', 'JP', 'KR'}: + # 美股常见 class/share 后缀、日韩 Yahoo 后缀都是代码身份的一部分。 return 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 字段(英文名称) Args: @@ -337,6 +351,20 @@ def get_stock_name(row: Dict[str, str], market: str) -> Optional[str]: 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]]: """ 解析单行股票数据 @@ -387,6 +415,7 @@ def parse_stock_row(row: Dict[str, str], preferred_market: Optional[str] = None) 'symbol': display_code, 'name': name, 'market': market, + 'aliases': parse_aliases(row), } @@ -395,10 +424,10 @@ def determine_market(ts_code: str) -> str: Determine market based on code 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: - Market code (CN, HK, US, BSE) + Market code (CN, HK, US, BSE, JP, KR) """ if '.' in ts_code: # 有后缀的情况 @@ -410,6 +439,10 @@ def determine_market(ts_code: str) -> str: return 'HK' elif suffix == 'BJ': return 'BSE' + elif suffix == 'T': + return 'JP' + elif suffix in ['KS', 'KQ']: + return 'KR' # 有后缀但不是中国市场后缀,检查是否为美股 # 美股可能有点号后缀(如 BRK.B, GOOG.A, AAPL.U) prefix = ts_code.split('.')[0] @@ -536,6 +569,9 @@ def build_stock_index(stocks: List[Dict[str, Any]]) -> List[Dict[str, Any]]: # Generate aliases. aliases = generate_aliases(name, market) + for alias in stock.get('aliases', []): + if alias != name and alias not in aliases: + aliases.append(alias) index.append({ "canonicalCode": ts_code, # Example: 000001.SZ, AAPL diff --git a/scripts/refresh_stock_index.py b/scripts/refresh_stock_index.py index 779253347..33efdfd04 100644 --- a/scripts/refresh_stock_index.py +++ b/scripts/refresh_stock_index.py @@ -4,7 +4,7 @@ Default flow: 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. """ diff --git a/scripts/stock_index_seeds/stock_list_jp.csv b/scripts/stock_index_seeds/stock_list_jp.csv new file mode 100644 index 000000000..cfd228838 --- /dev/null +++ b/scripts/stock_index_seeds/stock_list_jp.csv @@ -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|优衣库|迅销 diff --git a/scripts/stock_index_seeds/stock_list_kr.csv b/scripts/stock_index_seeds/stock_list_kr.csv new file mode 100644 index 000000000..3dc81bfcf --- /dev/null +++ b/scripts/stock_index_seeds/stock_list_kr.csv @@ -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|셀트리온헬스케어 diff --git a/tests/test_generate_index_from_csv.py b/tests/test_generate_index_from_csv.py index 97802346f..0d2e9cf12 100644 --- a/tests/test_generate_index_from_csv.py +++ b/tests/test_generate_index_from_csv.py @@ -55,6 +55,16 @@ class TestExtractSymbol: result = extract_symbol_from_ts_code("AAPL", "US") 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): """测试空 ts_code""" result = extract_symbol_from_ts_code("", "CN") @@ -114,6 +124,21 @@ class TestDetermineMarket: result = determine_market("AAPL.U") 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: """测试股票名称获取函数""" @@ -214,6 +239,38 @@ class TestDataCleaning: assert result['name'] == "BERKSHIRE HATHAWAY 'B'" 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): """测试美股 DUMMY 记录被过滤""" row = { @@ -430,23 +487,47 @@ class TestIntegration: '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) # 验证数据 - assert len(stocks) == 3 + assert len(stocks) == 5 # 构建索引 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) # 验证压缩 - assert len(compressed) == 3 + assert len(compressed) == 5 # 验证字段数量 for item in compressed: