Bug Fixes/Enhancements:
- Added the chart.spinner method, which when set to `True` shows a loading spinner on the chart (a nice visual for API calls, large datasets etc). - If an empty data frame is passed to set (eg.`chart.set(pd.DataFrame())`) the volume series and candle series will be cleared. - added the `cumulative_volume` parameter to `update_from_tick`, which adds the given volume tick onto the latest bar. - Added `vert_visible` and `horz_visible` parameters to `crosshair`. - Small style improvements to the searchbox and topbar. - Fixed a bug preventing callbacks within `WxChart` and `QtChart` Thanks to @emma-uw for the following fixes and enhancements! - Methods `hide_data`, `show_data` and `price_line` can be used within Charts, Subcharts and Lines to change the visibility of data, price lines, and the price line labels. - Added the `delete` method to Line, which irreversably deletes the Line on the chart as well as its objects in JavaScript and Python. - Added the `lines` common method, which returns a list of all Line objects for the chart. - Added the `fit` method to the common methods, which uses the `fitContent()` method from Lightweight Charts. - Fixed a big which caused synced SubCharts to be out of sync upon loading. BETA: Polygon.io integration - Added the `PolygonChart` and `polygon` method, allowing for direct integration of polygon.io’s API. - This feature is still in beta, and there will be a full announcement and update once the feature is complete!
This commit is contained in:
10
README.md
10
README.md
@ -25,11 +25,11 @@ ___
|
|||||||
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:
|
||||||
* PyQt
|
* PyQt -> [`QtChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#qtchart)
|
||||||
* wxPython
|
* wxPython -> [`WxChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#wxchart)
|
||||||
* Streamlit
|
* Streamlit -> [`StreamlitChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#streamlitchart)
|
||||||
* asyncio
|
* asyncio -> [`show_async()`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#show-async)
|
||||||
* Jupyter Notebooks using the [`JupyterChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#jupyterchart)
|
* 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).
|
||||||
___
|
___
|
||||||
|
|||||||
@ -26,6 +26,8 @@ The `time` column can also be named `date`, and the `volume` column can be omitt
|
|||||||
```{important}
|
```{important}
|
||||||
the `time` column must have rows all of the same timezone and locale. This is particularly noticeable for data which crosses over daylight saving hours on data with intervals of less than 1 day. Errors are likely to be raised if they are not converted beforehand.
|
the `time` column must have rows all of the same timezone and locale. This is particularly noticeable for data which crosses over daylight saving hours on data with intervals of less than 1 day. Errors are likely to be raised if they are not converted beforehand.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
An empty `DataFrame` object can also be given to this method, which will erase all candle and volume data displayed on the chart.
|
||||||
___
|
___
|
||||||
|
|
||||||
### `update`
|
### `update`
|
||||||
@ -37,7 +39,7 @@ The bar should contain values with labels of the same name as the columns requir
|
|||||||
___
|
___
|
||||||
|
|
||||||
### `update_from_tick`
|
### `update_from_tick`
|
||||||
`series: pd.Series`
|
`series: pd.Series` | `cumulative_volume: bool`
|
||||||
|
|
||||||
Updates the chart from a tick.
|
Updates the chart from a tick.
|
||||||
|
|
||||||
@ -51,6 +53,7 @@ As before, the `time` can also be named `date`, and the `volume` can be omitted
|
|||||||
The provided ticks do not need to be rounded to an interval (1 min, 5 min etc.), as the library handles this automatically.```````
|
The provided ticks do not need to be rounded to an interval (1 min, 5 min etc.), as the library handles this automatically.```````
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `cumulative_volume` is used, the volume data given to this method will be added onto the latest bar of volume data.
|
||||||
___
|
___
|
||||||
|
|
||||||
### `create_line`
|
### `create_line`
|
||||||
@ -59,6 +62,11 @@ ___
|
|||||||
Creates and returns a [Line](#line) object.
|
Creates and returns a [Line](#line) object.
|
||||||
___
|
___
|
||||||
|
|
||||||
|
### `lines`
|
||||||
|
`-> List[Line]`
|
||||||
|
|
||||||
|
Returns a list of all Line objects for the chart or subchart.
|
||||||
|
___
|
||||||
### `marker`
|
### `marker`
|
||||||
`time: datetime` | `position: 'above'/'below'/'inside'` | `shape: 'arrow_up'/'arrow_down'/'circle'/'square'` | `color: str` | `text: str` | `-> str`
|
`time: datetime` | `position: 'above'/'below'/'inside'` | `shape: 'arrow_up'/'arrow_down'/'circle'/'square'` | `color: str` | `text: str` | `-> str`
|
||||||
|
|
||||||
@ -141,7 +149,7 @@ The float values given to scale the margins must be greater than 0 and less than
|
|||||||
___
|
___
|
||||||
|
|
||||||
### `crosshair`
|
### `crosshair`
|
||||||
`mode` | `vert_width: int` | `vert_color: str` | `vert_style: str` | `vert_label_background_color: str` | `horz_width: int` | `horz_color: str` | `horz_style: str` | `horz_label_background_color: str`
|
`mode` | `vert_visible: bool` | `vert_width: int` | `vert_color: str` | `vert_style: str` | `vert_label_background_color: str` | `horz_visible: bool` | `horz_width: int` | `horz_color: str` | `horz_style: str` | `horz_label_background_color: str`
|
||||||
|
|
||||||
Crosshair formatting for its vertical and horizontal axes.
|
Crosshair formatting for its vertical and horizontal axes.
|
||||||
|
|
||||||
@ -166,6 +174,34 @@ ___
|
|||||||
Configures the legend of the chart.
|
Configures the legend of the chart.
|
||||||
___
|
___
|
||||||
|
|
||||||
|
### `spinner`
|
||||||
|
`visible: bool`
|
||||||
|
|
||||||
|
Shows a loading spinner on the chart, which can be used to visualise the loading of large datasets, API calls, etc.
|
||||||
|
___
|
||||||
|
|
||||||
|
### `price_line`
|
||||||
|
`label_visible: bool` | `line_visible: bool`
|
||||||
|
|
||||||
|
Configures the visibility of the last value price line and its label.
|
||||||
|
___
|
||||||
|
|
||||||
|
### `fit`
|
||||||
|
|
||||||
|
Attempts to fit all data displayed on the chart within the viewport (`fitContent()`).
|
||||||
|
___
|
||||||
|
|
||||||
|
### `hide_data`
|
||||||
|
|
||||||
|
Hides the candles on the chart.
|
||||||
|
___
|
||||||
|
|
||||||
|
### `show_data`
|
||||||
|
|
||||||
|
Shows the hidden candles on the chart.
|
||||||
|
___
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### `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`
|
||||||
@ -218,7 +254,9 @@ ___
|
|||||||
|
|
||||||
## Line
|
## Line
|
||||||
|
|
||||||
The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to the [`title`](#title), [`marker`](#marker) and [`horizontal_line`](#horizontal-line) methods.
|
The `Line` object represents a `LineSeries` object in Lightweight Charts and can be used to create indicators. As well as the methods described below, the `Line` object also has access to:
|
||||||
|
|
||||||
|
[`title`](#title), [`marker`](#marker), [`horizontal_line`](#horizontal-line) [`hide_data`](#hide-data), [`show_data`](#show-data) and[`price_line`](#price-line) methods.
|
||||||
|
|
||||||
```{important}
|
```{important}
|
||||||
The `line` object should only be accessed from the [`create_line`](#create-line) method of `Chart`.
|
The `line` object should only be accessed from the [`create_line`](#create-line) method of `Chart`.
|
||||||
@ -241,6 +279,11 @@ Updates the data for the line.
|
|||||||
This should be given as a Series object, with labels akin to the `line.set()` function.
|
This should be given as a Series object, with labels akin to the `line.set()` function.
|
||||||
___
|
___
|
||||||
|
|
||||||
|
### `delete`
|
||||||
|
|
||||||
|
Irreversibly deletes the line on the chart as well as the Line object.
|
||||||
|
___
|
||||||
|
|
||||||
## SubChart
|
## SubChart
|
||||||
|
|
||||||
The `SubChart` object allows for the use of multiple chart panels within the same `Chart` window. All of the [Common Methods](#common-methods) can be used within a `SubChart`. Its instance should be accessed using the [create_subchart](#create-subchart) method.
|
The `SubChart` object allows for the use of multiple chart panels within the same `Chart` window. All of the [Common Methods](#common-methods) can be used within a `SubChart`. Its instance should be accessed using the [create_subchart](#create-subchart) method.
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
from .chart import Chart
|
|
||||||
from .js import LWC
|
from .js import LWC
|
||||||
|
from .chart import Chart
|
||||||
from .widgets import JupyterChart
|
from .widgets import JupyterChart
|
||||||
|
from .polygon import PolygonChart
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import webview
|
import time
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
|
import webview
|
||||||
|
|
||||||
from lightweight_charts.js import LWC, CALLBACK_SCRIPT, TopBar
|
from lightweight_charts.js import LWC, CALLBACK_SCRIPT, TopBar
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ class Chart(LWC):
|
|||||||
if not topbar and not searchbox:
|
if not topbar and not searchbox:
|
||||||
return
|
return
|
||||||
self.run_script(CALLBACK_SCRIPT)
|
self.run_script(CALLBACK_SCRIPT)
|
||||||
|
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
|
||||||
|
|
||||||
@ -79,10 +81,18 @@ class Chart(LWC):
|
|||||||
self._q.put('show')
|
self._q.put('show')
|
||||||
if block:
|
if block:
|
||||||
try:
|
try:
|
||||||
self._exit.wait()
|
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:
|
except KeyboardInterrupt:
|
||||||
return
|
return
|
||||||
self._exit.clear()
|
|
||||||
|
|
||||||
async def show_async(self, block=False):
|
async def show_async(self, block=False):
|
||||||
if not self.loaded:
|
if not self.loaded:
|
||||||
@ -94,17 +104,23 @@ class Chart(LWC):
|
|||||||
if block:
|
if block:
|
||||||
try:
|
try:
|
||||||
while 1:
|
while 1:
|
||||||
while self._emit.empty() and not self._exit.is_set():
|
while self._emit.empty() and not self._exit.is_set() and self.polygon._q.empty():
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.05)
|
||||||
if self._exit.is_set():
|
if self._exit.is_set():
|
||||||
|
self._exit.clear()
|
||||||
return
|
return
|
||||||
key, chart_id, arg = self._emit.get()
|
elif not self._emit.empty():
|
||||||
self.api.chart = self._charts[chart_id]
|
key, chart_id, arg = self._emit.get()
|
||||||
if widget := self.api.chart.topbar._widget_with_method(key):
|
self.api.chart = self._charts[chart_id]
|
||||||
widget.value = arg
|
if widget := self.api.chart.topbar._widget_with_method(key):
|
||||||
await getattr(self.api, key)()
|
widget.value = arg
|
||||||
else:
|
await getattr(self.api, key)()
|
||||||
await getattr(self.api, key)(arg)
|
else:
|
||||||
|
await getattr(self.api, key)(arg)
|
||||||
|
continue
|
||||||
|
value = self.polygon._q.get()
|
||||||
|
func, args = value[0], value[1:]
|
||||||
|
func(*args)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
return
|
return
|
||||||
asyncio.create_task(self.show_async(block=True))
|
asyncio.create_task(self.show_async(block=True))
|
||||||
@ -123,4 +139,3 @@ class Chart(LWC):
|
|||||||
self._exit.wait()
|
self._exit.wait()
|
||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
del self
|
del self
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,10 @@ from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, C
|
|||||||
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()
|
||||||
self._interval = common_interval.index[0]
|
try:
|
||||||
|
self._interval = common_interval.index[0]
|
||||||
|
except IndexError:
|
||||||
|
raise IndexError('Not enough bars within the given data to calculate the interval/timeframe.')
|
||||||
|
|
||||||
def _df_datetime_format(self, df: pd.DataFrame):
|
def _df_datetime_format(self, df: pd.DataFrame):
|
||||||
df = df.copy()
|
df = df.copy()
|
||||||
@ -81,9 +84,11 @@ class SeriesCommon:
|
|||||||
"""
|
"""
|
||||||
Creates a horizontal line at the given price.\n
|
Creates a horizontal line at the given price.\n
|
||||||
"""
|
"""
|
||||||
|
line_id = self._rand.generate()
|
||||||
self.run_script(f"""
|
self.run_script(f"""
|
||||||
makeHorizontalLine({self.id}, {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
|
makeHorizontalLine({self.id}, '{line_id}', {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
|
||||||
""")
|
""")
|
||||||
|
return line_id
|
||||||
|
|
||||||
def remove_horizontal_line(self, price: Union[float, int]):
|
def remove_horizontal_line(self, price: Union[float, int]):
|
||||||
"""
|
"""
|
||||||
@ -97,8 +102,24 @@ class SeriesCommon:
|
|||||||
}}
|
}}
|
||||||
}});''')
|
}});''')
|
||||||
|
|
||||||
def title(self, title: str):
|
def title(self, title: str): self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})')
|
||||||
self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})')
|
|
||||||
|
def price_line(self, label_visible: bool = True, line_visible: bool = True):
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.id}.series.applyOptions({{
|
||||||
|
lastValueVisible: {_js_bool(label_visible)},
|
||||||
|
priceLineVisible: {_js_bool(line_visible)},
|
||||||
|
}})''')
|
||||||
|
|
||||||
|
def hide_data(self): self._toggle_data(False)
|
||||||
|
|
||||||
|
def show_data(self): self._toggle_data(True)
|
||||||
|
|
||||||
|
def _toggle_data(self, arg):
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.id}.series.applyOptions({{visible: {_js_bool(arg)}}})
|
||||||
|
{f'{self.id}.volumeSeries.applyOptions({{visible: {_js_bool(arg)}}})' if hasattr(self, 'volume_enabled') and self.volume_enabled else ''}
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
class Line(SeriesCommon):
|
class Line(SeriesCommon):
|
||||||
@ -137,16 +158,27 @@ class Line(SeriesCommon):
|
|||||||
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()})')
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""
|
||||||
|
Irreversibly deletes the line, as well as the object that contains the line.
|
||||||
|
"""
|
||||||
|
self._parent._lines.remove(self)
|
||||||
|
self.run_script(f'''
|
||||||
|
{self._parent.id}.chart.removeSeries({self.id}.series)
|
||||||
|
delete {self.id}
|
||||||
|
''')
|
||||||
|
del self
|
||||||
|
|
||||||
|
|
||||||
class Widget:
|
class Widget:
|
||||||
def __init__(self, chart):
|
def __init__(self, topbar):
|
||||||
self._chart = chart
|
self._chart = topbar._chart
|
||||||
self.method = None
|
self.method = None
|
||||||
|
|
||||||
|
|
||||||
class TextWidget(Widget):
|
class TextWidget(Widget):
|
||||||
def __init__(self, chart, initial_text):
|
def __init__(self, topbar, initial_text):
|
||||||
super().__init__(chart)
|
super().__init__(topbar)
|
||||||
self.value = initial_text
|
self.value = initial_text
|
||||||
self.id = f"window.{self._chart._rand.generate()}"
|
self.id = f"window.{self._chart._rand.generate()}"
|
||||||
self._chart.run_script(f'''{self.id} = makeTextBoxWidget({self._chart.id}, "{initial_text}")''')
|
self._chart.run_script(f'''{self.id} = makeTextBoxWidget({self._chart.id}, "{initial_text}")''')
|
||||||
@ -157,12 +189,13 @@ class TextWidget(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class SwitcherWidget(Widget):
|
class SwitcherWidget(Widget):
|
||||||
def __init__(self, chart, method, *options, default):
|
def __init__(self, topbar, method, *options, default):
|
||||||
super().__init__(chart)
|
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}')
|
||||||
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width}, (window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
|
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width}, (window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
|
||||||
''')
|
''')
|
||||||
|
|
||||||
@ -173,15 +206,20 @@ class TopBar:
|
|||||||
self._widgets: Dict[str, Widget] = {}
|
self._widgets: Dict[str, Widget] = {}
|
||||||
self._chart.run_script(f'''
|
self._chart.run_script(f'''
|
||||||
makeTopBar({self._chart.id})
|
makeTopBar({self._chart.id})
|
||||||
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width}, (window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
|
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width},
|
||||||
|
(window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
|
||||||
''')
|
''')
|
||||||
|
self.active_background_color = 'rgba(0, 122, 255, 0.7)'
|
||||||
|
self.active_text_color = 'rgb(240, 240, 240)'
|
||||||
|
self.text_color = 'lightgrey'
|
||||||
|
self.hover_color = 'rgb(60, 60, 60)'
|
||||||
|
|
||||||
def __getitem__(self, item): return self._widgets.get(item)
|
def __getitem__(self, item): return self._widgets.get(item)
|
||||||
|
|
||||||
def switcher(self, name, method, *options, default=None):
|
def switcher(self, name, method, *options, default=None):
|
||||||
self._widgets[name] = SwitcherWidget(self._chart, method, *options, default=default if default else options[0])
|
self._widgets[name] = SwitcherWidget(self, method, *options, default=default if default else options[0])
|
||||||
|
|
||||||
def textbox(self, name, initial_text=''): self._widgets[name] = TextWidget(self._chart, initial_text)
|
def textbox(self, name, initial_text=''): self._widgets[name] = TextWidget(self, initial_text)
|
||||||
|
|
||||||
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():
|
||||||
@ -191,7 +229,7 @@ class TopBar:
|
|||||||
|
|
||||||
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):
|
||||||
self._volume_enabled = volume_enabled
|
self.volume_enabled = volume_enabled
|
||||||
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
|
||||||
@ -206,13 +244,15 @@ class LWC(SeriesCommon):
|
|||||||
self._last_bar = None
|
self._last_bar = None
|
||||||
self._interval = None
|
self._interval = None
|
||||||
self._charts = {self.id: self}
|
self._charts = {self.id: self}
|
||||||
|
self._lines = []
|
||||||
self._js_api_code = None
|
self._js_api_code = 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)'
|
||||||
self._volume_down_color = 'rgba(200,127,130,0.8)'
|
self._volume_down_color = 'rgba(200,127,130,0.8)'
|
||||||
|
|
||||||
# self.polygon: PolygonAPI = PolygonAPI(self)
|
from lightweight_charts.polygon import PolygonAPI
|
||||||
|
self.polygon: PolygonAPI = PolygonAPI(self)
|
||||||
|
|
||||||
def _on_js_load(self):
|
def _on_js_load(self):
|
||||||
if self.loaded:
|
if self.loaded:
|
||||||
@ -228,7 +268,7 @@ class LWC(SeriesCommon):
|
|||||||
''')
|
''')
|
||||||
|
|
||||||
def _make_search_box(self):
|
def _make_search_box(self):
|
||||||
self.run_script(f'makeSearchBox({self.id}, {self._js_api_code})')
|
self.run_script(f'{self.id}.search = makeSearchBox({self.id}, {self._js_api_code})')
|
||||||
|
|
||||||
def run_script(self, script):
|
def run_script(self, script):
|
||||||
"""
|
"""
|
||||||
@ -241,9 +281,13 @@ class LWC(SeriesCommon):
|
|||||||
Sets the initial data for the chart.\n
|
Sets the initial data for the chart.\n
|
||||||
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
|
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
|
||||||
"""
|
"""
|
||||||
|
if df.empty:
|
||||||
|
self.run_script(f'{self.id}.series.setData([])')
|
||||||
|
self.run_script(f'{self.id}.volumeSeries.setData([])')
|
||||||
|
return
|
||||||
bars = self._df_datetime_format(df)
|
bars = self._df_datetime_format(df)
|
||||||
self._last_bar = bars.iloc[-1]
|
self._last_bar = bars.iloc[-1]
|
||||||
if self._volume_enabled:
|
if self.volume_enabled:
|
||||||
if 'volume' not in bars:
|
if 'volume' not in bars:
|
||||||
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
||||||
|
|
||||||
@ -292,6 +336,12 @@ class LWC(SeriesCommon):
|
|||||||
}});
|
}});
|
||||||
''') 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}.series.setData({bars})')
|
||||||
|
|
||||||
|
def fit(self):
|
||||||
|
"""
|
||||||
|
Fits the maximum amount of the chart data within the viewport.
|
||||||
|
"""
|
||||||
|
self.run_script(f'{self.id}.chart.timeScale().fitContent()')
|
||||||
|
|
||||||
def update(self, series, from_tick=False):
|
def update(self, series, from_tick=False):
|
||||||
"""
|
"""
|
||||||
Updates the data from a bar;
|
Updates the data from a bar;
|
||||||
@ -300,7 +350,7 @@ class LWC(SeriesCommon):
|
|||||||
"""
|
"""
|
||||||
series = self._series_datetime_format(series) if not from_tick else series
|
series = self._series_datetime_format(series) if not from_tick else series
|
||||||
self._last_bar = series
|
self._last_bar = series
|
||||||
if self._volume_enabled:
|
if self.volume_enabled:
|
||||||
if 'volume' not in series:
|
if 'volume' not in series:
|
||||||
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
||||||
|
|
||||||
@ -332,10 +382,11 @@ class LWC(SeriesCommon):
|
|||||||
{self.id}.series.update({self.id}.shownData[{self.id}.shownData.length-1])
|
{self.id}.series.update({self.id}.shownData[{self.id}.shownData.length-1])
|
||||||
''') if self._dynamic_loading else self.run_script(f'{self.id}.series.update({bar})')
|
''') if self._dynamic_loading else self.run_script(f'{self.id}.series.update({bar})')
|
||||||
|
|
||||||
def update_from_tick(self, series):
|
def update_from_tick(self, series, cumulative_volume=False):
|
||||||
"""
|
"""
|
||||||
Updates the data from a tick.\n
|
Updates the data from a tick.\n
|
||||||
:param series: labels: date/time, price, volume (if volume enabled).
|
:param series: labels: date/time, price, volume (if volume enabled).
|
||||||
|
:param cumulative_volume: Adds the given volume onto the latest bar.
|
||||||
"""
|
"""
|
||||||
series = self._series_datetime_format(series)
|
series = self._series_datetime_format(series)
|
||||||
bar = pd.Series()
|
bar = pd.Series()
|
||||||
@ -344,10 +395,13 @@ class LWC(SeriesCommon):
|
|||||||
bar['high'] = max(self._last_bar['high'], series['price'])
|
bar['high'] = max(self._last_bar['high'], series['price'])
|
||||||
bar['low'] = min(self._last_bar['low'], series['price'])
|
bar['low'] = min(self._last_bar['low'], series['price'])
|
||||||
bar['close'] = series['price']
|
bar['close'] = series['price']
|
||||||
if self._volume_enabled:
|
if self.volume_enabled:
|
||||||
if 'volume' not in series:
|
if 'volume' not in series:
|
||||||
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
||||||
bar['volume'] = series['volume']
|
elif cumulative_volume:
|
||||||
|
bar['volume'] += series['volume']
|
||||||
|
else:
|
||||||
|
bar['volume'] = series['volume']
|
||||||
else:
|
else:
|
||||||
for key in ('open', 'high', 'low', 'close'):
|
for key in ('open', 'high', 'low', 'close'):
|
||||||
bar[key] = series['price']
|
bar[key] = series['price']
|
||||||
@ -355,11 +409,19 @@ class LWC(SeriesCommon):
|
|||||||
bar['volume'] = 0
|
bar['volume'] = 0
|
||||||
self.update(bar, from_tick=True)
|
self.update(bar, from_tick=True)
|
||||||
|
|
||||||
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
|
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2) -> Line:
|
||||||
"""
|
"""
|
||||||
Creates and returns a Line object.)\n
|
Creates and returns a Line object.)\n
|
||||||
"""
|
"""
|
||||||
return Line(self, color, width)
|
self._lines.append(Line(self, color, width))
|
||||||
|
return self._lines[-1]
|
||||||
|
|
||||||
|
def lines(self):
|
||||||
|
"""
|
||||||
|
Returns all lines for the chart.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self._lines
|
||||||
|
|
||||||
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, ticks_visible: bool = False):
|
border_color: str = None, text_color: str = None, entire_text_only: bool = False, ticks_visible: bool = False):
|
||||||
@ -471,9 +533,9 @@ class LWC(SeriesCommon):
|
|||||||
}}
|
}}
|
||||||
}})''')
|
}})''')
|
||||||
|
|
||||||
def crosshair(self, mode: CROSSHAIR_MODE = 'normal', vert_width: int = 1, vert_color: str = None,
|
def crosshair(self, mode: CROSSHAIR_MODE = 'normal', vert_visible: bool = True, vert_width: int = 1, vert_color: str = None,
|
||||||
vert_style: LINE_STYLE = 'dashed', vert_label_background_color: str = 'rgb(46, 46, 46)', horz_width: int = 1,
|
vert_style: LINE_STYLE = 'large_dashed', vert_label_background_color: str = 'rgb(46, 46, 46)', horz_visible: bool = True,
|
||||||
horz_color: str = None, horz_style: LINE_STYLE = 'dashed', horz_label_background_color: str = 'rgb(55, 55, 55)'):
|
horz_width: int = 1, horz_color: str = None, horz_style: LINE_STYLE = 'large_dashed', horz_label_background_color: str = 'rgb(55, 55, 55)'):
|
||||||
"""
|
"""
|
||||||
Crosshair formatting for its vertical and horizontal axes.
|
Crosshair formatting for its vertical and horizontal axes.
|
||||||
"""
|
"""
|
||||||
@ -482,12 +544,14 @@ class LWC(SeriesCommon):
|
|||||||
crosshair: {{
|
crosshair: {{
|
||||||
mode: {_crosshair_mode(mode)},
|
mode: {_crosshair_mode(mode)},
|
||||||
vertLine: {{
|
vertLine: {{
|
||||||
|
visible: {_js_bool(vert_visible)},
|
||||||
width: {vert_width},
|
width: {vert_width},
|
||||||
{f'color: "{vert_color}",' if vert_color else ''}
|
{f'color: "{vert_color}",' if vert_color else ''}
|
||||||
style: {_line_style(vert_style)},
|
style: {_line_style(vert_style)},
|
||||||
labelBackgroundColor: "{vert_label_background_color}"
|
labelBackgroundColor: "{vert_label_background_color}"
|
||||||
}},
|
}},
|
||||||
horzLine: {{
|
horzLine: {{
|
||||||
|
visible: {_js_bool(horz_visible)},
|
||||||
width: {horz_width},
|
width: {horz_width},
|
||||||
{f'color: "{horz_color}",' if horz_color else ''}
|
{f'color: "{horz_color}",' if horz_color else ''}
|
||||||
style: {_line_style(horz_style)},
|
style: {_line_style(horz_style)},
|
||||||
@ -524,13 +588,13 @@ class LWC(SeriesCommon):
|
|||||||
|
|
||||||
{self.id}.chart.subscribeCrosshairMove((param) => {{
|
{self.id}.chart.subscribeCrosshairMove((param) => {{
|
||||||
if (param.time){{
|
if (param.time){{
|
||||||
const data = param.seriesData.get({self.id}.series);
|
let data = param.seriesData.get({self.id}.series);
|
||||||
if (!data) {{return}}
|
if (!data) {{return}}
|
||||||
let percentMove = ((data.close-data.open)/data.open)*100
|
|
||||||
let ohlc = `O ${{legendItemFormat(data.open)}}
|
let ohlc = `O ${{legendItemFormat(data.open)}}
|
||||||
| H ${{legendItemFormat(data.high)}}
|
| H ${{legendItemFormat(data.high)}}
|
||||||
| L ${{legendItemFormat(data.low)}}
|
| L ${{legendItemFormat(data.low)}}
|
||||||
| C ${{legendItemFormat(data.close)}} `
|
| C ${{legendItemFormat(data.close)}} `
|
||||||
|
let percentMove = ((data.close-data.open)/data.open)*100
|
||||||
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
|
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
|
||||||
let finalString = ''
|
let finalString = ''
|
||||||
{'finalString += ohlc' if ohlc else ''}
|
{'finalString += ohlc' if ohlc else ''}
|
||||||
@ -542,6 +606,8 @@ class LWC(SeriesCommon):
|
|||||||
}}
|
}}
|
||||||
}});''')
|
}});''')
|
||||||
|
|
||||||
|
def spinner(self, visible): self.run_script(f"{self.id}.spinner.style.display = '{'block' if visible else 'none'}'")
|
||||||
|
|
||||||
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):
|
||||||
@ -561,17 +627,19 @@ class SubChart(LWC):
|
|||||||
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()}'
|
||||||
|
self.polygon = self._chart.polygon._subchart(self)
|
||||||
|
|
||||||
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
|
||||||
if not sync:
|
if not sync:
|
||||||
return
|
return
|
||||||
sync_parent_var = self._parent.id if isinstance(sync, bool) else sync
|
sync_parent_id = self._parent.id if isinstance(sync, bool) else sync
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
{sync_parent_var}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
|
{sync_parent_id}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
|
||||||
{self.id}.chart.timeScale().setVisibleLogicalRange(timeRange)
|
{self.id}.chart.timeScale().setVisibleLogicalRange(timeRange)
|
||||||
}});
|
}});
|
||||||
|
{self.id}.chart.timeScale().setVisibleLogicalRange({sync_parent_id}.chart.timeScale().getVisibleLogicalRange())
|
||||||
''')
|
''')
|
||||||
|
|
||||||
|
|
||||||
@ -664,7 +732,7 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
|
|||||||
});
|
});
|
||||||
return chart
|
return chart
|
||||||
}
|
}
|
||||||
function makeHorizontalLine(chart, price, color, width, style, axisLabelVisible, text) {
|
function makeHorizontalLine(chart, lineId, price, color, width, style, axisLabelVisible, text) {
|
||||||
let priceLine = {
|
let priceLine = {
|
||||||
price: price,
|
price: price,
|
||||||
color: color,
|
color: color,
|
||||||
@ -676,6 +744,7 @@ function makeHorizontalLine(chart, price, color, width, style, axisLabelVisible,
|
|||||||
let line = {
|
let line = {
|
||||||
line: chart.series.createPriceLine(priceLine),
|
line: chart.series.createPriceLine(priceLine),
|
||||||
price: price,
|
price: price,
|
||||||
|
id: lineId,
|
||||||
};
|
};
|
||||||
chart.horizontal_lines.push(line)
|
chart.horizontal_lines.push(line)
|
||||||
}
|
}
|
||||||
@ -725,7 +794,7 @@ function makeSearchBox(chart, callbackFunction) {
|
|||||||
searchWindow.style.height = '30px'
|
searchWindow.style.height = '30px'
|
||||||
searchWindow.style.padding = '10px'
|
searchWindow.style.padding = '10px'
|
||||||
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
||||||
searchWindow.style.border = '3px solid #3C434C'
|
searchWindow.style.border = '2px solid #3C434C'
|
||||||
searchWindow.style.zIndex = '1000'
|
searchWindow.style.zIndex = '1000'
|
||||||
searchWindow.style.display = 'none'
|
searchWindow.style.display = 'none'
|
||||||
searchWindow.style.borderRadius = '5px'
|
searchWindow.style.borderRadius = '5px'
|
||||||
@ -734,14 +803,14 @@ function makeSearchBox(chart, callbackFunction) {
|
|||||||
magnifyingGlass.style.display = 'inline-block';
|
magnifyingGlass.style.display = 'inline-block';
|
||||||
magnifyingGlass.style.width = '12px';
|
magnifyingGlass.style.width = '12px';
|
||||||
magnifyingGlass.style.height = '12px';
|
magnifyingGlass.style.height = '12px';
|
||||||
magnifyingGlass.style.border = '2px solid #FFF';
|
magnifyingGlass.style.border = '2px solid rgb(240, 240, 240)';
|
||||||
magnifyingGlass.style.borderRadius = '50%';
|
magnifyingGlass.style.borderRadius = '50%';
|
||||||
magnifyingGlass.style.position = 'relative';
|
magnifyingGlass.style.position = 'relative';
|
||||||
let handle = document.createElement('span');
|
let handle = document.createElement('span');
|
||||||
handle.style.display = 'block';
|
handle.style.display = 'block';
|
||||||
handle.style.width = '7px';
|
handle.style.width = '7px';
|
||||||
handle.style.height = '2px';
|
handle.style.height = '2px';
|
||||||
handle.style.backgroundColor = '#FFF';
|
handle.style.backgroundColor = 'rgb(240, 240, 240)';
|
||||||
handle.style.position = 'absolute';
|
handle.style.position = 'absolute';
|
||||||
handle.style.top = 'calc(50% + 7px)';
|
handle.style.top = 'calc(50% + 7px)';
|
||||||
handle.style.right = 'calc(50% - 11px)';
|
handle.style.right = 'calc(50% - 11px)';
|
||||||
@ -749,15 +818,14 @@ function makeSearchBox(chart, callbackFunction) {
|
|||||||
|
|
||||||
let sBox = document.createElement('input');
|
let sBox = document.createElement('input');
|
||||||
sBox.type = 'text';
|
sBox.type = 'text';
|
||||||
sBox.placeholder = 'search';
|
|
||||||
sBox.style.position = 'relative';
|
sBox.style.position = 'relative';
|
||||||
sBox.style.display = 'inline-block';
|
sBox.style.display = 'inline-block';
|
||||||
sBox.style.zIndex = '1000';
|
sBox.style.zIndex = '1000';
|
||||||
sBox.style.textAlign = 'center'
|
sBox.style.textAlign = 'center'
|
||||||
sBox.style.width = '100px'
|
sBox.style.width = '100px'
|
||||||
sBox.style.marginLeft = '15px'
|
sBox.style.marginLeft = '15px'
|
||||||
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.2)'
|
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.3)'
|
||||||
sBox.style.color = 'lightgrey'
|
sBox.style.color = 'rgb(240,240,240)'
|
||||||
sBox.style.fontSize = '20px'
|
sBox.style.fontSize = '20px'
|
||||||
sBox.style.border = 'none'
|
sBox.style.border = 'none'
|
||||||
sBox.style.outline = 'none'
|
sBox.style.outline = 'none'
|
||||||
@ -774,7 +842,7 @@ function makeSearchBox(chart, callbackFunction) {
|
|||||||
yPrice = param.point.y;
|
yPrice = param.point.y;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let selectedChart = false
|
let selectedChart = true
|
||||||
chart.wrapper.addEventListener('mouseover', (event) => {
|
chart.wrapper.addEventListener('mouseover', (event) => {
|
||||||
selectedChart = true
|
selectedChart = true
|
||||||
})
|
})
|
||||||
@ -785,7 +853,18 @@ function makeSearchBox(chart, callbackFunction) {
|
|||||||
if (!selectedChart) {return}
|
if (!selectedChart) {return}
|
||||||
if (event.altKey && event.code === 'KeyH') {
|
if (event.altKey && event.code === 'KeyH') {
|
||||||
let price = chart.series.coordinateToPrice(yPrice)
|
let price = chart.series.coordinateToPrice(yPrice)
|
||||||
makeHorizontalLine(chart, price, '#FFFFFF', 1, LightweightCharts.LineStyle.Solid, true, '')
|
|
||||||
|
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 (searchWindow.style.display === 'none') {
|
||||||
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
||||||
@ -806,29 +885,58 @@ function makeSearchBox(chart, callbackFunction) {
|
|||||||
sBox.addEventListener('input', function() {
|
sBox.addEventListener('input', function() {
|
||||||
sBox.value = sBox.value.toUpperCase();
|
sBox.value = sBox.value.toUpperCase();
|
||||||
});
|
});
|
||||||
|
return {
|
||||||
|
window: searchWindow,
|
||||||
|
box: sBox,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName) {
|
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');
|
let switcherElement = document.createElement('div');
|
||||||
switcherElement.style.margin = '4px 18px'
|
switcherElement.style.margin = '4px 14px'
|
||||||
switcherElement.style.zIndex = '1000'
|
switcherElement.style.zIndex = '1000'
|
||||||
|
|
||||||
let intervalElements = items.map(function(item) {
|
let intervalElements = items.map(function(item) {
|
||||||
let itemEl = document.createElement('button');
|
let itemEl = document.createElement('button');
|
||||||
itemEl.style.cursor = 'pointer'
|
itemEl.style.cursor = 'pointer'
|
||||||
itemEl.style.padding = '3px 6px'
|
itemEl.style.padding = '2px 5px'
|
||||||
itemEl.style.margin = '0px 4px'
|
itemEl.style.margin = '0px 4px'
|
||||||
itemEl.style.fontSize = '14px'
|
itemEl.style.fontSize = '13px'
|
||||||
itemEl.style.color = 'lightgrey'
|
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : 'transparent'
|
||||||
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
itemEl.style.color = item === activeItem ? activeColor : inactiveColor
|
||||||
|
|
||||||
itemEl.style.border = 'none'
|
itemEl.style.border = 'none'
|
||||||
itemEl.style.borderRadius = '4px'
|
itemEl.style.borderRadius = '4px'
|
||||||
|
|
||||||
itemEl.addEventListener('mouseenter', function() {
|
itemEl.addEventListener('mouseenter', function() {
|
||||||
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'rgb(19, 40, 84)'
|
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : hoverColor
|
||||||
|
itemEl.style.color = activeColor
|
||||||
})
|
})
|
||||||
itemEl.addEventListener('mouseleave', function() {
|
itemEl.addEventListener('mouseleave', function() {
|
||||||
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
itemEl.style.backgroundColor = item === activeItem ? activeBackgroundColor : 'transparent'
|
||||||
|
itemEl.style.color = item === activeItem ? activeColor : inactiveColor
|
||||||
})
|
})
|
||||||
itemEl.innerText = item;
|
itemEl.innerText = item;
|
||||||
itemEl.addEventListener('click', function() {
|
itemEl.addEventListener('click', function() {
|
||||||
@ -842,7 +950,8 @@ function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
intervalElements.forEach(function(element, index) {
|
intervalElements.forEach(function(element, index) {
|
||||||
element.style.backgroundColor = items[index] === item ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
element.style.backgroundColor = items[index] === item ? activeBackgroundColor : 'transparent'
|
||||||
|
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
|
||||||
});
|
});
|
||||||
activeItem = item;
|
activeItem = item;
|
||||||
callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
||||||
@ -856,7 +965,8 @@ function makeTextBoxWidget(chart, text) {
|
|||||||
let textBox = document.createElement('div')
|
let textBox = document.createElement('div')
|
||||||
textBox.style.margin = '0px 18px'
|
textBox.style.margin = '0px 18px'
|
||||||
textBox.style.position = 'relative'
|
textBox.style.position = 'relative'
|
||||||
textBox.style.color = 'lightgrey'
|
textBox.style.fontSize = '16px'
|
||||||
|
textBox.style.color = 'rgb(220, 220, 220)'
|
||||||
textBox.innerText = text
|
textBox.innerText = text
|
||||||
chart.topBar.append(textBox)
|
chart.topBar.append(textBox)
|
||||||
makeSeperator(chart.topBar)
|
makeSeperator(chart.topBar)
|
||||||
@ -865,7 +975,7 @@ function makeTextBoxWidget(chart, text) {
|
|||||||
function makeTopBar(chart) {
|
function makeTopBar(chart) {
|
||||||
chart.topBar = document.createElement('div')
|
chart.topBar = document.createElement('div')
|
||||||
chart.topBar.style.backgroundColor = '#191B1E'
|
chart.topBar.style.backgroundColor = '#191B1E'
|
||||||
chart.topBar.style.borderBottom = '3px solid #3C434C'
|
chart.topBar.style.borderBottom = '2px solid #3C434C'
|
||||||
chart.topBar.style.display = 'flex'
|
chart.topBar.style.display = 'flex'
|
||||||
chart.topBar.style.alignItems = 'center'
|
chart.topBar.style.alignItems = 'center'
|
||||||
chart.wrapper.prepend(chart.topBar)
|
chart.wrapper.prepend(chart.topBar)
|
||||||
@ -878,4 +988,3 @@ function makeSeperator(topBar) {
|
|||||||
topBar.appendChild(seperator)
|
topBar.appendChild(seperator)
|
||||||
}
|
}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|||||||
335
lightweight_charts/polygon.py
Normal file
335
lightweight_charts/polygon.py
Normal file
@ -0,0 +1,335 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import datetime as dt
|
||||||
|
import threading
|
||||||
|
import queue
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
from typing import Literal, Union, List
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from lightweight_charts.util import _convert_timeframe
|
||||||
|
from lightweight_charts import Chart
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
requests = None
|
||||||
|
try:
|
||||||
|
import websockets
|
||||||
|
except ImportError:
|
||||||
|
websockets = None
|
||||||
|
|
||||||
|
|
||||||
|
class PolygonAPI:
|
||||||
|
def __init__(self, chart):
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
ch.setFormatter(logging.Formatter('%(asctime)s | [polygon.io] %(levelname)s: %(message)s', datefmt='%H:%M:%S'))
|
||||||
|
ch.setLevel(logging.DEBUG)
|
||||||
|
self._log = logging.getLogger('polygon')
|
||||||
|
self._log.setLevel(logging.ERROR)
|
||||||
|
self._log.addHandler(ch)
|
||||||
|
|
||||||
|
self._chart = chart
|
||||||
|
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._q = queue.Queue()
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def _subchart(self, subchart):
|
||||||
|
return PolygonAPISubChart(self, subchart)
|
||||||
|
|
||||||
|
def log(self, info: bool):
|
||||||
|
self._log.setLevel(logging.INFO) if info else self._log.setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
def api_key(self, key: str): self._key = key
|
||||||
|
|
||||||
|
def stock(self, symbol: str, timeframe: str, start_date: str, end_date='now', limit: int = 5_000, live: bool = False):
|
||||||
|
"""
|
||||||
|
Requests and displays stock 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 True if self._set(self._chart, 'stocks', symbol, timeframe, start_date, end_date, limit, live) else False
|
||||||
|
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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.')
|
||||||
|
|
||||||
|
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}"
|
||||||
|
|
||||||
|
response = requests.get(query_url, headers={'User-Agent': 'lightweight_charts/1.0'})
|
||||||
|
if response.status_code != 200:
|
||||||
|
error = response.json()
|
||||||
|
self._log.error(f'({response.status_code}) Request failed: {error["error"]}')
|
||||||
|
return
|
||||||
|
data = response.json()
|
||||||
|
if 'results' not in data:
|
||||||
|
self._log.error(f'No results for "{ticker}" ({sec_type})')
|
||||||
|
return
|
||||||
|
|
||||||
|
for child in self._lasts:
|
||||||
|
for subbed_chart in child['charts']:
|
||||||
|
if subbed_chart == chart:
|
||||||
|
self._send_q.put(('_unsubscribe', chart, sec_type, ticker))
|
||||||
|
|
||||||
|
df = pd.DataFrame(data['results'])
|
||||||
|
columns = ['t', 'o', 'h', 'l', 'c']
|
||||||
|
rename = {'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 't': 'time'}
|
||||||
|
if sec_type != 'indices':
|
||||||
|
rename['v'] = 'volume'
|
||||||
|
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))
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def _thread_loop(self):
|
||||||
|
while 1:
|
||||||
|
while self._send_q.empty():
|
||||||
|
await asyncio.sleep(0.05)
|
||||||
|
value = self._send_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]
|
||||||
|
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] = {
|
||||||
|
'sec_type': sec_type,
|
||||||
|
'sub_type': sub_type[sec_type],
|
||||||
|
'price': chart._last_bar['close'],
|
||||||
|
'charts': [],
|
||||||
|
}
|
||||||
|
quotes, aggs = self._lasts[key]['sub_type']
|
||||||
|
await self._send(self._lasts[key]['sec_type'], 'subscribe', f'{quotes}.{ticker}')
|
||||||
|
await self._send(self._lasts[key]['sec_type'], 'subscribe', f'{aggs}.{ticker}') if aggs else None
|
||||||
|
|
||||||
|
if sec_type != 'indices':
|
||||||
|
self._lasts[key]['volume'] = chart._last_bar['volume']
|
||||||
|
if chart in self._lasts[key]['charts']:
|
||||||
|
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']:
|
||||||
|
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}')
|
||||||
|
|
||||||
|
async def _send(self, sec_type, action, params):
|
||||||
|
while 1:
|
||||||
|
with self._lock:
|
||||||
|
ws = self._ws[sec_type]
|
||||||
|
if ws:
|
||||||
|
break
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
await ws.send(json.dumps({'action': action, 'params': params}))
|
||||||
|
|
||||||
|
async def _handle_tick(self, sec_type, data):
|
||||||
|
data['ticker_key'] = {
|
||||||
|
'stocks': 'sym',
|
||||||
|
'options': 'sym',
|
||||||
|
'indices': 'T',
|
||||||
|
'forex': 'p',
|
||||||
|
'crypto': 'pair',
|
||||||
|
}[sec_type]
|
||||||
|
key = data[data['ticker_key']].replace('/', '-')
|
||||||
|
if ':' in key:
|
||||||
|
key = key[key.index(':')+1:]
|
||||||
|
data['t'] = pd.to_datetime(data.pop('s'), unit='ms') if 't' not in data else pd.to_datetime(data['t'], unit='ms')
|
||||||
|
|
||||||
|
if data['ev'] in ('Q', 'V', 'C', 'XQ'):
|
||||||
|
self._lasts[key]['time'] = data['t']
|
||||||
|
if sec_type == 'forex':
|
||||||
|
data['bp'] = data.pop('b')
|
||||||
|
data['ap'] = data.pop('a')
|
||||||
|
self._lasts[key]['price'] = (data['bp']+data['ap'])/2 if sec_type != 'indices' else data['val']
|
||||||
|
self._lasts[key]['volume'] = 0
|
||||||
|
elif data['ev'] in ('A', 'CA', 'XA'):
|
||||||
|
self._lasts[key]['volume'] = data['v']
|
||||||
|
if not self._lasts[key].get('time'):
|
||||||
|
return
|
||||||
|
for chart in self._lasts[key]['charts']:
|
||||||
|
self._q.put((chart.update_from_tick, pd.Series(self._lasts[key]), True))
|
||||||
|
|
||||||
|
async def _websocket_connect(self, api_key, sec_type):
|
||||||
|
if websockets is None:
|
||||||
|
raise ImportError('The "websockets" library was not found, and must be installed to pull live data.')
|
||||||
|
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
|
||||||
|
await self._send(sec_type, 'auth', api_key)
|
||||||
|
while 1:
|
||||||
|
response = await ws.recv()
|
||||||
|
data_list: List[dict] = json.loads(response)
|
||||||
|
for i, data in enumerate(data_list):
|
||||||
|
if data['ev'] == 'status':
|
||||||
|
self._log.info(f'{data["message"]}')
|
||||||
|
continue
|
||||||
|
elif data_list.index(data) < len(data_list)-max_ticks:
|
||||||
|
continue
|
||||||
|
await self._handle_tick(sec_type, data)
|
||||||
|
|
||||||
|
|
||||||
|
class PolygonAPISubChart(PolygonAPI):
|
||||||
|
def __init__(self, polygon, subchart):
|
||||||
|
super().__init__(subchart)
|
||||||
|
self._set = polygon._set
|
||||||
|
|
||||||
|
|
||||||
|
class PolygonChart(Chart):
|
||||||
|
def __init__(self, api_key: str, live: bool = False, num_bars: int = 200, 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,
|
||||||
|
api=self, topbar=True, searchbox=True)
|
||||||
|
self.chart = self
|
||||||
|
self.num_bars = num_bars
|
||||||
|
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.legend(True)
|
||||||
|
self.grid(False, False)
|
||||||
|
self.crosshair(vert_visible=False, horz_visible=False)
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.id}.search.box.style.backgroundColor = 'rgba(91, 98, 246, 0.5)'
|
||||||
|
{self.id}.spinner.style.borderTop = '4px solid rgba(91, 98, 246, 0.8)'
|
||||||
|
|
||||||
|
{self.id}.search.window.style.display = "block"
|
||||||
|
{self.id}.search.box.focus()
|
||||||
|
|
||||||
|
window.stat = document.createElement('div')
|
||||||
|
window.stat.style.position = 'absolute'
|
||||||
|
window.stat.style.backgroundColor = '#E35C58'
|
||||||
|
window.stat.style.borderRadius = '50%'
|
||||||
|
window.stat.style.height = '8px'
|
||||||
|
window.stat.style.width = '8px'
|
||||||
|
window.stat.style.top = '10px'
|
||||||
|
window.stat.style.right = '25px'
|
||||||
|
{self.id}.topBar.appendChild(window.stat)
|
||||||
|
''')
|
||||||
|
|
||||||
|
def show(self):
|
||||||
|
"""
|
||||||
|
Shows the PolygonChart window (this method will block).
|
||||||
|
"""
|
||||||
|
asyncio.run(self.show_async(block=True))
|
||||||
|
|
||||||
|
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)})
|
||||||
|
start_date = dt.datetime.now()
|
||||||
|
remaining_bars = self.num_bars
|
||||||
|
while remaining_bars > 0:
|
||||||
|
start_date -= delta
|
||||||
|
if start_date.weekday() > 4: # Monday to Friday (0 to 4)
|
||||||
|
continue
|
||||||
|
remaining_bars -= 1
|
||||||
|
epoch = dt.datetime.fromtimestamp(0)
|
||||||
|
start_date = epoch if start_date < epoch else start_date
|
||||||
|
success = getattr(self.polygon, self.topbar['security'].value.lower())(
|
||||||
|
symbol,
|
||||||
|
timeframe=self.topbar['timeframe'].value,
|
||||||
|
start_date=start_date.strftime('%Y-%m-%d'),
|
||||||
|
limit=self.limit,
|
||||||
|
live=self.live
|
||||||
|
)
|
||||||
|
self.spinner(False)
|
||||||
|
self.crosshair(vert_visible=True, horz_visible=True) if success else None
|
||||||
|
if not success:
|
||||||
|
self.run_script(f'window.stat.style.backgroundColor = "#E35C58"')
|
||||||
|
return False
|
||||||
|
self.run_script(f'window.stat.style.backgroundColor = "#4CDE67"') if self.live else None
|
||||||
|
return True
|
||||||
|
|
||||||
|
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_security_selection(self):
|
||||||
|
sec_type = self.topbar['security'].value
|
||||||
|
self.volume_enabled = False if sec_type == 'Index' else True
|
||||||
|
|
||||||
|
precision = 5 if sec_type == 'Forex' else 2
|
||||||
|
min_move = 1 / (10 ** precision) # 2 -> 0.1, 5 -> 0.00005 etc.
|
||||||
|
self.run_script(f'''
|
||||||
|
{self.chart.id}.series.applyOptions({{
|
||||||
|
priceFormat: {{precision: {precision}, minMove: {min_move}}}
|
||||||
|
}})''')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import re
|
||||||
from random import choices
|
from random import choices
|
||||||
from string import ascii_lowercase
|
from string import ascii_lowercase
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
@ -73,4 +74,20 @@ def _marker_position(p: MARKER_POSITION):
|
|||||||
'below': 'belowBar',
|
'below': 'belowBar',
|
||||||
'inside': 'inBar',
|
'inside': 'inBar',
|
||||||
None: None,
|
None: None,
|
||||||
}[p]
|
}[p]
|
||||||
|
|
||||||
|
|
||||||
|
def _convert_timeframe(timeframe):
|
||||||
|
spans = {
|
||||||
|
'min': 'minute',
|
||||||
|
'H': 'hour',
|
||||||
|
'D': 'day',
|
||||||
|
'W': 'week',
|
||||||
|
'M': 'month',
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
multiplier = re.findall(r'\d+', timeframe)[0]
|
||||||
|
except IndexError:
|
||||||
|
return 1, spans[timeframe]
|
||||||
|
timespan = spans[timeframe.replace(multiplier, '')]
|
||||||
|
return multiplier, timespan
|
||||||
@ -35,10 +35,14 @@ from lightweight_charts.js import LWC, TopBar, CALLBACK_SCRIPT
|
|||||||
def _widget_message(chart, string):
|
def _widget_message(chart, string):
|
||||||
messages = string.split('__')
|
messages = string.split('__')
|
||||||
name, chart_id = messages[:2]
|
name, chart_id = messages[:2]
|
||||||
args = messages[2:]
|
arg = messages[2]
|
||||||
chart.api.chart = chart._charts[chart_id]
|
chart.api.chart = chart._charts[chart_id]
|
||||||
method = getattr(chart.api, name)
|
method = getattr(chart.api, name)
|
||||||
asyncio.create_task(getattr(chart.api, name)(*args)) if iscoroutinefunction(method) else method(*args)
|
if widget := chart.api.chart.topbar._widget_with_method(name):
|
||||||
|
widget.value = arg
|
||||||
|
asyncio.create_task(getattr(chart.api, name)()) if iscoroutinefunction(method) else method()
|
||||||
|
else:
|
||||||
|
asyncio.create_task(getattr(chart.api, name)(arg)) if iscoroutinefunction(method) else method(arg)
|
||||||
|
|
||||||
|
|
||||||
class WxChart(LWC):
|
class WxChart(LWC):
|
||||||
|
|||||||
Reference in New Issue
Block a user