summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-03-18 10:11:17 -0700
committerGitHub <noreply@github.com>2024-03-18 17:11:17 +0000
commitc5c7adeef19e357911f21dea0601219dcb2f9e24 (patch)
tree06329b6583ae7981cb4b5c3bc4e3ba16af711892
parent655b1c861fc38ff386b3d5c8b03d1d6214a0741a (diff)
[Feature] Add Charts For FRED Series (#6234)
* add fred_series charts * pylint
-rw-r--r--openbb_platform/obbject_extensions/charting/integration/test_charting_api.py33
-rw-r--r--openbb_platform/obbject_extensions/charting/integration/test_charting_python.py25
-rw-r--r--openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py7
-rw-r--r--openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py268
-rw-r--r--openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py105
-rw-r--r--openbb_platform/providers/fred/openbb_fred/models/series.py1
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")