Skip to content

Commit

Permalink
Add error classes for symbol delisting errors, closes ranaroussi#270
Browse files Browse the repository at this point in the history
  • Loading branch information
elibroftw committed Apr 24, 2024
1 parent a1bcb4c commit 5225c53
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 29 deletions.
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 YFinanceEarningsDateMissing
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 = YFinanceEarningsDateMissing(self.ticker)
err_msg = str(_exception)
logger.error(f'{self.ticker}: {err_msg}')
return None
dates = dates.reset_index(drop=True)
Expand Down
39 changes: 38 additions & 1 deletion yfinance/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,49 @@
class YFinanceException(Exception):
pass
def __init__(self, description=""):
super().__init__(description)


class YFinanceDataException(YFinanceException):
pass


class YFinanceChartError(YFinanceException):
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 YFinanceTickerMissingError(YFinanceException):
def __init__(self, ticker, rationale):
super().__init__(f"${ticker}: possibly delisted; {rationale}")
self.rationale = rationale
self.ticker = rationale


class YFinanceTimezoneMissingError(YFinanceTickerMissingError):
def __init__(self, ticker):
super().__init__(ticker, "No timezone found")


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


class YFinanceEarningsDateMissing(YFinanceTickerMissingError):
def __init__(self, ticker):
super().__init__(ticker, "No earnings dates found")


class YFinanceInvalidPeriodError(YFinanceException):
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}")
59 changes: 33 additions & 26 deletions yfinance/scrapers/history.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

import datetime as _datetime
import dateutil as _dateutil
import logging
Expand All @@ -8,6 +7,7 @@

from yfinance import shared, utils
from yfinance.const import _BASE_URL_, _PRICE_COLNAMES_
from yfinance.exceptions import YFinanceChartError, YFinanceInvalidPeriodError, YFinancePriceDataMissingError, YFinanceTimezoneMissingError

class PriceHistory:
def __init__(self, data, ticker, tz, session=None, proxy=None):
Expand All @@ -23,7 +23,7 @@ def __init__(self, data, ticker, tz, session=None, proxy=None):

# Limit recursion depth when repairing prices
self._reconstruct_start_interval = None

@utils.log_indent_decorator
def history(self, period="1mo", interval="1d",
start=None, end=None, prepost=False, actions=True,
Expand Down Expand Up @@ -80,14 +80,15 @@ def history(self, period="1mo", interval="1d",
# Check can get TZ. Fail => probably delisted
tz = self.tz
if tz is None:
# Every valid ticker has a timezone. Missing = problem
err_msg = "No timezone found, symbol may be delisted"
# Every valid ticker has a timezone. A missing timezone is a problem problem
_exception = YFinanceTimezoneMissingError(self.ticker)
err_msg = str(_exception)
shared._DFS[self.ticker] = utils.empty_df()
shared._ERRORS[self.ticker] = err_msg
shared._ERRORS[self.ticker] = err_msg.split(': ', 1)[1]
if raise_errors:
raise Exception(f'{self.ticker}: {err_msg}')
raise _exception
else:
logger.error(f'{self.ticker}: {err_msg}')
logger.error(err_msg)
return utils.empty_df()

if end is None:
Expand Down Expand Up @@ -159,48 +160,54 @@ def history(self, period="1mo", interval="1d",
self._history_metadata = {}

intraday = params["interval"][-1] in ("m", 'h')
err_msg = "No price data found, symbol may be delisted"
_price_data_debug = ''
_exception = YFinancePriceDataMissingError(self.ticker, '')
if start or period is None or period.lower() == "max":
err_msg += f' ({params["interval"]} '
_price_data_debug += f' ({params["interval"]} '
if start_user is not None:
err_msg += f'{start_user}'
_price_data_debug += f'{start_user}'
elif not intraday:
err_msg += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz).date()}'
_price_data_debug += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz).date()}'
else:
err_msg += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz)}'
err_msg += ' -> '
_price_data_debug += f'{pd.Timestamp(start, unit="s").tz_localize("UTC").tz_convert(tz)}'
_price_data_debug += ' -> '
if end_user is not None:
err_msg += f'{end_user})'
_price_data_debug += f'{end_user})'
elif not intraday:
err_msg += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz).date()})'
_price_data_debug += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz).date()})'
else:
err_msg += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz)})'
_price_data_debug += f'{pd.Timestamp(end, unit="s").tz_localize("UTC").tz_convert(tz)})'
else:
err_msg += f' (period={period})'
_price_data_debug += f' (period={period})'

fail = False
if data is None or not isinstance(data, dict):
fail = True
elif isinstance(data, dict) and 'status_code' in data:
err_msg += f"(Yahoo status_code = {data['status_code']})"
_price_data_debug += f"(Yahoo status_code = {data['status_code']})"
fail = True
elif "chart" in data and data["chart"]["error"]:
err_msg = data["chart"]["error"]["description"]
_exception = YFinanceChartError(self.ticker, data["chart"]["error"]["description"])
fail = True
elif "chart" not in data or data["chart"]["result"] is None or not data["chart"]["result"]:
fail = True
elif period is not None and "timestamp" not in data["chart"]["result"][0] and period not in \
self._history_metadata["validRanges"]:
# User provided a bad period. The minimum should be '1d', but sometimes Yahoo accepts '1h'.
err_msg = f"Period '{period}' is invalid, must be one of {self._history_metadata['validRanges']}"
_exception = YFinanceInvalidPeriodError(self.ticker, period, self._history_metadata['validRanges'])
fail = True

if isinstance(_exception, YFinancePriceDataMissingError):
_exception = YFinancePriceDataMissingError(self.ticker, _price_data_debug)

err_msg = str(_exception)
if fail:
shared._DFS[self.ticker] = utils.empty_df()
shared._ERRORS[self.ticker] = err_msg
shared._ERRORS[self.ticker] = err_msg.split(': ', 1)[1]
if raise_errors:
raise Exception(f'{self.ticker}: {err_msg}')
raise _exception
else:
logger.error(f'{self.ticker}: {err_msg}')
logger.error(err_msg)
if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
self._reconstruct_start_interval = None
return utils.empty_df()
Expand All @@ -215,11 +222,11 @@ def history(self, period="1mo", interval="1d",
quotes = quotes.iloc[0:quotes.shape[0] - 1]
except Exception:
shared._DFS[self.ticker] = utils.empty_df()
shared._ERRORS[self.ticker] = err_msg
shared._ERRORS[self.ticker] = err_msg.split(': ', 1)[1]
if raise_errors:
raise Exception(f'{self.ticker}: {err_msg}')
raise Exception(err_msg)
else:
logger.error(f'{self.ticker}: {err_msg}')
logger.error(err_msg)
if self._reconstruct_start_interval is not None and self._reconstruct_start_interval == interval:
self._reconstruct_start_interval = None
return shared._DFS[self.ticker]
Expand Down

0 comments on commit 5225c53

Please sign in to comment.