Skip to content

Commit

Permalink
Merge pull request #1928 from marcofognog/dev
Browse files Browse the repository at this point in the history
Add more specific error thowring base on PR 1918
  • Loading branch information
ValueRaider authored May 11, 2024
2 parents 098e776 + 7628bec commit 070f135
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 66 deletions.
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

0 comments on commit 070f135

Please sign in to comment.