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:
louisnw
2023-06-28 18:36:32 +01:00
parent adfc58a8af
commit d9c8aa3bd8
16 changed files with 932 additions and 649 deletions

View File

@ -24,14 +24,10 @@ ___
1. Simple and easy to use. 1. Simple and easy to use.
2. Blocking or non-blocking GUI. 2. Blocking or non-blocking GUI.
3. Streamlined for live data, with methods for updating directly from tick data. 3. Streamlined for live data, with methods for updating directly from tick data.
4. Supports: 4. __Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
* 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)
5. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#callbacks) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more. 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). 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. Display data from a csv:

View File

@ -1,7 +1,7 @@
project = 'lightweight-charts-python' project = 'lightweight-charts-python'
copyright = '2023, louisnw' copyright = '2023, louisnw'
author = 'louisnw' author = 'louisnw'
release = '1.0.12' release = '1.0.13'
extensions = ["myst_parser"] extensions = ["myst_parser"]

View File

@ -99,6 +99,16 @@ ___
Removes a horizontal line at the given price. 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` ### `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` `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` ### `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. Configures the legend of the chart.
___ ___
@ -201,7 +211,9 @@ ___
Shows the hidden candles on the chart. 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` ### `create_subchart`
`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `-> SubChart` `volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `-> SubChart`
@ -222,7 +234,7 @@ ___
## Chart ## 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` `api: object` | `topbar: bool` | `searchbox: bool`
The main object used for the normal functionality of lightweight-charts-python, built on the pywebview library. 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). 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 ## Line
@ -264,11 +297,21 @@ The `line` object should only be accessed from the [`create_line`](#create-line)
___ ___
### `set` ### `set`
`data: pd.DataFrame` `data: pd.DataFrame` `name: str`
Sets the data for the line. 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` ### `update`

View File

@ -1,11 +1,11 @@
```{toctree} ```{toctree}
:hidden: :hidden:
:caption: Contents
:maxdepth: 3
docs docs
polygon
Github Repository <https://github.com/louisnw01/lightweight-charts-python> Github Repository <https://github.com/louisnw01/lightweight-charts-python>
``` ```
```{include} ../../README.md ```{include} ../../README.md
```

135
docs/source/polygon.md Normal file
View 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)
```
![PolygonChart png](https://github.com/louisnw01/lightweight-charts-python/blob/main/docs/source/polygonchart.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -1,4 +1,4 @@
from .js import LWC from .abstract import LWC
from .chart import Chart from .chart import Chart
from .widgets import JupyterChart from .widgets import JupyterChart
from .polygon import PolygonChart from .polygon import PolygonChart

View File

@ -1,12 +1,51 @@
import pandas as pd import os
from datetime import timedelta, datetime 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, \
from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_style, \ _line_style, \
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _marker_position, _marker_shape, IDGen 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: class SeriesCommon:
def _set_interval(self, df: pd.DataFrame): def _set_interval(self, df: pd.DataFrame):
common_interval = pd.to_datetime(df['time']).diff().value_counts() common_interval = pd.to_datetime(df['time']).diff().value_counts()
@ -14,6 +53,11 @@ class SeriesCommon:
self._interval = common_interval.index[0] self._interval = common_interval.index[0]
except IndexError: except IndexError:
raise IndexError('Not enough bars within the given data to calculate the interval/timeframe.') 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): def _df_datetime_format(self, df: pd.DataFrame):
df = df.copy() df = df.copy()
@ -95,12 +139,27 @@ class SeriesCommon:
Removes a horizontal line at the given price. Removes a horizontal line at the given price.
""" """
self.run_script(f''' self.run_script(f'''
{self.id}.horizontal_lines.forEach(function (line) {{ {self.id}.horizontal_lines.forEach(function (line) {{
if ({price} === line.price) {{ if ({price} === line.price) {{
{self.id}.series.removePriceLine(line.line); {self.id}.series.removePriceLine(line.line);
{self.id}.horizontal_lines.splice({self.id}.horizontal_lines.indexOf(line), 1) {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}"}})') def title(self, title: str): self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})')
@ -123,30 +182,43 @@ class SeriesCommon:
class Line(SeriesCommon): class Line(SeriesCommon):
def __init__(self, parent, color, width, price_line, price_label): def __init__(self, chart, color, width, price_line, price_label):
self._parent = parent self.color = color
self._rand = self._parent._rand self.name = ''
self._chart = chart
self._rand = chart._rand
self.id = f'window.{self._rand.generate()}' 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.run_script(f'''
{self.id} = {{ {self.id} = {{
series: {self._parent.id}.chart.addLineSeries({{ series: {self._chart.id}.chart.addLineSeries({{
color: '{color}', color: '{color}',
lineWidth: {width}, lineWidth: {width},
lastValueVisible: {_js_bool(price_label)}, lastValueVisible: {_js_bool(price_label)},
priceLineVisible: {_js_bool(price_line)}, priceLineVisible: {_js_bool(price_line)},
{"""autoscaleInfoProvider: () => ({
priceRange: {
minValue: 1_000_000_000,
maxValue: 0,
},
}),""" if self._chart._scale_candles_only else ''}
}}), }}),
markers: [], markers: [],
horizontal_lines: [], horizontal_lines: [],
}} }}''')
''')
def set(self, data: pd.DataFrame): def set(self, data: pd.DataFrame, name=None):
""" """
Sets the line data.\n 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._last_bar = df.iloc[-1]
self.run_script(f'{self.id}.series.setData({df.to_dict("records")})') self.run_script(f'{self.id}.series.setData({df.to_dict("records")})')
@ -155,7 +227,7 @@ class Line(SeriesCommon):
Updates the line data.\n Updates the line data.\n
:param series: labels: date/time, value :param series: labels: date/time, value
""" """
series = self._parent._series_datetime_format(series) series = self._series_datetime_format(series)
self._last_bar = series self._last_bar = series
self.run_script(f'{self.id}.series.update({series.to_dict()})') 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. 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.run_script(f'''
{self._parent.id}.chart.removeSeries({self.id}.series) {self._chart.id}.chart.removeSeries({self.id}.series)
delete {self.id} delete {self.id}
''') ''')
del self del self
@ -174,7 +246,7 @@ class Line(SeriesCommon):
class Widget: class Widget:
def __init__(self, topbar): def __init__(self, topbar):
self._chart = topbar._chart self._chart = topbar._chart
self.method = None self._method = None
class TextWidget(Widget): class TextWidget(Widget):
@ -193,7 +265,7 @@ class SwitcherWidget(Widget):
def __init__(self, topbar, method, *options, default): def __init__(self, topbar, method, *options, default):
super().__init__(topbar) super().__init__(topbar)
self.value = default self.value = default
self.method = method.__name__ self._method = method.__name__
self._chart.run_script(f''' self._chart.run_script(f'''
makeSwitcher({self._chart.id}, {list(options)}, '{default}', {self._chart._js_api_code}, '{method.__name__}', 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}') '{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): def _widget_with_method(self, method_name):
for widget in self._widgets.values(): for widget in self._widgets.values():
if widget.method == method_name: if widget._method == method_name:
return widget return widget
class LWC(SeriesCommon): 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.volume_enabled = volume_enabled
self._scale_candles_only = scale_candles_only
self._inner_width = inner_width self._inner_width = inner_width
self._inner_height = inner_height self._inner_height = inner_height
self._dynamic_loading = dynamic_loading self._dynamic_loading = dynamic_loading
@ -248,6 +322,7 @@ class LWC(SeriesCommon):
self._charts = {self.id: self} self._charts = {self.id: self}
self._lines = [] self._lines = []
self._js_api_code = None self._js_api_code = None
self._return_q = None
self._background_color = '#000000' self._background_color = '#000000'
self._volume_up_color = 'rgba(83,141,131,0.8)' self._volume_up_color = 'rgba(83,141,131,0.8)'
@ -340,7 +415,7 @@ class LWC(SeriesCommon):
timer = null; timer = null;
}}, 50); }}, 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): def fit(self):
""" """
@ -423,12 +498,11 @@ class LWC(SeriesCommon):
self._lines.append(Line(self, color, width, price_line, price_label)) self._lines.append(Line(self, color, width, price_line, price_label))
return self._lines[-1] return self._lines[-1]
def lines(self): def lines(self) -> List[Line]:
""" """
Returns all lines for the chart. 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, 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, 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)}, secondsVisible: {_js_bool(seconds_visible)},
borderVisible: {_js_bool(border_visible)}, borderVisible: {_js_bool(border_visible)},
{f'borderColor: "{border_color}",' if border_color else ''} {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): font_size: int = None, font_family: str = None):
""" """
Configures the legend of the chart. Configures the legend of the chart.
""" """
if visible: if not visible:
self.run_script(f''' return
{f"{self.id}.legend.style.color = '{color}'" if color else ''} lines_code = ''
{f"{self.id}.legend.style.fontSize = {font_size}" if font_size else ''} for i, line in enumerate(self._lines):
{f"{self.id}.legend.style.fontFamily = '{font_family}'" if font_family else ''} 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){{ self.run_script(f'''
let data = param.seriesData.get({self.id}.series); {f"{self.id}.legend.style.color = '{color}'" if color else ''}
if (!data) {{return}} {f"{self.id}.legend.style.fontSize = {font_size}" if font_size else ''}
let ohlc = `O ${{legendItemFormat(data.open)}} {f"{self.id}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
| H ${{legendItemFormat(data.high)}}
| L ${{legendItemFormat(data.low)}} {self.id}.chart.subscribeCrosshairMove((param) => {{
| C ${{legendItemFormat(data.close)}} ` if (param.time){{
let percentMove = ((data.close-data.open)/data.open)*100 let data = param.seriesData.get({self.id}.series);
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %` if (!data) {{return}}
let finalString = '' let ohlc = `O ${{legendItemFormat(data.open)}}
{'finalString += ohlc' if ohlc else ''} | H ${{legendItemFormat(data.high)}}
{'finalString += percent' if percent else ''} | L ${{legendItemFormat(data.low)}}
{self.id}.legend.innerHTML = finalString | C ${{legendItemFormat(data.close)}} `
}} let percentMove = ((data.close-data.open)/data.open)*100
else {{ let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
{self.id}.legend.innerHTML = '' 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 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', 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, width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False,
topbar: bool = False, searchbox: bool = False): topbar: bool = False, searchbox: bool = False):
@ -633,6 +731,7 @@ class SubChart(LWC):
self._position = position self._position = position
self._rand = self._chart._rand self._rand = self._chart._rand
self._js_api_code = self._chart._js_api_code self._js_api_code = self._chart._js_api_code
self._return_q = self._chart._return_q
self.run_script = self._chart.run_script self.run_script = self._chart.run_script
self._charts = self._chart._charts self._charts = self._chart._charts
self.id = f'window.{self._rand.generate()}' self.id = f'window.{self._rand.generate()}'
@ -655,407 +754,4 @@ class SubChart(LWC):
''', run_last=True) ''', 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)
}
'''

View File

@ -1,28 +1,30 @@
import asyncio import asyncio
import time
import multiprocessing as mp import multiprocessing as mp
import webview import webview
from lightweight_charts.js import LWC, CALLBACK_SCRIPT, TopBar from lightweight_charts.abstract import LWC, JS, TopBar
class CallbackAPI: 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): def callback(self, message: str):
messages = message.split('__') messages = message.split('__')
name, chart_id = messages[:2] name, chart_id = messages[:2]
args = 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: 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.queue = q
self.exit = exit self.exit = exit
self.loaded = loaded self.loaded = loaded
self.debug = debug 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, 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') x=x, y=y, background_color='#000000')
self.webview.events.loaded += self.on_js_load self.webview.events.loaded += self.on_js_load
@ -40,30 +42,28 @@ class PyWV:
except KeyError: except KeyError:
return return
def on_js_load(self): def on_js_load(self): self.loaded.set(), self.loop()
self.loaded.set(), self.loop()
class Chart(LWC): class Chart(LWC):
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None, 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, on_top: bool = False, maximize: bool = False, debug: bool = False,
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False): api: object = None, topbar: bool = False, searchbox: bool = False,
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading) inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False, scale_candles_only: bool = False):
self._emit = mp.Queue() super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading, scale_candles_only)
self._q = mp.Queue() 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._script_func = self._q.put
self._exit = mp.Event() self._api = api
self._loaded = mp.Event() self._js_api_code = 'pywebview.api.callback'
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html, 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._process.start()
self._create_chart() self._create_chart()
self.api = api
self._js_api_code = 'pywebview.api.callback'
if not topbar and not searchbox: if not topbar and not searchbox:
return return
self.run_script(CALLBACK_SCRIPT) self.run_script(JS['callback'])
self.run_script(f'makeSpinner({self.id})') self.run_script(f'makeSpinner({self.id})')
self.topbar = TopBar(self) if topbar else None self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None self._make_search_box() if searchbox else None
@ -80,50 +80,35 @@ class Chart(LWC):
else: else:
self._q.put('show') self._q.put('show')
if block: if block:
try: asyncio.run(self.show_async(block=True))
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
async def show_async(self, block=False): async def show_async(self, block=False):
if not self.loaded: self.show(block=False)
self._q.put('start') if not block:
self._loaded.wait() asyncio.create_task(self.show_async(block=True))
self._on_js_load() return
else: try:
self._q.put('show') while 1:
if block: while self._emit_q.empty() and not self._exit.is_set() and self.polygon._q.empty():
try: await asyncio.sleep(0.05)
while 1: if self._exit.is_set():
while self._emit.empty() and not self._exit.is_set() and self.polygon._q.empty(): self._exit.clear()
await asyncio.sleep(0.05) return
if self._exit.is_set(): elif not self._emit_q.empty():
self._exit.clear() key, chart_id, arg = self._emit_q.get()
return self._api.chart = self._charts[chart_id]
elif not self._emit.empty(): if widget := self._api.chart.topbar._widget_with_method(key):
key, chart_id, arg = self._emit.get() widget.value = arg
self.api.chart = self._charts[chart_id] await getattr(self._api, key)()
if widget := self.api.chart.topbar._widget_with_method(key): else:
widget.value = arg await getattr(self._api, key)(arg)
await getattr(self.api, key)() continue
else: value = self.polygon._q.get()
await getattr(self.api, key)(arg) func, args = value[0], value[1:]
continue func(*args)
value = self.polygon._q.get() except KeyboardInterrupt:
func, args = value[0], value[1:] return
func(*args)
except KeyboardInterrupt:
return
asyncio.create_task(self.show_async(block=True))
def hide(self): def hide(self):
""" """

View 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)
}

View 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

View File

@ -22,6 +22,12 @@ except ImportError:
class PolygonAPI: 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): def __init__(self, chart):
ch = logging.StreamHandler() ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(asctime)s | [polygon.io] %(levelname)s: %(message)s', datefmt='%H:%M:%S')) 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.setLevel(logging.ERROR)
self._log.addHandler(ch) self._log.addHandler(ch)
self.max_ticks_per_response = 20
self._chart = chart self._chart = chart
self._lasts = {} # $$ self._lasts = {}
self._key = None self._key = None
self._using_live_data = False
self._using_live = {'stocks': False, 'options': False, 'indices': False, 'crypto': False, 'forex': False} self._ws_q = queue.Queue()
self._ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
self._send_q = queue.Queue()
self._q = queue.Queue() self._q = queue.Queue()
self._lock = threading.Lock() self._lock = threading.Lock()
def _subchart(self, subchart): self._using_live_data = False
return PolygonAPISubChart(self, subchart) 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): 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) 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): 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 timeframe: Timeframe to request (1min, 5min, 2H, 1D, 1W, 2M, etc).
:param start_date: Start date of the data (YYYY-MM-DD). :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 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. :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, 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): 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)): if any((expiration, right, strike)):
symbol = f'O:{symbol}{dt.datetime.strptime(expiration, "%Y-%m-%d").strftime("%y%m%d")}{right}{strike * 1000:08d}' symbol = f'{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 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): 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): 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): 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): def _set(self, chart, sec_type, ticker, timeframe, start_date, end_date, limit, live):
if requests is None: if requests is None:
raise ImportError('The "requests" library was not found, and must be installed to use polygon.io.') 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 end_date = dt.datetime.now().strftime('%Y-%m-%d') if end_date == 'now' else end_date
mult, span = _convert_timeframe(timeframe) 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'}) response = requests.get(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
if response.status_code != 200: if response.status_code != 200:
error = response.json() error = response.json()
@ -93,11 +148,6 @@ class PolygonAPI:
self._log.error(f'No results for "{ticker}" ({sec_type})') self._log.error(f'No results for "{ticker}" ({sec_type})')
return 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']) df = pd.DataFrame(data['results'])
columns = ['t', 'o', 'h', 'l', 'c'] columns = ['t', 'o', 'h', 'l', 'c']
rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'} rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'}
@ -106,45 +156,40 @@ class PolygonAPI:
columns.append('v') columns.append('v')
df = df[columns].rename(columns=rename) df = df[columns].rename(columns=rename)
df['time'] = pd.to_datetime(df['time'], unit='ms') df['time'] = pd.to_datetime(df['time'], unit='ms')
chart.set(df) chart.set(df)
if not live: if not live:
return True return True
if not self._using_live_data: if not self._using_live_data:
threading.Thread(target=asyncio.run, args=[self._thread_loop()], daemon=True).start() threading.Thread(target=asyncio.run, args=[self._thread_loop()], daemon=True).start()
self._using_live_data = True self._using_live_data = True
with self._lock: with self._lock:
if not self._ws[sec_type]: if not self._ws[sec_type]:
self._send_q.put(('_websocket_connect', self._key, sec_type)) self._ws_q.put(('_websocket_connect', self._key, sec_type))
self._send_q.put(('_subscribe', chart, sec_type, ticker)) self._ws_q.put(('_subscribe', chart, ticker, sec_type))
return True return True
async def _thread_loop(self): async def _thread_loop(self):
while 1: while 1:
while self._send_q.empty(): while self._ws_q.empty():
await asyncio.sleep(0.05) await asyncio.sleep(0.05)
value = self._send_q.get() value = self._ws_q.get()
func, args = value[0], value[1:] func, args = value[0], value[1:]
asyncio.create_task(getattr(self, func)(*args)) asyncio.create_task(getattr(self, func)(*args))
def unsubscribe(self, symbol): async def _subscribe(self, chart, ticker, sec_type):
self._send_q.put(('_unsubscribe', self._chart, symbol)) key = ticker if ':' not in ticker else ticker.split(':')[1]
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]
if not self._lasts.get(key): 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] = { self._lasts[key] = {
'ticker': ticker,
'sec_type': sec_type, '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'], 'price': chart._last_bar['close'],
'charts': [], 'charts': [],
} }
@ -158,18 +203,22 @@ class PolygonAPI:
return return
self._lasts[key]['charts'].append(chart) self._lasts[key]['charts'].append(chart)
async def _unsubscribe(self, chart, ticker): async def _unsubscribe(self, chart):
key = ticker if '.' not in ticker else ticker.split('.')[1] for data in self._lasts.values():
key = key if ':' not in key else key.split(':')[1] if chart in data['charts']:
if chart in self._lasts[key]['charts']: break
self._lasts[key]['charts'].remove(chart) else:
if self._lasts[key]['charts']:
return return
if chart in data['charts']:
data['charts'].remove(chart)
if data['charts']:
return
while self._q.qsize(): while self._q.qsize():
self._q.get() # Flush the queue self._q.get() # Flush the queue
quotes, aggs = self._lasts[key]['sub_type'] quotes, aggs = data['sub_type']
await self._send(self._lasts[key]['sec_type'], 'unsubscribe', f'{quotes}.{ticker}') await self._send(data['sec_type'], 'unsubscribe', f'{quotes}.{data["ticker"]}')
await self._send(self._lasts[key]['sec_type'], 'unsubscribe', f'{aggs}.{ticker}') await self._send(data['sec_type'], 'unsubscribe', f'{aggs}.{data["ticker"]}')
async def _send(self, sec_type, action, params): async def _send(self, sec_type, action, params):
while 1: while 1:
@ -213,7 +262,6 @@ class PolygonAPI:
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE 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: async with websockets.connect(f'wss://socket.polygon.io/{sec_type}', ssl=ssl_context) as ws:
with self._lock: with self._lock:
self._ws[sec_type] = ws self._ws[sec_type] = ws
@ -225,10 +273,13 @@ class PolygonAPI:
if data['ev'] == 'status': if data['ev'] == 'status':
self._log.info(f'{data["message"]}') self._log.info(f'{data["message"]}')
continue 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 continue
await self._handle_tick(sec_type, data) await self._handle_tick(sec_type, data)
def _subchart(self, subchart):
return PolygonAPISubChart(self, subchart)
class PolygonAPISubChart(PolygonAPI): class PolygonAPISubChart(PolygonAPI):
def __init__(self, polygon, subchart): def __init__(self, polygon, subchart):
@ -237,22 +288,33 @@ class PolygonAPISubChart(PolygonAPI):
class PolygonChart(Chart): 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'), timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'), 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): width: int = 800, height: int = 600, x: int = None, y: int = None,
super().__init__(volume_enabled=True, width=width, height=height, x=x, y=y, on_top=on_top, debug=debug, 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) api=self, topbar=True, searchbox=True)
self.chart = self self.chart = self
self.num_bars = num_bars self.num_bars = num_bars
self.end_date = end_date
self.limit = limit self.limit = limit
self.live = live self.live = live
self.polygon.api_key(api_key) self.polygon.api_key(api_key)
self.topbar.active_background_color = 'rgb(91, 98, 246)' self.topbar.active_background_color = 'rgb(91, 98, 246)'
self.topbar.textbox('symbol') self.topbar.textbox('symbol')
self.topbar.switcher('timeframe', self.on_timeframe_selection, *timeframe_options) self.topbar.switcher('timeframe', self._on_timeframe_selection, *timeframe_options)
self.topbar.switcher('security', self.on_security_selection, *security_options) self.topbar.switcher('security', self._on_security_selection, *security_options)
self.legend(True) self.legend(True)
self.grid(False, False) self.grid(False, False)
self.crosshair(vert_visible=False, horz_visible=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.window.style.display = "block"
{self.id}.search.box.focus() {self.id}.search.box.focus()
//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 show(self):
"""
Shows the PolygonChart window (this method will block).
"""
asyncio.run(self.show_async(block=True))
def _polygon(self, symbol): def _polygon(self, symbol):
self.spinner(True) self.spinner(True)
self.set(pd.DataFrame()) self.set(pd.DataFrame())
self.crosshair(vert_visible=False, horz_visible=False) 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) mult, span = _convert_timeframe(self.topbar['timeframe'].value)
delta = dt.timedelta(**{span + 's': int(mult)}) delta = dt.timedelta(**{span + 's': int(mult)})
short_delta = (delta < dt.timedelta(days=7)) 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 remaining_bars = self.num_bars
while remaining_bars > 0: while remaining_bars > 0:
start_date -= delta start_date -= delta
@ -293,20 +356,20 @@ class PolygonChart(Chart):
symbol, symbol,
timeframe=self.topbar['timeframe'].value, timeframe=self.topbar['timeframe'].value,
start_date=start_date.strftime('%Y-%m-%d'), start_date=start_date.strftime('%Y-%m-%d'),
end_date=self.end_date,
limit=self.limit, limit=self.limit,
live=self.live live=self.live
) )
self.spinner(False) self.spinner(False)
self.crosshair(vert_visible=True, horz_visible=True) if success else None 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): async def on_search(self, searched_string): self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
self.topbar['symbol'].set(searched_string if self._polygon(searched_string) else '')
async def on_timeframe_selection(self): async def _on_timeframe_selection(self):
self._polygon(self.topbar['symbol'].value) 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 sec_type = self.topbar['security'].value
self.volume_enabled = False if sec_type == 'Index' else True self.volume_enabled = False if sec_type == 'Index' else True
@ -316,7 +379,3 @@ class PolygonChart(Chart):
{self.chart.id}.series.applyOptions({{ {self.chart.id}.series.applyOptions({{
priceFormat: {{precision: {precision}, minMove: {min_move}}} priceFormat: {{precision: {precision}, minMove: {min_move}}}
}})''') }})''')

View File

@ -90,4 +90,4 @@ def _convert_timeframe(timeframe):
except IndexError: except IndexError:
return 1, spans[timeframe] return 1, spans[timeframe]
timespan = spans[timeframe.replace(multiplier, '')] timespan = spans[timeframe.replace(multiplier, '')]
return multiplier, timespan return multiplier, timespan

View File

@ -4,7 +4,7 @@ from inspect import iscoroutinefunction
try: try:
import wx.html2 import wx.html2
except ImportError: except ImportError:
pass wx = None
try: try:
from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebChannel import QWebChannel from PyQt5.QtWebChannel import QWebChannel
@ -19,17 +19,17 @@ try:
def callback(self, message): def callback(self, message):
_widget_message(self.chart, message) _widget_message(self.chart, message)
except ImportError: except ImportError:
pass QWebEngineView = None
try: try:
from streamlit.components.v1 import html from streamlit.components.v1 import html
except ImportError: except ImportError:
pass html = None
try: try:
from IPython.display import HTML, display from IPython.display import HTML, display
except ImportError: 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): def _widget_message(chart, string):
@ -47,13 +47,12 @@ def _widget_message(chart, string):
class WxChart(LWC): class WxChart(LWC):
def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, 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): scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False):
try: if wx is None:
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
except NameError:
raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.') 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.api = api
self._script_func = self.webview.RunScript self._script_func = self.webview.RunScript
self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)' 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.AddScriptMessageHandler('wx_msg')
self.webview.SetPage(self._html, '') self.webview.SetPage(self._html, '')
self.webview.AddUserScript(CALLBACK_SCRIPT) self.webview.AddUserScript(JS['callback'])
self._create_chart() self._create_chart()
self.topbar = TopBar(self) if topbar else None self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None self._make_search_box() if searchbox else None
@ -73,12 +72,12 @@ class WxChart(LWC):
class QtChart(LWC): class QtChart(LWC):
def __init__(self, widget=None, api: object = None, topbar: bool = False, searchbox: bool = False, 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): volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, scale_candles_only: bool = False):
try: if QWebEngineView is None:
self.webview = QWebEngineView(widget)
except NameError:
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.') 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.api = api
self._script_func = self.webview.page().runJavaScript self._script_func = self.webview.page().runJavaScript
self._js_api_code = 'window.pythonObject.callback' self._js_api_code = 'window.pythonObject.callback'
@ -101,7 +100,7 @@ class QtChart(LWC):
''' '''
self.webview.page().setHtml(self._html) self.webview.page().setHtml(self._html)
self.run_script(CALLBACK_SCRIPT) self.run_script(JS['callback'])
self._create_chart() self._create_chart()
self.topbar = TopBar(self) if topbar else None self.topbar = TopBar(self) if topbar else None
self._make_search_box() if searchbox else None self._make_search_box() if searchbox else None
@ -110,8 +109,8 @@ class QtChart(LWC):
class StaticLWC(LWC): class StaticLWC(LWC):
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1): 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) super().__init__(volume_enabled, inner_width, inner_height, scale_candles_only=scale_candles_only)
self.width = width self.width = width
self.height = height self.height = height
self._html = self._html.replace('</script>\n</body>\n</html>', '') self._html = self._html.replace('</script>\n</body>\n</html>', '')
@ -134,20 +133,19 @@ class StaticLWC(LWC):
class StreamlitChart(StaticLWC): class StreamlitChart(StaticLWC):
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1): 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) super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
self._create_chart() self._create_chart()
def _load(self): def _load(self):
try: if html is None:
html(f'{self._html}</script></body></html>', width=self.width, height=self.height)
except NameError:
raise ModuleNotFoundError('streamlit.components.v1.html was not found, and must be installed to use StreamlitChart.') 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): class JupyterChart(StaticLWC):
def __init__(self, volume_enabled=True, width=800, height=350, inner_width=1, inner_height=1): 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) super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
self._position = "" self._position = ""
self._create_chart(autosize=False) self._create_chart(autosize=False)
@ -164,8 +162,6 @@ class JupyterChart(StaticLWC):
self.run_script(f'{self.id}.chart.resize({width}, {height})') self.run_script(f'{self.id}.chart.resize({width}, {height})')
def _load(self): def _load(self):
try: if HTML is None:
display(HTML(f'{self._html}</script></body></html>'))
except NameError:
raise ModuleNotFoundError('IPython.display.HTML was not found, and must be installed to use JupyterChart.') raise ModuleNotFoundError('IPython.display.HTML was not found, and must be installed to use JupyterChart.')
display(HTML(f'{self._html}</script></body></html>'))

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup( setup(
name='lightweight_charts', name='lightweight_charts',
version='1.0.12', version='1.0.13',
packages=find_packages(), packages=find_packages(),
python_requires='>=3.9', python_requires='>=3.9',
install_requires=[ install_requires=[