New Feature: ChartAsync
- Added the ChartAsync class, allowing for more sophisticated Charts and SubCharts. - Symbol searching, timeframe selectors, and more is now possible with this varation of Chart. `QtChart` and `WxChart` have access to all the methods that `ChartAsync` has, however they utilize their own respective event loops rather than asyncio. New Feature: `StreamlitChart` - Chart window that can display static data within a Streamlit application. Removed the `subscribe_click` method.
This commit is contained in:
81
README.md
81
README.md
@ -1,13 +1,13 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# lightweight_charts_python
|
# lightweight-charts-python
|
||||||
|
|
||||||
[](https://pypi.org/project/lightweight-charts/)
|
[](https://pypi.org/project/lightweight-charts/)
|
||||||
[](https://python.org "Go to Python homepage")
|
[](https://python.org "Go to Python homepage")
|
||||||
[](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE)
|
[](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE)
|
||||||
[](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
|
[](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
|
||||||
|
|
||||||
|

|
||||||

|

|
||||||
|
|
||||||
lightweight-charts-python aims to provide a simple and pythonic way to access and implement [TradingView's Lightweight Charts](https://www.tradingview.com/lightweight-charts/).
|
lightweight-charts-python aims to provide a simple and pythonic way to access and implement [TradingView's Lightweight Charts](https://www.tradingview.com/lightweight-charts/).
|
||||||
@ -15,7 +15,7 @@ lightweight-charts-python aims to provide a simple and pythonic way to access an
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
```
|
```
|
||||||
pip install lightweight_charts
|
pip install lightweight-charts
|
||||||
```
|
```
|
||||||
___
|
___
|
||||||
|
|
||||||
@ -23,8 +23,13 @@ ___
|
|||||||
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. Support for PyQt and wxPython.
|
4. Supports:
|
||||||
5. Multi-Pane Charts using the `SubChart` ([examples](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart)).
|
* PyQt
|
||||||
|
* wxPython
|
||||||
|
* Streamlit
|
||||||
|
* asyncio
|
||||||
|
5. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#chartasync) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more.
|
||||||
|
6. Multi-Pane Charts using the `SubChart` ([examples](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart)).
|
||||||
___
|
___
|
||||||
|
|
||||||
### 1. Display data from a csv:
|
### 1. Display data from a csv:
|
||||||
@ -183,30 +188,68 @@ if __name__ == '__main__':
|
|||||||

|

|
||||||
___
|
___
|
||||||
|
|
||||||
### 6. Callbacks:
|
### 6. ChartAsync:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
import asyncio
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from lightweight_charts import Chart
|
|
||||||
|
from lightweight_charts import ChartAsync
|
||||||
|
|
||||||
|
|
||||||
def on_click(bar: dict):
|
def get_bar_data(symbol, timeframe):
|
||||||
print(f"Time: {bar['time']} | Close: {bar['close']}")
|
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
|
||||||
|
|
||||||
|
|
||||||
|
class API:
|
||||||
|
def __init__(self):
|
||||||
|
self.chart = None # Changes after each callback.
|
||||||
|
self.symbol = 'TSLA'
|
||||||
|
self.timeframe = '5min'
|
||||||
|
|
||||||
|
async def on_search(self, searched_string): # Called when the user searches.
|
||||||
|
self.symbol = searched_string
|
||||||
|
new_data = await self.get_data()
|
||||||
|
if new_data.empty:
|
||||||
|
return
|
||||||
|
self.chart.set(new_data)
|
||||||
|
self.chart.corner_text(searched_string)
|
||||||
|
|
||||||
|
async def on_timeframe_selection(self, timeframe): # Called when the user changes the timeframe.
|
||||||
|
self.timeframe = timeframe
|
||||||
|
new_data = await self.get_data()
|
||||||
|
if new_data.empty:
|
||||||
|
return
|
||||||
|
self.chart.set(new_data)
|
||||||
|
|
||||||
|
async def get_data(self):
|
||||||
|
if self.symbol not in ('AAPL', 'GOOGL', 'TSLA'):
|
||||||
|
print(f'No data for "{self.symbol}"')
|
||||||
|
return pd.DataFrame()
|
||||||
|
data = get_bar_data(self.symbol, self.timeframe)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
api = API()
|
||||||
|
|
||||||
|
chart = ChartAsync(api=api, debug=True)
|
||||||
|
chart.legend(True)
|
||||||
|
|
||||||
|
chart.create_switcher(api.on_timeframe_selection, '1min', '5min', '30min', default='5min')
|
||||||
|
chart.corner_text(api.symbol)
|
||||||
|
|
||||||
|
df = get_bar_data(api.symbol, api.timeframe)
|
||||||
|
chart.set(df)
|
||||||
|
|
||||||
|
await chart.show(block=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
chart = Chart()
|
|
||||||
|
|
||||||
df = pd.read_csv('ohlcv.csv')
|
|
||||||
chart.set(df)
|
|
||||||
|
|
||||||
chart.subscribe_click(on_click)
|
|
||||||
|
|
||||||
chart.show(block=True)
|
|
||||||
|
|
||||||
```
|
```
|
||||||

|

|
||||||
___
|
___
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|||||||
@ -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.8'
|
release = '1.0.9'
|
||||||
|
|
||||||
extensions = ["myst_parser"]
|
extensions = ["myst_parser"]
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
___
|
___
|
||||||
|
|
||||||
## Common Methods
|
## Common Methods
|
||||||
These methods can be used within the `Chart`, `SubChart`, `QtChart`, and `WxChart` objects.
|
These methods can be used within the [`Chart`](#chart), [`SubChart`](#subchart), [`ChartAsync`](#chartasync), [`QtChart`](#qtchart), [`WxChart`](#wxchart) and [`StreamlitChart`](#streamlitchart) objects.
|
||||||
|
|
||||||
___
|
___
|
||||||
### `set`
|
### `set`
|
||||||
@ -22,6 +22,10 @@ The data must be given as a DataFrame, with the columns:
|
|||||||
`time | open | high | low | close | volume`
|
`time | open | high | low | close | volume`
|
||||||
|
|
||||||
The `time` column can also be named `date`, and the `volume` column can be omitted if volume is not enabled.
|
The `time` column can also be named `date`, and the `volume` column can be omitted if volume is not enabled.
|
||||||
|
|
||||||
|
```{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.
|
||||||
|
```
|
||||||
___
|
___
|
||||||
|
|
||||||
### `update`
|
### `update`
|
||||||
@ -47,7 +51,6 @@ 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.```````
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
### `create_line`
|
### `create_line`
|
||||||
@ -163,15 +166,6 @@ ___
|
|||||||
Configures the legend of the chart.
|
Configures the legend of the chart.
|
||||||
___
|
___
|
||||||
|
|
||||||
### `subscribe_click`
|
|
||||||
`function: object`
|
|
||||||
|
|
||||||
Subscribes the given function to a chart 'click' event.
|
|
||||||
|
|
||||||
The event emits a dictionary containing the bar at the time clicked, the id of the `Chart` or `SubChart`, and the hover price:
|
|
||||||
|
|
||||||
`time | open | high | low | close | id | hover`
|
|
||||||
___
|
|
||||||
|
|
||||||
### `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`
|
||||||
@ -191,7 +185,7 @@ Creates and returns a [SubChart](#subchart) object, placing it adjacent to the d
|
|||||||
___
|
___
|
||||||
|
|
||||||
|
|
||||||
## `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` | `debug: 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.
|
||||||
@ -214,7 +208,7 @@ Exits and destroys the chart and window.
|
|||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
## `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 the [`title`](#title), [`marker`](#marker) and [`horizontal_line`](#horizontal-line) methods.
|
||||||
|
|
||||||
@ -239,7 +233,7 @@ 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.
|
||||||
___
|
___
|
||||||
|
|
||||||
## `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.
|
||||||
|
|
||||||
@ -302,11 +296,112 @@ if __name__ == '__main__':
|
|||||||
```
|
```
|
||||||
___
|
___
|
||||||
|
|
||||||
|
## ChartAsync
|
||||||
|
`api: object` | `top_bar: bool` | `search_box: bool`
|
||||||
|
|
||||||
## `QtChart`
|
The `ChartAsync` object allows for asyncronous callbacks to be passed back to python, allowing for more sophisticated chart layouts including search boxes and timeframe selectors.
|
||||||
|
|
||||||
|
[`QtChart`](#qtchart) and [`WxChart`](#wxchart) also have access to the methods specific to `ChartAsync`, however they use their respective event loops to emit callbacks rather than asyncio.
|
||||||
|
|
||||||
|
* `api`: The class object that the callbacks will be emitted to (see [How to use Callbacks](#how-to-use-callbacks)).
|
||||||
|
* `top_bar`: Adds a Top Bar to the `Chart` or `SubChart` and allows use of the `create_switcher` method.
|
||||||
|
* `search_box`: Adds a search box onto the `Chart` or `SubChart` that is activated by typing.
|
||||||
|
|
||||||
|
___
|
||||||
|
### How to use Callbacks
|
||||||
|
|
||||||
|
Callbacks are emitted to the class given as the `api` parameter shown above.
|
||||||
|
|
||||||
|
Take a look at this minimal example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class API:
|
||||||
|
def __init__(self):
|
||||||
|
self.chart = None
|
||||||
|
|
||||||
|
async def on_search(self, string):
|
||||||
|
print(f'You searched for {string}, within the chart holding the id: "{self.chart.id}"')
|
||||||
|
```
|
||||||
|
Upon searching in a `Chart` or `SubChart` window, the expected output would be akin to:
|
||||||
|
```
|
||||||
|
You searched for AAPL, within the chart holding the id: "window.blyjagcr"
|
||||||
|
```
|
||||||
|
When using `SubChart`'s, the id will change depending upon which pane was used to search, due to the instance of `self.chart` dynamically updating to the latest pane which triggered the callback.
|
||||||
|
This allows access to the specific [Common Methods](#common-methods) for the pane in question.
|
||||||
|
|
||||||
|
Certain callback methods must be specifically named:
|
||||||
|
* Search callbacks will always be emitted to a method named `on_search`
|
||||||
|
___
|
||||||
|
|
||||||
|
### `create_switcher`
|
||||||
|
`method: function` | `*options: str` | `default: str`
|
||||||
|
|
||||||
|
* `method`: The function from the `api` class given to the constructor that will receive the callback.
|
||||||
|
* `options`: The strings to be displayed within the switcher. This may be a variety of timeframes, security types, or whatever needs to be updated directly from the chart.
|
||||||
|
* `default`: The initial switcher option set.
|
||||||
|
___
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import pandas as pd
|
||||||
|
from my_favorite_broker import get_bar_data
|
||||||
|
|
||||||
|
from lightweight_charts import ChartAsync
|
||||||
|
|
||||||
|
|
||||||
|
class API:
|
||||||
|
def __init__(self):
|
||||||
|
self.chart = None
|
||||||
|
self.symbol = 'TSLA'
|
||||||
|
self.timeframe = '5min'
|
||||||
|
|
||||||
|
async def on_search(self, searched_string): # Called when the user searches.
|
||||||
|
self.symbol = searched_string
|
||||||
|
new_data = await self.get_data()
|
||||||
|
if not new_data:
|
||||||
|
return
|
||||||
|
self.chart.set(new_data) # sets data for the Chart or SubChart in question.
|
||||||
|
self.chart.corner_text(searched_string)
|
||||||
|
|
||||||
|
async def on_timeframe(self, timeframe): # Called when the user changes the timeframe.
|
||||||
|
self.timeframe = timeframe
|
||||||
|
new_data = await self.get_data()
|
||||||
|
if not new_data:
|
||||||
|
return
|
||||||
|
self.chart.set(new_data)
|
||||||
|
|
||||||
|
async def get_data(self):
|
||||||
|
data = await get_bar_data(self.symbol, self.timeframe)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
api = API()
|
||||||
|
|
||||||
|
chart = ChartAsync(api=api, debug=True)
|
||||||
|
|
||||||
|
chart.corner_text('TSLA')
|
||||||
|
chart.create_switcher(api.on_timeframe, '1min', '5min', '30min', 'H', 'D', 'W', default='5min')
|
||||||
|
|
||||||
|
df = pd.read_csv('ohlcv.csv')
|
||||||
|
chart.set(df)
|
||||||
|
|
||||||
|
await chart.show(block=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
___
|
||||||
|
|
||||||
|
## QtChart
|
||||||
`widget: QWidget` | `volume_enabled: bool`
|
`widget: QWidget` | `volume_enabled: bool`
|
||||||
|
|
||||||
The `QtChart` object allows the use of charts within a `QMainWindow` object, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
|
The `QtChart` object allows the use of charts within a `QMainWindow` object, and has similar functionality to the `Chart` and `ChartAsync` objects for manipulating data, configuring and styling.
|
||||||
|
|
||||||
|
Callbacks can be recieved through the Qt event loop, using an [API](#how-to-use-callbacks) class that uses **syncronous** methods instead of **asyncronous** methods.
|
||||||
___
|
___
|
||||||
|
|
||||||
### `get_webview`
|
### `get_webview`
|
||||||
@ -347,11 +442,12 @@ app.exec_()
|
|||||||
```
|
```
|
||||||
___
|
___
|
||||||
|
|
||||||
## `WxChart`
|
## WxChart
|
||||||
`parent: wx.Panel` | `volume_enabled: bool`
|
`parent: wx.Panel` | `volume_enabled: bool`
|
||||||
|
|
||||||
The WxChart object allows the use of charts within a `wx.Frame` object, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
|
The WxChart object allows the use of charts within a `wx.Frame` object, and has similar functionality to the `Chart` and `ChartAsync` objects for manipulating data, configuring and styling.
|
||||||
|
|
||||||
|
Callbacks can be recieved through the Wx event loop, using an [API](#how-to-use-callbacks) class that uses **syncronous** methods instead of **asyncronous** methods.
|
||||||
___
|
___
|
||||||
|
|
||||||
### `get_webview`
|
### `get_webview`
|
||||||
@ -394,6 +490,30 @@ if __name__ == '__main__':
|
|||||||
app.MainLoop()
|
app.MainLoop()
|
||||||
|
|
||||||
```
|
```
|
||||||
|
___
|
||||||
|
|
||||||
|
## StreamlitChart
|
||||||
|
`parent: wx.Panel` | `volume_enabled: bool`
|
||||||
|
|
||||||
|
The `StreamlitChart` object allows the use of charts within a Streamlit app, and has similar functionality to the `Chart` object for manipulating data, configuring and styling.
|
||||||
|
|
||||||
|
This object only supports the displaying of **static** data, and should not be used with the `update_from_tick` or `update` methods. Every call to the chart object must occur **before** calling `load`.
|
||||||
|
___
|
||||||
|
|
||||||
|
### `load`
|
||||||
|
|
||||||
|
Loads the chart into the Streamlit app. This should be called after setting, styling, and configuring the chart, as no further calls to the `StreamlitChart` will be acknowledged.
|
||||||
|
___
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
```python
|
||||||
|
import pandas as pd
|
||||||
|
from lightweight_charts.widgets import StreamlitChart
|
||||||
|
|
||||||
|
chart = StreamlitChart(width=900, height=600)
|
||||||
|
|
||||||
|
df = pd.read_csv('ohlcv.csv')
|
||||||
|
chart.set(df)
|
||||||
|
|
||||||
|
chart.load()
|
||||||
|
```
|
||||||
|
|||||||
BIN
examples/6_async/async.gif
Normal file
BIN
examples/6_async/async.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
BIN
examples/6_async/async.png
Normal file
BIN
examples/6_async/async.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
56
examples/6_async/async.py
Normal file
56
examples/6_async/async.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import asyncio
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from lightweight_charts import ChartAsync
|
||||||
|
|
||||||
|
|
||||||
|
def get_bar_data(symbol, timeframe):
|
||||||
|
return pd.read_csv(f'bar_data/{symbol}_{timeframe}.csv')
|
||||||
|
|
||||||
|
|
||||||
|
class API:
|
||||||
|
def __init__(self):
|
||||||
|
self.chart = None # Changes after each callback.
|
||||||
|
self.symbol = 'TSLA'
|
||||||
|
self.timeframe = '5min'
|
||||||
|
|
||||||
|
async def on_search(self, searched_string): # Called when the user searches.
|
||||||
|
self.symbol = searched_string
|
||||||
|
new_data = await self.get_data()
|
||||||
|
if new_data.empty:
|
||||||
|
return
|
||||||
|
self.chart.set(new_data)
|
||||||
|
self.chart.corner_text(searched_string)
|
||||||
|
|
||||||
|
async def on_timeframe_selection(self, timeframe): # Called when the user changes the timeframe.
|
||||||
|
self.timeframe = timeframe
|
||||||
|
new_data = await self.get_data()
|
||||||
|
if new_data.empty:
|
||||||
|
return
|
||||||
|
self.chart.set(new_data)
|
||||||
|
|
||||||
|
async def get_data(self):
|
||||||
|
if self.symbol not in ('AAPL', 'GOOGL', 'TSLA'):
|
||||||
|
print(f'No data for "{self.symbol}"')
|
||||||
|
return pd.DataFrame()
|
||||||
|
data = get_bar_data(self.symbol, self.timeframe)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
api = API()
|
||||||
|
|
||||||
|
chart = ChartAsync(api=api, debug=True)
|
||||||
|
chart.legend(True)
|
||||||
|
|
||||||
|
chart.create_switcher(api.on_timeframe_selection, '1min', '5min', '30min', default='5min')
|
||||||
|
chart.corner_text(api.symbol)
|
||||||
|
|
||||||
|
df = get_bar_data(api.symbol, api.timeframe)
|
||||||
|
chart.set(df)
|
||||||
|
|
||||||
|
await chart.show(block=True)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
2084
examples/6_async/bar_data/AAPL_1min.csv
Normal file
2084
examples/6_async/bar_data/AAPL_1min.csv
Normal file
File diff suppressed because it is too large
Load Diff
2371
examples/6_async/bar_data/AAPL_30min.csv
Normal file
2371
examples/6_async/bar_data/AAPL_30min.csv
Normal file
File diff suppressed because it is too large
Load Diff
1780
examples/6_async/bar_data/AAPL_5min.csv
Normal file
1780
examples/6_async/bar_data/AAPL_5min.csv
Normal file
File diff suppressed because it is too large
Load Diff
2085
examples/6_async/bar_data/GOOGL_1min.csv
Normal file
2085
examples/6_async/bar_data/GOOGL_1min.csv
Normal file
File diff suppressed because it is too large
Load Diff
2371
examples/6_async/bar_data/GOOGL_30min.csv
Normal file
2371
examples/6_async/bar_data/GOOGL_30min.csv
Normal file
File diff suppressed because it is too large
Load Diff
1780
examples/6_async/bar_data/GOOGL_5min.csv
Normal file
1780
examples/6_async/bar_data/GOOGL_5min.csv
Normal file
File diff suppressed because it is too large
Load Diff
2087
examples/6_async/bar_data/TSLA_1min.csv
Normal file
2087
examples/6_async/bar_data/TSLA_1min.csv
Normal file
File diff suppressed because it is too large
Load Diff
2371
examples/6_async/bar_data/TSLA_30min.csv
Normal file
2371
examples/6_async/bar_data/TSLA_30min.csv
Normal file
File diff suppressed because it is too large
Load Diff
1802
examples/6_async/bar_data/TSLA_5min.csv
Normal file
1802
examples/6_async/bar_data/TSLA_5min.csv
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 633 KiB |
@ -1,18 +0,0 @@
|
|||||||
import pandas as pd
|
|
||||||
from lightweight_charts import Chart
|
|
||||||
|
|
||||||
|
|
||||||
def on_click(bar: dict):
|
|
||||||
print(f"Time: {bar['time']} | Close: {bar['close']}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
|
||||||
chart = Chart()
|
|
||||||
|
|
||||||
df = pd.read_csv('ohlcv.csv')
|
|
||||||
chart.set(df)
|
|
||||||
|
|
||||||
chart.subscribe_click(on_click)
|
|
||||||
|
|
||||||
chart.show(block=True)
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,5 @@
|
|||||||
from .chart import Chart
|
from .chart import Chart
|
||||||
from .js import LWC
|
from .js import LWC
|
||||||
|
from .chartasync import ChartAsync
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -4,15 +4,25 @@ import multiprocessing as mp
|
|||||||
from lightweight_charts.js import LWC
|
from lightweight_charts.js import LWC
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackAPI:
|
||||||
|
def __init__(self, emit): self.emit = emit
|
||||||
|
|
||||||
|
def callback(self, message: str):
|
||||||
|
messages = message.split('__')
|
||||||
|
name, chart_id = messages[:2]
|
||||||
|
args = messages[2:]
|
||||||
|
self.emit.put((name, chart_id, *args))
|
||||||
|
|
||||||
|
|
||||||
class PyWV:
|
class PyWV:
|
||||||
def __init__(self, q, exit, loaded, html, js_api, width, height, x, y, on_top, debug):
|
def __init__(self, q, exit, loaded, html, width, height, x, y, on_top, debug, emit=None):
|
||||||
self.queue = q
|
self.queue = q
|
||||||
self.exit = exit
|
self.exit = exit
|
||||||
self.loaded = loaded
|
self.loaded = loaded
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.js_api = js_api
|
js_api = CallbackAPI(emit) if emit else None
|
||||||
self.webview = webview.create_window('', html=html, on_top=on_top, js_api=js_api,
|
self.webview = webview.create_window('', html=html, on_top=on_top, js_api=js_api, width=width, height=height,
|
||||||
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
|
||||||
self.loop()
|
self.loop()
|
||||||
|
|
||||||
@ -22,9 +32,6 @@ class PyWV:
|
|||||||
if arg in ('start', 'show', 'hide', 'exit'):
|
if arg in ('start', 'show', 'hide', 'exit'):
|
||||||
webview.start(debug=self.debug) if arg == 'start' else getattr(self.webview, arg)()
|
webview.start(debug=self.debug) if arg == 'start' else getattr(self.webview, arg)()
|
||||||
self.exit.set() if arg in ('start', 'exit') else None
|
self.exit.set() if arg in ('start', 'exit') else None
|
||||||
elif arg == 'subscribe':
|
|
||||||
func, c_id = (self.queue.get() for _ in range(2))
|
|
||||||
self.js_api.click_funcs[str(c_id)] = func
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.webview.evaluate_js(arg)
|
self.webview.evaluate_js(arg)
|
||||||
@ -40,12 +47,11 @@ class Chart(LWC):
|
|||||||
on_top: bool = False, debug: bool = False,
|
on_top: bool = False, debug: bool = False,
|
||||||
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
||||||
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
||||||
self._js_api_code = 'pywebview.api.onClick'
|
|
||||||
self._q = mp.Queue()
|
self._q = mp.Queue()
|
||||||
self._script_func = self._q.put
|
self._script_func = self._q.put
|
||||||
self._exit = mp.Event()
|
self._exit = mp.Event()
|
||||||
self._loaded = mp.Event()
|
self._loaded = mp.Event()
|
||||||
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html, self._js_api,
|
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
|
||||||
width, height, x, y, on_top, debug,), daemon=True)
|
width, height, x, y, on_top, debug,), daemon=True)
|
||||||
self._process.start()
|
self._process.start()
|
||||||
self._create_chart()
|
self._create_chart()
|
||||||
@ -83,9 +89,3 @@ class Chart(LWC):
|
|||||||
self._process.terminate()
|
self._process.terminate()
|
||||||
del self
|
del self
|
||||||
|
|
||||||
def subscribe_click(self, function: object):
|
|
||||||
self._q.put('subscribe')
|
|
||||||
self._q.put(function)
|
|
||||||
self._q.put(self.id)
|
|
||||||
super().subscribe_click(function)
|
|
||||||
|
|
||||||
|
|||||||
246
lightweight_charts/chartasync.py
Normal file
246
lightweight_charts/chartasync.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import asyncio
|
||||||
|
import multiprocessing as mp
|
||||||
|
from typing import Literal, Union
|
||||||
|
|
||||||
|
from lightweight_charts import LWC
|
||||||
|
from lightweight_charts.chart import PyWV
|
||||||
|
|
||||||
|
|
||||||
|
class LWCAsync(LWC):
|
||||||
|
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
||||||
|
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
||||||
|
self._charts = {self.id: self}
|
||||||
|
|
||||||
|
def _make_search_box(self):
|
||||||
|
self.run_script(f'makeSearchBox({self.id}, {self._js_api_code})')
|
||||||
|
|
||||||
|
def corner_text(self, text: str):
|
||||||
|
self.run_script(f'{self.id}.cornerText.innerText = "{text}"')
|
||||||
|
|
||||||
|
def create_switcher(self, method, *options, default=None):
|
||||||
|
self.run_script(f'''
|
||||||
|
makeSwitcher({self.id}, {list(options)}, '{default if default else options[0]}', {self._js_api_code}, '{method.__name__}')
|
||||||
|
{self.id}.chart.resize(window.innerWidth*{self._inner_width}, (window.innerHeight*{self._inner_height})-{self.id}.topBar.offsetHeight)
|
||||||
|
''')
|
||||||
|
|
||||||
|
def create_subchart(self, top_bar: bool = True, volume_enabled: bool = True, position: Literal['left', 'right', 'top', 'bottom'] = 'left',
|
||||||
|
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False):
|
||||||
|
subchart = SubChartAsync(self, top_bar, volume_enabled, position, width, height, sync)
|
||||||
|
self._charts[subchart.id] = subchart
|
||||||
|
return subchart
|
||||||
|
|
||||||
|
|
||||||
|
class ChartAsync(LWCAsync):
|
||||||
|
def __init__(self, api: object, top_bar: bool = True, search_box: bool = True, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||||
|
on_top: bool = False, debug: bool = False,
|
||||||
|
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False):
|
||||||
|
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading)
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
self._js_api_code = 'pywebview.api.callback'
|
||||||
|
self._emit = mp.Queue()
|
||||||
|
|
||||||
|
self._q = mp.Queue()
|
||||||
|
self._script_func = self._q.put
|
||||||
|
self._exit = mp.Event()
|
||||||
|
self._loaded = mp.Event()
|
||||||
|
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)
|
||||||
|
self._process.start()
|
||||||
|
|
||||||
|
self.run_script(ASYNC_SCRIPT)
|
||||||
|
self._create_chart(top_bar)
|
||||||
|
self._make_search_box() if search_box else None
|
||||||
|
|
||||||
|
async def show(self, block=False):
|
||||||
|
if not self.loaded:
|
||||||
|
self._q.put('start')
|
||||||
|
self._loaded.wait()
|
||||||
|
self._on_js_load()
|
||||||
|
else:
|
||||||
|
self._q.put('show')
|
||||||
|
if block:
|
||||||
|
try:
|
||||||
|
while 1:
|
||||||
|
while self._emit.empty() and not self._exit.is_set():
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
if self._exit.is_set():
|
||||||
|
return
|
||||||
|
key, chart_id, args = self._emit.get()
|
||||||
|
self.api.chart = self._charts[chart_id]
|
||||||
|
await getattr(self.api, key)(args)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
return
|
||||||
|
asyncio.create_task(self.show(block=True))
|
||||||
|
|
||||||
|
|
||||||
|
class SubChartAsync(LWCAsync):
|
||||||
|
def __init__(self, parent, top_bar, volume_enabled, position, width, height, sync):
|
||||||
|
super().__init__(volume_enabled, width, height)
|
||||||
|
self._chart = parent._chart if isinstance(parent, SubChartAsync) else parent
|
||||||
|
self._parent = parent
|
||||||
|
self._position = position
|
||||||
|
self._rand = self._chart._rand
|
||||||
|
self.id = f'window.{self._rand.generate()}'
|
||||||
|
self._js_api_code = self._chart._js_api_code
|
||||||
|
self.run_script = self._chart.run_script
|
||||||
|
self._charts = self._chart._charts
|
||||||
|
self._create_chart(top_bar)
|
||||||
|
self._make_search_box()
|
||||||
|
if not sync:
|
||||||
|
return
|
||||||
|
sync_parent_var = self._parent.id if isinstance(sync, bool) else sync
|
||||||
|
self.run_script(f'''
|
||||||
|
{sync_parent_var}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
|
||||||
|
{self.id}.chart.timeScale().setVisibleLogicalRange(timeRange)
|
||||||
|
}});
|
||||||
|
''')
|
||||||
|
|
||||||
|
|
||||||
|
ASYNC_SCRIPT = '''
|
||||||
|
function makeSearchBox(chart, callbackFunction) {
|
||||||
|
let searchWindow = document.createElement('div')
|
||||||
|
searchWindow.style.position = 'absolute'
|
||||||
|
searchWindow.style.top = '30%'
|
||||||
|
searchWindow.style.left = '50%'
|
||||||
|
searchWindow.style.transform = 'translate(-50%, -30%)'
|
||||||
|
searchWindow.style.width = '200px'
|
||||||
|
searchWindow.style.height = '200px'
|
||||||
|
searchWindow.style.backgroundColor = 'rgba(30, 30, 30, 0.9)'
|
||||||
|
searchWindow.style.zIndex = '1000'
|
||||||
|
searchWindow.style.display = 'none'
|
||||||
|
searchWindow.style.borderRadius = '10px'
|
||||||
|
|
||||||
|
let sBox = document.createElement('input');
|
||||||
|
sBox.type = 'text';
|
||||||
|
sBox.placeholder = 'search';
|
||||||
|
sBox.style.position = 'absolute';
|
||||||
|
sBox.style.zIndex = '1000';
|
||||||
|
|
||||||
|
sBox.style.textAlign = 'center'
|
||||||
|
sBox.style.left = '50%';
|
||||||
|
sBox.style.top = '30%';
|
||||||
|
sBox.style.transform = 'translate(-50%, -30%)'
|
||||||
|
sBox.style.width = '100px'
|
||||||
|
|
||||||
|
sBox.style.backgroundColor = 'rgba(0, 122, 255, 0.2)'
|
||||||
|
sBox.style.color = 'white'
|
||||||
|
sBox.style.fontSize = '20px'
|
||||||
|
sBox.style.border = 'none'
|
||||||
|
sBox.style.borderRadius = '5px'
|
||||||
|
|
||||||
|
searchWindow.appendChild(sBox)
|
||||||
|
chart.div.appendChild(searchWindow);
|
||||||
|
|
||||||
|
let yPrice = null
|
||||||
|
chart.chart.subscribeCrosshairMove((param) => {
|
||||||
|
if (param.point){
|
||||||
|
yPrice = param.point.y;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectedChart = false
|
||||||
|
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)
|
||||||
|
makeHorizontalLine(chart, price, '#FFFFFF', 1, 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName) {
|
||||||
|
let switcherElement = document.createElement('div');
|
||||||
|
switcherElement.style.margin = '4px 18px'
|
||||||
|
switcherElement.style.zIndex = '1000'
|
||||||
|
|
||||||
|
let intervalElements = items.map(function(item) {
|
||||||
|
let itemEl = document.createElement('button');
|
||||||
|
itemEl.style.cursor = 'pointer'
|
||||||
|
itemEl.style.padding = '3px 6px'
|
||||||
|
itemEl.style.margin = '0px 4px'
|
||||||
|
itemEl.style.fontSize = '14px'
|
||||||
|
itemEl.style.color = 'lightgrey'
|
||||||
|
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
||||||
|
|
||||||
|
|
||||||
|
itemEl.style.border = 'none'
|
||||||
|
itemEl.style.borderRadius = '4px'
|
||||||
|
itemEl.addEventListener('mouseenter', function() {
|
||||||
|
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'rgb(19, 40, 84)'
|
||||||
|
})
|
||||||
|
itemEl.addEventListener('mouseleave', function() {
|
||||||
|
itemEl.style.backgroundColor = item === activeItem ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
||||||
|
})
|
||||||
|
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 ? 'rgba(0, 122, 255, 0.7)' : 'transparent'
|
||||||
|
});
|
||||||
|
activeItem = item;
|
||||||
|
callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
||||||
|
}
|
||||||
|
chart.topBar.appendChild(switcherElement)
|
||||||
|
makeSeperator(chart.topBar)
|
||||||
|
return switcherElement;
|
||||||
|
}
|
||||||
|
function makeTopBar(chart) {
|
||||||
|
chart.topBar = document.createElement('div')
|
||||||
|
chart.topBar.style.backgroundColor = '#191B1E'
|
||||||
|
chart.topBar.style.borderBottom = '3px solid #3C434C'
|
||||||
|
chart.topBar.style.borderRadius = '2px'
|
||||||
|
chart.topBar.style.display = 'flex'
|
||||||
|
chart.topBar.style.alignItems = 'center'
|
||||||
|
chart.wrapper.prepend(chart.topBar)
|
||||||
|
|
||||||
|
chart.cornerText = document.createElement('div')
|
||||||
|
chart.cornerText.style.margin = '0px 18px'
|
||||||
|
chart.cornerText.style.position = 'relative'
|
||||||
|
chart.cornerText.style.fontFamily = 'SF Pro'
|
||||||
|
chart.cornerText.style.color = 'lightgrey'
|
||||||
|
chart.topBar.appendChild(chart.cornerText)
|
||||||
|
|
||||||
|
makeSeperator(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)
|
||||||
|
}
|
||||||
|
'''
|
||||||
@ -26,7 +26,7 @@ class SeriesCommon:
|
|||||||
series['time'] = self._datetime_format(series['time'])
|
series['time'] = self._datetime_format(series['time'])
|
||||||
return series
|
return series
|
||||||
|
|
||||||
def _datetime_format(self, arg):
|
def _datetime_format(self, arg: Union[pd.Series, str]):
|
||||||
arg = pd.to_datetime(arg)
|
arg = pd.to_datetime(arg)
|
||||||
if self._interval != timedelta(days=1):
|
if self._interval != timedelta(days=1):
|
||||||
arg = arg.astype('int64') // 10 ** 9 if isinstance(arg, pd.Series) else arg.timestamp()
|
arg = arg.astype('int64') // 10 ** 9 if isinstance(arg, pd.Series) else arg.timestamp()
|
||||||
@ -80,21 +80,9 @@ class SeriesCommon:
|
|||||||
"""
|
"""
|
||||||
Creates a horizontal line at the given price.\n
|
Creates a horizontal line at the given price.\n
|
||||||
"""
|
"""
|
||||||
var = self._rand.generate()
|
|
||||||
self.run_script(f"""
|
self.run_script(f"""
|
||||||
let priceLine{var} = {{
|
makeHorizontalLine({self.id}, {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
|
||||||
price: {price},
|
""")
|
||||||
color: '{color}',
|
|
||||||
lineWidth: {width},
|
|
||||||
lineStyle: {_line_style(style)},
|
|
||||||
axisLabelVisible: {_js_bool(axis_label_visible)},
|
|
||||||
title: '{text}',
|
|
||||||
}};
|
|
||||||
let line{var} = {{
|
|
||||||
line: {self.id}.series.createPriceLine(priceLine{var}),
|
|
||||||
price: {price},
|
|
||||||
}};
|
|
||||||
{self.id}.horizontal_lines.push(line{var})""")
|
|
||||||
|
|
||||||
def remove_horizontal_line(self, price: Union[float, int]):
|
def remove_horizontal_line(self, price: Union[float, int]):
|
||||||
"""
|
"""
|
||||||
@ -116,11 +104,11 @@ class Line(SeriesCommon):
|
|||||||
def __init__(self, parent, color, width):
|
def __init__(self, parent, color, width):
|
||||||
self._parent = parent
|
self._parent = parent
|
||||||
self._rand = self._parent._rand
|
self._rand = self._parent._rand
|
||||||
self.id = self._rand.generate()
|
self.id = f'window.{self._rand.generate()}'
|
||||||
self.run_script = self._parent.run_script
|
self.run_script = self._parent.run_script
|
||||||
|
|
||||||
self._parent.run_script(f'''
|
self._parent.run_script(f'''
|
||||||
var {self.id} = {{
|
{self.id} = {{
|
||||||
series: {self._parent.id}.chart.addLineSeries({{
|
series: {self._parent.id}.chart.addLineSeries({{
|
||||||
color: '{color}',
|
color: '{color}',
|
||||||
lineWidth: {width},
|
lineWidth: {width},
|
||||||
@ -149,19 +137,6 @@ class Line(SeriesCommon):
|
|||||||
self.run_script(f'{self.id}.series.update({series.to_dict()})')
|
self.run_script(f'{self.id}.series.update({series.to_dict()})')
|
||||||
|
|
||||||
|
|
||||||
class API:
|
|
||||||
def __init__(self):
|
|
||||||
self.click_funcs = {}
|
|
||||||
|
|
||||||
def onClick(self, data):
|
|
||||||
click_func = self.click_funcs[data['id']]
|
|
||||||
if isinstance(data['time'], int):
|
|
||||||
data['time'] = datetime.fromtimestamp(data['time'])
|
|
||||||
else:
|
|
||||||
data['time'] = datetime.strptime(data['time'], '%Y-%m-%d')
|
|
||||||
click_func(data) if click_func else None
|
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@ -170,35 +145,30 @@ class LWC(SeriesCommon):
|
|||||||
self._dynamic_loading = dynamic_loading
|
self._dynamic_loading = dynamic_loading
|
||||||
|
|
||||||
self._rand = IDGen()
|
self._rand = IDGen()
|
||||||
self.id = self._rand.generate()
|
self.id = f'window.{self._rand.generate()}'
|
||||||
self._position = 'left'
|
self._position = 'left'
|
||||||
self.loaded = False
|
self.loaded = False
|
||||||
self._html = HTML
|
self._html = HTML
|
||||||
self._append_js = f'document.body.append({self.id}.div)'
|
|
||||||
self._scripts = []
|
self._scripts = []
|
||||||
self._script_func = None
|
self._script_func = None
|
||||||
self._last_bar = None
|
self._last_bar = None
|
||||||
self._interval = None
|
self._interval = None
|
||||||
self._js_api = API()
|
|
||||||
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)'
|
||||||
|
|
||||||
def _on_js_load(self):
|
def _on_js_load(self):
|
||||||
|
if self.loaded:
|
||||||
|
return
|
||||||
self.loaded = True
|
self.loaded = True
|
||||||
for script in self._scripts:
|
[self.run_script(script) for script in self._scripts]
|
||||||
self.run_script(script)
|
|
||||||
|
|
||||||
def _create_chart(self):
|
def _create_chart(self, top_bar=False):
|
||||||
self.run_script(f'''
|
self.run_script(f'''
|
||||||
{self.id} = makeChart({self._inner_width}, {self._inner_height})
|
{self.id} = makeChart({self._inner_width}, {self._inner_height}, topBar={_js_bool(top_bar)})
|
||||||
{self.id}.div.style.float = "{self._position}"
|
{self.id}.id = '{self.id}'
|
||||||
{self._append_js}
|
{self.id}.wrapper.style.float = "{self._position}"
|
||||||
window.addEventListener('resize', function() {{
|
|
||||||
{self.id}.chart.resize(window.innerWidth*{self.id}.scale.width, window.innerHeight*{self.id}.scale.height)
|
|
||||||
}});
|
|
||||||
''')
|
''')
|
||||||
|
|
||||||
def run_script(self, script):
|
def run_script(self, script):
|
||||||
@ -498,11 +468,11 @@ class LWC(SeriesCommon):
|
|||||||
const data = param.seriesData.get({self.id}.series);
|
const data = param.seriesData.get({self.id}.series);
|
||||||
if (!data) {{return}}
|
if (!data) {{return}}
|
||||||
let percentMove = ((data.close-data.open)/data.open)*100
|
let percentMove = ((data.close-data.open)/data.open)*100
|
||||||
let ohlc = `open: ${{legendItemFormat(data.open)}}
|
let ohlc = `O ${{legendItemFormat(data.open)}}
|
||||||
| high: ${{legendItemFormat(data.high)}}
|
| H ${{legendItemFormat(data.high)}}
|
||||||
| low: ${{legendItemFormat(data.low)}}
|
| L ${{legendItemFormat(data.low)}}
|
||||||
| close: ${{legendItemFormat(data.close)}} `
|
| C ${{legendItemFormat(data.close)}} `
|
||||||
let percent = `| daily: ${{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 ''}
|
||||||
{'finalString += percent' if percent else ''}
|
{'finalString += percent' if percent else ''}
|
||||||
@ -513,28 +483,6 @@ class LWC(SeriesCommon):
|
|||||||
}}
|
}}
|
||||||
}});''')
|
}});''')
|
||||||
|
|
||||||
def subscribe_click(self, function: object):
|
|
||||||
"""
|
|
||||||
Subscribes the given function to a chart click event.
|
|
||||||
The event returns a dictionary containing the bar object at the time clicked, and the price at the crosshair.
|
|
||||||
"""
|
|
||||||
self._js_api.click_funcs[self.id] = function
|
|
||||||
self.run_script(f'''
|
|
||||||
{self.id}.chart.subscribeClick((param) => {{
|
|
||||||
if (!param.point) {{return}}
|
|
||||||
let prices = param.seriesData.get({self.id}.series);
|
|
||||||
let data = {{
|
|
||||||
time: param.time,
|
|
||||||
open: prices.open,
|
|
||||||
high: prices.high,
|
|
||||||
low: prices.low,
|
|
||||||
close: prices.close,
|
|
||||||
hover: {self.id}.series.coordinateToPrice(param.point.y),
|
|
||||||
id: '{self.id}'
|
|
||||||
}}
|
|
||||||
{self._js_api_code}(data)
|
|
||||||
}})''')
|
|
||||||
|
|
||||||
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):
|
||||||
return SubChart(self, volume_enabled, position, width, height, sync)
|
return SubChart(self, volume_enabled, position, width, height, sync)
|
||||||
@ -548,9 +496,6 @@ class SubChart(LWC):
|
|||||||
self._position = position
|
self._position = position
|
||||||
self._rand = self._chart._rand
|
self._rand = self._chart._rand
|
||||||
self.id = f'window.{self._rand.generate()}'
|
self.id = f'window.{self._rand.generate()}'
|
||||||
self._append_js = f'{self._parent.id}.div.parentNode.insertBefore({self.id}.div, {self._parent.id}.div.nextSibling)'
|
|
||||||
self._js_api = self._chart._js_api
|
|
||||||
self._js_api_code = self._chart._js_api_code
|
|
||||||
self.run_script = self._chart.run_script
|
self.run_script = self._chart.run_script
|
||||||
self._create_chart()
|
self._create_chart()
|
||||||
if not sync:
|
if not sync:
|
||||||
@ -569,22 +514,30 @@ const up = 'rgba(39, 157, 130, 100)'
|
|||||||
const down = 'rgba(200, 97, 100, 100)'
|
const down = 'rgba(200, 97, 100, 100)'
|
||||||
|
|
||||||
const wrapper = document.createElement('div')
|
const wrapper = document.createElement('div')
|
||||||
|
wrapper.className = 'wrapper'
|
||||||
document.body.appendChild(wrapper)
|
document.body.appendChild(wrapper)
|
||||||
|
|
||||||
function makeChart(innerWidth, innerHeight) {
|
function makeChart(innerWidth, innerHeight, topBar=false) {
|
||||||
let chart = {
|
let chart = {
|
||||||
markers: [],
|
markers: [],
|
||||||
horizontal_lines: [],
|
horizontal_lines: [],
|
||||||
div: document.createElement('div'),
|
div: document.createElement('div'),
|
||||||
|
wrapper: document.createElement('div'),
|
||||||
legend: document.createElement('div'),
|
legend: document.createElement('div'),
|
||||||
scale: {
|
scale: {
|
||||||
width: innerWidth,
|
width: innerWidth,
|
||||||
height: innerHeight
|
height: innerHeight
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
let topBarOffset = 0
|
||||||
|
if (topBar) {
|
||||||
|
makeTopBar(chart)
|
||||||
|
topBarOffset = chart.topBar.offsetHeight
|
||||||
|
}
|
||||||
|
|
||||||
chart.chart = LightweightCharts.createChart(chart.div, {
|
chart.chart = LightweightCharts.createChart(chart.div, {
|
||||||
width: window.innerWidth*innerWidth,
|
width: window.innerWidth*innerWidth,
|
||||||
height: window.innerHeight*innerHeight,
|
height: (window.innerHeight*innerHeight)-topBarOffset,
|
||||||
layout: {
|
layout: {
|
||||||
textColor: '#d1d4dc',
|
textColor: '#d1d4dc',
|
||||||
background: {
|
background: {
|
||||||
@ -612,6 +565,12 @@ function makeChart(innerWidth, innerHeight) {
|
|||||||
},
|
},
|
||||||
handleScroll: {vertTouchDrag: true},
|
handleScroll: {vertTouchDrag: true},
|
||||||
})
|
})
|
||||||
|
window.addEventListener('resize', function() {
|
||||||
|
if (topBar) {
|
||||||
|
topBarOffset = chart.topBar.offsetHeight
|
||||||
|
}
|
||||||
|
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
|
||||||
|
});
|
||||||
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
||||||
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
||||||
})
|
})
|
||||||
@ -631,9 +590,34 @@ function makeChart(innerWidth, innerHeight) {
|
|||||||
chart.legend.style.fontFamily = 'Monaco'
|
chart.legend.style.fontFamily = 'Monaco'
|
||||||
chart.legend.style.fontSize = '11px'
|
chart.legend.style.fontSize = '11px'
|
||||||
chart.legend.style.color = 'rgb(191, 195, 203)'
|
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.div.appendChild(chart.legend)
|
||||||
|
chart.wrapper.appendChild(chart.div)
|
||||||
|
wrapper.append(chart.wrapper)
|
||||||
|
|
||||||
return chart
|
return chart
|
||||||
}
|
}
|
||||||
|
function makeHorizontalLine(chart, 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,
|
||||||
|
};
|
||||||
|
chart.horizontal_lines.push(line)
|
||||||
|
}
|
||||||
function legendItemFormat(num) {
|
function legendItemFormat(num) {
|
||||||
return num.toFixed(2).toString().padStart(8, ' ')
|
return num.toFixed(2).toString().padStart(8, ' ')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,49 +5,115 @@ except ImportError:
|
|||||||
try:
|
try:
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt5.QtWebChannel import QWebChannel
|
from PyQt5.QtWebChannel import QWebChannel
|
||||||
from PyQt5.QtCore import QObject
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
|
class Bridge(QObject):
|
||||||
|
def __init__(self, chart):
|
||||||
|
super().__init__()
|
||||||
|
self.chart = chart
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def callback(self, message):
|
||||||
|
_widget_message(self.chart, message)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from streamlit.components.v1 import html
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
from lightweight_charts.chartasync import LWCAsync, ASYNC_SCRIPT
|
||||||
from lightweight_charts.js import LWC
|
from lightweight_charts.js import LWC
|
||||||
|
|
||||||
|
|
||||||
class WxChart(LWC):
|
def _widget_message(chart, string):
|
||||||
def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
|
messages = string.split('__')
|
||||||
|
name, chart_id = messages[:2]
|
||||||
|
args = messages[2:]
|
||||||
|
chart.api.chart = chart._charts[chart_id]
|
||||||
|
getattr(chart.api, name)(*args)
|
||||||
|
|
||||||
|
|
||||||
|
class WxChart(LWCAsync):
|
||||||
|
def __init__(self, parent, api: object = None, top_bar: bool = False, search_box: bool = False,
|
||||||
|
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
|
||||||
try:
|
try:
|
||||||
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
|
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
|
||||||
except NameError:
|
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.')
|
||||||
|
|
||||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
|
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
|
||||||
|
self.api = api
|
||||||
self._script_func = self.webview.RunScript
|
self._script_func = self.webview.RunScript
|
||||||
self._js_api_code = 'window.wx_msg.postMessage'
|
self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)'
|
||||||
|
|
||||||
|
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(200, self._on_js_load))
|
||||||
|
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: _widget_message(self, e.GetString()))
|
||||||
self.webview.AddScriptMessageHandler('wx_msg')
|
self.webview.AddScriptMessageHandler('wx_msg')
|
||||||
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: self._js_api.onClick(eval(e.GetString())))
|
|
||||||
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, self._on_js_load)
|
|
||||||
self.webview.SetPage(self._html, '')
|
|
||||||
self._create_chart()
|
|
||||||
|
|
||||||
def _on_js_load(self, e): super()._on_js_load()
|
self.webview.SetPage(self._html, '')
|
||||||
|
self.webview.AddUserScript(ASYNC_SCRIPT)
|
||||||
|
self._create_chart(top_bar)
|
||||||
|
self._make_search_box() if search_box else None
|
||||||
|
|
||||||
def get_webview(self): return self.webview
|
def get_webview(self): return self.webview
|
||||||
|
|
||||||
|
|
||||||
class QtChart(LWC):
|
class QtChart(LWCAsync):
|
||||||
def __init__(self, widget=None, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
|
def __init__(self, widget=None, api: object = None, top_bar: bool = False, search_box: bool = False,
|
||||||
|
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
|
||||||
try:
|
try:
|
||||||
self.webview = QWebEngineView(widget)
|
self.webview = QWebEngineView(widget)
|
||||||
except NameError:
|
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)
|
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
|
||||||
|
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.web_channel = QWebChannel()
|
||||||
|
self.bridge = Bridge(self)
|
||||||
|
self.web_channel.registerObject('bridge', self.bridge)
|
||||||
|
self.webview.page().setWebChannel(self.web_channel)
|
||||||
self.webview.loadFinished.connect(self._on_js_load)
|
self.webview.loadFinished.connect(self._on_js_load)
|
||||||
|
self._html = f'''
|
||||||
|
{self._html[:85]}
|
||||||
|
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
|
||||||
|
<script>
|
||||||
|
var bridge = new QWebChannel(qt.webChannelTransport, function(channel) {{
|
||||||
|
var pythonObject = channel.objects.bridge;
|
||||||
|
window.pythonObject = pythonObject
|
||||||
|
}});
|
||||||
|
</script>
|
||||||
|
{self._html[85:]}
|
||||||
|
'''
|
||||||
self.webview.page().setHtml(self._html)
|
self.webview.page().setHtml(self._html)
|
||||||
self._create_chart()
|
self.run_script(ASYNC_SCRIPT)
|
||||||
|
self._create_chart(top_bar)
|
||||||
|
self._make_search_box() if search_box else None
|
||||||
|
|
||||||
def get_webview(self): return self.webview
|
def get_webview(self): return self.webview
|
||||||
|
|
||||||
|
|
||||||
|
class StreamlitChart(LWC):
|
||||||
|
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1):
|
||||||
|
super().__init__(volume_enabled, inner_width, inner_height)
|
||||||
|
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
|
||||||
|
self._html = self._html.replace('</script>\n</body>\n</html>', '')
|
||||||
|
self._create_chart()
|
||||||
|
|
||||||
|
def run_script(self, script): self._html += '\n' + script
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
if self.loaded:
|
||||||
|
return
|
||||||
|
self.loaded = True
|
||||||
|
try:
|
||||||
|
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.')
|
||||||
|
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -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.8',
|
version='1.0.9',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
python_requires='>=3.9',
|
python_requires='>=3.9',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
|||||||
Reference in New Issue
Block a user