Repository: ranaroussi/yfinance

Issue:
 Have not implemented fetching 'recommendations' from Yahoo API
CODE:
`import yfinance as yf)`

`tsla = yf.Ticker('TSLA'))`



`recommendations = tsla.get_recommendations())`

`print(recommendations)`


ERROR:
yfinance.exceptions.YFNotImplementedError: Have not implemented fetching 'recommendations' from Yahoo API


Discussion:
It seems that the 'recommendations' feature is not currently implemented in the yfinance library for fetching data from the Yahoo API. The error you encountered indicates that this specific functionality is not available.

To work around this issue, you can try using other libraries or APIs that provide recommendations data for stocks. Some popular alternatives include Alpha Vantage, Finnhub, or using a financial data provider such as Bloomberg Terminal or Refinitiv Eikon.

Here's an example of using the Alpha Vantage API to fetch stock recommendations for Tesla (TSLA):
TRy it and let me know

import requests

API_KEY = 'YOUR_ALPHA_VANTAGE_API_KEY'
symbol = 'TSLA'

url = f'https://www.alphavantage.co/query?function=OVERVIEW&symbol={symbol}&apikey={API_KEY}'

response = requests.get(url)
data = response.json()

if 'RecommendationKey' in data:
    recommendation = data['RecommendationKey']
    print(f"Recommendation for {symbol}: {recommendation}")
else:
    print("Recommendation data not available.")

Thanks for the code, but this url does *NOT* return recommendations for me. According to https://www.alphavantage.co/documentation/#company-overview, function=OVERVIEW response does not include analyst recommendations. My API KEY appears to work okay. Do you maybe have a higher-level service/subscription??
Yes sir I do, what do you want to achieve so I scale, and see my availability for your project 
Ooops! I omitted a *NOT* from my comment above, rendering it incoherent. I apologize. Edited it to now say:
    '...this url does NOT return recommendations for me.'
My question about your possible subscription to AlfaVantage was just to determine whether that could be the reason that you get more information than I do from that url.
Thanks for your patience.
AlphaVantage has changed its web services availability at some point in the last week of 2023-06. "Sustainability" is currently no longer available. Please check on https://www.alphavantage.co/ the updated documentation.
Any movement on this? 
yfinance version = 1.3.5

Still getting 
```
yfinance.exceptions.YFNotImplementedError: Have not implemented fetching 'recommendations' from Yahoo API
```


Yeah, that would be really nice. I'm using version 0.2.28 from Anaconda repository and still getting not implemented exception.
With a little bit of research, it seems that Yahoo Finance has cut off the API for sustainability and recommendations. I think we're going to have to find another way to scrape it. 
@AustinBrownLaw That's hard to believe. Normally the Yahoo webpage fetches data from API. A quick browser inspection shows me the API for ESG scores.
> @AustinBrownLaw That's hard to believe. Normally the Yahoo webpage fetches data from API. A quick browser inspection shows me the API for ESG scores.

I have to admit that I'm pretty new at this--I'm coming from doing more straightforward stats on my own data--but I'm not finding sustainability. When I inspect the site, I'm seeing nothing but formatting around the score written as a float. 
Firefox -> F12 -> Network -> F5. It isn't hard, just takes time.

Resolving patch:
diff --git a/yfinance/base.py b/yfinance/base.py
--- a/yfinance/base.py
+++ b/yfinance/base.py
@@ -1698,6 +1698,10 @@ def _fetch_ticker_tz(self, timeout, proxy=None):
         return None
 
     def get_recommendations(self, proxy=None, as_dict=False):
+        """
+        Returns a DataFrame with the recommendations
+        Columns: period  strongBuy  buy  hold  sell  strongSell
+        """
         self._quote.proxy = proxy or self.proxy
         data = self._quote.recommendations
         if as_dict:
@@ -1757,11 +1761,7 @@ def get_sustainability(self, proxy=None, as_dict=False):
         return data
 
     def get_recommendations_summary(self, proxy=None, as_dict=False):
-        self._quote.proxy = proxy or self.proxy
-        data = self._quote.recommendations
-        if as_dict:
-            return data.to_dict()
-        return data
+        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
diff --git a/yfinance/const.py b/yfinance/const.py
--- a/yfinance/const.py
+++ b/yfinance/const.py
@@ -116,3 +116,39 @@
                   "OtherCashReceiptsfromOperatingActivities", "ReceiptsfromGovernmentGrants", "ReceiptsfromCustomers"]}
 
 price_colnames = ['Open', 'High', 'Low', 'Close', 'Adj Close']
+
+quote_summary_valid_modules = (
+    "summaryProfile",  # contains general information about the company
+    "summaryDetail",  # prices + volume + market cap + etc
+    "assetProfile",  # summaryProfile + company officers
+    "fundProfile",
+    "price",  # current prices
+    "quoteType",  # quoteType
+    "esgScores",  # Environmental, social, and governance (ESG) scores, sustainability and ethical performance of companies
+    "incomeStatementHistory",
+    "incomeStatementHistoryQuarterly",
+    "balanceSheetHistory",
+    "balanceSheetHistoryQuarterly",
+    "cashFlowStatementHistory",
+    "cashFlowStatementHistoryQuarterly",
+    "defaultKeyStatistics",  # KPIs (PE, enterprise value, EPS, EBITA, and more)
+    "financialData",  # Financial KPIs (revenue, gross margins, operating cash flow, free cash flow, and more)
+    "calendarEvents",  # future earnings date
+    "secFilings",  # SEC filings, such as 10K and 10Q reports
+    "upgradeDowngradeHistory",  # upgrades and downgrades that analysts have given a company's stock
+    "institutionOwnership",  # institutional ownership, holders and shares outstanding
+    "fundOwnership",  # mutual fund ownership, holders and shares outstanding
+    "majorDirectHolders",
+    "majorHoldersBreakdown",
+    "insiderTransactions",  # insider transactions, such as the number of shares bought and sold by company executives
+    "insiderHolders",  # insider holders, such as the number of shares held by company executives
+    "netSharePurchaseActivity",  # net share purchase activity, such as the number of shares bought and sold by company executives
+    "earnings",  # earnings history
+    "earningsHistory",
+    "earningsTrend",  # earnings trend
+    "industryTrend",
+    "indexTrend",
+    "sectorTrend",
+    "recommendationTrend",
+    "futuresChain",
+)
diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py
--- a/yfinance/scrapers/quote.py
+++ b/yfinance/scrapers/quote.py
@@ -9,7 +9,8 @@
 
 from yfinance import utils
 from yfinance.data import YfData
-from yfinance.exceptions import YFNotImplementedError
+from yfinance.const import quote_summary_valid_modules, _BASE_URL_
+from yfinance.exceptions import YFNotImplementedError, YFinanceDataException, YFinanceException
 
 info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose", "volume", "volume24Hr"}
 info_retired_keys_price.update({"regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price", "Volume"]})
@@ -21,7 +22,7 @@
 info_retired_keys = info_retired_keys_price | info_retired_keys_exchange | info_retired_keys_marketCap | info_retired_keys_symbol
 
 
-_BASIC_URL_ = "https://query2.finance.yahoo.com/v10/finance/quoteSummary"
+_QUOTE_SUMMARY_URL_ = f"{_BASE_URL_}/v10/finance/quoteSummary"
 
 
 class InfoDictWrapper(MutableMapping):
@@ -569,7 +570,7 @@ def __init__(self, data: YfData, symbol: str, proxy=None):
     @property
     def info(self) -> dict:
         if self._info is None:
-            self._fetch(self.proxy)
+            self._fetch_info(self.proxy)
             self._fetch_complementary(self.proxy)
 
         return self._info
@@ -583,7 +584,12 @@ def sustainability(self) -> pd.DataFrame:
     @property
     def recommendations(self) -> pd.DataFrame:
         if self._recommendations is None:
-            raise YFNotImplementedError('recommendations')
+            result = self._fetch(self.proxy, modules=['recommendationTrend'])
+            try:
+                data = result["quoteSummary"]["result"][0]["recommendationTrend"]["trend"]
+            except (KeyError, IndexError):
+                raise YFinanceDataException(f"Failed to parse json response from Yahoo Finance: {result}")
+            self._recommendations = pd.DataFrame(data)
         return self._recommendations
 
     @property
@@ -592,16 +598,27 @@ def calendar(self) -> pd.DataFrame:
             raise YFNotImplementedError('calendar')
         return self._calendar
 
-    def _fetch(self, proxy):
+    @staticmethod
+    def valid_modules():
+        return quote_summary_valid_modules
+
+    def _fetch(self, proxy, modules: list):
+        if not isinstance(modules, list):
+            raise YFinanceException("Should provide a list of modules, see available modules using `valid_modules`")
+
+        modules = ','.join([m for m in modules if m in quote_summary_valid_modules])
+        if len(modules) == 0:
+            raise YFinanceException("No valid modules provided, see available modules using `valid_modules`")
+        params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "symbol": self._symbol}
+        result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", user_agent_headers=self._data.user_agent_headers, params=params_dict, proxy=proxy)
+        return result
+
+    def _fetch_info(self, proxy):
         if self._already_fetched:
             return
         self._already_fetched = True
         modules = ['financialData', 'quoteType', 'defaultKeyStatistics', 'assetProfile', 'summaryDetail']
-        modules = ','.join(modules)
-        params_dict = {"modules": modules, "ssl": "true"}
-        result = self._data.get_raw_json(
-            _BASIC_URL_ + f"/{self._symbol}", params=params_dict, proxy=proxy
-        )
+        result = self._fetch(proxy, modules=modules)
         result["quoteSummary"]["result"][0]["symbol"] = self._symbol
         query1_info = next(
             (info for info in result.get("quoteSummary", {}).get("result", []) if info["symbol"] == self._symbol),
@@ -643,7 +660,7 @@ def _fetch_complementary(self, proxy):
         self._already_fetched_complementary = True
 
         # self._scrape(proxy)  # decrypt broken
-        self._fetch(proxy)
+        self._fetch_info(proxy)
         if self._info is None:
             return
 


Test patch:
diff --git a/tests/ticker.py b/tests/ticker.py
--- a/tests/ticker.py
+++ b/tests/ticker.py
@@ -627,13 +627,15 @@ def test_bad_freq_value_raises_exception(self):
     #     data_cached = self.ticker.sustainability
     #     self.assertIs(data, data_cached, "data not cached")
 
-    # def test_recommendations(self):
-    #     data = self.ticker.recommendations
-    #     self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
-    #     self.assertFalse(data.empty, "data is empty")
+    def test_recommendations(self):
+        data = self.ticker.recommendations
+        data_summary = self.ticker.recommendations_summary
+        self.assertTrue(data.equals(data_summary))
+        self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
+        self.assertFalse(data.empty, "data is empty")
 
-    #     data_cached = self.ticker.recommendations
-    #     self.assertIs(data, data_cached, "data not cached")
+        data_cached = self.ticker.recommendations
+        self.assertIs(data, data_cached, "data not cached")
 
     # def test_recommendations_summary(self):
     #     data = self.ticker.recommendations_summary