fix: support commodity/forex/crypto tickers and never invent prices (#781)
Analyzing a symbol Yahoo Finance does not recognize (e.g. XAUUSD+) could
produce an invented price instead of an error. The agent now either prices
the correct instrument or clearly reports that data is unavailable.
Ticker support:
- Commodities/forex/crypto resolve to the symbol Yahoo actually serves, so
you can enter the common form and it just works:
XAUUSD / XAUUSD+ / GOLD -> GC=F (gold)
USOIL -> CL=F (WTI crude)
EURUSD -> EURUSD=X
BTCUSD -> BTC-USD
SPX500 / NAS100 -> ^GSPC / ^NDX
Native Yahoo symbols (AAPL, GC=F, ^GSPC) keep working unchanged. New
instruments are added by extending the alias table.
Reliability:
- Unknown or delisted symbols now return a clear "data unavailable" result
the agent reports verbatim, instead of a value the model fills in.
- A failed fetch no longer leaves a broken symbol cached until the cache is
cleared by hand.
This commit is contained in:
10 files changed
+460
-53
No files matched your search
@@ -5,6 +5,7 @@ import pandas as pd
|
||||
import yfinance as yf
|
||||
import os
|
||||
from .stockstats_utils import StockstatsUtils, _clean_dataframe, yf_retry, load_ohlcv, filter_financials_by_date
|
||||
from .symbol_utils import normalize_symbol, NoMarketDataError
|
||||
|
||||
def get_YFin_data_online(
|
||||
symbol: Annotated[str, "ticker symbol of the company"],
|
||||
@@ -15,16 +16,19 @@ def get_YFin_data_online(
|
||||
datetime.strptime(start_date, "%Y-%m-%d")
|
||||
datetime.strptime(end_date, "%Y-%m-%d")
|
||||
|
||||
# Create ticker object
|
||||
ticker = yf.Ticker(symbol.upper())
|
||||
# Resolve broker/forex symbols to Yahoo's convention (XAUUSD+ -> GC=F).
|
||||
canonical = normalize_symbol(symbol)
|
||||
ticker = yf.Ticker(canonical)
|
||||
|
||||
# Fetch historical data for the specified date range
|
||||
data = yf_retry(lambda: ticker.history(start=start_date, end=end_date))
|
||||
|
||||
# Check if data is empty
|
||||
# Empty result means the symbol is unknown/delisted. Raise a typed error
|
||||
# instead of returning prose: the routing layer turns it into a single
|
||||
# unambiguous "no data" signal so the agent never fabricates a price.
|
||||
if data.empty:
|
||||
return (
|
||||
f"No data found for symbol '{symbol}' between {start_date} and {end_date}"
|
||||
raise NoMarketDataError(
|
||||
symbol, canonical, f"no rows between {start_date} and {end_date}"
|
||||
)
|
||||
|
||||
# Remove timezone info from index for cleaner output
|
||||
@@ -40,8 +44,10 @@ def get_YFin_data_online(
|
||||
# Convert DataFrame to CSV string
|
||||
csv_string = data.to_csv()
|
||||
|
||||
# Add header information
|
||||
header = f"# Stock data for {symbol.upper()} from {start_date} to {end_date}\n"
|
||||
# Add header information; note the resolved symbol when it differs so the
|
||||
# agent (and user) can see which instrument was actually priced.
|
||||
label = canonical if canonical == symbol.upper() else f"{canonical} (from {symbol})"
|
||||
header = f"# Stock data for {label} from {start_date} to {end_date}\n"
|
||||
header += f"# Total records: {len(data)}\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
@@ -162,7 +168,9 @@ def get_stock_stats_indicators_window(
|
||||
ind_string = ""
|
||||
for date_str, value in date_values:
|
||||
ind_string += f"{date_str}: {value}\n"
|
||||
|
||||
|
||||
except NoMarketDataError:
|
||||
raise # Unknown/delisted symbol — let the router emit the sentinel
|
||||
except Exception as e:
|
||||
print(f"Error getting bulk stockstats data: {e}")
|
||||
# Fallback to original implementation if bulk method fails
|
||||
@@ -236,6 +244,8 @@ def get_stockstats_indicator(
|
||||
indicator,
|
||||
curr_date,
|
||||
)
|
||||
except NoMarketDataError:
|
||||
raise # Unknown/delisted symbol — let the router emit the sentinel
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Error getting stockstats indicator data for indicator {indicator} on {curr_date}: {e}"
|
||||
@@ -250,12 +260,13 @@ def get_fundamentals(
|
||||
curr_date: Annotated[str, "current date (not used for yfinance)"] = None
|
||||
):
|
||||
"""Get company fundamentals overview from yfinance."""
|
||||
canonical = normalize_symbol(ticker)
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
ticker_obj = yf.Ticker(canonical)
|
||||
info = yf_retry(lambda: ticker_obj.info)
|
||||
|
||||
if not info:
|
||||
return f"No fundamentals data found for symbol '{ticker}'"
|
||||
raise NoMarketDataError(ticker, canonical, "no fundamentals returned")
|
||||
|
||||
fields = [
|
||||
("Name", info.get("longName")),
|
||||
@@ -293,11 +304,20 @@ def get_fundamentals(
|
||||
if value is not None:
|
||||
lines.append(f"{label}: {value}")
|
||||
|
||||
header = f"# Company Fundamentals for {ticker.upper()}\n"
|
||||
# yfinance returns a stub dict (e.g. {"trailingPegRatio": None}) for
|
||||
# unknown symbols, so `info` is truthy but every field is empty. Treat
|
||||
# "no usable fields" as no data rather than emitting a bare header the
|
||||
# agent might fabricate around.
|
||||
if not lines:
|
||||
raise NoMarketDataError(ticker, canonical, "no fundamental fields returned")
|
||||
|
||||
header = f"# Company Fundamentals for {canonical}\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
return header + "\n".join(lines)
|
||||
|
||||
except NoMarketDataError:
|
||||
raise
|
||||
except Exception as e:
|
||||
return f"Error retrieving fundamentals for {ticker}: {str(e)}"
|
||||
|
||||
@@ -308,8 +328,9 @@ def get_balance_sheet(
|
||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||
):
|
||||
"""Get balance sheet data from yfinance."""
|
||||
canonical = normalize_symbol(ticker)
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
ticker_obj = yf.Ticker(canonical)
|
||||
|
||||
if freq.lower() == "quarterly":
|
||||
data = yf_retry(lambda: ticker_obj.quarterly_balance_sheet)
|
||||
@@ -319,17 +340,19 @@ def get_balance_sheet(
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
return f"No balance sheet data found for symbol '{ticker}'"
|
||||
|
||||
raise NoMarketDataError(ticker, canonical, "no balance sheet data")
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
|
||||
# Add header information
|
||||
header = f"# Balance Sheet data for {ticker.upper()} ({freq})\n"
|
||||
header = f"# Balance Sheet data for {canonical} ({freq})\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
|
||||
return header + csv_string
|
||||
|
||||
|
||||
except NoMarketDataError:
|
||||
raise
|
||||
except Exception as e:
|
||||
return f"Error retrieving balance sheet for {ticker}: {str(e)}"
|
||||
|
||||
@@ -340,8 +363,9 @@ def get_cashflow(
|
||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||
):
|
||||
"""Get cash flow data from yfinance."""
|
||||
canonical = normalize_symbol(ticker)
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
ticker_obj = yf.Ticker(canonical)
|
||||
|
||||
if freq.lower() == "quarterly":
|
||||
data = yf_retry(lambda: ticker_obj.quarterly_cashflow)
|
||||
@@ -351,17 +375,19 @@ def get_cashflow(
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
return f"No cash flow data found for symbol '{ticker}'"
|
||||
|
||||
raise NoMarketDataError(ticker, canonical, "no cash flow data")
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
|
||||
# Add header information
|
||||
header = f"# Cash Flow data for {ticker.upper()} ({freq})\n"
|
||||
header = f"# Cash Flow data for {canonical} ({freq})\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
|
||||
return header + csv_string
|
||||
|
||||
|
||||
except NoMarketDataError:
|
||||
raise
|
||||
except Exception as e:
|
||||
return f"Error retrieving cash flow for {ticker}: {str(e)}"
|
||||
|
||||
@@ -372,8 +398,9 @@ def get_income_statement(
|
||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||
):
|
||||
"""Get income statement data from yfinance."""
|
||||
canonical = normalize_symbol(ticker)
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
ticker_obj = yf.Ticker(canonical)
|
||||
|
||||
if freq.lower() == "quarterly":
|
||||
data = yf_retry(lambda: ticker_obj.quarterly_income_stmt)
|
||||
@@ -383,17 +410,19 @@ def get_income_statement(
|
||||
data = filter_financials_by_date(data, curr_date)
|
||||
|
||||
if data.empty:
|
||||
return f"No income statement data found for symbol '{ticker}'"
|
||||
|
||||
raise NoMarketDataError(ticker, canonical, "no income statement data")
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
|
||||
# Add header information
|
||||
header = f"# Income Statement data for {ticker.upper()} ({freq})\n"
|
||||
header = f"# Income Statement data for {canonical} ({freq})\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
|
||||
return header + csv_string
|
||||
|
||||
|
||||
except NoMarketDataError:
|
||||
raise
|
||||
except Exception as e:
|
||||
return f"Error retrieving income statement for {ticker}: {str(e)}"
|
||||
|
||||
@@ -402,18 +431,21 @@ def get_insider_transactions(
|
||||
ticker: Annotated[str, "ticker symbol of the company"]
|
||||
):
|
||||
"""Get insider transactions data from yfinance."""
|
||||
canonical = normalize_symbol(ticker)
|
||||
try:
|
||||
ticker_obj = yf.Ticker(ticker.upper())
|
||||
ticker_obj = yf.Ticker(canonical)
|
||||
data = yf_retry(lambda: ticker_obj.insider_transactions)
|
||||
|
||||
|
||||
# Empty is normal here (many valid symbols have no insider filings),
|
||||
# so report it plainly rather than treating the symbol as invalid.
|
||||
if data is None or data.empty:
|
||||
return f"No insider transactions data found for symbol '{ticker}'"
|
||||
|
||||
return f"No insider transactions reported for symbol '{canonical}'"
|
||||
|
||||
# Convert to CSV string for consistency with other functions
|
||||
csv_string = data.to_csv()
|
||||
|
||||
|
||||
# Add header information
|
||||
header = f"# Insider Transactions data for {ticker.upper()}\n"
|
||||
header = f"# Insider Transactions data for {canonical}\n"
|
||||
header += f"# Data retrieved on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||
|
||||
return header + csv_string
|
||||
|
||||
Reference in new issue
Block a user