Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more specific error throwing based on PR 1918 #1928

Merged
merged 5 commits into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ msft.recommendations
msft.recommendations_summary
msft.upgrades_downgrades

# Show future and historic earnings dates, returns at most next 4 quarters and last 8 quarters by default.
# Show future and historic earnings dates, returns at most next 4 quarters and last 8 quarters by default.
# Note: If more are needed use msft.get_earnings_dates(limit=XX) with increased limit argument.
msft.earnings_dates

Expand Down Expand Up @@ -183,7 +183,7 @@ data = yf.download("SPY AAPL", period="1mo")

### Smarter scraping

Install the `nospam` packages for smarter scraping using `pip` (see [Installation](#installation)). These packages help cache calls such that Yahoo is not spammed with requests.
Install the `nospam` packages for smarter scraping using `pip` (see [Installation](#installation)). These packages help cache calls such that Yahoo is not spammed with requests.

To use a custom `requests` session, pass a `session=` argument to
the Ticker constructor. This allows for caching calls to the API as well as a custom way to modify requests via the `User-agent` header.
Expand Down Expand Up @@ -231,11 +231,13 @@ yfinance?](https://stackoverflow.com/questions/63107801)
### Persistent cache store

To reduce Yahoo, yfinance store some data locally: timezones to localize dates, and cookie. Cache location is:

- Windows = C:/Users/\<USER\>/AppData/Local/py-yfinance
- Linux = /home/\<USER\>/.cache/py-yfinance
- MacOS = /Users/\<USER\>/Library/Caches/py-yfinance

You can direct cache to use a different location with `set_tz_cache_location()`:

```python
import yfinance as yf
yf.set_tz_cache_location("custom/cache/location")
Expand All @@ -262,7 +264,7 @@ intended for research and educational purposes. You should refer to Yahoo!'s ter
([here](https://policies.yahoo.com/us/en/yahoo/terms/product-atos/apiforydn/index.htm),
[here](https://legal.yahoo.com/us/en/yahoo/terms/otos/index.html), and
[here](https://policies.yahoo.com/us/en/yahoo/terms/index.htm)) for
detailes on your rights to use the actual data downloaded.
details on your rights to use the actual data downloaded.

---

Expand Down
1 change: 0 additions & 1 deletion tests/__init__.py

This file was deleted.

4 changes: 2 additions & 2 deletions tests/prices.py → tests/test_prices.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ def test_download(self):

df_tkrs = df.columns.levels[1]
self.assertEqual(sorted(tkrs), sorted(df_tkrs))

def test_download_with_invalid_ticker(self):
#Checks if using an invalid symbol gives the same output as not using an invalid symbol in combination with a valid symbol (AAPL)
#Checks to make sure that invalid symbol handling for the date column is the same as the base case (no invalid symbols)

invalid_tkrs = ["AAPL", "ATVI"] #AAPL exists and ATVI does not exist
valid_tkrs = ["AAPL", "INTC"] #AAPL and INTC both exist

data_invalid_sym = yf.download(invalid_tkrs, start='2023-11-16', end='2023-11-17')
data_valid_sym = yf.download(valid_tkrs, start='2023-11-16', end='2023-11-17')

Expand Down
45 changes: 34 additions & 11 deletions tests/ticker.py → 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 YFNotImplementedError
from yfinance.exceptions import YFChartError, YFInvalidPeriodError, YFNotImplementedError, YFPricesMissingError, YFTickerMissingError, YFTzMissingError


import unittest
Expand Down Expand Up @@ -129,6 +129,30 @@ def test_badTicker(self):
assert isinstance(dat.actions, pd.DataFrame)
assert dat.actions.empty

def test_invalid_period(self):
tkr = 'VALE'
dat = yf.Ticker(tkr, session=self.session)
with self.assertRaises(YFInvalidPeriodError):
dat.history(period="2wks", interval="1d", raise_errors=True)
with self.assertRaises(YFInvalidPeriodError):
dat.history(period="2mo", interval="1d", raise_errors=True)


def test_prices_missing(self):
# this test will need to be updated every time someone wants to run a test
# hard to find a ticker that matches this error other than options
# META call option, 2024 April 26th @ strike of 180000
tkr = 'META240426C00180000'
dat = yf.Ticker(tkr, session=self.session)
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)):
dat.history(period="3mo", interval="1d", raise_errors=True)

def test_goodTicker(self):
# that yfinance works when full api is called on same instance of ticker
Expand All @@ -150,8 +174,8 @@ def test_goodTicker(self):
dat.fast_info[k]

for attribute_name, attribute_type in ticker_attributes:
assert_attribute_type(self, dat, attribute_name, attribute_type)
assert_attribute_type(self, dat, attribute_name, attribute_type)

def test_goodTicker_withProxy(self):
tkr = "IBM"
dat = yf.Ticker(tkr, session=self.session, proxy=self.proxy)
Expand All @@ -163,7 +187,7 @@ def test_goodTicker_withProxy(self):
for attribute_name, attribute_type in ticker_attributes:
assert_attribute_type(self, dat, attribute_name, attribute_type)


class TestTickerHistory(unittest.TestCase):
session = None

Expand Down Expand Up @@ -370,7 +394,7 @@ def test_insider_transactions(self):

data_cached = self.ticker.insider_transactions
self.assertIs(data, data_cached, "data not cached")

def test_insider_purchases(self):
data = self.ticker.insider_purchases
self.assertIsInstance(data, pd.DataFrame, "data has wrong type")
Expand Down Expand Up @@ -402,9 +426,9 @@ def tearDownClass(cls):

def setUp(self):
self.ticker = yf.Ticker("GOOGL", session=self.session)
# For ticker 'BSE.AX' (and others), Yahoo not returning
# full quarterly financials (usually cash-flow) with all entries,

# For ticker 'BSE.AX' (and others), Yahoo not returning
# full quarterly financials (usually cash-flow) with all entries,
# instead returns a smaller version in different data store.
self.ticker_old_fmt = yf.Ticker("BSE.AX", session=self.session)

Expand Down Expand Up @@ -713,7 +737,7 @@ def tearDownClass(cls):

def setUp(self):
self.ticker = yf.Ticker("GOOGL", session=self.session)

def tearDown(self):
self.ticker = None

Expand Down Expand Up @@ -813,7 +837,6 @@ def test_complementary_info(self):
# This one should have a trailing PEG ratio
data2 = self.tickers[2].info
self.assertIsInstance(data2['trailingPegRatio'], float)
pass

# def test_fast_info_matches_info(self):
# fast_info_keys = set()
Expand Down Expand Up @@ -851,7 +874,7 @@ def test_complementary_info(self):
# key_rename_map[yf.utils.snake_case_2_camelCase(k)] = key_rename_map[k]

# # Note: share count items in info[] are bad. Sometimes the float > outstanding!
# # So often fast_info["shares"] does not match.
# # So often fast_info["shares"] does not match.
# # Why isn't fast_info["shares"] wrong? Because using it to calculate market cap always correct.
# bad_keys = {"shares"}

Expand Down
File renamed without changes.
6 changes: 4 additions & 2 deletions yfinance/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from . import utils, cache
from .data import YfData
from .exceptions import YFEarningsDateMissing
from .scrapers.analysis import Analysis
from .scrapers.fundamentals import Fundamentals
from .scrapers.holders import Holders
Expand Down Expand Up @@ -192,7 +193,7 @@ def get_mutualfund_holders(self, proxy=None, as_dict=False):
if as_dict:
return data.to_dict()
return data

def get_insider_purchases(self, proxy=None, as_dict=False):
self._holders.proxy = proxy or self.proxy
data = self._holders.insider_purchases
Expand Down Expand Up @@ -567,7 +568,8 @@ def get_earnings_dates(self, limit=12, proxy=None) -> Optional[pd.DataFrame]:
page_size = min(limit - len(dates), page_size)

if dates is None or dates.shape[0] == 0:
err_msg = "No earnings dates found, symbol may be delisted"
_exception = YFEarningsDateMissing(self.ticker)
err_msg = str(_exception)
logger.error(f'{self.ticker}: {err_msg}')
return None
dates = dates.reset_index(drop=True)
Expand Down
44 changes: 41 additions & 3 deletions yfinance/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
class YFinanceException(Exception):
pass
class YFException(Exception):
def __init__(self, description=""):
super().__init__(description)


class YFinanceDataException(YFinanceException):
class YFDataException(YFException):
pass


class YFChartError(YFException):
def __init__(self, ticker, description):
self.ticker = ticker
super().__init__(f"{self.ticker}: {description}")


class YFNotImplementedError(NotImplementedError):
def __init__(self, method_name):
super().__init__(f"Have not implemented fetching '{method_name}' from Yahoo API")


class YFTickerMissingError(YFException):
def __init__(self, ticker, rationale):
super().__init__(f"${ticker}: possibly delisted; {rationale}")
self.rationale = rationale
self.ticker = ticker


class YFTzMissingError(YFTickerMissingError):
def __init__(self, ticker):
super().__init__(ticker, "No timezone found")


class YFPricesMissingError(YFTickerMissingError):
def __init__(self, ticker, debug_info):
self.debug_info = debug_info
super().__init__(ticker, f"No price data found {debug_info}")


class YFEarningsDateMissing(YFTickerMissingError):
# note that this does not get raised. Added in case of raising it in the future
def __init__(self, ticker):
super().__init__(ticker, "No earnings dates found")


class YFInvalidPeriodError(YFException):
def __init__(self, ticker, invalid_period, valid_ranges):
self.ticker = ticker
self.invalid_period = invalid_period
self.valid_ranges = valid_ranges
super().__init__(f"{self.ticker}: Period '{invalid_period}' is invalid, must be one of {valid_ranges}")
6 changes: 3 additions & 3 deletions yfinance/scrapers/fundamentals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from yfinance import utils, const
from yfinance.data import YfData
from yfinance.exceptions import YFinanceException, YFNotImplementedError
from yfinance.exceptions import YFException, YFNotImplementedError


class Fundamentals:
Expand Down Expand Up @@ -70,7 +70,7 @@ def get_cash_flow_time_series(self, freq="yearly", proxy=None) -> pd.DataFrame:
@utils.log_indent_decorator
def _fetch_time_series(self, name, timescale, proxy=None):
# Fetching time series preferred over scraping 'QuoteSummaryStore',
# because it matches what Yahoo shows. But for some tickers returns nothing,
# because it matches what Yahoo shows. But for some tickers returns nothing,
# despite 'QuoteSummaryStore' containing valid data.

allowed_names = ["income", "balance-sheet", "cash-flow"]
Expand All @@ -86,7 +86,7 @@ def _fetch_time_series(self, name, timescale, proxy=None):

if statement is not None:
return statement
except YFinanceException as e:
except YFException as e:
utils.get_yf_logger().error(f"{self._symbol}: Failed to create {name} financials table for reason: {e}")
return pd.DataFrame()

Expand Down
Loading
Loading