Files
Buffteks-Website/venv/lib/python3.12/site-packages/yfinance/scrapers/funds.py
2025-05-08 21:10:14 -05:00

336 lines
12 KiB
Python

import pandas as pd
from yfinance.data import YfData
from yfinance.const import _BASE_URL_, _SENTINEL_
from yfinance.exceptions import YFDataException
from yfinance import utils
from typing import Dict, Optional
_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, proxy=_SENTINEL_):
"""
Args:
data (YfData): The YfData object for fetching data.
symbol (str): The symbol of the fund.
"""
self._data = data
self._symbol = symbol
if proxy is not _SENTINEL_:
utils.print_once("YF deprecation warning: set proxy via new config function: yf.set_config(proxy=proxy)")
self._data._set_proxy(proxy)
# 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:
raise YFDataException("No Fund data found.")
except Exception as e:
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")