Import python venv for stability
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import curl_cffi
|
||||
import pandas as pd
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.config import YfConfig
|
||||
from yfinance.const import quote_summary_valid_modules
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFException
|
||||
from yfinance.scrapers.quote import _QUOTE_SUMMARY_URL_
|
||||
|
||||
class Analysis:
|
||||
|
||||
def __init__(self, data: YfData, symbol: str):
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
|
||||
# In quoteSummary the 'earningsTrend' module contains most of the data below.
|
||||
# The format of data is not optimal so each function will process it's part of the data.
|
||||
# This variable works as a cache.
|
||||
self._earnings_trend = None
|
||||
|
||||
self._analyst_price_targets = None
|
||||
self._earnings_estimate = None
|
||||
self._revenue_estimate = None
|
||||
self._earnings_history = None
|
||||
self._eps_trend = None
|
||||
self._eps_revisions = None
|
||||
self._growth_estimates = None
|
||||
|
||||
def _get_periodic_df(self, key) -> pd.DataFrame:
|
||||
if self._earnings_trend is None:
|
||||
self._fetch_earnings_trend()
|
||||
|
||||
data = []
|
||||
for item in self._earnings_trend[:4]:
|
||||
row = {'period': item['period']}
|
||||
for k, v in item[key].items():
|
||||
if not isinstance(v, dict) or len(v) == 0:
|
||||
continue
|
||||
row[k] = v['raw']
|
||||
data.append(row)
|
||||
if len(data) == 0:
|
||||
return pd.DataFrame()
|
||||
return pd.DataFrame(data).set_index('period')
|
||||
|
||||
@property
|
||||
def earnings_estimate(self) -> pd.DataFrame:
|
||||
if self._earnings_estimate is not None:
|
||||
return self._earnings_estimate
|
||||
self._earnings_estimate = self._get_periodic_df('earningsEstimate')
|
||||
return self._earnings_estimate
|
||||
|
||||
@property
|
||||
def revenue_estimate(self) -> pd.DataFrame:
|
||||
if self._revenue_estimate is not None:
|
||||
return self._revenue_estimate
|
||||
self._revenue_estimate = self._get_periodic_df('revenueEstimate')
|
||||
return self._revenue_estimate
|
||||
|
||||
@property
|
||||
def eps_trend(self) -> pd.DataFrame:
|
||||
if self._eps_trend is not None:
|
||||
return self._eps_trend
|
||||
self._eps_trend = self._get_periodic_df('epsTrend')
|
||||
return self._eps_trend
|
||||
|
||||
@property
|
||||
def eps_revisions(self) -> pd.DataFrame:
|
||||
if self._eps_revisions is not None:
|
||||
return self._eps_revisions
|
||||
self._eps_revisions = self._get_periodic_df('epsRevisions')
|
||||
return self._eps_revisions
|
||||
|
||||
@property
|
||||
def analyst_price_targets(self) -> dict:
|
||||
if self._analyst_price_targets is not None:
|
||||
return self._analyst_price_targets
|
||||
|
||||
try:
|
||||
data = self._fetch(['financialData'])
|
||||
data = data['quoteSummary']['result'][0]['financialData']
|
||||
except (TypeError, KeyError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
self._analyst_price_targets = {}
|
||||
return self._analyst_price_targets
|
||||
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
if key.startswith('target'):
|
||||
new_key = key.replace('target', '').lower().replace('price', '').strip()
|
||||
result[new_key] = value
|
||||
elif key == 'currentPrice':
|
||||
result['current'] = value
|
||||
|
||||
self._analyst_price_targets = result
|
||||
return self._analyst_price_targets
|
||||
|
||||
@property
|
||||
def earnings_history(self) -> pd.DataFrame:
|
||||
if self._earnings_history is not None:
|
||||
return self._earnings_history
|
||||
|
||||
try:
|
||||
data = self._fetch(['earningsHistory'])
|
||||
data = data['quoteSummary']['result'][0]['earningsHistory']['history']
|
||||
except (TypeError, KeyError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
self._earnings_history = pd.DataFrame()
|
||||
return self._earnings_history
|
||||
|
||||
rows = []
|
||||
for item in data:
|
||||
row = {'quarter': item.get('quarter', {}).get('fmt', None)}
|
||||
for k, v in item.items():
|
||||
if k == 'quarter':
|
||||
continue
|
||||
if not isinstance(v, dict) or len(v) == 0:
|
||||
continue
|
||||
row[k] = v.get('raw', None)
|
||||
rows.append(row)
|
||||
if len(data) == 0:
|
||||
return pd.DataFrame()
|
||||
|
||||
df = pd.DataFrame(rows)
|
||||
if 'quarter' in df.columns:
|
||||
df['quarter'] = pd.to_datetime(df['quarter'], format='%Y-%m-%d')
|
||||
df.set_index('quarter', inplace=True)
|
||||
|
||||
self._earnings_history = df
|
||||
return self._earnings_history
|
||||
|
||||
@property
|
||||
def growth_estimates(self) -> pd.DataFrame:
|
||||
if self._growth_estimates is not None:
|
||||
return self._growth_estimates
|
||||
|
||||
if self._earnings_trend is None:
|
||||
self._fetch_earnings_trend()
|
||||
|
||||
try:
|
||||
trends = self._fetch(['industryTrend', 'sectorTrend', 'indexTrend'])
|
||||
trends = trends['quoteSummary']['result'][0]
|
||||
except (TypeError, KeyError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
self._growth_estimates = pd.DataFrame()
|
||||
return self._growth_estimates
|
||||
|
||||
data = []
|
||||
for item in self._earnings_trend:
|
||||
period = item['period']
|
||||
row = {'period': period, 'stockTrend': item.get('growth', {}).get('raw', None)}
|
||||
data.append(row)
|
||||
|
||||
for trend_name, trend_info in trends.items():
|
||||
if trend_info.get('estimates'):
|
||||
for estimate in trend_info['estimates']:
|
||||
period = estimate['period']
|
||||
existing_row = next((row for row in data if row['period'] == period), None)
|
||||
if existing_row:
|
||||
existing_row[trend_name] = estimate.get('growth')
|
||||
else:
|
||||
row = {'period': period, trend_name: estimate.get('growth')}
|
||||
data.append(row)
|
||||
if len(data) == 0:
|
||||
return pd.DataFrame()
|
||||
|
||||
self._growth_estimates = pd.DataFrame(data).set_index('period').dropna(how='all')
|
||||
return self._growth_estimates
|
||||
|
||||
# modified version from quote.py
|
||||
def _fetch(self, modules: list):
|
||||
if not isinstance(modules, list):
|
||||
raise YFException("Should provide a list of modules, see available modules using `valid_modules`")
|
||||
|
||||
modules = ','.join([m for m in modules if m in quote_summary_valid_modules])
|
||||
if len(modules) == 0:
|
||||
raise YFException("No valid modules provided, see available modules using `valid_modules`")
|
||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol}
|
||||
try:
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
return None
|
||||
return result
|
||||
|
||||
def _fetch_earnings_trend(self) -> None:
|
||||
try:
|
||||
data = self._fetch(['earningsTrend'])
|
||||
self._earnings_trend = data['quoteSummary']['result'][0]['earningsTrend']['trend']
|
||||
except (TypeError, KeyError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
self._earnings_trend = []
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import datetime
|
||||
import json
|
||||
import warnings
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from yfinance import utils, const
|
||||
from yfinance.config import YfConfig
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFException, YFNotImplementedError
|
||||
|
||||
class Fundamentals:
|
||||
|
||||
def __init__(self, data: YfData, symbol: str):
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
|
||||
self._earnings = None
|
||||
self._financials = None
|
||||
self._shares = None
|
||||
|
||||
self._financials_data = None
|
||||
self._fin_data_quote = None
|
||||
self._basics_already_scraped = False
|
||||
self._financials = Financials(data, symbol)
|
||||
|
||||
@property
|
||||
def financials(self) -> "Financials":
|
||||
return self._financials
|
||||
|
||||
@property
|
||||
def earnings(self) -> dict:
|
||||
warnings.warn("'Ticker.earnings' is deprecated as not available via API. Look for \"Net Income\" in Ticker.income_stmt.", DeprecationWarning)
|
||||
return None
|
||||
|
||||
@property
|
||||
def shares(self) -> pd.DataFrame:
|
||||
if self._shares is None:
|
||||
raise YFNotImplementedError('shares')
|
||||
return self._shares
|
||||
|
||||
|
||||
class Financials:
|
||||
def __init__(self, data: YfData, symbol: str):
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
self._income_time_series = {}
|
||||
self._balance_sheet_time_series = {}
|
||||
self._cash_flow_time_series = {}
|
||||
|
||||
def get_income_time_series(self, freq="yearly") -> pd.DataFrame:
|
||||
res = self._income_time_series
|
||||
if freq not in res:
|
||||
res[freq] = self._fetch_time_series("income", freq)
|
||||
return res[freq]
|
||||
|
||||
def get_balance_sheet_time_series(self, freq="yearly") -> pd.DataFrame:
|
||||
res = self._balance_sheet_time_series
|
||||
if freq not in res:
|
||||
res[freq] = self._fetch_time_series("balance-sheet", freq)
|
||||
return res[freq]
|
||||
|
||||
def get_cash_flow_time_series(self, freq="yearly") -> pd.DataFrame:
|
||||
res = self._cash_flow_time_series
|
||||
if freq not in res:
|
||||
res[freq] = self._fetch_time_series("cash-flow", freq)
|
||||
return res[freq]
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def _fetch_time_series(self, name, timescale):
|
||||
# Fetching time series preferred over scraping 'QuoteSummaryStore',
|
||||
# because it matches what Yahoo shows. But for some tickers returns nothing,
|
||||
# despite 'QuoteSummaryStore' containing valid data.
|
||||
|
||||
allowed_names = ["income", "balance-sheet", "cash-flow"]
|
||||
allowed_timescales = ["yearly", "quarterly", "trailing"]
|
||||
|
||||
if name not in allowed_names:
|
||||
raise ValueError(f"Illegal argument: name must be one of: {allowed_names}")
|
||||
if timescale not in allowed_timescales:
|
||||
raise ValueError(f"Illegal argument: timescale must be one of: {allowed_timescales}")
|
||||
if timescale == "trailing" and name not in ('income', 'cash-flow'):
|
||||
raise ValueError("Illegal argument: frequency 'trailing'" +
|
||||
" only available for cash-flow or income data.")
|
||||
|
||||
try:
|
||||
statement = self._create_financials_table(name, timescale)
|
||||
|
||||
if statement is not None:
|
||||
return statement
|
||||
except YFException as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
utils.get_yf_logger().error(f"{self._symbol}: Failed to create {name} financials table for reason: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def _create_financials_table(self, name, timescale):
|
||||
if name == "income":
|
||||
# Yahoo stores the 'income' table internally under 'financials' key
|
||||
name = "financials"
|
||||
|
||||
keys = const.fundamentals_keys[name]
|
||||
|
||||
try:
|
||||
return self._get_financials_time_series(timescale, keys)
|
||||
except Exception:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
pass
|
||||
|
||||
def _get_financials_time_series(self, timescale, keys: list) -> pd.DataFrame:
|
||||
timescale_translation = {"yearly": "annual", "quarterly": "quarterly", "trailing": "trailing"}
|
||||
timescale = timescale_translation[timescale]
|
||||
|
||||
# Step 2: construct url:
|
||||
ts_url_base = f"https://query2.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/{self._symbol}?symbol={self._symbol}"
|
||||
url = ts_url_base + "&type=" + ",".join([timescale + k for k in keys])
|
||||
# Yahoo returns maximum 4 years or 5 quarters, regardless of start_dt:
|
||||
start_dt = datetime.datetime(2016, 12, 31)
|
||||
end = pd.Timestamp.now('UTC').ceil("D")
|
||||
url += f"&period1={int(start_dt.timestamp())}&period2={int(end.timestamp())}"
|
||||
|
||||
# Step 3: fetch and reshape data
|
||||
json_str = self._data.cache_get(url=url).text
|
||||
json_data = json.loads(json_str)
|
||||
data_raw = json_data["timeseries"]["result"]
|
||||
# data_raw = [v for v in data_raw if len(v) > 1] # Discard keys with no data
|
||||
for d in data_raw:
|
||||
del d["meta"]
|
||||
|
||||
# Now reshape data into a table:
|
||||
# Step 1: get columns and index:
|
||||
timestamps = set()
|
||||
data_unpacked = {}
|
||||
for x in data_raw:
|
||||
for k in x.keys():
|
||||
if k == "timestamp":
|
||||
timestamps.update(x[k])
|
||||
else:
|
||||
data_unpacked[k] = x[k]
|
||||
timestamps = sorted(list(timestamps))
|
||||
dates = pd.to_datetime(timestamps, unit="s")
|
||||
df = pd.DataFrame(columns=dates, index=list(data_unpacked.keys()))
|
||||
for k, v in data_unpacked.items():
|
||||
if df is None:
|
||||
df = pd.DataFrame(columns=dates, index=[k])
|
||||
df.loc[k] = {pd.Timestamp(x["asOfDate"]): x["reportedValue"]["raw"] for x in v}
|
||||
|
||||
df.index = df.index.str.replace("^" + timescale, "", regex=True)
|
||||
|
||||
# Ensure float type, not object
|
||||
for d in df.columns:
|
||||
df[d] = df[d].astype('float')
|
||||
|
||||
# Reorder table to match order on Yahoo website
|
||||
df = df.reindex([k for k in keys if k in df.index])
|
||||
df = df[sorted(df.columns, reverse=True)]
|
||||
|
||||
# Trailing 12 months return only the first column.
|
||||
if (timescale == "trailing"):
|
||||
df = df.iloc[:, [0]]
|
||||
|
||||
return df
|
||||
@@ -0,0 +1,336 @@
|
||||
import pandas as pd
|
||||
from typing import Dict, Optional
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.config import YfConfig
|
||||
from yfinance.const import _BASE_URL_
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFDataException
|
||||
|
||||
_QUOTE_SUMMARY_URL_ = f"{_BASE_URL_}/v10/finance/quoteSummary/"
|
||||
|
||||
class FundsData:
|
||||
"""
|
||||
ETF and Mutual Funds Data
|
||||
Queried Modules: quoteType, summaryProfile, fundProfile, topHoldings
|
||||
|
||||
Notes:
|
||||
- fundPerformance module is not implemented as better data is queryable using history
|
||||
"""
|
||||
def __init__(self, data: YfData, symbol: str):
|
||||
"""
|
||||
Args:
|
||||
data (YfData): The YfData object for fetching data.
|
||||
symbol (str): The symbol of the fund.
|
||||
"""
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
|
||||
# quoteType
|
||||
self._quote_type = None
|
||||
|
||||
# summaryProfile
|
||||
self._description = None
|
||||
|
||||
# fundProfile
|
||||
self._fund_overview = None
|
||||
self._fund_operations = None
|
||||
|
||||
# topHoldings
|
||||
self._asset_classes = None
|
||||
self._top_holdings = None
|
||||
self._equity_holdings = None
|
||||
self._bond_holdings = None
|
||||
self._bond_ratings = None
|
||||
self._sector_weightings = None
|
||||
|
||||
def quote_type(self) -> str:
|
||||
"""
|
||||
Returns the quote type of the fund.
|
||||
|
||||
Returns:
|
||||
str: The quote type.
|
||||
"""
|
||||
if self._quote_type is None:
|
||||
self._fetch_and_parse()
|
||||
return self._quote_type
|
||||
|
||||
@property
|
||||
def description(self) -> str:
|
||||
"""
|
||||
Returns the description of the fund.
|
||||
|
||||
Returns:
|
||||
str: The description.
|
||||
"""
|
||||
if self._description is None:
|
||||
self._fetch_and_parse()
|
||||
return self._description
|
||||
|
||||
@property
|
||||
def fund_overview(self) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Returns the fund overview.
|
||||
|
||||
Returns:
|
||||
Dict[str, Optional[str]]: The fund overview.
|
||||
"""
|
||||
if self._fund_overview is None:
|
||||
self._fetch_and_parse()
|
||||
return self._fund_overview
|
||||
|
||||
@property
|
||||
def fund_operations(self) -> pd.DataFrame:
|
||||
"""
|
||||
Returns the fund operations.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: The fund operations.
|
||||
"""
|
||||
if self._fund_operations is None:
|
||||
self._fetch_and_parse()
|
||||
return self._fund_operations
|
||||
|
||||
@property
|
||||
def asset_classes(self) -> Dict[str, float]:
|
||||
"""
|
||||
Returns the asset classes of the fund.
|
||||
|
||||
Returns:
|
||||
Dict[str, float]: The asset classes.
|
||||
"""
|
||||
if self._asset_classes is None:
|
||||
self._fetch_and_parse()
|
||||
return self._asset_classes
|
||||
|
||||
@property
|
||||
def top_holdings(self) -> pd.DataFrame:
|
||||
"""
|
||||
Returns the top holdings of the fund.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: The top holdings.
|
||||
"""
|
||||
if self._top_holdings is None:
|
||||
self._fetch_and_parse()
|
||||
return self._top_holdings
|
||||
|
||||
@property
|
||||
def equity_holdings(self) -> pd.DataFrame:
|
||||
"""
|
||||
Returns the equity holdings of the fund.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: The equity holdings.
|
||||
"""
|
||||
if self._equity_holdings is None:
|
||||
self._fetch_and_parse()
|
||||
return self._equity_holdings
|
||||
|
||||
@property
|
||||
def bond_holdings(self) -> pd.DataFrame:
|
||||
"""
|
||||
Returns the bond holdings of the fund.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: The bond holdings.
|
||||
"""
|
||||
if self._bond_holdings is None:
|
||||
self._fetch_and_parse()
|
||||
return self._bond_holdings
|
||||
|
||||
@property
|
||||
def bond_ratings(self) -> Dict[str, float]:
|
||||
"""
|
||||
Returns the bond ratings of the fund.
|
||||
|
||||
Returns:
|
||||
Dict[str, float]: The bond ratings.
|
||||
"""
|
||||
if self._bond_ratings is None:
|
||||
self._fetch_and_parse()
|
||||
return self._bond_ratings
|
||||
|
||||
@property
|
||||
def sector_weightings(self) -> Dict[str,float]:
|
||||
"""
|
||||
Returns the sector weightings of the fund.
|
||||
|
||||
Returns:
|
||||
Dict[str, float]: The sector weightings.
|
||||
"""
|
||||
if self._sector_weightings is None:
|
||||
self._fetch_and_parse()
|
||||
return self._sector_weightings
|
||||
|
||||
def _fetch(self):
|
||||
"""
|
||||
Fetches the raw JSON data from the API.
|
||||
|
||||
Returns:
|
||||
dict: The raw JSON data.
|
||||
"""
|
||||
modules = ','.join(["quoteType", "summaryProfile", "topHoldings", "fundProfile"])
|
||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "symbol": self._symbol, "formatted": "false"}
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_+self._symbol, params=params_dict)
|
||||
return result
|
||||
|
||||
def _fetch_and_parse(self) -> None:
|
||||
"""
|
||||
Fetches and parses the data from the API.
|
||||
"""
|
||||
result = self._fetch()
|
||||
try:
|
||||
data = result["quoteSummary"]["result"][0]
|
||||
# check quote type
|
||||
self._quote_type = data["quoteType"]["quoteType"]
|
||||
|
||||
# parse "summaryProfile", "topHoldings", "fundProfile"
|
||||
self._parse_description(data["summaryProfile"])
|
||||
self._parse_top_holdings(data["topHoldings"])
|
||||
self._parse_fund_profile(data["fundProfile"])
|
||||
except KeyError:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
raise YFDataException(f"{self._symbol}: No Fund data found.")
|
||||
except Exception as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
logger = utils.get_yf_logger()
|
||||
logger.error(f"Failed to get fund data for '{self._symbol}' reason: {e}")
|
||||
logger.debug("Got response: ")
|
||||
logger.debug("-------------")
|
||||
logger.debug(f" {data}")
|
||||
logger.debug("-------------")
|
||||
|
||||
@staticmethod
|
||||
def _parse_raw_values(data, default=None):
|
||||
"""
|
||||
Parses raw values from the data.
|
||||
|
||||
Args:
|
||||
data: The data to parse.
|
||||
default: The default value if data is not a dictionary.
|
||||
|
||||
Returns:
|
||||
The parsed value or the default value.
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
return data.get("raw", default)
|
||||
|
||||
def _parse_description(self, data) -> None:
|
||||
"""
|
||||
Parses the description from the data.
|
||||
|
||||
Args:
|
||||
data: The data to parse.
|
||||
"""
|
||||
self._description = data.get("longBusinessSummary", "")
|
||||
|
||||
def _parse_top_holdings(self, data) -> None:
|
||||
"""
|
||||
Parses the top holdings from the data.
|
||||
|
||||
Args:
|
||||
data: The data to parse.
|
||||
"""
|
||||
# asset classes
|
||||
self._asset_classes = {
|
||||
"cashPosition": self._parse_raw_values(data.get("cashPosition", None)),
|
||||
"stockPosition": self._parse_raw_values(data.get("stockPosition", None)),
|
||||
"bondPosition": self._parse_raw_values(data.get("bondPosition", None)),
|
||||
"preferredPosition": self._parse_raw_values(data.get("preferredPosition", None)),
|
||||
"convertiblePosition": self._parse_raw_values(data.get("convertiblePosition", None)),
|
||||
"otherPosition": self._parse_raw_values(data.get("otherPosition", None))
|
||||
}
|
||||
|
||||
# top holdings
|
||||
_holdings = data.get("holdings", [])
|
||||
_symbol, _name, _holding_percent = [], [], []
|
||||
|
||||
for item in _holdings:
|
||||
_symbol.append(item["symbol"])
|
||||
_name.append(item["holdingName"])
|
||||
_holding_percent.append(item["holdingPercent"])
|
||||
|
||||
self._top_holdings = pd.DataFrame({
|
||||
"Symbol": _symbol,
|
||||
"Name": _name,
|
||||
"Holding Percent": _holding_percent
|
||||
}).set_index("Symbol")
|
||||
|
||||
# equity holdings
|
||||
_equity_holdings = data.get("equityHoldings", {})
|
||||
self._equity_holdings = pd.DataFrame({
|
||||
"Average": ["Price/Earnings", "Price/Book", "Price/Sales", "Price/Cashflow", "Median Market Cap", "3 Year Earnings Growth"],
|
||||
self._symbol: [
|
||||
self._parse_raw_values(_equity_holdings.get("priceToEarnings", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("priceToBook", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("priceToSales", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("priceToCashflow", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("medianMarketCap", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("threeYearEarningsGrowth", pd.NA)),
|
||||
],
|
||||
"Category Average": [
|
||||
self._parse_raw_values(_equity_holdings.get("priceToEarningsCat", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("priceToBookCat", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("priceToSalesCat", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("priceToCashflowCat", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("medianMarketCapCat", pd.NA)),
|
||||
self._parse_raw_values(_equity_holdings.get("threeYearEarningsGrowthCat", pd.NA)),
|
||||
]
|
||||
}).set_index("Average")
|
||||
|
||||
# bond holdings
|
||||
_bond_holdings = data.get("bondHoldings", {})
|
||||
self._bond_holdings = pd.DataFrame({
|
||||
"Average": ["Duration", "Maturity", "Credit Quality"],
|
||||
self._symbol: [
|
||||
self._parse_raw_values(_bond_holdings.get("duration", pd.NA)),
|
||||
self._parse_raw_values(_bond_holdings.get("maturity", pd.NA)),
|
||||
self._parse_raw_values(_bond_holdings.get("creditQuality", pd.NA)),
|
||||
],
|
||||
"Category Average": [
|
||||
self._parse_raw_values(_bond_holdings.get("durationCat", pd.NA)),
|
||||
self._parse_raw_values(_bond_holdings.get("maturityCat", pd.NA)),
|
||||
self._parse_raw_values(_bond_holdings.get("creditQualityCat", pd.NA)),
|
||||
]
|
||||
}).set_index("Average")
|
||||
|
||||
# bond ratings
|
||||
self._bond_ratings = dict((key, d[key]) for d in data.get("bondRatings", []) for key in d)
|
||||
|
||||
# sector weightings
|
||||
self._sector_weightings = dict((key, d[key]) for d in data.get("sectorWeightings", []) for key in d)
|
||||
|
||||
def _parse_fund_profile(self, data):
|
||||
"""
|
||||
Parses the fund profile from the data.
|
||||
|
||||
Args:
|
||||
data: The data to parse.
|
||||
"""
|
||||
self._fund_overview = {
|
||||
"categoryName": data.get("categoryName", None),
|
||||
"family": data.get("family", None),
|
||||
"legalType": data.get("legalType", None)
|
||||
}
|
||||
|
||||
_fund_operations = data.get("feesExpensesInvestment", {})
|
||||
_fund_operations_cat = data.get("feesExpensesInvestmentCat", {})
|
||||
|
||||
self._fund_operations = pd.DataFrame({
|
||||
"Attributes": ["Annual Report Expense Ratio", "Annual Holdings Turnover", "Total Net Assets"],
|
||||
self._symbol: [
|
||||
self._parse_raw_values(_fund_operations.get("annualReportExpenseRatio", pd.NA)),
|
||||
self._parse_raw_values(_fund_operations.get("annualHoldingsTurnover", pd.NA)),
|
||||
self._parse_raw_values(_fund_operations.get("totalNetAssets", pd.NA))
|
||||
],
|
||||
"Category Average": [
|
||||
self._parse_raw_values(_fund_operations_cat.get("annualReportExpenseRatio", pd.NA)),
|
||||
self._parse_raw_values(_fund_operations_cat.get("annualHoldingsTurnover", pd.NA)),
|
||||
self._parse_raw_values(_fund_operations_cat.get("totalNetAssets", pd.NA))
|
||||
]
|
||||
}).set_index("Attributes")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,241 @@
|
||||
import curl_cffi
|
||||
import pandas as pd
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.config import YfConfig
|
||||
from yfinance.const import _BASE_URL_
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFDataException
|
||||
|
||||
_QUOTE_SUMMARY_URL_ = f"{_BASE_URL_}/v10/finance/quoteSummary"
|
||||
|
||||
class Holders:
|
||||
_SCRAPE_URL_ = 'https://finance.yahoo.com/quote'
|
||||
|
||||
def __init__(self, data: YfData, symbol: str):
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
|
||||
self._major = None
|
||||
self._major_direct_holders = None
|
||||
self._institutional = None
|
||||
self._mutualfund = None
|
||||
|
||||
self._insider_transactions = None
|
||||
self._insider_purchases = None
|
||||
self._insider_roster = None
|
||||
|
||||
@property
|
||||
def major(self) -> pd.DataFrame:
|
||||
if self._major is None:
|
||||
self._fetch_and_parse()
|
||||
return self._major
|
||||
|
||||
@property
|
||||
def institutional(self) -> pd.DataFrame:
|
||||
if self._institutional is None:
|
||||
self._fetch_and_parse()
|
||||
return self._institutional
|
||||
|
||||
@property
|
||||
def mutualfund(self) -> pd.DataFrame:
|
||||
if self._mutualfund is None:
|
||||
self._fetch_and_parse()
|
||||
return self._mutualfund
|
||||
|
||||
@property
|
||||
def insider_transactions(self) -> pd.DataFrame:
|
||||
if self._insider_transactions is None:
|
||||
self._fetch_and_parse()
|
||||
return self._insider_transactions
|
||||
|
||||
@property
|
||||
def insider_purchases(self) -> pd.DataFrame:
|
||||
if self._insider_purchases is None:
|
||||
self._fetch_and_parse()
|
||||
return self._insider_purchases
|
||||
|
||||
@property
|
||||
def insider_roster(self) -> pd.DataFrame:
|
||||
if self._insider_roster is None:
|
||||
self._fetch_and_parse()
|
||||
return self._insider_roster
|
||||
|
||||
def _fetch(self):
|
||||
modules = ','.join(
|
||||
["institutionOwnership", "fundOwnership", "majorDirectHolders", "majorHoldersBreakdown", "insiderTransactions", "insiderHolders", "netSharePurchaseActivity"])
|
||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false"}
|
||||
result = self._data.get_raw_json(f"{_QUOTE_SUMMARY_URL_}/{self._symbol}", params=params_dict)
|
||||
return result
|
||||
|
||||
def _fetch_and_parse(self):
|
||||
try:
|
||||
result = self._fetch()
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
|
||||
self._major = pd.DataFrame()
|
||||
self._major_direct_holders = pd.DataFrame()
|
||||
self._institutional = pd.DataFrame()
|
||||
self._mutualfund = pd.DataFrame()
|
||||
self._insider_transactions = pd.DataFrame()
|
||||
self._insider_purchases = pd.DataFrame()
|
||||
self._insider_roster = pd.DataFrame()
|
||||
|
||||
return
|
||||
|
||||
try:
|
||||
data = result["quoteSummary"]["result"][0]
|
||||
# parse "institutionOwnership", "fundOwnership", "majorDirectHolders", "majorHoldersBreakdown", "insiderTransactions", "insiderHolders", "netSharePurchaseActivity"
|
||||
self._parse_institution_ownership(data.get("institutionOwnership", {}))
|
||||
self._parse_fund_ownership(data.get("fundOwnership", {}))
|
||||
# self._parse_major_direct_holders(data.get("majorDirectHolders", {})) # need more data to investigate
|
||||
self._parse_major_holders_breakdown(data.get("majorHoldersBreakdown", {}))
|
||||
self._parse_insider_transactions(data.get("insiderTransactions", {}))
|
||||
self._parse_insider_holders(data.get("insiderHolders", {}))
|
||||
self._parse_net_share_purchase_activity(data.get("netSharePurchaseActivity", {}))
|
||||
except (KeyError, IndexError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
raise YFDataException("Failed to parse holders json data.")
|
||||
|
||||
@staticmethod
|
||||
def _parse_raw_values(data):
|
||||
if isinstance(data, dict) and "raw" in data:
|
||||
return data["raw"]
|
||||
return data
|
||||
|
||||
def _parse_institution_ownership(self, data):
|
||||
holders = data.get("ownershipList", {})
|
||||
for owner in holders:
|
||||
for k, v in owner.items():
|
||||
owner[k] = self._parse_raw_values(v)
|
||||
del owner["maxAge"]
|
||||
df = pd.DataFrame(holders)
|
||||
if not df.empty:
|
||||
df["reportDate"] = pd.to_datetime(df["reportDate"], unit="s")
|
||||
df.rename(columns={"reportDate": "Date Reported", "organization": "Holder", "position": "Shares", "value": "Value"}, inplace=True) # "pctHeld": "% Out"
|
||||
self._institutional = df
|
||||
|
||||
def _parse_fund_ownership(self, data):
|
||||
holders = data.get("ownershipList", {})
|
||||
for owner in holders:
|
||||
for k, v in owner.items():
|
||||
owner[k] = self._parse_raw_values(v)
|
||||
del owner["maxAge"]
|
||||
df = pd.DataFrame(holders)
|
||||
if not df.empty:
|
||||
df["reportDate"] = pd.to_datetime(df["reportDate"], unit="s")
|
||||
df.rename(columns={"reportDate": "Date Reported", "organization": "Holder", "position": "Shares", "value": "Value"}, inplace=True)
|
||||
self._mutualfund = df
|
||||
|
||||
def _parse_major_direct_holders(self, data):
|
||||
holders = data.get("holders", {})
|
||||
for owner in holders:
|
||||
for k, v in owner.items():
|
||||
owner[k] = self._parse_raw_values(v)
|
||||
del owner["maxAge"]
|
||||
df = pd.DataFrame(holders)
|
||||
if not df.empty:
|
||||
df["reportDate"] = pd.to_datetime(df["reportDate"], unit="s")
|
||||
df.rename(columns={"reportDate": "Date Reported", "organization": "Holder", "positionDirect": "Shares", "valueDirect": "Value"}, inplace=True)
|
||||
self._major_direct_holders = df
|
||||
|
||||
def _parse_major_holders_breakdown(self, data):
|
||||
if "maxAge" in data:
|
||||
del data["maxAge"]
|
||||
df = pd.DataFrame.from_dict(data, orient="index")
|
||||
if not df.empty:
|
||||
df.columns.name = "Breakdown"
|
||||
df.rename(columns={df.columns[0]: 'Value'}, inplace=True)
|
||||
self._major = df
|
||||
|
||||
def _parse_insider_transactions(self, data):
|
||||
holders = data.get("transactions", {})
|
||||
for owner in holders:
|
||||
for k, v in owner.items():
|
||||
owner[k] = self._parse_raw_values(v)
|
||||
del owner["maxAge"]
|
||||
df = pd.DataFrame(holders)
|
||||
if not df.empty:
|
||||
df["startDate"] = pd.to_datetime(df["startDate"], unit="s")
|
||||
df.rename(columns={
|
||||
"startDate": "Start Date",
|
||||
"filerName": "Insider",
|
||||
"filerRelation": "Position",
|
||||
"filerUrl": "URL",
|
||||
"moneyText": "Transaction",
|
||||
"transactionText": "Text",
|
||||
"shares": "Shares",
|
||||
"value": "Value",
|
||||
"ownership": "Ownership" # ownership flag, direct or institutional
|
||||
}, inplace=True)
|
||||
self._insider_transactions = df
|
||||
|
||||
def _parse_insider_holders(self, data):
|
||||
holders = data.get("holders", {})
|
||||
for owner in holders:
|
||||
for k, v in owner.items():
|
||||
owner[k] = self._parse_raw_values(v)
|
||||
del owner["maxAge"]
|
||||
df = pd.DataFrame(holders)
|
||||
if not df.empty:
|
||||
if "positionDirectDate" in df:
|
||||
df["positionDirectDate"] = pd.to_datetime(df["positionDirectDate"], unit="s")
|
||||
if "latestTransDate" in df:
|
||||
df["latestTransDate"] = pd.to_datetime(df["latestTransDate"], unit="s")
|
||||
|
||||
df.rename(columns={
|
||||
"name": "Name",
|
||||
"relation": "Position",
|
||||
"url": "URL",
|
||||
"transactionDescription": "Most Recent Transaction",
|
||||
"latestTransDate": "Latest Transaction Date",
|
||||
"positionDirectDate": "Position Direct Date",
|
||||
"positionDirect": "Shares Owned Directly",
|
||||
"positionIndirectDate": "Position Indirect Date",
|
||||
"positionIndirect": "Shares Owned Indirectly"
|
||||
}, inplace=True)
|
||||
|
||||
df["Name"] = df["Name"].astype(str)
|
||||
df["Position"] = df["Position"].astype(str)
|
||||
df["URL"] = df["URL"].astype(str)
|
||||
df["Most Recent Transaction"] = df["Most Recent Transaction"].astype(str)
|
||||
|
||||
self._insider_roster = df
|
||||
|
||||
def _parse_net_share_purchase_activity(self, data):
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"Insider Purchases Last " + data.get("period", ""): [
|
||||
"Purchases",
|
||||
"Sales",
|
||||
"Net Shares Purchased (Sold)",
|
||||
"Total Insider Shares Held",
|
||||
"% Net Shares Purchased (Sold)",
|
||||
"% Buy Shares",
|
||||
"% Sell Shares"
|
||||
],
|
||||
"Shares": [
|
||||
data.get('buyInfoShares'),
|
||||
data.get('sellInfoShares'),
|
||||
data.get('netInfoShares'),
|
||||
data.get('totalInsiderShares'),
|
||||
data.get('netPercentInsiderShares'),
|
||||
data.get('buyPercentInsiderShares'),
|
||||
data.get('sellPercentInsiderShares')
|
||||
],
|
||||
"Trans": [
|
||||
data.get('buyInfoCount'),
|
||||
data.get('sellInfoCount'),
|
||||
data.get('netInfoCount'),
|
||||
pd.NA,
|
||||
pd.NA,
|
||||
pd.NA,
|
||||
pd.NA
|
||||
]
|
||||
}
|
||||
).convert_dtypes()
|
||||
self._insider_purchases = df
|
||||
@@ -0,0 +1,779 @@
|
||||
import curl_cffi
|
||||
import datetime
|
||||
import json
|
||||
import numpy as _np
|
||||
import pandas as pd
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.config import YfConfig
|
||||
from yfinance.const import quote_summary_valid_modules, _BASE_URL_, _QUERY1_URL_
|
||||
from yfinance.data import YfData
|
||||
from yfinance.exceptions import YFDataException, YFException
|
||||
|
||||
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", "52WeekChange", "fiftyDayAverage", "twoHundredDayAverage"})
|
||||
info_retired_keys_price.update({"averageDailyVolume10Day", "averageVolume10days", "averageVolume"})
|
||||
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
|
||||
|
||||
|
||||
_QUOTE_SUMMARY_URL_ = f"{_BASE_URL_}/v10/finance/quoteSummary"
|
||||
|
||||
|
||||
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 not '{type(k)}'")
|
||||
if k not 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):
|
||||
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="1y", auto_adjust=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 Exception:
|
||||
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.now('UTC').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="5d", interval="1h", auto_adjust=False, prepost=True)
|
||||
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="5d", interval="1h", auto_adjust=False, prepost=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.now('UTC')
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.now('UTC').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:
|
||||
def __init__(self, data: YfData, symbol: str):
|
||||
self._data = data
|
||||
self._symbol = symbol
|
||||
|
||||
self._info = None
|
||||
self._retired_info = None
|
||||
self._sustainability = None
|
||||
self._recommendations = None
|
||||
self._upgrades_downgrades = None
|
||||
self._calendar = None
|
||||
self._sec_filings = None
|
||||
|
||||
self._already_scraped = False
|
||||
self._already_fetched = False
|
||||
self._already_fetched_complementary = False
|
||||
|
||||
@property
|
||||
def info(self) -> dict:
|
||||
if self._info is None:
|
||||
self._fetch_info()
|
||||
self._fetch_complementary()
|
||||
|
||||
return self._info
|
||||
|
||||
@property
|
||||
def sustainability(self) -> pd.DataFrame:
|
||||
if self._sustainability is None:
|
||||
result = self._fetch(modules=['esgScores'])
|
||||
if result is None:
|
||||
self._sustainability = pd.DataFrame()
|
||||
else:
|
||||
try:
|
||||
data = result["quoteSummary"]["result"][0]
|
||||
except (KeyError, IndexError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
raise YFDataException(f"Failed to parse json response from Yahoo Finance: {result}")
|
||||
self._sustainability = pd.DataFrame(data)
|
||||
return self._sustainability
|
||||
|
||||
@property
|
||||
def recommendations(self) -> pd.DataFrame:
|
||||
if self._recommendations is None:
|
||||
result = self._fetch(modules=['recommendationTrend'])
|
||||
if result is None:
|
||||
self._recommendations = pd.DataFrame()
|
||||
else:
|
||||
try:
|
||||
data = result["quoteSummary"]["result"][0]["recommendationTrend"]["trend"]
|
||||
except (KeyError, IndexError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
raise YFDataException(f"Failed to parse json response from Yahoo Finance: {result}")
|
||||
self._recommendations = pd.DataFrame(data)
|
||||
return self._recommendations
|
||||
|
||||
@property
|
||||
def upgrades_downgrades(self) -> pd.DataFrame:
|
||||
if self._upgrades_downgrades is None:
|
||||
result = self._fetch(modules=['upgradeDowngradeHistory'])
|
||||
if result is None:
|
||||
self._upgrades_downgrades = pd.DataFrame()
|
||||
else:
|
||||
try:
|
||||
data = result["quoteSummary"]["result"][0]["upgradeDowngradeHistory"]["history"]
|
||||
if len(data) == 0:
|
||||
raise YFDataException(f"No upgrade/downgrade history found for {self._symbol}")
|
||||
df = pd.DataFrame(data)
|
||||
df.rename(columns={"epochGradeDate": "GradeDate", 'firm': 'Firm', 'toGrade': 'ToGrade', 'fromGrade': 'FromGrade', 'action': 'Action'}, inplace=True)
|
||||
df.set_index('GradeDate', inplace=True)
|
||||
df.index = pd.to_datetime(df.index, unit='s')
|
||||
self._upgrades_downgrades = df
|
||||
except (KeyError, IndexError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
raise YFDataException(f"Failed to parse json response from Yahoo Finance: {result}")
|
||||
return self._upgrades_downgrades
|
||||
|
||||
@property
|
||||
def calendar(self) -> dict:
|
||||
if self._calendar is None:
|
||||
self._fetch_calendar()
|
||||
return self._calendar
|
||||
|
||||
@property
|
||||
def sec_filings(self) -> dict:
|
||||
if self._sec_filings is None:
|
||||
f = self._fetch_sec_filings()
|
||||
self._sec_filings = {} if f is None else f
|
||||
return self._sec_filings
|
||||
|
||||
@staticmethod
|
||||
def valid_modules():
|
||||
return quote_summary_valid_modules
|
||||
|
||||
def _fetch(self, modules: list):
|
||||
if not isinstance(modules, list):
|
||||
raise YFException("Should provide a list of modules, see available modules using `valid_modules`")
|
||||
|
||||
modules = ','.join([m for m in modules if m in quote_summary_valid_modules])
|
||||
if len(modules) == 0:
|
||||
raise YFException("No valid modules provided, see available modules using `valid_modules`")
|
||||
params_dict = {"modules": modules, "corsDomain": "finance.yahoo.com", "formatted": "false", "symbol": self._symbol}
|
||||
try:
|
||||
result = self._data.get_raw_json(_QUOTE_SUMMARY_URL_ + f"/{self._symbol}", params=params_dict)
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
return None
|
||||
return result
|
||||
|
||||
def _fetch_additional_info(self):
|
||||
params_dict = {"symbols": self._symbol, "formatted": "false"}
|
||||
try:
|
||||
result = self._data.get_raw_json(f"{_QUERY1_URL_}/v7/finance/quote?", params=params_dict)
|
||||
except curl_cffi.requests.exceptions.HTTPError as e:
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
utils.get_yf_logger().error(str(e) + e.response.text)
|
||||
return None
|
||||
return result
|
||||
|
||||
def _fetch_info(self):
|
||||
if self._already_fetched:
|
||||
return
|
||||
self._already_fetched = True
|
||||
modules = ['financialData', 'quoteType', 'defaultKeyStatistics', 'assetProfile', 'summaryDetail']
|
||||
result = self._fetch(modules=modules)
|
||||
additional_info = self._fetch_additional_info()
|
||||
if additional_info is not None and result is not None:
|
||||
result.update(additional_info)
|
||||
else:
|
||||
result = additional_info
|
||||
|
||||
query1_info = {}
|
||||
for quote in ["quoteSummary", "quoteResponse"]:
|
||||
if quote in result and len(result[quote]["result"]) > 0:
|
||||
result[quote]["result"][0]["symbol"] = self._symbol
|
||||
query_info = next(
|
||||
(info for info in result.get(quote, {}).get("result", [])
|
||||
if info["symbol"] == self._symbol),
|
||||
None,
|
||||
)
|
||||
if query_info:
|
||||
query1_info.update(query_info)
|
||||
|
||||
# Normalize and flatten nested dictionaries while converting maxAge from days (1) to seconds (86400).
|
||||
# This handles Yahoo Finance API inconsistency where maxAge is sometimes expressed in days instead of seconds.
|
||||
processed_info = {}
|
||||
for k, v in query1_info.items():
|
||||
|
||||
# Handle nested dictionary
|
||||
if isinstance(v, dict):
|
||||
for k1, v1 in v.items():
|
||||
if v1 is not None:
|
||||
processed_info[k1] = 86400 if k1 == "maxAge" and v1 == 1 else v1
|
||||
|
||||
elif v is not None:
|
||||
processed_info[k] = v
|
||||
|
||||
query1_info = processed_info
|
||||
|
||||
# recursively format but only because of 'companyOfficers'
|
||||
|
||||
def _format(k, v):
|
||||
if isinstance(v, dict) and "raw" in v and "fmt" in v:
|
||||
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):
|
||||
v2 = v.replace("\xa0", " ")
|
||||
else:
|
||||
v2 = v
|
||||
return v2
|
||||
|
||||
self._info = {k: _format(k, v) for k, v in query1_info.items()}
|
||||
|
||||
def _fetch_complementary(self):
|
||||
if self._already_fetched_complementary:
|
||||
return
|
||||
self._already_fetched_complementary = True
|
||||
|
||||
self._fetch_info()
|
||||
if self._info is None:
|
||||
return
|
||||
|
||||
# Complementary key-statistics. For now just want 'trailing PEG ratio'
|
||||
keys = {"trailingPegRatio"}
|
||||
if keys:
|
||||
# Simplified the original scrape code for key-statistics. Very expensive for fetching
|
||||
# just one value, best if scraping most/all:
|
||||
#
|
||||
# p = _re.compile(r'root\.App\.main = (.*);')
|
||||
# url = 'https://finance.yahoo.com/quote/{}/key-statistics?p={}'.format(self._ticker.ticker, self._ticker.ticker)
|
||||
# try:
|
||||
# r = session.get(url)
|
||||
# data = _json.loads(p.findall(r.text)[0])
|
||||
# key_stats = data['context']['dispatcher']['stores']['QuoteTimeSeriesStore']["timeSeries"]
|
||||
# for k in keys:
|
||||
# if k not in key_stats or len(key_stats[k])==0:
|
||||
# # Yahoo website prints N/A, indicates Yahoo lacks necessary data to calculate
|
||||
# v = None
|
||||
# else:
|
||||
# # Select most recent (last) raw value in list:
|
||||
# v = key_stats[k][-1]["reportedValue"]["raw"]
|
||||
# self._info[k] = v
|
||||
# except Exception:
|
||||
# raise
|
||||
# pass
|
||||
#
|
||||
# For just one/few variable is faster to query directly:
|
||||
url = f"https://query1.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/{self._symbol}?symbol={self._symbol}"
|
||||
for k in keys:
|
||||
url += "&type=" + k
|
||||
# Request 6 months of data
|
||||
start = pd.Timestamp.now('UTC').floor("D") - datetime.timedelta(days=365 // 2)
|
||||
start = int(start.timestamp())
|
||||
end = pd.Timestamp.now('UTC').ceil("D")
|
||||
end = int(end.timestamp())
|
||||
url += f"&period1={start}&period2={end}"
|
||||
|
||||
json_str = self._data.cache_get(url=url).text
|
||||
json_data = json.loads(json_str)
|
||||
json_result = json_data.get("timeseries") or json_data.get("finance")
|
||||
if json_result["error"] is not None:
|
||||
raise YFException("Failed to parse json response from Yahoo Finance: " + str(json_result["error"]))
|
||||
for k in keys:
|
||||
keydict = json_result["result"][0]
|
||||
if k in keydict:
|
||||
self._info[k] = keydict[k][-1]["reportedValue"]["raw"]
|
||||
else:
|
||||
self.info[k] = None
|
||||
|
||||
def _fetch_calendar(self):
|
||||
# secFilings return too old data, so not requesting it for now
|
||||
result = self._fetch(modules=['calendarEvents'])
|
||||
if result is None:
|
||||
self._calendar = {}
|
||||
return
|
||||
|
||||
try:
|
||||
self._calendar = dict()
|
||||
_events = result["quoteSummary"]["result"][0]["calendarEvents"]
|
||||
if 'dividendDate' in _events:
|
||||
self._calendar['Dividend Date'] = datetime.datetime.fromtimestamp(_events['dividendDate']).date()
|
||||
if 'exDividendDate' in _events:
|
||||
self._calendar['Ex-Dividend Date'] = datetime.datetime.fromtimestamp(_events['exDividendDate']).date()
|
||||
# splits = _events.get('splitDate') # need to check later, i will add code for this if found data
|
||||
earnings = _events.get('earnings')
|
||||
if earnings is not None:
|
||||
self._calendar['Earnings Date'] = [datetime.datetime.fromtimestamp(d).date() for d in earnings.get('earningsDate', [])]
|
||||
self._calendar['Earnings High'] = earnings.get('earningsHigh', None)
|
||||
self._calendar['Earnings Low'] = earnings.get('earningsLow', None)
|
||||
self._calendar['Earnings Average'] = earnings.get('earningsAverage', None)
|
||||
self._calendar['Revenue High'] = earnings.get('revenueHigh', None)
|
||||
self._calendar['Revenue Low'] = earnings.get('revenueLow', None)
|
||||
self._calendar['Revenue Average'] = earnings.get('revenueAverage', None)
|
||||
except (KeyError, IndexError):
|
||||
if not YfConfig.debug.hide_exceptions:
|
||||
raise
|
||||
raise YFDataException(f"Failed to parse json response from Yahoo Finance: {result}")
|
||||
|
||||
|
||||
def _fetch_sec_filings(self):
|
||||
result = self._fetch(modules=['secFilings'])
|
||||
if result is None:
|
||||
return None
|
||||
|
||||
filings = result["quoteSummary"]["result"][0]["secFilings"]["filings"]
|
||||
|
||||
# Improve structure
|
||||
for f in filings:
|
||||
if 'exhibits' in f:
|
||||
f['exhibits'] = {e['type']:e['url'] for e in f['exhibits']}
|
||||
f['date'] = datetime.datetime.strptime(f['date'], '%Y-%m-%d').date()
|
||||
|
||||
# Experimental: convert to pandas
|
||||
# for i in range(len(filings)):
|
||||
# f = filings[i]
|
||||
# if 'exhibits' in f:
|
||||
# for e in f['exhibits']:
|
||||
# f[e['type']] = e['url']
|
||||
# del f['exhibits']
|
||||
# filings[i] = f
|
||||
# filings = pd.DataFrame(filings)
|
||||
# for c in filings.columns:
|
||||
# if c.startswith('EX-'):
|
||||
# filings[c] = filings[c].astype(str)
|
||||
# filings.loc[filings[c]=='nan', c] = ''
|
||||
# filings = filings.drop('epochDate', axis=1)
|
||||
# filings = filings.set_index('date')
|
||||
|
||||
return filings
|
||||
Reference in New Issue
Block a user