Skip to content

Commit

Permalink
Merge pull request #2034 from ranaroussi/dev
Browse files Browse the repository at this point in the history
sync dev -> main
  • Loading branch information
ValueRaider committed Aug 22, 2024
2 parents a0dc252 + a2b5d6b commit 0193cec
Show file tree
Hide file tree
Showing 61 changed files with 34,750 additions and 809 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ msft.capital_gains # only for mutual funds & etfs
msft.get_shares_full(start="2022-01-01", end=None)

# show financials:
msft.calendar
msft.sec_filings
# - income statement
msft.income_stmt
msft.quarterly_income_stmt
Expand All @@ -126,6 +128,15 @@ msft.recommendations
msft.recommendations_summary
msft.upgrades_downgrades

# show analysts data
msft.analyst_price_targets
msft.earnings_estimate
msft.revenue_estimate
msft.earnings_history
msft.eps_trend
msft.eps_revisions
msft.growth_estimates

# Show future and historic earnings dates, returns at most next 4 quarters and last 8 quarters by default.
# Note: If more are needed use msft.get_earnings_dates(limit=XX) with increased limit argument.
msft.earnings_dates
Expand Down
649 changes: 649 additions & 0 deletions tests/data/1398-HK-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

649 changes: 649 additions & 0 deletions tests/data/1398-HK-1d-bad-div.csv

Large diffs are not rendered by default.

649 changes: 649 additions & 0 deletions tests/data/3988-HK-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

649 changes: 649 additions & 0 deletions tests/data/3988-HK-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/ABDP-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/ABDP-L-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/ADIG-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/ADIG-L-1d-bad-div.csv

Large diffs are not rendered by default.

663 changes: 663 additions & 0 deletions tests/data/CALM-1d-no-bad-divs.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/CLC-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/CLC-L-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/ELCO-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/ELCO-L-1d-bad-div.csv

Large diffs are not rendered by default.

663 changes: 663 additions & 0 deletions tests/data/EWG-1d-no-bad-divs.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/HSBK-IL-1d-no-bad-divs.csv

Large diffs are not rendered by default.

678 changes: 678 additions & 0 deletions tests/data/IBE-MC-1d-no-bad-divs.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/KAP-IL-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/KAP-IL-1d-bad-div.csv

Large diffs are not rendered by default.

652 changes: 652 additions & 0 deletions tests/data/KEN-TA-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

652 changes: 652 additions & 0 deletions tests/data/KEN-TA-1d-bad-div.csv

Large diffs are not rendered by default.

675 changes: 675 additions & 0 deletions tests/data/KME-MI-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

675 changes: 675 additions & 0 deletions tests/data/KME-MI-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/KMR-L-1d-no-bad-divs.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/KWS-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/KWS-L-1d-bad-div.csv

Large diffs are not rendered by default.

665 changes: 665 additions & 0 deletions tests/data/LSC-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

665 changes: 665 additions & 0 deletions tests/data/LSC-L-1d-bad-div.csv

Large diffs are not rendered by default.

663 changes: 663 additions & 0 deletions tests/data/NPK-1d-no-bad-divs.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/NVT-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/NVT-L-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/PSH-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/PSH-L-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/REL-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/REL-L-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/RGL-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/RGL-L-1d-bad-div.csv

Large diffs are not rendered by default.

663 changes: 663 additions & 0 deletions tests/data/SAND-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

663 changes: 663 additions & 0 deletions tests/data/SAND-1d-bad-div.csv

Large diffs are not rendered by default.

663 changes: 663 additions & 0 deletions tests/data/SCR-TO-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

663 changes: 663 additions & 0 deletions tests/data/SCR-TO-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/SERE-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/SERE-L-1d-bad-div.csv

Large diffs are not rendered by default.

678 changes: 678 additions & 0 deletions tests/data/SOLB-BR-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

678 changes: 678 additions & 0 deletions tests/data/SOLB-BR-1d-bad-div.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/TEM-L-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

666 changes: 666 additions & 0 deletions tests/data/TEM-L-1d-bad-div.csv

Large diffs are not rendered by default.

678 changes: 678 additions & 0 deletions tests/data/TEP-PA-1d-bad-div-fixed.csv

Large diffs are not rendered by default.

678 changes: 678 additions & 0 deletions tests/data/TEP-PA-1d-bad-div.csv

Large diffs are not rendered by default.

584 changes: 584 additions & 0 deletions tests/data/TISG-MI-1d-no-bad-divs.csv

Large diffs are not rendered by default.

730 changes: 730 additions & 0 deletions tests/test_price_repair.py

Large diffs are not rendered by default.

512 changes: 9 additions & 503 deletions tests/test_prices.py

Large diffs are not rendered by default.

118 changes: 101 additions & 17 deletions tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@
("balance_sheet", pd.DataFrame),
("quarterly_income_stmt", pd.DataFrame),
("income_stmt", pd.DataFrame),
("analyst_price_target", pd.DataFrame),
("revenue_forecasts", pd.DataFrame),
("analyst_price_targets", dict),
("earnings_estimate", pd.DataFrame),
("revenue_estimate", pd.DataFrame),
("earnings_history", pd.DataFrame),
("eps_trend", pd.DataFrame),
("eps_revisions", pd.DataFrame),
("growth_estimates", pd.DataFrame),
("sustainability", pd.DataFrame),
("options", tuple),
("news", Any),
("earnings_trend", pd.DataFrame),
("earnings_dates", pd.DataFrame),
("earnings_forecasts", pd.DataFrame),
)

def assert_attribute_type(testClass: unittest.TestCase, instance, attribute_name, expected_type):
Expand Down Expand Up @@ -715,9 +718,11 @@ def tearDownClass(cls):

def setUp(self):
self.ticker = yf.Ticker("GOOGL", session=self.session)
self.ticker_no_analysts = yf.Ticker("^GSPC", session=self.session)

def tearDown(self):
self.ticker = None
self.ticker_no_analysts = None

def test_recommendations(self):
data = self.ticker.recommendations
Expand Down Expand Up @@ -748,23 +753,102 @@ def test_upgrades_downgrades(self):
data_cached = self.ticker.upgrades_downgrades
self.assertIs(data, data_cached, "data not cached")

# Below will fail because not ported to Yahoo API
def test_analyst_price_targets(self):
data = self.ticker.analyst_price_targets
self.assertIsInstance(data, dict, "data has wrong type")

# def test_analyst_price_target(self):
# data = self.ticker.analyst_price_target
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
# self.assertFalse(data.empty, "data is empty")
keys = {'current', 'low', 'high', 'mean', 'median'}
self.assertEqual(data.keys(), keys, "data has wrong keys")

# data_cached = self.ticker.analyst_price_target
# self.assertIs(data, data_cached, "data not cached")
data_cached = self.ticker.analyst_price_targets
self.assertIs(data, data_cached, "data not cached")

# def test_revenue_forecasts(self):
# data = self.ticker.revenue_forecasts
# self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
# self.assertFalse(data.empty, "data is empty")
def test_earnings_estimate(self):
data = self.ticker.earnings_estimate
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

# data_cached = self.ticker.revenue_forecasts
# self.assertIs(data, data_cached, "data not cached")
columns = ['numberOfAnalysts', 'avg', 'low', 'high', 'yearAgoEps', 'growth']
self.assertEqual(data.columns.values.tolist(), columns, "data has wrong column names")

index = ['0q', '+1q', '0y', '+1y']
self.assertEqual(data.index.values.tolist(), index, "data has wrong row names")

data_cached = self.ticker.earnings_estimate
self.assertIs(data, data_cached, "data not cached")

def test_revenue_estimate(self):
data = self.ticker.revenue_estimate
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

columns = ['numberOfAnalysts', 'avg', 'low', 'high', 'yearAgoRevenue', 'growth']
self.assertEqual(data.columns.values.tolist(), columns, "data has wrong column names")

index = ['0q', '+1q', '0y', '+1y']
self.assertEqual(data.index.values.tolist(), index, "data has wrong row names")

data_cached = self.ticker.revenue_estimate
self.assertIs(data, data_cached, "data not cached")

def test_earnings_history(self):
data = self.ticker.earnings_history
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

columns = ['epsEstimate', 'epsActual', 'epsDifference', 'surprisePercent']
self.assertEqual(data.columns.values.tolist(), columns, "data has wrong column names")
self.assertIsInstance(data.index, pd.DatetimeIndex, "data has wrong index type")

data_cached = self.ticker.earnings_history
self.assertIs(data, data_cached, "data not cached")

def test_eps_trend(self):
data = self.ticker.eps_trend
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

columns = ['current', '7daysAgo', '30daysAgo', '60daysAgo', '90daysAgo']
self.assertEqual(data.columns.values.tolist(), columns, "data has wrong column names")

index = ['0q', '+1q', '0y', '+1y']
self.assertEqual(data.index.values.tolist(), index, "data has wrong row names")

data_cached = self.ticker.eps_trend
self.assertIs(data, data_cached, "data not cached")

def test_growth_estimates(self):
data = self.ticker.growth_estimates
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertFalse(data.empty, "data is empty")

columns = ['stock', 'industry', 'sector', 'index']
self.assertEqual(data.columns.values.tolist(), columns, "data has wrong column names")

index = ['0q', '+1q', '0y', '+1y', '+5y', '-5y']
self.assertEqual(data.index.values.tolist(), index, "data has wrong row names")

data_cached = self.ticker.growth_estimates
self.assertIs(data, data_cached, "data not cached")

def test_no_analysts(self):
attributes = [
'recommendations',
'upgrades_downgrades',
'earnings_estimate',
'revenue_estimate',
'earnings_history',
'eps_trend',
'growth_estimates',
]

for attribute in attributes:
try:
data = getattr(self.ticker_no_analysts, attribute)
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
self.assertTrue(data.empty, "data is not empty")
except Exception as e:
self.fail(f"Excpetion raised for attribute '{attribute}': {e}")



Expand Down
88 changes: 62 additions & 26 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ def get_calendar(self, proxy=None) -> dict:
self._quote.proxy = proxy or self.proxy
return self._quote.calendar

def get_sec_filings(self, proxy=None) -> dict:
self._quote.proxy = proxy or self.proxy
return self._quote.sec_filings

def get_major_holders(self, proxy=None, as_dict=False):
self._holders.proxy = proxy or self.proxy
data = self._holders.major
Expand Down Expand Up @@ -240,40 +244,67 @@ def get_sustainability(self, proxy=None, as_dict=False):
return data.to_dict()
return data

def get_analyst_price_target(self, proxy=None, as_dict=False):
def get_analyst_price_targets(self, proxy=None) -> dict:
"""
Keys: current low high mean median
"""
self._analysis.proxy = proxy or self.proxy
data = self._analysis.analyst_price_target
if as_dict:
return data.to_dict()
data = self._analysis.analyst_price_targets
return data

def get_rev_forecast(self, proxy=None, as_dict=False):
def get_earnings_estimate(self, proxy=None, as_dict=False):
"""
Index: 0q +1q 0y +1y
Columns: numberOfAnalysts avg low high yearAgoEps growth
"""
self._analysis.proxy = proxy or self.proxy
data = self._analysis.rev_est
if as_dict:
return data.to_dict()
return data
data = self._analysis.earnings_estimate
return data.to_dict() if as_dict else data

def get_earnings_forecast(self, proxy=None, as_dict=False):
def get_revenue_estimate(self, proxy=None, as_dict=False):
"""
Index: 0q +1q 0y +1y
Columns: numberOfAnalysts avg low high yearAgoRevenue growth
"""
self._analysis.proxy = proxy or self.proxy
data = self._analysis.eps_est
if as_dict:
return data.to_dict()
return data
data = self._analysis.revenue_estimate
return data.to_dict() if as_dict else data

def get_earnings_history(self, proxy=None, as_dict=False):
"""
Index: pd.DatetimeIndex
Columns: epsEstimate epsActual epsDifference surprisePercent
"""
self._analysis.proxy = proxy or self.proxy
data = self._analysis.earnings_history
return data.to_dict() if as_dict else data

def get_trend_details(self, proxy=None, as_dict=False):
def get_eps_trend(self, proxy=None, as_dict=False):
"""
Index: 0q +1q 0y +1y
Columns: current 7daysAgo 30daysAgo 60daysAgo 90daysAgo
"""
self._analysis.proxy = proxy or self.proxy
data = self._analysis.analyst_trend_details
if as_dict:
return data.to_dict()
return data
data = self._analysis.eps_trend
return data.to_dict() if as_dict else data

def get_eps_revisions(self, proxy=None, as_dict=False):
"""
Index: 0q +1q 0y +1y
Columns: upLast7days upLast30days downLast7days downLast30days
"""
self._analysis.proxy = proxy or self.proxy
data = self._analysis.eps_revisions
return data.to_dict() if as_dict else data

def get_earnings_trend(self, proxy=None, as_dict=False):
def get_growth_estimates(self, proxy=None, as_dict=False):
"""
Index: 0q +1q 0y +1y +5y -5y
Columns: stock industry sector index
"""
self._analysis.proxy = proxy or self.proxy
data = self._analysis.earnings_trend
if as_dict:
return data.to_dict()
return data
data = self._analysis.growth_estimates
return data.to_dict() if as_dict else data

def get_earnings(self, proxy=None, as_dict=False, freq="yearly"):
"""
Expand Down Expand Up @@ -508,11 +539,16 @@ def get_news(self, proxy=None) -> list:
# Getting data from json
url = f"{_BASE_URL_}/v1/finance/search?q={self.ticker}"
data = self._data.cache_get(url=url, proxy=proxy)
if "Will be right back" in data.text:
if data is None or "Will be right back" in data.text:
raise RuntimeError("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***\n"
"Our engineers are working quickly to resolve "
"the issue. Thank you for your patience.")
data = data.json()
try:
data = data.json()
except (_json.JSONDecodeError):
logger = utils.get_yf_logger()
logger.error(f"{self.ticker}: Failed to retrieve the news and received faulty response instead.")
data = {}

# parse news
self._news = data.get("news", [])
Expand Down
15 changes: 6 additions & 9 deletions yfinance/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@ class YFDataException(YFException):
pass


class YFChartError(YFException):
def __init__(self, ticker, description):
self.ticker = ticker
super().__init__(f"{self.ticker}: {description}")


class YFNotImplementedError(NotImplementedError):
def __init__(self, method_name):
super().__init__(f"Have not implemented fetching '{method_name}' from Yahoo API")
Expand All @@ -27,19 +21,22 @@ def __init__(self, ticker, rationale):

class YFTzMissingError(YFTickerMissingError):
def __init__(self, ticker):
super().__init__(ticker, "No timezone found")
super().__init__(ticker, "no timezone found")


class YFPricesMissingError(YFTickerMissingError):
def __init__(self, ticker, debug_info):
self.debug_info = debug_info
super().__init__(ticker, f"No price data found {debug_info}")
if debug_info != '':
super().__init__(ticker, f"no price data found {debug_info}")
else:
super().__init__(ticker, "no price data found")


class YFEarningsDateMissing(YFTickerMissingError):
# note that this does not get raised. Added in case of raising it in the future
def __init__(self, ticker):
super().__init__(ticker, "No earnings dates found")
super().__init__(ticker, "no earnings dates found")


class YFInvalidPeriodError(YFException):
Expand Down
Loading

0 comments on commit 0193cec

Please sign in to comment.