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:
@@ -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
-1
@@ -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"]
|
||||||
|
|
||||||
|
|||||||
+138
-18
@@ -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()
|
||||||
|
```
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 504 KiB |
@@ -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())
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -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)
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
'''
|
||||||
+59
-75
@@ -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.')
|
||||||
|
|
||||||
|
|||||||
@@ -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