mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 02:43:35 +08:00
fix(market): bound AkShare market stats calls (#2345)
This commit is contained in:
@@ -64,6 +64,7 @@ logger = logging.getLogger(__name__)
|
||||
SINA_REALTIME_ENDPOINT = "hq.sinajs.cn/list"
|
||||
TENCENT_REALTIME_ENDPOINT = "qt.gtimg.cn/q"
|
||||
_AKSHARE_HISTORY_CALL_TIMEOUT = 30.0
|
||||
_AKSHARE_MARKET_STATS_CALL_TIMEOUT = 30.0
|
||||
_AKSHARE_TIMEOUT_PROCESS_JOIN_GRACE = 1.0
|
||||
_AKSHARE_TIMEOUT_PROCESS_START_METHOD = "spawn"
|
||||
|
||||
@@ -428,6 +429,7 @@ class AkshareFetcher(BaseFetcher):
|
||||
self.sleep_max = sleep_max
|
||||
self._last_request_time: Optional[float] = None
|
||||
self._history_call_timeout = _AKSHARE_HISTORY_CALL_TIMEOUT
|
||||
self._market_stats_call_timeout = _AKSHARE_MARKET_STATS_CALL_TIMEOUT
|
||||
# 东财补丁开启才执行打补丁操作
|
||||
if get_config().enable_eastmoney_patch:
|
||||
eastmoney_patch()
|
||||
@@ -2013,7 +2015,11 @@ class AkshareFetcher(BaseFetcher):
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
"api=ak.stock_zh_a_spot_em action=request_start"
|
||||
)
|
||||
df = ak.stock_zh_a_spot_em()
|
||||
df = _akshare_call_with_timeout(
|
||||
ak.stock_zh_a_spot_em,
|
||||
timeout=self._market_stats_call_timeout,
|
||||
call_name="ak.stock_zh_a_spot_em",
|
||||
)
|
||||
elapsed = time.monotonic() - started_at
|
||||
logger.info(
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
@@ -2026,6 +2032,15 @@ class AkshareFetcher(BaseFetcher):
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
"api=ak.stock_zh_a_spot_em action=parse status=empty"
|
||||
)
|
||||
except TimeoutError as e:
|
||||
elapsed = time.monotonic() - started_at
|
||||
logger.warning(
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
"api=ak.stock_zh_a_spot_em action=request_timeout elapsed=%.2fs "
|
||||
"error=%s fallback=ak.stock_zh_a_spot",
|
||||
elapsed,
|
||||
e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
@@ -2043,7 +2058,11 @@ class AkshareFetcher(BaseFetcher):
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
"api=ak.stock_zh_a_spot action=request_start"
|
||||
)
|
||||
df = ak.stock_zh_a_spot()
|
||||
df = _akshare_call_with_timeout(
|
||||
ak.stock_zh_a_spot,
|
||||
timeout=self._market_stats_call_timeout,
|
||||
call_name="ak.stock_zh_a_spot",
|
||||
)
|
||||
elapsed = time.monotonic() - started_at
|
||||
logger.info(
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
@@ -2056,6 +2075,15 @@ class AkshareFetcher(BaseFetcher):
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
"api=ak.stock_zh_a_spot action=parse status=empty"
|
||||
)
|
||||
except TimeoutError as e:
|
||||
elapsed = time.monotonic() - started_at
|
||||
logger.error(
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
"api=ak.stock_zh_a_spot action=request_timeout elapsed=%.2fs "
|
||||
"error=%s fallback=none",
|
||||
elapsed,
|
||||
e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"[MarketStats] component=market_stats provider=AkshareFetcher "
|
||||
|
||||
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [测试] 修复股票名称解析冷启动超时并发测试的同步竞态:在放行后台抓取前确认两个等待者均已结束并返回空结果,避免 Docker 发布门禁偶发失败。
|
||||
- [文档] 将仓库内所有 SerpApi 链接统一更新为新的赞助转化追踪地址。
|
||||
- [修复] 智能导入兼容带 UTF-8 BOM 的 CSV 与剪贴板文本,避免 `code` 表头被误当成数据并丢失有效股票代码。
|
||||
- [修复] 为 AkShare 大盘涨跌统计的东财与新浪降级调用增加可强制终止的子进程超时,避免外部接口无响应时长期占用分析线程(Fixes #2340)。
|
||||
|
||||
<!-- 新条目格式:- [类型] 描述(类型取值:新功能/改进/修复/文档/测试/chore)-->
|
||||
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
|
||||
|
||||
134
tests/test_akshare_market_stats_timeout.py
Normal file
134
tests/test_akshare_market_stats_timeout.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Regression tests for AkShare market statistics timeout handling."""
|
||||
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from tests.litellm_stub import ensure_litellm_stub
|
||||
|
||||
ensure_litellm_stub()
|
||||
|
||||
from data_provider.akshare_fetcher import AkshareFetcher
|
||||
|
||||
|
||||
def _market_frame() -> pd.DataFrame:
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"代码": ["600000", "000001"],
|
||||
"名称": ["浦发银行", "平安银行"],
|
||||
"最新价": [11.0, 9.0],
|
||||
"昨收": [10.0, 10.0],
|
||||
"成交额": [100_000_000, 200_000_000],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_market_stats_eastmoney_call_uses_timeout_wrapper(monkeypatch) -> None:
|
||||
calls = []
|
||||
eastmoney_api = object()
|
||||
sina_api = object()
|
||||
|
||||
def fake_call(func, *args, timeout=None, call_name="", **kwargs):
|
||||
calls.append((func, timeout, call_name))
|
||||
return _market_frame()
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"akshare",
|
||||
SimpleNamespace(
|
||||
stock_zh_a_spot_em=eastmoney_api,
|
||||
stock_zh_a_spot=sina_api,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"data_provider.akshare_fetcher._akshare_call_with_timeout",
|
||||
fake_call,
|
||||
)
|
||||
|
||||
fetcher = AkshareFetcher(sleep_min=0, sleep_max=0)
|
||||
fetcher._market_stats_call_timeout = 7
|
||||
|
||||
stats = fetcher.get_market_stats()
|
||||
|
||||
assert calls == [(eastmoney_api, 7, "ak.stock_zh_a_spot_em")]
|
||||
assert stats == {
|
||||
"up_count": 1,
|
||||
"down_count": 1,
|
||||
"flat_count": 0,
|
||||
"limit_up_count": 1,
|
||||
"limit_down_count": 1,
|
||||
"total_amount": 3.0,
|
||||
}
|
||||
|
||||
|
||||
def test_market_stats_falls_back_to_sina_after_eastmoney_timeout(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
calls = []
|
||||
eastmoney_api = object()
|
||||
sina_api = object()
|
||||
|
||||
def fake_call(func, *args, timeout=None, call_name="", **kwargs):
|
||||
calls.append((func, timeout, call_name))
|
||||
if func is eastmoney_api:
|
||||
raise TimeoutError("eastmoney timeout")
|
||||
return _market_frame()
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"akshare",
|
||||
SimpleNamespace(
|
||||
stock_zh_a_spot_em=eastmoney_api,
|
||||
stock_zh_a_spot=sina_api,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"data_provider.akshare_fetcher._akshare_call_with_timeout",
|
||||
fake_call,
|
||||
)
|
||||
|
||||
fetcher = AkshareFetcher(sleep_min=0, sleep_max=0)
|
||||
fetcher._market_stats_call_timeout = 9
|
||||
|
||||
stats = fetcher.get_market_stats()
|
||||
|
||||
assert calls == [
|
||||
(eastmoney_api, 9, "ak.stock_zh_a_spot_em"),
|
||||
(sina_api, 9, "ak.stock_zh_a_spot"),
|
||||
]
|
||||
assert stats is not None
|
||||
assert stats["total_amount"] == 3.0
|
||||
|
||||
|
||||
def test_market_stats_returns_none_after_both_calls_timeout(monkeypatch) -> None:
|
||||
calls = []
|
||||
eastmoney_api = object()
|
||||
sina_api = object()
|
||||
|
||||
def fake_call(func, *args, timeout=None, call_name="", **kwargs):
|
||||
calls.append((func, timeout, call_name))
|
||||
raise TimeoutError(f"{call_name} timeout")
|
||||
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"akshare",
|
||||
SimpleNamespace(
|
||||
stock_zh_a_spot_em=eastmoney_api,
|
||||
stock_zh_a_spot=sina_api,
|
||||
),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"data_provider.akshare_fetcher._akshare_call_with_timeout",
|
||||
fake_call,
|
||||
)
|
||||
|
||||
fetcher = AkshareFetcher(sleep_min=0, sleep_max=0)
|
||||
fetcher._market_stats_call_timeout = 5
|
||||
|
||||
assert fetcher.get_market_stats() is None
|
||||
assert calls == [
|
||||
(eastmoney_api, 5, "ak.stock_zh_a_spot_em"),
|
||||
(sina_api, 5, "ak.stock_zh_a_spot"),
|
||||
]
|
||||
Reference in New Issue
Block a user