Skip to content

Commit acbd2a8

Browse files
authored
Merge pull request #1816 from ranaroussi/fix/ticker-api
0.2.34 fixes: Add new data to README, remove deprecated stuff, fix tests
2 parents a7c41af + 61c4696 commit acbd2a8

File tree

10 files changed

+138
-158
lines changed

10 files changed

+138
-158
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Change Log
22
===========
33

4+
0.2.35
5+
------
6+
Internal fixes for 0.2.34
7+
48
0.2.34
59
------
610
Features:

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ msft.insider_transactions
111111
msft.insider_purchases
112112
msft.insider_roster_holders
113113

114+
# show recommendations
115+
msft.recommendations
116+
msft.recommendations_summary
117+
msft.upgrades_downgrades
118+
114119
# Show future and historic earnings dates, returns at most next 4 quarters and last 8 quarters by default.
115120
# Note: If more are needed use msft.get_earnings_dates(limit=XX) with increased limit argument.
116121
msft.earnings_dates

meta.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{% set name = "yfinance" %}
2-
{% set version = "0.2.34" %}
2+
{% set version = "0.2.35" %}
33

44
package:
55
name: "{{ name|lower }}"

tests/prices.py

Lines changed: 10 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -399,71 +399,20 @@ def test_dst_fix(self):
399399
raise
400400

401401
def test_prune_post_intraday_us(self):
402-
# Half-day before USA Thanksgiving. Yahoo normally
402+
# Half-day at USA Thanksgiving. Yahoo normally
403403
# returns an interval starting when regular trading closes,
404404
# even if prepost=False.
405405

406406
# Setup
407407
tkr = "AMZN"
408-
interval = "1h"
409-
interval_td = _dt.timedelta(hours=1)
410-
time_open = _dt.time(9, 30)
411-
time_close = _dt.time(16)
412-
special_day = _dt.date(2022, 11, 25)
408+
special_day = _dt.date(2023, 11, 24)
413409
time_early_close = _dt.time(13)
414410
dat = yf.Ticker(tkr, session=self.session)
415411

416412
# Run
417413
start_d = special_day - _dt.timedelta(days=7)
418414
end_d = special_day + _dt.timedelta(days=7)
419-
df = dat.history(start=start_d, end=end_d, interval=interval, prepost=False, keepna=True)
420-
tg_last_dt = df.loc[str(special_day)].index[-1]
421-
self.assertTrue(tg_last_dt.time() < time_early_close)
422-
423-
# Test no other afternoons (or mornings) were pruned
424-
start_d = _dt.date(special_day.year, 1, 1)
425-
end_d = _dt.date(special_day.year+1, 1, 1)
426415
df = dat.history(start=start_d, end=end_d, interval="1h", prepost=False, keepna=True)
427-
last_dts = _pd.Series(df.index).groupby(df.index.date).last()
428-
f_early_close = (last_dts+interval_td).dt.time < time_close
429-
early_close_dates = last_dts.index[f_early_close].values
430-
self.assertEqual(len(early_close_dates), 1)
431-
self.assertEqual(early_close_dates[0], special_day)
432-
433-
first_dts = _pd.Series(df.index).groupby(df.index.date).first()
434-
f_late_open = first_dts.dt.time > time_open
435-
late_open_dates = first_dts.index[f_late_open]
436-
self.assertEqual(len(late_open_dates), 0)
437-
438-
def test_prune_post_intraday_omx(self):
439-
# Half-day before Sweden Christmas. Yahoo normally
440-
# returns an interval starting when regular trading closes,
441-
# even if prepost=False.
442-
# If prepost=False, test that yfinance is removing prepost intervals.
443-
444-
# Setup
445-
tkr = "AEC.ST"
446-
interval = "1h"
447-
interval_td = _dt.timedelta(hours=1)
448-
time_open = _dt.time(9)
449-
time_close = _dt.time(17, 30)
450-
special_day = _dt.date(2022, 12, 23)
451-
time_early_close = _dt.time(13, 2)
452-
dat = yf.Ticker(tkr, session=self.session)
453-
454-
# Half trading day Jan 5, Apr 14, May 25, Jun 23, Nov 4, Dec 23, Dec 30
455-
half_days = [_dt.date(special_day.year, x[0], x[1]) for x in [(1, 5), (4, 14), (5, 25), (6, 23), (11, 4), (12, 23), (12, 30)]]
456-
457-
# Yahoo has incorrectly classified afternoon of 2022-04-13 as post-market.
458-
# Nothing yfinance can do because Yahoo doesn't return data with prepost=False.
459-
# But need to handle in this test.
460-
expected_incorrect_half_days = [_dt.date(2022, 4, 13)]
461-
half_days = sorted(half_days+expected_incorrect_half_days)
462-
463-
# Run
464-
start_d = special_day - _dt.timedelta(days=7)
465-
end_d = special_day + _dt.timedelta(days=7)
466-
df = dat.history(start=start_d, end=end_d, interval=interval, prepost=False, keepna=True)
467416
tg_last_dt = df.loc[str(special_day)].index[-1]
468417
self.assertTrue(tg_last_dt.time() < time_early_close)
469418

@@ -472,40 +421,22 @@ def test_prune_post_intraday_omx(self):
472421
end_d = _dt.date(special_day.year+1, 1, 1)
473422
df = dat.history(start=start_d, end=end_d, interval="1h", prepost=False, keepna=True)
474423
last_dts = _pd.Series(df.index).groupby(df.index.date).last()
475-
f_early_close = (last_dts+interval_td).dt.time < time_close
476-
early_close_dates = last_dts.index[f_early_close].values
477-
unexpected_early_close_dates = [d for d in early_close_dates if d not in half_days]
478-
self.assertEqual(len(unexpected_early_close_dates), 0)
479-
self.assertEqual(len(early_close_dates), len(half_days))
480-
self.assertTrue(_np.equal(early_close_dates, half_days).all())
481-
482-
first_dts = _pd.Series(df.index).groupby(df.index.date).first()
483-
f_late_open = first_dts.dt.time > time_open
484-
late_open_dates = first_dts.index[f_late_open]
485-
self.assertEqual(len(late_open_dates), 0)
424+
dfd = dat.history(start=start_d, end=end_d, interval='1d', prepost=False, keepna=True)
425+
self.assertTrue(_np.equal(dfd.index.date, _pd.to_datetime(last_dts.index).date).all())
486426

487427
def test_prune_post_intraday_asx(self):
488428
# Setup
489429
tkr = "BHP.AX"
490-
interval_td = _dt.timedelta(hours=1)
491-
time_open = _dt.time(10)
492-
time_close = _dt.time(16, 12)
493-
# No early closes in 2022
430+
# No early closes in 2023
494431
dat = yf.Ticker(tkr, session=self.session)
495432

496-
# Test no afternoons (or mornings) were pruned
497-
start_d = _dt.date(2022, 1, 1)
498-
end_d = _dt.date(2022+1, 1, 1)
433+
# Test no other afternoons (or mornings) were pruned
434+
start_d = _dt.date(2023, 1, 1)
435+
end_d = _dt.date(2023+1, 1, 1)
499436
df = dat.history(start=start_d, end=end_d, interval="1h", prepost=False, keepna=True)
500437
last_dts = _pd.Series(df.index).groupby(df.index.date).last()
501-
f_early_close = (last_dts+interval_td).dt.time < time_close
502-
early_close_dates = last_dts.index[f_early_close].values
503-
self.assertEqual(len(early_close_dates), 0)
504-
505-
first_dts = _pd.Series(df.index).groupby(df.index.date).first()
506-
f_late_open = first_dts.dt.time > time_open
507-
late_open_dates = first_dts.index[f_late_open]
508-
self.assertEqual(len(late_open_dates), 0)
438+
dfd = dat.history(start=start_d, end=end_d, interval='1d', prepost=False, keepna=True)
439+
self.assertTrue(_np.equal(dfd.index.date, _pd.to_datetime(last_dts.index).date).all())
509440

510441
def test_weekly_2rows_fix(self):
511442
tkr = "AMZN"

tests/ticker.py

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import unittest
1919
import requests_cache
20-
from typing import Union, Any
20+
from typing import Union, Any, get_args, _GenericAlias
2121
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse
2222

2323
ticker_attributes = (
@@ -31,11 +31,10 @@
3131
("actions", pd.DataFrame),
3232
("shares", pd.DataFrame),
3333
("info", dict),
34-
("calendar", pd.DataFrame),
34+
("calendar", dict),
3535
("recommendations", Union[pd.DataFrame, dict]),
3636
("recommendations_summary", Union[pd.DataFrame, dict]),
3737
("upgrades_downgrades", Union[pd.DataFrame, dict]),
38-
("recommendations_history", Union[pd.DataFrame, dict]),
3938
("earnings", pd.DataFrame),
4039
("quarterly_earnings", pd.DataFrame),
4140
("quarterly_cashflow", pd.DataFrame),
@@ -58,7 +57,12 @@ def assert_attribute_type(testClass: unittest.TestCase, instance, attribute_name
5857
try:
5958
attribute = getattr(instance, attribute_name)
6059
if attribute is not None and expected_type is not Any:
61-
testClass.assertEqual(type(attribute), expected_type)
60+
err_msg = f'{attribute_name} type is {type(attribute)} not {expected_type}'
61+
if isinstance(expected_type, _GenericAlias) and expected_type.__origin__ is Union:
62+
allowed_types = get_args(expected_type)
63+
testClass.assertTrue(isinstance(attribute, allowed_types), err_msg)
64+
else:
65+
testClass.assertEqual(type(attribute), expected_type, err_msg)
6266
except Exception:
6367
testClass.assertRaises(
6468
YFNotImplementedError, lambda: getattr(instance, attribute_name)
@@ -136,8 +140,8 @@ def test_goodTicker_withProxy(self):
136140
tkr = "IBM"
137141
dat = yf.Ticker(tkr, session=self.session, proxy=self.proxy)
138142

139-
dat._fetch_ticker_tz(timeout=5)
140-
dat._get_ticker_tz(timeout=5)
143+
dat._fetch_ticker_tz(proxy=None, timeout=5)
144+
dat._get_ticker_tz(proxy=None, timeout=5)
141145
dat.history(period="1wk")
142146

143147
for attribute_name, attribute_type in ticker_attributes:
@@ -654,6 +658,24 @@ def test_cash_flow_alt_names(self):
654658
def test_bad_freq_value_raises_exception(self):
655659
self.assertRaises(ValueError, lambda: self.ticker.get_cashflow(freq="badarg"))
656660

661+
def test_calendar(self):
662+
data = self.ticker.calendar
663+
self.assertIsInstance(data, dict, "data has wrong type")
664+
self.assertTrue(len(data) > 0, "data is empty")
665+
self.assertIn("Earnings Date", data.keys(), "data missing expected key")
666+
self.assertIn("Earnings Average", data.keys(), "data missing expected key")
667+
self.assertIn("Earnings Low", data.keys(), "data missing expected key")
668+
self.assertIn("Earnings High", data.keys(), "data missing expected key")
669+
self.assertIn("Revenue Average", data.keys(), "data missing expected key")
670+
self.assertIn("Revenue Low", data.keys(), "data missing expected key")
671+
self.assertIn("Revenue High", data.keys(), "data missing expected key")
672+
# dividend date is not available for tested ticker GOOGL
673+
if self.ticker.ticker != "GOOGL":
674+
self.assertIn("Dividend Date", data.keys(), "data missing expected key")
675+
# ex-dividend date is not always available
676+
data_cached = self.ticker.calendar
677+
self.assertIs(data, data_cached, "data not cached")
678+
657679
# Below will fail because not ported to Yahoo API
658680

659681
# def test_sustainability(self):
@@ -664,6 +686,30 @@ def test_bad_freq_value_raises_exception(self):
664686
# data_cached = self.ticker.sustainability
665687
# self.assertIs(data, data_cached, "data not cached")
666688

689+
# def test_shares(self):
690+
# data = self.ticker.shares
691+
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
692+
# self.assertFalse(data.empty, "data is empty")
693+
694+
695+
class TestTickerAnalysts(unittest.TestCase):
696+
session = None
697+
698+
@classmethod
699+
def setUpClass(cls):
700+
cls.session = session_gbl
701+
702+
@classmethod
703+
def tearDownClass(cls):
704+
if cls.session is not None:
705+
cls.session.close()
706+
707+
def setUp(self):
708+
self.ticker = yf.Ticker("GOOGL", session=self.session)
709+
710+
def tearDown(self):
711+
self.ticker = None
712+
667713
def test_recommendations(self):
668714
data = self.ticker.recommendations
669715
data_summary = self.ticker.recommendations_summary
@@ -674,18 +720,16 @@ def test_recommendations(self):
674720
data_cached = self.ticker.recommendations
675721
self.assertIs(data, data_cached, "data not cached")
676722

677-
# def test_recommendations_summary(self): # currently alias for recommendations
678-
# data = self.ticker.recommendations_summary
679-
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
680-
# self.assertFalse(data.empty, "data is empty")
723+
def test_recommendations_summary(self): # currently alias for recommendations
724+
data = self.ticker.recommendations_summary
725+
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
726+
self.assertFalse(data.empty, "data is empty")
681727

682-
# data_cached = self.ticker.recommendations_summary
683-
# self.assertIs(data, data_cached, "data not cached")
728+
data_cached = self.ticker.recommendations_summary
729+
self.assertIs(data, data_cached, "data not cached")
684730

685-
def test_recommendations_history(self): # alias for upgrades_downgrades
731+
def test_upgrades_downgrades(self):
686732
data = self.ticker.upgrades_downgrades
687-
data_history = self.ticker.recommendations_history
688-
self.assertTrue(data.equals(data_history))
689733
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
690734
self.assertFalse(data.empty, "data is empty")
691735
self.assertTrue(len(data.columns) == 4, "data has wrong number of columns")
@@ -695,6 +739,8 @@ def test_recommendations_history(self): # alias for upgrades_downgrades
695739
data_cached = self.ticker.upgrades_downgrades
696740
self.assertIs(data, data_cached, "data not cached")
697741

742+
# Below will fail because not ported to Yahoo API
743+
698744
# def test_analyst_price_target(self):
699745
# data = self.ticker.analyst_price_target
700746
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
@@ -711,28 +757,6 @@ def test_recommendations_history(self): # alias for upgrades_downgrades
711757
# data_cached = self.ticker.revenue_forecasts
712758
# self.assertIs(data, data_cached, "data not cached")
713759

714-
def test_calendar(self):
715-
data = self.ticker.calendar
716-
self.assertIsInstance(data, dict, "data has wrong type")
717-
self.assertTrue(len(data) > 0, "data is empty")
718-
self.assertIn("Earnings Date", data.keys(), "data missing expected key")
719-
self.assertIn("Earnings Average", data.keys(), "data missing expected key")
720-
self.assertIn("Earnings Low", data.keys(), "data missing expected key")
721-
self.assertIn("Earnings High", data.keys(), "data missing expected key")
722-
self.assertIn("Revenue Average", data.keys(), "data missing expected key")
723-
self.assertIn("Revenue Low", data.keys(), "data missing expected key")
724-
self.assertIn("Revenue High", data.keys(), "data missing expected key")
725-
# dividend date is not available for tested ticker GOOGL
726-
if self.ticker.ticker != "GOOGL":
727-
self.assertIn("Dividend Date", data.keys(), "data missing expected key")
728-
# ex-dividend date is not always available
729-
data_cached = self.ticker.calendar
730-
self.assertIs(data, data_cached, "data not cached")
731-
732-
# def test_shares(self):
733-
# data = self.ticker.shares
734-
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
735-
# self.assertFalse(data.empty, "data is empty")
736760

737761

738762
class TestTickerInfo(unittest.TestCase):
@@ -777,11 +801,11 @@ def test_complementary_info(self):
777801

778802
# We don't expect this one to have a trailing PEG ratio
779803
data1 = self.tickers[0].info
780-
self.assertEqual(data1['trailingPegRatio'], None)
804+
self.assertIsNone(data1['trailingPegRatio'])
781805

782806
# This one should have a trailing PEG ratio
783807
data2 = self.tickers[2].info
784-
self.assertEqual(data2['trailingPegRatio'], 1.2713)
808+
self.assertIsInstance(data2['trailingPegRatio'], float)
785809
pass
786810

787811
# def test_fast_info_matches_info(self):

yfinance/base.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@ def history(self, period="1mo", interval="1d",
8686
start=None, end=None, prepost=False, actions=True,
8787
auto_adjust=True, back_adjust=False, repair=False, keepna=False,
8888
proxy=None, rounding=False, timeout=10,
89-
debug=None, # deprecated
9089
raise_errors=False) -> pd.DataFrame:
9190
"""
9291
:Parameters:
@@ -126,23 +125,12 @@ def history(self, period="1mo", interval="1d",
126125
If not None stops waiting for a response after given number of
127126
seconds. (Can also be a fraction of a second e.g. 0.01)
128127
Default is 10 seconds.
129-
debug: bool
130-
If passed as False, will suppress message printing to console.
131-
DEPRECATED, will be removed in future version
132128
raise_errors: bool
133129
If True, then raise errors as Exceptions instead of logging.
134130
"""
135131
logger = utils.get_yf_logger()
136132
proxy = proxy or self.proxy
137133

138-
if debug is not None:
139-
if debug:
140-
utils.print_once(f"yfinance: Ticker.history(debug={debug}) argument is deprecated and will be removed in future version. Do this instead: logging.getLogger('yfinance').setLevel(logging.ERROR)")
141-
logger.setLevel(logging.ERROR)
142-
else:
143-
utils.print_once(f"yfinance: Ticker.history(debug={debug}) argument is deprecated and will be removed in future version. Do this instead to suppress error messages: logging.getLogger('yfinance').setLevel(logging.CRITICAL)")
144-
logger.setLevel(logging.CRITICAL)
145-
146134
start_user = start
147135
end_user = end
148136
if start or period is None or period.lower() == "max":
@@ -395,9 +383,6 @@ def history(self, period="1mo", interval="1d",
395383

396384
df = df[~df.index.duplicated(keep='first')] # must do before repair
397385

398-
if isinstance(repair, str) and repair=='silent':
399-
utils.log_once(logging.WARNING, "yfinance: Ticker.history(repair='silent') value is deprecated and will be removed in future version. Repair now silent by default, use logging module to increase verbosity.")
400-
repair = True
401386
if repair:
402387
# Do this before auto/back adjust
403388
logger.debug(f'{self.ticker}: checking OHLC for repairs ...')

0 commit comments

Comments
 (0)