Skip to content

Commit 9d672c6

Browse files
Don't claim 'possibly delisted' when Yahoo explains the missing data
Every YFPricesMissingError message speculates 'possibly delisted', even when Yahoo's response carries an explicit reason that has nothing to do with delisting, e.g. an intraday range outside the last 60 days. That speculation has misled users for years (see #1713, #1797, #2044, #2052). And when a ticker really is dead, Yahoo's quoted description already says 'No data found, symbol may be delisted', making the prefix redundant there too. Add possibly_delisted=True parameter to YFTickerMissingError and YFPricesMissingError (default keeps every other callsite unchanged) and pass False in the one history() branch where Yahoo supplied an error description, letting Yahoo's stated reason stand. Adds an offline test of both message formats and a live test that a range error no longer claims delisting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a9de4ba commit 9d672c6

3 files changed

Lines changed: 44 additions & 6 deletions

File tree

‎tests/test_prices.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,31 @@ def test_30m_interval_error_mentions_requested_interval(self):
496496
self.assertIn("(30m ", msg)
497497
self.assertNotIn("(15m ", msg)
498498

499+
def test_prices_missing_error_delisted_prefix(self):
500+
# Offline check of YFPricesMissingError message formats: the
501+
# 'possibly delisted' speculation must be suppressible when the
502+
# real cause of missing data is known.
503+
from yfinance.exceptions import YFPricesMissingError
504+
e = YFPricesMissingError("AAA", "(1d 2020-01-01 -> 2020-01-02)")
505+
self.assertEqual(str(e), "$AAA: possibly delisted; no price data found (1d 2020-01-01 -> 2020-01-02)")
506+
e = YFPricesMissingError("AAA", "(1d 2020-01-01 -> 2020-01-02)", possibly_delisted=False)
507+
self.assertEqual(str(e), "$AAA: no price data found (1d 2020-01-01 -> 2020-01-02)")
508+
509+
def test_range_error_does_not_claim_delisting(self):
510+
# A request Yahoo rejects with an explicit range error is not
511+
# delisting evidence, so the message must not speculate 'possibly
512+
# delisted'. When a ticker really is dead, Yahoo's quoted error
513+
# text still communicates it.
514+
from yfinance.exceptions import YFPricesMissingError
515+
dat = yf.Ticker("SPY", session=self.session)
516+
start_d = _dt.date.today() - _dt.timedelta(days=120)
517+
end_d = start_d + _dt.timedelta(days=7)
518+
with self.assertRaises(YFPricesMissingError) as cm:
519+
dat.history(start=start_d, end=end_d, interval="30m", raise_errors=True)
520+
msg = str(cm.exception)
521+
self.assertNotIn("possibly delisted", msg)
522+
self.assertIn("no price data found", msg)
523+
499524

500525
if __name__ == '__main__':
501526
unittest.main()

‎yfinance/exceptions.py‎

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,15 @@ def __init__(self, method_name):
1313

1414

1515
class YFTickerMissingError(YFException):
16-
def __init__(self, ticker, rationale):
17-
super().__init__(f"${ticker}: possibly delisted; {rationale}")
16+
"""Expected ticker data is missing.
17+
18+
By default the message speculates the ticker is delisted, because Yahoo
19+
rarely explains the absence. Pass possibly_delisted=False when the real
20+
cause is known, so the message does not mislead.
21+
"""
22+
def __init__(self, ticker, rationale, possibly_delisted=True):
23+
prefix = "possibly delisted; " if possibly_delisted else ""
24+
super().__init__(f"${ticker}: {prefix}{rationale}")
1825
self.rationale = rationale
1926
self.ticker = ticker
2027

@@ -25,12 +32,17 @@ def __init__(self, ticker):
2532

2633

2734
class YFPricesMissingError(YFTickerMissingError):
28-
def __init__(self, ticker, debug_info):
35+
"""Yahoo returned no price data for the requested range/interval.
36+
37+
possibly_delisted=False suppresses the delisting speculation, e.g. when
38+
Yahoo's response contains an explicit reason for the missing data.
39+
"""
40+
def __init__(self, ticker, debug_info, possibly_delisted=True):
2941
self.debug_info = debug_info
3042
if debug_info != '':
31-
super().__init__(ticker, f"no price data found {debug_info}")
43+
super().__init__(ticker, f"no price data found {debug_info}", possibly_delisted)
3244
else:
33-
super().__init__(ticker, "no price data found")
45+
super().__init__(ticker, "no price data found", possibly_delisted)
3446

3547

3648
class YFEarningsDateMissing(YFTickerMissingError):

‎yfinance/scrapers/history.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,8 @@ def history(self, period=period_default, interval="1d",
282282
# Yahoo's error names the fetched interval, which can differ from
283283
# the user's request (e.g. 30m is fetched as 15m).
284284
_price_data_debug += f' ({interval_user} resampled from {params["interval"]})'
285-
_exception = YFPricesMissingError(self.ticker, _price_data_debug)
285+
# Yahoo gave an explicit reason, so don't speculate about delisting.
286+
_exception = YFPricesMissingError(self.ticker, _price_data_debug, possibly_delisted=False)
286287
fail = True
287288
elif "chart" not in data or not data["chart"] or data["chart"]["result"] is None or not data["chart"]["result"] or not data["chart"]["result"][0]["indicators"]["quote"][0]:
288289
_exception = YFPricesMissingError(self.ticker, _price_data_debug)

0 commit comments

Comments
 (0)