Skip to content

Commit 28f7764

Browse files
committed
Fix HK historical data provider routing
1 parent 8816421 commit 28f7764

6 files changed

Lines changed: 94 additions & 3 deletions

File tree

data_provider/base.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,16 @@ class DataFetcherManager:
482482
- 失败后自动切换到下一个
483483
- 所有数据源都失败时抛出异常
484484
"""
485+
486+
_DAILY_MARKET_FETCHER_SUPPORT = {
487+
"EfinanceFetcher": {"cn"},
488+
"AkshareFetcher": {"cn", "hk"},
489+
"TushareFetcher": {"cn", "hk"},
490+
"PytdxFetcher": {"cn"},
491+
"BaostockFetcher": {"cn"},
492+
"YfinanceFetcher": {"cn", "hk", "us"},
493+
"LongbridgeFetcher": {"hk", "us"},
494+
}
485495

486496
def __init__(self, fetchers: Optional[List[BaseFetcher]] = None):
487497
"""
@@ -546,6 +556,33 @@ def _call_fetcher_method(self, fetcher: BaseFetcher, method_name: str, *args, **
546556
with self._get_fetcher_call_lock(fetcher):
547557
return method(*args, **kwargs)
548558

559+
@classmethod
560+
def _filter_daily_fetchers_for_market(
561+
cls,
562+
fetchers: List[BaseFetcher],
563+
market: str,
564+
) -> List[BaseFetcher]:
565+
"""Skip built-in daily fetchers that are known not to support a market."""
566+
if market not in {"cn", "hk", "us"}:
567+
return fetchers
568+
569+
kept: List[BaseFetcher] = []
570+
skipped: List[str] = []
571+
for fetcher in fetchers:
572+
supported = cls._DAILY_MARKET_FETCHER_SUPPORT.get(fetcher.name)
573+
if supported is not None and market not in supported:
574+
skipped.append(fetcher.name)
575+
else:
576+
kept.append(fetcher)
577+
578+
if skipped:
579+
logger.info(
580+
"[数据源路由] %s 日线跳过不支持的数据源: %s",
581+
market,
582+
", ".join(skipped),
583+
)
584+
return kept
585+
549586
def _get_cached_stock_name(self, stock_code: str) -> Optional[str]:
550587
self._ensure_concurrency_guards()
551588
with self._stock_name_cache_lock:
@@ -935,16 +972,18 @@ def get_daily_data(
935972

936973
fetchers = self._get_fetchers_snapshot()
937974
errors = []
938-
total_fetchers = len(fetchers)
939975
request_start = time.time()
940976

941-
# 快速路径:美股/港股使用专用数据源路由
977+
# 快速路径:美股使用专用数据源路由;港股先过滤不支持港股日线的数据源
942978
# - 配置长桥凭据后: Longbridge 为首选, YFinance/AkShare 兜底
943979
# - 未配置长桥: YFinance 为首选(美股), 通用 fetcher 循环(港股)
944980
# - 美股指数: 始终 YFinance 为首选(Longbridge 不提供指数K线)
945981
is_us_index = is_us_index_code(stock_code)
946982
is_us = is_us_index or is_us_stock_code(stock_code)
947983
is_hk = (not is_us) and _is_hk_market(stock_code)
984+
if is_hk:
985+
fetchers = self._filter_daily_fetchers_for_market(fetchers, "hk")
986+
total_fetchers = len(fetchers)
948987

949988
# 美股(含美股指数)使用 Longbridge/YFinance 特殊路由;港股走下方通用数据源循环
950989
if is_us:

data_provider/efinance_fetcher.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353

5454
from patch.eastmoney_patch import eastmoney_patch
5555
from src.config import get_config
56-
from .base import BaseFetcher, DataFetchError, RateLimitError, STANDARD_COLUMNS,is_bse_code, is_st_stock, is_kc_cy_stock, normalize_stock_code
56+
from .base import BaseFetcher, DataFetchError, RateLimitError, STANDARD_COLUMNS,is_bse_code, is_st_stock, is_kc_cy_stock, normalize_stock_code, _is_hk_market
5757
from .realtime_types import (
5858
UnifiedRealtimeQuote, RealtimeSource,
5959
get_realtime_circuit_breaker,
@@ -354,6 +354,11 @@ def _fetch_raw_data(self, stock_code: str, start_date: str, end_date: str) -> pd
354354
# 美股不支持,抛出异常让 DataFetcherManager 切换到 AkshareFetcher/YfinanceFetcher
355355
if _is_us_code(stock_code):
356356
raise DataFetchError(f"EfinanceFetcher 不支持美股 {stock_code},请使用 AkshareFetcher 或 YfinanceFetcher")
357+
358+
# efinance 的历史 K 线接口在港股代码上可能返回非预期市场数据,
359+
# 明确跳过并交给 AkShare/Tushare/YFinance/Longbridge 等港股路径兜底。
360+
if _is_hk_market(stock_code):
361+
raise DataFetchError(f"EfinanceFetcher 不支持港股日线 {stock_code},请使用 AkshareFetcher 或其他港股数据源")
357362

358363
# 根据代码类型选择不同的获取方法
359364
if _is_etf_code(stock_code):

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+
- [修复] 港股日线跳过不支持港股的内置历史数据源,避免港股代码错配到非港股市场数据。
2526

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

docs/full-guide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -905,6 +905,8 @@ PUSHOVER_API_TOKEN=your_api_token
905905
STOCK_LIST=600519,hk00700,hk01810
906906
```
907907

908+
港股日线会跳过 efinance、pytdx、baostock 等不支持港股日线的数据源,避免把港股代码错配到非港股市场;默认改由 AkShare/Tushare/YFinance/Longbridge 等港股路径继续兜底。
909+
908910
### ETF 与指数分析
909911

910912
针对指数跟踪型 ETF 和美股指数(如 VOO、QQQ、SPY、510050、SPX、DJI、IXIC),分析仅关注**指数走势、跟踪误差、市场流动性**,不纳入基金管理人/发行方的公司层面风险(诉讼、声誉、高管变动等)。风险警报与业绩预期均基于指数成分股整体表现,避免将基金公司新闻误判为标的本身利空。详见 Issue #274。

docs/full-guide_EN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,8 @@ Use `hk` prefix for HK stock codes:
770770
STOCK_LIST=600519,hk00700,hk01810
771771
```
772772

773+
HK daily history skips efinance, pytdx, baostock, and other built-in providers that do not support HK daily data, avoiding mismatches between HK symbols and non-HK market data. AkShare/Tushare/YFinance/Longbridge continue to provide HK fallback paths.
774+
773775
### Multi-Model Switching
774776

775777
Configure multiple models, system auto-switches:

tests/test_fetcher_logging.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ def _normalize_data(self, df: pd.DataFrame, stock_code: str) -> pd.DataFrame:
5555
return df
5656

5757

58+
class _RecordingFetcher(BaseFetcher):
59+
def __init__(self, name: str, priority: int):
60+
self.name = name
61+
self.priority = priority
62+
self.calls = []
63+
64+
def _fetch_raw_data(self, stock_code: str, start_date: str, end_date: str) -> pd.DataFrame:
65+
self.calls.append(stock_code)
66+
return _sample_df()
67+
68+
def _normalize_data(self, df: pd.DataFrame, stock_code: str) -> pd.DataFrame:
69+
return df
70+
71+
5872
class TestFetcherLogging(unittest.TestCase):
5973
def test_base_fetcher_logs_start_and_success(self):
6074
fetcher = _SuccessFetcher()
@@ -82,6 +96,34 @@ def test_manager_logs_fallback_and_final_success(self):
8296
self.assertIn("[数据源切换] 601006: [FailureFetcher] -> [SuccessFetcher]", log_text)
8397
self.assertIn("[数据源完成] 601006 使用 [SuccessFetcher] 获取成功:", log_text)
8498

99+
def test_manager_skips_builtin_fetchers_that_do_not_support_hk_daily(self):
100+
efinance = _RecordingFetcher("EfinanceFetcher", 0)
101+
pytdx = _RecordingFetcher("PytdxFetcher", 1)
102+
akshare = _RecordingFetcher("AkshareFetcher", 2)
103+
yfinance = _RecordingFetcher("YfinanceFetcher", 3)
104+
105+
manager = DataFetcherManager(fetchers=[efinance, pytdx, akshare, yfinance])
106+
df, source = manager.get_daily_data("1211.HK", start_date="2026-05-01", end_date="2026-05-08")
107+
108+
self.assertFalse(df.empty)
109+
self.assertEqual(source, "AkshareFetcher")
110+
self.assertEqual(efinance.calls, [])
111+
self.assertEqual(pytdx.calls, [])
112+
self.assertEqual(akshare.calls, ["HK01211"])
113+
self.assertEqual(yfinance.calls, [])
114+
115+
@patch("data_provider.efinance_fetcher.get_config")
116+
def test_efinance_rejects_hk_daily_without_calling_eastmoney(self, mock_get_config):
117+
mock_get_config.return_value = types.SimpleNamespace(enable_eastmoney_patch=False)
118+
fetcher = EfinanceFetcher(sleep_min=0, sleep_max=0)
119+
120+
with patch.object(fetcher, "_fetch_stock_data") as mock_fetch_stock_data:
121+
with self.assertRaises(DataFetchError) as captured:
122+
fetcher.get_daily_data("1211.HK", start_date="2026-05-01", end_date="2026-05-08")
123+
124+
mock_fetch_stock_data.assert_not_called()
125+
self.assertIn("不支持港股日线", str(captured.exception))
126+
85127
def test_efinance_logs_eastmoney_endpoint_on_remote_disconnect(self):
86128
fetcher = EfinanceFetcher()
87129
fake_efinance = types.SimpleNamespace(

0 commit comments

Comments
 (0)