mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: stabilize yfinance dividend TTM fixtures (#2214)
This commit is contained in:
@@ -8,13 +8,11 @@ graceful degradation when yfinance is unavailable.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
import unittest
|
import unittest
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timezone
|
||||||
from unittest.mock import patch, MagicMock
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
|
||||||
|
|
||||||
from data_provider.yfinance_fundamental_adapter import (
|
from data_provider.yfinance_fundamental_adapter import (
|
||||||
YfinanceFundamentalAdapter,
|
YfinanceFundamentalAdapter,
|
||||||
@@ -22,6 +20,14 @@ from data_provider.yfinance_fundamental_adapter import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_DIVIDEND_EVENT_DATES = (
|
||||||
|
"2025-08-11",
|
||||||
|
"2025-11-10",
|
||||||
|
"2026-02-09",
|
||||||
|
"2026-05-11",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _build_mock_ticker(
|
def _build_mock_ticker(
|
||||||
info: dict,
|
info: dict,
|
||||||
income_stmt: pd.DataFrame | None = None,
|
income_stmt: pd.DataFrame | None = None,
|
||||||
@@ -82,14 +88,6 @@ class TestYfinanceFundamentalAdapter(unittest.TestCase):
|
|||||||
"trailingAnnualDividendRate": 1.04,
|
"trailingAnnualDividendRate": 1.04,
|
||||||
"dividendYield": 0.36,
|
"dividendYield": 0.36,
|
||||||
}
|
}
|
||||||
income_df = pd.DataFrame(
|
|
||||||
{
|
|
||||||
pd.Timestamp("2026-03-31"): {"Total Revenue": 1.11e11, "Net Income": 2.95e10},
|
|
||||||
pd.Timestamp("2025-12-31"): {"Total Revenue": 1.24e11, "Net Income": 3.62e10},
|
|
||||||
pd.Timestamp("2025-09-30"): {"Total Revenue": 9.49e10, "Net Income": 2.49e10},
|
|
||||||
pd.Timestamp("2025-06-30"): {"Total Revenue": 9.40e10, "Net Income": 2.34e10},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
# Need at least 5 columns to trigger statement-derived YoY.
|
# Need at least 5 columns to trigger statement-derived YoY.
|
||||||
income_df_with_yoy = pd.DataFrame(
|
income_df_with_yoy = pd.DataFrame(
|
||||||
{
|
{
|
||||||
@@ -106,18 +104,10 @@ class TestYfinanceFundamentalAdapter(unittest.TestCase):
|
|||||||
pd.Timestamp("2025-12-31"): {"Operating Cash Flow": 3.5e10},
|
pd.Timestamp("2025-12-31"): {"Operating Cash Flow": 3.5e10},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
# Use dates relative to now so the 365-day TTM window always
|
|
||||||
# contains all 4 events regardless of when the test runs (#2204).
|
|
||||||
now_ny = datetime.now(pytz.timezone("America/New_York"))
|
|
||||||
dividends = pd.Series(
|
dividends = pd.Series(
|
||||||
[0.26, 0.26, 0.26, 0.27],
|
[0.26, 0.26, 0.26, 0.27],
|
||||||
index=pd.DatetimeIndex(
|
index=pd.DatetimeIndex(
|
||||||
[
|
_DIVIDEND_EVENT_DATES,
|
||||||
(now_ny - timedelta(days=330)).strftime("%Y-%m-%d"),
|
|
||||||
(now_ny - timedelta(days=240)).strftime("%Y-%m-%d"),
|
|
||||||
(now_ny - timedelta(days=150)).strftime("%Y-%m-%d"),
|
|
||||||
(now_ny - timedelta(days=60)).strftime("%Y-%m-%d"),
|
|
||||||
],
|
|
||||||
tz="America/New_York",
|
tz="America/New_York",
|
||||||
),
|
),
|
||||||
name="Dividends",
|
name="Dividends",
|
||||||
@@ -147,6 +137,7 @@ class TestYfinanceFundamentalAdapter(unittest.TestCase):
|
|||||||
# info.dividendYield (0.36) is intentionally ignored when TTM cash exists.
|
# info.dividendYield (0.36) is intentionally ignored when TTM cash exists.
|
||||||
self.assertAlmostEqual(div["ttm_dividend_yield_pct"], 0.3762, places=4)
|
self.assertAlmostEqual(div["ttm_dividend_yield_pct"], 0.3762, places=4)
|
||||||
self.assertEqual(div["currency"], "USD")
|
self.assertEqual(div["currency"], "USD")
|
||||||
|
self.assertEqual(div["events"][0]["ex_dividend_date"], "2026-05-11")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
bundle["belong_boards"],
|
bundle["belong_boards"],
|
||||||
[
|
[
|
||||||
@@ -160,15 +151,8 @@ class TestYfinanceFundamentalAdapter(unittest.TestCase):
|
|||||||
# Series. Without coercion, `.items()` yields (column_name, Series), every event
|
# Series. Without coercion, `.items()` yields (column_name, Series), every event
|
||||||
# is dropped, and TTM silently falls back to the annual-rate estimate — the real
|
# 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).
|
# bug seen on live US/HK/JP/KR/TW reports (24.0 / "0 次" instead of the true sum).
|
||||||
# Use dates relative to now so the 365-day TTM window is always satisfied (#2204).
|
|
||||||
now_ny = datetime.now(pytz.timezone("America/New_York"))
|
|
||||||
idx = pd.DatetimeIndex(
|
idx = pd.DatetimeIndex(
|
||||||
[
|
_DIVIDEND_EVENT_DATES,
|
||||||
(now_ny - timedelta(days=330)).strftime("%Y-%m-%d"),
|
|
||||||
(now_ny - timedelta(days=240)).strftime("%Y-%m-%d"),
|
|
||||||
(now_ny - timedelta(days=150)).strftime("%Y-%m-%d"),
|
|
||||||
(now_ny - timedelta(days=60)).strftime("%Y-%m-%d"),
|
|
||||||
],
|
|
||||||
tz="America/New_York",
|
tz="America/New_York",
|
||||||
)
|
)
|
||||||
dividends_df = pd.DataFrame({"Dividends": [0.26, 0.26, 0.26, 0.27]}, index=idx)
|
dividends_df = pd.DataFrame({"Dividends": [0.26, 0.26, 0.26, 0.27]}, index=idx)
|
||||||
@@ -191,7 +175,7 @@ class TestYfinanceFundamentalAdapter(unittest.TestCase):
|
|||||||
|
|
||||||
def test_ttm_dividend_window_uses_as_of_date_cutoff(self) -> None:
|
def test_ttm_dividend_window_uses_as_of_date_cutoff(self) -> None:
|
||||||
idx = pd.DatetimeIndex(
|
idx = pd.DatetimeIndex(
|
||||||
["2025-08-11", "2025-11-10", "2026-02-09", "2026-05-11"],
|
_DIVIDEND_EVENT_DATES,
|
||||||
tz="America/New_York",
|
tz="America/New_York",
|
||||||
)
|
)
|
||||||
dividends = pd.Series([0.26, 0.26, 0.26, 0.27], index=idx, name="Dividends")
|
dividends = pd.Series([0.26, 0.26, 0.26, 0.27], index=idx, name="Dividends")
|
||||||
|
|||||||
Reference in New Issue
Block a user