mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix(data): parse yfinance dividends from the single-column DataFrame yfinance 1.2.x returns (#1867)
yfinance 1.2.x returns Ticker.dividends as a single-column DataFrame instead of a Series. YfinanceFundamentalAdapter iterated it with `.items()`, which for a DataFrame yields (column_name, Series) — so `_safe_float(Series)` returned None, every dividend event was dropped, and the TTM cash/count silently fell back to `info.trailingAnnualDividendRate` (e.g. 24.0 with "0 次") instead of the true TTM sum. Coerce to a Series before iterating. Affects every yfinance-backed market (US/HK/JP/KR/TW); surfaced on a live TW report (2330.TW showed 24.0 / 0 payouts vs the real ~22 across 4). + a regression test that feeds a single-column DataFrame and asserts the events + TTM sum are parsed. Co-authored-by: zhulinsen <42829555+ZhuLinsen@users.noreply.github.com>
This commit is contained in:
@@ -274,6 +274,12 @@ class YfinanceFundamentalAdapter:
|
||||
result["errors"].append(f"dividends:{type(exc).__name__}")
|
||||
div_series = None
|
||||
if div_series is not None and not div_series.empty:
|
||||
# yfinance 1.2.x returns Ticker.dividends as a single-column DataFrame, not a
|
||||
# Series; coerce so `.items()` yields (timestamp, value) rather than
|
||||
# (column_name, Series). Otherwise every event is dropped (`_safe_float(Series)`
|
||||
# -> None) and TTM silently falls back to the annual-rate estimate.
|
||||
if hasattr(div_series, "columns"):
|
||||
div_series = div_series.iloc[:, 0]
|
||||
try:
|
||||
# Index is timezone-aware (ex-dividend date)
|
||||
cutoff = pd.Timestamp.now(tz=div_series.index.tz) - pd.Timedelta(days=365)
|
||||
|
||||
@@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [修复] #1743 Phase 4 修正 `opencode_cli` 静态指令,避免全局 JSON-only 约束影响 `generate_text()` 与大盘复盘自由文本输出。
|
||||
- [文档] #1743 Phase 4 同步本地 CLI backend 隐私/部署边界:local CLI 不是离线模型,Docker/CI/远端需自行安装登录,DSA 不读取 Claude/OpenCode credential 文件。
|
||||
- [新功能] 台股报告接入三大法人:tw 个股分析报告的 institution 区块改为展示 TWSE T86 / TPEx 三大法人原始买卖超净额(外资/投信/自营/合计,单位:股);tw-only、严格 additive(A股/港股/美股/日韩股 offshore 流程字节不变)、fail-open(取不到数据维持 not_supported,绝不中断分析);不接 Web、不派生 capital_flow_signal、不改评分权重或 schema。
|
||||
- [修复] yfinance 分红解析:yfinance 1.2.x 将 `Ticker.dividends` 返回为单列 DataFrame(而非 Series),适配器按 Series 迭代 `.items()` 导致每笔分红被丢弃、TTM 每股分红与分红次数静默退回 `trailingAnnualDividendRate` 年率估算(如 24.0 / 分红次数 0,而非真实 TTM 合计);现在迭代前强制转为 Series。影响所有 yfinance 后缀市场(美股/港股/日韩股/台股)。
|
||||
- [改进] 台股报告完整消费三大法人:tw 个股报告的 `institution` 区块现会在报告中渲染三大法人净买卖超表格,并注入 LLM 分析 prompt 作为台股筹码过滤器(此前仅接入数据层,报告与 prompt 均未消费,导致报告出现「筹码结构:数据缺失」);同时三大法人整市场抓取改用剩余 stage 预算而非较小的 per-symbol fetch 超时,避免单股/首档分析因冷抓取(~4-5s)超时而降级为 not_supported。tw-only、严格 additive、fail-open。
|
||||
- [修复] 台股财务金额币别标示:TWD 金额此前落入默认「元」(在 A 股语境易误读为人民币),`_CURRENCY_SUFFIX` 补入 TWD→「新台币」,营业收入/归母净利润/经营现金流/每股现金分红均正确标注新台币。
|
||||
- [改进] 台股三大法人 fetcher 韧性加固:(1) 接入熔断器(复用 `realtime_types.CircuitBreaker`,按市场 twse/tpex 分流,连续失败 3 次→冷却 ~5min→半开探测),TWSE/TPEx 端点异常时快速跳过网络往返并 fail-open,避免端点故障时每档个股都付 timeout+throttle;(2) TPEx OpenAPI 仅服务最新交易日,调用方传入与服务日期不符的明确日期时改为 fail-open(返回无数据),避免静默返回错日资料。
|
||||
|
||||
@@ -136,6 +136,32 @@ class TestYfinanceFundamentalAdapter(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_dividends_parsed_from_single_column_dataframe(self) -> None:
|
||||
# yfinance 1.2.x returns Ticker.dividends as a single-column DataFrame, not a
|
||||
# Series. Without coercion, `.items()` yields (column_name, Series), every event
|
||||
# is dropped, and TTM silently falls back to the annual-rate estimate — the real
|
||||
# bug seen on live US/HK/JP/KR/TW reports (24.0 / "0 次" instead of the true sum).
|
||||
idx = pd.DatetimeIndex(
|
||||
["2025-08-11", "2025-11-10", "2026-02-09", "2026-05-11"],
|
||||
tz="America/New_York",
|
||||
)
|
||||
dividends_df = pd.DataFrame({"Dividends": [0.26, 0.26, 0.26, 0.27]}, index=idx)
|
||||
info = {
|
||||
"currency": "USD",
|
||||
"financialCurrency": "USD",
|
||||
"currentPrice": 210,
|
||||
"trailingAnnualDividendRate": 99.0, # a WRONG fallback we must NOT fall back to
|
||||
}
|
||||
ticker = _build_mock_ticker(info, dividends=dividends_df)
|
||||
with patch("yfinance.Ticker", return_value=ticker):
|
||||
bundle = YfinanceFundamentalAdapter().get_fundamental_bundle("AAPL")
|
||||
|
||||
div = bundle["earnings"]["dividend"]
|
||||
self.assertEqual(div["ttm_event_count"], 4) # was 0 before the fix
|
||||
self.assertEqual(len(div["events"]), 4)
|
||||
# summed TTM (0.26*3 + 0.27 = 1.05), NOT the trailingAnnualDividendRate 99.0 fallback
|
||||
self.assertAlmostEqual(div["ttm_cash_dividend_per_share"], 1.05, places=2)
|
||||
|
||||
def test_falls_back_to_info_when_statements_only_have_4_quarters(self) -> None:
|
||||
"""yfinance default is 4 quarters → statement-derived YoY refuses to use QoQ.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user