refactor: name the data layer's date argument as_of_date

- vendor functions and date_window take as_of_date, the date data is served as of; the model-facing tool arguments are unchanged
- build_instrument_context and resolve_instrument_context take trade_date, which is what they receive
This commit is contained in:
Yijia-Xiao
2026-09-24 19:38:41 +00:00
parent 5ac5786d0d
commit 674f1087d1
19 changed files with 165 additions and 165 deletions
+2 -2
View File
@@ -82,7 +82,7 @@ def test_fundamentals_look_ahead_filter_runs_on_json_string(monkeypatch):
# #1115: the payload arrives as a JSON *string*; the old dict-only guard let # #1115: the payload arrives as a JSON *string*; the old dict-only guard let
# future-dated fiscal periods leak into historical runs. # future-dated fiscal periods leak into historical runs.
monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: _FUNDAMENTALS_JSON) monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: _FUNDAMENTALS_JSON)
out = avf.get_balance_sheet("AAPL", curr_date="2024-01-01") out = avf.get_balance_sheet("AAPL", as_of_date="2024-01-01")
assert isinstance(out, str) # callers still receive a str assert isinstance(out, str) # callers still receive a str
parsed = json.loads(out) parsed = json.loads(out)
assert [r["fiscalDateEnding"] for r in parsed["annualReports"]] == ["2023-12-31"] assert [r["fiscalDateEnding"] for r in parsed["annualReports"]] == ["2023-12-31"]
@@ -98,7 +98,7 @@ def test_fundamentals_no_curr_date_passes_through(monkeypatch):
@pytest.mark.unit @pytest.mark.unit
def test_fundamentals_non_json_body_unchanged(monkeypatch): def test_fundamentals_non_json_body_unchanged(monkeypatch):
monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: "not-json") monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: "not-json")
assert avf.get_cashflow("AAPL", curr_date="2024-01-01") == "not-json" assert avf.get_cashflow("AAPL", as_of_date="2024-01-01") == "not-json"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+6 -6
View File
@@ -141,7 +141,7 @@ class FredFormattingTests(unittest.TestCase):
self.assertEqual(len(body_rows), fred.MAX_ROWS) self.assertEqual(len(body_rows), fred.MAX_ROWS)
def test_window_is_lookahead_safe(self): def test_window_is_lookahead_safe(self):
# observation_end must equal curr_date so a past date never pulls future data. # observation_end must equal as_of_date so a past date never pulls future data.
captured = {} captured = {}
def _capture(path, params): def _capture(path, params):
@@ -156,9 +156,9 @@ class FredFormattingTests(unittest.TestCase):
def test_requests_pin_the_data_vintage(self): def test_requests_pin_the_data_vintage(self):
# #1275: both the metadata and observations requests must pin the vintage # #1275: both the metadata and observations requests must pin the vintage
# to curr_date (clamped to FRED's today), or FRED serves the latest # to as_of_date (clamped to FRED's today), or FRED serves the latest
# revision and revision-prone series leak future information. A past # revision and revision-prone series leak future information. A past
# curr_date sits below FRED's today, so it pins through unchanged. # as_of_date sits below FRED's today, so it pins through unchanged.
captured = {} captured = {}
def _capture(path, params): def _capture(path, params):
@@ -174,11 +174,11 @@ class FredFormattingTests(unittest.TestCase):
self.assertEqual(captured[path]["realtime_end"], "2025-09-30", path) self.assertEqual(captured[path]["realtime_end"], "2025-09-30", path)
def test_future_curr_date_clamps_vintage_to_fred_today(self): def test_future_curr_date_clamps_vintage_to_fred_today(self):
# #1275 regression: on a live run curr_date is the caller's LOCAL date, # #1275 regression: on a live run as_of_date is the caller's LOCAL date,
# which can be a day ahead of FRED's US-Central clock. Pinning the vintage # which can be a day ahead of FRED's US-Central clock. Pinning the vintage
# to that future date 400s, and the routing layer then drops macro data # to that future date 400s, and the routing layer then drops macro data
# silently. The pin must clamp to FRED's today; the observation window # silently. The pin must clamp to FRED's today; the observation window
# (future bars can't exist yet) stays at curr_date. # (future bars can't exist yet) stays at as_of_date.
captured = {} captured = {}
def _capture(path, params): def _capture(path, params):
@@ -192,7 +192,7 @@ class FredFormattingTests(unittest.TestCase):
for path in ("series", "series/observations"): for path in ("series", "series/observations"):
self.assertEqual(captured[path]["realtime_start"], "2026-08-31", path) self.assertEqual(captured[path]["realtime_start"], "2026-08-31", path)
self.assertEqual(captured[path]["realtime_end"], "2026-08-31", path) self.assertEqual(captured[path]["realtime_end"], "2026-08-31", path)
# the observation window still tracks curr_date, not the clamped vintage # the observation window still tracks as_of_date, not the clamped vintage
self.assertEqual(captured["series/observations"]["observation_end"], "2026-09-01") self.assertEqual(captured["series/observations"]["observation_end"], "2026-09-01")
+5 -5
View File
@@ -10,7 +10,7 @@ FRED (#1275), social (#1220) and memory (#1251) leaks.
Both vendors withhold on one shared rule (``date_window.withhold_live_profile``) Both vendors withhold on one shared rule (``date_window.withhold_live_profile``)
so switching ``fundamental_data`` between them cannot reintroduce the leak. The so switching ``fundamental_data`` between them cannot reintroduce the leak. The
statement tools stay point-in-time by filtering on ``curr_date``, and a live run statement tools stay point-in-time by filtering on ``as_of_date``, and a live run
is unchanged. All API access is mocked. is unchanged. All API access is mocked.
""" """
from __future__ import annotations from __future__ import annotations
@@ -45,19 +45,19 @@ _LEAKY = ("3500000000000", "34.2", "260.1", "391000000000",
"Apple Inc.", "Technology", "Consumer Electronics") "Apple Inc.", "Technology", "Consumer Electronics")
def _yf(curr_date, info=_INFO, today=_TODAY): def _yf(as_of_date, info=_INFO, today=_TODAY):
with mock.patch.object(date_window, "get_current_date", return_value=today), \ with mock.patch.object(date_window, "get_current_date", return_value=today), \
mock.patch.object(yahoo_fundamentals, "yf_retry", lambda fn: info), \ mock.patch.object(yahoo_fundamentals, "yf_retry", lambda fn: info), \
mock.patch.object(yahoo_market.yf, "Ticker"): mock.patch.object(yahoo_market.yf, "Ticker"):
return yahoo_fundamentals.get_fundamentals("AAPL", curr_date) return yahoo_fundamentals.get_fundamentals("AAPL", as_of_date)
def _av(curr_date, today=_TODAY): def _av(as_of_date, today=_TODAY):
"""Alpha Vantage path; the API call is mocked so a leak would be visible.""" """Alpha Vantage path; the API call is mocked so a leak would be visible."""
with mock.patch.object(date_window, "get_current_date", return_value=today), \ with mock.patch.object(date_window, "get_current_date", return_value=today), \
mock.patch.object(av, "_make_api_request", mock.patch.object(av, "_make_api_request",
return_value="MarketCapitalization: 3500000000000") as req: return_value="MarketCapitalization: 3500000000000") as req:
return av.get_fundamentals("AAPL", curr_date), req return av.get_fundamentals("AAPL", as_of_date), req
@pytest.mark.unit @pytest.mark.unit
+1 -1
View File
@@ -102,4 +102,4 @@ def test_an_unreachable_yahoo_is_not_reported_as_a_symbol_without_insider_data()
with mock.patch.object(fundamentals.yf, "Ticker", return_value=ticker), \ with mock.patch.object(fundamentals.yf, "Ticker", return_value=ticker), \
mock.patch.object(fundamentals, "vendor_reachable", return_value=False), \ mock.patch.object(fundamentals, "vendor_reachable", return_value=False), \
pytest.raises(VendorRateLimitError): pytest.raises(VendorRateLimitError):
fundamentals.get_insider_transactions("AAPL", curr_date="2026-09-21") fundamentals.get_insider_transactions("AAPL", as_of_date="2026-09-21")
+5 -5
View File
@@ -70,14 +70,14 @@ def test_alpha_vantage_insider_filings_after_the_date_are_dropped():
@pytest.mark.unit @pytest.mark.unit
def test_polymarket_withholds_live_odds_from_a_historical_run(): def test_polymarket_withholds_live_odds_from_a_historical_run():
with mock.patch.object(polymarket, "_request", side_effect=AssertionError("must not fetch")): with mock.patch.object(polymarket, "_request", side_effect=AssertionError("must not fetch")):
out = polymarket.get_prediction_markets("Fed rate cut", curr_date="2025-06-01") out = polymarket.get_prediction_markets("Fed rate cut", as_of_date="2025-06-01")
assert "withheld" in out assert "withheld" in out
@pytest.mark.unit @pytest.mark.unit
def test_polymarket_serves_a_current_run(): def test_polymarket_serves_a_current_run():
with mock.patch.object(polymarket, "_request", return_value={"events": []}) as req: with mock.patch.object(polymarket, "_request", return_value={"events": []}) as req:
polymarket.get_prediction_markets("Fed rate cut", curr_date=polymarket.get_current_date()) polymarket.get_prediction_markets("Fed rate cut", as_of_date=polymarket.get_current_date())
req.assert_called_once() req.assert_called_once()
@@ -103,7 +103,7 @@ def test_a_historical_run_is_told_the_identity_is_current(monkeypatch):
identity = {"company_name": "Example Corp", "sector": "Technology", identity = {"company_name": "Example Corp", "sector": "Technology",
"industry": "Software", "exchange": "NMS"} "industry": "Software", "exchange": "NMS"}
historical = build_instrument_context("EXMP", "stock", identity, curr_date="2024-03-14") historical = build_instrument_context("EXMP", "stock", identity, trade_date="2024-03-14")
assert "Example Corp" in historical assert "Example Corp" in historical
assert "2024-03-14" in historical and "today" in historical.lower() assert "2024-03-14" in historical and "today" in historical.lower()
@@ -114,7 +114,7 @@ def test_a_current_run_is_not_cluttered_with_a_vintage_note(monkeypatch):
from tradingagents.dataflows.date_window import get_current_date from tradingagents.dataflows.date_window import get_current_date
today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"}, today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"},
curr_date=get_current_date()) trade_date=get_current_date())
assert "Example Corp" in today assert "Example Corp" in today
assert "resolved today" not in today.lower() assert "resolved today" not in today.lower()
@@ -297,7 +297,7 @@ def test_an_unavailable_notice_names_no_date_after_the_run():
coverage_gap([pd.Timestamp(today, tz="UTC")], "2025-01-01", "2025-01-07", "Feed", "news"), coverage_gap([pd.Timestamp(today, tz="UTC")], "2025-01-01", "2025-01-07", "Feed", "news"),
withhold_live_profile("2025-01-07", "AAPL"), withhold_live_profile("2025-01-07", "AAPL"),
_yf_insider(_insider_frame(today), "2025-01-07"), _yf_insider(_insider_frame(today), "2025-01-07"),
build_instrument_context("EXMP", "stock", {"company_name": "Example"}, curr_date="2025-01-07"), build_instrument_context("EXMP", "stock", {"company_name": "Example"}, trade_date="2025-01-07"),
] ]
for notice in notices: for notice in notices:
assert _dates_after(notice, "2025-01-07") == [], notice assert _dates_after(notice, "2025-01-07") == [], notice
+3 -3
View File
@@ -100,7 +100,7 @@ def build_instrument_context(
ticker: str, ticker: str,
asset_type: str = "stock", asset_type: str = "stock",
identity: Mapping[str, str] | None = None, identity: Mapping[str, str] | None = None,
curr_date: str | None = None, trade_date: str | None = None,
) -> str: ) -> str:
"""Describe the exact instrument so agents preserve identity and ticker. """Describe the exact instrument so agents preserve identity and ticker.
@@ -144,10 +144,10 @@ def build_instrument_context(
"result explicitly disproves this resolved identity." "result explicitly disproves this resolved identity."
) )
today = get_current_date() today = get_current_date()
if curr_date and str(curr_date) < today: if trade_date and str(trade_date) < today:
context += ( context += (
f" This identity is how the vendor describes the instrument today, " f" This identity is how the vendor describes the instrument today, "
f"not necessarily on {curr_date}: a name or classification changed " f"not necessarily on {trade_date}: a name or classification changed "
f"since then would read as the current one." f"since then would read as the current one."
) )
+7 -7
View File
@@ -95,7 +95,7 @@ def as_of_window(start_date: str, end_date: str, trade_date: str) -> tuple[str,
return f"{_parse(end) - span:%Y-%m-%d}", end return f"{_parse(end) - span:%Y-%m-%d}", end
def withhold_live_profile(curr_date: str | None, label: str) -> str | None: def withhold_live_profile(as_of_date: str | None, label: str) -> str | None:
"""Notice to serve instead of a live-only company profile, or None to serve it. """Notice to serve instead of a live-only company profile, or None to serve it.
Vendor "company overview" endpoints (yfinance ``Ticker.info``, Alpha Vantage Vendor "company overview" endpoints (yfinance ``Ticker.info``, Alpha Vantage
@@ -105,20 +105,20 @@ def withhold_live_profile(curr_date: str | None, label: str) -> str | None:
Every fundamentals vendor withholds on this rule, so switching between them Every fundamentals vendor withholds on this rule, so switching between them
cannot reintroduce the leak. cannot reintroduce the leak.
""" """
if not curr_date: if not as_of_date:
return None return None
today = get_current_date() today = get_current_date()
if curr_date >= today: if as_of_date >= today:
return None return None
return ( return (
f"# Company Fundamentals for {label}\n" f"# Company Fundamentals for {label}\n"
f"# Point-in-time as of: {curr_date}\n\n" f"# Point-in-time as of: {as_of_date}\n\n"
f"Profile fundamentals are withheld for this date. This vendor serves " f"Profile fundamentals are withheld for this date. This vendor serves "
f"only present-day values with no historical vintage: market " f"only present-day values with no historical vintage: market "
f"cap, valuation multiples, the 52-week range and TTM income move with " f"cap, valuation multiples, the 52-week range and TTM income move with "
f"today's quote, and even the name, sector and industry reflect today " f"today's quote, and even the name, sector and industry reflect today "
f"rather than {curr_date} (companies rename and get reclassified). " f"rather than {as_of_date} (companies rename and get reclassified). "
f"Serving them would put post-decision information into a {curr_date} " f"Serving them would put post-decision information into a {as_of_date} "
f"analysis. Point-in-time fundamentals for {curr_date} are available " f"analysis. Point-in-time fundamentals for {as_of_date} are available "
f"from the balance sheet, income statement, and cash flow tools." f"from the balance sheet, income statement, and cash flow tools."
) )
+15 -15
View File
@@ -4,14 +4,14 @@ from tradingagents.dataflows.date_window import withhold_live_profile
from tradingagents.dataflows.vendors.alpha_vantage.common import _make_api_request from tradingagents.dataflows.vendors.alpha_vantage.common import _make_api_request
def _filter_reports_by_date(result, curr_date: str): def _filter_reports_by_date(result, as_of_date: str):
"""Drop annual/quarterly reports dated after curr_date to prevent look-ahead. """Drop annual/quarterly reports dated after as_of_date to prevent look-ahead.
``_make_api_request`` returns the fundamentals payload as a JSON string, so ``_make_api_request`` returns the fundamentals payload as a JSON string, so
parse, filter, and re-serialize. A non-JSON body or an unset ``curr_date`` is parse, filter, and re-serialize. A non-JSON body or an unset ``as_of_date`` is
returned unchanged. returned unchanged.
""" """
if not curr_date or not isinstance(result, str): if not as_of_date or not isinstance(result, str):
return result return result
try: try:
payload = json.loads(result) payload = json.loads(result)
@@ -23,28 +23,28 @@ def _filter_reports_by_date(result, curr_date: str):
if isinstance(payload.get(key), list): if isinstance(payload.get(key), list):
payload[key] = [ payload[key] = [
r for r in payload[key] r for r in payload[key]
if r.get("fiscalDateEnding", "") <= curr_date if r.get("fiscalDateEnding", "") <= as_of_date
] ]
return json.dumps(payload) return json.dumps(payload)
def get_fundamentals(ticker: str, curr_date: str = None) -> str: def get_fundamentals(ticker: str, as_of_date: str = None) -> str:
""" """
Retrieve comprehensive fundamental data for a given ticker symbol using Alpha Vantage. Retrieve comprehensive fundamental data for a given ticker symbol using Alpha Vantage.
OVERVIEW serves only present-day values and carries no historical vintage, so OVERVIEW serves only present-day values and carries no historical vintage, so
a past ``curr_date`` withholds it rather than leaking post-decision figures a past ``as_of_date`` withholds it rather than leaking post-decision figures
into a backtest (#1300); the statement endpoints below stay point-in-time via into a backtest (#1300); the statement endpoints below stay point-in-time via
``_filter_reports_by_date``. ``_filter_reports_by_date``.
Args: Args:
ticker (str): Ticker symbol of the company ticker (str): Ticker symbol of the company
curr_date (str): Analysis date, yyyy-mm-dd as_of_date (str): Analysis date, yyyy-mm-dd
Returns: Returns:
str: Company overview data including financial ratios and key metrics str: Company overview data including financial ratios and key metrics
""" """
withheld = withhold_live_profile(curr_date, ticker) withheld = withhold_live_profile(as_of_date, ticker)
if withheld: if withheld:
return withheld return withheld
@@ -55,20 +55,20 @@ def get_fundamentals(ticker: str, curr_date: str = None) -> str:
return _make_api_request("OVERVIEW", params) return _make_api_request("OVERVIEW", params)
def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str = None): def get_balance_sheet(ticker: str, freq: str = "quarterly", as_of_date: str = None):
"""Retrieve balance sheet data for a given ticker symbol using Alpha Vantage.""" """Retrieve balance sheet data for a given ticker symbol using Alpha Vantage."""
result = _make_api_request("BALANCE_SHEET", {"symbol": ticker}) result = _make_api_request("BALANCE_SHEET", {"symbol": ticker})
return _filter_reports_by_date(result, curr_date) return _filter_reports_by_date(result, as_of_date)
def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str = None): def get_cashflow(ticker: str, freq: str = "quarterly", as_of_date: str = None):
"""Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage.""" """Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage."""
result = _make_api_request("CASH_FLOW", {"symbol": ticker}) result = _make_api_request("CASH_FLOW", {"symbol": ticker})
return _filter_reports_by_date(result, curr_date) return _filter_reports_by_date(result, as_of_date)
def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str = None): def get_income_statement(ticker: str, freq: str = "quarterly", as_of_date: str = None):
"""Retrieve income statement data for a given ticker symbol using Alpha Vantage.""" """Retrieve income statement data for a given ticker symbol using Alpha Vantage."""
result = _make_api_request("INCOME_STATEMENT", {"symbol": ticker}) result = _make_api_request("INCOME_STATEMENT", {"symbol": ticker})
return _filter_reports_by_date(result, curr_date) return _filter_reports_by_date(result, as_of_date)
+6 -6
View File
@@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
def get_indicator( def get_indicator(
symbol: str, symbol: str,
indicator: str, indicator: str,
curr_date: str, as_of_date: str,
look_back_days: int, look_back_days: int,
interval: str = "daily", interval: str = "daily",
time_period: int = 14, time_period: int = 14,
@@ -21,7 +21,7 @@ def get_indicator(
Args: Args:
symbol: ticker symbol of the company symbol: ticker symbol of the company
indicator: technical indicator to get the analysis and report of indicator: technical indicator to get the analysis and report of
curr_date: The current trading date you are trading on, YYYY-mm-dd as_of_date: The current trading date you are trading on, YYYY-mm-dd
look_back_days: how many days to look back look_back_days: how many days to look back
interval: Time interval (daily, weekly, monthly) interval: Time interval (daily, weekly, monthly)
time_period: Number of data points for calculation time_period: Number of data points for calculation
@@ -72,8 +72,8 @@ def get_indicator(
f"Alpha Vantage does not serve {indicator}; it serves {list(supported_indicators)}" f"Alpha Vantage does not serve {indicator}; it serves {list(supported_indicators)}"
) )
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d") as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
before = curr_date_dt - relativedelta(days=look_back_days) before = as_of_dt - relativedelta(days=look_back_days)
# Get the full data for the period instead of making individual calls # Get the full data for the period instead of making individual calls
_, required_series_type = supported_indicators[indicator] _, required_series_type = supported_indicators[indicator]
@@ -184,7 +184,7 @@ def get_indicator(
date_str = values[date_col_idx].strip() date_str = values[date_col_idx].strip()
date_dt = datetime.strptime(date_str, "%Y-%m-%d") date_dt = datetime.strptime(date_str, "%Y-%m-%d")
if before <= date_dt <= curr_date_dt: if before <= date_dt <= as_of_dt:
value = values[value_col_idx].strip() value = values[value_col_idx].strip()
result_data.append((date_dt, value)) result_data.append((date_dt, value))
except (ValueError, IndexError): except (ValueError, IndexError):
@@ -201,7 +201,7 @@ def get_indicator(
ind_string = "No data available for the specified date range.\n" ind_string = "No data available for the specified date range.\n"
result_str = ( result_str = (
f"## {indicator.upper()} values from {before.strftime('%Y-%m-%d')} to {curr_date}:\n\n" f"## {indicator.upper()} values from {before.strftime('%Y-%m-%d')} to {as_of_date}:\n\n"
+ ind_string + ind_string
+ "\n\n" + "\n\n"
+ indicator_descriptions.get(indicator, "No description available.") + indicator_descriptions.get(indicator, "No description available.")
+8 -8
View File
@@ -33,13 +33,13 @@ def get_news(ticker, start_date, end_date) -> dict[str, str] | str:
return _make_api_request("NEWS_SENTIMENT", params) return _make_api_request("NEWS_SENTIMENT", params)
def get_global_news(curr_date, look_back_days: int | None = None, limit: int | None = None) -> dict[str, str] | str: def get_global_news(as_of_date, look_back_days: int | None = None, limit: int | None = None) -> dict[str, str] | str:
"""Returns global market news & sentiment data without ticker-specific filtering. """Returns global market news & sentiment data without ticker-specific filtering.
Covers broad market topics like financial markets, economy, and more. Covers broad market topics like financial markets, economy, and more.
Args: Args:
curr_date: Current date in yyyy-mm-dd format. as_of_date: Current date in yyyy-mm-dd format.
look_back_days: Number of days to look back; ``None`` uses look_back_days: Number of days to look back; ``None`` uses
``global_news_lookback_days`` from the active config. ``global_news_lookback_days`` from the active config.
limit: Maximum number of articles; ``None`` uses limit: Maximum number of articles; ``None`` uses
@@ -56,28 +56,28 @@ def get_global_news(curr_date, look_back_days: int | None = None, limit: int | N
if limit is None: if limit is None:
limit = config["global_news_article_limit"] limit = config["global_news_article_limit"]
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d") curr_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
start_dt = curr_dt - timedelta(days=look_back_days) start_dt = curr_dt - timedelta(days=look_back_days)
start_date = start_dt.strftime("%Y-%m-%d") start_date = start_dt.strftime("%Y-%m-%d")
params = { params = {
"topics": "financial_markets,economy_macro,economy_monetary", "topics": "financial_markets,economy_macro,economy_monetary",
"time_from": format_datetime_for_api(start_date), "time_from": format_datetime_for_api(start_date),
"time_to": format_datetime_for_api(curr_date, end_of_day=True), "time_to": format_datetime_for_api(as_of_date, end_of_day=True),
"limit": str(limit), "limit": str(limit),
} }
return _make_api_request("NEWS_SENTIMENT", params) return _make_api_request("NEWS_SENTIMENT", params)
def get_insider_transactions(symbol: str, curr_date: str | None = None) -> dict[str, str] | str: def get_insider_transactions(symbol: str, as_of_date: str | None = None) -> dict[str, str] | str:
"""Returns latest and historical insider transactions by key stakeholders. """Returns latest and historical insider transactions by key stakeholders.
Covers transactions by founders, executives, board members, etc. Covers transactions by founders, executives, board members, etc.
Args: Args:
symbol: Ticker symbol. Example: "IBM". symbol: Ticker symbol. Example: "IBM".
curr_date: When given, only transactions on or before it (yyyy-mm-dd). as_of_date: When given, only transactions on or before it (yyyy-mm-dd).
Returns: Returns:
Dictionary containing insider transaction data or JSON string. Dictionary containing insider transaction data or JSON string.
@@ -88,8 +88,8 @@ def get_insider_transactions(symbol: str, curr_date: str | None = None) -> dict[
} }
response = _make_api_request("INSIDER_TRANSACTIONS", params) response = _make_api_request("INSIDER_TRANSACTIONS", params)
if not curr_date: if not as_of_date:
return response return response
payload = json.loads(response) payload = json.loads(response)
payload["data"] = [t for t in payload["data"] if t["transaction_date"] <= curr_date] payload["data"] = [t for t in payload["data"] if t["transaction_date"] <= as_of_date]
return json.dumps(payload) return json.dumps(payload)
+10 -10
View File
@@ -126,7 +126,7 @@ def _fred_today() -> str:
"""FRED's current calendar date (US Central) as ``yyyy-mm-dd``. """FRED's current calendar date (US Central) as ``yyyy-mm-dd``.
The vintage pin is clamped to this: FRED rejects a ``realtime_start`` after The vintage pin is clamped to this: FRED rejects a ``realtime_start`` after
its own today with a 400, and ``curr_date`` on a live run comes from the its own today with a 400, and ``as_of_date`` on a live run comes from the
caller's local clock, which can already be tomorrow in Chicago. caller's local clock, which can already be tomorrow in Chicago.
""" """
return datetime.now(FRED_TZ).strftime("%Y-%m-%d") return datetime.now(FRED_TZ).strftime("%Y-%m-%d")
@@ -155,7 +155,7 @@ def _request(path: str, params: dict) -> dict:
def get_macro_data( def get_macro_data(
indicator: str, indicator: str,
curr_date: str, as_of_date: str,
look_back_days: int | None = None, look_back_days: int | None = None,
) -> str: ) -> str:
"""Fetch a FRED macroeconomic series as a formatted markdown report. """Fetch a FRED macroeconomic series as a formatted markdown report.
@@ -163,9 +163,9 @@ def get_macro_data(
Args: Args:
indicator: A friendly alias (e.g. "cpi", "unemployment", "10y_treasury") indicator: A friendly alias (e.g. "cpi", "unemployment", "10y_treasury")
or a raw FRED series ID (e.g. "CPIAUCSL", "DGS10"). or a raw FRED series ID (e.g. "CPIAUCSL", "DGS10").
curr_date: The as-of date (yyyy-mm-dd). It bounds the observation window as_of_date: The as-of date (yyyy-mm-dd). It bounds the observation window
AND pins the data vintage: FRED is queried with the realtime bounds AND pins the data vintage: FRED is queried with the realtime bounds
set to ``curr_date`` (clamped to FRED's own today) so a historical set to ``as_of_date`` (clamped to FRED's own today) so a historical
run sees the values that were actually published by that date, not run sees the values that were actually published by that date, not
later revisions. Without this, revision-prone series (CPI, GDP, ...) later revisions. Without this, revision-prone series (CPI, GDP, ...)
would leak future information into a backtest (#1275). would leak future information into a backtest (#1275).
@@ -178,18 +178,18 @@ def get_macro_data(
if look_back_days is None: if look_back_days is None:
look_back_days = DEFAULT_LOOKBACK_DAYS look_back_days = DEFAULT_LOOKBACK_DAYS
end_dt = datetime.strptime(curr_date, "%Y-%m-%d") end_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
start_date = (end_dt - timedelta(days=look_back_days)).strftime("%Y-%m-%d") start_date = (end_dt - timedelta(days=look_back_days)).strftime("%Y-%m-%d")
# Pin the data vintage. FRED defaults both realtime bounds to today, serving # Pin the data vintage. FRED defaults both realtime bounds to today, serving
# the LATEST revision of every observation; a single-day realtime interval # the LATEST revision of every observation; a single-day realtime interval
# asks for the values known as of the pin instead, on both the metadata and # asks for the values known as of the pin instead, on both the metadata and
# observations requests (#1275). Clamp to FRED's today: on a live run # observations requests (#1275). Clamp to FRED's today: on a live run
# curr_date is the caller's local date, which can be a day ahead of Chicago, # as_of_date is the caller's local date, which can be a day ahead of Chicago,
# and a realtime date in FRED's future 400s -> the routing layer would then # and a realtime date in FRED's future 400s -> the routing layer would then
# drop macro data silently. A past curr_date is unaffected, so historical # drop macro data silently. A past as_of_date is unaffected, so historical
# point-in-time behaviour is preserved. # point-in-time behaviour is preserved.
pit = min(curr_date, _fred_today()) pit = min(as_of_date, _fred_today())
realtime = {"realtime_start": pit, "realtime_end": pit} realtime = {"realtime_start": pit, "realtime_end": pit}
# Invalid LLM-supplied indicator: return guidance rather than raising, so a # Invalid LLM-supplied indicator: return guidance rather than raising, so a
@@ -217,7 +217,7 @@ def get_macro_data(
{ {
"series_id": series_id, "series_id": series_id,
"observation_start": start_date, "observation_start": start_date,
"observation_end": curr_date, "observation_end": as_of_date,
"sort_order": "asc", "sort_order": "asc",
**realtime, **realtime,
}, },
@@ -235,7 +235,7 @@ def get_macro_data(
f"- Units: {units}\n" f"- Units: {units}\n"
f"- Frequency: {frequency}" f"- Frequency: {frequency}"
f"{f' ({seasonal})' if seasonal else ''}\n" f"{f' ({seasonal})' if seasonal else ''}\n"
f"- Window: {start_date} to {curr_date}\n" f"- Window: {start_date} to {as_of_date}\n"
) )
if not points: if not points:
+5 -5
View File
@@ -67,7 +67,7 @@ def _is_forward_looking(market: dict, now: datetime) -> bool:
) )
def get_prediction_markets(topic: str, limit: int | None = None, curr_date: str | None = None) -> str: def get_prediction_markets(topic: str, limit: int | None = None, as_of_date: str | None = None) -> str:
"""Return live prediction-market probabilities for an event topic. """Return live prediction-market probabilities for an event topic.
Args: Args:
@@ -75,7 +75,7 @@ def get_prediction_markets(topic: str, limit: int | None = None, curr_date: str
"US election", or a sector/company event. "US election", or a sector/company event.
limit: Max markets to return (ranked by traded volume); ``None`` uses limit: Max markets to return (ranked by traded volume); ``None`` uses
DEFAULT_LIMIT. DEFAULT_LIMIT.
curr_date: The analysis date. Polymarket serves only live odds, so a as_of_date: The analysis date. Polymarket serves only live odds, so a
date before today withholds them. date before today withholds them.
Returns: Returns:
@@ -83,11 +83,11 @@ def get_prediction_markets(topic: str, limit: int | None = None, curr_date: str
each with its implied probability, traded volume, resolution date, and each with its implied probability, traded volume, resolution date, and
recent (1-week) move. recent (1-week) move.
""" """
if curr_date and curr_date < get_current_date(): if as_of_date and as_of_date < get_current_date():
return ( return (
f"Prediction-market odds are withheld for {curr_date}. Polymarket serves " f"Prediction-market odds are withheld for {as_of_date}. Polymarket serves "
f"only live odds on open markets, with no historical vintage, so serving " f"only live odds on open markets, with no historical vintage, so serving "
f"them would put post-decision information into a {curr_date} analysis." f"them would put post-decision information into a {as_of_date} analysis."
) )
if limit is None: if limit is None:
limit = DEFAULT_LIMIT limit = DEFAULT_LIMIT
+17 -17
View File
@@ -5,7 +5,7 @@ statement at the fiscal period end. That is two claims a run should not make: a
period that has ended is not public until the company files, weeks later, and a period that has ended is not public until the company files, weeks later, and a
figure that was later restated is not what investors saw at the time. figure that was later restated is not what investors saw at the time.
EDGAR reports every fact with the date it was filed, so a run dated ``curr_date`` EDGAR reports every fact with the date it was filed, so a run dated ``as_of_date``
serves exactly what was on file by then, restatements included at the vintage serves exactly what was on file by then, restatements included at the vintage
that was current: Apple's 2008 total assets read 39.6B until the 2010 amendment that was current: Apple's 2008 total assets read 39.6B until the 2010 amendment
restated them to 36.2B. restated them to 36.2B.
@@ -144,7 +144,7 @@ def cik_for(ticker: str) -> str | None:
return None return None
def _as_of(facts: dict, tags: tuple[str, ...], curr_date: str, span: tuple[int, int], def _as_of(facts: dict, tags: tuple[str, ...], as_of_date: str, span: tuple[int, int],
forms: tuple[str, ...] = ()) -> tuple[dict, str]: forms: tuple[str, ...] = ()) -> tuple[dict, str]:
"""({period end: value}, unit) for the first tag the filer reports, as known then. """({period end: value}, unit) for the first tag the filer reports, as known then.
@@ -164,7 +164,7 @@ def _as_of(facts: dict, tags: tuple[str, ...], curr_date: str, span: tuple[int,
latest: dict[str, dict] = {} latest: dict[str, dict] = {}
covered: set[str] = set() # period ends a filing of ``forms`` reports covered: set[str] = set() # period ends a filing of ``forms`` reports
for fact in unit_values: for fact in unit_values:
if fact["filed"] > curr_date or fact["end"] in values: if fact["filed"] > as_of_date or fact["end"] in values:
continue continue
# A duration fact (revenue, cash flow) must cover the span asked # A duration fact (revenue, cash flow) must cover the span asked
# for. An instant fact (a balance) has no span and serves both. # for. An instant fact (a balance) has no span and serves both.
@@ -184,8 +184,8 @@ def _as_of(facts: dict, tags: tuple[str, ...], curr_date: str, span: tuple[int,
return dict(sorted(values.items())), chosen_unit return dict(sorted(values.items())), chosen_unit
def _statement(kind: str, ticker: str, freq: str, curr_date: str, title: str) -> str: def _statement(kind: str, ticker: str, freq: str, as_of_date: str, title: str) -> str:
curr_date = curr_date or datetime.now().strftime("%Y-%m-%d") as_of_date = as_of_date or datetime.now().strftime("%Y-%m-%d")
cik = cik_for(ticker) cik = cik_for(ticker)
if cik is None: if cik is None:
raise NoMarketDataError(ticker, ticker, "not a US SEC filer") raise NoMarketDataError(ticker, ticker, "not a US SEC filer")
@@ -198,14 +198,14 @@ def _statement(kind: str, ticker: str, freq: str, curr_date: str, title: str) ->
quarterly = freq.lower() == "quarterly" quarterly = freq.lower() == "quarterly"
span = _SPANS["quarterly" if quarterly else "annual"] span = _SPANS["quarterly" if quarterly else "annual"]
forms = () if quarterly else _ANNUAL_FORMS forms = () if quarterly else _ANNUAL_FORMS
lines = {label: _as_of(us_gaap, tags, curr_date, span, forms) for label, tags in _STATEMENTS[kind]} lines = {label: _as_of(us_gaap, tags, as_of_date, span, forms) for label, tags in _STATEMENTS[kind]}
periods = sorted({end for values, _ in lines.values() for end in values}) periods = sorted({end for values, _ in lines.values() for end in values})
if not periods: if not periods:
raise NoMarketDataError(ticker, ticker, f"no {freq} {title.lower()} filed by {curr_date}") raise NoMarketDataError(ticker, ticker, f"no {freq} {title.lower()} filed by {as_of_date}")
header = ( header = (
f"# {title} for {ticker.upper()} ({freq}), USD in millions unless the row says otherwise\n" f"# {title} for {ticker.upper()} ({freq}), USD in millions unless the row says otherwise\n"
f"# SEC EDGAR facts filed on or before {curr_date}, at the values filed then\n\n" f"# SEC EDGAR facts filed on or before {as_of_date}, at the values filed then\n\n"
) )
rows = [",".join([""] + periods)] rows = [",".join([""] + periods)]
for label, (values, unit) in lines.items(): for label, (values, unit) in lines.items():
@@ -223,21 +223,21 @@ def _statement(kind: str, ticker: str, freq: str, curr_date: str, title: str) ->
return header + "\n".join(rows) + "\n" return header + "\n".join(rows) + "\n"
def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str | None = None) -> str: def get_balance_sheet(ticker: str, freq: str = "quarterly", as_of_date: str | None = None) -> str:
"""Balance sheet as filed on or before ``curr_date``.""" """Balance sheet as filed on or before ``as_of_date``."""
return _statement("balance_sheet", ticker, freq, curr_date, "Balance Sheet") return _statement("balance_sheet", ticker, freq, as_of_date, "Balance Sheet")
def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str | None = None) -> str: def get_income_statement(ticker: str, freq: str = "quarterly", as_of_date: str | None = None) -> str:
"""Income statement as filed on or before ``curr_date``. """Income statement as filed on or before ``as_of_date``.
A fourth quarter is never derived: filers report it only inside the annual A fourth quarter is never derived: filers report it only inside the annual
figure, and subtracting three separately filed quarters would invent a number figure, and subtracting three separately filed quarters would invent a number
with no filing date behind it. with no filing date behind it.
""" """
return _statement("income_statement", ticker, freq, curr_date, "Income Statement") return _statement("income_statement", ticker, freq, as_of_date, "Income Statement")
def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str | None = None) -> str: def get_cashflow(ticker: str, freq: str = "quarterly", as_of_date: str | None = None) -> str:
"""Cash flow statement as filed on or before ``curr_date``.""" """Cash flow statement as filed on or before ``as_of_date``."""
return _statement("cashflow", ticker, freq, curr_date, "Cash Flow Statement") return _statement("cashflow", ticker, freq, as_of_date, "Cash Flow Statement")
+21 -21
View File
@@ -16,19 +16,19 @@ from tradingagents.dataflows.vendors.yahoo.ohlcv import (
def get_fundamentals( def get_fundamentals(
ticker: Annotated[str, "ticker symbol of the company"], ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "analysis date in YYYY-MM-DD format"] = None as_of_date: Annotated[str, "analysis date in YYYY-MM-DD format"] = None
): ):
"""Get company fundamentals overview from yfinance. """Get company fundamentals overview from yfinance.
``Ticker.info`` is a present-day snapshot with no historical vintage, so a ``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 past ``as_of_date`` withholds it through the shared point-in-time guard
(``date_window.withhold_live_profile``, #1300). (``date_window.withhold_live_profile``, #1300).
""" """
canonical = normalize_symbol(ticker) canonical = normalize_symbol(ticker)
# Guard before the request: the response would only be discarded, and the # Guard before the request: the response would only be discarded, and the
# answer does not depend on it. # answer does not depend on it.
withheld = withhold_live_profile(curr_date, canonical) withheld = withhold_live_profile(as_of_date, canonical)
if withheld: if withheld:
return withheld return withheld
@@ -99,14 +99,14 @@ _PERIOD_END_VINTAGE = (
) )
def _statement(ticker, freq, curr_date, title, quarterly_attr, annual_attr) -> str: def _statement(ticker, freq, as_of_date, title, quarterly_attr, annual_attr) -> str:
"""One financial statement as CSV, cut at ``curr_date`` by period end.""" """One financial statement as CSV, cut at ``as_of_date`` by period end."""
canonical = normalize_symbol(ticker) canonical = normalize_symbol(ticker)
what = title.lower() what = title.lower()
try: try:
ticker_obj = yf.Ticker(canonical) ticker_obj = yf.Ticker(canonical)
attr = quarterly_attr if freq.lower() == "quarterly" else annual_attr attr = quarterly_attr if freq.lower() == "quarterly" else annual_attr
data = filter_financials_by_date(yf_retry(lambda: getattr(ticker_obj, attr)), curr_date) data = filter_financials_by_date(yf_retry(lambda: getattr(ticker_obj, attr)), as_of_date)
if data.empty: if data.empty:
raise_for_empty(ticker, canonical, f"{what} data") raise_for_empty(ticker, canonical, f"{what} data")
return f"# {title} data for {canonical} ({freq})\n" + _PERIOD_END_VINTAGE + data.to_csv() return f"# {title} data for {canonical} ({freq})\n" + _PERIOD_END_VINTAGE + data.to_csv()
@@ -119,28 +119,28 @@ def _statement(ticker, freq, curr_date, title, quarterly_attr, annual_attr) -> s
def get_balance_sheet( def get_balance_sheet(
ticker: Annotated[str, "ticker symbol of the company"], ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None as_of_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
): ):
"""Get balance sheet data from yfinance.""" """Get balance sheet data from yfinance."""
return _statement(ticker, freq, curr_date, "Balance Sheet", "quarterly_balance_sheet", "balance_sheet") return _statement(ticker, freq, as_of_date, "Balance Sheet", "quarterly_balance_sheet", "balance_sheet")
def get_cashflow( def get_cashflow(
ticker: Annotated[str, "ticker symbol of the company"], ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None as_of_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
): ):
"""Get cash flow data from yfinance.""" """Get cash flow data from yfinance."""
return _statement(ticker, freq, curr_date, "Cash Flow", "quarterly_cashflow", "cashflow") return _statement(ticker, freq, as_of_date, "Cash Flow", "quarterly_cashflow", "cashflow")
def get_income_statement( def get_income_statement(
ticker: Annotated[str, "ticker symbol of the company"], ticker: Annotated[str, "ticker symbol of the company"],
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly", freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None as_of_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
): ):
"""Get income statement data from yfinance.""" """Get income statement data from yfinance."""
return _statement(ticker, freq, curr_date, "Income Statement", "quarterly_income_stmt", "income_stmt") return _statement(ticker, freq, as_of_date, "Income Statement", "quarterly_income_stmt", "income_stmt")
# Rows are dated by the transaction, which is when the insider traded, not when # Rows are dated by the transaction, which is when the insider traded, not when
@@ -156,7 +156,7 @@ _TRANSACTION_DATE_VINTAGE = (
def get_insider_transactions( def get_insider_transactions(
ticker: Annotated[str, "ticker symbol of the company"], ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str | None, "only transactions on or before this date, yyyy-mm-dd"] = None, as_of_date: Annotated[str | None, "only transactions on or before this date, yyyy-mm-dd"] = None,
): ):
"""Get insider transactions data from yfinance.""" """Get insider transactions data from yfinance."""
canonical = normalize_symbol(ticker) canonical = normalize_symbol(ticker)
@@ -171,12 +171,12 @@ def get_insider_transactions(
raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved") raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved")
return f"No insider transactions reported for symbol '{canonical}'" return f"No insider transactions reported for symbol '{canonical}'"
if curr_date: if as_of_date:
traded = data["Start Date"] traded = data["Start Date"]
kept = data[traded <= pd.Timestamp(curr_date)] kept = data[traded <= pd.Timestamp(as_of_date)]
if kept.empty: if kept.empty:
return ( return (
f"<insider transactions unavailable for {canonical} as of {curr_date}: " f"<insider transactions unavailable for {canonical} as of {as_of_date}: "
"Yahoo serves recent transactions only>" "Yahoo serves recent transactions only>"
) )
data = kept data = kept
@@ -198,15 +198,15 @@ def get_company_profile(ticker: str) -> dict:
raise NoMarketDataError(ticker, canonical, f"profile unavailable: {e}") from e raise NoMarketDataError(ticker, canonical, f"profile unavailable: {e}") from e
def filter_financials_by_date(data: pd.DataFrame, curr_date: str) -> pd.DataFrame: def filter_financials_by_date(data: pd.DataFrame, as_of_date: str) -> pd.DataFrame:
"""Drop financial statement columns (fiscal period timestamps) after curr_date. """Drop financial statement columns (fiscal period timestamps) after as_of_date.
yfinance financial statements use fiscal period end dates as columns. yfinance financial statements use fiscal period end dates as columns.
Columns after curr_date represent future data and are removed to Columns after as_of_date represent future data and are removed to
prevent look-ahead bias. prevent look-ahead bias.
""" """
if not curr_date or data.empty: if not as_of_date or data.empty:
return data return data
cutoff = pd.Timestamp(curr_date) cutoff = pd.Timestamp(as_of_date)
mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff
return data.loc[:, mask] return data.loc[:, mask]
+22 -22
View File
@@ -73,7 +73,7 @@ def get_YFin_data_online(
def get_stock_stats_indicators_window( def get_stock_stats_indicators_window(
symbol: Annotated[str, "ticker symbol of the company"], symbol: Annotated[str, "ticker symbol of the company"],
indicator: Annotated[str, "technical indicator to get the analysis and report of"], indicator: Annotated[str, "technical indicator to get the analysis and report of"],
curr_date: Annotated[ as_of_date: Annotated[
str, "The current trading date you are trading on, YYYY-mm-dd" str, "The current trading date you are trading on, YYYY-mm-dd"
], ],
look_back_days: Annotated[int, "how many days to look back"], look_back_days: Annotated[int, "how many days to look back"],
@@ -157,16 +157,16 @@ def get_stock_stats_indicators_window(
f"Indicator {indicator} is not supported. Please choose from: {list(best_ind_params.keys())}" f"Indicator {indicator} is not supported. Please choose from: {list(best_ind_params.keys())}"
) )
end_date = curr_date end_date = as_of_date
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d") as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
before = curr_date_dt - relativedelta(days=look_back_days) before = as_of_dt - relativedelta(days=look_back_days)
# Optimized: Get stock data once and calculate indicators for all dates # Optimized: Get stock data once and calculate indicators for all dates
try: try:
indicator_data = _get_stock_stats_bulk(symbol, indicator, curr_date) indicator_data = _get_stock_stats_bulk(symbol, indicator, as_of_date)
# Generate the date range we need # Generate the date range we need
current_dt = curr_date_dt current_dt = as_of_dt
date_values = [] date_values = []
while current_dt >= before: while current_dt >= before:
@@ -191,13 +191,13 @@ def get_stock_stats_indicators_window(
logger.warning("Bulk stockstats fetch failed, falling back per-day: %s", e) logger.warning("Bulk stockstats fetch failed, falling back per-day: %s", e)
# Fallback to original implementation if bulk method fails # Fallback to original implementation if bulk method fails
ind_string = "" ind_string = ""
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d") as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
while curr_date_dt >= before: while as_of_dt >= before:
indicator_value = get_stockstats_indicator( indicator_value = get_stockstats_indicator(
symbol, indicator, curr_date_dt.strftime("%Y-%m-%d") symbol, indicator, as_of_dt.strftime("%Y-%m-%d")
) )
ind_string += f"{curr_date_dt.strftime('%Y-%m-%d')}: {indicator_value}\n" ind_string += f"{as_of_dt.strftime('%Y-%m-%d')}: {indicator_value}\n"
curr_date_dt = curr_date_dt - relativedelta(days=1) as_of_dt = as_of_dt - relativedelta(days=1)
result_str = ( result_str = (
f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n" f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n"
@@ -212,7 +212,7 @@ def get_stock_stats_indicators_window(
def _get_stock_stats_bulk( def _get_stock_stats_bulk(
symbol: Annotated[str, "ticker symbol of the company"], symbol: Annotated[str, "ticker symbol of the company"],
indicator: Annotated[str, "technical indicator to calculate"], indicator: Annotated[str, "technical indicator to calculate"],
curr_date: Annotated[str, "current date for reference"] as_of_date: Annotated[str, "current date for reference"]
) -> dict: ) -> dict:
""" """
Optimized bulk calculation of stock stats indicators. Optimized bulk calculation of stock stats indicators.
@@ -221,7 +221,7 @@ def _get_stock_stats_bulk(
""" """
from stockstats import wrap from stockstats import wrap
data = load_ohlcv(symbol, curr_date) data = load_ohlcv(symbol, as_of_date)
df = wrap(data) df = wrap(data)
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d") df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
@@ -243,19 +243,19 @@ def _get_stock_stats_bulk(
def get_stockstats_indicator( def get_stockstats_indicator(
symbol: Annotated[str, "ticker symbol of the company"], symbol: Annotated[str, "ticker symbol of the company"],
indicator: Annotated[str, "technical indicator to get the analysis and report of"], indicator: Annotated[str, "technical indicator to get the analysis and report of"],
curr_date: Annotated[ as_of_date: Annotated[
str, "The current trading date you are trading on, YYYY-mm-dd" str, "The current trading date you are trading on, YYYY-mm-dd"
], ],
) -> str: ) -> str:
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d") as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
curr_date = curr_date_dt.strftime("%Y-%m-%d") as_of_date = as_of_dt.strftime("%Y-%m-%d")
try: try:
indicator_value = get_stock_stats( indicator_value = get_stock_stats(
symbol, symbol,
indicator, indicator,
curr_date, as_of_date,
) )
except VendorError: except VendorError:
raise # Unknown/delisted symbol — let the router emit the sentinel raise # Unknown/delisted symbol — let the router emit the sentinel
@@ -264,7 +264,7 @@ def get_stockstats_indicator(
# reads as no value that day rather than a read that failed. Raise so the # reads as no value that day rather than a read that failed. Raise so the
# router can try the next vendor or report the series unavailable. # router can try the next vendor or report the series unavailable.
raise NoMarketDataError( raise NoMarketDataError(
symbol, symbol, f"{indicator} could not be read for {curr_date}: {e}" symbol, symbol, f"{indicator} could not be read for {as_of_date}: {e}"
) from e ) from e
return str(indicator_value) return str(indicator_value)
@@ -285,17 +285,17 @@ def get_stock_stats(
indicator: Annotated[ indicator: Annotated[
str, "quantitative indicators based off of the stock data for the company" str, "quantitative indicators based off of the stock data for the company"
], ],
curr_date: Annotated[ as_of_date: Annotated[
str, "curr date for retrieving stock price data, YYYY-mm-dd" str, "curr date for retrieving stock price data, YYYY-mm-dd"
], ],
): ):
data = load_ohlcv(symbol, curr_date) data = load_ohlcv(symbol, as_of_date)
df = wrap(data) df = wrap(data)
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d") df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
curr_date_str = pd.to_datetime(curr_date).strftime("%Y-%m-%d") as_of_str = pd.to_datetime(as_of_date).strftime("%Y-%m-%d")
df[indicator] # trigger stockstats to calculate the indicator df[indicator] # trigger stockstats to calculate the indicator
matching_rows = df[df["Date"].str.startswith(curr_date_str)] matching_rows = df[df["Date"].str.startswith(as_of_str)]
if not matching_rows.empty: if not matching_rows.empty:
indicator_value = matching_rows[indicator].values[0] indicator_value = matching_rows[indicator].values[0]
+6 -6
View File
@@ -121,7 +121,7 @@ def get_news_yfinance(
def get_global_news_yfinance( def get_global_news_yfinance(
curr_date: str, as_of_date: str,
look_back_days: int | None = None, look_back_days: int | None = None,
limit: int | None = None, limit: int | None = None,
) -> str: ) -> str:
@@ -129,7 +129,7 @@ def get_global_news_yfinance(
Retrieve global/macro economic news using yfinance Search. Retrieve global/macro economic news using yfinance Search.
Args: Args:
curr_date: Current date in yyyy-mm-dd format as_of_date: Current date in yyyy-mm-dd format
look_back_days: Number of days to look back. ``None`` falls back to look_back_days: Number of days to look back. ``None`` falls back to
``global_news_lookback_days`` from the active config. ``global_news_lookback_days`` from the active config.
limit: Maximum number of articles to return. ``None`` falls back to limit: Maximum number of articles to return. ``None`` falls back to
@@ -145,7 +145,7 @@ def get_global_news_yfinance(
limit = config["global_news_article_limit"] limit = config["global_news_article_limit"]
search_queries = config["global_news_queries"] search_queries = config["global_news_queries"]
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d") curr_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
start_dt = curr_dt - relativedelta(days=look_back_days) start_dt = curr_dt - relativedelta(days=look_back_days)
start_date = start_dt.strftime("%Y-%m-%d") start_date = start_dt.strftime("%Y-%m-%d")
@@ -189,10 +189,10 @@ def get_global_news_yfinance(
if not news_str: if not news_str:
# Results merge several fuzzy searches, so their timestamps prove no # Results merge several fuzzy searches, so their timestamps prove no
# continuous coverage; judge the window against the present only. # continuous coverage; judge the window against the present only.
gap = coverage_gap((), start_date, curr_date, "Yahoo Finance global news", "market news") gap = coverage_gap((), start_date, as_of_date, "Yahoo Finance global news", "market news")
return gap or f"No global news found between {start_date} and {curr_date}" return gap or f"No global news found between {start_date} and {as_of_date}"
return f"## Global Market News, from {start_date} to {curr_date}:\n\n{news_str}" return f"## Global Market News, from {start_date} to {as_of_date}:\n\n{news_str}"
except VendorError: except VendorError:
raise raise
+16 -16
View File
@@ -93,7 +93,7 @@ def _local_midnight(value) -> pd.Timestamp:
def _normalize_dates(dates) -> pd.Series: def _normalize_dates(dates) -> pd.Series:
"""Parse to naive, midnight-normalized dates so tz-aware or intraday """Parse to naive, midnight-normalized dates so tz-aware or intraday
timestamps compare correctly against the naive ``curr_date`` cutoff (#1201). timestamps compare correctly against the naive ``as_of_date`` cutoff (#1201).
Normalized per element: 5 years of yfinance bars span daylight-saving Normalized per element: 5 years of yfinance bars span daylight-saving
changes (and cache CSVs round-trip the offsets as strings), so the series can changes (and cache CSVs round-trip the offsets as strings), so the series can
@@ -147,13 +147,13 @@ def _coerce_ohlcv_dates(data: pd.DataFrame) -> pd.Series:
def _assert_ohlcv_not_stale( def _assert_ohlcv_not_stale(
data: pd.DataFrame, data: pd.DataFrame,
curr_date: str, as_of_date: str,
symbol: str, symbol: str,
canonical: str | None = None, canonical: str | None = None,
*, *,
max_stale_days: int = MAX_OHLCV_STALE_DAYS, max_stale_days: int = MAX_OHLCV_STALE_DAYS,
) -> None: ) -> None:
"""Reject OHLCV whose latest row is far older than curr_date. """Reject OHLCV whose latest row is far older than as_of_date.
Raises NoMarketDataError (with a stale-specific detail) so the router treats Raises NoMarketDataError (with a stale-specific detail) so the router treats
it like any other "no usable data from this vendor" — try the next vendor, it like any other "no usable data from this vendor" — try the next vendor,
@@ -164,7 +164,7 @@ def _assert_ohlcv_not_stale(
""" """
if data is None or data.empty: if data is None or data.empty:
return return
requested = pd.to_datetime(curr_date, errors="coerce") requested = pd.to_datetime(as_of_date, errors="coerce")
if pd.isna(requested): if pd.isna(requested):
return return
requested = requested.normalize() requested = requested.normalize()
@@ -182,7 +182,7 @@ def _assert_ohlcv_not_stale(
) )
def _cache_is_fresh(data_file, curr_date_dt, now) -> bool: def _cache_is_fresh(data_file, as_of_dt, now) -> bool:
"""Whether the symbol's cached download can serve this request. """Whether the symbol's cached download can serve this request.
The file holds the download made on the day it was written, so it serves The file holds the download made on the day it was written, so it serves
@@ -194,14 +194,14 @@ def _cache_is_fresh(data_file, curr_date_dt, now) -> bool:
written = pd.Timestamp.fromtimestamp(os.path.getmtime(data_file)) written = pd.Timestamp.fromtimestamp(os.path.getmtime(data_file))
if written.date() != now.date(): if written.date() != now.date():
return False return False
return curr_date_dt.date() < now.date() or (now - written).total_seconds() <= OHLCV_CACHE_TTL_SECONDS return as_of_dt.date() < now.date() or (now - written).total_seconds() <= OHLCV_CACHE_TTL_SECONDS
def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFrame: def load_ohlcv(symbol: str, as_of_date: str, fill_gaps: bool = True) -> pd.DataFrame:
"""Fetch OHLCV data with caching, filtered to prevent look-ahead bias. """Fetch OHLCV data with caching, filtered to prevent look-ahead bias.
Downloads 5 years of data up to today and caches per symbol. On Downloads 5 years of data up to today and caches per symbol. On
subsequent calls the cache is reused. Rows after curr_date are subsequent calls the cache is reused. Rows after as_of_date are
filtered out so backtests never see future prices. filtered out so backtests never see future prices.
``fill_gaps`` carries prices forward over gaps so indicators compute on a ``fill_gaps`` carries prices forward over gaps so indicators compute on a
@@ -215,15 +215,15 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
safe_symbol = safe_ticker_component(canonical) safe_symbol = safe_ticker_component(canonical)
config = get_config() config = get_config()
curr_date_dt = pd.to_datetime(curr_date).normalize() as_of_dt = pd.to_datetime(as_of_date).normalize()
# One cache file per symbol, holding the latest 5y-to-today download. # One cache file per symbol, holding the latest 5y-to-today download.
now = pd.Timestamp.today() now = pd.Timestamp.today()
start_date = now - pd.DateOffset(years=5) start_date = now - pd.DateOffset(years=5)
start_str = start_date.strftime("%Y-%m-%d") start_str = start_date.strftime("%Y-%m-%d")
# yfinance ``end`` is EXCLUSIVE; request tomorrow so today's row is included # yfinance ``end`` is EXCLUSIVE; request tomorrow so today's row is included
# when curr_date is the current day (#986). Look-ahead is still prevented by # when as_of_date is the current day (#986). Look-ahead is still prevented by
# the curr_date filter below. # the as_of_date filter below.
end_str = (now + pd.Timedelta(days=1)).strftime("%Y-%m-%d") end_str = (now + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
os.makedirs(config["data_cache_dir"], exist_ok=True) os.makedirs(config["data_cache_dir"], exist_ok=True)
@@ -241,7 +241,7 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
if ( if (
not cached.empty not cached.empty
and "Close" in cached.columns and "Close" in cached.columns
and _cache_is_fresh(data_file, curr_date_dt, now) and _cache_is_fresh(data_file, as_of_dt, now)
): ):
data = cached data = cached
@@ -271,8 +271,8 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
data = _clean_dataframe(data) data = _clean_dataframe(data)
# Filter to curr_date to prevent look-ahead bias in backtesting. # Filter to as_of_date to prevent look-ahead bias in backtesting.
data = data[data["Date"] <= curr_date_dt] data = data[data["Date"] <= as_of_dt]
# A closeless newest bar is an unsettled session, not a symbol without data. # A closeless newest bar is an unsettled session, not a symbol without data.
# _fill_price_gaps below drops it, here and mid-series alike, so the frame # _fill_price_gaps below drops it, here and mid-series alike, so the frame
@@ -295,9 +295,9 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
# a filled cell is the previous session's price under this session's date. # a filled cell is the previous session's price under this session's date.
data = _fill_price_gaps(data) if fill_gaps else data.dropna(subset=["Close"]).copy() data = _fill_price_gaps(data) if fill_gaps else data.dropna(subset=["Close"]).copy()
# Reject a stale frame (latest row far older than curr_date) rather than # Reject a stale frame (latest row far older than as_of_date) rather than
# feeding year-old prices into indicators (#1021). # feeding year-old prices into indicators (#1021).
_assert_ohlcv_not_stale(data, curr_date, symbol, canonical) _assert_ohlcv_not_stale(data, as_of_date, symbol, canonical)
return data return data
+8 -8
View File
@@ -25,8 +25,8 @@ DEFAULT_SNAPSHOT_INDICATORS: tuple[str, ...] = (
) )
def _verified_rows(symbol: str, curr_date: str) -> pd.DataFrame: def _verified_rows(symbol: str, as_of_date: str) -> pd.DataFrame:
"""OHLCV on or before curr_date, date-sorted. Raises if nothing usable. """OHLCV on or before as_of_date, date-sorted. Raises if nothing usable.
``load_ohlcv`` already normalizes the Date column and filters out ``load_ohlcv`` already normalizes the Date column and filters out
look-ahead rows, but we re-apply the cutoff defensively — this is a look-ahead rows, but we re-apply the cutoff defensively — this is a
@@ -34,16 +34,16 @@ def _verified_rows(symbol: str, curr_date: str) -> pd.DataFrame:
""" """
# As reported: this snapshot is quoted by the agents as exact prices, so a # As reported: this snapshot is quoted by the agents as exact prices, so a
# gap-filled cell would put the previous session's number under this date. # gap-filled cell would put the previous session's number under this date.
data = load_ohlcv(symbol, curr_date, fill_gaps=False) data = load_ohlcv(symbol, as_of_date, fill_gaps=False)
if data is None or data.empty: if data is None or data.empty:
raise ValueError(f"No OHLCV data available for {symbol}.") raise ValueError(f"No OHLCV data available for {symbol}.")
df = data.copy() df = data.copy()
df["Date"] = pd.to_datetime(df["Date"], errors="coerce") df["Date"] = pd.to_datetime(df["Date"], errors="coerce")
df = df.dropna(subset=["Date"]) df = df.dropna(subset=["Date"])
df = df[df["Date"] <= pd.to_datetime(curr_date)].sort_values("Date") df = df[df["Date"] <= pd.to_datetime(as_of_date)].sort_values("Date")
if df.empty: if df.empty:
raise ValueError(f"No OHLCV rows on or before {curr_date} for {symbol}.") raise ValueError(f"No OHLCV rows on or before {as_of_date} for {symbol}.")
return df return df
@@ -63,7 +63,7 @@ def _fmt(value) -> str:
def build_verified_market_snapshot( def build_verified_market_snapshot(
symbol: str, symbol: str,
curr_date: str, as_of_date: str,
look_back_days: int = 30, look_back_days: int = 30,
indicators: Iterable[str] | None = None, indicators: Iterable[str] | None = None,
) -> str: ) -> str:
@@ -71,7 +71,7 @@ def build_verified_market_snapshot(
# `df` keeps the original capitalized OHLCV columns (Open/High/Low/Close/ # `df` keeps the original capitalized OHLCV columns (Open/High/Low/Close/
# Volume); stockstats `wrap()` lowercases columns and adds indicator # Volume); stockstats `wrap()` lowercases columns and adds indicator
# columns, so read raw prices from `df` and indicators from `stock_df`. # columns, so read raw prices from `df` and indicators from `stock_df`.
df = _verified_rows(symbol, curr_date) df = _verified_rows(symbol, as_of_date)
stock_df = wrap(df.copy()) stock_df = wrap(df.copy())
selected = tuple(indicators or DEFAULT_SNAPSHOT_INDICATORS) selected = tuple(indicators or DEFAULT_SNAPSHOT_INDICATORS)
@@ -91,7 +91,7 @@ def build_verified_market_snapshot(
lines = [ lines = [
f"## Verified market data snapshot for {symbol.upper()}", f"## Verified market data snapshot for {symbol.upper()}",
"", "",
f"- Requested analysis date: {curr_date}", f"- Requested analysis date: {as_of_date}",
f"- Latest trading row used: {latest_date}", f"- Latest trading row used: {latest_date}",
"- Rows after the requested analysis date are excluded before verification.", "- Rows after the requested analysis date are excluded before verification.",
"", "",
+2 -2
View File
@@ -114,7 +114,7 @@ class TradingAgentsGraph:
self._resuming = False self._resuming = False
def resolve_instrument_context(self, ticker: str, asset_type: str = "stock", def resolve_instrument_context(self, ticker: str, asset_type: str = "stock",
curr_date: str | None = None) -> str: trade_date: str | None = None) -> str:
"""Resolve ticker identity once and return the full instrument context. """Resolve ticker identity once and return the full instrument context.
Deterministic yfinance lookup (cached, fail-open) injected into a Deterministic yfinance lookup (cached, fail-open) injected into a
@@ -124,7 +124,7 @@ class TradingAgentsGraph:
graph regardless of entry point. graph regardless of entry point.
""" """
identity = resolve_instrument_identity(ticker) identity = resolve_instrument_identity(ticker)
return build_instrument_context(ticker, asset_type, identity, curr_date) return build_instrument_context(ticker, asset_type, identity, trade_date)
def _memory_as_of(self, trade_date) -> str | None: def _memory_as_of(self, trade_date) -> str | None:
"""Point-in-time cutoff for past-context lessons (#1251). """Point-in-time cutoff for past-context lessons (#1251).