From 650912bf0c21714a109ff50ff8ed94651eaf1d63 Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Sun, 16 Jun 2024 14:30:46 -0700 Subject: add mortgage indices (#6501) --- .../provider/standard_models/mortgage_indices.py | 46 +++ .../integration/test_fixedincome_api.py | 26 ++ .../integration/test_fixedincome_python.py | 25 ++ .../openbb_fixedincome/fixedincome_router.py | 26 ++ openbb_platform/openbb/assets/reference.json | 185 ++++++++++++ openbb_platform/openbb/package/fixedincome.py | 127 +++++++++ .../providers/fred/openbb_fred/__init__.py | 2 + .../fred/openbb_fred/models/mortgage_indices.py | 312 +++++++++++++++++++++ .../test_fred_bond_mortgage_fetcher.yaml | 105 +++++++ .../providers/fred/tests/test_fred_fetchers.py | 15 + 10 files changed, 869 insertions(+) create mode 100644 openbb_platform/core/openbb_core/provider/standard_models/mortgage_indices.py create mode 100644 openbb_platform/providers/fred/openbb_fred/models/mortgage_indices.py create mode 100644 openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_bond_mortgage_fetcher.yaml diff --git a/openbb_platform/core/openbb_core/provider/standard_models/mortgage_indices.py b/openbb_platform/core/openbb_core/provider/standard_models/mortgage_indices.py new file mode 100644 index 00000000000..bfba892bb2d --- /dev/null +++ b/openbb_platform/core/openbb_core/provider/standard_models/mortgage_indices.py @@ -0,0 +1,46 @@ +"""Mortgage Indices Standard Model.""" + +from datetime import ( + date as dateType, +) +from typing import Optional + +from pydantic import Field + +from openbb_core.provider.abstract.data import Data +from openbb_core.provider.abstract.query_params import QueryParams +from openbb_core.provider.utils.descriptions import ( + DATA_DESCRIPTIONS, + QUERY_DESCRIPTIONS, +) + + +class MortgageIndicesQueryParams(QueryParams): + """Mortgage Indices Query.""" + + start_date: Optional[dateType] = Field( + default=None, + description=QUERY_DESCRIPTIONS.get("start_date", ""), + ) + end_date: Optional[dateType] = Field( + default=None, + description=QUERY_DESCRIPTIONS.get("end_date", ""), + ) + + +class MortgageIndicesData(Data): + """Mortgage Indices Data.""" + + date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", "")) + symbol: Optional[str] = Field( + default=None, + description=DATA_DESCRIPTIONS.get("symbol", ""), + ) + name: Optional[str] = Field( + default=None, + description="Name of the index.", + ) + rate: float = Field( + description="Mortgage rate.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) diff --git a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py index 1e9844767f3..521040398c2 100644 --- a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py +++ b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_api.py @@ -694,3 +694,29 @@ def test_fixedincome_bond_indices(params, headers): result = requests.get(url, headers=headers, timeout=10) assert isinstance(result, requests.Response) assert result.status_code == 200 + + +@parametrize( + "params", + [ + { + "provider": "fred", + "index": "usda_30y,fha_30y", + "start_date": "2023-05-31", + "end_date": "2024-06-01", + "transform": None, + "frequency": None, + "aggregation_method": "avg", + }, + ], +) +@pytest.mark.integration +def test_fixedincome_mortgage_indices(params, headers): + """Test the mortgage indices endpoint.""" + params = {p: v for p, v in params.items() if v} + + query_str = get_querystring(params, []) + url = f"http://0.0.0.0:8000/api/v1/fixedincome/mortgage_indices?{query_str}" + result = requests.get(url, headers=headers, timeout=10) + assert isinstance(result, requests.Response) + assert result.status_code == 200 diff --git a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py index 433b96a3d8f..6058c18ecc9 100644 --- a/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py +++ b/openbb_platform/extensions/fixedincome/integration/test_fixedincome_python.py @@ -647,3 +647,28 @@ def test_fixedincome_bond_indices(params, obb): assert result assert isinstance(result, OBBject) assert len(result.results) > 0 + + +@parametrize( + "params", + [ + { + "provider": "fred", + "index": "usda_30y,fha_30y", + "start_date": "2023-05-31", + "end_date": "2024-06-01", + "transform": None, + "frequency": None, + "aggregation_method": "avg", + }, + ], +) +@pytest.mark.integration +def test_fixedincome_mortgage_indices(params, obb): + """Test the mortgage indices endpoint.""" + params = {p: v for p, v in params.items() if v} + + result = obb.fixedincome.mortgage_indices(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 diff --git a/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py b/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py index fb346dd3cc3..f7d62a1b16b 100644 --- a/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py +++ b/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_router.py @@ -81,3 +81,29 @@ async def bond_indices( ) -> OBBject: # type: ignore """Bond Indices.""" return await OBBject.from_query(Query(**locals())) + + +@router.command( + model="MortgageIndices", + examples=[ + APIEx( + description="The default state for FRED are the primary mortgage indices from Optimal Blue.", + parameters={"provider": "fred"}, + ), + APIEx( + description="Multiple indices can be requested.", + parameters={ + "index": "jumbo_30y,conforming_30y,conforming_15y", + "provider": "fred", + }, + ), + ], +) +async def mortgage_indices( + cc: CommandContext, + provider_choices: ProviderChoices, + standard_params: StandardParams, + extra_params: ExtraParams, +) -> OBBject: # type: ignore + """Mortgage Indices.""" + return await OBBject.from_query(Query(**locals())) diff --git a/openbb_platform/openbb/assets/reference.json b/openbb_platform/openbb/assets/reference.json index 7b5bf13451f..ca1dcb3f376 100644 --- a/openbb_platform/openbb/assets/reference.json +++ b/openbb_platform/openbb/assets/reference.json @@ -32235,6 +32235,191 @@ }, "model": "BondIndices" }, + "/fixedincome/mortgage_indices": { + "deprecated": { + "flag": null, + "message": null + }, + "description": "Mortgage Indices.", + "examples": "\nExamples\n--------\n\n```python\nfrom openbb import obb\n# The default state for FRED are the primary mortgage indices from Optimal Blue.\nobb.fixedincome.mortgage_indices(provider='fred')\n# Multiple indices can be requested.\nobb.fixedincome.mortgage_indices(index=jumbo_30y,conforming_30y,conforming_15y, provider='fred')\n```\n\n", + "parameters": { + "standard": [ + { + "name": "start_date", + "type": "Union[date, str]", + "description": "Start date of the data, in YYYY-MM-DD format.", + "default": null, + "optional": true, + "choices": null + }, + { + "name": "end_date", + "type": "Union[date, str]", + "description": "End date of the data, in YYYY-MM-DD format.", + "default": null, + "optional": true, + "choices": null + }, + { + "name": "provider", + "type": "Literal['fred']", + "description": "The provider to use, by default None. If None, the priority list configured in the settings is used. Default priority: f, r, e, d.", + "default": null, + "optional": true + } + ], + "fred": [ + { + "name": "index", + "type": "Union[Union[Literal['primary', 'ltv_lte_80', 'ltv_gt_80', 'conforming_30y', 'conforming_30y_na', 'jumbo_30y', 'fha_30y', 'va_30y', 'usda_30y', 'conforming_15y', 'ltv_lte80_fico_ge740', 'ltv_lte80_fico_a720b739', 'ltv_lte80_fico_a700b719', 'ltv_lte80_fico_a680b699', 'ltv_lte80_fico_lt680', 'ltv_gt80_fico_ge740', 'ltv_gt80_fico_a720b739', 'ltv_gt80_fico_a700b719', 'ltv_gt80_fico_a680b699', 'ltv_gt80_fico_lt680'], str], List[Union[Literal['primary', 'ltv_lte_80', 'ltv_gt_80', 'conforming_30y', 'conforming_30y_na', 'jumbo_30y', 'fha_30y', 'va_30y', 'usda_30y', 'conforming_15y', 'ltv_lte80_fico_ge740', 'ltv_lte80_fico_a720b739', 'ltv_lte80_fico_a700b719', 'ltv_lte80_fico_a680b699', 'ltv_lte80_fico_lt680', 'ltv_gt80_fico_ge740', 'ltv_gt80_fico_a720b739', 'ltv_gt80_fico_a700b719', 'ltv_gt80_fico_a680b699', 'ltv_gt80_fico_lt680'], str]]]", + "description": "The specific index, or index group, to query. Default is the 'primary' group. Multiple items allowed for provider(s): fred.", + "default": "primary", + "optional": true, + "choices": [ + "primary", + "ltv_lte_80", + "ltv_gt_80", + "conforming_30y", + "conforming_30y_na", + "jumbo_30y", + "fha_30y", + "va_30y", + "usda_30y", + "conforming_15y", + "ltv_lte80_fico_ge740", + "ltv_lte80_fico_a720b739", + "ltv_lte80_fico_a700b719", + "ltv_lte80_fico_a680b699", + "ltv_lte80_fico_lt680", + "ltv_gt80_fico_ge740", + "ltv_gt80_fico_a720b739", + "ltv_gt80_fico_a700b719", + "ltv_gt80_fico_a680b699", + "ltv_gt80_fico_lt680" + ] + }, + { + "name": "frequency", + "type": "Literal['a', 'q', 'm', 'w', 'd', 'wef', 'weth', 'wew', 'wetu', 'wem', 'wesu', 'wesa', 'bwew', 'bwem']", + "description": "Frequency aggregation to convert daily data to lower frequency. None = No change a = Annual q = Quarterly m = Monthly w = Weekly d = Daily wef = Weekly, Ending Friday weth = Weekly, Ending Thursday wew = Weekly, Ending Wednesday wetu = Weekly, Ending Tuesday wem = Weekly, Ending Monday wesu = Weekly, Ending Sunday wesa = Weekly, Ending Saturday bwew = Biweekly, Ending Wednesday bwem = Biweekly, Ending Monday", + "default": null, + "optional": true, + "choices": [ + "a", + "q", + "m", + "w", + "d", + "wef", + "weth", + "wew", + "wetu", + "wem", + "wesu", + "wesa", + "bwew", + "bwem" + ] + }, + { + "name": "aggregation_method", + "type": "Literal['avg', 'sum', 'eop']", + "description": "A key that indicates the aggregation method used for frequency aggregation. This parameter has no affect if the frequency parameter is not set, default is 'avg'. avg = Average sum = Sum eop = End of Period", + "default": "avg", + "optional": true, + "choices": [ + "avg", + "sum", + "eop" + ] + }, + { + "name": "transform", + "type": "Literal['chg', 'ch1', 'pch', 'pc1', 'pca', 'cch', 'cca', 'log']", + "description": "Transformation type None = No transformation chg = Change ch1 = Change from Year Ago pch = Percent Change pc1 = Percent Change from Year Ago pca = Compounded Annual Rate of Change cch = Continuously Compounded Rate of Change cca = Continuously Compounded Annual Rate of Change log = Natural Log", + "default": null, + "optional": true, + "choices": [ + "chg", + "ch1", + "pch", + "pc1", + "pca", + "cch", + "cca", + "log" + ] + } + ] + }, + "returns": { + "OBBject": [ + { + "name": "results", + "type": "List[MortgageIndices]", + "description": "Serializable results." + }, + { + "name": "provider", + "type": "Optional[Literal['fred']]", + "description": "Provider name." + }, + { + "name": "warnings", + "type": "Optional[List[Warning_]]", + "description": "List of warnings." + }, + { + "name": "chart", + "type": "Optional[Chart]", + "description": "Chart object." + }, + { + "name": "extra", + "type": "Dict[str, Any]", + "description": "Extra info." + } + ] + }, + "data": { + "standard": [ + { + "name": "date", + "type": "date", + "description": "The date of the data.", + "default": "", + "optional": false, + "choices": null + }, + { + "name": "symbol", + "type": "str", + "description": "Symbol representing the entity requested in the data.", + "default": null, + "optional": true, + "choices": null + }, + { + "name": "name", + "type": "str", + "description": "Name of the index.", + "default": null, + "optional": true, + "choices": null + }, + { + "name": "rate", + "type": "float", + "description": "Mortgage rate.", + "default": "", + "optional": false, + "choices": null + } + ], + "fred": [] + }, + "model": "MortgageIndices" + }, "/index/price/historical": { "deprecated": { "flag": null, diff --git a/openbb_platform/openbb/package/fixedincome.py b/openbb_platform/openbb/package/fixedincome.py index 456845abcf0..6f5dbb27f60 100644 --- a/openbb_platform/openbb/package/fixedincome.py +++ b/openbb_platform/openbb/package/fixedincome.py @@ -16,6 +16,7 @@ class ROUTER_fixedincome(Container): bond_indices /corporate /government + mortgage_indices /rate sofr /spreads @@ -184,6 +185,132 @@ class ROUTER_fixedincome(Container): command_runner=self._command_runner ) + @exception_handler + @validate + def mortgage_indices( + self, + start_date: Annotated[ + Union[datetime.date, None, str], + OpenBBField(description="Start date of the data, in YYYY-MM-DD format."), + ] = None, + end_date: Annotated[ + Union[datetime.date, None, str], + OpenBBField(description="End date of the data, in YYYY-MM-DD format."), + ] = None, + provider: Annotated[ + Optional[Literal["fred"]], + OpenBBField( + description="The provider to use, by default None. If None, the priority list configured in the settings is used. Default priority: fred." + ), + ] = None, + **kwargs + ) -> OBBject: + """Mortgage Indices. + + Parameters + ---------- + start_date : Union[datetime.date, None, str] + Start date of the data, in YYYY-MM-DD format. + end_date : Union[datetime.date, None, str] + End date of the data, in YYYY-MM-DD format. + provider : Optional[Literal['fred']] + The provider to use, by default None. If None, the priority list configured in the settings is used. Default priority: fred. + index : Union[Literal['primary', 'ltv_lte_80', 'ltv_gt_80', 'conforming_30y', 'conforming_30y_na', 'jumbo_30y', 'fha_30y', 'va_30y', 'usda_30y', 'conforming_15y', 'ltv_lte80_fico_ge740', 'ltv_lte80_fico_a720b739', 'ltv_lte80_fico_a700b719', 'ltv_lte80_fico_a680b699', 'ltv_lte80_fico_lt680', 'ltv_gt80_fico_ge740', 'ltv_gt80_fico_a720b739', 'ltv_gt80_fico_a700b719', 'ltv_gt80_fico_a680b699', 'ltv_gt80_fico_lt680'], str] + The specific index, or index group, to query. Default is the 'primary' group. Multiple comma separated items allowed. (provider: fred) + frequency : Optional[Literal['a', 'q', 'm', 'w', 'd', 'wef', 'weth', 'wew', 'wetu', 'wem', 'wesu', 'wesa', 'bwew', 'bwem']] + + Frequency aggregation to convert daily data to lower frequency. + None = No change + a = Annual + q = Quarterly + m = Monthly + w = Weekly + d = Daily + wef = Weekly, Ending Friday + weth = Weekly, Ending Thursday + wew = Weekly, Ending Wednesday + wetu = Weekly, Ending Tuesday + wem = Weekly, Ending Monday + wesu = Weekly, Ending Sunday + wesa = Weekly, Ending Saturday + bwew = Biweekly, Ending Wednesday + bwem = Biweekly, Ending Monday + (provider: fred) + aggregation_method : Literal['avg', 'sum', 'eop'] + + A key that indicates the aggregation method used for frequency aggregation. + This parameter has no affect if the frequency parameter is not set, default is 'avg'. + avg = Average + sum = Sum + eop = End of Period + (provider: fred) + transform : Optional[Literal['chg', 'ch1', 'pch', 'pc1', 'pca', 'cch', 'cca', 'log']] + + Transformation type + None = No transformation + chg = Change + ch1 = Change from Year Ago + pch = Percent Change + pc1 = Percent Change from Year Ago + pca = Compounded Annual Rate of Change + cch = Continuously Compounded Rate of Change + cca = Continuously Compounded Annual Rate of Change + log = Natural Log + (provider: fred) + + Returns + ------- + OBBject + results : List[MortgageIndices] + Serializable results. + provider : Optional[Literal['fred']] + Provider name. + warnings : Optional[List[Warning_]] + List of warnings. + chart : Optional[Chart] + Chart object. + extra : Dict[str, Any] + Extra info. + + MortgageIndices + --------------- + date : date + The date of the data. + symbol : Optional[str] + Symbol representing the entity requested in the data. + name : Optional[str] + Name of the index. + rate : float + Mortgage rate. + + Examples + -------- + >>> from openbb import obb + >>> # The default state for FRED are the primary mortgage indices from Optimal Blue. + >>> obb.fixedincome.mortgage_indices(provider='fred') + >>> # Multiple indices can be requested. + >>> obb.fixedincome.mortgage_indices(index='jumbo_30y,conforming_30y,conforming_15y', provider='fred') + """ # noqa: E501 + + return self._run( + "/fixedincome/mortgage_indices", + **filter_inputs( + provider_choices={ + "provider": self._get_provider( + provider, + "fixedincome.mortgage_indices", + ("fred",), + ) + }, + standard_params={ + "start_date": start_date, + "end_date": end_date, + }, + extra_params=kwargs, + info={"index": {"fred": {"multiple_items_allowed": True}}}, + ) + ) + @property def rate(self): # pylint: disable=import-outside-toplevel diff --git a/openbb_platform/providers/fred/openbb_fred/__init__.py b/openbb_platform/providers/fred/openbb_fred/__init__.py index d68a8d1558e..c55e2d55e16 100644 --- a/openbb_platform/providers/fred/openbb_fred/__init__.py +++ b/openbb_platform/providers/fred/openbb_fred/__init__.py @@ -18,6 +18,7 @@ from openbb_fred.models.hqm import FREDHighQualityMarketCorporateBondFetcher from openbb_fred.models.ice_bofa import FREDICEBofAFetcher from openbb_fred.models.iorb_rates import FREDIORBFetcher from openbb_fred.models.moody import FREDMoodyCorporateBondIndexFetcher +from openbb_fred.models.mortgage_indices import FredMortgageIndicesFetcher from openbb_fred.models.regional import FredRegionalDataFetcher from openbb_fred.models.retail_prices import FredRetailPricesFetcher from openbb_fred.models.search import ( @@ -57,6 +58,7 @@ Research division of the Federal Reserve Bank of St. Louis that has more than "EuropeanCentralBankInterestRates": FREDEuropeanCentralBankInterestRatesFetcher, "ICEBofA": FREDICEBofAFetcher, "MoodyCorporateBondIndex": FREDMoodyCorporateBondIndexFetcher, + "MortgageIndices": FredMortgageIndicesFetcher, "CommercialPaper": FREDCommercialPaperFetcher, "FredSearch": FredSearchFetcher, "FredSeries": FredSeriesFetcher, diff --git a/openbb_platform/providers/fred/openbb_fred/models/mortgage_indices.py b/openbb_platform/providers/fred/openbb_fred/models/mortgage_indices.py new file mode 100644 index 00000000000..68a59dca4fd --- /dev/null +++ b/openbb_platform/providers/fred/openbb_fred/models/mortgage_indices.py @@ -0,0 +1,312 @@ +"""FRED Mortgage Indices Model.""" + +# pylint: disable=unused-argument + +from typing import Any, Dict, List, Literal, Optional, Union +from warnings import warn + +from openbb_core.app.model.abstract.error import OpenBBError +from openbb_core.provider.abstract.annotated_result import AnnotatedResult +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.mortgage_indices import ( + MortgageIndicesData, + MortgageIndicesQueryParams, +) +from openbb_core.provider.utils.errors import EmptyDataError +from openbb_fred.models.series import FredSeriesFetcher +from pandas import Categorical, DataFrame +from pydantic import Field, field_validator + +MORTGAGE_ID_TO_TITLE = { + "OBMMIC30YF": "30-Year Fixed Rate Conforming", + "OBMMIC30YFNA": "30-Year Fixed Rate Conforming Non-Adjusted", + "OBMMIJUMBO30YF": "30-Year Fixed Rate Jumbo", + "OBMMIFHA30YF": "30-Year Fixed Rate FHA", + "OBMMIVA30YF": "30-Year Fixed Rate Veterans Affairs", + "OBMMIUSDA30YF": "30-Year Fixed Rate USDA", + "OBMMIC15YF": "15-Year Fixed Rate Conforming", + "OBMMIC30YFLVLE80FGE740": "30-Year Fixed Rate Conforming LTV <= 80 FICO >= 740", + "OBMMIC30YFLVLE80FB720A739": "30-Year Fixed Rate Conforming LTV <= 80 FICO 720-739", + "OBMMIC30YFLVLE80FB700A719": "30-Year Fixed Rate Conforming LTV <= 80 FICO 700-719", + "OBMMIC30YFLVLE80FB680A699": "30-Year Fixed Rate Conforming LTV <= 80 FICO 680-699", + "OBMMIC30YFLVLE80FLT680": "30-Year Fixed Rate Conforming LTV <= 80 FICO < 680", + "OBMMIC30YFLVGT80FGE740": "30-Year Fixed Rate Conforming LTV > 80 FICO >= 740", + "OBMMIC30YFLVGT80FB720A739": "30-Year Fixed Rate Conforming LTV > 80 FICO 720-739", + "OBMMIC30YFLVGT80FB700A719": "30-Year Fixed Rate Conforming LTV > 80 FICO 700-719", + "OBMMIC30YFLVGT80FB680A699": "30-Year Fixed Rate Conforming LTV > 80 FICO 680-699", + "OBMMIC30YFLVGT80FLT680": "30-Year Fixed Rate Conforming LTV > 80 FICO < 680", +} + +MORTGAGE_GROUPS = { + "primary": [ + "OBMMIC30YF", + "OBMMIC30YFNA", + "OBMMIJUMBO30YF", + "OBMMIFHA30YF", + "OBMMIVA30YF", + "OBMMIUSDA30YF", + "OBMMIC15YF", + ], + "ltv_lte_80": [ + "OBMMIC30YFLVLE80FGE740", + "OBMMIC30YFLVLE80FB720A739", + "OBMMIC30YFLVLE80FB700A719", + "OBMMIC30YFLVLE80FB680A699", + "OBMMIC30YFLVLE80FLT680", + ], + "ltv_gt_80": [ + "OBMMIC30YFLVGT80FGE740", + "OBMMIC30YFLVGT80FB720A739", + "OBMMIC30YFLVGT80FB700A719", + "OBMMIC30YFLVGT80FB680A699", + "OBMMIC30YFLVGT80FLT680", + ], +} + +MORTGAGE_CHOICES_TO_ID = { + "primary": ",".join(MORTGAGE_GROUPS["primary"]), + "ltv_lte_80": ",".join(MORTGAGE_GROUPS["ltv_lte_80"]), + "ltv_gt_80": ",".join(MORTGAGE_GROUPS["ltv_gt_80"]), + "conforming_30y": "OBMMIC30YF", + "conforming_30y_na": "OBMMIC30YFNA", + "jumbo_30y": "OBMMIJUMBO30YF", + "fha_30y": "OBMMIFHA30YF", + "va_30y": "OBMMIVA30YF", + "usda_30y": "OBMMIUSDA30YF", + "conforming_15y": "OBMMIC15YF", + "ltv_lte80_fico_ge740": "OBMMIC30YFLVLE80FGE740", + "ltv_lte80_fico_a720b739": "OBMMIC30YFLVLE80FB720A739", + "ltv_lte80_fico_a700b719": "OBMMIC30YFLVLE80FB700A719", + "ltv_lte80_fico_a680b699": "OBMMIC30YFLVLE80FB680A699", + "ltv_lte80_fico_lt680": "OBMMIC30YFLVLE80FLT680", + "ltv_gt80_fico_ge740": "OBMMIC30YFLVGT80FGE740", + "ltv_gt80_fico_a720b739": "OBMMIC30YFLVGT80FB720A739", + "ltv_gt80_fico_a700b719": "OBMMIC30YFLVGT80FB700A719", + "ltv_gt80_fico_a680b699": "OBMMIC30YFLVGT80FB680A699", + "ltv_gt80_fico_lt680": "OBMMIC30YFLVGT80FLT680", +} + +MortgageChoices = Literal[ + "primary", + "ltv_lte_80", + "ltv_gt_80", + "conforming_30y", + "conforming_30y_na", + "jumbo_30y", + "fha_30y", + "va_30y", + "usda_30y", + "conforming_15y", + "ltv_lte80_fico_ge740", + "ltv_lte80_fico_a720b739", + "ltv_lte80_fico_a700b719", + "ltv_lte80_fico_a680b699", + "ltv_lte80_fico_lt680", + "ltv_gt80_fico_ge740", + "ltv_gt80_fico_a720b739", + "ltv_gt80_fico_a700b719", + "ltv_gt80_fico_a680b699", + "ltv_gt80_fico_lt680", +] + + +class FredMortgageIndicesQueryParams(MortgageIndicesQueryParams): + """FRED Mortgage Indices Query.""" + + __json_schema_extra__ = {"index": {"multiple_items_allowed": True}} + + index: Union[MortgageChoices, str] = Field( + default="primary", + description="The specific index, or index group, to query. Default is the 'primary' group.", + choices=list(MORTGAGE_CHOICES_TO_ID.keys()), + ) + frequency: Union[ + None, + Literal[ + "a", + "q", + "m", + "w", + "d", + "wef", + "weth", + "wew", + "wetu", + "wem", + "wesu", + "wesa", + "bwew", + "bwem", + ], + ] = Field( + default=None, + description=""" + Frequency aggregation to convert daily data to lower frequency. + None = No change + a = Annual + q = Quarterly + m = Monthly + w = Weekly + d = Daily + wef = Weekly, Ending Friday + weth = Weekly, Ending Thursday + wew = Weekly, Ending Wednesday + wetu = Weekly, Ending Tuesday + wem = Weekly, Ending Monday + wesu = Weekly, Ending Sunday + wesa = Weekly, Ending Saturday + bwew = Biweekly, Ending Wednesday + bwem = Biweekly, Ending Monday + """, + json_schema_extra={ + "choices": [ + "a", + "q", + "m", + "w", + "d", + "wef", + "weth", + "wew", + "wetu", + "wem", + "wesu", + "wesa", + "bwew", + "bwem", + ] + }, + ) + aggregation_method: Literal["avg", "sum", "eop"] = Field( + default="avg", + description=""" + A key that indicates the aggregation method used for frequency aggregation. + This parameter has no affect if the frequency parameter is not set, default is 'avg'. + avg = Average + sum = Sum + eop = End of Period + """, + json_schema_extra={"choices": ["avg", "sum", "eop"]}, + ) + transform: Union[ + None, Literal["chg", "ch1", "pch", "pc1", "pca", "cch", "cca", "log"] + ] = Field( + default=None, + description=""" + Transformation type + None = No transformation + chg = Change + ch1 = Change from Year Ago + pch = Percent Change + pc1 = Percent Change from Year Ago + pca = Compounded Annual Rate of Change + cch = Continuously Compounded Rate of Change + cca = Continuously Compounded Annual Rate of Change + log = Natural Log + """, + json_schema_extra={ + "choices": ["chg", "ch1", "pch", "pc1", "pca", "cch", "cca", "log"] + }, + ) + + @field_validator("index", mode="before", check_fields=False) + @classmethod + def validate_index(cls, v): + """Validate index.""" + indices = v.split(",") + new_indices: List = [] + for index in indices: + if index in MORTGAGE_CHOICES_TO_ID: + new_indices.append(index) + else: + warn(f"Invalid index '{index}' will be ignored.") + if not new_indices: + raise OpenBBError( + f"No valid indices found. Must be any of: {list(MORTGAGE_CHOICES_TO_ID.keys())}" + ) + return ",".join(new_indices) + + +class FredMortgageIndicesData(MortgageIndicesData): + """FRED Mortgage Indices Data.""" + + +class FredMortgageIndicesFetcher( + Fetcher[ + FredMortgageIndicesQueryParams, + List[FredMortgageIndicesData], + ] +): + """FRED Mortgage Indices Fetcher.""" + + @staticmethod + def transform_query(params: Dict[str, Any]) -> FredMortgageIndicesQueryParams: + """Transform query.""" + return FredMortgageIndicesQueryParams(**params) + + @staticmethod + async def aextract_data( + query: FredMortgageIndicesQueryParams, + credentials: Optional[Dict[str, str]], + **kwargs: Any, + ) -> Dict: + """Extract data.""" + indices = query.index.split(",") + ids = [MORTGAGE_CHOICES_TO_ID[index] for index in indices] + try: + response = await FredSeriesFetcher.fetch_data( + dict( + symbol=",".join(ids), + start_date=query.start_date, + end_date=query.end_date, + transform=query.transform, + frequency=query.frequency, + aggregation_method=query.aggregation_method, + ), + credentials, + ) + except Exception as e: + raise e from e + + return { + "metadata": response.metadata, + "data": [d.model_dump() for d in response.result], + } + + @staticmethod + def transform_data( + query: FredMortgageIndicesQueryParams, + data: Dict, + **kwargs: Any, + ) -> AnnotatedResult[List[FredMortgageIndicesData]]: + """Transform data.""" + if not data.get("data"): + raise EmptyDataError("The request was returned empty.") + df = DataFrame.from_records(data["data"]) + metadata = data.get("metadata", {}) + # Flatten the data. + df = ( + df.melt(id_vars="date", var_name="symbol", value_name="value") + .query("value.notnull()") + .rename(columns={"value": "rate"}) + ) + df["name"] = df.symbol.map(MORTGAGE_ID_TO_TITLE) + # Normalize the percent values. + df["rate"] = df["rate"] / 100 + df = df.fillna("N/A").replace("N/A", None) + df["name"] = Categorical( + df["name"], + categories=[ + d + for d in list(MORTGAGE_ID_TO_TITLE.values()) + if d in df["name"].unique() + ], + ordered=True, + ) + df.sort_values(["date", "name"], inplace=True) + records = df.to_dict(orient="records") + + return AnnotatedResult( + result=[FredMortgageIndicesData.model_validate(r) for r in records], + metadata=metadata, + ) diff --git a/openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_bond_mortgage_fetcher.yaml b/openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_bond_mortgage_fetcher.yaml new file mode 100644 index 00000000000..c86b1408abb --- /dev/null +++ b/openbb_platform/providers/fred/tests/record/http/test_fred_fetchers/test_fred_bond_mortgage_fetcher.yaml @@ -0,0 +1,105 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api.stlouisfed.org/fred/series/observations?aggregation_method=avg&api_key=MOCK_API_KEY&file_type=json&limit=100000&observation_end=2024-06-04&observation_start=2024-06-01&series_id=OBMMIJUMBO30YF + response: + body: + string: !!binary | + H4sIAAAAAAAEA6pWKkpNzCnJzE2NLy5JLCpRslIyMjAy0TUw0zU0UdJByKbmpaDL5ScVpxaVJZZk + 5udhaDYwVNJRQlaAqt8AZHZpXmZJsZKVUk5mHkhxaUlBaUl8SWVBqpKVoY5SWmZOKpSnlFWcD1ZS + lJJaFJ9UqWSFYnRKYkmqko5ScX5RSXw+SImSlVJicbKSjlJyfmleiZKVkY5SflpacWqJkpWBjlJO + Zm5miZKVoQEIoDiyWMkqmpIAATsEEYAGxko6SmWJOaWpSlZK5nqGppZKtTrUNB8UiEjmA2ZmrFQb + WwsAU848LNIBAAA= + headers: + Cache-Control: + - max-age=0, no-cache + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '239' + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 14 Jun 2024 05:34:53 GMT + Expires: + - Fri, 14 Jun 2024 05:34:53 GMT + Last-Modified: + - Thu, 13 Jun 2024 12:01:06 GMT + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=86400 + Vary: + - Accept-Encoding + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api.stlouisfed.org/fred/series?api_key=MOCK_API_KEY&file_type=json&series_id=OBMMIJUMBO30YF + response: + body: + string: !!binary | + H4sIAAAAAAAEA6pWKkpNzCnJzE2NLy5JLCpRslIyMjAy0TUw0zU0VtJByKbmpaDLFacWZaYWFytZ + RVcrZYJk/Z18fT29Qn2d/I0NIt2QdZNsdklmSU6qkpWSsYFuZGpikYJbZkVqikJQYkmqgldpblK+ + gm9+UUl6YnqqgmdeSmqFko5SflJxalFZYklmfh6SVwzNdQ0MdQ1AXkFWgOYbIyUdpbSi1MLS1Lzk + SiUrJZfEzJxKZLH44ox8cOC4KOkoleZllhQrWSkFpBYlp+aVwETgalSVdJSKUxOL8/MSc+ITU7JK + i0tyQeqslPzySxSCoTI5lQqOYLnUFOzq4eb5BTsq6SjlJBaXxJcWpCSWpILCGhFLCgbmVgaGVgZm + ugamSjpKBfkFpTmJRZkllUpWZqY6Snn5Jakg1/oXlGTmJuYoOOWUpiJCzzexKDu1BBSImcmpxQoa + GSUlBcVWMfox+uXl5UZ6+RBNSTmlqXrJ+bkx+vlJubmZMfqaMaVGhkZGChr+oCiHcDQVMosVkhNz + kktzQG5USCvKz1VITC4pTcxRyMlPzk5NUShKLEktVijPLMlQSM7PKy7NTS0qVkhMLsovLlbIL0st + UsjPS9UtycgsSlHIT1NIzMlRyIVFc0lRYl5xYjIoeosV8sDRXJ6ZkqqnAHaBQmZeck5pSmqxQm5p + TklmQU4qQmdBUWZyZl66QmZeCtiPKallqTn5BakpColF+aV5KQolGSDVxSUK0KBTKCjKTylNLilW + SMxLUSguSE3OTMtMVkjKLyrKL08tAovm5CfmKeSATFJILCkpykwqLUkt1ovJi8lzTUzOANmVWoEW + HonFYJsSy1KLQOkW6sHEgoKi/IKiTFDKBgUPOKiKwWQqyGlF+aXpGWCNKBEIdaJCak5memZSZk5m + SSXYXTDPpualZ+alKuTnKSQqpGeWpeYppCRW6oHiPVUhJbUkMTOnWCExKb+0RCE3tSQjPyU/Jz8d + YkJKalpmXiYknBOLUhUSyxIzcxKTclIVMlKLUolPIYDpKdXG1gIAFK52ql8EAAA= + headers: + Cache-Control: + - max-age=0, no-cache + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '788' + Content-Type: + - application/json; charset=UTF-8 + Date: + - Fri, 14 Jun 2024 05:34:53 GMT + Expires: + - Fri, 14 Jun 2024 05:34:53 GMT + Last-Modified: + - Thu, 13 Jun 2024 12:01:06 GMT + Pragma: + - no-cache + Server: + - Apache + Strict-Transport-Security: + - max-age=86400 + Vary: + - Accept-Encoding + 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 736098c7247..16ab9ae7916 100644 --- a/openbb_platform/providers/fred/tests/test_fred_fetchers.py +++ b/openbb_platform/providers/fred/tests/test_fred_fetchers.py @@ -21,6 +21,7 @@ from openbb_fred.models.hqm import FREDHighQualityMarketCorporateBondFetcher from openbb_fred.models.ice_bofa import FREDICEBofAFetcher from openbb_fred.models.iorb_rates import FREDIORBFetcher from openbb_fred.models.moody import FREDMoodyCorporateBondIndexFetcher +from openbb_fred.models.mortgage_indices import FredMortgageIndicesFetcher from openbb_fred.models.regional import FredRegionalDataFetcher from openbb_fred.models.retail_prices import FredRetailPricesFetcher from openbb_fred.models.search import ( @@ -357,3 +358,17 @@ def test_fred_bond_indices_fetcher(credentials=test_credentials): fetcher = FredBondIndicesFetcher() result = fetcher.test(params, credentials) assert result is None + + +@pytest.mark.record_http +def test_fred_bond_mortgage_fetcher(credentials=test_credentials): + """Test FredMortgageIndicesFetcher.""" + params = { + "index": "jumbo_30y", + "start_date": datetime.date(2024, 6, 1), + "end_date": datetime.date(2024, 6, 4), + } + + fetcher = FredMortgageIndicesFetcher() + result = fetcher.test(params, credentials) + assert result is None -- cgit v1.2.3