Skip to content

Commit

Permalink
Merge pull request #1383 from ranaroussi/fix/fast-info-prepost
Browse files Browse the repository at this point in the history
Fix fast_info["previousClose"]
  • Loading branch information
ValueRaider committed Jan 31, 2023
2 parents 84a31ae + 0934298 commit 3e964d5
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 32 deletions.
30 changes: 20 additions & 10 deletions tests/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -702,18 +703,20 @@ 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"]
key_rename_map["day_low"] = ["dayLow", "regularMarketDayLow"]
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"

Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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



Expand Down
90 changes: 71 additions & 19 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()}
Expand Down Expand Up @@ -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"]
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -288,23 +320,38 @@ 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
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
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
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions yfinance/scrapers/quote.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3e964d5

Please sign in to comment.