CatPtain commited on
Commit
4ca09f3
·
verified ·
1 Parent(s): a2afe2f

Upload 51 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. openbb_platform/obbject_extensions/charting/README.md +174 -0
  3. openbb_platform/obbject_extensions/charting/examples.md +166 -0
  4. openbb_platform/obbject_extensions/charting/index.md +359 -0
  5. openbb_platform/obbject_extensions/charting/indicators.md +384 -0
  6. openbb_platform/obbject_extensions/charting/installation.md +71 -0
  7. openbb_platform/obbject_extensions/charting/integration/test_charting_api.py +908 -0
  8. openbb_platform/obbject_extensions/charting/integration/test_charting_python.py +747 -0
  9. openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py +25 -0
  10. openbb_platform/obbject_extensions/charting/openbb_charting/charting.py +681 -0
  11. openbb_platform/obbject_extensions/charting/openbb_charting/charts/__init__.py +1 -0
  12. openbb_platform/obbject_extensions/charting/openbb_charting/charts/correlation_matrix.py +119 -0
  13. openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py +654 -0
  14. openbb_platform/obbject_extensions/charting/openbb_charting/charts/helpers.py +123 -0
  15. openbb_platform/obbject_extensions/charting/openbb_charting/charts/price_historical.py +335 -0
  16. openbb_platform/obbject_extensions/charting/openbb_charting/charts/price_performance.py +108 -0
  17. openbb_platform/obbject_extensions/charting/openbb_charting/charts/relative_rotation.py +674 -0
  18. openbb_platform/obbject_extensions/charting/openbb_charting/core/__init__.py +1 -0
  19. openbb_platform/obbject_extensions/charting/openbb_charting/core/assets/Terminal_icon.png +3 -0
  20. openbb_platform/obbject_extensions/charting/openbb_charting/core/assets/plotly-3.0.0.min.js +0 -0
  21. openbb_platform/obbject_extensions/charting/openbb_charting/core/backend.py +445 -0
  22. openbb_platform/obbject_extensions/charting/openbb_charting/core/chart_style.py +229 -0
  23. openbb_platform/obbject_extensions/charting/openbb_charting/core/config/__init__.py +1 -0
  24. openbb_platform/obbject_extensions/charting/openbb_charting/core/config/openbb_styles.py +308 -0
  25. openbb_platform/obbject_extensions/charting/openbb_charting/core/dummy_backend.py +56 -0
  26. openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py +1662 -0
  27. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly.html +0 -0
  28. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/__init__.py +1 -0
  29. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/base.py +220 -0
  30. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/data_classes.py +395 -0
  31. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/__init__.py +1 -0
  32. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/custom_indicators_plugin.py +218 -0
  33. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/momentum_plugin.py +629 -0
  34. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/overlap_plugin.py +96 -0
  35. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/trend_indicators_plugin.py +197 -0
  36. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/volatility_plugin.py +223 -0
  37. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/volume_plugin.py +125 -0
  38. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/ta_class.py +676 -0
  39. openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/ta_helpers.py +53 -0
  40. openbb_platform/obbject_extensions/charting/openbb_charting/core/table.html +0 -0
  41. openbb_platform/obbject_extensions/charting/openbb_charting/core/to_chart.py +68 -0
  42. openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py +852 -0
  43. openbb_platform/obbject_extensions/charting/openbb_charting/styles/__init__.py +1 -0
  44. openbb_platform/obbject_extensions/charting/openbb_charting/styles/colors.py +39 -0
  45. openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/dark.pltstyle.json +143 -0
  46. openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/light.pltstyle.json +142 -0
  47. openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/tables.pltstyle.json +102 -0
  48. openbb_platform/obbject_extensions/charting/poetry.lock +0 -0
  49. openbb_platform/obbject_extensions/charting/pyproject.toml +26 -0
  50. openbb_platform/obbject_extensions/charting/tests/__init__.py +1 -0
.gitattributes CHANGED
@@ -1,3 +1,4 @@
1
  * linguist-vendored
2
  *.py linguist-vendored=false
3
  text eol=lf
 
 
1
  * linguist-vendored
2
  *.py linguist-vendored=false
3
  text eol=lf
4
+ openbb_platform/obbject_extensions/charting/openbb_charting/core/assets/Terminal_icon.png filter=lfs diff=lfs merge=lfs -text
openbb_platform/obbject_extensions/charting/README.md ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # OpenBB Charting extension
2
+
3
+ This extension provides a charting library for OpenBB Platform.
4
+
5
+ The library includes:
6
+
7
+ - a charting infrastructure based on Plotly
8
+ - a set of charting components
9
+ - prebuilt charts for a set of commands that are built-in OpenBB extensions
10
+
11
+ >[!NOTE]
12
+ > The charting library is an `OBBject` extension which means you'll have the functionality it exposes on every command result.
13
+
14
+ ## Installation
15
+
16
+ To install the extension, run the following command in this folder:
17
+
18
+ ```bash
19
+ pip install openbb-charting
20
+ ```
21
+
22
+ ## PyWry dependency on Linux
23
+
24
+ The PyWry dependency handles the display of interactive charts and tables in a separate window. It is installed automatically with the OpenBB Charting extension.
25
+
26
+ When using Linux distributions, the PyWry dependency requires certain dependencies to be installed first.
27
+
28
+ - Debian-based / Ubuntu / Mint:
29
+ `sudo apt install libwebkit2gtk-4.0-dev`
30
+
31
+ - Arch Linux / Manjaro:
32
+ `sudo pacman -S webkit2gtk`
33
+
34
+ - Fedora:
35
+ `sudo dnf install gtk3-devel webkit2gtk3-devel`
36
+
37
+ ## Usage
38
+
39
+ To use the extension, run any of the OpenBB Platform endpoints with the `chart` argument set to `True`.
40
+
41
+ Here's an example of how it would look like in a python interface:
42
+
43
+ ```python
44
+ from openbb import obb
45
+ equity_data = obb.equity.price.historical(symbol="TSLA", chart=True)
46
+ ```
47
+
48
+ This results in a `OBBject` object containing a `chart` attribute, which contains Plotly JSON data.
49
+
50
+ In order to display the chart, you need to call the `show()` method:
51
+
52
+ ```python
53
+ equity_data.show()
54
+ ```
55
+
56
+ > Note: The `show()` method currently works either in a Jupyter Notebook or in a standalone python script with a PyWry based backend properly initialized.
57
+
58
+ Alternatively, you can use the fact that the `openbb-charting` is an `OBBject` extension and use its available methods.
59
+
60
+ ```python
61
+ from openbb import obb
62
+ res = obb.equity.price.historical("AAPL")
63
+ res.charting.show()
64
+ ```
65
+
66
+ The above code will produce the same effect as the previous example.
67
+
68
+ ### Discovering available charts
69
+
70
+ Not all the endpoints are currently supported by the charting extension. To discover which endpoints are supported, you can run the following command:
71
+
72
+ ```python
73
+ from openbb_charting import Charting
74
+ Charting.functions()
75
+ ```
76
+
77
+ ### Using the `to_chart` method
78
+
79
+ 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.
80
+
81
+ 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.
82
+
83
+ > Note that, this method will only work to some limited extent with data that is not standardized.
84
+ > Also, it is currently designed only to handle time series (OHLCV) data.
85
+
86
+ Example usage:
87
+
88
+ - Plotting a time series with TA indicators
89
+
90
+ ```python
91
+
92
+ from openbb import obb
93
+ res = obb.equity.price.historical("AAPL")
94
+
95
+ indicators = dict(
96
+ sma=dict(length=[20,30,50]),
97
+ adx=dict(length=14),
98
+ rsi=dict(length=14),
99
+ macd=dict(fast=12, slow=26, signal=9),
100
+ bbands=dict(length=20, std=2),
101
+ stoch=dict(length=14),
102
+ ema=dict(length=[20,30,50]),
103
+ )
104
+ res.charting.to_chart(**{"indicators": indicators})
105
+
106
+ ```
107
+
108
+ - Get all the available indicators
109
+
110
+ ```python
111
+
112
+ # if you have a command result already
113
+ res.charting.indicators
114
+
115
+ # or if you want to know in standalone fashion
116
+ from openbb_charting import Charting
117
+ Charting.indicators()
118
+
119
+ ```
120
+
121
+ ## Add a visualization to an existing Platform command
122
+
123
+ To add a visualization to an existing command, you'll need to add a `poetry` plugin to your `pyproject.toml` file. The syntax should be the following:
124
+
125
+ ```toml
126
+ [tool.poetry.plugins."openbb_charting_extension"]
127
+ my_extension = "openbb_my_extension.my_extension_views:MyExtensionViews"
128
+ ```
129
+
130
+ Where the `openbb_charting_extension` is **mandatory**, otherwise the charting extension won't be able to find the visualization.
131
+
132
+ And the suggested structure for the `my_extension_views` module is the following:
133
+
134
+ ```python
135
+ """Views for MyExtension."""
136
+
137
+ from typing import Any, Dict, Tuple
138
+
139
+ from openbb_charting.charts.price_historical import price_historical
140
+ from openbb_charting.core.openbb_figure import OpenBBFigure
141
+
142
+
143
+ class MyExtensionViews:
144
+ """MyExtension Views."""
145
+
146
+ @staticmethod
147
+ def my_extension_price_historical(
148
+ **kwargs,
149
+ ) -> Tuple[OpenBBFigure, Dict[str, Any]]:
150
+ """MyExtension Price Historical Chart."""
151
+ return price_historical(**kwargs)
152
+ ```
153
+
154
+ > Note that `my_extension_views` lives under the `openbb_my_extension` package.
155
+
156
+ Afterwards, you'll need to add the visualization to your new `MyExtensionViews` class. The convention to match the endpoint with the respective charting function is the following:
157
+
158
+ - `/equity/price/historical` -> `equity_price_historical`
159
+ - `/technical/ema` -> `technical_ema`
160
+ - `/my_extension/price_historical` -> `my_extension_price_historical`
161
+
162
+ When you spot the charting function on the charting router file, you can add the visualization to it.
163
+
164
+ The implementation should leverage the already existing classes and methods to do so, namely:
165
+
166
+ - `OpenBBFigure`
167
+ - `PlotlyTA`
168
+
169
+ Note that the return of each charting function should respect the already defined return types: `Tuple[OpenBBFigure, Dict[str, Any]]`.
170
+
171
+ 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.
172
+
173
+ 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`.
174
+ Or accessing the `charting` attribute of the `OBBject` object: `my_obbject.charting.show()`.
openbb_platform/obbject_extensions/charting/examples.md ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Examples
3
+ sidebar_position: 1
4
+ description: This page provides examples of creating charts with the `openbb-charting` extension.
5
+ keywords:
6
+ - tutorial
7
+ - OpenBB Platform
8
+ - Python client
9
+ - Fast API
10
+ - getting started
11
+ - extensions
12
+ - charting
13
+ - view
14
+ - Plotly
15
+ - toolkits
16
+ - how-to
17
+ - generic
18
+ - figure
19
+ ---
20
+
21
+ import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
22
+
23
+ <HeadTitle title="Examples - OpenBB Charting - Extensions | OpenBB Platform Docs" />
24
+
25
+ ## Overview
26
+
27
+ This page will walk through creating different charts using the `openbb-charting` extension.
28
+ The perspective for this content is from the Python Interface,
29
+ and the examples will assume that the OpenBB Platform is installed with all optional packages.
30
+
31
+ ```python
32
+ from datetime import datetime, timedelta
33
+ from openbb import obb
34
+ ```
35
+
36
+ ## Cumulative Returns
37
+
38
+ The historical (equity) prices can be requested for multiple symbols.
39
+ The extension will attempt to handle variations accordingly.
40
+ By default, more than three symbols will draw the chart as cumulative returns from the beginning of the series.
41
+
42
+ ### Default View
43
+
44
+ The tickers below are a collection of State Street Global Advisors SPDR funds, representing S&P 500 components.
45
+ The data is looking back five years.
46
+
47
+ ```python
48
+ SPDRS = [
49
+ "SPY",
50
+ "XLE",
51
+ "XLB",
52
+ "XLI",
53
+ "XHB",
54
+ "XLP",
55
+ "XLY",
56
+ "XRT",
57
+ "XLF",
58
+ "XLV",
59
+ "XLK",
60
+ "XLC",
61
+ "XLU",
62
+ "XLRE",
63
+ ]
64
+ start_date = (datetime.now() - timedelta(weeks=52*5)).date()
65
+ spdrs = obb.equity.price.historical(SPDRS, start_date=start_date, provider="yfinance", chart=True)
66
+
67
+ spdrs.show()
68
+ ```
69
+
70
+ ![SPDRs Cumulative Returns - 5 years](https://github.com/OpenBB-finance/OpenBB/assets/85772166/8884f4ed-b09c-4161-9dc6-87ad66d9fc8b)
71
+
72
+ ### Redraw as YTD
73
+
74
+ The `charting` attribute of the command output has methods for creating the chart again.
75
+ The `data` parameter allows modifications to the data before creating the figure.
76
+ In this example, the length of the data is trimmed to the beginning of the year.
77
+
78
+ ```python
79
+ new_data = spdrs.to_df().loc[datetime(2024,12,29).date():]
80
+ spdrs.charting.to_chart(data=new_data, title="YTD")
81
+ ```
82
+
83
+ :::note
84
+ This replaces the chart that was already created.
85
+ :::
86
+
87
+ ![SPDRs Cumulative Returns - YTD](https://github.com/OpenBB-finance/OpenBB/assets/85772166/22ed2588-1098-4712-aec1-54dd22c324ef)
88
+
89
+ ## Price Performance Bar Chart
90
+
91
+ The `obb.equity.price.performance` endpoint will create a bar chart over intervals.
92
+
93
+ ```python
94
+ price_performance = obb.equity.price.performance(SPDRS, chart=True)
95
+ price_performance.show()
96
+ ```
97
+
98
+ ![Price Performance](https://github.com/OpenBB-finance/OpenBB/assets/85772166/0de3260d-7fce-490b-90e1-bdfa38d6ab23)
99
+
100
+ ### Create Bar Chart
101
+
102
+ This example uses the `create_bar_chart()` method, which does not replace the existing chart, in `price_performance.chart`.
103
+ It isolates the one-month performance and orients the layout as horizontal.
104
+
105
+ ```python
106
+ new_data = price_performance.to_df().set_index("symbol").multiply(100).reset_index()
107
+ price_performance.charting.create_bar_chart(
108
+ data=new_data,
109
+ x="symbol",
110
+ y="one_month",
111
+ orientation="h",
112
+ title="One Month Price Performance",
113
+ xtitle="Percent (%)"
114
+ )
115
+ ```
116
+
117
+ ![Horizonontal Price Performance](https://github.com/OpenBB-finance/OpenBB/assets/85772166/8da01f73-d7a8-4168-846a-9fa9ed6a0e39)
118
+
119
+ ## Create Your Own
120
+
121
+ This example analyzes the share volume turnover of the S&P 500 Energy Sector constituents, year-to-date.
122
+
123
+ ```python
124
+ symbols = [
125
+ 'XOM',
126
+ 'CVX',
127
+ 'COP',
128
+ 'WMB',
129
+ 'EOG',
130
+ 'KMI',
131
+ 'OKE',
132
+ 'MPC',
133
+ 'PSX',
134
+ 'SLB',
135
+ 'VLO',
136
+ 'BKR',
137
+ 'HES',
138
+ 'TRGP',
139
+ 'EQT',
140
+ 'OXY',
141
+ 'TPL',
142
+ 'FANG',
143
+ 'EXE',
144
+ 'DVN',
145
+ 'HAL',
146
+ 'CTRA',
147
+ 'APA',
148
+ ]
149
+ data = obb.equity.price.historical(symbols, start_date="2025-01-01", provider="yfinance")
150
+ create_bar_chart = data.charting.create_bar_chart
151
+ volume = data.to_df().groupby("symbol").sum()["volume"]
152
+ shares = obb.equity.profile(
153
+ symbols, provider="yfinance"
154
+ ).to_df().set_index("symbol")["shares_float"]
155
+ df = volume.to_frame().join(shares)
156
+ df["Turnover"] = (df.volume/df.shares_float).round(4)
157
+ df = df.sort_values(by="Turnover", ascending=False).reset_index()
158
+ create_bar_chart(
159
+ data=df,
160
+ x="symbol",
161
+ y="Turnover",
162
+ title="S&P Energy Sector YTD Turnover Rate",
163
+ )
164
+ ```
165
+
166
+ ![S&P 500 Energy Sector Turnover Rate](https://github.com/OpenBB-finance/OpenBB/assets/85772166/d29a1c17-6d3b-4925-8b7e-f661da404967)
openbb_platform/obbject_extensions/charting/index.md ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: OpenBB Charting
3
+ sidebar_position: 1
4
+ description: This page introduces the optional openbb-charting extension.
5
+ keywords:
6
+ - explanation
7
+ - OpenBB Platform
8
+ - Python client
9
+ - Fast API
10
+ - getting started
11
+ - extensions
12
+ - charting
13
+ - view
14
+ - Plotly
15
+ - toolkits
16
+ - community
17
+ - Plotly
18
+ - OpenBBFigure
19
+ - PyWry
20
+ ---
21
+
22
+ import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
23
+
24
+ <HeadTitle title="OpenBB Charting - Extensions | OpenBB Platform Docs" />
25
+
26
+ ## Overview
27
+
28
+ The `openbb-charting` extension provides elements for building and displaying interactive charts, tables, dashboards, and more, directly from the OpenBB Platform's Python Interface and FAST API.
29
+
30
+ It allows users to create a custom view, without any previous experience working with Plotly, from any response served by the OpenBB Platform.
31
+
32
+ The Python Interface includes a custom [PyWry](https://github.com/OpenBB-finance/pywry) backend for displaying any content, in a WebKit HTML window served over `localhost`. In an IDE setting, they will be rendered inline.
33
+
34
+ To install, follow the instructions [here](installation). The sections below provide a general explanation of the extension.
35
+
36
+ ## How Does It Work?
37
+
38
+ It works by extending the `OBBject` class with a new attribute, `charting`. When it is installed, every response from the OpenBB Platform will be equipped with these tools.
39
+
40
+ For functions that have pre-defined views, it serves as an intermediary between the user request and the response, activated when `chart=True`. When a chart is created, it will populate the existing, `chart`, attribute of the `OBBject`. This is where it is served by the FAST API from the function request. In the Python Interface, charts can be generated post-request, regardless of `chart=True`.
41
+
42
+ The `chart` attribute in the OBBject contains three items, responses from the API have two:
43
+
44
+ - `fig`: The OpenBBFigure object - an extended Plotly GraphObjects class. Not included in the API response.
45
+ - `content`: The Plotly JSON representation of the chart - Returned to the API.
46
+ - `format`: The format of the chart - 'plotly' is currently the only charting library.
47
+
48
+ There is one OBBject class method, `show()`, which will display the contents of the `chart` attribute, if populated.
49
+
50
+ The new `charting` attribute that binds to the OBBject also has a `show()` method. This differs in that it overwrites the existing chart, effectively a 'reset' for the view.
51
+
52
+ The extension has a docstring, and it lists the class methods within `charting`.
53
+
54
+ ```python
55
+ from openbb import obb
56
+ data = obb.equity.price.historical("AAPL")
57
+ data.charting?
58
+ ```
59
+
60
+ ```console
61
+ Charting extension.
62
+
63
+ Methods
64
+ -------
65
+ show
66
+ Display chart and save it to the OBBject.
67
+ to_chart
68
+ Redraw the chart and save it to the OBBject, with an optional entry point for Data.
69
+ functions
70
+ Return a list of Platform commands with charting functions.
71
+ get_params
72
+ Return the charting parameters for the function the OBBject was created from.
73
+ indicators
74
+ Return the list of the available technical indicators to use with the `to_chart` method and OHLC+V data.
75
+ table
76
+ Display an interactive table.
77
+ create_line_chart
78
+ Create a line chart from external data.
79
+ create_bar_chart
80
+ Create a bar chart, on a single x-axis with one or more values for the y-axis, from external data.
81
+ ```
82
+
83
+ :::note
84
+ When creating a chart directly from the OpenBB Platform endpoint, chart parameters must be passed as a nested dictionary under the name, `chart_params`.
85
+
86
+ ```python
87
+ chart_params = dict(
88
+ title="AAPL 50/200 Day EMA",
89
+ indicators=dict(
90
+ ema=dict(length=[50,200]),
91
+ ),
92
+ )
93
+ params = dict(
94
+ symbol="AAPL",
95
+ start_date="2022-01-01",
96
+ provider="yfinance",
97
+ chart=True,
98
+ chart_params=chart_params,
99
+ )
100
+ data = obb.equity.price.historical(**params)
101
+ ```
102
+
103
+ `chart_params` are sent in the body of the request when using the API.
104
+ :::
105
+
106
+ Passing only `chart=True` will return a default view which can be modified and drawn again post-request, via the `OBBject`.
107
+
108
+ ```console
109
+ OBBject
110
+
111
+ id: 06614d74-7443-7201-8000-a65f358136a3
112
+ results: [{'date': datetime.date(2022, 1, 3), 'open': 177.8300018310547, 'high': 18...
113
+ provider: yfinance
114
+ warnings: None
115
+ chart: {'content': {'data': [{'close': [182.00999450683594, 179.6999969482422, 174....
116
+ extra: {'metadata': {'arguments': {'provider_choices': {'provider': 'yfinance'}, 's...
117
+ ```
118
+
119
+ ```python
120
+ data.show()
121
+ ```
122
+
123
+ ![candles with ema](https://github.com/OpenBB-finance/OpenBB/assets/85772166/b427d68b-777e-4230-852a-df749c5dbc46)
124
+
125
+ ### No Render
126
+
127
+ The charts can be created without opening the PyWry window, and this is the default behaviour when `chart=True`.
128
+ With the `charting.show()` and `charting.to_chart()` methods, the default is `render=True`.
129
+ Setting as `False` will return the chart to itself, populating the `chart` attribute of OBBject.
130
+
131
+ ## What Endpoints Have Charts?
132
+
133
+ The OpenBB Platform router, open_api.json, function signatures, and documentation are all generated based on your specific configuration. When the `openbb-charting` extension is installed, any function found in the "[charting_router](https://github.com/OpenBB-finance/OpenBB/blob/develop/openbb_platform/obbject_extensions/charting/openbb_charting/charting_router.py)" adds `chart: bool = False` to the command on build. For example, `obb.index.price.historical?`
134
+
135
+ ```python
136
+ Signature:
137
+ obb.index.price.historical(
138
+ symbol: Annotated[Union[str, List[str]], OpenBBCustomParameter(description='Symbol to get data for. Multiple comma separated items allowed for provider(s): cboe, fmp, intrinio, polygon, yfinance.')],
139
+ ...
140
+ chart: typing.Annotated[bool, OpenBBCustomParameter(description='Whether to create a chart or not, by default False.')] = False,
141
+ **kwargs,
142
+ ) -> openbb_core.app.model.obbject.OBBject
143
+ ```
144
+
145
+ ### Charting Functions
146
+
147
+ The `charting` attribute of every command output has methods for identifying the charting functions and parameters.
148
+ While able to serve JSON-serializable charts, the `openbb-charting` extension is best-suited for use with the Python Interface. Much of the functionality is realized post-request.
149
+
150
+ Examine the extension by returning any command at all.
151
+
152
+ ```python
153
+ from openbb import obb
154
+
155
+ data = obb.equity.price.historical("SPY,QQQ,XLK,BTC-USD", provider="yfinance")
156
+
157
+ data.charting.functions()
158
+ ```
159
+
160
+ ```console
161
+ ['crypto_price_historical',
162
+ 'currency_price_historical',
163
+ 'economy_fred_series',
164
+ 'equity_price_historical',
165
+ 'equity_price_performance',
166
+ 'etf_historical',
167
+ 'etf_holdings',
168
+ 'etf_price_performance',
169
+ 'index_price_historical',
170
+ 'technical_adx',
171
+ 'technical_aroon',
172
+ 'technical_cones',
173
+ 'technical_ema',
174
+ 'technical_hma',
175
+ 'technical_macd',
176
+ 'technical_rsi',
177
+ 'technical_sma',
178
+ 'technical_wma',
179
+ 'technical_zlma']
180
+ ```
181
+
182
+ :::tip
183
+ The list above should, as shown here, should not be considered as the source of truth. It's just a sample.
184
+ :::
185
+
186
+ If the `OBBject` in question has a dedicated charting function associated with it, parameters are detailed by the `get_params()` method.
187
+
188
+ ```console
189
+ EquityPriceHistoricalChartQueryParams
190
+
191
+ Parameters
192
+ ----------
193
+
194
+ data : Union[Data, list[Data], NoneType]
195
+ Filtered versions of the data contained in the original `self.results`.
196
+ Columns should be the same as the original data.
197
+ Example use is to reduce the number of columns, or the length of data, to plot.
198
+
199
+ title : Union[str, NoneType]
200
+ Title of the chart.
201
+
202
+ target : Union[str, NoneType]
203
+ The specific column to target.
204
+ If supplied, this will override the candles and volume parameters.
205
+
206
+ multi_symbol : bool
207
+ Flag to indicate whether the data contains multiple symbols.
208
+ This is mostly handled automatically, but if the chart fails to generate try setting this to True.
209
+
210
+ same_axis : bool
211
+ If True, forces all data to be plotted on the same axis.
212
+
213
+ normalize : bool
214
+ If True, the data will be normalized and placed on the same axis.
215
+
216
+ returns : bool
217
+ If True, the cumulative returns for the length of the time series will be calculated and plotted.
218
+
219
+ candles : bool
220
+ If True, and OHLC exists, and there is only one symbol in the data, candles will be plotted.
221
+
222
+ heikin_ashi : bool
223
+ If True, and `candles=True`, Heikin Ashi candles will be plotted.
224
+
225
+ volume : bool
226
+ If True, and volume exists, and `candles=True`, volume will be plotted.
227
+
228
+ indicators : Union[ChartIndicators, dict[str, dict[str, Any]], NoneType]
229
+ Indicators to be plotted, formatted as a dictionary.
230
+ Data containing multiple symbols will ignore indicators.
231
+ Example:
232
+ indicators = dict(
233
+ sma=dict(length=[20,30,50]),
234
+ adx=dict(length=14),
235
+ rsi=dict(length=14),
236
+ )
237
+ ```
238
+
239
+ Not all commands will have the same `chart_params`, and some less than others, but it is always possible to redraw the chart with a different combination post-request. Here's what the default chart is from the output of the command above.
240
+
241
+ If `chart=True` was not specified, it will need to be created.
242
+
243
+ ```python
244
+ data.charting.to_chart()
245
+ ```
246
+
247
+ ![obb.equity.price.historical()](https://github.com/OpenBB-finance/OpenBB/assets/85772166/9231c455-ee1b-47a8-a627-b0034ea52ecd)
248
+
249
+ The extension recognized that multiple symbols were within the object, and made a determination to display cumulative returns by default.
250
+
251
+ A candlestick chart will draw only when there is one symbol in the data.
252
+
253
+ ```python
254
+ obb.equity.price.historical(
255
+ symbol="XLK",
256
+ start_date="2024-01-01",
257
+ provider="yfinance",
258
+ chart=True,
259
+ chart_params=dict(title="XLK YTD", heikin_ashi=True)
260
+ ).show()
261
+ ```
262
+
263
+ ![obb.equity.price.historical()](https://github.com/OpenBB-finance/OpenBB/assets/85772166/13af30b3-7298-402d-ac32-1f7700cd08fd)
264
+
265
+ ## Endpoints Without Charts
266
+
267
+ Most functions do not have dedicated charts. However, it's still possible to generate one automatically. Using the `data` above, we can try passing it through a quantitative analysis command.
268
+
269
+ ```python
270
+ data = obb.equity.price.historical(
271
+ symbol="XLK",
272
+ start_date="2023-01-01",
273
+ provider="yfinance",
274
+ )
275
+ qa = obb.quantitative.rolling.stdev(data.results, target="close")
276
+
277
+ qa.charting.show(title="XLK Rolling 21 Day Standard Deviation")
278
+ ```
279
+
280
+ ![auto chart](https://github.com/OpenBB-finance/OpenBB/assets/85772166/f87a6648-7365-4529-a254-35897af448ca)
281
+
282
+ ## Charts From Any Data
283
+
284
+ There are methods for creating a generic chart from any external data.
285
+ They will bypass any data contained in the parent object, unless specifically fed into itself.
286
+
287
+ - charting.create_bar_chart()
288
+ - charting.create_line_chart()
289
+
290
+ They can also be used as standalone components by initializing an empty instance of the OBBject class.
291
+
292
+ ```python
293
+ from openbb import obb
294
+ from openbb_core.app.model.obbject import OBBject
295
+ create_bar_chart = OBBject(results=None).charting.create_bar_chart
296
+
297
+ create_bar_chart?
298
+ ````
299
+
300
+ ```console
301
+ Create a bar chart on a single x-axis with one or more values for the y-axis.
302
+
303
+ Parameters
304
+ ----------
305
+ data : Union[list, dict, pd.DataFrame, List[pd.DataFrame], pd.Series, List[pd.Series], np.ndarray, Data]
306
+ Data to plot.
307
+ x : str
308
+ The x-axis column name.
309
+ y : Union[str, List[str]]
310
+ The y-axis column name(s).
311
+ barmode : Literal["group", "stack", "relative", "overlay"], optional
312
+ The bar mode, by default "group".
313
+ xtype : Literal["category", "multicategory", "date", "log", "linear"], optional
314
+ The x-axis type, by default "category".
315
+ title : Optional[str], optional
316
+ The title of the chart, by default None.
317
+ xtitle : Optional[str], optional
318
+ The x-axis title, by default None.
319
+ ytitle : Optional[str], optional
320
+ The y-axis title, by default None.
321
+ orientation : Literal["h", "v"], optional
322
+ The orientation of the chart, by default "v".
323
+ colors: Optional[List[str]], optional
324
+ Manually set the colors to cycle through for each column in 'y', by default None.
325
+ layout_kwargs : Optional[Dict[str, Any]], optional
326
+ Additional keyword arguments to apply with figure.update_layout(), by default None.
327
+
328
+ Returns
329
+ -------
330
+ OpenBBFigure
331
+ The OpenBBFigure object.
332
+ ```
333
+
334
+ ## Tables
335
+
336
+ The `openbb-charting` extension is equipped with interactive tables, utilizing the React framework. They are displayed by using the `table` method.
337
+
338
+ ```python
339
+ data = obb.equity.price.quote("AAPL,MSFT,GOOGL,META,TSLA,AMZN", provider="yfinance")
340
+ data.charting.table()
341
+ ```
342
+
343
+ ![Interactive Tables](https://github.com/OpenBB-finance/OpenBB/assets/85772166/77f5f812-b933-4ced-929c-c1e39b2a3eed)
344
+
345
+ External data can also be supplied, providing an opportunity to filter or apply Pandas operations before display.
346
+
347
+ ```python
348
+ new_df = df.to_df().T
349
+ new_df.index.name="metric"
350
+ new_df.columns = new_df.loc["symbol"]
351
+ new_df.drop("symbol", inplace=True)
352
+ data.charting.table(data=new_df)
353
+ ```
354
+
355
+ ![Tables From External Data](https://github.com/OpenBB-finance/OpenBB/assets/85772166/d02f8c34-e1d1-4001-a73e-d3b948a4c5c1)
356
+
357
+ :::important
358
+ This does not alter the contents of the original object, the displayed data is a copy.
359
+ :::
openbb_platform/obbject_extensions/charting/indicators.md ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Indicators
3
+ sidebar_position: 2
4
+ description: A tutorial of the technical indicators included with the openbb-charting library, including how to get started using them.
5
+ keywords:
6
+ - tutorial
7
+ - OpenBB Platform
8
+ - getting started
9
+ - extensions
10
+ - charting
11
+ - view
12
+ - Plotly
13
+ - toolkits
14
+ - indicators
15
+ - Plotly
16
+ - OpenBBFigure
17
+ - PyWry
18
+ ---
19
+
20
+ import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
21
+
22
+ <HeadTitle title="Indicators - OpenBB Charting - Extensions | OpenBB Platform Docs" />
23
+
24
+ ## Introduction
25
+
26
+ Select indicators (technical) can be added to a chart where the data is OHLC+V prices over time, and the data is for one symbol only.
27
+ They are meant as quick visualizations, and a way to build more complex charts.
28
+ As starting points, they can be refined to perfection by manipulating the figure object directly.
29
+
30
+ ```python
31
+ from datetime import datetime, timedelta
32
+ from openbb import obb
33
+ data = obb.equity.price.historical(
34
+ "TSLA",
35
+ provider="yfinance",
36
+ interval="15m",
37
+ start_date=(datetime.now()-timedelta(days=21)).date(),
38
+ chart=True,
39
+ chart_params=dict(
40
+ heikin_ashi=True,
41
+ indicators=(dict(
42
+ ema=dict(length=[8,32]),
43
+ srlines={}, # For indicators, an empty dictionary implies the default state.
44
+ rsi=dict(length=32)
45
+ ))
46
+ )
47
+ )
48
+ data.show()
49
+ ```
50
+
51
+ ![TSLA Intraday With Indicators](https://github.com/OpenBB-finance/OpenBB/assets/85772166/7d8d95d8-0383-4e9d-9477-7ad2424328df)
52
+
53
+ ## Available Indicators
54
+
55
+ To get all the indicators, use the `charting.indicators()` method.
56
+ The object returned is a Pydantic model where each indicator is field.
57
+ If you don't catch it, it will print as a docstring to the console.
58
+
59
+ :::danger
60
+ Some indicators, like RSI and MACD, create subplots. Only 4 subplots (not including the main candles + volume) can be created within the same view.
61
+ :::
62
+
63
+ ```python
64
+ data.charting.indicators()
65
+ ```
66
+
67
+ ```console
68
+ SMA:
69
+
70
+ Parameters
71
+ ----------
72
+
73
+ length : Union[int, list[int]]
74
+ Window length for the moving average, by default is 50.
75
+ The number is relative to the interval of the time series data.
76
+
77
+ offset : int
78
+ Number of periods to offset for the moving average, by default is 0.
79
+
80
+ EMA:
81
+
82
+ Parameters
83
+ ----------
84
+
85
+ length : Union[int, list[int]]
86
+ Window length for the moving average, by default is 50.
87
+ The number is relative to the interval of the time series data.
88
+
89
+ offset : int
90
+ Number of periods to offset for the moving average, by default is 0.
91
+
92
+ HMA:
93
+
94
+ Parameters
95
+ ----------
96
+
97
+ length : Union[int, list[int]]
98
+ Window length for the moving average, by default is 50.
99
+ The number is relative to the interval of the time series data.
100
+
101
+ offset : int
102
+ Number of periods to offset for the moving average, by default is 0.
103
+
104
+ WMA:
105
+
106
+ Parameters
107
+ ----------
108
+
109
+ length : Union[int, list[int]]
110
+ Window length for the moving average, by default is 50.
111
+ The number is relative to the interval of the time series data.
112
+
113
+ offset : int
114
+ Number of periods to offset for the moving average, by default is 0.
115
+
116
+ ZLMA:
117
+
118
+ Parameters
119
+ ----------
120
+
121
+ length : Union[int, list[int]]
122
+ Window length for the moving average, by default is 50.
123
+ The number is relative to the interval of the time series data.
124
+
125
+ offset : int
126
+ Number of periods to offset for the moving average, by default is 0.
127
+
128
+ AD:
129
+
130
+ Parameters
131
+ ----------
132
+
133
+ offset : int
134
+ Offset value for the AD, by default is 0.
135
+
136
+ AD Oscillator:
137
+
138
+ Parameters
139
+ ----------
140
+
141
+ fast : int
142
+ Number of periods to use for the fast calculation, by default 3.
143
+
144
+ slow : int
145
+ Number of periods to use for the slow calculation, by default 10.
146
+
147
+ offset : int
148
+ Offset to be used for the calculation, by default is 0.
149
+
150
+ ADX:
151
+
152
+ Parameters
153
+ ----------
154
+
155
+ length : int
156
+ Window length for the ADX, by default is 50.
157
+
158
+ scalar : float
159
+ Scalar to multiply the ADX by, default is 100.
160
+
161
+ drift : int
162
+ Drift value for the ADX, by default is 1.
163
+
164
+ Aroon:
165
+
166
+ Parameters
167
+ ----------
168
+
169
+ length : int
170
+ Window length for the Aroon, by default is 50.
171
+
172
+ scalar : float
173
+ Scalar to multiply the Aroon by, default is 100.
174
+
175
+ ATR:
176
+
177
+ Parameters
178
+ ----------
179
+
180
+ length : int
181
+ Window length for the ATR, by default is 14.
182
+
183
+ mamode : Literal[rma, ema, sma, wma]
184
+ The mode to use for the moving average calculation.
185
+
186
+ drift : int
187
+ The difference period.
188
+
189
+ offset : int
190
+ Number of periods to offset the result, by default is 0.
191
+
192
+ CCI:
193
+
194
+ Parameters
195
+ ----------
196
+
197
+ length : int
198
+ Window length for the CCI, by default is 14.
199
+
200
+ scalar : float
201
+ Scalar to multiply the CCI by, default is 0.015.
202
+
203
+ Clenow:
204
+
205
+ Parameters
206
+ ----------
207
+
208
+ period : int
209
+ The number of periods for the momentum, by default 90.
210
+
211
+ Demark:
212
+
213
+ Parameters
214
+ ----------
215
+
216
+ show_all : bool
217
+ Show 1 - 13.
218
+ If set to False, show 6 - 9.
219
+
220
+ offset : int
221
+ Number of periods to offset the result, by default is 0.
222
+
223
+ Donchian:
224
+
225
+ Parameters
226
+ ----------
227
+
228
+ lower : Union[int, NoneType]
229
+ Window length for the lower band, by default is 20.
230
+
231
+ upper : Union[int, NoneType]
232
+ Window length for the upper band, by default is 20.
233
+
234
+ offset : Union[int, NoneType]
235
+ Number of periods to offset the result, by default is 0.
236
+
237
+ Fib:
238
+
239
+ Parameters
240
+ ----------
241
+
242
+ period : int
243
+ The period to calculate the Fibonacci Retracement, by default 120.
244
+
245
+ start_date : Union[str, NoneType]
246
+ The start date for the Fibonacci Retracement.
247
+
248
+ end_date : Union[str, NoneType]
249
+ The end date for the Fibonacci Retracement.
250
+
251
+ Fisher:
252
+
253
+ Parameters
254
+ ----------
255
+
256
+ length : int
257
+ Window length for the Fisher Transform, by default is 14.
258
+
259
+ signal : int
260
+ Fisher Signal Period
261
+
262
+ Ichimoku:
263
+
264
+ Parameters
265
+ ----------
266
+
267
+ conversion : int
268
+ The conversion line period, by default 9.
269
+
270
+ base : int
271
+ The base line period, by default 26.
272
+
273
+ lagging : int
274
+ The lagging line period, by default 52.
275
+
276
+ offset : int
277
+ The offset period, by default 26.
278
+
279
+ lookahead : bool
280
+ Drops the Chikou Span Column to prevent potential data leak
281
+
282
+ KC:
283
+
284
+ Parameters
285
+ ----------
286
+
287
+ length : int
288
+ Window length for the Keltner Channel, by default is 20.
289
+
290
+ scalar : float
291
+ Scalar to multiply the ATR, by default is 2.
292
+
293
+ mamode : Literal[ema, sma, wma, hna, zlma, rma]
294
+ The mode to use for the moving average calculation, by default is ema.
295
+
296
+ offset : int
297
+ Number of periods to offset the result, by default is 0.
298
+
299
+ MACD:
300
+
301
+ Parameters
302
+ ----------
303
+
304
+ fast : Union[int, NoneType]
305
+ Window length for the fast EMA, by default is 12.
306
+
307
+ slow : Union[int, NoneType]
308
+ Window length for the slow EMA, by default is 26.
309
+
310
+ signal : Union[int, NoneType]
311
+ Window length for the signal line, by default is 9.
312
+
313
+ scalar : Union[float, NoneType]
314
+ Scalar to multiply the MACD by, default is 100.
315
+
316
+ OBV:
317
+
318
+ Parameters
319
+ ----------
320
+
321
+ offset : int
322
+ Number of periods to offset the result, by default is 0.
323
+
324
+ RSI:
325
+
326
+ Parameters
327
+ ----------
328
+
329
+ length : int
330
+ Window length for the RSI, by default is 14.
331
+
332
+ scalar : float
333
+ Scalar to multiply the RSI by, default is 100.
334
+
335
+ drift : int
336
+ Drift value for the RSI, by default is 1.
337
+
338
+ SRLines:
339
+
340
+ Parameters
341
+ ----------
342
+
343
+ show : bool
344
+ Show the support and resistance lines.
345
+
346
+ Stoch:
347
+
348
+ Parameters
349
+ ----------
350
+
351
+ fast_k : int
352
+ The fast K period, by default 14.
353
+
354
+ slow_d : int
355
+ The slow D period, by default 3.
356
+
357
+ slow_k : int
358
+ The slow K period, by default 3.
359
+ ```
360
+
361
+ The model can be converted to a dictionary and then passed through the `indicators` params.
362
+
363
+ The chart below is built from the same object as the one above.
364
+
365
+ ```python
366
+ indicators = data.charting.indicators().dict()
367
+ macd=indicators.get("macd")
368
+ kc=indicators.get("kc")
369
+ chart_params=dict(
370
+ candles=False,
371
+ title="My New Chart",
372
+ indicators=(dict(
373
+ macd=macd,
374
+ kc=kc,
375
+ ))
376
+ )
377
+ data.charting.to_chart(**chart_params)
378
+ ```
379
+
380
+ ![indicators2](https://github.com/OpenBB-finance/OpenBB/assets/85772166/76c06aff-a568-4b7f-80d4-c58a73c0f1d7)
381
+
382
+ :::tip
383
+ Data can be exported directly from the chart as a CSV. Use the button at the bottom-right of the mode bar.
384
+ :::
openbb_platform/obbject_extensions/charting/installation.md ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Installation
3
+ sidebar_position: 1
4
+ description: This page outlines the installation of the openbb-charting extension.
5
+ keywords:
6
+ - tutorial
7
+ - OpenBB Platform
8
+ - Installation
9
+ - Python client
10
+ - Fast API
11
+ - getting started
12
+ - extensions
13
+ - charting
14
+ - view
15
+ - Plotly
16
+ - toolkit
17
+ - community
18
+ - Plotly
19
+ - OpenBBFigure
20
+ - PyWry
21
+ ---
22
+
23
+ import HeadTitle from '@site/src/components/General/HeadTitle.tsx';
24
+
25
+ <HeadTitle title="Installation - Charting - Extensions | OpenBB Platform Docs" />
26
+
27
+ ## PyPI
28
+
29
+ To install the extension, run the following command in this folder:
30
+
31
+ ```bash
32
+ pip install openbb-charting
33
+ ```
34
+
35
+ > Find the latest version on [PyPI](https://pypi.org/project/openbb-charting/).
36
+
37
+ ## Editable Mode
38
+
39
+ To install from source in editable mode, navigate into the folder, `~/openbb_platform/extensions/charting`, and enter:
40
+
41
+ ```console
42
+ pip install -e .
43
+ ```
44
+
45
+ After installation, the Python interface will automatically rebuild on initialization. This process can also be triggered manually with:
46
+
47
+ ```python
48
+ import openbb
49
+ openbb.build()
50
+ ```
51
+
52
+ The Python interpreter may need to be restarted.
53
+
54
+ ## PyWry Dependency In Linux
55
+
56
+ When using Linux distributions, the PyWry dependency requires certain dependencies to be installed first.
57
+
58
+ - Debian-based / Ubuntu / Mint:
59
+ `sudo apt install libwebkit2gtk-4.0-dev`
60
+
61
+ - Arch Linux / Manjaro:
62
+ `sudo pacman -S webkit2gtk`
63
+
64
+ - Fedora:
65
+ `sudo dnf install gtk3-devel webkit2gtk3-devel`
66
+
67
+ If Rust (Cargo) is required, install it:
68
+
69
+ ```console
70
+ curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh
71
+ ```
openbb_platform/obbject_extensions/charting/integration/test_charting_api.py ADDED
@@ -0,0 +1,908 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Integration tests for charting API."""
2
+
3
+ import base64
4
+ import json
5
+
6
+ import pytest
7
+ import requests
8
+ from extensions.tests.conftest import parametrize
9
+ from openbb_core.env import Env
10
+ from openbb_core.provider.utils.helpers import get_querystring
11
+
12
+
13
+ @pytest.fixture(scope="session")
14
+ def headers():
15
+ """Headers fixture."""
16
+ return get_headers()
17
+
18
+
19
+ # pylint:disable=redefined-outer-name
20
+
21
+ data: dict = {}
22
+
23
+
24
+ def get_headers():
25
+ """Get headers for requests."""
26
+ if "headers" in data:
27
+ return data["headers"]
28
+
29
+ userpass = f"{Env().API_USERNAME}:{Env().API_PASSWORD}"
30
+ userpass_bytes = userpass.encode("ascii")
31
+ base64_bytes = base64.b64encode(userpass_bytes)
32
+
33
+ data["headers"] = {"Authorization": f"Basic {base64_bytes.decode('ascii')}"}
34
+ return data["headers"]
35
+
36
+
37
+ def get_equity_data():
38
+ """Get equity data."""
39
+ if "stocks_data" in data:
40
+ return data["stocks_data"]
41
+
42
+ url = "http://0.0.0.0:8000/api/v1/equity/price/historical?symbol=AAPL&provider=fmp"
43
+ result = requests.get(url, headers=get_headers(), timeout=10)
44
+ data["stocks_data"] = result.json()["results"]
45
+
46
+ return data["stocks_data"]
47
+
48
+
49
+ @parametrize(
50
+ "params",
51
+ [
52
+ (
53
+ {
54
+ "provider": "yfinance",
55
+ "symbol": "AAPL",
56
+ "chart": True,
57
+ }
58
+ ),
59
+ ],
60
+ )
61
+ @pytest.mark.integration
62
+ def test_charting_equity_price_historical(params, headers):
63
+ """Test chart equity price historical.."""
64
+ params = {p: v for p, v in params.items() if v}
65
+
66
+ query_str = get_querystring(params, [])
67
+ url = f"http://0.0.0.0:8000/api/v1/equity/price/historical?{query_str}"
68
+ result = requests.get(url, headers=headers, timeout=40)
69
+ assert isinstance(result, requests.Response)
70
+ assert result.status_code == 200
71
+
72
+ chart = result.json()["chart"]
73
+ fig = chart.pop("fig", {})
74
+
75
+ assert chart
76
+ assert not fig
77
+ assert list(chart.keys()) == ["content", "format"]
78
+
79
+
80
+ @parametrize(
81
+ "params",
82
+ [
83
+ (
84
+ {
85
+ "provider": "yfinance",
86
+ "symbol": "USDGBP",
87
+ "chart": True,
88
+ }
89
+ ),
90
+ ],
91
+ )
92
+ @pytest.mark.integration
93
+ def test_charting_currency_price_historical(params, headers):
94
+ """Test chart currency price historical."""
95
+ params = {p: v for p, v in params.items() if v}
96
+
97
+ query_str = get_querystring(params, [])
98
+ url = f"http://0.0.0.0:8000/api/v1/currency/price/historical?{query_str}"
99
+ result = requests.get(url, headers=headers, timeout=40)
100
+ assert isinstance(result, requests.Response)
101
+ assert result.status_code == 200
102
+
103
+ chart = result.json()["chart"]
104
+ fig = chart.pop("fig", {})
105
+
106
+ assert chart
107
+ assert not fig
108
+ assert list(chart.keys()) == ["content", "format"]
109
+
110
+
111
+ @parametrize(
112
+ "params",
113
+ [
114
+ (
115
+ {
116
+ "provider": "yfinance",
117
+ "symbol": "QQQ",
118
+ "chart": True,
119
+ }
120
+ ),
121
+ ],
122
+ )
123
+ @pytest.mark.integration
124
+ def test_charting_etf_historical(params, headers):
125
+ """Test chart etf historical."""
126
+ params = {p: v for p, v in params.items() if v}
127
+
128
+ query_str = get_querystring(params, [])
129
+ url = f"http://0.0.0.0:8000/api/v1/etf/historical?{query_str}"
130
+ result = requests.get(url, headers=headers, timeout=40)
131
+ assert isinstance(result, requests.Response)
132
+ assert result.status_code == 200
133
+
134
+ chart = result.json()["chart"]
135
+ fig = chart.pop("fig", {})
136
+
137
+ assert chart
138
+ assert not fig
139
+ assert list(chart.keys()) == ["content", "format"]
140
+
141
+
142
+ @parametrize(
143
+ "params",
144
+ [
145
+ (
146
+ {
147
+ "provider": "yfinance",
148
+ "symbol": "NDX",
149
+ "chart": True,
150
+ }
151
+ ),
152
+ ],
153
+ )
154
+ @pytest.mark.integration
155
+ def test_charting_index_price_historical(params, headers):
156
+ """Test chart index price historical."""
157
+ params = {p: v for p, v in params.items() if v}
158
+
159
+ query_str = get_querystring(params, [])
160
+ url = f"http://0.0.0.0:8000/api/v1/index/price/historical?{query_str}"
161
+ result = requests.get(url, headers=headers, timeout=40)
162
+ assert isinstance(result, requests.Response)
163
+ assert result.status_code == 200
164
+
165
+ chart = result.json()["chart"]
166
+ fig = chart.pop("fig", {})
167
+
168
+ assert chart
169
+ assert not fig
170
+ assert list(chart.keys()) == ["content", "format"]
171
+
172
+
173
+ @parametrize(
174
+ "params",
175
+ [
176
+ (
177
+ {
178
+ "provider": "yfinance",
179
+ "symbol": "BTCUSD",
180
+ "chart": True,
181
+ }
182
+ ),
183
+ ],
184
+ )
185
+ @pytest.mark.integration
186
+ def test_charting_crypto_price_historical(params, headers):
187
+ """Test chart crypto price historical."""
188
+ params = {p: v for p, v in params.items() if v}
189
+
190
+ query_str = get_querystring(params, [])
191
+ url = f"http://0.0.0.0:8000/api/v1/crypto/price/historical?{query_str}"
192
+ result = requests.get(url, headers=headers, timeout=40)
193
+ assert isinstance(result, requests.Response)
194
+ assert result.status_code == 200
195
+
196
+ chart = result.json()["chart"]
197
+ fig = chart.pop("fig", {})
198
+
199
+ assert chart
200
+ assert not fig
201
+ assert list(chart.keys()) == ["content", "format"]
202
+
203
+
204
+ @parametrize(
205
+ "params",
206
+ [
207
+ {
208
+ "data": "",
209
+ "index": "date",
210
+ "length": "60",
211
+ "scalar": "90.0",
212
+ "drift": "2",
213
+ "chart": True,
214
+ }
215
+ ],
216
+ )
217
+ @pytest.mark.integration
218
+ def test_charting_technical_adx(params, headers):
219
+ """Test chart ta adx."""
220
+ params = {p: v for p, v in params.items() if v}
221
+ body = json.dumps(get_equity_data())
222
+
223
+ query_str = get_querystring(params, [])
224
+ url = f"http://0.0.0.0:8000/api/v1/technical/adx?{query_str}"
225
+ result = requests.post(url, headers=headers, timeout=10, data=body)
226
+ assert isinstance(result, requests.Response)
227
+ assert result.status_code == 200
228
+
229
+ chart = result.json()["chart"]
230
+ fig = chart.pop("fig", {})
231
+
232
+ assert chart
233
+ assert not fig
234
+ assert list(chart.keys()) == ["content", "format"]
235
+
236
+
237
+ @parametrize(
238
+ "params",
239
+ [{"data": "", "index": "date", "length": "30", "scalar": "110", "chart": True}],
240
+ )
241
+ @pytest.mark.integration
242
+ def test_charting_technical_aroon(params, headers):
243
+ """Test chart ta aroon."""
244
+ params = {p: v for p, v in params.items() if v}
245
+ body = json.dumps(get_equity_data())
246
+
247
+ query_str = get_querystring(params, [])
248
+ url = f"http://0.0.0.0:8000/api/v1/technical/aroon?{query_str}"
249
+ result = requests.post(url, headers=headers, timeout=10, data=body)
250
+ assert isinstance(result, requests.Response)
251
+ assert result.status_code == 200
252
+
253
+ chart = result.json()["chart"]
254
+ fig = chart.pop("fig", {})
255
+
256
+ assert chart
257
+ assert not fig
258
+ assert list(chart.keys()) == ["content", "format"]
259
+
260
+
261
+ @parametrize(
262
+ "params",
263
+ [
264
+ {
265
+ "data": "",
266
+ "target": "high",
267
+ "index": "",
268
+ "length": "60",
269
+ "offset": "10",
270
+ "chart": True,
271
+ }
272
+ ],
273
+ )
274
+ @pytest.mark.integration
275
+ def test_charting_technical_ema(params, headers):
276
+ """Test chart ta ema."""
277
+ params = {p: v for p, v in params.items() if v}
278
+ body = json.dumps(get_equity_data())
279
+
280
+ query_str = get_querystring(params, [])
281
+ url = f"http://0.0.0.0:8000/api/v1/technical/ema?{query_str}"
282
+ result = requests.post(url, headers=headers, timeout=10, data=body)
283
+ assert isinstance(result, requests.Response)
284
+ assert result.status_code == 200
285
+
286
+ chart = result.json()["chart"]
287
+ fig = chart.pop("fig", {})
288
+
289
+ assert chart
290
+ assert not fig
291
+ assert list(chart.keys()) == ["content", "format"]
292
+
293
+
294
+ @parametrize(
295
+ "params",
296
+ [
297
+ {
298
+ "data": "",
299
+ "target": "high",
300
+ "index": "date",
301
+ "length": "55",
302
+ "offset": "2",
303
+ "chart": True,
304
+ }
305
+ ],
306
+ )
307
+ @pytest.mark.integration
308
+ def test_charting_technical_hma(params, headers):
309
+ """Test chart ta hma."""
310
+ params = {p: v for p, v in params.items() if v}
311
+ body = json.dumps(get_equity_data())
312
+
313
+ query_str = get_querystring(params, [])
314
+ url = f"http://0.0.0.0:8000/api/v1/technical/hma?{query_str}"
315
+ result = requests.post(url, headers=headers, timeout=10, data=body)
316
+ assert isinstance(result, requests.Response)
317
+ assert result.status_code == 200
318
+
319
+ chart = result.json()["chart"]
320
+ fig = chart.pop("fig", {})
321
+
322
+ assert chart
323
+ assert not fig
324
+ assert list(chart.keys()) == ["content", "format"]
325
+
326
+
327
+ @parametrize(
328
+ "params",
329
+ [
330
+ {
331
+ "data": "",
332
+ "target": "high",
333
+ "index": "date",
334
+ "fast": "10",
335
+ "slow": "30",
336
+ "signal": "10",
337
+ "chart": True,
338
+ }
339
+ ],
340
+ )
341
+ @pytest.mark.integration
342
+ def test_charting_technical_macd(params, headers):
343
+ """Test chart ta macd."""
344
+ params = {p: v for p, v in params.items() if v}
345
+ body = json.dumps(get_equity_data())
346
+
347
+ query_str = get_querystring(params, [])
348
+ url = f"http://0.0.0.0:8000/api/v1/technical/macd?{query_str}"
349
+ result = requests.post(url, headers=headers, timeout=10, data=body)
350
+ assert isinstance(result, requests.Response)
351
+ assert result.status_code == 200
352
+
353
+ chart = result.json()["chart"]
354
+ fig = chart.pop("fig", {})
355
+
356
+ assert chart
357
+ assert not fig
358
+ assert list(chart.keys()) == ["content", "format"]
359
+
360
+
361
+ @parametrize(
362
+ "params",
363
+ [
364
+ {
365
+ "data": "",
366
+ "target": "high",
367
+ "index": "date",
368
+ "length": "16",
369
+ "scalar": "90.0",
370
+ "drift": "2",
371
+ "chart": True,
372
+ }
373
+ ],
374
+ )
375
+ @pytest.mark.integration
376
+ def test_charting_technical_rsi(params, headers):
377
+ """Test chart ta rsi."""
378
+ params = {p: v for p, v in params.items() if v}
379
+ body = json.dumps(get_equity_data())
380
+
381
+ query_str = get_querystring(params, [])
382
+ url = f"http://0.0.0.0:8000/api/v1/technical/rsi?{query_str}"
383
+ result = requests.post(url, headers=headers, timeout=10, data=body)
384
+ assert isinstance(result, requests.Response)
385
+ assert result.status_code == 200
386
+
387
+ chart = result.json()["chart"]
388
+ fig = chart.pop("fig", {})
389
+
390
+ assert chart
391
+ assert not fig
392
+ assert list(chart.keys()) == ["content", "format"]
393
+
394
+
395
+ @parametrize(
396
+ "params",
397
+ [
398
+ {
399
+ "data": "",
400
+ "target": "high",
401
+ "index": "date",
402
+ "length": "55",
403
+ "offset": "2",
404
+ "chart": True,
405
+ }
406
+ ],
407
+ )
408
+ @pytest.mark.integration
409
+ def test_charting_technical_sma(params, headers):
410
+ """Test chart ta sma."""
411
+ params = {p: v for p, v in params.items() if v}
412
+ body = json.dumps(get_equity_data())
413
+
414
+ query_str = get_querystring(params, [])
415
+ url = f"http://0.0.0.0:8000/api/v1/technical/sma?{query_str}"
416
+ result = requests.post(url, headers=headers, timeout=10, data=body)
417
+ assert isinstance(result, requests.Response)
418
+ assert result.status_code == 200
419
+
420
+ chart = result.json()["chart"]
421
+ fig = chart.pop("fig", {})
422
+
423
+ assert chart
424
+ assert not fig
425
+ assert list(chart.keys()) == ["content", "format"]
426
+
427
+
428
+ @parametrize(
429
+ "params",
430
+ [
431
+ {
432
+ "data": "",
433
+ "target": "high",
434
+ "index": "date",
435
+ "length": "60",
436
+ "offset": "10",
437
+ "chart": True,
438
+ }
439
+ ],
440
+ )
441
+ @pytest.mark.integration
442
+ def test_charting_technical_wma(params, headers):
443
+ """Test chart ta wma."""
444
+ params = {p: v for p, v in params.items() if v}
445
+ body = json.dumps(get_equity_data())
446
+
447
+ query_str = get_querystring(params, [])
448
+ url = f"http://0.0.0.0:8000/api/v1/technical/wma?{query_str}"
449
+ result = requests.post(url, headers=headers, timeout=10, data=body)
450
+ assert isinstance(result, requests.Response)
451
+ assert result.status_code == 200
452
+
453
+ chart = result.json()["chart"]
454
+ fig = chart.pop("fig", {})
455
+
456
+ assert chart
457
+ assert not fig
458
+ assert list(chart.keys()) == ["content", "format"]
459
+
460
+
461
+ @parametrize(
462
+ "params",
463
+ [
464
+ {
465
+ "data": "",
466
+ "target": "high",
467
+ "index": "date",
468
+ "length": "55",
469
+ "offset": "5",
470
+ "chart": True,
471
+ }
472
+ ],
473
+ )
474
+ @pytest.mark.integration
475
+ def test_charting_technical_zlma(params, headers):
476
+ """Test chart ta zlma."""
477
+ params = {p: v for p, v in params.items() if v}
478
+ body = json.dumps(get_equity_data())
479
+
480
+ query_str = get_querystring(params, [])
481
+ url = f"http://0.0.0.0:8000/api/v1/technical/zlma?{query_str}"
482
+ result = requests.post(url, headers=headers, timeout=10, data=body)
483
+ assert isinstance(result, requests.Response)
484
+ assert result.status_code == 200
485
+
486
+ chart = result.json()["chart"]
487
+ fig = chart.pop("fig", {})
488
+
489
+ assert chart
490
+ assert not fig
491
+ assert list(chart.keys()) == ["content", "format"]
492
+
493
+
494
+ @parametrize(
495
+ "params",
496
+ [
497
+ {
498
+ "data": "",
499
+ "model": "yang_zhang",
500
+ "chart": True,
501
+ }
502
+ ],
503
+ )
504
+ @pytest.mark.integration
505
+ def test_charting_technical_cones(params, headers):
506
+ """Test chart ta cones."""
507
+ params = {p: v for p, v in params.items() if v}
508
+ body = json.dumps(get_equity_data())
509
+
510
+ query_str = get_querystring(params, [])
511
+ url = f"http://0.0.0.0:8000/api/v1/technical/cones?{query_str}"
512
+ result = requests.post(url, headers=headers, timeout=10, data=body)
513
+ assert isinstance(result, requests.Response)
514
+ assert result.status_code == 200
515
+
516
+ chart = result.json()["chart"]
517
+ fig = chart.pop("fig", {})
518
+
519
+ assert chart
520
+ assert not fig
521
+ assert list(chart.keys()) == ["content", "format"]
522
+
523
+
524
+ @parametrize(
525
+ "params",
526
+ [
527
+ {
528
+ "data": None,
529
+ "symbol": "DGS10",
530
+ "transform": "pc1",
531
+ "chart": True,
532
+ "provider": "fred",
533
+ }
534
+ ],
535
+ )
536
+ @pytest.mark.integration
537
+ def test_charting_economy_fred_series(params, headers):
538
+ """Test chart ta cones."""
539
+ params = {p: v for p, v in params.items() if v}
540
+
541
+ query_str = get_querystring(params, [])
542
+ url = f"http://0.0.0.0:8000/api/v1/economy/fred_series?{query_str}"
543
+ result = requests.get(url, headers=headers, timeout=10)
544
+ assert isinstance(result, requests.Response)
545
+ assert result.status_code == 200
546
+
547
+ chart = result.json()["chart"]
548
+ fig = chart.pop("fig", {})
549
+
550
+ assert chart
551
+ assert not fig
552
+ assert list(chart.keys()) == ["content", "format"]
553
+
554
+
555
+ @parametrize(
556
+ "params",
557
+ [
558
+ (
559
+ {
560
+ "data": "",
561
+ "study": "price",
562
+ "benchmark": "SPY",
563
+ "long_period": 252,
564
+ "short_period": 21,
565
+ "window": 21,
566
+ "trading_periods": 252,
567
+ "chart": True,
568
+ }
569
+ ),
570
+ ],
571
+ )
572
+ @pytest.mark.integration
573
+ def test_charting_technical_relative_rotation(params):
574
+ params = {p: v for p, v in params.items() if v}
575
+ data_params = dict(
576
+ symbol="AAPL,MSFT,GOOGL,AMZN,SPY",
577
+ provider="yfinance",
578
+ start_date="2022-01-01",
579
+ end_date="2024-01-01",
580
+ )
581
+ data_query_str = get_querystring(data_params, [])
582
+ data_url = f"http://0.0.0.0:8000/api/v1/equity/price/historical?{data_query_str}"
583
+ data_result = requests.get(data_url, headers=get_headers(), timeout=10).json()[
584
+ "results"
585
+ ]
586
+ body = json.dumps({"data": data_result})
587
+ query_str = get_querystring(params, ["data"])
588
+ url = f"http://0.0.0.0:8000/api/v1/technical/relative_rotation?{query_str}"
589
+ result = requests.post(url, headers=get_headers(), timeout=10, data=body)
590
+ assert isinstance(result, requests.Response)
591
+ assert result.status_code == 200
592
+ chart = result.json()["chart"]
593
+ fig = chart.pop("fig", {})
594
+
595
+ assert chart
596
+ assert not fig
597
+ assert list(chart.keys()) == ["content", "format"]
598
+
599
+
600
+ @parametrize(
601
+ "params",
602
+ [
603
+ {
604
+ "data": None,
605
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
606
+ "chart": True,
607
+ "provider": "finviz",
608
+ }
609
+ ],
610
+ )
611
+ @pytest.mark.integration
612
+ def test_charting_equity_price_performance(params, headers):
613
+ """Test chart equity price performance."""
614
+ params = {p: v for p, v in params.items() if v}
615
+ body = (
616
+ json.dumps(
617
+ {"extra_params": {"chart_params": {"limit": 4, "orientation": "h"}}}
618
+ ),
619
+ )
620
+ query_str = get_querystring(params, [])
621
+ url = f"http://0.0.0.0:8000/api/v1/equity/price/performance?{query_str}"
622
+ result = requests.get(url, headers=headers, timeout=10, json=body)
623
+ assert isinstance(result, requests.Response)
624
+ assert result.status_code == 200
625
+
626
+ chart = result.json()["chart"]
627
+ fig = chart.pop("fig", {})
628
+
629
+ assert chart
630
+ assert not fig
631
+ assert list(chart.keys()) == ["content", "format"]
632
+
633
+
634
+ @parametrize(
635
+ "params",
636
+ [
637
+ {
638
+ "data": None,
639
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
640
+ "chart": True,
641
+ "provider": "fmp",
642
+ }
643
+ ],
644
+ )
645
+ @pytest.mark.integration
646
+ def test_charting_etf_price_performance(params, headers):
647
+ """Test chart equity price performance."""
648
+ params = {p: v for p, v in params.items() if v}
649
+ body = (json.dumps({"extra_params": {"chart_params": {"orientation": "v"}}}),)
650
+ query_str = get_querystring(params, [])
651
+ url = f"http://0.0.0.0:8000/api/v1/etf/price_performance?{query_str}"
652
+ result = requests.get(url, headers=headers, timeout=10, json=body)
653
+ assert isinstance(result, requests.Response)
654
+ assert result.status_code == 200
655
+
656
+ chart = result.json()["chart"]
657
+ fig = chart.pop("fig", {})
658
+
659
+ assert chart
660
+ assert not fig
661
+ assert list(chart.keys()) == ["content", "format"]
662
+
663
+
664
+ @parametrize(
665
+ "params",
666
+ [
667
+ {
668
+ "data": None,
669
+ "symbol": "XRT",
670
+ "chart": True,
671
+ "provider": "fmp",
672
+ }
673
+ ],
674
+ )
675
+ @pytest.mark.integration
676
+ def test_charting_etf_holdings(params, headers):
677
+ """Test chart etf holdings."""
678
+ params = {p: v for p, v in params.items() if v}
679
+ body = (
680
+ json.dumps(
681
+ {"extra_params": {"chart_params": {"orientation": "v", "limit": 10}}}
682
+ ),
683
+ )
684
+ query_str = get_querystring(params, [])
685
+ url = f"http://0.0.0.0:8000/api/v1/etf/holdings?{query_str}"
686
+ result = requests.get(url, headers=headers, timeout=10, json=body)
687
+ assert isinstance(result, requests.Response)
688
+ assert result.status_code == 200
689
+
690
+ chart = result.json()["chart"]
691
+ fig = chart.pop("fig", {})
692
+
693
+ assert chart
694
+ assert not fig
695
+ assert list(chart.keys()) == ["content", "format"]
696
+
697
+
698
+ @parametrize(
699
+ "params",
700
+ [
701
+ (
702
+ {
703
+ "provider": "econdb",
704
+ "country": "united_kingdom",
705
+ "date": None,
706
+ "chart": True,
707
+ }
708
+ ),
709
+ (
710
+ {
711
+ "provider": "fred",
712
+ "date": "2023-05-10,2024-05-10",
713
+ "chart": True,
714
+ }
715
+ ),
716
+ ],
717
+ )
718
+ @pytest.mark.integration
719
+ def test_charting_fixedincome_government_yield_curve(params, headers):
720
+ """Test chart fixedincome government yield curve."""
721
+ params = {p: v for p, v in params.items() if v}
722
+ body = (json.dumps({"extra_params": {"chart_params": {"title": "test chart"}}}),)
723
+ query_str = get_querystring(params, [])
724
+ url = f"http://0.0.0.0:8000/api/v1/fixedincome/government/yield_curve?{query_str}"
725
+ result = requests.get(url, headers=headers, timeout=10, json=body)
726
+ assert isinstance(result, requests.Response)
727
+ assert result.status_code == 200
728
+
729
+ chart = result.json()["chart"]
730
+ fig = chart.pop("fig", {})
731
+
732
+ assert chart
733
+ assert not fig
734
+ assert list(chart.keys()) == ["content", "format"]
735
+
736
+
737
+ @parametrize(
738
+ "params",
739
+ [
740
+ {
741
+ "provider": "yfinance",
742
+ "symbol": "ES",
743
+ "start_date": "2022-01-01",
744
+ "end_date": "2022-02-01",
745
+ "chart": True,
746
+ }
747
+ ],
748
+ )
749
+ @pytest.mark.integration
750
+ def test_charting_derivatives_futures_historical(params, headers):
751
+ """Test chart derivatives futures historical."""
752
+ params = {p: v for p, v in params.items() if v}
753
+ body = (json.dumps({"extra_params": {"chart_params": {"title": "test chart"}}}),)
754
+ query_str = get_querystring(params, [])
755
+ url = f"http://0.0.0.0:8000/api/v1/derivatives/futures/historical?{query_str}"
756
+ result = requests.get(url, headers=headers, timeout=10, json=body)
757
+ assert isinstance(result, requests.Response)
758
+ assert result.status_code == 200
759
+
760
+ chart = result.json()["chart"]
761
+ fig = chart.pop("fig", {})
762
+
763
+ assert chart
764
+ assert not fig
765
+ assert list(chart.keys()) == ["content", "format"]
766
+
767
+
768
+ @parametrize(
769
+ "params",
770
+ [
771
+ (
772
+ {
773
+ "provider": "yfinance",
774
+ "symbol": "ES",
775
+ "date": None,
776
+ "chart": True,
777
+ }
778
+ ),
779
+ (
780
+ {
781
+ "provider": "cboe",
782
+ "symbol": "VX",
783
+ "date": "2024-06-25",
784
+ "chart": True,
785
+ }
786
+ ),
787
+ ],
788
+ )
789
+ @pytest.mark.integration
790
+ def test_charting_derivatives_futures_curve(params, headers):
791
+ """Test chart derivatives futures curve."""
792
+ params = {p: v for p, v in params.items() if v}
793
+ body = (json.dumps({"extra_params": {"chart_params": {"title": "test chart"}}}),)
794
+ query_str = get_querystring(params, [])
795
+ url = f"http://0.0.0.0:8000/api/v1/derivatives/futures/curve?{query_str}"
796
+ result = requests.get(url, headers=headers, timeout=30, json=body)
797
+ assert isinstance(result, requests.Response)
798
+ assert result.status_code == 200
799
+
800
+ chart = result.json()["chart"]
801
+ fig = chart.pop("fig", {})
802
+
803
+ assert chart
804
+ assert not fig
805
+ assert list(chart.keys()) == ["content", "format"]
806
+
807
+
808
+ @parametrize(
809
+ "params",
810
+ [
811
+ {
812
+ "provider": "fmp",
813
+ "symbol": "AAPL",
814
+ "start_date": "2024-01-01",
815
+ "end_date": "2024-06-30",
816
+ "chart": True,
817
+ }
818
+ ],
819
+ )
820
+ @pytest.mark.integration
821
+ def test_charting_equity_historical_market_cap(params, headers):
822
+ """Test chart equity historical market cap."""
823
+ params = {p: v for p, v in params.items() if v}
824
+ body = (json.dumps({"extra_params": {"chart_params": {"title": "test chart"}}}),)
825
+ query_str = get_querystring(params, [])
826
+ url = f"http://0.0.0.0:8000/api/v1/equity/historical_market_cap?{query_str}"
827
+ result = requests.get(url, headers=headers, timeout=10, json=body)
828
+ assert isinstance(result, requests.Response)
829
+ assert result.status_code == 200
830
+
831
+ chart = result.json()["chart"]
832
+ fig = chart.pop("fig", {})
833
+
834
+ assert chart
835
+ assert not fig
836
+ assert list(chart.keys()) == ["content", "format"]
837
+
838
+
839
+ @parametrize(
840
+ "params",
841
+ [
842
+ (
843
+ {
844
+ "provider": "bls",
845
+ "symbol": "APUS49D74714,APUS49D74715,APUS49D74716",
846
+ "start_date": "2014-01-01",
847
+ "end_date": "2024-07-01",
848
+ "chart": True,
849
+ }
850
+ ),
851
+ ],
852
+ )
853
+ @pytest.mark.integration
854
+ def test_charting_economy_survey_bls_series(params, headers):
855
+ """Test chart economy survey bls series."""
856
+ params = {p: v for p, v in params.items() if v}
857
+ body = (json.dumps({"extra_params": {"chart_params": {"title": "test chart"}}}),)
858
+ query_str = get_querystring(params, [])
859
+ url = f"http://0.0.0.0:8000/api/v1/economy/survey/bls_series?{query_str}"
860
+ result = requests.get(url, headers=headers, timeout=10, json=body)
861
+ assert isinstance(result, requests.Response)
862
+ assert result.status_code == 200
863
+
864
+ chart = result.json()["chart"]
865
+ fig = chart.pop("fig", {})
866
+
867
+ assert chart
868
+ assert not fig
869
+ assert list(chart.keys()) == ["content", "format"]
870
+
871
+
872
+ @parametrize(
873
+ "params",
874
+ [
875
+ {
876
+ "data": "",
877
+ "method": "pearson",
878
+ "chart": True,
879
+ }
880
+ ],
881
+ )
882
+ @pytest.mark.integration
883
+ def test_charting_econometrics_correlation_matrix(params, headers):
884
+ """Test chart econometrics correlation matrix."""
885
+ # pylint:disable=import-outside-toplevel
886
+ from pandas import DataFrame
887
+
888
+ url = "http://0.0.0.0:8000/api/v1/equity/price/historical?symbol=AAPL,MSFT,GOOG&provider=yfinance"
889
+ result = requests.get(url, headers=headers, timeout=10)
890
+ df = DataFrame(result.json()["results"])
891
+ df = df.pivot(index="date", columns="symbol", values="close").reset_index()
892
+ body = df.to_dict(orient="records")
893
+
894
+ params = {p: v for p, v in params.items() if v}
895
+
896
+ query_str = get_querystring(params, [])
897
+ url = f"http://0.0.0.0:8000/api/v1/econometrics/correlation_matrix?{query_str}"
898
+ result = requests.post(url, headers=headers, timeout=10, data=json.dumps(body))
899
+
900
+ assert isinstance(result, requests.Response)
901
+ assert result.status_code == 200
902
+
903
+ chart = result.json()["chart"]
904
+ fig = chart.pop("fig", {})
905
+
906
+ assert chart
907
+ assert not fig
908
+ assert list(chart.keys()) == ["content", "format"]
openbb_platform/obbject_extensions/charting/integration/test_charting_python.py ADDED
@@ -0,0 +1,747 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Test charting extension."""
2
+
3
+ import pytest
4
+ from extensions.tests.conftest import parametrize
5
+ from openbb_charting.core.openbb_figure import OpenBBFigure
6
+ from openbb_core.app.model.obbject import OBBject
7
+
8
+
9
+ # pylint:disable=inconsistent-return-statements
10
+ @pytest.fixture(scope="session")
11
+ def obb(pytestconfig):
12
+ """Fixture to setup obb."""
13
+ if pytestconfig.getoption("markexpr") != "not integration":
14
+ import openbb # pylint:disable=import-outside-toplevel
15
+
16
+ return openbb.obb
17
+
18
+
19
+ # pylint:disable=redefined-outer-name
20
+
21
+ data: dict = {}
22
+
23
+
24
+ def get_equity_data():
25
+ """Get equity data."""
26
+ import openbb # pylint:disable=import-outside-toplevel
27
+
28
+ if "stocks_data" in data:
29
+ return data["stocks_data"]
30
+
31
+ symbol = "AAPL"
32
+ provider = "fmp"
33
+
34
+ data["stocks_data"] = openbb.obb.equity.price.historical(
35
+ symbol=symbol, provider=provider
36
+ ).results
37
+ return data["stocks_data"]
38
+
39
+
40
+ @parametrize(
41
+ "params",
42
+ [
43
+ (
44
+ {
45
+ "provider": "yfinance",
46
+ "symbol": "AAPL",
47
+ "chart": True,
48
+ }
49
+ ),
50
+ ],
51
+ )
52
+ @pytest.mark.integration
53
+ def test_charting_equity_price_historical(params, obb):
54
+ """Test chart equity price historical."""
55
+ result = obb.equity.price.historical(**params)
56
+ assert result
57
+ assert isinstance(result, OBBject)
58
+ assert len(result.results) > 0
59
+ assert result.chart.content
60
+ assert isinstance(result.chart.fig, OpenBBFigure)
61
+
62
+
63
+ @parametrize(
64
+ "params",
65
+ [
66
+ (
67
+ {
68
+ "provider": "yfinance",
69
+ "symbol": "JPYUSD",
70
+ "chart": True,
71
+ }
72
+ ),
73
+ ],
74
+ )
75
+ @pytest.mark.integration
76
+ def test_charting_currency_price_historical(params, obb):
77
+ """Test chart currency price historical."""
78
+ result = obb.currency.price.historical(**params)
79
+ assert result
80
+ assert isinstance(result, OBBject)
81
+ assert len(result.results) > 0
82
+ assert result.chart.content
83
+ assert isinstance(result.chart.fig, OpenBBFigure)
84
+
85
+
86
+ @parametrize(
87
+ "params",
88
+ [
89
+ (
90
+ {
91
+ "provider": "yfinance",
92
+ "symbol": "BTCUSD",
93
+ "chart": True,
94
+ }
95
+ ),
96
+ ],
97
+ )
98
+ @pytest.mark.integration
99
+ def test_charting_crypto_price_historical(params, obb):
100
+ """Test chart crypto price historical."""
101
+ result = obb.crypto.price.historical(**params)
102
+ assert result
103
+ assert isinstance(result, OBBject)
104
+ assert len(result.results) > 0
105
+ assert result.chart.content
106
+ assert isinstance(result.chart.fig, OpenBBFigure)
107
+
108
+
109
+ @parametrize(
110
+ "params",
111
+ [
112
+ (
113
+ {
114
+ "provider": "yfinance",
115
+ "symbol": "NDX",
116
+ "chart": True,
117
+ }
118
+ ),
119
+ ],
120
+ )
121
+ @pytest.mark.integration
122
+ def test_charting_index_price_historical(params, obb):
123
+ """Test chart index price historical."""
124
+ result = obb.index.price.historical(**params)
125
+ assert result
126
+ assert isinstance(result, OBBject)
127
+ assert len(result.results) > 0
128
+ assert result.chart.content
129
+ assert isinstance(result.chart.fig, OpenBBFigure)
130
+
131
+
132
+ @parametrize(
133
+ "params",
134
+ [
135
+ (
136
+ {
137
+ "provider": "yfinance",
138
+ "symbol": "QQQ",
139
+ "chart": True,
140
+ }
141
+ ),
142
+ ],
143
+ )
144
+ @pytest.mark.integration
145
+ def test_charting_etf_historical(params, obb):
146
+ """Test chart etf historical."""
147
+ result = obb.etf.historical(**params)
148
+ assert result
149
+ assert isinstance(result, OBBject)
150
+ assert len(result.results) > 0
151
+ assert result.chart.content
152
+ assert isinstance(result.chart.fig, OpenBBFigure)
153
+
154
+
155
+ @parametrize(
156
+ "params",
157
+ [
158
+ (
159
+ {
160
+ "data": "",
161
+ "index": "date",
162
+ "length": "60",
163
+ "scalar": "90.0",
164
+ "drift": "2",
165
+ "chart": "True",
166
+ }
167
+ ),
168
+ ],
169
+ )
170
+ @pytest.mark.integration
171
+ def test_charting_technical_adx(params, obb):
172
+ """Test chart ta adx."""
173
+ params = {p: v for p, v in params.items() if v}
174
+
175
+ params["data"] = get_equity_data()
176
+
177
+ result = obb.technical.adx(**params)
178
+ assert result
179
+ assert isinstance(result, OBBject)
180
+ assert len(result.results) > 0
181
+ assert result.chart.content
182
+ assert isinstance(result.chart.fig, OpenBBFigure)
183
+
184
+
185
+ @parametrize(
186
+ "params",
187
+ [
188
+ (
189
+ {
190
+ "data": "",
191
+ "index": "date",
192
+ "length": "30",
193
+ "scalar": "110",
194
+ "chart": "True",
195
+ }
196
+ ),
197
+ ],
198
+ )
199
+ @pytest.mark.integration
200
+ def test_charting_technical_aroon(params, obb):
201
+ """Test chart ta aroon."""
202
+ params = {p: v for p, v in params.items() if v}
203
+
204
+ params["data"] = get_equity_data()
205
+
206
+ result = obb.technical.aroon(**params)
207
+ assert result
208
+ assert isinstance(result, OBBject)
209
+ assert len(result.results) > 0
210
+ assert result.chart.content
211
+ assert isinstance(result.chart.fig, OpenBBFigure)
212
+
213
+
214
+ @parametrize(
215
+ "params",
216
+ [
217
+ (
218
+ {
219
+ "data": "",
220
+ "target": "high",
221
+ "index": "",
222
+ "length": "60",
223
+ "offset": "10",
224
+ "chart": "True",
225
+ }
226
+ ),
227
+ ],
228
+ )
229
+ @pytest.mark.integration
230
+ def test_charting_technical_ema(params, obb):
231
+ """Test chart ta ema."""
232
+ params = {p: v for p, v in params.items() if v}
233
+
234
+ params["data"] = get_equity_data()
235
+
236
+ result = obb.technical.ema(**params)
237
+ assert result
238
+ assert isinstance(result, OBBject)
239
+ assert len(result.results) > 0
240
+ assert result.chart.content
241
+ assert isinstance(result.chart.fig, OpenBBFigure)
242
+
243
+
244
+ @parametrize(
245
+ "params",
246
+ [
247
+ (
248
+ {
249
+ "data": "",
250
+ "target": "high",
251
+ "index": "date",
252
+ "length": "55",
253
+ "offset": "2",
254
+ "chart": "True",
255
+ }
256
+ ),
257
+ ],
258
+ )
259
+ @pytest.mark.integration
260
+ def test_charting_technical_hma(params, obb):
261
+ """Test chart ta hma."""
262
+ params = {p: v for p, v in params.items() if v}
263
+
264
+ params["data"] = get_equity_data()
265
+
266
+ result = obb.technical.hma(**params)
267
+ assert result
268
+ assert isinstance(result, OBBject)
269
+ assert len(result.results) > 0
270
+ assert result.chart.content
271
+ assert isinstance(result.chart.fig, OpenBBFigure)
272
+
273
+
274
+ @parametrize(
275
+ "params",
276
+ [
277
+ (
278
+ {
279
+ "data": "",
280
+ "target": "high",
281
+ "index": "date",
282
+ "fast": "10",
283
+ "slow": "30",
284
+ "signal": "10",
285
+ "chart": "True",
286
+ }
287
+ ),
288
+ ],
289
+ )
290
+ @pytest.mark.integration
291
+ def test_charting_technical_macd(params, obb):
292
+ """Test chart ta macd."""
293
+ params = {p: v for p, v in params.items() if v}
294
+
295
+ params["data"] = get_equity_data()
296
+
297
+ result = obb.technical.macd(**params)
298
+ assert result
299
+ assert isinstance(result, OBBject)
300
+ assert len(result.results) > 0
301
+ assert result.chart.content
302
+ assert isinstance(result.chart.fig, OpenBBFigure)
303
+
304
+
305
+ @parametrize(
306
+ "params",
307
+ [
308
+ (
309
+ {
310
+ "data": "",
311
+ "target": "high",
312
+ "index": "date",
313
+ "length": "16",
314
+ "scalar": "90.0",
315
+ "drift": "2",
316
+ "chart": "True",
317
+ }
318
+ ),
319
+ ],
320
+ )
321
+ @pytest.mark.integration
322
+ def test_charting_technical_rsi(params, obb):
323
+ """Test chart ta rsi."""
324
+ params = {p: v for p, v in params.items() if v}
325
+
326
+ params["data"] = get_equity_data()
327
+
328
+ result = obb.technical.rsi(**params)
329
+ assert result
330
+ assert isinstance(result, OBBject)
331
+ assert len(result.results) > 0
332
+ assert result.chart.content
333
+ assert isinstance(result.chart.fig, OpenBBFigure)
334
+
335
+
336
+ @parametrize(
337
+ "params",
338
+ [
339
+ (
340
+ {
341
+ "data": "",
342
+ "target": "high",
343
+ "index": "date",
344
+ "length": "55",
345
+ "offset": "2",
346
+ "chart": "True",
347
+ }
348
+ ),
349
+ ],
350
+ )
351
+ @pytest.mark.integration
352
+ def test_charting_technical_sma(params, obb):
353
+ """Test chart ta sma."""
354
+ params = {p: v for p, v in params.items() if v}
355
+
356
+ params["data"] = get_equity_data()
357
+
358
+ result = obb.technical.sma(**params)
359
+ assert result
360
+ assert isinstance(result, OBBject)
361
+ assert len(result.results) > 0
362
+ assert result.chart.content
363
+ assert isinstance(result.chart.fig, OpenBBFigure)
364
+
365
+
366
+ @parametrize(
367
+ "params",
368
+ [
369
+ (
370
+ {
371
+ "data": "",
372
+ "target": "high",
373
+ "index": "date",
374
+ "length": "60",
375
+ "offset": "10",
376
+ "chart": "True",
377
+ }
378
+ ),
379
+ ],
380
+ )
381
+ @pytest.mark.integration
382
+ def test_charting_technical_wma(params, obb):
383
+ """Test chart ta wma."""
384
+ params = {p: v for p, v in params.items() if v}
385
+
386
+ params["data"] = get_equity_data()
387
+
388
+ result = obb.technical.wma(**params)
389
+ assert result
390
+ assert isinstance(result, OBBject)
391
+ assert len(result.results) > 0
392
+ assert result.chart.content
393
+ assert isinstance(result.chart.fig, OpenBBFigure)
394
+
395
+
396
+ @parametrize(
397
+ "params",
398
+ [
399
+ (
400
+ {
401
+ "data": "",
402
+ "target": "high",
403
+ "index": "date",
404
+ "length": "55",
405
+ "offset": "5",
406
+ "chart": "True",
407
+ }
408
+ ),
409
+ ],
410
+ )
411
+ @pytest.mark.integration
412
+ def test_charting_technical_zlma(params, obb):
413
+ """Test chart ta zlma."""
414
+ params = {p: v for p, v in params.items() if v}
415
+
416
+ params["data"] = get_equity_data()
417
+
418
+ result = obb.technical.zlma(**params)
419
+ assert result
420
+ assert isinstance(result, OBBject)
421
+ assert len(result.results) > 0
422
+ assert result.chart.content
423
+ assert isinstance(result.chart.fig, OpenBBFigure)
424
+
425
+
426
+ @parametrize(
427
+ "params",
428
+ [
429
+ {
430
+ "data": "",
431
+ "model": "yang_zhang",
432
+ "chart": True,
433
+ }
434
+ ],
435
+ )
436
+ @pytest.mark.integration
437
+ def test_charting_technical_cones(params, obb):
438
+ """Test chart ta cones."""
439
+ params = {p: v for p, v in params.items() if v}
440
+
441
+ params["data"] = get_equity_data()
442
+
443
+ result = obb.technical.cones(**params)
444
+ assert result
445
+ assert isinstance(result, OBBject)
446
+ assert len(result.results) > 0
447
+ assert result.chart.content
448
+ assert isinstance(result.chart.fig, OpenBBFigure)
449
+
450
+
451
+ @parametrize(
452
+ "params",
453
+ [
454
+ {
455
+ "data": None,
456
+ "symbol": "DGS10",
457
+ "transform": "pc1",
458
+ "chart": True,
459
+ "provider": "fred",
460
+ }
461
+ ],
462
+ )
463
+ @pytest.mark.integration
464
+ def test_charting_economy_fred_series(params, obb):
465
+ """Test chart economy fred series."""
466
+ result = obb.economy.fred_series(**params)
467
+ assert result
468
+ assert isinstance(result, OBBject)
469
+ assert len(result.results) > 0
470
+ assert result.chart.content
471
+ assert isinstance(result.chart.fig, OpenBBFigure)
472
+
473
+
474
+ @parametrize(
475
+ "params",
476
+ [
477
+ (
478
+ {
479
+ "data": "",
480
+ "study": "price",
481
+ "benchmark": "SPY",
482
+ "long_period": 252,
483
+ "short_period": 21,
484
+ "window": 21,
485
+ "trading_periods": 252,
486
+ "chart": True,
487
+ }
488
+ ),
489
+ ],
490
+ )
491
+ @pytest.mark.integration
492
+ def test_charting_technical_relative_rotation(params, obb):
493
+ params["data"] = obb.equity.price.historical(
494
+ "AAPL,MSFT,GOOGL,AMZN,SPY",
495
+ provider="yfinance",
496
+ start_date="2022-01-01",
497
+ end_date="2024-01-01",
498
+ ).results
499
+ result = obb.technical.relative_rotation(
500
+ data=params["data"],
501
+ benchmark=params["benchmark"],
502
+ study=params["study"],
503
+ long_period=params["long_period"],
504
+ short_period=params["short_period"],
505
+ window=params["window"],
506
+ trading_periods=params["trading_periods"],
507
+ chart=params["chart"],
508
+ )
509
+ assert result
510
+ assert isinstance(result, OBBject)
511
+ assert len(result.results.rs_ratios) > 0 # type: ignore
512
+ assert result.chart.content # type: ignore
513
+ assert isinstance(result.chart.fig, OpenBBFigure) # type: ignore
514
+
515
+
516
+ @parametrize(
517
+ "params",
518
+ [
519
+ {
520
+ "data": None,
521
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
522
+ "chart": True,
523
+ "provider": "finviz",
524
+ "chart_params": {"limit": 4, "orientation": "h"},
525
+ }
526
+ ],
527
+ )
528
+ @pytest.mark.integration
529
+ def test_charting_equity_price_performance(params, obb):
530
+ """Test chart equity price performance."""
531
+ result = obb.equity.price.performance(**params)
532
+ assert result
533
+ assert isinstance(result, OBBject)
534
+ assert len(result.results) > 0
535
+ assert result.chart.content
536
+ assert isinstance(result.chart.fig, OpenBBFigure)
537
+
538
+
539
+ @parametrize(
540
+ "params",
541
+ [
542
+ {
543
+ "data": None,
544
+ "symbol": "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK",
545
+ "chart": True,
546
+ "provider": "fmp",
547
+ "chart_params": {"orientation": "v"},
548
+ }
549
+ ],
550
+ )
551
+ @pytest.mark.integration
552
+ def test_charting_etf_price_performance(params, obb):
553
+ """Test chart etf price performance."""
554
+ result = obb.etf.price_performance(**params)
555
+ assert result
556
+ assert isinstance(result, OBBject)
557
+ assert len(result.results) > 0
558
+ assert result.chart.content
559
+ assert isinstance(result.chart.fig, OpenBBFigure)
560
+
561
+
562
+ @parametrize(
563
+ "params",
564
+ [
565
+ {
566
+ "data": None,
567
+ "symbol": "XRT",
568
+ "chart": True,
569
+ "provider": "fmp",
570
+ "chart_params": {"orientation": "v", "limit": 10},
571
+ }
572
+ ],
573
+ )
574
+ @pytest.mark.integration
575
+ def test_charting_etf_holdings(params, obb):
576
+ """Test chart etf holdings."""
577
+ result = obb.etf.holdings(**params)
578
+ assert result
579
+ assert isinstance(result, OBBject)
580
+ assert len(result.results) > 0
581
+ assert result.chart.content
582
+ assert isinstance(result.chart.fig, OpenBBFigure)
583
+
584
+
585
+ @parametrize(
586
+ "params",
587
+ [
588
+ (
589
+ {
590
+ "provider": "econdb",
591
+ "country": "united_kingdom",
592
+ "date": None,
593
+ "chart": True,
594
+ }
595
+ ),
596
+ (
597
+ {
598
+ "provider": "fred",
599
+ "date": "2023-05-10,2024-05-10",
600
+ "chart": True,
601
+ }
602
+ ),
603
+ ],
604
+ )
605
+ @pytest.mark.integration
606
+ def test_charting_fixedincome_government_yield_curve(params, obb):
607
+ """Test chart fixedincome government yield curve."""
608
+ result = obb.fixedincome.government.yield_curve(**params)
609
+ assert result
610
+ assert isinstance(result, OBBject)
611
+ assert len(result.results) > 0
612
+ assert result.chart.content
613
+ assert isinstance(result.chart.fig, OpenBBFigure)
614
+
615
+
616
+ @parametrize(
617
+ "params",
618
+ [
619
+ {
620
+ "provider": "yfinance",
621
+ "symbol": "ES",
622
+ "start_date": "2022-01-01",
623
+ "end_date": "2022-02-01",
624
+ "chart": True,
625
+ }
626
+ ],
627
+ )
628
+ @pytest.mark.integration
629
+ def test_charting_derivatives_futures_historical(params, obb):
630
+ """Test chart derivatives futures historical."""
631
+ result = obb.derivatives.futures.historical(**params)
632
+ assert result
633
+ assert isinstance(result, OBBject)
634
+ assert len(result.results) > 0
635
+ assert result.chart.content
636
+ assert isinstance(result.chart.fig, OpenBBFigure)
637
+
638
+
639
+ @parametrize(
640
+ "params",
641
+ [
642
+ (
643
+ {
644
+ "provider": "yfinance",
645
+ "symbol": "ES",
646
+ "date": None,
647
+ "chart": True,
648
+ }
649
+ ),
650
+ (
651
+ {
652
+ "provider": "cboe",
653
+ "symbol": "VX",
654
+ "date": "2024-06-25",
655
+ "chart": True,
656
+ }
657
+ ),
658
+ ],
659
+ )
660
+ @pytest.mark.integration
661
+ def test_charting_derivatives_futures_curve(params, obb):
662
+ """Test chart derivatives futures curve."""
663
+ result = obb.derivatives.futures.curve(**params)
664
+ assert result
665
+ assert isinstance(result, OBBject)
666
+ assert len(result.results) > 0
667
+ assert result.chart.content
668
+ assert isinstance(result.chart.fig, OpenBBFigure)
669
+
670
+
671
+ @parametrize(
672
+ "params",
673
+ [
674
+ (
675
+ {
676
+ "provider": "fmp",
677
+ "symbol": "AAPL",
678
+ "start_date": "2024-01-01",
679
+ "end_date": "2024-06-30",
680
+ "chart": True,
681
+ }
682
+ ),
683
+ ],
684
+ )
685
+ @pytest.mark.integration
686
+ def test_charting_equity_historical_market_cap(params, obb):
687
+ """Test chart equity historical market cap."""
688
+ result = obb.equity.historical_market_cap(**params)
689
+ assert result
690
+ assert isinstance(result, OBBject)
691
+ assert len(result.results) > 0
692
+ assert result.chart.content
693
+ assert isinstance(result.chart.fig, OpenBBFigure)
694
+
695
+
696
+ @parametrize(
697
+ "params",
698
+ [
699
+ (
700
+ {
701
+ "provider": "bls",
702
+ "symbol": "APUS49D74714,APUS49D74715,APUS49D74716",
703
+ "start_date": "2014-01-01",
704
+ "end_date": "2024-07-01",
705
+ "chart": True,
706
+ }
707
+ ),
708
+ ],
709
+ )
710
+ @pytest.mark.integration
711
+ def test_charting_economy_survey_bls_series(params, obb):
712
+ """Test chart economy survey bls series."""
713
+ result = obb.economy.survey.bls_series(**params)
714
+ assert result
715
+ assert isinstance(result, OBBject)
716
+ assert len(result.results) > 0
717
+ assert result.chart.content
718
+ assert isinstance(result.chart.fig, OpenBBFigure)
719
+
720
+
721
+ @parametrize(
722
+ "params",
723
+ [
724
+ {
725
+ "data": "",
726
+ "method": "pearson",
727
+ "chart": True,
728
+ }
729
+ ],
730
+ )
731
+ @pytest.mark.integration
732
+ def test_charting_econometrics_correlation_matrix(params, obb):
733
+ """Test chart econometrics correlation matrix."""
734
+
735
+ symbols = "XRT,XLB,XLI,XLH,XLC,XLY,XLU,XLK".split(",")
736
+ params["data"] = (
737
+ obb.equity.price.historical(symbol=symbols, provider="yfinance")
738
+ .to_df()
739
+ .pivot(columns="symbol", values="close")
740
+ .filter(items=symbols, axis=1)
741
+ )
742
+ result = obb.econometrics.correlation_matrix(**params)
743
+ assert result
744
+ assert isinstance(result, OBBject)
745
+ assert len(result.results) > 0
746
+ assert result.chart.content
747
+ assert isinstance(result.chart.fig, OpenBBFigure)
openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenBB OBBject extension for charting."""
2
+
3
+ import warnings
4
+
5
+ from openbb_core.app.model.extension import Extension
6
+
7
+ warnings.filterwarnings(
8
+ "ignore",
9
+ category=UserWarning,
10
+ module="openbb_core.app.model.extension",
11
+ )
12
+
13
+
14
+ def get_charting_module():
15
+ """Get the Charting module."""
16
+ # pylint: disable=import-outside-toplevel
17
+ import importlib
18
+
19
+ _Charting = importlib.import_module("openbb_charting.charting").Charting
20
+ return _Charting
21
+
22
+
23
+ ext = Extension(name="charting", description="Create custom charts from OBBject data.")
24
+
25
+ Charting = ext.obbject_accessor(get_charting_module())
openbb_platform/obbject_extensions/charting/openbb_charting/charting.py ADDED
@@ -0,0 +1,681 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Charting Class implementation."""
2
+
3
+ # pylint: disable=too-many-arguments,unused-argument,too-many-positional-arguments
4
+
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Callable,
9
+ ClassVar,
10
+ Dict,
11
+ List,
12
+ Literal,
13
+ Optional,
14
+ Tuple,
15
+ Type,
16
+ Union,
17
+ )
18
+ from warnings import warn
19
+
20
+ from importlib_metadata import entry_points
21
+ from openbb_core.app.model.charts.chart import Chart
22
+ from openbb_core.app.model.obbject import OBBject
23
+ from openbb_core.provider.abstract.data import Data
24
+
25
+ from openbb_charting.charts.helpers import (
26
+ get_charting_functions,
27
+ get_charting_functions_list,
28
+ )
29
+
30
+ if TYPE_CHECKING:
31
+ from numpy import ndarray # noqa
32
+ from pandas import DataFrame, Series # noqa
33
+ from plotly.graph_objs import Figure # noqa
34
+ from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
35
+ from openbb_charting.query_params import ChartParams # noqa
36
+ from openbb_charting.core.backend import Backend # noqa
37
+
38
+
39
+ class Charting:
40
+ """Charting extension.
41
+
42
+ Methods
43
+ -------
44
+ show
45
+ Display chart and save it to the OBBject.
46
+ to_chart
47
+ Redraw the chart and save it to the OBBject, with an optional entry point for Data.
48
+ functions
49
+ Return a list of Platform commands with charting functions.
50
+ get_params
51
+ Return the charting parameters for the function the OBBject was created from.
52
+ indicators
53
+ Return the list of the available technical indicators to use with the `to_chart` method and OHLC+V data.
54
+ table
55
+ Display an interactive table.
56
+ create_line_chart
57
+ Create a line chart from external data.
58
+ create_bar_chart
59
+ Create a bar chart, on a single x-axis with one or more values for the y-axis, from external data.
60
+ create_correlation_matrix
61
+ Create a correlation matrix from external data.
62
+ toggle_chart_style
63
+ Toggle the chart style, of an existing chart, between light and dark mode.
64
+ """
65
+
66
+ _extension_views: ClassVar[List[Type]] = [
67
+ entry_point.load()
68
+ for entry_point in entry_points(group="openbb_charting_extension")
69
+ ]
70
+ _format = "plotly" # the charts computed by this extension will be in plotly format
71
+
72
+ def __init__(self, obbject):
73
+ """Initialize Charting extension."""
74
+ # pylint: disable=import-outside-toplevel
75
+ import importlib # noqa
76
+
77
+ charting_settings_module = importlib.import_module(
78
+ "openbb_core.app.model.charts.charting_settings", "ChartingSettings"
79
+ )
80
+ ChartingSettings = charting_settings_module.ChartingSettings
81
+
82
+ self._obbject: OBBject = obbject
83
+ self._charting_settings = ChartingSettings(
84
+ user_settings=self._obbject._user_settings, # type: ignore
85
+ system_settings=self._obbject._system_settings, # type: ignore
86
+ )
87
+ self._backend = self._handle_backend()
88
+ self._functions: Dict[str, Callable] = self._get_functions()
89
+
90
+ @classmethod
91
+ def indicators(cls):
92
+ """Return an instance of the IndicatorsParams class, containing all available indicators and their parameters.
93
+
94
+ Without assigning to a variable, it will print the the information to the console.
95
+ """
96
+ # pylint: disable=import-outside-toplevel
97
+ from openbb_charting.query_params import IndicatorsParams
98
+
99
+ return IndicatorsParams()
100
+
101
+ @classmethod
102
+ def functions(cls) -> List[str]:
103
+ """Return a list of the available functions."""
104
+ functions: List[str] = []
105
+ for view in cls._extension_views:
106
+ functions.extend(get_charting_functions_list(view))
107
+
108
+ return functions
109
+
110
+ def _get_functions(self) -> Dict[str, Callable]:
111
+ """Return a dict with the available functions."""
112
+ functions: Dict[str, Callable] = {}
113
+ for view in self._extension_views:
114
+ functions.update(get_charting_functions(view))
115
+
116
+ return functions
117
+
118
+ def _handle_backend(self) -> "Backend":
119
+ """Create and start the backend."""
120
+ # pylint: disable=import-outside-toplevel
121
+ from openbb_charting.core.backend import create_backend, get_backend
122
+
123
+ create_backend(self._charting_settings)
124
+ backend = get_backend()
125
+ backend.start(debug=self._charting_settings.debug_mode)
126
+ return backend
127
+
128
+ def _get_chart_function(self, route: str) -> Callable:
129
+ """Given a route, it returns the chart function. The module must contain the given route."""
130
+ if route is None:
131
+ raise ValueError("OBBject was initialized with no function route.")
132
+ adjusted_route = route.replace("/", "_")[1:]
133
+ if adjusted_route not in self._functions:
134
+ raise ValueError(
135
+ f"Could not find the route `{adjusted_route}` in the charting functions."
136
+ )
137
+ return self._functions[adjusted_route]
138
+
139
+ def get_params(self) -> Union["ChartParams", None]:
140
+ """Return the ChartQueryParams class for the function the OBBject was created from.
141
+
142
+ Without assigning to a variable, it will print the docstring to the console.
143
+ If the class is not defined, the help for the function will be returned.
144
+ """
145
+ # pylint: disable=import-outside-toplevel
146
+ from openbb_charting.query_params import ChartParams
147
+
148
+ if self._obbject._route is None: # pylint: disable=protected-access
149
+ raise ValueError("OBBject was initialized with no function route.")
150
+ charting_function = (
151
+ self._obbject._route # pylint: disable=protected-access
152
+ ).replace("/", "_")[1:]
153
+ if hasattr(ChartParams, charting_function):
154
+ return getattr(ChartParams, charting_function)()
155
+
156
+ return help( # type: ignore
157
+ self._get_chart_function( # pylint: disable=protected-access
158
+ self._obbject.extra[ # pylint: disable=protected-access
159
+ "metadata"
160
+ ].route
161
+ )
162
+ )
163
+
164
+ def _prepare_data_as_df(
165
+ self, data: Optional[Union["DataFrame", "Series"]]
166
+ ) -> Tuple["DataFrame", bool]:
167
+ """Convert supplied data to a DataFrame."""
168
+ # pylint: disable=import-outside-toplevel
169
+ from openbb_core.app.utils import basemodel_to_df, convert_to_basemodel
170
+ from pandas import DataFrame, Series
171
+
172
+ has_data = (
173
+ isinstance(data, (Data, DataFrame, Series)) and not data.empty # type: ignore
174
+ ) or (bool(data))
175
+ index = (
176
+ data.index.name
177
+ if has_data and isinstance(data, (DataFrame, Series))
178
+ else None
179
+ )
180
+ data_as_df: DataFrame = (
181
+ basemodel_to_df(convert_to_basemodel(data), index=index)
182
+ if has_data
183
+ else self._obbject.to_dataframe(index=index)
184
+ )
185
+ if "date" in data_as_df.columns:
186
+ data_as_df = data_as_df.set_index("date")
187
+ if "provider" in data_as_df.columns:
188
+ data_as_df.drop(columns="provider", inplace=True)
189
+ return data_as_df, has_data
190
+
191
+ # pylint: disable=too-many-locals
192
+ def create_line_chart(
193
+ self,
194
+ data: Union[
195
+ list,
196
+ dict,
197
+ "DataFrame",
198
+ List["DataFrame"],
199
+ "Series",
200
+ List["Series"],
201
+ "ndarray",
202
+ Data,
203
+ ],
204
+ index: Optional[str] = None,
205
+ target: Optional[str] = None,
206
+ title: Optional[str] = None,
207
+ x: Optional[str] = None,
208
+ xtitle: Optional[str] = None,
209
+ y: Optional[Union[str, List[str]]] = None,
210
+ ytitle: Optional[str] = None,
211
+ y2: Optional[Union[str, List[str]]] = None,
212
+ y2title: Optional[str] = None,
213
+ layout_kwargs: Optional[dict] = None,
214
+ scatter_kwargs: Optional[dict] = None,
215
+ normalize: bool = False,
216
+ returns: bool = False,
217
+ same_axis: bool = False,
218
+ render: bool = True,
219
+ **kwargs,
220
+ ) -> Union["OpenBBFigure", "Figure", None]:
221
+ """Create a line chart from external data and render a chart or return the OpenBBFigure.
222
+
223
+ Parameters
224
+ ----------
225
+ data : Union[Data, DataFrame, Series]
226
+ Data to be plotted (OHLCV data).
227
+ index : Optional[str], optional
228
+ Index column, by default None
229
+ target : Optional[str], optional
230
+ Target column to be plotted, by default None
231
+ title : Optional[str], optional
232
+ Chart title, by default None
233
+ x : Optional[str], optional
234
+ X-axis column, by default None
235
+ xtitle : Optional[str], optional
236
+ X-axis title, by default None
237
+ y : Optional[Union[str, List[str]]], optional
238
+ Y-axis column(s), by default None
239
+ If None are supplied, the layout is optimized for the contents of data.
240
+ Where many units/scales are present,
241
+ it will attempt to divide based on the range of values.
242
+ ytitle : Optional[str], optional
243
+ Y-axis title, by default None
244
+ y2 : Optional[Union[str, List[str]]], optional
245
+ Y2-axis column(s), by default None
246
+ y2title : Optional[str], optional
247
+ Y2-axis title, by default None
248
+ layout_kwargs : Optional[dict], optional
249
+ Additional Plotly Layout parameters for `fig.update_layout`, by default None
250
+ scatter_kwargs : Optional[dict], optional
251
+ Additional Plotly parameters applied on creation of each scatter plot, by default None
252
+ normalize : bool, optional
253
+ Normalize the data with Z-Score Standardization, by default False
254
+ returns : bool, optional
255
+ Convert the data to cumulative returns, by default False
256
+ same_axis: bool, optional
257
+ If True, forces all data onto the same Y-axis, by default False
258
+ render: bool, optional
259
+ If True, the chart will be rendered, by default True
260
+ **kwargs: Dict[str, Any]
261
+ Extra parameters to be passed to `figure.show()`
262
+ """
263
+ # pylint: disable=import-outside-toplevel
264
+ from openbb_charting.charts.generic_charts import line_chart
265
+
266
+ fig = line_chart(
267
+ data=data,
268
+ index=index,
269
+ target=target,
270
+ title=title,
271
+ x=x,
272
+ xtitle=xtitle,
273
+ y=y,
274
+ ytitle=ytitle,
275
+ y2=y2,
276
+ y2title=y2title,
277
+ layout_kwargs=layout_kwargs,
278
+ scatter_kwargs=scatter_kwargs,
279
+ normalize=normalize,
280
+ returns=returns,
281
+ same_axis=same_axis,
282
+ **kwargs,
283
+ )
284
+ fig = self._set_chart_style(fig)
285
+ if render:
286
+ return fig.show(**kwargs)
287
+
288
+ return fig
289
+
290
+ def create_bar_chart(
291
+ self,
292
+ data: Union[
293
+ list,
294
+ dict,
295
+ "DataFrame",
296
+ List["DataFrame"],
297
+ "Series",
298
+ List["Series"],
299
+ "ndarray",
300
+ Data,
301
+ ],
302
+ x: str,
303
+ y: Union[str, List[str]],
304
+ barmode: Literal["group", "stack", "relative", "overlay"] = "group",
305
+ xtype: Literal[
306
+ "category", "multicategory", "date", "log", "linear"
307
+ ] = "category",
308
+ title: Optional[str] = None,
309
+ xtitle: Optional[str] = None,
310
+ ytitle: Optional[str] = None,
311
+ orientation: Literal["h", "v"] = "v",
312
+ colors: Optional[List[str]] = None,
313
+ layout_kwargs: Optional[Dict[str, Any]] = None,
314
+ bar_kwargs: Optional[Dict[str, Any]] = None,
315
+ render: bool = True,
316
+ **kwargs,
317
+ ) -> Union["OpenBBFigure", "Figure", None]:
318
+ """Create a bar chart on a single x-axis with one or more values for the y-axis.
319
+
320
+ Parameters
321
+ ----------
322
+ data : Union[list, dict, DataFrame, List[DataFrame], Series, List[Series], ndarray, Data]
323
+ Data to plot.
324
+ x : str
325
+ The x-axis column name.
326
+ y : Union[str, List[str]]
327
+ The y-axis column name(s).
328
+ barmode : Literal["group", "stack", "relative", "overlay"], optional
329
+ The bar mode, by default "group".
330
+ xtype : Literal["category", "multicategory", "date", "log", "linear"], optional
331
+ The x-axis type, by default "category".
332
+ title : str, optional
333
+ The title of the chart, by default None.
334
+ xtitle : str, optional
335
+ The x-axis title, by default None.
336
+ ytitle : str, optional
337
+ The y-axis title, by default None.
338
+ colors: List[str], optional
339
+ Manually set the colors to cycle through for each column in 'y', by default None.
340
+ bar_kwargs : Dict[str, Any], optional
341
+ Additional keyword arguments to apply with figure.add_bar(), by default None.
342
+ layout_kwargs : Dict[str, Any], optional
343
+ Additional keyword arguments to apply with figure.update_layout(), by default None.
344
+ Returns
345
+ -------
346
+ OpenBBFigure
347
+ The OpenBBFigure object.
348
+ """
349
+ # pylint: disable=import-outside-toplevel
350
+ from openbb_charting.charts.generic_charts import bar_chart
351
+
352
+ fig = bar_chart(
353
+ data=data,
354
+ x=x,
355
+ y=y,
356
+ barmode=barmode,
357
+ xtype=xtype,
358
+ title=title,
359
+ xtitle=xtitle,
360
+ ytitle=ytitle,
361
+ orientation=orientation,
362
+ colors=colors,
363
+ bar_kwargs=bar_kwargs,
364
+ layout_kwargs=layout_kwargs,
365
+ )
366
+ fig = self._set_chart_style(fig)
367
+ if render:
368
+ return fig.show(**kwargs)
369
+
370
+ return fig
371
+
372
+ def create_correlation_matrix(
373
+ self,
374
+ data: Union[
375
+ list[Data],
376
+ "DataFrame",
377
+ ],
378
+ method: Literal["pearson", "kendall", "spearman"] = "pearson",
379
+ colorscale: str = "RdBu",
380
+ title: str = "Asset Correlation Matrix",
381
+ layout_kwargs: Optional[Dict[str, Any]] = None,
382
+ ):
383
+ """Create a correlation matrix from external data.
384
+
385
+ Parameters
386
+ ----------
387
+ data : Union[list[Data], DataFrame]
388
+ Input dataset.
389
+ method : Literal["pearson", "kendall", "spearman"]
390
+ Method to use for correlation calculation. Default is "pearson".
391
+ pearson : standard correlation coefficient
392
+ kendall : Kendall Tau correlation coefficient
393
+ spearman : Spearman rank correlation
394
+ colorscale : str
395
+ Plotly colorscale to use for the heatmap. Default is "RdBu".
396
+ title : str
397
+ Title of the chart. Default is "Asset Correlation Matrix".
398
+ layout_kwargs : Dict[str, Any]
399
+ Additional keyword arguments to apply with figure.update_layout(), by default None.
400
+
401
+ Returns
402
+ -------
403
+ OpenBBFigure
404
+ The OpenBBFigure object.
405
+ """
406
+ # pylint: disable=import-outside-toplevel
407
+ from openbb_charting.charts.correlation_matrix import correlation_matrix
408
+
409
+ kwargs = {
410
+ "data": data,
411
+ "method": method,
412
+ "colorscale": colorscale,
413
+ "title": title,
414
+ "layout_kwargs": layout_kwargs,
415
+ }
416
+ fig, _ = correlation_matrix(**kwargs)
417
+ fig = self._set_chart_style(fig)
418
+ return fig
419
+
420
+ # pylint: disable=inconsistent-return-statements
421
+ def show(self, render: bool = True, **kwargs):
422
+ """Display chart and save it to the OBBject."""
423
+ try:
424
+ charting_function = self._get_chart_function(
425
+ self._obbject._route # pylint: disable=protected-access
426
+ )
427
+ kwargs["obbject_item"] = self._obbject.results
428
+ kwargs["charting_settings"] = self._charting_settings
429
+ kwargs["standard_params"] = (
430
+ self._obbject._standard_params # pylint: disable=protected-access
431
+ )
432
+ kwargs["extra_params"] = (
433
+ self._obbject._extra_params # pylint: disable=protected-access
434
+ )
435
+ kwargs["provider"] = self._obbject.provider
436
+ kwargs["extra"] = self._obbject.extra
437
+ fig, content = charting_function(**kwargs)
438
+ fig = self._set_chart_style(fig)
439
+ content = fig.show(external=True, **kwargs).to_plotly_json()
440
+ self._obbject.chart = Chart(fig=fig, content=content, format=self._format)
441
+ if render:
442
+ fig.show(**kwargs)
443
+ except Exception: # pylint: disable=W0718
444
+ try:
445
+ fig = self.create_line_chart(data=self._obbject.results, render=False, **kwargs) # type: ignore
446
+ fig = self._set_chart_style(fig)
447
+ content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore
448
+ self._obbject.chart = Chart(
449
+ fig=fig, content=content, format=self._format
450
+ )
451
+ if render:
452
+ return fig.show(**kwargs) # type: ignore
453
+ except Exception as e:
454
+ raise RuntimeError(
455
+ "Failed to automatically create a generic chart with the data provided."
456
+ ) from e
457
+
458
+ # pylint: disable=too-many-locals,inconsistent-return-statements
459
+ def to_chart(
460
+ self,
461
+ data: Optional[
462
+ Union[
463
+ list,
464
+ dict,
465
+ "DataFrame",
466
+ List["DataFrame"],
467
+ "Series",
468
+ List["Series"],
469
+ "ndarray",
470
+ Data,
471
+ ]
472
+ ] = None,
473
+ target: Optional[str] = None,
474
+ index: Optional[str] = None,
475
+ indicators: Optional[Dict[str, Dict[str, Any]]] = None,
476
+ symbol: str = "",
477
+ candles: bool = True,
478
+ volume: bool = True,
479
+ volume_ticks_x: int = 7,
480
+ render: bool = True,
481
+ **kwargs,
482
+ ):
483
+ """Create an OpenBBFigure with user customizations (if any) and save it to the OBBject.
484
+
485
+ This function is used to populate, or re-populate, the OBBject with a chart using the data within
486
+ the OBBject or external data supplied via the `data` parameter.
487
+ This function modifies the original OBBject by overwriting the existing chart.
488
+
489
+ Parameters
490
+ ----------
491
+ data : Union[Data, DataFrame, Series]
492
+ Data to be plotted.
493
+ indicators : Dict[str, Dict[str, Any]], optional
494
+ Indicators to be plotted, by default None
495
+ symbol : str, optional
496
+ Symbol to be plotted. This is used for labels and titles, by default ""
497
+ candles : bool, optional
498
+ If True, candles will be plotted, by default True
499
+ volume : bool, optional
500
+ If True, volume will be plotted, by default True
501
+ volume_ticks_x : int, optional
502
+ Volume ticks, by default 7
503
+ render : bool, optional
504
+ If True, the chart will be rendered, by default True
505
+ kwargs: Dict[str, Any]
506
+ Extra parameters to be passed to the chart constructor.
507
+
508
+ Examples
509
+ --------
510
+ Plotting a time series with TA indicators
511
+
512
+ >>> from openbb import obb
513
+ >>> res = obb.equity.price.historical("AAPL")
514
+ >>> indicators = dict(
515
+ >>> sma=dict(length=[20,30,50]),
516
+ >>> adx=dict(length=14),
517
+ >>> rsi=dict(length=14),
518
+ >>> macd=dict(fast=12, slow=26, signal=9),
519
+ >>> bbands=dict(length=20, std=2),
520
+ >>> stoch=dict(length=14),
521
+ >>> ema=dict(length=[20,30,50]),
522
+ >>> )
523
+ >>> res.charting.to_chart(**{"indicators": indicators})
524
+
525
+ Get all the available indicators
526
+
527
+ >>> res = obb.equity.price.historical("AAPL")
528
+ >>> indicators = res.charting.indicators()
529
+ >>> indicators?
530
+ """
531
+ data_as_df, has_data = self._prepare_data_as_df(data) # type: ignore
532
+ if target is not None:
533
+ data_as_df = data_as_df[[target]]
534
+ kwargs["candles"] = candles
535
+ kwargs["volume"] = volume
536
+ kwargs["volume_ticks_x"] = volume_ticks_x
537
+ kwargs["indicators"] = indicators if indicators else {}
538
+ kwargs["symbol"] = symbol
539
+ kwargs["target"] = target
540
+ kwargs["index"] = index
541
+ kwargs["obbject_item"] = self._obbject.results
542
+ kwargs["charting_settings"] = self._charting_settings
543
+ kwargs["standard_params"] = (
544
+ self._obbject._standard_params # pylint: disable=protected-access
545
+ )
546
+ kwargs["extra_params"] = (
547
+ self._obbject._extra_params # pylint: disable=protected-access
548
+ )
549
+ kwargs["provider"] = self._obbject.provider # pylint: disable=protected-access
550
+ kwargs["extra"] = self._obbject.extra # pylint: disable=protected-access
551
+ try:
552
+ if has_data:
553
+ self.show(data=data_as_df, render=render, **kwargs)
554
+ else:
555
+ self.show(**kwargs, render=render)
556
+ except Exception: # pylint: disable=W0718
557
+ try:
558
+ fig = self.create_line_chart(data=data_as_df, render=False, **kwargs)
559
+ fig = self._set_chart_style(fig)
560
+ content = fig.show(external=True, **kwargs).to_plotly_json() # type: ignore
561
+ self._obbject.chart = Chart(
562
+ fig=fig, content=content, format=self._format
563
+ )
564
+ if render:
565
+ return fig.show(**kwargs) # type: ignore
566
+ except Exception as e: # pylint: disable=W0718
567
+ raise RuntimeError(
568
+ "Failed to automatically create a generic chart with the data provided."
569
+ ) from e
570
+
571
+ def _set_chart_style(self, figure: "Figure"):
572
+ """Set the user preference for light or dark mode."""
573
+ style = self._charting_settings.chart_style
574
+ font_color = "black" if style == "light" else "white"
575
+ paper_bgcolor = "white" if style == "light" else "black"
576
+ figure = figure.update_layout(
577
+ dict( # pylint: disable=R1735
578
+ font_color=font_color, paper_bgcolor=paper_bgcolor
579
+ )
580
+ )
581
+ return figure
582
+
583
+ def toggle_chart_style(self):
584
+ """Toggle the chart style between light and dark mode."""
585
+ if not hasattr(self._obbject.chart, "fig"):
586
+ raise ValueError(
587
+ "Error: No chart has been created. Please create a chart first."
588
+ )
589
+ current = self._charting_settings.chart_style
590
+ new = "light" if current == "dark" else "dark"
591
+ self._charting_settings.chart_style = new
592
+ figure = self._obbject.chart.fig # type: ignore[union-attr]
593
+ updated_figure = self._set_chart_style(figure)
594
+ self._obbject.chart.fig = updated_figure # type: ignore[union-attr]
595
+ self._obbject.chart.content = updated_figure.show( # type: ignore[union-attr]
596
+ external=True
597
+ ).to_plotly_json()
598
+
599
+ @staticmethod
600
+ def _convert_to_string(x):
601
+ """Sanitize the data for the table."""
602
+ # pylint: disable=import-outside-toplevel
603
+ from numpy import isnan
604
+
605
+ if isinstance(x, (float, int)) and not isnan(x):
606
+ return x
607
+ if isinstance(x, dict):
608
+ return ", ".join([str(v) for v in x.values()])
609
+ if isinstance(x, list):
610
+ if all(isinstance(i, dict) for i in x):
611
+ return ", ".join(
612
+ str(", ".join([str(v) for v in i.values()])) for i in x
613
+ )
614
+ return ", ".join([str(i) for i in x])
615
+
616
+ return (
617
+ str(x)
618
+ .replace("[", "")
619
+ .replace("]", "")
620
+ .replace("'{", "")
621
+ .replace("}'", "")
622
+ .replace("nan", "")
623
+ )
624
+
625
+ def table(
626
+ self,
627
+ data: Optional[Union["DataFrame", "Series"]] = None,
628
+ title: str = "",
629
+ ):
630
+ """Display an interactive table.
631
+
632
+ Parameters
633
+ ----------
634
+ data : Optional[Union[DataFrame, Series]], optional
635
+ Data to be plotted, by default None.
636
+ If no data is provided the OBBject results will be used.
637
+ title : str, optional
638
+ Title of the table, by default "".
639
+ """
640
+ # pylint: disable=import-outside-toplevel
641
+ from pandas import RangeIndex
642
+
643
+ data_as_df, _ = self._prepare_data_as_df(data)
644
+ if isinstance(data_as_df.index, RangeIndex):
645
+ data_as_df.reset_index(inplace=True, drop=True)
646
+ else:
647
+ data_as_df.reset_index(inplace=True)
648
+ for col in data_as_df.columns:
649
+ data_as_df[col] = data_as_df[col].apply(self._convert_to_string)
650
+ if self._backend.isatty:
651
+ try:
652
+ self._backend.send_table(
653
+ df_table=data_as_df,
654
+ title=title
655
+ or self._obbject._route, # pylint: disable=protected-access
656
+ theme=self._charting_settings.table_style,
657
+ )
658
+ except Exception as e: # pylint: disable=W0718
659
+ warn(f"Failed to show figure with backend. {e}")
660
+
661
+ else:
662
+ from plotly import optional_imports
663
+
664
+ ipython_display = optional_imports.get_module("IPython.display")
665
+ if ipython_display:
666
+ ipython_display.display(ipython_display.HTML(data_as_df.to_html()))
667
+ else:
668
+ warn("IPython.display is not available.")
669
+
670
+ def url(
671
+ self,
672
+ url: str,
673
+ title: str = "",
674
+ width: Optional[int] = None,
675
+ height: Optional[int] = None,
676
+ ):
677
+ """Return the URL of the chart."""
678
+ try:
679
+ self._backend.send_url(url=url, title=title, width=width, height=height)
680
+ except Exception as e: # pylint: disable=W0718
681
+ warn(f"Failed to show figure with backend. {e}")
openbb_platform/obbject_extensions/charting/openbb_charting/charts/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """OpenBB Charting utils."""
openbb_platform/obbject_extensions/charting/openbb_charting/charts/correlation_matrix.py ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Correlation Matrix Chart."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Union
4
+
5
+ if TYPE_CHECKING:
6
+ from plotly.graph_objs import Figure # noqa
7
+ from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
8
+
9
+
10
+ def correlation_matrix( # noqa: PLR0912
11
+ **kwargs,
12
+ ) -> tuple[Union["OpenBBFigure", "Figure"], dict[str, Any]]:
13
+ """Correlation Matrix Chart."""
14
+ # pylint: disable=import-outside-toplevel
15
+ from numpy import ones_like, triu # noqa
16
+ from openbb_core.app.utils import basemodel_to_df # noqa
17
+ from openbb_charting.core.openbb_figure import OpenBBFigure
18
+ from openbb_charting.core.chart_style import ChartStyle
19
+ from plotly.graph_objs import Figure, Heatmap, Layout
20
+ from pandas import DataFrame
21
+
22
+ if "data" in kwargs and isinstance(kwargs["data"], DataFrame):
23
+ corr = kwargs["data"]
24
+ elif "data" in kwargs and isinstance(kwargs["data"], list):
25
+ corr = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date")) # type: ignore
26
+ else:
27
+ corr = basemodel_to_df(
28
+ kwargs["obbject_item"], index=kwargs.get("index", "date") # type: ignore
29
+ )
30
+ if (
31
+ "symbol" in corr.columns
32
+ and len(corr.symbol.unique()) > 1
33
+ and "close" in corr.columns
34
+ ):
35
+ corr = corr.pivot(
36
+ columns="symbol",
37
+ values="close",
38
+ )
39
+
40
+ method = kwargs.get("method") or "pearson"
41
+ corr = corr.corr(method=method, numeric_only=True)
42
+
43
+ X = corr.columns.to_list()
44
+ x_replace = X[-1]
45
+ Y = X.copy()
46
+ y_replace = Y[0]
47
+ X = [x if x != x_replace else "" for x in X]
48
+ Y = [y if y != y_replace else "" for y in Y]
49
+ mask = triu(ones_like(corr, dtype=bool))
50
+ df = corr.mask(mask)
51
+ title = kwargs.get("title") or "Asset Correlation Matrix"
52
+ text_color = "white" if ChartStyle().plt_style == "dark" else "black"
53
+ colorscale = kwargs.get("colorscale") or "RdBu"
54
+
55
+ heatmap = Heatmap(
56
+ z=df,
57
+ x=X,
58
+ y=Y,
59
+ xgap=1,
60
+ ygap=1,
61
+ colorscale=colorscale,
62
+ colorbar=dict(
63
+ orientation="v",
64
+ x=0.8,
65
+ y=0.5,
66
+ xanchor="left",
67
+ yanchor="middle",
68
+ xref="container",
69
+ yref="paper",
70
+ len=0.66,
71
+ bgcolor="rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)",
72
+ ),
73
+ text=df.fillna(""),
74
+ texttemplate="%{text:.4f}",
75
+ hoverongaps=False,
76
+ hovertemplate="%{x} - %{y} : %{z:.4f}<extra></extra>",
77
+ )
78
+ layout = Layout(
79
+ title=title,
80
+ title_x=0.5,
81
+ xaxis=dict(
82
+ showgrid=False,
83
+ showline=False,
84
+ ticklen=0,
85
+ domain=[0.03, 1],
86
+ tickangle=90,
87
+ automargin=False,
88
+ ),
89
+ yaxis=dict(
90
+ showgrid=False,
91
+ side="left",
92
+ autorange="reversed",
93
+ showline=False,
94
+ ticklen=0,
95
+ automargin="height+width+left",
96
+ tickmode="auto",
97
+ ),
98
+ margin=dict(l=10, r=0, t=0, b=10),
99
+ dragmode="pan",
100
+ )
101
+ fig = Figure(data=[heatmap], layout=layout)
102
+ figure = OpenBBFigure(fig=fig)
103
+ figure.update_layout(
104
+ font=dict(color=text_color),
105
+ paper_bgcolor=(
106
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
107
+ ),
108
+ plot_bgcolor=(
109
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
110
+ ),
111
+ )
112
+ layout_kwargs = kwargs.get("layout_kwargs", {})
113
+
114
+ if layout_kwargs:
115
+ figure.update_layout(**layout_kwargs)
116
+
117
+ content = figure.show(external=True).to_plotly_json() # type: ignore
118
+
119
+ return figure, content
openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py ADDED
@@ -0,0 +1,654 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Generic Charts Module."""
2
+
3
+ # pylint: disable=too-many-arguments,unused-argument,too-many-locals, too-many-branches, too-many-lines, too-many-statements, use-dict-literal, broad-exception-caught, too-many-nested-blocks, too-many-positional-arguments
4
+
5
+ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union
6
+
7
+ from openbb_core.app.utils import basemodel_to_df, convert_to_basemodel
8
+ from openbb_core.provider.abstract.data import Data
9
+
10
+ from openbb_charting.charts.helpers import (
11
+ calculate_returns,
12
+ should_share_axis,
13
+ z_score_standardization,
14
+ )
15
+ from openbb_charting.core.chart_style import ChartStyle
16
+ from openbb_charting.styles.colors import LARGE_CYCLER
17
+
18
+ if TYPE_CHECKING:
19
+ from numpy import ndarray # noqa
20
+ from pandas import DataFrame, Series # noqa
21
+ from plotly.graph_objs import Figure # noqa
22
+ from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
23
+
24
+
25
+ def line_chart( # noqa: PLR0912
26
+ data: Union[
27
+ list,
28
+ dict,
29
+ "DataFrame",
30
+ List["DataFrame"],
31
+ "Series",
32
+ List["Series"],
33
+ "ndarray",
34
+ Data,
35
+ ],
36
+ index: Optional[str] = None,
37
+ target: Optional[str] = None,
38
+ title: Optional[str] = None,
39
+ x: Optional[str] = None,
40
+ xtitle: Optional[str] = None,
41
+ y: Optional[Union[str, List[str]]] = None,
42
+ ytitle: Optional[str] = None,
43
+ y2: Optional[Union[str, List[str]]] = None,
44
+ y2title: Optional[str] = None,
45
+ layout_kwargs: Optional[dict] = None,
46
+ scatter_kwargs: Optional[dict] = None,
47
+ normalize: bool = False,
48
+ returns: bool = False,
49
+ same_axis: bool = False,
50
+ **kwargs,
51
+ ) -> Union["OpenBBFigure", "Figure"]:
52
+ """Create a line chart."""
53
+ # pylint: disable=import-outside-toplevel
54
+ from pandas import DataFrame, Series, to_datetime # noqa
55
+ from openbb_charting.core.openbb_figure import OpenBBFigure
56
+
57
+ if data is None:
58
+ raise ValueError("Error: Data is a required field.")
59
+
60
+ auto_layout = False
61
+ index = (
62
+ data.index.name
63
+ if isinstance(data, (DataFrame, Series))
64
+ else index if index is not None else x if x is not None else "date"
65
+ )
66
+ df: DataFrame = (basemodel_to_df(convert_to_basemodel(data), index=index)).dropna(
67
+ how="all", axis=1
68
+ )
69
+
70
+ if df.index.name is None:
71
+ if "date" in df.columns:
72
+ df.date = df.date.apply(to_datetime)
73
+ df.set_index("date", inplace=True)
74
+ else:
75
+ found_index = False
76
+ for col in df.columns:
77
+ if df[col].dtype == "object":
78
+ try:
79
+ df[col] = df[col].apply(to_datetime)
80
+ index = df[col].name # type: ignore
81
+ df.set_index(col, inplace=True)
82
+ df.index.name = "date"
83
+ found_index = True
84
+ except Exception as _: # noqa: S112
85
+ continue
86
+ if found_index is True:
87
+ break
88
+ if found_index is False:
89
+ df.set_index(df.iloc[:, 0], inplace=True)
90
+
91
+ if "symbol" in df.columns and len(df.symbol.unique()) > 1:
92
+ df = df.pivot(columns="symbol", values=target if target else "close")
93
+
94
+ if "symbol" not in df.columns and target in df.columns:
95
+ df = df[[target]]
96
+
97
+ y = y.split(",") if isinstance(y, str) else y
98
+
99
+ if y is None or same_axis is True:
100
+ y = df.columns.to_list()
101
+ auto_layout = True
102
+
103
+ if same_axis is True:
104
+ auto_layout = False
105
+
106
+ if returns is True:
107
+ df = df.apply(calculate_returns)
108
+ auto_layout = False
109
+
110
+ if normalize is True:
111
+ df = df.apply(z_score_standardization)
112
+ auto_layout = False
113
+
114
+ if layout_kwargs is None:
115
+ layout_kwargs = {}
116
+
117
+ if scatter_kwargs is None:
118
+ scatter_kwargs = {}
119
+
120
+ try:
121
+ fig = OpenBBFigure()
122
+ except Exception as _:
123
+ fig = OpenBBFigure(create_backend=True)
124
+
125
+ fig.update_layout(ChartStyle().plotly_template.get("layout", {}))
126
+ text_color = "white" if ChartStyle().plt_style == "dark" else "black"
127
+ title = f"{title}" if title else ""
128
+ xtitle = xtitle if xtitle else ""
129
+ y1title = ytitle if ytitle else ""
130
+ y2title = y2title if y2title else ""
131
+ y2 = y2 if y2 else []
132
+ yaxis_num = 1
133
+ yaxis = f"y{yaxis_num}"
134
+ first_y = y[0] # type: ignore[index]
135
+ second_y = None
136
+ third_y = None
137
+ add_scatter = False
138
+
139
+ # Attempt to layout the chart automatically with multiple y-axis.
140
+ mode = scatter_kwargs.pop("mode", "lines")
141
+ hovertemplate = scatter_kwargs.pop("hovertemplate", None)
142
+
143
+ if auto_layout is True:
144
+ # Sort columns by the difference between the max and min values.
145
+ # This is to help determine which columns should share the same y-axis.
146
+ diff = df.max(numeric_only=True) - df.min(numeric_only=True)
147
+ sorted_columns = diff.sort_values(ascending=False).index
148
+ if sorted_columns is None or len(sorted_columns) == 0:
149
+ raise ValueError("Error: expected data with numeric values.")
150
+ df = df[sorted_columns]
151
+
152
+ for i, col in enumerate(df.columns):
153
+
154
+ if col in y: # type: ignore[operator]
155
+ hovertemplate = (
156
+ hovertemplate
157
+ if hovertemplate
158
+ else f"{df[col].name}: %{{y}}<extra></extra>"
159
+ )
160
+ share_yaxis = should_share_axis(df, first_y, col, threshold=2.5)
161
+ if share_yaxis is True:
162
+ add_scatter = True
163
+ if share_yaxis is False:
164
+ yaxis_num = 2
165
+ yaxis = f"y{yaxis_num}"
166
+ if second_y is None:
167
+ second_y = col
168
+ add_scatter = True
169
+ if second_y is not None:
170
+ add_scatter = False
171
+ share_yaxis = should_share_axis(df, col, second_y, threshold=3)
172
+ if share_yaxis is True:
173
+ add_scatter = True
174
+ if share_yaxis is False:
175
+ yaxis_num = 3
176
+ yaxis = f"y{yaxis_num}"
177
+ third_y = col
178
+ add_scatter = True
179
+
180
+ if add_scatter is True:
181
+ fig = fig.add_scatter(
182
+ x=df.index,
183
+ y=df[col],
184
+ name=col,
185
+ mode=mode,
186
+ line=dict(width=1, color=LARGE_CYCLER[i % len(LARGE_CYCLER)]),
187
+ hovertemplate=hovertemplate,
188
+ hoverlabel=dict(font_size=10),
189
+ yaxis=yaxis,
190
+ **scatter_kwargs,
191
+ )
192
+
193
+ if auto_layout is False:
194
+ color = 0
195
+ for i, col in enumerate(y): # type: ignore[arg-type]
196
+ hovertemplate = (
197
+ hovertemplate
198
+ if hovertemplate
199
+ else f"{df[col].name}: %{{y}}<extra></extra>"
200
+ )
201
+ fig = fig.add_scatter(
202
+ x=df.index,
203
+ y=df[col],
204
+ name=col,
205
+ mode=mode,
206
+ line=dict(width=1, color=LARGE_CYCLER[color]),
207
+ hovertemplate=hovertemplate,
208
+ hoverlabel=dict(font_size=10),
209
+ yaxis="y1",
210
+ **scatter_kwargs,
211
+ )
212
+ color += 1
213
+ if y2:
214
+ second_y = y2[0]
215
+ for i, col in enumerate(y2):
216
+ hovertemplate = (
217
+ hovertemplate
218
+ if hovertemplate
219
+ else f"{df[col].name}: %{{y}}<extra></extra>"
220
+ )
221
+ fig = fig.add_scatter(
222
+ x=df.index,
223
+ y=df[col],
224
+ name=col,
225
+ mode=mode,
226
+ line=dict(width=1, color=LARGE_CYCLER[color]),
227
+ hovertemplate=hovertemplate,
228
+ hoverlabel=dict(font_size=10),
229
+ yaxis="y2",
230
+ **scatter_kwargs,
231
+ )
232
+ color += 1
233
+
234
+ if returns is True:
235
+ y1title = "Percent"
236
+ title = f"{title} - Cumulative Returns" if title else "Cumulative Returns"
237
+
238
+ if normalize is True:
239
+ y1title = "Z-Score"
240
+ title = f"{title} - Z-Score" if title else "Z-Score"
241
+
242
+ if not title and target is not None:
243
+ title = f"{target.replace('_', ' ').title()}"
244
+
245
+ fig.update_layout(
246
+ title=dict(text=title if title else None, x=0.5, font=dict(size=16)),
247
+ font=dict(color=text_color),
248
+ paper_bgcolor=(
249
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
250
+ ),
251
+ plot_bgcolor=(
252
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
253
+ ),
254
+ legend=dict(
255
+ orientation="v",
256
+ yanchor="top",
257
+ xanchor="right",
258
+ y=0.95,
259
+ x=-0.01,
260
+ xref="paper",
261
+ font=dict(size=12),
262
+ bgcolor=(
263
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
264
+ ),
265
+ ),
266
+ yaxis=(
267
+ dict(
268
+ ticklen=0,
269
+ side="right",
270
+ title=dict(
271
+ text=y1title if ytitle else None, standoff=30, font=dict(size=16)
272
+ ),
273
+ tickfont=dict(size=14),
274
+ anchor="x",
275
+ showgrid=True,
276
+ mirror=True,
277
+ showline=True,
278
+ zeroline=False,
279
+ gridcolor="rgba(128,128,128,0.25)",
280
+ )
281
+ ),
282
+ yaxis2=(
283
+ dict(
284
+ overlaying="y",
285
+ side="left",
286
+ ticklen=0,
287
+ showgrid=False,
288
+ showline=True,
289
+ zeroline=False,
290
+ mirror=True,
291
+ title=dict(
292
+ text=y2title if y2title else None, standoff=10, font=dict(size=16)
293
+ ),
294
+ tickfont=dict(size=14),
295
+ anchor="x",
296
+ )
297
+ ),
298
+ yaxis3=(
299
+ dict(
300
+ overlaying="y",
301
+ side="left",
302
+ ticklen=0,
303
+ position=0,
304
+ showgrid=False,
305
+ showline=False,
306
+ zeroline=False,
307
+ showticklabels=True,
308
+ mirror=False,
309
+ tickfont=dict(size=12, color="rgba(128,128,128,0.75)"),
310
+ anchor="free",
311
+ )
312
+ ),
313
+ xaxis=dict(
314
+ ticklen=0,
315
+ showgrid=True,
316
+ title=(
317
+ dict(text=xtitle, standoff=30, font=dict(size=16)) if xtitle else None
318
+ ),
319
+ zeroline=False,
320
+ showline=True,
321
+ mirror=True,
322
+ gridcolor="rgba(128,128,128,0.25)",
323
+ domain=[0.095, 0.95] if third_y else None,
324
+ ),
325
+ margin=dict(r=25, l=25) if normalize is False else None,
326
+ autosize=True,
327
+ dragmode="pan",
328
+ hovermode="x",
329
+ )
330
+
331
+ if df.index.name not in ("date", "timestamp"):
332
+ fig.update_xaxes(type="category")
333
+
334
+ if layout_kwargs:
335
+ fig.update_layout(
336
+ **layout_kwargs,
337
+ )
338
+
339
+ return fig
340
+
341
+
342
+ def bar_chart( # noqa: PLR0912
343
+ data: Union[
344
+ list,
345
+ dict,
346
+ "DataFrame",
347
+ List["DataFrame"],
348
+ "Series",
349
+ List["Series"],
350
+ "ndarray",
351
+ Data,
352
+ ],
353
+ x: str,
354
+ y: Union[str, List[str]],
355
+ barmode: Literal["group", "stack", "relative", "overlay"] = "group",
356
+ xtype: Literal["category", "multicategory", "date", "log", "linear"] = "category",
357
+ title: Optional[str] = None,
358
+ xtitle: Optional[str] = None,
359
+ ytitle: Optional[str] = None,
360
+ orientation: Literal["h", "v"] = "v",
361
+ colors: Optional[List[str]] = None,
362
+ bar_kwargs: Optional[Dict[str, Any]] = None,
363
+ layout_kwargs: Optional[Dict[str, Any]] = None,
364
+ **kwargs,
365
+ ) -> Union["OpenBBFigure", "Figure"]:
366
+ """Create a vertical bar chart on a single x-axis with one or more values for the y-axis.
367
+
368
+ Parameters
369
+ ----------
370
+ data : Union[
371
+ list, dict, "DataFrame", List["DataFrame"], "Series", List["Series"], "ndarray", Data
372
+ ]
373
+ Data to plot.
374
+ x : str
375
+ The x-axis column name.
376
+ y : Union[str, List[str]]
377
+ The y-axis column name(s).
378
+ barmode : Literal["group", "stack", "relative", "overlay"], optional
379
+ The bar mode, by default "group".
380
+ xtype : Literal["category", "multicategory", "date", "log", "linear"], optional
381
+ The x-axis type, by default "category".
382
+ title : str, optional
383
+ The title of the chart, by default None.
384
+ xtitle : str, optional
385
+ The x-axis title, by default None.
386
+ ytitle : str, optional
387
+ The y-axis title, by default None.
388
+ colors: List[str], optional
389
+ Manually set the colors to cycle through for each column in 'y', by default None.
390
+ bar_kwargs : Dict[str, Any], optional
391
+ Additional keyword arguments to apply with figure.add_bar(), by default None.
392
+ layout_kwargs : Dict[str, Any], optional
393
+ Additional keyword arguments to apply with figure.update_layout(), by default None.
394
+
395
+ Returns
396
+ -------
397
+ OpenBBFigure
398
+ The OpenBBFigure object.
399
+ """
400
+ # pylint: disable=import-outside-toplevel
401
+ from openbb_charting.core.openbb_figure import OpenBBFigure
402
+
403
+ try:
404
+ figure = OpenBBFigure()
405
+ except Exception as _:
406
+ figure = OpenBBFigure(create_backend=True)
407
+
408
+ figure = figure.create_subplots(
409
+ 1,
410
+ 1,
411
+ shared_xaxes=True,
412
+ vertical_spacing=0.06,
413
+ horizontal_spacing=0.01,
414
+ row_width=[1],
415
+ specs=[[{"secondary_y": True}]],
416
+ )
417
+
418
+ figure.update_layout(ChartStyle().plotly_template.get("layout", {}))
419
+ text_color = "white" if ChartStyle().plt_style == "dark" else "black"
420
+ if colors is not None:
421
+ figure.update_layout(colorway=colors)
422
+ if bar_kwargs is None:
423
+ bar_kwargs = {}
424
+ if isinstance(data, (Data, list, dict)):
425
+ data = basemodel_to_df(convert_to_basemodel(data), index=None)
426
+
427
+ bar_df = data.copy().set_index(x) # type: ignore
428
+ y = y.split(",") if isinstance(y, str) else y
429
+ hovertemplate = bar_kwargs.pop("hovertemplate", None)
430
+ width = bar_kwargs.pop("width", None)
431
+ for item in y:
432
+ figure.add_bar(
433
+ x=bar_df.index if orientation == "v" else bar_df[item],
434
+ y=bar_df[item] if orientation == "v" else bar_df.index,
435
+ name=bar_df[item].name,
436
+ showlegend=len(y) > 1,
437
+ legendgroup=bar_df[item].name,
438
+ orientation=orientation,
439
+ hovertemplate=(
440
+ hovertemplate
441
+ if hovertemplate
442
+ else (
443
+ "%{fullData.name}:%{y}<extra></extra>"
444
+ if orientation == "v"
445
+ else "%{fullData.name}:%{x}<extra></extra>"
446
+ )
447
+ ),
448
+ width=(
449
+ width
450
+ if width
451
+ else 0.95 / len(y) * 0.75 if barmode == "group" and len(y) > 1 else 0.95
452
+ ),
453
+ **bar_kwargs,
454
+ )
455
+
456
+ figure.update_layout(
457
+ title=dict(text=title if title else None, x=0.5, font=dict(size=16)),
458
+ paper_bgcolor=(
459
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
460
+ ),
461
+ plot_bgcolor=(
462
+ "rgba(0,0,0,0)" if text_color == "white" else "rbga(255,255,255,0)"
463
+ ),
464
+ legend=dict(
465
+ orientation="v",
466
+ yanchor="top",
467
+ xanchor="right",
468
+ y=0.95,
469
+ x=-0.01 if orientation == "v" else 1.01,
470
+ xref="paper",
471
+ font=dict(size=12),
472
+ bgcolor=(
473
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
474
+ ),
475
+ ),
476
+ xaxis=dict(
477
+ type=xtype,
478
+ title=dict(
479
+ text=xtitle if xtitle else None, standoff=30, font=dict(size=16)
480
+ ),
481
+ ticklen=0,
482
+ showgrid=orientation == "h",
483
+ tickfont=dict(size=12, family="sans-serif"),
484
+ categoryorder="array" if orientation == "v" else None,
485
+ categoryarray=bar_df.index if orientation == "v" else None,
486
+ ),
487
+ yaxis=dict(
488
+ title=dict(
489
+ text=ytitle if ytitle else None, standoff=30, font=dict(size=16)
490
+ ),
491
+ ticklen=0,
492
+ showgrid=orientation == "v",
493
+ tickfont=dict(size=12),
494
+ side="left" if orientation == "h" else "right",
495
+ categoryorder="array" if orientation == "h" else None,
496
+ categoryarray=bar_df.index if orientation == "h" else None,
497
+ ),
498
+ margin=dict(pad=5),
499
+ barmode=barmode,
500
+ font=dict(color=text_color),
501
+ )
502
+ if orientation == "h":
503
+ figure.update_layout(
504
+ xaxis=dict(
505
+ type="linear",
506
+ showspikes=False,
507
+ ),
508
+ yaxis=dict(
509
+ type="category",
510
+ showspikes=False,
511
+ ),
512
+ hoverlabel=dict(
513
+ font=dict(size=12),
514
+ ),
515
+ hovermode="y unified",
516
+ )
517
+ if layout_kwargs:
518
+ figure.update_layout(
519
+ **layout_kwargs,
520
+ )
521
+ return figure
522
+
523
+
524
+ def bar_increasing_decreasing( # pylint: disable=W0102
525
+ keys: List[str],
526
+ values: List[Union[int, float]],
527
+ title: Optional[str] = None,
528
+ xtitle: Optional[str] = None,
529
+ ytitle: Optional[str] = None,
530
+ colors: List[str] = ["blue", "red"],
531
+ orientation: Literal["h", "v"] = "h",
532
+ barmode: Literal["group", "stack", "relative", "overlay"] = "relative",
533
+ layout_kwargs: Optional[Dict[str, Any]] = None,
534
+ ) -> Union["OpenBBFigure", "Figure"]:
535
+ """Create a bar chart with increasing and decreasing values represented by two colors.
536
+
537
+ Parameters
538
+ ----------
539
+ keys : List[str]
540
+ The x-axis keys.
541
+ values : List[Any]
542
+ The y-axis values.
543
+ title : Optional[str], optional
544
+ The title of the chart, by default None.
545
+ xtitle : Optional[str], optional
546
+ The x-axis title, by default None.
547
+ ytitle : Optional[str], optional
548
+ The y-axis title, by default None.
549
+ colors : List[str], optional
550
+ The colors to use for increasing and decreasing values, by default ["blue", "red"].
551
+ orientation : Literal["h", "v"], optional
552
+ The orientation of the bars, by default "h".
553
+ barmode : Literal["group", "stack", "relative", "overlay"], optional
554
+ The bar mode, by default "relative".
555
+ layout_kwargs : Optional[Dict[str, Any]], optional
556
+ Additional keyword arguments to apply with figure.update_layout(), by default None.
557
+
558
+ Returns
559
+ -------
560
+ OpenBBFigure
561
+ The OpenBBFigure object.
562
+ """
563
+ # pylint: disable=import-outside-toplevel
564
+ from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
565
+ from pandas import Series
566
+
567
+ try:
568
+ figure = OpenBBFigure()
569
+ except Exception as _:
570
+ figure = OpenBBFigure(create_backend=True)
571
+
572
+ figure = figure.create_subplots(
573
+ 1,
574
+ 1,
575
+ shared_xaxes=False,
576
+ vertical_spacing=0.06,
577
+ horizontal_spacing=0.01,
578
+ row_width=[1],
579
+ specs=[[{"secondary_y": True}]],
580
+ )
581
+ figure.update_layout(ChartStyle().plotly_template.get("layout", {}))
582
+ text_color = "white" if ChartStyle().plt_style == "dark" else "black"
583
+
584
+ try:
585
+ data = Series(data=values, index=keys)
586
+ increasing_data = data[data > 0] # type: ignore
587
+ decreasing_data = data[data < 0] # type: ignore
588
+ except Exception as e:
589
+ raise ValueError(f"Error: {e}") from e
590
+
591
+ if not increasing_data.empty:
592
+ figure.add_bar(
593
+ x=increasing_data.index if orientation == "v" else increasing_data,
594
+ y=increasing_data if orientation == "v" else increasing_data.index,
595
+ marker=dict(color=colors[0]),
596
+ orientation=orientation,
597
+ showlegend=False,
598
+ width=0.95 / len(keys) * 0.75 if barmode == "group" else 0.95,
599
+ hoverinfo="y" if orientation == "v" else "x",
600
+ )
601
+ if not decreasing_data.empty:
602
+ figure.add_bar(
603
+ x=decreasing_data.index if orientation == "v" else decreasing_data,
604
+ y=decreasing_data if orientation == "v" else decreasing_data.index,
605
+ marker=dict(color=colors[1]),
606
+ orientation=orientation,
607
+ showlegend=False,
608
+ width=0.95 / len(keys) * 0.75 if barmode == "group" else 0.95,
609
+ hoverinfo="y" if orientation == "v" else "x",
610
+ )
611
+
612
+ figure.update_layout(
613
+ title=dict(text=title if title else None, x=0.5, font=dict(size=20)),
614
+ hovermode="x" if orientation == "v" else "y",
615
+ hoverlabel=dict(align="left" if orientation == "h" else "auto"),
616
+ yaxis=dict(
617
+ title=dict(
618
+ text=ytitle if ytitle else None, standoff=30, font=dict(size=16)
619
+ ),
620
+ side="left" if orientation == "h" else "right",
621
+ showgrid=orientation == "v",
622
+ gridcolor="rgba(128,128,128,0.25)",
623
+ tickfont=dict(size=12),
624
+ ticklen=0,
625
+ categoryorder="array" if orientation == "h" else None,
626
+ categoryarray=keys if orientation == "h" else None,
627
+ ),
628
+ xaxis=dict(
629
+ title=dict(
630
+ text=xtitle if xtitle else None, standoff=30, font=dict(size=16)
631
+ ),
632
+ showgrid=orientation == "h",
633
+ gridcolor="rgba(128,128,128,0.25)",
634
+ tickfont=dict(size=12),
635
+ ticklen=0,
636
+ categoryorder="array" if orientation == "v" else None,
637
+ categoryarray=keys if orientation == "v" else None,
638
+ ),
639
+ paper_bgcolor=(
640
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
641
+ ),
642
+ plot_bgcolor=(
643
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
644
+ ),
645
+ font=dict(color="white" if text_color == "white" else "black"),
646
+ margin=dict(pad=5),
647
+ )
648
+
649
+ if layout_kwargs:
650
+ figure.update_layout(
651
+ **layout_kwargs,
652
+ )
653
+
654
+ return figure
openbb_platform/obbject_extensions/charting/openbb_charting/charts/helpers.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper functions for charting."""
2
+
3
+ # pylint: disable=R0917
4
+
5
+ from typing import TYPE_CHECKING, Callable, Dict, List, Type
6
+
7
+ if TYPE_CHECKING:
8
+ from pandas import DataFrame, Series
9
+
10
+
11
+ def get_charting_functions(view: Type) -> Dict[str, Callable]:
12
+ """Discover charting functions."""
13
+ # pylint: disable=import-outside-toplevel
14
+ from inspect import getmembers, getsource, isfunction
15
+
16
+ implemented_functions: Dict[str, Callable] = {}
17
+
18
+ for name, obj in getmembers(view, isfunction):
19
+ if (
20
+ obj.__module__ == view.__module__
21
+ and not name.startswith("_")
22
+ and "NotImplementedError" not in getsource(obj)
23
+ ):
24
+ implemented_functions[name] = obj
25
+
26
+ return implemented_functions
27
+
28
+
29
+ def get_charting_functions_list(view: Type) -> List[str]:
30
+ """Get a list of all the charting functions."""
31
+ return list(get_charting_functions(view).keys())
32
+
33
+
34
+ def z_score_standardization(data: "Series") -> "Series":
35
+ """Z-Score Standardization Method."""
36
+ return (data - data.mean()) / data.std()
37
+
38
+
39
+ def calculate_returns(data: "Series") -> "Series":
40
+ """Calculate the returns of a column."""
41
+ return ((1 + data.pct_change(fill_method=None).fillna(0)).cumprod() - 1) * 100
42
+
43
+
44
+ def should_share_axis(
45
+ df: "DataFrame", col1: str, col2: str, threshold: float = 0.15
46
+ ) -> bool:
47
+ """Determine whether two columns should share an axis."""
48
+ # pylint: disable=import-outside-toplevel
49
+ from pandas import Series
50
+
51
+ try:
52
+ if isinstance(df, Series):
53
+ df = df.to_frame()
54
+ range1 = df[col1].max() - df[col1].min()
55
+ range2 = df[col2].max() - df[col2].min()
56
+ # Calculate the ratio of the two ranges
57
+ ratio = max(range1, range2) / min(range1, range2)
58
+ # If the ratio is less than the threshold, the two columns can share an axis
59
+ if ratio == 1:
60
+ return True
61
+ return ratio < threshold
62
+ except Exception:
63
+ return False
64
+
65
+
66
+ def heikin_ashi(data: "DataFrame") -> "DataFrame":
67
+ """Return OHLC data as Heikin Ashi Candles.
68
+
69
+ Parameters
70
+ ----------
71
+ data: DataFrame
72
+ DataFrame containing OHLC data.
73
+
74
+ Returns
75
+ -------
76
+ DataFrame
77
+ DataFrame copy with Heikin Ashi candle calculations.
78
+ """
79
+ # pylint: disable=import-outside-toplevel
80
+ from pandas_ta import candles
81
+
82
+ df = data.copy()
83
+
84
+ check_columns = ["open", "high", "low", "close"]
85
+
86
+ for item in check_columns:
87
+ if item not in df.columns:
88
+ raise ValueError(
89
+ "The expected column labels, "
90
+ f"{check_columns}"
91
+ ", were not found in DataFrame."
92
+ )
93
+
94
+ ha = candles.ha(
95
+ df["open"],
96
+ df["high"],
97
+ df["low"],
98
+ df["close"],
99
+ )
100
+
101
+ for item in check_columns:
102
+ df[item] = ha[f"HA_{item}"]
103
+
104
+ return df
105
+
106
+
107
+ def duration_sorter(durations: list) -> list:
108
+ """Sort durations labeled as month_5, year_5, etc."""
109
+
110
+ def duration_to_months(duration):
111
+ """Convert duration to months."""
112
+ if duration == "long_term":
113
+ return 360
114
+ parts = duration.split("_")
115
+ months = 0
116
+ for i in range(0, len(parts), 2):
117
+ number = int(parts[i + 1])
118
+ if parts[i] == "year":
119
+ number *= 12 # Convert years to months
120
+ months += number
121
+ return months
122
+
123
+ return sorted(durations, key=duration_to_months)
openbb_platform/obbject_extensions/charting/openbb_charting/charts/price_historical.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Price historical charting utility."""
2
+
3
+ # pylint: disable=too-many-branches, too-many-locals, unused-argument
4
+
5
+
6
+ from typing import TYPE_CHECKING, Any, Dict, Tuple
7
+
8
+ from openbb_charting.styles.colors import LARGE_CYCLER
9
+
10
+ if TYPE_CHECKING:
11
+ from openbb_charting.core.openbb_figure import OpenBBFigure
12
+
13
+
14
+ def price_historical( # noqa: PLR0912
15
+ **kwargs,
16
+ ) -> Tuple["OpenBBFigure", Dict[str, Any]]:
17
+ """Equity Price Historical Chart."""
18
+ # pylint: disable=import-outside-toplevel
19
+ from pandas import DataFrame # noqa
20
+ from openbb_core.app.utils import basemodel_to_df # noqa
21
+ from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
22
+ from openbb_charting.core.plotly_ta.ta_class import PlotlyTA # noqa
23
+ from openbb_charting.core.chart_style import ChartStyle # noqa
24
+ from openbb_charting.charts.helpers import ( # noqa
25
+ calculate_returns,
26
+ heikin_ashi,
27
+ should_share_axis,
28
+ z_score_standardization,
29
+ )
30
+
31
+ if "data" in kwargs and isinstance(kwargs["data"], DataFrame):
32
+ data = kwargs["data"]
33
+ elif "data" in kwargs and isinstance(kwargs["data"], list):
34
+ data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "date")) # type: ignore
35
+ else:
36
+ data = basemodel_to_df(
37
+ kwargs["obbject_item"], index=kwargs.get("index", "date") # type: ignore
38
+ )
39
+
40
+ if "date" in data.columns:
41
+ data = data.set_index("date")
42
+
43
+ target = str(kwargs.get("target"))
44
+ normalize = kwargs.get("normalize") is True
45
+ returns = kwargs.get("returns") is True
46
+ same_axis = kwargs.get("same_axis") is True
47
+ text_color = "black" if ChartStyle().plt_style == "light" else "white"
48
+ title = f"{kwargs.get('title')}" if "title" in kwargs else "Historical Prices"
49
+ y1title = ""
50
+ y2title = ""
51
+ candles = True
52
+ multi_symbol = (
53
+ bool(kwargs.get("multi_symbol") is True)
54
+ or (
55
+ "symbol" in data.columns
56
+ and target in data.columns
57
+ and len(data.symbol.unique()) > 1
58
+ )
59
+ or ("target" in kwargs and kwargs.get("target") is not None)
60
+ or "symbol" in data.columns
61
+ or (
62
+ "symbol" not in data.columns
63
+ and bool(data.columns.isin(["open", "high", "low", "close"]).all())
64
+ )
65
+ )
66
+ target = "close" if target is None or target == "None" or target == "" else target
67
+
68
+ if multi_symbol is True:
69
+ if "symbol" not in data.columns and target in data.columns:
70
+ data = data[[target]]
71
+ y1title = target.title()
72
+ if "symbol" in data.columns and target in data.columns:
73
+ data = data.pivot(columns="symbol", values=target)
74
+ y1title = target
75
+ title = f"Historical {target.title()}"
76
+
77
+ indicators = kwargs.get("indicators", {})
78
+ candles = bool(~data.columns.isin(["open", "high", "low", "close"]).all())
79
+ candles = candles if kwargs.get("candles", True) else False
80
+ volume = kwargs.get("volume", True) if "volume" in data.columns else False
81
+
82
+ if normalize is True:
83
+ if "symbol" not in data.columns and target in data.columns:
84
+ data = data[[target]]
85
+ multi_symbol = True
86
+ candles = False
87
+ volume = False
88
+
89
+ if returns is True:
90
+ if "symbol" not in data.columns and target in data.columns:
91
+ data = data[[target]]
92
+ multi_symbol = True
93
+ candles = False
94
+ volume = False
95
+ if ( # pylint: disable = R0916
96
+ multi_symbol is False
97
+ and normalize is False
98
+ and returns is False
99
+ and candles is True
100
+ ) or (indicators and multi_symbol is False):
101
+ if (
102
+ "heikin_ashi" in kwargs
103
+ and kwargs["heikin_ashi"] is True
104
+ and candles is True
105
+ ):
106
+ data = heikin_ashi(data)
107
+ title = f"{title} - Heikin Ashi"
108
+ _volume = False
109
+ if "atr" in indicators: # type: ignore
110
+ _volume = volume
111
+ volume = False
112
+ ta = PlotlyTA()
113
+ fig = ta.plot( # type: ignore
114
+ data,
115
+ indicators=indicators if indicators else {}, # type: ignore
116
+ symbol=target if candles is False else "",
117
+ candles=candles,
118
+ volume=volume, # type: ignore
119
+ )
120
+ if _volume is True and "atr" in indicators: # type: ignore
121
+ fig.add_inchart_volume(data)
122
+ fig.update_layout(
123
+ paper_bgcolor=(
124
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
125
+ ),
126
+ plot_bgcolor=(
127
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
128
+ ),
129
+ font=dict(color=text_color),
130
+ showlegend=True,
131
+ legend=dict(
132
+ orientation="v",
133
+ yanchor="top",
134
+ xanchor="right",
135
+ y=0.95,
136
+ x=-0.01,
137
+ xref="paper",
138
+ font=dict(size=12),
139
+ bgcolor=(
140
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
141
+ ),
142
+ ),
143
+ xaxis=dict(
144
+ ticklen=0,
145
+ showgrid=True,
146
+ gridcolor="rgba(128,128,128,0.3)",
147
+ zeroline=True,
148
+ mirror=True,
149
+ showline=True,
150
+ ),
151
+ xaxis2=dict(
152
+ ticklen=0,
153
+ showgrid=True,
154
+ gridcolor="rgba(128,128,128,0.3)",
155
+ zeroline=True,
156
+ mirror=True,
157
+ showline=True,
158
+ ),
159
+ yaxis=dict(
160
+ ticklen=0,
161
+ showgrid=True,
162
+ gridcolor="rgba(128,128,128,0.3)",
163
+ zeroline=True,
164
+ mirror=True,
165
+ showline=True,
166
+ tickfont=dict(size=14),
167
+ ),
168
+ yaxis2=dict(
169
+ ticklen=0,
170
+ gridcolor="rgba(128,128,128,0.3)",
171
+ ),
172
+ yaxis3=dict(
173
+ ticklen=0,
174
+ gridcolor="rgba(128,128,128,0.3)",
175
+ ),
176
+ dragmode="pan",
177
+ hovermode="x",
178
+ )
179
+
180
+ if kwargs.get("title"):
181
+ title = kwargs["title"]
182
+ fig.update_layout(title=dict(text=title, x=0.5))
183
+
184
+ content = fig.to_plotly_json()
185
+
186
+ return fig, content
187
+
188
+ if multi_symbol is True or candles is False:
189
+
190
+ if "symbol" not in data.columns and target in data.columns:
191
+ data = data[[target]]
192
+
193
+ if "symbol" in data.columns:
194
+ data = data.pivot(columns="symbol", values=target)
195
+
196
+ title: str = kwargs.get("title", "Historical Prices") # type: ignore
197
+
198
+ y1title = data.iloc[:, 0].name
199
+ y2title = ""
200
+
201
+ if len(data.columns) > 2 or normalize is True or returns is True:
202
+ if returns is True or (len(data.columns) > 2 and normalize is False):
203
+ data = data.apply(calculate_returns)
204
+ title = f"{title} - Cumulative Returns"
205
+ y1title = "Percent"
206
+ if normalize is True:
207
+ if returns is True:
208
+ title = f"{title.replace(' - Cumulative Returns', '')} - Normalized Cumulative Returns"
209
+ else:
210
+ title = title + " - Normalized"
211
+ data = data.apply(z_score_standardization)
212
+ y1title = None # type: ignore
213
+ y2title = None # type: ignore
214
+
215
+ fig = OpenBBFigure()
216
+ fig.update_layout(ChartStyle().plotly_template.get("layout", {}))
217
+ text_color = "white" if ChartStyle().plt_style == "dark" else "black"
218
+
219
+ for i, col in enumerate(data.columns):
220
+
221
+ hovertemplate = f"{data[col].name}: %{{y}}<extra></extra>"
222
+ yaxis = "y1"
223
+ if y1title and y1title != "Percent":
224
+ yaxis = (
225
+ (
226
+ "y1"
227
+ if should_share_axis(data, col, y1title) # type: ignore
228
+ or col == y1title
229
+ or normalize is True
230
+ or returns is True
231
+ else "y2"
232
+ )
233
+ if same_axis is False
234
+ else "y1"
235
+ )
236
+
237
+ if yaxis == "y2":
238
+ y2title = data[col].name
239
+
240
+ fig.add_scatter(
241
+ x=data.index,
242
+ y=data[col],
243
+ name=data[col].name,
244
+ mode="lines",
245
+ hovertemplate=hovertemplate,
246
+ line=dict(width=2, color=LARGE_CYCLER[i % len(LARGE_CYCLER)]),
247
+ yaxis=yaxis,
248
+ )
249
+
250
+ if normalize is True or returns is True:
251
+ y1title = "Percent" if returns is True else None # type: ignore
252
+ y2title = None # type: ignore
253
+
254
+ if same_axis is True:
255
+ y1title = None # type: ignore
256
+ y2title = None # type: ignore
257
+
258
+ fig.update_layout(
259
+ paper_bgcolor=(
260
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
261
+ ),
262
+ plot_bgcolor=(
263
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
264
+ ),
265
+ legend=(
266
+ dict(
267
+ orientation="v",
268
+ yanchor="top",
269
+ xanchor="right",
270
+ y=0.95,
271
+ x=-0.01,
272
+ bgcolor=(
273
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
274
+ ),
275
+ )
276
+ if len(data.columns) > 2
277
+ else dict(
278
+ orientation="h",
279
+ yanchor="bottom",
280
+ xanchor="right",
281
+ y=1.02,
282
+ x=0.98,
283
+ bgcolor=(
284
+ "rgba(0,0,0,0)" if text_color == "white" else "rgba(255,255,255,0)"
285
+ ),
286
+ )
287
+ ),
288
+ yaxis1=(
289
+ dict(
290
+ side="right",
291
+ ticklen=0,
292
+ showgrid=True,
293
+ showline=True,
294
+ mirror=True,
295
+ gridcolor="rgba(128,128,128,0.3)",
296
+ title=dict(
297
+ text=y1title if y1title else None, standoff=20, font=dict(size=20)
298
+ ),
299
+ tickfont=dict(size=14),
300
+ anchor="x",
301
+ )
302
+ ),
303
+ yaxis2=(
304
+ dict(
305
+ overlaying="y",
306
+ side="left",
307
+ ticklen=0,
308
+ showgrid=False,
309
+ title=dict(
310
+ text=y2title if y2title else None, standoff=10, font=dict(size=20)
311
+ ),
312
+ tickfont=dict(size=14),
313
+ anchor="x",
314
+ )
315
+ if y2title
316
+ else None
317
+ ),
318
+ xaxis=dict(
319
+ ticklen=0,
320
+ showgrid=True,
321
+ gridcolor="rgba(128,128,128,0.3)",
322
+ showline=True,
323
+ mirror=True,
324
+ ),
325
+ margin=dict(l=20, r=20, b=20, t=20),
326
+ dragmode="pan",
327
+ hovermode="x",
328
+ )
329
+ if kwargs.get("title"):
330
+ title = kwargs["title"]
331
+ fig.update_layout(title=dict(text=title, x=0.5))
332
+
333
+ content = fig.show(external=True).to_plotly_json()
334
+
335
+ return fig, content
openbb_platform/obbject_extensions/charting/openbb_charting/charts/price_performance.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Price performance charting implementation."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, Tuple, Union
4
+
5
+ if TYPE_CHECKING:
6
+ from plotly.graph_objs import Figure # noqa
7
+ from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
8
+
9
+
10
+ def price_performance(
11
+ **kwargs,
12
+ ) -> Tuple[Union["OpenBBFigure", "Figure"], Dict[str, Any]]: # noqa: PLR0912
13
+ """Equity Price Performance Chart."""
14
+ # pylint: disable=import-outside-toplevel
15
+ from pandas import DataFrame # noqa
16
+ from openbb_core.app.utils import basemodel_to_df # noqa
17
+ from openbb_charting.charts.generic_charts import bar_chart # noqa
18
+
19
+ if "data" in kwargs and isinstance(kwargs["data"], DataFrame):
20
+ data = kwargs["data"]
21
+ elif "data" in kwargs and isinstance(kwargs["data"], list):
22
+ data = basemodel_to_df(kwargs["data"], index=kwargs.get("index", "symbol")) # type: ignore
23
+ else:
24
+ data = basemodel_to_df(
25
+ kwargs["obbject_item"], index=kwargs.get("index", "symbol") # type: ignore
26
+ )
27
+
28
+ cols = [
29
+ "one_day",
30
+ "one_week",
31
+ "one_month",
32
+ "three_month",
33
+ "six_month",
34
+ "ytd",
35
+ "one_year",
36
+ "two_year",
37
+ "three_year",
38
+ "four_year",
39
+ "five_year",
40
+ ]
41
+
42
+ df = DataFrame()
43
+ chart_df = DataFrame()
44
+
45
+ if "symbol" in data.columns:
46
+ data = data.set_index("symbol")
47
+ chart_cols = []
48
+
49
+ if len(data) == 0:
50
+ raise ValueError("No data was found in the DataFrame.")
51
+
52
+ data = data.drop_duplicates(keep="first")
53
+
54
+ for col in cols:
55
+ if col in data.columns and data[col].notnull().any():
56
+ df[col.replace("_", " ").title() if col != "ytd" else col.upper()] = data[
57
+ col
58
+ ].apply(lambda x: round(x * 100, 4) if x is not None else None)
59
+
60
+ if df.empty:
61
+ raise ValueError(f"No columns matching, {cols}, were found in the data.")
62
+
63
+ chart_df = df.T
64
+ chart_cols = chart_df.columns.to_list()
65
+
66
+ if "limit" in kwargs and isinstance(kwargs.get("limit"), int):
67
+ limit = kwargs.pop("limit", 10)
68
+ chart_df = chart_df.head(limit) # type: ignore
69
+
70
+ layout_kwargs: Dict[str, Any] = kwargs.get("layout_kwargs", {})
71
+
72
+ title = (
73
+ f"{kwargs.pop('title')}" if "title" in kwargs else "Equity Price Performance"
74
+ )
75
+ orientation = (
76
+ kwargs.pop("orientation")
77
+ if "orientation" in kwargs and kwargs.get("orientation") is not None
78
+ else "v"
79
+ )
80
+
81
+ ytitle = "Performance (%)"
82
+ xtitle = None
83
+
84
+ if orientation == "h":
85
+ xtitle = ytitle # type: ignore
86
+ ytitle = None # type: ignore
87
+
88
+ fig = bar_chart(
89
+ chart_df.reset_index(),
90
+ x="index",
91
+ y=chart_cols,
92
+ title=title,
93
+ xtitle=xtitle,
94
+ ytitle=ytitle,
95
+ orientation=orientation, # type: ignore
96
+ )
97
+ fig.update_traces(
98
+ hovertemplate=(
99
+ "%{fullData.name}:%{y:.2f}%<extra></extra>"
100
+ if orientation == "v"
101
+ else "%{fullData.name}:%{x:.2f}%<extra></extra>"
102
+ )
103
+ )
104
+
105
+ fig.update_layout(**layout_kwargs)
106
+ content = fig.show(external=True).to_plotly_json() # type: ignore
107
+
108
+ return fig, content
openbb_platform/obbject_extensions/charting/openbb_charting/charts/relative_rotation.py ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Relative Rotation Chart Helpers."""
2
+
3
+ # pylint: disable=R0917
4
+
5
+ from datetime import date as dateType
6
+ from typing import TYPE_CHECKING, Literal, Optional
7
+ from warnings import warn
8
+
9
+ if TYPE_CHECKING:
10
+ from pandas import DataFrame
11
+ from plotly.graph_objects import Figure
12
+
13
+ color_sequence = [
14
+ "burlywood",
15
+ "orange",
16
+ "grey",
17
+ "magenta",
18
+ "cyan",
19
+ "yellowgreen",
20
+ "#1f77b4",
21
+ "#aec7e8",
22
+ "#ff7f0e",
23
+ "#ffbb78",
24
+ "#d62728",
25
+ "#ff9896",
26
+ "#9467bd",
27
+ "#c5b0d5",
28
+ "#8c564b",
29
+ "#c49c94",
30
+ "#e377c2",
31
+ "#f7b6d2",
32
+ "#7f7f7f",
33
+ "#c7c7c7",
34
+ "#bcbd22",
35
+ "#dbdb8d",
36
+ "#17becf",
37
+ "#9edae5",
38
+ "#7e7e7e",
39
+ "#1b9e77",
40
+ "#d95f02",
41
+ "#7570b3",
42
+ "#e7298a",
43
+ "#66a61e",
44
+ "#e6ab02",
45
+ "#a6761d",
46
+ "#666666",
47
+ "#f0027f",
48
+ "#bf5b17",
49
+ "#d9f202",
50
+ "#8dd3c7",
51
+ "#ffffb3",
52
+ "#bebada",
53
+ "#fb8072",
54
+ "#80b1d3",
55
+ "#fdb462",
56
+ "#b3de69",
57
+ "#fccde5",
58
+ "#d9d9d9",
59
+ "#bc80bd",
60
+ "#ccebc5",
61
+ "#ffed6f",
62
+ "#6a3d9a",
63
+ "#b15928",
64
+ "#b2df8a",
65
+ "#33a02c",
66
+ "#fb9a99",
67
+ "#e31a1c",
68
+ "#fdbf6f",
69
+ "#ff7f00",
70
+ "#cab2d6",
71
+ "#6a3d9a",
72
+ "#ffff99",
73
+ "#b15928",
74
+ ]
75
+
76
+
77
+ def create_rrg_with_tails(
78
+ ratios_data: "DataFrame",
79
+ momentum_data: "DataFrame",
80
+ study: str,
81
+ benchmark_symbol: str,
82
+ tail_periods: int,
83
+ tail_interval: Literal["day", "week", "month"],
84
+ ) -> "Figure":
85
+ """Create The Relative Rotation Graph With Tails.
86
+
87
+ Parameters
88
+ ----------
89
+ ratios_data : DataFrame
90
+ The DataFrame containing the RS-Ratio values.
91
+ momentum_data : DataFrame
92
+ The DataFrame containing the RS-Momentum values.
93
+ study : str
94
+ The study that was selected when loading the raw data.
95
+ If custom data is supplied, this will override the study for the chart titles.
96
+ benchmark_symbol : str
97
+ The symbol of the benchmark.
98
+ tail_periods : int
99
+ The number of periods to display in the tails.
100
+ tail_interval : Literal["day", "week", "month"]
101
+
102
+ Returns
103
+ -------
104
+ Figure
105
+ Plotly GraphObjects Figure.
106
+ """
107
+ # pylint: disable=import-outside-toplevel
108
+ from pandas import to_datetime
109
+ from plotly import graph_objects as go
110
+
111
+ symbols = ratios_data.columns.to_list()
112
+
113
+ tail_dict = {"week": "W", "month": "ME"}
114
+ ratios_data.index = to_datetime(ratios_data.index)
115
+ momentum_data.index = to_datetime(momentum_data.index)
116
+
117
+ if tail_interval != "day":
118
+ ratios_data = ratios_data.resample(tail_dict[tail_interval]).last()
119
+ momentum_data = momentum_data.resample(tail_dict[tail_interval]).last()
120
+ ratios_data = ratios_data.iloc[-tail_periods:]
121
+ momentum_data = momentum_data.iloc[-tail_periods:]
122
+ _tail_periods = len(ratios_data)
123
+ tail_title = (
124
+ f"The Previous {_tail_periods} {tail_interval.capitalize()}s "
125
+ f"Ending {ratios_data.index[-1].strftime('%Y-%m-%d')}"
126
+ )
127
+ x_min = ratios_data.min().min()
128
+ x_max = ratios_data.max().max()
129
+ y_min = momentum_data.min().min()
130
+ y_max = momentum_data.max().max()
131
+ # Create an empty list to store the scatter traces
132
+ frames: list = []
133
+ x_data = ratios_data
134
+ y_data = momentum_data
135
+ for i, date in enumerate(ratios_data.index): # pylint: disable=unused-variable
136
+
137
+ frame_data: list = []
138
+
139
+ for j, symbol in enumerate(symbols):
140
+ x_frame_data = x_data[symbol].iloc[: i + 1]
141
+ y_frame_data = y_data[symbol].iloc[: i + 1]
142
+ name = symbol.upper().replace("^", "").replace(":US", "")
143
+ special_name = "-" in name or len(name) > 7
144
+ marker_size = 34 if special_name else 30
145
+ line_frame_trace = go.Scatter(
146
+ x=x_frame_data,
147
+ y=y_frame_data,
148
+ mode="markers+lines",
149
+ line=dict(color=color_sequence[j], width=2, dash="dash"),
150
+ marker=dict(
151
+ size=5, color=color_sequence[j], line=dict(color="black", width=1)
152
+ ),
153
+ showlegend=False,
154
+ opacity=0.3,
155
+ name=name,
156
+ text=name,
157
+ hovertemplate="<b>%{fullData.name}</b>: "
158
+ + "RS-Ratio: %{x:.4f}, "
159
+ + "RS-Momentum: %{y:.4f}"
160
+ + "<extra></extra>",
161
+ hoverlabel=dict(font_size=10),
162
+ )
163
+
164
+ marker_frame_trace = go.Scatter(
165
+ x=[x_frame_data.iloc[-1]],
166
+ y=[y_frame_data.iloc[-1]],
167
+ mode="markers+text",
168
+ name=name,
169
+ text=name,
170
+ textposition="middle center",
171
+ textfont=(
172
+ dict(size=10, color="black")
173
+ if len(symbol) < 4
174
+ else dict(size=7, color="black")
175
+ ),
176
+ line=dict(color=color_sequence[j], width=2, dash="dash"),
177
+ marker=dict(
178
+ size=marker_size,
179
+ color=color_sequence[j],
180
+ line=dict(color="black", width=1),
181
+ ),
182
+ opacity=0.9,
183
+ showlegend=False,
184
+ hovertemplate="<b>%{fullData.name}</b>: RS-Ratio: %{x:.4f}, RS-Momentum: %{y:.4f}<extra></extra>",
185
+ )
186
+
187
+ frame_data.extend([line_frame_trace, marker_frame_trace])
188
+
189
+ frames.append(go.Frame(data=frame_data, name=f"Frame {i}"))
190
+
191
+ # Define the initial trace for the figure
192
+ initial_trace = frames[0]["data"]
193
+
194
+ padding = 0.1
195
+ y_range = [y_min - padding * abs(y_min) - 0.3, y_max + padding * abs(y_max) + 0.3]
196
+ x_range = [x_min - padding * abs(x_min) - 0.3, x_max + padding * abs(x_max) + 0.3]
197
+
198
+ # Create the layout for the figure
199
+ layout = go.Layout(
200
+ title={
201
+ "text": (
202
+ f"Relative Rotation Against {benchmark_symbol.replace('^', '')} {study.capitalize()} For {tail_title}"
203
+ ),
204
+ "x": 0.5,
205
+ "xanchor": "center",
206
+ "font": dict(size=18),
207
+ },
208
+ xaxis=dict(
209
+ title=dict(text="RS-Ratio", font=dict(size=16)),
210
+ showgrid=True,
211
+ zeroline=True,
212
+ showline=True,
213
+ mirror=True,
214
+ ticklen=0,
215
+ zerolinecolor="black",
216
+ range=x_range,
217
+ gridcolor="lightgrey",
218
+ showspikes=False,
219
+ ),
220
+ yaxis=dict(
221
+ title=dict(text="RS-Momentum", font=dict(size=16)),
222
+ showgrid=True,
223
+ zeroline=True,
224
+ showline=True,
225
+ mirror=True,
226
+ ticklen=0,
227
+ zerolinecolor="black",
228
+ range=y_range,
229
+ gridcolor="lightgrey",
230
+ side="left",
231
+ title_standoff=5,
232
+ ),
233
+ plot_bgcolor="rgba(255,255,255,1)",
234
+ shapes=[
235
+ go.layout.Shape(
236
+ type="rect",
237
+ xref="x",
238
+ yref="y",
239
+ x0=0,
240
+ y0=0,
241
+ x1=x_range[1],
242
+ y1=y_range[1],
243
+ fillcolor="lightgreen",
244
+ opacity=0.3,
245
+ layer="below",
246
+ line_width=0,
247
+ ),
248
+ go.layout.Shape(
249
+ type="rect",
250
+ xref="x",
251
+ yref="y",
252
+ x0=x_range[0],
253
+ y0=0,
254
+ x1=0,
255
+ y1=y_range[1],
256
+ fillcolor="lightblue",
257
+ opacity=0.3,
258
+ layer="below",
259
+ line_width=0,
260
+ ),
261
+ go.layout.Shape(
262
+ type="rect",
263
+ xref="x",
264
+ yref="y",
265
+ x0=x_range[0],
266
+ y0=y_range[0],
267
+ x1=0,
268
+ y1=0,
269
+ fillcolor="lightpink",
270
+ opacity=0.3,
271
+ layer="below",
272
+ line_width=0,
273
+ ),
274
+ go.layout.Shape(
275
+ type="rect",
276
+ xref="x",
277
+ yref="y",
278
+ x0=0,
279
+ y0=y_range[0],
280
+ x1=x_range[1],
281
+ y1=0,
282
+ fillcolor="lightyellow",
283
+ opacity=0.3,
284
+ layer="below",
285
+ line_width=0,
286
+ ),
287
+ go.layout.Shape(
288
+ type="rect",
289
+ xref="x",
290
+ yref="y",
291
+ x0=x_range[0],
292
+ y0=y_range[0],
293
+ x1=x_range[1],
294
+ y1=y_range[1],
295
+ line=dict(
296
+ color="Black",
297
+ width=1,
298
+ ),
299
+ fillcolor="rgba(0,0,0,0)",
300
+ layer="above",
301
+ ),
302
+ ],
303
+ annotations=[
304
+ go.layout.Annotation(
305
+ x=1,
306
+ xref="paper",
307
+ y=1,
308
+ yref="paper",
309
+ text="Leading",
310
+ showarrow=False,
311
+ font=dict(
312
+ size=18,
313
+ color="darkgreen",
314
+ ),
315
+ ),
316
+ go.layout.Annotation(
317
+ x=1,
318
+ xref="paper",
319
+ y=0,
320
+ yref="paper",
321
+ text="Weakening",
322
+ showarrow=False,
323
+ font=dict(
324
+ size=18,
325
+ color="goldenrod",
326
+ ),
327
+ ),
328
+ go.layout.Annotation(
329
+ x=0,
330
+ xref="paper",
331
+ y=0,
332
+ yref="paper",
333
+ text="Lagging",
334
+ showarrow=False,
335
+ font=dict(
336
+ size=18,
337
+ color="red",
338
+ ),
339
+ ),
340
+ go.layout.Annotation(
341
+ x=0,
342
+ xref="paper",
343
+ yref="paper",
344
+ y=1,
345
+ text="Improving",
346
+ showarrow=False,
347
+ font=dict(
348
+ size=18,
349
+ color="blue",
350
+ ),
351
+ ),
352
+ ],
353
+ autosize=True,
354
+ margin=dict(
355
+ l=30,
356
+ r=50,
357
+ b=50,
358
+ t=50,
359
+ pad=0,
360
+ ),
361
+ dragmode="pan",
362
+ hovermode="closest",
363
+ updatemenus=[
364
+ {
365
+ "buttons": [
366
+ {
367
+ "args": [
368
+ None,
369
+ {
370
+ "frame": {"duration": 500, "redraw": False},
371
+ "fromcurrent": True,
372
+ "transition": {"duration": 500, "easing": "linear"},
373
+ },
374
+ ],
375
+ "label": "Play",
376
+ "method": "animate",
377
+ }
378
+ ],
379
+ "direction": "left",
380
+ "pad": {"r": 0, "t": 75},
381
+ "showactive": False,
382
+ "type": "buttons",
383
+ "x": -0.025,
384
+ "xanchor": "left",
385
+ "y": 0,
386
+ "yanchor": "top",
387
+ "bgcolor": "rgba(150, 150, 150, 0.8)",
388
+ "bordercolor": "rgba(100, 100, 100, 0.5)",
389
+ "borderwidth": 1,
390
+ "font": {"color": "black"},
391
+ }
392
+ ],
393
+ sliders=[
394
+ {
395
+ "active": 0,
396
+ "yanchor": "top",
397
+ "xanchor": "center",
398
+ "currentvalue": {
399
+ "font": {"size": 16},
400
+ "prefix": "Date: ",
401
+ "visible": True,
402
+ "xanchor": "right",
403
+ },
404
+ "transition": {"duration": 300, "easing": "cubic-in-out"},
405
+ "pad": {"b": 10, "t": 50},
406
+ "len": 0.9,
407
+ "x": 0.5,
408
+ "y": 0,
409
+ "steps": [
410
+ {
411
+ "label": f"{x_data.index[i].strftime('%Y-%m-%d')}",
412
+ "method": "animate",
413
+ "args": [
414
+ [f"Frame {i}"],
415
+ {
416
+ "mode": "immediate",
417
+ "transition": {"duration": 300},
418
+ "frame": {"duration": 300, "redraw": False},
419
+ },
420
+ ],
421
+ }
422
+ for i in range(len(x_data.index))
423
+ ],
424
+ }
425
+ ],
426
+ )
427
+
428
+ # Create the figure and add the initial trace
429
+ fig = go.Figure(data=initial_trace, layout=layout, frames=frames)
430
+
431
+ return fig
432
+
433
+
434
+ def create_rrg_without_tails(
435
+ ratios_data: "DataFrame",
436
+ momentum_data: "DataFrame",
437
+ benchmark_symbol: str,
438
+ study: str,
439
+ date: Optional[dateType] = None,
440
+ ) -> "Figure":
441
+ """Create the Plotly Figure Object without Tails.
442
+
443
+ Parameters
444
+ ----------
445
+ ratios_data : DataFrame
446
+ The DataFrame containing the RS-Ratio values.
447
+ momentum_data : DataFrame
448
+ The DataFrame containing the RS-Momentum values.
449
+ benchmark_symbol : str
450
+ The symbol of the benchmark.
451
+ study: str
452
+ The study that was selected when loading the raw data.
453
+ If custom data is supplied, this will override the study for the chart titles.
454
+ date : Optional[dateType], optional
455
+ A specific date within the data to target for display, by default None.
456
+
457
+ Returns
458
+ -------
459
+ Figure
460
+ Plotly GraphObjects Figure.
461
+ """
462
+ # pylint: disable=import-outside-toplevel
463
+ from plotly import graph_objects as go # noqa
464
+ from pandas import to_datetime # noqa
465
+
466
+ if date is not None and date not in ratios_data.index.astype(str):
467
+ warn(f"Date {str(date)} not found in data, using the last available date.")
468
+ date = ratios_data.index[-1]
469
+ if date is None:
470
+ date = ratios_data.index[-1]
471
+
472
+ # Select a single row from each dataframe
473
+ row_x = ratios_data.loc[to_datetime(date).date()] # type: ignore
474
+ row_y = momentum_data.loc[to_datetime(date).date()] # type: ignore
475
+
476
+ x_max = row_x.max() + 0.5
477
+ x_min = row_x.min() - 0.5
478
+ y_max = row_y.max() + 0.5
479
+ y_min = row_y.min() - 0.5
480
+
481
+ # Create an empty list to store the scatter traces
482
+ traces = []
483
+
484
+ # Loop through each column in the row_x dataframe
485
+ for i, (column_name, value_x) in enumerate(row_x.items()):
486
+ # Retrieve the corresponding value from the row_y dataframe
487
+ value_y = row_y[column_name] # type: ignore
488
+ marker_name = column_name.upper().replace("^", "").replace(":US", "") # type: ignore
489
+ special_name = "-" in marker_name or len(marker_name) > 5
490
+ marker_size = 38 if special_name else 30
491
+ # Create a scatter trace for each column
492
+ trace = go.Scatter(
493
+ x=[value_x],
494
+ y=[value_y],
495
+ mode="markers+text",
496
+ text=[marker_name],
497
+ textposition="middle center",
498
+ textfont=dict(size=10 if len(marker_name) < 4 else 8, color="black"),
499
+ marker=dict(
500
+ size=marker_size,
501
+ color=color_sequence[i % len(color_sequence)],
502
+ line=dict(color="black", width=1),
503
+ ),
504
+ name=column_name,
505
+ showlegend=False,
506
+ hovertemplate="<b>%{fullData.name}</b>"
507
+ + "<br>RS-Ratio: %{x:.4f}</br>"
508
+ + "RS-Momentum: %{y:.4f}"
509
+ + "<extra></extra>",
510
+ )
511
+ # Add the trace to the list
512
+ traces.append(trace)
513
+
514
+ padding = 0.1
515
+ y_range = [y_min - padding * abs(y_min) - 0.3, y_max + padding * abs(y_max)]
516
+ x_range = [x_min - padding * abs(x_min), x_max + padding * abs(x_max)]
517
+
518
+ layout = go.Layout(
519
+ title={
520
+ "text": (
521
+ f"RS-Ratio vs RS-Momentum of {study.capitalize()} "
522
+ f"Against {benchmark_symbol.replace('^', '')} - {to_datetime(row_x.name).strftime('%Y-%m-%d')}" # type: ignore
523
+ ),
524
+ "x": 0.5,
525
+ "xanchor": "center",
526
+ "font": dict(size=20),
527
+ },
528
+ xaxis=dict(
529
+ title="RS-Ratio",
530
+ zerolinecolor="black",
531
+ range=x_range,
532
+ showspikes=False,
533
+ ),
534
+ yaxis=dict(
535
+ title="<br>RS-Momentum",
536
+ zerolinecolor="black",
537
+ range=y_range,
538
+ side="left",
539
+ title_standoff=5,
540
+ showspikes=False,
541
+ ),
542
+ shapes=[
543
+ go.layout.Shape(
544
+ type="rect",
545
+ xref="x",
546
+ yref="y",
547
+ x0=0,
548
+ y0=0,
549
+ x1=x_range[1],
550
+ y1=y_range[1],
551
+ fillcolor="lightgreen",
552
+ opacity=0.3,
553
+ layer="below",
554
+ line_width=0,
555
+ ),
556
+ go.layout.Shape(
557
+ type="rect",
558
+ xref="x",
559
+ yref="y",
560
+ x0=x_range[0],
561
+ y0=0,
562
+ x1=0,
563
+ y1=y_range[1],
564
+ fillcolor="lightblue",
565
+ opacity=0.3,
566
+ layer="below",
567
+ line_width=0,
568
+ ),
569
+ go.layout.Shape(
570
+ type="rect",
571
+ xref="x",
572
+ yref="y",
573
+ x0=x_range[0],
574
+ y0=y_range[0],
575
+ x1=0,
576
+ y1=0,
577
+ fillcolor="lightpink",
578
+ opacity=0.3,
579
+ layer="below",
580
+ line_width=0,
581
+ ),
582
+ go.layout.Shape(
583
+ type="rect",
584
+ xref="x",
585
+ yref="y",
586
+ x0=0,
587
+ y0=y_range[0],
588
+ x1=x_range[1],
589
+ y1=0,
590
+ fillcolor="lightyellow",
591
+ opacity=0.3,
592
+ layer="below",
593
+ line_width=0,
594
+ ),
595
+ go.layout.Shape(
596
+ type="rect",
597
+ xref="x",
598
+ yref="y",
599
+ x0=x_range[0],
600
+ y0=y_range[0],
601
+ x1=x_range[1],
602
+ y1=y_range[1],
603
+ line=dict(
604
+ color="Black",
605
+ width=1,
606
+ ),
607
+ fillcolor="rgba(0,0,0,0)",
608
+ layer="above",
609
+ ),
610
+ ],
611
+ annotations=[
612
+ go.layout.Annotation(
613
+ x=1,
614
+ xref="paper",
615
+ y=1,
616
+ yref="paper",
617
+ text="Leading",
618
+ showarrow=False,
619
+ font=dict(
620
+ size=18,
621
+ color="darkgreen",
622
+ ),
623
+ ),
624
+ go.layout.Annotation(
625
+ x=1,
626
+ xref="paper",
627
+ y=0,
628
+ yref="paper",
629
+ text="Weakening",
630
+ showarrow=False,
631
+ font=dict(
632
+ size=18,
633
+ color="goldenrod",
634
+ ),
635
+ ),
636
+ go.layout.Annotation(
637
+ x=0,
638
+ xref="paper",
639
+ y=0,
640
+ yref="paper",
641
+ text="Lagging",
642
+ showarrow=False,
643
+ font=dict(
644
+ size=18,
645
+ color="red",
646
+ ),
647
+ ),
648
+ go.layout.Annotation(
649
+ x=0,
650
+ xref="paper",
651
+ yref="paper",
652
+ y=1,
653
+ text="Improving",
654
+ showarrow=False,
655
+ font=dict(
656
+ size=18,
657
+ color="blue",
658
+ ),
659
+ ),
660
+ ],
661
+ autosize=True,
662
+ margin=dict(
663
+ l=30,
664
+ r=50,
665
+ b=50,
666
+ t=50,
667
+ pad=0,
668
+ ),
669
+ dragmode="pan",
670
+ )
671
+
672
+ fig = go.Figure(data=traces, layout=layout)
673
+
674
+ return fig
openbb_platform/obbject_extensions/charting/openbb_charting/core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """OpenBB Charting core."""
openbb_platform/obbject_extensions/charting/openbb_charting/core/assets/Terminal_icon.png ADDED

Git LFS Details

  • SHA256: c35949a0354ad27527099161f9cb97503d6bfdc42769e31eca5e589062bddae3
  • Pointer size: 131 Bytes
  • Size of remote file: 419 kB
openbb_platform/obbject_extensions/charting/openbb_charting/core/assets/plotly-3.0.0.min.js ADDED
The diff for this file is too large to render. See raw diff
 
openbb_platform/obbject_extensions/charting/openbb_charting/core/backend.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Backend for Plotly."""
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Union
5
+
6
+ if TYPE_CHECKING:
7
+ from openbb_core.app.model.charts.charting_settings import ChartingSettings
8
+ from pandas import DataFrame
9
+ from plotly.graph_objs import Figure
10
+
11
+ PLOTS_CORE_PATH = Path(__file__).parent.resolve()
12
+ PLOTLYJS_PATH = PLOTS_CORE_PATH / "assets" / "plotly-3.0.0.min.js"
13
+ BACKEND = None
14
+
15
+ try:
16
+ from pywry import PyWry # pylint: disable=import-outside-toplevel
17
+ except ImportError:
18
+ from .dummy_backend import DummyBackend # pylint: disable=import-outside-toplevel
19
+
20
+ class PyWry(DummyBackend): # type: ignore
21
+ """Dummy backend for charts."""
22
+
23
+
24
+ class Backend(PyWry):
25
+ """Custom backend for Plotly."""
26
+
27
+ def __new__(cls, *args, **kwargs): # pylint: disable=W0613
28
+ """Create a singleton instance of the backend."""
29
+ if not hasattr(cls, "instance"):
30
+ cls.instance = super().__new__(cls) # pylint: disable=E1120
31
+ return cls.instance
32
+
33
+ def __init__(
34
+ self,
35
+ charting_settings: "ChartingSettings",
36
+ daemon: bool = True,
37
+ max_retries: int = 30,
38
+ proc_name: str = "OpenBB Platform",
39
+ ):
40
+ """Create a new instance of the backend."""
41
+ # pylint: disable=import-outside-toplevel
42
+ import atexit # noqa
43
+ import sys # noqa
44
+ from multiprocessing import current_process # noqa
45
+ from packaging import version # noqa
46
+
47
+ self.charting_settings = charting_settings
48
+ has_version = hasattr(PyWry, "__version__")
49
+ init_kwargs: Dict[str, Any] = dict(daemon=daemon, max_retries=max_retries)
50
+
51
+ if has_version and version.parse(PyWry.__version__) >= version.parse("0.4.8"):
52
+ init_kwargs.update(dict(proc_name=proc_name))
53
+
54
+ super().__init__(**init_kwargs)
55
+
56
+ try:
57
+ from IPython import get_ipython # pylint: disable=import-outside-toplevel
58
+
59
+ if "IPKernelApp" not in get_ipython().config:
60
+ raise ImportError("console")
61
+ if (
62
+ "parent_header"
63
+ in get_ipython().kernel._parent_ident # pylint: disable=protected-access
64
+ ):
65
+ raise ImportError("notebook")
66
+ except (ImportError, AttributeError):
67
+ JUPYTER_NOTEBOOK = False
68
+ else:
69
+ JUPYTER_NOTEBOOK = True
70
+
71
+ self.plotly_html: Path = (PLOTS_CORE_PATH / "plotly.html").resolve()
72
+ self.table_html: Path = (PLOTS_CORE_PATH / "table.html").resolve()
73
+ self.isatty = (
74
+ not JUPYTER_NOTEBOOK
75
+ and sys.stdin.isatty()
76
+ and current_process().name == "MainProcess"
77
+ )
78
+ if has_version and PyWry.__version__ == "0.0.0":
79
+ self.isatty = False
80
+
81
+ self.WIDTH, self.HEIGHT = 1400, 762
82
+ self.logged_in: bool = False
83
+
84
+ atexit.register(self.close)
85
+
86
+ def set_window_dimensions(self):
87
+ """Set the window dimensions."""
88
+ width = 1400
89
+ height = 762
90
+
91
+ self.WIDTH, self.HEIGHT = int(width), int(height)
92
+
93
+ def get_pending(self) -> list:
94
+ """Get the pending data that has not been sent to the backend."""
95
+ # pylint: disable=W0201,E0203
96
+ pending = self.outgoing + self.init_engine
97
+ self.outgoing: list = []
98
+ self.init_engine: list = []
99
+ return pending
100
+
101
+ def get_plotly_html(self) -> Path:
102
+ """Get the plotly html file."""
103
+ # pylint: disable=import-outside-toplevel
104
+ import warnings
105
+
106
+ self.set_window_dimensions()
107
+ if self.plotly_html.exists():
108
+ return self.plotly_html
109
+
110
+ warnings.warn(
111
+ "[bold red]plotly.html file not found, check the path:[/]"
112
+ f"[green]{PLOTS_CORE_PATH / 'plotly.html'}[/]"
113
+ )
114
+ self.max_retries = 0 # pylint: disable=W0201
115
+ raise FileNotFoundError
116
+
117
+ def get_table_html(self) -> Path:
118
+ """Get the table html file."""
119
+ # pylint: disable=import-outside-toplevel
120
+ import warnings
121
+
122
+ self.set_window_dimensions()
123
+ if self.table_html.exists():
124
+ return self.table_html
125
+ warnings.warn(
126
+ "[bold red]table.html file not found, check the path:[/]"
127
+ f"[green]{PLOTS_CORE_PATH / 'table.html'}[/]"
128
+ )
129
+ self.max_retries = 0 # pylint: disable=W0201
130
+ raise FileNotFoundError
131
+
132
+ def get_window_icon(self) -> Optional[Path]:
133
+ """Get the window icon."""
134
+ icon_path = PLOTS_CORE_PATH / "assets" / "Terminal_icon.png"
135
+ if icon_path.exists():
136
+ return icon_path
137
+ return None
138
+
139
+ def get_json_update(
140
+ self,
141
+ cmd_loc: Optional[str] = None,
142
+ theme: Optional[str] = None,
143
+ ) -> dict:
144
+ """Get the json update for the backend."""
145
+ if self.charting_settings.user_uuid and not self.logged_in:
146
+ self.logged_in = True
147
+
148
+ return dict(
149
+ theme=theme or self.charting_settings.chart_style,
150
+ log_id=self.charting_settings.app_id,
151
+ pywry_version=self.__version__,
152
+ platform_version=self.charting_settings.version,
153
+ python_version=self.charting_settings.python_version,
154
+ command_location=cmd_loc,
155
+ )
156
+
157
+ def send_figure(
158
+ self,
159
+ fig: "Figure",
160
+ export_image: Optional[Union[Path, str]] = "",
161
+ command_location: Optional[str] = "",
162
+ ):
163
+ """Send a Plotly figure to the backend.
164
+
165
+ Parameters
166
+ ----------
167
+ fig : Figure
168
+ Plotly figure to send to backend.
169
+ export_image : str, optional
170
+ Path to export image to, by default ""
171
+ command_location : str, optional
172
+ Location of the command, by default "".
173
+ We can use the route here to display it on the chart title.
174
+ """
175
+ # pylint: disable=import-outside-toplevel
176
+ import asyncio
177
+ import json
178
+ import re
179
+
180
+ self.check_backend()
181
+ # pylint: disable=C0415
182
+
183
+ paper_bg = (
184
+ "rgba(0,0,0,0)"
185
+ if self.charting_settings.chart_style == "dark"
186
+ else "rgba(255,255,255,0)"
187
+ )
188
+ title = "OpenBB Platform"
189
+ fig.layout.title.text = re.sub(
190
+ r"<[^>]*>", "", fig.layout.title.text if fig.layout.title.text else title
191
+ )
192
+ fig.layout.height += 69
193
+
194
+ export_image = Path(export_image).resolve() if export_image else None
195
+
196
+ json_data = json.loads(fig.to_json())
197
+ json_data.update(self.get_json_update(command_location))
198
+ json_data["layout"]["paper_bgcolor"] = paper_bg
199
+
200
+ outgoing = dict(
201
+ html=self.get_plotly_html(),
202
+ json_data=json_data,
203
+ export_image=export_image,
204
+ **self.get_kwargs(command_location),
205
+ )
206
+ self.send_outgoing(outgoing)
207
+
208
+ if export_image:
209
+ if self.loop.is_closed(): # type: ignore[has-type]
210
+ # Create a new event loop
211
+ self.loop = asyncio.new_event_loop()
212
+ asyncio.set_event_loop(self.loop)
213
+
214
+ self.loop.run_until_complete(self.process_image(export_image))
215
+
216
+ async def process_image(self, export_image: Path):
217
+ """Check if the image has been exported to the path."""
218
+ # pylint: disable=import-outside-toplevel
219
+ import asyncio
220
+ import subprocess
221
+ import sys
222
+
223
+ img_path = export_image.resolve()
224
+
225
+ checks = 0
226
+ while not img_path.exists():
227
+ await asyncio.sleep(0.2)
228
+ checks += 1
229
+ if checks > 50:
230
+ break
231
+
232
+ if img_path.exists(): # noqa: SIM102
233
+ opener = "open" if sys.platform == "darwin" else "xdg-open"
234
+ subprocess.check_call([opener, export_image]) # nosec: B603 # noqa: S603
235
+
236
+ def send_table( # pylint: disable=too-many-positional-arguments
237
+ self,
238
+ df_table: "DataFrame",
239
+ title: str = "",
240
+ source: str = "",
241
+ theme: str = "dark",
242
+ command_location: Optional[str] = "",
243
+ ):
244
+ """Send table data to the backend to be displayed in a table.
245
+
246
+ Parameters
247
+ ----------
248
+ df_table : DataFrame
249
+ Dataframe to send to backend.
250
+ title : str, optional
251
+ Title to display in the window, by default ""
252
+ source : str, optional
253
+ Source of the data, by default ""
254
+ theme : light or dark, optional
255
+ Theme of the table, by default "light"
256
+ """
257
+ # pylint: disable=import-outside-toplevel
258
+ import json
259
+ import re
260
+
261
+ self.check_backend()
262
+
263
+ if title:
264
+ # We remove any html tags and markdown from the title
265
+ title = re.sub(r"<[^>]*>", "", title)
266
+ title = re.sub(r"\[\/?[a-z]+\]", "", title)
267
+
268
+ # we get the length of each column using the max length of the column
269
+ # name and the max length of the column values as the column width
270
+ columnwidth = [
271
+ max(
272
+ len(str(df_table[col].name)),
273
+ df_table[col].astype(str).str.len().max(),
274
+ )
275
+ for col in df_table.columns
276
+ if hasattr(df_table[col], "name") and hasattr(df_table[col], "dtype")
277
+ ]
278
+
279
+ # we add a percentage of max to the min column width
280
+ columnwidth = [
281
+ int(x + (max(columnwidth) - min(columnwidth)) * 0.2) for x in columnwidth
282
+ ]
283
+
284
+ # in case of a very small table we set a min width
285
+ width = max(int(min(sum(columnwidth) * 9.7, self.WIDTH + 100)), 800)
286
+
287
+ json_data = json.loads(df_table.to_json(orient="split", date_format="iso"))
288
+ json_data.update(
289
+ dict(
290
+ title=title,
291
+ source=source or "",
292
+ **self.get_json_update(command_location, theme or "dark"),
293
+ )
294
+ )
295
+
296
+ outgoing = dict(
297
+ html=self.get_table_html(),
298
+ json_data=json.dumps(json_data),
299
+ width=width,
300
+ height=self.HEIGHT - 100,
301
+ **self.get_kwargs(command_location),
302
+ )
303
+ self.send_outgoing(outgoing)
304
+
305
+ def send_url(
306
+ self,
307
+ url: str,
308
+ title: str = "",
309
+ width: Optional[int] = None,
310
+ height: Optional[int] = None,
311
+ ):
312
+ """Send a URL to the backend to be displayed in a window.
313
+
314
+ Parameters
315
+ ----------
316
+ url : str
317
+ URL to display in the window.
318
+ title : str, optional
319
+ Title to display in the window, by default ""
320
+ width : int, optional
321
+ Width of the window, by default 1200
322
+ height : int, optional
323
+ Height of the window, by default 800
324
+ """
325
+ self.check_backend()
326
+ script = f"""
327
+ <script>
328
+ window.location.replace("{url}");
329
+ </script>
330
+ """
331
+ outgoing = dict(
332
+ html=script,
333
+ **self.get_kwargs(title),
334
+ width=width or self.WIDTH,
335
+ height=height or self.HEIGHT,
336
+ )
337
+ self.send_outgoing(outgoing)
338
+
339
+ def get_kwargs(self, title: Optional[str] = "") -> dict:
340
+ """Get the kwargs for the backend."""
341
+ return {
342
+ "title": "OpenBB Platform" + (f" - {title}" if title else ""),
343
+ "icon": self.get_window_icon(),
344
+ "download_path": str(self.charting_settings.user_exports_directory),
345
+ }
346
+
347
+ def start(self, debug: bool = False, headless: bool = False):
348
+ """Start the backend WindowManager process."""
349
+ if self.isatty:
350
+ super().start(debug, headless)
351
+
352
+ def check_backend(self):
353
+ """Override to check if isatty."""
354
+ # pylint: disable=import-outside-toplevel
355
+ import warnings # noqa
356
+ from packaging import version # noqa
357
+
358
+ if not self.isatty:
359
+ return None
360
+
361
+ message = (
362
+ "[bold red]PyWry version 0.5.12 or higher is required to use the "
363
+ "OpenBB Plots backend.[/]\n"
364
+ "[yellow]Please update pywry with 'pip install pywry --upgrade'[/]"
365
+ )
366
+ if not hasattr(PyWry, "__version__"):
367
+ try:
368
+ # pylint: disable=C0415
369
+ from pywry import __version__ as pywry_version
370
+ except ImportError:
371
+ self.max_retries = 0
372
+ return warnings.warn(message)
373
+
374
+ PyWry.__version__ = pywry_version # pylint: disable=W0201
375
+
376
+ if version.parse(PyWry.__version__) < version.parse("0.5.12"):
377
+ self.max_retries = 0 # pylint: disable=W0201
378
+ return warnings.warn(message)
379
+
380
+ if version.parse(PyWry.__version__) > version.parse("0.5.12"):
381
+ return super().check_backend()
382
+
383
+ try:
384
+ return self.loop.run_until_complete(super().check_backend())
385
+ except Exception:
386
+ return None
387
+
388
+ def close(self, reset: bool = False):
389
+ """Close the backend."""
390
+ if reset:
391
+ self.max_retries = 50 # pylint: disable=W0201
392
+
393
+ super().close()
394
+
395
+
396
+ async def download_plotly_js():
397
+ """Download or updates plotly.js to the assets folder."""
398
+ # pylint: disable=import-outside-toplevel
399
+ import aiohttp # noqa
400
+ import warnings # noqa
401
+
402
+ js_filename = PLOTLYJS_PATH.name
403
+ try:
404
+ # we use aiohttp to download plotly.js
405
+ # this is so we don't have to block the main thread
406
+ async with aiohttp.ClientSession(
407
+ connector=aiohttp.TCPConnector(verify_ssl=False), trust_env=True
408
+ ) as session, session.get(f"https://cdn.plot.ly/{js_filename}") as resp:
409
+ with open(str(PLOTLYJS_PATH), "wb") as f:
410
+ while True:
411
+ chunk = await resp.content.read(1024)
412
+ if not chunk:
413
+ break
414
+ f.write(chunk)
415
+
416
+ # We delete the old version of plotly.js
417
+ for file in (PLOTS_CORE_PATH / "assets").glob("plotly*.js"):
418
+ if file.name != js_filename:
419
+ file.unlink(missing_ok=True)
420
+
421
+ except Exception as err: # pylint: disable=W0703
422
+ warnings.warn(f"Error downloading plotly.js: {err}")
423
+
424
+
425
+ def create_backend(charting_settings: Optional["ChartingSettings"] = None):
426
+ """Create the backend."""
427
+ # pylint: disable=import-outside-toplevel
428
+ import importlib
429
+
430
+ charting_module = importlib.import_module(
431
+ "openbb_core.app.model.charts.charting_settings", "charting_settings"
432
+ )
433
+
434
+ ChartingSettings = charting_module.ChartingSettings
435
+ charting_settings = charting_settings or ChartingSettings()
436
+ global BACKEND # pylint: disable=W0603 # noqa
437
+ if BACKEND is None:
438
+ BACKEND = Backend(charting_settings)
439
+
440
+
441
+ def get_backend():
442
+ """Get the backend instance."""
443
+ if BACKEND is None:
444
+ create_backend()
445
+ return BACKEND
openbb_platform/obbject_extensions/charting/openbb_charting/core/chart_style.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Chart and style helpers for Plotly."""
2
+
3
+ # pylint: disable=C0302,R0902,W3301
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import (
8
+ Any,
9
+ Dict,
10
+ List,
11
+ Optional,
12
+ Union,
13
+ )
14
+ from warnings import warn
15
+
16
+ import plotly.graph_objects as go
17
+ import plotly.io as pio
18
+
19
+ from openbb_charting.core.config.openbb_styles import (
20
+ PLT_COLORWAY,
21
+ PLT_DECREASING_COLORWAY,
22
+ PLT_INCREASING_COLORWAY,
23
+ )
24
+
25
+
26
+ class ChartStyle:
27
+ """The class that helps with handling of style configurations.
28
+
29
+ It serves styles for 2 libraries. For `Plotly` this class serves absolute paths
30
+ to the .pltstyle files. For `Plotly` and `Rich` this class serves custom
31
+ styles as python dictionaries.
32
+ """
33
+
34
+ STYLES_REPO = Path(__file__).parent.parent / "styles"
35
+ user_styles_directory: Path = STYLES_REPO
36
+
37
+ plt_styles_available: Dict[str, Path] = {}
38
+ plt_style: str = "dark"
39
+ plotly_template: Dict[str, Any] = {}
40
+ mapbox_style: str = "dark"
41
+
42
+ line_color: str = ""
43
+ up_color: str = ""
44
+ down_color: str = ""
45
+ up_colorway: List[str] = []
46
+ down_colorway: List[str] = []
47
+ up_color_transparent: str = ""
48
+ down_color_transparent: str = ""
49
+
50
+ line_width: float = 1.5
51
+
52
+ initialized: bool = False
53
+
54
+ def __new__(cls, *args, **kwargs): # pylint: disable=W0613
55
+ """Create a singleton."""
56
+ if not hasattr(cls, "instance"):
57
+ cls.instance = super().__new__(cls) # pylint: disable=E1120
58
+ return cls.instance
59
+
60
+ def __init__(
61
+ self,
62
+ plt_style: Optional[str] = "",
63
+ user_styles_directory: Optional[Path] = None,
64
+ ):
65
+ """Initialize the class.
66
+
67
+ Parameters
68
+ ----------
69
+ plt_style : `str`, optional
70
+ The name of the Plotly style to use, by default ""
71
+ console_style : `str`, optional
72
+ The name of the Rich style to use, by default ""
73
+ """
74
+ if self.initialized:
75
+ return
76
+
77
+ self.initialized = True
78
+ self.user_styles_directory = user_styles_directory or self.user_styles_directory
79
+ self.plt_style = plt_style or self.plt_style
80
+ self.load_available_styles()
81
+ self.load_style(plt_style)
82
+ self.apply_style()
83
+
84
+ def apply_style(self, style: Optional[str] = "") -> None:
85
+ """Apply the style to the libraries."""
86
+ style = style or self.plt_style
87
+
88
+ if style != self.plt_style:
89
+ self.load_style(style)
90
+
91
+ style = style.lower().replace("light", "white") # type: ignore
92
+
93
+ if self.plt_style and self.plotly_template:
94
+ self.plotly_template.setdefault("layout", {}).setdefault(
95
+ "mapbox", {}
96
+ ).setdefault("style", "dark")
97
+ if "tables" in self.plt_styles_available:
98
+ tables = self.load_json_style(self.plt_styles_available["tables"])
99
+ pio.templates["openbb_tables"] = go.layout.Template(tables)
100
+ try:
101
+ pio.templates["openbb"] = go.layout.Template(self.plotly_template)
102
+ except ValueError as err:
103
+ if "plotly.graph_objs.Layout: 'legend2'" in str(err):
104
+ warn(
105
+ "[red]Warning: Plotly multiple legends are "
106
+ "not supported in currently installed version.[/]\n\n"
107
+ "[yellow]Please update plotly to version >= 5.15.0[/]\n"
108
+ "[green]pip install plotly --upgrade[/]"
109
+ )
110
+ sys.exit(1)
111
+
112
+ if style in ["dark", "white"]:
113
+ pio.templates.default = f"plotly_{style}+openbb"
114
+ return
115
+
116
+ pio.templates.default = "openbb"
117
+ self.mapbox_style = (
118
+ self.plotly_template.setdefault("layout", {})
119
+ .setdefault("mapbox", {})
120
+ .setdefault("style", "dark")
121
+ )
122
+
123
+ def load_available_styles_from_folder(self, folder: Union[Path, str]) -> None:
124
+ """Load custom styles from folder.
125
+
126
+ Parses the styles/default and styles/user folders and loads style files.
127
+ To be recognized files need to follow a naming convention:
128
+ *.pltstyle - plotly stylesheets
129
+ *.richstyle.json - rich stylesheets
130
+
131
+ Parameters
132
+ ----------
133
+ folder : str
134
+ Path to the folder containing the stylesheets
135
+ """
136
+
137
+ if not isinstance(folder, Path) or not folder.exists():
138
+ return
139
+
140
+ for attr, ext in zip(
141
+ ["plt_styles_available", "console_styles_available"],
142
+ [".pltstyle.json", ".richstyle.json"],
143
+ ):
144
+ for file in folder.rglob(f"*{ext}"):
145
+ getattr(self, attr)[file.name.replace(ext, "")] = file
146
+
147
+ def load_available_styles(self) -> None:
148
+ """Load custom styles from default and user folders."""
149
+ self.load_available_styles_from_folder(self.STYLES_REPO)
150
+ self.load_available_styles_from_folder(self.user_styles_directory)
151
+
152
+ def load_json_style(self, file: Path) -> Dict[str, Any]:
153
+ """Load style from json file.
154
+
155
+ Parameters
156
+ ----------
157
+ file : Path
158
+ Path to the file containing the style
159
+
160
+ Returns
161
+ -------
162
+ Dict[str, Any]
163
+ Style as a dictionary
164
+ """
165
+ with open(file) as f:
166
+ return json.load(f)
167
+
168
+ def load_style(self, style: Optional[str] = "") -> None:
169
+ """Load style from file.
170
+
171
+ Parameters
172
+ ----------
173
+ style : str
174
+ Name of the style to load
175
+ """
176
+ style = style or self.plt_style
177
+
178
+ if style not in self.plt_styles_available:
179
+ warn(
180
+ f"[red]Plot Style {style} not found. Using default style.[/red]",
181
+ )
182
+ style = "dark"
183
+
184
+ self.load_plt_style(style)
185
+
186
+ def load_plt_style(self, style: str) -> None:
187
+ """Load Plotly style from file.
188
+
189
+ Parameters
190
+ ----------
191
+ style : str
192
+ Name of the style to load
193
+ """
194
+ self.plt_style = style
195
+ self.plotly_template = self.load_json_style(self.plt_styles_available[style])
196
+ line = self.plotly_template.pop("line", {})
197
+
198
+ self.up_color = line.get("up_color", "#00ACFF")
199
+ self.down_color = line.get("down_color", "#FF0000")
200
+ self.up_color_transparent = line.get(
201
+ "up_color_transparent", "rgba(0, 170, 255, 0.50)"
202
+ )
203
+ self.down_color_transparent = line.get(
204
+ "down_color_transparent", "rgba(230, 0, 57, 0.50)"
205
+ )
206
+ self.line_color = line.get("color", "#ffed00")
207
+ self.line_width = line.get("width", self.line_width)
208
+ self.down_colorway = line.get("down_colorway", PLT_DECREASING_COLORWAY)
209
+ self.up_colorway = line.get("up_colorway", PLT_INCREASING_COLORWAY)
210
+
211
+ def get_colors(self, reverse: bool = False) -> list:
212
+ """Get colors for the plot.
213
+
214
+ Parameters
215
+ ----------
216
+ reverse : bool, optional
217
+ Whether to reverse the colors, by default False
218
+
219
+ Returns
220
+ -------
221
+ list
222
+ List of colors e.g. ["#00ACFF", "#FF0000"]
223
+ """
224
+ colors = (
225
+ self.plotly_template.get("layout", {}).get("colorway", PLT_COLORWAY).copy()
226
+ )
227
+ if reverse:
228
+ colors.reverse()
229
+ return colors
openbb_platform/obbject_extensions/charting/openbb_charting/core/config/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """OpenBB Charting core configuration."""
openbb_platform/obbject_extensions/charting/openbb_charting/core/config/openbb_styles.py ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenBB Charting Styles."""
2
+
3
+ from typing import TYPE_CHECKING, Any, List, Optional
4
+
5
+ if TYPE_CHECKING:
6
+ from pandas import DataFrame
7
+
8
+ # Vsurf Plot Settings
9
+ PLT_3DMESH_COLORSCALE = "Jet"
10
+ PLT_3DMESH_SCENE = dict(
11
+ xaxis=dict(
12
+ backgroundcolor="rgb(94, 94, 94)",
13
+ gridcolor="white",
14
+ showbackground=True,
15
+ zerolinecolor="white",
16
+ ),
17
+ yaxis=dict(
18
+ backgroundcolor="rgb(94, 94, 94)",
19
+ gridcolor="white",
20
+ showbackground=True,
21
+ zerolinecolor="white",
22
+ ),
23
+ zaxis=dict(
24
+ backgroundcolor="rgb(94, 94, 94)",
25
+ gridcolor="white",
26
+ showbackground=True,
27
+ zerolinecolor="white",
28
+ ),
29
+ aspectratio=dict(x=1.2, y=1.2, z=0.8),
30
+ )
31
+ PLT_3DMESH_HOVERLABEL = dict(bgcolor="gold")
32
+
33
+ # Chart Plots Settings
34
+ PLT_STYLE_TEMPLATE = "plotly_dark"
35
+ PLT_STYLE_INCREASING = "#00ACFF"
36
+ PLT_STYLE_DECREASING = "#e4003a"
37
+ PLT_CANDLESTICKS = dict(
38
+ increasing=dict(line_color=PLT_STYLE_INCREASING, fillcolor=PLT_STYLE_INCREASING),
39
+ decreasing=dict(line_color=PLT_STYLE_DECREASING, fillcolor=PLT_STYLE_DECREASING),
40
+ )
41
+ PLT_STYLE_INCREASING_GREEN = "#00ACFF"
42
+ PLT_STYLE_DECREASING_RED = "#e4003a"
43
+ PLT_FONT = dict(family="Arial", size=16)
44
+ PLOTLY_FONT = dict(family="Arial", size=16)
45
+
46
+ PLT_COLORWAY = [
47
+ "#ffed00",
48
+ "#ef7d00",
49
+ "#e4003a",
50
+ "#c13246",
51
+ "#822661",
52
+ "#48277c",
53
+ "#005ca9",
54
+ "#00aaff",
55
+ "#9b30d9",
56
+ "#af005f",
57
+ "#5f00af",
58
+ "#af87ff",
59
+ ]
60
+
61
+ PLT_FIB_COLORWAY: List[Any] = [
62
+ "rgb(195, 50, 69)", # 0
63
+ "rgb(130, 38, 96)", # 0.235
64
+ "rgb(120, 70, 200)", # 0.382
65
+ "rgb(0, 93, 168)", # 0.5
66
+ "rgb(173, 0, 95)", # 0.618
67
+ "rgb(235, 184, 0)", # 0.65 Golden Pocket
68
+ "rgb(162, 115, 206)", # 1
69
+ dict(family="Arial Black", size=10), # Fib's Text
70
+ dict(color="rgb(0, 230, 195)", width=0.9, dash="dash"), # Fib Trendline
71
+ ]
72
+
73
+ PLT_INCREASING_COLORWAY = [
74
+ "rgba(0, 150, 255, 1)",
75
+ "rgba(0, 170, 255, 0.92)",
76
+ "rgba(0, 170, 255, 0.90)",
77
+ "rgba(0, 170, 255, 0.80)",
78
+ "rgba(0, 170, 255, 0.70)",
79
+ "rgba(0, 170, 255, 0.60)",
80
+ "rgba(0, 170, 255, 0.50)",
81
+ "rgba(0, 170, 255, 0.40)",
82
+ "rgba(0, 170, 255, 0.34)",
83
+ "rgba(0, 170, 255, 0.22)",
84
+ "rgba(0, 170, 255, 0.10)",
85
+ "rgba(0, 170, 255, 0.05)",
86
+ ]
87
+
88
+ PLT_DECREASING_COLORWAY = [
89
+ "rgba(230, 0, 57, 1)",
90
+ "rgba(230, 0, 57, 0.92)",
91
+ "rgba(230, 0, 57, 0.90)",
92
+ "rgba(230, 0, 57, 0.80)",
93
+ "rgba(230, 0, 57, 0.70)",
94
+ "rgba(230, 0, 57, 0.60)",
95
+ "rgba(230, 0, 57, 0.50)",
96
+ "rgba(230, 0, 57, 0.40)",
97
+ "rgba(230, 0, 57, 0.34)",
98
+ "rgba(230, 0, 57, 0.22)",
99
+ "rgba(230, 0, 57, 0.10)",
100
+ "rgba(230, 0, 57, 0.05)",
101
+ ]
102
+
103
+ PLT_INCREASING_COLORWAY_GREEN = [
104
+ "rgba(0, 150, 0, 1)",
105
+ "rgba(0, 150, 0, 0.92)",
106
+ "rgba(0, 150, 0, 0.90)",
107
+ "rgba(0, 150, 0, 0.80)",
108
+ "rgba(0, 150, 0, 0.70)",
109
+ "rgba(0, 150, 0, 0.60)",
110
+ "rgba(0, 150, 0, 0.50)",
111
+ "rgba(0, 150, 0, 0.40)",
112
+ "rgba(0, 150, 0, 0.34)",
113
+ "rgba(0, 150, 0, 0.22)",
114
+ "rgba(0, 150, 0, 0.10)",
115
+ "rgba(0, 150, 0, 0.05)",
116
+ ]
117
+
118
+ PLT_DECREASING_COLORWAY_RED = [
119
+ "rgba(200, 0, 0, 1)",
120
+ "rgba(200, 0, 0, 0.92)",
121
+ "rgba(200, 0, 0, 0.90)",
122
+ "rgba(200, 0, 0, 0.80)",
123
+ "rgba(200, 0, 0, 0.70)",
124
+ "rgba(200, 0, 0, 0.60)",
125
+ "rgba(200, 0, 0, 0.50)",
126
+ "rgba(200, 0, 0, 0.40)",
127
+ "rgba(200, 0, 0, 0.34)",
128
+ "rgba(200, 0, 0, 0.22)",
129
+ "rgba(200, 0, 0, 0.10)",
130
+ "rgba(200, 0, 0, 0.05)",
131
+ ]
132
+
133
+
134
+ # Table Plots Settings
135
+ PLT_TBL_HEADER = dict(
136
+ fill_color="rgb(30, 30, 30)",
137
+ font_color="white",
138
+ line_color="#6e6e6e",
139
+ line_width=1,
140
+ )
141
+ PLT_TBL_CELLS = dict(
142
+ font_color="white",
143
+ line_color="#6e6e6e",
144
+ line_width=0,
145
+ )
146
+ PLT_TBL_ROW_COLORS = (
147
+ "#333333",
148
+ "#242424",
149
+ )
150
+
151
+
152
+ def de_increasing_color_list(
153
+ df_column: Optional["DataFrame"] = None,
154
+ text: Optional[str] = None,
155
+ contains_str: str = "-",
156
+ increasing_color: str = PLT_STYLE_INCREASING,
157
+ decreasing_color: str = PLT_STYLE_DECREASING,
158
+ ) -> List[str]:
159
+ """Make a colorlist for decrease/increase if value in df_column.
160
+
161
+ Contains "{contains_str}" default is "-"
162
+
163
+ Parameters
164
+ ----------
165
+ df_column : DataFrame, optional
166
+ Dataframe column to create colorlist. by default None
167
+ text : str, optional
168
+ Search in a string, by default None
169
+ contains_str : str, optional
170
+ Decreasing String to search for in df_column. The default is "-".
171
+ increasing_color : str, optional
172
+ Color to use for increasing values. The default is PLT_STYLE_INCREASING.
173
+ decreasing_color : str, optional
174
+ Color to use for decreasing values. The default is PLT_STYLE_DECREASING.
175
+
176
+ Returns
177
+ -------
178
+ List[str]
179
+ List of colors for df_column
180
+ """
181
+ if df_column is None:
182
+ colorlist = [decreasing_color if contains_str in text else increasing_color] # type: ignore
183
+ else:
184
+ colorlist = [
185
+ decreasing_color if boolv else increasing_color
186
+ for boolv in df_column.astype(str).str.contains(contains_str)
187
+ ]
188
+ return colorlist
189
+
190
+
191
+ PLOTLY_THEME = dict(
192
+ # Layout
193
+ layout=dict(
194
+ colorway=PLT_COLORWAY,
195
+ font=PLOTLY_FONT,
196
+ yaxis=dict(
197
+ side="right",
198
+ zeroline=True,
199
+ fixedrange=False,
200
+ title_standoff=20,
201
+ nticks=15,
202
+ showline=True,
203
+ showgrid=True,
204
+ ticklen=0,
205
+ ),
206
+ yaxis2=dict(
207
+ side="left",
208
+ zeroline=False,
209
+ fixedrange=False,
210
+ anchor="x",
211
+ layer="above traces",
212
+ overlaying="y2",
213
+ nticks=6,
214
+ tick0=0.5,
215
+ title_standoff=10,
216
+ tickfont=dict(size=12),
217
+ showline=False,
218
+ ticklen=0,
219
+ ),
220
+ yaxis3=dict(
221
+ zeroline=False,
222
+ fixedrange=False,
223
+ anchor="x",
224
+ layer="above traces",
225
+ overlaying="y3",
226
+ nticks=6,
227
+ tick0=0.5,
228
+ title_standoff=10,
229
+ tickfont=dict(size=12),
230
+ showline=True,
231
+ ticklen=0,
232
+ ),
233
+ yaxis4=dict(
234
+ zeroline=False,
235
+ fixedrange=False,
236
+ anchor="x",
237
+ layer="above traces",
238
+ overlaying="y4",
239
+ nticks=6,
240
+ tick0=0.5,
241
+ title_standoff=10,
242
+ tickfont=dict(size=12),
243
+ showline=True,
244
+ ticklen=0,
245
+ ),
246
+ xaxis=dict(
247
+ showgrid=True,
248
+ zeroline=False,
249
+ showline=True,
250
+ rangeslider=dict(visible=False),
251
+ tickfont=dict(size=16),
252
+ title_standoff=20,
253
+ ticklen=0,
254
+ ),
255
+ xaxis2=dict(
256
+ showgrid=True,
257
+ zeroline=False,
258
+ showline=True,
259
+ rangeslider=dict(visible=False),
260
+ tickfont=dict(size=12),
261
+ title_standoff=20,
262
+ ticklen=0,
263
+ ),
264
+ xaxis3=dict(
265
+ showgrid=True,
266
+ zeroline=False,
267
+ showline=True,
268
+ rangeslider=dict(visible=False),
269
+ tickfont=dict(size=12),
270
+ title_standoff=20,
271
+ ticklen=0,
272
+ ),
273
+ xaxis4=dict(
274
+ showgrid=True,
275
+ zeroline=False,
276
+ showline=True,
277
+ rangeslider=dict(visible=False),
278
+ tickfont=dict(size=12),
279
+ title_standoff=20,
280
+ ticklen=0,
281
+ ),
282
+ legend=dict(
283
+ orientation="h",
284
+ yanchor="bottom",
285
+ y=1.02,
286
+ xanchor="right",
287
+ x=0.95,
288
+ font=dict(size=12),
289
+ ),
290
+ dragmode="pan",
291
+ hovermode="x",
292
+ hoverlabel=dict(align="left"),
293
+ ),
294
+ data=dict(
295
+ candlestick=[
296
+ dict(
297
+ increasing=dict(
298
+ line=dict(color=PLT_STYLE_INCREASING),
299
+ fillcolor=PLT_STYLE_INCREASING,
300
+ ),
301
+ decreasing=dict(
302
+ line=dict(color=PLT_STYLE_DECREASING),
303
+ fillcolor=PLT_STYLE_DECREASING,
304
+ ),
305
+ )
306
+ ]
307
+ ),
308
+ )
openbb_platform/obbject_extensions/charting/openbb_charting/core/dummy_backend.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dummy backend for charting to avoid import errors."""
2
+
3
+ import asyncio
4
+ from queue import Queue
5
+ from typing import List
6
+
7
+ import dotenv
8
+ from openbb_core.app.constants import OPENBB_DIRECTORY
9
+
10
+ SETTINGS_ENV_FILE = OPENBB_DIRECTORY / ".env"
11
+
12
+
13
+ class DummyBackend:
14
+ """Dummy class to avoid import errors."""
15
+
16
+ __version__ = "0.0.0"
17
+
18
+ max_retries = 0
19
+ outgoing: List[str] = []
20
+ init_engine: List[str] = []
21
+ daemon = True
22
+ debug = False
23
+ shell = False
24
+ base = None
25
+ recv: Queue = Queue()
26
+
27
+ def __new__(cls, *args, **kwargs): # pylint: disable=W0613
28
+ """Create a singleton instance of the backend."""
29
+ if not hasattr(cls, "instance"):
30
+ cls.instance = super().__new__(cls) # pylint: disable=E1120
31
+ return cls.instance
32
+
33
+ def __init__(self, daemon: bool = True, max_retries: int = 30):
34
+ """Use cummy init to avoid import errors."""
35
+ self.daemon = daemon
36
+ self.max_retries = max_retries
37
+ try:
38
+ self.loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
39
+ except RuntimeError:
40
+ self.loop = asyncio.new_event_loop()
41
+ asyncio.set_event_loop(self.loop)
42
+
43
+ dotenv.set_key(SETTINGS_ENV_FILE, "PLOT_ENABLE_PYWRY", "0")
44
+
45
+ def close(self, reset: bool = False): # pylint: disable=W0613
46
+ """Close the backend."""
47
+
48
+ def start(self, debug: bool = False): # pylint: disable=W0613
49
+ """Start the backend."""
50
+
51
+ def send_outgoing(self, outgoing: dict):
52
+ """Send outgoing data to the backend."""
53
+
54
+ async def check_backend(self):
55
+ """Check backend method to avoid errors and revert to browser."""
56
+ raise Exception
openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py ADDED
@@ -0,0 +1,1662 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """OpenBB Figure Class."""
2
+
3
+ # pylint: disable=C0302,R0902,W3301,R0917
4
+ import json
5
+ import textwrap
6
+ from datetime import datetime, timedelta
7
+ from math import floor
8
+ from pathlib import Path
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Dict,
13
+ List,
14
+ Literal,
15
+ Optional,
16
+ Tuple,
17
+ TypeVar,
18
+ Union,
19
+ )
20
+ from warnings import warn
21
+
22
+ import plotly.graph_objects as go
23
+ import plotly.io as pio
24
+ from plotly.subplots import make_subplots
25
+
26
+ from openbb_charting.core.backend import PLOTLYJS_PATH, create_backend, get_backend
27
+ from openbb_charting.core.config.openbb_styles import (
28
+ PLT_TBL_ROW_COLORS,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from numpy import ndarray
33
+ from openbb_core.app.model.charts.charting_settings import ChartingSettings
34
+ from pandas import DataFrame, Series
35
+
36
+ try: # noqa: SIM105
37
+ # pylint: disable=W0611 # noqa: F401
38
+ from darts import TimeSeries
39
+ except ImportError:
40
+ pass
41
+
42
+ TimeSeriesT = TypeVar("TimeSeriesT", bound="TimeSeries")
43
+
44
+ pio.default_renderers = "notebook"
45
+
46
+
47
+ class OpenBBFigure(go.Figure):
48
+ """Custom Figure class for OpenBB Terminal.
49
+
50
+ Parameters
51
+ ----------
52
+ fig : `go.Figure`, optional
53
+ Figure to copy, by default None
54
+ **kwargs
55
+ Keyword arguments to pass to `go.Figure.update_layout`
56
+
57
+ Attributes
58
+ ----------
59
+ has_subplots : `bool`
60
+ Whether the figure has subplots
61
+
62
+ Class Methods
63
+ -------------
64
+ create_subplots(rows: `int`, cols: `int`, **kwargs) -> `OpenBBFigure`
65
+ Creates a subplots figure
66
+ to_table(data: `DataFrame`, columnwidth: `list`, print_index: `bool`, ...)
67
+ Converts a DataFrame to a table figure
68
+
69
+ Methods
70
+ -------
71
+ add_hline_legend(y: `float`, name: `str`, line: `dict`, legendrank: `int`, **kwargs)
72
+ Adds a horizontal line with a legend label
73
+ add_vline_legend(x: `float`, name: `str`, line: `dict`, legendrank: `int`, **kwargs)
74
+ Adds a vertical line with a legend label
75
+ add_legend_label(trace: `str`, label: `str`, mode: `str`, marker: `dict`, **kwargs)
76
+ Adds a legend label
77
+ add_histplot(x: `list`, name: `str`, colors: `list`, bins: `int`, show_curve: `str`, ...)
78
+ Adds a histogram plot
79
+ horizontal_legend(x: `float`, y: `float`, xanchor: `str`, yanchor: `str`, ...)
80
+ Moves the legend to a horizontal position
81
+ to_subplot(subplot: `OpenBBFigure`, row: `int`, col: `int`, secondary_y: `bool`, ...)
82
+ Returns the figure as a subplot of another figure
83
+ """
84
+
85
+ plotlyjs_path: Path = PLOTLYJS_PATH
86
+
87
+ def __init__(self, fig: Optional[go.Figure] = None, **kwargs) -> None:
88
+ """Initialize the OpenBBFigure."""
89
+ # pylint: disable=import-outside-toplevel
90
+ from openbb_charting.core.chart_style import ChartStyle
91
+
92
+ super().__init__()
93
+ if fig:
94
+ self.__dict__ = fig.__dict__
95
+
96
+ self._charting_settings: Optional[ChartingSettings] = kwargs.pop(
97
+ "charting_settings", None
98
+ )
99
+ self._has_secondary_y = kwargs.pop("has_secondary_y", False)
100
+ self._subplots_kwargs: Dict[str, Any] = kwargs.pop("subplots_kwargs", {})
101
+ self._multi_rows = kwargs.pop("multi_rows", False)
102
+ self._added_logscale = False
103
+ self._date_xaxs: dict = {}
104
+ self._margin_adjusted = False
105
+ self._feature_flags_applied = False
106
+ self._exported = False
107
+ self._cmd_xshift = 0
108
+ self._bar_width = 0.15
109
+ self._export_image: Optional[Union[Path, str]] = ""
110
+ self._subplot_xdates: Dict[int, Dict[int, List[Any]]] = {}
111
+
112
+ if kwargs.pop("create_backend", False):
113
+ create_backend(self._charting_settings)
114
+ get_backend().start(
115
+ debug=getattr(self._charting_settings, "debug_mode", False)
116
+ )
117
+ self._theme = ChartStyle(
118
+ getattr(self._charting_settings, "plt_style", ""),
119
+ getattr(self._charting_settings, "user_styles_directory", None),
120
+ )
121
+
122
+ if xaxis := kwargs.pop("xaxis", None):
123
+ self.update_xaxes(xaxis)
124
+ if yaxis := kwargs.pop("yaxis", None):
125
+ self.update_yaxes(yaxis)
126
+
127
+ self.update_layout(**kwargs)
128
+
129
+ self._backend = get_backend()
130
+
131
+ if self._backend.isatty:
132
+ self.update_layout(
133
+ margin=dict(l=0, r=0, t=0, b=0, pad=0, autoexpand=True),
134
+ height=self._backend.HEIGHT,
135
+ width=self._backend.WIDTH,
136
+ )
137
+
138
+ @property
139
+ def theme(self):
140
+ """Theme property."""
141
+ return self._theme
142
+
143
+ @property
144
+ def subplots_kwargs(self):
145
+ """Get subplots kwargs property."""
146
+ return self._subplots_kwargs
147
+
148
+ @subplots_kwargs.setter
149
+ def subplots_kwargs(self, value):
150
+ """Get subplots kwargs setter."""
151
+ self._subplots_kwargs = value
152
+
153
+ @property
154
+ def has_subplots(self):
155
+ """Has subplots property."""
156
+ return self._has_subplots()
157
+
158
+ @property
159
+ def bar_width(self):
160
+ """Bar width property."""
161
+ return self._bar_width
162
+
163
+ @bar_width.setter
164
+ def bar_width(self, value):
165
+ """Bar width setter."""
166
+ self._bar_width = value
167
+
168
+ @property
169
+ def cmd_xshift(self):
170
+ """Command line x shift property."""
171
+ return self._cmd_xshift
172
+
173
+ @cmd_xshift.setter
174
+ def cmd_xshift(self, value):
175
+ """Command line x shift setter."""
176
+ self._cmd_xshift = value
177
+
178
+ @classmethod
179
+ def create_subplots( # pylint: disable=too-many-arguments
180
+ cls,
181
+ rows: int = 1,
182
+ cols: int = 1,
183
+ shared_xaxes: bool = True,
184
+ vertical_spacing: Optional[float] = None,
185
+ horizontal_spacing: Optional[float] = None,
186
+ subplot_titles: Optional[Union[List[str], tuple]] = None,
187
+ row_width: Optional[List[Union[float, int]]] = None,
188
+ specs: Optional[List[List[Optional[Dict[Any, Any]]]]] = None,
189
+ **kwargs,
190
+ ) -> "OpenBBFigure":
191
+ """Create a new Plotly figure with subplots.
192
+
193
+ Parameters
194
+ ----------
195
+ rows : `int`, optional
196
+ Number of rows, by default 1
197
+ cols : `int`, optional
198
+ Number of columns, by default 1
199
+ shared_xaxes : `bool`, optional
200
+ Whether to share x axes, by default True
201
+ vertical_spacing : `float`, optional
202
+ Vertical spacing between subplots, by default None
203
+ horizontal_spacing : `float`, optional
204
+ Horizontal spacing between subplots, by default None
205
+ subplot_titles : `Union[List[str], tuple]`, optional
206
+ Titles for each subplot, by default None
207
+ row_width : `List[Union[float, int]]`, optional
208
+ Width of each row, by default [1]
209
+ specs : `List[List[dict]]`, optional
210
+ Subplot specs, by default `[[{}] * cols] * rows` (all subplots are the same size)
211
+ """
212
+ # We save the original kwargs to store them in the figure for later use
213
+ subplots_kwargs = dict(
214
+ rows=rows,
215
+ cols=cols,
216
+ shared_xaxes=shared_xaxes,
217
+ vertical_spacing=vertical_spacing,
218
+ horizontal_spacing=horizontal_spacing,
219
+ subplot_titles=subplot_titles,
220
+ row_width=row_width or [1] * rows,
221
+ specs=specs or [[{}] * cols] * rows,
222
+ **kwargs,
223
+ )
224
+
225
+ fig = make_subplots(**subplots_kwargs)
226
+
227
+ kwargs = {
228
+ "multi_rows": rows > 1,
229
+ "subplots_kwargs": subplots_kwargs,
230
+ }
231
+ if specs and any(
232
+ spec.get("secondary_y", False) for row in specs for spec in row if spec
233
+ ):
234
+ kwargs["has_secondary_y"] = True
235
+
236
+ return cls(fig, **kwargs)
237
+
238
+ def add_trend(
239
+ self,
240
+ data: "DataFrame",
241
+ row: int = 1,
242
+ col: int = 1,
243
+ secondary_y: bool = False,
244
+ **kwargs,
245
+ ):
246
+ """Add a trend line to the figure.
247
+
248
+ Parameters
249
+ ----------
250
+ data : `DataFrame`
251
+ Data to plot
252
+ row : `int`, optional
253
+ Row number, by default 1
254
+ col : `int`, optional
255
+ Column number, by default 1
256
+ secondary_y : `bool`, optional
257
+ Whether to plot on secondary y axis, by default None
258
+ """
259
+ try:
260
+ for column, color in zip(
261
+ ["OC_High_trend", "OC_Low_trend"],
262
+ [self._theme.up_color, self._theme.down_color],
263
+ ):
264
+ if column in data.columns:
265
+ name = column.split("_")[1].title()
266
+ trend = data.copy().dropna()
267
+ self.add_shape(
268
+ type="line",
269
+ name=f"{name} Trend",
270
+ x0=trend.index[0],
271
+ y0=trend[column].iloc[0],
272
+ x1=trend.index[-1],
273
+ y1=trend[column].iloc[-1],
274
+ line=dict(color=color, width=2),
275
+ row=row,
276
+ col=col,
277
+ secondary_y=secondary_y,
278
+ **kwargs,
279
+ )
280
+
281
+ except Exception as e:
282
+ raise ValueError(f"Error adding trend line: {e}") from e
283
+
284
+ def add_histplot( # pylint: disable=too-many-arguments,too-many-locals
285
+ self,
286
+ dataset: Union["ndarray", "Series", TimeSeriesT],
287
+ name: Optional[Union[str, List[str]]] = None,
288
+ colors: Optional[List[str]] = None,
289
+ bins: Union[int, str] = 15,
290
+ curve: Literal["normal", "kde"] = "normal",
291
+ show_curve: bool = True,
292
+ show_rug: bool = True,
293
+ show_hist: bool = True,
294
+ forecast: bool = False,
295
+ row: int = 1,
296
+ col: int = 1,
297
+ ) -> None:
298
+ """Add a histogram with a curve and rug plot if desired.
299
+
300
+ Parameters
301
+ ----------
302
+ dataset : `Union[ndarray, Series, TimeSeriesT]`
303
+ Data to plot
304
+ name : `Optional[Union[str, List[str]]]`, optional
305
+ Name of the plot, by default None
306
+ colors : `Optional[List[str]]`, optional
307
+ Colors of the plot, by default None
308
+ bins : `Union[int, str]`, optional
309
+ Number of bins, by default 15
310
+ curve : `Literal["normal", "kde"]`, optional
311
+ Type of curve to plot, by default "normal"
312
+ show_curve : `bool`, optional
313
+ Whether to show the curve, by default True
314
+ show_rug : `bool`, optional
315
+ Whether to show the rug plot, by default True
316
+ show_hist : `bool`, optional
317
+ Whether to show the histogram, by default True
318
+ forecast : `bool`, optional
319
+ Whether the data is a darts forecast TimeSeries, by default False
320
+ row : `int`, optional
321
+ Row of the subplot, by default 1
322
+ col : `int`, optional
323
+ Column of the subplot, by default 1
324
+ """
325
+ # pylint: disable=import-outside-toplevel
326
+ from numpy import linspace, mean, ndarray, std
327
+ from pandas import Series
328
+ from scipy import stats
329
+
330
+ callback = stats.norm if curve == "normal" else stats.gaussian_kde
331
+
332
+ def _validate_x(data: Union[ndarray, Series, type[TimeSeriesT]]):
333
+ if forecast:
334
+ data = data.univariate_values() # type: ignore
335
+ if isinstance(data, Series):
336
+ data = data.to_numpy()
337
+ if isinstance(data, ndarray):
338
+ data = data.tolist()
339
+ if isinstance(data, list):
340
+ data = [data]
341
+
342
+ return data
343
+
344
+ valid_x = _validate_x(dataset)
345
+
346
+ if isinstance(name, str):
347
+ name = [name]
348
+
349
+ if isinstance(colors, str):
350
+ colors = [colors]
351
+ if not name:
352
+ name = [None] * len(valid_x) # type: ignore
353
+ if not colors:
354
+ colors = [None] * len(valid_x) # type: ignore
355
+
356
+ max_y = 0
357
+ for i, (x_i, name_i, color_i) in enumerate(zip(valid_x, name, colors)):
358
+ if not color_i:
359
+ color_i = ( # noqa: PLW2901
360
+ self._theme.up_color if i % 2 == 0 else self._theme.down_color
361
+ )
362
+
363
+ res_mean, res_std = mean(x_i), std(x_i)
364
+ res_min, res_max = min(x_i), max(x_i)
365
+ x = linspace(res_min, res_max, 100)
366
+ if show_hist:
367
+ if forecast:
368
+ components = list(dataset.components[:4]) # type: ignore
369
+ values = (
370
+ dataset[components].all_values(copy=False).flatten(order="F") # type: ignore
371
+ )
372
+ n_components = len(components)
373
+ n_entries = len(values) // n_components
374
+ for i2, label in zip(range(n_components), components):
375
+ self.add_histogram(
376
+ x=values[
377
+ i2 * n_entries : (i2 + 1) * n_entries # noqa: E203
378
+ ], # noqa: E203
379
+ name=label,
380
+ marker_color=color_i,
381
+ nbinsx=bins,
382
+ opacity=0.7,
383
+ row=row,
384
+ col=col,
385
+ )
386
+ else:
387
+ self.add_histogram(
388
+ x=x_i,
389
+ name=name_i,
390
+ marker_color=color_i,
391
+ nbinsx=bins,
392
+ histnorm="probability density",
393
+ histfunc="sum",
394
+ opacity=0.7,
395
+ row=row,
396
+ col=col,
397
+ )
398
+
399
+ if show_rug:
400
+ self.add_scatter(
401
+ x=x_i,
402
+ y=[0.00002] * len(x_i),
403
+ name=name_i if len(name) < 2 else name[1],
404
+ mode="markers",
405
+ marker=dict(
406
+ color=self._theme.down_color,
407
+ symbol="line-ns-open",
408
+ size=10,
409
+ ),
410
+ row=row,
411
+ col=col,
412
+ )
413
+ if show_curve:
414
+ # type: ignore
415
+ if curve == "kde":
416
+ curve_x = [None] * len(valid_x)
417
+ curve_y = [None] * len(valid_x)
418
+ # pylint: disable=consider-using-enumerate
419
+ for index in range(len(valid_x)):
420
+ curve_x[index] = [ # type: ignore
421
+ res_min + xx * (res_max - res_min) / 500
422
+ for xx in range(500)
423
+ ]
424
+ curve_y[index] = stats.gaussian_kde(valid_x[index])(
425
+ curve_x[index]
426
+ )
427
+ for index in range(len(valid_x)):
428
+ self.add_scatter(
429
+ x=curve_x[index], # type: ignore
430
+ y=curve_y[index], # type: ignore
431
+ name=name_i,
432
+ mode="lines",
433
+ showlegend=False,
434
+ marker=dict(color=color_i),
435
+ row=row,
436
+ col=col,
437
+ )
438
+ max_y = max(max_y, max(curve_y[index]) * 1.2) # type: ignore
439
+
440
+ else:
441
+ y = (
442
+ callback(res_mean, res_std).pdf(x)
443
+ * len(valid_x[0])
444
+ * (res_max - res_min)
445
+ / bins
446
+ )
447
+
448
+ self.add_scatter(
449
+ x=x,
450
+ y=y,
451
+ name=name_i,
452
+ mode="lines",
453
+ marker=dict(color=color_i),
454
+ showlegend=False,
455
+ row=row,
456
+ col=col,
457
+ )
458
+
459
+ max_y = max(max_y, y * 2)
460
+
461
+ self.update_yaxes(
462
+ position=0.0,
463
+ range=[0, max_y],
464
+ row=row,
465
+ col=col,
466
+ automargin=False,
467
+ autorange=False,
468
+ )
469
+
470
+ self.update_layout(barmode="overlay", bargap=0.01, bargroupgap=0)
471
+
472
+ def set_title(
473
+ self, title: str, wrap: bool = False, wrap_width: int = 80, **kwargs
474
+ ) -> "OpenBBFigure":
475
+ """Set the main title of the figure.
476
+
477
+ Parameters
478
+ ----------
479
+ title : `str`
480
+ Title of the figure
481
+ wrap : `bool`
482
+ If True, the title will be wrapped according to the wrap_width parameter
483
+ wrap_width : `int`
484
+ Width in characters to wrap the title if wrap is True, default is 80
485
+ """
486
+ if wrap:
487
+ title = "<br>".join(textwrap.wrap(title, width=wrap_width))
488
+
489
+ if kwargs.get("row") is not None and kwargs.get("col") is not None:
490
+ self.add_annotation(
491
+ text=title,
492
+ xref="x domain",
493
+ yref="y domain",
494
+ x=0.5,
495
+ y=1.0,
496
+ xanchor="center",
497
+ yanchor="bottom",
498
+ **kwargs,
499
+ )
500
+ return self
501
+
502
+ self.update_layout(title=dict(text=title, **kwargs))
503
+ return self
504
+
505
+ def set_xaxis_title(
506
+ self,
507
+ title: str,
508
+ row: Optional[int] = None,
509
+ col: Optional[int] = None,
510
+ **kwargs,
511
+ ) -> "OpenBBFigure":
512
+ """Set the x axis title of the figure or subplot (if row and col are specified).
513
+
514
+ Parameters
515
+ ----------
516
+ title : `str`
517
+ Title of the x axis
518
+ row : `int`, optional
519
+ Row number, by default None
520
+ col : `int`, optional
521
+ Column number, by default None
522
+ """
523
+ self.update_xaxes(title=title, row=row, col=col, **kwargs)
524
+ return self
525
+
526
+ def set_yaxis_title(
527
+ self, title: str, row: Optional[int] = None, col: Optional[int] = None, **kwargs
528
+ ) -> "OpenBBFigure":
529
+ """Set the y axis title of the figure or subplot (if row and col are specified).
530
+
531
+ Parameters
532
+ ----------
533
+ title : `str`
534
+ Title of the x axis
535
+ row : `int`, optional
536
+ Row number, by default None
537
+ col : `int`, optional
538
+ Column number, by default None
539
+ """
540
+ self.update_yaxes(title=title, row=row, col=col, **kwargs)
541
+ return self
542
+
543
+ def add_hline_legend(
544
+ self,
545
+ y: float,
546
+ name: str,
547
+ line: Optional[dict] = None,
548
+ legendrank: Optional[int] = None,
549
+ **kwargs,
550
+ ) -> None:
551
+ """Add a horizontal line with a legend label.
552
+
553
+ Parameters
554
+ ----------
555
+ y : `float`
556
+ y value of the line
557
+ name : `str`
558
+ Name of the to display in the legend
559
+ line : `dict`
560
+ Line style
561
+ legendrank : `int`, optional
562
+ Legend rank, by default None (e.g. 1 is above 2)
563
+ """
564
+ if line is None:
565
+ line = {}
566
+
567
+ self.add_hline(
568
+ y,
569
+ line=line,
570
+ )
571
+ self.add_legend_label(
572
+ label=name,
573
+ mode="lines",
574
+ line_dash=line.get("dash", "solid"),
575
+ marker=dict(color=line.get("color", self._theme.line_color)),
576
+ legendrank=legendrank,
577
+ **kwargs,
578
+ )
579
+
580
+ def add_vline_legend(
581
+ self,
582
+ x: float,
583
+ name: str,
584
+ line: Optional[dict] = None,
585
+ legendrank: Optional[int] = None,
586
+ **kwargs,
587
+ ) -> None:
588
+ """Add a vertical line with a legend label.
589
+
590
+ Parameters
591
+ ----------
592
+ x : `float`
593
+ x value of the line
594
+ name : `str`
595
+ Name of the to display in the legend
596
+ line : `dict`
597
+ Line style
598
+ legendrank : `int`, optional
599
+ Legend rank, by default None (e.g. 1 is above 2)
600
+ """
601
+ if line is None:
602
+ line = {}
603
+
604
+ self.add_vline(
605
+ x,
606
+ line=line,
607
+ )
608
+ self.add_legend_label(
609
+ label=name,
610
+ mode="lines",
611
+ line_dash=line.get("dash", "solid"),
612
+ marker=dict(color=line.get("color", self._theme.line_color)),
613
+ legendrank=legendrank,
614
+ **kwargs,
615
+ )
616
+
617
+ def horizontal_legend( # noqa: PLR0913
618
+ self,
619
+ x: float = 1,
620
+ y: float = 1.02,
621
+ xanchor: str = "right",
622
+ yanchor: str = "bottom",
623
+ orientation: str = "h",
624
+ **kwargs,
625
+ ) -> None:
626
+ """Set the legend to be horizontal.
627
+
628
+ Parameters
629
+ ----------
630
+ x : `float`, optional
631
+ The x position of the legend, by default 1
632
+ y : `float`, optional
633
+ The y position of the legend, by default 1.02
634
+ xanchor : `str`, optional
635
+ The x anchor of the legend, by default "right"
636
+ yanchor : `str`, optional
637
+ The y anchor of the legend, by default "bottom"
638
+ orientation : `str`, optional
639
+ The orientation of the legend, by default "h"
640
+ """
641
+ self.update_layout(
642
+ legend=dict(
643
+ x=x,
644
+ y=y,
645
+ xanchor=xanchor,
646
+ yanchor=yanchor,
647
+ orientation=orientation,
648
+ **kwargs,
649
+ )
650
+ )
651
+
652
+ @staticmethod
653
+ def chart_volume_scaling(
654
+ df_volume: "DataFrame", volume_ticks_x: int = 7
655
+ ) -> Dict[str, list]:
656
+ """Take df_volume and returns volume_ticks, tickvals for chart volume scaling.
657
+
658
+ Parameters
659
+ ----------
660
+ df_volume : DataFrame
661
+ Dataframe of volume (e.g. df_volume = df["volume"])
662
+ volume_ticks_x : int, optional
663
+ Number to multiply volume, by default 7
664
+
665
+ Returns
666
+ -------
667
+ Dict[str, list]
668
+ {"range": volume_range, "ticks": tickvals}
669
+ """
670
+ # pylint: disable=import-outside-toplevel
671
+ from pandas import Series, to_numeric
672
+
673
+ df_volume = df_volume.apply(lambda x: f"{x:.1f}")
674
+ df_volume = to_numeric(df_volume.astype(float))
675
+
676
+ if isinstance(df_volume, Series):
677
+ df_volume = df_volume.to_frame()
678
+
679
+ volume_ticks = int(df_volume.max().max())
680
+ round_digits = -3
681
+ first_val = round(volume_ticks * 0.20, round_digits)
682
+
683
+ for x, y in zip([2, 5, 6, 7, 8, 9, 10], [1, 4, 5, 6, 7, 8, 9]):
684
+ if len(str(volume_ticks)) > x:
685
+ round_digits = -y
686
+ first_val = round(volume_ticks * 0.20, round_digits)
687
+
688
+ tickvals = [
689
+ floor(first_val),
690
+ floor(first_val * 2),
691
+ floor(first_val * 3),
692
+ floor(first_val * 4),
693
+ ]
694
+ volume_range = [0, floor(volume_ticks * volume_ticks_x)]
695
+
696
+ return {"range": volume_range, "ticks": tickvals}
697
+
698
+ def add_inchart_volume( # noqa: PLR0913
699
+ self,
700
+ df_stock: "DataFrame",
701
+ close_col: Optional[str] = "close",
702
+ volume_col: Optional[str] = "volume",
703
+ row: Optional[int] = 1,
704
+ col: Optional[int] = 1,
705
+ volume_ticks_x: int = 7,
706
+ ) -> None:
707
+ """Add in-chart volume to a subplot.
708
+
709
+ Parameters
710
+ ----------
711
+ df_stock : `DataFrame`
712
+ Dataframe of the stock
713
+ close_col : `str`, optional
714
+ Name of the close column, by default "close"
715
+ volume_col : `str`, optional
716
+ Name of the volume column, by default "volume"
717
+ row : `int`, optional
718
+ Row number, by default 2
719
+ col : `int`, optional
720
+ Column number, by default 1
721
+ volume_ticks_x : int, optional
722
+ Number to multiply volume, by default 7
723
+ """
724
+ # pylint: disable=import-outside-toplevel
725
+ from numpy import where
726
+
727
+ colors = where(
728
+ df_stock.open < df_stock[close_col],
729
+ self._theme.up_color,
730
+ self._theme.down_color,
731
+ )
732
+ vol_scale = self.chart_volume_scaling(df_stock[volume_col], volume_ticks_x)
733
+ self.add_bar(
734
+ x=df_stock.index,
735
+ y=df_stock[volume_col],
736
+ name="Volume",
737
+ marker_color=colors,
738
+ yaxis="y2",
739
+ row=row,
740
+ col=col,
741
+ opacity=0.5,
742
+ secondary_y=True,
743
+ showlegend=False,
744
+ hovertemplate="%{y}<extra></extra>",
745
+ )
746
+ ticksize = 13 - (self.subplots_kwargs["rows"] // 2)
747
+ self.update_layout(
748
+ yaxis2=dict(
749
+ fixedrange=False,
750
+ side="left",
751
+ nticks=8,
752
+ autorange=False,
753
+ range=vol_scale["range"],
754
+ tickvals=vol_scale["ticks"],
755
+ showgrid=False,
756
+ showline=False,
757
+ zeroline=False,
758
+ tickfont=dict(size=ticksize),
759
+ overlaying="y",
760
+ ),
761
+ yaxis=dict(
762
+ autorange=True,
763
+ automargin=True,
764
+ side="right",
765
+ fixedrange=False,
766
+ anchor="x",
767
+ layer="above traces",
768
+ ),
769
+ )
770
+
771
+ def add_legend_label( # noqa: PLR0913
772
+ self,
773
+ trace: Optional[str] = None,
774
+ label: Optional[str] = None,
775
+ mode: Optional[str] = None,
776
+ marker: Optional[dict] = None,
777
+ line_dash: Optional[str] = None,
778
+ legendrank: Optional[int] = None,
779
+ **kwargs,
780
+ ) -> None:
781
+ """Add a legend label.
782
+
783
+ Parameters
784
+ ----------
785
+ trace : `str`, optional
786
+ The name of the trace to use as a template, by default None
787
+ label : `str`, optional
788
+ The label to use, by default None (uses the trace name if trace is specified)
789
+ If trace is not specified, label must be specified
790
+ mode : `str`, optional
791
+ The mode to use, by default "lines" (uses the trace mode if trace is specified)
792
+ marker : `dict`, optional
793
+ The marker to use, by default dict() (uses the trace marker if trace is specified)
794
+ line_dash : `str`, optional
795
+ The line dash to use, by default "solid" (uses the trace line dash if trace is specified)
796
+ legendrank : `int`, optional
797
+ The legend rank, by default None (e.g. 1 is above 2)
798
+
799
+ Raises
800
+ ------
801
+ ValueError
802
+ If trace is not found
803
+ ValueError
804
+ If label is not specified and trace is not specified
805
+ """
806
+ if trace:
807
+ for trace_ in self.data:
808
+ if trace_.name == trace:
809
+ for arg, default in zip(
810
+ [label, mode, marker, line_dash],
811
+ [trace, trace_.mode, trace_.marker, trace_.line_dash],
812
+ ):
813
+ if not arg and default:
814
+ arg = default # noqa: PLW2901
815
+
816
+ kwargs.update(dict(yaxis=trace_.yaxis))
817
+ break
818
+ else:
819
+ raise ValueError(f"Trace '{trace}' not found")
820
+
821
+ if not label:
822
+ raise ValueError("Label must be specified")
823
+
824
+ self.add_scatter(
825
+ x=[None],
826
+ y=[None],
827
+ mode=mode or "lines",
828
+ name=label,
829
+ marker=marker or dict(),
830
+ line_dash=line_dash or "solid",
831
+ legendrank=legendrank,
832
+ **kwargs,
833
+ )
834
+
835
+ def show( # noqa: PLR0915
836
+ self,
837
+ *args,
838
+ external: bool = False,
839
+ export_image: Optional[Union[Path, str]] = "",
840
+ **kwargs,
841
+ ) -> "OpenBBFigure":
842
+ """Show the figure.
843
+
844
+ Parameters
845
+ ----------
846
+ external : `bool`, optional
847
+ Whether to return the figure object instead of showing it, by default False
848
+ export_image : `Union[Path, str]`, optional
849
+ The path to export the figure image to, by default ""
850
+ cmd_xshift : `int`, optional
851
+ The x shift of the command source annotation, by default 0
852
+ bar_width : `float`, optional
853
+ The width of the bars, by default 0.0001
854
+ date_xaxis : `bool`, optional
855
+ Whether to check if the xaxis is a date axis, by default True
856
+ """
857
+ self.cmd_xshift = kwargs.pop("cmd_xshift", self.cmd_xshift)
858
+ self.bar_width = kwargs.pop("bar_width", self.bar_width)
859
+ self._export_image = export_image
860
+
861
+ if export_image and not self._backend.isatty:
862
+ if isinstance(export_image, str):
863
+ export_image = Path(export_image).resolve()
864
+ export_image.touch()
865
+
866
+ if kwargs.pop("margin", True):
867
+ self._adjust_margins()
868
+
869
+ self._apply_feature_flags()
870
+ if kwargs.pop("date_xaxis", True):
871
+ self.add_rangebreaks()
872
+ self._xaxis_tickformatstops()
873
+
874
+ self.update_traces(marker_line_width=self.bar_width, selector=dict(type="bar"))
875
+ self.update_traces(
876
+ selector=dict(type="scatter", hovertemplate=None),
877
+ hovertemplate="%{y}",
878
+ )
879
+
880
+ # Set modebar style
881
+ if self._backend.isatty:
882
+ self.update_layout( # type: ignore
883
+ newshape_line_color=(
884
+ "gold" if self._theme.mapbox_style == "dark" else "#0d0887"
885
+ ),
886
+ modebar=dict(
887
+ orientation="v",
888
+ bgcolor="#2A2A2A" if self._theme.mapbox_style == "dark" else "gray",
889
+ color="#FFFFFF" if self._theme.mapbox_style == "dark" else "black",
890
+ activecolor=(
891
+ "#d1030d" if self._theme.mapbox_style == "dark" else "blue"
892
+ ),
893
+ ),
894
+ spikedistance=2,
895
+ hoverdistance=2,
896
+ )
897
+
898
+ if external or self._exported:
899
+ return self # type: ignore
900
+
901
+ if getattr(self._charting_settings, "headless", False):
902
+ return self.to_json()
903
+
904
+ kwargs.update(config=dict(scrollZoom=True, displaylogo=False))
905
+ if self._backend.isatty:
906
+ try:
907
+ # We check if we need to export the image
908
+ # This is done to avoid opening after exporting
909
+ if export_image:
910
+ self._exported = True
911
+
912
+ # We send the figure to the backend to be displayed
913
+ return self._backend.send_figure(fig=self, export_image=export_image)
914
+ except Exception as e:
915
+ # If the backend fails, we just show the figure normally
916
+ # This is a very rare case, but it's better to have a fallback
917
+
918
+ warn(f"Failed to show figure with backend. {e}")
919
+
920
+ # We check if any figures were initialized before the backend failed
921
+ # If so, we show them with the default plotly backend
922
+ queue = self._backend.get_pending()
923
+ for pending in queue:
924
+ data = json.loads(pending).get("json_data", {})
925
+ if data.get("layout", {}):
926
+ pio.show(data, *args, **kwargs)
927
+
928
+ return pio.show(self, *args, **kwargs)
929
+
930
+ def _xaxis_tickformatstops(self) -> None:
931
+ """Set the datetickformatstops for the xaxis if the x data is datetime."""
932
+ if (dateindex := self.get_dateindex()) is None or list(
933
+ self.select_xaxes(lambda x: hasattr(x, "tickformat") and x.tickformat)
934
+ ):
935
+ return
936
+
937
+ tickformatstops = [
938
+ dict(dtickrange=[None, 86_400_000], value="%I:%M%p\n%b,%d"),
939
+ dict(dtickrange=[86_400_000, 604_800_000], value="%Y-%m-%d"),
940
+ ]
941
+ xhoverformat = "%I:%M%p %Y-%m-%d"
942
+
943
+ # We check if daily data if the first and second time are the same
944
+ # since daily data will have the same time (2021-01-01 00:00:00)
945
+ if (
946
+ not hasattr(dateindex[-1], "time")
947
+ or dateindex[-1].time() == dateindex[-2].time()
948
+ ):
949
+ xhoverformat = "%Y-%m-%d"
950
+ tickformatstops = [dict(dtickrange=[None, 604_800_000], value="%Y-%m-%d")]
951
+
952
+ for entry in self._date_xaxs.values():
953
+ self.update_xaxes(
954
+ tickformatstops=[
955
+ *tickformatstops,
956
+ dict(dtickrange=[604_800_000, "M1"], value="%Y-%m-%d"),
957
+ dict(dtickrange=["M1", None], value="%Y-%m-%d"),
958
+ ],
959
+ type="date",
960
+ row=entry["row"],
961
+ col=entry["col"],
962
+ tick0=0.5,
963
+ )
964
+ self.update_traces(
965
+ xhoverformat=xhoverformat, selector=dict(name=entry["name"])
966
+ )
967
+
968
+ def get_subplots_dict(self) -> Dict[str, Dict[str, List[Any]]]:
969
+ """Return the subplots dict.
970
+
971
+ Returns
972
+ -------
973
+ `dict`
974
+ The subplots dict
975
+ """
976
+ subplots: Dict[str, Dict[str, List[Any]]] = {}
977
+
978
+ if not self.has_subplots:
979
+ return subplots
980
+
981
+ grid_ref = self._validate_get_grid_ref() # pylint: disable=protected-access
982
+ for r, plot_row in enumerate(grid_ref):
983
+ for c, plot_refs in enumerate(plot_row):
984
+ if not plot_refs:
985
+ continue
986
+ for subplot_ref in plot_refs:
987
+ if subplot_ref.subplot_type == "xy":
988
+ xaxis, yaxis = subplot_ref.layout_keys
989
+ xref = xaxis.replace("axis", "")
990
+ yref = yaxis.replace("axis", "")
991
+ row = r + 1
992
+ col = c + 1
993
+ subplots.setdefault(xref, {}).setdefault(yref, []).append(
994
+ (row, col)
995
+ )
996
+
997
+ return subplots
998
+
999
+ def get_dateindex(self) -> Optional[List[datetime]]:
1000
+ """Return the dateindex of the figure.
1001
+
1002
+ Returns
1003
+ -------
1004
+ `list`
1005
+ The dateindex
1006
+ """
1007
+ # pylint: disable=import-outside-toplevel
1008
+ from numpy import datetime64
1009
+ from pandas import DatetimeIndex, to_datetime
1010
+
1011
+ output: Optional[List[datetime]] = None
1012
+ subplots = self.get_subplots_dict()
1013
+
1014
+ try:
1015
+ false_y = list(self.select_traces(secondary_y=False))
1016
+ true_y = list(self.select_traces(secondary_y=True))
1017
+ except Exception:
1018
+ false_y = []
1019
+ true_y = []
1020
+
1021
+ for trace in self.select_traces():
1022
+ if not hasattr(trace, "xaxis"):
1023
+ continue
1024
+ xref, yref = trace.xaxis, trace.yaxis
1025
+ row, col = subplots.get(xref, {}).get(yref, [(None, None)])[0]
1026
+
1027
+ if trace.x is not None and len(trace.x) > 5:
1028
+ for x in trace.x[:2]:
1029
+ if isinstance(x, (datetime, datetime64, DatetimeIndex)):
1030
+ output = trace.x
1031
+ name = trace.name if hasattr(trace, "name") else f"{trace}"
1032
+
1033
+ secondary_y: Optional[bool] = trace in true_y
1034
+ if trace not in (false_y + true_y):
1035
+ secondary_y = None
1036
+
1037
+ self._date_xaxs[trace.xaxis] = {
1038
+ "yaxis": trace.yaxis,
1039
+ "name": name,
1040
+ "row": row,
1041
+ "col": col,
1042
+ "secondary_y": secondary_y,
1043
+ }
1044
+ self._subplot_xdates.setdefault(row, {}).setdefault(
1045
+ col, []
1046
+ ).append(trace.x)
1047
+
1048
+ # We convert the dateindex to a list of datetime objects if it's a numpy array
1049
+ if output is not None and isinstance(output[0], datetime64):
1050
+ output = (
1051
+ to_datetime(output).to_pydatetime().astype("datetime64[ms]").tolist()
1052
+ )
1053
+
1054
+ return output
1055
+
1056
+ def hide_date_gaps(
1057
+ self,
1058
+ df_data: "DataFrame",
1059
+ row: Optional[int] = None,
1060
+ col: Optional[int] = None,
1061
+ ) -> None:
1062
+ """Add rangebreaks to hide datetime gaps on the xaxis.
1063
+
1064
+ Parameters
1065
+ ----------
1066
+ df_data : `DataFrame`
1067
+ The dataframe with the data.
1068
+ row : `int`, optional
1069
+ The row of the subplot to hide the gaps, by default None
1070
+ col : `int`, optional
1071
+ The column of the subplot to hide the gaps, by default None
1072
+ """
1073
+ # pylint: disable=import-outside-toplevel
1074
+ from pandas import date_range, to_datetime
1075
+
1076
+ # We get the min and max dates
1077
+ dt_start, dt_end = df_data.index.min(), df_data.index.max()
1078
+ rangebreaks: List[Dict[str, Any]] = []
1079
+
1080
+ # if weekly or monthly data, we don't need to hide gaps
1081
+ # this prevents distortions in the plot
1082
+ check_freq = df_data.index.to_series().diff(-5).dt.days.abs().mode().iloc[0]
1083
+ if check_freq > 7:
1084
+ return
1085
+
1086
+ # We get the missing days
1087
+ is_daily = df_data.index[-1].time() == df_data.index[-2].time()
1088
+ dt_days = date_range(start=dt_start, end=dt_end, normalize=True)
1089
+
1090
+ # We get the dates that are missing
1091
+ dt_missing_days = list(
1092
+ set(dt_days.strftime("%Y-%m-%d")) - set(df_data.index.strftime("%Y-%m-%d"))
1093
+ )
1094
+ dt_missing_days = to_datetime(dt_missing_days)
1095
+
1096
+ if len(dt_missing_days) < 2_000:
1097
+ rangebreaks = [dict(values=dt_missing_days)]
1098
+
1099
+ df_data = df_data.sort_index()
1100
+ # We add a rangebreak if the first and second time are not the same
1101
+ # since daily data will have the same time (00:00)
1102
+ if not is_daily:
1103
+ for i in range(len(df_data) - 1):
1104
+ if df_data.index[i + 1] - df_data.index[i] > timedelta(hours=2):
1105
+ rangebreaks.insert(
1106
+ 0,
1107
+ dict(
1108
+ bounds=[
1109
+ df_data.index[i]
1110
+ + timedelta(minutes=60 - df_data.index[i].minute),
1111
+ df_data.index[i + 1],
1112
+ ]
1113
+ ),
1114
+ )
1115
+
1116
+ self.update_xaxes(rangebreaks=rangebreaks, row=row, col=col)
1117
+
1118
+ def add_rangebreaks(self) -> None:
1119
+ """Add rangebreaks to hide datetime gaps on the xaxis."""
1120
+ # pylint: disable=import-outside-toplevel
1121
+ from numpy import concatenate
1122
+ from pandas import DataFrame, to_datetime
1123
+
1124
+ if self.get_dateindex() is None:
1125
+ return
1126
+
1127
+ for row, row_dict in self._subplot_xdates.items():
1128
+ for col, values in row_dict.items():
1129
+ try:
1130
+ x_values = to_datetime(concatenate(values)).to_pydatetime()
1131
+ self.hide_date_gaps(
1132
+ DataFrame(index=x_values.tolist()),
1133
+ row=row,
1134
+ col=col,
1135
+ )
1136
+ except ValueError:
1137
+ continue
1138
+
1139
+ def to_subplot(
1140
+ self,
1141
+ subplot: "OpenBBFigure",
1142
+ row: int,
1143
+ col: int,
1144
+ secondary_y: bool = False,
1145
+ **kwargs,
1146
+ ) -> "OpenBBFigure":
1147
+ """Return the figure as a subplot of another figure.
1148
+
1149
+ Parameters
1150
+ ----------
1151
+ subplot : `plotly.graph_objects.Figure`
1152
+ Figure object created with `OpenBBFigure.create_subplots` / `plotly.subplots.make_subplots`
1153
+ row : `int`
1154
+ Row number
1155
+ col : `int`
1156
+ Column number
1157
+ secondary_y : `bool`, optional
1158
+ Whether to use the secondary y axis, by default False
1159
+
1160
+ Returns
1161
+ -------
1162
+ `plotly.graph_objects.Figure`
1163
+ The subplot with the figure added
1164
+ """
1165
+ for trace in self.data:
1166
+ if kwargs:
1167
+ trace.update(**kwargs)
1168
+
1169
+ subplot.add_trace(trace, row=row, col=col, secondary_y=secondary_y)
1170
+ subplot.set_xaxis_title(self.layout.xaxis.title.text, row=row, col=col)
1171
+ subplot.set_yaxis_title(self.layout.yaxis.title.text, row=row, col=col)
1172
+
1173
+ return subplot
1174
+
1175
+ def to_html(self, *args, **kwargs) -> str:
1176
+ """Return the figure as HTML."""
1177
+ self.update_traces(marker_line_width=0.0001, selector=dict(type="bar"))
1178
+ kwargs.update(
1179
+ dict(
1180
+ config={
1181
+ "scrollZoom": True,
1182
+ "displaylogo": False,
1183
+ "editable": True,
1184
+ "displayModeBar": "hover",
1185
+ },
1186
+ include_plotlyjs=kwargs.pop("include_plotlyjs", False),
1187
+ full_html=False,
1188
+ )
1189
+ )
1190
+ self._apply_feature_flags()
1191
+ self._xaxis_tickformatstops()
1192
+
1193
+ if not self._backend.isatty and self.data[0].type != "table":
1194
+ for key, max_val in zip(["l", "r", "b", "t"], [60, 60, 80, 40]):
1195
+ if key in self.layout.margin and (
1196
+ self.layout.margin[key] is None
1197
+ or (self.layout.margin[key] > max_val)
1198
+ ):
1199
+ self.layout.margin[key] = max_val
1200
+
1201
+ orientation = "v" if self.layout.legend.orientation is None else "h"
1202
+
1203
+ for annotation in self.select_annotations(
1204
+ selector=dict(x=0, xanchor="right")
1205
+ ):
1206
+ annotation.font.size = (
1207
+ annotation.font.size - 1.8 if annotation.font.size else 10
1208
+ )
1209
+
1210
+ for trace in self.select_traces(
1211
+ lambda trace: hasattr(trace, "legend") and trace.legend is not None
1212
+ ):
1213
+ if trace.legend in self.layout:
1214
+ self.layout[trace.legend].font.size = 12
1215
+
1216
+ self.update_layout(
1217
+ legend=dict(orientation=orientation, font=dict(size=12)),
1218
+ font=dict(size=14),
1219
+ )
1220
+ self.update_xaxes(tickfont=dict(size=13))
1221
+ self.update_yaxes(tickfont=dict(size=13))
1222
+
1223
+ return super().to_html(*args, **kwargs)
1224
+
1225
+ def to_plotly_json(self) -> dict:
1226
+ """Serialize, then deserialize, the figure to JSON. Returns as a Python dictionary."""
1227
+ fig = json.loads(self.to_json())
1228
+
1229
+ fig.update(
1230
+ {
1231
+ "config": {
1232
+ "displayModeBar": "hover",
1233
+ "displaylogo": False,
1234
+ "editable": True,
1235
+ "scrollZoom": True,
1236
+ }
1237
+ }
1238
+ )
1239
+ return fig
1240
+
1241
+ @staticmethod
1242
+ def row_colors(data: "DataFrame") -> Optional[List[str]]:
1243
+ """Return the row colors of the table.
1244
+
1245
+ Parameters
1246
+ ----------
1247
+ data : `DataFrame`
1248
+ The dataframe
1249
+
1250
+ Returns
1251
+ -------
1252
+ `list`
1253
+ The list of colors
1254
+ """
1255
+ row_count = len(data)
1256
+ # we determine how many rows in `data` and then create a list with alternating
1257
+ # row colors
1258
+ row_odd_count = floor(row_count / 2) + row_count % 2
1259
+ row_even_count = floor(row_count / 2)
1260
+ odd_list = [PLT_TBL_ROW_COLORS[0]] * row_odd_count
1261
+ even_list = [PLT_TBL_ROW_COLORS[1]] * row_even_count
1262
+ color_list = [x for y in zip(odd_list, even_list) for x in y]
1263
+ if row_odd_count > row_even_count:
1264
+ color_list.append(PLT_TBL_ROW_COLORS[0])
1265
+
1266
+ return color_list
1267
+
1268
+ @staticmethod
1269
+ def _tbl_values(data: "DataFrame", print_index: bool) -> Tuple[List[str], List]:
1270
+ """Return the values of the table.
1271
+
1272
+ Parameters
1273
+ ----------
1274
+ data : `DataFrame`
1275
+ The dataframe to convert
1276
+ print_index : `bool`
1277
+ Whether to print the index
1278
+
1279
+ Returns
1280
+ -------
1281
+ `tuple`
1282
+ The header values and the cell values
1283
+ """
1284
+ if print_index:
1285
+ header_values = list(
1286
+ [data.index.name if data.index.name is not None else "", *data.columns]
1287
+ )
1288
+ cell_values = [data.index, *[data[col] for col in data]]
1289
+
1290
+ else:
1291
+ header_values = data.columns.to_list()
1292
+ cell_values = [data[col] for col in data]
1293
+
1294
+ header_values = [f"<b>{x}</b>" for x in header_values]
1295
+
1296
+ return header_values, cell_values
1297
+
1298
+ @classmethod
1299
+ def to_table(
1300
+ cls,
1301
+ data: "DataFrame",
1302
+ columnwidth: Optional[List[Union[int, float]]] = None,
1303
+ print_index: bool = True,
1304
+ **kwargs,
1305
+ ) -> "OpenBBFigure":
1306
+ """Convert a dataframe to a table figure.
1307
+
1308
+ Parameters
1309
+ ----------
1310
+ data : `DataFrame`
1311
+ The dataframe to convert
1312
+ columnwidth : `list`, optional
1313
+ The width of each column, by default None (auto)
1314
+ print_index : `bool`, optional
1315
+ Whether to print the index, by default True
1316
+ height : `int`, optional
1317
+ The height of the table, by default len(data.index) * 28 + 25
1318
+ width : `int`, optional
1319
+ The width of the table, by default sum(columnwidth) * 8.7
1320
+
1321
+ Returns
1322
+ -------
1323
+ `plotly.graph_objects.Figure`
1324
+ The figure as a table
1325
+ """
1326
+ if not columnwidth:
1327
+ # we get the length of each column using the max length of the column
1328
+ # name and the max length of the column values as the column width
1329
+ columnwidth = [
1330
+ max(len(str(data[col].name)), data[col].astype(str).str.len().max())
1331
+ for col in data.columns
1332
+ ]
1333
+ # we add the length of the index column if we are printing the index
1334
+ if print_index:
1335
+ columnwidth.insert(
1336
+ 0,
1337
+ max(
1338
+ len(str(data.index.name)),
1339
+ data.index.astype(str).str.len().max(),
1340
+ ),
1341
+ )
1342
+
1343
+ # we add a percentage of max to the min column width
1344
+ columnwidth = [
1345
+ int(x + (max(columnwidth) - min(columnwidth)) * 0.2)
1346
+ for x in columnwidth
1347
+ ]
1348
+
1349
+ header_values, cell_values = cls._tbl_values(data, print_index)
1350
+
1351
+ if (height := kwargs.get("height")) and height < len(data.index) * 28 + 25:
1352
+ kwargs.pop("height")
1353
+ if (width := kwargs.get("width")) and width < sum(columnwidth) * 8.7:
1354
+ kwargs.pop("width")
1355
+
1356
+ height = kwargs.pop("height", len(data.index) * 28 + 25)
1357
+ width = kwargs.pop("width", sum(columnwidth) * 8.7)
1358
+
1359
+ fig = OpenBBFigure()
1360
+ fig.add_table(
1361
+ header=dict(values=header_values),
1362
+ cells=dict(
1363
+ values=cell_values,
1364
+ align="left",
1365
+ height=25,
1366
+ ),
1367
+ columnwidth=columnwidth,
1368
+ **kwargs,
1369
+ )
1370
+ fig.update_layout(
1371
+ height=height,
1372
+ width=width,
1373
+ template="openbb_tables",
1374
+ margin=dict(l=0, r=0, b=0, t=0, pad=0),
1375
+ font=dict(size=14),
1376
+ )
1377
+
1378
+ return fig
1379
+
1380
+ def _adjust_margins(self) -> None:
1381
+ """Adjust the margins of the figure."""
1382
+ if self._margin_adjusted:
1383
+ return
1384
+
1385
+ margin_add = (
1386
+ dict(l=80, r=60, b=80, t=40, pad=0)
1387
+ if not self._has_secondary_y or not self.has_subplots
1388
+ else dict(l=60, r=50, b=85, t=40, pad=0)
1389
+ )
1390
+
1391
+ # We adjust margins
1392
+ if self._backend.isatty:
1393
+ for key in ["l", "r", "b", "t", "pad"]:
1394
+ if key in self.layout.margin and self.layout.margin[key] is not None:
1395
+ self.layout.margin[key] += margin_add.get(key, 0)
1396
+ else:
1397
+ self.layout.margin[key] = margin_add.get(key, 0)
1398
+
1399
+ if not self._backend.isatty:
1400
+ org_margin = self.layout.margin
1401
+ margin = dict(l=40, r=60, b=80, t=50)
1402
+ for key, max_val in zip(["l", "r", "b", "t"], [60, 50, 80, 50]):
1403
+ org = org_margin[key] or 0
1404
+ if (org + margin[key]) > max_val:
1405
+ self.layout.margin[key] = max_val
1406
+ else:
1407
+ self.layout.margin[key] = org + margin[key]
1408
+
1409
+ self._margin_adjusted = True
1410
+
1411
+ # pylint: disable=import-outside-toplevel
1412
+ def _add_cmd_source(self, command_location: Optional[str] = "") -> None:
1413
+ """Set the watermark for OpenBB Terminal."""
1414
+ if command_location:
1415
+ yaxis = self.layout.yaxis
1416
+ yaxis2 = self.layout.yaxis2 if hasattr(self.layout, "yaxis2") else None
1417
+ xshift = -70 if yaxis.side == "right" else -80
1418
+
1419
+ if self.layout.margin["l"] > 100:
1420
+ xshift -= 50 if self._added_logscale else 40
1421
+
1422
+ if (
1423
+ yaxis2
1424
+ and (yaxis.title.text and yaxis2.title.text)
1425
+ and (yaxis.side == "left" or yaxis2.side == "left")
1426
+ ):
1427
+ self.layout.margin["l"] += 20
1428
+
1429
+ if (yaxis2 and yaxis2.side == "left") or yaxis.side == "left":
1430
+ title = (
1431
+ yaxis.title.text
1432
+ if not yaxis2 or yaxis2.side != "left"
1433
+ else yaxis2.title.text
1434
+ )
1435
+ xshift = -110 if not title else -135
1436
+ self.layout.margin["l"] += 60
1437
+
1438
+ self.add_annotation(
1439
+ x=0,
1440
+ y=0.5,
1441
+ yref="paper",
1442
+ xref="paper",
1443
+ text=command_location,
1444
+ textangle=-90,
1445
+ font_size=24,
1446
+ font_color="gray" if self._theme.mapbox_style == "dark" else "black",
1447
+ opacity=0.5,
1448
+ yanchor="middle",
1449
+ xanchor="left",
1450
+ xshift=xshift + self.cmd_xshift,
1451
+ )
1452
+
1453
+ # pylint: disable=import-outside-toplevel
1454
+ def _apply_feature_flags(self) -> None:
1455
+ """Apply watermark and command source annotations."""
1456
+ if self._feature_flags_applied:
1457
+ return
1458
+
1459
+ self._add_cmd_source()
1460
+
1461
+ self._feature_flags_applied = True
1462
+
1463
+ def add_logscale_menus(self, yaxis: str = "yaxis") -> None:
1464
+ """Set the menus for the figure."""
1465
+ self._added_logscale = True
1466
+ bg_color = "#000000" if self._theme.mapbox_style == "dark" else "#FFFFFF" # type: ignore
1467
+ font_color = "#FFFFFF" if self._theme.mapbox_style == "dark" else "#000000" # type: ignore
1468
+ self.update_layout(
1469
+ xaxis=dict(
1470
+ rangeslider=dict(visible=False),
1471
+ rangeselector=dict(
1472
+ bgcolor=bg_color,
1473
+ font=dict(color=font_color),
1474
+ buttons=list(
1475
+ [
1476
+ dict(
1477
+ count=1,
1478
+ label="1M",
1479
+ step="month",
1480
+ stepmode="backward",
1481
+ ),
1482
+ dict(
1483
+ count=3,
1484
+ label="3M",
1485
+ step="month",
1486
+ stepmode="backward",
1487
+ ),
1488
+ dict(count=1, label="YTD", step="year", stepmode="todate"),
1489
+ dict(
1490
+ count=1,
1491
+ label="1y",
1492
+ step="year",
1493
+ stepmode="backward",
1494
+ ),
1495
+ dict(step="all"),
1496
+ ]
1497
+ ),
1498
+ ),
1499
+ ),
1500
+ bargap=0,
1501
+ bargroupgap=0,
1502
+ )
1503
+
1504
+ self.update_layout(
1505
+ updatemenus=[
1506
+ dict(
1507
+ bgcolor=bg_color,
1508
+ font=dict(color=font_color, size=14),
1509
+ buttons=[
1510
+ dict(
1511
+ label="linear ",
1512
+ method="relayout",
1513
+ args=[{f"{yaxis}.type": "linear"}],
1514
+ ),
1515
+ dict(
1516
+ label="log",
1517
+ method="relayout",
1518
+ args=[{f"{yaxis}.type": "log"}],
1519
+ ),
1520
+ ],
1521
+ y=1.07,
1522
+ x=-0.01,
1523
+ )
1524
+ ],
1525
+ )
1526
+
1527
+ def add_corr_plot( # pylint: disable=too-many-arguments
1528
+ self,
1529
+ series: "DataFrame",
1530
+ max_lag: int = 20,
1531
+ m: Optional[int] = None,
1532
+ alpha: Optional[float] = 0.05,
1533
+ marker: Optional[dict] = None,
1534
+ row: Optional[int] = None,
1535
+ col: Optional[int] = None,
1536
+ pacf: bool = False,
1537
+ **kwargs,
1538
+ ) -> None:
1539
+ """Add a correlation plot to a figure object.
1540
+
1541
+ Parameters
1542
+ ----------
1543
+ fig : OpenBBFigure
1544
+ Figure object to add plot to
1545
+ series : DataFrame
1546
+ Dataframe to look at
1547
+ max_lag : int, optional
1548
+ Number of lags to look at, by default 15
1549
+ m: Optional[int]
1550
+ Optionally, a time lag to highlight on the plot. Default is none.
1551
+ alpha: Optional[float]
1552
+ Optionally, a significance level to highlight on the plot. Default is 0.05.
1553
+ row : int, optional
1554
+ Row to add plot to, by default None
1555
+ col : int, optional
1556
+ Column to add plot to, by default None
1557
+ pacf : bool, optional
1558
+ Flag to indicate whether to use partial autocorrelation or not, by default False
1559
+ """
1560
+ # pylint: disable=import-outside-toplevel
1561
+ import statsmodels.api as sm # noqa
1562
+ from numpy import arange, asanyarray, ceil, log10, isscalar # noqa
1563
+
1564
+ mode = "markers+lines" if marker else "lines"
1565
+ line = kwargs.pop("line", None)
1566
+
1567
+ def _prepare_data_corr_plot(x, lags):
1568
+ zero = True
1569
+ irregular = False
1570
+ if lags is None:
1571
+ # GH 4663 - use a sensible default value
1572
+ nobs = x.shape[0]
1573
+ lim = min(int(ceil(10 * log10(nobs))), nobs - 1)
1574
+ lags = arange(not zero, lim + 1)
1575
+ elif isscalar(lags):
1576
+ lags = arange(
1577
+ not zero, int(lags) + 1 # type: ignore
1578
+ ) # +1 for zero lag
1579
+ else:
1580
+ irregular = True
1581
+ lags = asanyarray(lags).astype(int)
1582
+ nlags = lags.max(0)
1583
+
1584
+ return lags, nlags, irregular
1585
+
1586
+ lags, nlags, irregular = _prepare_data_corr_plot(series, max_lag)
1587
+
1588
+ callback = sm.tsa.stattools.pacf if pacf else sm.tsa.stattools.acf
1589
+ if not pacf:
1590
+ kwargs.update(dict(fft=False))
1591
+
1592
+ acf_x = callback(
1593
+ series,
1594
+ nlags=nlags,
1595
+ alpha=alpha,
1596
+ **kwargs,
1597
+ )
1598
+
1599
+ acf_x, confint = acf_x[:2] if not pacf else acf_x
1600
+
1601
+ if irregular:
1602
+ acf_x = acf_x[lags]
1603
+
1604
+ try:
1605
+ confint = confint[lags]
1606
+ if lags[0] == 0:
1607
+ lags = lags[1:]
1608
+ confint = confint[1:]
1609
+ acf_x = acf_x[1:]
1610
+ lags = lags.astype(float)
1611
+ lags[0] -= 0.5
1612
+ lags[-1] += 0.5
1613
+
1614
+ upp_band = confint[:, 0] - acf_x
1615
+ low_band = confint[:, 1] - acf_x
1616
+
1617
+ # pylint: disable=C0200
1618
+ for x in range(len(acf_x)):
1619
+ self.add_scatter(
1620
+ x=(x, x),
1621
+ y=(0, acf_x[x]),
1622
+ mode=mode,
1623
+ marker=marker,
1624
+ line=line,
1625
+ line_width=(2 if m is not None and x == m else 1),
1626
+ row=row,
1627
+ col=col,
1628
+ )
1629
+
1630
+ self.add_scatter(
1631
+ x=lags,
1632
+ y=upp_band,
1633
+ mode="lines",
1634
+ line_color="rgba(0, 0, 0, 0)",
1635
+ opacity=0,
1636
+ row=row,
1637
+ col=col,
1638
+ )
1639
+
1640
+ self.add_scatter(
1641
+ x=lags,
1642
+ y=low_band,
1643
+ mode="lines",
1644
+ fillcolor="rgba(255, 217, 0, 0.30)",
1645
+ fill="tonexty",
1646
+ line_color="rgba(0, 0, 0, 0.0)",
1647
+ opacity=0,
1648
+ row=row,
1649
+ col=col,
1650
+ )
1651
+ self.add_scatter(
1652
+ x=[0, max_lag + 1],
1653
+ y=[0, 0],
1654
+ mode="lines",
1655
+ line_color="white",
1656
+ row=row,
1657
+ col=col,
1658
+ )
1659
+ self.update_traces(showlegend=False)
1660
+
1661
+ except ValueError:
1662
+ pass
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly.html ADDED
The diff for this file is too large to render. See raw diff
 
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Plotly TA core package."""
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/base.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base class for charting plugins."""
2
+
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ Callable,
7
+ Dict,
8
+ Iterator,
9
+ List,
10
+ Optional,
11
+ Type,
12
+ Union,
13
+ )
14
+
15
+ from .data_classes import ChartIndicators, TAIndicator
16
+
17
+ if TYPE_CHECKING:
18
+ import pandas as pd
19
+
20
+
21
+ def columns_regex(df_ta: "pd.DataFrame", name: str) -> List[str]:
22
+ """Return columns that match regex name."""
23
+ column_name = df_ta.filter(regex=rf"{name}(?=[^\d]|$)").columns.tolist()
24
+
25
+ return column_name
26
+
27
+
28
+ class Indicator:
29
+ """Class for technical indicator."""
30
+
31
+ def __init__(
32
+ self,
33
+ func: Callable,
34
+ name: str = "",
35
+ **attrs: Any,
36
+ ) -> None:
37
+ """Initialize the indicator."""
38
+ self.func = func
39
+ self.name = name
40
+ self.attrs = attrs
41
+
42
+ def __call__(self, *args: Any, **kwargs: Any) -> Any:
43
+ """Call the indicator function."""
44
+ return self.func(*args, **kwargs)
45
+
46
+
47
+ class PluginMeta(type):
48
+ """Metaclass for all Plotly plugins."""
49
+
50
+ __indicators__: List[Indicator] = []
51
+ __static_methods__: list = []
52
+ __ma_mode__: List[str] = []
53
+ __inchart__: List[str] = []
54
+ __subplots__: List[str] = []
55
+
56
+ def __new__(mcs: Type["PluginMeta"], *args: Any, **kwargs: Any) -> "PluginMeta":
57
+ """Create a new instance of the class."""
58
+ name, bases, attrs = args
59
+ indicators: Dict[str, Indicator] = {}
60
+ cls_attrs: Dict[str, list] = {
61
+ "__ma_mode__": [],
62
+ "__inchart__": [],
63
+ "__subplots__": [],
64
+ }
65
+ new_cls = super().__new__(mcs, name, bases, attrs, **kwargs)
66
+ for base in reversed(new_cls.__mro__):
67
+ for elem, value in base.__dict__.items():
68
+ if elem in indicators:
69
+ del indicators[elem]
70
+
71
+ is_static_method = isinstance(value, staticmethod)
72
+ if is_static_method:
73
+ value = value.__func__ # noqa: PLW2901
74
+ if isinstance(value, Indicator):
75
+ if is_static_method:
76
+ raise TypeError(
77
+ f"Indicator {value.name} can't be a static method"
78
+ )
79
+ indicators[value.name] = value
80
+ elif is_static_method and elem not in new_cls.__static_methods__:
81
+ new_cls.__static_methods__.append(elem)
82
+
83
+ if elem in ["__ma_mode__", "__inchart__", "__subplots__"]:
84
+ cls_attrs[elem].extend(value)
85
+
86
+ new_cls.__indicators__ = list(indicators.values())
87
+ new_cls.__static_methods__ = list(set(new_cls.__static_methods__))
88
+ new_cls.__ma_mode__ = list(set(cls_attrs["__ma_mode__"]))
89
+ new_cls.__inchart__ = list(set(cls_attrs["__inchart__"]))
90
+ new_cls.__subplots__ = list(set(cls_attrs["__subplots__"]))
91
+
92
+ return new_cls
93
+
94
+ def __iter__(cls: Type["PluginMeta"]) -> Iterator[Indicator]: # type: ignore
95
+ """Iterate over the indicators."""
96
+ return iter(cls.__indicators__)
97
+
98
+ # pylint: disable=unused-argument
99
+ def __init__(cls, *args: Any, **kwargs: Any) -> None:
100
+ """Initialize the class."""
101
+ super().__init__(*args, **kwargs)
102
+
103
+
104
+ class PltTA(metaclass=PluginMeta):
105
+ """The base class that all Plotly plugins must inherit from."""
106
+
107
+ indicators: ChartIndicators
108
+ intraday: bool = False
109
+ df_stock: Union["pd.DataFrame", "pd.Series"]
110
+ df_ta: Optional["pd.DataFrame"] = None
111
+ df_fib: "pd.DataFrame"
112
+ close_column: Optional[str] = "close"
113
+ params: Optional[Dict[str, TAIndicator]] = {}
114
+ inchart_colors: List[str] = []
115
+ show_volume: bool = True
116
+
117
+ __static_methods__: list = []
118
+ __indicators__: List[Indicator] = []
119
+ __ma_mode__: List[str] = []
120
+ __inchart__: List[str] = []
121
+ __subplots__: List[str] = []
122
+
123
+ # pylint: disable=unused-argument
124
+ def __new__(cls, *args: Any, **kwargs: Any) -> "PltTA":
125
+ """Create a new instance of the class."""
126
+ if cls is PltTA:
127
+ raise TypeError("Can't instantiate abstract class Plugin directly")
128
+ self = super().__new__(cls)
129
+
130
+ static_methods = cls.__static_methods__
131
+ indicators = cls.__indicators__
132
+
133
+ for item in indicators:
134
+ # we make sure that the indicator is bound to the instance
135
+ if not hasattr(self, item.name):
136
+ setattr(self, item.name, item.func.__get__(self, cls))
137
+
138
+ for static_method in static_methods:
139
+ if not hasattr(self, static_method):
140
+ setattr(self, static_method, staticmethod(getattr(self, static_method)))
141
+
142
+ for attr, value in cls.__dict__.items():
143
+ if attr in [
144
+ "__ma_mode__",
145
+ "__inchart__",
146
+ "__subplots__",
147
+ ] and value not in getattr(self, attr):
148
+ getattr(self, attr).extend(value)
149
+
150
+ return self
151
+
152
+ @property
153
+ def ma_mode(self) -> List[str]:
154
+ """Moving average mode."""
155
+ return list(set(self.__ma_mode__))
156
+
157
+ @ma_mode.setter
158
+ def ma_mode(self, value: List[str]):
159
+ self.__ma_mode__ = value
160
+
161
+ def add_plugins(self, plugins: List["PltTA"]) -> None:
162
+ """Add plugins to current instance."""
163
+ for plugin in plugins:
164
+ for item in plugin.__indicators__:
165
+ # pylint: disable=unnecessary-dunder-call
166
+ if not hasattr(self, item.name):
167
+ setattr(self, item.name, item.func.__get__(self, type(self)))
168
+ self.__indicators__.append(item)
169
+
170
+ for static_method in plugin.__static_methods__:
171
+ if not hasattr(self, static_method):
172
+ setattr(
173
+ self, static_method, staticmethod(getattr(self, static_method))
174
+ )
175
+ for attr, value in plugin.__dict__.items():
176
+ if attr in [
177
+ "__ma_mode__",
178
+ "__inchart__",
179
+ "__subplots__",
180
+ ] and value not in getattr(self, attr):
181
+ getattr(self, attr).extend(value)
182
+
183
+ def remove_plugins(self, plugins: List["PltTA"]) -> None:
184
+ """Remove plugins from current instance."""
185
+ for plugin in plugins:
186
+ for item in plugin.__indicators__:
187
+ delattr(self, item.name)
188
+ self.__indicators__.remove(item)
189
+
190
+ for static_method in plugin.__static_methods__:
191
+ delattr(self, static_method)
192
+
193
+ def __iter__(self) -> Iterator[Indicator]:
194
+ """Iterate over the indicators."""
195
+ return iter(self.__indicators__)
196
+
197
+ def get_float_precision(self) -> str:
198
+ """Return f-string precision format."""
199
+ price = self.df_stock[self.close_column].tail(1).values[0]
200
+ float_precision = (
201
+ ",.2f" if price > 1.10 else "" if len(str(price)) < 8 else ".6f"
202
+ )
203
+ return float_precision
204
+
205
+
206
+ def indicator(
207
+ name: str = "",
208
+ **attrs: Any,
209
+ ) -> Callable:
210
+ """Use this decorator for adding indicators to a plugin class."""
211
+ attrs["name"] = name
212
+
213
+ def decorator(func: Callable) -> Indicator:
214
+ if not attrs.pop("name", ""):
215
+ name = func.__name__
216
+
217
+ # pylint: disable=possibly-used-before-assignment
218
+ return Indicator(func, name, **attrs)
219
+
220
+ return decorator
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/data_classes.py ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dataclasses for the charting extension."""
2
+
3
+ # pylint: disable=C0302,R0915,R0914,R0913,R0903,R0904
4
+
5
+ import sys
6
+ import warnings
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple, Union
9
+
10
+ if TYPE_CHECKING:
11
+ from pandas import DataFrame, Series
12
+
13
+ # pylint: disable=E1123
14
+ datacls_kwargs = {"slots": True} if sys.version_info >= (3, 10) else {}
15
+
16
+
17
+ def columns_regex(df_ta: "DataFrame", name: str) -> List[str]:
18
+ """Return columns that match regex name."""
19
+ column_name = df_ta.filter(regex=rf"{name}(?=[^\d]|$)").columns.tolist()
20
+
21
+ return column_name
22
+
23
+
24
+ @dataclass(**datacls_kwargs)
25
+ class Arguments:
26
+ """Arguments for technical analysis indicators."""
27
+
28
+ label: str
29
+ values: Any
30
+
31
+ def __post_init__(self):
32
+ """Post init."""
33
+ if isinstance(self.values, list) and len(self.values) == 1:
34
+ self.values = self.values[0]
35
+
36
+
37
+ @dataclass(**datacls_kwargs)
38
+ class TAIndicator:
39
+ """Technical analysis indicator."""
40
+
41
+ name: Literal[
42
+ "ad",
43
+ "adosc",
44
+ "adx",
45
+ "aroon",
46
+ "atr",
47
+ "cci",
48
+ "donchian",
49
+ "fisher",
50
+ "kc",
51
+ "obv",
52
+ "stoch",
53
+ "vwap",
54
+ "fib",
55
+ "srlines",
56
+ "clenow",
57
+ "demark",
58
+ "ichimoku",
59
+ "sma",
60
+ "ema",
61
+ "wma",
62
+ "hma",
63
+ "zlma",
64
+ "rma",
65
+ ]
66
+ args: List[Arguments]
67
+
68
+ def __iter__(self):
69
+ """Return iterator."""
70
+ return iter(self.args)
71
+
72
+ def get_args(self, label: str) -> Union[Arguments, None]:
73
+ """Return arguments by label."""
74
+ output = None
75
+ for opt in self.args:
76
+ if opt.label == label:
77
+ output = opt
78
+ return output
79
+
80
+ def get_argument_values(self, label: str) -> Union[List[Any], Any]:
81
+ """Return arguments values by label."""
82
+ output = []
83
+ options = self.get_args(label)
84
+ if options is not None:
85
+ output = options.values
86
+ return output
87
+
88
+
89
+ @dataclass(**datacls_kwargs)
90
+ class ChartIndicators:
91
+ """Chart technical analysis indicators."""
92
+
93
+ indicators: Optional[List[TAIndicator]] = None
94
+
95
+ def get_indicator(self, name: str) -> Union[TAIndicator, None]:
96
+ """Return indicator with given name."""
97
+ output = None
98
+ for indicator in self.indicators: # type: ignore
99
+ if indicator.name == name:
100
+ output = indicator
101
+ return output
102
+
103
+ def get_indicator_args(self, name: str, label: str) -> Union[Arguments, None]:
104
+ """Return argument values for given indicator and label."""
105
+ output = None
106
+ indicator = self.get_indicator(name)
107
+ if indicator is not None:
108
+ output = indicator.get_args(label)
109
+ if output is not None:
110
+ output = output.values
111
+ return output
112
+
113
+ def get_indicators(self) -> Optional[List[TAIndicator]]:
114
+ """Return active indicators and their arguments."""
115
+ return self.indicators
116
+
117
+ def get_params(self) -> Dict[str, TAIndicator]:
118
+ """Return dictionary of active indicators and their arguments."""
119
+ output = {}
120
+ if self.indicators:
121
+ output = {str(indicator.name): indicator for indicator in self.indicators}
122
+ return output
123
+
124
+ def get_active_ids(self) -> List[str]:
125
+ """Return list of names of active indicators."""
126
+ active_ids = []
127
+ if self.indicators:
128
+ active_ids = [str(indicator.name) for indicator in self.indicators]
129
+ return active_ids
130
+
131
+ def get_arg_names(self, name: str) -> List[str]:
132
+ """Return list of argument labels for given indicator."""
133
+ output = []
134
+ indicator = self.get_indicator(name)
135
+ if indicator is not None:
136
+ for opt in indicator.args:
137
+ output.append(opt.label)
138
+ return output
139
+
140
+ def get_options_dict(self, name: str) -> Dict[str, Optional[Arguments]]:
141
+ """Return dictionary of argument labels and values for given indicator."""
142
+ output = None
143
+ options = self.get_arg_names(name)
144
+ if options:
145
+ output = {}
146
+ for opt in options:
147
+ output[opt] = self.get_indicator_args(name, opt)
148
+
149
+ return output
150
+
151
+ @staticmethod
152
+ def get_available_indicators() -> Tuple[str, ...]:
153
+ """Return tuple of available indicators."""
154
+ return tuple(
155
+ TAIndicator.__annotations__["name"].__args__ # pylint: disable=E1101
156
+ )
157
+
158
+ @classmethod
159
+ def from_dict(
160
+ cls, indicators: Dict[str, Dict[str, List[Dict[str, Any]]]]
161
+ ) -> "ChartIndicators":
162
+ """Return ChartIndicators from dictionary.
163
+
164
+ Example
165
+ -------
166
+ ChartIndicators.from_dict(
167
+ {
168
+ "ad": {
169
+ "args": [
170
+ {
171
+ "label": "AD_LABEL",
172
+ "values": [1, 2, 3],
173
+ }
174
+ ]
175
+ }
176
+ }
177
+ )
178
+ """
179
+ return cls(
180
+ indicators=[
181
+ TAIndicator(
182
+ name=name, # type: ignore[arg-type]
183
+ args=[
184
+ Arguments(label=label, values=values)
185
+ for label, values in args.items()
186
+ ],
187
+ )
188
+ for name, args in indicators.items()
189
+ ]
190
+ )
191
+
192
+ def to_dataframe(
193
+ self, df_ta: "DataFrame", ma_mode: Optional[List[str]] = None
194
+ ) -> "DataFrame":
195
+ """Calculate technical analysis indicators and return dataframe."""
196
+ output = df_ta.copy()
197
+ if not output.empty and self.indicators:
198
+ try:
199
+ output = TA_Data(output, self, ma_mode).to_dataframe()
200
+ except Exception as err:
201
+ warnings.warn(str(err))
202
+
203
+ return output
204
+
205
+ def get_indicator_data(self, df_ta: "DataFrame", indicator: TAIndicator, **kwargs):
206
+ """Return dataframe with technical analysis indicators."""
207
+ output = None
208
+ if self.indicators:
209
+ try:
210
+ output = TA_Data(df_ta, self).get_indicator_data(indicator, **kwargs)
211
+ except Exception as err:
212
+ warnings.warn(str(err))
213
+
214
+ return output
215
+
216
+ def remove_indicator(self, name: str) -> None:
217
+ """Remove indicator from active indicators."""
218
+ if self.indicators:
219
+ for indicator in self.indicators:
220
+ if indicator.name == name:
221
+ self.indicators.remove(indicator)
222
+
223
+
224
+ class TA_DataException(Exception):
225
+ """Exception for TA_Data."""
226
+
227
+
228
+ class TA_Data:
229
+ """
230
+ Process technical analysis data.
231
+
232
+ Parameters
233
+ ----------
234
+ df_ta : DataFrame
235
+ Dataframe with OHLCV data
236
+ indicators : Union[ChartIndicators, Dict[str, Dict[str, Any]]]
237
+ ChartIndicators object or dictionary with indicators and arguments
238
+ Example:
239
+ dict(
240
+ sma=dict(length=[20, 50, 100]),
241
+ adx=dict(length=14),
242
+ macd=dict(fast=12, slow=26, signal=9),
243
+ rsi=dict(length=14),
244
+ )
245
+
246
+ Methods
247
+ -------
248
+ to_dataframe()
249
+ Return dataframe with technical analysis indicators
250
+ get_indicator_data(indicator: TAIndicator, **kwargs)
251
+ Return dataframe given indicator and arguments
252
+ """
253
+
254
+ def __init__(
255
+ self,
256
+ df_ta: Union["DataFrame", "Series"],
257
+ indicators: Union[ChartIndicators, Dict[str, Dict[str, Any]]],
258
+ ma_mode: Optional[List[str]] = None,
259
+ ):
260
+ """Initialize."""
261
+ # pylint: disable=import-outside-toplevel
262
+ from pandas import DataFrame, Series # noqa
263
+ from openbb_charting.core.plotly_ta.ta_helpers import check_columns # noqa
264
+
265
+ if isinstance(df_ta, Series):
266
+ df_ta = df_ta.to_frame()
267
+
268
+ if not isinstance(indicators, ChartIndicators):
269
+ indicators = ChartIndicators.from_dict(indicators)
270
+
271
+ self.df_ta: DataFrame = df_ta
272
+ self.indicators: ChartIndicators = indicators
273
+ self.ma_mode: List[str] = ma_mode or ["sma", "ema", "wma", "hma", "zlma", "rma"]
274
+ self.close_col = check_columns(df_ta)
275
+ if self.close_col is None:
276
+ raise ValueError("No close column found in dataframe")
277
+
278
+ self.columns: Dict[str, List[str]] = {
279
+ "ad": ["high", "low", self.close_col, "volume"],
280
+ "adosc": ["high", "low", self.close_col, "volume"],
281
+ "adx": ["high", "low", self.close_col],
282
+ "aroon": ["high", "low"],
283
+ "atr": ["high", "low", self.close_col],
284
+ "cci": ["high", "low", self.close_col],
285
+ "donchian": ["high", "low"],
286
+ "fisher": ["high", "low"],
287
+ "kc": ["high", "low", self.close_col],
288
+ "obv": [self.close_col, "volume"],
289
+ "stoch": ["high", "low", self.close_col],
290
+ "vwap": ["high", "low", self.close_col, "volume"],
291
+ }
292
+
293
+ self.has_volume = "volume" in df_ta.columns and bool(df_ta["volume"].sum() > 0)
294
+
295
+ def get_indicator_data(self, indicator: TAIndicator, **args) -> "DataFrame":
296
+ """
297
+ Return dataframe with indicator data.
298
+
299
+ Parameters
300
+ ----------
301
+ indicator : TAIndicator
302
+ TAIndicator object
303
+ args : dict
304
+ Arguments for given indicator
305
+
306
+ Return
307
+ -------
308
+ DataFrame
309
+ Dataframe with indicator data
310
+ """
311
+ # pylint: disable=import-outside-toplevel
312
+ import pandas_ta as ta
313
+ from pandas import DataFrame
314
+
315
+ output = None
316
+ if indicator and indicator.name in self.ma_mode:
317
+ if isinstance(indicator.get_argument_values("length"), list):
318
+ df_ta = DataFrame()
319
+
320
+ for length in indicator.get_argument_values("length"):
321
+ df_ma = getattr(ta, indicator.name)(
322
+ self.df_ta[self.close_col], length=length
323
+ )
324
+ df_ta.insert(0, f"{indicator.name.upper()}_{length}", df_ma)
325
+
326
+ output = df_ta
327
+
328
+ else:
329
+ output = getattr(ta, indicator.name)(
330
+ self.df_ta[self.close_col],
331
+ length=indicator.get_argument_values("length"),
332
+ )
333
+ if indicator.name == "zlma" and output is not None:
334
+ output.name = output.name.replace("ZL_EMA", "ZLMA")
335
+
336
+ elif indicator.name == "vwap":
337
+ ta_columns = self.columns[indicator.name]
338
+ ta_columns = [self.df_ta[col] for col in ta_columns] # type: ignore
339
+
340
+ output = getattr(ta, indicator.name)(
341
+ *ta_columns,
342
+ )
343
+ elif indicator.name in self.columns:
344
+ ta_columns = self.columns[indicator.name]
345
+ ta_columns = [self.df_ta[col] for col in ta_columns] # type: ignore
346
+
347
+ if indicator.get_argument_values("use_open") is True:
348
+ ta_columns.append(self.df_ta["open"])
349
+
350
+ output = getattr(ta, indicator.name)(*ta_columns, **args)
351
+ else:
352
+ output = getattr(ta, indicator.name)(self.df_ta[self.close_col], **args)
353
+
354
+ # Drop NaN values from output and return None if empty
355
+ if output is not None:
356
+ output.dropna(inplace=True)
357
+ if output.empty:
358
+ output = None
359
+
360
+ return output
361
+
362
+ def to_dataframe(self) -> "DataFrame":
363
+ """Return dataframe with all indicators."""
364
+ active_indicators = self.indicators.get_indicators()
365
+
366
+ if not active_indicators:
367
+ return None
368
+
369
+ output = self.df_ta
370
+ for indicator in active_indicators:
371
+ if (
372
+ indicator.name in self.columns
373
+ and "volume" in self.columns[indicator.name]
374
+ and not self.has_volume
375
+ ):
376
+ continue
377
+ if indicator.name in ["fib", "srlines", "clenow", "demark", "ichimoku"]:
378
+ continue
379
+ try:
380
+ indicator_data = self.get_indicator_data(
381
+ indicator,
382
+ **self.indicators.get_options_dict(indicator.name) or {},
383
+ )
384
+ except Exception as e:
385
+ indicator_data = None
386
+ raise TA_DataException(
387
+ f"Error processing indicator {indicator.name}: {e}"
388
+ ) from e
389
+
390
+ if indicator_data is not None:
391
+ output = output.join(indicator_data).infer_objects(copy=False)
392
+ numeric_cols = output.select_dtypes(include=["number"]).columns
393
+ output[numeric_cols] = output[numeric_cols].interpolate("linear")
394
+
395
+ return output
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Plotly TA Plugins."""
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/custom_indicators_plugin.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Custom technical indicators."""
2
+
3
+ import warnings
4
+ from datetime import datetime, timedelta
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from openbb_charting.core.config.openbb_styles import PLT_FIB_COLORWAY
10
+ from openbb_charting.core.openbb_figure import OpenBBFigure
11
+ from openbb_charting.core.plotly_ta.base import PltTA, indicator
12
+
13
+
14
+ class Custom(PltTA):
15
+ """Volatility technical indicators."""
16
+
17
+ @indicator()
18
+ def plot_srlines(self, fig: OpenBBFigure, df_ta: pd.DataFrame):
19
+ """Add support and resistance lines to plotly figure."""
20
+ window = self.params["srlines"].get_argument_values("window") # type: ignore
21
+ window = window[0] if isinstance(window, list) and len(window) > 0 else 200
22
+
23
+ def is_far_from_level(value, levels, df_stock):
24
+ ave = np.mean(df_stock["high"] - df_stock["low"])
25
+ return np.sum([abs(value - level) < ave for _, level in levels]) == 0
26
+
27
+ def is_support(df, i):
28
+ cond1 = df["low"][i] < df["low"][i - 1]
29
+ cond2 = df["low"][i] < df["low"][i + 1]
30
+ cond3 = df["low"][i + 1] < df["low"][i + 2]
31
+ cond4 = df["low"][i - 1] < df["low"][i - 2]
32
+ return cond1 and cond2 and cond3 and cond4
33
+
34
+ def is_resistance(df, i):
35
+ cond1 = df["high"][i] > df["high"][i - 1]
36
+ cond2 = df["high"][i] > df["high"][i + 1]
37
+ cond3 = df["high"][i + 1] > df["high"][i + 2]
38
+ cond4 = df["high"][i - 1] > df["high"][i - 2]
39
+ return cond1 and cond2 and cond3 and cond4
40
+
41
+ df_ta2 = df_ta.copy()
42
+ today = pd.to_datetime(datetime.now(), unit="ns")
43
+ start_date = pd.to_datetime(datetime.now() - timedelta(days=window), unit="ns")
44
+
45
+ df_ta2 = df_ta2.loc[(df_ta2.index >= start_date) & (df_ta2.index < today)]
46
+
47
+ if df_ta2.index[-2].date() != df_ta2.index[-1].date():
48
+ interval = 1440
49
+ else:
50
+ interval = (df_ta2.index[1] - df_ta2.index[0]).seconds / 60
51
+
52
+ if interval <= 15:
53
+ cut_days = 1 if interval < 15 else 2
54
+ dt_unique_days = df_ta2.index.normalize().unique() # type: ignore
55
+ df_ta2 = df_ta2.loc[
56
+ (df_ta2.index >= pd.to_datetime(dt_unique_days[-cut_days], unit="ns"))
57
+ & (df_ta2.index < today)
58
+ ].copy()
59
+
60
+ levels: list = []
61
+ x_range = df_ta2.index[-1].replace(hour=15, minute=59)
62
+ if interval > 15:
63
+ x_range = df_ta2.index[-1] + timedelta(days=15)
64
+ if x_range.weekday() > 4:
65
+ x_range = x_range + timedelta(days=7 - x_range.weekday())
66
+
67
+ elif df_ta2.index[-1] >= today.replace(hour=15, minute=0):
68
+ x_range = (df_ta2.index[-1] + timedelta(days=1)).replace(hour=11, minute=0)
69
+ if x_range.weekday() > 4:
70
+ x_range = x_range + timedelta(days=7 - x_range.weekday())
71
+
72
+ for i in range(2, len(df_ta2) - 2):
73
+ if is_support(df_ta2, i):
74
+ lv = df_ta2["low"][i]
75
+ if is_far_from_level(lv, levels, df_ta2):
76
+ levels.append((i, lv))
77
+ fig.add_scatter(
78
+ x=[df_ta.index[0], x_range],
79
+ y=[lv, lv],
80
+ opacity=0.8,
81
+ mode="lines+text",
82
+ text=["", f"{lv:{self.get_float_precision()}}"],
83
+ textposition="top center",
84
+ textfont=dict(
85
+ family="Arial Black", color="rgb(120, 70, 200)", size=10
86
+ ),
87
+ line=dict(
88
+ width=2, dash="dash", color="rgba(120, 70, 200, 0.70)"
89
+ ),
90
+ connectgaps=True,
91
+ showlegend=False,
92
+ row=1,
93
+ col=1,
94
+ secondary_y=False,
95
+ )
96
+ elif is_resistance(df_ta2, i):
97
+ lv = df_ta2["high"][i]
98
+ if is_far_from_level(lv, levels, df_ta2):
99
+ levels.append((i, lv))
100
+ fig.add_scatter(
101
+ x=[df_ta.index[0], x_range],
102
+ y=[lv, lv],
103
+ opacity=0.85,
104
+ mode="lines+text",
105
+ text=["", f"{lv:{self.get_float_precision()}}"],
106
+ textposition="top center",
107
+ textfont=dict(
108
+ family="Arial Black", color="rgb(120, 70, 200)", size=10
109
+ ),
110
+ line=dict(
111
+ width=2, dash="dash", color="rgba(120, 70, 200, 0.70)"
112
+ ),
113
+ connectgaps=True,
114
+ showlegend=False,
115
+ row=1,
116
+ col=1,
117
+ secondary_y=False,
118
+ )
119
+
120
+ return fig
121
+
122
+ @indicator()
123
+ def plot_fib(self, fig: OpenBBFigure, df_ta: pd.DataFrame):
124
+ """Add fibonacci to plotly figure."""
125
+ try:
126
+ from openbb_technical.helpers import ( # pylint: disable=import-outside-toplevel
127
+ calculate_fib_levels,
128
+ )
129
+ except ImportError:
130
+ warnings.warn(
131
+ "In order to use the Fibonacci indicator in your plot,"
132
+ " you need to install the `openbb-technical` package."
133
+ )
134
+ return fig
135
+
136
+ limit = self.params["fib"].get_argument_values("limit") or 120 # type: ignore
137
+ start_date = self.params["fib"].get_argument_values("start_date") or None # type: ignore
138
+ end_date = self.params["fib"].get_argument_values("end_date") or None # type: ignore
139
+ close = self.params["fib"].get_argument_values("close") or "close" # type: ignore
140
+ (
141
+ df_fib,
142
+ min_date,
143
+ max_date,
144
+ min_pr,
145
+ max_pr,
146
+ lvl_text,
147
+ ) = calculate_fib_levels(
148
+ df_ta, close, limit, start_date, end_date # type: ignore
149
+ )
150
+ levels = df_fib.Price
151
+ fibs = [
152
+ "<b>0</b>",
153
+ "<b>0.235</b>",
154
+ "<b>0.382</b>",
155
+ "<b>0.5</b>",
156
+ "<b>0.618</b>",
157
+ "<b>0.65</b>",
158
+ "<b>1</b>",
159
+ ]
160
+ min_date = pd.to_datetime(min_date).to_pydatetime()
161
+ max_date = pd.to_datetime(max_date).to_pydatetime()
162
+ self.df_fib = df_fib # pylint: disable=attribute-defined-outside-init
163
+
164
+ fig.add_scatter(
165
+ x=[min_date, max_date],
166
+ y=[min_pr, max_pr],
167
+ opacity=0.85,
168
+ mode="lines",
169
+ connectgaps=True,
170
+ line=PLT_FIB_COLORWAY[8],
171
+ showlegend=False,
172
+ row=1,
173
+ col=1,
174
+ secondary_y=False,
175
+ )
176
+ df_ta2 = df_ta.copy()
177
+ interval = 1440
178
+ if df_ta2.index[-2].date() == df_ta2.index[-1].date():
179
+ interval = (df_ta2.index[1] - df_ta2.index[0]).seconds / 60
180
+ dt_unique_days = df_ta2.index.normalize().unique() # type: ignore
181
+
182
+ if interval not in [15, 30, 60] and len(dt_unique_days) <= 3:
183
+ df_ta2 = df_ta2.loc[
184
+ (df_ta2.index >= dt_unique_days[-1])
185
+ & (df_ta2.index < datetime.now())
186
+ ].copy()
187
+ df_ta2 = df_ta2.between_time("09:30", "20:00").copy()
188
+
189
+ for i in range(7):
190
+ idx_int = 4 if lvl_text == "left" else 5
191
+ text_pos = f"bottom {lvl_text}" if i != idx_int else f"top {lvl_text}"
192
+
193
+ if fibs[i] == "<b>0</b>":
194
+ text_pos = (
195
+ f"top {lvl_text}" if lvl_text != "right" else f"bottom {lvl_text}"
196
+ )
197
+ text = ["", f"<b>{fibs[i]} ({levels[i]:{self.get_float_precision()}})</b>"]
198
+ if lvl_text == "right":
199
+ text = [text[1], text[0]]
200
+
201
+ fig.add_scatter(
202
+ name=fibs[i],
203
+ x=[min_date, df_ta2.index.max()],
204
+ y=[levels[i], levels[i]],
205
+ opacity=0.85,
206
+ mode="lines+text",
207
+ text=text,
208
+ textposition=text_pos,
209
+ textfont=dict(PLT_FIB_COLORWAY[7], color=PLT_FIB_COLORWAY[i]),
210
+ line_color=PLT_FIB_COLORWAY[i],
211
+ line_width=1.5,
212
+ showlegend=False,
213
+ row=1,
214
+ col=1,
215
+ secondary_y=False,
216
+ )
217
+
218
+ return fig
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/momentum_plugin.py ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Momentum technical indicators."""
2
+
3
+ import warnings
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import pandas_ta as ta
8
+
9
+ from openbb_charting.core.openbb_figure import OpenBBFigure
10
+ from openbb_charting.core.plotly_ta.base import (
11
+ PltTA,
12
+ indicator,
13
+ )
14
+ from openbb_charting.core.plotly_ta.data_classes import (
15
+ columns_regex,
16
+ )
17
+
18
+
19
+ class Momentum(PltTA):
20
+ """Momentum technical indicators."""
21
+
22
+ __subplots__ = ["rsi", "macd", "stoch", "cci", "fisher", "cg"]
23
+ __inchart__ = ["clenow", "demark", "ichimoku"]
24
+
25
+ @indicator()
26
+ def plot_cci(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
27
+ """Add CCI to plotly figure."""
28
+ cci_col = columns_regex(df_ta, "CCI")[0]
29
+ dmax = df_ta[cci_col].max()
30
+ dmin = df_ta[cci_col].min()
31
+ fig.add_scatter(
32
+ name="CCI",
33
+ mode="lines",
34
+ line=dict(width=1.5, color="#e0b700"),
35
+ x=df_ta.index,
36
+ y=df_ta[cci_col].values,
37
+ opacity=0.9,
38
+ row=subplot_row,
39
+ col=1,
40
+ secondary_y=False,
41
+ )
42
+
43
+ fig.add_hrect(
44
+ y0=100,
45
+ y1=dmax,
46
+ fillcolor=fig.theme.down_color,
47
+ opacity=0.2,
48
+ layer="below",
49
+ line_width=0,
50
+ row=subplot_row, # type: ignore
51
+ col=1, # type: ignore
52
+ secondary_y=False,
53
+ )
54
+ fig.add_hrect(
55
+ y0=-100,
56
+ y1=dmin,
57
+ fillcolor=fig.theme.up_color,
58
+ opacity=0.2,
59
+ layer="below",
60
+ line_width=0,
61
+ row=subplot_row, # type: ignore
62
+ col=1, # type: ignore
63
+ secondary_y=False,
64
+ )
65
+ fig.add_hline(
66
+ y=100,
67
+ opacity=1,
68
+ layer="below",
69
+ line=dict(width=2, color=fig.theme.down_color, dash="dash"),
70
+ row=subplot_row, # type: ignore
71
+ col=1, # type: ignore
72
+ secondary_y=False,
73
+ )
74
+ fig.add_hline(
75
+ y=-100,
76
+ opacity=1,
77
+ layer="below",
78
+ line=dict(width=2, color=fig.theme.up_color, dash="dash"),
79
+ row=subplot_row, # type: ignore
80
+ col=1, # type: ignore
81
+ secondary_y=False,
82
+ )
83
+
84
+ fig.add_annotation(
85
+ xref=f"x{subplot_row} domain",
86
+ yref=f"y{subplot_row + 1} domain",
87
+ text="<b>CCI</b>",
88
+ x=0,
89
+ xanchor="right",
90
+ xshift=-6,
91
+ y=0.97,
92
+ font_size=14,
93
+ font_color="#e0b700",
94
+ )
95
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True) # type: ignore
96
+
97
+ return fig, subplot_row + 1
98
+
99
+ @indicator()
100
+ def plot_cg(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
101
+ """Add cg to plotly figure."""
102
+ cg_col = columns_regex(df_ta, "CG")[0]
103
+ fig.add_scatter(
104
+ name="CG",
105
+ mode="lines",
106
+ line=dict(width=1.5, color="#ffed00"),
107
+ x=df_ta.index,
108
+ y=df_ta[cg_col].values,
109
+ opacity=0.9,
110
+ row=subplot_row,
111
+ col=1,
112
+ secondary_y=False,
113
+ )
114
+ fig.add_scatter(
115
+ name="Signal",
116
+ mode="lines",
117
+ line=dict(width=1.5, color="#ef7d00"),
118
+ x=df_ta.index,
119
+ y=df_ta[cg_col].shift(1),
120
+ opacity=0.9,
121
+ row=subplot_row,
122
+ col=1,
123
+ secondary_y=False,
124
+ )
125
+ fig.add_annotation(
126
+ xref=f"x{subplot_row} domain",
127
+ yref=f"y{subplot_row + 1} domain",
128
+ text="<b>CG</b>",
129
+ x=0,
130
+ xanchor="right",
131
+ xshift=-10,
132
+ y=0.97,
133
+ font_size=14,
134
+ font_color="#ffed00",
135
+ )
136
+ fig.add_annotation(
137
+ xref=f"x{subplot_row} domain",
138
+ yref=f"y{subplot_row + 1} domain",
139
+ text="Signal",
140
+ x=0,
141
+ xanchor="right",
142
+ xshift=-6,
143
+ y=0.97,
144
+ yshift=-20,
145
+ font_size=14,
146
+ font_color="#ef7d00",
147
+ )
148
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True) # type: ignore
149
+
150
+ return fig, subplot_row + 1
151
+
152
+ @indicator()
153
+ def plot_clenow(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
154
+ """Add current close price to plotly figure."""
155
+ try:
156
+ from openbb_technical.helpers import ( # pylint: disable=import-outside-toplevel
157
+ clenow_momentum,
158
+ )
159
+ except ImportError:
160
+ warnings.warn(
161
+ "In order to use the Clenow momentum indicator in your plot,"
162
+ " you need to install the `openbb-technical` package."
163
+ )
164
+ return fig, inchart_index
165
+
166
+ window = self.params["clenow"].get_argument_values("window") or 90 # type: ignore
167
+ _, _, fit_data = clenow_momentum(df_ta[self.close_column], window=window) # type: ignore
168
+
169
+ fig.add_scatter(
170
+ x=df_ta.index[-window:], # type: ignore
171
+ y=pow(np.e, fit_data),
172
+ name="CLenow",
173
+ mode="lines",
174
+ line=dict(color=self.inchart_colors[inchart_index], width=2),
175
+ row=1,
176
+ col=1,
177
+ secondary_y=False,
178
+ )
179
+ fig.add_annotation(
180
+ xref="paper",
181
+ yref="paper",
182
+ text="<b>CLenow</b>",
183
+ x=0,
184
+ xanchor="right",
185
+ xshift=-6,
186
+ yshift=-inchart_index * 18,
187
+ y=0.98,
188
+ font_size=14,
189
+ font_color=self.inchart_colors[inchart_index],
190
+ opacity=1,
191
+ )
192
+ return fig, inchart_index + 1
193
+
194
+ @indicator()
195
+ def plot_demark(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
196
+ """Add demark to plotly figure."""
197
+ min_val = self.params["demark"].get_argument_values("min_val") or 5 # type: ignore
198
+
199
+ demark = ta.td_seq(df_ta[self.close_column], asint=True)
200
+ demark = demark.set_index(df_ta.index)
201
+
202
+ # pylint: disable=unsupported-assignment-operation
203
+ demark["up"] = demark.TD_SEQ_UPa.apply(
204
+ lambda x: f"<b>{x}</b>" if x >= min_val else None
205
+ )
206
+
207
+ # pylint: disable=unsupported-assignment-operation
208
+ demark["down"] = demark.TD_SEQ_DNa.apply(
209
+ lambda x: f"<b>{x}</b>" if x >= min_val else None
210
+ )
211
+
212
+ # we only keep the values that are not None in up/down columns
213
+ high = df_ta["high"][demark["up"].notnull()]
214
+ low = df_ta["low"][demark["down"].notnull()]
215
+ demark = demark[demark["up"].notnull() | demark["down"].notnull()]
216
+
217
+ fig.add_scatter(
218
+ x=low.index,
219
+ y=low,
220
+ name="Demark Down",
221
+ mode="text",
222
+ text=demark["down"],
223
+ textposition="bottom center",
224
+ textfont=dict(color=fig.theme.down_color, size=14.5),
225
+ row=1,
226
+ col=1,
227
+ secondary_y=False,
228
+ )
229
+ fig.add_scatter(
230
+ x=high.index,
231
+ y=high,
232
+ name="Demark Up",
233
+ mode="text",
234
+ text=demark["up"],
235
+ textposition="top center",
236
+ textfont=dict(color=fig.theme.up_color, size=14.5),
237
+ row=1,
238
+ col=1,
239
+ secondary_y=False,
240
+ )
241
+
242
+ fig.add_annotation(
243
+ xref="paper",
244
+ yref="paper",
245
+ text="<b>Demark</b>",
246
+ x=0,
247
+ xanchor="right",
248
+ xshift=-6,
249
+ yshift=-inchart_index * 18,
250
+ y=0.98,
251
+ font_size=14,
252
+ font_color=self.inchart_colors[inchart_index],
253
+ opacity=1,
254
+ )
255
+
256
+ return fig, inchart_index + 1
257
+
258
+ @indicator()
259
+ def plot_fisher(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
260
+ """Add fisher to plotly figure."""
261
+ fishert_col = columns_regex(df_ta, "FISHERT")[0]
262
+ fishers_col = columns_regex(df_ta, "FISHERTs")[0]
263
+ fig.add_scatter(
264
+ name="Fisher",
265
+ mode="lines",
266
+ line=dict(width=1.5, color="#e0b700"),
267
+ x=df_ta.index,
268
+ y=df_ta[fishert_col].values,
269
+ opacity=0.9,
270
+ row=subplot_row,
271
+ col=1,
272
+ secondary_y=False,
273
+ )
274
+ fig.add_scatter(
275
+ name="Fisher Signal",
276
+ mode="lines",
277
+ line=dict(width=1.5, color="#9467bd"),
278
+ x=df_ta.index,
279
+ y=df_ta[fishers_col].values,
280
+ opacity=0.9,
281
+ row=subplot_row,
282
+ col=1,
283
+ secondary_y=False,
284
+ )
285
+ dmax = df_ta[fishers_col].max()
286
+ dmin = df_ta[fishers_col].min()
287
+
288
+ fig.add_annotation(
289
+ xref=f"x{subplot_row} domain",
290
+ yref=f"y{subplot_row + 1} domain",
291
+ text="<b>FISHER</b>",
292
+ x=0,
293
+ xanchor="right",
294
+ xshift=-6,
295
+ y=0.98,
296
+ font_size=14,
297
+ font_color="#e0b700",
298
+ )
299
+ fig.add_annotation(
300
+ xref=f"x{subplot_row} domain",
301
+ yref=f"y{subplot_row + 1} domain",
302
+ text="<b>SIGNAL</b>",
303
+ x=0,
304
+ xanchor="right",
305
+ xshift=-6,
306
+ y=0.01,
307
+ font_size=13,
308
+ font_color="#9467bd",
309
+ )
310
+ fig.add_hrect(
311
+ y0=2,
312
+ y1=(dmax + 4),
313
+ fillcolor=fig.theme.down_color,
314
+ opacity=0.2,
315
+ layer="below",
316
+ line_width=0,
317
+ row=subplot_row, # type: ignore
318
+ col=1, # type: ignore
319
+ secondary_y=False,
320
+ )
321
+ fig.add_hrect(
322
+ y0=-2,
323
+ y1=(dmin - 4),
324
+ fillcolor=fig.theme.up_color,
325
+ opacity=0.2,
326
+ layer="below",
327
+ line_width=0,
328
+ row=subplot_row, # type: ignore
329
+ col=1, # type: ignore
330
+ secondary_y=False,
331
+ )
332
+ fig.add_hline(
333
+ y=2,
334
+ fillcolor=fig.theme.down_color,
335
+ opacity=1,
336
+ layer="below",
337
+ line=dict(width=2, color=fig.theme.down_color, dash="dash"),
338
+ row=subplot_row, # type: ignore
339
+ col=1, # type: ignore
340
+ secondary_y=False,
341
+ )
342
+ fig.add_hline(
343
+ y=-2,
344
+ fillcolor=fig.theme.up_color,
345
+ opacity=1,
346
+ layer="below",
347
+ line=dict(width=2, color=fig.theme.up_color, dash="dash"),
348
+ row=subplot_row, # type: ignore
349
+ col=1, # type: ignore
350
+ secondary_y=False,
351
+ )
352
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True) # type: ignore
353
+
354
+ return fig, subplot_row + 1
355
+
356
+ @indicator()
357
+ def plot_macd(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
358
+ """Add macd to plotly figure."""
359
+ fig.add_bar(
360
+ name="MACD Histogram",
361
+ x=df_ta.index,
362
+ y=df_ta[columns_regex(df_ta, "MACDh")[0]].values,
363
+ opacity=0.9,
364
+ marker_color="#1a97ea",
365
+ row=subplot_row,
366
+ col=1,
367
+ secondary_y=False,
368
+ )
369
+ fig.add_scatter(
370
+ name="MACD",
371
+ mode="lines",
372
+ line=dict(width=1.5, color="#9467bd"),
373
+ x=df_ta.index,
374
+ y=df_ta[columns_regex(df_ta, "MACD")[0]].values,
375
+ opacity=0.9,
376
+ row=subplot_row,
377
+ col=1,
378
+ secondary_y=False,
379
+ )
380
+ fig.add_scatter(
381
+ name="MACD Signal",
382
+ mode="lines",
383
+ line=dict(width=1.5, color="rgb(7, 166, 52)"),
384
+ x=df_ta.index,
385
+ y=df_ta[columns_regex(df_ta, "MACDs")[0]].values,
386
+ opacity=0.9,
387
+ row=subplot_row,
388
+ col=1,
389
+ secondary_y=False,
390
+ )
391
+
392
+ fig.add_annotation(
393
+ xref=f"x{subplot_row} domain",
394
+ yref=f"y{subplot_row + 1} domain",
395
+ text="<b>MACD</b>",
396
+ x=0,
397
+ xanchor="right",
398
+ xshift=-6,
399
+ y=0.98,
400
+ font_size=14,
401
+ font_color="#9467bd",
402
+ )
403
+ fig.add_annotation(
404
+ xref=f"x{subplot_row} domain",
405
+ yref=f"y{subplot_row + 1} domain",
406
+ text="<b>SIGNAL</b>",
407
+ x=0,
408
+ xanchor="right",
409
+ y=0.70,
410
+ font_size=13,
411
+ xshift=-3,
412
+ font_color="rgb(7, 166, 52)",
413
+ )
414
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(autorange=True, nticks=5) # type: ignore
415
+
416
+ return fig, subplot_row + 1
417
+
418
+ @indicator()
419
+ def plot_rsi(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
420
+ """Add rsi to plotly figure."""
421
+ rsi_col = columns_regex(df_ta, "RSI")[0]
422
+ fig.add_scatter(
423
+ name="RSI",
424
+ mode="lines",
425
+ line=dict(width=1.5, color="rgb(0, 122, 204, 1)"),
426
+ x=df_ta.index,
427
+ y=df_ta[rsi_col].values,
428
+ opacity=1,
429
+ row=subplot_row,
430
+ col=1,
431
+ secondary_y=False,
432
+ )
433
+
434
+ fig.add_annotation(
435
+ xref=f"x{subplot_row} domain",
436
+ yref=f"y{subplot_row + 1} domain",
437
+ text="<b>RSI</b>",
438
+ x=0,
439
+ xanchor="right",
440
+ xshift=-6,
441
+ y=0.98,
442
+ font_size=14,
443
+ font_color="rgb(0, 122, 204, 1)",
444
+ )
445
+ fig.add_hrect(
446
+ y0=70,
447
+ y1=100,
448
+ fillcolor=fig.theme.down_color,
449
+ opacity=0.2,
450
+ layer="below",
451
+ line_width=0,
452
+ row=subplot_row, # type: ignore
453
+ col=1, # type: ignore
454
+ secondary_y=False,
455
+ )
456
+ fig.add_hrect(
457
+ y0=0,
458
+ y1=30,
459
+ fillcolor=fig.theme.up_color,
460
+ opacity=0.2,
461
+ layer="below",
462
+ line_width=0,
463
+ row=subplot_row, # type: ignore
464
+ col=1, # type: ignore
465
+ secondary_y=False,
466
+ )
467
+ fig.add_hline(
468
+ y=70,
469
+ fillcolor=fig.theme.down_color,
470
+ opacity=1,
471
+ layer="below",
472
+ line=dict(width=2, color=fig.theme.down_color, dash="dash"),
473
+ row=subplot_row, # type: ignore
474
+ col=1, # type: ignore
475
+ secondary_y=False,
476
+ )
477
+ fig.add_hline(
478
+ y=30,
479
+ fillcolor=fig.theme.up_color,
480
+ opacity=1,
481
+ layer="below",
482
+ line=dict(width=2, color=fig.theme.up_color, dash="dash"),
483
+ row=subplot_row, # type: ignore
484
+ col=1, # type: ignore
485
+ secondary_y=False,
486
+ )
487
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(tickvals=[0, 30, 70, 100]) # type: ignore
488
+
489
+ return fig, subplot_row + 1
490
+
491
+ @indicator()
492
+ def plot_stoch(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
493
+ """Add stoch to plotly figure."""
494
+ fig.add_scatter(
495
+ name="STOCH %K",
496
+ mode="lines",
497
+ line=dict(width=1.5, color="#e0b700"),
498
+ x=df_ta.index,
499
+ y=df_ta[columns_regex(df_ta, "STOCHk")[0]].values,
500
+ opacity=0.9,
501
+ row=subplot_row,
502
+ col=1,
503
+ secondary_y=False,
504
+ )
505
+ fig.add_scatter(
506
+ name="STOCH %D",
507
+ mode="lines",
508
+ line=dict(width=1.5, color="#9467bd", dash="dash"),
509
+ x=df_ta.index,
510
+ y=df_ta[columns_regex(df_ta, "STOCHd")[0]].values,
511
+ opacity=0.9,
512
+ row=subplot_row,
513
+ col=1,
514
+ secondary_y=False,
515
+ )
516
+
517
+ fig.add_annotation(
518
+ xref=f"x{subplot_row} domain",
519
+ yref=f"y{subplot_row + 1} domain",
520
+ text="<b>STOCH</b>",
521
+ x=0,
522
+ xanchor="right",
523
+ xshift=-6,
524
+ y=0.98,
525
+ font_size=14,
526
+ font_color="#e0b700",
527
+ )
528
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True) # type: ignore
529
+
530
+ return fig, subplot_row + 1
531
+
532
+ @indicator()
533
+ def plot_ichimoku(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
534
+ """Calculate Ichimoku indicator."""
535
+ conversion_period = (
536
+ self.params["ichimoku"].get_argument_values("conversion_period") or 9 # type: ignore
537
+ )
538
+ base_period = self.params["ichimoku"].get_argument_values("base_period") or 26 # type: ignore
539
+ lagging_line_period = (
540
+ self.params["ichimoku"].get_argument_values("lagging_line_period") or 52 # type: ignore
541
+ )
542
+ displacement = self.params["ichimoku"].get_argument_values("displacement") or 26 # type: ignore
543
+
544
+ # Tenkan-sen (Conversion Line)
545
+ conversion_line = (
546
+ df_ta["high"].rolling(window=conversion_period).max() # type: ignore
547
+ + df_ta["low"].rolling(window=conversion_period).min() # type: ignore
548
+ ) / 2
549
+
550
+ # Kijun-sen (Base Line)
551
+ base_line = (
552
+ df_ta["high"].rolling(window=base_period).max() # type: ignore
553
+ + df_ta["low"].rolling(window=base_period).min() # type: ignore
554
+ ) / 2
555
+
556
+ # Senkou Span A (Leading Span A)
557
+ leading_span_a = ((conversion_line + base_line) / 2).shift(displacement) # type: ignore
558
+
559
+ # Senkou Span B (Leading Span B)
560
+ lagging_line = df_ta[self.close_column].shift(-lagging_line_period) # type: ignore
561
+ leading_span_b = (
562
+ (
563
+ lagging_line.rolling(window=base_period).max() # type: ignore
564
+ + lagging_line.rolling(window=base_period).min() # type: ignore
565
+ )
566
+ / 2
567
+ ).shift(
568
+ displacement # type: ignore
569
+ )
570
+
571
+ # Plot Tenkan-sen and Kijun-sen
572
+ fig.add_scatter(
573
+ x=df_ta.index,
574
+ y=conversion_line,
575
+ line=dict(color="orange", width=1),
576
+ name="Tenkan-sen",
577
+ secondary_y=False,
578
+ showlegend=True,
579
+ opacity=1,
580
+ )
581
+ fig.add_scatter(
582
+ x=df_ta.index,
583
+ y=base_line,
584
+ line=dict(color="blue", width=1),
585
+ name="Kijun-sen",
586
+ secondary_y=False,
587
+ showlegend=True,
588
+ opacity=1,
589
+ )
590
+
591
+ # Plot Senkou Span A and Senkou Span B as a filled area
592
+ fig.add_scatter(
593
+ x=df_ta.index,
594
+ y=leading_span_a,
595
+ line=dict(color="#009600", width=1),
596
+ fill="tonexty",
597
+ fillcolor="rgba(0, 150, 0, 0.1)",
598
+ name="Senkou Span A",
599
+ secondary_y=False,
600
+ showlegend=False,
601
+ opacity=0.2,
602
+ )
603
+ fig.add_scatter(
604
+ x=df_ta.index,
605
+ y=leading_span_b,
606
+ line=dict(color="#c80000", width=1),
607
+ fill="tonexty",
608
+ fillcolor="rgba(200, 0, 0, 0.1)",
609
+ name="Senkou Span B",
610
+ showlegend=False,
611
+ secondary_y=False,
612
+ opacity=0.2,
613
+ )
614
+
615
+ fig.add_annotation(
616
+ xref="paper",
617
+ yref="paper",
618
+ text="<b><span style='color:#009600'>Ichi</span>"
619
+ "<span style='color:#c80000'>moku</span></b>",
620
+ x=0,
621
+ xanchor="right",
622
+ xshift=-6,
623
+ yshift=-inchart_index * 18,
624
+ y=0.98,
625
+ font_size=14,
626
+ opacity=1,
627
+ )
628
+
629
+ return fig, inchart_index + 1
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/overlap_plugin.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Overlap technical indicators plugin for Plotly TA."""
2
+
3
+ import warnings
4
+
5
+ import pandas as pd
6
+
7
+ from openbb_charting.core.openbb_figure import OpenBBFigure
8
+ from openbb_charting.core.plotly_ta.base import (
9
+ PltTA,
10
+ indicator,
11
+ )
12
+ from openbb_charting.core.plotly_ta.data_classes import (
13
+ columns_regex,
14
+ )
15
+
16
+
17
+ class Overlap(PltTA):
18
+ """Overlap technical indicators."""
19
+
20
+ __inchart__ = ["vwap"]
21
+ __ma_mode__ = ["sma", "ema", "wma", "hma", "zlma", "rma"]
22
+
23
+ @indicator()
24
+ def plot_ma(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
25
+ """Add moving average to plotly figure."""
26
+ check_ma = [ma for ma in self.ma_mode if ma in self.indicators.get_active_ids()]
27
+ if check_ma:
28
+ for ma in check_ma:
29
+ column_names = columns_regex(df_ta, ma.upper())
30
+ try:
31
+ for column in column_names:
32
+ if df_ta[column].empty:
33
+ continue
34
+
35
+ fig.add_scatter(
36
+ name=column.replace("RMA", "MA"),
37
+ mode="lines",
38
+ line=dict(
39
+ width=1.2, color=self.inchart_colors[inchart_index]
40
+ ),
41
+ x=df_ta.index,
42
+ y=df_ta[column].values,
43
+ opacity=0.9,
44
+ connectgaps=True,
45
+ row=1,
46
+ col=1,
47
+ secondary_y=False,
48
+ )
49
+ fig.add_annotation(
50
+ xref="paper",
51
+ yref="paper",
52
+ text=f"<b>{column.replace('_', '').replace('RMA', 'MA')}</b>",
53
+ x=0,
54
+ xanchor="right",
55
+ xshift=-6,
56
+ yshift=-inchart_index * 18,
57
+ y=0.98,
58
+ font_size=14,
59
+ font_color=self.inchart_colors[inchart_index],
60
+ opacity=1,
61
+ )
62
+ inchart_index += 1
63
+
64
+ except Exception as e:
65
+ warnings.warn(f"Error adding {ma.upper()} to plot - {e}")
66
+
67
+ return fig, inchart_index
68
+
69
+ @indicator()
70
+ def plot_vwap(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
71
+ """Add vwap to plotly figure."""
72
+ fig.add_scatter(
73
+ name=columns_regex(df_ta, "VWAP_")[0],
74
+ mode="lines",
75
+ line=dict(width=1.5, color=self.inchart_colors[inchart_index]),
76
+ x=df_ta.index,
77
+ y=df_ta[columns_regex(df_ta, "VWAP_")[0]].values,
78
+ opacity=0.8,
79
+ row=1,
80
+ col=1,
81
+ secondary_y=False,
82
+ )
83
+ fig.add_annotation(
84
+ xref="paper",
85
+ yref="paper",
86
+ text="<b>VWAP</b>",
87
+ x=0,
88
+ xanchor="right",
89
+ xshift=-6,
90
+ yshift=-inchart_index * 18,
91
+ y=0.98,
92
+ font_size=14,
93
+ font_color=self.inchart_colors[inchart_index],
94
+ )
95
+
96
+ return fig, inchart_index + 1
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/trend_indicators_plugin.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Trend technical indicators."""
2
+
3
+ import pandas as pd
4
+
5
+ from openbb_charting.core.openbb_figure import OpenBBFigure
6
+ from openbb_charting.core.plotly_ta.base import (
7
+ PltTA,
8
+ indicator,
9
+ )
10
+ from openbb_charting.core.plotly_ta.data_classes import (
11
+ columns_regex,
12
+ )
13
+
14
+
15
+ class Trend(PltTA):
16
+ """Trend technical indicators."""
17
+
18
+ __subplots__ = ["adx", "aroon"]
19
+
20
+ @indicator()
21
+ def plot_adx(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
22
+ """Add ADX to plotly figure."""
23
+ fig.add_scatter(
24
+ name="ADX",
25
+ mode="lines",
26
+ line=dict(width=1.5, color="#e0b700"),
27
+ x=df_ta.index,
28
+ y=df_ta[columns_regex(df_ta, "ADX")[0]].values,
29
+ opacity=0.9,
30
+ row=subplot_row,
31
+ col=1,
32
+ secondary_y=False,
33
+ )
34
+ fig.add_scatter(
35
+ name="+DI",
36
+ mode="lines",
37
+ line=dict(width=1.5, color=fig.theme.up_color),
38
+ x=df_ta.index,
39
+ y=df_ta[columns_regex(df_ta, "DMP")[0]].values,
40
+ opacity=0.9,
41
+ row=subplot_row,
42
+ col=1,
43
+ secondary_y=False,
44
+ )
45
+ fig.add_scatter(
46
+ name="-DI",
47
+ mode="lines",
48
+ line=dict(width=1.5, color=fig.theme.down_color),
49
+ x=df_ta.index,
50
+ y=df_ta[columns_regex(df_ta, "DMN")[0]].values,
51
+ opacity=0.9,
52
+ row=subplot_row,
53
+ col=1,
54
+ secondary_y=False,
55
+ )
56
+
57
+ fig.add_annotation(
58
+ xref=f"x{subplot_row} domain",
59
+ yref=f"y{subplot_row + 1} domain",
60
+ text="<b>ADX</b>",
61
+ x=0,
62
+ xanchor="right",
63
+ xshift=-6,
64
+ y=0.97,
65
+ font_size=14,
66
+ font_color="#e0b700",
67
+ )
68
+ fig.add_annotation(
69
+ xref=f"x{subplot_row} domain",
70
+ yref=f"y{subplot_row + 1} domain",
71
+ text=(
72
+ f"<span style='color: {fig.theme.up_color}'>D+</span><br>"
73
+ f"<span style='color: {fig.theme.down_color}'>D-</span>"
74
+ ),
75
+ x=0,
76
+ xanchor="right",
77
+ xshift=-14,
78
+ y=0.97,
79
+ yshift=-20,
80
+ font_size=14,
81
+ font_color=fig.theme.up_color,
82
+ )
83
+ fig.add_hline(
84
+ y=25,
85
+ fillcolor="white",
86
+ opacity=1,
87
+ layer="below",
88
+ line_width=1.5,
89
+ line=dict(color="white", dash="dash"),
90
+ row=subplot_row,
91
+ col=1,
92
+ secondary_y=False,
93
+ )
94
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
95
+
96
+ return fig, subplot_row + 1
97
+
98
+ @indicator()
99
+ def plot_aroon(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
100
+ """Add aroon to plotly figure."""
101
+ aroon_up_col = columns_regex(df_ta, "AROONU")[0]
102
+ aroon_down_col = columns_regex(df_ta, "AROOND")[0]
103
+ aroon_osc_col = columns_regex(df_ta, "AROONOSC")[0]
104
+ fig.add_scatter(
105
+ name="Aroon Up",
106
+ mode="lines",
107
+ line=dict(width=1.5, color=fig.theme.up_color),
108
+ x=df_ta.index,
109
+ y=df_ta[aroon_up_col].values,
110
+ opacity=0.9,
111
+ row=subplot_row,
112
+ col=1,
113
+ secondary_y=False,
114
+ )
115
+ fig.add_scatter(
116
+ name="Aroon Down",
117
+ mode="lines",
118
+ line=dict(width=1.5, color=fig.theme.down_color),
119
+ x=df_ta.index,
120
+ y=df_ta[aroon_down_col].values,
121
+ opacity=0.9,
122
+ row=subplot_row,
123
+ col=1,
124
+ secondary_y=False,
125
+ )
126
+
127
+ fig.add_annotation(
128
+ xref=f"x{subplot_row} domain",
129
+ yref=f"y{subplot_row + 1} domain",
130
+ text="<b>Aroon</b>",
131
+ x=0,
132
+ xanchor="right",
133
+ xshift=-6,
134
+ y=1,
135
+ font_size=14,
136
+ font_color="#e0b700",
137
+ )
138
+ fig.add_annotation(
139
+ xref=f"x{subplot_row} domain",
140
+ yref=f"y{subplot_row + 1} domain",
141
+ text=(
142
+ f"<span style='color: {fig.theme.up_color}'>↑</span><br>"
143
+ f"<span style='color: {fig.theme.down_color}'>↓</span>"
144
+ ),
145
+ x=0,
146
+ xanchor="right",
147
+ xshift=-14,
148
+ y=0.75,
149
+ font_size=14,
150
+ font_color=fig.theme.down_color,
151
+ )
152
+ fig.add_hline(
153
+ y=50,
154
+ fillcolor="white",
155
+ opacity=1,
156
+ layer="below",
157
+ line_width=1.5,
158
+ line=dict(color="white", dash="dash"),
159
+ row=subplot_row,
160
+ col=1,
161
+ secondary_y=False,
162
+ )
163
+
164
+ subplot_row += 1
165
+
166
+ fig.add_scatter(
167
+ name="Aroon Oscillator",
168
+ mode="lines",
169
+ line=dict(width=1.5, color="#e0b700"),
170
+ x=df_ta.index,
171
+ y=df_ta[aroon_osc_col].values,
172
+ connectgaps=True,
173
+ opacity=0.9,
174
+ row=subplot_row,
175
+ col=1,
176
+ secondary_y=False,
177
+ )
178
+
179
+ fig.add_annotation(
180
+ xref=f"x{subplot_row} domain",
181
+ yref=f"y{subplot_row + 1} domain",
182
+ text="<b>Aroon<br>OSC</b>",
183
+ x=0,
184
+ xanchor="right",
185
+ xshift=-6,
186
+ y=0.98,
187
+ font_size=14,
188
+ font_color="#e0b700",
189
+ )
190
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(
191
+ tickvals=[-100, 0, 100],
192
+ ticktext=["-100", "0", "100"],
193
+ nticks=5,
194
+ autorange=True,
195
+ )
196
+
197
+ return fig, subplot_row + 1
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/volatility_plugin.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Volatility technical indicators plugin for Plotly TA."""
2
+
3
+ import pandas as pd
4
+
5
+ from openbb_charting.core.openbb_figure import OpenBBFigure
6
+ from openbb_charting.core.plotly_ta.base import (
7
+ PltTA,
8
+ indicator,
9
+ )
10
+ from openbb_charting.core.plotly_ta.data_classes import (
11
+ columns_regex,
12
+ )
13
+
14
+
15
+ class Volatility(PltTA):
16
+ """Volatility technical indicators."""
17
+
18
+ __inchart__ = ["bbands", "donchian", "kc"]
19
+ __subplots__ = ["atr"]
20
+
21
+ @indicator()
22
+ def plot_atr(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
23
+ """Add average true range to plotly figure."""
24
+ fig.add_scatter(
25
+ name=f"{columns_regex(df_ta, 'ATR')[0]}",
26
+ x=df_ta.index,
27
+ y=df_ta[columns_regex(df_ta, "ATR")[0]].values,
28
+ mode="lines",
29
+ line=dict(width=1, color=fig.theme.get_colors()[1]),
30
+ row=subplot_row,
31
+ col=1,
32
+ secondary_y=False,
33
+ )
34
+
35
+ fig.add_annotation(
36
+ xref=f"x{subplot_row} domain",
37
+ yref=f"y{subplot_row + 1} domain",
38
+ text="<b>ATR</b>",
39
+ x=0,
40
+ xanchor="right",
41
+ xshift=-6,
42
+ y=0.98,
43
+ font_size=14,
44
+ font_color=fig.theme.get_colors()[1],
45
+ )
46
+ fig["layout"][f"yaxis{subplot_row}"].update(nticks=5, autorange=True)
47
+
48
+ return fig, subplot_row + 1
49
+
50
+ @indicator()
51
+ def plot_bbands(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
52
+ """Add bollinger bands to plotly figure."""
53
+ bbands_opacity = 0.8 if fig.theme.plt_style == "light" else 1
54
+
55
+ fig.add_scatter(
56
+ name=f"{columns_regex(df_ta, 'BBU')[0]}",
57
+ x=df_ta.index,
58
+ y=df_ta[columns_regex(df_ta, "BBU")[0]].values,
59
+ opacity=bbands_opacity,
60
+ mode="lines",
61
+ line=dict(width=1, color=fig.theme.up_color),
62
+ row=1,
63
+ col=1,
64
+ secondary_y=False,
65
+ )
66
+ fig.add_scatter(
67
+ name=f"{columns_regex(df_ta, 'BBL')[0]}",
68
+ x=df_ta.index,
69
+ y=df_ta[columns_regex(df_ta, "BBL")[0]].values,
70
+ opacity=bbands_opacity,
71
+ mode="lines",
72
+ line=dict(width=1, color=fig.theme.down_color),
73
+ row=1,
74
+ col=1,
75
+ secondary_y=False,
76
+ )
77
+ fig.add_scatter(
78
+ name=f"{columns_regex(df_ta, 'BBM')[0]}",
79
+ x=df_ta.index,
80
+ y=df_ta[columns_regex(df_ta, "BBM")[0]].values,
81
+ opacity=1,
82
+ mode="lines",
83
+ line=dict(width=1, color=fig.theme.get_colors()[1], dash="dash"),
84
+ row=1,
85
+ col=1,
86
+ secondary_y=False,
87
+ )
88
+ bbands_text = (
89
+ columns_regex(df_ta, "BBL")[0].replace("BBL_", "BB").replace("_", ",")
90
+ )
91
+ if float(bbands_text.split(",")[1]) % 1 == 0:
92
+ bbands_text = bbands_text.split(".")[0]
93
+ fig.add_annotation(
94
+ xref="paper",
95
+ yref="paper",
96
+ text=f"<b>{bbands_text}</b>",
97
+ x=0,
98
+ xanchor="right",
99
+ xshift=-6,
100
+ yshift=-inchart_index * 18,
101
+ y=0.98,
102
+ font_size=14,
103
+ font_color=fig.theme.get_colors()[1],
104
+ opacity=0.9,
105
+ )
106
+
107
+ return fig, inchart_index + 1
108
+
109
+ @indicator()
110
+ def plot_donchian(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
111
+ """Add donchian channels to plotly figure."""
112
+ if fig.theme.plt_style == "light":
113
+ fillcolor = "rgba(239, 103, 137, 0.05)"
114
+ donchian_opacity = 0.4
115
+ else:
116
+ fillcolor = "rgba(239, 103, 137, 0.05)"
117
+ donchian_opacity = 0.4
118
+
119
+ fig.add_scatter(
120
+ name=f"{columns_regex(df_ta, 'DCU')[0]}",
121
+ x=df_ta.index,
122
+ y=df_ta[columns_regex(df_ta, "DCU")[0]].values,
123
+ opacity=donchian_opacity,
124
+ mode="lines",
125
+ line=dict(width=0.3, color="#EF6689"),
126
+ row=1,
127
+ col=1,
128
+ secondary_y=False,
129
+ )
130
+ fig.add_scatter(
131
+ name=f"{columns_regex(df_ta, 'DCL')[0]}",
132
+ x=df_ta.index,
133
+ y=df_ta[columns_regex(df_ta, "DCL")[0]].values,
134
+ opacity=donchian_opacity,
135
+ mode="lines",
136
+ line=dict(width=0.3, color="#EF6689"),
137
+ fill="tonexty",
138
+ fillcolor=fillcolor,
139
+ row=1,
140
+ col=1,
141
+ secondary_y=False,
142
+ )
143
+
144
+ donchian_text = (
145
+ columns_regex(df_ta, "DCL")[0]
146
+ .replace("DCL_", "DC")
147
+ .replace("_", ",")
148
+ .split(".")[0]
149
+ )
150
+
151
+ fig.add_annotation(
152
+ xref="paper",
153
+ yref="paper",
154
+ text=f"<b>{donchian_text}</b>",
155
+ x=0,
156
+ xanchor="right",
157
+ xshift=-6,
158
+ yshift=-inchart_index * 18,
159
+ y=0.98,
160
+ font_size=14,
161
+ font_color="#B47DA0",
162
+ opacity=0.9,
163
+ )
164
+
165
+ return fig, inchart_index + 1
166
+
167
+ @indicator()
168
+ def plot_kc(self, fig: OpenBBFigure, df_ta: pd.DataFrame, inchart_index: int):
169
+ """Add Keltner channels to plotly figure."""
170
+ mamode = (self.params["kc"].get_argument_values("mamode") or "ema").lower() # type: ignore
171
+
172
+ if fig.theme.plt_style == "light":
173
+ fillcolor = "rgba(239, 103, 137, 0.05)"
174
+ kc_opacity = 0.4
175
+ else:
176
+ fillcolor = "rgba(239, 103, 137, 0.05)"
177
+ kc_opacity = 0.4
178
+
179
+ fig.add_scatter(
180
+ name=f"{columns_regex(df_ta, 'KCU')[0]}",
181
+ x=df_ta.index,
182
+ y=df_ta[columns_regex(df_ta, "KCU")[0]].values,
183
+ opacity=kc_opacity,
184
+ mode="lines",
185
+ line=dict(width=0.3, color="#EF6689"),
186
+ row=1,
187
+ col=1,
188
+ secondary_y=False,
189
+ )
190
+ fig.add_scatter(
191
+ name=f"{columns_regex(df_ta, 'KCL')[0]}",
192
+ x=df_ta.index,
193
+ y=df_ta[columns_regex(df_ta, "KCL")[0]].values,
194
+ opacity=kc_opacity,
195
+ mode="lines",
196
+ line=dict(width=0.3, color="#EF6689"),
197
+ fill="tonexty",
198
+ fillcolor=fillcolor,
199
+ row=1,
200
+ col=1,
201
+ secondary_y=False,
202
+ )
203
+ kctext = (
204
+ columns_regex(df_ta, "KCL")[0]
205
+ .replace(f"KCL{mamode[0]}_", "KC")
206
+ .replace("_", ",")
207
+ .split(".")[0]
208
+ )
209
+ fig.add_annotation(
210
+ xref="paper",
211
+ yref="paper",
212
+ text=f"<b>{kctext}</b>",
213
+ x=0,
214
+ xanchor="right",
215
+ xshift=-6,
216
+ yshift=-inchart_index * 18,
217
+ y=0.98,
218
+ font_size=14,
219
+ font_color="#B47DA0",
220
+ opacity=0.9,
221
+ )
222
+
223
+ return fig, inchart_index + 1
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/volume_plugin.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Volume technical indicators."""
2
+
3
+ import pandas as pd
4
+
5
+ from openbb_charting.core.openbb_figure import OpenBBFigure
6
+ from openbb_charting.core.plotly_ta.base import (
7
+ PltTA,
8
+ indicator,
9
+ )
10
+ from openbb_charting.core.plotly_ta.data_classes import (
11
+ columns_regex,
12
+ )
13
+
14
+
15
+ class Volume(PltTA):
16
+ """Volume technical indicators."""
17
+
18
+ __subplots__ = ["ad", "adosc", "obv"]
19
+
20
+ # Useless super delegation
21
+ # def __init__(self, *args, **kwargs):
22
+ # super().__init__(*args, **kwargs)
23
+
24
+ @indicator()
25
+ def plot_ad(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
26
+ """Add ad to plotly figure."""
27
+ ad_col = columns_regex(df_ta, "AD")[0]
28
+ fig.add_scatter(
29
+ name="AD",
30
+ mode="lines",
31
+ line=dict(width=1.5, color=fig.theme.get_colors()[1]),
32
+ x=df_ta.index,
33
+ y=df_ta[ad_col].values,
34
+ opacity=0.9,
35
+ row=subplot_row,
36
+ col=1,
37
+ secondary_y=False,
38
+ )
39
+
40
+ fig.add_hline(
41
+ y=0,
42
+ fillcolor="white",
43
+ opacity=1,
44
+ layer="below",
45
+ line=dict(color="white", dash="dash", width=2),
46
+ row=subplot_row,
47
+ col=1,
48
+ secondary_y=False,
49
+ )
50
+
51
+ fig.add_annotation(
52
+ xref=f"x{subplot_row} domain",
53
+ yref=f"y{subplot_row + 1} domain",
54
+ text="<b>AD</b>",
55
+ x=0,
56
+ xanchor="right",
57
+ xshift=-6,
58
+ y=0.98,
59
+ font_size=14,
60
+ font_color=fig.theme.get_colors()[1],
61
+ )
62
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=3, autorange=True)
63
+
64
+ return fig, subplot_row + 1
65
+
66
+ @indicator()
67
+ def plot_adosc(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
68
+ """Add adosc to plotly figure."""
69
+ ad_col = columns_regex(df_ta, "ADOSC")[0]
70
+ fig.add_scatter(
71
+ name="ADOSC",
72
+ mode="lines",
73
+ line=dict(width=1.5, color=fig.theme.get_colors()[1]),
74
+ x=df_ta.index,
75
+ y=df_ta[ad_col].values,
76
+ opacity=0.9,
77
+ row=subplot_row,
78
+ col=1,
79
+ secondary_y=False,
80
+ )
81
+
82
+ fig.add_annotation(
83
+ xref=f"x{subplot_row} domain",
84
+ yref=f"y{subplot_row + 1} domain",
85
+ text="<b>ADOSC</b>",
86
+ x=0,
87
+ xanchor="right",
88
+ xshift=-6,
89
+ y=0.98,
90
+ font_size=14,
91
+ font_color=fig.theme.get_colors()[1],
92
+ )
93
+
94
+ return fig, subplot_row + 1
95
+
96
+ @indicator()
97
+ def plot_obv(self, fig: OpenBBFigure, df_ta: pd.DataFrame, subplot_row: int):
98
+ """Add obv to plotly figure."""
99
+ obv_col = columns_regex(df_ta, "OBV")[0]
100
+ fig.add_scatter(
101
+ name="OBV",
102
+ mode="lines",
103
+ line=dict(width=1.5, color=fig.theme.get_colors()[1]),
104
+ x=df_ta.index,
105
+ y=df_ta[obv_col].values,
106
+ opacity=0.9,
107
+ row=subplot_row,
108
+ col=1,
109
+ secondary_y=False,
110
+ )
111
+
112
+ fig.add_annotation(
113
+ xref=f"x{subplot_row} domain",
114
+ yref=f"y{subplot_row + 1} domain",
115
+ text="<b>OBV</b>",
116
+ x=0,
117
+ xanchor="right",
118
+ xshift=-6,
119
+ y=0.98,
120
+ font_size=14,
121
+ font_color=fig.theme.get_colors()[1],
122
+ )
123
+ fig["layout"][f"yaxis{subplot_row + 1}"].update(nticks=5, autorange=True)
124
+
125
+ return fig, subplot_row + 1
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/ta_class.py ADDED
@@ -0,0 +1,676 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Technical Analysis class for Plotly."""
2
+
3
+ # pylint: disable=R0902,R0916,R0912,R0917 # type: ignore[index, assignment]
4
+
5
+ import importlib
6
+ import inspect
7
+ import sys
8
+ import warnings
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union
11
+
12
+ from openbb_charting.core.chart_style import ChartStyle
13
+ from openbb_charting.core.openbb_figure import OpenBBFigure
14
+ from openbb_charting.core.plotly_ta.base import PltTA
15
+ from openbb_charting.core.plotly_ta.data_classes import ChartIndicators
16
+ from openbb_charting.core.plotly_ta.ta_helpers import check_columns
17
+
18
+ charting_EXTENSION_PATH = Path(__file__).parent.parent.parent
19
+ CHARTING_INSTALL_PATH = charting_EXTENSION_PATH.parent
20
+ PLUGINS_PATH = Path(__file__).parent / "plugins"
21
+ PLOTLY_TA: Optional["PlotlyTA"] = None
22
+
23
+ if TYPE_CHECKING:
24
+ import pandas as pd
25
+ from openbb_core.app.model.charts.charting_settings import ChartingSettings
26
+
27
+
28
+ class PlotlyTA(PltTA):
29
+ """Plotly Technical Analysis class.
30
+
31
+ This class is a singleton. It is created and then reused, to assure
32
+ the plugins are only loaded once. This is done by overriding the __new__
33
+ method. The __init__ method is overridden to do nothing, except to clear
34
+ the internal data structures.
35
+
36
+ Attributes
37
+ ----------
38
+ inchart_colors (List[str]):
39
+ List of colors for inchart indicators
40
+ show_volume (bool):
41
+ Whether to show the volume subplot
42
+ ma_mode (List[str]):
43
+ List of available moving average modes
44
+ inchart (List[str]):
45
+ List of available inchart indicators
46
+ subplots (List[str]):
47
+ List of available subplots
48
+
49
+ StaticMethods
50
+ -------------
51
+ plot(
52
+ df: pd.DataFrame,
53
+ indicators: ChartIndicators,
54
+ fig: Optional[OpenBBFigure] = None,
55
+ symbol: Optional[str] = "",
56
+ candles: bool = True,
57
+ volume: bool = True,
58
+ ) -> OpenBBFigure:
59
+ Plots the chart with the given indicators
60
+
61
+
62
+ Examples
63
+ --------
64
+ >>> from openbb import obb
65
+ >>> from charting.core.plotly_ta.ta_class import PlotlyTA
66
+
67
+ >>> df = obb.equity.price.historical("SPY")
68
+ >>> indicators = dict(
69
+ >>> sma=dict(length=[20, 50, 100]),
70
+ >>> adx=dict(length=14),
71
+ >>> macd=dict(fast=12, slow=26, signal=9),
72
+ >>> rsi=dict(length=14),
73
+ >>> )
74
+ >>> fig = PlotlyTA.plot(df, indicators=indicators)
75
+ >>> fig.show()
76
+
77
+ If you want to plot the chart with the same indicators, you can
78
+ reuse the same instance of the class as follows:
79
+
80
+ >>> ta = PlotlyTA()
81
+ >>> fig = ta.plot(df, indicators=indicators)
82
+ >>> df2 = obb.equity.price.historical("AAPL")
83
+ >>> fig2 = ta.plot(df2)
84
+ >>> fig.show()
85
+ >>> fig2.show()
86
+ """
87
+
88
+ inchart_colors: List[str] = []
89
+ plugins: List[Type[PltTA]] = []
90
+ df_ta: Optional["pd.DataFrame"] = None
91
+ close_column: Optional[str] = "close"
92
+ has_volume: bool = True
93
+ show_volume: bool = True
94
+ prepost: bool = False
95
+ charting_settings: Optional["ChartingSettings"] = None
96
+ theme: Optional[ChartStyle] = None
97
+
98
+ def __new__(cls, *args, **kwargs):
99
+ """Create a new instance of the class.
100
+
101
+ Method is overridden to create a singleton instance of the class.
102
+ """
103
+ cls.charting_settings = kwargs.pop("charting_settings", cls.charting_settings)
104
+ cls.theme = cls.setup_theme(
105
+ chart_style=getattr(cls.charting_settings, "chart_style", ""),
106
+ user_styles_directory=getattr(
107
+ cls.charting_settings, "user_styles_directory", ""
108
+ ),
109
+ )
110
+ cls.inchart_colors = cls.theme.get_colors()
111
+
112
+ global PLOTLY_TA # pylint: disable=global-statement # noqa
113
+ if PLOTLY_TA is None:
114
+ # Creates the instance of the class and loads the plugins
115
+ # We set the global variable to the instance of the class so that
116
+ # the plugins are only loaded once
117
+ PLOTLY_TA = super().__new__(cls) # type: ignore[attr-defined, assignment]
118
+ PLOTLY_TA._locate_plugins( # type: ignore[attr-defined]
119
+ getattr(cls.charting_settings, "debug_mode", False)
120
+ )
121
+ PLOTLY_TA.add_plugins(PLOTLY_TA.plugins) # type: ignore[attr-defined, assignment]
122
+
123
+ return PLOTLY_TA
124
+
125
+ def __init__(self, *args, **kwargs):
126
+ """Initialize the class.
127
+
128
+ Method is overridden to do nothing, except to clear the internal data structures.
129
+ """
130
+ if not args and not kwargs:
131
+ self._clear_data()
132
+ else:
133
+ self.df_fib = None # type: ignore
134
+ super().__init__(*args, **kwargs)
135
+
136
+ @staticmethod
137
+ def setup_theme(chart_style, user_styles_directory) -> ChartStyle:
138
+ """Set up theme for charting."""
139
+ return ChartStyle(chart_style, user_styles_directory)
140
+
141
+ @property
142
+ def ma_mode(self) -> List[str]:
143
+ """List of available moving average modes."""
144
+ return list(set(self.__ma_mode__))
145
+
146
+ @ma_mode.setter
147
+ def ma_mode(self, value: List[str]):
148
+ """Set list of available moving average modes."""
149
+ self.__ma_mode__ = value
150
+
151
+ @property
152
+ def inchart(self) -> List[str]:
153
+ """List of available inchart indicators."""
154
+ return list(set(self.__inchart__))
155
+
156
+ @inchart.setter
157
+ def inchart(self, value: List[str]):
158
+ """Set list of available inchart indicators."""
159
+ self.__inchart__ = value
160
+
161
+ @property
162
+ def subplots(self) -> List[str]:
163
+ """List of available subplots."""
164
+ return list(set(self.__subplots__))
165
+
166
+ @subplots.setter
167
+ def subplots(self, value: List[str]):
168
+ """Set list of available subplots."""
169
+ self.__subplots__ = value
170
+
171
+ # pylint: disable=R0913
172
+ def __plot__(
173
+ self,
174
+ df_stock: Union["pd.DataFrame", "pd.Series"],
175
+ indicators: Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]] = None,
176
+ symbol: str = "",
177
+ candles: bool = True,
178
+ volume: bool = True,
179
+ prepost: bool = False,
180
+ fig: Optional[OpenBBFigure] = None,
181
+ volume_ticks_x: int = 7,
182
+ ) -> OpenBBFigure:
183
+ """Do not call this directly.
184
+
185
+ Use the PlotlyTA.plot() static method instead.
186
+ """
187
+ # pylint: disable=import-outside-toplevel
188
+ import pandas as pd
189
+
190
+ if isinstance(df_stock, pd.Series):
191
+ df_stock = df_stock.to_frame()
192
+
193
+ if not isinstance(indicators, ChartIndicators):
194
+ indicators = ChartIndicators.from_dict(indicators or {})
195
+
196
+ # Apply to_datetime to the index in a way that handles daylight savings.
197
+ df_stock.loc[:, "date"] = df_stock.index # type: ignore
198
+ df_stock["date"] = df_stock["date"].apply(pd.to_datetime)
199
+ df_stock.index = df_stock["date"] # type: ignore
200
+ df_stock.drop(columns=["date"], inplace=True)
201
+
202
+ self.indicators = indicators
203
+ self.intraday = df_stock.index[-2].time() != df_stock.index[-1].time()
204
+ self.df_stock = df_stock.sort_index(ascending=True)
205
+ self.close_column = check_columns(self.df_stock)
206
+ self.params = self.indicators.get_params()
207
+
208
+ self.has_volume = "volume" in self.df_stock.columns and bool(
209
+ self.df_stock["volume"].sum() > 0
210
+ )
211
+ self.show_volume = volume and self.has_volume
212
+
213
+ self.prepost = prepost
214
+
215
+ return self.plot_fig(
216
+ fig=fig, symbol=symbol, candles=candles, volume_ticks_x=volume_ticks_x
217
+ )
218
+
219
+ @staticmethod
220
+ def plot(
221
+ df_stock: Union["pd.DataFrame", "pd.Series"],
222
+ indicators: Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]] = None,
223
+ symbol: str = "",
224
+ candles: bool = True,
225
+ volume: bool = True,
226
+ prepost: bool = False,
227
+ fig: Optional[OpenBBFigure] = None,
228
+ volume_ticks_x: int = 7,
229
+ ) -> OpenBBFigure:
230
+ """Plot a chart with the given indicators.
231
+
232
+ Parameters
233
+ ----------
234
+ df_stock : pd.DataFrame
235
+ Dataframe with stock data
236
+ indicators : Union[ChartIndicators, Dict[str, Dict[str, Any]]]
237
+ ChartIndicators object or dictionary with indicators and parameters to plot
238
+ Example:
239
+ dict(
240
+ sma=dict(length=[20, 50, 100]),
241
+ adx=dict(length=14),
242
+ macd=dict(fast=12, slow=26, signal=9),
243
+ rsi=dict(length=14),
244
+ )
245
+ symbol : str, optional
246
+ Symbol to plot, by default uses the dataframe.name attribute if available or ""
247
+ candles : bool, optional
248
+ Plot a candlestick chart, by default True (if False, plots a line chart)
249
+ volume : bool, optional
250
+ Plot volume, by default True
251
+ prepost : bool, optional
252
+ Plot pre and post market data, by default False
253
+ fig : OpenBBFigure, optional
254
+ Plotly figure to plot on, by default None
255
+ volume_ticks_x : int, optional
256
+ Number to multiply volume, by default 7
257
+ """
258
+ if indicators is None and PLOTLY_TA is not None:
259
+ indicators = PLOTLY_TA.indicators
260
+
261
+ return PlotlyTA().__plot__( # type: ignore
262
+ df_stock, indicators, symbol, candles, volume, prepost, fig, volume_ticks_x
263
+ )
264
+
265
+ @staticmethod
266
+ def _locate_plugins(debug: Optional[bool] = False) -> None:
267
+ """Locate all the plugins in the plugins folder."""
268
+ path = (
269
+ Path(sys.executable).parent
270
+ if hasattr(sys, "frozen")
271
+ else CHARTING_INSTALL_PATH
272
+ )
273
+ if debug:
274
+ warnings.warn(f"[bold green]Loading plugins from {path}[/]")
275
+ warnings.warn("[bold green]Plugins found:[/]")
276
+
277
+ for plugin in Path(__file__).parent.glob("plugins/*_plugin.py"):
278
+ python_path = plugin.relative_to(path).with_suffix("")
279
+
280
+ if debug:
281
+ warnings.warn(f" [bold red]{plugin.name}[/]")
282
+ warnings.warn(f" [bold yellow]{python_path}[/]")
283
+ warnings.warn(f" [bold bright_cyan]{__package__}[/]")
284
+ warnings.warn(f" [bold magenta]{python_path.parts}[/]")
285
+ warnings.warn(
286
+ f" [bold bright_magenta]{'.'.join(python_path.parts)}[/]"
287
+ )
288
+
289
+ module = importlib.import_module(
290
+ ".".join(python_path.parts), package=__package__
291
+ )
292
+ for _, obj in inspect.getmembers(module):
293
+ if (
294
+ inspect.isclass(obj)
295
+ and issubclass(obj, (PltTA))
296
+ and obj != PlotlyTA.__class__
297
+ ) and obj not in PlotlyTA.plugins:
298
+ PlotlyTA.plugins.append(obj)
299
+
300
+ def _clear_data(self):
301
+ """Clear and reset all data to default values."""
302
+ self.df_stock = None # type: ignore
303
+ self.indicators = ChartIndicators.from_dict({})
304
+ self.params = None
305
+ self.intraday = False
306
+ self.show_volume = True
307
+
308
+ def calculate_indicators(self):
309
+ """Return dataframe with all indicators."""
310
+ return self.indicators.to_dataframe(self.df_stock.copy(), self.ma_mode) # type: ignore
311
+
312
+ def get_subplot(self, subplot: str) -> bool:
313
+ """Return True if subplots will be able to be plotted with current data."""
314
+ if subplot == "volume":
315
+ return self.show_volume
316
+
317
+ if subplot in ["ad", "adosc", "obv", "vwap"] and not self.has_volume:
318
+ self.indicators.remove_indicator(subplot)
319
+ warnings.warn(
320
+ f"[bold red]Warning:[/] [yellow]{subplot.upper()}"
321
+ " requires volume data to be plotted but no volume data was found."
322
+ " Indicator will not be plotted.[/]"
323
+ )
324
+ return False
325
+
326
+ output = False
327
+
328
+ try:
329
+ indicator = self.indicators.get_indicator(subplot)
330
+ if indicator is None:
331
+ return False
332
+
333
+ output = self.indicators.get_indicator_data(
334
+ self.df_stock.copy(), # type: ignore
335
+ indicator,
336
+ **self.indicators.get_options_dict(indicator.name) or {},
337
+ )
338
+ if not isinstance(output, bool):
339
+ output = output.dropna() # type: ignore
340
+
341
+ if output is None or output.empty:
342
+ output = False
343
+
344
+ return True
345
+
346
+ except Exception:
347
+ output = False
348
+
349
+ return output
350
+
351
+ def check_subplots(self, subplots: list) -> list:
352
+ """Return list of subplots that can be plotted with current data."""
353
+ output = []
354
+ for subplot in subplots:
355
+ if self.get_subplot(subplot):
356
+ output.append(subplot)
357
+
358
+ return output
359
+
360
+ def get_fig_settings_dict(self):
361
+ """Return dictionary with settings for plotly figure."""
362
+ row_params = {
363
+ "0": dict(rows=1, row_width=[1]),
364
+ "1": dict(rows=2, row_width=[0.3, 0.7]),
365
+ "2": dict(rows=3, row_width=[0.15, 0.15, 0.7]),
366
+ "3": dict(rows=4, row_width=[0.2, 0.2, 0.2, 0.4]),
367
+ "4": dict(rows=5, row_width=[0.15, 0.15, 0.15, 0.15, 0.4]),
368
+ }
369
+
370
+ check_active = self.indicators.get_active_ids()
371
+ subplots = [subplot for subplot in self.subplots if subplot in check_active]
372
+
373
+ check_rows = min(len(self.check_subplots(subplots)), 4)
374
+ check_rows += 1 if "aroon" in subplots and (check_rows + 1) < 5 else 0
375
+
376
+ specs = [[{"secondary_y": True}]] + [[{"secondary_y": False}]] * check_rows
377
+
378
+ output = row_params.get(str(check_rows), dict(rows=1, row_width=[1]))
379
+ output.update(dict(cols=1, vertical_spacing=0.04, specs=specs))
380
+
381
+ return output
382
+
383
+ def init_plot(self, symbol: str = "", candles: bool = True) -> OpenBBFigure:
384
+ """Create plotly figure with subplots.
385
+
386
+ Parameters
387
+ ----------
388
+ symbol : str, optional
389
+ Symbol to plot, by default uses the dataframe.name attribute if available or ""
390
+ candles : bool, optional
391
+ Plot a candlestick chart, by default True (if False, plots a line chart)
392
+
393
+ Returns
394
+ -------
395
+ fig : OpenBBFigure
396
+ Plotly figure with candlestick/line chart and volume bar chart (if enabled)
397
+ """
398
+ fig = OpenBBFigure(charting_settings=self.charting_settings)
399
+ fig = fig.create_subplots(
400
+ 1,
401
+ 1,
402
+ shared_xaxes=True,
403
+ vertical_spacing=0.06,
404
+ horizontal_spacing=0.01,
405
+ row_width=[1],
406
+ specs=[[{"secondary_y": True}]],
407
+ )
408
+ cc_linewidth = (
409
+ 0.8 if len(self.df_stock.index) > 500 else 0.9 if self.intraday else 1.1
410
+ )
411
+ if candles:
412
+ fig.add_candlestick(
413
+ x=self.df_stock.index,
414
+ open=self.df_stock.open,
415
+ high=self.df_stock.high,
416
+ low=self.df_stock.low,
417
+ close=self.df_stock.close,
418
+ decreasing=dict(line=dict(width=cc_linewidth)),
419
+ increasing=dict(line=dict(width=cc_linewidth)),
420
+ name=f"{symbol}",
421
+ showlegend=False,
422
+ row=1,
423
+ col=1,
424
+ secondary_y=False,
425
+ hoverinfo="x+y",
426
+ )
427
+ else:
428
+ fig.add_scatter(
429
+ x=self.df_stock.index,
430
+ y=self.df_stock[self.close_column], # type: ignore
431
+ name=f"{symbol}",
432
+ connectgaps=True,
433
+ row=1,
434
+ col=1,
435
+ secondary_y=False,
436
+ )
437
+ fig.update_layout(yaxis=dict(nticks=15))
438
+ if self.theme:
439
+ self.inchart_colors = self.theme.get_colors()[1:]
440
+
441
+ fig.set_title(symbol, x=0.5, y=0.98, xanchor="center", yanchor="top")
442
+ return fig
443
+
444
+ def plot_fig( # noqa: PLR0912
445
+ self,
446
+ fig: Optional[OpenBBFigure] = None,
447
+ symbol: str = "",
448
+ candles: bool = True,
449
+ volume_ticks_x: int = 7,
450
+ ) -> OpenBBFigure:
451
+ """Plot indicators on plotly figure.
452
+
453
+ Parameters
454
+ ----------
455
+ fig : OpenBBFigure, optional
456
+ Plotly figure to plot indicators on, by default None
457
+ symbol : str, optional
458
+ Symbol to plot, by default uses the dataframe.name attribute if available or ""
459
+ candles : bool, optional
460
+ Plot a candlestick chart, by default True (if False, plots a line chart)
461
+ volume_ticks_x : int, optional
462
+ Number to multiply volume, by default 7
463
+
464
+ Returns
465
+ -------
466
+ fig : OpenBBFigure
467
+ Plotly figure with candlestick/line chart and volume bar chart (if enabled)
468
+ """
469
+ self.df_ta = self.calculate_indicators()
470
+
471
+ symbol = ( # type: ignore
472
+ self.df_stock.name
473
+ if hasattr(self.df_stock, "name") and not symbol
474
+ else symbol
475
+ )
476
+
477
+ figure = self.init_plot(symbol, candles) if fig is None else fig
478
+ subplot_row, fig_new = 2, {}
479
+ inchart_index, ma_done = 0, False
480
+
481
+ figure = self.process_fig(figure, volume_ticks_x)
482
+
483
+ # Aroon indicator is always plotted first since it has 2 subplot rows.
484
+ # ATR messes up the volume layout so we plot it last.
485
+ plot_indicators = sorted(
486
+ self.indicators.get_active_ids(),
487
+ key=lambda x: (
488
+ 50
489
+ if x == "aroon"
490
+ else 1000 if x == "atr" else 999 if x in self.subplots else 1
491
+ ),
492
+ )
493
+
494
+ for indicator in plot_indicators:
495
+ try:
496
+ if indicator in self.subplots:
497
+ figure, subplot_row = getattr(self, f"plot_{indicator}")(
498
+ figure, self.df_ta, subplot_row
499
+ )
500
+ elif indicator in self.ma_mode or indicator in self.inchart:
501
+ if indicator in self.ma_mode:
502
+ if ma_done:
503
+ continue
504
+ indicator, ma_done = "ma", True # noqa
505
+
506
+ figure, inchart_index = getattr(self, f"plot_{indicator}")(
507
+ figure, self.df_ta, inchart_index
508
+ )
509
+ figure.layout.annotations = None
510
+ elif indicator in ["fib", "srlines", "demark", "clenow", "ichimoku"]:
511
+ figure = getattr(self, f"plot_{indicator}")(figure, self.df_ta)
512
+ else:
513
+ raise ValueError(f"Unknown indicator: {indicator}")
514
+
515
+ fig_new.update(figure.to_plotly_json())
516
+
517
+ remaining_subplots = (
518
+ list(
519
+ set(plot_indicators[plot_indicators.index(indicator) + 1 :])
520
+ - set(self.inchart)
521
+ )
522
+ if indicator != "ma"
523
+ else []
524
+ )
525
+ if subplot_row > 5 and remaining_subplots:
526
+ warnings.warn(
527
+ f"[bold red]Reached max number of subplots. "
528
+ f"Skipping {', '.join(remaining_subplots)}[/]"
529
+ )
530
+ break
531
+ except Exception as e:
532
+ warnings.warn(f"[bold red]Error plotting {indicator}: {e}[/]")
533
+ continue
534
+
535
+ for row in range(0, subplot_row + 1):
536
+ figure.update_yaxes(
537
+ row=row,
538
+ col=1,
539
+ secondary_y=False,
540
+ nticks=15 if subplot_row < 3 else 6,
541
+ tickfont=dict(size=12),
542
+ )
543
+ figure.update_traces(
544
+ selector=dict(type="scatter", mode="lines"), connectgaps=True
545
+ )
546
+ if hasattr(figure, "hide_holidays"):
547
+ figure.hide_holidays(self.prepost) # type: ignore
548
+
549
+ if not self.show_volume:
550
+ figure.update_layout(margin=dict(l=20))
551
+
552
+ # We remove xaxis labels from all but bottom subplot,
553
+ # and we make sure they all match the bottom one
554
+ xbottom = f"y{subplot_row+1}"
555
+ xaxes = list(figure.select_xaxes())
556
+ for xa in xaxes:
557
+ if xa == xaxes[-1]:
558
+ xa.showticklabels = True
559
+ if not xa.showticklabels and xa.anchor != xbottom:
560
+ xa.showticklabels = False
561
+ if xa.anchor != xbottom:
562
+ xa.matches = xbottom.replace("y", "x")
563
+
564
+ fib_legend_shown = False
565
+ sr_legend_shown = False
566
+ for item in figure.data:
567
+ if item.name:
568
+ item.name = item.name.replace("_", " ")
569
+ if "MA " not in item.name:
570
+ item.showlegend = False
571
+ if "<b>" in item.name:
572
+ item.name = "Fib"
573
+ item.hoverinfo = "none"
574
+ item.hoveron = "fills"
575
+ item.pop("hovertemplate", None)
576
+ item.legendgroup = "Fib"
577
+ if not fib_legend_shown:
578
+ item.showlegend = True
579
+ fib_legend_shown = True
580
+ if (
581
+ "Historical" not in item.name
582
+ and "Candlestick" not in item.name
583
+ and "Fib" not in item.name
584
+ and item.name is not None
585
+ ):
586
+ if (
587
+ "MA " in item.name
588
+ or "VWAP" in item.name
589
+ or "DC" in item.name
590
+ or "KC" in item.name
591
+ or "-sen" in item.name
592
+ or "Senkou" in item.name
593
+ ):
594
+ item.showlegend = True
595
+ item.hoverinfo = "y"
596
+ item.hovertemplate = "%{fullData.name}:%{y}<extra></extra>"
597
+ else:
598
+ item.hovertemplate = "%{y}<extra></extra>"
599
+ if item.name is None:
600
+ item.name = "SR Lines"
601
+ item.hoverinfo = "none"
602
+ item.hoveron = "fills"
603
+ item.legendgroup = "SR Lines"
604
+ item.pop("hovertemplate", None)
605
+ item.opacity = 0.5
606
+ if not sr_legend_shown:
607
+ item.showlegend = True
608
+ sr_legend_shown = True
609
+
610
+ if "annotations" in figure.layout:
611
+ for item in figure.layout.annotations: # type: ignore
612
+ item["font"]["size"] = 14
613
+ figure.update_layout(margin=dict(l=50, r=10, b=10, t=20))
614
+ return figure
615
+
616
+ def process_fig(self, fig: OpenBBFigure, volume_ticks_x: int = 7) -> OpenBBFigure:
617
+ """Process plotly figure before plotting indicators.
618
+
619
+ Parameters
620
+ ----------
621
+ fig : OpenBBFigure
622
+ Plotly figure to process
623
+ volume_ticks_x : int, optional
624
+ Number to multiply volume, by default 7
625
+
626
+ Returns
627
+ -------
628
+ fig : OpenBBFigure
629
+ Processed plotly figure
630
+ """
631
+ new_subplot = OpenBBFigure(charting_settings=self.charting_settings)
632
+ new_subplot = fig.create_subplots(
633
+ shared_xaxes=True, **self.get_fig_settings_dict()
634
+ )
635
+ subplots: Dict[str, Dict[str, List[Any]]] = {}
636
+ grid_ref = fig._validate_get_grid_ref() # pylint: disable=protected-access
637
+ for r, plot_row in enumerate(grid_ref):
638
+ for c, plot_refs in enumerate(plot_row):
639
+ if not plot_refs:
640
+ continue
641
+ for subplot_ref in plot_refs:
642
+ if subplot_ref.subplot_type == "xy":
643
+ xaxis, yaxis = subplot_ref.layout_keys
644
+ xref = xaxis.replace("axis", "")
645
+ yref = yaxis.replace("axis", "")
646
+ row = r + 1
647
+ col = c + 1
648
+ subplots.setdefault(xref, {}).setdefault(yref, []).append(
649
+ (row, col)
650
+ )
651
+
652
+ for trace in fig.select_traces():
653
+ xref, yref = trace.xaxis, trace.yaxis
654
+ row, col = subplots[xref][yref][0]
655
+ new_subplot.add_trace(trace, row=row, col=col, secondary_y=False)
656
+
657
+ fig_json = fig.to_plotly_json()["layout"]
658
+ for layout in fig_json:
659
+ if (
660
+ isinstance(fig_json[layout], dict)
661
+ and "domain" in fig_json[layout]
662
+ and any(x in layout for x in ["xaxis", "yaxis"])
663
+ ):
664
+ fig_json[layout]["domain"] = new_subplot.to_plotly_json()["layout"][
665
+ layout
666
+ ]["domain"]
667
+
668
+ fig.layout.update({layout: fig_json[layout]}) # type: ignore
669
+ new_subplot.layout.update({layout: fig.layout[layout]}) # type: ignore
670
+
671
+ if self.show_volume:
672
+ new_subplot.add_inchart_volume(
673
+ self.df_stock, self.close_column, volume_ticks_x=volume_ticks_x # type: ignore
674
+ )
675
+
676
+ return new_subplot
openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/ta_helpers.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper functions for technical analysis indicators."""
2
+
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ if TYPE_CHECKING:
6
+ from pandas import DataFrame
7
+
8
+
9
+ def check_columns(
10
+ data: "DataFrame", high: bool = True, low: bool = True, close: bool = True
11
+ ) -> Optional[str]:
12
+ """Return the close columns, or None if the dataframe does not have required columns.
13
+
14
+ Parameters
15
+ ----------
16
+ data: DataFrame
17
+ The dataframe to check
18
+ high: bool
19
+ Whether to check for high column
20
+ low: bool
21
+ Whether to check for low column
22
+ close: bool
23
+ Whether to check for close column
24
+
25
+ Returns
26
+ -------
27
+ Optional[str]
28
+ The name of the close column, none if df is invalid
29
+ """
30
+ # pylint: disable=import-outside-toplevel
31
+ import re
32
+
33
+ close_regex = r"(Adj\sClose|adj_close|Close)"
34
+ # pylint: disable=too-many-boolean-expressions
35
+ if (
36
+ (re.findall(r"High", str(data.columns), re.IGNORECASE) is None and high)
37
+ or (re.findall(r"Low", str(data.columns), re.IGNORECASE) is None and low)
38
+ or (close_col := re.findall(close_regex, str(data.columns), re.IGNORECASE))
39
+ is None
40
+ and close
41
+ ):
42
+ raise ValueError(
43
+ " Please make sure that the columns 'High', 'Low', and 'Close'"
44
+ " are in the dataframe."
45
+ )
46
+
47
+ close_col = [col for col in close_col if col in data.columns]
48
+
49
+ # giving priority to the standard close column
50
+ if "close" in close_col:
51
+ return "close"
52
+
53
+ return close_col[-1]
openbb_platform/obbject_extensions/charting/openbb_charting/core/table.html ADDED
The diff for this file is too large to render. See raw diff
 
openbb_platform/obbject_extensions/charting/openbb_charting/core/to_chart.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Module containing the to_chart function."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
4
+
5
+ if TYPE_CHECKING:
6
+ from openbb_charting.core.plotly_ta.data_classes import ChartIndicators # noqa
7
+ from openbb_charting.core.openbb_figure import OpenBBFigure # noqa
8
+ from pandas import DataFrame, Series # noqa
9
+
10
+
11
+ def to_chart(
12
+ data: Union["DataFrame", "Series"],
13
+ indicators: Optional[Union["ChartIndicators", Dict[str, Dict[str, Any]]]] = None,
14
+ symbol: str = "",
15
+ candles: bool = True,
16
+ volume: bool = True,
17
+ prepost: bool = False,
18
+ volume_ticks_x: int = 7,
19
+ ) -> Tuple["OpenBBFigure", Dict[str, Any]]:
20
+ """Return the plotly json representation of the chart.
21
+
22
+ This function is used so it can be called at the module level and used out of the box,
23
+ which allows some more flexibility, ease of use and doesn't require the user to know
24
+ about the PlotlyTA class.
25
+
26
+ Parameters
27
+ ----------
28
+ data : Union[DataFrame, Series]
29
+ Data to be plotted.
30
+ indicators : Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]], optional
31
+ Indicators to be plotted, by default None
32
+ symbol : str, optional
33
+ Symbol to be plotted, by default ""
34
+ candles : bool, optional
35
+ If True, candles will be plotted, by default True
36
+ volume : bool, optional
37
+ If True, volume will be plotted, by default True
38
+ prepost : bool, optional
39
+ If True, prepost will be plotted, by default False
40
+ volume_ticks_x : int, optional
41
+ Volume ticks, by default 7
42
+
43
+ Returns
44
+ -------
45
+ Tuple[OpenBBFigure, Dict[str, Any]]
46
+ Tuple containing the OpenBBFigure and the plotly json representation of the chart.
47
+ """
48
+ # pylint: disable=import-outside-toplevel
49
+ from openbb_charting.core.plotly_ta.ta_class import PlotlyTA
50
+
51
+ try:
52
+ ta = PlotlyTA()
53
+ fig = ta.plot(
54
+ df_stock=data,
55
+ indicators=indicators,
56
+ symbol=symbol,
57
+ candles=candles,
58
+ volume=volume,
59
+ prepost=prepost,
60
+ volume_ticks_x=volume_ticks_x,
61
+ )
62
+ content = fig.show(external=True).to_plotly_json()
63
+
64
+ return fig, content
65
+ except Exception as e:
66
+ raise Exception(
67
+ f"Failed to convert results to chart. Ensure the provided data is a valid time series. {e}"
68
+ ) from e
openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py ADDED
@@ -0,0 +1,852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Charting Extension Query Params."""
2
+
3
+ # pylint: disable=unused-variable,unused-argument
4
+
5
+ from typing import Any, Dict, List, Literal, Optional, Union
6
+
7
+ from openbb_core.provider.abstract.data import Data
8
+ from openbb_core.provider.abstract.query_params import QueryParams
9
+ from pydantic import Field, model_validator
10
+
11
+ from openbb_charting.core.plotly_ta.data_classes import ChartIndicators
12
+
13
+ MAMODES = Literal["ema", "sma", "wma", "hna", "zlma", "rma"]
14
+
15
+
16
+ def _get_type_name(t):
17
+ """Get the type name of a type hint."""
18
+ if hasattr(t, "__origin__"):
19
+ if hasattr(t.__origin__, "__name__"):
20
+ return f"{t.__origin__.__name__}[{', '.join([_get_type_name(arg) for arg in t.__args__])}]"
21
+ if hasattr(t.__origin__, "_name"):
22
+ return f"{t.__origin__._name}[{', '.join([_get_type_name(arg) for arg in t.__args__])}]" # pylint: disable=W0212
23
+ if isinstance(t, str):
24
+ return t
25
+ if hasattr(t, "__name__"):
26
+ return t.__name__
27
+ if hasattr(t, "_name"):
28
+ return t._name # pylint: disable=W0212
29
+ return str(t)
30
+
31
+
32
+ class BaseQueryParams(QueryParams):
33
+ """Base Query Parmams Base Model."""
34
+
35
+ def __init__(self, **data):
36
+ """Initialize the BaseQueryParams."""
37
+ super().__init__(**data)
38
+ self.__doc__ = self.__repr__()
39
+
40
+ def __repr__(self):
41
+ """Return the string representation of the model."""
42
+ fields = self.__class__.model_fields
43
+ repr_str = (
44
+ "\n"
45
+ + self.__class__.__name__
46
+ + "\n\n"
47
+ + " Parameters\n"
48
+ + " ----------\n"
49
+ + "\n".join(
50
+ [
51
+ f"\n {k} : {_get_type_name(v.annotation)}\n {v.description}".replace(
52
+ ". ", ".\n "
53
+ )
54
+ for k, v in fields.items()
55
+ ]
56
+ )
57
+ )
58
+ return repr_str
59
+
60
+
61
+ class ChartQueryParams(BaseQueryParams):
62
+ """ChartParams."""
63
+
64
+ data: Optional[Union[Data, List[Data]]] = Field(
65
+ default=None,
66
+ description="Filtered versions of the data contained in the original `self.results`."
67
+ + " Columns should be the same as the original data."
68
+ + " Example use is to reduce the number of columns, or the length of data, to plot.",
69
+ )
70
+
71
+
72
+ class EquityPricePerformanceChartQueryParams(ChartQueryParams):
73
+ """Equity Price Performance Chart Query Params."""
74
+
75
+ title: Optional[str] = Field(
76
+ default=None,
77
+ description="Title of the chart.",
78
+ )
79
+ orientation: Literal["v", "h"] = Field(
80
+ default="v",
81
+ description="Orientation of the bars.",
82
+ )
83
+ limit: Optional[int] = Field(
84
+ default=None,
85
+ description="Limit the number of bars to plot, from the most recent."
86
+ + " If None, the periods from one-day to five-years will be plotted, if available.",
87
+ )
88
+ layout_kwargs: Optional[Dict[str, Any]] = Field(
89
+ default=None,
90
+ description="Additional keyword arguments to pass to the Plotly `update_layout` method.",
91
+ )
92
+
93
+
94
+ class EtfPricePerformanceChartQueryParams(EquityPricePerformanceChartQueryParams):
95
+ """ETF Price Performance Chart Query Params."""
96
+
97
+
98
+ class EtfHoldingsChartQueryParams(ChartQueryParams):
99
+ """ETF Holdings Chart Query Params."""
100
+
101
+ title: Optional[str] = Field(
102
+ default=None,
103
+ description="Title of the chart.",
104
+ )
105
+ orientation: Literal["v", "h"] = Field(
106
+ default="v",
107
+ description="Orientation of the bars.",
108
+ )
109
+ limit: Optional[int] = Field(
110
+ default=20,
111
+ description="Limit the number of bars to plot, ranked by top weighting.",
112
+ )
113
+ layout_kwargs: Optional[Dict[str, Any]] = Field(
114
+ default=None,
115
+ description="Additional keyword arguments to pass to the Plotly `update_layout` method.",
116
+ )
117
+
118
+
119
+ class EquityPriceHistoricalChartQueryParams(ChartQueryParams):
120
+ """Equity Historical Price Chart Query Params."""
121
+
122
+ title: Optional[str] = Field(
123
+ default=None,
124
+ description="Title of the chart.",
125
+ )
126
+ target: Optional[str] = Field(
127
+ default=None,
128
+ description="The specific column to target. If supplied, this will override the candles and volume parameters.",
129
+ )
130
+ multi_symbol: bool = Field(
131
+ default=False,
132
+ description="Flag to indicate whether the data contains multiple symbols."
133
+ + " This is mostly handled automatically, but if the chart fails to generate try setting this to True.",
134
+ )
135
+ same_axis: bool = Field(
136
+ default=False,
137
+ description="If True, forces all data to be plotted on the same axis.",
138
+ )
139
+ normalize: bool = Field(
140
+ default=False,
141
+ description="If True, the data will be normalized and placed on the same axis.",
142
+ )
143
+ returns: bool = Field(
144
+ default=False,
145
+ description="If True, the cumulative returns for the length of the time series will be calculated and plotted.",
146
+ )
147
+ candles: bool = Field(
148
+ default=True,
149
+ description="If True, and OHLC exists, and there is only one symbol in the data, candles will be plotted.",
150
+ )
151
+ heikin_ashi: bool = Field(
152
+ default=False,
153
+ description="If True, and `candles=True`, Heikin Ashi candles will be plotted.",
154
+ )
155
+ volume: bool = Field(
156
+ default=True,
157
+ description="If True, and volume exists, and `candles=True`, volume will be plotted.",
158
+ )
159
+ indicators: Optional[Union[ChartIndicators, Dict[str, Dict[str, Any]]]] = Field(
160
+ default=None,
161
+ description="Indicators to be plotted, formatted as a dictionary."
162
+ + " Data containing multiple symbols will ignore indicators."
163
+ + """
164
+ Example:
165
+ indicators = dict(
166
+ sma=dict(length=[20,30,50]),
167
+ adx=dict(length=14),
168
+ rsi=dict(length=14),
169
+ )""",
170
+ )
171
+
172
+
173
+ class EconomyFredSeriesChartQueryParams(ChartQueryParams):
174
+ """FRED Series Chart Query Params."""
175
+
176
+ title: Optional[str] = Field(
177
+ default=None,
178
+ description="Title of the chart.",
179
+ )
180
+ y1title: Optional[str] = Field(
181
+ default=None,
182
+ description="Right Y-axis title.",
183
+ )
184
+ y2title: Optional[str] = Field(
185
+ default=None,
186
+ description="Left Y-axis title.",
187
+ )
188
+ xtitle: Optional[str] = Field(
189
+ default=None,
190
+ description="X-axis title.",
191
+ )
192
+ dropnan: bool = Field(
193
+ default=True,
194
+ description="If True, rows containing NaN will be dropped.",
195
+ )
196
+ normalize: bool = Field(
197
+ default=False,
198
+ description="If True, the data will be normalized and placed on the same axis.",
199
+ )
200
+ allow_unsafe: bool = Field(
201
+ default=False,
202
+ description="If True, the method will attempt to pass all supplied data to the chart constructor."
203
+ + " This can result in unexpected behavior.",
204
+ )
205
+ plot_bar: bool = Field(
206
+ default=False,
207
+ description="If True, a bar chart will be plotted instead of a line."
208
+ + " If multiple units of measure are present, they will be normalized and plotted on the same axis.",
209
+ )
210
+ barmode: Literal["stack", "group", "relative"] = Field(
211
+ default="group",
212
+ description="The mode to use for the bar chart, by default is 'group'."
213
+ + " Has no effect if `bar=False`.",
214
+ )
215
+ layout_kwargs: Optional[Dict[str, Any]] = Field(
216
+ default=None,
217
+ description="Additional keyword arguments to pass to the Plotly `update_layout` method.",
218
+ )
219
+
220
+
221
+ class TechnicalConesChartQueryParams(ChartQueryParams):
222
+ """Technical Cones Chart Query Params."""
223
+
224
+ title: Optional[str] = Field(
225
+ default=None,
226
+ description="Title of the chart.",
227
+ )
228
+ symbol: Optional[str] = Field(
229
+ default=None,
230
+ description="Symbol represented by the data. Used to label the chart.",
231
+ )
232
+
233
+
234
+ class MAQueryParams(ChartQueryParams):
235
+ """Moving Average Query Params."""
236
+
237
+ target: str = Field(
238
+ default="close",
239
+ description="The column to calculate the moving average on.",
240
+ )
241
+ index: str = Field(
242
+ default="date",
243
+ description="The index column.",
244
+ )
245
+ length: Optional[Union[int, List[int]]] = Field(
246
+ default=50,
247
+ description="Window length for the moving average."
248
+ "+ The number is relative to the interval of the time series data.",
249
+ )
250
+ offset: Optional[int] = Field(
251
+ default=0,
252
+ description="Number of periods to offset for the moving average.",
253
+ )
254
+ dropnan: bool = Field(
255
+ default=False,
256
+ description="If True, rows containing NaN will be dropped."
257
+ + " This will reduce the length of the charted data by the longest window.",
258
+ )
259
+ symbol: Optional[str] = Field(
260
+ default=None,
261
+ description="Symbol represented by the data. Used to label the chart.",
262
+ )
263
+
264
+
265
+ class FixedincomeGovernmentYieldCurve(ChartQueryParams):
266
+ """Fixed Income Government Yield Curve Chart Query Params."""
267
+
268
+ title: Optional[str] = Field(
269
+ default=None,
270
+ description="Title of the chart.",
271
+ )
272
+ colors: Optional[List[str]] = Field(
273
+ default=None,
274
+ description="List of colors to use for the lines.",
275
+ )
276
+ layout_kwargs: Optional[Dict[str, Any]] = Field(
277
+ default=None,
278
+ description="Additional keyword arguments to pass to the Plotly `update_layout` method.",
279
+ )
280
+
281
+
282
+ class TechnicalSMAChartQueryParams(MAQueryParams):
283
+ """Technical SMA Chart Query Params."""
284
+
285
+
286
+ class TechnicalEMAChartQueryParams(MAQueryParams):
287
+ """Technical EMA Chart Query Params."""
288
+
289
+
290
+ class TechnicalHMAChartQueryParams(MAQueryParams):
291
+ """Technical HMA Chart Query Params."""
292
+
293
+
294
+ class TechnicalWMAChartQueryParams(MAQueryParams):
295
+ """Technical WMA Chart Query Params."""
296
+
297
+
298
+ class TechnicalZLMAChartQueryParams(MAQueryParams):
299
+ """Technical ZLMA Chart Query Params."""
300
+
301
+
302
+ class TechnicalADXChartQueryParams(ChartQueryParams):
303
+ """Technical ADX Chart Query Params."""
304
+
305
+ length: Optional[int] = Field(
306
+ default=50,
307
+ description="Window length for the ADX, by default is 50.",
308
+ )
309
+ scalar: Optional[float] = Field(
310
+ default=100,
311
+ description="Scalar to multiply the ADX by, default is 100.",
312
+ )
313
+ drift: Optional[int] = Field(
314
+ default=1,
315
+ description="Drift value for the ADX, by default is 1.",
316
+ )
317
+
318
+
319
+ class TechnicalArooonChartQueryParams(ChartQueryParams):
320
+ """Technical Aroon Chart Query Params."""
321
+
322
+ length: Optional[int] = Field(
323
+ default=25,
324
+ description="Window length for the Aroon, by default is 50.",
325
+ )
326
+ scalar: Optional[float] = Field(
327
+ default=100,
328
+ description="Scalar to multiply the Aroon by, default is 100.",
329
+ )
330
+
331
+
332
+ class TechnicalMACDChartQueryParams(ChartQueryParams):
333
+ """Technical MACD Chart Query Params."""
334
+
335
+ fast: Optional[int] = Field(
336
+ default=12,
337
+ description="Window length for the fast EMA, by default is 12.",
338
+ )
339
+ slow: Optional[int] = Field(
340
+ default=26,
341
+ description="Window length for the slow EMA, by default is 26.",
342
+ )
343
+ signal: Optional[int] = Field(
344
+ default=9,
345
+ description="Window length for the signal line, by default is 9.",
346
+ )
347
+ scalar: Optional[float] = Field(
348
+ default=100,
349
+ description="Scalar to multiply the MACD by, default is 100.",
350
+ )
351
+
352
+
353
+ class TechnicalRSIChartQueryParams(ChartQueryParams):
354
+ """Technical RSI Chart Query Params."""
355
+
356
+ length: Optional[int] = Field(
357
+ default=14,
358
+ description="Window length for the RSI, by default is 14.",
359
+ )
360
+ scalar: Optional[float] = Field(
361
+ default=100,
362
+ description="Scalar to multiply the RSI by, default is 100.",
363
+ )
364
+ drift: Optional[int] = Field(
365
+ default=1,
366
+ description="Drift value for the RSI, by default is 1.",
367
+ )
368
+
369
+
370
+ class TechnicalRelativeRotationChartQueryParams(ChartQueryParams):
371
+ """Technical Relative Rotation Chart Query Params."""
372
+
373
+ date: Optional[str] = Field(
374
+ default=None,
375
+ description="A target end date within the data to use for the chart, by default is the last date in the data.",
376
+ )
377
+ show_tails: bool = Field(
378
+ default=True,
379
+ description="Show the tails on the chart, by default True.",
380
+ )
381
+ tail_periods: Optional[int] = Field(
382
+ default=16,
383
+ description="Number of periods to show in the tails, by default 16.",
384
+ )
385
+ tail_interval: Literal["day", "week", "month"] = Field(
386
+ default="week",
387
+ description="The interval to show the tails, by default 'week'.",
388
+ )
389
+ title: Optional[str] = Field(
390
+ default=None,
391
+ description="Title of the chart.",
392
+ )
393
+
394
+
395
+ class ChartParams:
396
+ """Chart Query Params."""
397
+
398
+ crypto_price_historical = EquityPriceHistoricalChartQueryParams
399
+ derivatives_futures_historical = EquityPriceHistoricalChartQueryParams
400
+ equity_price_historical = EquityPriceHistoricalChartQueryParams
401
+ economy_fred_series = EconomyFredSeriesChartQueryParams
402
+ equity_price_historical = EquityPriceHistoricalChartQueryParams
403
+ equity_price_performance = EquityPricePerformanceChartQueryParams
404
+ etf_historical = EtfPricePerformanceChartQueryParams
405
+ etf_holdings = EtfHoldingsChartQueryParams
406
+ etf_price_performance = EquityPricePerformanceChartQueryParams
407
+ index_price_historical = EquityPriceHistoricalChartQueryParams
408
+ technical_adx = TechnicalADXChartQueryParams
409
+ technical_aroon = TechnicalArooonChartQueryParams
410
+ technical_cones = TechnicalConesChartQueryParams
411
+ technical_ema = TechnicalEMAChartQueryParams
412
+ technical_hma = TechnicalHMAChartQueryParams
413
+ technical_macd = TechnicalMACDChartQueryParams
414
+ technical_relative_rotation = TechnicalRelativeRotationChartQueryParams
415
+ technical_rsi = TechnicalRSIChartQueryParams
416
+ technical_sma = TechnicalSMAChartQueryParams
417
+ technical_wma = TechnicalWMAChartQueryParams
418
+ technical_zlma = TechnicalZLMAChartQueryParams
419
+
420
+
421
+ class IndicatorsQueryParams(BaseQueryParams):
422
+ """Indicators Query Params."""
423
+
424
+
425
+ class MAIndicatorsQueryParams(IndicatorsQueryParams):
426
+ """Moving Average Indicators Query Params."""
427
+
428
+ length: Union[int, List[int]] = Field(
429
+ default=50,
430
+ description="Window length for the moving average, by default is 50."
431
+ + " The number is relative to the interval of the time series data.",
432
+ )
433
+ offset: int = Field(
434
+ default=0,
435
+ description="Number of periods to offset for the moving average, by default is 0.",
436
+ )
437
+
438
+
439
+ class SMAIndicatorsQueryParams(MAIndicatorsQueryParams):
440
+ """Simple Moving Average Indicators Query Params."""
441
+
442
+
443
+ class EMAIndicatorsQueryParams(MAIndicatorsQueryParams):
444
+ """Exponential Moving Average Indicators Query Params."""
445
+
446
+
447
+ class HMAIndicatorsQueryParams(MAIndicatorsQueryParams):
448
+ """Hull Moving Average Indicators Query Params."""
449
+
450
+
451
+ class WMAIndicatorsQueryParams(MAIndicatorsQueryParams):
452
+ """Weighted Moving Average Indicators Query Params."""
453
+
454
+
455
+ class ZLMAIndicatorsQueryParams(MAIndicatorsQueryParams):
456
+ """Zero-Lag Moving Average Indicators Query Params."""
457
+
458
+
459
+ class ADIndicatorsQueryParams(IndicatorsQueryParams):
460
+ """Accumulation/Distribution Indicators Query Params."""
461
+
462
+ offset: int = Field(
463
+ default=0,
464
+ description="Offset value for the AD, by default is 0.",
465
+ )
466
+
467
+
468
+ class ADOscillatorIndicatorsQueryParams(IndicatorsQueryParams):
469
+ """Accumulation/Distribution Oscillator Indicators Query Params."""
470
+
471
+ fast: int = Field(
472
+ default=3,
473
+ description="Number of periods to use for the fast calculation, by default 3.",
474
+ )
475
+ slow: int = Field(
476
+ default=10,
477
+ description="Number of periods to use for the slow calculation, by default 10.",
478
+ )
479
+ offset: int = Field(
480
+ default=0,
481
+ description="Offset to be used for the calculation, by default is 0.",
482
+ )
483
+
484
+
485
+ class ADXIndicatorsQueryParams(IndicatorsQueryParams):
486
+ """Average Directional Index Indicators Query Params."""
487
+
488
+ length: int = Field(
489
+ default=50,
490
+ description="Window length for the ADX, by default is 50.",
491
+ )
492
+ scalar: float = Field(
493
+ default=100,
494
+ description="Scalar to multiply the ADX by, default is 100.",
495
+ )
496
+ drift: int = Field(
497
+ default=1,
498
+ description="Drift value for the ADX, by default is 1.",
499
+ )
500
+
501
+
502
+ class AroonIndicatorsQueryParams(IndicatorsQueryParams):
503
+ """Aroon Indicators Query Params."""
504
+
505
+ length: int = Field(
506
+ default=25,
507
+ description="Window length for the Aroon, by default is 50.",
508
+ )
509
+ scalar: float = Field(
510
+ default=100,
511
+ description="Scalar to multiply the Aroon by, default is 100.",
512
+ )
513
+
514
+
515
+ class ATRIndicatorsQueryParams(IndicatorsQueryParams):
516
+ """Average True Range Indicators Query Params."""
517
+
518
+ length: int = Field(
519
+ default=14,
520
+ description="Window length for the ATR, by default is 14.",
521
+ )
522
+ mamode: Literal["rma", "ema", "sma", "wma"] = Field(
523
+ default="rma",
524
+ description="The mode to use for the moving average calculation.",
525
+ )
526
+ drift: int = Field(
527
+ default=1,
528
+ description="The difference period.",
529
+ )
530
+ offset: int = Field(
531
+ default=0,
532
+ description="Number of periods to offset the result, by default is 0.",
533
+ )
534
+
535
+
536
+ class CCIIndicatorsQueryParams(IndicatorsQueryParams):
537
+ """Commodity Channel Index Indicators Query Params."""
538
+
539
+ length: int = Field(
540
+ default=14,
541
+ description="Window length for the CCI, by default is 14.",
542
+ )
543
+ scalar: float = Field(
544
+ default=0.015,
545
+ description="Scalar to multiply the CCI by, default is 0.015.",
546
+ )
547
+
548
+
549
+ class DonchianIndicatorsQueryParams(IndicatorsQueryParams):
550
+ """Donchian Channel Indicators Query Params."""
551
+
552
+ lower: Optional[int] = Field(
553
+ default=20,
554
+ description="Window length for the lower band, by default is 20.",
555
+ )
556
+ upper: Optional[int] = Field(
557
+ default=20,
558
+ description="Window length for the upper band, by default is 20.",
559
+ )
560
+ offset: Optional[int] = Field(
561
+ default=0,
562
+ description="Number of periods to offset the result, by default is 0.",
563
+ )
564
+
565
+
566
+ class FisherIndicatorsQueryParams(IndicatorsQueryParams):
567
+ """Fisher Transform Indicators Query Params."""
568
+
569
+ length: int = Field(
570
+ default=14,
571
+ description="Window length for the Fisher Transform, by default is 14.",
572
+ )
573
+ signal: int = Field(
574
+ default=1,
575
+ description="Fisher Signal Period",
576
+ )
577
+
578
+
579
+ class KCIndicatorsQueryParams(IndicatorsQueryParams):
580
+ """Keltner Channel Indicators Query Params."""
581
+
582
+ length: int = Field(
583
+ default=20,
584
+ description="Window length for the Keltner Channel, by default is 20.",
585
+ )
586
+ scalar: float = Field(
587
+ default=2,
588
+ description="Scalar to multiply the ATR, by default is 2.",
589
+ )
590
+ mamode: MAMODES = Field(
591
+ default="rma",
592
+ description="The mode to use for the moving average calculation, by default is ema.",
593
+ )
594
+ offset: int = Field(
595
+ default=0,
596
+ description="Number of periods to offset the result, by default is 0.",
597
+ )
598
+
599
+
600
+ class MACDIndicatorsQueryParams(IndicatorsQueryParams):
601
+ """MACD Indicators Query Params."""
602
+
603
+ fast: Optional[int] = Field(
604
+ default=12,
605
+ description="Window length for the fast EMA, by default is 12.",
606
+ )
607
+ slow: Optional[int] = Field(
608
+ default=26,
609
+ description="Window length for the slow EMA, by default is 26.",
610
+ )
611
+ signal: Optional[int] = Field(
612
+ default=9,
613
+ description="Window length for the signal line, by default is 9.",
614
+ )
615
+ scalar: Optional[float] = Field(
616
+ default=100,
617
+ description="Scalar to multiply the MACD by, default is 100.",
618
+ )
619
+
620
+
621
+ class OBVIndicatorsQueryParams(IndicatorsQueryParams):
622
+ """On Balance Volume Indicators Query Params."""
623
+
624
+ offset: int = Field(
625
+ default=0,
626
+ description="Number of periods to offset the result, by default is 0.",
627
+ )
628
+
629
+
630
+ class RSIIndicatorsQueryParams(IndicatorsQueryParams):
631
+ """RSI Indicators Query Params."""
632
+
633
+ length: int = Field(
634
+ default=14,
635
+ description="Window length for the RSI, by default is 14.",
636
+ )
637
+ scalar: float = Field(
638
+ default=100,
639
+ description="Scalar to multiply the RSI by, default is 100.",
640
+ )
641
+ drift: int = Field(
642
+ default=1,
643
+ description="Drift value for the RSI, by default is 1.",
644
+ )
645
+
646
+
647
+ class StochIndicatorsQueryParams(IndicatorsQueryParams):
648
+ """Stochastic Oscillator Indicators Query Params."""
649
+
650
+ fast_k: int = Field(
651
+ default=14,
652
+ description="The fast K period, by default 14.",
653
+ )
654
+ slow_d: int = Field(
655
+ default=3,
656
+ description="The slow D period, by default 3.",
657
+ )
658
+ slow_k: int = Field(
659
+ default=3,
660
+ description="The slow K period, by default 3.",
661
+ )
662
+
663
+
664
+ class FibIndicatorsQueryParams(IndicatorsQueryParams):
665
+ """Fibonacci Retracement Indicators Query Params."""
666
+
667
+ period: int = Field(
668
+ default=120,
669
+ description="The period to calculate the Fibonacci Retracement, by default 120.",
670
+ )
671
+ start_date: Optional[str] = Field(
672
+ default=None,
673
+ description="The start date for the Fibonacci Retracement.",
674
+ )
675
+ end_date: Optional[str] = Field(
676
+ default=None,
677
+ description="The end date for the Fibonacci Retracement.",
678
+ )
679
+
680
+
681
+ class ClenowIndicatorsQueryParams(IndicatorsQueryParams):
682
+ """Clenow Volatility Adjusted Momentum Indicators Query Params."""
683
+
684
+ period: int = Field(
685
+ default=90,
686
+ description="The number of periods for the momentum, by default 90.",
687
+ )
688
+
689
+
690
+ class DemarkIndicatorsQueryParams(IndicatorsQueryParams):
691
+ """Demark Indicators Query Params."""
692
+
693
+ show_all: bool = Field(
694
+ default=False,
695
+ description="Show 1 - 13. If set to False, show 6 - 9.",
696
+ )
697
+ offset: int = Field(
698
+ default=0,
699
+ description="Number of periods to offset the result, by default is 0.",
700
+ )
701
+
702
+
703
+ class IchimokuIndicatorsQueryParams(IndicatorsQueryParams):
704
+ """Ichimoku Cloud Indicators Query Params."""
705
+
706
+ conversion: int = Field(
707
+ default=9,
708
+ description="The conversion line period, by default 9.",
709
+ )
710
+ base: int = Field(
711
+ default=26,
712
+ description="The base line period, by default 26.",
713
+ )
714
+ lagging: int = Field(
715
+ default=52,
716
+ description="The lagging line period, by default 52.",
717
+ )
718
+ offset: int = Field(
719
+ default=26,
720
+ description="The offset period, by default 26.",
721
+ )
722
+ lookahead: bool = Field(
723
+ default=False,
724
+ description="Drops the Chikou Span Column to prevent potential data leak",
725
+ )
726
+
727
+
728
+ class SRLinesIndicatorsQueryParams(IndicatorsQueryParams):
729
+ """Support and Resistance Lines Indicators Query Params."""
730
+
731
+ show: bool = Field(
732
+ default=True,
733
+ description="Show the support and resistance lines.",
734
+ )
735
+
736
+
737
+ class IndicatorsParams(QueryParams):
738
+ """Indicators Query Params."""
739
+
740
+ sma: SMAIndicatorsQueryParams = Field(
741
+ default=SMAIndicatorsQueryParams(),
742
+ description=repr(SMAIndicatorsQueryParams()),
743
+ )
744
+ ema: EMAIndicatorsQueryParams = Field(
745
+ default=EMAIndicatorsQueryParams(),
746
+ description=repr(EMAIndicatorsQueryParams()),
747
+ )
748
+ hma: HMAIndicatorsQueryParams = Field(
749
+ default=HMAIndicatorsQueryParams(),
750
+ description=repr(HMAIndicatorsQueryParams()),
751
+ )
752
+ wma: WMAIndicatorsQueryParams = Field(
753
+ default=WMAIndicatorsQueryParams(),
754
+ description=repr(WMAIndicatorsQueryParams()),
755
+ )
756
+ zlma: ZLMAIndicatorsQueryParams = Field(
757
+ default=ZLMAIndicatorsQueryParams(),
758
+ description=repr(ZLMAIndicatorsQueryParams()),
759
+ )
760
+ ad: ADIndicatorsQueryParams = Field(
761
+ default=ADIndicatorsQueryParams(),
762
+ description=repr(ADIndicatorsQueryParams()),
763
+ )
764
+ adoscillator: ADOscillatorIndicatorsQueryParams = Field(
765
+ default=ADOscillatorIndicatorsQueryParams(),
766
+ description=repr(ADOscillatorIndicatorsQueryParams()),
767
+ )
768
+ adx: ADXIndicatorsQueryParams = Field(
769
+ default=ADXIndicatorsQueryParams(),
770
+ description=repr(ADXIndicatorsQueryParams()),
771
+ )
772
+ aroon: AroonIndicatorsQueryParams = Field(
773
+ default=AroonIndicatorsQueryParams(),
774
+ description=repr(AroonIndicatorsQueryParams()),
775
+ )
776
+ atr: ATRIndicatorsQueryParams = Field(
777
+ default=ATRIndicatorsQueryParams(),
778
+ description=repr(ATRIndicatorsQueryParams()),
779
+ )
780
+ cci: CCIIndicatorsQueryParams = Field(
781
+ default=CCIIndicatorsQueryParams(),
782
+ description=repr(CCIIndicatorsQueryParams()),
783
+ )
784
+ clenow: ClenowIndicatorsQueryParams = Field(
785
+ default=ClenowIndicatorsQueryParams(),
786
+ description=repr(ClenowIndicatorsQueryParams()),
787
+ )
788
+ demark: DemarkIndicatorsQueryParams = Field(
789
+ default=DemarkIndicatorsQueryParams(),
790
+ description=repr(DemarkIndicatorsQueryParams()),
791
+ )
792
+ donchian: DonchianIndicatorsQueryParams = Field(
793
+ default=DonchianIndicatorsQueryParams(),
794
+ description=repr(DonchianIndicatorsQueryParams()),
795
+ )
796
+ fib: FibIndicatorsQueryParams = Field(
797
+ default=FibIndicatorsQueryParams(),
798
+ description=repr(FibIndicatorsQueryParams()),
799
+ )
800
+ fisher: FisherIndicatorsQueryParams = Field(
801
+ default=FisherIndicatorsQueryParams(),
802
+ description=repr(FisherIndicatorsQueryParams()),
803
+ )
804
+ ichimoku: IchimokuIndicatorsQueryParams = Field(
805
+ default=IchimokuIndicatorsQueryParams(),
806
+ description=repr(IchimokuIndicatorsQueryParams()),
807
+ )
808
+ kc: KCIndicatorsQueryParams = Field(
809
+ default=KCIndicatorsQueryParams(),
810
+ description=repr(KCIndicatorsQueryParams()),
811
+ )
812
+ macd: MACDIndicatorsQueryParams = Field(
813
+ default=MACDIndicatorsQueryParams(),
814
+ description=repr(MACDIndicatorsQueryParams()),
815
+ )
816
+ obv: OBVIndicatorsQueryParams = Field(
817
+ default=OBVIndicatorsQueryParams(),
818
+ description=repr(OBVIndicatorsQueryParams()),
819
+ )
820
+ rsi: RSIIndicatorsQueryParams = Field(
821
+ default=RSIIndicatorsQueryParams(),
822
+ description=repr(RSIIndicatorsQueryParams()),
823
+ )
824
+ srlines: SRLinesIndicatorsQueryParams = Field(
825
+ default=SRLinesIndicatorsQueryParams(),
826
+ description=repr(SRLinesIndicatorsQueryParams()),
827
+ )
828
+ stoch: StochIndicatorsQueryParams = Field(
829
+ default=StochIndicatorsQueryParams(),
830
+ description=repr(StochIndicatorsQueryParams()),
831
+ )
832
+
833
+ def __repr__(self):
834
+ """Return the string representation of the model."""
835
+ fields = self.__class__.model_fields
836
+ repr_str = "\n" + "\n".join(
837
+ [
838
+ f"{str(v.description).replace('IndicatorsQueryParams', ':').replace('ADOs', 'AD Os')}"
839
+ for k, v in fields.items()
840
+ ]
841
+ )
842
+ return repr_str
843
+
844
+ @model_validator(mode="before")
845
+ @classmethod
846
+ def validate_model(cls, values):
847
+ """Validate the model."""
848
+ indicators = list(ChartIndicators.get_available_indicators())
849
+ for k, v in values.items():
850
+ if k not in indicators:
851
+ raise ValueError(f"{k} is not a valid indicator.")
852
+ return values
openbb_platform/obbject_extensions/charting/openbb_charting/styles/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Charting Extension Styles"""
openbb_platform/obbject_extensions/charting/openbb_charting/styles/colors.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Color Palettes and Sequences for OpenBB Charting"""
2
+
3
+ LARGE_CYCLER = [
4
+ "#1f77b4",
5
+ "#7f7f7f",
6
+ "#ff7f0e",
7
+ "#2ca02c",
8
+ "#d62728",
9
+ "#9467bd",
10
+ "#8c564b",
11
+ "#e377c2",
12
+ "#bcbd22",
13
+ "#17becf",
14
+ "burlywood",
15
+ "magenta",
16
+ "cyan",
17
+ "yellowgreen",
18
+ "#aec7e8",
19
+ "#ffbb78",
20
+ "#ff9896",
21
+ "#c5b0d5",
22
+ "#c49c94",
23
+ "#f7b6d2",
24
+ "#c7c7c7",
25
+ "#dbdb8d",
26
+ "#9edae5",
27
+ "#7e7e7e",
28
+ "#1b9e77",
29
+ "#d95f02",
30
+ "#7570b3",
31
+ "#e7298a",
32
+ "#66a61e",
33
+ "#e6ab02",
34
+ "#a6761d",
35
+ "#666666",
36
+ "#f0027f",
37
+ "#bf5b17",
38
+ "#d9f202",
39
+ ]
openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/dark.pltstyle.json ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "line": {
3
+ "up_color": "#00ACFF",
4
+ "down_color": "#e4003a",
5
+ "color": "#ffed00",
6
+ "width": 1.5
7
+ },
8
+ "data": {
9
+ "candlestick": [
10
+ {
11
+ "decreasing": {
12
+ "fillcolor": "#e4003a",
13
+ "line": {
14
+ "color": "#e4003a"
15
+ }
16
+ },
17
+ "increasing": {
18
+ "fillcolor": "#00ACFF",
19
+ "line": {
20
+ "color": "#00ACFF"
21
+ }
22
+ },
23
+ "type": "candlestick"
24
+ }
25
+ ]
26
+ },
27
+ "layout": {
28
+ "annotationdefaults": {
29
+ "showarrow": false
30
+ },
31
+ "autotypenumbers": "strict",
32
+ "colorway": [
33
+ "#1f77b4",
34
+ "#ff7f0e",
35
+ "#2ca02c",
36
+ "#d62728",
37
+ "#9467bd",
38
+ "#8c564b",
39
+ "#e377c2",
40
+ "#bcbd22",
41
+ "#17becf",
42
+ "#aec7e8",
43
+ "#ffbb78",
44
+ "#ff9896",
45
+ "#c5b0d5",
46
+ "#f7b6d2",
47
+ "#dbdb8d",
48
+ "#9edae5"
49
+ ],
50
+ "dragmode": "pan",
51
+ "font": {
52
+ "family": "Arial",
53
+ "size": 18
54
+ },
55
+ "hoverlabel": {
56
+ "align": "left"
57
+ },
58
+ "mapbox": {
59
+ "style": "dark"
60
+ },
61
+ "hovermode": "x",
62
+ "legend": {
63
+ "bgcolor": "rgba(0, 0, 0, 0)",
64
+ "x": 1,
65
+ "xanchor": "right",
66
+ "y": 1.02,
67
+ "yanchor": "bottom",
68
+ "font": {
69
+ "size": 12
70
+ }
71
+ },
72
+ "legend2": {
73
+ "bgcolor": "rgba(0, 0, 0, 0.5)",
74
+ "font": {
75
+ "size": 15
76
+ }
77
+ },
78
+ "legend3": {
79
+ "bgcolor": "rgba(0, 0, 0, 0.5)",
80
+ "font": {
81
+ "size": 15
82
+ }
83
+ },
84
+ "legend4": {
85
+ "bgcolor": "rgba(0, 0, 0, 0.5)",
86
+ "font": {
87
+ "size": 15
88
+ }
89
+ },
90
+ "legend5": {
91
+ "bgcolor": "rgba(0, 0, 0, 0.5)",
92
+ "font": {
93
+ "size": 15
94
+ }
95
+ },
96
+ "paper_bgcolor": "#000000",
97
+ "plot_bgcolor": "#000000",
98
+ "xaxis": {
99
+ "automargin": true,
100
+ "autorange": true,
101
+ "rangeslider": {
102
+ "visible": false
103
+ },
104
+ "showgrid": true,
105
+ "showline": true,
106
+ "tickfont": {
107
+ "size": 14
108
+ },
109
+ "zeroline": false,
110
+ "tick0": 1,
111
+ "title": {
112
+ "standoff": 20,
113
+ "font": {
114
+ "size": 16
115
+ }
116
+ },
117
+ "linecolor": "#F5EFF3",
118
+ "mirror": true,
119
+ "ticks": "outside"
120
+ },
121
+ "yaxis": {
122
+ "anchor": "x",
123
+ "automargin": true,
124
+ "fixedrange": false,
125
+ "zeroline": false,
126
+ "showgrid": true,
127
+ "showline": true,
128
+ "side": "right",
129
+ "tick0": 0.5,
130
+ "title": {
131
+ "standoff": 20,
132
+ "font": {
133
+ "size": 16
134
+ }
135
+ },
136
+
137
+ "gridcolor": "rgba(128, 128, 128, 0.33)",
138
+ "linecolor": "#F5EFF3",
139
+ "mirror": true,
140
+ "ticks": "outside"
141
+ }
142
+ }
143
+ }
openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/light.pltstyle.json ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "line": {
3
+ "up_color": "#0074D9",
4
+ "down_color": "#FF4136",
5
+ "color": "#111111",
6
+ "width": 1.5
7
+ },
8
+ "data": {
9
+ "candlestick": [
10
+ {
11
+ "decreasing": {
12
+ "fillcolor": "#e4003a",
13
+ "line": {
14
+ "color": "#e4003a"
15
+ }
16
+ },
17
+ "increasing": {
18
+ "fillcolor": "#00ACFF",
19
+ "line": {
20
+ "color": "#00ACFF"
21
+ }
22
+ },
23
+ "type": "candlestick"
24
+ }
25
+ ]
26
+ },
27
+ "layout": {
28
+ "annotationdefaults": {
29
+ "showarrow": false
30
+ },
31
+ "autotypenumbers": "strict",
32
+ "colorway": [
33
+ "#1f77b4",
34
+ "#ff7f0e",
35
+ "#2ca02c",
36
+ "#d62728",
37
+ "#9467bd",
38
+ "#8c564b",
39
+ "#e377c2",
40
+ "#bcbd22",
41
+ "#17becf",
42
+ "#aec7e8",
43
+ "#ffbb78",
44
+ "#ff9896",
45
+ "#c5b0d5",
46
+ "#f7b6d2",
47
+ "#dbdb8d",
48
+ "#9edae5"
49
+ ],
50
+ "dragmode": "pan",
51
+ "font": {
52
+ "family": "Arial",
53
+ "size": 18
54
+ },
55
+ "hoverlabel": {
56
+ "align": "left"
57
+ },
58
+ "mapbox": {
59
+ "style": "light"
60
+ },
61
+ "hovermode": "x",
62
+ "legend": {
63
+ "bgcolor": "rgba(255, 255, 255, 0)",
64
+ "x": 1,
65
+ "xanchor": "right",
66
+ "y": 1.02,
67
+ "yanchor": "bottom",
68
+ "font": {
69
+ "size": 12
70
+ }
71
+ },
72
+ "legend2": {
73
+ "bgcolor": "rgba(255, 255, 255, 0.5)",
74
+ "font": {
75
+ "size": 15
76
+ }
77
+ },
78
+ "legend3": {
79
+ "bgcolor": "rgba(255, 255, 255, 0.5)",
80
+ "font": {
81
+ "size": 15
82
+ }
83
+ },
84
+ "legend4": {
85
+ "bgcolor": "rgba(255, 255, 255, 0.5)",
86
+ "font": {
87
+ "size": 15
88
+ }
89
+ },
90
+ "legend5": {
91
+ "bgcolor": "rgba(255, 255, 255, 0.5)",
92
+ "font": {
93
+ "size": 15
94
+ }
95
+ },
96
+ "paper_bgcolor": "#FFFFFF",
97
+ "plot_bgcolor": "#FFFFFF",
98
+ "xaxis": {
99
+ "automargin": true,
100
+ "autorange": true,
101
+ "rangeslider": {
102
+ "visible": false
103
+ },
104
+ "showgrid": true,
105
+ "showline": true,
106
+ "tickfont": {
107
+ "size": 14
108
+ },
109
+ "zeroline": false,
110
+ "tick0": 1,
111
+ "title": {
112
+ "standoff": 20,
113
+ "font": {
114
+ "size": 16
115
+ }
116
+ },
117
+ "linecolor": "#A9A9A9",
118
+ "mirror": true,
119
+ "ticks": "outside"
120
+ },
121
+ "yaxis": {
122
+ "anchor": "x",
123
+ "automargin": true,
124
+ "fixedrange": false,
125
+ "zeroline": false,
126
+ "showgrid": true,
127
+ "showline": true,
128
+ "side": "right",
129
+ "tick0": 0.5,
130
+ "title": {
131
+ "standoff": 20,
132
+ "font": {
133
+ "size": 16
134
+ }
135
+ },
136
+ "gridcolor": "rgba(128, 128, 128, 0.33)",
137
+ "linecolor": "#A9A9A9",
138
+ "mirror": true,
139
+ "ticks": "outside"
140
+ }
141
+ }
142
+ }
openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/tables.pltstyle.json ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "data": {
3
+ "candlestick": [
4
+ {
5
+ "decreasing": {
6
+ "fillcolor": "#e4003a",
7
+ "line": {
8
+ "color": "#e4003a"
9
+ }
10
+ },
11
+ "increasing": {
12
+ "fillcolor": "#00ACFF",
13
+ "line": {
14
+ "color": "#00ACFF"
15
+ }
16
+ },
17
+ "type": "candlestick"
18
+ }
19
+ ]
20
+ },
21
+ "layout": {
22
+ "annotationdefaults": {
23
+ "showarrow": false
24
+ },
25
+ "autotypenumbers": "strict",
26
+ "colorway": [
27
+ "#ffed00",
28
+ "#ef7d00",
29
+ "#e4003a",
30
+ "#c13246",
31
+ "#822661",
32
+ "#48277c",
33
+ "#005ca9",
34
+ "#00aaff",
35
+ "#9b30d9",
36
+ "#af005f",
37
+ "#5f00af",
38
+ "#af87ff"
39
+ ],
40
+ "dragmode": "pan",
41
+ "font": {
42
+ "family": "Fira Code",
43
+ "size": 18
44
+ },
45
+ "hoverlabel": {
46
+ "align": "left"
47
+ },
48
+ "mapbox": {
49
+ "style": "light"
50
+ },
51
+ "hovermode": "x",
52
+ "legend": {
53
+ "bgcolor": "rgba(0, 0, 0, 0)",
54
+ "x": 0.01,
55
+ "xanchor": "left",
56
+ "y": 0.99,
57
+ "yanchor": "top",
58
+ "font": {
59
+ "size": 15
60
+ }
61
+ },
62
+ "paper_bgcolor": "white",
63
+ "plot_bgcolor": "white",
64
+ "xaxis": {
65
+ "automargin": true,
66
+ "autorange": true,
67
+ "rangeslider": {
68
+ "visible": false
69
+ },
70
+ "showgrid": true,
71
+ "showline": true,
72
+ "tickfont": {
73
+ "size": 14
74
+ },
75
+ "zeroline": false,
76
+ "tick0": 1,
77
+ "title": {
78
+ "standoff": 20
79
+ },
80
+ "linecolor": "#F5EFF3",
81
+ "mirror": true,
82
+ "ticks": "outside"
83
+ },
84
+ "yaxis": {
85
+ "anchor": "x",
86
+ "automargin": true,
87
+ "fixedrange": false,
88
+ "zeroline": false,
89
+ "showgrid": true,
90
+ "showline": true,
91
+ "side": "right",
92
+ "tick0": 0.5,
93
+ "title": {
94
+ "standoff": 20
95
+ },
96
+ "gridcolor": "#283442",
97
+ "linecolor": "#F5EFF3",
98
+ "mirror": true,
99
+ "ticks": "outside"
100
+ }
101
+ }
102
+ }
openbb_platform/obbject_extensions/charting/poetry.lock ADDED
The diff for this file is too large to render. See raw diff
 
openbb_platform/obbject_extensions/charting/pyproject.toml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "openbb-charting"
3
+ version = "2.3.4"
4
+ description = "Charting extension for OpenBB"
5
+ authors = ["OpenBB Team <hello@openbb.co>"]
6
+ license = "AGPL-3.0-only"
7
+ readme = "README.md"
8
+ packages = [{ include = "openbb_charting" }]
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.9.21,<3.13" # scipy forces python <4.0 explicitly
12
+ openbb-core = "^1.4.7"
13
+ pandas-ta-openbb = "^0.4.20"
14
+ plotly = "^6.1.1"
15
+ pywry = { version = "^0.6.2", optional = true }
16
+ nbformat = "^5.10.0"
17
+
18
+ [tool.poetry.extras]
19
+ pywry = ["pywry"]
20
+
21
+ [build-system]
22
+ requires = ["poetry-core"]
23
+ build-backend = "poetry.core.masonry.api"
24
+
25
+ [tool.poetry.plugins."openbb_obbject_extension"]
26
+ openbb_charting = "openbb_charting:ext"
openbb_platform/obbject_extensions/charting/tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """OBBject Extensions Charting Tests Module."""