Files
OpenBB/openbb_platform/extensions/economy/openbb_economy/economy_views.py
dlee-bq 7f04334398 - example.py: mark the base Example.scope field frozen so the (#7798)
Literal[api]/Literal[python] subclass overrides are covariant
  read-only narrowings. Subclass fields stay writable at runtime.
- method_definition.py, options_chains.py: pydantic 2.14 types
  FieldInfo.annotation as TypeForm[Any] | None, which ty rejects in
  Union[]/Annotated[] and where  is expected; bind it as Any.
  Use FieldInfo.get_default(call_default_factory=True) instead of
  calling the default_factory union directly.
- economy_views.py: read data/obbject_item without None defaults in the
  IMF shipping views. charting always sets obbject_item, and data is
  only popped when present and not None.- finra, jodi, eia: annotate @asynccontextmanager/@contextmanager
  functions with AsyncGenerator/Generator instead of the deprecated
  AsyncIterator/Iterator return types.
- charting: cast the darts forecast dataset to TimeSeries before
  indexing so .all_values resolves instead of hitting ndarray.
- fred, imf, nasdaq, sec, tmx: turn off invalid-attribute-override and
  invalid-mutable-override for models/** only, following the cboe
  precedent. Provider models extend the standard models by narrowing
  query params to Literals or widening fields to Optional, which these
  rules flag by design.

Co-authored-by: dlee-bq <325002271+dlee-bq@users.noreply.github.com>
2026-10-17 20:50:39 +00:00

622 lines
22 KiB
Python

"""Views for the Economy Extension."""
from importlib import import_module
from typing import TYPE_CHECKING, Any
from warnings import warn
if TYPE_CHECKING:
from openbb_charting.core.openbb_figure import (
OpenBBFigure,
)
class EconomyViews:
"""economy Views."""
@staticmethod
def economy_fred_series(
**kwargs,
) -> tuple["OpenBBFigure", dict[str, Any]]:
"""FRED Series Chart."""
from openbb_core.app.utils import basemodel_to_df
from pandas import DataFrame
bar_chart = import_module("openbb_charting.charts.generic_charts").bar_chart
z_score_standardization = import_module(
"openbb_charting.charts.helpers"
).z_score_standardization
ChartStyle = import_module("openbb_charting.core.chart_style").ChartStyle
OpenBBFigure = import_module("openbb_charting.core.openbb_figure").OpenBBFigure
LARGE_CYCLER = import_module("openbb_charting.styles.colors").LARGE_CYCLER
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",
}
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()
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, 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")
# 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()
metadata = kwargs["extra"].get("results_metadata", {})
# 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
# 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}
)
if has_params is True and not y_units:
y_units = [ytitle_dict.get(params.transform)] # ty: ignore[unresolved-attribute]
if normalize or (
kwargs.get("bar") is True
and len(y_units) > 1
and (
has_params is False
or not any(
i in params.transform # ty: ignore[unresolved-attribute]
for i in ["pc1", "pch", "pca", "cch", "cca", "log"]
)
)
):
normalize = True
df_ta = df_ta.apply(z_score_standardization)
if len(y_units) > 2 and has_params is False and allow_unsafe is False:
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] if y_units else None
y1title = y1_units
y2title = y_units[1] if len(y_units) > 1 else None
xtitle = str(kwargs.get("xtitle", ""))
# If the request was transformed, the y-axis will be shared under these conditions.
if has_params and any(
i in params.transform # ty: ignore[unsupported-operator]
for i in ["pc1", "pch", "pca", "cch", "cca", "log"]
):
y1title = "Log" if params.transform == "Log" else "Percent"
y2title = None
# Set the title for the chart.
title: str = ""
if isinstance(kwargs, dict) and title in kwargs:
title = kwargs["title"]
else:
if metadata.get(columns[0]):
title = (
metadata.get(columns[0]).get("title")
if len(columns) == 1
else "FRED Series"
)
else:
title = "FRED Series"
transform_title = (
ytitle_dict.get(params.transform) # ty: ignore[unresolved-attribute]
if has_params is True
else ""
)
title = f"{title} - {transform_title}" if transform_title else title
# Define this to use as a check.
y3title: str | None = ""
if kwargs.get("plot_bar") is True or len(df_ta.index) < 100:
margin = dict(l=10, r=5, b=75 if xtitle else 30)
try:
if normalize:
y1title = None
title = f"{title} - Normalized" if title else "Normalized"
bar_mode = kwargs.get("barmode", "group")
fig = bar_chart(
df_ta.reset_index(),
"date",
df_ta.columns.to_list(),
title=title,
xtitle=xtitle,
ytitle=y1title,
barmode=bar_mode,
layout_kwargs=dict(margin=margin),
)
if kwargs.get("layout_kwargs"):
fig.update_layout(kwargs.get("layout_kwargs"))
if kwargs.get("title"):
fig.set_title(str(kwargs.get("title"))) # ty: ignore[unresolved-attribute]
content = fig.to_plotly_json()
return fig, content # ty: ignore[invalid-return-type]
except Exception as _:
warn("Bar chart failed. Attempting line chart.")
# 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 = "white" if ChartStyle().plt_style == "dark" else "black"
# 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
or kwargs.get("same_axis") is True
)
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=2, color=LARGE_CYCLER[i % len(LARGE_CYCLER)]),
yaxis="y1" if kwargs.get("same_axis") is True else 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 isinstance(kwargs, dict) and "xtitle" in kwargs:
xtitle = kwargs["xtitle"]
# If the data was normalized, set the title to reflect this.
if normalize:
y1title = None
y2title = None
y3title = None
title = f"{title} - Normalized" if title else "Normalized"
# Now update the layout of the complete figure.
fig.update_layout(
title=dict(text=title, x=0.5, font=dict(size=16)),
legend=dict(
orientation="h",
yanchor="bottom",
xanchor="right",
y=1.02,
x=0.95,
bgcolor=(
"rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
),
font=dict(size=12),
),
yaxis=(
dict(
ticklen=0,
side="right",
showline=True,
mirror=True,
title=dict(text=y1title, standoff=30, font=dict(size=16)),
tickfont=dict(size=14),
anchor="x",
gridcolor="rgba(128,128,128,0.3)",
)
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=16),
),
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.9)"),
anchor="free",
)
if y3title
else None
),
xaxis=dict(
ticklen=0,
showgrid=True,
showline=True,
mirror=True,
title=(
dict(text=xtitle, standoff=30, font=dict(size=16))
if xtitle
else None
),
gridcolor="rgba(128,128,128,0.3)",
domain=[0.095, 0.95] if y3title else None,
),
margin=(
dict(r=25, l=25, b=75 if xtitle else 30) if normalize is False else None
),
font=dict(color=text_color),
autosize=True,
dragmode="pan",
)
if kwargs.get("layout_kwargs"):
fig.update_layout(kwargs.get("layout_kwargs"))
if kwargs.get("title"):
fig.set_title(str(kwargs.get("title")))
content = fig.to_plotly_json()
return fig, content
@staticmethod
def economy_survey_bls_series(
**kwargs,
) -> tuple["OpenBBFigure", dict[str, Any]]:
"""Economy Survey BLS Series Chart.
Parameters
----------
data: Optional[Union[DataFrame, List[Data]]]
Filtered subset of the parent results.
target_symbol: Optional[str]
The target symbol(s) to plot. Plot multiple symbols by separating them with a comma. Max 10 symbols.
target_col: Optional[str]
The target column to plot. Default is 'value'.
plot_type: Literal["line", "bar"]
The type of plot to display. Default is 'line', unless the data is significantly small.
normalize: bool
Normalize the data before displaying. Default is False.
title: Optional[str]
The title of the chart.
xtitle: Optional[str]
The title of the x-axis.
ytitle: Optional[str]
The title of the y-axis.
bar_kwargs: Optional[dict]
Additional keyword arguments applied to `fig.add_bar`.
scatter_kwargs: Optional[dict]
Additional keyword arguments applied to `fig.add_scatter`.
layout_kwargs: Optional[dict]
Additional keyword arguments applied to `fig.update_layout`.
"""
from openbb_core.app.utils import basemodel_to_df
from pandas import DataFrame
_generic_charts = import_module("openbb_charting.charts.generic_charts")
bar_chart = _generic_charts.bar_chart
line_chart = _generic_charts.line_chart
z_score_standardization = import_module(
"openbb_charting.charts.helpers"
).z_score_standardization
provider = kwargs.get("provider")
if provider != "bls":
raise RuntimeError(
f"This charting method does not support {provider}. Supported providers: bls."
)
_data = (
kwargs.pop("data", None)
if "data" in kwargs and kwargs["data"] is not None
else kwargs.get("obbject_item")
)
df = DataFrame()
if isinstance(_data, DataFrame) and not _data.empty:
df = _data.reset_index() if _data.index.name == "date" else _data
else:
try:
df = basemodel_to_df(_data, index=None) # ty: ignore[invalid-argument-type]
except Exception as e:
raise RuntimeError("Unable to process supplied data.") from e
if df.empty or len(df) < 2:
raise RuntimeError("No data found to plot.")
cols = df.columns.to_list()
target_col = kwargs.get("target_col", "value")
if target_col not in cols:
raise RuntimeError(f"Column '{target_col}' not found in the data.")
new_df = df.pivot(columns="symbol", values=target_col, index="date")
target_symbols = kwargs.get("target_symbol", "").split(",")[:10]
if not target_symbols or len(target_symbols) == 0 or target_symbols[0] == "":
target_symbols = new_df.columns.to_list()[:10]
metadata = kwargs["extra"].get("results_metadata", {})
ytitle = kwargs.get("ytitle", "")
new_df = new_df.filter(target_symbols, axis=1)
if "percent" in target_col.lower():
ytitle = (
ytitle
if ytitle
else target_col.replace("change_percent_", "").replace("M", " Month")
+ " Change (%)"
)
new_df = new_df.apply(lambda x: x * 100)
elif "change" in target_col.lower() and "percent" not in target_col.lower():
ytitle = (
ytitle
if ytitle
else target_col.replace("change_", "").replace("M", " Month")
+ " Change"
)
title_map: dict = {}
for symbol in target_symbols:
if symbol not in new_df.columns:
continue
survey_name = metadata.get(symbol, {}).get("survey_name", symbol)
series_title = metadata.get(symbol, {}).get("series_title", symbol)
if survey_name != series_title:
title_map[symbol] = f"{survey_name} \n {series_title}"
normalize = kwargs.get("normalize", False)
same_axis = kwargs.get("same_axis", False)
if normalize:
new_df = new_df.apply(z_score_standardization)
same_axis = True
if ytitle:
ytitle = f"Normalized {ytitle.replace('(%)', '')}"
plot_type = kwargs.get("plot_type")
if plot_type is None:
plot_type = (
"line"
if (len(new_df.index) > 36 and len(new_df.columns.to_list()) >= 1)
else "bar"
)
layout_kwargs: dict = kwargs.pop("layout_kwargs", {})
scatter_kwargs: dict = kwargs.pop("scatter_kwargs", {})
bar_kwargs: dict = kwargs.pop("bar_kwargs", {})
hovertemplate = scatter_kwargs.pop("hovertemplate", None)
trace_titles = {
symbol: metadata.get(symbol, {})
.get("series_title", symbol)
.replace(",", " -")
for symbol in target_symbols
}
new_df.columns = [trace_titles.get(col, col) for col in new_df.columns]
scatter_kwargs["hovertemplate"] = (
hovertemplate if hovertemplate else "%{fullData.name}:%{y}<extra></extra>"
)
if len(target_symbols) == 1:
title = title_map.get(target_symbols[0], target_symbols[0])
fig = (
line_chart(
data=new_df,
title=title,
ytitle=ytitle,
y=list(trace_titles.values()),
scatter_kwargs=scatter_kwargs,
layout_kwargs=layout_kwargs,
**kwargs,
)
if plot_type == "line"
else bar_chart(
data=new_df,
title=title,
ytitle=ytitle,
x=new_df.index,
y=list(trace_titles.values()),
layout_kwargs=layout_kwargs,
bar_kwargs=bar_kwargs,
**kwargs,
)
)
else:
survey_name = (
metadata.get(target_symbols[0], {})
.get("survey_name", target_symbols[0])
.split("\n")[0]
.strip()
)
_t = kwargs.pop("title", None)
title = _t if _t else f"{survey_name} - {ytitle}" if ytitle else survey_name
fig = (
line_chart(
data=new_df,
y=list(trace_titles.values()),
title=title,
ytitle=ytitle,
same_axis=same_axis,
normalize=False,
scatter_kwargs=scatter_kwargs,
layout_kwargs=layout_kwargs,
**kwargs,
)
if plot_type == "line"
else bar_chart(
data=new_df,
title=title,
ytitle=ytitle,
x=new_df.index,
y=list(trace_titles.values()),
layout_kwargs=layout_kwargs,
bar_kwargs=bar_kwargs,
**kwargs,
)
)
fig.update_layout(
margin=dict(b=20),
legend=dict(
orientation="h",
yanchor="top",
xanchor="left",
y=-0.075,
x=0,
font=dict(size=12),
),
)
content = fig.to_plotly_json()
return fig, content # ty: ignore[invalid-return-type]
@staticmethod
def economy_shipping_chokepoint_info(
**kwargs,
) -> tuple["OpenBBFigure", dict[str, Any]]:
"""Maritime Chokepoint Info Chart."""
provider = kwargs.get("provider")
if provider != "imf":
raise RuntimeError(
f"This charting method does not support {provider}. Supported providers: imf."
)
try:
plot_chokepoint_annual_avg_vessels = import_module(
"openbb_imf.views.maritime_chokepoint_info"
).plot_chokepoint_annual_avg_vessels
except Exception as e:
raise RuntimeError("Unable to import the required module.") from e
theme = (
kwargs.get("extra_params", {}).get("theme")
or kwargs.get("theme")
or getattr(kwargs["charting_settings"], "chart_style", "dark")
)
data = (
kwargs.pop("data")
if "data" in kwargs and kwargs["data"] is not None
else kwargs["obbject_item"]
)
fig = plot_chokepoint_annual_avg_vessels(data, theme=theme)
fig.update_layout(
margin=dict(l=25, r=25, t=50, b=0),
)
content = fig.to_plotly_json()
content["config"] = dict(responsive=False)
return fig, content
@staticmethod
def economy_shipping_port_info(
**kwargs,
) -> tuple["OpenBBFigure", dict[str, Any]]:
"""Port Info Chart."""
provider = kwargs.get("provider")
if provider != "imf":
raise RuntimeError(
f"This charting method does not support {provider}. Supported providers: imf."
)
try:
plot_port_info_map = import_module(
"openbb_imf.views.port_info"
).plot_port_info_map
except Exception as e:
raise RuntimeError("Unable to import the required module.") from e
data = (
kwargs.pop("data")
if "data" in kwargs and kwargs["data"] is not None
else kwargs["obbject_item"]
)
fig = plot_port_info_map(data)
fig.update_layout(
margin=dict(l=0, r=0, t=0, b=0),
)
content = fig.to_plotly_json()
content["config"] = dict(
responsive=False,
displayModeBar=False,
dragMode="pan",
doubleClick="reset",
)
return fig, content