Repository: ranaroussi/yfinance

Issue:
previous_close not working on some stock symbols
not working stock icon
`import yfinance as yf

ticker = yf.Ticker('SOKE.IS').fast_info
previous_close = ticker['previous_close']

print(previous_close)`

working stock icon
`import yfinance as yf

ticker = yf.Ticker('MAVI.IS').fast_info
previous_close = ticker['previous_close']

print(previous_close)`


traceback:
`Traceback (most recent call last):
  File "D:\Python\PyCharm\yfinancetesting\main.py", line 4, in <module>
    previous_close = ticker['previous_close']
                     ~~~~~~^^^^^^^^^^^^^^^^^^
  File "D:\Python\PyCharm\yfinancetesting\venv\Lib\site-packages\yfinance\base.py", line 110, in __getitem__
    return getattr(self, k)
           ^^^^^^^^^^^^^^^^
  File "D:\Python\PyCharm\yfinancetesting\venv\Lib\site-packages\yfinance\base.py", line 247, in previous_close
    self._prev_close = float(prices["Close"].iloc[-2])
                             ~~~~~~~~~~~~~~~~~~~~^^^^
  File "D:\Python\PyCharm\yfinancetesting\venv\Lib\site-packages\pandas\core\indexing.py", line 1073, in __getitem__
    return self._getitem_axis(maybe_callable, axis=axis)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\Python\PyCharm\yfinancetesting\venv\Lib\site-packages\pandas\core\indexing.py", line 1625, in _getitem_axis
    self._validate_integer(key, axis)
  File "D:\Python\PyCharm\yfinancetesting\venv\Lib\site-packages\pandas\core\indexing.py", line 1557, in _validate_integer
    raise IndexError("single positional indexer is out-of-bounds")
IndexError: single positional indexer is out-of-bounds`

- `yfinance` version = 0.2.9
- Python version = 3.11.1
- Windows 11



Discussion:
Interesting ticker. Yahoo normally calculates "previous close" as _before_ last trading day, but Yahoo only has trading data for today. What do you think the previous close should be?
Previous close is wrong for majority of data. 

For example:
adidas AG (ADS.DE)
On summary tab Previous Close is 148.30
fast_info.previous_close returns 145.28

Data on historical data are not the same as data on summary tab.
> Interesting ticker. Yahoo normally calculates "previous close" as _before_ last trading day, but Yahoo only has trading data for today. What do you think the previous close should be?

I just wanted to pull the previous closing info.
OK I got solutions:

> 'SOKE.IS'

Fall back to accessing `info`. No other way to access it.

> 'ADS.DE'

That previous close is in post-market, and Yahoo wasn't returning in a `interval="1d" prepost=True` fetch (some exchanges they do). A `interval="1h" prepost=True` fetch returns correct close.

Resolving patch:
diff --git a/yfinance/base.py b/yfinance/base.py
--- a/yfinance/base.py
+++ b/yfinance/base.py
@@ -55,9 +55,12 @@ 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
 
@@ -86,19 +89,19 @@ def __init__(self, tickerBaseObject):
         # attrs = utils.attributes(self)
         # self.keys = attrs.keys()
         # utils.attributes is calling each method, bad! Have to hardcode
-        orig_keys = ["currency", "exchange", "timezone"]
-        orig_keys += ["shares", "market_cap"]
-        orig_keys += ["last_price", "previous_close", "open", "day_high", "day_low"]
-        orig_keys += ["regular_market_previous_close"]
-        orig_keys += ["last_volume"]
-        orig_keys += ["fifty_day_average", "two_hundred_day_average", "ten_day_average_volume", "three_month_average_volume"]
-        orig_keys += ["year_high", "year_low", "year_change"]
+        _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 orig_keys if not '_' in k]
+        base_keys = [k for k in _properties if not '_' in k]
 
-        sc_keys = [k for k in orig_keys if '_' 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()}
@@ -143,7 +146,7 @@ def toJSON(self, indent=4):
 
     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)
+            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"]
@@ -161,12 +164,22 @@ def _get_1y_prices(self, fullDaysOnly=False):
 
         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")
-        d0 = d1 - utils._interval_to_timedelta("1y")
         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
@@ -209,6 +222,17 @@ def currency(self):
         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:
@@ -249,14 +273,17 @@ def last_price(self):
             self._last_price = self._get_exchange_metadata()["regularMarketPrice"]
         else:
             self._last_price = float(prices["Close"].iloc[-1])
+            if _np.isnan(self._last_price):
+                self._last_price = self._get_exchange_metadata()["regularMarketPrice"]
         return self._last_price
 
     @property
     def previous_close(self):
         if self._prev_close is not None:
             return self._prev_close
-        prices = self._get_1y_prices()
-        if prices.empty:
+        prices = self._get_1wk_1h_prepost_prices()
+        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'.
             # So fallback to original info[] if available.
@@ -272,7 +299,12 @@ 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.empty:
+        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.
@@ -288,7 +320,12 @@ def open(self):
         if self._open is not None:
             return self._open
         prices = self._get_1y_prices()
-        self._open = None if prices.empty else float(prices["Open"].iloc[-1])
+        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
@@ -296,7 +333,12 @@ def day_high(self):
         if self._day_high is not None:
             return self._day_high
         prices = self._get_1y_prices()
-        self._day_high = None if prices.empty else float(prices["High"].iloc[-1])
+        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
@@ -304,7 +346,12 @@ def day_low(self):
         if self._day_low is not None:
             return self._day_low
         prices = self._get_1y_prices()
-        self._day_low = None if prices.empty else float(prices["Low"].iloc[-1])
+        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
@@ -391,6 +438,8 @@ def year_high(self):
             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
 
@@ -400,6 +449,8 @@ def year_low(self):
             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
 
@@ -409,8 +460,9 @@ def year_change(self):
             return self._year_change
 
         prices = self._get_1y_prices(fullDaysOnly=True)
-        self._year_change = (prices["Close"].iloc[-1] - prices["Close"].iloc[0]) / prices["Close"].iloc[0]
-        self._year_change = float(self._year_change)
+        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
diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py
--- a/yfinance/scrapers/quote.py
+++ b/yfinance/scrapers/quote.py
@@ -7,11 +7,11 @@
 from yfinance.data import TickerData
 
 
-info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose", "volume"}
+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"]})
-info_retired_keys_price.update({"fiftyTwoWeekLow", "fiftyTwoWeekHigh", "fiftyTwoWeekChange", "fiftyDayAverage", "twoHundredDayAverage"})
+info_retired_keys_price.update({"fiftyTwoWeekLow", "fiftyTwoWeekHigh", "fiftyTwoWeekChange", "52WeekChange", "fiftyDayAverage", "twoHundredDayAverage"})
 info_retired_keys_price.update({"averageDailyVolume10Day", "averageVolume10days", "averageVolume"})
-info_retired_keys_exchange = {"currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"}
+info_retired_keys_exchange = {"currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName", "quoteType"}
 info_retired_keys_marketCap = {"marketCap"}
 info_retired_keys_symbol = {"symbol"}
 info_retired_keys = info_retired_keys_price | info_retired_keys_exchange | info_retired_keys_marketCap | info_retired_keys_symbol


Test patch:
diff --git a/tests/ticker.py b/tests/ticker.py
--- a/tests/ticker.py
+++ b/tests/ticker.py
@@ -682,6 +682,7 @@ def setUp(self):
         self.symbols += ["ESLT.TA", "BP.L", "GOOGL"]
         self.symbols.append("QCSTIX")  # good for testing, doesn't trade
         self.symbols += ["BTC-USD", "IWO", "VFINX", "^GSPC"]
+        self.symbols += ["SOKE.IS", "ADS.DE"]  # detected bugs
         self.tickers = [yf.Ticker(s, session=self.session) for s in self.symbols]
 
     def tearDown(self):
@@ -702,6 +703,10 @@ def test_fast_info(self):
         fast_info_keys = sorted(list(fast_info_keys))
 
         key_rename_map = {}
+        key_rename_map["currency"] = "currency"
+        key_rename_map["quote_type"] = "quoteType"
+        key_rename_map["timezone"] = "exchangeTimezoneName"
+
         key_rename_map["last_price"] = ["currentPrice", "regularMarketPrice"]
         key_rename_map["open"] = ["open", "regularMarketOpen"]
         key_rename_map["day_high"] = ["dayHigh", "regularMarketDayHigh"]
@@ -709,11 +714,9 @@ def test_fast_info(self):
         key_rename_map["previous_close"] = ["previousClose"]
         key_rename_map["regular_market_previous_close"] = ["regularMarketPreviousClose"]
 
-        # preMarketPrice
-
         key_rename_map["fifty_day_average"] = "fiftyDayAverage"
         key_rename_map["two_hundred_day_average"] = "twoHundredDayAverage"
-        key_rename_map["year_change"] = "52WeekChange"
+        key_rename_map["year_change"] = ["52WeekChange", "fiftyTwoWeekChange"]
         key_rename_map["year_high"] = "fiftyTwoWeekHigh"
         key_rename_map["year_low"] = "fiftyTwoWeekLow"
 
@@ -723,7 +726,6 @@ def test_fast_info(self):
 
         key_rename_map["market_cap"] = "marketCap"
         key_rename_map["shares"] = "sharesOutstanding"
-        key_rename_map["timezone"] = "exchangeTimezoneName"
 
         for k in list(key_rename_map.keys()):
             if '_' in k:
@@ -736,6 +738,7 @@ def test_fast_info(self):
 
         # Loose tolerance for averages, no idea why don't match info[]. Is info wrong?
         custom_tolerances = {}
+        custom_tolerances["year_change"] = 1.0
         # custom_tolerances["ten_day_average_volume"] = 1e-3
         custom_tolerances["ten_day_average_volume"] = 1e-1
         # custom_tolerances["three_month_average_volume"] = 1e-2
@@ -776,12 +779,19 @@ def test_fast_info(self):
                     if k in ["market_cap","marketCap"] and ticker.fast_info["currency"] in ["GBp", "ILA"]:
                         # Adjust for currency to match Yahoo:
                         test *= 0.01
-                    if correct is None:
-                        self.assertTrue(test is None or (not np.isnan(test)), f"{k}: {test} must be None or real value because correct={correct}")
-                    elif isinstance(test, float) or isinstance(correct, int):
-                        self.assertTrue(np.isclose(test, correct, rtol=rtol), f"{k}: {test} != {correct}")
-                    else:
-                        self.assertEqual(test, correct, f"{k}: {test} != {correct}")
+                    try:
+                        if correct is None:
+                            self.assertTrue(test is None or (not np.isnan(test)), f"{k}: {test} must be None or real value because correct={correct}")
+                        elif isinstance(test, float) or isinstance(correct, int):
+                            self.assertTrue(np.isclose(test, correct, rtol=rtol), f"{ticker.ticker} {k}: {test} != {correct}")
+                        else:
+                            self.assertEqual(test, correct, f"{k}: {test} != {correct}")
+                    except:
+                        if k in ["regularMarketPreviousClose"] and ticker.ticker in ["ADS.DE"]:
+                            # Yahoo is wrong, is returning post-market close not regular
+                            continue
+                        else:
+                            raise