mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-09 10:51:57 +00:00
Feature/charting obbject (#5938)
* fix circular import issues on the obbject extension framework * settings available on the obbject * extension init * charting assets and code * introduce to_chart functionality * removes the to_chart method from obbject in detriment of the one from the obbject extension * Update openbb_platform/obbject_extensions/charting/charting/__init__.py Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com> * fix on the show method * adding the example to the to_chart docstring * copying the exsting README from charting extension - needs to be revamped * moving to openbb_charting instead * update the pyproject.toml file * pydocstyle * versions * better docstrings * making accessors public * removing unnecessary credentials variable cc @montezdesousa * removing access to extra/metadata and use designated private attrs instead * Fixes linting for the logging service (#5992) * lints * typing.Type instead * deprecating ChartingService to use the new extension instead - adding function discovery capabilities using it * tests for the obbject charting extension * removing charting service from command runner * filter functions not implemented in the module * remove charting service tests * removing charting service on the tests for int tests * charting integration tests * removing charting service file * removing old charting extension code * revamp builder * fix obbject tests * fix command_runner tests * removing openbb_figure_table * better website docs * reviewed readme * unsetting api key for posthog (pywry) windows * added debug_mode to the system settings * removing api key * update nightly script * fix tests * black * black * Revert "black" This reverts commit21d76ad689. * Revert "black" This reverts commit0904653fd7. * black for henrique * returning a list instead * better docstrings * removing unnecessary indicators docstring * typo * black * intentional access to protected member * changes to publish script * filtering deleted files * black * adding render argument * fixing the df_to_basemodel function: it should include the index in the columns either if flagged or if the index is named * adding the hability to add you own data to to_chart method * tests * black * disable protected access * accept kwargs to eg. save image * adjustment cc Igor * add if statement for protection * remove unused return typing * fix technical charting views * better error message cc @IgorWounds * better structure and fix test * Revert "better structure and fix test" This reverts commit612ea6117c. * Revert "better error message cc @IgorWounds" This reverts commit3d7ee53878. --------- Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com> Co-authored-by: Danglewood <85772166+deeleeramone@users.noreply.github.com> Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> Co-authored-by: James Maslek <jmaslek11@gmail.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
"""Commands: generates the command map."""
|
||||
|
||||
import inspect
|
||||
from functools import partial, wraps
|
||||
from inspect import Parameter, Signature, signature
|
||||
@@ -5,7 +7,6 @@ from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
|
||||
|
||||
from fastapi import APIRouter, Depends, Header
|
||||
from fastapi.routing import APIRoute
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_core.app.command_runner import CommandRunner
|
||||
from openbb_core.app.model.command_context import CommandContext
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
@@ -18,6 +19,13 @@ from openbb_core.env import Env
|
||||
from pydantic import BaseModel
|
||||
from typing_extensions import Annotated, ParamSpec
|
||||
|
||||
try:
|
||||
from openbb_charting import Charting
|
||||
|
||||
CHARTING_INSTALLED = True
|
||||
except ImportError:
|
||||
CHARTING_INSTALLED = False
|
||||
|
||||
T = TypeVar("T")
|
||||
P = ParamSpec("P")
|
||||
|
||||
@@ -57,10 +65,7 @@ def build_new_signature(path: str, func: Callable) -> Signature:
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
path.replace("/", "_")[1:]
|
||||
in ChartingService.get_implemented_charting_functions()
|
||||
):
|
||||
if CHARTING_INSTALLED and path.replace("/", "_")[1:] in Charting.functions():
|
||||
new_parameter_list.append(
|
||||
Parameter(
|
||||
"chart",
|
||||
@@ -104,6 +109,7 @@ def build_new_signature(path: str, func: Callable) -> Signature:
|
||||
def validate_output(c_out: OBBject) -> OBBject:
|
||||
"""
|
||||
Validate OBBject object.
|
||||
|
||||
Checks against the OBBject schema and removes fields that contain the
|
||||
`exclude_from_api` extra `pydantic.Field` kwarg.
|
||||
Note that the modification to the `OBBject` object is done in-place.
|
||||
|
||||
@@ -1,287 +0,0 @@
|
||||
"""Charting service."""
|
||||
|
||||
from importlib import import_module
|
||||
from inspect import getmembers, getsource, isfunction
|
||||
from typing import Callable, List, Optional, Tuple, TypeVar
|
||||
|
||||
from openbb_core.app.extension_loader import ExtensionLoader
|
||||
from openbb_core.app.model.abstract.singleton import SingletonMeta
|
||||
from openbb_core.app.model.charts.chart import Chart, ChartFormat
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from openbb_core.env import Env
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# this is needed because static assets and api endpoints are built before any user is instantiated
|
||||
EXTENSION_NAME = Env().CHARTING_EXTENSION
|
||||
|
||||
|
||||
class ChartingServiceError(Exception):
|
||||
"""Charting service error."""
|
||||
|
||||
|
||||
class ChartingService(metaclass=SingletonMeta):
|
||||
"""
|
||||
Charting Service.
|
||||
|
||||
It is responsible for retrieving and executing the charting function, corresponding
|
||||
to a given route, from the user's preferred charting extension.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user_settings : UserSettings
|
||||
User settings.
|
||||
system_settings : SystemSettings
|
||||
System settings.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
_charting_extension : str
|
||||
Charting extension name, which is retrieved from the user preferences.
|
||||
_charting_extension_installed : bool
|
||||
Either charting extension is installed or not.
|
||||
|
||||
Raises
|
||||
------
|
||||
ChartingServiceError
|
||||
If charting extension is not installed.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_settings: Optional[UserSettings] = None,
|
||||
system_settings: Optional[SystemSettings] = None,
|
||||
) -> None:
|
||||
"""Initializes ChartingService."""
|
||||
# Although the __init__ method states that both the user_settings and the system_settings
|
||||
# are optional, they are actually required for the first initialization of the ChartingService.
|
||||
# This is because the ChartingService is a singleton and it is initialized only once.
|
||||
# We want to be able to call the ChartingService from other parts of the code without having
|
||||
# to pass the user_settings and the system_settings every time.
|
||||
if not user_settings or not system_settings:
|
||||
raise ChartingServiceError(
|
||||
"User settings and system settings must be provided."
|
||||
)
|
||||
|
||||
self._charting_settings = ChartingSettings(user_settings, system_settings)
|
||||
self._charting_extension = self._check_and_get_charting_extension_name(
|
||||
user_settings.preferences.charting_extension
|
||||
)
|
||||
self._charting_extension_installed = self._check_charting_extension_installed(
|
||||
self._charting_extension
|
||||
)
|
||||
|
||||
@property
|
||||
def charting_settings(self) -> ChartingSettings:
|
||||
"""Gets charting settings."""
|
||||
return self._charting_settings
|
||||
|
||||
@charting_settings.setter
|
||||
def charting_settings(self, value: Tuple[SystemSettings, UserSettings]):
|
||||
system_settings, user_settings = value
|
||||
self._charting_settings = ChartingSettings(
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _check_and_get_charting_extension_name(
|
||||
user_preferences_charting_extension: str,
|
||||
):
|
||||
"""Checks if the charting extension defined on user preferences is the same as the one defined in the env file."""
|
||||
if user_preferences_charting_extension != EXTENSION_NAME:
|
||||
raise ChartingServiceError(
|
||||
f"The charting extension defined on user preferences must be the same as the one defined in the env file."
|
||||
f"diff: {user_preferences_charting_extension} != {EXTENSION_NAME}"
|
||||
)
|
||||
return user_preferences_charting_extension
|
||||
|
||||
@staticmethod
|
||||
def _check_charting_extension_installed(ext_name: str) -> bool:
|
||||
"""Checks if a given extension is installed."""
|
||||
extension = ExtensionLoader().get_core_entry_point(ext_name) or False
|
||||
return extension and ext_name == extension.name # type: ignore
|
||||
|
||||
@staticmethod
|
||||
def _handle_backend(charting_extension: str, charting_settings: ChartingSettings):
|
||||
"""
|
||||
Handles the backend of the given charting extension.
|
||||
|
||||
This function that the module expose in its root (__init__.py) the following functions:
|
||||
- `create_backend(charting_settings: ChartingSettings)`
|
||||
- `get_backend()`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
charting_extension : str
|
||||
Charting extension name.
|
||||
charting_settings : ChartingSettings
|
||||
Charting settings.
|
||||
"""
|
||||
# Dynamically import the backend module
|
||||
backend_module = import_module(charting_extension)
|
||||
|
||||
create_backend_func = getattr(backend_module, "create_backend")
|
||||
get_backend_func = getattr(backend_module, "get_backend")
|
||||
|
||||
create_backend_func(charting_settings=charting_settings)
|
||||
get_backend_func().start(debug=charting_settings.debug_mode)
|
||||
|
||||
@classmethod
|
||||
def _get_extension_router(cls, extension_name: str):
|
||||
"""Get the module of the given extension."""
|
||||
extension = ExtensionLoader().get_core_entry_point(extension_name)
|
||||
if not extension or extension_name != extension.name:
|
||||
raise ChartingServiceError(
|
||||
f"Extension '{extension_name}' is not installed."
|
||||
)
|
||||
return import_module(extension.module)
|
||||
|
||||
@classmethod
|
||||
def _get_chart_format(cls, extension_name: str) -> ChartFormat:
|
||||
"""
|
||||
Given an extension name, it returns the chart format.
|
||||
|
||||
The module must contain the `CHART_FORMAT` attribute.
|
||||
"""
|
||||
module = cls._get_extension_router(extension_name)
|
||||
return getattr(module, "CHART_FORMAT")
|
||||
|
||||
@classmethod
|
||||
def _get_chart_function(cls, extension_name: str, route: str) -> Callable:
|
||||
"""
|
||||
Given an extension name and a route, it returns the chart function.
|
||||
|
||||
The module must contain the given route.
|
||||
"""
|
||||
adjusted_route = route.replace("/", "_")[1:]
|
||||
module = cls._get_extension_router(extension_name)
|
||||
return getattr(module, adjusted_route)
|
||||
|
||||
@classmethod
|
||||
def get_implemented_charting_functions(
|
||||
cls, extension_name: str = EXTENSION_NAME
|
||||
) -> List[str]:
|
||||
"""Given an extension name, it returns the implemented charting functions from its router."""
|
||||
implemented_functions = []
|
||||
|
||||
try:
|
||||
if module := cls._get_extension_router(extension_name):
|
||||
module_name = module.__name__
|
||||
|
||||
for name, obj in getmembers(module, isfunction):
|
||||
if (
|
||||
obj.__module__ == module_name
|
||||
and not name.startswith("_")
|
||||
and "NotImplementedError" not in getsource(obj)
|
||||
):
|
||||
implemented_functions.append(name)
|
||||
except ChartingServiceError:
|
||||
pass
|
||||
|
||||
return implemented_functions
|
||||
|
||||
def to_chart(self, **kwargs) -> Chart:
|
||||
"""
|
||||
Returns the chart object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs
|
||||
Keyword arguments to be passed to the charting extension.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Chart
|
||||
Chart object.
|
||||
|
||||
Raises
|
||||
------
|
||||
ChartingServiceError
|
||||
If charting extension is not installed.
|
||||
Exception
|
||||
If the charting extension module does not contain the `to_chart` function.
|
||||
"""
|
||||
if not self._charting_extension_installed:
|
||||
raise ChartingServiceError(
|
||||
f"Charting extension `{self._charting_extension}` is not installed"
|
||||
)
|
||||
self._handle_backend(self._charting_extension, self._charting_settings)
|
||||
|
||||
# Dynamically import the charting module
|
||||
backend_module = import_module(self._charting_extension)
|
||||
# Get the `to_chart` function from the charting module
|
||||
to_chart_func = getattr(backend_module, "to_chart")
|
||||
|
||||
# Add the charting settings to the kwargs
|
||||
kwargs["charting_settings"] = self._charting_settings
|
||||
|
||||
fig, content = to_chart_func(**kwargs)
|
||||
|
||||
return Chart(
|
||||
content=content,
|
||||
format=self._get_chart_format(self._charting_extension),
|
||||
fig=fig,
|
||||
)
|
||||
|
||||
def chart(
|
||||
self,
|
||||
user_settings: UserSettings,
|
||||
system_settings: SystemSettings,
|
||||
route: str,
|
||||
obbject_item: T,
|
||||
**kwargs,
|
||||
) -> Chart:
|
||||
"""
|
||||
Given a route and an obbject item, it returns the chart object.
|
||||
|
||||
If the charting extension is not installed, an error is raised.
|
||||
Otherwise, a charting function will be retrieved and executed from the user's preferred charting extension.
|
||||
This function assumes that, in order to successfully retrieve the charting function,
|
||||
the charting extension uses the following naming convention to convert routes into charting functions:
|
||||
- Route: `/stocks/load`
|
||||
- Charting function: `stocks_load()`
|
||||
- Route: `/ta/ema`
|
||||
- Charting function: `ta_ema()`
|
||||
Note that the route should be in its original format, since it will be converted inside this function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
user_settings : UserSettings
|
||||
User settings.
|
||||
route : str
|
||||
Route name, example: `/stocks/load`.
|
||||
obbject_item
|
||||
Command output item.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Chart
|
||||
Chart object.
|
||||
"""
|
||||
self._charting_settings = ChartingSettings(user_settings, system_settings)
|
||||
self._charting_extension = user_settings.preferences.charting_extension
|
||||
self._charting_extension_installed = self._check_charting_extension_installed(
|
||||
self._charting_extension
|
||||
)
|
||||
|
||||
if not self._charting_extension_installed:
|
||||
raise ChartingServiceError(
|
||||
f"Charting extension `{self._charting_extension}` is not installed"
|
||||
)
|
||||
|
||||
self._handle_backend(self._charting_extension, self._charting_settings)
|
||||
|
||||
kwargs["obbject_item"] = obbject_item
|
||||
kwargs["charting_settings"] = self._charting_settings
|
||||
|
||||
charting_function = self._get_chart_function(self._charting_extension, route)
|
||||
fig, content = charting_function(**kwargs)
|
||||
|
||||
return Chart(
|
||||
content=content,
|
||||
format=self._get_chart_format(self._charting_extension),
|
||||
fig=fig,
|
||||
)
|
||||
@@ -10,7 +10,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from pydantic import ConfigDict, create_model
|
||||
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_core.app.logs.logging_service import LoggingService
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.abstract.warning import cast_warning
|
||||
@@ -273,22 +272,14 @@ class StaticCommandRunner:
|
||||
def _chart(
|
||||
cls,
|
||||
obbject: OBBject,
|
||||
user_settings: UserSettings,
|
||||
system_settings: SystemSettings,
|
||||
route: str,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Create a chart from the command output."""
|
||||
cs = ChartingService(
|
||||
user_settings=user_settings, system_settings=system_settings
|
||||
)
|
||||
obbject.chart = cs.chart(
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
route=route,
|
||||
obbject_item=obbject.results,
|
||||
**kwargs,
|
||||
)
|
||||
if "charting" not in obbject.accessors:
|
||||
raise OpenBBError(
|
||||
"Charting is not installed. Please install `openbb-charting`."
|
||||
)
|
||||
obbject.charting.show(render=False, **kwargs) # type: ignore
|
||||
|
||||
@classmethod
|
||||
async def _execute_func(
|
||||
@@ -339,13 +330,13 @@ class StaticCommandRunner:
|
||||
kwargs=kwargs,
|
||||
show_warnings=user_settings.preferences.show_warnings,
|
||||
)
|
||||
# pylint: disable=protected-access
|
||||
obbject._route = route
|
||||
obbject._standard_params = kwargs.get("standard_params", None)
|
||||
|
||||
if chart and obbject.results:
|
||||
cls._chart(
|
||||
obbject=obbject,
|
||||
user_settings=user_settings,
|
||||
system_settings=system_settings,
|
||||
route=route,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
@@ -424,10 +415,6 @@ class CommandRunner:
|
||||
self._system_settings = system_settings or SystemService().system_settings
|
||||
self._user_settings = user_settings or UserService.read_default_user_settings()
|
||||
|
||||
self._charting_service = ChartingService(
|
||||
system_settings=self._system_settings, user_settings=self._user_settings
|
||||
)
|
||||
|
||||
def init_logging_service(self) -> None:
|
||||
"""Initialize the logging service."""
|
||||
_ = LoggingService(
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Logging Service Module."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from enum import Enum
|
||||
from types import TracebackType
|
||||
from typing import Any, Callable, Dict, Optional, Tuple, Type, cast
|
||||
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union, cast
|
||||
|
||||
from openbb_core.app.logs.formatters.formatter_with_exceptions import (
|
||||
FormatterWithExceptions,
|
||||
@@ -58,7 +60,8 @@ class LoggingService(metaclass=SingletonMeta):
|
||||
user_settings: UserSettings,
|
||||
) -> None:
|
||||
"""
|
||||
Logging Manager Constructor
|
||||
Logging Service Constructor.
|
||||
|
||||
Sets up the logging settings and handlers and then logs the startup information.
|
||||
|
||||
Parameters
|
||||
@@ -140,9 +143,7 @@ class LoggingService(metaclass=SingletonMeta):
|
||||
route: Optional[str] = None,
|
||||
custom_headers: Optional[Dict[str, Any]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Log startup information.
|
||||
"""
|
||||
"""Log startup information."""
|
||||
|
||||
def check_credentials_defined(credentials: Dict[str, Any]):
|
||||
class CredentialsDefinition(Enum):
|
||||
@@ -184,9 +185,10 @@ class LoggingService(metaclass=SingletonMeta):
|
||||
route: str,
|
||||
func: Callable,
|
||||
kwargs: Dict[str, Any],
|
||||
exec_info: Optional[
|
||||
Tuple[Type[BaseException], BaseException, Optional[TracebackType]]
|
||||
] = None,
|
||||
exec_info: Union[
|
||||
Tuple[Type[BaseException], BaseException, TracebackType],
|
||||
Tuple[None, None, None],
|
||||
],
|
||||
custom_headers: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Charting settings."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -9,11 +11,14 @@ from openbb_core.env import Env
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
class ChartingSettings:
|
||||
"""Charting settings."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
user_settings: Optional[UserSettings] = None,
|
||||
system_settings: Optional[SystemSettings] = None,
|
||||
):
|
||||
"""Initialize charting settings."""
|
||||
user_settings = user_settings or UserSettings()
|
||||
system_settings = system_settings or SystemSettings()
|
||||
|
||||
@@ -29,7 +34,7 @@ class ChartingSettings:
|
||||
self.python_version: str = system_settings.python_version
|
||||
self.test_mode = system_settings.test_mode
|
||||
self.app_id: str = get_app_id(user_data_directory)
|
||||
self.debug_mode: bool = Env().DEBUG_MODE
|
||||
self.debug_mode: bool = system_settings.debug_mode or Env().DEBUG_MODE
|
||||
self.headless: bool = system_settings.headless
|
||||
# User
|
||||
self.plot_enable_pywry: bool = user_settings.preferences.plot_enable_pywry
|
||||
|
||||
@@ -52,7 +52,7 @@ class Extension:
|
||||
)
|
||||
setattr(cls, name, CachedAccessor(name, accessor))
|
||||
# pylint: disable=protected-access
|
||||
cls._accessors.add(name)
|
||||
cls.accessors.add(name)
|
||||
return accessor
|
||||
|
||||
return decorator
|
||||
|
||||
@@ -4,6 +4,7 @@ from re import sub
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
Dict,
|
||||
Generic,
|
||||
@@ -17,17 +18,18 @@ from typing import (
|
||||
|
||||
import pandas as pd
|
||||
from numpy import ndarray
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, PrivateAttr
|
||||
|
||||
from openbb_core.app.model.abstract.error import OpenBBError
|
||||
from openbb_core.app.model.abstract.tagged import Tagged
|
||||
from openbb_core.app.model.abstract.warning import Warning_
|
||||
from openbb_core.app.model.charts.chart import Chart
|
||||
from openbb_core.app.query import Query
|
||||
from openbb_core.app.utils import basemodel_to_df
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openbb_core.app.query import Query
|
||||
|
||||
try:
|
||||
from polars import DataFrame as PolarsDataFrame # type: ignore
|
||||
except ImportError:
|
||||
@@ -39,6 +41,10 @@ T = TypeVar("T")
|
||||
class OBBject(Tagged, Generic[T]):
|
||||
"""OpenBB object."""
|
||||
|
||||
accessors: ClassVar[Set[str]] = set()
|
||||
_user_settings: ClassVar[Optional[BaseModel]] = None
|
||||
_system_settings: ClassVar[Optional[BaseModel]] = None
|
||||
|
||||
results: Optional[T] = Field(
|
||||
default=None,
|
||||
description="Serializable results.",
|
||||
@@ -59,8 +65,12 @@ class OBBject(Tagged, Generic[T]):
|
||||
default_factory=dict,
|
||||
description="Extra info.",
|
||||
)
|
||||
_credentials: ClassVar[Optional[BaseModel]] = None
|
||||
_accessors: ClassVar[Set[str]] = set()
|
||||
_route: str = PrivateAttr(
|
||||
default=None,
|
||||
)
|
||||
_standard_params: Optional[Dict[str, Any]] = PrivateAttr(
|
||||
default_factory=dict,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Human readable representation of the object."""
|
||||
@@ -268,48 +278,16 @@ class OBBject(Tagged, Generic[T]):
|
||||
del results["index"]
|
||||
return results
|
||||
|
||||
def to_chart(self, **kwargs):
|
||||
"""
|
||||
Create or update the `Chart`.
|
||||
|
||||
This function assumes that the provided data is a time series, if it's not, it will
|
||||
most likely result in an Exception.
|
||||
|
||||
Note that the `chart` attribute is composed by: `content`, `format` and `fig`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs
|
||||
Keyword arguments to be passed to the charting extension.
|
||||
This implies that the user has some knowledge on the charting extension API.
|
||||
This is the case because the charting extension may vary on user preferences.
|
||||
|
||||
Returns
|
||||
-------
|
||||
chart.fig
|
||||
The chart figure.
|
||||
"""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
# Avoids circular import
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
|
||||
cs = ChartingService()
|
||||
kwargs["data"] = self.to_dataframe()
|
||||
|
||||
self.chart = cs.to_chart(**kwargs)
|
||||
return self.chart.fig
|
||||
|
||||
def show(self):
|
||||
def show(self, **kwargs: Any) -> None:
|
||||
"""Display chart."""
|
||||
# pylint: disable=no-member
|
||||
if not self.chart or not self.chart.fig:
|
||||
raise OpenBBError(
|
||||
"Chart not found. "
|
||||
"Please compute the chart first by using the `chart=True` argument."
|
||||
)
|
||||
self.chart.fig.show()
|
||||
raise OpenBBError("Chart not found.")
|
||||
show_function: Callable = getattr(self.chart.fig, "show")
|
||||
show_function(**kwargs)
|
||||
|
||||
@classmethod
|
||||
async def from_query(cls, query: Query) -> "OBBject":
|
||||
async def from_query(cls, query: "Query") -> "OBBject":
|
||||
"""Create OBBject from query.
|
||||
|
||||
Parameters
|
||||
|
||||
@@ -48,6 +48,7 @@ class SystemSettings(Tagged):
|
||||
api_settings: FastAPISettings = Field(default_factory=FastAPISettings)
|
||||
|
||||
# Others
|
||||
debug_mode: bool = False
|
||||
test_mode: bool = False
|
||||
headless: bool = False
|
||||
|
||||
@@ -87,7 +88,7 @@ class SystemSettings(Tagged):
|
||||
def validate_posthog_handler(cls, values: "SystemSettings") -> "SystemSettings":
|
||||
"""If the user has enabled log collection, then we need to add the Posthog."""
|
||||
if (
|
||||
not any([values.test_mode, values.logging_suppress])
|
||||
not any([values.test_mode, values.debug_mode, values.logging_suppress])
|
||||
and values.log_collect
|
||||
and "posthog" not in values.logging_handlers
|
||||
):
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""System service."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
@@ -18,11 +20,10 @@ class SystemService(metaclass=SingletonMeta):
|
||||
"headless",
|
||||
"logging_sub_app",
|
||||
"api_settings",
|
||||
"debug_mode",
|
||||
}
|
||||
|
||||
PRO_VALIDATION_HASH = (
|
||||
"300ac59fdcc8f899e0bc5c18cda8652220735da1a00e2af365efe9d8e5fe8306"
|
||||
)
|
||||
PRO_VALIDATION_HASH = "300ac59fdcc8f899e0bc5c18cda8652220735da1a00e2af365efe9d8e5fe8306" # pragma: allowlist secret
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -11,7 +11,8 @@ class Container:
|
||||
|
||||
def __init__(self, command_runner: CommandRunner) -> None:
|
||||
self._command_runner = command_runner
|
||||
OBBject._credentials = command_runner.user_settings.credentials
|
||||
OBBject._user_settings = command_runner.user_settings
|
||||
OBBject._system_settings = command_runner.system_settings
|
||||
|
||||
def _run(self, *args, **kwargs) -> Any:
|
||||
"""Run a command in the container."""
|
||||
|
||||
@@ -34,7 +34,6 @@ from pydantic_core import PydanticUndefined
|
||||
from starlette.routing import BaseRoute
|
||||
from typing_extensions import Annotated, _AnnotatedAlias
|
||||
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_core.app.extension_loader import ExtensionLoader, OpenBBGroups
|
||||
from openbb_core.app.model.custom_parameter import OpenBBCustomParameter
|
||||
from openbb_core.app.provider_interface import ProviderInterface
|
||||
@@ -44,6 +43,13 @@ from openbb_core.app.static.utils.linters import Linters
|
||||
from openbb_core.env import Env
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
|
||||
try:
|
||||
from openbb_charting import Charting
|
||||
|
||||
CHARTING_INSTALLED = True
|
||||
except ImportError:
|
||||
CHARTING_INSTALLED = False
|
||||
|
||||
DataProcessingSupportedTypes = TypeVar(
|
||||
"DataProcessingSupportedTypes",
|
||||
list,
|
||||
@@ -533,10 +539,7 @@ class MethodDefinition:
|
||||
|
||||
parameter_map.pop("cc", None)
|
||||
# we need to add the chart parameter here bc of the docstring generation
|
||||
if (
|
||||
path.replace("/", "_")[1:]
|
||||
in ChartingService.get_implemented_charting_functions()
|
||||
):
|
||||
if CHARTING_INSTALLED and path.replace("/", "_")[1:] in Charting.functions():
|
||||
parameter_map["chart"] = Parameter(
|
||||
name="chart",
|
||||
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
||||
@@ -706,10 +709,7 @@ class MethodDefinition:
|
||||
parameter_map.pop("cc", None)
|
||||
code = ""
|
||||
|
||||
if (
|
||||
path.replace("/", "_")[1:]
|
||||
in ChartingService.get_implemented_charting_functions()
|
||||
):
|
||||
if CHARTING_INSTALLED and path.replace("/", "_")[1:] in Charting.functions():
|
||||
parameter_map["chart"] = Parameter(
|
||||
name="chart",
|
||||
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Utility functions for the OpenBB Core app."""
|
||||
|
||||
import ast
|
||||
import json
|
||||
from typing import Dict, Iterable, List, Optional, Union
|
||||
@@ -43,8 +45,10 @@ def df_to_basemodel(
|
||||
df: Union[pd.DataFrame, pd.Series], index: bool = False
|
||||
) -> List[Data]:
|
||||
"""Convert from a Pandas DataFrame to list of BaseModel."""
|
||||
if index and not isinstance(df.index, pd.MultiIndex):
|
||||
df = df.reset_index(drop=True)
|
||||
is_multiindex = isinstance(df.index, pd.MultiIndex)
|
||||
|
||||
if not is_multiindex and (index or df.index.name):
|
||||
df = df.reset_index()
|
||||
if isinstance(df, pd.Series):
|
||||
df = df.to_frame()
|
||||
|
||||
@@ -120,6 +124,7 @@ def get_target_column(df: pd.DataFrame, target: str) -> pd.Series:
|
||||
|
||||
|
||||
def get_target_columns(df: pd.DataFrame, target_columns: List[str]) -> pd.DataFrame:
|
||||
"""Get target columns from time series data."""
|
||||
df_result = pd.DataFrame()
|
||||
for target in target_columns:
|
||||
df_result[target] = get_target_column(df, target).to_frame()
|
||||
@@ -127,6 +132,7 @@ def get_target_columns(df: pd.DataFrame, target_columns: List[str]) -> pd.DataFr
|
||||
|
||||
|
||||
def get_user_cache_directory() -> str:
|
||||
"""Get user cache directory."""
|
||||
file = SystemSettings().model_dump()["user_settings_path"]
|
||||
with open(file) as settings_file:
|
||||
contents = settings_file.read()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for the OBBject class."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
@@ -321,45 +321,6 @@ def test_to_dict(results, expected_dict):
|
||||
assert str(exc_info.value) == str(expected_dict)
|
||||
|
||||
|
||||
@patch("openbb_core.app.model.obbject.OBBject.to_dataframe")
|
||||
@patch("openbb_core.app.charting_service.ChartingService")
|
||||
def test_to_chart_with_new_chart(
|
||||
mock_charting_service,
|
||||
mock_to_dataframe,
|
||||
):
|
||||
"""Test helper."""
|
||||
|
||||
def get_mock_dataframe():
|
||||
data = {
|
||||
"col1": [1, 2, 3],
|
||||
"col2": ["a", "b", "c"],
|
||||
"col3": [True, False, True],
|
||||
}
|
||||
|
||||
return pd.DataFrame(data)
|
||||
|
||||
# Arrange
|
||||
mock_instance = OBBject()
|
||||
mock_charting_service_instance = mock_charting_service.return_value
|
||||
mock_charting_service_instance.to_chart.return_value = Chart(
|
||||
content={"content": "some_new_content"}, fig={"fig": "some_mock_fig"}
|
||||
)
|
||||
mock_to_dataframe.return_value = get_mock_dataframe()
|
||||
|
||||
# Act
|
||||
result = mock_instance.to_chart()
|
||||
|
||||
# Assert
|
||||
assert result == {"fig": "some_mock_fig"}
|
||||
assert mock_instance.chart.content == {"content": "some_new_content"}
|
||||
|
||||
# Ensure self.to_dataframe() was called
|
||||
mock_to_dataframe.assert_called_once()
|
||||
|
||||
# Ensure ChartingService was called with the correct parameters
|
||||
mock_charting_service_instance.to_chart.assert_called_once()
|
||||
|
||||
|
||||
def test_show_chart_exists():
|
||||
"""Test helper."""
|
||||
mock_instance = OBBject()
|
||||
@@ -372,7 +333,7 @@ def test_show_chart_exists():
|
||||
mock_instance.show()
|
||||
|
||||
# Assert
|
||||
mock_instance.chart.fig.show.assert_called_once()
|
||||
mock_instance.chart.fig.show.assert_called_once() # pylint: disable=no-member
|
||||
|
||||
|
||||
def test_show_chart_no_chart():
|
||||
|
||||
@@ -90,6 +90,7 @@ def test_create_openbb_directory_directory_exists_system_settings_missing(tmpdir
|
||||
# Test case: test_mode is True, logging_suppress is True
|
||||
(
|
||||
{
|
||||
"debug_mode": True,
|
||||
"test_mode": True,
|
||||
"logging_suppress": True,
|
||||
"log_collect": True,
|
||||
@@ -100,6 +101,7 @@ def test_create_openbb_directory_directory_exists_system_settings_missing(tmpdir
|
||||
# Test case: test_mode is False, logging_suppress is True
|
||||
(
|
||||
{
|
||||
"debug_mode": False,
|
||||
"test_mode": False,
|
||||
"logging_suppress": True,
|
||||
"log_collect": True,
|
||||
@@ -111,6 +113,7 @@ def test_create_openbb_directory_directory_exists_system_settings_missing(tmpdir
|
||||
# and "posthog" handler is not present in logging_handlers
|
||||
(
|
||||
{
|
||||
"debug_mode": False,
|
||||
"test_mode": False,
|
||||
"logging_suppress": False,
|
||||
"log_collect": True,
|
||||
@@ -122,6 +125,7 @@ def test_create_openbb_directory_directory_exists_system_settings_missing(tmpdir
|
||||
# and "posthog" handler is already present in logging_handlers
|
||||
(
|
||||
{
|
||||
"debug_mode": False,
|
||||
"test_mode": False,
|
||||
"logging_suppress": False,
|
||||
"log_collect": True,
|
||||
|
||||
@@ -1,384 +0,0 @@
|
||||
from typing import Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.charting_service import (
|
||||
Chart,
|
||||
ChartingService,
|
||||
ChartingServiceError,
|
||||
)
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_and_teardown():
|
||||
# Code to run before each test function
|
||||
yield # This is where the test function runs
|
||||
# Code to run after each test function
|
||||
ChartingService._instances = {}
|
||||
|
||||
|
||||
# fixture for system_settings
|
||||
@pytest.fixture()
|
||||
def system_settings():
|
||||
return SystemSettings()
|
||||
|
||||
|
||||
# fixture for user_settings
|
||||
@pytest.fixture()
|
||||
def user_settings():
|
||||
return UserSettings()
|
||||
|
||||
|
||||
# fixture for charting_service
|
||||
@pytest.fixture()
|
||||
def charting_service(user_settings, system_settings):
|
||||
return ChartingService(user_settings=user_settings, system_settings=system_settings)
|
||||
|
||||
|
||||
def test_charting_service(user_settings, system_settings):
|
||||
assert ChartingService(user_settings=user_settings, system_settings=system_settings)
|
||||
|
||||
|
||||
def test_charting_service_singleton_prop(user_settings, system_settings):
|
||||
cm_1 = ChartingService(user_settings=user_settings, system_settings=system_settings)
|
||||
cm_2 = ChartingService(user_settings=user_settings, system_settings=system_settings)
|
||||
|
||||
assert cm_1 is cm_2
|
||||
|
||||
|
||||
def test_charting_settings():
|
||||
sys = SystemSettings(test_mode=True)
|
||||
user = UserSettings(preferences={"plot_enable_pywry": False})
|
||||
|
||||
cm = ChartingService(user_settings=user, system_settings=sys)
|
||||
|
||||
assert cm.charting_settings.plot_enable_pywry is False
|
||||
assert cm.charting_settings.test_mode is True
|
||||
|
||||
|
||||
def test_charting_settings_setter(charting_service):
|
||||
sys = SystemSettings(test_mode=True)
|
||||
user = UserSettings(preferences={"plot_enable_pywry": False})
|
||||
|
||||
charting_service.charting_settings = (sys, user)
|
||||
|
||||
assert charting_service.charting_settings.plot_enable_pywry is False
|
||||
assert charting_service.charting_settings.test_mode is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_preferences_charting_extension, expected_result",
|
||||
[
|
||||
("openbb_charting", "openbb_charting"), # Matching extension names
|
||||
("openbb_charting", ChartingServiceError), # Mismatching extension names
|
||||
],
|
||||
)
|
||||
def test_check_and_get_charting_extension_name(
|
||||
system_settings, user_preferences_charting_extension, expected_result
|
||||
):
|
||||
user = UserSettings(
|
||||
preferences={"charting_extension": user_preferences_charting_extension}
|
||||
)
|
||||
cm = ChartingService(user_settings=user, system_settings=system_settings)
|
||||
|
||||
if expected_result == ChartingServiceError:
|
||||
with pytest.raises(ChartingServiceError) as exc_info, patch(
|
||||
"openbb_core.app.charting_service.EXTENSION_NAME", "other_extension"
|
||||
):
|
||||
cm._check_and_get_charting_extension_name(
|
||||
user_preferences_charting_extension
|
||||
)
|
||||
assert str(exc_info.value) == (
|
||||
"The charting extension defined on user preferences must be "
|
||||
"the same as the one defined in the env file."
|
||||
f"diff: {user_preferences_charting_extension} != openbb_charting"
|
||||
)
|
||||
else:
|
||||
with patch(
|
||||
"openbb_core.app.charting_service.EXTENSION_NAME", "openbb_charting"
|
||||
):
|
||||
result = cm._check_and_get_charting_extension_name(
|
||||
user_preferences_charting_extension
|
||||
)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@patch("openbb_core.app.charting_service.ExtensionLoader")
|
||||
@pytest.mark.parametrize(
|
||||
"entry_point_name, extension_name, expected_result",
|
||||
[
|
||||
(
|
||||
"mock_extension",
|
||||
"mock_extension",
|
||||
True,
|
||||
), # Extension is installed and names match
|
||||
(
|
||||
"mock_extension",
|
||||
"another_extension",
|
||||
False,
|
||||
), # Extension is installed but names don't match
|
||||
(None, "mock_extension", False), # Extension is not installed
|
||||
],
|
||||
)
|
||||
def test_check_charting_extension_installed(
|
||||
mock_extension_loader,
|
||||
charting_service,
|
||||
entry_point_name,
|
||||
extension_name,
|
||||
expected_result,
|
||||
):
|
||||
class MockEntryPoint:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
# Set the return value of get_core_entry_point
|
||||
if entry_point_name is not None:
|
||||
mock_extension_loader.return_value.get_core_entry_point.return_value = (
|
||||
MockEntryPoint(entry_point_name)
|
||||
)
|
||||
else:
|
||||
mock_extension_loader.return_value.get_core_entry_point.return_value = None
|
||||
|
||||
# Call the function and assert the result
|
||||
result = charting_service._check_charting_extension_installed(extension_name)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"extension_name, expected_result",
|
||||
[
|
||||
("mock_extension", "mock_module"),
|
||||
("another_extension", ChartingServiceError),
|
||||
],
|
||||
)
|
||||
@patch("openbb_core.app.charting_service.import_module")
|
||||
@patch("openbb_core.app.charting_service.ExtensionLoader")
|
||||
def test_get_extension_router(
|
||||
mock_extension_loader,
|
||||
mock_import_module,
|
||||
charting_service,
|
||||
extension_name,
|
||||
expected_result,
|
||||
):
|
||||
class MockEntryPoint:
|
||||
def __init__(self, name, module):
|
||||
self.name = name
|
||||
self.module = module
|
||||
|
||||
mock_extension_loader.return_value.get_core_entry_point.return_value = (
|
||||
MockEntryPoint("mock_extension", "mock_module")
|
||||
)
|
||||
mock_import_module.return_value = "mock_module"
|
||||
|
||||
if expected_result == ChartingServiceError:
|
||||
with pytest.raises(ChartingServiceError) as exc_info:
|
||||
charting_service._get_extension_router(extension_name)
|
||||
assert str(exc_info.value) == (
|
||||
f"Extension '{extension_name}' is not installed."
|
||||
)
|
||||
else:
|
||||
result = charting_service._get_extension_router("mock_extension")
|
||||
assert result == "mock_module"
|
||||
|
||||
|
||||
@patch("openbb_core.app.charting_service.import_module")
|
||||
def test_handle_back_end(mock_import_module, charting_service):
|
||||
class MockBackend:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def start(self, debug):
|
||||
pass
|
||||
|
||||
class MockBackendModule:
|
||||
def __init__(self):
|
||||
self._backend = None
|
||||
|
||||
def create_backend(self, charting_settings):
|
||||
self._backend = MockBackend()
|
||||
|
||||
def get_backend(self):
|
||||
return self._backend
|
||||
|
||||
mock_import_module.return_value = MockBackendModule()
|
||||
assert (
|
||||
charting_service._handle_backend(
|
||||
"mock_extension", charting_service.charting_settings
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@patch("openbb_core.app.charting_service.ChartingService._get_extension_router")
|
||||
def test_get_chart_format(mock_get_extension_router, charting_service):
|
||||
class MockChartingRouterModule:
|
||||
CHART_FORMAT = "mock_chart_format"
|
||||
|
||||
mock_get_extension_router.return_value = MockChartingRouterModule()
|
||||
|
||||
assert charting_service._get_chart_format("mock_extension") == "mock_chart_format"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"route, expected_result",
|
||||
[
|
||||
("/stocks/load", ...),
|
||||
("/crypto/load", ...),
|
||||
("/ta/ema", ...),
|
||||
("wrong/route", AttributeError),
|
||||
],
|
||||
)
|
||||
@patch("openbb_core.app.charting_service.ChartingService._get_extension_router")
|
||||
def test_get_chart_function(
|
||||
mock_get_extension_router, charting_service, route, expected_result
|
||||
):
|
||||
class MockChartingRouterModule:
|
||||
def stocks_load(self):
|
||||
pass
|
||||
|
||||
def crypto_load(self):
|
||||
pass
|
||||
|
||||
def ta_ema(self):
|
||||
pass
|
||||
|
||||
mock_get_extension_router.return_value = MockChartingRouterModule()
|
||||
|
||||
if expected_result != AttributeError:
|
||||
assert isinstance(
|
||||
charting_service._get_chart_function("mock_extension", route), Callable
|
||||
)
|
||||
else:
|
||||
with pytest.raises(AttributeError) as exc_info:
|
||||
charting_service._get_chart_function("mock_extension", route)
|
||||
assert str(exc_info.value) == f"Attribute '{route}' not found."
|
||||
|
||||
|
||||
@pytest.mark.parametrize("charting_router_module", [(...), (None)])
|
||||
@patch("openbb_core.app.charting_service.ChartingService._get_extension_router")
|
||||
def test_get_implemented_charting_functions(
|
||||
mock_get_extension_router, charting_service, charting_router_module
|
||||
):
|
||||
if not charting_router_module:
|
||||
mock_get_extension_router.return_value = None
|
||||
assert charting_service.get_implemented_charting_functions() == []
|
||||
|
||||
else:
|
||||
|
||||
class MockChartingRouterModule:
|
||||
__name__ = "MockChartingRouterModule"
|
||||
|
||||
def stocks_load(self):
|
||||
pass
|
||||
|
||||
def crypto_load(self):
|
||||
pass
|
||||
|
||||
def ta_ema(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
MockChartingRouterModule.stocks_load.__module__ = (
|
||||
MockChartingRouterModule.__name__
|
||||
)
|
||||
MockChartingRouterModule.crypto_load.__module__ = (
|
||||
MockChartingRouterModule.__name__
|
||||
)
|
||||
MockChartingRouterModule.ta_ema.__module__ = MockChartingRouterModule.__name__
|
||||
|
||||
mock_get_extension_router.return_value = MockChartingRouterModule
|
||||
result = charting_service.get_implemented_charting_functions()
|
||||
assert result == ["crypto_load", "stocks_load"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"charting_extension_installed",
|
||||
[(True), (False)],
|
||||
)
|
||||
@patch("openbb_core.app.charting_service.ChartingService._handle_backend")
|
||||
@patch("openbb_core.app.charting_service.import_module")
|
||||
@patch("openbb_core.app.charting_service.ChartingService._get_chart_format")
|
||||
def test_to_chart(
|
||||
mock_get_chart_format,
|
||||
mock_import_module,
|
||||
mock_handle_backend,
|
||||
charting_service,
|
||||
charting_extension_installed,
|
||||
):
|
||||
charting_service._charting_extension_installed = charting_extension_installed
|
||||
|
||||
class MockBackendModule:
|
||||
CHART_FORMAT = "plotly"
|
||||
|
||||
@staticmethod
|
||||
def to_chart(**kwargs):
|
||||
return ("mock_fig", {"content": "mock_content"})
|
||||
|
||||
mock_import_module.return_value = MockBackendModule()
|
||||
mock_get_chart_format.return_value = "plotly"
|
||||
|
||||
if charting_extension_installed:
|
||||
result = charting_service.to_chart()
|
||||
|
||||
assert isinstance(result, Chart)
|
||||
assert result.fig == "mock_fig"
|
||||
assert result.content == {"content": "mock_content"}
|
||||
|
||||
assert mock_handle_backend.called_once()
|
||||
|
||||
else:
|
||||
with pytest.raises(ChartingServiceError) as exc_info:
|
||||
charting_service.to_chart()
|
||||
assert str(exc_info.value) == (
|
||||
"Charting extension `openbb_charting` is not installed"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"charting_extension_installed, expected_result",
|
||||
[(True, ...), (False, ChartingServiceError)],
|
||||
)
|
||||
@patch("openbb_core.app.charting_service.ChartingService._handle_backend")
|
||||
@patch(
|
||||
"openbb_core.app.charting_service.ChartingService._check_charting_extension_installed"
|
||||
)
|
||||
@patch("openbb_core.app.charting_service.ChartingService._get_chart_format")
|
||||
@patch("openbb_core.app.charting_service.ChartingService._get_chart_function")
|
||||
def test_chart(
|
||||
mock_get_chart_function,
|
||||
mock_get_chart_format,
|
||||
mock_check_charting_extension_installed,
|
||||
mock_handle_backend,
|
||||
user_settings,
|
||||
system_settings,
|
||||
charting_service,
|
||||
charting_extension_installed,
|
||||
expected_result,
|
||||
):
|
||||
def mock_chart_function(**kwargs):
|
||||
return ("mock_fig", {"content": "mock_content"})
|
||||
|
||||
mock_check_charting_extension_installed.return_value = charting_extension_installed
|
||||
mock_get_chart_function.return_value = mock_chart_function
|
||||
mock_get_chart_format.return_value = "plotly"
|
||||
|
||||
if expected_result != ChartingServiceError:
|
||||
result = charting_service.chart(
|
||||
user_settings, system_settings, "/mock/route", "mock_obbject_item"
|
||||
)
|
||||
|
||||
assert isinstance(result, Chart)
|
||||
assert result.fig == "mock_fig"
|
||||
assert result.content == {"content": "mock_content"}
|
||||
|
||||
assert mock_handle_backend.called_once()
|
||||
|
||||
else:
|
||||
with pytest.raises(ChartingServiceError) as exc_info:
|
||||
charting_service.chart(
|
||||
user_settings, system_settings, "/mock/route", "mock_obbject_item"
|
||||
)
|
||||
assert str(exc_info.value) == (
|
||||
"Charting extension `openbb_charting` is not installed"
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
from inspect import Parameter
|
||||
from typing import Dict, List
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_core.app.command_runner import (
|
||||
@@ -361,30 +361,15 @@ async def test_static_command_runner_execute_func(
|
||||
assert mock_chart.called_once()
|
||||
|
||||
|
||||
@patch("openbb_core.app.command_runner.ChartingService.chart")
|
||||
def test_static_command_runner_chart(mock_charting_service_chart, execution_context):
|
||||
"""Test chart."""
|
||||
def test_static_command_runner_chart():
|
||||
"""Test _chart method when charting is in obbject.accessors."""
|
||||
mock_obbject = Mock()
|
||||
mock_obbject.accessors = ["charting"]
|
||||
mock_obbject.charting.show = Mock()
|
||||
|
||||
class MockOBBject:
|
||||
"""Mock OBBject"""
|
||||
StaticCommandRunner._chart(mock_obbject) # pylint: disable=protected-access
|
||||
|
||||
def __init__(self, results):
|
||||
self.results = results
|
||||
self.chart = {}
|
||||
|
||||
mock_charting_service_chart.return_value = {"mock": "chart"}
|
||||
mock_obbject = MockOBBject(results=[1, 2, 3, 4])
|
||||
|
||||
StaticCommandRunner._chart(
|
||||
obbject=mock_obbject,
|
||||
user_settings=execution_context.user_settings,
|
||||
system_settings=execution_context.system_settings,
|
||||
route="mock/route",
|
||||
)
|
||||
|
||||
assert mock_charting_service_chart.called_once()
|
||||
assert mock_obbject.results == [1, 2, 3, 4]
|
||||
assert mock_obbject.chart == {"mock": "chart"}
|
||||
mock_obbject.charting.show.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -54,7 +54,7 @@ openbb-seeking-alpha = { path = "./providers/seeking_alpha", optional = true, de
|
||||
openbb-stockgrid = { path = "./providers/stockgrid" , optional = true, develop = true }
|
||||
openbb-wsj = { path = "./providers/wsj", optional = true, develop = true }
|
||||
|
||||
openbb-charting = { path = "./extensions/charting", optional = true, develop = true }
|
||||
openbb-charting = { path = "./obbject_extensions/charting", optional = true, develop = true }
|
||||
openbb-econometrics = { path = "./extensions/econometrics", optional = true, develop = true }
|
||||
openbb-quantitative = { path = "./extensions/quantitative", optional = true, develop = true }
|
||||
openbb-technical = { path = "./extensions/technical", optional = true, develop = true }
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
# flake8: noqa: F401
|
||||
from openbb_charting.core.backend import (
|
||||
create_backend,
|
||||
get_backend,
|
||||
)
|
||||
from openbb_charting.core.to_chart import to_chart
|
||||
@@ -1,95 +0,0 @@
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import pandas as pd
|
||||
from openbb_charting.core.openbb_figure import OpenBBFigure
|
||||
|
||||
|
||||
class OpenBBFigureTable(OpenBBFigure):
|
||||
def __init__(
|
||||
self,
|
||||
tabular_data: pd.DataFrame,
|
||||
columnwidth: Optional[List[Union[int, float]]] = None,
|
||||
):
|
||||
super().__init__()
|
||||
self._tabular_data = tabular_data
|
||||
self._columnwidth = columnwidth
|
||||
|
||||
def show(
|
||||
self,
|
||||
show_index: Optional[bool] = True,
|
||||
index_name: Optional[str] = "",
|
||||
headers: Optional[Union[List[str], pd.Index]] = None,
|
||||
title: Optional[str] = "",
|
||||
source: Optional[str] = "",
|
||||
external: Optional[bool] = False,
|
||||
) -> Optional["OpenBBFigureTable"]:
|
||||
if external:
|
||||
return self
|
||||
|
||||
if not self._backend.isatty:
|
||||
tmp_fig = self.to_table()
|
||||
tmp_fig.show()
|
||||
return None
|
||||
else:
|
||||
# Make a copy of the dataframe to avoid SettingWithCopyWarning
|
||||
df = self._tabular_data.copy()
|
||||
|
||||
show_index = not isinstance(df.index, pd.RangeIndex) and show_index
|
||||
# convert non-str that are not timestamp or int into str
|
||||
# eg) praw.models.reddit.subreddit.Subreddit
|
||||
for col in df.columns:
|
||||
try:
|
||||
if not any(
|
||||
isinstance(df[col].iloc[x], pd.Timestamp)
|
||||
for x in range(min(10, len(df)))
|
||||
):
|
||||
df[col] = pd.to_numeric(df[col], errors="ignore")
|
||||
except (ValueError, TypeError):
|
||||
df[col] = df[col].astype(str)
|
||||
|
||||
def _get_headers(_headers: Union[List[str], pd.Index]) -> List[str]:
|
||||
"""Check if headers are valid and return them."""
|
||||
output = _headers
|
||||
if isinstance(_headers, pd.Index):
|
||||
output = list(_headers)
|
||||
if len(output) != len(df.columns):
|
||||
raise (
|
||||
ValueError(
|
||||
"Length of headers does not match length of DataFrame"
|
||||
)
|
||||
)
|
||||
return output
|
||||
|
||||
df_outgoing = df.copy()
|
||||
# If headers are provided, use them
|
||||
if headers is not None:
|
||||
# We check if headers are valid
|
||||
df_outgoing.columns = _get_headers(headers)
|
||||
|
||||
if show_index and index_name not in df_outgoing.columns:
|
||||
# If index name is provided, we use it
|
||||
df_outgoing.index.name = index_name or "Index"
|
||||
df_outgoing = df_outgoing.reset_index()
|
||||
|
||||
for col in df_outgoing.columns:
|
||||
if not col:
|
||||
df_outgoing = df_outgoing.rename(columns={col: " "})
|
||||
|
||||
self._backend.send_table(
|
||||
df_table=df_outgoing,
|
||||
title=title,
|
||||
source=source, # type: ignore
|
||||
theme=self._charting_settings.table_style,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def to_table(
|
||||
self,
|
||||
print_index: bool = True,
|
||||
**kwargs,
|
||||
) -> "OpenBBFigure":
|
||||
data = self._tabular_data
|
||||
columnwidth = self._columnwidth
|
||||
|
||||
return super().to_table(data, columnwidth, print_index, **kwargs)
|
||||
2196
openbb_platform/extensions/charting/poetry.lock
generated
vendored
2196
openbb_platform/extensions/charting/poetry.lock
generated
vendored
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_charting import Charting
|
||||
|
||||
from extensions.tests.utils.integration_tests_testers import (
|
||||
check_missing_integration_test_params,
|
||||
@@ -41,7 +41,8 @@ def test_api_interface_wrong_integration_test_params() -> None:
|
||||
|
||||
def test_charting_extension_function_coverage() -> None:
|
||||
"""Test if all charting extension functions are covered by integration tests."""
|
||||
functions = ChartingService.get_implemented_charting_functions()
|
||||
functions = Charting.functions()
|
||||
|
||||
test_names = [f"test_chart_{func}" for func in functions]
|
||||
integration_tests_modules = get_integration_tests(
|
||||
test_type="api", filter_charting_ext=False
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_charting import Charting
|
||||
|
||||
from extensions.tests.utils.integration_tests_testers import (
|
||||
check_missing_integration_test_params,
|
||||
@@ -41,7 +41,8 @@ def test_python_interface_wrong_integration_test_params() -> None:
|
||||
|
||||
def test_charting_extension_function_coverage() -> None:
|
||||
"""Test if all charting extension functions are covered by integration tests."""
|
||||
functions = ChartingService.get_implemented_charting_functions()
|
||||
functions = Charting.functions()
|
||||
|
||||
test_names = [f"test_chart_{func}" for func in functions]
|
||||
integration_tests_modules = get_integration_tests(
|
||||
test_type="python", filter_charting_ext=False
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Literal, Type, get_type_hints
|
||||
|
||||
import requests
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_charting import Charting
|
||||
from openbb_core.app.provider_interface import ProviderInterface
|
||||
from openbb_core.app.router import CommandMap
|
||||
|
||||
@@ -160,25 +161,21 @@ def write_commands_integration_tests(
|
||||
|
||||
def write_charting_extension_integration_tests():
|
||||
"""Write the charting extension integration tests."""
|
||||
functions = ChartingService.get_implemented_charting_functions()
|
||||
import openbb_charting # pylint: disable=import-outside-toplevel
|
||||
|
||||
# we assume test file exists
|
||||
path = os.path.join(
|
||||
"openbb_platform",
|
||||
"extensions",
|
||||
"charting",
|
||||
"integration",
|
||||
"test_charting_api.py",
|
||||
)
|
||||
functions = Charting.functions()
|
||||
|
||||
charting_ext_path = Path(openbb_charting.__file__).parent.parent
|
||||
test_file = charting_ext_path / "integration" / "test_charting_api.py"
|
||||
|
||||
for function in functions:
|
||||
route = "/" + function.replace("_", "/")
|
||||
if not test_exists(route=function, path=path):
|
||||
if not test_exists(route=function, path=test_file):
|
||||
write_test_w_template(
|
||||
http_method="post",
|
||||
params_list=[{"chart": True}],
|
||||
route=route,
|
||||
path=path,
|
||||
path=test_file,
|
||||
chart=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import (
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from openbb_core.app.charting_service import ChartingService
|
||||
from openbb_charting import Charting
|
||||
from openbb_core.app.provider_interface import ProviderInterface
|
||||
from openbb_core.app.router import CommandMap
|
||||
from pydantic.fields import FieldInfo
|
||||
@@ -44,6 +44,9 @@ def find_extensions(filter_chart: Optional[bool] = True):
|
||||
if filter_chart:
|
||||
filter_ext.append("charting")
|
||||
extensions = [x for x in Path("openbb_platform/extensions").iterdir() if x.is_dir()]
|
||||
extensions.extend(
|
||||
[x for x in Path("openbb_platform/obbject_extensions").iterdir() if x.is_dir()]
|
||||
)
|
||||
extensions = [x for x in extensions if x.name not in filter_ext]
|
||||
return extensions
|
||||
|
||||
@@ -279,7 +282,7 @@ def write_charting_extension_integration_tests():
|
||||
"""Write charting extension integration tests."""
|
||||
import openbb_charting # pylint: disable=import-outside-toplevel
|
||||
|
||||
functions = ChartingService.get_implemented_charting_functions()
|
||||
functions = Charting.functions()
|
||||
|
||||
charting_ext_path = Path(openbb_charting.__file__).parent.parent
|
||||
test_file = charting_ext_path / "integration" / "test_charting_python.py"
|
||||
@@ -306,15 +309,15 @@ if __name__ == "__main__":
|
||||
parser.add_argument(
|
||||
"--charting",
|
||||
dest="charting",
|
||||
default=True,
|
||||
action="store_false",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Generate charting extension integration tests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--commands",
|
||||
dest="commands",
|
||||
default=True,
|
||||
action="store_false",
|
||||
default=False,
|
||||
action="store_true",
|
||||
help="Generate commands integration tests",
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ The library includes:
|
||||
- a set of charting components
|
||||
- prebuilt charts for a set of commands that are built-in OpenBB extensions
|
||||
|
||||
>[!NOTE]
|
||||
> The charting library is an [`OBBject` extension](https://docs.openbb.co/platform/development/developer-guidelines/obbject_extensions) which means you'll have the functionality it exposes on every command result.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the extension, run the following command in this folder:
|
||||
@@ -52,14 +55,77 @@ equity_data.show()
|
||||
|
||||
> Note: The `show()` method currently works either in a Jupyter Notebook or in a standalone python script with a PyWry based backend properly initialized.
|
||||
|
||||
Alternatively, you can use the fact that the `openbb-charting` is an `OBBject` extension and use its available methods.
|
||||
|
||||
```python
|
||||
from openbb import obb
|
||||
res = obb.equity.price.historical("AAPL")
|
||||
res.charting.show()
|
||||
```
|
||||
|
||||
The above code will produce the same effect as the previous example.
|
||||
|
||||
### Discovering available charts
|
||||
|
||||
Not all the endpoints are currently supported by the charting extension. To discover which endpoints are supported, you can run the following command:
|
||||
|
||||
```python
|
||||
from openbb_charting import Charting
|
||||
Charting.functions()
|
||||
```
|
||||
|
||||
### Using the `to_chart` method
|
||||
|
||||
The `to_chart` function should be taken as an advanced feature, as it requires the user to have a good understanding of the charting extension and the `OpenBBFigure` class.
|
||||
|
||||
The user can use any number of `**kwargs` that will be passed to the `PlotlyTA` class in order to build custom visualizations with custom indicators and similar.
|
||||
|
||||
> Note that, this method will only work to some limited extent with data that is not standardized.
|
||||
> Also, it is currently designed only to handle time series (OHLCV) data.
|
||||
|
||||
Example usage:
|
||||
|
||||
- Plotting a time series with TA indicators
|
||||
|
||||
```python
|
||||
|
||||
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
|
||||
|
||||
```python
|
||||
|
||||
# if you have a command result already
|
||||
res.charting.indicators
|
||||
|
||||
# or if you want to know in standalone fashion
|
||||
from openbb_charting import Charting
|
||||
Charting.indicators()
|
||||
|
||||
```
|
||||
|
||||
## Add a visualization to an existing Platform command
|
||||
|
||||
One should first ensure that the already implemented endpoint is available in the [charting router](/openbb_platform/extensions/charting/openbb_charting/charting_router.py).
|
||||
One should first ensure that the already implemented endpoint is available in the [charting router](/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py).
|
||||
|
||||
To do so, you can run:
|
||||
`python openbb_platform/extensions/charting/openbb_charting/builder.py` - which will read all the available endpoints and add them to the charting router.
|
||||
`python openbb_platform/obbject_extensions/charting/openbb_charting/builder.py` - which will read all the available endpoints and add them to the charting router.
|
||||
|
||||
Afterwards, you'll need to add the visualization to the [charting router](/openbb_platform/extensions/charting/openbb_charting/charting_router.py). The convention to match the endpoint with the respective charting function is the following:
|
||||
Afterwards, you'll need to add the visualization to the [charting router]. The convention to match the endpoint with the respective charting function is the following:
|
||||
|
||||
- `/equity/price/historical` -> `equity_price_historical`
|
||||
- `/technical/ema` -> `technical_ema`
|
||||
@@ -69,7 +135,6 @@ When you spot the charting function on the charting router file, you can add the
|
||||
The implementation should leverage the already existing classes and methods to do so, namely:
|
||||
|
||||
- `OpenBBFigure`
|
||||
- `OpenBBFigureTable`
|
||||
- `PlotlyTA`
|
||||
|
||||
Note that the return of each charting function should respect the already defined return types: `Tuple[OpenBBFigure, Dict[str, Any]]`.
|
||||
@@ -77,17 +142,4 @@ Note that the return of each charting function should respect the already define
|
||||
The returned tuple contains a `OpenBBFigure` that is an interactive plotly figure which can be used in a Python interpreter, and a `Dict[str, Any]` that contains the raw data leveraged by the API.
|
||||
|
||||
After you're done implementing the charting function, you can use either the Python interface or the API to get the chart. To do so, you'll only need to set the already available `chart` argument to `True`.
|
||||
|
||||
### Using the `to_chart` OBBject method
|
||||
|
||||
The `OBBject` is the custom OpenBB object that is returned by the Platform commands.
|
||||
It implements a set of `to_<something>` functions that enable the user to easily transform the data into a different format.
|
||||
|
||||
The `to_chart` function should be taken as an advanced feature, as it requires the user to have a good understanding of the charting extension and the `OpenBBFigure` class.
|
||||
|
||||
The user can use any number of `**kwargs` that will be passed to the `PlotlyTA` class in order to build custom visualizations with custom indicators and similar.
|
||||
|
||||
Refer to the [`to_chart` implementation](openbb_charting/core/to_chart.py) for further details.
|
||||
|
||||
> Note that, this method will only work to some limited extent with data that is not standardized.
|
||||
> Also, it is currently designed only to handle time series data.
|
||||
Or accessing the `charting` attribute of the `OBBject` object: `my_obbject.charting.show()`.
|
||||
@@ -0,0 +1,201 @@
|
||||
"""OpenBB OBBject extension for charting."""
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from openbb_core.app.model.charts.chart import Chart
|
||||
from openbb_core.app.model.extension import Extension
|
||||
from openbb_core.app.utils import basemodel_to_df, convert_to_basemodel
|
||||
from openbb_core.provider.abstract.data import Data
|
||||
|
||||
from openbb_charting import charting_router
|
||||
from openbb_charting.core.to_chart import ChartIndicators, OpenBBFigure, to_chart
|
||||
from openbb_charting.utils.helpers import get_charting_functions
|
||||
|
||||
ext = Extension(name="charting")
|
||||
|
||||
|
||||
@ext.obbject_accessor
|
||||
class Charting:
|
||||
"""
|
||||
Charting extension.
|
||||
|
||||
Methods
|
||||
-------
|
||||
show
|
||||
Display chart and save it to the OBBject.
|
||||
to_chart
|
||||
Returns the plotly json representation of the chart.
|
||||
functions
|
||||
Returns a list of Platform commands with charting functions.
|
||||
indicators
|
||||
Returns a list of the available indicators to use with the `to_chart` method.
|
||||
"""
|
||||
|
||||
def __init__(self, obbject):
|
||||
"""Initialize Charting extension."""
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
from openbb_core.app.model.obbject import OBBject
|
||||
|
||||
self._obbject: OBBject = obbject
|
||||
self._charting_settings = ChartingSettings(
|
||||
user_settings=self._obbject._user_settings,
|
||||
system_settings=self._obbject._system_settings,
|
||||
)
|
||||
self._handle_backend()
|
||||
|
||||
@classmethod
|
||||
def indicators(cls):
|
||||
"""Returns a list of the available indicators."""
|
||||
return ChartIndicators.get_available_indicators()
|
||||
|
||||
@classmethod
|
||||
def functions(cls):
|
||||
"""Returns a list of the available functions."""
|
||||
return get_charting_functions()
|
||||
|
||||
def _handle_backend(self):
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from openbb_charting.core.backend import create_backend, get_backend
|
||||
|
||||
create_backend(self._charting_settings)
|
||||
get_backend().start(debug=self._charting_settings.debug_mode)
|
||||
|
||||
@staticmethod
|
||||
def _get_chart_function(route: str) -> Callable:
|
||||
"""Given a route, it returns the chart function. The module must contain the given route."""
|
||||
adjusted_route = route.replace("/", "_")[1:]
|
||||
return getattr(charting_router, adjusted_route)
|
||||
|
||||
def show(self, render: bool = True, **kwargs):
|
||||
"""Display chart and save it to the OBBject."""
|
||||
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
|
||||
if (
|
||||
hasattr(self._obbject, "_standard_params")
|
||||
and self._obbject._standard_params # pylint: disable=protected-access
|
||||
):
|
||||
kwargs["standard_params"] = (
|
||||
self._obbject._standard_params.__dict__ # pylint: disable=protected-access
|
||||
)
|
||||
|
||||
fig, content = charting_function(**kwargs)
|
||||
self._obbject.chart = Chart(
|
||||
fig=fig, content=content, format=charting_router.CHART_FORMAT
|
||||
)
|
||||
if render:
|
||||
fig.show(**kwargs)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def to_chart(
|
||||
self,
|
||||
data: Optional[
|
||||
Union[
|
||||
list,
|
||||
dict,
|
||||
pd.DataFrame,
|
||||
List[pd.DataFrame],
|
||||
pd.Series,
|
||||
List[pd.Series],
|
||||
np.ndarray,
|
||||
Data,
|
||||
]
|
||||
] = None,
|
||||
indicators: Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]] = None,
|
||||
symbol: str = "",
|
||||
candles: bool = True,
|
||||
volume: bool = True,
|
||||
prepost: bool = False,
|
||||
volume_ticks_x: int = 7,
|
||||
render: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Creates a OpenBBFigure with user customizations (if any) and saves it to the OBBject.
|
||||
|
||||
This function is used so it can be called at the module level and used out of the box,
|
||||
which allows some more flexibility, ease of use and doesn't require the user to know
|
||||
about the PlotlyTA class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : Union[pd.DataFrame, pd.Series]
|
||||
Data to be plotted Data to be plotted (OHLCV data).
|
||||
indicators : Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]], optional
|
||||
Indicators to be plotted, by default None
|
||||
symbol : str, optional
|
||||
Symbol to be plotted, 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
|
||||
prepost : bool, optional
|
||||
If True, prepost will be plotted, by default False
|
||||
volume_ticks_x : int, optional
|
||||
Volume ticks, by default 7
|
||||
render : bool, optional
|
||||
If True, the chart will be rendered, by default True
|
||||
|
||||
Examples
|
||||
--------
|
||||
1) 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})
|
||||
|
||||
2) Get all the available indicators
|
||||
> from openbb_charting.core.plotly_ta.data_classes import ChartIndicators
|
||||
> ChartIndicators.get_available_indicators()
|
||||
or
|
||||
> from openbb_charting import Charting
|
||||
> Charting.indicators()
|
||||
"""
|
||||
has_data = (isinstance(data, (pd.DataFrame, pd.Series)) and not data.empty) or (
|
||||
data
|
||||
)
|
||||
index = (
|
||||
data.index.name
|
||||
if has_data and isinstance(data, (pd.DataFrame, pd.Series))
|
||||
else ""
|
||||
)
|
||||
data_as_df: pd.DataFrame = (
|
||||
basemodel_to_df(convert_to_basemodel(data), index=index)
|
||||
if has_data
|
||||
else self._obbject.to_dataframe()
|
||||
)
|
||||
fig, content = to_chart(
|
||||
data_as_df,
|
||||
indicators=indicators,
|
||||
symbol=symbol,
|
||||
candles=candles,
|
||||
volume=volume,
|
||||
prepost=prepost,
|
||||
volume_ticks_x=volume_ticks_x,
|
||||
)
|
||||
self._obbject.chart = Chart(
|
||||
fig=fig, content=content, format=charting_router.CHART_FORMAT
|
||||
)
|
||||
if render:
|
||||
fig.show(**kwargs)
|
||||
@@ -1,35 +1,40 @@
|
||||
"""Script used to create charting functions in charting_router.py."""
|
||||
|
||||
from inspect import getmembers, isfunction
|
||||
from typing import List
|
||||
|
||||
from openbb_charting import charting_router
|
||||
from openbb_core.app.router import RouterLoader
|
||||
|
||||
from openbb_charting import charting_router
|
||||
|
||||
# refers to the root of the repo, not the root of the extension
|
||||
# this script should be run from the root of the repo
|
||||
CHARTING_ROUTER = (
|
||||
"openbb_platform/extensions/charting/openbb_charting/charting_router.py"
|
||||
"openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py"
|
||||
)
|
||||
|
||||
|
||||
def get_routes() -> List[str]:
|
||||
"""Get all routes from the api router."""
|
||||
router = RouterLoader.from_extensions()
|
||||
return [route.path for route in router.api_router.routes] # type: ignore
|
||||
|
||||
|
||||
def create_charting_functions_w_routes(routes_to_add: List[str]):
|
||||
"""Create new charting functions in charting_router.py."""
|
||||
with open(CHARTING_ROUTER, "a") as f:
|
||||
for route in routes_to_add:
|
||||
original_route = route.replace("_", "/")
|
||||
f.write(
|
||||
f"""
|
||||
|
||||
def {route}(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def {route}(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
raise NotImplementedError("Command `{original_route}` does not have a charting function")
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def get_charting_functions() -> List[str]:
|
||||
"""Get all functions from charting_router.py."""
|
||||
functions = getmembers(charting_router, isfunction)
|
||||
if functions:
|
||||
return [function[0] for function in functions]
|
||||
@@ -1,21 +1,21 @@
|
||||
"""Charting router."""
|
||||
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import TYPE_CHECKING, Any, Dict, Tuple
|
||||
|
||||
from openbb_core.app.model.charts.chart import ChartFormat
|
||||
from openbb_core.app.router import Router
|
||||
from openbb_core.app.utils import basemodel_to_df
|
||||
|
||||
from .core.openbb_figure import OpenBBFigure
|
||||
from .core.openbb_figure_table import OpenBBFigureTable
|
||||
from .core.plotly_ta.ta_class import PlotlyTA
|
||||
|
||||
router = Router(prefix="")
|
||||
|
||||
CHART_FORMAT = ChartFormat.plotly
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.openbb_figure import OpenBBFigure
|
||||
|
||||
def equity_price_historical(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
# from .core.openbb_figure_table import OpenBBFigureTable
|
||||
|
||||
|
||||
def equity_price_historical(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Equity price chart."""
|
||||
|
||||
def handle_indicators(ma):
|
||||
@@ -26,12 +26,12 @@ def equity_price_historical(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
return k
|
||||
|
||||
data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
|
||||
standard_params = kwargs["standard_params"].__dict__
|
||||
standard_params = kwargs["standard_params"]
|
||||
ma = standard_params.get("ma", None)
|
||||
prepost = standard_params.get("prepost", False)
|
||||
symbol = standard_params.get("symbol", "")
|
||||
|
||||
ta = PlotlyTA(charting_settings=kwargs["charting_settings"])
|
||||
ta = PlotlyTA()
|
||||
fig = ta.plot(
|
||||
data,
|
||||
indicators=dict(**handle_indicators(ma)),
|
||||
@@ -45,12 +45,12 @@ def equity_price_historical(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
|
||||
def _ta_ma(ma_type: str, **kwargs):
|
||||
"""Plot moving average helper."""
|
||||
data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date"))
|
||||
data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
|
||||
window = kwargs.get("window", 50)
|
||||
offset = kwargs.get("offset", 0)
|
||||
symbol = kwargs.get("symbol", "")
|
||||
|
||||
ta = PlotlyTA(charting_settings=kwargs["charting_settings"])
|
||||
ta = PlotlyTA()
|
||||
fig = ta.plot(
|
||||
data,
|
||||
{f"{ma_type.lower()}": dict(length=window, offset=offset)},
|
||||
@@ -63,20 +63,20 @@ def _ta_ma(ma_type: str, **kwargs):
|
||||
return fig, content
|
||||
|
||||
|
||||
def technical_zlma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_zlma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Zero lag moving average chart."""
|
||||
ma_type = "zlma"
|
||||
return _ta_ma(ma_type, **kwargs)
|
||||
|
||||
|
||||
def technical_aroon(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_aroon(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Aroon chart."""
|
||||
data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date"))
|
||||
data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
|
||||
length = kwargs.get("length", 25)
|
||||
scalar = kwargs.get("scalar", 100)
|
||||
symbol = kwargs.get("symbol", "")
|
||||
|
||||
ta = PlotlyTA(charting_settings=kwargs["charting_settings"])
|
||||
ta = PlotlyTA()
|
||||
fig = ta.plot(
|
||||
data,
|
||||
dict(aroon=dict(length=length, scalar=scalar)),
|
||||
@@ -89,21 +89,21 @@ def technical_aroon(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
return fig, content
|
||||
|
||||
|
||||
def technical_sma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_sma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Plot simple moving average chart."""
|
||||
ma_type = "sma"
|
||||
return _ta_ma(ma_type, **kwargs)
|
||||
|
||||
|
||||
def technical_macd(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_macd(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Plot moving average convergence divergence chart."""
|
||||
data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date"))
|
||||
data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
|
||||
fast = kwargs.get("fast", 12)
|
||||
slow = kwargs.get("slow", 26)
|
||||
signal = kwargs.get("signal", 9)
|
||||
symbol = kwargs.get("symbol", "")
|
||||
|
||||
ta = PlotlyTA(charting_settings=kwargs["charting_settings"])
|
||||
ta = PlotlyTA()
|
||||
fig = ta.plot(
|
||||
data,
|
||||
dict(macd=dict(fast=fast, slow=slow, signal=signal)),
|
||||
@@ -116,21 +116,21 @@ def technical_macd(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
return fig, content
|
||||
|
||||
|
||||
def technical_hma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_hma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Hull moving average chart."""
|
||||
ma_type = "hma"
|
||||
return _ta_ma(ma_type, **kwargs)
|
||||
|
||||
|
||||
def technical_adx(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_adx(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Average directional movement index chart."""
|
||||
data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date"))
|
||||
data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
|
||||
length = kwargs.get("length", 14)
|
||||
scalar = kwargs.get("scalar", 100.0)
|
||||
drift = kwargs.get("drift", 1)
|
||||
symbol = kwargs.get("symbol", "")
|
||||
|
||||
ta = PlotlyTA(charting_settings=kwargs["charting_settings"])
|
||||
ta = PlotlyTA()
|
||||
fig = ta.plot(
|
||||
data,
|
||||
dict(adx=dict(length=length, scalar=scalar, drift=drift)),
|
||||
@@ -143,21 +143,21 @@ def technical_adx(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
return fig, content
|
||||
|
||||
|
||||
def technical_wma(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_wma(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Weighted moving average chart."""
|
||||
ma_type = "wma"
|
||||
return _ta_ma(ma_type, **kwargs)
|
||||
|
||||
|
||||
def technical_rsi(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_rsi(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Relative strength index chart."""
|
||||
data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date"))
|
||||
data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
|
||||
window = kwargs.get("window", 14)
|
||||
scalar = kwargs.get("scalar", 100.0)
|
||||
drift = kwargs.get("drift", 1)
|
||||
symbol = kwargs.get("symbol", "")
|
||||
|
||||
ta = PlotlyTA(charting_settings=kwargs["charting_settings"])
|
||||
ta = PlotlyTA()
|
||||
fig = ta.plot(
|
||||
data,
|
||||
dict(rsi=dict(length=window, scalar=scalar, drift=drift)),
|
||||
@@ -170,19 +170,7 @@ def technical_rsi(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
return fig, content
|
||||
|
||||
|
||||
def technical_ema(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
def technical_ema(**kwargs) -> Tuple["OpenBBFigure", Dict[str, Any]]:
|
||||
"""Exponential moving average chart."""
|
||||
ma_type = "ema"
|
||||
return _ta_ma(ma_type, **kwargs)
|
||||
|
||||
|
||||
def equity_fundamental_multiples(**kwargs) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
"""Equity multiples chart."""
|
||||
data = basemodel_to_df(kwargs["obbject_item"], index=kwargs.get("index", "date"))
|
||||
standard_params = kwargs["standard_params"].__dict__
|
||||
columnwidth = standard_params.get("columnwidth", None)
|
||||
|
||||
tbl_fig = OpenBBFigureTable(tabular_data=data, columnwidth=columnwidth)
|
||||
content = tbl_fig.to_table().show(external=True).to_plotly_json()
|
||||
|
||||
return tbl_fig, content
|
||||
|
Before Width: | Height: | Size: 409 KiB After Width: | Height: | Size: 409 KiB |
@@ -11,15 +11,19 @@ import warnings
|
||||
from multiprocessing import current_process
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
from typing import Any, Dict, Optional, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
|
||||
|
||||
import aiohttp
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
from openbb_core.env import Env
|
||||
from packaging import version
|
||||
from reportlab.graphics import renderPDF
|
||||
from svglib.svglib import svg2rlg
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
|
||||
|
||||
# pylint: disable=C0415
|
||||
try:
|
||||
@@ -34,8 +38,6 @@ except ImportError as e:
|
||||
"""Dummy backend for charts."""
|
||||
|
||||
|
||||
from svglib.svglib import svg2rlg
|
||||
|
||||
try:
|
||||
from IPython import get_ipython
|
||||
|
||||
@@ -67,7 +69,7 @@ class Backend(PyWry):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
charting_settings: ChartingSettings,
|
||||
charting_settings: "ChartingSettings",
|
||||
daemon: bool = True,
|
||||
max_retries: int = 30,
|
||||
proc_name: str = "OpenBB Terminal",
|
||||
@@ -504,7 +506,10 @@ if not PLOTLYJS_PATH.exists() and not JUPYTER_NOTEBOOK:
|
||||
Thread(target=asyncio.run, args=(download_plotly_js(),)).start()
|
||||
|
||||
|
||||
def create_backend(charting_settings: Optional[ChartingSettings] = None):
|
||||
def create_backend(charting_settings: Optional["ChartingSettings"] = None):
|
||||
# # pylint: disable=import-outside-toplevel
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
|
||||
charting_settings = charting_settings or ChartingSettings()
|
||||
global BACKEND # pylint: disable=W0603 # noqa
|
||||
if BACKEND is None:
|
||||
@@ -15,6 +15,7 @@ from warnings import warn
|
||||
|
||||
import plotly.graph_objects as go
|
||||
import plotly.io as pio
|
||||
|
||||
from openbb_charting.core.config.openbb_styles import (
|
||||
PLT_COLORWAY,
|
||||
PLT_DECREASING_COLORWAY,
|
||||
@@ -23,15 +23,17 @@ import numpy as np
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
import plotly.io as pio
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
from openbb_charting.core.backend import PLOTLYJS_PATH, create_backend, get_backend
|
||||
from openbb_charting.core.chart_style import ChartStyle
|
||||
from openbb_charting.core.config.openbb_styles import (
|
||||
PLT_TBL_ROW_COLORS,
|
||||
)
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
from plotly.subplots import make_subplots
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
|
||||
try: # noqa: SIM105
|
||||
# pylint: disable=W0611 # noqa: F401
|
||||
from darts import TimeSeries
|
||||
@@ -86,7 +88,9 @@ class OpenBBFigure(go.Figure):
|
||||
if fig:
|
||||
self.__dict__ = fig.__dict__
|
||||
|
||||
self._charting_settings = kwargs.pop("charting_settings", ChartingSettings())
|
||||
self._charting_settings: Optional["ChartingSettings"] = kwargs.pop(
|
||||
"charting_settings", None
|
||||
)
|
||||
self._has_secondary_y = kwargs.pop("has_secondary_y", False)
|
||||
self._subplots_kwargs: Dict[str, Any] = kwargs.pop("subplots_kwargs", {})
|
||||
self._multi_rows = kwargs.pop("multi_rows", False)
|
||||
@@ -102,10 +106,12 @@ class OpenBBFigure(go.Figure):
|
||||
|
||||
if kwargs.pop("create_backend", False):
|
||||
create_backend(self._charting_settings)
|
||||
get_backend().start(debug=self._charting_settings.debug_mode)
|
||||
get_backend().start(
|
||||
debug=getattr(self._charting_settings, "debug_mode", False)
|
||||
)
|
||||
self._theme = ChartStyle(
|
||||
self._charting_settings.chart_style,
|
||||
self._charting_settings.user_styles_directory,
|
||||
getattr(self._charting_settings, "plt_style", ""),
|
||||
getattr(self._charting_settings, "user_styles_directory", None),
|
||||
)
|
||||
|
||||
if xaxis := kwargs.pop("xaxis", None):
|
||||
@@ -232,7 +238,8 @@ class OpenBBFigure(go.Figure):
|
||||
secondary_y: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Add a trend line to the figure.
|
||||
"""
|
||||
Add a trend line to the figure.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -245,7 +252,6 @@ class OpenBBFigure(go.Figure):
|
||||
secondary_y : `bool`, optional
|
||||
Whether to plot on secondary y axis, by default None
|
||||
"""
|
||||
|
||||
try:
|
||||
for column, color in zip(
|
||||
["OC_High_trend", "OC_Low_trend"],
|
||||
@@ -285,7 +291,8 @@ class OpenBBFigure(go.Figure):
|
||||
row: int = 1,
|
||||
col: int = 1,
|
||||
) -> None:
|
||||
"""Add a histogram with a curve and rug plot if desired.
|
||||
"""
|
||||
Add a histogram with a curve and rug plot if desired.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -312,7 +319,6 @@ class OpenBBFigure(go.Figure):
|
||||
col : `int`, optional
|
||||
Column of the subplot, by default 1
|
||||
"""
|
||||
|
||||
from scipy import stats # pylint: disable=import-outside-toplevel
|
||||
|
||||
callback = stats.norm if curve == "normal" else stats.gaussian_kde
|
||||
@@ -554,7 +560,8 @@ class OpenBBFigure(go.Figure):
|
||||
legendrank: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Add a horizontal line with a legend label.
|
||||
"""
|
||||
Add a horizontal line with a legend label.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -567,7 +574,6 @@ class OpenBBFigure(go.Figure):
|
||||
legendrank : `int`, optional
|
||||
Legend rank, by default None (e.g. 1 is above 2)
|
||||
"""
|
||||
|
||||
if line is None:
|
||||
line = {}
|
||||
|
||||
@@ -592,7 +598,8 @@ class OpenBBFigure(go.Figure):
|
||||
legendrank: Optional[int] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Add a vertical line with a legend label.
|
||||
"""
|
||||
Add a vertical line with a legend label.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -605,7 +612,6 @@ class OpenBBFigure(go.Figure):
|
||||
legendrank : `int`, optional
|
||||
Legend rank, by default None (e.g. 1 is above 2)
|
||||
"""
|
||||
|
||||
if line is None:
|
||||
line = {}
|
||||
|
||||
@@ -661,7 +667,8 @@ class OpenBBFigure(go.Figure):
|
||||
def chart_volume_scaling(
|
||||
df_volume: pd.DataFrame, volume_ticks_x: int = 7
|
||||
) -> Dict[str, list]:
|
||||
"""Takes df_volume and returns volume_ticks, tickvals for chart volume scaling
|
||||
"""
|
||||
Takes df_volume and returns volume_ticks, tickvals for chart volume scaling.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -709,7 +716,8 @@ class OpenBBFigure(go.Figure):
|
||||
col: Optional[int] = 1,
|
||||
volume_ticks_x: int = 7,
|
||||
) -> None:
|
||||
"""Add in-chart volume to a subplot.
|
||||
"""
|
||||
Add in-chart volume to a subplot.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -726,7 +734,6 @@ class OpenBBFigure(go.Figure):
|
||||
volume_ticks_x : int, optional
|
||||
Number to multiply volume, by default 7
|
||||
"""
|
||||
|
||||
colors = np.where(
|
||||
df_stock.open < df_stock[close_col],
|
||||
self._theme.up_color,
|
||||
@@ -838,8 +845,9 @@ class OpenBBFigure(go.Figure):
|
||||
external: bool = False,
|
||||
export_image: Optional[Union[Path, str]] = "",
|
||||
**kwargs,
|
||||
) -> Optional["OpenBBFigure"]:
|
||||
"""Show the figure.
|
||||
) -> "OpenBBFigure":
|
||||
"""
|
||||
Show the figure.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -854,7 +862,6 @@ class OpenBBFigure(go.Figure):
|
||||
date_xaxis : `bool`, optional
|
||||
Whether to check if the xaxis is a date axis, by default True
|
||||
"""
|
||||
|
||||
self.cmd_xshift = kwargs.pop("cmd_xshift", self.cmd_xshift)
|
||||
self.bar_width = kwargs.pop("bar_width", self.bar_width)
|
||||
self._export_image = export_image
|
||||
@@ -899,7 +906,7 @@ class OpenBBFigure(go.Figure):
|
||||
if external or self._exported:
|
||||
return self # type: ignore
|
||||
|
||||
if self._charting_settings.headless:
|
||||
if getattr(self._charting_settings, "headless", False):
|
||||
return self.to_json()
|
||||
|
||||
kwargs.update(config=dict(scrollZoom=True, displaylogo=False))
|
||||
@@ -916,7 +923,7 @@ class OpenBBFigure(go.Figure):
|
||||
# If the backend fails, we just show the figure normally
|
||||
# This is a very rare case, but it's better to have a fallback
|
||||
|
||||
if self._charting_settings.debug_mode:
|
||||
if getattr(self._charting_settings, "debug_mode", False):
|
||||
warn(f"Failed to show figure with backend: {e}")
|
||||
warn(
|
||||
f"Failed to show figure with backend: {e}"
|
||||
@@ -1232,8 +1239,7 @@ class OpenBBFigure(go.Figure):
|
||||
self, ndarray: bool = False, np_nan: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert figure to a JSON representation as a Python dict
|
||||
|
||||
Convert figure to a JSON representation as a Python dict.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -1512,7 +1518,6 @@ class OpenBBFigure(go.Figure):
|
||||
|
||||
def add_logscale_menus(self, yaxis: str = "yaxis") -> None:
|
||||
"""Set the menus for the figure."""
|
||||
|
||||
self._added_logscale = True
|
||||
bg_color = "#000000" if self._theme.mapbox_style == "dark" else "#FFFFFF" # type: ignore
|
||||
font_color = "#FFFFFF" if self._theme.mapbox_style == "dark" else "#000000" # type: ignore
|
||||
File diff suppressed because one or more lines are too long
@@ -1,9 +1,11 @@
|
||||
"""Dataclasses for the charting extension."""
|
||||
|
||||
# pylint: disable=C0302,R0915,R0914,R0913,R0903,R0904
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||
|
||||
import pandas as pd
|
||||
import pandas_ta as ta
|
||||
@@ -15,7 +17,7 @@ datacls_kwargs = {"slots": True} if sys.version_info >= (3, 10) else {}
|
||||
|
||||
|
||||
def columns_regex(df_ta: pd.DataFrame, name: str) -> List[str]:
|
||||
"""Return columns that match regex name"""
|
||||
"""Return columns that match regex name."""
|
||||
column_name = df_ta.filter(regex=rf"{name}(?=[^\d]|$)").columns.tolist()
|
||||
|
||||
return column_name
|
||||
@@ -23,28 +25,58 @@ def columns_regex(df_ta: pd.DataFrame, name: str) -> List[str]:
|
||||
|
||||
@dataclass(**datacls_kwargs)
|
||||
class Arguments:
|
||||
"""Arguments for technical analysis indicators."""
|
||||
|
||||
label: str
|
||||
values: Any
|
||||
|
||||
def __post_init__(self):
|
||||
"""Post init."""
|
||||
if isinstance(self.values, list) and len(self.values) == 1:
|
||||
self.values = self.values[0]
|
||||
|
||||
|
||||
@dataclass(**datacls_kwargs)
|
||||
class TAIndicator:
|
||||
name: str
|
||||
"""Technical analysis indicator."""
|
||||
|
||||
name: Literal[
|
||||
"ad",
|
||||
"adosc",
|
||||
"adx",
|
||||
"aroon",
|
||||
"atr",
|
||||
"cci",
|
||||
"donchian",
|
||||
"fisher",
|
||||
"kc",
|
||||
"obv",
|
||||
"stoch",
|
||||
"vwap",
|
||||
"fib",
|
||||
"srlines",
|
||||
"clenow",
|
||||
"demark",
|
||||
"ichimoku",
|
||||
"sma",
|
||||
"ema",
|
||||
"wma",
|
||||
"hma",
|
||||
"zlma",
|
||||
"rma",
|
||||
]
|
||||
args: List[Arguments]
|
||||
|
||||
def __post_init__(self):
|
||||
"""Post init."""
|
||||
self.args = [Arguments(**arg) for arg in self.args]
|
||||
|
||||
def __iter__(self):
|
||||
"""Return iterator"""
|
||||
"""Return iterator."""
|
||||
return iter(self.args)
|
||||
|
||||
def get_args(self, label: str) -> Union[Arguments, None]:
|
||||
"""Return arguments by label"""
|
||||
"""Return arguments by label."""
|
||||
output = None
|
||||
for opt in self.args:
|
||||
if opt.label == label:
|
||||
@@ -52,7 +84,7 @@ class TAIndicator:
|
||||
return output
|
||||
|
||||
def get_argument_values(self, label: str) -> Union[List[Any], Any]:
|
||||
"""Returns arguments values by label"""
|
||||
"""Return arguments values by label."""
|
||||
output = []
|
||||
options = self.get_args(label)
|
||||
if options is not None:
|
||||
@@ -62,9 +94,12 @@ class TAIndicator:
|
||||
|
||||
@dataclass(**datacls_kwargs)
|
||||
class ChartIndicators:
|
||||
"""Chart technical analysis indicators."""
|
||||
|
||||
indicators: Optional[List[TAIndicator]] = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Post init."""
|
||||
self.indicators = (
|
||||
[TAIndicator(**indicator) for indicator in self.indicators]
|
||||
if self.indicators
|
||||
@@ -72,7 +107,7 @@ class ChartIndicators:
|
||||
)
|
||||
|
||||
def get_indicator(self, name: str) -> Union[TAIndicator, None]:
|
||||
"""Returns indicator with given name"""
|
||||
"""Return indicator with given name."""
|
||||
output = None
|
||||
for indicator in self.indicators: # type: ignore
|
||||
if indicator.name == name:
|
||||
@@ -80,7 +115,7 @@ class ChartIndicators:
|
||||
return output
|
||||
|
||||
def get_indicator_args(self, name: str, label: str) -> Union[Arguments, None]:
|
||||
"""Returns argument values for given indicator and label"""
|
||||
"""Return argument values for given indicator and label."""
|
||||
output = None
|
||||
indicator = self.get_indicator(name)
|
||||
if indicator is not None:
|
||||
@@ -90,25 +125,25 @@ class ChartIndicators:
|
||||
return output
|
||||
|
||||
def get_indicators(self) -> Optional[List[TAIndicator]]:
|
||||
"""Return active indicators and their arguments"""
|
||||
"""Return active indicators and their arguments."""
|
||||
return self.indicators
|
||||
|
||||
def get_params(self) -> Dict[str, TAIndicator]:
|
||||
"""Return dictionary of active indicators and their arguments"""
|
||||
"""Return dictionary of active indicators and their arguments."""
|
||||
output = {}
|
||||
if self.indicators:
|
||||
output = {indicator.name: indicator for indicator in self.indicators}
|
||||
output = {str(indicator.name): indicator for indicator in self.indicators}
|
||||
return output
|
||||
|
||||
def get_active_ids(self) -> List[str]:
|
||||
"""Returns list of names of active indicators"""
|
||||
"""Return list of names of active indicators."""
|
||||
active_ids = []
|
||||
if self.indicators:
|
||||
active_ids = [indicator.name for indicator in self.indicators]
|
||||
active_ids = [str(indicator.name) for indicator in self.indicators]
|
||||
return active_ids
|
||||
|
||||
def get_arg_names(self, name: str) -> List[str]:
|
||||
"""Returns list of argument labels for given indicator"""
|
||||
"""Return list of argument labels for given indicator."""
|
||||
output = []
|
||||
indicator = self.get_indicator(name)
|
||||
if indicator is not None:
|
||||
@@ -117,7 +152,7 @@ class ChartIndicators:
|
||||
return output
|
||||
|
||||
def get_options_dict(self, name: str) -> Dict[str, Optional[Arguments]]:
|
||||
"""Returns dictionary of argument labels and values for given indicator"""
|
||||
"""Return dictionary of argument labels and values for given indicator."""
|
||||
output = None
|
||||
options = self.get_arg_names(name)
|
||||
if options:
|
||||
@@ -127,9 +162,16 @@ class ChartIndicators:
|
||||
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def get_available_indicators() -> Tuple[str, ...]:
|
||||
"""Return tuple of available indicators."""
|
||||
return list(
|
||||
TAIndicator.__annotations__["name"].__args__ # pylint: disable=E1101
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, indicators: Dict[str, Dict[str, Any]]) -> "ChartIndicators":
|
||||
"""Return ChartIndicators from dictionary"""
|
||||
"""Return ChartIndicators from dictionary."""
|
||||
data = []
|
||||
for indicator in indicators:
|
||||
args = []
|
||||
@@ -164,7 +206,7 @@ class ChartIndicators:
|
||||
return output
|
||||
|
||||
def remove_indicator(self, name: str) -> None:
|
||||
"""Remove indicator from active indicators"""
|
||||
"""Remove indicator from active indicators."""
|
||||
if self.indicators:
|
||||
for indicator in self.indicators:
|
||||
if indicator.name == name:
|
||||
@@ -172,12 +214,12 @@ class ChartIndicators:
|
||||
|
||||
|
||||
class TA_DataException(Exception):
|
||||
pass
|
||||
"""Exception for TA_Data."""
|
||||
|
||||
|
||||
class TA_Data:
|
||||
"""Process technical analysis data
|
||||
|
||||
"""
|
||||
Process technical analysis data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -193,14 +235,12 @@ class TA_Data:
|
||||
rsi=dict(length=14),
|
||||
)
|
||||
|
||||
|
||||
Methods
|
||||
-------
|
||||
to_dataframe()
|
||||
Return dataframe with technical analysis indicators
|
||||
get_indicator_data(indicator: TAIndicator, **kwargs)
|
||||
Return dataframe given indicator and arguments
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -209,6 +249,7 @@ class TA_Data:
|
||||
indicators: Union[ChartIndicators, Dict[str, Dict[str, Any]]],
|
||||
ma_mode: Optional[List[str]] = None,
|
||||
):
|
||||
"""Initialize."""
|
||||
if isinstance(df_ta, pd.Series):
|
||||
df_ta = df_ta.to_frame()
|
||||
|
||||
@@ -240,7 +281,8 @@ class TA_Data:
|
||||
self.has_volume = "volume" in df_ta.columns and bool(df_ta["volume"].sum() > 0)
|
||||
|
||||
def get_indicator_data(self, indicator: TAIndicator, **args) -> pd.DataFrame:
|
||||
"""Returns dataframe with indicator data
|
||||
"""
|
||||
Return dataframe with indicator data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -249,7 +291,7 @@ class TA_Data:
|
||||
args : dict
|
||||
Arguments for given indicator
|
||||
|
||||
Returns
|
||||
Return
|
||||
-------
|
||||
pd.DataFrame
|
||||
Dataframe with indicator data
|
||||
@@ -302,7 +344,7 @@ class TA_Data:
|
||||
return output
|
||||
|
||||
def to_dataframe(self) -> pd.DataFrame:
|
||||
"""Returns dataframe with all indicators"""
|
||||
"""Return dataframe with all indicators."""
|
||||
active_indicators = self.indicators.get_indicators()
|
||||
|
||||
if not active_indicators:
|
||||
@@ -6,22 +6,25 @@ import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from openbb_charting.core.chart_style import ChartStyle
|
||||
from openbb_charting.core.openbb_figure import OpenBBFigure
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
|
||||
from .base import PltTA
|
||||
from .data_classes import ChartIndicators
|
||||
from .ta_helpers import check_columns
|
||||
|
||||
OPENBB_CHARTING_EXTENSION_PATH = Path(__file__).parent.parent.parent
|
||||
CHARTING_INSTALL_PATH = OPENBB_CHARTING_EXTENSION_PATH.parent
|
||||
charting_EXTENSION_PATH = Path(__file__).parent.parent.parent
|
||||
CHARTING_INSTALL_PATH = charting_EXTENSION_PATH.parent
|
||||
PLUGINS_PATH = Path(__file__).parent / "plugins"
|
||||
PLOTLY_TA: Optional["PlotlyTA"] = None
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
|
||||
|
||||
class PlotlyTA(PltTA):
|
||||
"""Plotly Technical Analysis class.
|
||||
@@ -60,7 +63,7 @@ class PlotlyTA(PltTA):
|
||||
Examples
|
||||
--------
|
||||
>>> from openbb import obb
|
||||
>>> from openbb_charting.core.plotly_ta.ta_class import PlotlyTA
|
||||
>>> from charting.core.plotly_ta.ta_class import PlotlyTA
|
||||
|
||||
>>> df = obb.equity.price.historical("SPY")
|
||||
>>> indicators = dict(
|
||||
@@ -90,13 +93,18 @@ class PlotlyTA(PltTA):
|
||||
has_volume: bool = True
|
||||
show_volume: bool = True
|
||||
prepost: bool = False
|
||||
charting_settings: ChartingSettings = ChartingSettings()
|
||||
charting_settings: Optional["ChartingSettings"] = None
|
||||
theme: Optional[ChartStyle] = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""Method is overridden to create a singleton instance of the class."""
|
||||
cls.charting_settings = kwargs.pop("charting_settings", cls.charting_settings)
|
||||
cls.theme = cls.setup_theme(cls.charting_settings)
|
||||
cls.theme = cls.setup_theme(
|
||||
chart_style=getattr(cls.charting_settings, "chart_style", ""),
|
||||
user_styles_directory=getattr(
|
||||
cls.charting_settings, "user_styles_directory", ""
|
||||
),
|
||||
)
|
||||
cls.inchart_colors = cls.theme.get_colors()
|
||||
|
||||
global PLOTLY_TA # pylint: disable=global-statement # noqa
|
||||
@@ -105,14 +113,14 @@ class PlotlyTA(PltTA):
|
||||
# We set the global variable to the instance of the class so that
|
||||
# the plugins are only loaded once
|
||||
PLOTLY_TA = super().__new__(cls)
|
||||
PLOTLY_TA._locate_plugins(cls.charting_settings.debug_mode)
|
||||
PLOTLY_TA._locate_plugins(
|
||||
getattr(cls.charting_settings, "debug_mode", False)
|
||||
)
|
||||
PLOTLY_TA.add_plugins(PLOTLY_TA.plugins)
|
||||
|
||||
return PLOTLY_TA
|
||||
|
||||
def __init__(
|
||||
self, *args, charting_settings: Optional[ChartingSettings] = None, **kwargs
|
||||
): # pylint: disable=unused-argument
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Method is overridden to do nothing, except to clear the internal data structures."""
|
||||
if not args and not kwargs:
|
||||
self._clear_data()
|
||||
@@ -121,12 +129,9 @@ class PlotlyTA(PltTA):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def setup_theme(charting_settings: ChartingSettings) -> ChartStyle:
|
||||
def setup_theme(chart_style, user_styles_directory) -> ChartStyle:
|
||||
"""Setup theme for charting."""
|
||||
return ChartStyle(
|
||||
charting_settings.chart_style,
|
||||
charting_settings.user_styles_directory,
|
||||
)
|
||||
return ChartStyle(chart_style, user_styles_directory)
|
||||
|
||||
@property
|
||||
def ma_mode(self) -> List[str]:
|
||||
@@ -8,8 +8,8 @@
|
||||
<script>
|
||||
if (window.json_data && JSON.parse(window.json_data)?.posthog?.collect_logs) {
|
||||
!function (t, e) { var o, n, p, r; e.__SV || (window.posthog = e, e._i = [], e.init = function (i, s, a) { function g(t, e) { var o = e.split("."); 2 == o.length && (t = t[o[0]], e = o[1]), t[e] = function () { t.push([e].concat(Array.prototype.slice.call(arguments, 0))) } } (p = t.createElement("script")).type = "text/javascript", p.async = !0, p.src = s.api_host + "/static/array.js", (r = t.getElementsByTagName("script")[0]).parentNode.insertBefore(p, r); var u = e; for (void 0 !== a ? u = e[a] = [] : a = "posthog", u.people = u.people || [], u.toString = function (t) { var e = "posthog"; return "posthog" !== a && (e += "." + a), t || (e += " (stub)"), e }, u.people.toString = function () { return u.toString(1) + ".people (stub)" }, o = "capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "), n = 0; n < o.length; n++)g(u, o[n]); e._i.push([i, s, a]) }, e.__SV = 1) }(document, window.posthog || []);
|
||||
posthog.init("phc_6FXLqu4uW9yxfyN8DpPdgzCdlYXOmIWdMGh6GnBgJLX", {
|
||||
api_host: "https://eu.posthog.com",
|
||||
posthog.init("", {
|
||||
api_host: "https://app.posthog.com",
|
||||
autocapture: {
|
||||
css_selector_allowlist: [".ph-capture"],
|
||||
},
|
||||
@@ -1,14 +1,15 @@
|
||||
"""Module containing the to_chart function."""
|
||||
|
||||
from typing import Any, Dict, Optional, Tuple, Union
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from openbb_charting.core.openbb_figure import OpenBBFigure
|
||||
from openbb_charting.core.plotly_ta.data_classes import ChartIndicators
|
||||
from openbb_charting.core.plotly_ta.ta_class import PlotlyTA
|
||||
from openbb_core.app.model.charts.charting_settings import ChartingSettings
|
||||
|
||||
|
||||
def to_chart(
|
||||
charting_settings: ChartingSettings,
|
||||
data: Union[pd.DataFrame, pd.Series],
|
||||
indicators: Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]] = None,
|
||||
symbol: str = "",
|
||||
@@ -19,14 +20,13 @@ def to_chart(
|
||||
) -> Tuple[OpenBBFigure, Dict[str, Any]]:
|
||||
"""
|
||||
Returns the plotly json representation of the chart.
|
||||
|
||||
This function is used so it can be called at the module level and used out of the box,
|
||||
which allows some more flexibility, ease of use and doesn't require the user to know
|
||||
about the PlotlyTA class.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
charting_settings : ChartingSettings
|
||||
Charting settings.
|
||||
data : Union[pd.DataFrame, pd.Series]
|
||||
Data to be plotted.
|
||||
indicators : Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]], optional
|
||||
@@ -47,9 +47,8 @@ def to_chart(
|
||||
Tuple[OpenBBFigure, Dict[str, Any]]
|
||||
Tuple containing the OpenBBFigure and the plotly json representation of the chart.
|
||||
"""
|
||||
|
||||
try:
|
||||
ta = PlotlyTA(charting_settings=charting_settings)
|
||||
ta = PlotlyTA()
|
||||
fig = ta.plot(
|
||||
df_stock=data,
|
||||
indicators=indicators,
|
||||
@@ -0,0 +1 @@
|
||||
"""OpenBB Charting utils."""
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Helper functions for charting."""
|
||||
|
||||
from inspect import getmembers, getsource, isfunction
|
||||
from typing import List
|
||||
|
||||
from openbb_charting import charting_router
|
||||
|
||||
|
||||
def get_charting_functions() -> List[str]:
|
||||
"""Discover charting functions."""
|
||||
implemented_functions = []
|
||||
|
||||
for name, obj in getmembers(charting_router, isfunction):
|
||||
if (
|
||||
obj.__module__ == charting_router.__name__
|
||||
and not name.startswith("_")
|
||||
and "NotImplementedError" not in getsource(obj)
|
||||
):
|
||||
implemented_functions.append(name)
|
||||
|
||||
return implemented_functions
|
||||
1382
openbb_platform/obbject_extensions/charting/poetry.lock
generated
vendored
Normal file
1382
openbb_platform/obbject_extensions/charting/poetry.lock
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
[tool.poetry]
|
||||
name = "openbb-charting"
|
||||
version = "1.1.1"
|
||||
version = "2.0.0"
|
||||
description = "Charting extension for OpenBB"
|
||||
authors = ["OpenBB Team <hello@openbb.co>"]
|
||||
readme = "README.md"
|
||||
packages = [{ include = "openbb_charting" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.8,<3.12" # scipy forces python <4.0 explicitly
|
||||
python = ">=3.8,<3.12" # scipy forces python <4.0 explicitly
|
||||
scipy = "^1.10.0"
|
||||
plotly = "^5.17.0"
|
||||
statsmodels = "^0.14.0"
|
||||
@@ -22,5 +22,5 @@ openbb-core = "^1.1.1"
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.plugins."openbb_core_extension"]
|
||||
openbb_charting = "openbb_charting.charting_router:router"
|
||||
[tool.poetry.plugins."openbb_obbject_extension"]
|
||||
openbb_charting = "openbb_charting:ext"
|
||||
@@ -0,0 +1,131 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openbb_charting import Charting
|
||||
from openbb_core.app.model.system_settings import SystemSettings
|
||||
from openbb_core.app.model.user_settings import UserSettings
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def obbject():
|
||||
class MockStdParams(BaseModel):
|
||||
param1: str
|
||||
param2: str
|
||||
|
||||
class MockOOBject:
|
||||
def __init__(self):
|
||||
self._user_settings = UserSettings()
|
||||
self._system_settings = SystemSettings()
|
||||
self._route = "mock/route"
|
||||
self._standard_params = MockStdParams(
|
||||
param1="mock_param1", param2="mock_param2"
|
||||
)
|
||||
self.results = "mock_results"
|
||||
|
||||
def to_dataframe(self):
|
||||
return "mock_dataframe"
|
||||
|
||||
return MockOOBject()
|
||||
|
||||
|
||||
def test_charting_settings(obbject):
|
||||
cm = Charting(obbject)
|
||||
assert isinstance(cm, Charting)
|
||||
|
||||
|
||||
@patch("openbb_charting.ChartIndicators.get_available_indicators")
|
||||
def test_indicators(mock_get_available_indicators, obbject):
|
||||
# Arrange
|
||||
mock_get_available_indicators.return_value = [
|
||||
"indicator1",
|
||||
"indicator2",
|
||||
"indicator3",
|
||||
]
|
||||
obj = Charting(obbject)
|
||||
|
||||
# Act
|
||||
result = obj.indicators()
|
||||
|
||||
# Assert
|
||||
assert result == ["indicator1", "indicator2", "indicator3"]
|
||||
mock_get_available_indicators.assert_called_once()
|
||||
|
||||
|
||||
@patch("openbb_charting.get_charting_functions")
|
||||
def test_functions(mock_get_charting_functions):
|
||||
# Arrange
|
||||
mock_get_charting_functions.return_value = ["function1", "function2", "function3"]
|
||||
|
||||
# Act
|
||||
result = Charting.functions()
|
||||
|
||||
# Assert
|
||||
assert result == ["function1", "function2", "function3"]
|
||||
mock_get_charting_functions.assert_called_once()
|
||||
|
||||
|
||||
@patch("openbb_charting.core.backend.get_backend")
|
||||
@patch("openbb_charting.core.backend.create_backend")
|
||||
def test_handle_backend(mock_create_backend, mock_get_backend, obbject):
|
||||
# Act -> _handle backend is called in the constructor
|
||||
obj = Charting(obbject)
|
||||
|
||||
# Assert
|
||||
mock_create_backend.assert_called_once_with(obj._charting_settings)
|
||||
mock_get_backend.assert_called_once()
|
||||
|
||||
|
||||
@patch("openbb_charting.charting_router")
|
||||
def test_get_chart_function(mock_charting_router):
|
||||
# Arrange
|
||||
mock_function = MagicMock()
|
||||
mock_charting_router.some_function = mock_function
|
||||
route = "/some/function"
|
||||
|
||||
# Act
|
||||
result = Charting._get_chart_function(route)
|
||||
|
||||
# Assert
|
||||
assert result == mock_function
|
||||
|
||||
|
||||
@patch("openbb_charting.Charting._get_chart_function")
|
||||
def test_show(mock_get_chart_function, obbject):
|
||||
# Arrange
|
||||
mock_function = MagicMock()
|
||||
mock_get_chart_function.return_value = mock_function
|
||||
mock_fig = MagicMock()
|
||||
mock_function.return_value = (mock_fig, {"content": "mock_content"})
|
||||
obj = Charting(obbject)
|
||||
|
||||
# Act
|
||||
obj.show()
|
||||
|
||||
# Assert
|
||||
mock_get_chart_function.assert_called_once()
|
||||
mock_function.assert_called_once()
|
||||
mock_fig.show.assert_called_once()
|
||||
|
||||
|
||||
@patch("openbb_charting.to_chart")
|
||||
def test_to_chart(mock_to_chart, obbject):
|
||||
# Arrange
|
||||
mock_fig = MagicMock()
|
||||
mock_to_chart.return_value = (mock_fig, {"content": "mock_content"})
|
||||
obj = Charting(obbject)
|
||||
|
||||
# Act
|
||||
obj.to_chart()
|
||||
|
||||
# Assert
|
||||
assert obj._obbject.chart.fig == mock_fig
|
||||
mock_to_chart.assert_called_once_with(
|
||||
"mock_dataframe",
|
||||
indicators=None,
|
||||
symbol="",
|
||||
candles=True,
|
||||
volume=True,
|
||||
prepost=False,
|
||||
volume_ticks_x=7,
|
||||
)
|
||||
2
openbb_platform/pyproject.toml
vendored
2
openbb_platform/pyproject.toml
vendored
@@ -47,7 +47,7 @@ openbb-seeking-alpha = { version = "^1.1.1", optional = true }
|
||||
openbb-stockgrid = { version = "^1.1.1", optional = true }
|
||||
openbb-wsj = { version = "^1.1.1", optional = true }
|
||||
|
||||
openbb-charting = { version = "^1.1.1", optional = true }
|
||||
openbb-charting = { version = "^2.0.0", optional = true }
|
||||
openbb-econometrics = { version = "^1.1.1", optional = true }
|
||||
openbb-quantitative = { version = "^1.1.1", optional = true }
|
||||
openbb-technical = { version = "^1.1.2", optional = true }
|
||||
|
||||
Reference in New Issue
Block a user