CatPtain's picture
Upload 51 files
4ca09f3 verified
"""Charting Class implementation."""
# pylint: disable=too-many-arguments,unused-argument,too-many-positional-arguments
from typing import (
TYPE_CHECKING,
Any,
Callable,
ClassVar,
Dict,
List,
Literal,
Optional,
Tuple,
Type,
Union,
)
from warnings import warn
from importlib_metadata import entry_points
from openbb_core.app.model.charts.chart import Chart
from openbb_core.app.model.obbject import OBBject
from openbb_core.provider.abstract.data import Data
from openbb_charting.charts.helpers import (
get_charting_functions,
get_charting_functions_list,
)
if TYPE_CHECKING:
from numpy import ndarray # noqa
from pandas import DataFrame, Series # noqa
from plotly.graph_objs import Figure # noqa
from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
from openbb_charting.query_params import ChartParams # noqa
from openbb_charting.core.backend import Backend # noqa
class Charting:
"""Charting extension.
Methods
-------
show
Display chart and save it to the OBBject.
to_chart
Redraw the chart and save it to the OBBject, with an optional entry point for Data.
functions
Return a list of Platform commands with charting functions.
get_params
Return the charting parameters for the function the OBBject was created from.
indicators
Return the list of the available technical indicators to use with the `to_chart` method and OHLC+V data.
table
Display an interactive table.
create_line_chart
Create a line chart from external data.
create_bar_chart
Create a bar chart, on a single x-axis with one or more values for the y-axis, from external data.
create_correlation_matrix
Create a correlation matrix from external data.
toggle_chart_style
Toggle the chart style, of an existing chart, between light and dark mode.
"""
_extension_views: ClassVar[List[Type]] = [
entry_point.load()
for entry_point in entry_points(group="openbb_charting_extension")
]
_format = "plotly" # the charts computed by this extension will be in plotly format
def __init__(self, obbject):
"""Initialize Charting extension."""
# pylint: disable=import-outside-toplevel
import importlib # noqa
charting_settings_module = importlib.import_module(
"openbb_core.app.model.charts.charting_settings", "ChartingSettings"
)
ChartingSettings = charting_settings_module.ChartingSettings
self._obbject: OBBject = obbject
self._charting_settings = ChartingSettings(
user_settings=self._obbject._user_settings, # type: ignore
system_settings=self._obbject._system_settings, # type: ignore
)
self._backend = self._handle_backend()
self._functions: Dict[str, Callable] = self._get_functions()
@classmethod
def indicators(cls):
"""Return an instance of the IndicatorsParams class, containing all available indicators and their parameters.
Without assigning to a variable, it will print the the information to the console.
"""
# pylint: disable=import-outside-toplevel
from openbb_charting.query_params import IndicatorsParams
return IndicatorsParams()
@classmethod
def functions(cls) -> List[str]:
"""Return a list of the available functions."""
functions: List[str] = []
for view in cls._extension_views:
functions.extend(get_charting_functions_list(view))
return functions
def _get_functions(self) -> Dict[str, Callable]:
"""Return a dict with the available functions."""
functions: Dict[str, Callable] = {}
for view in self._extension_views:
functions.update(get_charting_functions(view))
return functions
def _handle_backend(self) -> "Backend":
"""Create and start the backend."""
# pylint: disable=import-outside-toplevel
from openbb_charting.core.backend import create_backend, get_backend
create_backend(self._charting_settings)
backend = get_backend()
backend.start(debug=self._charting_settings.debug_mode)
return backend
def _get_chart_function(self, route: str) -> Callable:
"""Given a route, it returns the chart function. The module must contain the given route."""
if route is None:
raise ValueError("OBBject was initialized with no function route.")
adjusted_route = route.replace("/", "_")[1:]
if adjusted_route not in self._functions:
raise ValueError(
f"Could not find the route `{adjusted_route}` in the charting functions."
)
return self._functions[adjusted_route]
def get_params(self) -> Union["ChartParams", None]:
"""Return the ChartQueryParams class for the function the OBBject was created from.
Without assigning to a variable, it will print the docstring to the console.
If the class is not defined, the help for the function will be returned.
"""
# pylint: disable=import-outside-toplevel
from openbb_charting.query_params import ChartParams
if self._obbject._route is None: # pylint: disable=protected-access
raise ValueError("OBBject was initialized with no function route.")
charting_function = (
self._obbject._route # pylint: disable=protected-access
).replace("/", "_")[1:]
if hasattr(ChartParams, charting_function):
return getattr(ChartParams, charting_function)()
return help( # type: ignore
self._get_chart_function( # pylint: disable=protected-access
self._obbject.extra[ # pylint: disable=protected-access
"metadata"
].route
)
)
def _prepare_data_as_df(
self, data: Optional[Union["DataFrame", "Series"]]
) -> Tuple["DataFrame", bool]:
"""Convert supplied data to a DataFrame."""
# pylint: disable=import-outside-toplevel
from openbb_core.app.utils import basemodel_to_df, convert_to_basemodel
from pandas import DataFrame, Series
has_data = (
isinstance(data, (Data, DataFrame, Series)) and not data.empty # type: ignore
) or (bool(data))
index = (
data.index.name
if has_data and isinstance(data, (DataFrame, Series))
else None
)
data_as_df: DataFrame = (
basemodel_to_df(convert_to_basemodel(data), index=index)
if has_data
else self._obbject.to_dataframe(index=index)
)
if "date" in data_as_df.columns:
data_as_df = data_as_df.set_index("date")
if "provider" in data_as_df.columns:
data_as_df.drop(columns="provider", inplace=True)
return data_as_df, has_data
# pylint: disable=too-many-locals
def create_line_chart(
self,
data: Union[
list,
dict,
"DataFrame",
List["DataFrame"],
"Series",
List["Series"],
"ndarray",
Data,
],
index: Optional[str] = None,
target: Optional[str] = None,
title: Optional[str] = None,
x: Optional[str] = None,
xtitle: Optional[str] = None,
y: Optional[Union[str, List[str]]] = None,
ytitle: Optional[str] = None,
y2: Optional[Union[str, List[str]]] = None,
y2title: Optional[str] = None,
layout_kwargs: Optional[dict] = None,
scatter_kwargs: Optional[dict] = None,
normalize: bool = False,
returns: bool = False,
same_axis: bool = False,
render: bool = True,
**kwargs,
) -> Union["OpenBBFigure", "Figure", None]:
"""Create a line chart from external data and render a chart or return the OpenBBFigure.
Parameters
----------
data : Union[Data, DataFrame, Series]
Data to be plotted (OHLCV data).
index : Optional[str], optional
Index column, by default None
target : Optional[str], optional
Target column to be plotted, by default None
title : Optional[str], optional
Chart title, by default None
x : Optional[str], optional
X-axis column, by default None
xtitle : Optional[str], optional
X-axis title, by default None
y : Optional[Union[str, List[str]]], optional
Y-axis column(s), by default None
If None are supplied, the layout is optimized for the contents of data.
Where many units/scales are present,
it will attempt to divide based on the range of values.
ytitle : Optional[str], optional
Y-axis title, by default None
y2 : Optional[Union[str, List[str]]], optional
Y2-axis column(s), by default None
y2title : Optional[str], optional
Y2-axis title, by default None
layout_kwargs : Optional[dict], optional
Additional Plotly Layout parameters for `fig.update_layout`, by default None
scatter_kwargs : Optional[dict], optional
Additional Plotly parameters applied on creation of each scatter plot, by default None
normalize : bool, optional
Normalize the data with Z-Score Standardization, by default False
returns : bool, optional
Convert the data to cumulative returns, by default False
same_axis: bool, optional
If True, forces all data onto the same Y-axis, by default False
render: bool, optional
If True, the chart will be rendered, by default True
**kwargs: Dict[str, Any]
Extra parameters to be passed to `figure.show()`
"""
# pylint: disable=import-outside-toplevel
from openbb_charting.charts.generic_charts import line_chart
fig = line_chart(
data=data,
index=index,
target=target,
title=title,
x=x,
xtitle=xtitle,
y=y,
ytitle=ytitle,
y2=y2,
y2title=y2title,
layout_kwargs=layout_kwargs,
scatter_kwargs=scatter_kwargs,
normalize=normalize,
returns=returns,
same_axis=same_axis,
**kwargs,
)
fig = self._set_chart_style(fig)
if render:
return fig.show(**kwargs)
return fig
def create_bar_chart(
self,
data: Union[
list,
dict,
"DataFrame",
List["DataFrame"],
"Series",
List["Series"],
"ndarray",
Data,
],
x: str,
y: Union[str, List[str]],
barmode: Literal["group", "stack", "relative", "overlay"] = "group",
xtype: Literal[
"category", "multicategory", "date", "log", "linear"
] = "category",
title: Optional[str] = None,
xtitle: Optional[str] = None,
ytitle: Optional[str] = None,
orientation: Literal["h", "v"] = "v",
colors: Optional[List[str]] = None,
layout_kwargs: Optional[Dict[str, Any]] = None,
bar_kwargs: Optional[Dict[str, Any]] = None,
render: bool = True,
**kwargs,
) -> Union["OpenBBFigure", "Figure", None]:
"""Create a bar chart on a single x-axis with one or more values for the y-axis.
Parameters
----------
data : Union[list, dict, DataFrame, List[DataFrame], Series, List[Series], ndarray, Data]
Data to plot.
x : str
The x-axis column name.
y : Union[str, List[str]]
The y-axis column name(s).
barmode : Literal["group", "stack", "relative", "overlay"], optional
The bar mode, by default "group".
xtype : Literal["category", "multicategory", "date", "log", "linear"], optional
The x-axis type, by default "category".
title : str, optional
The title of the chart, by default None.
xtitle : str, optional
The x-axis title, by default None.
ytitle : str, optional
The y-axis title, by default None.
colors: List[str], optional
Manually set the colors to cycle through for each column in 'y', by default None.
bar_kwargs : Dict[str, Any], optional
Additional keyword arguments to apply with figure.add_bar(), by default None.
layout_kwargs : Dict[str, Any], optional
Additional keyword arguments to apply with figure.update_layout(), by default None.
Returns
-------
OpenBBFigure
The OpenBBFigure object.
"""
# pylint: disable=import-outside-toplevel
from openbb_charting.charts.generic_charts import bar_chart
fig = bar_chart(
data=data,
x=x,
y=y,
barmode=barmode,
xtype=xtype,
title=title,
xtitle=xtitle,
ytitle=ytitle,
orientation=orientation,
colors=colors,
bar_kwargs=bar_kwargs,
layout_kwargs=layout_kwargs,
)
fig = self._set_chart_style(fig)
if render:
return fig.show(**kwargs)
return fig
def create_correlation_matrix(
self,
data: Union[
list[Data],
"DataFrame",
],
method: Literal["pearson", "kendall", "spearman"] = "pearson",
colorscale: str = "RdBu",
title: str = "Asset Correlation Matrix",
layout_kwargs: Optional[Dict[str, Any]] = None,
):
"""Create a correlation matrix from external data.
Parameters
----------
data : Union[list[Data], DataFrame]
Input dataset.
method : Literal["pearson", "kendall", "spearman"]
Method to use for correlation calculation. Default is "pearson".
pearson : standard correlation coefficient
kendall : Kendall Tau correlation coefficient
spearman : Spearman rank correlation
colorscale : str
Plotly colorscale to use for the heatmap. Default is "RdBu".
title : str
Title of the chart. Default is "Asset Correlation Matrix".
layout_kwargs : Dict[str, Any]
Additional keyword arguments to apply with figure.update_layout(), by default None.
Returns
-------
OpenBBFigure
The OpenBBFigure object.
"""
# pylint: disable=import-outside-toplevel
from openbb_charting.charts.correlation_matrix import correlation_matrix
kwargs = {
"data": data,
"method": method,
"colorscale": colorscale,
"title": title,
"layout_kwargs": layout_kwargs,
}
fig, _ = correlation_matrix(**kwargs)
fig = self._set_chart_style(fig)
return fig
# pylint: disable=inconsistent-return-statements
def show(self, render: bool = True, **kwargs):
"""Display chart and save it to the OBBject."""
try:
charting_function = self._get_chart_function(
self._obbject._route # pylint: disable=protected-access
)
kwargs["obbject_item"] = self._obbject.results
kwargs["charting_settings"] = self._charting_settings
kwargs["standard_params"] = (
self._obbject._standard_params # pylint: disable=protected-access
)
kwargs["extra_params"] = (
self._obbject._extra_params # pylint: disable=protected-access
)
kwargs["provider"] = self._obbject.provider
kwargs["extra"] = self._obbject.extra
fig, content = charting_function(**kwargs)
fig = self._set_chart_style(fig)
content = fig.show(external=True, **kwargs).to_plotly_json()
self._obbject.chart = Chart(fig=fig, content=content, format=self._format)
if render:
fig.show(**kwargs)
except Exception: # pylint: disable=W0718
try:
fig = self.create_line_chart(data=self._obbject.results, render=False, **kwargs) # type: ignore
fig = self._set_chart_style(fig)
content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore
self._obbject.chart = Chart(
fig=fig, content=content, format=self._format
)
if render:
return fig.show(**kwargs) # type: ignore
except Exception as e:
raise RuntimeError(
"Failed to automatically create a generic chart with the data provided."
) from e
# pylint: disable=too-many-locals,inconsistent-return-statements
def to_chart(
self,
data: Optional[
Union[
list,
dict,
"DataFrame",
List["DataFrame"],
"Series",
List["Series"],
"ndarray",
Data,
]
] = None,
target: Optional[str] = None,
index: Optional[str] = None,
indicators: Optional[Dict[str, Dict[str, Any]]] = None,
symbol: str = "",
candles: bool = True,
volume: bool = True,
volume_ticks_x: int = 7,
render: bool = True,
**kwargs,
):
"""Create an OpenBBFigure with user customizations (if any) and save it to the OBBject.
This function is used to populate, or re-populate, the OBBject with a chart using the data within
the OBBject or external data supplied via the `data` parameter.
This function modifies the original OBBject by overwriting the existing chart.
Parameters
----------
data : Union[Data, DataFrame, Series]
Data to be plotted.
indicators : Dict[str, Dict[str, Any]], optional
Indicators to be plotted, by default None
symbol : str, optional
Symbol to be plotted. This is used for labels and titles, by default ""
candles : bool, optional
If True, candles will be plotted, by default True
volume : bool, optional
If True, volume will be plotted, by default True
volume_ticks_x : int, optional
Volume ticks, by default 7
render : bool, optional
If True, the chart will be rendered, by default True
kwargs: Dict[str, Any]
Extra parameters to be passed to the chart constructor.
Examples
--------
Plotting a time series with TA indicators
>>> from openbb import obb
>>> res = obb.equity.price.historical("AAPL")
>>> indicators = dict(
>>> sma=dict(length=[20,30,50]),
>>> adx=dict(length=14),
>>> rsi=dict(length=14),
>>> macd=dict(fast=12, slow=26, signal=9),
>>> bbands=dict(length=20, std=2),
>>> stoch=dict(length=14),
>>> ema=dict(length=[20,30,50]),
>>> )
>>> res.charting.to_chart(**{"indicators": indicators})
Get all the available indicators
>>> res = obb.equity.price.historical("AAPL")
>>> indicators = res.charting.indicators()
>>> indicators?
"""
data_as_df, has_data = self._prepare_data_as_df(data) # type: ignore
if target is not None:
data_as_df = data_as_df[[target]]
kwargs["candles"] = candles
kwargs["volume"] = volume
kwargs["volume_ticks_x"] = volume_ticks_x
kwargs["indicators"] = indicators if indicators else {}
kwargs["symbol"] = symbol
kwargs["target"] = target
kwargs["index"] = index
kwargs["obbject_item"] = self._obbject.results
kwargs["charting_settings"] = self._charting_settings
kwargs["standard_params"] = (
self._obbject._standard_params # pylint: disable=protected-access
)
kwargs["extra_params"] = (
self._obbject._extra_params # pylint: disable=protected-access
)
kwargs["provider"] = self._obbject.provider # pylint: disable=protected-access
kwargs["extra"] = self._obbject.extra # pylint: disable=protected-access
try:
if has_data:
self.show(data=data_as_df, render=render, **kwargs)
else:
self.show(**kwargs, render=render)
except Exception: # pylint: disable=W0718
try:
fig = self.create_line_chart(data=data_as_df, render=False, **kwargs)
fig = self._set_chart_style(fig)
content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore
self._obbject.chart = Chart(
fig=fig, content=content, format=self._format
)
if render:
return fig.show(**kwargs) # type: ignore
except Exception as e: # pylint: disable=W0718
raise RuntimeError(
"Failed to automatically create a generic chart with the data provided."
) from e
def _set_chart_style(self, figure: "Figure"):
"""Set the user preference for light or dark mode."""
style = self._charting_settings.chart_style
font_color = "black" if style == "light" else "white"
paper_bgcolor = "white" if style == "light" else "black"
figure = figure.update_layout(
dict( # pylint: disable=R1735
font_color=font_color, paper_bgcolor=paper_bgcolor
)
)
return figure
def toggle_chart_style(self):
"""Toggle the chart style between light and dark mode."""
if not hasattr(self._obbject.chart, "fig"):
raise ValueError(
"Error: No chart has been created. Please create a chart first."
)
current = self._charting_settings.chart_style
new = "light" if current == "dark" else "dark"
self._charting_settings.chart_style = new
figure = self._obbject.chart.fig # type: ignore[union-attr]
updated_figure = self._set_chart_style(figure)
self._obbject.chart.fig = updated_figure # type: ignore[union-attr]
self._obbject.chart.content = updated_figure.show( # type: ignore[union-attr]
external=True
).to_plotly_json()
@staticmethod
def _convert_to_string(x):
"""Sanitize the data for the table."""
# pylint: disable=import-outside-toplevel
from numpy import isnan
if isinstance(x, (float, int)) and not isnan(x):
return x
if isinstance(x, dict):
return ", ".join([str(v) for v in x.values()])
if isinstance(x, list):
if all(isinstance(i, dict) for i in x):
return ", ".join(
str(", ".join([str(v) for v in i.values()])) for i in x
)
return ", ".join([str(i) for i in x])
return (
str(x)
.replace("[", "")
.replace("]", "")
.replace("'{", "")
.replace("}'", "")
.replace("nan", "")
)
def table(
self,
data: Optional[Union["DataFrame", "Series"]] = None,
title: str = "",
):
"""Display an interactive table.
Parameters
----------
data : Optional[Union[DataFrame, Series]], optional
Data to be plotted, by default None.
If no data is provided the OBBject results will be used.
title : str, optional
Title of the table, by default "".
"""
# pylint: disable=import-outside-toplevel
from pandas import RangeIndex
data_as_df, _ = self._prepare_data_as_df(data)
if isinstance(data_as_df.index, RangeIndex):
data_as_df.reset_index(inplace=True, drop=True)
else:
data_as_df.reset_index(inplace=True)
for col in data_as_df.columns:
data_as_df[col] = data_as_df[col].apply(self._convert_to_string)
if self._backend.isatty:
try:
self._backend.send_table(
df_table=data_as_df,
title=title
or self._obbject._route, # pylint: disable=protected-access
theme=self._charting_settings.table_style,
)
except Exception as e: # pylint: disable=W0718
warn(f"Failed to show figure with backend. {e}")
else:
from plotly import optional_imports
ipython_display = optional_imports.get_module("IPython.display")
if ipython_display:
ipython_display.display(ipython_display.HTML(data_as_df.to_html()))
else:
warn("IPython.display is not available.")
def url(
self,
url: str,
title: str = "",
width: Optional[int] = None,
height: Optional[int] = None,
):
"""Return the URL of the chart."""
try:
self._backend.send_url(url=url, title=title, width=width, height=height)
except Exception as e: # pylint: disable=W0718
warn(f"Failed to show figure with backend. {e}")