diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-03-18 10:11:17 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-18 17:11:17 +0000 |
commit | c5c7adeef19e357911f21dea0601219dcb2f9e24 (patch) | |
tree | 06329b6583ae7981cb4b5c3bc4e3ba16af711892 | |
parent | 655b1c861fc38ff386b3d5c8b03d1d6214a0741a (diff) |
[Feature] Add Charts For FRED Series (#6234)
* add fred_series charts
* pylint
6 files changed, 435 insertions, 4 deletions
diff --git a/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py b/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py index 2c5a8cbb47e..cacfac05daa 100644 --- a/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py +++ b/openbb_platform/obbject_extensions/charting/integration/test_charting_api.py @@ -434,3 +434,36 @@ def test_charting_technical_cones(params, headers): assert chart assert not fig assert list(chart.keys()) == ["content", "format"] + + +@parametrize( + "params", + [ + ( + { + "data": None, + "symbol": "DGS10", + "transform": "pc1", + "chart": True, + "provider": "fred", + } + ) + ], +) +@pytest.mark.integration +def test_charting_economy_fred_series(params, headers): + """Test chart ta cones.""" + 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/economy/fred_series?{query_str}" + result = requests.get(url, headers=headers, timeout=10) + assert isinstance(result, requests.Response) + assert result.status_code == 200 + + chart = result.json()["chart"] + fig = chart.pop("fig", {}) + + assert chart + assert not fig + assert list(chart.keys()) == ["content", "format"] diff --git a/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py b/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py index f15df117905..5e904803561 100644 --- a/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py +++ b/openbb_platform/obbject_extensions/charting/integration/test_charting_python.py @@ -375,3 +375,28 @@ def test_charting_technical_cones(params, obb): assert len(result.results) > 0 assert result.chart.content assert isinstance(result.chart.fig, OpenBBFigure) + + +@parametrize( + "params", + [ + ( + { + "data": None, + "symbol": "DGS10", + "transform": "pc1", + "chart": True, + "provider": "fred", + } + ) + ], +) +@pytest.mark.integration +def test_charting_economy_fred_series(params, obb): + """Test chart economy fred series.""" + result = obb.economy.fred_series(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 + assert result.chart.content + assert isinstance(result.chart.fig, OpenBBFigure) diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py b/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py index 8ca3641e437..ac585fefada 100644 --- a/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py +++ b/openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py @@ -90,7 +90,9 @@ class Charting: kwargs["standard_params"] = ( self._obbject._standard_params.__dict__ # pylint: disable=protected-access ) - + kwargs["provider"] = self._obbject.provider # pylint: disable=protected-access + kwargs["extra"] = self._obbject.extra # pylint: disable=protected-access + kwargs["warnings"] = self._obbject.warnings # pylint: disable=protected-access fig, content = charting_function(**kwargs) self._obbject.chart = Chart( fig=fig, content=content, format=charting_router.CHART_FORMAT @@ -184,6 +186,8 @@ class Charting: if has_data else self._obbject.to_dataframe() ) + if "date" in data_as_df.columns: + data_as_df = data_as_df.set_index("date") try: fig, content = to_chart( data_as_df, @@ -194,7 +198,6 @@ class Charting: prepost=prepost, volume_ticks_x=volume_ticks_x, ) - self._obbject.chart = Chart( fig=fig, content=content, format=charting_router.CHART_FORMAT ) diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py b/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py index f13ba058623..44b92a2c6c5 100644 --- a/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py +++ b/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py @@ -1,5 +1,6 @@ """Charting router.""" +import json from typing import Any, Dict, Tuple import pandas as pd @@ -9,6 +10,10 @@ from openbb_core.app.utils import basemodel_to_df from openbb_charting.core.chart_style import ChartStyle from openbb_charting.core.openbb_figure import OpenBBFigure from openbb_charting.core.plotly_ta.ta_class import PlotlyTA +from openbb_charting.query_params import ( + FredSeriesChartQueryParams, + TechnicalConesChartQueryParams, +) CHART_FORMAT = ChartFormat.plotly @@ -179,7 +184,9 @@ def technical_ema(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]: return _ta_ma(ma_type, **kwargs) -def technical_cones(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]: +def technical_cones( + **kwargs: TechnicalConesChartQueryParams, +) -> Tuple["OpenBBFigure", Dict[str, Any]]: """Volatility Cones Chart.""" data = kwargs.get("data") @@ -187,7 +194,7 @@ def technical_cones(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]: if isinstance(data, pd.DataFrame) and not data.empty and "window" in data.columns: df_ta = data.set_index("window") else: - df_ta = basemodel_to_df(kwargs["obbject_item"], index="window") + df_ta = basemodel_to_df(kwargs["obbject_item"], index="window") # type: ignore df_ta.columns = [col.title().replace("_", " ") for col in df_ta.columns] @@ -276,3 +283,260 @@ def technical_cones(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]: content = fig.to_plotly_json() return fig, content + + +def economy_fred_series( + **kwargs: FredSeriesChartQueryParams, +) -> Tuple["OpenBBFigure", Dict[str, Any]]: + """FRED Series Chart.""" + + ytitle_dict = { + "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", + } + + colors = [ + "#1f77b4", + "#7f7f7f", + "#ff7f0e", + "#2ca02c", + "#d62728", + "#9467bd", + "#8c564b", + "#e377c2", + "#7f7f7f", + "#bcbd22", + "#17becf", + ] + + provider = kwargs.get("provider") + + if provider != "fred": + raise RuntimeError( + f"This charting method does not support {provider}. Supported providers: fred." + ) + + columns = basemodel_to_df(kwargs["obbject_item"], index=None).columns.to_list() # type: ignore + + allow_unsafe = kwargs.get("allow_unsafe", False) + dropnan = kwargs.get("dropna", True) + normalize = kwargs.get("normalize", False) + + data_cols = [] + data = kwargs.get("data") + + if isinstance(data, pd.DataFrame) and not data.empty: + data_cols = data.columns.to_list() + df_ta = data + + else: + df_ta = basemodel_to_df(kwargs["obbject_item"], index="date") # type: ignore + + # Check for unsupported external data injection. + if allow_unsafe is False and data_cols: + for data_col in data_cols: + if data_col not in columns: + raise RuntimeError( + f"Column '{data_col}' was not found in the original data." + + " External data injection is not supported unless `allow_unsafe = True`." + ) + + # Align the data so each column has the same index and length. + if dropnan: + df_ta = df_ta.dropna(how="any") + + if df_ta.empty or len(df_ta) < 2: + raise ValueError( + "No data is left after dropping NaN values. Try setting `dropnan = False`," + + " or use the `frequency` parameter on request ." + ) + + columns = df_ta.columns.to_list() + + def z_score_standardization(data: pd.Series) -> pd.Series: + """Z-Score Standardization Method.""" + return (data - data.mean()) / data.std() + + if normalize: + df_ta = df_ta.apply(z_score_standardization) + + # Extract the metadata from the warnings. + warnings = kwargs.get("warnings") + metadata = json.loads(warnings[0].message) if warnings else {} # type: ignore + + # Check if the request was transformed by the FRED API. + params = kwargs["extra_params"] if kwargs.get("extra_params") else {} + has_params = hasattr(params, "transform") and params.transform is not None # type: ignore + + # Get a unique list of all units of measurement in the DataFrame. + y_units = list({metadata.get(col).get("units") for col in columns if col in metadata}) # type: ignore + + if len(y_units) > 2 and has_params is False and allow_unsafe is True: + raise RuntimeError( + "This method supports up to 2 y-axis units." + + " Please use the 'transform' parameter, in the data request," + + " to compare all series on the same scale, or set `normalize = True`." + + " Override this error by setting `allow_unsafe = True`." + ) + + y1_units = y_units[0] + + y1title = y1_units + + y2title = y_units[1] if len(y_units) > 1 else None + + xtitle = "" + + # If the request was transformed, the y-axis will be shared under these conditions. + if has_params and any( + i in params.transform for i in ["pc1", "pch", "pca", "cch", "cca", "log"] # type: ignore + ): + y1title = "Log" if params.transform == "Log" else "Percent" # type: ignore + y2title = None + + # Set the title for the chart. + if kwargs.get("title"): + title = kwargs.get("title") + else: + if metadata.get(columns[0]): + title = metadata.get(columns[0]).get("title") if len(columns) == 1 else "FRED Series" # type: ignore + else: + title = "FRED Series" + transform_title = ytitle_dict.get(params.transform) if has_params is True else "" # type: ignore + title = f"{title} - {transform_title}" if transform_title else title + + # Define this to use as a check. + y3title = "" + + # Create the figure object with subplots. + fig = OpenBBFigure().create_subplots( + rows=1, cols=1, shared_xaxes=True, shared_yaxes=False + ) + fig.update_layout(ChartStyle().plotly_template.get("layout", {})) + text_color = "black" if ChartStyle().plt_style == "light" else "white" + + # For each series in the DataFrame, add a scatter plot. + for i, col in enumerate(df_ta.columns): + + # Check if the y-axis should be shared for this series. + on_y1 = ( + ( + metadata.get(col).get("units") == y1_units + or y2title is None # type: ignore + ) + if metadata.get(col) + else False + ) + if normalize: + on_y1 = True + yaxes = "y2" if not on_y1 else "y1" + on_y3 = not metadata.get(col) and normalize is False + if on_y3: + yaxes = "y3" + y3title = df_ta[col].name + fig.add_scatter( + x=df_ta.index, + y=df_ta[col], + name=df_ta[col].name, + mode="lines", + hovertemplate=f"{df_ta[col].name}: %{{y}}<extra></extra>", + line=dict(width=1, color=colors[i % len(colors)]), + yaxis=yaxes, + ) + + # Set the y-axis titles, if supplied. + if kwargs.get("y1title"): + y1title = kwargs.get("y1title") + if kwargs.get("y2title") and y2title is not None: + y2title = kwargs.get("y2title") + # Set the x-axis title, if suppiled. + if kwargs.get("xtitle"): + xtitle = kwargs.get("xtitle") + # If the data was normalized, set the title to reflect this. + if normalize: + y1title = None + y2title = None + y3title = None + title = f"{title} - Normalized" + + # Now update the layout of the complete figure. + fig.update_layout( + title=dict(text=title, x=0.5, font=dict(size=16)), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + font=dict(color=text_color), + legend=dict( + orientation="h", + yanchor="bottom", + xanchor="right", + y=1.02, + x=1, + bgcolor="rgba(0,0,0,0)", + ), + yaxis=( + dict( + ticklen=0, + side="right", + title=dict(text=y1title, standoff=30, font=dict(size=18)), + tickfont=dict(size=14), + anchor="x", + ) + if y1title + else None + ), + yaxis2=( + dict( + overlaying="y", + side="left", + ticklen=0, + showgrid=False, + title=dict( + text=y2title if y2title else None, standoff=10, font=dict(size=18) + ), + tickfont=dict(size=14), + anchor="x", + ) + if y2title + else None + ), + yaxis3=( + dict( + overlaying="y", + side="left", + ticklen=0, + position=0, + showgrid=False, + showticklabels=True, + title=( + dict(text=y3title, standoff=10, font=dict(size=16)) + if y3title + else None + ), + tickfont=dict(size=12, color="rgba(128,128,128,0.75)"), + anchor="free", + ) + if y3title + else None + ), + xaxis=dict( + ticklen=0, + showgrid=False, + title=( + dict(text=xtitle, standoff=30, font=dict(size=18)) if xtitle else None + ), + domain=[0.095, 0.95] if y3title else None, + ), + margin=dict(r=25, l=25) if normalize is False else None, + autosize=True, + dragmode="pan", + ) + + content = fig.to_plotly_json() + + return fig, content diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py b/openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py new file mode 100644 index 00000000000..0ae69972dc3 --- /dev/null +++ b/openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py @@ -0,0 +1,105 @@ +"""Charting Extension Query Params.""" + +from typing import List, Optional, Union + +from openbb_core.provider.abstract.data import Data +from openbb_core.provider.abstract.query_params import QueryParams +from pydantic import Field + + +class FredSeriesChartQueryParams(QueryParams): + """ + FRED Series Chart Query Params. + + kwargs + ------ + + data : List[Data], optional + Filtered versions of the data contained in the original results. + Example use is to reduce the number of columns or the length of data to plot. + To supply additional columns, set `allow_unsafe = True`. + title : str, optional + Title of the chart. + y1title : str, optional + Right Y-axis title. + y2title : str, optional + Left Y-axis title. + xtitle : str, optional + X-axis title. + dropnan: bool, optional (default: True) + If True, rows containing NaN will be dropped. + normalize: bool, optional (default: False) + If True, the data will be normalized and placed on the same axis. + allow_unsafe: bool, optional (default: False) + If True, the method will attempt to pass all supplied data to the chart constructor. + This can result in unexpected behavior. + """ + + data: Optional[Union[Data, List[Data]]] = Field( + default=None, + description="Filtered versions of the data contained in the original `self.results`." + + " Columns should be the same as the original data." + + " Example use is to reduce the number of columns or the length of data to plot." + + " To supply additional columns, set `allow_unsafe = True`.", + ) + title: Optional[str] = Field( + default=None, + description="Title of the chart.", + ) + y1title: Optional[str] = Field( + default=None, + description="Right Y-axis title.", + ) + y2title: Optional[str] = Field( + default=None, + description="Left Y-axis title.", + ) + xtitle: Optional[str] = Field( + default=None, + description="X-axis title.", + ) + dropnan: bool = Field( + default=True, + description="If True, rows containing NaN will be dropped.", + ) + normalize: bool = Field( + default=False, + description="If True, the data will be normalized and placed on the same axis.", + ) + allow_unsafe: bool = Field( + default=False, + description="If True, the method will attempt to pass all supplied data to the chart constructor." + + " This can result in unexpected behavior.", + ) + + +class TechnicalConesChartQueryParams(QueryParams): + """ + Technical Cones Chart Query Params. + + kwargs + ------ + + data : List[Data], optional + Filtered versions of the data contained in the original results. + Example use is to reduce the number of windows to plot. + title : str, optional + Title of the chart. + symbol: str, optional + Symbol represented by the data. Used to label the chart. + """ + + data: Optional[Union[Data, List[Data]]] = Field( + default=None, + description="Filtered versions of the data contained in the original results." + + " Columns should be the same as the original data." + + " Example use is to reduce the number of columns or the length of data to plot.", + ) + title: Optional[str] = Field( + default=None, + description="Title of the chart.", + ) + symbol: Optional[str] = Field( + default=None, + description="Symbol represented by the data. Used to label the chart.", + ) diff --git a/openbb_platform/providers/fred/openbb_fred/models/series.py b/openbb_platform/providers/fred/openbb_fred/models/series.py index 8b4eb8cda88..35db3d0a2a5 100644 --- a/openbb_platform/providers/fred/openbb_fred/models/series.py +++ b/openbb_platform/providers/fred/openbb_fred/models/series.py @@ -195,6 +195,7 @@ class FredSeriesFetcher( """Transform data.""" results = ( pd.DataFrame(data) + .filter(items=query.symbol.split(","), axis=1) .reset_index() .rename(columns={"index": "date"}) .fillna("N/A") |