mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 15:02:39 +03:00
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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -0,0 +1 @@
|
||||
"""Data vendors: one module or package per source, serving the router's methods."""
|
||||
tradingagents/dataflows/alpha_vantage.py → tradingagents/dataflows/vendors/alpha_vantage/__init__.py
Vendored
+4
-4
@@ -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
-1
@@ -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
-1
@@ -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__)
|
||||
|
||||
+4
-1
@@ -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
-1
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Yahoo Finance: prices, indicators, statements, insider filings and news."""
|
||||
@@ -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]
|
||||
+29
-191
@@ -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)"
|
||||
+1
-1
@@ -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:
|
||||
Vendored
+2
-42
@@ -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)"
|
||||
tradingagents/dataflows/market_data_validator.py → tradingagents/dataflows/vendors/yahoo/snapshot.py
Vendored
+1
-1
@@ -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, ...] = (
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user