From a9edacd3aa77ce92ab587f1f2559c3944fb544d3 Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Tue, 3 Mar 2026 08:44:51 -0800 Subject: [PATCH] [Feature] Add FRED Release Calendar to `economy.calendar` Endpoint (#7366) * add fred release calendar to economy.calendar endpoint * fix deribit test fixture --- .../economy/integration/test_economy_api.py | 8 + .../integration/test_economy_python.py | 8 + .../deribit/tests/test_deribit_fetchers.py | 17 ++ .../providers/fred/openbb_fred/__init__.py | 2 + .../openbb_fred/models/economic_calendar.py | 241 ++++++++++++++++++ ..._economic_calendar_fetcher_urllib3_v2.yaml | 164 ++++++++++++ .../fred/tests/test_fred_fetchers.py | 14 + 7 files changed, 454 insertions(+) create mode 100644 openbb_platform/providers/fred/openbb_fred/models/economic_calendar.py create mode 100644 openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_economic_calendar_fetcher_urllib3_v2.yaml diff --git a/openbb_platform/extensions/economy/integration/test_economy_api.py b/openbb_platform/extensions/economy/integration/test_economy_api.py index 9b7a4c03487..6dd3ffd3a81 100644 --- a/openbb_platform/extensions/economy/integration/test_economy_api.py +++ b/openbb_platform/extensions/economy/integration/test_economy_api.py @@ -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 diff --git a/openbb_platform/extensions/economy/integration/test_economy_python.py b/openbb_platform/extensions/economy/integration/test_economy_python.py index b274137f823..c352bc29c61 100644 --- a/openbb_platform/extensions/economy/integration/test_economy_python.py +++ b/openbb_platform/extensions/economy/integration/test_economy_python.py @@ -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 diff --git a/openbb_platform/providers/deribit/tests/test_deribit_fetchers.py b/openbb_platform/providers/deribit/tests/test_deribit_fetchers.py index ed5508a139f..00e4b3f34c5 100644 --- a/openbb_platform/providers/deribit/tests/test_deribit_fetchers.py +++ b/openbb_platform/providers/deribit/tests/test_deribit_fetchers.py @@ -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.""" diff --git a/openbb_platform/providers/fred/openbb_fred/__init__.py b/openbb_platform/providers/fred/openbb_fred/__init__.py index 3c8a21cc4cf..fef5189ef85 100644 --- a/openbb_platform/providers/fred/openbb_fred/__init__.py +++ b/openbb_platform/providers/fred/openbb_fred/__init__.py @@ -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, diff --git a/openbb_platform/providers/fred/openbb_fred/models/economic_calendar.py b/openbb_platform/providers/fred/openbb_fred/models/economic_calendar.py new file mode 100644 index 00000000000..968174310eb --- /dev/null +++ b/openbb_platform/providers/fred/openbb_fred/models/economic_calendar.py @@ -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] diff --git a/openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_economic_calendar_fetcher_urllib3_v2.yaml b/openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_economic_calendar_fetcher_urllib3_v2.yaml new file mode 100644 index 00000000000..500374790b3 --- /dev/null +++ b/openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_economic_calendar_fetcher_urllib3_v2.yaml @@ -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 diff --git a/openbb_platform/providers/fred/tests/test_fred_fetchers.py b/openbb_platform/providers/fred/tests/test_fred_fetchers.py index 25a07551130..d7c948cae91 100644 --- a/openbb_platform/providers/fred/tests/test_fred_fetchers.py +++ b/openbb_platform/providers/fred/tests/test_fred_fetchers.py @@ -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