mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: support JP/KR web autocomplete (#1721)
This commit is contained in:
@@ -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]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
30
apps/dsa-web/src/utils/__tests__/validation.test.ts
Normal file
30
apps/dsa-web/src/utils/__tests__/validation.test.ts
Normal 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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 日本/韩国个股 suffix-only MVP(Issue #1718)
|
## 日本/韩国个股 suffix-only MVP(Issue #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/` 日韩种子索引,并删除本文档中的能力声明。
|
||||||
|
|
||||||
配置兼容性:
|
配置兼容性:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
11
scripts/stock_index_seeds/stock_list_jp.csv
Normal file
11
scripts/stock_index_seeds/stock_list_jp.csv
Normal 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|优衣库|迅销
|
||||||
|
11
scripts/stock_index_seeds/stock_list_kr.csv
Normal file
11
scripts/stock_index_seeds/stock_list_kr.csv
Normal 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|셀트리온헬스케어
|
||||||
|
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user