Upload 51 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .gitattributes +1 -0
- openbb_platform/obbject_extensions/charting/README.md +174 -0
- openbb_platform/obbject_extensions/charting/examples.md +166 -0
- openbb_platform/obbject_extensions/charting/index.md +359 -0
- openbb_platform/obbject_extensions/charting/indicators.md +384 -0
- openbb_platform/obbject_extensions/charting/installation.md +71 -0
- openbb_platform/obbject_extensions/charting/integration/test_charting_api.py +908 -0
- openbb_platform/obbject_extensions/charting/integration/test_charting_python.py +747 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/__init__.py +25 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charting.py +681 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charts/__init__.py +1 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charts/correlation_matrix.py +119 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charts/generic_charts.py +654 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charts/helpers.py +123 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charts/price_historical.py +335 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charts/price_performance.py +108 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/charts/relative_rotation.py +674 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/__init__.py +1 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/assets/Terminal_icon.png +3 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/assets/plotly-3.0.0.min.js +0 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/backend.py +445 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/chart_style.py +229 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/config/__init__.py +1 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/config/openbb_styles.py +308 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/dummy_backend.py +56 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/openbb_figure.py +1662 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly.html +0 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/__init__.py +1 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/base.py +220 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/data_classes.py +395 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/__init__.py +1 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/custom_indicators_plugin.py +218 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/momentum_plugin.py +629 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/overlap_plugin.py +96 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/trend_indicators_plugin.py +197 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/volatility_plugin.py +223 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/plugins/volume_plugin.py +125 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/ta_class.py +676 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/plotly_ta/ta_helpers.py +53 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/table.html +0 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/core/to_chart.py +68 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/query_params.py +852 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/styles/__init__.py +1 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/styles/colors.py +39 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/dark.pltstyle.json +143 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/light.pltstyle.json +142 -0
- openbb_platform/obbject_extensions/charting/openbb_charting/styles/default/tables.pltstyle.json +102 -0
- openbb_platform/obbject_extensions/charting/poetry.lock +0 -0
- openbb_platform/obbject_extensions/charting/pyproject.toml +26 -0
- 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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 |
+

|
| 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
|
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."""
|