Skip to content

Commit 7d0c93d

Browse files
committed
fix(issue-1234): [bug]-北交所股票处理差异,异常
1 parent 8816421 commit 7d0c93d

10 files changed

Lines changed: 117 additions & 6 deletions

File tree

apps/dsa-web/src/components/StockAutocomplete/__tests__/StockAutocomplete.test.tsx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ const hkSuggestion = {
7979
score: 100,
8080
};
8181

82+
const bseSuggestion = {
83+
canonicalCode: "920493.BJ",
84+
displayCode: "920493",
85+
nameZh: "示例北交所股票",
86+
market: "BSE" as const,
87+
matchType: "exact" as const,
88+
matchField: "code" as const,
89+
score: 100,
90+
};
91+
8292
describe('StockAutocomplete', () => {
8393
const mockOnChange = vi.fn();
8494
const mockOnSubmit = vi.fn();
@@ -416,6 +426,40 @@ describe('StockAutocomplete', () => {
416426
expect(mockOnChange).toHaveBeenCalledWith('00700');
417427
expect(mockOnSubmit).toHaveBeenCalledWith('00700.HK', '腾讯控股', 'autocomplete');
418428
});
429+
430+
it('submits the highlighted BSE suggestion using the canonical .BJ code', () => {
431+
autocompleteHookImpl = () => ({
432+
query: '',
433+
setQuery: vi.fn(),
434+
suggestions: [bseSuggestion],
435+
isOpen: true,
436+
highlightedIndex: 0,
437+
setHighlightedIndex: vi.fn(),
438+
highlightPrevious: vi.fn(),
439+
highlightNext: vi.fn(),
440+
handleSelect: vi.fn(),
441+
close: vi.fn(),
442+
reset: vi.fn(),
443+
isComposing: false,
444+
setIsComposing: vi.fn(),
445+
runtimeFallback: false,
446+
error: null,
447+
});
448+
449+
render(
450+
<StockAutocomplete
451+
value="920493"
452+
onChange={mockOnChange}
453+
onSubmit={mockOnSubmit}
454+
/>
455+
);
456+
457+
const input = screen.getByDisplayValue('920493');
458+
fireEvent.keyDown(input, { key: 'Enter' });
459+
460+
expect(mockOnChange).toHaveBeenCalledWith('920493');
461+
expect(mockOnSubmit).toHaveBeenCalledWith('920493.BJ', '示例北交所股票', 'autocomplete');
462+
});
419463
});
420464

421465
describe('runtime boundary', () => {

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
2222
- [改进] 设置项帮助窗口支持键盘焦点限制、Esc 关闭和关闭后焦点恢复,并移除短描述重复 hover tooltip。
2323
- [文档] 新增设置页配置帮助维护说明,明确帮助元数据字段、首批覆盖范围、事实源和多语言文案同步规则。
2424
- [测试] 补充设置项帮助元数据、API schema、前端弹窗交互测试,并修复 Bot 名称路由与调度时间 provider 测试的离线 CI 稳定性问题。
25+
- [修复] 修正分析 API 对北交所 `BJ` 前缀与 `.BJ` 后缀股票代码的校验,保持前端自动补全与 Tushare `ts_code` 调用格式一致。
2526

2627
## [3.15.0] - 2026-05-05
2728

docs/full-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,7 @@ python main.py --serve-only --host 0.0.0.0 --port 8888
11421142
| 类型 | 格式 | 示例 |
11431143
|------|------|------|
11441144
| A股 | 6位数字 | `600519`、`000001`、`300750` |
1145-
| 北交所 | 8/4/92 开头 6 位 | `920748`、`838163`、`430047` |
1145+
| 北交所 | 8/4/92 开头 6 位,支持 `BJ` 前缀或 `.BJ` 后缀 | `920748`、`BJ920493`、`920493.BJ` |
11461146
| 港股 | hk + 5位数字 | `hk00700`、`hk09988` |
11471147
| 美股 | 1-5 字母(可选 .X 后缀) | `AAPL`、`TSLA`、`BRK.B` |
11481148
| 美股指数 | SPX/DJI/IXIC 等 | `SPX`、`DJI`、`NASDAQ`、`VIX` |

docs/full-guide_EN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -981,7 +981,7 @@ python main.py --serve-only --host 0.0.0.0 --port 8888
981981
| Type | Format | Examples |
982982
|------|------|------|
983983
| A-shares | 6-digit number | `600519`, `000001`, `300750` |
984-
| BSE (Beijing) | 8/4/92 prefix, 6-digit | `920748`, `838163`, `430047` |
984+
| BSE (Beijing) | 8/4/92 prefix, 6-digit; supports `BJ` prefix or `.BJ` suffix | `920748`, `BJ920493`, `920493.BJ` |
985985
| HK stocks | hk + 5-digit number | `hk00700`, `hk09988` |
986986

987987
### Notes

docs/openclaw-skill-integration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@
7575
| 类型 | 格式 | 示例 |
7676
|------|------|------|
7777
| A股 | 6位数字 | `600519``000001``300750` |
78-
| 北交所 | 8/4/92 开头 6 位 | `920748``838163``430047` |
78+
| 北交所 | 8/4/92 开头 6 位,支持 `BJ` 前缀或 `.BJ` 后缀 | `920748``BJ920493``920493.BJ` |
7979
| 港股 | hk + 5位数字 | `hk00700``hk09988` |
8080
| 美股 | 1-5 字母(可选 .X 后缀) | `AAPL``TSLA``BRK.B` |
8181
| 美股指数 | SPX/DJI/IXIC 等 | `SPX``DJI``NASDAQ``VIX` |
@@ -145,6 +145,7 @@ metadata:
145145
## 股票代码格式
146146

147147
- A股:6位数字(600519、000001)
148+
- 北交所:8/4/92 开头 6 位,支持 BJ 前缀或 .BJ 后缀(920748、BJ920493、920493.BJ)
148149
- 港股:hk + 5位数字(hk00700)
149150
- 美股:1–5 字母(AAPL、TSLA、BRK.B)
150151
- 美股指数:SPX、DJI、IXIC 等

src/services/stock_code_utils.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
"SH": (6,),
1616
"SZ": (6,),
1717
"SS": (6,),
18+
"BJ": (6,),
1819
"HK": (1, 2, 3, 4, 5),
1920
}
2021

2122
_SUFFIX_DIGIT_LENS: dict = {
2223
".SH": (6,),
2324
".SZ": (6,),
2425
".SS": (6,),
26+
".BJ": (6,),
2527
".HK": (1, 2, 3, 4, 5),
2628
}
2729

@@ -57,7 +59,7 @@ def is_code_like(value: str) -> bool:
5759
return True
5860
if re.match(r"^[A-Z]{1,5}(?:\.(?:US|[A-Z]))?$", text):
5961
return True
60-
# Support exchange-prefixed codes: SH600519, SZ000001, HK00700
62+
# Support exchange-prefixed codes: SH600519, SZ000001, BJ920493, HK00700
6163
if _strip_exchange_prefix(text) is not None:
6264
return True
6365
return False
@@ -69,7 +71,7 @@ def normalize_code(raw: str) -> Optional[str]:
6971
Supports:
7072
- Plain digit codes: 600519, 00700
7173
- Suffix format: 600519.SH, 600519.SZ, 00700.HK
72-
- Prefix format: SH600519, SZ000001, HK00700 (case-insensitive)
74+
- Prefix format: SH600519, SZ000001, BJ920493, HK00700 (case-insensitive)
7375
- US ticker symbols: AAPL, TSLA
7476
"""
7577
text = raw.strip().upper()
@@ -82,7 +84,7 @@ def normalize_code(raw: str) -> Optional[str]:
8284
stripped_suffix = _strip_exchange_suffix(text)
8385
if stripped_suffix is not None:
8486
return stripped_suffix
85-
# Support exchange-prefixed codes: SH600519 -> 600519, HK00700 -> 00700
87+
# Support exchange-prefixed codes: SH600519 -> 600519, BJ920493 -> 920493
8688
stripped = _strip_exchange_prefix(text)
8789
if stripped is not None:
8890
return stripped

tests/test_analysis_api_contract.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,42 @@ def test_trigger_analysis_accepts_hk_suffix_code_from_autocomplete(self) -> None
689689
notify=True,
690690
)
691691

692+
def test_trigger_analysis_accepts_bse_suffix_code_from_autocomplete(self) -> None:
693+
if trigger_analysis is None:
694+
self.skipTest("fastapi is not installed in this test environment")
695+
696+
queue = MagicMock()
697+
queue.submit_tasks_batch.return_value = ([], [])
698+
699+
with patch("api.v1.endpoints.analysis.get_task_queue", return_value=queue), \
700+
patch("api.v1.endpoints.analysis.resolve_name_to_code") as resolve_mock:
701+
response = trigger_analysis(
702+
request=SimpleNamespace(
703+
stock_code="920493.BJ",
704+
stock_codes=None,
705+
stock_name="示例北交所股票",
706+
original_query="920493",
707+
selection_source="autocomplete",
708+
report_type="detailed",
709+
force_refresh=False,
710+
async_mode=True,
711+
notify=True,
712+
),
713+
config=SimpleNamespace(),
714+
)
715+
716+
self.assertEqual(response.status_code, 202)
717+
resolve_mock.assert_not_called()
718+
queue.submit_tasks_batch.assert_called_once_with(
719+
stock_codes=["920493.BJ"],
720+
stock_name="示例北交所股票",
721+
original_query="920493",
722+
selection_source="autocomplete",
723+
report_type="detailed",
724+
force_refresh=False,
725+
notify=True,
726+
)
727+
692728
def test_trigger_analysis_accepts_hk_prefixed_code(self) -> None:
693729
if trigger_analysis is None:
694730
self.skipTest("fastapi is not installed in this test environment")

tests/test_name_to_code_resolver.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ def test_a_share_5_digits(self):
3333
def test_a_share_6_digits(self):
3434
assert _is_code_like("300750") is True
3535

36+
def test_bse_with_exchange_hint(self):
37+
assert _is_code_like("920493.BJ") is True
38+
assert _is_code_like("BJ920493") is True
39+
3640
def test_hk_5_digits(self):
3741
assert _is_code_like("00700") is True
3842

@@ -61,6 +65,10 @@ def test_preserves_valid_a_share(self):
6165
def test_strips_suffix(self):
6266
assert _normalize_code("600519.SH") == "600519"
6367
assert _normalize_code("000001.SZ") == "000001"
68+
assert _normalize_code("920493.BJ") == "920493"
69+
70+
def test_strips_bse_prefix(self):
71+
assert _normalize_code("BJ920493") == "920493"
6472

6573
def test_preserves_us_stock(self):
6674
assert _normalize_code("AAPL") == "AAPL"
@@ -99,6 +107,7 @@ class TestResolveNameToCode:
99107
def test_code_like_input_returned_normalized(self):
100108
assert resolve_name_to_code("600519") == "600519"
101109
assert resolve_name_to_code("600519.SH") == "600519"
110+
assert resolve_name_to_code("920493.BJ") == "920493"
102111
assert resolve_name_to_code(" AAPL ") == "AAPL"
103112

104113
def test_local_map_exact_match(self):

tests/test_stock_code_bse.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ def test_bse_returns_bj_suffix(self):
120120
self.assertEqual(fetcher._convert_stock_code("838163"), "838163.BJ")
121121
self.assertEqual(fetcher._convert_stock_code("430047"), "430047.BJ")
122122

123+
def test_bse_explicit_exchange_hint_is_preserved(self):
124+
"""BSE prefix/suffix forms should keep the BJ Tushare ts_code."""
125+
fetcher = TushareFetcher()
126+
self.assertEqual(fetcher._convert_stock_code("920493.BJ"), "920493.BJ")
127+
self.assertEqual(fetcher._convert_stock_code("BJ920493"), "920493.BJ")
128+
123129

124130
@unittest.skipIf(not _AKSHARE_IMPORTS_OK, f"akshare fetcher imports failed: {_AKSHARE_IMPORT_ERROR}")
125131
class TestAkshareToSinaTxSymbol(unittest.TestCase):

tests/test_stock_code_utils.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ def test_suffix_sh(self):
2727
def test_suffix_sz(self):
2828
assert is_code_like("000001.SZ") is True
2929

30+
def test_suffix_bj(self):
31+
assert is_code_like("920493.BJ") is True
32+
3033
def test_suffix_lowercase(self):
3134
assert is_code_like("600519.sh") is True
3235

@@ -56,6 +59,9 @@ def test_prefix_sh_lower(self):
5659
def test_prefix_sz(self):
5760
assert is_code_like("SZ000001") is True
5861

62+
def test_prefix_bj(self):
63+
assert is_code_like("BJ920493") is True
64+
5965
def test_prefix_hk(self):
6066
assert is_code_like("HK00700") is True
6167

@@ -104,6 +110,9 @@ def test_suffix_sh_strips(self):
104110
def test_suffix_sz_strips(self):
105111
assert normalize_code("000001.SZ") == "000001"
106112

113+
def test_suffix_bj_strips(self):
114+
assert normalize_code("920493.BJ") == "920493"
115+
107116
def test_suffix_ss_strips(self):
108117
assert normalize_code("600000.SS") == "600000"
109118

@@ -132,6 +141,9 @@ def test_prefix_sh_lower(self):
132141
def test_prefix_sz(self):
133142
assert normalize_code("SZ000001") == "000001"
134143

144+
def test_prefix_bj(self):
145+
assert normalize_code("BJ920493") == "920493"
146+
135147
def test_prefix_hk(self):
136148
assert normalize_code("HK00700") == "00700"
137149

0 commit comments

Comments
 (0)