[Feature] Add FRED Release Calendar to economy.calendar Endpoint (#7366)

* add fred release calendar to economy.calendar endpoint

* fix deribit test fixture
This commit is contained in:
Danglewood
2026-03-03 08:44:51 -08:00
committed by GitHub
parent 616c54d79e
commit a9edacd3aa
7 changed files with 454 additions and 0 deletions

View File

@@ -52,6 +52,14 @@ def headers():
"end_date": "2023-11-03",
}
),
(
{
"provider": "fred",
"start_date": "2025-07-01",
"end_date": "2025-07-02",
"release_id": None,
}
),
],
)
@pytest.mark.integration

View File

@@ -46,6 +46,14 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
"end_date": "2023-11-03",
}
),
(
{
"provider": "fred",
"start_date": "2025-07-01",
"end_date": "2025-07-02",
"release_id": None,
}
),
],
)
@pytest.mark.integration

View File

@@ -14,6 +14,7 @@ from openbb_deribit.models.options_chains import (
DeribitOptionsChainsData,
DeribitOptionsChainsFetcher,
)
from openbb_deribit.utils.helpers import get_instruments
test_credentials = UserService().default_user_settings.credentials.model_dump(
mode="json"
@@ -62,6 +63,22 @@ MOCK_OPTIONS_DATA = DeribitOptionsChainsData.model_validate(
)
@pytest.fixture(autouse=True)
def _disable_alru_cache_loop_check(monkeypatch):
"""Disable the alru_cache event loop safety check during tests.
async-lru >= 2.2.0 tracks the event loop and raises RuntimeError
when the cache is reused across different event loops. During tests,
multiple event loops are created within a single test (e.g. validators
calling run_async inside transform_query, then fetcher.test calling
run_async for extract_data), so we patch out the loop check entirely.
"""
from async_lru import _LRUCacheWrapper
monkeypatch.setattr(_LRUCacheWrapper, "_check_loop", lambda self, loop: None)
get_instruments.cache_clear()
@pytest.fixture(scope="module")
def vcr_config():
"""VCR configuration."""

View File

@@ -11,6 +11,7 @@ from openbb_fred.models.dwpcr_rates import FREDDiscountWindowPrimaryCreditRateFe
from openbb_fred.models.ecb_interest_rates import (
FREDEuropeanCentralBankInterestRatesFetcher,
)
from openbb_fred.models.economic_calendar import FredEconomicCalendarFetcher
from openbb_fred.models.euro_short_term_rate import FredEuroShortTermRateFetcher
from openbb_fred.models.fed_projections import FREDPROJECTIONFetcher
from openbb_fred.models.federal_funds_rate import FredFederalFundsRateFetcher
@@ -75,6 +76,7 @@ Research division of the Federal Reserve Bank of St. Louis that has more than
"PROJECTIONS": FREDPROJECTIONFetcher,
"IORB": FREDIORBFetcher,
"DiscountWindowPrimaryCreditRate": FREDDiscountWindowPrimaryCreditRateFetcher,
"EconomicCalendar": FredEconomicCalendarFetcher,
"EuropeanCentralBankInterestRates": FREDEuropeanCentralBankInterestRatesFetcher,
"ManufacturingOutlookNY": FredManufacturingOutlookNYFetcher,
"ManufacturingOutlookTexas": FredManufacturingOutlookTexasFetcher,

View File

@@ -0,0 +1,241 @@
"""FRED Economic Calendar Model."""
# pylint: disable=unused-argument
from typing import Any
from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.economic_calendar import (
EconomicCalendarData,
EconomicCalendarQueryParams,
)
from pydantic import Field
class FredEconomicCalendarQueryParams(EconomicCalendarQueryParams):
"""FRED Economic Calendar Query.
Source: https://fred.stlouisfed.org/releases/calendar
"""
release_id: int | None = Field(default=None, description="Filter by release ID.")
class FredEconomicCalendarData(EconomicCalendarData):
"""FRED Economic Calendar Data."""
release_id: int | None = Field(
default=None, description="Release ID associated with the economic event."
)
class FredEconomicCalendarFetcher(
Fetcher[FredEconomicCalendarQueryParams, list[FredEconomicCalendarData]]
):
"""FRED Economic Calendar Fetcher."""
require_credentials = False
@staticmethod
def transform_query(params: dict[str, Any]) -> FredEconomicCalendarQueryParams:
"""Transform query parameters."""
return FredEconomicCalendarQueryParams(**params)
@staticmethod
async def aextract_data(
query: FredEconomicCalendarQueryParams,
credentials: dict[str, Any] | None = None,
**kwargs: Any,
) -> list:
"""Extract data from the source."""
# pylint: disable=import-outside-toplevel
import asyncio
import json
from datetime import datetime, timedelta
from io import StringIO
from math import ceil
from openbb_core.provider.utils.helpers import amake_request
from pandas import (
DataFrame,
concat,
date_range as pd_date_range,
read_html,
)
def process_pager(pager: str, date: str = "") -> DataFrame:
"""Process a page of events HTML into a DataFrame."""
buf = StringIO(pager)
df = read_html(buf, extract_links="all")[0]
df.columns = ["date", "event"]
df["date"] = df.date.apply(lambda x: x[0].replace("N/A", "") or None)
# Extract release_id from link; rows without a link are date-header rows.
df["release_id"] = df.event.apply(
lambda x: x[1].split("rid=")[-1] if x[1] else None
)
df["event"] = df.event.apply(lambda x: x[0])
is_header = df["release_id"].isna()
if is_header.any():
# Header rows carry full date strings like
# "Wednesday February 05, 2025 Updated".
# Event rows carry times like "7:30 AM ET".
# Clean header dates.
df.loc[is_header, "date"] = (
df.loc[is_header, "date"]
.str.replace(r"\s*(Updated|Revised)\s*$", "", regex=True)
.str.strip()
)
# Forward-fill header dates so each event row knows its date.
df["_event_date"] = (
df["date"]
.where(is_header)
.ffill()
.astype("datetime64[ns]")
.dt.strftime("%Y-%m-%d")
)
# Forward-fill times among event rows (handles N/A time rows).
df["_time"] = df["date"].where(~is_header).ffill().fillna("")
# Build full datetime: "YYYY-MM-DD HH:MM AM ET"
df.loc[~is_header, "date"] = (
df.loc[~is_header, "_event_date"]
+ " "
+ df.loc[~is_header, "_time"]
).str.strip()
# Drop header rows and temp columns.
df = (
df[~is_header]
.drop(columns=["_event_date", "_time"])
.reset_index(drop=True)
)
else:
# Fallback: all rows are events with time values (no headers present).
df["date"] = df["date"].ffill()
if date:
df["date"] = df["date"].apply(lambda x: f"{date} {x}")
df["date"] = (
df["date"].astype("datetime64[ns]").dt.tz_localize("US/Central")
)
return df
async def response_callback(response, _):
"""Process the response text."""
text = await response.text()
return json.loads(text)
async def fetch_date_pages(
start_date: str,
end_date: str = "",
rid: str = "",
) -> DataFrame:
"""Fetch all pages of the FRED releases calendar for a single date or date range."""
end_date = end_date or start_date
base_url = (
"https://fred.stlouisfed.org/releases/calendar"
f"?po=1&ptic=0&vs={start_date}&ve={end_date}&rid={rid}"
)
first_page = await amake_request(
base_url, response_callback=response_callback
)
n_results = first_page.get("ptic", 0) # type: ignore
if n_results == 0:
return DataFrame()
records = [process_pager(first_page["pager"], date=start_date)] # type: ignore
if len(records[0]) >= n_results:
return records[0]
# Build URLs for remaining pages (50 results per page).
total_pages = ceil(n_results / 50)
remaining_urls = [
f"{base_url}&pageID={page}" for page in range(2, total_pages + 1)
]
async def _fetch_page(url: str):
return await amake_request(url, response_callback=response_callback)
responses = await asyncio.gather(*[_fetch_page(u) for u in remaining_urls])
for resp in responses:
records.append(process_pager(resp["pager"], date=start_date)) # type: ignore
result = concat(records, ignore_index=True)
return result
async def fetch_all_pages(
start_date: str,
end_date: str = "",
rid: str = "",
) -> DataFrame:
"""Fetch all pages of the FRED releases calendar for each date in a range.
If a release ID is supplied, the entire date range is fetched in a single
paginated call. Otherwise, each date is fetched individually to ensure
complete results.
"""
end_date = end_date or start_date
if rid:
# A specific release ID can be fetched across the full range at once.
return await fetch_date_pages(start_date, end_date, rid)
# Without a release ID, fetch each date individually.
dates = [
d.strftime("%Y-%m-%d") for d in pd_date_range(start_date, end_date)
]
results = await asyncio.gather(*[fetch_date_pages(d) for d in dates])
frames = [r for r in results if not r.empty]
if not frames:
return DataFrame()
return concat(frames, ignore_index=True).sort_values(by="date")
start_date = (
query.start_date.isoformat()
if query.start_date
else (datetime.today() - timedelta(days=1)).isoformat()
)
end_date = (
query.end_date.isoformat()
if query.end_date
else (
(query.start_date + timedelta(days=7)).isoformat()
if query.start_date
else (datetime.today() + timedelta(days=1)).isoformat()
)
)
rid = str(query.release_id) if query.release_id else ""
try:
all_results = await fetch_all_pages(start_date, end_date, rid)
except Exception as e:
raise OpenBBError(e) from e
if all_results.empty:
raise OpenBBError(
"The requested returned no data. Check the parameters and try again."
)
return all_results.to_dict("records")
@staticmethod
def transform_data(
query: FredEconomicCalendarQueryParams,
data: list,
**kwargs: Any,
) -> list[FredEconomicCalendarData]:
"""Transform and validate the response."""
return [FredEconomicCalendarData.model_validate(d) for d in data]

View File

@@ -0,0 +1,164 @@
interactions:
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
method: GET
uri: https://fred.stlouisfed.org/releases/calendar?po=1&ptic=0&ve=2025-07-02&vs=2025-07-02
response:
body:
string: !!binary |
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
eWi+0b7ytoLJvV/pNE7/+hslGg8l7TcAAA==
headers:
Alt-Svc:
- h3=":443"; ma=93600
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Length:
- '1507'
Content-Type:
- text/html; charset=UTF-8
Date:
- Fri, 20 Feb 2026 20:04:11 GMT
Expires:
- Fri, 20 Feb 2026 20:14:11 GMT
Server:
- Apache
Set-Cookie:
- _abck=6730888CA4F2D60FE2DF36FD689B92A2~-1~YAAQC3k1F/kZdVOcAQAACBmnfA8QKYd3F187f3Y6ygaiVVXuSBn+r/qUGGNomnpxh9CEGjFgqSSjAdgW8gbibOaazp2wWwL0EVU1NNnbnTu6+vNN9uEO0QQ123Cys3FDlANTOldMaYu9NyZr5LdSEqb7GIzx/5r3HKFWwyZOcjMe5MgOC+AJnxeMgywLQszQpCuKWQLMQHF0T4p7atCL3aw+F71N/qNBZ4j0yOgvWX/GFFMAUqd3WhRJV3lW4DpJQR4fGdJsQqiTgmXkHtbBi2GU0SMQYAE4v9N5EEccFuITZ3VVdikJ19252Q7B+xd8Ak6FvQxN5B5NinxuD5mssDX5Hvccta8myFkZlvyVbuu2ECFfPem47F72Ie5FPIje3zajyDWvH6TfcLiTP68UHXHxPugrpUi6629R/+XLzTImws89PXeYuUaYxfkRhx+oKN4WFuItARveP4gw71uWjDi5qRv3+Dxs~-1~-1~-1~-1~-1;
Domain=.stlouisfed.org; Path=/; Expires=Sat, 20 Feb 2027 20:04:11 GMT; Max-Age=31536000;
Secure
- bm_sz=322DD331BDB174348E9B9E045652AA65~YAAQC3k1F/oZdVOcAQAACRmnfB52oqemJpaP4OaHY9774TKChrpsRHSy4NbSZomUqNd150qIyJJS/xQLIKFya0jwYULGSQ4IVx7q7BKY+wlgghHj8mr9oRZBv2nyPQnwyMT3NIUdQFzv8+ye7nLvA51ePBnwecX1t4xvVJd72ekhwCIu3AdOAgNUvkq/MSzWIwOZJapUCEsrzsdveYh7rxKYJdczHLdVcaydBsnC16sas/zGteG1wjq0A/THzUkopxgiR7MLlzPcl7RoPTVOqupVwgq+O7vNmBmhpV7YhTiMHn0w8PfHuy9wHx2hyNesOIVu1MTzfrJQnJyGoLDb8RsAPDWQSvwKUOB/s32+bZw=~4535351~3748406;
Domain=.stlouisfed.org; Path=/; Expires=Sat, 21 Feb 2026 00:04:09 GMT; Max-Age=14398
Strict-Transport-Security:
- max-age=86400
Vary:
- Accept-Encoding
X-Akamai-Transformed:
- 0 - 0 -
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
method: GET
uri: https://fred.stlouisfed.org/releases/calendar?po=1&ptic=0&ve=2025-07-01&vs=2025-07-01
response:
body:
string: !!binary |
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
vvSmFFsRlFqN+r//AUF532vPQQAA
headers:
Alt-Svc:
- h3=":443"; ma=93600
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Length:
- '1731'
Content-Type:
- text/html; charset=UTF-8
Date:
- Fri, 20 Feb 2026 20:04:13 GMT
Expires:
- Fri, 20 Feb 2026 20:14:13 GMT
Server:
- Apache
Set-Cookie:
- _abck=C80A0F4E8F224714B5CC71B6D6C92BF2~-1~YAAQC3k1FxAddVOcAQAAhCCnfA9G9T6rDQXkzLeG5Ux6XaQBQgohJbicjRSAPSDkXOsKQqRZEHP87xN9Ty573pxSpJMnFkwrtwkrSsLaeiS7L0glW8h1li0lFdLL17cTV7FCmrZ0BWp9ZJbi/rp88fuUcDjgc0lATMWaIGi8tFUkLb8zVWJj4JA2nE8ZLCN9ngclaIOMaAn87fC4AWSyYUoWpB/e8M+Fs6aYB6kN78OnxwGjovQpsGsGdVvKBMEOX5tsQJKNWsK05YBt/lPoqcgWJFVQFEi+24avdyFTASUXkiLYtELFAEnZnc+n2QweUvimJf/nOKF6wndeZt00NWswIhb8ThX5CKY6ffdoJyHkswTyyUHYBr3yujYzh0JXLOYLEB5JzhDiTJVi9ed+BarVPku7wctx31zX4hkddcjYg9Gd/AkxAXOS/R0+uea+tSd2IwxA7J7wPdYb5m/Vmx3tijY=~-1~-1~-1~-1~-1;
Domain=.stlouisfed.org; Path=/; Expires=Sat, 20 Feb 2027 20:04:13 GMT; Max-Age=31536000;
Secure
- bm_sz=2CEF56C32C2AE52C8000EDAD1E963D27~YAAQC3k1FxIddVOcAQAAhCCnfB6hYdgVhSx/71YWQpVcJnG5KqKjrmzHe1kL+6Zf7P8Vv+FOTZ2VUXOnTq8z0XsTTOLG/JlAqT8MRqwYgALCHKkL+PCbSyCU65dbEU/i68Vu4hSSW4Pz8sfVk7HBUsnamPrTIQSj4nHg3jp40opBvfqAxBNSmvxV8vHOPQILMPt6G93/sSC7lJxwKIvTVopkmn9hJuugdpXpigkGy2vO44acJiBJfqqQn3Wyxx0TKUOCTpU7LVE3CVW92LLAn0TIB9jXrKXL5jPfeukoLncbCMugreR3xKlwx2ojt70Lv8vHs13ZkJDUABrKesLsl8nr5l9hg90nVPEkVibSAg==~4535351~3748406;
Domain=.stlouisfed.org; Path=/; Expires=Sat, 21 Feb 2026 00:04:09 GMT; Max-Age=14396
Strict-Transport-Security:
- max-age=86400
Vary:
- Accept-Encoding
X-Akamai-Transformed:
- 0 - 0 -
X-Content-Type-Options:
- nosniff
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -14,6 +14,7 @@ from openbb_fred.models.dwpcr_rates import FREDDiscountWindowPrimaryCreditRateFe
from openbb_fred.models.ecb_interest_rates import (
FREDEuropeanCentralBankInterestRatesFetcher,
)
from openbb_fred.models.economic_calendar import FredEconomicCalendarFetcher
from openbb_fred.models.euro_short_term_rate import FredEuroShortTermRateFetcher
from openbb_fred.models.fed_projections import FREDPROJECTIONFetcher
from openbb_fred.models.federal_funds_rate import FredFederalFundsRateFetcher
@@ -529,3 +530,16 @@ def test_fred_manufacturing_outlook_ny_fetcher(credentials=test_credentials):
fetcher = FredManufacturingOutlookNYFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_fred_economic_calendar_fetcher(credentials=test_credentials):
"""Test FRED Economic Calendar Fetcher."""
params = {
"start_date": datetime.date(2025, 7, 1),
"end_date": datetime.date(2025, 7, 2),
}
fetcher = FredEconomicCalendarFetcher()
result = fetcher.test(params, credentials)
assert result is None