mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-04 00:22:43 +02:00
- the price table and the verified snapshot keep six significant digits and at least two decimals, so a sub-cent close is no longer 0.00 and a currency pair keeps its pips
92 lines
3.9 KiB
Python
92 lines
3.9 KiB
Python
"""What every Yahoo request shares: its retry, how a failure or an empty answer is
|
|
reported, and how a price is written out."""
|
|
|
|
import logging
|
|
import math
|
|
import time
|
|
|
|
import yfinance as yf
|
|
from yfinance.exceptions import YFPricesMissingError, YFRateLimitError
|
|
|
|
from tradingagents.dataflows.errors import NoMarketDataError, VendorUnavailableError
|
|
from tradingagents.dataflows.net import vendor_reachable
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
YAHOO_HOST = "https://query2.finance.yahoo.com"
|
|
|
|
|
|
def price_text(value: float) -> str:
|
|
"""A price with six significant digits and at least two decimals.
|
|
|
|
Fixed decimals print a sub-cent coin as 0.00 and a currency pair without its
|
|
pips; this keeps both readable.
|
|
"""
|
|
if not math.isfinite(value):
|
|
return str(value)
|
|
decimals = 2 if value == 0 else max(2, 5 - math.floor(math.log10(abs(value))))
|
|
whole, _, fraction = f"{value:.{decimals}f}".partition(".")
|
|
return f"{whole}.{fraction.rstrip('0').ljust(2, '0')}"
|
|
|
|
|
|
def raise_for_empty(symbol: str, canonical: str, what: str) -> None:
|
|
"""Report an empty Yahoo answer as an absence, or as an outage if it is one.
|
|
|
|
An empty answer from a Yahoo that cannot be reached is not an answer about
|
|
the symbol, so the host is probed before "this symbol has no {what}" is said.
|
|
"""
|
|
if not vendor_reachable(YAHOO_HOST):
|
|
raise VendorUnavailableError(f"Yahoo Finance is unreachable; no {what} was retrieved")
|
|
raise NoMarketDataError(symbol, canonical, f"no {what}")
|
|
|
|
|
|
# yfinance answers some failed requests with an empty result, which would read as
|
|
# "no data" for a symbol nobody checked; raised, the failure is reported as one.
|
|
yf.config.debug.hide_exceptions = False
|
|
|
|
|
|
def _answered_empty(exc: Exception) -> bool:
|
|
"""Whether Yahoo answered that it has nothing: no such symbol (HTTP 404), or a
|
|
price window with no prices in it.
|
|
|
|
yfinance raises YFPricesMissingError for that answer and also for an answer
|
|
that was an error (an error status, or Yahoo describing a failure), so only
|
|
a chart with no prices, or Yahoo saying the data does not exist, counts.
|
|
A missing time zone is not an answer: yfinance reports a failed lookup the same way.
|
|
"""
|
|
if isinstance(exc, YFPricesMissingError):
|
|
reason = exc.yahoo_reason
|
|
return "status_code" not in (exc.debug_info or "") and (
|
|
reason is None or reason.startswith("Data doesn't exist"))
|
|
return getattr(getattr(exc, "response", None), "status_code", None) == 404
|
|
|
|
|
|
def yf_retry(func, max_retries=3, base_delay=2.0):
|
|
"""Execute a yfinance call with exponential backoff on rate limits.
|
|
|
|
yfinance raises YFRateLimitError on HTTP 429 responses but does not
|
|
retry them internally, so this wrapper retries them. A rate limit that
|
|
outlasts the retries, or any other exception, is raised as
|
|
VendorUnavailableError: it failed in transit and says nothing about the
|
|
symbol. Yahoo answering that it has nothing for the symbol returns None,
|
|
an empty answer. ``func`` should build its own Ticker and make the request
|
|
itself: a Ticker keeps a failed ``info`` fetch as done, so asking the same
|
|
one again reads an empty profile.
|
|
"""
|
|
for attempt in range(max_retries + 1):
|
|
try:
|
|
return func()
|
|
except YFRateLimitError as exc:
|
|
if attempt < max_retries:
|
|
delay = base_delay * (2 ** attempt)
|
|
logger.warning(f"Yahoo Finance rate limited, retrying in {delay:.0f}s (attempt {attempt + 1}/{max_retries})")
|
|
time.sleep(delay)
|
|
else:
|
|
raise VendorUnavailableError(
|
|
f"Yahoo Finance rate limited after {max_retries} retries: {exc}"
|
|
) from exc
|
|
except Exception as exc:
|
|
if _answered_empty(exc):
|
|
return None
|
|
raise VendorUnavailableError(f"Yahoo Finance request failed: {type(exc).__name__}") from exc
|