Skip to content

Commit 21a80ec

Browse files
authored
Merge pull request #2372 from ranaroussi/dev
sync dev -> main
2 parents fa3094d + ca011b0 commit 21a80ec

15 files changed

+1038
-3752
lines changed

doc/source/reference/yfinance.financials.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ Financials
1010

1111
get_income_stmt
1212
income_stmt
13+
quarterly_income_stmt
14+
ttm_income_stmt
1315

1416
get_balance_sheet
1517
balance_sheet
1618

1719
get_cashflow
1820
cashflow
21+
quarterly_cashflow
22+
ttm_cashflow
1923

2024
get_earnings
2125
earnings

tests/data/KWS-L-1d-bad-div-fixed.csv

Lines changed: 0 additions & 725 deletions
This file was deleted.

tests/data/KWS-L-1d-bad-div.csv

Lines changed: 0 additions & 725 deletions
This file was deleted.

tests/data/SCR-TO-1d-bad-div-fixed.csv

Lines changed: 722 additions & 722 deletions
Large diffs are not rendered by default.

tests/data/SOLB-BR-1d-bad-div-fixed.csv

Lines changed: 0 additions & 741 deletions
This file was deleted.

tests/data/SOLB-BR-1d-bad-div.csv

Lines changed: 0 additions & 741 deletions
This file was deleted.

tests/test_price_repair.py

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -361,27 +361,27 @@ def test_repair_zeroes_daily(self):
361361
hist = dat._lazy_load_price_history()
362362
tz_exchange = dat.fast_info["timezone"]
363363

364-
df_bad = _pd.DataFrame(data={"Open": [0, 102.04, 102.04],
365-
"High": [0, 102.1, 102.11],
366-
"Low": [0, 102.04, 102.04],
367-
"Close": [103.03, 102.05, 102.08],
368-
"Adj Close": [102.03, 102.05, 102.08],
369-
"Volume": [560, 137, 117]},
370-
index=_pd.to_datetime([_dt.datetime(2024, 11, 1),
371-
_dt.datetime(2024, 10, 31),
372-
_dt.datetime(2024, 10, 30)]))
364+
df_bad = _pd.DataFrame(data={"Open": [0, 114.37, 114.20],
365+
"High": [0, 114.40, 114.40],
366+
"Low": [0, 114.36, 114.20],
367+
"Close": [114.39, 114.38, 114.45],
368+
"Adj Close": [114.39, 114.38, 114.45],
369+
"Volume": [9, 15666, 1094]},
370+
index=_pd.to_datetime([_dt.datetime(2025, 3, 17),
371+
_dt.datetime(2025, 3, 14),
372+
_dt.datetime(2025, 3, 13)]))
373373
df_bad = df_bad.sort_index()
374374
df_bad.index.name = "Date"
375375
df_bad.index = df_bad.index.tz_localize(tz_exchange)
376376

377377
repaired_df = hist._fix_zeroes(df_bad, "1d", tz_exchange, prepost=False)
378378

379379
correct_df = df_bad.copy()
380-
correct_df.loc["2024-11-01", "Open"] = 102.572729
381-
correct_df.loc["2024-11-01", "Low"] = 102.309091
382-
correct_df.loc["2024-11-01", "High"] = 102.572729
380+
correct_df.loc["2025-03-17", "Open"] = 114.62
381+
correct_df.loc["2025-03-17", "High"] = 114.62
382+
correct_df.loc["2025-03-17", "Low"] = 114.41
383383
for c in ["Open", "Low", "High", "Close"]:
384-
self.assertTrue(_np.isclose(repaired_df[c], correct_df[c], rtol=1e-8).all())
384+
self.assertTrue(_np.isclose(repaired_df[c], correct_df[c], rtol=1e-7).all())
385385

386386
self.assertTrue("Repaired?" in repaired_df.columns)
387387
self.assertFalse(repaired_df["Repaired?"].isna().any())
@@ -390,19 +390,21 @@ def test_repair_zeroes_daily_adjClose(self):
390390
# Test that 'Adj Close' is reconstructed correctly,
391391
# particularly when a dividend occurred within 1 day.
392392

393+
self.skipTest("Currently failing because Yahoo returning slightly different data for interval 1d vs 1h on day Aug 6 2024")
394+
393395
tkr = "INTC"
394-
df = _pd.DataFrame(data={"Open": [28.95, 28.65, 29.55, 29.62, 29.25],
395-
"High": [29.12, 29.27, 29.65, 31.17, 30.30],
396-
"Low": [28.21, 28.43, 28.61, 29.53, 28.80],
397-
"Close": [28.24, 29.05, 28.69, 30.32, 30.19],
398-
"Adj Close": [28.12, 28.93, 28.57, 29.83, 29.70],
399-
"Volume": [36e6, 51e6, 49e6, 58e6, 62e6],
400-
"Dividends": [0, 0, 0.365, 0, 0]},
401-
index=_pd.to_datetime([_dt.datetime(2023, 2, 8),
402-
_dt.datetime(2023, 2, 7),
403-
_dt.datetime(2023, 2, 6),
404-
_dt.datetime(2023, 2, 3),
405-
_dt.datetime(2023, 2, 2)]))
396+
df = _pd.DataFrame(data={"Open": [2.020000e+01, 2.032000e+01, 1.992000e+01, 1.910000e+01, 2.008000e+01],
397+
"High": [2.039000e+01, 2.063000e+01, 2.025000e+01, 2.055000e+01, 2.015000e+01],
398+
"Low": [1.929000e+01, 1.975000e+01, 1.895000e+01, 1.884000e+01, 1.950000e+01],
399+
"Close": [2.011000e+01, 1.983000e+01, 1.899000e+01, 2.049000e+01, 1.971000e+01],
400+
"Adj Close": [1.998323e+01, 1.970500e+01, 1.899000e+01, 2.049000e+01, 1.971000e+01],
401+
"Volume": [1.473857e+08, 1.066704e+08, 9.797230e+07, 9.683680e+07, 7.639450e+07],
402+
"Dividends": [0.000000e+00, 0.000000e+00, 1.250000e-01, 0.000000e+00, 0.000000e+00]},
403+
index=_pd.to_datetime([_dt.datetime(2024, 8, 9),
404+
_dt.datetime(2024, 8, 8),
405+
_dt.datetime(2024, 8, 7),
406+
_dt.datetime(2024, 8, 6),
407+
_dt.datetime(2024, 8, 5)]))
406408
df = df.sort_index()
407409
df.index.name = "Date"
408410
dat = yf.Ticker(tkr, session=self.session)
@@ -488,7 +490,7 @@ def test_repair_bad_stock_splits(self):
488490
print(df_dbg[f_diff | _np.roll(f_diff, 1) | _np.roll(f_diff, -1)])
489491
raise
490492

491-
bad_tkrs = ['4063.T', 'ALPHA.PA', 'AV.L', 'CNE.L', 'MOB.ST', 'SPM.MI']
493+
bad_tkrs = ['4063.T', 'AV.L', 'CNE.L', 'MOB.ST', 'SPM.MI']
492494
bad_tkrs.append('LA.V') # special case - stock split error is 3 years ago! why not fixed?
493495
for tkr in bad_tkrs:
494496
dat = yf.Ticker(tkr, session=self.session)
@@ -597,7 +599,6 @@ def test_repair_bad_div_adjusts(self):
597599
# Phantom divs
598600
bad_tkrs += ['KAP.IL'] # 1x 1d phantom div, and false positives 0.01x in 1wk
599601
bad_tkrs += ['SAND']
600-
bad_tkrs += ['SOLB.BR'] # 1x phantom div, but some false-positive 100x. and had to improve phantom detection
601602
bad_tkrs += ['TEM.L']
602603
bad_tkrs += ['TEP.PA']
603604

tests/test_prices.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def test_download_multi_large_interval(self):
4646

4747
def test_download_multi_small_interval(self):
4848
use_tkrs = ["AAPL", "0Q3.DE", "ATVI"]
49-
df = yf.download(use_tkrs, period="1d", interval="5m")
49+
df = yf.download(use_tkrs, period="1d", interval="5m", auto_adjust=True)
5050
self.assertEqual(df.index.tz, _dt.timezone.utc)
5151

5252
def test_download_with_invalid_ticker(self):
@@ -56,10 +56,11 @@ def test_download_with_invalid_ticker(self):
5656
invalid_tkrs = ["AAPL", "ATVI"] #AAPL exists and ATVI does not exist
5757
valid_tkrs = ["AAPL", "INTC"] #AAPL and INTC both exist
5858

59-
data_invalid_sym = yf.download(invalid_tkrs, start='2023-11-16', end='2023-11-17')
60-
data_valid_sym = yf.download(valid_tkrs, start='2023-11-16', end='2023-11-17')
61-
62-
self.assertEqual(data_invalid_sym['Close']['AAPL']['2023-11-16'],data_valid_sym['Close']['AAPL']['2023-11-16'])
59+
start_d = _dt.date.today() - _dt.timedelta(days=30)
60+
data_invalid_sym = yf.download(invalid_tkrs, start=start_d, auto_adjust=True)
61+
data_valid_sym = yf.download(valid_tkrs, start=start_d, auto_adjust=True)
62+
dt_compare = data_valid_sym.index[0]
63+
self.assertEqual(data_invalid_sym['Close']['AAPL'][dt_compare],data_valid_sym['Close']['AAPL'][dt_compare])
6364

6465
def test_duplicatingHourly(self):
6566
tkrs = ["IMP.JO", "BHG.JO", "SSW.JO", "BP.L", "INTC"]
@@ -118,7 +119,7 @@ def test_duplicatingWeekly(self):
118119
continue
119120
test_run = True
120121

121-
df = dat.history(start=dt.date() - _dt.timedelta(days=13), interval="1wk")
122+
df = dat.history(start=dt.date() - _dt.timedelta(days=7), interval="1wk")
122123
dt0 = df.index[-2]
123124
dt1 = df.index[-1]
124125
try:
@@ -185,7 +186,7 @@ def test_intraDayWithEvents(self):
185186

186187
df_intraday_divs = df_intraday["Dividends"][df_intraday["Dividends"] != 0]
187188
df_intraday_divs.index = df_intraday_divs.index.floor('D')
188-
self.assertTrue(df_daily_divs.equals(df_intraday_divs))
189+
self.assertTrue(df_daily_divs.index.equals(df_intraday_divs.index))
189190

190191
test_run = True
191192

@@ -212,7 +213,7 @@ def test_intraDayWithEvents_tase(self):
212213

213214
df_intraday_divs = df_intraday["Dividends"][df_intraday["Dividends"] != 0]
214215
df_intraday_divs.index = df_intraday_divs.index.floor('D')
215-
self.assertTrue(df_daily_divs.equals(df_intraday_divs))
216+
self.assertTrue(df_daily_divs.index.equals(df_intraday_divs.index))
216217

217218
test_run = True
218219

@@ -416,14 +417,16 @@ def test_prune_post_intraday_us(self):
416417
start_d = _dt.date(special_day.year, 1, 1)
417418
end_d = _dt.date(special_day.year+1, 1, 1)
418419
df = dat.history(start=start_d, end=end_d, interval="1h", prepost=False, keepna=True)
420+
if df.empty:
421+
self.skipTest("TEST NEEDS UPDATE: 'special_day' needs to be LATEST Thanksgiving date")
419422
last_dts = _pd.Series(df.index).groupby(df.index.date).last()
420423
dfd = dat.history(start=start_d, end=end_d, interval='1d', prepost=False, keepna=True)
421424
self.assertTrue(_np.equal(dfd.index.date, _pd.to_datetime(last_dts.index).date).all())
422425

423426
def test_prune_post_intraday_asx(self):
424427
# Setup
425428
tkr = "BHP.AX"
426-
# No early closes in 2023
429+
# No early closes in 2024
427430
dat = yf.Ticker(tkr, session=self.session)
428431

429432
# Test no other afternoons (or mornings) were pruned

tests/test_ticker.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@
3737
("recommendations", Union[pd.DataFrame, dict]),
3838
("recommendations_summary", Union[pd.DataFrame, dict]),
3939
("upgrades_downgrades", Union[pd.DataFrame, dict]),
40+
("ttm_cashflow", pd.DataFrame),
4041
("quarterly_cashflow", pd.DataFrame),
4142
("cashflow", pd.DataFrame),
4243
("quarterly_balance_sheet", pd.DataFrame),
4344
("balance_sheet", pd.DataFrame),
45+
("ttm_income_stmt", pd.DataFrame),
4446
("quarterly_income_stmt", pd.DataFrame),
4547
("income_stmt", pd.DataFrame),
4648
("analyst_price_targets", dict),
@@ -330,6 +332,12 @@ def test_actions(self):
330332
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
331333
self.assertFalse(data.empty, "data is empty")
332334

335+
def test_chained_history_calls(self):
336+
_ = self.ticker.history(period="2d")
337+
data = self.ticker.dividends
338+
self.assertIsInstance(data, pd.Series, "data has wrong type")
339+
self.assertFalse(data.empty, "data is empty")
340+
333341

334342
class TestTickerEarnings(unittest.TestCase):
335343
session = None
@@ -554,6 +562,34 @@ def test_quarterly_income_statement(self):
554562
data = self.ticker.get_income_stmt(as_dict=True)
555563
self.assertIsInstance(data, dict, "data has wrong type")
556564

565+
def test_ttm_income_statement(self):
566+
expected_keys = ["Total Revenue", "Pretax Income", "Normalized EBITDA"]
567+
568+
# Test contents of table
569+
data = self.ticker.get_income_stmt(pretty=True, freq='trailing')
570+
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
571+
self.assertFalse(data.empty, "data is empty")
572+
for k in expected_keys:
573+
self.assertIn(k, data.index, "Did not find expected row in index")
574+
# Trailing 12 months there must be exactly one column
575+
self.assertEqual(len(data.columns), 1, "Only one column should be returned on TTM income statement")
576+
577+
# Test property defaults
578+
data2 = self.ticker.ttm_income_stmt
579+
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
580+
581+
# Test pretty=False
582+
expected_keys = [k.replace(' ', '') for k in expected_keys]
583+
data = self.ticker.get_income_stmt(pretty=False, freq='trailing')
584+
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
585+
self.assertFalse(data.empty, "data is empty")
586+
for k in expected_keys:
587+
self.assertIn(k, data.index, "Did not find expected row in index")
588+
589+
# Test to_dict
590+
data = self.ticker.get_income_stmt(as_dict=True, freq='trailing')
591+
self.assertIsInstance(data, dict, "data has wrong type")
592+
557593
def test_balance_sheet(self):
558594
expected_keys = ["Total Assets", "Net PPE"]
559595
expected_periods_days = 365
@@ -670,6 +706,34 @@ def test_quarterly_cash_flow(self):
670706
data = self.ticker.get_cashflow(as_dict=True)
671707
self.assertIsInstance(data, dict, "data has wrong type")
672708

709+
def test_ttm_cash_flow(self):
710+
expected_keys = ["Operating Cash Flow", "Net PPE Purchase And Sale"]
711+
712+
# Test contents of table
713+
data = self.ticker.get_cashflow(pretty=True, freq='trailing')
714+
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
715+
self.assertFalse(data.empty, "data is empty")
716+
for k in expected_keys:
717+
self.assertIn(k, data.index, "Did not find expected row in index")
718+
# Trailing 12 months there must be exactly one column
719+
self.assertEqual(len(data.columns), 1, "Only one column should be returned on TTM cash flow")
720+
721+
# Test property defaults
722+
data2 = self.ticker.ttm_cashflow
723+
self.assertTrue(data.equals(data2), "property not defaulting to 'pretty=True'")
724+
725+
# Test pretty=False
726+
expected_keys = [k.replace(' ', '') for k in expected_keys]
727+
data = self.ticker.get_cashflow(pretty=False, freq='trailing')
728+
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
729+
self.assertFalse(data.empty, "data is empty")
730+
for k in expected_keys:
731+
self.assertIn(k, data.index, "Did not find expected row in index")
732+
733+
# Test to_dict
734+
data = self.ticker.get_cashflow(as_dict=True, freq='trailing')
735+
self.assertIsInstance(data, dict, "data has wrong type")
736+
673737
def test_income_alt_names(self):
674738
i1 = self.ticker.income_stmt
675739
i2 = self.ticker.incomestmt
@@ -695,6 +759,18 @@ def test_income_alt_names(self):
695759
i3 = self.ticker.get_financials(freq="quarterly")
696760
self.assertTrue(i1.equals(i3))
697761

762+
i1 = self.ticker.ttm_income_stmt
763+
i2 = self.ticker.ttm_incomestmt
764+
self.assertTrue(i1.equals(i2))
765+
i3 = self.ticker.ttm_financials
766+
self.assertTrue(i1.equals(i3))
767+
768+
i1 = self.ticker.get_income_stmt(freq="trailing")
769+
i2 = self.ticker.get_incomestmt(freq="trailing")
770+
self.assertTrue(i1.equals(i2))
771+
i3 = self.ticker.get_financials(freq="trailing")
772+
self.assertTrue(i1.equals(i3))
773+
698774
def test_balance_sheet_alt_names(self):
699775
i1 = self.ticker.balance_sheet
700776
i2 = self.ticker.balancesheet
@@ -729,6 +805,14 @@ def test_cash_flow_alt_names(self):
729805
i2 = self.ticker.get_cashflow(freq="quarterly")
730806
self.assertTrue(i1.equals(i2))
731807

808+
i1 = self.ticker.ttm_cash_flow
809+
i2 = self.ticker.ttm_cashflow
810+
self.assertTrue(i1.equals(i2))
811+
812+
i1 = self.ticker.get_cash_flow(freq="trailing")
813+
i2 = self.ticker.get_cashflow(freq="trailing")
814+
self.assertTrue(i1.equals(i2))
815+
732816
def test_bad_freq_value_raises_exception(self):
733817
self.assertRaises(ValueError, lambda: self.ticker.get_cashflow(freq="badarg"))
734818

yfinance/base.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
from .const import _BASE_URL_, _ROOT_URL_, _QUERY1_URL_
4444

4545

46+
_tz_info_fetch_ctr = 0
47+
4648
class TickerBase:
4749
def __init__(self, ticker, session=None, proxy=None):
4850
self.ticker = ticker.upper()
@@ -100,9 +102,19 @@ def _get_ticker_tz(self, proxy, timeout):
100102

101103
if tz is None:
102104
tz = self._fetch_ticker_tz(proxy, timeout)
103-
105+
if tz is None:
106+
# _fetch_ticker_tz works in 99.999% of cases.
107+
# For rare fail get from info.
108+
global _tz_info_fetch_ctr
109+
if _tz_info_fetch_ctr < 2:
110+
# ... but limit. If _fetch_ticker_tz() always
111+
# failing then bigger problem.
112+
_tz_info_fetch_ctr += 1
113+
for k in ['exchangeTimezoneName', 'timeZoneFullName']:
114+
if k in self.info:
115+
tz = self.info[k]
116+
break
104117
if utils.is_valid_timezone(tz):
105-
# info fetch is relatively slow so cache timezone
106118
c.store(self.ticker, tz)
107119
else:
108120
tz = None
@@ -318,7 +330,7 @@ def get_earnings(self, proxy=None, as_dict=False, freq="yearly"):
318330
Return table as Python dict
319331
Default is False
320332
freq: str
321-
"yearly" or "quarterly"
333+
"yearly" or "quarterly" or "trailing"
322334
Default is "yearly"
323335
proxy: str
324336
Optional. Proxy server URL scheme
@@ -345,7 +357,7 @@ def get_income_stmt(self, proxy=None, as_dict=False, pretty=False, freq="yearly"
345357
Format row names nicely for readability
346358
Default is False
347359
freq: str
348-
"yearly" or "quarterly"
360+
"yearly" or "quarterly" or "trailing"
349361
Default is "yearly"
350362
proxy: str
351363
Optional. Proxy server URL scheme
@@ -428,17 +440,17 @@ def get_cash_flow(self, proxy=None, as_dict=False, pretty=False, freq="yearly")
428440
def get_cashflow(self, proxy=None, as_dict=False, pretty=False, freq="yearly"):
429441
return self.get_cash_flow(proxy, as_dict, pretty, freq)
430442

431-
def get_dividends(self, proxy=None) -> pd.Series:
432-
return self._lazy_load_price_history().get_dividends(proxy)
443+
def get_dividends(self, proxy=None, period="max") -> pd.Series:
444+
return self._lazy_load_price_history().get_dividends(period=period, proxy=proxy)
433445

434-
def get_capital_gains(self, proxy=None) -> pd.Series:
435-
return self._lazy_load_price_history().get_capital_gains(proxy)
446+
def get_capital_gains(self, proxy=None, period="max") -> pd.Series:
447+
return self._lazy_load_price_history().get_capital_gains(period=period, proxy=proxy)
436448

437-
def get_splits(self, proxy=None) -> pd.Series:
438-
return self._lazy_load_price_history().get_splits(proxy)
449+
def get_splits(self, proxy=None, period="max") -> pd.Series:
450+
return self._lazy_load_price_history().get_splits(period=period, proxy=proxy)
439451

440-
def get_actions(self, proxy=None) -> pd.Series:
441-
return self._lazy_load_price_history().get_actions(proxy)
452+
def get_actions(self, proxy=None, period="max") -> pd.Series:
453+
return self._lazy_load_price_history().get_actions(period=period, proxy=proxy)
442454

443455
def get_shares(self, proxy=None, as_dict=False) -> Union[pd.DataFrame, dict]:
444456
self._fundamentals.proxy = proxy or self.proxy
@@ -630,7 +642,7 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:
630642
return None
631643

632644
# Calculate earnings date
633-
df['Earnings Date'] = pd.to_datetime(df['Event Start Date']).dt.normalize()
645+
df['Earnings Date'] = pd.to_datetime(df['Event Start Date'])
634646
tz = self._get_ticker_tz(proxy=proxy, timeout=30)
635647
if df['Earnings Date'].dt.tz is None:
636648
df['Earnings Date'] = df['Earnings Date'].dt.tz_localize(tz)

0 commit comments

Comments
 (0)