From 8b7ece8a3e04550ea84e99210eea070d65918c65 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Sun, 30 Aug 2026 06:18:59 +0000 Subject: [PATCH] fix(dataflows): pin the FRED data vintage to the as-of date - FRED defaults both realtime bounds to today, so historical macro requests served the latest revision and leaked future information into backtests - set realtime_start=realtime_end=curr_date on both the metadata and observations requests #1275 --- tests/test_fred.py | 17 +++++++++++++++++ tradingagents/dataflows/fred.py | 17 ++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/tests/test_fred.py b/tests/test_fred.py index 367c28feb..2a03f6456 100644 --- a/tests/test_fred.py +++ b/tests/test_fred.py @@ -150,6 +150,23 @@ class FredFormattingTests(unittest.TestCase): self.assertEqual(obs_params["observation_end"], "2025-09-30") self.assertEqual(obs_params["observation_start"], "2025-07-02") # 90d back + def test_requests_pin_the_data_vintage(self): + # #1275: both the metadata and observations requests must set + # realtime_start=realtime_end=curr_date, or FRED serves the latest + # revision and revision-prone series leak future information. + captured = {} + + def _capture(path, params): + captured[path] = params + return _META if path == "series" else _OBS + + with mock.patch.object(fred, "_request", side_effect=_capture): + fred.get_macro_data("cpi", "2025-09-30", 90) + + for path in ("series", "series/observations"): + self.assertEqual(captured[path]["realtime_start"], "2025-09-30", path) + self.assertEqual(captured[path]["realtime_end"], "2025-09-30", path) + @pytest.mark.unit class FredRoutingTests(unittest.TestCase): diff --git a/tradingagents/dataflows/fred.py b/tradingagents/dataflows/fred.py index eb3856e6a..38ab7d340 100644 --- a/tradingagents/dataflows/fred.py +++ b/tradingagents/dataflows/fred.py @@ -143,8 +143,12 @@ def get_macro_data( Args: indicator: A friendly alias (e.g. "cpi", "unemployment", "10y_treasury") or a raw FRED series ID (e.g. "CPIAUCSL", "DGS10"). - curr_date: End of the window (yyyy-mm-dd); no later observations are - returned, so a past date never leaks future data. + curr_date: The as-of date (yyyy-mm-dd). It bounds the observation window + AND pins the data vintage: FRED is queried with + ``realtime_start = realtime_end = curr_date`` so a historical run sees + the values that were actually published by that date, not later + revisions. Without this, revision-prone series (CPI, GDP, ...) would + leak future information into a backtest (#1275). look_back_days: Trailing window length; ``None`` uses DEFAULT_LOOKBACK_DAYS. Returns: @@ -157,6 +161,12 @@ def get_macro_data( end_dt = datetime.strptime(curr_date, "%Y-%m-%d") start_date = (end_dt - timedelta(days=look_back_days)).strftime("%Y-%m-%d") + # Pin the data vintage to curr_date. FRED defaults both realtime bounds to + # today, which serves the LATEST revision of every observation; a single-day + # realtime interval asks for the values known as of curr_date instead. This + # is applied to both the metadata and observations requests (#1275). + realtime = {"realtime_start": curr_date, "realtime_end": curr_date} + # Invalid LLM-supplied indicator: return guidance rather than raising, so a # bad argument doesn't abort the run (the routing layer also degrades macro # data, but a specific message is more useful to the analyst). @@ -165,7 +175,7 @@ def get_macro_data( except ValueError as e: return f"FRED: {e}" - meta = _request("series", {"series_id": series_id}).get("seriess") or [] + meta = _request("series", {"series_id": series_id, **realtime}).get("seriess") or [] if not meta: return ( f"FRED series '{series_id}' not found. Pass a known alias " @@ -184,6 +194,7 @@ def get_macro_data( "observation_start": start_date, "observation_end": curr_date, "sort_order": "asc", + **realtime, }, ).get("observations", [])