Import python venv for stability

This commit is contained in:
2026-02-15 21:24:16 -08:00
parent 1343e93a59
commit 7d784705c9
4997 changed files with 1628270 additions and 0 deletions
@@ -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 = []
@@ -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")
@@ -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