Repository: ranaroussi/yfinance

Issue:
0.2.14 `yf.Ticker("<ticker").info` No Longer has Sector, Industry, Description Data as in 0.2.12
In version 0.2.12, running the code 

```
import yfinance as yf

msft = yf.Ticker("MSFT")

# get all stock info (slow)
msft.info
```

yielded sector, industry, and description, taken from the profile page, as seen below. However, this is not the case on 0.2.14. If this is intentional and the information is moved somewhere else, it is not well documented. Else, this is a bug.

![image](https://user-images.githubusercontent.com/73120774/228978253-b6ab32d9-1084-4de5-8eb8-baf1442009dd.png)



Discussion:
@qianyun210603 replaced web scraping (broken) with an API fetch #1461. Is is possible to get these missing keys?
I've been getting the same data from
```
from yahooquery import Ticker

symbols = 'fb aapl amzn nflx goog'

tickers = Ticker(symbols)

# Retrieve each company's profile information
data = tickers.asset_profile
```

And they use the API (I believe) `yfinance` should be able to as well
@ValueRaider My usage is satisfied with current version and I don't have much time on open source thing recently. Maybe will check later. 
If someone needs that urgently, they can create pr. should not be hard since the api can be found in yahooquery.
> In version 0.2.12, running the code
> 
> ```
> import yfinance as yf
> 
> msft = yf.Ticker("MSFT")
> 
> # get all stock info (slow)
> msft.info
> ```
> 
> yielded sector, industry, and description, taken from the profile page, as seen below. However, this is not the case on 0.2.14. If this is intentional and the information is moved somewhere else, it is not well documented. Else, this is a bug.
> 
> ![image](https://user-images.githubusercontent.com/73120774/228978253-b6ab32d9-1084-4de5-8eb8-baf1442009dd.png)

Hey, I got the same issue here… kind of annoying with those section/industry/summary info missing. Can I downgrade the package to temporarily solve this issue?
Noted here, for inclusiveness (alongside appreciation for the fixes to yf.Ticker that have been provided so far)...  

The other data that appears missing now from yf.Ticker includes (as well as: 
'sector', 
'industry', and 
description - i.e. 'longBusinessSummary), includes the following:

1. 'website'
2. 'longName'
3. 'exchange'
4. 'targetLowPrice'
5. 'targetHighPrice'
6. 'targetMeanPrice'
7. 'targetMedianPrice'
8. 'lastDividendValue'

And simply renamed from prior versions:

1. 'forwardEps' ... is now 'epsForward'

Missing names for data items referenced above align with those that existed from yf.Ticker, pre-decryption issue, version 2.09.





















































I keep getting this error : AttributeError: 'NoneType' object has no attribute 'group' when trying to access .info or any other method.

Resolving patch:
diff --git a/yfinance/base.py b/yfinance/base.py
--- a/yfinance/base.py
+++ b/yfinance/base.py
@@ -40,7 +40,7 @@
 from .scrapers.analysis import Analysis
 from .scrapers.fundamentals import Fundamentals
 from .scrapers.holders import Holders
-from .scrapers.quote import Quote
+from .scrapers.quote import Quote, FastInfo
 import json as _json
 
 _BASE_URL_ = 'https://query2.finance.yahoo.com'
@@ -48,461 +48,6 @@
 _ROOT_URL_ = 'https://finance.yahoo.com'
 
 
-class FastInfo:
-    # Contain small subset of info[] items that can be fetched faster elsewhere.
-    # Imitates a dict.
-    def __init__(self, tickerBaseObject):
-        self._tkr = tickerBaseObject
-
-        self._prices_1y = None
-        self._prices_1wk_1h_prepost = None
-        self._prices_1wk_1h_reg = None
-        self._md = None
-
-        self._currency = None
-        self._quote_type = None
-        self._exchange = None
-        self._timezone = None
-
-        self._shares = None
-        self._mcap = None
-
-        self._open = None
-        self._day_high = None
-        self._day_low = None
-        self._last_price = None
-        self._last_volume = None
-
-        self._prev_close = None
-
-        self._reg_prev_close = None
-
-        self._50d_day_average = None
-        self._200d_day_average = None
-        self._year_high = None
-        self._year_low = None
-        self._year_change = None
-
-        self._10d_avg_vol = None
-        self._3mo_avg_vol = None
-
-        # attrs = utils.attributes(self)
-        # self.keys = attrs.keys()
-        # utils.attributes is calling each method, bad! Have to hardcode
-        _properties = ["currency", "quote_type", "exchange", "timezone"]
-        _properties += ["shares", "market_cap"]
-        _properties += ["last_price", "previous_close", "open", "day_high", "day_low"]
-        _properties += ["regular_market_previous_close"]
-        _properties += ["last_volume"]
-        _properties += ["fifty_day_average", "two_hundred_day_average", "ten_day_average_volume", "three_month_average_volume"]
-        _properties += ["year_high", "year_low", "year_change"]
-
-        # Because released before fixing key case, need to officially support 
-        # camel-case but also secretly support snake-case
-        base_keys = [k for k in _properties if not '_' in k]
-
-        sc_keys = [k for k in _properties if '_' in k]
-
-        self._sc_to_cc_key = {k:utils.snake_case_2_camelCase(k) for k in sc_keys}
-        self._cc_to_sc_key = {v:k for k,v in self._sc_to_cc_key.items()}
- 
-        self._public_keys = sorted(base_keys + list(self._sc_to_cc_key.values()))
-        self._keys = sorted(self._public_keys + sc_keys)
-
-    # dict imitation:
-    def keys(self):
-        return self._public_keys
-    def items(self):
-        return [(k,self[k]) for k in self._public_keys]
-    def values(self):
-        return [self[k] for k in self._public_keys]
-    def get(self, key, default=None):
-        if key in self.keys():
-            if key in self._cc_to_sc_key:
-                key = self._cc_to_sc_key[key]
-            return self[key]
-        return default
-    def __getitem__(self, k):
-        if not isinstance(k, str):
-            raise KeyError(f"key must be a string")
-        if not k in self._keys:
-            raise KeyError(f"'{k}' not valid key. Examine 'FastInfo.keys()'")
-        if k in self._cc_to_sc_key:
-            k = self._cc_to_sc_key[k]
-        return getattr(self, k)
-    def __contains__(self, k):
-        return k in self.keys()
-    def __iter__(self):
-        return iter(self.keys())
-
-    def __str__(self):
-        return "lazy-loading dict with keys = " + str(self.keys())
-    def __repr__(self):
-        return self.__str__()
-
-    def toJSON(self, indent=4):
-        d = {k:self[k] for k in self.keys()}
-        return _json.dumps({k:self[k] for k in self.keys()}, indent=indent)
-
-    def _get_1y_prices(self, fullDaysOnly=False):
-        if self._prices_1y is None:
-            self._prices_1y = self._tkr.history(period="380d", auto_adjust=False, debug=False, keepna=True)
-            self._md = self._tkr.get_history_metadata()
-            try:
-                ctp = self._md["currentTradingPeriod"]
-                self._today_open = pd.to_datetime(ctp["regular"]["start"], unit='s', utc=True).tz_convert(self.timezone)
-                self._today_close = pd.to_datetime(ctp["regular"]["end"], unit='s', utc=True).tz_convert(self.timezone)
-                self._today_midnight = self._today_close.ceil("D")
-            except:
-                self._today_open = None
-                self._today_close = None
-                self._today_midnight = None
-                raise
-
-        if self._prices_1y.empty:
-            return self._prices_1y
-
-        dnow = pd.Timestamp.utcnow().tz_convert(self.timezone).date()
-        d1 = dnow
-        d0 = (d1 + _datetime.timedelta(days=1)) - utils._interval_to_timedelta("1y")
-        if fullDaysOnly and self._exchange_open_now():
-            # Exclude today
-            d1 -= utils._interval_to_timedelta("1d")
-        return self._prices_1y.loc[str(d0):str(d1)]
-
-    def _get_1wk_1h_prepost_prices(self):
-        if self._prices_1wk_1h_prepost is None:
-            self._prices_1wk_1h_prepost = self._tkr.history(period="1wk", interval="1h", auto_adjust=False, prepost=True, debug=False)
-        return self._prices_1wk_1h_prepost
-
-    def _get_1wk_1h_reg_prices(self):
-        if self._prices_1wk_1h_reg is None:
-            self._prices_1wk_1h_reg = self._tkr.history(period="1wk", interval="1h", auto_adjust=False, prepost=False, debug=False)
-        return self._prices_1wk_1h_reg
-
-    def _get_exchange_metadata(self):
-        if self._md is not None:
-            return self._md
-
-        self._get_1y_prices()
-        self._md = self._tkr.get_history_metadata()
-        return self._md
-
-    def _exchange_open_now(self):
-        t = pd.Timestamp.utcnow()
-        self._get_exchange_metadata()
-
-        # if self._today_open is None and self._today_close is None:
-        #     r = False
-        # else:
-        #     r = self._today_open <= t and t < self._today_close
-
-        # if self._today_midnight is None:
-        #     r = False
-        # elif self._today_midnight.date() > t.tz_convert(self.timezone).date():
-        #     r = False
-        # else:
-        #     r = t < self._today_midnight
-
-        last_day_cutoff = self._get_1y_prices().index[-1] + _datetime.timedelta(days=1)
-        last_day_cutoff += _datetime.timedelta(minutes=20)
-        r = t < last_day_cutoff
-
-        # print("_exchange_open_now() returning", r)
-        return r
-
-    @property
-    def currency(self):
-        if self._currency is not None:
-            return self._currency
-
-        if self._tkr._history_metadata is None:
-            self._get_1y_prices()
-        md = self._tkr.get_history_metadata()
-        self._currency = md["currency"]
-        return self._currency
-
-    @property
-    def quote_type(self):
-        if self._quote_type is not None:
-            return self._quote_type
-
-        if self._tkr._history_metadata is None:
-            self._get_1y_prices()
-        md = self._tkr.get_history_metadata()
-        self._quote_type = md["instrumentType"]
-        return self._quote_type
-
-    @property
-    def exchange(self):
-        if self._exchange is not None:
-            return self._exchange
-
-        self._exchange = self._get_exchange_metadata()["exchangeName"]
-        return self._exchange
-
-    @property
-    def timezone(self):
-        if self._timezone is not None:
-            return self._timezone
-
-        self._timezone = self._get_exchange_metadata()["exchangeTimezoneName"]
-        return self._timezone
-
-    @property
-    def shares(self):
-        if self._shares is not None:
-            return self._shares
-
-        shares = self._tkr.get_shares_full(start=pd.Timestamp.utcnow().date()-pd.Timedelta(days=548))
-        if shares is None:
-            # Requesting 18 months failed, so fallback to shares which should include last year
-            shares = self._tkr.get_shares()
-        if shares is not None:
-            if isinstance(shares, pd.DataFrame):
-                shares = shares[shares.columns[0]]
-            self._shares = int(shares.iloc[-1])
-        return self._shares
-
-    @property
-    def last_price(self):
-        if self._last_price is not None:
-            return self._last_price
-        prices = self._get_1y_prices()
-        if prices.empty:
-            md = self._get_exchange_metadata()
-            if "regularMarketPrice" in md:
-                self._last_price = md["regularMarketPrice"]
-        else:
-            self._last_price = float(prices["Close"].iloc[-1])
-            if _np.isnan(self._last_price):
-                md = self._get_exchange_metadata()
-                if "regularMarketPrice" in md:
-                    self._last_price = md["regularMarketPrice"]
-        return self._last_price
-
-    @property
-    def previous_close(self):
-        if self._prev_close is not None:
-            return self._prev_close
-        prices = self._get_1wk_1h_prepost_prices()
-        fail = False
-        if prices.empty:
-            fail = True
-        else:
-            prices = prices[["Close"]].groupby(prices.index.date).last()
-            if prices.shape[0] < 2:
-                # Very few symbols have previousClose despite no 
-                # no trading data e.g. 'QCSTIX'.
-                fail = True
-            else:
-                self._prev_close = float(prices["Close"].iloc[-2])
-        if fail:
-            # Fallback to original info[] if available.
-            self._tkr.info  # trigger fetch
-            k = "previousClose"
-            if self._tkr._quote._retired_info is not None and k in self._tkr._quote._retired_info:
-                self._prev_close = self._tkr._quote._retired_info[k]
-        return self._prev_close
-
-    @property
-    def regular_market_previous_close(self):
-        if self._reg_prev_close is not None:
-            return self._reg_prev_close
-        prices = self._get_1y_prices()
-        if prices.shape[0] == 1:
-            # Tiny % of tickers don't return daily history before last trading day, 
-            # so backup option is hourly history:
-            prices = self._get_1wk_1h_reg_prices()
-            prices = prices[["Close"]].groupby(prices.index.date).last()
-        if prices.shape[0] < 2:
-            # Very few symbols have regularMarketPreviousClose despite no 
-            # no trading data. E.g. 'QCSTIX'.
-            # So fallback to original info[] if available.
-            self._tkr.info  # trigger fetch
-            k = "regularMarketPreviousClose"
-            if self._tkr._quote._retired_info is not None and k in self._tkr._quote._retired_info:
-                self._reg_prev_close = self._tkr._quote._retired_info[k]
-        else:
-            self._reg_prev_close = float(prices["Close"].iloc[-2])
-        return self._reg_prev_close
-
-    @property
-    def open(self):
-        if self._open is not None:
-            return self._open
-        prices = self._get_1y_prices()
-        if prices.empty:
-            self._open = None
-        else:
-            self._open = float(prices["Open"].iloc[-1])
-            if _np.isnan(self._open):
-                self._open = None
-        return self._open
-
-    @property
-    def day_high(self):
-        if self._day_high is not None:
-            return self._day_high
-        prices = self._get_1y_prices()
-        if prices.empty:
-            self._day_high = None
-        else:
-            self._day_high = float(prices["High"].iloc[-1])
-            if _np.isnan(self._day_high):
-                self._day_high = None
-        return self._day_high
-
-    @property
-    def day_low(self):
-        if self._day_low is not None:
-            return self._day_low
-        prices = self._get_1y_prices()
-        if prices.empty:
-            self._day_low = None
-        else:
-            self._day_low = float(prices["Low"].iloc[-1])
-            if _np.isnan(self._day_low):
-                self._day_low = None
-        return self._day_low
-
-    @property
-    def last_volume(self):
-        if self._last_volume is not None:
-            return self._last_volume
-        prices = self._get_1y_prices()
-        self._last_volume = None if prices.empty else int(prices["Volume"].iloc[-1])
-        return self._last_volume
-
-    @property
-    def fifty_day_average(self):
-        if self._50d_day_average is not None:
-            return self._50d_day_average
-
-        prices = self._get_1y_prices(fullDaysOnly=True)
-        if prices.empty:
-            self._50d_day_average = None
-        else:
-            n = prices.shape[0]
-            a = n-50
-            b = n
-            if a < 0:
-                a = 0
-            self._50d_day_average = float(prices["Close"].iloc[a:b].mean())
-
-        return self._50d_day_average
-
-    @property
-    def two_hundred_day_average(self):
-        if self._200d_day_average is not None:
-            return self._200d_day_average
-
-        prices = self._get_1y_prices(fullDaysOnly=True)
-        if prices.empty:
-            self._200d_day_average = None
-        else:
-            n = prices.shape[0]
-            a = n-200
-            b = n
-            if a < 0:
-                a = 0
-
-            self._200d_day_average = float(prices["Close"].iloc[a:b].mean())
-
-        return self._200d_day_average
-
-    @property
-    def ten_day_average_volume(self):
-        if self._10d_avg_vol is not None:
-            return self._10d_avg_vol
-
-        prices = self._get_1y_prices(fullDaysOnly=True)
-        if prices.empty:
-            self._10d_avg_vol = None
-        else:
-            n = prices.shape[0]
-            a = n-10
-            b = n
-            if a < 0:
-                a = 0
-            self._10d_avg_vol = int(prices["Volume"].iloc[a:b].mean())
-
-        return self._10d_avg_vol
-
-    @property
-    def three_month_average_volume(self):
-        if self._3mo_avg_vol is not None:
-            return self._3mo_avg_vol
-
-        prices = self._get_1y_prices(fullDaysOnly=True)
-        if prices.empty:
-            self._3mo_avg_vol = None
-        else:
-            dt1 = prices.index[-1]
-            dt0 = dt1 - utils._interval_to_timedelta("3mo") + utils._interval_to_timedelta("1d")
-            self._3mo_avg_vol = int(prices.loc[dt0:dt1, "Volume"].mean())
-
-        return self._3mo_avg_vol
-
-    @property
-    def year_high(self):
-        if self._year_high is not None:
-            return self._year_high
-
-        prices = self._get_1y_prices(fullDaysOnly=True)
-        if prices.empty:
-            prices = self._get_1y_prices(fullDaysOnly=False)
-        self._year_high = float(prices["High"].max())
-        return self._year_high
-
-    @property
-    def year_low(self):
-        if self._year_low is not None:
-            return self._year_low
-
-        prices = self._get_1y_prices(fullDaysOnly=True)
-        if prices.empty:
-            prices = self._get_1y_prices(fullDaysOnly=False)
-        self._year_low = float(prices["Low"].min())
-        return self._year_low
-
-    @property
-    def year_change(self):
-        if self._year_change is not None:
-            return self._year_change
-
-        prices = self._get_1y_prices(fullDaysOnly=True)
-        if prices.shape[0] >= 2:
-            self._year_change = (prices["Close"].iloc[-1] - prices["Close"].iloc[0]) / prices["Close"].iloc[0]
-            self._year_change = float(self._year_change)
-        return self._year_change
-
-    @property
-    def market_cap(self):
-        if self._mcap is not None:
-            return self._mcap
-
-        try:
-            shares = self.shares
-        except Exception as e:
-            if "Cannot retrieve share count" in str(e):
-                shares = None
-            else:
-                raise
-
-        if shares is None:
-            # Very few symbols have marketCap despite no share count.
-            # E.g. 'BTC-USD'
-            # So fallback to original info[] if available.
-            self._tkr.info
-            k = "marketCap"
-            if self._tkr._quote._retired_info is not None and k in self._tkr._quote._retired_info:
-                self._mcap = self._tkr._quote._retired_info[k]
-        else:
-            self._mcap = float(shares * self.last_price)
-        return self._mcap
-
-
 class TickerBase:
     def __init__(self, ticker, session=None):
         self.ticker = ticker.upper()
diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py
--- a/yfinance/scrapers/quote.py
+++ b/yfinance/scrapers/quote.py
@@ -19,7 +19,7 @@
 
 PRUNE_INFO = True
 # PRUNE_INFO = False
-_BASIC_URL_ = "https://query1.finance.yahoo.com/v7/finance/quote"
+_BASIC_URL_ = "https://query2.finance.yahoo.com/v10/finance/quoteSummary"
 
 
 from collections.abc import MutableMapping
@@ -74,6 +74,462 @@ def _keytransform(self, k):
         return k
 
 
+class FastInfo:
+    # Contain small subset of info[] items that can be fetched faster elsewhere.
+    # Imitates a dict.
+    def __init__(self, tickerBaseObject):
+        utils.print_once("Note: 'info' dict is now fixed & improved, 'fast_info' no longer faster")
+
+        self._tkr = tickerBaseObject
+
+        self._prices_1y = None
+        self._prices_1wk_1h_prepost = None
+        self._prices_1wk_1h_reg = None
+        self._md = None
+
+        self._currency = None
+        self._quote_type = None
+        self._exchange = None
+        self._timezone = None
+
+        self._shares = None
+        self._mcap = None
+
+        self._open = None
+        self._day_high = None
+        self._day_low = None
+        self._last_price = None
+        self._last_volume = None
+
+        self._prev_close = None
+
+        self._reg_prev_close = None
+
+        self._50d_day_average = None
+        self._200d_day_average = None
+        self._year_high = None
+        self._year_low = None
+        self._year_change = None
+
+        self._10d_avg_vol = None
+        self._3mo_avg_vol = None
+
+        # attrs = utils.attributes(self)
+        # self.keys = attrs.keys()
+        # utils.attributes is calling each method, bad! Have to hardcode
+        _properties = ["currency", "quote_type", "exchange", "timezone"]
+        _properties += ["shares", "market_cap"]
+        _properties += ["last_price", "previous_close", "open", "day_high", "day_low"]
+        _properties += ["regular_market_previous_close"]
+        _properties += ["last_volume"]
+        _properties += ["fifty_day_average", "two_hundred_day_average", "ten_day_average_volume", "three_month_average_volume"]
+        _properties += ["year_high", "year_low", "year_change"]
+
+        # Because released before fixing key case, need to officially support 
+        # camel-case but also secretly support snake-case
+        base_keys = [k for k in _properties if not '_' in k]
+
+        sc_keys = [k for k in _properties if '_' in k]
+
+        self._sc_to_cc_key = {k:utils.snake_case_2_camelCase(k) for k in sc_keys}
+        self._cc_to_sc_key = {v:k for k,v in self._sc_to_cc_key.items()}
+ 
+        self._public_keys = sorted(base_keys + list(self._sc_to_cc_key.values()))
+        self._keys = sorted(self._public_keys + sc_keys)
+
+    # dict imitation:
+    def keys(self):
+        return self._public_keys
+    def items(self):
+        return [(k,self[k]) for k in self._public_keys]
+    def values(self):
+        return [self[k] for k in self._public_keys]
+    def get(self, key, default=None):
+        if key in self.keys():
+            if key in self._cc_to_sc_key:
+                key = self._cc_to_sc_key[key]
+            return self[key]
+        return default
+    def __getitem__(self, k):
+        if not isinstance(k, str):
+            raise KeyError(f"key must be a string")
+        if not k in self._keys:
+            raise KeyError(f"'{k}' not valid key. Examine 'FastInfo.keys()'")
+        if k in self._cc_to_sc_key:
+            k = self._cc_to_sc_key[k]
+        return getattr(self, k)
+    def __contains__(self, k):
+        return k in self.keys()
+    def __iter__(self):
+        return iter(self.keys())
+
+    def __str__(self):
+        return "lazy-loading dict with keys = " + str(self.keys())
+    def __repr__(self):
+        return self.__str__()
+
+    def toJSON(self, indent=4):
+        d = {k:self[k] for k in self.keys()}
+        return _json.dumps({k:self[k] for k in self.keys()}, indent=indent)
+
+    def _get_1y_prices(self, fullDaysOnly=False):
+        if self._prices_1y is None:
+            self._prices_1y = self._tkr.history(period="380d", auto_adjust=False, debug=False, keepna=True)
+            self._md = self._tkr.get_history_metadata()
+            try:
+                ctp = self._md["currentTradingPeriod"]
+                self._today_open = pd.to_datetime(ctp["regular"]["start"], unit='s', utc=True).tz_convert(self.timezone)
+                self._today_close = pd.to_datetime(ctp["regular"]["end"], unit='s', utc=True).tz_convert(self.timezone)
+                self._today_midnight = self._today_close.ceil("D")
+            except:
+                self._today_open = None
+                self._today_close = None
+                self._today_midnight = None
+                raise
+
+        if self._prices_1y.empty:
+            return self._prices_1y
+
+        dnow = pd.Timestamp.utcnow().tz_convert(self.timezone).date()
+        d1 = dnow
+        d0 = (d1 + datetime.timedelta(days=1)) - utils._interval_to_timedelta("1y")
+        if fullDaysOnly and self._exchange_open_now():
+            # Exclude today
+            d1 -= utils._interval_to_timedelta("1d")
+        return self._prices_1y.loc[str(d0):str(d1)]
+
+    def _get_1wk_1h_prepost_prices(self):
+        if self._prices_1wk_1h_prepost is None:
+            self._prices_1wk_1h_prepost = self._tkr.history(period="1wk", interval="1h", auto_adjust=False, prepost=True, debug=False)
+        return self._prices_1wk_1h_prepost
+
+    def _get_1wk_1h_reg_prices(self):
+        if self._prices_1wk_1h_reg is None:
+            self._prices_1wk_1h_reg = self._tkr.history(period="1wk", interval="1h", auto_adjust=False, prepost=False, debug=False)
+        return self._prices_1wk_1h_reg
+
+    def _get_exchange_metadata(self):
+        if self._md is not None:
+            return self._md
+
+        self._get_1y_prices()
+        self._md = self._tkr.get_history_metadata()
+        return self._md
+
+    def _exchange_open_now(self):
+        t = pd.Timestamp.utcnow()
+        self._get_exchange_metadata()
+
+        # if self._today_open is None and self._today_close is None:
+        #     r = False
+        # else:
+        #     r = self._today_open <= t and t < self._today_close
+
+        # if self._today_midnight is None:
+        #     r = False
+        # elif self._today_midnight.date() > t.tz_convert(self.timezone).date():
+        #     r = False
+        # else:
+        #     r = t < self._today_midnight
+
+        last_day_cutoff = self._get_1y_prices().index[-1] + datetime.timedelta(days=1)
+        last_day_cutoff += datetime.timedelta(minutes=20)
+        r = t < last_day_cutoff
+
+        # print("_exchange_open_now() returning", r)
+        return r
+
+    @property
+    def currency(self):
+        if self._currency is not None:
+            return self._currency
+
+        if self._tkr._history_metadata is None:
+            self._get_1y_prices()
+        md = self._tkr.get_history_metadata()
+        self._currency = md["currency"]
+        return self._currency
+
+    @property
+    def quote_type(self):
+        if self._quote_type is not None:
+            return self._quote_type
+
+        if self._tkr._history_metadata is None:
+            self._get_1y_prices()
+        md = self._tkr.get_history_metadata()
+        self._quote_type = md["instrumentType"]
+        return self._quote_type
+
+    @property
+    def exchange(self):
+        if self._exchange is not None:
+            return self._exchange
+
+        self._exchange = self._get_exchange_metadata()["exchangeName"]
+        return self._exchange
+
+    @property
+    def timezone(self):
+        if self._timezone is not None:
+            return self._timezone
+
+        self._timezone = self._get_exchange_metadata()["exchangeTimezoneName"]
+        return self._timezone
+
+    @property
+    def shares(self):
+        if self._shares is not None:
+            return self._shares
+
+        shares = self._tkr.get_shares_full(start=pd.Timestamp.utcnow().date()-pd.Timedelta(days=548))
+        if shares is None:
+            # Requesting 18 months failed, so fallback to shares which should include last year
+            shares = self._tkr.get_shares()
+        if shares is not None:
+            if isinstance(shares, pd.DataFrame):
+                shares = shares[shares.columns[0]]
+            self._shares = int(shares.iloc[-1])
+        return self._shares
+
+    @property
+    def last_price(self):
+        if self._last_price is not None:
+            return self._last_price
+        prices = self._get_1y_prices()
+        if prices.empty:
+            md = self._get_exchange_metadata()
+            if "regularMarketPrice" in md:
+                self._last_price = md["regularMarketPrice"]
+        else:
+            self._last_price = float(prices["Close"].iloc[-1])
+            if _np.isnan(self._last_price):
+                md = self._get_exchange_metadata()
+                if "regularMarketPrice" in md:
+                    self._last_price = md["regularMarketPrice"]
+        return self._last_price
+
+    @property
+    def previous_close(self):
+        if self._prev_close is not None:
+            return self._prev_close
+        prices = self._get_1wk_1h_prepost_prices()
+        fail = False
+        if prices.empty:
+            fail = True
+        else:
+            prices = prices[["Close"]].groupby(prices.index.date).last()
+            if prices.shape[0] < 2:
+                # Very few symbols have previousClose despite no 
+                # no trading data e.g. 'QCSTIX'.
+                fail = True
+            else:
+                self._prev_close = float(prices["Close"].iloc[-2])
+        if fail:
+            # Fallback to original info[] if available.
+            self._tkr.info  # trigger fetch
+            k = "previousClose"
+            if self._tkr._quote._retired_info is not None and k in self._tkr._quote._retired_info:
+                self._prev_close = self._tkr._quote._retired_info[k]
+        return self._prev_close
+
+    @property
+    def regular_market_previous_close(self):
+        if self._reg_prev_close is not None:
+            return self._reg_prev_close
+        prices = self._get_1y_prices()
+        if prices.shape[0] == 1:
+            # Tiny % of tickers don't return daily history before last trading day, 
+            # so backup option is hourly history:
+            prices = self._get_1wk_1h_reg_prices()
+            prices = prices[["Close"]].groupby(prices.index.date).last()
+        if prices.shape[0] < 2:
+            # Very few symbols have regularMarketPreviousClose despite no 
+            # no trading data. E.g. 'QCSTIX'.
+            # So fallback to original info[] if available.
+            self._tkr.info  # trigger fetch
+            k = "regularMarketPreviousClose"
+            if self._tkr._quote._retired_info is not None and k in self._tkr._quote._retired_info:
+                self._reg_prev_close = self._tkr._quote._retired_info[k]
+        else:
+            self._reg_prev_close = float(prices["Close"].iloc[-2])
+        return self._reg_prev_close
+
+    @property
+    def open(self):
+        if self._open is not None:
+            return self._open
+        prices = self._get_1y_prices()
+        if prices.empty:
+            self._open = None
+        else:
+            self._open = float(prices["Open"].iloc[-1])
+            if _np.isnan(self._open):
+                self._open = None
+        return self._open
+
+    @property
+    def day_high(self):
+        if self._day_high is not None:
+            return self._day_high
+        prices = self._get_1y_prices()
+        if prices.empty:
+            self._day_high = None
+        else:
+            self._day_high = float(prices["High"].iloc[-1])
+            if _np.isnan(self._day_high):
+                self._day_high = None
+        return self._day_high
+
+    @property
+    def day_low(self):
+        if self._day_low is not None:
+            return self._day_low
+        prices = self._get_1y_prices()
+        if prices.empty:
+            self._day_low = None
+        else:
+            self._day_low = float(prices["Low"].iloc[-1])
+            if _np.isnan(self._day_low):
+                self._day_low = None
+        return self._day_low
+
+    @property
+    def last_volume(self):
+        if self._last_volume is not None:
+            return self._last_volume
+        prices = self._get_1y_prices()
+        self._last_volume = None if prices.empty else int(prices["Volume"].iloc[-1])
+        return self._last_volume
+
+    @property
+    def fifty_day_average(self):
+        if self._50d_day_average is not None:
+            return self._50d_day_average
+
+        prices = self._get_1y_prices(fullDaysOnly=True)
+        if prices.empty:
+            self._50d_day_average = None
+        else:
+            n = prices.shape[0]
+            a = n-50
+            b = n
+            if a < 0:
+                a = 0
+            self._50d_day_average = float(prices["Close"].iloc[a:b].mean())
+
+        return self._50d_day_average
+
+    @property
+    def two_hundred_day_average(self):
+        if self._200d_day_average is not None:
+            return self._200d_day_average
+
+        prices = self._get_1y_prices(fullDaysOnly=True)
+        if prices.empty:
+            self._200d_day_average = None
+        else:
+            n = prices.shape[0]
+            a = n-200
+            b = n
+            if a < 0:
+                a = 0
+
+            self._200d_day_average = float(prices["Close"].iloc[a:b].mean())
+
+        return self._200d_day_average
+
+    @property
+    def ten_day_average_volume(self):
+        if self._10d_avg_vol is not None:
+            return self._10d_avg_vol
+
+        prices = self._get_1y_prices(fullDaysOnly=True)
+        if prices.empty:
+            self._10d_avg_vol = None
+        else:
+            n = prices.shape[0]
+            a = n-10
+            b = n
+            if a < 0:
+                a = 0
+            self._10d_avg_vol = int(prices["Volume"].iloc[a:b].mean())
+
+        return self._10d_avg_vol
+
+    @property
+    def three_month_average_volume(self):
+        if self._3mo_avg_vol is not None:
+            return self._3mo_avg_vol
+
+        prices = self._get_1y_prices(fullDaysOnly=True)
+        if prices.empty:
+            self._3mo_avg_vol = None
+        else:
+            dt1 = prices.index[-1]
+            dt0 = dt1 - utils._interval_to_timedelta("3mo") + utils._interval_to_timedelta("1d")
+            self._3mo_avg_vol = int(prices.loc[dt0:dt1, "Volume"].mean())
+
+        return self._3mo_avg_vol
+
+    @property
+    def year_high(self):
+        if self._year_high is not None:
+            return self._year_high
+
+        prices = self._get_1y_prices(fullDaysOnly=True)
+        if prices.empty:
+            prices = self._get_1y_prices(fullDaysOnly=False)
+        self._year_high = float(prices["High"].max())
+        return self._year_high
+
+    @property
+    def year_low(self):
+        if self._year_low is not None:
+            return self._year_low
+
+        prices = self._get_1y_prices(fullDaysOnly=True)
+        if prices.empty:
+            prices = self._get_1y_prices(fullDaysOnly=False)
+        self._year_low = float(prices["Low"].min())
+        return self._year_low
+
+    @property
+    def year_change(self):
+        if self._year_change is not None:
+            return self._year_change
+
+        prices = self._get_1y_prices(fullDaysOnly=True)
+        if prices.shape[0] >= 2:
+            self._year_change = (prices["Close"].iloc[-1] - prices["Close"].iloc[0]) / prices["Close"].iloc[0]
+            self._year_change = float(self._year_change)
+        return self._year_change
+
+    @property
+    def market_cap(self):
+        if self._mcap is not None:
+            return self._mcap
+
+        try:
+            shares = self.shares
+        except Exception as e:
+            if "Cannot retrieve share count" in str(e):
+                shares = None
+            else:
+                raise
+
+        if shares is None:
+            # Very few symbols have marketCap despite no share count.
+            # E.g. 'BTC-USD'
+            # So fallback to original info[] if available.
+            self._tkr.info
+            k = "marketCap"
+            if self._tkr._quote._retired_info is not None and k in self._tkr._quote._retired_info:
+                self._mcap = self._tkr._quote._retired_info[k]
+        else:
+            self._mcap = float(shares * self.last_price)
+        return self._mcap
+
 
 class Quote:
 
@@ -244,21 +700,43 @@ def _fetch(self, proxy):
         if self._already_fetched:
             return
         self._already_fetched = True
-
+        modules = ['summaryProfile', 'financialData', 'quoteType',
+                     'defaultKeyStatistics', 'assetProfile', 'summaryDetail']
         result = self._data.get_raw_json(
-            _BASIC_URL_, params={"formatted": "true", "lang": "en-US", "symbols": self._data.ticker}, proxy=proxy
+            _BASIC_URL_ + f"/{self._data.ticker}", params={"modules": ",".join(modules), "ssl": "true"}, proxy=proxy
         )
+        result["quoteSummary"]["result"][0]["symbol"] = self._data.ticker
         query1_info = next(
-            (info for info in result.get("quoteResponse", {}).get("result", []) if info["symbol"] == self._data.ticker),
+            (info for info in result.get("quoteSummary", {}).get("result", []) if info["symbol"] == self._data.ticker),
             None,
         )
-        for k, v in query1_info.items():
+        # Most keys that appear in multiple dicts have same value. Except 'maxAge' because
+        # Yahoo not consistent with days vs seconds. Fix it here:
+        for k in query1_info:
+            if "maxAge" in query1_info[k] and query1_info[k]["maxAge"] == 1:
+                query1_info[k]["maxAge"] = 86400
+        query1_info = {
+            k1: v1 
+            for k, v in query1_info.items() 
+            if isinstance(v, dict) 
+            for k1, v1 in v.items() 
+            if v1
+        }
+        # recursively format but only because of 'companyOfficers'
+        def _format(k, v):
             if isinstance(v, dict) and "raw" in v and "fmt" in v:
-                query1_info[k] = v["fmt"] if k in {"regularMarketTime", "postMarketTime"} else v["raw"]
+                v2 = v["fmt"] if k in {"regularMarketTime", "postMarketTime"} else v["raw"]
+            elif isinstance(v, list):
+                v2 = [_format(None, x) for x in v]
+            elif isinstance(v, dict):
+                v2 = {k:_format(k, x) for k, x in v.items()}
             elif isinstance(v, str):
-                query1_info[k] = v.replace("\xa0", " ")
-            elif isinstance(v, (int, bool)):
-                query1_info[k] = v
+                v2 = v.replace("\xa0", " ")
+            else:
+                v2 = v
+            return v2
+        for k, v in query1_info.items():
+            query1_info[k] = _format(k, v)
         self._info = query1_info
 
     def _fetch_complementary(self, proxy):
diff --git a/yfinance/utils.py b/yfinance/utils.py
--- a/yfinance/utils.py
+++ b/yfinance/utils.py
@@ -35,6 +35,7 @@
 import appdirs as _ad
 import sqlite3 as _sqlite3
 import atexit as _atexit
+from functools import lru_cache
 
 from threading import Lock
 
@@ -61,6 +62,13 @@ def attributes(obj):
         if name[0] != '_' and name not in disallowed_names and hasattr(obj, name)}
 
 
+@lru_cache(maxsize=20)
+def print_once(msg):
+    # 'warnings' module suppression of repeat messages does not work. 
+    # This function replicates correct behaviour
+    print(msg)
+
+
 def is_isin(string):
     return bool(_re.match("^([A-Z]{2})([A-Z0-9]{9})([0-9]{1})$", string))
 


Test patch: