refactor(dataflows): group the vendors under dataflows/vendors

- vendors/yahoo: ohlcv (loader and cache), market (prices, indicators), fundamentals (profile, statements, insider), news, snapshot
- vendors/alpha_vantage is a package; sec_edgar, fred, polymarket, reddit and stocktwits sit beside it
- the one-method StockstatsUtils class is a function; the duplicate Yahoo host constant is gone
- tests are named after the modules they cover: test_ohlcv_date_column, test_yahoo_snapshot, and the ohlcv and snapshot aliases
This commit is contained in:
Yijia-Xiao
2026-09-24 04:37:40 +00:00
parent c42a2f2c61
commit 6097b582d9
45 changed files with 417 additions and 389 deletions
@@ -32,8 +32,8 @@ from tradingagents.agents.utils.structured import (
bind_structured,
invoke_structured_or_freetext,
)
from tradingagents.dataflows.reddit import fetch_reddit_posts
from tradingagents.dataflows.stocktwits import fetch_stocktwits_messages
from tradingagents.dataflows.vendors.reddit import fetch_reddit_posts
from tradingagents.dataflows.vendors.stocktwits import fetch_stocktwits_messages
def _seven_days_back(trade_date: str) -> str:
+1 -1
View File
@@ -23,7 +23,7 @@ from tradingagents.agents.utils.news_data_tools import (
from tradingagents.agents.utils.prediction_markets_tools import get_prediction_markets
from tradingagents.agents.utils.technical_indicators_tools import get_indicators
from tradingagents.dataflows.date_window import get_current_date
from tradingagents.dataflows.y_finance import get_company_profile
from tradingagents.dataflows.vendors.yahoo.fundamentals import get_company_profile
# Public surface: the data tools are imported here so agents and the graph
# import them from one place, plus the instrument/language helpers defined below.
@@ -4,7 +4,7 @@ from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState
from tradingagents.dataflows.date_window import as_of
from tradingagents.dataflows.market_data_validator import build_verified_market_snapshot
from tradingagents.dataflows.vendors.yahoo.snapshot import build_verified_market_snapshot
@tool
+14 -12
View File
@@ -1,6 +1,12 @@
import logging
from tradingagents.dataflows.alpha_vantage import (
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import (
NoMarketDataError,
VendorNotConfiguredError,
VendorRateLimitError,
)
from tradingagents.dataflows.vendors.alpha_vantage import (
get_balance_sheet as get_alpha_vantage_balance_sheet,
get_cashflow as get_alpha_vantage_cashflow,
get_fundamentals as get_alpha_vantage_fundamentals,
@@ -11,31 +17,27 @@ from tradingagents.dataflows.alpha_vantage import (
get_news as get_alpha_vantage_news,
get_stock as get_alpha_vantage_stock,
)
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.errors import (
NoMarketDataError,
VendorNotConfiguredError,
VendorRateLimitError,
)
from tradingagents.dataflows.fred import get_macro_data as get_fred_macro_data
from tradingagents.dataflows.polymarket import (
from tradingagents.dataflows.vendors.fred import get_macro_data as get_fred_macro_data
from tradingagents.dataflows.vendors.polymarket import (
get_prediction_markets as get_polymarket_prediction_markets,
)
from tradingagents.dataflows.sec_edgar import (
from tradingagents.dataflows.vendors.sec_edgar import (
get_balance_sheet as get_sec_edgar_balance_sheet,
get_cashflow as get_sec_edgar_cashflow,
get_income_statement as get_sec_edgar_income_statement,
)
from tradingagents.dataflows.y_finance import (
from tradingagents.dataflows.vendors.yahoo.fundamentals import (
get_balance_sheet as get_yfinance_balance_sheet,
get_cashflow as get_yfinance_cashflow,
get_fundamentals as get_yfinance_fundamentals,
get_income_statement as get_yfinance_income_statement,
get_insider_transactions as get_yfinance_insider_transactions,
)
from tradingagents.dataflows.vendors.yahoo.market import (
get_stock_stats_indicators_window,
get_YFin_data_online,
)
from tradingagents.dataflows.yfinance_news import get_global_news_yfinance, get_news_yfinance
from tradingagents.dataflows.vendors.yahoo.news import get_global_news_yfinance, get_news_yfinance
logger = logging.getLogger(__name__)
+1
View File
@@ -0,0 +1 @@
"""Data vendors: one module or package per source, serving the router's methods."""
@@ -1,18 +1,18 @@
# Aggregates the per-category Alpha Vantage implementations into one module the
# vendor router imports from; the imports below are the public surface.
from tradingagents.dataflows.alpha_vantage_fundamentals import (
from tradingagents.dataflows.vendors.alpha_vantage.fundamentals import (
get_balance_sheet,
get_cashflow,
get_fundamentals,
get_income_statement,
)
from tradingagents.dataflows.alpha_vantage_indicator import get_indicator
from tradingagents.dataflows.alpha_vantage_news import (
from tradingagents.dataflows.vendors.alpha_vantage.indicator import get_indicator
from tradingagents.dataflows.vendors.alpha_vantage.news import (
get_global_news,
get_insider_transactions,
get_news,
)
from tradingagents.dataflows.alpha_vantage_stock import get_stock
from tradingagents.dataflows.vendors.alpha_vantage.stock import get_stock
__all__ = [
"get_balance_sheet",
@@ -1,7 +1,7 @@
import json
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
from tradingagents.dataflows.date_window import withhold_live_profile
from tradingagents.dataflows.vendors.alpha_vantage.common import _make_api_request
def _filter_reports_by_date(result, curr_date: str):
@@ -1,7 +1,7 @@
import logging
from tradingagents.dataflows.alpha_vantage_common import _make_api_request
from tradingagents.dataflows.errors import NoMarketDataError, VendorError
from tradingagents.dataflows.vendors.alpha_vantage.common import _make_api_request
logger = logging.getLogger(__name__)
@@ -1,7 +1,10 @@
import json
from tradingagents.dataflows.alpha_vantage_common import _make_api_request, format_datetime_for_api
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.vendors.alpha_vantage.common import (
_make_api_request,
format_datetime_for_api,
)
def get_news(ticker, start_date, end_date) -> dict[str, str] | str:
@@ -1,6 +1,6 @@
from datetime import datetime
from tradingagents.dataflows.alpha_vantage_common import (
from tradingagents.dataflows.vendors.alpha_vantage.common import (
_filter_csv_by_date_range,
_make_api_request,
)
+1
View File
@@ -0,0 +1 @@
"""Yahoo Finance: prices, indicators, statements, insider filings and news."""
+211
View File
@@ -0,0 +1,211 @@
from typing import Annotated
import pandas as pd
import yfinance as yf
from tradingagents.dataflows.date_window import withhold_live_profile
from tradingagents.dataflows.errors import NoMarketDataError, VendorError, VendorRateLimitError
from tradingagents.dataflows.net import vendor_reachable
from tradingagents.dataflows.symbols import normalize_symbol
from tradingagents.dataflows.vendors.yahoo.ohlcv import (
YAHOO_HOST,
raise_for_empty,
yf_retry,
)
def get_fundamentals(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "analysis date in YYYY-MM-DD format"] = None
):
"""Get company fundamentals overview from yfinance.
``Ticker.info`` is a present-day snapshot with no historical vintage, so a
past ``curr_date`` withholds it through the shared point-in-time guard
(``date_window.withhold_live_profile``, #1300).
"""
canonical = normalize_symbol(ticker)
# Guard before the request: the response would only be discarded, and the
# answer does not depend on it.
withheld = withhold_live_profile(curr_date, canonical)
if withheld:
return withheld
try:
ticker_obj = yf.Ticker(canonical)
info = yf_retry(lambda: ticker_obj.info)
if not info:
raise_for_empty(ticker, canonical, "fundamentals")
fields = [
("Name", info.get("longName")),
("Sector", info.get("sector")),
("Industry", info.get("industry")),
("Market Cap", info.get("marketCap")),
("PE Ratio (TTM)", info.get("trailingPE")),
("Forward PE", info.get("forwardPE")),
("PEG Ratio", info.get("pegRatio")),
("Price to Book", info.get("priceToBook")),
("EPS (TTM)", info.get("trailingEps")),
("Forward EPS", info.get("forwardEps")),
("Dividend Yield", info.get("dividendYield")),
("Beta", info.get("beta")),
("52 Week High", info.get("fiftyTwoWeekHigh")),
("52 Week Low", info.get("fiftyTwoWeekLow")),
("50 Day Average", info.get("fiftyDayAverage")),
("200 Day Average", info.get("twoHundredDayAverage")),
("Revenue (TTM)", info.get("totalRevenue")),
("Gross Profit", info.get("grossProfits")),
("EBITDA", info.get("ebitda")),
("Net Income", info.get("netIncomeToCommon")),
("Profit Margin", info.get("profitMargins")),
("Operating Margin", info.get("operatingMargins")),
("Return on Equity", info.get("returnOnEquity")),
("Return on Assets", info.get("returnOnAssets")),
("Debt to Equity", info.get("debtToEquity")),
("Current Ratio", info.get("currentRatio")),
("Book Value", info.get("bookValue")),
("Free Cash Flow", info.get("freeCashflow")),
]
lines = [f"{label}: {v}" for label, v in fields if v is not None]
# 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\n"
return header + "\n".join(lines)
except VendorError:
raise
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"fundamentals unavailable: {e}") from e
# This vendor dates a statement by the period it covers, not by the day it was
# filed, and carries no filing date to do better. A company files weeks after its
# period ends, so a run dated in that gap can be served figures that were not yet
# public. Say so rather than implying the stricter guarantee (SEC EDGAR, which
# does carry filing dates, serves US filers as filed).
_PERIOD_END_VINTAGE = (
"# Periods are cut at the fiscal period end; this vendor does not report "
"filing dates, so the most recent period may not have been published yet.\n\n"
)
def _statement(ticker, freq, curr_date, title, quarterly_attr, annual_attr) -> str:
"""One financial statement as CSV, cut at ``curr_date`` by period end."""
canonical = normalize_symbol(ticker)
what = title.lower()
try:
ticker_obj = yf.Ticker(canonical)
attr = quarterly_attr if freq.lower() == "quarterly" else annual_attr
data = filter_financials_by_date(yf_retry(lambda: getattr(ticker_obj, attr)), curr_date)
if data.empty:
raise_for_empty(ticker, canonical, f"{what} data")
return f"# {title} data for {canonical} ({freq})\n" + _PERIOD_END_VINTAGE + data.to_csv()
except VendorError:
raise
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"{what} unavailable: {e}") from e
def get_balance_sheet(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get balance sheet data from yfinance."""
return _statement(ticker, freq, curr_date, "Balance Sheet", "quarterly_balance_sheet", "balance_sheet")
def get_cashflow(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get cash flow data from yfinance."""
return _statement(ticker, freq, curr_date, "Cash Flow", "quarterly_cashflow", "cashflow")
def get_income_statement(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get income statement data from yfinance."""
return _statement(ticker, freq, curr_date, "Income Statement", "quarterly_income_stmt", "income_stmt")
# Rows are dated by the transaction, which is when the insider traded, not when
# the market learned of it: a Form 4 is filed up to two business days later and
# this vendor reports no filing date, so the most recent rows may not have been
# public on the analysis date.
_TRANSACTION_DATE_VINTAGE = (
"# Rows are dated by transaction date. A trade becomes public when its Form 4 "
"is filed, up to two business days later, so the newest rows may not have been "
"known on this date.\n\n"
)
def get_insider_transactions(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str | None, "only transactions on or before this date, yyyy-mm-dd"] = None,
):
"""Get insider transactions data from yfinance."""
canonical = normalize_symbol(ticker)
try:
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:
if not vendor_reachable(YAHOO_HOST):
raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved")
return f"No insider transactions reported for symbol '{canonical}'"
if curr_date:
traded = data["Start Date"]
kept = data[traded <= pd.Timestamp(curr_date)]
if kept.empty:
return (
f"<insider transactions unavailable for {canonical} as of {curr_date}: "
"Yahoo serves recent transactions only>"
)
data = kept
return f"# Insider Transactions data for {canonical}\n" + _TRANSACTION_DATE_VINTAGE + data.to_csv()
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"insider transactions unavailable: {e}") from e
def get_company_profile(ticker: str) -> dict:
"""Yahoo's current profile for ``ticker``: name, sector, industry and the like."""
canonical = normalize_symbol(ticker)
try:
return yf_retry(lambda: yf.Ticker(canonical).info) or {}
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"profile unavailable: {e}") from e
def filter_financials_by_date(data: pd.DataFrame, curr_date: str) -> pd.DataFrame:
"""Drop financial statement columns (fiscal period timestamps) after curr_date.
yfinance financial statements use fiscal period end dates as columns.
Columns after curr_date represent future data and are removed to
prevent look-ahead bias.
"""
if not curr_date or data.empty:
return data
cutoff = pd.Timestamp(curr_date)
mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff
return data.loc[:, mask]
@@ -5,21 +5,16 @@ from typing import Annotated
import pandas as pd
import yfinance as yf
from dateutil.relativedelta import relativedelta
from stockstats import wrap
from tradingagents.dataflows.date_window import withhold_live_profile
from tradingagents.dataflows.errors import NoMarketDataError, VendorError, VendorRateLimitError
from tradingagents.dataflows.net import vendor_reachable
from tradingagents.dataflows.stockstats_utils import (
StockstatsUtils,
from tradingagents.dataflows.errors import NoMarketDataError, VendorError
from tradingagents.dataflows.symbols import normalize_symbol
from tradingagents.dataflows.vendors.yahoo.ohlcv import (
_assert_ohlcv_not_stale,
filter_financials_by_date,
load_ohlcv,
raise_for_empty,
yf_retry,
)
from tradingagents.dataflows.symbols import normalize_symbol
_YAHOO_HOST = "https://query2.finance.yahoo.com"
logger = logging.getLogger(__name__)
@@ -260,7 +255,7 @@ def get_stockstats_indicator(
curr_date = curr_date_dt.strftime("%Y-%m-%d")
try:
indicator_value = StockstatsUtils.get_stock_stats(
indicator_value = get_stock_stats(
symbol,
indicator,
curr_date,
@@ -278,187 +273,6 @@ def get_stockstats_indicator(
return str(indicator_value)
def get_fundamentals(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "analysis date in YYYY-MM-DD format"] = None
):
"""Get company fundamentals overview from yfinance.
``Ticker.info`` is a present-day snapshot with no historical vintage, so a
past ``curr_date`` withholds it through the shared point-in-time guard
(``date_window.withhold_live_profile``, #1300).
"""
canonical = normalize_symbol(ticker)
# Guard before the request: the response would only be discarded, and the
# answer does not depend on it.
withheld = withhold_live_profile(curr_date, canonical)
if withheld:
return withheld
try:
ticker_obj = yf.Ticker(canonical)
info = yf_retry(lambda: ticker_obj.info)
if not info:
raise_for_empty(ticker, canonical, "fundamentals")
fields = [
("Name", info.get("longName")),
("Sector", info.get("sector")),
("Industry", info.get("industry")),
("Market Cap", info.get("marketCap")),
("PE Ratio (TTM)", info.get("trailingPE")),
("Forward PE", info.get("forwardPE")),
("PEG Ratio", info.get("pegRatio")),
("Price to Book", info.get("priceToBook")),
("EPS (TTM)", info.get("trailingEps")),
("Forward EPS", info.get("forwardEps")),
("Dividend Yield", info.get("dividendYield")),
("Beta", info.get("beta")),
("52 Week High", info.get("fiftyTwoWeekHigh")),
("52 Week Low", info.get("fiftyTwoWeekLow")),
("50 Day Average", info.get("fiftyDayAverage")),
("200 Day Average", info.get("twoHundredDayAverage")),
("Revenue (TTM)", info.get("totalRevenue")),
("Gross Profit", info.get("grossProfits")),
("EBITDA", info.get("ebitda")),
("Net Income", info.get("netIncomeToCommon")),
("Profit Margin", info.get("profitMargins")),
("Operating Margin", info.get("operatingMargins")),
("Return on Equity", info.get("returnOnEquity")),
("Return on Assets", info.get("returnOnAssets")),
("Debt to Equity", info.get("debtToEquity")),
("Current Ratio", info.get("currentRatio")),
("Book Value", info.get("bookValue")),
("Free Cash Flow", info.get("freeCashflow")),
]
lines = [f"{label}: {v}" for label, v in fields if v is not None]
# 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\n"
return header + "\n".join(lines)
except VendorError:
raise
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"fundamentals unavailable: {e}") from e
# This vendor dates a statement by the period it covers, not by the day it was
# filed, and carries no filing date to do better. A company files weeks after its
# period ends, so a run dated in that gap can be served figures that were not yet
# public. Say so rather than implying the stricter guarantee (SEC EDGAR, which
# does carry filing dates, serves US filers as filed).
_PERIOD_END_VINTAGE = (
"# Periods are cut at the fiscal period end; this vendor does not report "
"filing dates, so the most recent period may not have been published yet.\n\n"
)
def _statement(ticker, freq, curr_date, title, quarterly_attr, annual_attr) -> str:
"""One financial statement as CSV, cut at ``curr_date`` by period end."""
canonical = normalize_symbol(ticker)
what = title.lower()
try:
ticker_obj = yf.Ticker(canonical)
attr = quarterly_attr if freq.lower() == "quarterly" else annual_attr
data = filter_financials_by_date(yf_retry(lambda: getattr(ticker_obj, attr)), curr_date)
if data.empty:
raise_for_empty(ticker, canonical, f"{what} data")
return f"# {title} data for {canonical} ({freq})\n" + _PERIOD_END_VINTAGE + data.to_csv()
except VendorError:
raise
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"{what} unavailable: {e}") from e
def get_balance_sheet(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get balance sheet data from yfinance."""
return _statement(ticker, freq, curr_date, "Balance Sheet", "quarterly_balance_sheet", "balance_sheet")
def get_cashflow(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get cash flow data from yfinance."""
return _statement(ticker, freq, curr_date, "Cash Flow", "quarterly_cashflow", "cashflow")
def get_income_statement(
ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
):
"""Get income statement data from yfinance."""
return _statement(ticker, freq, curr_date, "Income Statement", "quarterly_income_stmt", "income_stmt")
# Rows are dated by the transaction, which is when the insider traded, not when
# the market learned of it: a Form 4 is filed up to two business days later and
# this vendor reports no filing date, so the most recent rows may not have been
# public on the analysis date.
_TRANSACTION_DATE_VINTAGE = (
"# Rows are dated by transaction date. A trade becomes public when its Form 4 "
"is filed, up to two business days later, so the newest rows may not have been "
"known on this date.\n\n"
)
def get_insider_transactions(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str | None, "only transactions on or before this date, yyyy-mm-dd"] = None,
):
"""Get insider transactions data from yfinance."""
canonical = normalize_symbol(ticker)
try:
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:
if not vendor_reachable(_YAHOO_HOST):
raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved")
return f"No insider transactions reported for symbol '{canonical}'"
if curr_date:
traded = data["Start Date"]
kept = data[traded <= pd.Timestamp(curr_date)]
if kept.empty:
return (
f"<insider transactions unavailable for {canonical} as of {curr_date}: "
"Yahoo serves recent transactions only>"
)
data = kept
return f"# Insider Transactions data for {canonical}\n" + _TRANSACTION_DATE_VINTAGE + data.to_csv()
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"insider transactions unavailable: {e}") from e
def get_company_profile(ticker: str) -> dict:
"""Yahoo's current profile for ``ticker``: name, sector, industry and the like."""
canonical = normalize_symbol(ticker)
try:
return yf_retry(lambda: yf.Ticker(canonical).info) or {}
except Exception as e:
raise NoMarketDataError(ticker, canonical, f"profile unavailable: {e}") from e
def get_closes(symbol: str, start_date: str, end_date: str) -> pd.Series:
@@ -469,3 +283,27 @@ def get_closes(symbol: str, start_date: str, end_date: str) -> pd.Series:
except Exception as e:
raise NoMarketDataError(symbol, canonical, f"prices unavailable: {e}") from e
return history["Close"] if "Close" in history else pd.Series(dtype=float)
def get_stock_stats(
symbol: Annotated[str, "ticker symbol for the company"],
indicator: Annotated[
str, "quantitative indicators based off of the stock data for the company"
],
curr_date: Annotated[
str, "curr date for retrieving stock price data, YYYY-mm-dd"
],
):
data = load_ohlcv(symbol, curr_date)
df = wrap(data)
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
curr_date_str = pd.to_datetime(curr_date).strftime("%Y-%m-%d")
df[indicator] # trigger stockstats to calculate the indicator
matching_rows = df[df["Date"].str.startswith(curr_date_str)]
if not matching_rows.empty:
indicator_value = matching_rows[indicator].values[0]
return indicator_value
else:
return "N/A: Not a trading day (weekend or holiday)"
@@ -9,8 +9,8 @@ from dateutil.relativedelta import relativedelta
from tradingagents.dataflows.config import get_config
from tradingagents.dataflows.date_window import coverage_gap, in_window
from tradingagents.dataflows.errors import NoMarketDataError
from tradingagents.dataflows.stockstats_utils import yf_retry
from tradingagents.dataflows.symbols import normalize_symbol
from tradingagents.dataflows.vendors.yahoo.ohlcv import yf_retry
def _extract_article_data(article: dict) -> dict:
@@ -1,11 +1,9 @@
import logging
import os
import time
from typing import Annotated
import pandas as pd
import yfinance as yf
from stockstats import wrap
from yfinance.exceptions import YFRateLimitError
from tradingagents.dataflows.config import get_config
@@ -15,7 +13,7 @@ from tradingagents.dataflows.symbols import normalize_symbol, safe_ticker_compon
logger = logging.getLogger(__name__)
_YAHOO_HOST = "https://query2.finance.yahoo.com"
YAHOO_HOST = "https://query2.finance.yahoo.com"
# A vendor's latest OHLCV row this many calendar days before the requested date
# is treated as stale. Generous enough to span long holiday weekends, tight
@@ -35,7 +33,7 @@ def raise_for_empty(symbol: str, canonical: str, what: str) -> None:
yfinance returns an empty frame for a failed request rather than raising, so
without this a Yahoo outage reads as "this symbol has no {what}".
"""
if not vendor_reachable(_YAHOO_HOST):
if not vendor_reachable(YAHOO_HOST):
raise VendorRateLimitError(f"Yahoo Finance is unreachable; no {what} was retrieved")
raise NoMarketDataError(symbol, canonical, f"no {what}")
@@ -290,41 +288,3 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
return data
def filter_financials_by_date(data: pd.DataFrame, curr_date: str) -> pd.DataFrame:
"""Drop financial statement columns (fiscal period timestamps) after curr_date.
yfinance financial statements use fiscal period end dates as columns.
Columns after curr_date represent future data and are removed to
prevent look-ahead bias.
"""
if not curr_date or data.empty:
return data
cutoff = pd.Timestamp(curr_date)
mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff
return data.loc[:, mask]
class StockstatsUtils:
@staticmethod
def get_stock_stats(
symbol: Annotated[str, "ticker symbol for the company"],
indicator: Annotated[
str, "quantitative indicators based off of the stock data for the company"
],
curr_date: Annotated[
str, "curr date for retrieving stock price data, YYYY-mm-dd"
],
):
data = load_ohlcv(symbol, curr_date)
df = wrap(data)
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
curr_date_str = pd.to_datetime(curr_date).strftime("%Y-%m-%d")
df[indicator] # trigger stockstats to calculate the indicator
matching_rows = df[df["Date"].str.startswith(curr_date_str)]
if not matching_rows.empty:
indicator_value = matching_rows[indicator].values[0]
return indicator_value
else:
return "N/A: Not a trading day (weekend or holiday)"
@@ -15,7 +15,7 @@ from collections.abc import Iterable
import pandas as pd
from stockstats import wrap
from tradingagents.dataflows.stockstats_utils import load_ohlcv
from tradingagents.dataflows.vendors.yahoo.ohlcv import load_ohlcv
# A fixed, common indicator set so the snapshot is the same shape every run.
DEFAULT_SNAPSHOT_INDICATORS: tuple[str, ...] = (
+1 -1
View File
@@ -17,7 +17,7 @@ from tradingagents.agents.utils.rating import parse_rating
from tradingagents.dataflows.config import run_config, set_config
from tradingagents.dataflows.date_window import get_current_date
from tradingagents.dataflows.symbols import safe_ticker_component
from tradingagents.dataflows.y_finance import get_closes
from tradingagents.dataflows.vendors.yahoo.market import get_closes
from tradingagents.default_config import DEFAULT_CONFIG
from tradingagents.llm_clients import create_llm_client
from tradingagents.reporting import write_report_tree