Validate Market region and stop returning misleading status data

Yahoo's markettime endpoint silently ignores the `market` parameter and
always returns U.S. data, so `Market("EUROPE").status` previously returned
U.S. status while pretending to be European. The summary endpoint does
honor the parameter and is unaffected.

- Introduce `MarketRegion(str, Enum)` as the single source of truth for
  the supported region values, exposed as `yfinance.MarketRegion`.
- Validate `Market(...)` input against the enum and raise `ValueError`
  on unknown regions.
- Detect the markettime mismatch: when a non-`US` region is requested
  but the response id is `us`, set `status` to `None` and log a warning
  rather than returning misleading data.
- Document the markettime limitation and add tests covering validation
  and the EUROPE/US fetch behavior.

Closes #2784
This commit is contained in:
Alessandro Colace
2026-05-09 13:33:50 +02:00
parent c8c3754ccd
commit 90c267103a
4 changed files with 103 additions and 5 deletions
+7 -1
View File
@@ -38,4 +38,10 @@ There are 8 different markets available in Yahoo Finance.
* RATES
* COMMODITIES
* CURRENCIES
* CRYPTOCURRENCIES
* CRYPTOCURRENCIES
.. note::
Only `Market.summary` returns regional data for all of the values above.
`Market.status` is backed by Yahoo's `markettime` endpoint, which currently
ignores the `market` parameter and only returns U.S. data; for any non-`US`
market, `status` will therefore be `None` and a warning is logged.
+56
View File
@@ -0,0 +1,56 @@
import unittest
from tests.context import yfinance as yf
class TestMarketValidation(unittest.TestCase):
def test_string_input(self):
m = yf.Market("EUROPE")
self.assertEqual(m.market, "EUROPE")
def test_enum_input(self):
m = yf.Market(yf.MarketRegion.EUROPE)
self.assertEqual(m.market, "EUROPE")
self.assertIsInstance(m.market, str)
def test_invalid_market_raises(self):
with self.assertRaises(ValueError) as ctx:
yf.Market("FR")
self.assertIn("FR", str(ctx.exception))
self.assertIn("EUROPE", str(ctx.exception))
def test_market_region_members(self):
expected = {
"US", "GB", "ASIA", "EUROPE",
"RATES", "COMMODITIES", "CURRENCIES", "CRYPTOCURRENCIES",
}
self.assertEqual({m.value for m in yf.MarketRegion}, expected)
def test_market_region_str_equality(self):
self.assertEqual(yf.MarketRegion.EUROPE, "EUROPE")
class TestMarketFetch(unittest.TestCase):
def test_us_summary_and_status(self):
m = yf.Market("US")
self.assertIsInstance(m.summary, dict)
self.assertGreater(len(m.summary), 0)
self.assertIsNotNone(m.status)
self.assertEqual(m.status.get("id"), "us")
def test_europe_summary_returns_regional_exchanges(self):
m = yf.Market("EUROPE")
self.assertIsInstance(m.summary, dict)
# EUROPE summary should not be dominated by U.S. exchanges
self.assertGreater(len(m.summary), 0)
self.assertNotIn("SNP", m.summary)
def test_non_us_status_is_none(self):
# Yahoo's markettime endpoint silently ignores `market` and returns
# U.S. data; Market should surface this as None rather than mislead.
m = yf.Market("EUROPE")
self.assertIsNone(m.status)
if __name__ == "__main__":
unittest.main()
+2 -2
View File
@@ -31,7 +31,7 @@ from .utils import enable_debug_mode
from .cache import set_tz_cache_location
from .domain.sector import Sector
from .domain.industry import Industry
from .domain.market import Market
from .domain.market import Market, MarketRegion
from .config import YfConfig as config
from .data import Auth
@@ -44,7 +44,7 @@ __author__ = "Ran Aroussi"
import warnings
warnings.filterwarnings('default', category=DeprecationWarning, module='^yfinance')
__all__ = ['download', 'Market', 'Search', 'Lookup', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location',
__all__ = ['download', 'Market', 'MarketRegion', 'Search', 'Lookup', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location',
'Sector', 'Industry', 'WebSocket', 'AsyncWebSocket', 'Calendars', 'Auth']
# screener stuff:
__all__ += ['EquityQuery', 'FundQuery', 'ETFQuery', 'screen', 'PREDEFINED_SCREENER_QUERIES']
+38 -2
View File
@@ -1,14 +1,39 @@
import datetime as dt
import json as _json
from enum import Enum
from ..config import YfConfig
from ..const import _QUERY1_URL_
from ..data import utils, YfData
from ..exceptions import YFDataException
class MarketRegion(str, Enum):
"""Market regions accepted by Yahoo's ``quote/marketSummary`` endpoint.
Members are plain strings, so ``MarketRegion.EUROPE == "EUROPE"`` and
``Market("EUROPE")`` continues to work. Pass an enum member for IDE
autocomplete and static checking: ``Market(MarketRegion.EUROPE)``.
"""
US = "US"
GB = "GB"
ASIA = "ASIA"
EUROPE = "EUROPE"
RATES = "RATES"
COMMODITIES = "COMMODITIES"
CURRENCIES = "CURRENCIES"
CRYPTOCURRENCIES = "CRYPTOCURRENCIES"
class Market:
def __init__(self, market:'str', session=None, timeout=30):
self.market = market
def __init__(self, market, session=None, timeout=30):
try:
self.market = MarketRegion(market).value
except ValueError:
valid = [m.value for m in MarketRegion]
raise ValueError(
f"Unknown market {market!r}. Valid markets: {valid}"
) from None
self.session = session
self.timeout = timeout
@@ -75,11 +100,22 @@ class Market:
self._status = self._status['finance']['marketTimes'][0]['marketTime'][0]
self._status['timezone'] = self._status['timezone'][0]
del self._status['time'] # redundant
# Yahoo's markettime endpoint silently ignores the `market` param
# and always returns U.S. data. Detect the mismatch so callers
# aren't misled into believing they got regional status data.
if self.market != "US" and self._status.get("id") == "us":
self._logger.warning(
f"{self.market}: Yahoo markettime endpoint does not support "
f"market={self.market!r}; status data unavailable."
)
self._status = None
except Exception as e:
if not YfConfig.debug.hide_exceptions:
raise
self._logger.error(f"{self.market}: Failed to parse market status")
self._logger.debug(f"{type(e)}: {e}")
if self._status is None:
return
try:
self._status.update({
"open": dt.datetime.fromisoformat(self._status["open"]),