Skip to content

Commit e947a61

Browse files
committed
Fix get_history_metadata() raising YFPricesMissingError for valid tickers (ranaroussi#2745)
Two bugs in scrapers/history.py: 1. history() always overwrote _history_metadata with {} on a failed fetch, destroying previously cached good metadata from a prior successful call. Fix: only update _history_metadata when the new meta is non-empty or when it has never been set. 2. get_history_metadata() let YFPricesMissingError from the optional intraday tradingPeriods enrichment call propagate to the caller, even when currency, exchangeName, timezone, etc. were already cached. Fix: catch YFPricesMissingError around the enrichment call and return whatever metadata is already available.
1 parent c8c3754 commit e947a61

2 files changed

Lines changed: 132 additions & 2 deletions

File tree

‎tests/test_get_history_metadata.py‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Regression tests for issue #2745:
3+
get_history_metadata() raised YFPricesMissingError for valid tickers (e.g. ROG.SW)
4+
when the intraday enrichment request returned no data, even though history() had
5+
already populated _history_metadata with currency, exchangeName, timezone, etc.
6+
7+
Two bugs fixed:
8+
1. history() always overwrote _history_metadata with {} on a failed fetch,
9+
destroying previously cached good metadata.
10+
2. get_history_metadata() let the optional intraday fetch propagate
11+
YFPricesMissingError instead of returning the metadata already cached.
12+
"""
13+
import unittest
14+
from unittest.mock import MagicMock, call, patch
15+
16+
from yfinance.scrapers.history import PriceHistory
17+
from yfinance.exceptions import YFPricesMissingError
18+
19+
20+
def _make_ph(json_payload):
21+
"""Return a PriceHistory whose HTTP layer returns json_payload."""
22+
mock_resp = MagicMock()
23+
mock_resp.text = ""
24+
mock_resp.json.return_value = json_payload
25+
26+
mock_data = MagicMock()
27+
mock_data.get.return_value = mock_resp
28+
mock_data.cache_get.return_value = mock_resp
29+
30+
return PriceHistory(data=mock_data, ticker="FAKE", tz="America/New_York")
31+
32+
33+
def _good_chart(meta: dict) -> dict:
34+
"""Minimal Yahoo chart payload with valid meta and one price row."""
35+
return {
36+
"chart": {
37+
"result": [{
38+
"meta": meta,
39+
"timestamp": [1700000000],
40+
"indicators": {
41+
"quote": [{
42+
"open": [100.0],
43+
"high": [101.0],
44+
"low": [99.0],
45+
"close": [100.5],
46+
"volume": [1000000],
47+
}],
48+
"adjclose": [{"adjclose": [100.5]}],
49+
},
50+
"events": {},
51+
}],
52+
"error": None,
53+
}
54+
}
55+
56+
57+
_GOOD_META = {
58+
"currency": "CHF",
59+
"exchangeName": "EBS",
60+
"exchangeTimezoneName": "Europe/Zurich",
61+
"instrumentType": "EQUITY",
62+
"validRanges": ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"],
63+
"regularMarketTime": 1700000000,
64+
"regularMarketPrice": 250.0,
65+
"priceHint": 2,
66+
}
67+
68+
_EMPTY_CHART = {"chart": {"result": None, "error": None}}
69+
70+
71+
class TestMetadataNotOverwrittenOnFailure(unittest.TestCase):
72+
"""Fix 1: failed intraday fetch must not clobber previously cached metadata."""
73+
74+
def test_good_metadata_preserved_after_empty_intraday(self):
75+
ph = _make_ph(_good_chart(_GOOD_META))
76+
# First call: daily history succeeds and caches metadata
77+
ph.history(period="1mo", raise_errors=False)
78+
cached = ph._history_metadata.copy()
79+
self.assertEqual(cached.get("currency"), "CHF")
80+
81+
# Second call: intraday returns empty chart
82+
ph._data.get.return_value.json.return_value = _EMPTY_CHART
83+
ph.history(period="5d", interval="1h", raise_errors=False)
84+
85+
# Metadata must not have been wiped out
86+
self.assertEqual(ph._history_metadata.get("currency"), "CHF",
87+
"Good metadata was overwritten by an empty intraday response")
88+
89+
def test_metadata_set_to_empty_when_never_populated(self):
90+
ph = _make_ph(_EMPTY_CHART)
91+
ph.history(period="1mo", raise_errors=False)
92+
# No prior metadata: it's fine to end up with {}
93+
self.assertEqual(ph._history_metadata, {})
94+
95+
96+
class TestGetHistoryMetadataDoesNotRaise(unittest.TestCase):
97+
"""Fix 2: get_history_metadata() must not raise when intraday enrichment fails."""
98+
99+
def test_returns_cached_metadata_when_intraday_fails(self):
100+
ph = _make_ph(_good_chart(_GOOD_META))
101+
# Populate metadata via a daily history call
102+
ph.history(period="1mo", raise_errors=False)
103+
self.assertIsNotNone(ph._history_metadata)
104+
105+
# Now make the intraday enrichment call fail
106+
ph._data.get.return_value.json.return_value = _EMPTY_CHART
107+
108+
# Should NOT raise — should return whatever metadata we have
109+
try:
110+
md = ph.get_history_metadata()
111+
except YFPricesMissingError:
112+
self.fail("get_history_metadata() raised YFPricesMissingError "
113+
"even though metadata was already cached")
114+
115+
self.assertIn("currency", md)
116+
self.assertEqual(md["currency"], "CHF")
117+
118+
def test_metadata_none_when_history_never_called(self):
119+
"""Before any history() call, _history_metadata is None."""
120+
ph = _make_ph(_EMPTY_CHART)
121+
self.assertIsNone(ph._history_metadata)
122+
123+
124+
if __name__ == "__main__":
125+
unittest.main()

‎yfinance/scrapers/history.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ def history(self, period=period_default, interval="1d",
244244
except Exception:
245245
meta = {}
246246

247-
self._history_metadata = meta
247+
if meta or self._history_metadata is None:
248+
self._history_metadata = meta
248249
self._history_metadata['YF repair?'] = repair
249250

250251
intraday = params["interval"][-1] in ("m", 'h')
@@ -569,7 +570,11 @@ def get_history_metadata(self, repair=_SENTINEL_) -> dict:
569570
else:
570571
# default
571572
repair = False
572-
self._get_history_cache(period="5d", interval="1h", repair=repair)['prices']
573+
try:
574+
self._get_history_cache(period="5d", interval="1h", repair=repair)['prices']
575+
except YFPricesMissingError:
576+
# tradingPeriods is optional enrichment; return whatever metadata we already have
577+
pass
573578

574579
if self._history_metadata_formatted is False:
575580
self._history_metadata = utils.format_history_metadata(self._history_metadata)

0 commit comments

Comments
 (0)