[Feature] Add Charts For FRED Series (#6234)

* add fred_series charts

* pylint
This commit is contained in:
Danglewood
2024-03-18 10:11:17 -07:00
committed by GitHub
parent 655b1c861f
commit c5c7adeef1
6 changed files with 435 additions and 4 deletions

View File

@@ -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"]

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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

View File

@@ -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.",
)

View File

@@ -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")