From 2865c0df9f20f60d53623ee38ded612d02ecfa2f Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Sat, 14 Jan 2023 22:20:48 +0000 Subject: [PATCH 01/11] Prune info[] with migration instructions Remove redundant keys from info[] that are better found elsewhere ; Print instructions if old keys accessed via InfoDictWrapper --- yfinance/scrapers/quote.py | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index e799ceae1..c0e44ed6f 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -7,6 +7,52 @@ from yfinance.data import TickerData +from collections.abc import MutableMapping +class InfoDictWrapper(MutableMapping): + """ Simple wrapper around info dict, to print messages for specific keys + instructing how to retrieve with new methods""" + + + def __init__(self, info): + self.info = info + + self.redundant_keys_price = ["currentPrice", "dayHigh", "dayLow", "open", "previousClose"] + self.redundant_keys_price += ["regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price"]] + self.redundant_keys_exchange = ["currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"] + self.redundant_keys_marketCap = ["marketCap"] + + def __contains__(self, k): + if k in self.redundant_keys_price: + print(f"Price data removed from info. Use Ticker.history(period='1wk') instead") + return False + elif k in self.redundant_keys_exchange: + print(f"Exchange data removed from info. Use Ticker.get_history_metadata() instead") + return False + elif k in self.redundant_keys_marketCap: + print(f"Market cap removed from info. Calculate using new Ticker.get_shares_full() * price") + return False + return k in self.info.keys() + + def __getitem__(self, key): + return self.info[self._keytransform(key)] + + def __setitem__(self, key, value): + self.info[self._keytransform(key)] = value + + def __delitem__(self, key): + del self.info[self._keytransform(key)] + + def __iter__(self): + return iter(self.info) + + def __len__(self): + return len(self.info) + + def _keytransform(self, key): + return key + + + class Quote: def __init__(self, data: TickerData, proxy=None): @@ -130,6 +176,18 @@ def _scrape(self, proxy): except Exception: pass + # Delete redundant info[] keys, because values can be accessed faster + # elsewhere - e.g. price keys. Hope is reduces Yahoo spam effect. + redundant_keys = ["currentPrice", "dayHigh", "dayLow", "open", "previousClose"] + redundant_keys += ["regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price"]] + redundant_keys += ["currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"] + redundant_keys += ["marketCap"] + for k in redundant_keys: + if k in self._info: + del self._info[k] + # InfoDictWrapper will explain how to access above data elsewhere + self._info = InfoDictWrapper(self._info) + # events try: cal = pd.DataFrame(quote_summary_store['calendarEvents']['earnings']) From 97671b78ddc9ebeea2703775946a4cfc8b9f5aea Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Sat, 14 Jan 2023 23:11:02 +0000 Subject: [PATCH 02/11] Move info migrate msgs from 'is in' to '[]' --- yfinance/scrapers/quote.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index c0e44ed6f..ea4d1abd8 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -22,25 +22,25 @@ def __init__(self, info): self.redundant_keys_marketCap = ["marketCap"] def __contains__(self, k): + return k in self.info.keys() + + def __getitem__(self, k): if k in self.redundant_keys_price: print(f"Price data removed from info. Use Ticker.history(period='1wk') instead") - return False + return None elif k in self.redundant_keys_exchange: print(f"Exchange data removed from info. Use Ticker.get_history_metadata() instead") - return False + return None elif k in self.redundant_keys_marketCap: print(f"Market cap removed from info. Calculate using new Ticker.get_shares_full() * price") - return False - return k in self.info.keys() - - def __getitem__(self, key): - return self.info[self._keytransform(key)] + return None + return self.info[self._keytransform(k)] - def __setitem__(self, key, value): - self.info[self._keytransform(key)] = value + def __setitem__(self, k, value): + self.info[self._keytransform(k)] = value - def __delitem__(self, key): - del self.info[self._keytransform(key)] + def __delitem__(self, k): + del self.info[self._keytransform(k)] def __iter__(self): return iter(self.info) @@ -48,8 +48,8 @@ def __iter__(self): def __len__(self): return len(self.info) - def _keytransform(self, key): - return key + def _keytransform(self, k): + return k From 677bbfed8b3e9b792a38aa92fc8c5b4b990a8f73 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Mon, 16 Jan 2023 11:23:35 +0000 Subject: [PATCH 03/11] Add Ticker.market_cap helper ; Tidy info[] blacklist --- README.md | 3 +++ yfinance/base.py | 18 ++++++++++++++++++ yfinance/scrapers/quote.py | 29 +++++++++++++++-------------- yfinance/ticker.py | 4 ++++ 4 files changed, 40 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c3ff752c3..783fbf7d0 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,9 @@ msft.capital_gains msft.shares msft.get_shares_full() +# calculate market cap +msft.market_cap + # show financials: # - income statement msft.income_stmt diff --git a/yfinance/base.py b/yfinance/base.py index 34deef9b0..5b5fd0a88 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -1172,6 +1172,24 @@ def get_shares_full(self, start=None, end=None, proxy=None): df = df.sort_index() return df + def calc_market_cap(self, proxy=None): + shares = self.get_shares_full(start=pd.Timestamp.utcnow().date()-pd.Timedelta(days=548), proxy=proxy) + if shares is None: + # Requesting 18 months failed, so fallback to shares which should include last year + shares = self.get_shares(proxy=proxy) + if shares is None: + raise Exception(f"{self.ticker}: Cannot retrieve share count for calculating market cap") + if isinstance(shares, pd.DataFrame): + shares = shares[shares.columns[0]] + shares = shares.iloc[-1] + # price = self.history(period="1wk")["Close"].iloc[-1] + # Use metadata, in case history() returns empty because stock hasn't traded for long time + df = self.history(period="1d") + md = self.get_history_metadata() + price = md["regularMarketPrice"] + mcap = price * shares + return mcap + def get_isin(self, proxy=None) -> Optional[str]: # *** experimental *** if self._isin is not None: diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index ea4d1abd8..550be0da1 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -7,33 +7,38 @@ from yfinance.data import TickerData +info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose"} +info_retired_keys_price.update({"regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price"]}) +info_retired_keys_exchange = {"currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"} +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 + + from collections.abc import MutableMapping class InfoDictWrapper(MutableMapping): """ Simple wrapper around info dict, to print messages for specific keys instructing how to retrieve with new methods""" - def __init__(self, info): self.info = info - self.redundant_keys_price = ["currentPrice", "dayHigh", "dayLow", "open", "previousClose"] - self.redundant_keys_price += ["regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price"]] - self.redundant_keys_exchange = ["currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"] - self.redundant_keys_marketCap = ["marketCap"] - def __contains__(self, k): return k in self.info.keys() def __getitem__(self, k): - if k in self.redundant_keys_price: + if k in info_retired_keys_price: print(f"Price data removed from info. Use Ticker.history(period='1wk') instead") return None - elif k in self.redundant_keys_exchange: + elif k in info_retired_keys_exchange: print(f"Exchange data removed from info. Use Ticker.get_history_metadata() instead") return None - elif k in self.redundant_keys_marketCap: + elif k in info_retired_keys_marketCap: print(f"Market cap removed from info. Calculate using new Ticker.get_shares_full() * price") return None + elif k in info_retired_keys_symbol: + print(f"Symbol removed from info. You know this already") + return None return self.info[self._keytransform(k)] def __setitem__(self, k, value): @@ -178,11 +183,7 @@ def _scrape(self, proxy): # Delete redundant info[] keys, because values can be accessed faster # elsewhere - e.g. price keys. Hope is reduces Yahoo spam effect. - redundant_keys = ["currentPrice", "dayHigh", "dayLow", "open", "previousClose"] - redundant_keys += ["regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price"]] - redundant_keys += ["currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"] - redundant_keys += ["marketCap"] - for k in redundant_keys: + for k in info_retired_keys: if k in self._info: del self._info[k] # InfoDictWrapper will explain how to access above data elsewhere diff --git a/yfinance/ticker.py b/yfinance/ticker.py index 34c7d17e6..498303a97 100644 --- a/yfinance/ticker.py +++ b/yfinance/ticker.py @@ -133,6 +133,10 @@ def actions(self) -> _pd.DataFrame: def shares(self) -> _pd.DataFrame : return self.get_shares() + @property + def market_cap(self) -> float: + return self.calc_market_cap() + @property def info(self) -> dict: return self.get_info() From e44c6f8b0ebcfbf35dc097183c7df64e8e3781ef Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Tue, 17 Jan 2023 14:10:28 +0000 Subject: [PATCH 04/11] Add 'Ticker.basic_info' --- yfinance/base.py | 124 +++++++++++++++++++++++++++++++------ yfinance/scrapers/quote.py | 6 +- yfinance/utils.py | 12 ++++ 3 files changed, 121 insertions(+), 21 deletions(-) diff --git a/yfinance/base.py b/yfinance/base.py index 5b5fd0a88..f032a67bf 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -47,6 +47,106 @@ _ROOT_URL_ = 'https://finance.yahoo.com' +class BasicInfo: + # Contain small subset of info[] items that can be fetched faster elsewhere. + # Imitates a dict. + def __init__(self, tickerBaseObject): + self._tkr = tickerBaseObject + + self._currency = None + self._exchange = None + self._timezone = None + + self._shares = None + self._last_price = None + self._mcap = None + + # dict imitation: + def keys(self): + attrs = utils.attributes(self) + return attrs.keys() + def items(self): + return [(k,self[k]) for k in self.keys()] + 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 'BasicInfo.keys()'") + return getattr(self, k) + def __contains__(self, k): + return k in self.keys() + def __iter__(self): + return iter(self.keys()) + + @property + def currency(self): + if self._currency is not None: + return self._currency + + if self._tkr._history_metadata is None: + self._tkr.history(period="1d") + md = self._tkr.get_history_metadata() + self._currency = md["currency"] + return self._currency + + @property + def exchange(self): + if self._exchange is not None: + return self._exchange + + if self._tkr._history_metadata is None: + self._tkr.history(period="1d") + md = self._tkr.get_history_metadata() + self._exchange = md["exchangeName"] + return self._exchange + + @property + def timezone(self): + if self._timezone is not None: + return self._timezone + + if self._tkr._history_metadata is None: + self._tkr.history(period="1d") + md = self._tkr.get_history_metadata() + self._timezone = md["exchangeTimezoneName"] + return self._currency + + @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 None: + raise Exception(f"{self._tkr.ticker}: Cannot retrieve share count for calculating market cap") + if isinstance(shares, pd.DataFrame): + shares = shares[shares.columns[0]] + self._shares = shares.iloc[-1] + return self._shares + + @property + def last_price(self): + if self._last_price is not None: + return self._last_price + + if self._tkr._history_metadata is None: + self._tkr.history(period="1d") + md = self._tkr.get_history_metadata() + self._last_price = md["regularMarketPrice"] + return self._last_price + + @property + def market_cap(self): + if self._mcap is not None: + return self._mcap + + self._mcap = self.shares * self.last_price + return self._mcap + + class TickerBase: def __init__(self, ticker, session=None): self.ticker = ticker.upper() @@ -77,6 +177,8 @@ def __init__(self, ticker, session=None): self._quote = Quote(self._data) self._fundamentals = Fundamentals(self._data) + self._basic_info = BasicInfo(self) + def stats(self, proxy=None): ticker_url = "{}/{}".format(self._scrape_url, self.ticker) @@ -895,6 +997,10 @@ def get_info(self, proxy=None) -> dict: data = self._quote.info return data + @property + def basic_info(self): + return self._basic_info + def get_sustainability(self, proxy=None, as_dict=False): self._quote.proxy = proxy data = self._quote.sustainability @@ -1172,24 +1278,6 @@ def get_shares_full(self, start=None, end=None, proxy=None): df = df.sort_index() return df - def calc_market_cap(self, proxy=None): - shares = self.get_shares_full(start=pd.Timestamp.utcnow().date()-pd.Timedelta(days=548), proxy=proxy) - if shares is None: - # Requesting 18 months failed, so fallback to shares which should include last year - shares = self.get_shares(proxy=proxy) - if shares is None: - raise Exception(f"{self.ticker}: Cannot retrieve share count for calculating market cap") - if isinstance(shares, pd.DataFrame): - shares = shares[shares.columns[0]] - shares = shares.iloc[-1] - # price = self.history(period="1wk")["Close"].iloc[-1] - # Use metadata, in case history() returns empty because stock hasn't traded for long time - df = self.history(period="1d") - md = self.get_history_metadata() - price = md["regularMarketPrice"] - mcap = price * shares - return mcap - def get_isin(self, proxy=None) -> Optional[str]: # *** experimental *** if self._isin is not None: diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index 550be0da1..398bb8c7d 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -28,13 +28,13 @@ def __contains__(self, k): def __getitem__(self, k): if k in info_retired_keys_price: - print(f"Price data removed from info. Use Ticker.history(period='1wk') instead") + print(f"Price data removed from info. Use Ticker.basic_info or history() instead") return None elif k in info_retired_keys_exchange: - print(f"Exchange data removed from info. Use Ticker.get_history_metadata() instead") + print(f"Exchange data removed from info. Use Ticker.basic_info or Ticker.get_history_metadata() instead") return None elif k in info_retired_keys_marketCap: - print(f"Market cap removed from info. Calculate using new Ticker.get_shares_full() * price") + print(f"Market cap removed from info. Use Ticker.basic_info instead") return None elif k in info_retired_keys_symbol: print(f"Symbol removed from info. You know this already") diff --git a/yfinance/utils.py b/yfinance/utils.py index 48b043435..cd3501c64 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -49,6 +49,18 @@ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} +# From https://stackoverflow.com/a/59128615 +from types import FunctionType +from inspect import getmembers +def attributes(obj): + disallowed_names = { + name for name, value in getmembers(type(obj)) + if isinstance(value, FunctionType)} + return { + name: getattr(obj, name) for name in dir(obj) + if name[0] != '_' and name not in disallowed_names and hasattr(obj, name)} + + def is_isin(string): return bool(_re.match("^([A-Z]{2})([A-Z0-9]{9})([0-9]{1})$", string)) From 89bbe8ad4cb88a5a3d1536fdd9951d85ae069fe1 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Tue, 17 Jan 2023 19:49:42 +0000 Subject: [PATCH 05/11] Override Ticker.basic_info __str__() --- yfinance/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/yfinance/base.py b/yfinance/base.py index f032a67bf..70e6ebe85 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -77,6 +77,11 @@ 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__() @property def currency(self): From d5a1266cbe5cf0588a66523630eb813c84c0e8a4 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Tue, 17 Jan 2023 20:13:32 +0000 Subject: [PATCH 06/11] Remove more info[] keys --- yfinance/base.py | 82 +++++++++++++++++++++++++++++++------- yfinance/scrapers/quote.py | 17 ++++++-- 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/yfinance/base.py b/yfinance/base.py index 70e6ebe85..cbae0e387 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -53,14 +53,22 @@ class BasicInfo: def __init__(self, tickerBaseObject): self._tkr = tickerBaseObject + self._prices_1y = None + self._md = None + self._currency = None self._exchange = None self._timezone = None self._shares = None - self._last_price = None self._mcap = None + self._last_price = None + self._last_volume = None + self._fifty_day_average = None + self._year_high = None + self._year_low = None + # dict imitation: def keys(self): attrs = utils.attributes(self) @@ -82,6 +90,21 @@ def __str__(self): return "lazy-loading dict with keys = " + str(self.keys()) def __repr__(self): return self.__str__() + + def _get_1y_prices(self): + if self._prices_1y is not None: + return self._prices_1y + + self._prices_1y = self._tkr.history(period="1y") + return self._prices_1y + + 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 @property def currency(self): @@ -89,7 +112,7 @@ def currency(self): return self._currency if self._tkr._history_metadata is None: - self._tkr.history(period="1d") + self._get_1y_prices() md = self._tkr.get_history_metadata() self._currency = md["currency"] return self._currency @@ -99,10 +122,7 @@ def exchange(self): if self._exchange is not None: return self._exchange - if self._tkr._history_metadata is None: - self._tkr.history(period="1d") - md = self._tkr.get_history_metadata() - self._exchange = md["exchangeName"] + self._exchange = self._get_exchange_metadata()["exchangeName"] return self._exchange @property @@ -110,10 +130,7 @@ def timezone(self): if self._timezone is not None: return self._timezone - if self._tkr._history_metadata is None: - self._tkr.history(period="1d") - md = self._tkr.get_history_metadata() - self._timezone = md["exchangeTimezoneName"] + self._timezone = self._get_exchange_metadata()["exchangeTimezoneName"] return self._currency @property @@ -137,12 +154,49 @@ def last_price(self): if self._last_price is not None: return self._last_price - if self._tkr._history_metadata is None: - self._tkr.history(period="1d") - md = self._tkr.get_history_metadata() - self._last_price = md["regularMarketPrice"] + self._last_price = self._get_exchange_metadata()["regularMarketPrice"] return self._last_price + @property + def last_volume(self): + if self._last_volume is not None: + return self._last_volume + + prices = self._get_1y_prices() + self._last_volume = prices["Volume"].iloc[-1] + return self._last_volume + + # @property + # def fifty_day_average(self): + # if self._fifty_day_average is not None: + # return self._fifty_day_average + + # prices = self._get_1y_prices() + # n = prices.shape[0] + # if n <= 50: + # self._fifty_day_average = prices["Close"].mean() + # else: + # self._fifty_day_average = prices["Close"].iloc[n-50:].mean() + # return self._fifty_day_average + + @property + def year_high(self): + if self._year_high is not None: + return self._year_high + + prices = self._get_1y_prices() + self._year_high = 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() + self._year_low = prices["Low"].min() + return self._year_low + @property def market_cap(self): if self._mcap is not None: diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index 398bb8c7d..a07cb4bd9 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -8,7 +8,8 @@ info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose"} -info_retired_keys_price.update({"regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price"]}) +info_retired_keys_price.update({"regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price", "Volume"]}) +info_retired_keys_price.update({"fiftyTwoWeekLow", "fiftyTwoWeekHigh"}) info_retired_keys_exchange = {"currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"} info_retired_keys_marketCap = {"marketCap"} info_retired_keys_symbol = {"symbol"} @@ -17,12 +18,22 @@ from collections.abc import MutableMapping class InfoDictWrapper(MutableMapping): - """ Simple wrapper around info dict, to print messages for specific keys - instructing how to retrieve with new methods""" + """ Simple wrapper around info dict, intercepting 'gets' to + print how-to-migrate messages for specific keys. Requires + override dict API""" def __init__(self, info): self.info = info + def keys(self): + return self.info.keys() + + def __str__(self): + return self.info.__str__() + + def __repr__(self): + return self.info.__repr__() + def __contains__(self, k): return k in self.info.keys() From 3fd9ea220421192f379f1406a88af8c28dfce29f Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Wed, 18 Jan 2023 16:55:31 +0000 Subject: [PATCH 07/11] Remove more info[] keys - #2 --- yfinance/base.py | 104 ++++++++++++++++++++++++++++++++----- yfinance/scrapers/quote.py | 5 +- yfinance/utils.py | 4 +- 3 files changed, 96 insertions(+), 17 deletions(-) diff --git a/yfinance/base.py b/yfinance/base.py index cbae0e387..286eac816 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -65,9 +65,12 @@ def __init__(self, tickerBaseObject): self._last_price = None self._last_volume = None - self._fifty_day_average = None + self._50d_day_average = None + self._200d_day_average = None self._year_high = None self._year_low = None + self._10d_avg_vol = None + self._3mo_avg_vol = None # dict imitation: def keys(self): @@ -96,6 +99,12 @@ def _get_1y_prices(self): return self._prices_1y self._prices_1y = self._tkr.history(period="1y") + self._md = self._tkr.get_history_metadata() + try: + self._today_close = pd.to_datetime(self._md["currentTradingPeriod"]["regular"]["end"], unit='s', utc=True).tz_convert(self._md["exchangeTimezoneName"]) + except: + pass + return self._prices_1y def _get_exchange_metadata(self): @@ -163,21 +172,88 @@ def last_volume(self): return self._last_volume prices = self._get_1y_prices() - self._last_volume = prices["Volume"].iloc[-1] + if prices.empty: + self._last_volume = 0 + else: + self._last_volume = prices["Volume"].iloc[-1] + return self._last_volume - # @property - # def fifty_day_average(self): - # if self._fifty_day_average is not None: - # return self._fifty_day_average - - # prices = self._get_1y_prices() - # n = prices.shape[0] - # if n <= 50: - # self._fifty_day_average = prices["Close"].mean() - # else: - # self._fifty_day_average = prices["Close"].iloc[n-50:].mean() - # return self._fifty_day_average + @property + def fifty_day_average(self): + if self._50d_day_average is not None: + return self._50d_day_average + + prices = self._get_1y_prices() + if prices.empty: + self._50d_day_average = _np.nan + else: + n = prices.shape[0] + a = n-50 + b = n + if a < 0: + b = 0 + self._50d_day_average = 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() + if prices.empty: + self._200d_day_average = _np.nan + else: + n = prices.shape[0] + a = n-200 + b = n + if a < 0: + b = 0 + self._200d_day_average = 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() + if prices.empty: + self._10d_avg_vol = 0 + else: + n = prices.shape[0] + a = n-10 + b = n + if self._today_close is not None and pd.Timestamp.utcnow() < self._today_close: + # Exclude today + a -= 1 + b -= 1 + if a < 0: + a = 0 + self._10d_avg_vol = 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() + if prices.empty: + self._3mo_avg_vol = 0 + else: + dt1 = prices.index[-1] + dt0 = dt1 - utils._interval_to_timedelta("3mo") + if self._today_close is not None and pd.Timestamp.utcnow() < self._today_close: + # Exclude today + dt1 -= utils._interval_to_timedelta("1d") + self._3mo_avg_vol = prices.loc[dt0:dt1, "Volume"].mean() + + return self._3mo_avg_vol @property def year_high(self): diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index a07cb4bd9..06df45568 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -7,9 +7,10 @@ from yfinance.data import TickerData -info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose"} +info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose", "volume"} info_retired_keys_price.update({"regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price", "Volume"]}) -info_retired_keys_price.update({"fiftyTwoWeekLow", "fiftyTwoWeekHigh"}) +info_retired_keys_price.update({"fiftyTwoWeekLow", "fiftyTwoWeekHigh", "fiftyDayAverage", "twoHundredDayAverage"}) +info_retired_keys_price.update({"averageDailyVolume10Day", "averageVolume10days", "averageVolume"}) info_retired_keys_exchange = {"currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"} info_retired_keys_marketCap = {"marketCap"} info_retired_keys_symbol = {"symbol"} diff --git a/yfinance/utils.py b/yfinance/utils.py index cd3501c64..383d84bbd 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -319,7 +319,9 @@ def _parse_user_dt(dt, exchange_tz): def _interval_to_timedelta(interval): if interval == "1mo": - return _dateutil.relativedelta(months=1) + return _dateutil.relativedelta.relativedelta(months=1) + elif interval == "3mo": + return _dateutil.relativedelta.relativedelta(months=3) elif interval == "1wk": return _pd.Timedelta(days=7, unit='d') else: From cd1e16ad9ea8871dbc2faeaf8aa78c4ac5cba226 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Thu, 19 Jan 2023 00:37:17 +0000 Subject: [PATCH 08/11] Add test ; Fix 1y price stats --- tests/ticker.py | 95 +++++++++++++++++++++++++++++++++- yfinance/base.py | 102 ++++++++++++++++++++++++++----------- yfinance/scrapers/quote.py | 19 ++++--- yfinance/utils.py | 2 + 4 files changed, 181 insertions(+), 37 deletions(-) diff --git a/tests/ticker.py b/tests/ticker.py index 75762d981..f619819e6 100644 --- a/tests/ticker.py +++ b/tests/ticker.py @@ -9,6 +9,7 @@ """ import pandas as pd +import numpy as np from .context import yfinance as yf @@ -660,14 +661,104 @@ def test_shares_full(self): self.assertIsInstance(data, pd.Series, "data has wrong type") self.assertFalse(data.empty, "data is empty") + def test_bad_freq_value_raises_exception(self): + self.assertRaises(ValueError, lambda: self.ticker.get_cashflow(freq="badarg")) + + +class TestTickerInfo(unittest.TestCase): + session = None + + @classmethod + def setUpClass(cls): + cls.session = requests_cache.CachedSession(backend='memory') + + @classmethod + def tearDownClass(cls): + if cls.session is not None: + cls.session.close() + + def setUp(self): + # self.ticker = yf.Ticker("GOOGL", session=self.session) + # self.ticker = yf.Ticker("BP.L", session=self.session) + self.ticker = yf.Ticker("AAU.L", session=self.session) + + def tearDown(self): + self.ticker = None + def test_info(self): data = self.ticker.info self.assertIsInstance(data, dict, "data has wrong type") self.assertIn("symbol", data.keys(), "Did not find expected key in info dict") self.assertEqual("GOOGL", data["symbol"], "Wrong symbol value in info dict") - def test_bad_freq_value_raises_exception(self): - self.assertRaises(ValueError, lambda: self.ticker.get_cashflow(freq="badarg")) + def test_basic_info(self): + yf.scrapers.quote.PRUNE_INFO = False + + bi = self.ticker.basic_info + + key_rename_map = {} + key_rename_map["last_price"] = "currentPrice" + 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_high"] = "fiftyTwoWeekHigh" + key_rename_map["year_low"] = "fiftyTwoWeekLow" + + key_rename_map["last_volume"] = "regularMarketVolume" + key_rename_map["ten_day_average_volume"] = ["averageVolume10days", "averageDailyVolume10Day"] + key_rename_map["three_month_average_volume"] = "averageVolume" + + key_rename_map["market_cap"] = "marketCap" + key_rename_map["shares"] = "floatShares" + key_rename_map["timezone"] = "exchangeTimezoneName" + + approximate_keys = {"fifty_day_average", "ten_day_average_volume"} + approximate_keys.update({"market_cap"}) + + # bad_keys = [] + bad_keys = {"shares"} + + custom_tolerances = {} + # custom_tolerances["ten_day_average_volume"] = 1e-3 + custom_tolerances["ten_day_average_volume"] = 1e-1 + custom_tolerances["three_month_average_volume"] = 1e-2 + custom_tolerances["fifty_day_average"] = 1e-2 + custom_tolerances["two_hundred_day_average"] = 1e-2 + + for k in bi.keys(): + if k in key_rename_map: + k2 = key_rename_map[k] + else: + k2 = k + + if not isinstance(k2, list): + k2 = [k2] + + for m in k2: + if not m in self.ticker.info: + print(sorted(list(self.ticker.info.keys()))) + raise Exception("Need to add/fix mapping for basic_info key", k) + + if k in bad_keys: + # Doesn't match, investigate why + continue + + if k in custom_tolerances: + rtol = custom_tolerances[k] + else: + # rtol = 1e-3 + # rtol = 5e-3 + rtol = 1e-4 + + print(f"Testing key {k} -> {m}") + # if k in approximate_keys: + v1 = self.ticker.basic_info[k] + v2 = self.ticker.info[m] + if isinstance(v1, float) or isinstance(v2, int): + self.assertTrue(np.isclose(v1, v2, rtol=rtol), f"{k}: {v1} != {v2}") + else: + self.assertEqual(v1, v2, f"{k}: {v1} != {v2}") + def suite(): diff --git a/yfinance/base.py b/yfinance/base.py index 286eac816..f11d77693 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -69,6 +69,7 @@ def __init__(self, tickerBaseObject): 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 @@ -94,18 +95,30 @@ def __str__(self): def __repr__(self): return self.__str__() - def _get_1y_prices(self): - if self._prices_1y is not None: - return self._prices_1y - - self._prices_1y = self._tkr.history(period="1y") - self._md = self._tkr.get_history_metadata() - try: - self._today_close = pd.to_datetime(self._md["currentTradingPeriod"]["regular"]["end"], unit='s', utc=True).tz_convert(self._md["exchangeTimezoneName"]) - except: - pass - - return self._prices_1y + def _get_1y_prices(self, fullDaysOnly=False): + if self._prices_1y is None: + self._prices_1y = self._tkr.history(period="380d", auto_adjust=False) + 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.self._prices_1y + + dt1 = self._prices_1y.index[-1] + if fullDaysOnly and self._exchange_open_now(): + # Exclude today + dt1 -= utils._interval_to_timedelta("1h") + dt0 = dt1 - utils._interval_to_timedelta("1y") + utils._interval_to_timedelta("1d") + return self._prices_1y.loc[dt0:dt1] def _get_exchange_metadata(self): if self._md is not None: @@ -114,6 +127,29 @@ def _get_exchange_metadata(self): 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): @@ -126,6 +162,9 @@ def currency(self): self._currency = md["currency"] return self._currency + def _currency_is_cents(self): + return self.currency in ["GBp"] + @property def exchange(self): if self._exchange is not None: @@ -140,7 +179,7 @@ def timezone(self): return self._timezone self._timezone = self._get_exchange_metadata()["exchangeTimezoneName"] - return self._currency + return self._timezone @property def shares(self): @@ -184,7 +223,7 @@ def fifty_day_average(self): if self._50d_day_average is not None: return self._50d_day_average - prices = self._get_1y_prices() + prices = self._get_1y_prices(fullDaysOnly=True) if prices.empty: self._50d_day_average = _np.nan else: @@ -192,7 +231,7 @@ def fifty_day_average(self): a = n-50 b = n if a < 0: - b = 0 + a = 0 self._50d_day_average = prices["Close"].iloc[a:b].mean() return self._50d_day_average @@ -202,7 +241,7 @@ def two_hundred_day_average(self): if self._200d_day_average is not None: return self._200d_day_average - prices = self._get_1y_prices() + prices = self._get_1y_prices(fullDaysOnly=True) if prices.empty: self._200d_day_average = _np.nan else: @@ -210,7 +249,8 @@ def two_hundred_day_average(self): a = n-200 b = n if a < 0: - b = 0 + a = 0 + self._200d_day_average = prices["Close"].iloc[a:b].mean() return self._200d_day_average @@ -220,17 +260,13 @@ def ten_day_average_volume(self): if self._10d_avg_vol is not None: return self._10d_avg_vol - prices = self._get_1y_prices() + prices = self._get_1y_prices(fullDaysOnly=True) if prices.empty: self._10d_avg_vol = 0 else: n = prices.shape[0] a = n-10 b = n - if self._today_close is not None and pd.Timestamp.utcnow() < self._today_close: - # Exclude today - a -= 1 - b -= 1 if a < 0: a = 0 self._10d_avg_vol = prices["Volume"].iloc[a:b].mean() @@ -242,15 +278,12 @@ def three_month_average_volume(self): if self._3mo_avg_vol is not None: return self._3mo_avg_vol - prices = self._get_1y_prices() + prices = self._get_1y_prices(fullDaysOnly=True) if prices.empty: self._3mo_avg_vol = 0 else: dt1 = prices.index[-1] - dt0 = dt1 - utils._interval_to_timedelta("3mo") - if self._today_close is not None and pd.Timestamp.utcnow() < self._today_close: - # Exclude today - dt1 -= utils._interval_to_timedelta("1d") + dt0 = dt1 - utils._interval_to_timedelta("3mo") + utils._interval_to_timedelta("1d") self._3mo_avg_vol = prices.loc[dt0:dt1, "Volume"].mean() return self._3mo_avg_vol @@ -260,7 +293,7 @@ def year_high(self): if self._year_high is not None: return self._year_high - prices = self._get_1y_prices() + prices = self._get_1y_prices(fullDaysOnly=True) self._year_high = prices["High"].max() return self._year_high @@ -269,16 +302,27 @@ def year_low(self): if self._year_low is not None: return self._year_low - prices = self._get_1y_prices() + prices = self._get_1y_prices(fullDaysOnly=True) self._year_low = 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) + self._year_change = (prices["Close"].iloc[-1] - prices["Close"].iloc[0]) / prices["Close"].iloc[0] + return self._year_change + @property def market_cap(self): if self._mcap is not None: return self._mcap self._mcap = self.shares * self.last_price + if self._currency_is_cents(): + self._mcap *= 0.01 return self._mcap diff --git a/yfinance/scrapers/quote.py b/yfinance/scrapers/quote.py index 06df45568..d25c9633f 100644 --- a/yfinance/scrapers/quote.py +++ b/yfinance/scrapers/quote.py @@ -9,12 +9,18 @@ info_retired_keys_price = {"currentPrice", "dayHigh", "dayLow", "open", "previousClose", "volume"} info_retired_keys_price.update({"regularMarket"+s for s in ["DayHigh", "DayLow", "Open", "PreviousClose", "Price", "Volume"]}) -info_retired_keys_price.update({"fiftyTwoWeekLow", "fiftyTwoWeekHigh", "fiftyDayAverage", "twoHundredDayAverage"}) +info_retired_keys_price.update({"fiftyTwoWeekLow", "fiftyTwoWeekHigh", "fiftyTwoWeekChange", "fiftyDayAverage", "twoHundredDayAverage"}) info_retired_keys_price.update({"averageDailyVolume10Day", "averageVolume10days", "averageVolume"}) info_retired_keys_exchange = {"currency", "exchange", "exchangeTimezoneName", "exchangeTimezoneShortName"} 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 +# +info_retired_keys = [] + + +PRUNE_INFO = True +# PRUNE_INFO = False from collections.abc import MutableMapping @@ -195,11 +201,12 @@ def _scrape(self, proxy): # Delete redundant info[] keys, because values can be accessed faster # elsewhere - e.g. price keys. Hope is reduces Yahoo spam effect. - for k in info_retired_keys: - if k in self._info: - del self._info[k] - # InfoDictWrapper will explain how to access above data elsewhere - self._info = InfoDictWrapper(self._info) + if PRUNE_INFO: + for k in info_retired_keys: + if k in self._info: + del self._info[k] + # InfoDictWrapper will explain how to access above data elsewhere + self._info = InfoDictWrapper(self._info) # events try: diff --git a/yfinance/utils.py b/yfinance/utils.py index 383d84bbd..1a55b338e 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -322,6 +322,8 @@ def _interval_to_timedelta(interval): return _dateutil.relativedelta.relativedelta(months=1) elif interval == "3mo": return _dateutil.relativedelta.relativedelta(months=3) + elif interval == "1y": + return _dateutil.relativedelta.relativedelta(years=1) elif interval == "1wk": return _pd.Timedelta(days=7, unit='d') else: From 6bd8fb2290fca7907e91cb318578bc9bda105769 Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Thu, 19 Jan 2023 14:57:34 +0000 Subject: [PATCH 09/11] Improve test ; Add more keys to basic_info --- tests/ticker.py | 74 ++++++++++++++++++++++++++++-------------------- yfinance/base.py | 55 ++++++++++++++++++++++++++++------- 2 files changed, 88 insertions(+), 41 deletions(-) diff --git a/tests/ticker.py b/tests/ticker.py index f619819e6..b99b27063 100644 --- a/tests/ticker.py +++ b/tests/ticker.py @@ -678,9 +678,8 @@ def tearDownClass(cls): cls.session.close() def setUp(self): - # self.ticker = yf.Ticker("GOOGL", session=self.session) - # self.ticker = yf.Ticker("BP.L", session=self.session) - self.ticker = yf.Ticker("AAU.L", session=self.session) + tkrs = ["ESLT.TA", "BP.L", "GOOGL"] + self.tickers = [yf.Ticker(tkr, session=self.session) for tkr in tkrs] def tearDown(self): self.ticker = None @@ -694,17 +693,28 @@ def test_info(self): def test_basic_info(self): yf.scrapers.quote.PRUNE_INFO = False - bi = self.ticker.basic_info + # basic_info_keys = self.ticker.basic_info.keys() + basic_info_keys = set() + for ticker in self.tickers: + basic_info_keys.update(set(ticker.basic_info.keys())) + basic_info_keys = sorted(list(basic_info_keys)) key_rename_map = {} - key_rename_map["last_price"] = "currentPrice" + 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", "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_high"] = "fiftyTwoWeekHigh" key_rename_map["year_low"] = "fiftyTwoWeekLow" - key_rename_map["last_volume"] = "regularMarketVolume" + key_rename_map["last_volume"] = ["volume", "regularMarketVolume"] key_rename_map["ten_day_average_volume"] = ["averageVolume10days", "averageDailyVolume10Day"] key_rename_map["three_month_average_volume"] = "averageVolume" @@ -718,14 +728,16 @@ def test_basic_info(self): # bad_keys = [] bad_keys = {"shares"} + # Loose tolerance for averages, no idea why don't match info[]. Is info wrong? custom_tolerances = {} # custom_tolerances["ten_day_average_volume"] = 1e-3 custom_tolerances["ten_day_average_volume"] = 1e-1 - custom_tolerances["three_month_average_volume"] = 1e-2 + # custom_tolerances["three_month_average_volume"] = 1e-2 + custom_tolerances["three_month_average_volume"] = 5e-1 custom_tolerances["fifty_day_average"] = 1e-2 custom_tolerances["two_hundred_day_average"] = 1e-2 - for k in bi.keys(): + for k in basic_info_keys: if k in key_rename_map: k2 = key_rename_map[k] else: @@ -735,29 +747,29 @@ def test_basic_info(self): k2 = [k2] for m in k2: - if not m in self.ticker.info: - print(sorted(list(self.ticker.info.keys()))) - raise Exception("Need to add/fix mapping for basic_info key", k) - - if k in bad_keys: - # Doesn't match, investigate why - continue - - if k in custom_tolerances: - rtol = custom_tolerances[k] - else: - # rtol = 1e-3 - # rtol = 5e-3 - rtol = 1e-4 - - print(f"Testing key {k} -> {m}") - # if k in approximate_keys: - v1 = self.ticker.basic_info[k] - v2 = self.ticker.info[m] - if isinstance(v1, float) or isinstance(v2, int): - self.assertTrue(np.isclose(v1, v2, rtol=rtol), f"{k}: {v1} != {v2}") - else: - self.assertEqual(v1, v2, f"{k}: {v1} != {v2}") + for ticker in self.tickers: + if not m in ticker.info: + print(sorted(list(ticker.info.keys()))) + raise Exception("Need to add/fix mapping for basic_info key", k) + + if k in bad_keys: + # Doesn't match, investigate why + continue + + if k in custom_tolerances: + rtol = custom_tolerances[k] + else: + rtol = 5e-3 + # rtol = 1e-4 + + print(f"Testing key {m} -> {k} ticker={ticker.ticker}") + # if k in approximate_keys: + v1 = ticker.basic_info[k] + v2 = ticker.info[m] + if isinstance(v1, float) or isinstance(v2, int): + self.assertTrue(np.isclose(v1, v2, rtol=rtol), f"{k}: {v1} != {v2}") + else: + self.assertEqual(v1, v2, f"{k}: {v1} != {v2}") diff --git a/yfinance/base.py b/yfinance/base.py index f11d77693..14844c357 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -63,13 +63,20 @@ def __init__(self, tickerBaseObject): 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._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 @@ -150,7 +157,7 @@ def _exchange_open_now(self): # print("_exchange_open_now() returning", r) return r - + @property def currency(self): if self._currency is not None: @@ -163,7 +170,7 @@ def currency(self): return self._currency def _currency_is_cents(self): - return self.currency in ["GBp"] + return self.currency in ["GBp", "ILA"] @property def exchange(self): @@ -201,21 +208,49 @@ def shares(self): def last_price(self): if self._last_price is not None: return self._last_price - - self._last_price = self._get_exchange_metadata()["regularMarketPrice"] + # self._last_price = self._get_exchange_metadata()["regularMarketPrice"] + prices = self._get_1y_prices() + self._last_price = _np.nan if prices.empty else prices["Close"].iloc[-1] 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() + self._prev_close = _np.nan if prices.empty else prices["Close"].iloc[-2] + return self._prev_close + + @property + def open(self): + if self._open is not None: + return self._open + prices = self._get_1y_prices() + self._open = _np.nan if prices.empty else prices["Open"].iloc[-1] + 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 = _np.nan if prices.empty else prices["High"].iloc[-1] + 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 = _np.nan if prices.empty else prices["Low"].iloc[-1] + 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() - if prices.empty: - self._last_volume = 0 - else: - self._last_volume = prices["Volume"].iloc[-1] - + self._last_volume = 0 if prices.empty else prices["Volume"].iloc[-1] return self._last_volume @property From fb77d358639460ff29e8f259f3f719d709e3f87b Mon Sep 17 00:00:00 2001 From: ValueRaider Date: Thu, 19 Jan 2023 22:33:54 +0000 Subject: [PATCH 10/11] Update README --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 783fbf7d0..c70ce414c 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,9 @@ import yfinance as yf msft = yf.Ticker("MSFT") -# get stock info +# fast access to subset of stock info +msft.basic_info +# slow access to all stock info msft.info # get historical market data @@ -86,9 +88,6 @@ msft.capital_gains msft.shares msft.get_shares_full() -# calculate market cap -msft.market_cap - # show financials: # - income statement msft.income_stmt From 9051fba601fb33dad9f60b224f8e5f733bf6c12d Mon Sep 17 00:00:00 2001 From: Collie Tsai Date: Mon, 23 Jan 2023 12:13:59 +0800 Subject: [PATCH 11/11] use dict comprehension to improve speed --- yfinance/tickers.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/yfinance/tickers.py b/yfinance/tickers.py index 7cb3f2ea5..36030b0a0 100644 --- a/yfinance/tickers.py +++ b/yfinance/tickers.py @@ -34,12 +34,8 @@ def __init__(self, tickers, session=None): tickers = tickers if isinstance( tickers, list) else tickers.replace(',', ' ').split() self.symbols = [ticker.upper() for ticker in tickers] - ticker_objects = {} + self.tickers = {ticker:Ticker(ticker, session=session) for ticker in self.symbols} - for ticker in self.symbols: - ticker_objects[ticker] = Ticker(ticker, session=session) - - self.tickers = ticker_objects # self.tickers = _namedtuple( # "Tickers", ticker_objects.keys(), rename=True # )(*ticker_objects.values())