fa45d8aa5f
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
Privoxy对node122:18003返回500,直连正常
144 lines
5.0 KiB
Python
144 lines
5.0 KiB
Python
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, 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
|
|
|
|
self._data = YfData(session=self.session)
|
|
|
|
self._logger = utils.get_yf_logger()
|
|
|
|
self._status = None
|
|
self._summary = None
|
|
|
|
def _fetch_json(self, url, params):
|
|
data = self._data.cache_get(url=url, params=params, timeout=self.timeout)
|
|
if data is None or "Will be right back" in data.text:
|
|
raise YFDataException("*** YAHOO! FINANCE IS CURRENTLY DOWN! ***")
|
|
try:
|
|
return data.json()
|
|
except _json.JSONDecodeError:
|
|
if not YfConfig.debug.hide_exceptions:
|
|
raise
|
|
self._logger.error(f"{self.market}: Failed to retrieve market data and received faulty data.")
|
|
return {}
|
|
|
|
def _parse_data(self):
|
|
# Fetch both to ensure they are at the same time
|
|
if (self._status is not None) and (self._summary is not None):
|
|
return
|
|
|
|
self._logger.debug(f"{self.market}: Parsing market data")
|
|
|
|
# Summary
|
|
|
|
summary_url = f"{_QUERY1_URL_}/v6/finance/quote/marketSummary"
|
|
summary_fields = ["shortName", "regularMarketPrice", "regularMarketChange", "regularMarketChangePercent"]
|
|
summary_params = {
|
|
"fields": ",".join(summary_fields),
|
|
"formatted": False,
|
|
"lang": "en-US",
|
|
"market": self.market
|
|
}
|
|
|
|
status_url = f"{_QUERY1_URL_}/v6/finance/markettime"
|
|
status_params = {
|
|
"formatted": True,
|
|
"key": "finance",
|
|
"lang": "en-US",
|
|
"market": self.market
|
|
}
|
|
|
|
self._summary = self._fetch_json(summary_url, summary_params)
|
|
self._status = self._fetch_json(status_url, status_params)
|
|
|
|
try:
|
|
self._summary = self._summary['marketSummaryResponse']['result']
|
|
self._summary = {x['exchange']:x for x in self._summary}
|
|
except Exception as e:
|
|
if not YfConfig.debug.hide_exceptions:
|
|
raise
|
|
self._logger.error(f"{self.market}: Failed to parse market summary")
|
|
self._logger.debug(f"{type(e)}: {e}")
|
|
|
|
|
|
try:
|
|
# Unpack
|
|
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"]),
|
|
"close": dt.datetime.fromisoformat(self._status["close"]),
|
|
"tz": dt.timezone(dt.timedelta(hours=int(self._status["timezone"]["gmtoffset"]))/1000, self._status["timezone"]["short"])
|
|
})
|
|
except Exception as e:
|
|
if not YfConfig.debug.hide_exceptions:
|
|
raise
|
|
self._logger.error(f"{self.market}: Failed to update market status")
|
|
self._logger.debug(f"{type(e)}: {e}")
|
|
|
|
|
|
|
|
|
|
@property
|
|
def status(self):
|
|
self._parse_data()
|
|
return self._status
|
|
|
|
|
|
@property
|
|
def summary(self):
|
|
self._parse_data()
|
|
return self._summary
|