v1.0.13
NEW FEATURE: Polygon.io Full integration - Added `polygon` to the common methods, allowing for data to be pulled from polygon.io. (`chart.polygon.<method>`) - Added the `PolygonChart` object, which allows for a plug and play solution with the Polygon API. - Check the docs for more details and examples! Enhancements: - Added `clear_markers` and `clear_horizontal_lines` to the common methods. - Added the `maximize` parameter to the `Chart` object, which maximizes the chart window when shown. - The Legend will now show Line values, and can be disabled using the `lines` parameter. - Added the `name` parameter to the `set` method of line, using the column within the dataframe as the value and using its name within the legend. - Added the `scale_candles_only` parameter to all Chart objects, which prevents the autoscaling of Lines. - new `screenshot` method, which returns a bytes object of the displayed chart. Fixes: - `chart.lines()` now returns a copy of the list rather than the original.
This commit is contained in:
@ -24,14 +24,10 @@ ___
|
||||
1. Simple and easy to use.
|
||||
2. Blocking or non-blocking GUI.
|
||||
3. Streamlined for live data, with methods for updating directly from tick data.
|
||||
4. Supports:
|
||||
* PyQt -> [`QtChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#qtchart)
|
||||
* wxPython -> [`WxChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#wxchart)
|
||||
* Streamlit -> [`StreamlitChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#streamlitchart)
|
||||
* asyncio -> [`show_async()`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#show-async)
|
||||
* Jupyter Notebooks -> [`JupyterChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#jupyterchart)
|
||||
4. __Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
|
||||
5. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#callbacks) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more.
|
||||
6. Multi-Pane Charts using the [`SubChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart).
|
||||
7. Direct integration of market data through [Polygon.io's](https://polygon.io) market data API.
|
||||
___
|
||||
|
||||
### 1. Display data from a csv:
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
project = 'lightweight-charts-python'
|
||||
copyright = '2023, louisnw'
|
||||
author = 'louisnw'
|
||||
release = '1.0.12'
|
||||
release = '1.0.13'
|
||||
|
||||
extensions = ["myst_parser"]
|
||||
|
||||
|
||||
@ -99,6 +99,16 @@ ___
|
||||
Removes a horizontal line at the given price.
|
||||
___
|
||||
|
||||
### `clear_markers`
|
||||
|
||||
Clears the markers displayed on the data.
|
||||
___
|
||||
|
||||
### `clear_horizontal_lines`
|
||||
|
||||
Clears the horizontal lines displayed on the data.
|
||||
___
|
||||
|
||||
### `price_scale`
|
||||
`mode: 'normal'/'logarithmic'/'percentage'/'index100'` | `align_labels: bool` | `border_visible: bool` | `border_color: str` | `text_color: str` | `entire_text_only: bool` | `ticks_visible: bool` | `scale_margin_top: float` | `scale_margin_bottom: float`
|
||||
|
||||
@ -169,7 +179,7 @@ Sets the title label for the chart.
|
||||
___
|
||||
|
||||
### `legend`
|
||||
`visible: bool` | `ohlc: bool` | `percent: bool` | `color: str` | `font_size: int` | `font_family: str`
|
||||
`visible: bool` | `ohlc: bool` | `percent: bool` | `lines: bool` | `color: str` | `font_size: int` | `font_family: str`
|
||||
|
||||
Configures the legend of the chart.
|
||||
___
|
||||
@ -201,7 +211,9 @@ ___
|
||||
Shows the hidden candles on the chart.
|
||||
___
|
||||
|
||||
|
||||
### `polygon`
|
||||
Used to access Polygon.io's API (see [here](https://lightweight-charts-python.readthedocs.io/en/latest/polygon.html))
|
||||
___
|
||||
|
||||
### `create_subchart`
|
||||
`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `-> SubChart`
|
||||
@ -222,7 +234,7 @@ ___
|
||||
|
||||
|
||||
## Chart
|
||||
`volume_enabled: bool` | `width: int` | `height: int` | `x: int` | `y: int` | `on_top: bool` | `debug: bool` |
|
||||
`volume_enabled: bool` | `width: int` | `height: int` | `x: int` | `y: int` | `on_top: bool` | `maximize: bool` | `debug: bool` |
|
||||
`api: object` | `topbar: bool` | `searchbox: bool`
|
||||
|
||||
The main object used for the normal functionality of lightweight-charts-python, built on the pywebview library.
|
||||
@ -250,6 +262,27 @@ ___
|
||||
|
||||
Show the chart asynchronously. This should be utilised when using [Callbacks](#callbacks).
|
||||
|
||||
### `screenshot`
|
||||
`-> bytes`
|
||||
|
||||
Takes a screenshot of the chart, and returns a bytes object containing the image. For example:
|
||||
|
||||
```python
|
||||
if __name__ == '__main__':
|
||||
chart = Chart()
|
||||
df = pd.read_csv('ohlcv.csv')
|
||||
chart.set(df)
|
||||
chart.show()
|
||||
|
||||
img = chart.screenshot()
|
||||
with open('screenshot.png', 'wb') as f:
|
||||
f.write(img)
|
||||
```
|
||||
|
||||
```{important}
|
||||
This method must be called after the chart window is open.
|
||||
```
|
||||
|
||||
___
|
||||
|
||||
## Line
|
||||
@ -264,11 +297,21 @@ The `line` object should only be accessed from the [`create_line`](#create-line)
|
||||
___
|
||||
|
||||
### `set`
|
||||
`data: pd.DataFrame`
|
||||
`data: pd.DataFrame` `name: str`
|
||||
|
||||
Sets the data for the line.
|
||||
|
||||
This should be given as a DataFrame, with the columns: `time | value`
|
||||
When not using the `name` parameter, the columns should be named: `time | value`.
|
||||
|
||||
Otherwise, the method will use the column named after the string given in `name`. This name will also be used within the legend of the chart. For example:
|
||||
```python
|
||||
line = chart.create_line()
|
||||
|
||||
# DataFrame with columns: date | SMA 50
|
||||
df = pd.read_csv('sma50.csv')
|
||||
|
||||
line.set(df, name='SMA 50')
|
||||
```
|
||||
___
|
||||
|
||||
### `update`
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
```{toctree}
|
||||
:hidden:
|
||||
:caption: Contents
|
||||
:maxdepth: 3
|
||||
|
||||
docs
|
||||
polygon
|
||||
Github Repository <https://github.com/louisnw01/lightweight-charts-python>
|
||||
```
|
||||
|
||||
```{include} ../../README.md
|
||||
```
|
||||
|
||||
|
||||
135
docs/source/polygon.md
Normal file
135
docs/source/polygon.md
Normal file
@ -0,0 +1,135 @@
|
||||
# Polygon.io
|
||||
|
||||
[Polygon.io's](https://polygon.io) market data API is directly integrated within lightweight-charts-python, and is easy to use within the library.
|
||||
___
|
||||
## Requirements
|
||||
To use data from Polygon, there are certain libraries (not listed as requirements) that must be installed:
|
||||
* Static data requires the `requests` library.
|
||||
* Live data requires the `websockets` library.
|
||||
___
|
||||
## `polygon`
|
||||
`polygon` is a [Common Method](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#common-methods), and can be accessed from within any chart type.
|
||||
|
||||
`chart.polygon.<method>`
|
||||
|
||||
The `stock`, `option`, `index`, `forex`, and `crypto` methods of `chart.polygon` have common parameters:
|
||||
|
||||
* `timeframe`: The timeframe to be used (`'1min'`, `'5min'`, `'H'`, `'2D'`, `'5W'` etc.)
|
||||
* `start_date`: The start date given in the format `YYYY-MM-DD`.
|
||||
* `end_date`: The end date given in the same format. By default this is `'now'`, which uses the time now.
|
||||
* `limit`: The maximum number of base aggregates to be queried to create the aggregate results.
|
||||
* `live`: When set to `True`, a websocket connection will be used to update the chart or subchart in real-time.
|
||||
* These methods will also return a boolean representing whether the request was successful.
|
||||
|
||||
```{important}
|
||||
When using live data and the standard `show` method, the `block` parameter __must__ be set to `True` in order for the data to congregate on the chart (`chart.show(block=True)`).
|
||||
If `show_async` is used with live data, `block` can be either value.
|
||||
|
||||
```
|
||||
___
|
||||
|
||||
### Example:
|
||||
|
||||
```python
|
||||
from lightweight_charts import Chart
|
||||
|
||||
if __name__ == '__main__':
|
||||
chart = Chart()
|
||||
chart.polygon.api_key('<API-KEY>')
|
||||
chart.polygon.stock(
|
||||
symbol='AAPL',
|
||||
timeframe='5min',
|
||||
start_date='2023-06-09'
|
||||
)
|
||||
chart.show(block=True)
|
||||
```
|
||||
___
|
||||
|
||||
### `api_key`
|
||||
`key: str`
|
||||
|
||||
Sets the API key for the chart. Subsequent `SubChart` objects will inherit the API key given to the parent chart.
|
||||
___
|
||||
### `stock`
|
||||
`symbol: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
|
||||
|
||||
Requests and displays stock data pulled from Polygon.io.
|
||||
___
|
||||
|
||||
### `option`
|
||||
`symbol: str` | `timeframe: str` | `start_date: str` | `expiration` | `right: 'C' | 'P'` | `strike: int | float` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
|
||||
|
||||
Requests and displays option data pulled from Polygon.io.
|
||||
|
||||
A formatted option ticker (SPY251219C00650000) can also be given to the `symbol` parameter, allowing for `expiration`, `right`, and `strike` to be left blank.
|
||||
___
|
||||
|
||||
### `index`
|
||||
`symbol: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
|
||||
|
||||
Requests and displays index data pulled from Polygon.io.
|
||||
|
||||
___
|
||||
|
||||
### `forex`
|
||||
`fiat_pair: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
|
||||
|
||||
Requests and displays a forex pair pulled from Polygon.io.
|
||||
|
||||
The two currencies should be separated by a '-' (`USD-CAD`, `GBP-JPY`, etc.).
|
||||
|
||||
___
|
||||
|
||||
### `crypto`
|
||||
`crypto_pair: str` | `timeframe: str` | `start_date: str` | `end_date: str` | `limit: int` | `live: bool` | `-> bool`
|
||||
|
||||
Requests and displays a crypto pair pulled from Polygon.io.
|
||||
|
||||
The two currencies should be separated by a '-' (`BTC-USD`, `ETH-BTC`, etc.).
|
||||
|
||||
___
|
||||
|
||||
### `log`
|
||||
`info: bool`
|
||||
|
||||
If `True`, informational log messages (connection, subscriptions etc.) will be displayed in the console.
|
||||
|
||||
Data errors will always be shown in the console.
|
||||
___
|
||||
|
||||
## PolygonChart
|
||||
|
||||
`api_key: str` | `live: bool` | `num_bars: int`
|
||||
|
||||
The `PolygonChart` provides an easy and complete way to use the Polygon.io API within lightweight-charts-python.
|
||||
|
||||
This object requires the `requests` library for static data, and the `websockets` library for live data.
|
||||
|
||||
All data is requested within the chart window through searching and selectors.
|
||||
|
||||
As well as the parameters from the CHART LINK object, PolygonChart also has the parameters:
|
||||
|
||||
* `api_key`: The user's Polygon.io API key.
|
||||
* `num_bars`: The target number of bars to be displayed on the chart
|
||||
* `limit`: The maximum number of base aggregates to be queried to create the aggregate results.
|
||||
* `end_date`: The end date of the time window.
|
||||
* `timeframe_options`: The selectors to be included within the timeframe selector.
|
||||
* `security_options`: The selectors to be included within the security selector.
|
||||
* `live`: If True, the chart will update in real-time.
|
||||
___
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
from lightweight_charts import PolygonChart
|
||||
|
||||
if __name__ == '__main__':
|
||||
chart = PolygonChart(api_key='<API-KEY>',
|
||||
num_bars=200,
|
||||
limit=5000,
|
||||
live=True)
|
||||
chart.show(block=True)
|
||||
```
|
||||
|
||||

|
||||
|
||||
BIN
docs/source/polygonchart.png
Normal file
BIN
docs/source/polygonchart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
@ -1,4 +1,4 @@
|
||||
from .js import LWC
|
||||
from .abstract import LWC
|
||||
from .chart import Chart
|
||||
from .widgets import JupyterChart
|
||||
from .polygon import PolygonChart
|
||||
|
||||
@ -1,12 +1,51 @@
|
||||
import pandas as pd
|
||||
import os
|
||||
from datetime import timedelta, datetime
|
||||
from typing import Union, Literal, Dict
|
||||
from base64 import b64decode
|
||||
import pandas as pd
|
||||
from typing import Union, Literal, Dict, List
|
||||
|
||||
from lightweight_charts.pkg import LWC_4_0_1
|
||||
from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_style, \
|
||||
from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, \
|
||||
_line_style, \
|
||||
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _marker_position, _marker_shape, IDGen
|
||||
|
||||
|
||||
JS = {}
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
for file in ('pkg', 'funcs', 'callback'):
|
||||
with open(os.path.join(current_dir, 'js', f'{file}.js'), 'r') as f:
|
||||
JS[file] = f.read()
|
||||
|
||||
HTML = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<title>lightweight-charts-python</title>
|
||||
<script>{JS['pkg']}</script>
|
||||
<meta name="viewport" content ="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}}
|
||||
#wrapper {{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #000000;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper"></div>
|
||||
<script>
|
||||
{JS['funcs']}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
class SeriesCommon:
|
||||
def _set_interval(self, df: pd.DataFrame):
|
||||
common_interval = pd.to_datetime(df['time']).diff().value_counts()
|
||||
@ -14,6 +53,11 @@ class SeriesCommon:
|
||||
self._interval = common_interval.index[0]
|
||||
except IndexError:
|
||||
raise IndexError('Not enough bars within the given data to calculate the interval/timeframe.')
|
||||
self.run_script(f'''
|
||||
if ({self.id}.toolBox) {{
|
||||
{self.id}.toolBox.interval = {self._interval.total_seconds()*1000}
|
||||
}}
|
||||
''')
|
||||
|
||||
def _df_datetime_format(self, df: pd.DataFrame):
|
||||
df = df.copy()
|
||||
@ -95,12 +139,27 @@ class SeriesCommon:
|
||||
Removes a horizontal line at the given price.
|
||||
"""
|
||||
self.run_script(f'''
|
||||
{self.id}.horizontal_lines.forEach(function (line) {{
|
||||
if ({price} === line.price) {{
|
||||
{self.id}.series.removePriceLine(line.line);
|
||||
{self.id}.horizontal_lines.splice({self.id}.horizontal_lines.indexOf(line), 1)
|
||||
}}
|
||||
}});''')
|
||||
{self.id}.horizontal_lines.forEach(function (line) {{
|
||||
if ({price} === line.price) {{
|
||||
{self.id}.series.removePriceLine(line.line);
|
||||
{self.id}.horizontal_lines.splice({self.id}.horizontal_lines.indexOf(line), 1)
|
||||
}}
|
||||
}});''')
|
||||
|
||||
def clear_markers(self):
|
||||
"""
|
||||
Clears the markers displayed on the data.\n
|
||||
"""
|
||||
self.run_script(f'''{self.id}.markers = []; {self.id}.series.setMarkers([]])''')
|
||||
|
||||
def clear_horizontal_lines(self):
|
||||
"""
|
||||
Clears the horizontal lines displayed on the data.\n
|
||||
"""
|
||||
self.run_script(f'''
|
||||
{self.id}.horizontal_lines.forEach(function (line) {{{self.id}.series.removePriceLine(line.line);}});
|
||||
{self.id}.horizontal_lines = [];
|
||||
''')
|
||||
|
||||
def title(self, title: str): self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})')
|
||||
|
||||
@ -123,30 +182,43 @@ class SeriesCommon:
|
||||
|
||||
|
||||
class Line(SeriesCommon):
|
||||
def __init__(self, parent, color, width, price_line, price_label):
|
||||
self._parent = parent
|
||||
self._rand = self._parent._rand
|
||||
def __init__(self, chart, color, width, price_line, price_label):
|
||||
self.color = color
|
||||
self.name = ''
|
||||
self._chart = chart
|
||||
self._rand = chart._rand
|
||||
self.id = f'window.{self._rand.generate()}'
|
||||
self.run_script = self._parent.run_script
|
||||
self.run_script = self._chart.run_script
|
||||
self.run_script(f'''
|
||||
{self.id} = {{
|
||||
series: {self._parent.id}.chart.addLineSeries({{
|
||||
series: {self._chart.id}.chart.addLineSeries({{
|
||||
color: '{color}',
|
||||
lineWidth: {width},
|
||||
lastValueVisible: {_js_bool(price_label)},
|
||||
priceLineVisible: {_js_bool(price_line)},
|
||||
{"""autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),""" if self._chart._scale_candles_only else ''}
|
||||
}}),
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
}}
|
||||
''')
|
||||
}}''')
|
||||
|
||||
def set(self, data: pd.DataFrame):
|
||||
def set(self, data: pd.DataFrame, name=None):
|
||||
"""
|
||||
Sets the line data.\n
|
||||
:param data: columns: date/time, value
|
||||
:param data: If the name parameter is not used, the columns should be named: date/time, value.
|
||||
:param name: The column of the DataFrame to use as the line value. When used, the Line will be named after this column.
|
||||
"""
|
||||
df = self._parent._df_datetime_format(data)
|
||||
df = self._df_datetime_format(data)
|
||||
if name:
|
||||
if name not in data:
|
||||
raise NameError(f'No column named "{name}".')
|
||||
self.name = name
|
||||
df = df.rename(columns={name: 'value'})
|
||||
self._last_bar = df.iloc[-1]
|
||||
self.run_script(f'{self.id}.series.setData({df.to_dict("records")})')
|
||||
|
||||
@ -155,7 +227,7 @@ class Line(SeriesCommon):
|
||||
Updates the line data.\n
|
||||
:param series: labels: date/time, value
|
||||
"""
|
||||
series = self._parent._series_datetime_format(series)
|
||||
series = self._series_datetime_format(series)
|
||||
self._last_bar = series
|
||||
self.run_script(f'{self.id}.series.update({series.to_dict()})')
|
||||
|
||||
@ -163,9 +235,9 @@ class Line(SeriesCommon):
|
||||
"""
|
||||
Irreversibly deletes the line, as well as the object that contains the line.
|
||||
"""
|
||||
self._parent._lines.remove(self)
|
||||
self._chart._lines.remove(self)
|
||||
self.run_script(f'''
|
||||
{self._parent.id}.chart.removeSeries({self.id}.series)
|
||||
{self._chart.id}.chart.removeSeries({self.id}.series)
|
||||
delete {self.id}
|
||||
''')
|
||||
del self
|
||||
@ -174,7 +246,7 @@ class Line(SeriesCommon):
|
||||
class Widget:
|
||||
def __init__(self, topbar):
|
||||
self._chart = topbar._chart
|
||||
self.method = None
|
||||
self._method = None
|
||||
|
||||
|
||||
class TextWidget(Widget):
|
||||
@ -193,7 +265,7 @@ class SwitcherWidget(Widget):
|
||||
def __init__(self, topbar, method, *options, default):
|
||||
super().__init__(topbar)
|
||||
self.value = default
|
||||
self.method = method.__name__
|
||||
self._method = method.__name__
|
||||
self._chart.run_script(f'''
|
||||
makeSwitcher({self._chart.id}, {list(options)}, '{default}', {self._chart._js_api_code}, '{method.__name__}',
|
||||
'{topbar.active_background_color}', '{topbar.active_text_color}', '{topbar.text_color}', '{topbar.hover_color}')
|
||||
@ -224,13 +296,15 @@ class TopBar:
|
||||
|
||||
def _widget_with_method(self, method_name):
|
||||
for widget in self._widgets.values():
|
||||
if widget.method == method_name:
|
||||
if widget._method == method_name:
|
||||
return widget
|
||||
|
||||
|
||||
class LWC(SeriesCommon):
|
||||
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
||||
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False,
|
||||
scale_candles_only: bool = False):
|
||||
self.volume_enabled = volume_enabled
|
||||
self._scale_candles_only = scale_candles_only
|
||||
self._inner_width = inner_width
|
||||
self._inner_height = inner_height
|
||||
self._dynamic_loading = dynamic_loading
|
||||
@ -248,6 +322,7 @@ class LWC(SeriesCommon):
|
||||
self._charts = {self.id: self}
|
||||
self._lines = []
|
||||
self._js_api_code = None
|
||||
self._return_q = None
|
||||
|
||||
self._background_color = '#000000'
|
||||
self._volume_up_color = 'rgba(83,141,131,0.8)'
|
||||
@ -340,7 +415,7 @@ class LWC(SeriesCommon):
|
||||
timer = null;
|
||||
}}, 50);
|
||||
}});
|
||||
''') if self._dynamic_loading else self.run_script(f'{self.id}.series.setData({bars})')
|
||||
''') if self._dynamic_loading else self.run_script(f'{self.id}.candleData = {bars}; {self.id}.series.setData({self.id}.candleData)')
|
||||
|
||||
def fit(self):
|
||||
"""
|
||||
@ -423,12 +498,11 @@ class LWC(SeriesCommon):
|
||||
self._lines.append(Line(self, color, width, price_line, price_label))
|
||||
return self._lines[-1]
|
||||
|
||||
def lines(self):
|
||||
def lines(self) -> List[Line]:
|
||||
"""
|
||||
Returns all lines for the chart.
|
||||
:return:
|
||||
"""
|
||||
return self._lines
|
||||
return self._lines.copy()
|
||||
|
||||
def price_scale(self, mode: PRICE_SCALE_MODE = 'normal', align_labels: bool = True, border_visible: bool = False,
|
||||
border_color: str = None, text_color: str = None, entire_text_only: bool = False,
|
||||
@ -461,7 +535,6 @@ class LWC(SeriesCommon):
|
||||
secondsVisible: {_js_bool(seconds_visible)},
|
||||
borderVisible: {_js_bool(border_visible)},
|
||||
{f'borderColor: "{border_color}",' if border_color else ''}
|
||||
|
||||
}}
|
||||
}})''')
|
||||
|
||||
@ -584,39 +657,64 @@ class LWC(SeriesCommon):
|
||||
}}
|
||||
}})''')
|
||||
|
||||
def legend(self, visible: bool = False, ohlc: bool = True, percent: bool = True, color: str = None,
|
||||
def legend(self, visible: bool = False, ohlc: bool = True, percent: bool = True, lines: bool = True, color: str = None,
|
||||
font_size: int = None, font_family: str = None):
|
||||
"""
|
||||
Configures the legend of the chart.
|
||||
"""
|
||||
if visible:
|
||||
self.run_script(f'''
|
||||
{f"{self.id}.legend.style.color = '{color}'" if color else ''}
|
||||
{f"{self.id}.legend.style.fontSize = {font_size}" if font_size else ''}
|
||||
{f"{self.id}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
|
||||
if not visible:
|
||||
return
|
||||
lines_code = ''
|
||||
for i, line in enumerate(self._lines):
|
||||
lines_code += f'''finalString += `<br><span style="color: {line.color};">▨</span>{f' {line.name}'} :
|
||||
${{legendItemFormat(param.seriesData.get({line.id}.series).value)}}`;'''
|
||||
|
||||
{self.id}.chart.subscribeCrosshairMove((param) => {{
|
||||
if (param.time){{
|
||||
let data = param.seriesData.get({self.id}.series);
|
||||
if (!data) {{return}}
|
||||
let ohlc = `O ${{legendItemFormat(data.open)}}
|
||||
| H ${{legendItemFormat(data.high)}}
|
||||
| L ${{legendItemFormat(data.low)}}
|
||||
| C ${{legendItemFormat(data.close)}} `
|
||||
let percentMove = ((data.close-data.open)/data.open)*100
|
||||
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
|
||||
let finalString = ''
|
||||
{'finalString += ohlc' if ohlc else ''}
|
||||
{'finalString += percent' if percent else ''}
|
||||
{self.id}.legend.innerHTML = finalString
|
||||
}}
|
||||
else {{
|
||||
{self.id}.legend.innerHTML = ''
|
||||
}}
|
||||
}});''')
|
||||
self.run_script(f'''
|
||||
{f"{self.id}.legend.style.color = '{color}'" if color else ''}
|
||||
{f"{self.id}.legend.style.fontSize = {font_size}" if font_size else ''}
|
||||
{f"{self.id}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
|
||||
|
||||
{self.id}.chart.subscribeCrosshairMove((param) => {{
|
||||
if (param.time){{
|
||||
let data = param.seriesData.get({self.id}.series);
|
||||
if (!data) {{return}}
|
||||
let ohlc = `O ${{legendItemFormat(data.open)}}
|
||||
| H ${{legendItemFormat(data.high)}}
|
||||
| L ${{legendItemFormat(data.low)}}
|
||||
| C ${{legendItemFormat(data.close)}} `
|
||||
let percentMove = ((data.close-data.open)/data.open)*100
|
||||
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
|
||||
let finalString = '<span style="line-height: 1.8;">'
|
||||
{'finalString += ohlc' if ohlc else ''}
|
||||
{'finalString += percent' if percent else ''}
|
||||
{lines_code if lines else ''}
|
||||
{self.id}.legend.innerHTML = finalString+'</span>'
|
||||
}}
|
||||
else {{
|
||||
{self.id}.legend.innerHTML = ''
|
||||
}}
|
||||
}});''')
|
||||
|
||||
def spinner(self, visible): self.run_script(f"{self.id}.spinner.style.display = '{'block' if visible else 'none'}'")
|
||||
|
||||
def screenshot(self) -> bytes:
|
||||
"""
|
||||
Takes a screenshot. This method can only be used after the chart window is visible.
|
||||
:return: a bytes object containing a screenshot of the chart.
|
||||
"""
|
||||
self.run_script(f'''
|
||||
let canvas = {self.id}.chart.takeScreenshot()
|
||||
canvas.toBlob(function(blob) {{
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {{
|
||||
{self._js_api_code}(`return__{self.id}__${{event.target.result}}`)
|
||||
}};
|
||||
reader.readAsDataURL(blob);
|
||||
}})
|
||||
''')
|
||||
serial_data = self._return_q.get()
|
||||
return b64decode(serial_data.split(',')[1])
|
||||
|
||||
def create_subchart(self, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
||||
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False,
|
||||
topbar: bool = False, searchbox: bool = False):
|
||||
@ -633,6 +731,7 @@ class SubChart(LWC):
|
||||
self._position = position
|
||||
self._rand = self._chart._rand
|
||||
self._js_api_code = self._chart._js_api_code
|
||||
self._return_q = self._chart._return_q
|
||||
self.run_script = self._chart.run_script
|
||||
self._charts = self._chart._charts
|
||||
self.id = f'window.{self._rand.generate()}'
|
||||
@ -655,407 +754,4 @@ class SubChart(LWC):
|
||||
''', run_last=True)
|
||||
|
||||
|
||||
SCRIPT = """
|
||||
document.getElementById('wrapper').style.backgroundColor = '#000000'
|
||||
|
||||
function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
let chart = {
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
div: document.createElement('div'),
|
||||
wrapper: document.createElement('div'),
|
||||
legend: document.createElement('div'),
|
||||
scale: {
|
||||
width: innerWidth,
|
||||
height: innerHeight
|
||||
},
|
||||
}
|
||||
chart.chart = LightweightCharts.createChart(chart.div, {
|
||||
width: window.innerWidth*innerWidth,
|
||||
height: window.innerHeight*innerHeight,
|
||||
layout: {
|
||||
textColor: '#d1d4dc',
|
||||
background: {
|
||||
color:'#000000',
|
||||
type: LightweightCharts.ColorType.Solid,
|
||||
},
|
||||
fontSize: 12
|
||||
},
|
||||
rightPriceScale: {
|
||||
scaleMargins: {top: 0.3, bottom: 0.25},
|
||||
},
|
||||
timeScale: {timeVisible: true, secondsVisible: false},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
vertLine: {
|
||||
labelBackgroundColor: 'rgb(46, 46, 46)'
|
||||
},
|
||||
horzLine: {
|
||||
labelBackgroundColor: 'rgb(55, 55, 55)'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
vertLines: {color: 'rgba(29, 30, 38, 5)'},
|
||||
horzLines: {color: 'rgba(29, 30, 58, 5)'},
|
||||
},
|
||||
handleScroll: {vertTouchDrag: true},
|
||||
})
|
||||
let up = 'rgba(39, 157, 130, 100)'
|
||||
let down = 'rgba(200, 97, 100, 100)'
|
||||
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
||||
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
||||
})
|
||||
chart.volumeSeries = chart.chart.addHistogramSeries({
|
||||
color: '#26a69a',
|
||||
priceFormat: {type: 'volume'},
|
||||
priceScaleId: '',
|
||||
})
|
||||
chart.series.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.2, bottom: 0.2},
|
||||
});
|
||||
chart.volumeSeries.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.8, bottom: 0},
|
||||
});
|
||||
chart.legend.style.position = 'absolute'
|
||||
chart.legend.style.zIndex = 1000
|
||||
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
|
||||
chart.legend.style.top = '10px'
|
||||
chart.legend.style.left = '10px'
|
||||
chart.legend.style.fontFamily = 'Monaco'
|
||||
chart.legend.style.fontSize = '11px'
|
||||
chart.legend.style.color = 'rgb(191, 195, 203)'
|
||||
|
||||
chart.wrapper.style.width = `${100*innerWidth}%`
|
||||
chart.wrapper.style.height = `${100*innerHeight}%`
|
||||
chart.div.style.position = 'relative'
|
||||
chart.wrapper.style.display = 'flex'
|
||||
chart.wrapper.style.flexDirection = 'column'
|
||||
|
||||
chart.div.appendChild(chart.legend)
|
||||
chart.wrapper.appendChild(chart.div)
|
||||
document.getElementById('wrapper').append(chart.wrapper)
|
||||
|
||||
if (!autoSize) {
|
||||
return chart
|
||||
}
|
||||
let topBarOffset = 0
|
||||
window.addEventListener('resize', function() {
|
||||
if ('topBar' in chart) {
|
||||
topBarOffset = chart.topBar.offsetHeight
|
||||
}
|
||||
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
|
||||
});
|
||||
return chart
|
||||
}
|
||||
function makeHorizontalLine(chart, lineId, price, color, width, style, axisLabelVisible, text) {
|
||||
let priceLine = {
|
||||
price: price,
|
||||
color: color,
|
||||
lineWidth: width,
|
||||
lineStyle: style,
|
||||
axisLabelVisible: axisLabelVisible,
|
||||
title: text,
|
||||
};
|
||||
let line = {
|
||||
line: chart.series.createPriceLine(priceLine),
|
||||
price: price,
|
||||
id: lineId,
|
||||
};
|
||||
chart.horizontal_lines.push(line)
|
||||
}
|
||||
function legendItemFormat(num) {
|
||||
return num.toFixed(2).toString().padStart(8, ' ')
|
||||
}
|
||||
function syncCrosshairs(childChart, parentChart) {
|
||||
let parent = 0
|
||||
let child = 0
|
||||
|
||||
let parentCrosshairHandler = (e) => {
|
||||
parent ++
|
||||
if (parent < 10) {
|
||||
return
|
||||
}
|
||||
child = 0
|
||||
parentChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: true,
|
||||
labelVisible: true,
|
||||
}}})
|
||||
childChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: false,
|
||||
labelVisible: false,
|
||||
}}})
|
||||
|
||||
childChart.unsubscribeCrosshairMove(childCrosshairHandler)
|
||||
if (e.time !== undefined) {
|
||||
let xx = childChart.timeScale().timeToCoordinate(e.time);
|
||||
childChart.setCrosshairXY(xx,300,true);
|
||||
} else if (e.point !== undefined){
|
||||
childChart.setCrosshairXY(e.point.x,300,false);
|
||||
}
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
|
||||
let childCrosshairHandler = (e) => {
|
||||
child ++
|
||||
if (child < 10) {
|
||||
return
|
||||
}
|
||||
parent = 0
|
||||
childChart.applyOptions({crosshair: {horzLine: {
|
||||
visible: true,
|
||||
labelVisible: true,
|
||||
}}})
|
||||
parentChart.applyOptions({crosshair: {horzLine: {
|
||||
visible: false,
|
||||
labelVisible: false,
|
||||
}}})
|
||||
|
||||
parentChart.unsubscribeCrosshairMove(parentCrosshairHandler)
|
||||
if (e.time !== undefined) {
|
||||
let xx = parentChart.timeScale().timeToCoordinate(e.time);
|
||||
parentChart.setCrosshairXY(xx,300,true);
|
||||
} else if (e.point !== undefined){
|
||||
parentChart.setCrosshairXY(e.point.x,300,false);
|
||||
}
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
}
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
"""
|
||||
|
||||
HTML = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<title>lightweight-charts-python</title>
|
||||
<script>{LWC_4_0_1}</script>
|
||||
<meta name="viewport" content ="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}}
|
||||
#wrapper {{
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper"></div>
|
||||
<script>
|
||||
{SCRIPT}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
CALLBACK_SCRIPT = '''
|
||||
function makeSearchBox(chart, callbackFunction) {
|
||||
let searchWindow = document.createElement('div')
|
||||
searchWindow.style.position = 'absolute'
|
||||
searchWindow.style.top = '0'
|
||||
searchWindow.style.bottom = '200px'
|
||||
searchWindow.style.left = '0'
|
||||
searchWindow.style.right = '0'
|
||||
searchWindow.style.margin = 'auto'
|
||||
searchWindow.style.width = '150px'
|
||||
searchWindow.style.height = '30px'
|
||||
searchWindow.style.padding = '10px'
|
||||
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
||||
searchWindow.style.border = '2px solid #3C434C'
|
||||
searchWindow.style.zIndex = '1000'
|
||||
searchWindow.style.display = 'none'
|
||||
searchWindow.style.borderRadius = '5px'
|
||||
|
||||
let magnifyingGlass = document.createElement('span');
|
||||
magnifyingGlass.style.display = 'inline-block';
|
||||
magnifyingGlass.style.width = '12px';
|
||||
magnifyingGlass.style.height = '12px';
|
||||
magnifyingGlass.style.border = '2px solid rgb(240, 240, 240)';
|
||||
magnifyingGlass.style.borderRadius = '50%';
|
||||
magnifyingGlass.style.position = 'relative';
|
||||
let handle = document.createElement('span');
|
||||
handle.style.display = 'block';
|
||||
handle.style.width = '7px';
|
||||
handle.style.height = '2px';
|
||||
handle.style.backgroundColor = 'rgb(240, 240, 240)';
|
||||
handle.style.position = 'absolute';
|
||||
handle.style.top = 'calc(50% + 7px)';
|
||||
handle.style.right = 'calc(50% - 11px)';
|
||||
handle.style.transform = 'rotate(45deg)';
|
||||
|
||||
let sBox = document.createElement('input');
|
||||
sBox.type = 'text';
|
||||
sBox.style.position = 'relative';
|
||||
sBox.style.display = 'inline-block';
|
||||
sBox.style.zIndex = '1000';
|
||||
sBox.style.textAlign = 'center'
|
||||
sBox.style.width = '100px'
|
||||
sBox.style.marginLeft = '15px'
|
||||
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.3)'
|
||||
sBox.style.color = 'rgb(240,240,240)'
|
||||
sBox.style.fontSize = '20px'
|
||||
sBox.style.border = 'none'
|
||||
sBox.style.outline = 'none'
|
||||
sBox.style.borderRadius = '2px'
|
||||
|
||||
searchWindow.appendChild(magnifyingGlass)
|
||||
magnifyingGlass.appendChild(handle)
|
||||
searchWindow.appendChild(sBox)
|
||||
chart.div.appendChild(searchWindow);
|
||||
|
||||
let yPrice = null
|
||||
chart.chart.subscribeCrosshairMove((param) => {
|
||||
if (param.point){
|
||||
yPrice = param.point.y;
|
||||
}
|
||||
});
|
||||
let selectedChart = true
|
||||
chart.wrapper.addEventListener('mouseover', (event) => {
|
||||
selectedChart = true
|
||||
})
|
||||
chart.wrapper.addEventListener('mouseout', (event) => {
|
||||
selectedChart = false
|
||||
})
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (!selectedChart) {return}
|
||||
if (event.altKey && event.code === 'KeyH') {
|
||||
let price = chart.series.coordinateToPrice(yPrice)
|
||||
|
||||
let colorList = [
|
||||
'rgba(228, 0, 16, 0.7)',
|
||||
'rgba(255, 133, 34, 0.7)',
|
||||
'rgba(164, 59, 176, 0.7)',
|
||||
'rgba(129, 59, 102, 0.7)',
|
||||
'rgba(91, 20, 248, 0.7)',
|
||||
'rgba(32, 86, 249, 0.7)',
|
||||
]
|
||||
let color = colorList[Math.floor(Math.random()*colorList.length)]
|
||||
|
||||
makeHorizontalLine(chart, 0, price, color, 2, LightweightCharts.LineStyle.Solid, true, '')
|
||||
}
|
||||
if (searchWindow.style.display === 'none') {
|
||||
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
||||
searchWindow.style.display = 'block';
|
||||
sBox.focus();
|
||||
}
|
||||
}
|
||||
else if (event.key === 'Enter') {
|
||||
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
}
|
||||
else if (event.key === 'Escape') {
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
}
|
||||
});
|
||||
sBox.addEventListener('input', function() {
|
||||
sBox.value = sBox.value.toUpperCase();
|
||||
});
|
||||
return {
|
||||
window: searchWindow,
|
||||
box: sBox,
|
||||
}
|
||||
}
|
||||
|
||||
function makeSpinner(chart) {
|
||||
chart.spinner = document.createElement('div')
|
||||
chart.spinner.style.width = '30px'
|
||||
chart.spinner.style.height = '30px'
|
||||
chart.spinner.style.border = '4px solid rgba(255, 255, 255, 0.6)'
|
||||
chart.spinner.style.borderTop = '4px solid rgba(0, 122, 255, 0.8)'
|
||||
chart.spinner.style.borderRadius = '50%'
|
||||
chart.spinner.style.position = 'absolute'
|
||||
chart.spinner.style.top = '50%'
|
||||
chart.spinner.style.left = '50%'
|
||||
chart.spinner.style.zIndex = 1000
|
||||
chart.spinner.style.transform = 'translate(-50%, -50%)'
|
||||
chart.spinner.style.display = 'none'
|
||||
chart.wrapper.appendChild(chart.spinner)
|
||||
let rotation = 0;
|
||||
const speed = 10; // Adjust this value to change the animation speed
|
||||
function animateSpinner() {
|
||||
rotation += speed
|
||||
chart.spinner.style.transform = `translate(-50%, -50%) rotate(${rotation}deg)`
|
||||
requestAnimationFrame(animateSpinner)
|
||||
}
|
||||
animateSpinner();
|
||||
}
|
||||
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
|
||||
let switcherElement = document.createElement('div');
|
||||
switcherElement.style.margin = '4px 14px'
|
||||
switcherElement.style.zIndex = '1000'
|
||||
|
||||
let intervalElements = items.map(function(item) {
|
||||
let itemEl = document.createElement('button');
|
||||
itemEl.style.cursor = 'pointer'
|
||||
itemEl.style.padding = '2px 5px'
|
||||
itemEl.style.margin = '0px 4px'
|
||||
itemEl.style.fontSize = '13px'
|
||||
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : 'transparent'
|
||||
itemEl.style.color = item === activeItem ? activeColor : inactiveColor
|
||||
itemEl.style.border = 'none'
|
||||
itemEl.style.borderRadius = '4px'
|
||||
|
||||
itemEl.addEventListener('mouseenter', function() {
|
||||
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : hoverColor
|
||||
itemEl.style.color = activeColor
|
||||
})
|
||||
itemEl.addEventListener('mouseleave', function() {
|
||||
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : 'transparent'
|
||||
itemEl.style.color = item === activeItem ? activeColor : inactiveColor
|
||||
})
|
||||
itemEl.innerText = item;
|
||||
itemEl.addEventListener('click', function() {
|
||||
onItemClicked(item);
|
||||
});
|
||||
switcherElement.appendChild(itemEl);
|
||||
return itemEl;
|
||||
});
|
||||
function onItemClicked(item) {
|
||||
if (item === activeItem) {
|
||||
return;
|
||||
}
|
||||
intervalElements.forEach(function(element, index) {
|
||||
element.style.backgroundColor = items[index] === item ? activeBackgroundColor : 'transparent'
|
||||
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
|
||||
});
|
||||
activeItem = item;
|
||||
callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
||||
}
|
||||
chart.topBar.appendChild(switcherElement)
|
||||
makeSeperator(chart.topBar)
|
||||
return switcherElement;
|
||||
}
|
||||
|
||||
function makeTextBoxWidget(chart, text) {
|
||||
let textBox = document.createElement('div')
|
||||
textBox.style.margin = '0px 18px'
|
||||
textBox.style.position = 'relative'
|
||||
textBox.style.fontSize = '16px'
|
||||
textBox.style.color = 'rgb(220, 220, 220)'
|
||||
textBox.innerText = text
|
||||
chart.topBar.append(textBox)
|
||||
makeSeperator(chart.topBar)
|
||||
return textBox
|
||||
}
|
||||
function makeTopBar(chart) {
|
||||
chart.topBar = document.createElement('div')
|
||||
chart.topBar.style.backgroundColor = '#191B1E'
|
||||
chart.topBar.style.borderBottom = '2px solid #3C434C'
|
||||
chart.topBar.style.display = 'flex'
|
||||
chart.topBar.style.alignItems = 'center'
|
||||
chart.wrapper.prepend(chart.topBar)
|
||||
}
|
||||
function makeSeperator(topBar) {
|
||||
let seperator = document.createElement('div')
|
||||
seperator.style.width = '1px'
|
||||
seperator.style.height = '20px'
|
||||
seperator.style.backgroundColor = '#3C434C'
|
||||
topBar.appendChild(seperator)
|
||||
}
|
||||
'''
|
||||
@ -1,28 +1,30 @@
|
||||
import asyncio
|
||||
import time
|
||||
import multiprocessing as mp
|
||||
import webview
|
||||
|
||||
from lightweight_charts.js import LWC, CALLBACK_SCRIPT, TopBar
|
||||
from lightweight_charts.abstract import LWC, JS, TopBar
|
||||
|
||||
|
||||
class CallbackAPI:
|
||||
def __init__(self, emit): self.emit = emit
|
||||
def __init__(self, emit_queue, return_queue):
|
||||
self.emit_q, self.return_q = emit_queue, return_queue
|
||||
|
||||
def callback(self, message: str):
|
||||
messages = message.split('__')
|
||||
name, chart_id = messages[:2]
|
||||
args = messages[2:]
|
||||
self.emit.put((name, chart_id, *args))
|
||||
self.return_q.put(*args) if name == 'return' else self.emit_q.put((name, chart_id, *args))
|
||||
|
||||
|
||||
class PyWV:
|
||||
def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, debug, emit):
|
||||
def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, maximize, debug, emit_queue, return_queue):
|
||||
if maximize:
|
||||
width, height = webview.screens[0].width, webview.screens[0].height
|
||||
self.queue = q
|
||||
self.exit = exit
|
||||
self.loaded = loaded
|
||||
self.debug = debug
|
||||
js_api = CallbackAPI(emit)
|
||||
js_api = CallbackAPI(emit_queue, return_queue)
|
||||
self.webview = webview.create_window('', html=html, on_top=on_top, js_api=js_api, width=width, height=height,
|
||||
x=x, y=y, background_color='#000000')
|
||||
self.webview.events.loaded += self.on_js_load
|
||||
@ -40,30 +42,28 @@ class PyWV:
|
||||
except KeyError:
|
||||
return
|
||||
|
||||
def on_js_load(self):
|
||||
self.loaded.set(), self.loop()
|
||||
def on_js_load(self): self.loaded.set(), self.loop()
|
||||
|
||||
|
||||
class Chart(LWC):
|
||||
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||
on_top: bool = False, debug: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False,
|
||||
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
||||
self._emit = mp.Queue()
|
||||
self._q = mp.Queue()
|
||||
on_top: bool = False, maximize: bool = False, debug: bool = False,
|
||||
api: object = None, topbar: bool = False, searchbox: bool = False,
|
||||
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading, scale_candles_only)
|
||||
self._q, self._emit_q, self._return_q = (mp.Queue() for _ in range(3))
|
||||
self._exit, self._loaded = mp.Event(), mp.Event()
|
||||
self._script_func = self._q.put
|
||||
self._exit = mp.Event()
|
||||
self._loaded = mp.Event()
|
||||
self._api = api
|
||||
self._js_api_code = 'pywebview.api.callback'
|
||||
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
|
||||
width, height, x, y, on_top, debug, self._emit), daemon=True)
|
||||
width, height, x, y, on_top, maximize, debug,
|
||||
self._emit_q, self._return_q), daemon=True)
|
||||
self._process.start()
|
||||
self._create_chart()
|
||||
|
||||
self.api = api
|
||||
self._js_api_code = 'pywebview.api.callback'
|
||||
if not topbar and not searchbox:
|
||||
return
|
||||
self.run_script(CALLBACK_SCRIPT)
|
||||
self.run_script(JS['callback'])
|
||||
self.run_script(f'makeSpinner({self.id})')
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
@ -80,50 +80,35 @@ class Chart(LWC):
|
||||
else:
|
||||
self._q.put('show')
|
||||
if block:
|
||||
try:
|
||||
while 1:
|
||||
while not self._exit.is_set() and self.polygon._q.empty():
|
||||
time.sleep(0.05)
|
||||
continue
|
||||
if self._exit.is_set():
|
||||
self._exit.clear()
|
||||
return
|
||||
value = self.polygon._q.get_nowait()
|
||||
func, args = value[0], value[1:]
|
||||
func(*args)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
asyncio.run(self.show_async(block=True))
|
||||
|
||||
async def show_async(self, block=False):
|
||||
if not self.loaded:
|
||||
self._q.put('start')
|
||||
self._loaded.wait()
|
||||
self._on_js_load()
|
||||
else:
|
||||
self._q.put('show')
|
||||
if block:
|
||||
try:
|
||||
while 1:
|
||||
while self._emit.empty() and not self._exit.is_set() and self.polygon._q.empty():
|
||||
await asyncio.sleep(0.05)
|
||||
if self._exit.is_set():
|
||||
self._exit.clear()
|
||||
return
|
||||
elif not self._emit.empty():
|
||||
key, chart_id, arg = self._emit.get()
|
||||
self.api.chart = self._charts[chart_id]
|
||||
if widget := self.api.chart.topbar._widget_with_method(key):
|
||||
widget.value = arg
|
||||
await getattr(self.api, key)()
|
||||
else:
|
||||
await getattr(self.api, key)(arg)
|
||||
continue
|
||||
value = self.polygon._q.get()
|
||||
func, args = value[0], value[1:]
|
||||
func(*args)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
asyncio.create_task(self.show_async(block=True))
|
||||
self.show(block=False)
|
||||
if not block:
|
||||
asyncio.create_task(self.show_async(block=True))
|
||||
return
|
||||
try:
|
||||
while 1:
|
||||
while self._emit_q.empty() and not self._exit.is_set() and self.polygon._q.empty():
|
||||
await asyncio.sleep(0.05)
|
||||
if self._exit.is_set():
|
||||
self._exit.clear()
|
||||
return
|
||||
elif not self._emit_q.empty():
|
||||
key, chart_id, arg = self._emit_q.get()
|
||||
self._api.chart = self._charts[chart_id]
|
||||
if widget := self._api.chart.topbar._widget_with_method(key):
|
||||
widget.value = arg
|
||||
await getattr(self._api, key)()
|
||||
else:
|
||||
await getattr(self._api, key)(arg)
|
||||
continue
|
||||
value = self.polygon._q.get()
|
||||
func, args = value[0], value[1:]
|
||||
func(*args)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
|
||||
208
lightweight_charts/js/callback.js
Normal file
208
lightweight_charts/js/callback.js
Normal file
@ -0,0 +1,208 @@
|
||||
function makeSearchBox(chart, callbackFunction) {
|
||||
let searchWindow = document.createElement('div')
|
||||
searchWindow.style.position = 'absolute'
|
||||
searchWindow.style.top = '0'
|
||||
searchWindow.style.bottom = '200px'
|
||||
searchWindow.style.left = '0'
|
||||
searchWindow.style.right = '0'
|
||||
searchWindow.style.margin = 'auto'
|
||||
searchWindow.style.width = '150px'
|
||||
searchWindow.style.height = '30px'
|
||||
searchWindow.style.padding = '10px'
|
||||
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
||||
searchWindow.style.border = '2px solid #3C434C'
|
||||
searchWindow.style.zIndex = '1000'
|
||||
searchWindow.style.display = 'none'
|
||||
searchWindow.style.borderRadius = '5px'
|
||||
|
||||
let magnifyingGlass = document.createElement('span');
|
||||
magnifyingGlass.style.display = 'inline-block';
|
||||
magnifyingGlass.style.width = '12px';
|
||||
magnifyingGlass.style.height = '12px';
|
||||
magnifyingGlass.style.border = '2px solid rgb(240, 240, 240)';
|
||||
magnifyingGlass.style.borderRadius = '50%';
|
||||
magnifyingGlass.style.position = 'relative';
|
||||
let handle = document.createElement('span');
|
||||
handle.style.display = 'block';
|
||||
handle.style.width = '7px';
|
||||
handle.style.height = '2px';
|
||||
handle.style.backgroundColor = 'rgb(240, 240, 240)';
|
||||
handle.style.position = 'absolute';
|
||||
handle.style.top = 'calc(50% + 7px)';
|
||||
handle.style.right = 'calc(50% - 11px)';
|
||||
handle.style.transform = 'rotate(45deg)';
|
||||
|
||||
let sBox = document.createElement('input');
|
||||
sBox.type = 'text';
|
||||
sBox.style.position = 'relative';
|
||||
sBox.style.display = 'inline-block';
|
||||
sBox.style.zIndex = '1000';
|
||||
sBox.style.textAlign = 'center'
|
||||
sBox.style.width = '100px'
|
||||
sBox.style.marginLeft = '15px'
|
||||
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.3)'
|
||||
sBox.style.color = 'rgb(240,240,240)'
|
||||
sBox.style.fontSize = '20px'
|
||||
sBox.style.border = 'none'
|
||||
sBox.style.outline = 'none'
|
||||
sBox.style.borderRadius = '2px'
|
||||
|
||||
searchWindow.appendChild(magnifyingGlass)
|
||||
magnifyingGlass.appendChild(handle)
|
||||
searchWindow.appendChild(sBox)
|
||||
chart.div.appendChild(searchWindow);
|
||||
|
||||
let yPrice = null
|
||||
chart.chart.subscribeCrosshairMove((param) => {
|
||||
if (param.point){
|
||||
yPrice = param.point.y;
|
||||
}
|
||||
});
|
||||
let selectedChart = true
|
||||
chart.wrapper.addEventListener('mouseover', (event) => {
|
||||
selectedChart = true
|
||||
})
|
||||
chart.wrapper.addEventListener('mouseout', (event) => {
|
||||
selectedChart = false
|
||||
})
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (!selectedChart) {return}
|
||||
if (event.altKey && event.code === 'KeyH') {
|
||||
let price = chart.series.coordinateToPrice(yPrice)
|
||||
|
||||
let colorList = [
|
||||
'rgba(228, 0, 16, 0.7)',
|
||||
'rgba(255, 133, 34, 0.7)',
|
||||
'rgba(164, 59, 176, 0.7)',
|
||||
'rgba(129, 59, 102, 0.7)',
|
||||
'rgba(91, 20, 248, 0.7)',
|
||||
'rgba(32, 86, 249, 0.7)',
|
||||
]
|
||||
let color = colorList[Math.floor(Math.random()*colorList.length)]
|
||||
|
||||
makeHorizontalLine(chart, 0, price, color, 2, LightweightCharts.LineStyle.Solid, true, '')
|
||||
}
|
||||
if (searchWindow.style.display === 'none') {
|
||||
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
||||
searchWindow.style.display = 'block';
|
||||
sBox.focus();
|
||||
}
|
||||
}
|
||||
else if (event.key === 'Enter') {
|
||||
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
}
|
||||
else if (event.key === 'Escape') {
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
}
|
||||
});
|
||||
sBox.addEventListener('input', function() {
|
||||
sBox.value = sBox.value.toUpperCase();
|
||||
});
|
||||
return {
|
||||
window: searchWindow,
|
||||
box: sBox,
|
||||
}
|
||||
}
|
||||
|
||||
function makeSpinner(chart) {
|
||||
chart.spinner = document.createElement('div')
|
||||
chart.spinner.style.width = '30px'
|
||||
chart.spinner.style.height = '30px'
|
||||
chart.spinner.style.border = '4px solid rgba(255, 255, 255, 0.6)'
|
||||
chart.spinner.style.borderTop = '4px solid rgba(0, 122, 255, 0.8)'
|
||||
chart.spinner.style.borderRadius = '50%'
|
||||
chart.spinner.style.position = 'absolute'
|
||||
chart.spinner.style.top = '50%'
|
||||
chart.spinner.style.left = '50%'
|
||||
chart.spinner.style.zIndex = 1000
|
||||
chart.spinner.style.transform = 'translate(-50%, -50%)'
|
||||
chart.spinner.style.display = 'none'
|
||||
chart.wrapper.appendChild(chart.spinner)
|
||||
let rotation = 0;
|
||||
const speed = 10; // Adjust this value to change the animation speed
|
||||
function animateSpinner() {
|
||||
rotation += speed
|
||||
chart.spinner.style.transform = `translate(-50%, -50%) rotate(${rotation}deg)`
|
||||
requestAnimationFrame(animateSpinner)
|
||||
}
|
||||
animateSpinner();
|
||||
}
|
||||
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
|
||||
let switcherElement = document.createElement('div');
|
||||
switcherElement.style.margin = '4px 14px'
|
||||
switcherElement.style.zIndex = '1000'
|
||||
|
||||
let intervalElements = items.map(function(item) {
|
||||
let itemEl = document.createElement('button');
|
||||
itemEl.style.cursor = 'pointer'
|
||||
itemEl.style.padding = '2px 5px'
|
||||
itemEl.style.margin = '0px 4px'
|
||||
itemEl.style.fontSize = '13px'
|
||||
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : 'transparent'
|
||||
itemEl.style.color = item === activeItem ? activeColor : inactiveColor
|
||||
itemEl.style.border = 'none'
|
||||
itemEl.style.borderRadius = '4px'
|
||||
|
||||
itemEl.addEventListener('mouseenter', function() {
|
||||
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : hoverColor
|
||||
itemEl.style.color = activeColor
|
||||
})
|
||||
itemEl.addEventListener('mouseleave', function() {
|
||||
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : 'transparent'
|
||||
itemEl.style.color = item === activeItem ? activeColor : inactiveColor
|
||||
})
|
||||
itemEl.innerText = item;
|
||||
itemEl.addEventListener('click', function() {
|
||||
onItemClicked(item);
|
||||
});
|
||||
switcherElement.appendChild(itemEl);
|
||||
return itemEl;
|
||||
});
|
||||
function onItemClicked(item) {
|
||||
if (item === activeItem) {
|
||||
return;
|
||||
}
|
||||
intervalElements.forEach(function(element, index) {
|
||||
element.style.backgroundColor = items[index] === item ? activeBackgroundColor : 'transparent'
|
||||
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
|
||||
});
|
||||
activeItem = item;
|
||||
callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
||||
}
|
||||
chart.topBar.appendChild(switcherElement)
|
||||
makeSeperator(chart.topBar)
|
||||
return switcherElement;
|
||||
}
|
||||
|
||||
function makeTextBoxWidget(chart, text) {
|
||||
let textBox = document.createElement('div')
|
||||
textBox.style.margin = '0px 18px'
|
||||
textBox.style.position = 'relative'
|
||||
textBox.style.fontSize = '16px'
|
||||
textBox.style.color = 'rgb(220, 220, 220)'
|
||||
textBox.innerText = text
|
||||
chart.topBar.append(textBox)
|
||||
makeSeperator(chart.topBar)
|
||||
return textBox
|
||||
}
|
||||
function makeTopBar(chart) {
|
||||
chart.topBar = document.createElement('div')
|
||||
chart.topBar.style.backgroundColor = '#191B1E'
|
||||
chart.topBar.style.borderBottom = '2px solid #3C434C'
|
||||
chart.topBar.style.display = 'flex'
|
||||
chart.topBar.style.alignItems = 'center'
|
||||
chart.wrapper.prepend(chart.topBar)
|
||||
}
|
||||
|
||||
function makeSeperator(topBar) {
|
||||
let seperator = document.createElement('div')
|
||||
seperator.style.width = '1px'
|
||||
seperator.style.height = '20px'
|
||||
seperator.style.backgroundColor = '#3C434C'
|
||||
topBar.appendChild(seperator)
|
||||
}
|
||||
|
||||
|
||||
167
lightweight_charts/js/funcs.js
Normal file
167
lightweight_charts/js/funcs.js
Normal file
@ -0,0 +1,167 @@
|
||||
function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
let chart = {
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
wrapper: document.createElement('div'),
|
||||
div: document.createElement('div'),
|
||||
legend: document.createElement('div'),
|
||||
scale: {
|
||||
width: innerWidth,
|
||||
height: innerHeight
|
||||
},
|
||||
}
|
||||
chart.chart = LightweightCharts.createChart(chart.div, {
|
||||
width: window.innerWidth*innerWidth,
|
||||
height: window.innerHeight*innerHeight,
|
||||
layout: {
|
||||
textColor: '#d1d4dc',
|
||||
background: {
|
||||
color:'#000000',
|
||||
type: LightweightCharts.ColorType.Solid,
|
||||
},
|
||||
fontSize: 12
|
||||
},
|
||||
rightPriceScale: {
|
||||
scaleMargins: {top: 0.3, bottom: 0.25},
|
||||
},
|
||||
timeScale: {timeVisible: true, secondsVisible: false},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
vertLine: {
|
||||
labelBackgroundColor: 'rgb(46, 46, 46)'
|
||||
},
|
||||
horzLine: {
|
||||
labelBackgroundColor: 'rgb(55, 55, 55)'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
vertLines: {color: 'rgba(29, 30, 38, 5)'},
|
||||
horzLines: {color: 'rgba(29, 30, 58, 5)'},
|
||||
},
|
||||
handleScroll: {vertTouchDrag: true},
|
||||
})
|
||||
let up = 'rgba(39, 157, 130, 100)'
|
||||
let down = 'rgba(200, 97, 100, 100)'
|
||||
chart.series = chart.chart.addCandlestickSeries({
|
||||
color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
||||
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
||||
})
|
||||
chart.volumeSeries = chart.chart.addHistogramSeries({
|
||||
color: '#26a69a',
|
||||
priceFormat: {type: 'volume'},
|
||||
priceScaleId: '',
|
||||
})
|
||||
chart.series.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.2, bottom: 0.2},
|
||||
});
|
||||
chart.volumeSeries.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.8, bottom: 0},
|
||||
});
|
||||
chart.legend.style.position = 'absolute'
|
||||
chart.legend.style.zIndex = 1000
|
||||
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
|
||||
chart.legend.style.top = '10px'
|
||||
chart.legend.style.left = '10px'
|
||||
chart.legend.style.fontFamily = 'Monaco'
|
||||
chart.legend.style.fontSize = '11px'
|
||||
chart.legend.style.color = 'rgb(191, 195, 203)'
|
||||
|
||||
chart.wrapper.style.width = `${100*innerWidth}%`
|
||||
chart.wrapper.style.height = `${100*innerHeight}%`
|
||||
chart.wrapper.style.display = 'flex'
|
||||
chart.wrapper.style.flexDirection = 'column'
|
||||
|
||||
chart.div.style.position = 'relative'
|
||||
chart.div.style.display = 'flex'
|
||||
|
||||
chart.div.appendChild(chart.legend)
|
||||
chart.wrapper.appendChild(chart.div)
|
||||
document.getElementById('wrapper').append(chart.wrapper)
|
||||
|
||||
if (!autoSize) {
|
||||
return chart
|
||||
}
|
||||
let topBarOffset = 0
|
||||
window.addEventListener('resize', function() {
|
||||
if ('topBar' in chart) {
|
||||
topBarOffset = chart.topBar.offsetHeight
|
||||
}
|
||||
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
|
||||
});
|
||||
return chart
|
||||
}
|
||||
function makeHorizontalLine(chart, lineId, price, color, width, style, axisLabelVisible, text) {
|
||||
let priceLine = {
|
||||
price: price,
|
||||
color: color,
|
||||
lineWidth: width,
|
||||
lineStyle: style,
|
||||
axisLabelVisible: axisLabelVisible,
|
||||
title: text,
|
||||
};
|
||||
let line = {
|
||||
line: chart.series.createPriceLine(priceLine),
|
||||
price: price,
|
||||
id: lineId,
|
||||
};
|
||||
chart.horizontal_lines.push(line)
|
||||
}
|
||||
function legendItemFormat(num) {
|
||||
return num.toFixed(2).toString().padStart(8, ' ')
|
||||
}
|
||||
function syncCrosshairs(childChart, parentChart) {
|
||||
let parent = 0
|
||||
let child = 0
|
||||
|
||||
let parentCrosshairHandler = (e) => {
|
||||
parent ++
|
||||
if (parent < 10) {
|
||||
return
|
||||
}
|
||||
child = 0
|
||||
parentChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: true,
|
||||
labelVisible: true,
|
||||
}}})
|
||||
childChart.applyOptions({crosshair: { horzLine: {
|
||||
visible: false,
|
||||
labelVisible: false,
|
||||
}}})
|
||||
|
||||
childChart.unsubscribeCrosshairMove(childCrosshairHandler)
|
||||
if (e.time !== undefined) {
|
||||
let xx = childChart.timeScale().timeToCoordinate(e.time);
|
||||
childChart.setCrosshairXY(xx,300,true);
|
||||
} else if (e.point !== undefined){
|
||||
childChart.setCrosshairXY(e.point.x,300,false);
|
||||
}
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
|
||||
let childCrosshairHandler = (e) => {
|
||||
child ++
|
||||
if (child < 10) {
|
||||
return
|
||||
}
|
||||
parent = 0
|
||||
childChart.applyOptions({crosshair: {horzLine: {
|
||||
visible: true,
|
||||
labelVisible: true,
|
||||
}}})
|
||||
parentChart.applyOptions({crosshair: {horzLine: {
|
||||
visible: false,
|
||||
labelVisible: false,
|
||||
}}})
|
||||
|
||||
parentChart.unsubscribeCrosshairMove(parentCrosshairHandler)
|
||||
if (e.time !== undefined) {
|
||||
let xx = parentChart.timeScale().timeToCoordinate(e.time);
|
||||
parentChart.setCrosshairXY(xx,300,true);
|
||||
} else if (e.point !== undefined){
|
||||
parentChart.setCrosshairXY(e.point.x,300,false);
|
||||
}
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
}
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -22,6 +22,12 @@ except ImportError:
|
||||
|
||||
|
||||
class PolygonAPI:
|
||||
"""
|
||||
Offers direct access to Polygon API data within all Chart objects.
|
||||
|
||||
It is not designed to be initialized by the user, and should be utilised
|
||||
through the `polygon` method of `LWC` (chart.polygon.<method>).
|
||||
"""
|
||||
def __init__(self, chart):
|
||||
ch = logging.StreamHandler()
|
||||
ch.setFormatter(logging.Formatter('%(asctime)s | [polygon.io] %(levelname)s: %(message)s', datefmt='%H:%M:%S'))
|
||||
@ -30,23 +36,32 @@ class PolygonAPI:
|
||||
self._log.setLevel(logging.ERROR)
|
||||
self._log.addHandler(ch)
|
||||
|
||||
self.max_ticks_per_response = 20
|
||||
|
||||
self._chart = chart
|
||||
self._lasts = {} # $$
|
||||
self._lasts = {}
|
||||
self._key = None
|
||||
self._using_live_data = False
|
||||
self._using_live = {'stocks': False, 'options': False, 'indices': False, 'crypto': False, 'forex': False}
|
||||
self._ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
|
||||
self._send_q = queue.Queue()
|
||||
|
||||
self._ws_q = queue.Queue()
|
||||
self._q = queue.Queue()
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _subchart(self, subchart):
|
||||
return PolygonAPISubChart(self, subchart)
|
||||
self._using_live_data = False
|
||||
self._using_live = {'stocks': False, 'options': False, 'indices': False, 'crypto': False, 'forex': False}
|
||||
self._ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
|
||||
|
||||
|
||||
def log(self, info: bool):
|
||||
"""
|
||||
Streams informational messages related to Polygon.io.
|
||||
"""
|
||||
self._log.setLevel(logging.INFO) if info else self._log.setLevel(logging.ERROR)
|
||||
|
||||
def api_key(self, key: str): self._key = key
|
||||
def api_key(self, key: str):
|
||||
"""
|
||||
Sets the API key to be used with Polygon.io.
|
||||
"""
|
||||
self._key = key
|
||||
|
||||
def stock(self, symbol: str, timeframe: str, start_date: str, end_date='now', limit: int = 5_000, live: bool = False):
|
||||
"""
|
||||
@ -55,34 +70,74 @@ class PolygonAPI:
|
||||
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
||||
:param start_date: Start date of the data (YYYY-MM-DD).
|
||||
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
||||
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000)
|
||||
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
||||
:param live: If true, the data will be updated in real-time.
|
||||
"""
|
||||
return True if self._set(self._chart, 'stocks', symbol, timeframe, start_date, end_date, limit, live) else False
|
||||
return self._set(self._chart, 'stocks', symbol, timeframe, start_date, end_date, limit, live)
|
||||
|
||||
def option(self, symbol: str, timeframe: str, start_date: str, expiration: str = None, right: Literal['C', 'P'] = None, strike: Union[int, float] = None,
|
||||
end_date: str = 'now', limit: int = 5_000, live: bool = False):
|
||||
"""
|
||||
Requests and displays option data pulled from Polygon.io.\n
|
||||
:param symbol: The underlying ticker to request. A formatted option ticker can also be given instead of using the expiration, right, and strike parameters.
|
||||
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
||||
:param start_date: Start date of the data (YYYY-MM-DD).
|
||||
:param expiration: Expiration of the option (YYYY-MM-DD).
|
||||
:param right: Right of the option (C, P).
|
||||
:param strike: The strike price of the option.
|
||||
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
||||
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
||||
:param live: If true, the data will be updated in real-time.
|
||||
"""
|
||||
if any((expiration, right, strike)):
|
||||
symbol = f'O:{symbol}{dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")}{right}{strike * 1000:08d}'
|
||||
return True if self._set(self._chart, 'options', symbol, timeframe, start_date, end_date, limit, live) else False
|
||||
symbol = f'{symbol}{dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")}{right}{strike * 1000:08d}'
|
||||
return self._set(self._chart, 'options', f'O:{symbol}', timeframe, start_date, end_date, limit, live)
|
||||
|
||||
def index(self, symbol, timeframe, start_date, end_date='now', limit: int = 5_000, live=False):
|
||||
return True if self._set(self._chart, 'indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live) else False
|
||||
"""
|
||||
Requests and displays index data pulled from Polygon.io.\n
|
||||
:param symbol: Ticker to request.
|
||||
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
||||
:param start_date: Start date of the data (YYYY-MM-DD).
|
||||
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
||||
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
||||
:param live: If true, the data will be updated in real-time.
|
||||
"""
|
||||
return self._set(self._chart, 'indices', f'I:{symbol}', timeframe, start_date, end_date, limit, live)
|
||||
|
||||
def forex(self, fiat_pair, timeframe, start_date, end_date='now', limit: int = 5_000, live=False):
|
||||
return True if self._set(self._chart, 'forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live) else False
|
||||
"""
|
||||
Requests and displays forex data pulled from Polygon.io.\n
|
||||
:param fiat_pair: The fiat pair to request. (USD-CAD, GBP-JPY etc.)
|
||||
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
||||
:param start_date: Start date of the data (YYYY-MM-DD).
|
||||
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
||||
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
||||
:param live: If true, the data will be updated in real-time.
|
||||
"""
|
||||
return self._set(self._chart, 'forex', f'C:{fiat_pair}', timeframe, start_date, end_date, limit, live)
|
||||
|
||||
def crypto(self, crypto_pair, timeframe, start_date, end_date='now', limit: int = 5_000, live=False):
|
||||
return True if self._set(self._chart, 'crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live) else False
|
||||
"""
|
||||
Requests and displays crypto data pulled from Polygon.io.\n
|
||||
:param crypto_pair: The crypto pair to request. (BTC-USD, ETH-BTC etc.)
|
||||
:param timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
|
||||
:param start_date: Start date of the data (YYYY-MM-DD).
|
||||
:param end_date: End date of the data (YYYY-MM-DD). If left blank, this will be set to today.
|
||||
:param limit: The limit of base aggregates queried to create the timeframe given (max 50_000).
|
||||
:param live: If true, the data will be updated in real-time.
|
||||
"""
|
||||
return self._set(self._chart, 'crypto', f'X:{crypto_pair}', timeframe, start_date, end_date, limit, live)
|
||||
|
||||
def _set(self, chart, sec_type, ticker, timeframe, start_date, end_date, limit, live):
|
||||
if requests is None:
|
||||
raise ImportError('The "requests" library was not found, and must be installed to use polygon.io.')
|
||||
|
||||
self._ws_q.put(('_unsubscribe', chart))
|
||||
end_date = dt.datetime.now().strftime('%Y-%m-%d') if end_date == 'now' else end_date
|
||||
mult, span = _convert_timeframe(timeframe)
|
||||
query_url = f"https://api.polygon.io/v2/aggs/ticker/{ticker.replace('-', '')}/range/{mult}/{span}/{start_date}/{end_date}?limit={limit}&apiKey={self._key}"
|
||||
|
||||
query_url = f"https://api.polygon.io/v2/aggs/ticker/{ticker.replace('-', '')}/range/{mult}/{span}/{start_date}/{end_date}?limit={limit}&apiKey={self._key}"
|
||||
response = requests.get(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
|
||||
if response.status_code != 200:
|
||||
error = response.json()
|
||||
@ -93,11 +148,6 @@ class PolygonAPI:
|
||||
self._log.error(f'No results for "{ticker}" ({sec_type})')
|
||||
return
|
||||
|
||||
for child in self._lasts.values():
|
||||
for subbed_chart in child['charts']:
|
||||
if subbed_chart == chart:
|
||||
self._send_q.put(('_unsubscribe', chart, ticker))
|
||||
|
||||
df = pd.DataFrame(data['results'])
|
||||
columns = ['t', 'o', 'h', 'l', 'c']
|
||||
rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'}
|
||||
@ -106,45 +156,40 @@ class PolygonAPI:
|
||||
columns.append('v')
|
||||
df = df[columns].rename(columns=rename)
|
||||
df['time'] = pd.to_datetime(df['time'], unit='ms')
|
||||
|
||||
chart.set(df)
|
||||
|
||||
if not live:
|
||||
return True
|
||||
|
||||
if not self._using_live_data:
|
||||
threading.Thread(target=asyncio.run, args=[self._thread_loop()], daemon=True).start()
|
||||
self._using_live_data = True
|
||||
with self._lock:
|
||||
if not self._ws[sec_type]:
|
||||
self._send_q.put(('_websocket_connect', self._key, sec_type))
|
||||
self._send_q.put(('_subscribe', chart, sec_type, ticker))
|
||||
self._ws_q.put(('_websocket_connect', self._key, sec_type))
|
||||
self._ws_q.put(('_subscribe', chart, ticker, sec_type))
|
||||
return True
|
||||
|
||||
async def _thread_loop(self):
|
||||
while 1:
|
||||
while self._send_q.empty():
|
||||
while self._ws_q.empty():
|
||||
await asyncio.sleep(0.05)
|
||||
value = self._send_q.get()
|
||||
value = self._ws_q.get()
|
||||
func, args = value[0], value[1:]
|
||||
asyncio.create_task(getattr(self, func)(*args))
|
||||
|
||||
def unsubscribe(self, symbol):
|
||||
self._send_q.put(('_unsubscribe', self._chart, symbol))
|
||||
|
||||
async def _subscribe(self, chart, sec_type, ticker):
|
||||
key = ticker if '.' not in ticker else ticker.split('.')[1]
|
||||
key = key if ':' not in key else key.split(':')[1]
|
||||
async def _subscribe(self, chart, ticker, sec_type):
|
||||
key = ticker if ':' not in ticker else ticker.split(':')[1]
|
||||
if not self._lasts.get(key):
|
||||
sub_type = {
|
||||
'stocks': ('Q', 'A'),
|
||||
'options': ('Q', 'A'),
|
||||
'indices': ('V', None),
|
||||
'forex': ('C', 'CA'),
|
||||
'crypto': ('XQ', 'XA'),
|
||||
}
|
||||
self._lasts[key] = {
|
||||
'ticker': ticker,
|
||||
'sec_type': sec_type,
|
||||
'sub_type': sub_type[sec_type],
|
||||
'sub_type': {
|
||||
'stocks': ('Q', 'A'),
|
||||
'options': ('Q', 'A'),
|
||||
'indices': ('V', None),
|
||||
'forex': ('C', 'CA'),
|
||||
'crypto': ('XQ', 'XA'),
|
||||
}[sec_type],
|
||||
'price': chart._last_bar['close'],
|
||||
'charts': [],
|
||||
}
|
||||
@ -158,18 +203,22 @@ class PolygonAPI:
|
||||
return
|
||||
self._lasts[key]['charts'].append(chart)
|
||||
|
||||
async def _unsubscribe(self, chart, ticker):
|
||||
key = ticker if '.' not in ticker else ticker.split('.')[1]
|
||||
key = key if ':' not in key else key.split(':')[1]
|
||||
if chart in self._lasts[key]['charts']:
|
||||
self._lasts[key]['charts'].remove(chart)
|
||||
if self._lasts[key]['charts']:
|
||||
async def _unsubscribe(self, chart):
|
||||
for data in self._lasts.values():
|
||||
if chart in data['charts']:
|
||||
break
|
||||
else:
|
||||
return
|
||||
if chart in data['charts']:
|
||||
data['charts'].remove(chart)
|
||||
if data['charts']:
|
||||
return
|
||||
|
||||
while self._q.qsize():
|
||||
self._q.get() # Flush the queue
|
||||
quotes, aggs = self._lasts[key]['sub_type']
|
||||
await self._send(self._lasts[key]['sec_type'], 'unsubscribe', f'{quotes}.{ticker}')
|
||||
await self._send(self._lasts[key]['sec_type'], 'unsubscribe', f'{aggs}.{ticker}')
|
||||
quotes, aggs = data['sub_type']
|
||||
await self._send(data['sec_type'], 'unsubscribe', f'{quotes}.{data["ticker"]}')
|
||||
await self._send(data['sec_type'], 'unsubscribe', f'{aggs}.{data["ticker"]}')
|
||||
|
||||
async def _send(self, sec_type, action, params):
|
||||
while 1:
|
||||
@ -213,7 +262,6 @@ class PolygonAPI:
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
max_ticks = 20
|
||||
async with websockets.connect(f'wss://socket.polygon.io/{sec_type}', ssl=ssl_context) as ws:
|
||||
with self._lock:
|
||||
self._ws[sec_type] = ws
|
||||
@ -225,10 +273,13 @@ class PolygonAPI:
|
||||
if data['ev'] == 'status':
|
||||
self._log.info(f'{data["message"]}')
|
||||
continue
|
||||
elif data_list.index(data) < len(data_list)-max_ticks:
|
||||
elif data_list.index(data) < len(data_list)-self.max_ticks_per_response:
|
||||
continue
|
||||
await self._handle_tick(sec_type, data)
|
||||
|
||||
def _subchart(self, subchart):
|
||||
return PolygonAPISubChart(self, subchart)
|
||||
|
||||
|
||||
class PolygonAPISubChart(PolygonAPI):
|
||||
def __init__(self, polygon, subchart):
|
||||
@ -237,22 +288,33 @@ class PolygonAPISubChart(PolygonAPI):
|
||||
|
||||
|
||||
class PolygonChart(Chart):
|
||||
def __init__(self, api_key: str, live: bool = False, num_bars: int = 200, limit: int = 5_000,
|
||||
"""
|
||||
A prebuilt callback chart object allowing for a standalone and plug-and-play
|
||||
experience of Polygon.io's API.
|
||||
|
||||
Tickers, security types and timeframes are to be defined within the chart window.
|
||||
|
||||
If using the standard `show` method, the `block` parameter must be set to True.
|
||||
When using `show_async`, either is acceptable.
|
||||
"""
|
||||
def __init__(self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
|
||||
timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
|
||||
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
|
||||
width: int = 800, height: int = 600, x: int = None, y: int = None, on_top: bool = False, debug=False):
|
||||
super().__init__(volume_enabled=True, width=width, height=height, x=x, y=y, on_top=on_top, debug=debug,
|
||||
width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||
on_top: bool = False, maximize: bool = False, debug: bool = False):
|
||||
super().__init__(volume_enabled=True, width=width, height=height, x=x, y=y, on_top=on_top, maximize=maximize, debug=debug,
|
||||
api=self, topbar=True, searchbox=True)
|
||||
self.chart = self
|
||||
self.num_bars = num_bars
|
||||
self.end_date = end_date
|
||||
self.limit = limit
|
||||
self.live = live
|
||||
self.polygon.api_key(api_key)
|
||||
|
||||
self.topbar.active_background_color = 'rgb(91, 98, 246)'
|
||||
self.topbar.textbox('symbol')
|
||||
self.topbar.switcher('timeframe', self.on_timeframe_selection, *timeframe_options)
|
||||
self.topbar.switcher('security', self.on_security_selection, *security_options)
|
||||
self.topbar.switcher('timeframe', self._on_timeframe_selection, *timeframe_options)
|
||||
self.topbar.switcher('security', self._on_security_selection, *security_options)
|
||||
self.legend(True)
|
||||
self.grid(False, False)
|
||||
self.crosshair(vert_visible=False, horz_visible=False)
|
||||
@ -262,25 +324,26 @@ class PolygonChart(Chart):
|
||||
|
||||
{self.id}.search.window.style.display = "block"
|
||||
{self.id}.search.box.focus()
|
||||
''')
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Shows the PolygonChart window (this method will block).
|
||||
"""
|
||||
asyncio.run(self.show_async(block=True))
|
||||
//let polyLogo = document.createElement('div')
|
||||
//polyLogo.innerHTML = '<svg><g transform="scale(0.9)"><path d="M17.9821362,6 L24,12.1195009 L22.9236698,13.5060353 L17.9524621,27 L14.9907916,17.5798557 L12,12.0454987 L17.9821362,6 Z M21.437,15.304 L18.3670383,19.1065035 L18.367,23.637 L21.437,15.304 Z M18.203,7.335 L15.763,17.462 L17.595,23.287 L17.5955435,18.8249858 L22.963,12.176 L18.203,7.335 Z M17.297,7.799 L12.9564162,12.1857947 L15.228,16.389 L17.297,7.799 Z" fill="#FFFFFF"></path></g></svg>'
|
||||
//polyLogo.style.position = 'absolute'
|
||||
//polyLogo.style.width = '28px'
|
||||
//polyLogo.style.zIndex = 10000
|
||||
//polyLogo.style.right = '18px'
|
||||
//polyLogo.style.top = '-1px'
|
||||
//{self.id}.wrapper.appendChild(polyLogo)
|
||||
''')
|
||||
|
||||
def _polygon(self, symbol):
|
||||
self.spinner(True)
|
||||
self.set(pd.DataFrame())
|
||||
self.crosshair(vert_visible=False, horz_visible=False)
|
||||
if self.topbar['symbol'].value and self.topbar['symbol'].value != symbol:
|
||||
self.polygon.unsubscribe(self.topbar['symbol'].value)
|
||||
|
||||
mult, span = _convert_timeframe(self.topbar['timeframe'].value)
|
||||
delta = dt.timedelta(**{span + 's': int(mult)})
|
||||
short_delta = (delta < dt.timedelta(days=7))
|
||||
start_date = dt.datetime.now()
|
||||
start_date = dt.datetime.now() if self.end_date == 'now' else dt.datetime.strptime(self.end_date, '%Y-%m-%d')
|
||||
remaining_bars = self.num_bars
|
||||
while remaining_bars > 0:
|
||||
start_date -= delta
|
||||
@ -293,20 +356,20 @@ class PolygonChart(Chart):
|
||||
symbol,
|
||||
timeframe=self.topbar['timeframe'].value,
|
||||
start_date=start_date.strftime('%Y-%m-%d'),
|
||||
end_date=self.end_date,
|
||||
limit=self.limit,
|
||||
live=self.live
|
||||
)
|
||||
self.spinner(False)
|
||||
self.crosshair(vert_visible=True, horz_visible=True) if success else None
|
||||
return True if success else False
|
||||
return success
|
||||
|
||||
async def on_search(self, searched_string):
|
||||
self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
|
||||
async def on_search(self, searched_string): self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
|
||||
|
||||
async def on_timeframe_selection(self):
|
||||
self._polygon(self.topbar['symbol'].value)
|
||||
async def _on_timeframe_selection(self):
|
||||
self._polygon(self.topbar['symbol'].value) if self.topbar['symbol'].value else None
|
||||
|
||||
async def on_security_selection(self):
|
||||
async def _on_security_selection(self):
|
||||
sec_type = self.topbar['security'].value
|
||||
self.volume_enabled = False if sec_type == 'Index' else True
|
||||
|
||||
@ -316,7 +379,3 @@ class PolygonChart(Chart):
|
||||
{self.chart.id}.series.applyOptions({{
|
||||
priceFormat: {{precision: {precision}, minMove: {min_move}}}
|
||||
}})''')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ from inspect import iscoroutinefunction
|
||||
try:
|
||||
import wx.html2
|
||||
except ImportError:
|
||||
pass
|
||||
wx = None
|
||||
try:
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||
from PyQt5.QtWebChannel import QWebChannel
|
||||
@ -19,17 +19,17 @@ try:
|
||||
def callback(self, message):
|
||||
_widget_message(self.chart, message)
|
||||
except ImportError:
|
||||
pass
|
||||
QWebEngineView = None
|
||||
try:
|
||||
from streamlit.components.v1 import html
|
||||
except ImportError:
|
||||
pass
|
||||
html = None
|
||||
try:
|
||||
from IPython.display import HTML, display
|
||||
except ImportError:
|
||||
pass
|
||||
HTML = None
|
||||
|
||||
from lightweight_charts.js import LWC, TopBar, CALLBACK_SCRIPT
|
||||
from lightweight_charts.abstract import LWC, TopBar, JS
|
||||
|
||||
|
||||
def _widget_message(chart, string):
|
||||
@ -47,13 +47,12 @@ def _widget_message(chart, string):
|
||||
|
||||
class WxChart(LWC):
|
||||
def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0,
|
||||
api: object = None, topbar: bool = False, searchbox: bool = False):
|
||||
try:
|
||||
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
|
||||
except NameError:
|
||||
scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False):
|
||||
if wx is None:
|
||||
raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.')
|
||||
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
|
||||
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height, scale_candles_only=scale_candles_only)
|
||||
self.api = api
|
||||
self._script_func = self.webview.RunScript
|
||||
self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)'
|
||||
@ -63,7 +62,7 @@ class WxChart(LWC):
|
||||
self.webview.AddScriptMessageHandler('wx_msg')
|
||||
self.webview.SetPage(self._html, '')
|
||||
|
||||
self.webview.AddUserScript(CALLBACK_SCRIPT)
|
||||
self.webview.AddUserScript(JS['callback'])
|
||||
self._create_chart()
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
@ -73,12 +72,12 @@ class WxChart(LWC):
|
||||
|
||||
class QtChart(LWC):
|
||||
def __init__(self, widget=None, api: object = None, topbar: bool = False, searchbox: bool = False,
|
||||
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
|
||||
try:
|
||||
self.webview = QWebEngineView(widget)
|
||||
except NameError:
|
||||
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, scale_candles_only: bool = False):
|
||||
if QWebEngineView is None:
|
||||
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.')
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
|
||||
self.webview = QWebEngineView(widget)
|
||||
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height, scale_candles_only=scale_candles_only)
|
||||
self.api = api
|
||||
self._script_func = self.webview.page().runJavaScript
|
||||
self._js_api_code = 'window.pythonObject.callback'
|
||||
@ -101,7 +100,7 @@ class QtChart(LWC):
|
||||
'''
|
||||
self.webview.page().setHtml(self._html)
|
||||
|
||||
self.run_script(CALLBACK_SCRIPT)
|
||||
self.run_script(JS['callback'])
|
||||
self._create_chart()
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
@ -110,8 +109,8 @@ class QtChart(LWC):
|
||||
|
||||
|
||||
class StaticLWC(LWC):
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1):
|
||||
super().__init__(volume_enabled, inner_width, inner_height)
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, scale_candles_only=scale_candles_only)
|
||||
self.width = width
|
||||
self.height = height
|
||||
self._html = self._html.replace('</script>\n</body>\n</html>', '')
|
||||
@ -134,20 +133,19 @@ class StaticLWC(LWC):
|
||||
|
||||
|
||||
class StreamlitChart(StaticLWC):
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height)
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
|
||||
self._create_chart()
|
||||
|
||||
def _load(self):
|
||||
try:
|
||||
html(f'{self._html}</script></body></html>', width=self.width, height=self.height)
|
||||
except NameError:
|
||||
if html is None:
|
||||
raise ModuleNotFoundError('streamlit.components.v1.html was not found, and must be installed to use StreamlitChart.')
|
||||
html(f'{self._html}</script></body></html>', width=self.width, height=self.height)
|
||||
|
||||
|
||||
class JupyterChart(StaticLWC):
|
||||
def __init__(self, volume_enabled=True, width=800, height=350, inner_width=1, inner_height=1):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height)
|
||||
def __init__(self, volume_enabled=True, width=800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
|
||||
self._position = ""
|
||||
|
||||
self._create_chart(autosize=False)
|
||||
@ -164,8 +162,6 @@ class JupyterChart(StaticLWC):
|
||||
self.run_script(f'{self.id}.chart.resize({width}, {height})')
|
||||
|
||||
def _load(self):
|
||||
try:
|
||||
display(HTML(f'{self._html}</script></body></html>'))
|
||||
except NameError:
|
||||
if HTML is None:
|
||||
raise ModuleNotFoundError('IPython.display.HTML was not found, and must be installed to use JupyterChart.')
|
||||
|
||||
display(HTML(f'{self._html}</script></body></html>'))
|
||||
|
||||
Reference in New Issue
Block a user