From 07416e04a5daf81ca3ca5e32486c53d0a9fc093e Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:17:25 -0800 Subject: [PATCH 1/4] add multi-country suppport and ECB yield curves to EconDB --- .../provider/standard_models/yield_curve.py | 27 +- .../openbb_fixedincome/fixedincome_views.py | 64 ++-- openbb_platform/openbb/assets/reference.json | 33 +- .../openbb/package/fixedincome_government.py | 39 ++- .../openbb_econdb/models/yield_curve.py | 296 +++++++++++------- .../openbb_econdb/utils/yield_curves.py | 72 +++-- 6 files changed, 344 insertions(+), 187 deletions(-) diff --git a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py index 9d7169f80b7c..c5670d38b60c 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py @@ -3,20 +3,19 @@ from datetime import date as dateType from typing import Optional, Union -from pydantic import Field, field_validator - 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, ) +from pydantic import Field, computed_field, field_validator class YieldCurveQueryParams(QueryParams): """Yield Curve Query.""" - date: Union[None, dateType, str] = Field( + date: Optional[Union[dateType, str]] = Field( default=None, description=QUERY_DESCRIPTIONS.get("date", "") + " By default is the current data.", @@ -52,7 +51,23 @@ class YieldCurveData(Data): description=DATA_DESCRIPTIONS.get("date", ""), ) maturity: str = Field(description="Maturity length of the security.") - rate: float = Field( - description="The yield as a normalized percent (0.05 is 5%)", - json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + + @computed_field( + description="Maturity length as a decimal.", + return_type=Optional[float], ) + @property + def maturity_years(self) -> Optional[float]: + """Get the maturity in years as a decimal.""" + if self.maturity is None or "_" not in self.maturity: + return None + + parts = self.maturity.split("_") + months = 0 + for i in range(0, len(parts), 2): + number = int(parts[i + 1]) + if parts[i] == "year": + number *= 12 + months += number + + return months / 12 diff --git a/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_views.py b/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_views.py index 5a56923e1b38..a8b6cae73f26 100644 --- a/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_views.py +++ b/openbb_platform/extensions/fixedincome/openbb_fixedincome/fixedincome_views.py @@ -28,7 +28,7 @@ def fixedincome_government_yield_curve( # noqa: PLR0912 from openbb_core.app.utils import basemodel_to_df from pandas import DataFrame - data = kwargs.get("data", None) + data = kwargs.get("data") df: DataFrame = DataFrame() if data: if isinstance(data, DataFrame) and not data.empty: # noqa: SIM108 @@ -55,6 +55,9 @@ def fixedincome_government_yield_curve( # noqa: PLR0912 provider = kwargs.get("provider") df["date"] = df["date"].astype(str) maturities = duration_sorter(df["maturity"].unique().tolist()) + countries: list = ( + df["country"].unique().tolist() if "country" in df.columns else [] + ) # Use the supplied colors, if any. colors = kwargs.get("colors", []) @@ -66,11 +69,13 @@ def fixedincome_government_yield_curve( # noqa: PLR0912 figure.update_layout(ChartStyle().plotly_template.get("layout", {})) text_color = "white" if ChartStyle().plt_style == "dark" else "black" - def create_fig(figure, df, dates, color_count, country: Optional[str] = None): + def create_fig( + figure, dataframe, dates, color_count, country: Optional[str] = None + ): """Create a scatter for each date in the data.""" for date in dates: color = colors[color_count % len(colors)] - plot_df = df[df["date"] == date].copy() + plot_df = dataframe[dataframe["date"] == date].copy() plot_df["rate"] = plot_df["rate"].apply(lambda x: x * 100) plot_df = plot_df.rename(columns={"rate": "Yield"}) plot_df = ( @@ -81,31 +86,41 @@ def create_fig(figure, df, dates, color_count, country: Optional[str] = None): ) plot_df = plot_df.rename(columns={"index": "Maturity"}) plot_df["Maturity"] = [ - ( - d.split("_")[1] + " " + d.split("_")[0].title() - if d != "long_term" - else "Long Term" - ) + (d.split("_")[1] + " " + d.split("_")[0].title()) for d in plot_df["Maturity"] ] figure.add_scatter( x=plot_df["Maturity"], y=plot_df["Yield"], mode="lines+markers", - name=f"{country} - {date}" if country else date, + name=( + f"{country.replace('_', ' ').title().replace('Ecb', 'ECB')} {date}" + if country + else date + ), line=dict(width=3, color=color), marker=dict(size=10, color=color), hovertemplate=( "Maturity: %{x}
Yield: %{y}%" - if len(dates) == 1 + if len(dates) == 1 and not countries else "%{fullData.name}
Maturity: %{x}
Yield: %{y}%" ), ) color_count += 1 return figure, color_count - dates = df.date.unique().tolist() - figure, color_count = create_fig(figure, df, dates, color_count) + if countries: + for _country in countries: + _df = df[df["country"] == _country] + dates = _df.date.unique().tolist() + figure, color_count = create_fig( + figure, _df, dates, color_count, _country + ) + + else: + dates = df.date.unique().tolist() + figure, color_count = create_fig(figure, df, dates, color_count) + extra_params = kwargs.get("extra_params", {}) extra_params = ( extra_params if isinstance(extra_params, dict) else extra_params.__dict__ @@ -130,14 +145,26 @@ def create_fig(figure, df, dates, color_count, country: Optional[str] = None): ) country = f"United States {curve_type}" elif provider == "econdb": - country = extra_params.get("country", "") - country = country.replace("_", " ").title() if country else "United States" + country = ( + "" + if countries + else ( + extra_params.get("country", "") + .replace("_", " ") + .title() + .replace("Ecb", "ECB") + or "United States" + ) + ) + country = country + " " if country else "" title = kwargs.get("title", "") if not title: title = f"{country}Yield Curve" - if len(dates) == 1: + if len(dates) == 1 and len(countries) == 1: title = f"{country} Yield Curve - {dates[0]}" + elif countries: + title = f"Yield Curve - {', '.join(countries).replace('_', ' ').title().replace('Ecb', 'ECB')}" # Update the layout of the figure. figure.update_layout( @@ -156,11 +183,7 @@ def create_fig(figure, df, dates, color_count, country: Optional[str] = None): categoryorder="array", categoryarray=( [ - ( - d.split("_")[1] + " " + d.split("_")[0].title() - if d != "long_term" - else "Long Term" - ) + (d.split("_")[1] + " " + d.split("_")[0].title()) for d in maturities ] ), @@ -186,6 +209,7 @@ def create_fig(figure, df, dates, color_count, country: Optional[str] = None): margin=dict( b=25, t=10, + l=20, ), ) diff --git a/openbb_platform/openbb/assets/reference.json b/openbb_platform/openbb/assets/reference.json index bb1c239353f4..5943b74b1e4c 100644 --- a/openbb_platform/openbb/assets/reference.json +++ b/openbb_platform/openbb/assets/reference.json @@ -3020,7 +3020,7 @@ }, { "name": "date", - "type": "Union[Union[str, date], List[Union[str, date]]]", + "type": "Union[Union[date, str], List[Union[date, str]]]", "description": "A specific date to get data for. Multiple items allowed for provider(s): yfinance.", "default": null, "optional": true, @@ -4671,7 +4671,7 @@ "standard": [ { "name": "date", - "type": "Union[Union[str, date], List[Union[str, date]]]", + "type": "Union[Union[date, str], List[Union[date, str]]]", "description": "A specific date to get data for. Default is the latest report. Multiple items allowed for provider(s): fred.", "default": null, "optional": true, @@ -9657,7 +9657,7 @@ "standard": [ { "name": "date", - "type": "Union[Union[str, date], List[Union[str, date]]]", + "type": "Union[Union[date, str], List[Union[date, str]]]", "description": "A specific date to get data for. Default is the latest report. Multiple items allowed for provider(s): fred.", "default": null, "optional": true, @@ -32892,7 +32892,7 @@ "fmp": [ { "name": "date", - "type": "Union[Union[str, date], str]", + "type": "Union[Union[date, str], str]", "description": "A specific date to get data for. Entering a date will attempt to return the NPORT-P filing for the entered date. This needs to be _exactly_ the date of the filing. Use the holdings_date command/endpoint to find available filing dates for the ETF.", "default": null, "optional": true, @@ -32920,7 +32920,7 @@ "sec": [ { "name": "date", - "type": "Union[Union[str, date], str]", + "type": "Union[Union[date, str], str]", "description": "A specific date to get data for. The date represents the period ending. The date entered will return the closest filing.", "default": null, "optional": true, @@ -35771,7 +35771,7 @@ "standard": [ { "name": "date", - "type": "Union[Union[None, date, str], List[Union[None, date, str]]]", + "type": "Union[Union[date, str], List[Union[date, str]]]", "description": "A specific date to get data for. By default is the current data. Multiple items allowed for provider(s): econdb, federal_reserve, fmp, fred.", "default": null, "optional": true, @@ -35781,14 +35781,17 @@ "econdb": [ { "name": "country", - "type": "Literal['australia', 'canada', 'china', 'hong_kong', 'india', 'japan', 'mexico', 'new_zealand', 'russia', 'saudi_arabia', 'singapore', 'south_africa', 'south_korea', 'taiwan', 'thailand', 'united_kingdom', 'united_states']", - "description": "The country to get data. New Zealand, Mexico, Singapore, and Thailand have only monthly data. The nearest date to the requested one will be used.", + "type": "Union[str, List[str]]", + "description": "The country to get data. New Zealand, Mexico, Singapore, and Thailand have only monthly data. The nearest date to the requested one will be used. Multiple items allowed for provider(s): econdb.", "default": "united_states", "optional": true, "choices": [ "australia", "canada", "china", + "ecb_instantaneous_forward", + "ecb_par_yield", + "ecb_spot_rate", "hong_kong", "india", "japan", @@ -35880,14 +35883,6 @@ "default": "", "optional": false, "choices": null - }, - { - "name": "rate", - "type": "float", - "description": "The yield as a normalized percent (0.05 is 5%)", - "default": "", - "optional": false, - "choices": null } ], "econdb": [], @@ -36553,7 +36548,7 @@ "standard": [ { "name": "date", - "type": "Union[Union[str, date], List[Union[str, date]]]", + "type": "Union[Union[date, str], List[Union[date, str]]]", "description": "A specific date to get data for. Multiple items allowed for provider(s): fred.", "default": null, "optional": true, @@ -37864,7 +37859,7 @@ }, { "name": "date_first_added", - "type": "Union[str, date]", + "type": "Union[date, str]", "description": "Date the constituent company was added to the index.", "default": null, "optional": true, @@ -37880,7 +37875,7 @@ }, { "name": "founded", - "type": "Union[str, date]", + "type": "Union[date, str]", "description": "Founding year of the constituent company in the index.", "default": null, "optional": true, diff --git a/openbb_platform/openbb/package/fixedincome_government.py b/openbb_platform/openbb/package/fixedincome_government.py index e9c5204b8cfc..023694a40eaa 100644 --- a/openbb_platform/openbb/package/fixedincome_government.py +++ b/openbb_platform/openbb/package/fixedincome_government.py @@ -338,7 +338,7 @@ def us_yield_curve( def yield_curve( self, date: Annotated[ - Union[str, datetime.date, None, List[Union[str, datetime.date, None]]], + Union[datetime.date, str, None, List[Union[datetime.date, str, None]]], OpenBBField( description="A specific date to get data for. By default is the current data. Multiple comma separated items allowed for provider(s): econdb, federal_reserve, fmp, fred." ), @@ -355,12 +355,12 @@ def yield_curve( Parameters ---------- - date : Union[str, date, None, List[Union[str, date, None]]] + date : Union[date, str, None, List[Union[date, str, None]]] A specific date to get data for. By default is the current data. Multiple comma separated items allowed for provider(s): econdb, federal_reserve, fmp, fred. provider : Optional[Literal['econdb', 'federal_reserve', 'fmp', 'fred']] The provider to use, by default None. If None, the priority list configured in the settings is used. Default priority: econdb, federal_reserve, fmp, fred. - country : Literal['australia', 'canada', 'china', 'hong_kong', 'india', 'japan', 'mexico', 'new_zealand', 'russia', 'saudi_arabia', 'singapore', 'south_africa', 'south_korea', 'taiwan', 'thailand', 'united_kingdom', 'united_states'] - The country to get data. New Zealand, Mexico, Singapore, and Thailand have only monthly data. The nearest date to the requested one will be used. (provider: econdb) + country : str + The country to get data. New Zealand, Mexico, Singapore, and Thailand have only monthly data. The nearest date to the requested one will be used. Multiple comma separated items allowed. (provider: econdb) use_cache : bool If true, cache the request for four hours. (provider: econdb) yield_curve_type : Literal['nominal', 'real', 'breakeven', 'treasury_minus_fed_funds', 'corporate_spot', 'corporate_par'] @@ -386,8 +386,6 @@ def yield_curve( The date of the data. maturity : str Maturity length of the security. - rate : float - The yield as a normalized percent (0.05 is 5%) Examples -------- @@ -421,7 +419,34 @@ def yield_curve( }, "fmp": {"multiple_items_allowed": True, "choices": None}, "fred": {"multiple_items_allowed": True, "choices": None}, - } + }, + "country": { + "econdb": { + "multiple_items_allowed": True, + "choices": [ + "australia", + "canada", + "china", + "ecb_instantaneous_forward", + "ecb_par_yield", + "ecb_spot_rate", + "hong_kong", + "india", + "japan", + "mexico", + "new_zealand", + "russia", + "saudi_arabia", + "singapore", + "south_africa", + "south_korea", + "taiwan", + "thailand", + "united_kingdom", + "united_states", + ], + } + }, }, ) ) diff --git a/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py b/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py index 8b84371a08d7..9e5456768d85 100644 --- a/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py +++ b/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py @@ -14,7 +14,7 @@ ) from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS from openbb_core.provider.utils.errors import EmptyDataError -from openbb_econdb.utils.yield_curves import COUNTRIES, COUNTRIES_DICT, COUNTRIES_LIST +from openbb_econdb.utils.yield_curves import COUNTRIES_DICT, COUNTRIES_LIST from pydantic import Field, field_validator @@ -22,14 +22,14 @@ class EconDbYieldCurveQueryParams(YieldCurveQueryParams): """EconDB Economic Indicators Query.""" __json_schema_extra__ = { - "date": ["multiple_items_allowed"], + "date": {"multiple_items_allowed": True}, + "country": {"multiple_items_allowed": True, "choices": COUNTRIES_LIST}, } - country: COUNTRIES = Field( + country: str = Field( default="united_states", description=QUERY_DESCRIPTIONS.get("country", "") + " New Zealand, Mexico, Singapore, and Thailand have only monthly data." + " The nearest date to the requested one will be used.", - json_schema_extra={"choices": COUNTRIES_LIST}, ) use_cache: bool = Field( default=True, @@ -42,9 +42,18 @@ def validate_country(cls, v) -> str: """Validate the country.""" if v is None: return "united_states" - if v not in COUNTRIES_DICT: - raise OpenBBError(f"Country must be one of {COUNTRIES_DICT}") - return v + + countries = v.split(",") + new_countries: list = [] + + for country in countries: + if country not in COUNTRIES_DICT: + raise ValueError( + f"Invalid country, {country}, must be one of {list(COUNTRIES_DICT)}" + ) + new_countries.append(country) + + return ",".join(new_countries) class EconDbYieldCurveData(YieldCurveData): @@ -59,7 +68,7 @@ class EconDbYieldCurveFetcher( require_credentials = False @staticmethod - def transform_query(params: Dict[str, Any]) -> EconDbYieldCurveQueryParams: + def transform_query(params: dict[str, Any]) -> EconDbYieldCurveQueryParams: """Transform the query parameters.""" if not params.get("date"): params["date"] = datetime.now().strftime("%Y-%m-%d") @@ -68,133 +77,194 @@ def transform_query(params: Dict[str, Any]) -> EconDbYieldCurveQueryParams: @staticmethod async def aextract_data( # pylint: disable=R0914.R0912,R0915 query: EconDbYieldCurveQueryParams, - credentials: Optional[Dict[str, str]], + credentials: Optional[dict[str, str]], **kwargs: Any, - ) -> List[Dict]: + ) -> list[dict]: """Extract the data.""" # pylint: disable=import-outside-toplevel + import asyncio # noqa from openbb_econdb.utils import helpers + from warnings import warn token = credentials.get("econdb_api_key") if credentials else "" # type: ignore # Attempt to create a temporary token if one is not supplied. if not token: token = await helpers.create_token(use_cache=query.use_cache) credentials.update({"econdb_api_key": token}) # type: ignore - base_url = "https://www.econdb.com/api/series/?ticker=" - symbols = list(COUNTRIES_DICT[query.country].keys()) - url = ( - base_url - + f"%5B{','.join(symbols)}%5D&page_size=50&format=json&token={token}" - ) - data: List[Dict] = [] - response: Union[Dict, List[Dict]] = {} - if query.use_cache is True: - cache_dir = f"{helpers.get_user_cache_directory()}/http/econdb_yield_curve" - async with helpers.CachedSession( - cache=helpers.SQLiteBackend( - cache_dir, expire_after=3600 * 4, ignored_params=["token"] + + results: dict = {} + messages: list = [] + + async def get_one_country(country): + """Get the data for one country.""" + base_url = "https://www.econdb.com/api/series/?ticker=" + symbols = list(COUNTRIES_DICT[country].keys()) + url = ( + base_url + + f"%5B{','.join(symbols)}%5D&page_size=50&format=json&token={token}" + ) + data: list = [] + response: Union[dict, list[dict]] = {} + if query.use_cache is True: + cache_dir = ( + f"{helpers.get_user_cache_directory()}/http/econdb_yield_curve" ) - ) as session: - try: - response = await helpers.amake_request( # type: ignore - url, - session=session, - timeout=20, - **kwargs, + async with helpers.CachedSession( + cache=helpers.SQLiteBackend( + cache_dir, expire_after=3600 * 4, ignored_params=["token"] ) - finally: - await session.close() - else: - response = await helpers.amake_request( # type: ignore - url, timeout=20, **kwargs - ) - if not response: - raise OpenBBError("No data was returned.") - data = response.get("results") # type: ignore - if not data: - raise EmptyDataError("The response was returned empty.") - return data + ) as session: + await session.delete_expired_responses() + try: + response = await helpers.amake_request( # type: ignore + url, + session=session, + timeout=20, + **kwargs, + ) + finally: + await session.close() + else: + response = await helpers.amake_request( # type: ignore + url, timeout=20, **kwargs + ) + if not response: + messages.append(f"No data was returned for, {country}") + return + data = response.get("results") # type: ignore + if not data: + messages.append(f"The response for, {country}, was returned empty.") + return + results[country] = data + + return + + _countries = query.country.split(",") + + tasks = [asyncio.create_task(get_one_country(c)) for c in _countries] + await asyncio.gather(*tasks) + + if not results and messages: + msg_str = "\n".join(messages) + raise OpenBBError(msg_str) + + if not results and not messages: + raise OpenBBError("Unexpected outcome -> All requests were returned empty.") + + if results and messages: + for message in messages: + warn(message) + + return results @staticmethod def transform_data( query: EconDbYieldCurveQueryParams, - data: List[Dict], + data: list[dict], **kwargs: Any, - ) -> AnnotatedResult[List[EconDbYieldCurveData]]: + ) -> AnnotatedResult[list[EconDbYieldCurveData]]: """Transform the data.""" # pylint: disable=import-outside-toplevel + from numpy import nan from pandas import Categorical, DataFrame, DatetimeIndex - maturity_order = list(COUNTRIES_DICT[query.country].values()) - dates = query.date.split(",") # type: ignore - dates_list = DatetimeIndex(dates) - new_data: Dict = {} - metadata: Dict = {} - # Unpack the data for each maturity. - for item in data: - ticker = item.get("ticker") - maturity = COUNTRIES_DICT[query.country].get(ticker) - dataset = item.get("dataset") - additional_metadata = item.get("additional_metadata") - units = "percent" - description = item.get("description") - dates = item["data"]["dates"] - rates = item["data"]["values"] - meta = { - "symbol": ticker, - "description": description, - "dataset": dataset, - "units": units, - "additional_metadata": additional_metadata, - } - metadata[maturity] = meta - for d, r in zip(dates, rates): - if maturity not in new_data: - new_data[maturity] = {} - new_data[maturity][d] = r - - # Create a DataFrame from the data - df = DataFrame(new_data) - - # Convert the index to a DatetimeIndex - df.index = DatetimeIndex(df.index) - - # Find the nearest date in the DataFrame to each date in dates_list - def get_nearest_date(df, target_date): - differences = (df.index - target_date).to_series().abs() - nearest_date_index = differences.argmin() - nearest_date = df.index[nearest_date_index] - return nearest_date - - nearest_dates = [get_nearest_date(df, date) for date in dates_list] - - # Filter for only the nearest dates - df = df[df.index.isin(nearest_dates)] - - # Flatten the DataFrame - flattened_data = df.reset_index().melt( - id_vars="index", var_name="maturity", value_name="rate" - ) - flattened_data = flattened_data.rename(columns={"index": "date"}).sort_values( - "date" - ) - flattened_data["maturity"] = Categorical( - flattened_data["maturity"], categories=maturity_order, ordered=True - ) - flattened_data["rate"] = flattened_data["rate"].astype(float) / 100 - flattened_data = flattened_data.sort_values( - by=["date", "maturity"] - ).reset_index(drop=True) - flattened_data.loc[:, "date"] = flattened_data["date"].dt.strftime("%Y-%m-%d") - records = flattened_data.copy() - records["rate"] = records["rate"].fillna("N/A").replace("N/A", None) - records = records.to_dict("records") - results: List[EconDbYieldCurveData] = [ - EconDbYieldCurveData.model_validate(r) for r in records if r.get("rate") - ] - if not results: + results_metadata: dict = {} + results_data: list = [] + + def process_country_data(country, country_data): + """Process the data for one country.""" + maturity_order = list(COUNTRIES_DICT[country].values()) + dates = query.date.split(",") # type: ignore + dates_list = DatetimeIndex(dates) + new_data: Dict = {} + metadata: Dict = {} + # Unpack the data for each maturity. + for item in country_data: + ticker = item.get("ticker") + maturity = COUNTRIES_DICT[country].get(ticker) + dataset = item.get("dataset") + additional_metadata = item.get("additional_metadata") + units = "percent" + description = item.get("description") + dates = item["data"]["dates"] + rates = item["data"]["values"] + meta = { + "symbol": ticker, + "description": description, + "dataset": dataset, + "units": units, + "additional_metadata": additional_metadata, + } + metadata[maturity] = meta + for d, r in zip(dates, rates): + if maturity not in new_data: + new_data[maturity] = {} + new_data[maturity][d] = r + + # Create a DataFrame from the data + df = DataFrame(new_data) + + # Convert the index to a DatetimeIndex + df.index = DatetimeIndex(df.index) + + # Find the nearest date in the DataFrame to each date in dates_list + def get_nearest_date(df, target_date): + differences = (df.index - target_date).to_series().abs() + nearest_date_index = differences.argmin() + nearest_date = df.index[nearest_date_index] + return nearest_date + + nearest_dates = [get_nearest_date(df, date) for date in dates_list] + + # Filter for only the nearest dates + df = df[df.index.isin(nearest_dates)] + + # Flatten the DataFrame + flattened_data = df.reset_index().melt( + id_vars="index", var_name="maturity", value_name="rate" + ) + flattened_data = flattened_data.rename( + columns={"index": "date"} + ).sort_values("date") + flattened_data["maturity"] = Categorical( + flattened_data["maturity"], categories=maturity_order, ordered=True + ) + flattened_data["rate"] = flattened_data["rate"].astype(float) / 100 + flattened_data = flattened_data.sort_values( + by=["date", "maturity"] + ).reset_index(drop=True) + flattened_data.loc[:, "date"] = flattened_data["date"].dt.strftime( + "%Y-%m-%d" + ) + new_df = flattened_data.copy() + new_df.loc[:, "country"] = country + + def convert_duration(x): + """Convert the duration to a decimal representation of years.""" + period, unit = x.split("_") + if period == "long" and unit == "term": + return 30 + if period == "year": + return int(unit) + return int(unit) / 12 + + new_df.loc[:, "maturity_years"] = new_df.maturity.apply(convert_duration) + + new_df = new_df.replace({nan: None}) + records = [ + EconDbYieldCurveData.model_validate(r) + for r in new_df.to_dict("records") + if r.get("rate") + ] + results_data.extend(records) + results_metadata[country] = metadata + + for country, country_data in data.items(): + process_country_data(country, country_data) + + if not results_data: raise EmptyDataError( f"No data was found for the country, {query.country}," f" and dates, {query.date}" ) - return AnnotatedResult(result=results, metadata=metadata) + return AnnotatedResult(result=results_data, metadata=results_metadata) diff --git a/openbb_platform/providers/econdb/openbb_econdb/utils/yield_curves.py b/openbb_platform/providers/econdb/openbb_econdb/utils/yield_curves.py index d8d546eca8e7..ed31e15d4f2f 100644 --- a/openbb_platform/providers/econdb/openbb_econdb/utils/yield_curves.py +++ b/openbb_platform/providers/econdb/openbb_econdb/utils/yield_curves.py @@ -1,7 +1,5 @@ """EconDB Yield Curve Utilities""" -from typing import Literal - DAILY = { "australia": { "RBA_F02D.FCMYGBAG2D.D.AU": "year_2", @@ -20,7 +18,7 @@ "BOC_BOND_YIELDS_ALL.BD_CDN_5YR_DQ_YLD.D.CA": "year_5", "BOC_BOND_YIELDS_ALL.BD_CDN_7YR_DQ_YLD.D.CA": "year_7", "BOC_BOND_YIELDS_ALL.BD_CDN_10YR_DQ_YLD.D.CA": "year_10", - "BOC_BOND_YIELDS_ALL.BD_CDN_LONG_DQ_YLD.D.CA": "long_term", + "BOC_BOND_YIELDS_ALL.BD_CDN_LONG_DQ_YLD.D.CA": "year_30", }, "china": { "CCDC_CGB.CHB_3M.D.CN": "month_3", @@ -134,7 +132,56 @@ "FRB_H15.C279E.D.US": "year_20", "FRB_H15.F5280.D.US": "year_30", }, + "ecb_spot_rate": { + "ECBYC.035E9053E2.B.EA": "month_3", + "ECBYC.03610053E2.B.EA": "month_6", + "ECBYC.0354C053E2.B.EA": "year_1", + "ECBYC.035D1053E2.B.EA": "year_2", + "ECBYC.035EA053E2.B.EA": "year_3", + "ECBYC.035F7053E2.B.EA": "year_4", + "ECBYC.03604053E2.B.EA": "year_5", + "ECBYC.03611053E2.B.EA": "year_6", + "ECBYC.0361E053E2.B.EA": "year_7", + "ECBYC.0362B053E2.B.EA": "year_8", + "ECBYC.03638053E2.B.EA": "year_9", + "ECBYC.03558053E2.B.EA": "year_10", + "ECBYC.034D2053E2.B.EA": "year_20", + "ECBYC.035DD053E2.B.EA": "year_30", + }, + "ecb_par_yield": { + "ECBYC.0317C053E2.B.EA": "month_3", + "ECBYC.031A3053E2.B.EA": "month_6", + "ECBYC.030DF053E2.B.EA": "year_1", + "ECBYC.03164053E2.B.EA": "year_2", + "ECBYC.0317D053E2.B.EA": "year_3", + "ECBYC.0318A053E2.B.EA": "year_4", + "ECBYC.03197053E2.B.EA": "year_5", + "ECBYC.031A4053E2.B.EA": "year_6", + "ECBYC.031B1053E2.B.EA": "year_7", + "ECBYC.031BE053E2.B.EA": "year_8", + "ECBYC.031CB053E2.B.EA": "year_9", + "ECBYC.03065053E2.B.EA": "year_10", + "ECBYC.030EB053E2.B.EA": "year_20", + "ECBYC.03170053E2.B.EA": "year_30", + }, + "ecb_instantaneous_forward": { + "ECBYC.02C71053E2.B.EA": "month_3", + "ECBYC.02C98053E2.B.EA": "month_6", + "ECBYC.02BD4053E2.B.EA": "year_1", + "ECBYC.02C59053E2.B.EA": "year_2", + "ECBYC.02C72053E2.B.EA": "year_3", + "ECBYC.02C7F053E2.B.EA": "year_4", + "ECBYC.02C8C053E2.B.EA": "year_5", + "ECBYC.02C99053E2.B.EA": "year_6", + "ECBYC.02CA6053E2.B.EA": "year_7", + "ECBYC.02CB3053E2.B.EA": "year_8", + "ECBYC.02CC0053E2.B.EA": "year_9", + "ECBYC.02B5A053E2.B.EA": "year_10", + "ECBYC.02BE0053E2.B.EA": "year_20", + "ECBYC.02C65053E2.B.EA": "year_30", + }, } + MONTHLY = { "thailand": { "BOTFM_RT_001_S2.36.M.TH": "month_1", @@ -198,22 +245,3 @@ **DAILY, **MONTHLY, } -COUNTRIES = Literal[ - "australia", - "canada", - "china", - "hong_kong", - "india", - "japan", - "mexico", - "new_zealand", - "russia", - "saudi_arabia", - "singapore", - "south_africa", - "south_korea", - "taiwan", - "thailand", - "united_kingdom", - "united_states", -] From 1f877a048b7ae43fb79a2b8bc7e26d0ce7a65a61 Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:34:57 -0800 Subject: [PATCH 2/4] lint --- .../core/openbb_core/provider/standard_models/yield_curve.py | 2 +- .../providers/econdb/openbb_econdb/models/yield_curve.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py index c5670d38b60c..a542d02d093e 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py @@ -52,11 +52,11 @@ class YieldCurveData(Data): ) maturity: str = Field(description="Maturity length of the security.") + @property @computed_field( description="Maturity length as a decimal.", return_type=Optional[float], ) - @property def maturity_years(self) -> Optional[float]: """Get the maturity in years as a decimal.""" if self.maturity is None or "_" not in self.maturity: diff --git a/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py b/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py index 9e5456768d85..748b9a2c9a5b 100644 --- a/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py +++ b/openbb_platform/providers/econdb/openbb_econdb/models/yield_curve.py @@ -79,7 +79,7 @@ async def aextract_data( # pylint: disable=R0914.R0912,R0915 query: EconDbYieldCurveQueryParams, credentials: Optional[dict[str, str]], **kwargs: Any, - ) -> list[dict]: + ) -> dict: """Extract the data.""" # pylint: disable=import-outside-toplevel import asyncio # noqa @@ -160,7 +160,7 @@ async def get_one_country(country): @staticmethod def transform_data( query: EconDbYieldCurveQueryParams, - data: list[dict], + data: dict, **kwargs: Any, ) -> AnnotatedResult[list[EconDbYieldCurveData]]: """Transform the data.""" From ecb270336b89424f4c2a646d2b9b4c253ab70d38 Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Sat, 16 Nov 2024 13:45:02 -0800 Subject: [PATCH 3/4] do igor's suggestion --- .../provider/standard_models/yield_curve.py | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py index a542d02d093e..ee134b2891ff 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py @@ -33,9 +33,10 @@ def _validate_date(cls, v): if isinstance(v, dateType): return v.strftime("%Y-%m-%d") new_dates: list = [] + dates: list = [] if isinstance(v, str): dates = v.split(",") - if isinstance(v, list): + elif isinstance(v, list): dates = v for date in dates: new_dates.append(to_datetime(date).date().strftime("%Y-%m-%d")) @@ -52,22 +53,20 @@ class YieldCurveData(Data): ) maturity: str = Field(description="Maturity length of the security.") - @property @computed_field( - description="Maturity length as a decimal.", + description="Maturity length, in years, as a decimal.", return_type=Optional[float], ) + @property def maturity_years(self) -> Optional[float]: """Get the maturity in years as a decimal.""" - if self.maturity is None or "_" not in self.maturity: + if "_" not in self.maturity: # pylint: disable=E1135 return None - parts = self.maturity.split("_") - months = 0 - for i in range(0, len(parts), 2): - number = int(parts[i + 1]) - if parts[i] == "year": - number *= 12 - months += number + parts = self.maturity.split("_") # pylint: disable=E1101 + months = sum( + int(parts[i + 1]) * (12 if parts[i] == "year" else 1) + for i in range(0, len(parts), 2) + ) return months / 12 From 8096e62cd72812a863e35b03347aff7a5283244c Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Sat, 16 Nov 2024 14:12:30 -0800 Subject: [PATCH 4/4] mypy --- .../core/openbb_core/provider/standard_models/yield_curve.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py index ee134b2891ff..382d6c2ce076 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/yield_curve.py @@ -53,7 +53,7 @@ class YieldCurveData(Data): ) maturity: str = Field(description="Maturity length of the security.") - @computed_field( + @computed_field( # type: ignore description="Maturity length, in years, as a decimal.", return_type=Optional[float], )