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
This commit is contained in:
Yijia-Xiao
2026-08-30 06:18:59 +00:00
parent a33fd4c0f1
commit 8b7ece8a3e
2 changed files with 31 additions and 3 deletions

View File

@@ -150,6 +150,23 @@ class FredFormattingTests(unittest.TestCase):
self.assertEqual(obs_params["observation_end"], "2025-09-30") self.assertEqual(obs_params["observation_end"], "2025-09-30")
self.assertEqual(obs_params["observation_start"], "2025-07-02") # 90d back 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 @pytest.mark.unit
class FredRoutingTests(unittest.TestCase): class FredRoutingTests(unittest.TestCase):

View File

@@ -143,8 +143,12 @@ 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: End of the window (yyyy-mm-dd); no later observations are curr_date: The as-of date (yyyy-mm-dd). It bounds the observation window
returned, so a past date never leaks future data. 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. look_back_days: Trailing window length; ``None`` uses DEFAULT_LOOKBACK_DAYS.
Returns: Returns:
@@ -157,6 +161,12 @@ def get_macro_data(
end_dt = datetime.strptime(curr_date, "%Y-%m-%d") end_dt = datetime.strptime(curr_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 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 # Invalid LLM-supplied indicator: return guidance rather than raising, so a
# bad argument doesn't abort the run (the routing layer also degrades macro # bad argument doesn't abort the run (the routing layer also degrades macro
# data, but a specific message is more useful to the analyst). # data, but a specific message is more useful to the analyst).
@@ -165,7 +175,7 @@ def get_macro_data(
except ValueError as e: except ValueError as e:
return f"FRED: {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: if not meta:
return ( return (
f"FRED series '{series_id}' not found. Pass a known alias " f"FRED series '{series_id}' not found. Pass a known alias "
@@ -184,6 +194,7 @@ def get_macro_data(
"observation_start": start_date, "observation_start": start_date,
"observation_end": curr_date, "observation_end": curr_date,
"sort_order": "asc", "sort_order": "asc",
**realtime,
}, },
).get("observations", []) ).get("observations", [])