Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Analyst Data from the Analysis section of Yahoo Finance #1668

Open
wants to merge 2 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion test_yfinance.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ def test_attributes(self):
ticker.cashflow
ticker.quarterly_cashflow
ticker.recommendations_summary
ticker.analyst_price_target
ticker.analyst_growth_estimates
ticker.analyst_trend_details
ticker.rev_est
ticker.eps_est
ticker.revenue_forecasts
ticker.sustainability
ticker.options
Expand Down
67 changes: 66 additions & 1 deletion tests/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def test_goodTicker_withProxy(self):
self.assertIsNotNone(v)
self.assertFalse(v.empty)

v = dat.get_analyst_price_target(proxy=self.proxy)
v = dat.get_analyst_growth_estimates(proxy=self.proxy)
self.assertIsNotNone(v)
self.assertFalse(v.empty)

Expand Down Expand Up @@ -453,6 +453,70 @@ def test_mutualfund_holders(self):
self.assertIs(data, data_cached, "data not cached")


class TestTickerAnalysis(unittest.TestCase):
session = None

@classmethod
def setUpClass(cls):
cls.session = session_gbl

@classmethod
def tearDownClass(cls):
if cls.session is not None:
cls.session.close()

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

def tearDown(self):
self.ticker = None

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

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

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

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

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

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

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

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

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

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


class TestTickerMiscFinancials(unittest.TestCase):
session = None

Expand Down Expand Up @@ -927,6 +991,7 @@ def suite():
suite.addTest(TestTicker('Test ticker'))
suite.addTest(TestTickerEarnings('Test earnings'))
suite.addTest(TestTickerHolders('Test holders'))
suite.addTest(TestTickerAnalysis('Test analysis'))
suite.addTest(TestTickerHistory('Test Ticker history'))
suite.addTest(TestTickerMiscFinancials('Test misc financials'))
suite.addTest(TestTickerInfo('Test info & fast_info'))
Expand Down
4 changes: 2 additions & 2 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1703,9 +1703,9 @@ def get_recommendations_summary(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_growth_estimates(self, proxy=None, as_dict=False):
self._analysis.proxy = proxy
data = self._analysis.analyst_price_target
data = self._analysis.analyst_growth_estimates
if as_dict:
return data.to_dict()
return data
Expand Down
85 changes: 71 additions & 14 deletions yfinance/scrapers/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,105 @@

from yfinance import utils
from yfinance.data import TickerData
from yfinance.exceptions import YFNotImplementedError


class Analysis:
_SCRAPE_URL_ = 'https://finance.yahoo.com/quote'

def __init__(self, data: TickerData, proxy=None):
self._data = data
self.proxy = proxy

self._earnings_trend = None
self._analyst_trend_details = None
self._analyst_price_target = None
self._analyst_growth_estimates = None
self._rev_est = None
self._eps_est = None
self._already_scraped = False

@property
def earnings_trend(self) -> pd.DataFrame:
if self._earnings_trend is None:
raise YFNotImplementedError('earnings_trend')
if self._earnings_trend is None and not self._already_scraped:
self._scrape(self.proxy)
return self._earnings_trend

@property
def analyst_trend_details(self) -> pd.DataFrame:
if self._analyst_trend_details is None:
raise YFNotImplementedError('analyst_trend_details')
if self._analyst_trend_details is None and not self._already_scraped:
self._scrape(self.proxy)
return self._analyst_trend_details

@property
def analyst_price_target(self) -> pd.DataFrame:
if self._analyst_price_target is None:
raise YFNotImplementedError('analyst_price_target')
return self._analyst_price_target
def analyst_growth_estimates(self) -> pd.DataFrame:
if self._analyst_growth_estimates is None and not self._already_scraped:
self._scrape(self.proxy)
return self._analyst_growth_estimates

@property
def rev_est(self) -> pd.DataFrame:
if self._rev_est is None:
raise YFNotImplementedError('rev_est')
if self._rev_est is None and not self._already_scraped:
self._scrape(self.proxy)
return self._rev_est

@property
def eps_est(self) -> pd.DataFrame:
if self._eps_est is None:
raise YFNotImplementedError('eps_est')
if self._eps_est is None and not self._already_scraped:
self._scrape(self.proxy)
return self._eps_est

def _scrape(self, proxy):
ticker_url = f"{self._SCRAPE_URL_}/{self._data.ticker}"
try:
resp = self._data.cache_get(ticker_url + '/analysis', proxy=proxy)
analysis = pd.read_html(resp.text)
except Exception:
analysis = []

analysis_dict = {df.columns[0]: df for df in analysis}

for key, item in analysis_dict.items():
# Set index
item = item.set_index(key)
if key in ['Earnings History', 'Revenue Estimate']:
# Flip rows/columns
item = item.T
if key == 'Earnings History':
item.index = pd.to_datetime(item.index)

for c in item.columns:
# Format % columns
if item[c].dtype in ['str', 'object']:
if item[c].str.endswith('%').sum() == item.shape[0]:
# All % so convert to numeric
item[c] = item[c].str.rstrip('%').astype("float")
if not '%' in c:
item = item.rename(columns={c:c+' %'})
c += ' %'

else:
# convert number-like values to integer type
f = item[c].str.endswith(('K', 'M', 'B', 'T'))
if f.any():
fB = item[c].str.endswith('B')
fM = item[c].str.endswith('M')
fK = item[c].str.endswith('K')
fT = item[c].str.endswith('T')
item[c] = item[c].str.rstrip('KMBT').astype("float")
item.loc[fB, c] *= 1e9
item.loc[fM, c] *= 1e6
item.loc[fK, c] *= 1e3
item.loc[fT, c] *= 1e12
item[c] = item[c].astype("int")

if key == 'Earnings History':
self._earnings_trend = item
elif key == 'EPS Trend':
self._analyst_trend_details = item
elif key == 'Growth Estimates':
self._analyst_growth_estimates = item
elif key == 'Revenue Estimate':
self._rev_est = item
elif key == 'Earnings Estimate':
self._eps_est = item

self._already_scraped = True
14 changes: 11 additions & 3 deletions yfinance/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ def recommendations_summary(self):
return self.get_recommendations_summary()

@property
def analyst_price_target(self) -> _pd.DataFrame:
return self.get_analyst_price_target()
def analyst_growth_estimates(self) -> _pd.DataFrame:
return self.get_analyst_growth_estimates()

@property
def revenue_forecasts(self) -> _pd.DataFrame:
Expand All @@ -244,13 +244,21 @@ def news(self):
return self.get_news()

@property
def trend_details(self) -> _pd.DataFrame:
def analyst_trend_details(self) -> _pd.DataFrame:
return self.get_trend_details()

@property
def earnings_trend(self) -> _pd.DataFrame:
return self.get_earnings_trend()

@property
def rev_est(self) -> _pd.DataFrame:
return self.get_rev_forecast()

@property
def eps_est(self) -> _pd.DataFrame:
return self.get_earnings_forecast()

@property
def earnings_dates(self) -> _pd.DataFrame:
return self.get_earnings_dates()
Expand Down