fix(data): ak-stock top_holders KeyError 'sdltgd' 根治(_safe 捕 KeyError+匹配sdltgd,跳北交所920/83/87/43)
This commit is contained in:
@@ -196,3 +196,117 @@ class TestFetchTopHoldersOnePeriod:
|
||||
)
|
||||
# 重试耗尽返空 df (无 schema, 因为确实是失败)
|
||||
assert isinstance(df, pd.DataFrame)
|
||||
|
||||
|
||||
class TestSafeTop10EmKeyErrorSdltgd:
|
||||
"""_safe_top_10_em 同样捕获 KeyError('sdltgd') → 返空 df。
|
||||
|
||||
bug 根因 (实证 akshare 新版):
|
||||
北交所等不支持的标的, akshare 内部返缺 'sdltgd' 字段 → 抛 KeyError('sdltgd')。
|
||||
KeyError 不是 ValueError 子类, 旧版只 except ValueError 捕不到, 透传到
|
||||
call_ak_with_retry 走 3 次重试 (14s 退避) + 噪声日志。
|
||||
本测试集覆盖: KeyError('sdltgd') 被吞返空 df / 其他 KeyError 不被吞 / 集成路径无重试。
|
||||
"""
|
||||
|
||||
def test_keyerror_sdltgd_returns_empty_df_with_schema(self):
|
||||
"""akshare 抛 KeyError('sdltgd') → 返空 df 带 8 列 schema。"""
|
||||
from akshare_static_download import _safe_top_10_em
|
||||
|
||||
# akshare 实际抛 KeyError('sdltgd'), str(err) == "'sdltgd'" (含引号)
|
||||
err = KeyError("sdltgd")
|
||||
with patch.object(mod.ak, "stock_gdfx_free_top_10_em", side_effect=err):
|
||||
df = _safe_top_10_em(symbol="sh920510", date="20250930")
|
||||
|
||||
assert isinstance(df, pd.DataFrame)
|
||||
assert len(df) == 0
|
||||
assert df.columns.tolist() == EXPECTED_COLS
|
||||
|
||||
def test_keyerror_sdltgd_does_not_raise(self):
|
||||
"""KeyError('sdltgd') 必须被吞掉 (call_ak_with_retry 才不会重试)。"""
|
||||
from akshare_static_download import _safe_top_10_em
|
||||
|
||||
err = KeyError("sdltgd")
|
||||
with patch.object(mod.ak, "stock_gdfx_free_top_10_em", side_effect=err):
|
||||
# 不应抛
|
||||
df = _safe_top_10_em(symbol="sh920510", date="20250930")
|
||||
assert df.empty
|
||||
|
||||
def test_other_keyerror_reraises(self):
|
||||
"""非 'sdltgd' 的 KeyError 不应被吞 (让重试机制处理)。"""
|
||||
from akshare_static_download import _safe_top_10_em
|
||||
|
||||
# 任意其他 KeyError (网络层错误等), 不应被吞
|
||||
other_err = KeyError("some_other_field")
|
||||
with patch.object(mod.ak, "stock_gdfx_free_top_10_em", side_effect=other_err):
|
||||
with pytest.raises(KeyError, match="some_other_field"):
|
||||
_safe_top_10_em(symbol="sh920510", date="20250930")
|
||||
|
||||
def test_keyerror_sdltgd_no_retry_in_fetch(self, monkeypatch):
|
||||
"""KeyError('sdltgd') 走集成层也不重试 (call_ak_with_retry 只调一次)。"""
|
||||
monkeypatch.setattr(mod, "RETRY_BACKOFF", [0, 0, 0])
|
||||
|
||||
call_count = {"n": 0}
|
||||
|
||||
def side_effect(*args, **kwargs):
|
||||
call_count["n"] += 1
|
||||
raise KeyError("sdltgd")
|
||||
|
||||
with patch.object(mod.ak, "stock_gdfx_free_top_10_em", side_effect=side_effect):
|
||||
df = mod.fetch_top_holders_one_period(symbol="sh920510", period="20250930")
|
||||
|
||||
# 关键: 只调 1 次 (KeyError 被吞, 不重试)
|
||||
assert call_count["n"] == 1, (
|
||||
f"KeyError('sdltgd') 应该被 _safe_top_10_em 一次吞掉, 但调了 {call_count['n']} 次"
|
||||
)
|
||||
assert df.empty
|
||||
assert df.columns.tolist() == EXPECTED_COLS
|
||||
|
||||
|
||||
class TestBuildTopHoldersUnitsSkipBJ:
|
||||
"""build_top_holders_units 跳过北交所 920xxx / 83xxx / 87xxx / 43xxx。
|
||||
|
||||
根因: akshare 东财 stock_gdfx_free_top_10_em 不支持北交所。每只北交所 × 20 期 × 3 retry
|
||||
≈ 60 次失败调用/股 → weekly 跑不完。在 build 阶段直接跳过, 双保险。
|
||||
"""
|
||||
|
||||
def _make_args(self, codes: str = ""):
|
||||
import argparse
|
||||
return argparse.Namespace(codes=codes, limit=0)
|
||||
|
||||
def test_skips_920_prefix(self):
|
||||
"""920xxx (北交所新段) 不进 units。"""
|
||||
codes = [("920510", "BJ"), ("600519", "SH")]
|
||||
units = mod.build_top_holders_units(codes, self._make_args())
|
||||
|
||||
unit_ids = [uid for uid, _ in units]
|
||||
# 920510 应该被跳, 不出现
|
||||
assert not any("920510" in uid for uid in unit_ids), (
|
||||
f"北交所 920510 不该进 units, 但出现了: {unit_ids}"
|
||||
)
|
||||
# 600519 应该在 (每报告期一个 unit)
|
||||
assert any("600519" in uid for uid in unit_ids)
|
||||
|
||||
def test_skips_all_bj_prefixes(self):
|
||||
"""83xx/87xx/43xx (北交所历史段) 全跳。"""
|
||||
codes = [
|
||||
("830789", "BJ"),
|
||||
("870866", "BJ"),
|
||||
("430139", "BJ"),
|
||||
("000001", "SZ"),
|
||||
]
|
||||
units = mod.build_top_holders_units(codes, self._make_args())
|
||||
unit_ids = [uid for uid, _ in units]
|
||||
|
||||
for bj_code in ("830789", "870866", "430139"):
|
||||
assert not any(bj_code in uid for uid in unit_ids), (
|
||||
f"北交所 {bj_code} 不该进 units"
|
||||
)
|
||||
# 000001 应该在
|
||||
assert any("000001" in uid for uid in unit_ids)
|
||||
|
||||
def test_no_bj_codes_arg_mixed(self):
|
||||
"""--codes "920001,600519" 时 920001 跳, 600519 拉。"""
|
||||
units = mod.build_top_holders_units([], self._make_args(codes="920001,600519"))
|
||||
unit_ids = [uid for uid, _ in units]
|
||||
assert not any("920001" in uid for uid in unit_ids)
|
||||
assert any("600519" in uid for uid in unit_ids)
|
||||
|
||||
Reference in New Issue
Block a user