Skip to content

Commit

Permalink
implementing support for funds data
Browse files Browse the repository at this point in the history
- adding to scraper a new class FundsData.
- Ticker can now reference the data via ticker.funds_data when appropriate.
- Add unit tests in test_ticker.py for the funds data
  • Loading branch information
ericpien committed Sep 7, 2024
1 parent a2b5d6b commit f584b0c
Show file tree
Hide file tree
Showing 5 changed files with 350 additions and 3 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,31 @@ opt = msft.option_chain('YYYY-MM-DD')
# data available via: opt.calls, opt.puts
```

For tickers that are ETFs/Mutual Funds, `Ticker.funds_data` provides access to fund related data.

Funds' Top Holdings and other data with category average is returned as `pd.DataFrame`.

```python
import yfinance as yf
spy = yf.Ticker('SPY')
data = spy.funds_data

# show fund description
data.description

# show operational information
data.fund_overview
data.fund_operations

# show holdings related information
data.asset_classes
data.top_holdings
data.equity_holdings
data.bond_holdings
data.bond_ratings
data.sector_weightings
```

If you want to use a proxy server for downloading data, use:

```python
Expand Down
84 changes: 81 additions & 3 deletions tests/test_ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from .context import yfinance as yf
from .context import session_gbl
from yfinance.exceptions import YFChartError, YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError
from yfinance.exceptions import YFPricesMissingError, YFInvalidPeriodError, YFNotImplementedError, YFTickerMissingError, YFTzMissingError, YFDataException


import unittest
Expand Down Expand Up @@ -142,14 +142,14 @@ def test_prices_missing(self):
# META call option, 2024 April 26th @ strike of 180000
tkr = 'META240426C00180000'
dat = yf.Ticker(tkr, session=self.session)
with self.assertRaises(YFChartError):
with self.assertRaises(YFPricesMissingError):
dat.history(period="5d", interval="1m", raise_errors=True)

def test_ticker_missing(self):
tkr = 'ATVI'
dat = yf.Ticker(tkr, session=self.session)
# A missing ticker can trigger either a niche error or the generalized error
with self.assertRaises((YFTickerMissingError, YFTzMissingError, YFChartError)):
with self.assertRaises((YFTickerMissingError, YFTzMissingError, YFPricesMissingError)):
dat.history(period="3mo", interval="1d", raise_errors=True)

def test_goodTicker(self):
Expand Down Expand Up @@ -997,7 +997,84 @@ def test_complementary_info(self):
# else:
# raise

class TestTickerFundsData(unittest.TestCase):
session = None

@classmethod
def setUpClass(cls):
cls.session = session_gbl

@classmethod
def tearDownClass(cls):
if cls.session is not None:
cls.session.close()

def setUp(self):
self.test_tickers = [yf.Ticker("SPY", session=self.session), # equity etf
yf.Ticker("JNK", session=self.session), # bonds etf
yf.Ticker("VTSAX", session=self.session)] # mutual fund

def tearDown(self):
self.ticker = None

def test_fetch_and_parse(self):
try:
for ticker in self.test_tickers:
ticker.funds_data._fetch_and_parse()

except Exception as e:
self.fail(f"_fetch_and_parse raised an exception unexpectedly: {e}")

with self.assertRaises(YFDataException):
ticker = yf.Ticker("AAPL", session=self.session) # stock, not funds
ticker.funds_data._fetch_and_parse()
self.fail("_fetch_and_parse should have failed when calling for non-funds data")

def test_description(self):
for ticker in self.test_tickers:
description = ticker.funds_data.description
self.assertIsInstance(description, str)
self.assertTrue(len(description) > 0)

def test_fund_overview(self):
for ticker in self.test_tickers:
fund_overview = ticker.funds_data.fund_overview
self.assertIsInstance(fund_overview, dict)

def test_fund_operations(self):
for ticker in self.test_tickers:
fund_operations = ticker.funds_data.fund_operations
self.assertIsInstance(fund_operations, pd.DataFrame)

def test_asset_classes(self):
for ticker in self.test_tickers:
asset_classes = ticker.funds_data.asset_classes
self.assertIsInstance(asset_classes, dict)

def test_top_holdings(self):
for ticker in self.test_tickers:
top_holdings = ticker.funds_data.top_holdings
self.assertIsInstance(top_holdings, pd.DataFrame)

def test_equity_holdings(self):
for ticker in self.test_tickers:
equity_holdings = ticker.funds_data.equity_holdings
self.assertIsInstance(equity_holdings, pd.DataFrame)

def test_bond_holdings(self):
for ticker in self.test_tickers:
bond_holdings = ticker.funds_data.bond_holdings
self.assertIsInstance(bond_holdings, pd.DataFrame)

def test_bond_ratings(self):
for ticker in self.test_tickers:
bond_ratings = ticker.funds_data.bond_ratings
self.assertIsInstance(bond_ratings, dict)

def test_sector_weightings(self):
for ticker in self.test_tickers:
sector_weightings = ticker.funds_data.sector_weightings
self.assertIsInstance(sector_weightings, dict)

def suite():
suite = unittest.TestSuite()
Expand All @@ -1007,6 +1084,7 @@ def suite():
suite.addTest(TestTickerHistory('Test Ticker history'))
suite.addTest(TestTickerMiscFinancials('Test misc financials'))
suite.addTest(TestTickerInfo('Test info & fast_info'))
suite.addTest(TestTickerFundsData('Test Funds Data'))
return suite


Expand Down
8 changes: 8 additions & 0 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from .scrapers.holders import Holders
from .scrapers.quote import Quote, FastInfo
from .scrapers.history import PriceHistory
from .scrapers.funds import FundsData

from .const import _BASE_URL_, _ROOT_URL_

Expand Down Expand Up @@ -70,6 +71,7 @@ def __init__(self, ticker, session=None, proxy=None):
self._holders = Holders(self._data, self.ticker)
self._quote = Quote(self._data, self.ticker)
self._fundamentals = Fundamentals(self._data, self.ticker)
self._funds_data = None

self._fast_info = None

Expand Down Expand Up @@ -647,3 +649,9 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:

def get_history_metadata(self, proxy=None) -> dict:
return self._lazy_load_price_history().get_history_metadata(proxy)

def get_funds_data(self, proxy=None) -> Optional[FundsData]:
if not self._funds_data:
self._funds_data = FundsData(self._data, self.ticker)

return self._funds_data
Loading

0 comments on commit f584b0c

Please sign in to comment.