diff --git a/tests/ticker.py b/tests/ticker.py index ce8741dbb..45c6c0816 100644 --- a/tests/ticker.py +++ b/tests/ticker.py @@ -30,9 +30,11 @@ ("info", dict), ("calendar", pd.DataFrame), ("recommendations", Union[pd.DataFrame, dict]), + ("recommendations_summary", Union[pd.DataFrame, dict]), + ("upgrades_downgrades", Union[pd.DataFrame, dict]), + ("recommendations_history", Union[pd.DataFrame, dict]), ("earnings", pd.DataFrame), ("quarterly_earnings", pd.DataFrame), - ("recommendations_summary", Union[pd.DataFrame, dict]), ("quarterly_cashflow", pd.DataFrame), ("cashflow", pd.DataFrame), ("quarterly_balance_sheet", pd.DataFrame), @@ -645,7 +647,7 @@ def test_recommendations(self): data_cached = self.ticker.recommendations self.assertIs(data, data_cached, "data not cached") - # def test_recommendations_summary(self): + # def test_recommendations_summary(self): # currently alias for recommendations # data = self.ticker.recommendations_summary # self.assertIsInstance(data, pd.DataFrame, "data has wrong type") # self.assertFalse(data.empty, "data is empty") @@ -653,6 +655,19 @@ def test_recommendations(self): # data_cached = self.ticker.recommendations_summary # self.assertIs(data, data_cached, "data not cached") + def test_recommendations_history(self): # alias for upgrades_downgrades + data = self.ticker.upgrades_downgrades + data_history = self.ticker.recommendations_history + self.assertTrue(data.equals(data_history)) + self.assertIsInstance(data, pd.DataFrame, "data has wrong type") + self.assertFalse(data.empty, "data is empty") + self.assertTrue(len(data.columns) == 4, "data has wrong number of columns") + self.assertEqual(data.columns.values.tolist(), ['Firm', 'ToGrade', 'FromGrade', 'Action'], "data has wrong column names") + self.assertIsInstance(data.index, pd.DatetimeIndex, "data has wrong index type") + + data_cached = self.ticker.upgrades_downgrades + self.assertIs(data, data_cached, "data not cached") + # def test_analyst_price_target(self): # data = self.ticker.analyst_price_target # self.assertIsInstance(data, pd.DataFrame, "data has wrong type") diff --git a/yfinance/base.py b/yfinance/base.py index 5e14b6d95..159ced64c 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -1718,6 +1718,21 @@ def get_recommendations(self, proxy=None, as_dict=False): return data.to_dict() return data + def get_recommendations_summary(self, proxy=None, as_dict=False): + return self.get_recommendations(proxy=proxy, as_dict=as_dict) + + def get_upgrades_downgrades(self, proxy=None, as_dict=False): + """ + Returns a DataFrame with the recommendations changes (upgrades/downgrades) + Index: date of grade + Columns: firm toGrade fromGrade action + """ + self._quote.proxy = proxy or self.proxy + data = self._quote.upgrades_downgrades + if as_dict: + return data.to_dict() + return data + def get_calendar(self, proxy=None, as_dict=False): self._quote.proxy = proxy or self.proxy data = self._quote.calendar @@ -1770,9 +1785,6 @@ def get_sustainability(self, proxy=None, as_dict=False): return data.to_dict() return data - def get_recommendations_summary(self, proxy=None, as_dict=False): - return self.get_recommendations(proxy=proxy, as_dict=as_dict) - def get_analyst_price_target(self, proxy=None, as_dict=False): self._analysis.proxy = proxy or self.proxy data = self._analysis.analyst_price_target diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index c09cc8d36..f18a9b04a 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -560,6 +560,7 @@ def __init__(self, data: YfData, symbol: str, proxy=None): self._retired_info = None self._sustainability = None self._recommendations = None + self._upgrades_downgrades = None self._calendar = None self._already_scraped = False @@ -591,6 +592,23 @@ def recommendations(self) -> pd.DataFrame: self._recommendations = pd.DataFrame(data) return self._recommendations + @property + def upgrades_downgrades(self) -> pd.DataFrame: + if self._upgrades_downgrades is None: + result = self._fetch(self.proxy, modules=['upgradeDowngradeHistory']) + try: + data = result["quoteSummary"]["result"][0]["upgradeDowngradeHistory"]["history"] + if len(data) == 0: + raise YFinanceDataException(f"No upgrade/downgrade history found for {self._symbol}") + df = pd.DataFrame(data) + df.rename(columns={"epochGradeDate": "GradeDate", 'firm': 'Firm', 'toGrade': 'ToGrade', 'fromGrade': 'FromGrade', 'action': 'Action'}, inplace=True) + df.set_index('GradeDate', inplace=True) + df.index = pd.to_datetime(df.index, unit='s') + self._upgrades_downgrades = df + except (KeyError, IndexError): + raise YFinanceDataException(f"Failed to parse json response from Yahoo Finance: {result}") + return self._upgrades_downgrades + @property def calendar(self) -> pd.DataFrame: if self._calendar is None: diff --git a/yfinance/ticker.py b/yfinance/ticker.py index af8dd750c..c86414777 100644 --- a/yfinance/ticker.py +++ b/yfinance/ticker.py @@ -153,6 +153,18 @@ def calendar(self) -> _pd.DataFrame: def recommendations(self): return self.get_recommendations() + @property + def recommendations_summary(self): + return self.get_recommendations_summary() + + @property + def upgrades_downgrades(self): + return self.get_upgrades_downgrades() + + @property + def recommendations_history(self): + return self.get_upgrades_downgrades() + @property def earnings(self) -> _pd.DataFrame: return self.get_earnings() @@ -217,10 +229,6 @@ def cashflow(self) -> _pd.DataFrame: def quarterly_cashflow(self) -> _pd.DataFrame: return self.quarterly_cash_flow - @property - def recommendations_summary(self): - return self.get_recommendations_summary() - @property def analyst_price_target(self) -> _pd.DataFrame: return self.get_analyst_price_target()