diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-06-30 09:45:43 -0700 |
---|---|---|
committer | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-06-30 09:45:43 -0700 |
commit | 5458c6679d90cbc03e371d5f43a9f3ab2b31d1b5 (patch) | |
tree | c218bfa95ca7ae392c8a3b6a6e9efa128125779d | |
parent | e84b5a5be5009c5a6f0dea03a18b1894d04aa8cc (diff) |
futures curve chart with multiple dates allowed
4 files changed, 217 insertions, 91 deletions
diff --git a/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_views.py b/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_views.py index 2965e176a75..522df6a3b1f 100644 --- a/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_views.py +++ b/openbb_platform/extensions/derivatives/openbb_derivatives/derivatives_views.py @@ -1,10 +1,9 @@ """Views for the Derivatives Extension.""" -from typing import Any, Dict, Tuple +from typing import TYPE_CHECKING, Any, Dict, Tuple -from openbb_charting.charts.futures_curve import futures_curve -from openbb_charting.charts.price_historical import price_historical -from openbb_charting.core.openbb_figure import OpenBBFigure +if TYPE_CHECKING: + from openbb_charting.core.openbb_figure import OpenBBFigure class DerivativesViews: @@ -13,13 +12,213 @@ class DerivativesViews: @staticmethod def derivatives_futures_historical( # noqa: PLR0912 **kwargs, - ) -> Tuple[OpenBBFigure, Dict[str, Any]]: + ) -> Tuple["OpenBBFigure", Dict[str, Any]]: """Get Derivatives Futures Historical Chart.""" + # pylint: disable=import-outside-toplevel + from openbb_charting.charts.price_historical import price_historical + return price_historical(**kwargs) @staticmethod def derivatives_futures_curve( # noqa: PLR0912 **kwargs, - ) -> Tuple[OpenBBFigure, Dict[str, Any]]: - """Get Derivatives Futures Curve Chart.""" - return futures_curve(**kwargs) + ) -> Tuple["OpenBBFigure", Dict[str, Any]]: + """Futures curve chart. All parameters are optional, and are kwargs. + Parameters can be directly accessed from the function end point by + entering as a nested dictionary to the 'chart_params' key. + + From the API, `chart_params` must be passed as a JSON in the request body with `extra_params`. + + If using the chart post-request, the parameters are passed directly + as `key=value` pairs in the `charting.to_chart` or `charting.show` methods. + + Parameters + ---------- + data : Optional[Union[List[Data], DataFrame]] + Data for the chart. Required fields are: 'expiration' and 'price'. + Multiple dates will be plotted on the same chart. + If not supplied, the original OBBject.results will be used. + If a DataFrame is supplied, flat data is expected, without a set index. + title: Optional[str] + Title for the chart. If not supplied, a default title will be used. + colors: Optional[List[str]] + List of colors to use for the chart. If not supplied, the default colorway will be used. + Colors should be in hex format, or named Plotly colors. Invalid colors will raise a Plotly error. + layout_kwargs: Optional[Dict[str, Any]] + Additional layout parameters for the chart, passed directly to `figure.update_layout` before output. + See Plotly documentation for available options. + + Returns + ------- + Tuple[OpenBBFigure, Dict[str, Any]] + Tuple with the OpenBBFigure object, and the JSON-serialized content. + If using the API, only the JSON content will be returned. + + Examples + -------- + ```python + from openbb import obb + data = obb.derivatives.futures.curve(symbol="vx", provider="cboe", date=["2020-03-31", "2024-06-28"], chart=True) + data.show() + ``` + + Redraw the chart, from the same data, with a custom colorway and title: + + ```python + data.charting.to_chart(colors=["green", "red"], title="VIX Futures Curve - 2020 vs. 2024") + ``` + """ + # pylint: disable=import-outside-toplevel + from openbb_charting.core.chart_style import ChartStyle + from openbb_charting.core.openbb_figure import OpenBBFigure + from openbb_charting.styles.colors import LARGE_CYCLER + from openbb_core.app.model.abstract.error import OpenBBError + from openbb_core.provider.abstract.data import Data + from pandas import DataFrame, to_datetime + + data = kwargs.get("data", None) + symbol = kwargs.get("standard_params", {}).get("symbol", "") + df: DataFrame = DataFrame() + if data: + if isinstance(data, DataFrame) and not data.empty: # noqa: SIM108 + df = data + elif isinstance(data, (list, Data)): + df = DataFrame([d.model_dump(exclude_none=True, exclude_unset=True) for d in data]) # type: ignore + else: + pass + else: + df = DataFrame( + [ + d.model_dump(exclude_none=True, exclude_unset=True) # type: ignore + for d in kwargs["obbject_item"] + ] + if isinstance(kwargs.get("obbject_item"), list) + else kwargs["obbject_item"].model_dump(exclude_none=True, exclude_unset=True) # type: ignore + ) + + if df.empty: + raise OpenBBError("Error: No data to plot.") + + if "expiration" not in df.columns: + raise OpenBBError("Expiration field not found in the data.") + + if "price" not in df.columns: + raise ValueError("Price field not found in the data.") + + provider = kwargs.get("provider", "") + + df["expiration"] = [ + to_datetime(d).strftime("%b-%y") if d != "Current" else d + for d in df["expiration"] + ] + + if ( + provider == "cboe" + and "date" in df.columns + and len(df["date"].unique()) > 1 + and "symbol" in df.columns + ): + df["expiration"] = df.symbol + + # Use a complete list of expirations to categorize the x-axis across all dates. + expirations = df["expiration"].unique().tolist() + + # Use the supplied colors, if any. + colors = kwargs.get("colors", []) + if not colors: + colors = LARGE_CYCLER + color_count = 0 + + figure = OpenBBFigure().create_subplots(shared_xaxes=True) + figure.update_layout(ChartStyle().plotly_template.get("layout", {})) + + def create_fig(figure, df, dates, color_count): + """Create a scatter for each date in the data.""" + for date in dates: + color = colors[color_count % len(colors)] + plot_df = df[df["date"].astype(str) == date].copy() + plot_df = plot_df.drop( + columns=["date"] if "date" in plot_df.columns else [] + ).rename(columns={"expiration": "Expiration", "price": "Price"}) + figure.add_scatter( + x=plot_df["Expiration"], + y=plot_df["Price"], + # fill=fill, + mode="lines+markers", + name=date, + line=dict(width=3, color=color), + marker=dict(size=10, color=color), + hovertemplate=( + "Expiration: %{x}<br>Price: $%{y}<extra></extra>" + if len(dates) == 1 + else "%{fullData.name}<br>Expiration: %{x}<br>Price: $%{y}<extra></extra>" + ), + ) + color_count += 1 + return figure, color_count + + dates = ( + df.date.astype(str).unique().tolist() + if "date" in df.columns + else ["Current"] + ) + figure, color_count = create_fig(figure, df, dates, color_count) + + # Set the title for the chart + title: str = "" + if provider == "cboe": + vx_eod_symbols = ["vx", "vix", "vx_eod", "^vix"] + title = ( + "VIX EOD Futures Curve" + if symbol.lower() in vx_eod_symbols + else "VIX Mid-Morning TWAP Futures Curve" + ) + if len(dates) == 1 and dates[0] != "Current": + title = f"{title} for {dates[0]}" + elif provider == "yfinance": + title = f"{symbol.upper()} Futures Curve" + + # Use the supplied title, if any. + title = kwargs.get("title", title) + + # Update the layout of the figure. + figure.update_layout( + title=dict(text=title, x=0.5, font=dict(size=20)), + plot_bgcolor="rgba(255,255,255,0)", + xaxis=dict( + title="", + ticklen=0, + showgrid=False, + type="category", + categoryorder="array", + categoryarray=expirations, + ), + yaxis=dict( + title="Price ($)", + ticklen=0, + showgrid=True, + gridcolor="rgba(128,128,128,0.3)", + ), + legend=dict( + orientation="v", + yanchor="top", + xanchor="right", + y=0.95, + x=0, + xref="paper", + font=dict(size=12), + bgcolor="rgba(0,0,0,0)", + ), + margin=dict( + b=10, + t=10, + ), + ) + + layout_kwargs = kwargs.get("layout_kwargs", {}) + if layout_kwargs: + figure.update_layout(layout_kwargs) + + content = figure.show(external=True).to_plotly_json() + + return figure, content diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/charting.py b/openbb_platform/obbject_extensions/charting/openbb_charting/charting.py index efe4672533d..e757be90869 100644 --- a/openbb_platform/obbject_extensions/charting/openbb_charting/charting.py +++ b/openbb_platform/obbject_extensions/charting/openbb_charting/charting.py @@ -121,10 +121,11 @@ class Charting: ) return self._functions[adjusted_route] - def get_params(self) -> ChartParams: + def get_params(self) -> Union[ChartParams, Any]: """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. """ if self._obbject._route is None: # pylint: disable=protected-access raise ValueError("OBBject was initialized with no function route.") @@ -133,9 +134,14 @@ class Charting: ).replace("/", "_")[1:] if hasattr(ChartParams, charting_function): return getattr(ChartParams, charting_function)() - raise ValueError( - f"Error: No chart parameters are defined for the route: {charting_function}" - ) + else: + return help( + 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[pd.DataFrame, pd.Series]] diff --git a/openbb_platform/obbject_extensions/charting/openbb_charting/charts/futures_curve.py b/openbb_platform/obbject_extensions/charting/openbb_charting/charts/futures_curve.py deleted file mode 100644 index 719e805af48..00000000000 --- a/openbb_platform/obbject_extensions/charting/openbb_charting/charts/futures_curve.py +++ /dev/null @@ -1,61 +0,0 @@ -"""Yield curve chart.""" - -from typing import Any, Dict, Tuple, Union - -import pandas as pd -from openbb_core.app.utils import basemodel_to_df -from plotly.graph_objs import Figure - -from openbb_charting.charts.generic_charts import line_chart -from openbb_charting.core.openbb_figure import OpenBBFigure - - -def futures_curve( - **kwargs, -) -> Tuple[Union[OpenBBFigure, Figure], Dict[str, Any]]: # noqa: PLR0912 - """Futures curve chart.""" - if "data" in kwargs and isinstance(kwargs["data"], pd.DataFrame): - data = kwargs["data"] - elif "data" in kwargs and isinstance(kwargs["data"], list): - data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "symbol")) # type: ignore - else: - data = basemodel_to_df( - kwargs["obbject_item"], index=kwargs.get("index", "symbol") # type: ignore - ) - if not isinstance(data, pd.DataFrame): - raise ValueError("Data must be a pandas DataFrame") - - # Check for required columns - required_columns = {"expiration", "price", "symbol"} - if not required_columns.issubset(data.columns): - missing = required_columns - set(data.columns) - raise ValueError(f"Missing columns in the DataFrame: {missing}") - - data["expiration"] = pd.to_datetime(data["expiration"]) - - layout_kwargs: Dict[str, Any] = kwargs.get("layout_kwargs", {}) - title = kwargs.pop("title", "Futures Curve Chart") - orientation = kwargs.pop("orientation", "v") - - ytitle = kwargs.pop("ytitle", "Price") - xtitle = kwargs.pop("xtitle", "Expiration Date") - - fig = line_chart( - data=data, - x="expiration", - y="price", - title=title, - xtitle=xtitle, - ytitle=ytitle, - orientation=orientation, - layout_kwargs=layout_kwargs, - **kwargs, - ) - - fig.update_traces( - hovertemplate="Symbol: %{text}<br>Expiration: %{x|%Y-%m-%d}<br>Price: %{y:.2f}", - text=data["symbol"], - ) - content = {"plotly_json": fig.to_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 index 3eb92d55fbd..4d95aabf84b 100644 --- a/openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py +++ b/openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py @@ -116,23 +116,6 @@ class EtfHoldingsChartQueryParams(ChartQueryParams): ) -class FuturesCurveChartQueryParams(ChartQueryParams): - """ETF Holdings Chart Query Params.""" - - title: Optional[str] = Field( - default=None, - description="Title of the chart.", - ) - orientation: Literal["v", "h"] = Field( - default="v", - description="Orientation of the bars.", - ) - layout_kwargs: Optional[Dict[str, Any]] = Field( - default=None, - description="Additional keyword arguments to pass to the Plotly `update_layout` method.", - ) - - class EquityPriceHistoricalChartQueryParams(ChartQueryParams): """Equity Historical Price Chart Query Params.""" @@ -413,7 +396,6 @@ class ChartParams: """Chart Query Params.""" crypto_price_historical = EquityPriceHistoricalChartQueryParams - derivatives_futures_curve = FuturesCurveChartQueryParams derivatives_futures_historical = EquityPriceHistoricalChartQueryParams equity_price_historical = EquityPriceHistoricalChartQueryParams economy_fred_series = EconomyFredSeriesChartQueryParams |