mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* fix: route 4-5 digit bare HK codes to .HK in YfinanceFetcher (fixes #2091) Issue #2091: 5-digit HK listings without an explicit 'HK' prefix (e.g. 02513 for Zhipu) fell through to the 'cannot determine market, default to .SZ' tail branch in YfinanceFetcher._convert_stock_code(), producing '02513.SZ' which Yahoo Finance rejects with 404, breaking the daily OHLC chain and leaving LLM-driven stop-loss / MA levels unreliable. Fix: insert a new branch ahead of the .SZ fallback that routes 4-5 digit pure-numeric codes to '.HK' using the same zero-padding logic as the existing HK-prefix branch (lstrip leading zeros, zfill to 4). Safety: - A-share codes are always 6 digits, so no A-share rule is shadowed. - BSE codes are 6 digits (4xxxxx / 8xxxxx / 920xxx) and routed earlier via is_bse_code(); the new branch only sees 4-5 digit inputs. - ETF branches (15xx/16xx/18xx/51xx/52xx/56xx/58xx) all expect 6-digit codes and run earlier; no overlap. - JP/KR/TW/US suffix codes are routed earlier; unaffected. - Codes already carrying .SS/.SZ/.HK/.BJ pass through verbatim earlier. - 1-3 digit numerics continue to fall through to the .SZ default to preserve prior behaviour; the fix is intentionally scoped to 4-5 digits per maintainer note in issue #2091, avoiding speculative expansion of the HK rule to inputs users never fetch in practice. Tests: tests/test_yfinance_hk_bare_code.py covers: - HK-prefix still routes to .HK (4 cases, regression guard) - Bare 4-5 digit numeric -> .HK with correct zero padding (5 cases) - A-share 600/601/603/688 (.SS) and 000/002/300 (.SZ) unchanged (7 cases) - BSE 4xxxxx / 8xxxxx / 920xxx routes to .BJ unchanged (3 cases) - ETF (510300.SS / 159915.SZ), JP/KR/US suffix, and pre-suffixed codes pass through unchanged (7 cases) All 23 new tests pass; 71 yfinance / convert_stock_code tests total across the wider related test set pass with no regression. * fix: 修复 _is_hk_market 4位裸港股码路由 + DataFetcherManager 回归测试 修复 maintainer 在 PR #2097 review 中指出的 OR-COR-bfddfd66 blocker: _DataFetcherManager.get_daily_data() 仍按 A 股链路路由 4 位裸港股码。 根因:data_provider/base.py::_is_hk_market() 只把 5 位裸数字识别 为港股,4 位裸数字 (0001 长和 / 0941 中国移动) 被路由到 cn 分支, 导致 AkshareFetcher 走 stock_zh_a_hist、BaostockFetcher 兜成 sz.0001、 TushareFetcher 转成 0001.SZ——issue #2091 在主调用路径上未真正关闭。 修复:_is_hk_market 的裸数字分支从 len(normalized) == 5 改为 4 <= len(normalized) <= 5,与 YfinanceFetcher._convert_stock_code 的 4-5 位分支保持一致。A 股 (6 位) / BSE (6 位) / ETF (51/15 开头) 不受影响,因为它们走不同前缀或位数判定。 回归测试:新增 tests/test_data_fetcher_manager_hk_bare_code.py - _is_hk_market('0001'/'0941'/'0078') -> True - 4位裸港股码经 DataFetcherManager 只路由到 HK-capable fetcher (YfinanceFetcher/AkshareFetcher/TushareFetcher),Efinance/Tencent/ TickFlow/Pytdx/Baostock 完全不被调用 - 5位裸港股码行为不变 (00700 -> YfinanceFetcher) - 6位 A 股仍路由到 CN-only fetcher,YfinanceFetcher 不被调用 跨 7 个相关测试文件 186 passed,全量 135 passed,无回归。 * fix(#2091): 同步 akshare_fetcher._is_hk_code 到 4-5 位裸港股码 Review blocker OR-COR-ea09dfe8 (#2097): manager 层 _is_hk_market() 已在前序 commit 放开到 4-5 位裸数字,但 AkshareFetcher._is_hk_code() 仍只接受 5 位。DataFetcherManager 默认优先级下 AkshareFetcher 优先 于 YfinanceFetcher 执行,0001 在 manager 被判为 HK 后于 AkshareFetcher 内部因 _is_hk_code('0001')==False 落到 _fetch_stock_data A股链路,两套市场契约冲突。 同步放宽 _is_hk_code: - 无前缀裸数字从 len==5 改为 4<=len<=5 - docstring 补充 OR-COR-ea09dfe8 说明 新增两类 provider-level regression tests: 1. TestAkshareFetcherIsHkCodeContract: 直接断言 _is_hk_code 对 4/5 位裸码、6位裸码(排除)、前缀后缀的解码结果 2. TestAkshareFetcherRoutingCallsHkBranch: patch _fetch_hk_data / _fetch_stock_data,驱动 _fetch_raw_data('0001') 验证真实分流到 HK 分支而非 A股分支,防止 _is_hk_code 被重收紧后静默回归 * fix(#2091): 同步 longbridge_fetcher._is_hk_code 到 4-5 位裸港股码 OR-COR-ea09dfe8 关闭:DataFetcherManager 路由层 (data_provider/base.py) 已放宽到 4-5 位裸港股码,把 LongbridgeFetcher 视为 HK-capable provider 保留进港股链路;但 longbridge_fetcher._is_hk_code 仍只接受 5 位裸数字, 导致 4 位裸港股 (0001 长和 / 0941 中国移动) 在配置了 Longbridge 的真实 日线/实时链路上 _to_longbridge_symbol 返回 None,日线 fallback 抛 ValueError、实时兜底被静默跳过。 修复与 base._is_hk_market 的市场契约对齐: - _is_hk_code: 4-5 位裸数字判定为港股 (与 base._is_hk_market 一致); .HK 后缀严格校验后缀 base 部分为 1-5 位数字 (之前无条件 True,会把 类似 "XXX.HK" 也误判,本次顺带收紧);HK 前缀分支保持不变。 - _to_longbridge_symbol: 在 _is_hk_code 通过后的 .HK 后缀输入仍走 原路径直接 return upper,行为不变;4 位裸码现在能正确 zfill(4) 到 "0001.HK" / "0941.HK"。 回归测试: - tests/test_longbridge_fetcher.py 新增 TestSymbolConversion .test_hk_stock_4digit_bare_code_issue_2091: 显式 assert 0001/0941 _is_hk_code=True + _to_longbridge_symbol="0001.HK"/"0941.HK"。 - 现有 TestSymbolConversion 7 测试 + 全文件 30 测试 全通过。 - tests/test_yfinance_hk_bare_code.py + test_data_fetcher_manager_hk_bare_code.py 共 39 测试 全通过。 - 总计新相关回归 69/69 pass。 本地确定性复现 (reviewer 报告的 head 行为): - 修复前: _is_hk_code("0001") == False, _to_longbridge_symbol("0001") == None - 修复后: _is_hk_code("0001") == True, _to_longbridge_symbol("0001") == "0001.HK" - _is_hk_code("0941") == True, _to_longbridge_symbol("0941") == "0941.HK" - _is_hk_code("00700") == True, _to_longbridge_symbol("00700") == "0700.HK" - _is_hk_code("00700.HK") == True, _to_longbridge_symbol("00700.HK") == "00700.HK" - _is_hk_code("HK00700") == True, _to_longbridge_symbol("HK00700") == "0700.HK" - _is_hk_code("600690") == False (6 位 A 股不误判), _to_longbridge_symbol("600690") == None - _is_hk_code("AAPL") == False, _to_longbridge_symbol("AAPL") == "AAPL.US" 注:本轮只关闭 OR-COR-ea09dfe8 阻断;非阻断建议 (CHANGELOG #2063 条目 移出、PR 描述 sync、docstring 中 review blocker 编号清理) 将在下一条 commit 单独处理。 * docs(#2097): 清理评审叙事 + 收敛 CHANGELOG 范围 回应 reviewer 非阻断建议 (上一条 commita251e4f8提到的"下一 commit 处理"): 1. docs/CHANGELOG.md - 移除本 PR 主题 (#2091 yfinance/HK bare-code 路由) 之外混入的 "parse_analysis_target() / #2063 Phase 1" 条目;该 feat 属于 commit 0313dd3c (issue #2063 Phase 1) 单独 PR 的范围,不应在本 PR 中夹带。 - 把 #2091 条目改写为反映本 PR 实际范围:三处 _is_hk_code 同步 (YfinanceFetcher 在 4e915a4f、AkshareFetcher 在 e2bf3a88、 LongbridgeFetcher 在a251e4f8) + DataFetcherManager 港股路由 回归测试 (9c473787),统一描述为"DataFetcherManager 港股路由" 而非只提 yfinance 单侧,与当前累计 diff 一致。 2. data_provider/base.py:_is_hk_market / akshare_fetcher.py:_is_hk_code docstring 清理:移除"Review blocker OR-COR-... (PR #2097 / issue #2091)"评审叙事、PR 编号、blocker 编号、过程性"之前只接受..."。 改为稳定的市场识别规则说明:支持哪种形式 (.HK 后缀 / HK 前缀 / 4-5 位裸数字)、与 provider 内 _is_hk_code 的位数契约对齐即可。 长期保留的产品代码不应写入评审过程叙事。 行为无变化:仅 docstring + CHANGELOG 文本修改,无代码路径修改。 回归:tests/test_longbridge_fetcher.py + tests/test_yfinance_hk_bare_code.py + tests/test_data_fetcher_manager_hk_bare_code.py 共 69/69 pass。
111 lines
4.4 KiB
Python
111 lines
4.4 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Regression tests for YfinanceFetcher HK bare-code routing.
|
|
|
|
Covers issue #2091: 4-5 digit pure numeric codes (e.g. 02513, 00700, 0001)
|
|
must route to ``.HK`` rather than fall through to the ``.SZ`` default,
|
|
otherwise Yahoo Finance returns 404 and the daily-data chain breaks.
|
|
"""
|
|
|
|
from data_provider.yfinance_fetcher import YfinanceFetcher
|
|
|
|
|
|
class TestHKPrefixStillRoutesToHK:
|
|
"""Existing HK-prefix behaviour must remain unchanged."""
|
|
|
|
def test_hk_prefix_4digit(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("hk00700") == "0700.HK"
|
|
|
|
def test_hk_prefix_5digit(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("HK02513") == "2513.HK"
|
|
|
|
def test_hk_prefix_short(self) -> None:
|
|
# 1-3 digit HK prefix (e.g. hk0001 -> 0001.HK)
|
|
assert YfinanceFetcher()._convert_stock_code("hk0001") == "0001.HK"
|
|
|
|
|
|
class TestBareHKCodeRoutesToHK:
|
|
"""Regression for issue #2091: bare 4-5 digit numeric codes -> ``.HK``.
|
|
|
|
Previously the bare 5-digit path fell through to the ``.SZ`` default and
|
|
Yahoo Finance returned 404 for codes like ``02513``. The new routing
|
|
shunts 4-5 digit pure numeric codes to ``.HK`` ahead of the .SZ fallback,
|
|
because A-share codes are always 6 digits and BSE codes are 6 digits
|
|
starting with 4 / 8 / 920 — there is no A-share / BSE ambiguity for
|
|
4-5 digit numeric codes.
|
|
"""
|
|
|
|
def test_bare_5digit_new_listing(self) -> None:
|
|
# 智谱 02513 — the exact case from issue #2091
|
|
assert YfinanceFetcher()._convert_stock_code("02513") == "2513.HK"
|
|
|
|
def test_bare_5digit_tencent(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("00700") == "0700.HK"
|
|
|
|
def test_bare_5digit_keeps_leading_zero_padding(self) -> None:
|
|
# Alibaba 9988 -> padded to 9988 (already 4 digit, no extra padding)
|
|
assert YfinanceFetcher()._convert_stock_code("09988") == "9988.HK"
|
|
|
|
def test_bare_4digit_legacy_hk(self) -> None:
|
|
# 长江和记 0001 — 4-digit legacy HK code (per maintainer note)
|
|
assert YfinanceFetcher()._convert_stock_code("0001") == "0001.HK"
|
|
|
|
def test_bare_4digitmobile_carrier(self) -> None:
|
|
# 中国移动 0941 — 4-digit HK code from issue context
|
|
assert YfinanceFetcher()._convert_stock_code("0941") == "0941.HK"
|
|
|
|
|
|
class TestAShareRoutingUnchanged:
|
|
"""A-share / BSE routing must remain unchanged after the HK bare fix."""
|
|
|
|
def test_sh_600519(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("600519") == "600519.SS"
|
|
|
|
def test_sz_000001(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("000001") == "000001.SZ"
|
|
|
|
def test_sz_300750(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("300750") == "300750.SZ"
|
|
|
|
def test_sz_002594(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("002594") == "002594.SZ"
|
|
|
|
def test_kcb_688981(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("688981") == "688981.SS"
|
|
|
|
def test_bse_920xxx(self) -> None:
|
|
# BSE 920xxx is 6 digit — must NOT be caught by the 4-5 digit HK rule
|
|
assert YfinanceFetcher()._convert_stock_code("920019") == "920019.BJ"
|
|
|
|
def test_bse_8xxxxx(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("830799") == "830799.BJ"
|
|
|
|
def test_bse_4xxxxx(self) -> None:
|
|
# BSE 4xxxxx is 6 digit — must NOT match 4-digit HK rule
|
|
assert YfinanceFetcher()._convert_stock_code("430047") == "430047.BJ"
|
|
|
|
|
|
class TestETFAndSuffixUnchanged:
|
|
"""ETF and suffix codes must route exactly as before."""
|
|
|
|
def test_sh_etf_510300(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("510300") == "510300.SS"
|
|
|
|
def test_sz_etf_159915(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("159915") == "159915.SZ"
|
|
|
|
def test_us_ticker(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("AAPL") == "AAPL"
|
|
|
|
def test_jp_suffix(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("7203.T") == "7203.T"
|
|
|
|
def test_kr_suffix(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("005930.KS") == "005930.KS"
|
|
|
|
def test_with_hk_suffix(self) -> None:
|
|
# Already suffixed codes pass through verbatim
|
|
assert YfinanceFetcher()._convert_stock_code("0700.HK") == "0700.HK"
|
|
|
|
def test_with_ss_suffix(self) -> None:
|
|
assert YfinanceFetcher()._convert_stock_code("600519.SS") == "600519.SS"
|