mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-13 04:41:24 +00:00
[Feature] Add Charts For FRED Series (#6234)
* add fred_series charts * pylint
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user