- Significant refactoring resulting in a 34% reduction in size of the codebase (excluding the Lightweight Charts package) and greater efficiency.
- Upgraded to Lightweight Charts v4.0.1. - Added a ‘hover’ item to the returning dictionary from subscribe_click. - Markers and SubCharts no longer use a UUID for identification, but an 8 character string.
This commit is contained in:
15
README.md
15
README.md
@ -6,7 +6,7 @@
|
||||
[](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/).
|
||||
|
||||
@ -42,7 +42,7 @@ if __name__ == '__main__':
|
||||
chart.show(block=True)
|
||||
|
||||
```
|
||||

|
||||

|
||||
___
|
||||
|
||||
### 2. Updating bars in real-time:
|
||||
@ -76,7 +76,7 @@ if __name__ == '__main__':
|
||||
|
||||
```
|
||||
|
||||

|
||||

|
||||
___
|
||||
|
||||
### 3. Updating bars from tick data in real-time:
|
||||
@ -106,7 +106,7 @@ if __name__ == '__main__':
|
||||
sleep(0.3)
|
||||
|
||||
```
|
||||

|
||||

|
||||
___
|
||||
|
||||
### 4. Line Indicators:
|
||||
@ -140,7 +140,7 @@ if __name__ == '__main__':
|
||||
chart.show(block=True)
|
||||
|
||||
```
|
||||

|
||||

|
||||
___
|
||||
|
||||
### 5. Styling:
|
||||
@ -177,7 +177,7 @@ if __name__ == '__main__':
|
||||
chart.show(block=True)
|
||||
|
||||
```
|
||||

|
||||

|
||||
___
|
||||
|
||||
### 6. Callbacks:
|
||||
@ -203,7 +203,7 @@ if __name__ == '__main__':
|
||||
chart.show(block=True)
|
||||
|
||||
```
|
||||

|
||||

|
||||
___
|
||||
|
||||
[](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
|
||||
@ -213,4 +213,3 @@ ___
|
||||
_This package is an independent creation and has not been endorsed, sponsored, or approved by TradingView. The author of this package does not have any official relationship with TradingView, and the package does not represent the views or opinions of TradingView._
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
project = 'lightweight-charts-python'
|
||||
copyright = '2023, louisnw'
|
||||
author = 'louisnw'
|
||||
release = '1.0.3'
|
||||
release = '1.0.7'
|
||||
|
||||
extensions = ["myst_parser"]
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
[](https://github.com/louisnw01/lightweight-charts-python/blob/main/LICENSE)
|
||||
[](https://github.com/louisnw01/lightweight-charts-python)
|
||||
[](https://github.com/louisnw01/lightweight-charts-python)
|
||||
|
||||
___
|
||||
|
||||
## Common Methods
|
||||
These methods can be used within the `Chart`, `SubChart`, `QtChart`, and `WxChart` objects.
|
||||
@ -57,17 +57,17 @@ Creates and returns a [Line](#line) object.
|
||||
___
|
||||
|
||||
### `marker`
|
||||
`time: datetime` | `position: 'above'/'below'/'inside'` | `shape: 'arrow_up'/'arrow_down'/'circle'/'square'` | `color: str` | `text: str` | `-> UUID`
|
||||
`time: datetime` | `position: 'above'/'below'/'inside'` | `shape: 'arrow_up'/'arrow_down'/'circle'/'square'` | `color: str` | `text: str` | `-> str`
|
||||
|
||||
Adds a marker to the chart, and returns its UUID.
|
||||
Adds a marker to the chart, and returns its id.
|
||||
|
||||
If the `time` parameter is not given, the marker will be placed at the latest bar.
|
||||
___
|
||||
|
||||
### `remove_marker`
|
||||
`m_id: UUID`
|
||||
`marker_id: str`
|
||||
|
||||
Removes the marker with the given UUID.
|
||||
Removes the marker with the given id.
|
||||
|
||||
Usage:
|
||||
```python
|
||||
@ -162,13 +162,13 @@ ___
|
||||
|
||||
Subscribes the given function to a chart 'click' event.
|
||||
|
||||
The event emits a dictionary containing the bar at the time clicked, with the keys:
|
||||
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`
|
||||
`time | open | high | low | close | id | hover`
|
||||
___
|
||||
|
||||
### `create_subchart`
|
||||
`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/UUID` | `-> SubChart`
|
||||
`volume_enabled: bool` | `position: 'left'/'right'/'top'/'bottom'`, `width: float` | `height: float` | `sync: bool/str` | `-> SubChart`
|
||||
|
||||
Creates and returns a [SubChart](#subchart) object, placing it adjacent to the declaring `Chart` or `SubChart`.
|
||||
|
||||
@ -176,9 +176,7 @@ Creates and returns a [SubChart](#subchart) object, placing it adjacent to the d
|
||||
|
||||
`height` | `width`: Specifies the size of the `SubChart`, where `1` is the width/height of the window (100%)
|
||||
|
||||
`sync`: If given as `True`, the `SubChart`'s time scale will follow that of the declaring `Chart` or `SubChart`. If a `UUID` object is passed, the `SubChart` will follow the panel with the given `UUID`.
|
||||
|
||||
Chart `UUID`'s can be accessed from the`chart.id` and `subchart.id` attributes.
|
||||
`sync`: If given as `True`, the `SubChart`'s time scale will follow that of the declaring `Chart` or `SubChart`. If a `str` is passed, the `SubChart` will follow the panel with the given id. Chart ids can be accessed from the`chart.id` and `subchart.id` attributes.
|
||||
|
||||
```{important}
|
||||
`width` and `height` must be given as a number between 0 and 1.
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
:maxdepth: 2
|
||||
|
||||
docs
|
||||
Github Repository <https://github.com/louisnw01/lightweight-charts-python>
|
||||
```
|
||||
|
||||
```{include} ../../README.md
|
||||
|
||||
@ -1,34 +1,39 @@
|
||||
import pandas as pd
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import timedelta, datetime
|
||||
from typing import Dict, Union, Literal
|
||||
from typing import Union, Literal
|
||||
|
||||
from lightweight_charts.pkg import LWC_3_5_0
|
||||
from lightweight_charts.util import LINE_TYPE, POSITION, SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_type, \
|
||||
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _position, _shape, IDGen, _valid_color
|
||||
from lightweight_charts.pkg import LWC_4_0_1
|
||||
from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE, _crosshair_mode, _line_style, \
|
||||
MissingColumn, _js_bool, _price_scale_mode, PRICE_SCALE_MODE, _marker_position, _marker_shape, IDGen
|
||||
|
||||
|
||||
class Line:
|
||||
def __init__(self, lwc, line_id, color, width):
|
||||
self._lwc = lwc
|
||||
self.loaded = False
|
||||
self.id = line_id
|
||||
self.color = color
|
||||
self.width = width
|
||||
def __init__(self, parent, color, width):
|
||||
self._parent = parent
|
||||
self.id = self._parent._rand.generate()
|
||||
self._parent.run_script(f'''
|
||||
var {self.id} = {self._parent.id}.chart.addLineSeries({{
|
||||
color: '{color}',
|
||||
lineWidth: {width},
|
||||
}})''')
|
||||
|
||||
def set(self, data: pd.DataFrame):
|
||||
"""
|
||||
Sets the line data.\n
|
||||
:param data: columns: date/time, price
|
||||
:param data: columns: date/time, value
|
||||
"""
|
||||
self._lwc._set_line_data(self.id, data)
|
||||
df = self._parent._df_datetime_format(data)
|
||||
self._parent.run_script(f'{self.id}.setData({df.to_dict("records")})')
|
||||
|
||||
def update(self, series: pd.Series):
|
||||
"""
|
||||
Updates the line data.\n
|
||||
:param series: labels: date/time, price
|
||||
:param series: labels: date/time, value
|
||||
"""
|
||||
self._lwc._update_line_data(self.id, series)
|
||||
series = self._parent._series_datetime_format(series)
|
||||
self._parent.run_script(f'{self.id}.update({series.to_dict()})')
|
||||
|
||||
def marker(self): pass
|
||||
|
||||
|
||||
class API:
|
||||
@ -40,51 +45,34 @@ class API:
|
||||
if isinstance(data['time'], int):
|
||||
data['time'] = datetime.fromtimestamp(data['time'])
|
||||
else:
|
||||
data['time'] = datetime(data['time']['year'], data['time']['month'], data['time']['day'])
|
||||
data['time'] = datetime.strptime(data['time'], '%Y-%m-%d')
|
||||
click_func(data) if click_func else None
|
||||
|
||||
|
||||
class LWC:
|
||||
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0):
|
||||
self.id = uuid4()
|
||||
self.js_queue = []
|
||||
self._volume_enabled = volume_enabled
|
||||
self._inner_width = inner_width
|
||||
self._inner_height = inner_height
|
||||
self._position = 'left'
|
||||
self.loaded = False
|
||||
|
||||
self._rand = IDGen()
|
||||
self._chart_var = 'chart'
|
||||
|
||||
self.id = self._rand.generate()
|
||||
self._append_js = f'document.body.append({self.id}.div)'
|
||||
self._scripts = []
|
||||
self._script_func = None
|
||||
self._html = HTML
|
||||
self._last_bar = None
|
||||
self._interval = None
|
||||
self._background_color = '#000000'
|
||||
self._volume_up_color = 'rgba(83,141,131,0.8)'
|
||||
self._volume_down_color = 'rgba(200,127,130,0.8)'
|
||||
self._js_api = API()
|
||||
self._js_api_code = ''
|
||||
|
||||
self.volume_enabled = volume_enabled
|
||||
self.inner_width = inner_width
|
||||
self.inner_height = inner_height
|
||||
self._html = HTML.replace('__INNER_WIDTH__', str(self.inner_width)).replace('__INNER_HEIGHT__', str(self.inner_height))
|
||||
|
||||
self.last_bar = None
|
||||
self.interval = None
|
||||
self._lines: Dict[UUID, Line] = {}
|
||||
self._subcharts: Dict[UUID, LWC] = {self.id: self}
|
||||
|
||||
self.background_color = '#000000'
|
||||
self.volume_up_color = 'rgba(83,141,131,0.8)'
|
||||
self.volume_down_color = 'rgba(200,127,130,0.8)'
|
||||
|
||||
def _on_js_load(self): pass
|
||||
|
||||
def _stored(self, func, *args, **kwargs):
|
||||
if self.loaded:
|
||||
return False
|
||||
self.js_queue.append((func, args, kwargs))
|
||||
return True
|
||||
|
||||
def _set_last_bar(self, bar: pd.Series): self.last_bar = bar
|
||||
self._js_api_code = None
|
||||
|
||||
def _set_interval(self, df: pd.DataFrame):
|
||||
df['time'] = pd.to_datetime(df['time'])
|
||||
intervals = df['time'].diff()
|
||||
counts = intervals.value_counts()
|
||||
self.interval = counts.index[0]
|
||||
common_interval = pd.to_datetime(df['time']).diff().value_counts()
|
||||
self._interval = common_interval.index[0]
|
||||
|
||||
def _df_datetime_format(self, df: pd.DataFrame):
|
||||
if 'date' in df.columns:
|
||||
@ -101,81 +89,85 @@ class LWC:
|
||||
|
||||
def _datetime_format(self, string):
|
||||
string = pd.to_datetime(string)
|
||||
if self.interval != timedelta(days=1):
|
||||
if self._interval != timedelta(days=1):
|
||||
string = string.timestamp()
|
||||
string = self.interval.total_seconds() * (string // self.interval.total_seconds())
|
||||
string = self._interval.total_seconds() * (string // self._interval.total_seconds())
|
||||
else:
|
||||
string = string.strftime('%Y-%m-%d')
|
||||
return string
|
||||
|
||||
def run_script(self, script): pass
|
||||
def _on_js_load(self):
|
||||
self.loaded = True
|
||||
for script in self._scripts:
|
||||
self.run_script(script)
|
||||
|
||||
def _create_chart(self):
|
||||
self.run_script(f'''
|
||||
{self.id} = makeChart({self._inner_width}, {self._inner_height})
|
||||
{self.id}.div.style.float = "{self._position}"
|
||||
{self._append_js}
|
||||
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):
|
||||
"""
|
||||
For advanced users; evaluates JavaScript within the Webview.
|
||||
"""
|
||||
self._script_func(script) if self.loaded else self._scripts.append(script)
|
||||
|
||||
def set(self, df: pd.DataFrame):
|
||||
"""
|
||||
Sets the initial data for the chart.\n
|
||||
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
|
||||
"""
|
||||
if self._stored('set', df):
|
||||
return None
|
||||
|
||||
df = self._df_datetime_format(df)
|
||||
self._set_last_bar(df.iloc[-1])
|
||||
bars = df
|
||||
if self.volume_enabled:
|
||||
if 'volume' not in df:
|
||||
bars = self._df_datetime_format(df)
|
||||
self._last_bar = bars.iloc[-1]
|
||||
if self._volume_enabled:
|
||||
if 'volume' not in bars:
|
||||
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
||||
|
||||
volume = df.drop(columns=['open', 'high', 'low', 'close'])
|
||||
volume = volume.rename(columns={'volume': 'value'})
|
||||
volume['color'] = self.volume_down_color
|
||||
volume.loc[df['close'] > df['open'], 'color'] = self.volume_up_color
|
||||
|
||||
self.run_script(f'{self._chart_var}.volumeSeries.setData({volume.to_dict(orient="records")})')
|
||||
bars = df.drop(columns=['volume'])
|
||||
volume = bars.drop(columns=['open', 'high', 'low', 'close']).rename(columns={'volume': 'value'})
|
||||
volume['color'] = self._volume_down_color
|
||||
volume.loc[bars['close'] > bars['open'], 'color'] = self._volume_up_color
|
||||
self.run_script(f'{self.id}.volumeSeries.setData({volume.to_dict(orient="records")})')
|
||||
bars = bars.drop(columns=['volume'])
|
||||
|
||||
bars = bars.to_dict(orient='records')
|
||||
self.run_script(f'{self._chart_var}.series.setData({bars})')
|
||||
self.run_script(f'{self.id}.series.setData({bars})')
|
||||
|
||||
def update(self, series, from_tick=False):
|
||||
"""
|
||||
Updates the data from a bar;
|
||||
if series['time'] is the same time as the last bar, the last bar will be overwritten.\n
|
||||
:param series: columns: date/time, open, high, low, close, volume (if volume enabled).
|
||||
:param series: labels: date/time, open, high, low, close, volume (if volume enabled).
|
||||
"""
|
||||
if self._stored('update', series, from_tick):
|
||||
return None
|
||||
|
||||
series = self._series_datetime_format(series) if not from_tick else series
|
||||
self._set_last_bar(series)
|
||||
if self.volume_enabled:
|
||||
self._last_bar = series
|
||||
if self._volume_enabled:
|
||||
if 'volume' not in series:
|
||||
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
||||
|
||||
volume = series.drop(['open', 'high', 'low', 'close'])
|
||||
volume = volume.rename({'volume': 'value'})
|
||||
volume['color'] = self.volume_up_color if series['close'] > series['open'] else self.volume_down_color
|
||||
self.run_script(f'{self._chart_var}.volumeSeries.update({volume.to_dict()})')
|
||||
volume = series.drop(['open', 'high', 'low', 'close']).rename({'volume': 'value'})
|
||||
volume['color'] = self._volume_up_color if series['close'] > series['open'] else self._volume_down_color
|
||||
self.run_script(f'{self.id}.volumeSeries.update({volume.to_dict()})')
|
||||
series = series.drop(['volume'])
|
||||
|
||||
dictionary = series.to_dict()
|
||||
self.run_script(f'{self._chart_var}.series.update({dictionary})')
|
||||
self.run_script(f'{self.id}.series.update({series.to_dict()})')
|
||||
|
||||
def update_from_tick(self, series):
|
||||
"""
|
||||
Updates the data from a tick.\n
|
||||
:param series: columns: date/time, price, volume (if volume enabled).
|
||||
:param series: labels: date/time, price, volume (if volume enabled).
|
||||
"""
|
||||
if self._stored('update_from_tick', series):
|
||||
return None
|
||||
|
||||
series = self._series_datetime_format(series)
|
||||
bar = pd.Series()
|
||||
if series['time'] == self.last_bar['time']:
|
||||
bar = self.last_bar
|
||||
bar['high'] = max(self.last_bar['high'], series['price'])
|
||||
bar['low'] = min(self.last_bar['low'], series['price'])
|
||||
if series['time'] == self._last_bar['time']:
|
||||
bar = self._last_bar
|
||||
bar['high'] = max(self._last_bar['high'], series['price'])
|
||||
bar['low'] = min(self._last_bar['low'], series['price'])
|
||||
bar['close'] = series['price']
|
||||
if self.volume_enabled:
|
||||
if self._volume_enabled:
|
||||
if 'volume' not in series:
|
||||
raise MissingColumn("Volume enabled, but 'volume' column was not found.")
|
||||
bar['volume'] = series['volume']
|
||||
@ -189,53 +181,11 @@ class LWC:
|
||||
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
|
||||
"""
|
||||
Creates and returns a Line object.)\n
|
||||
:return a Line object used to set/update the line.
|
||||
"""
|
||||
line_id = uuid4()
|
||||
self._lines[line_id] = Line(self, line_id, color, width)
|
||||
return self._lines[line_id]
|
||||
return Line(self, color, width)
|
||||
|
||||
def _set_line_data(self, line_id, df: pd.DataFrame):
|
||||
if self._stored('_set_line_data', line_id, df):
|
||||
return None
|
||||
line = self._lines[line_id]
|
||||
|
||||
if not line.loaded:
|
||||
var = self._rand.generate()
|
||||
self.run_script(f'''
|
||||
let lineSeries{var} = {{
|
||||
color: '{line.color}',
|
||||
lineWidth: {line.width},
|
||||
}};
|
||||
let line{var} = {{
|
||||
series: {self._chart_var}.chart.addLineSeries(lineSeries{var}),
|
||||
id: '{line_id}',
|
||||
}};
|
||||
lines.push(line{var})
|
||||
''')
|
||||
line.loaded = True
|
||||
df = self._df_datetime_format(df)
|
||||
self.run_script(f'''
|
||||
lines.forEach(function (line) {{
|
||||
if ('{line_id}' === line.id) {{
|
||||
line.series.setData({df.to_dict('records')})
|
||||
}}
|
||||
}})''')
|
||||
|
||||
def _update_line_data(self, line_id, series: pd.Series):
|
||||
if self._stored('_update_line_data', line_id, series):
|
||||
return None
|
||||
|
||||
series = self._series_datetime_format(series)
|
||||
self.run_script(f'''
|
||||
lines.forEach(function (line) {{
|
||||
if ('{line_id}' === line.id) {{
|
||||
line.series.update({series.to_dict()})
|
||||
}}
|
||||
}})''')
|
||||
|
||||
def marker(self, time: datetime = None, position: POSITION = 'below', shape: SHAPE = 'arrow_up',
|
||||
color: str = '#2196F3', text: str = '', m_id: UUID = None) -> UUID:
|
||||
def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', shape: MARKER_SHAPE = 'arrow_up',
|
||||
color: str = '#2196F3', text: str = '') -> str:
|
||||
"""
|
||||
Creates a new marker.\n
|
||||
:param time: The time that the marker will be placed at. If no time is given, it will be placed at the last bar.
|
||||
@ -243,66 +193,54 @@ class LWC:
|
||||
:param color: The color of the marker (rgb, rgba or hex).
|
||||
:param shape: The shape of the marker.
|
||||
:param text: The text to be placed with the marker.
|
||||
:return: The UUID of the marker placed.
|
||||
:return: The id of the marker placed.
|
||||
"""
|
||||
_valid_color(color)
|
||||
if not m_id:
|
||||
m_id = uuid4()
|
||||
if self._stored('marker', time, position, shape, color, text, m_id):
|
||||
return m_id
|
||||
|
||||
try:
|
||||
time = self.last_bar['time'] if not time else self._datetime_format(time)
|
||||
time = self._last_bar['time'] if not time else self._datetime_format(time)
|
||||
except TypeError:
|
||||
raise TypeError('Chart marker created before data was set.')
|
||||
time = time if isinstance(time, float) else f"'{time}'"
|
||||
|
||||
marker_id = self._rand.generate()
|
||||
self.run_script(f"""
|
||||
markers.push({{
|
||||
time: {time},
|
||||
position: '{_position(position)}',
|
||||
color: '{color}', shape: '{_shape(shape)}',
|
||||
time: {time if isinstance(time, float) else f"'{time}'"},
|
||||
position: '{_marker_position(position)}',
|
||||
color: '{color}',
|
||||
shape: '{_marker_shape(shape)}',
|
||||
text: '{text}',
|
||||
id: '{m_id}'
|
||||
id: '{marker_id}'
|
||||
}});
|
||||
{self._chart_var}.series.setMarkers(markers)""")
|
||||
return m_id
|
||||
{self.id}.series.setMarkers(markers)""")
|
||||
return marker_id
|
||||
|
||||
def remove_marker(self, m_id: UUID):
|
||||
def remove_marker(self, marker_id: str):
|
||||
"""
|
||||
Removes the marker with the given UUID.\n
|
||||
Removes the marker with the given id.\n
|
||||
"""
|
||||
if self._stored('remove_marker', m_id):
|
||||
return None
|
||||
|
||||
self.run_script(f'''
|
||||
markers.forEach(function (marker) {{
|
||||
if ('{m_id}' === marker.id) {{
|
||||
if ('{marker_id}' === marker.id) {{
|
||||
markers.splice(markers.indexOf(marker), 1)
|
||||
{self._chart_var}.series.setMarkers(markers)
|
||||
{self.id}.series.setMarkers(markers)
|
||||
}}
|
||||
}});''')
|
||||
|
||||
def horizontal_line(self, price: Union[float, int], color: str = 'rgb(122, 146, 202)', width: int = 1,
|
||||
style: LINE_TYPE = 'solid', text: str = '', axis_label_visible=True):
|
||||
style: LINE_STYLE = 'solid', text: str = '', axis_label_visible=True):
|
||||
"""
|
||||
Creates a horizontal line at the given price.\n
|
||||
"""
|
||||
if self._stored('horizontal_line', price, color, width, style, text, axis_label_visible):
|
||||
return None
|
||||
|
||||
var = self._rand.generate()
|
||||
self.run_script(f"""
|
||||
let priceLine{var} = {{
|
||||
price: {price},
|
||||
color: '{color}',
|
||||
lineWidth: {width},
|
||||
lineStyle: LightweightCharts.LineStyle.{_line_type(style)},
|
||||
axisLabelVisible: {'true' if axis_label_visible else 'false'},
|
||||
lineStyle: {_line_style(style)},
|
||||
axisLabelVisible: {_js_bool(axis_label_visible)},
|
||||
title: '{text}',
|
||||
}};
|
||||
let line{var} = {{
|
||||
line: {self._chart_var}.series.createPriceLine(priceLine{var}),
|
||||
line: {self.id}.series.createPriceLine(priceLine{var}),
|
||||
price: {price},
|
||||
}};
|
||||
horizontal_lines.push(line{var})""")
|
||||
@ -311,30 +249,23 @@ class LWC:
|
||||
"""
|
||||
Removes a horizontal line at the given price.
|
||||
"""
|
||||
if self._stored('remove_horizontal_line', price):
|
||||
return None
|
||||
|
||||
self.run_script(f'''
|
||||
horizontal_lines.forEach(function (line) {{
|
||||
if ({price} === line.price) {{
|
||||
{self._chart_var}.series.removePriceLine(line.line);
|
||||
{self.id}.series.removePriceLine(line.line);
|
||||
horizontal_lines.splice(horizontal_lines.indexOf(line), 1)
|
||||
}}
|
||||
}});''')
|
||||
|
||||
def config(self, mode: PRICE_SCALE_MODE = None, title: str = None, right_padding: float = None):
|
||||
def config(self, mode: PRICE_SCALE_MODE = 'normal', title: str = None, right_padding: float = None):
|
||||
"""
|
||||
:param mode: Chart price scale mode.
|
||||
:param title: Last price label text.
|
||||
:param right_padding: How many bars of empty space to the right of the last bar.
|
||||
"""
|
||||
if self._stored('config', mode, title, right_padding):
|
||||
return None
|
||||
|
||||
self.run_script(f'{self._chart_var}.chart.timeScale().scrollToPosition({right_padding}, false)') if right_padding is not None else None
|
||||
self.run_script(f'{self._chart_var}.series.applyOptions({{title: "{title}"}})') if title else None
|
||||
self.run_script(
|
||||
f"{self._chart_var}.chart.priceScale().applyOptions({{mode: LightweightCharts.PriceScaleMode.{_price_scale_mode(mode)}}})") if mode else None
|
||||
self.run_script(f'{self.id}.chart.timeScale().scrollToPosition({right_padding}, false)') if right_padding is not None else None
|
||||
self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})') if title else None
|
||||
self.run_script(f"{self.id}.chart.priceScale().applyOptions({{mode: {_price_scale_mode(mode)}}})")
|
||||
|
||||
def time_scale(self, visible: bool = True, time_visible: bool = True, seconds_visible: bool = False):
|
||||
"""
|
||||
@ -344,18 +275,12 @@ class LWC:
|
||||
:param seconds_visible: Seconds visibility control.
|
||||
:return:
|
||||
"""
|
||||
if self._stored('time_scale', visible, time_visible, seconds_visible):
|
||||
return None
|
||||
|
||||
time_scale_visible = f'visible: {_js_bool(visible)},'
|
||||
time = f'timeVisible: {_js_bool(time_visible)},'
|
||||
seconds = f'secondsVisible: {_js_bool(seconds_visible)},'
|
||||
self.run_script(f'''
|
||||
{self._chart_var}.chart.applyOptions({{
|
||||
{self.id}.chart.applyOptions({{
|
||||
timeScale: {{
|
||||
{time_scale_visible if visible is not None else ''}
|
||||
{time if time_visible is not None else ''}
|
||||
{seconds if seconds_visible is not None else ''}
|
||||
visible: {_js_bool(visible)},
|
||||
timeVisible: {_js_bool(time_visible)},
|
||||
secondsVisible: {_js_bool(seconds_visible)},
|
||||
}}
|
||||
}})''')
|
||||
|
||||
@ -364,44 +289,38 @@ class LWC:
|
||||
"""
|
||||
Global layout options for the chart.
|
||||
"""
|
||||
if self._stored('layout', background_color, text_color, font_size, font_family):
|
||||
return None
|
||||
|
||||
self.background_color = background_color if background_color else self.background_color
|
||||
self._background_color = background_color if background_color else self._background_color
|
||||
self.run_script(f"""
|
||||
document.body.style.backgroundColor = '{self.background_color}'
|
||||
{self._chart_var}.chart.applyOptions({{
|
||||
document.body.style.backgroundColor = '{self._background_color}'
|
||||
{self.id}.chart.applyOptions({{
|
||||
layout: {{
|
||||
{f'backgroundColor: "{background_color}",' if background_color else ''}
|
||||
background: {{
|
||||
color: "{self._background_color}",
|
||||
}},
|
||||
{f'textColor: "{text_color}",' if text_color else ''}
|
||||
{f'fontSize: {font_size},' if font_size else ''}
|
||||
{f'fontFamily: "{font_family}",' if font_family else ''}
|
||||
}}}})""")
|
||||
|
||||
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True, color: str = 'rgba(29, 30, 38, 5)', style: LINE_TYPE = 'solid'):
|
||||
def grid(self, vert_enabled: bool = True, horz_enabled: bool = True, color: str = 'rgba(29, 30, 38, 5)', style: LINE_STYLE = 'solid'):
|
||||
"""
|
||||
Grid styling for the chart.
|
||||
"""
|
||||
if self._stored('grid', vert_enabled, horz_enabled, color, style):
|
||||
return None
|
||||
|
||||
self.run_script(f"""
|
||||
{self._chart_var}.chart.applyOptions({{
|
||||
{self.id}.chart.applyOptions({{
|
||||
grid: {{
|
||||
{f'''vertLines: {{
|
||||
{f'visible: {_js_bool(vert_enabled)},' if vert_enabled is not None else ''}
|
||||
{f'color: "{color}",' if color else ''}
|
||||
{f'style: LightweightCharts.LineStyle.{_line_type(style)},' if style else ''}
|
||||
}},''' if vert_enabled is not None or color or style else ''}
|
||||
|
||||
{f'''horzLines: {{
|
||||
{f'visible: {_js_bool(horz_enabled)},' if horz_enabled is not None else ''}
|
||||
{f'color: "{color}",' if color else ''}
|
||||
{f'style: LightweightCharts.LineStyle.{_line_type(style)},' if style else ''}
|
||||
}},''' if horz_enabled is not None or color or style else ''}
|
||||
vertLines: {{
|
||||
visible: {_js_bool(vert_enabled)},
|
||||
color: "{color}",
|
||||
style: {_line_style(style)},
|
||||
}},
|
||||
horzLines: {{
|
||||
visible: {_js_bool(horz_enabled)},
|
||||
color: "{color}",
|
||||
style: {_line_style(style)},
|
||||
}},
|
||||
}}
|
||||
}})
|
||||
""")
|
||||
}})""")
|
||||
|
||||
def candle_style(self, up_color: str = 'rgba(39, 157, 130, 100)', down_color: str = 'rgba(200, 97, 100, 100)',
|
||||
wick_enabled: bool = True, border_enabled: bool = True, border_up_color: str = '',
|
||||
@ -409,20 +328,16 @@ class LWC:
|
||||
"""
|
||||
Candle styling for each of its parts.
|
||||
"""
|
||||
if self._stored('candle_style', up_color, down_color, wick_enabled, border_enabled,
|
||||
border_up_color, border_down_color, wick_up_color, wick_down_color):
|
||||
return None
|
||||
|
||||
self.run_script(f"""
|
||||
{self._chart_var}.series.applyOptions({{
|
||||
{f'upColor: "{up_color}",' if up_color else ''}
|
||||
{f'downColor: "{down_color}",' if down_color else ''}
|
||||
{f'wickVisible: {_js_bool(wick_enabled)},' if wick_enabled is not None else ''}
|
||||
{f'borderVisible: {_js_bool(border_enabled)},' if border_enabled is not None else ''}
|
||||
{f'borderUpColor: "{border_up_color}",' if border_up_color else ''}
|
||||
{f'borderDownColor: "{border_down_color}",' if border_down_color else ''}
|
||||
{f'wickUpColor: "{wick_up_color}",' if wick_up_color else ''}
|
||||
{f'wickDownColor: "{wick_down_color}",' if wick_down_color else ''}
|
||||
{self.id}.series.applyOptions({{
|
||||
upColor: "{up_color}",
|
||||
downColor: "{down_color}",
|
||||
wickVisible: {_js_bool(wick_enabled)},
|
||||
borderVisible: {_js_bool(border_enabled)},
|
||||
{f'borderUpColor: "{border_up_color}",' if border_up_color else up_color if border_enabled else ''}
|
||||
{f'borderDownColor: "{border_down_color}",' if border_down_color else down_color if border_enabled else ''}
|
||||
{f'wickUpColor: "{wick_up_color}",' if wick_up_color else wick_up_color if wick_enabled else ''}
|
||||
{f'wickDownColor: "{wick_down_color}",' if wick_down_color else wick_down_color if wick_enabled else ''}
|
||||
}})""")
|
||||
|
||||
def volume_config(self, scale_margin_top: float = 0.8, scale_margin_bottom: float = 0.0,
|
||||
@ -436,13 +351,10 @@ class LWC:
|
||||
:param up_color: Volume color for upward direction (rgb, rgba or hex)
|
||||
:param down_color: Volume color for downward direction (rgb, rgba or hex)
|
||||
"""
|
||||
if self._stored('volume_config', scale_margin_top, scale_margin_bottom, up_color, down_color):
|
||||
return None
|
||||
|
||||
self.volume_up_color = up_color if up_color else self.volume_up_color
|
||||
self.volume_down_color = down_color if down_color else self.volume_down_color
|
||||
self._volume_up_color = up_color if up_color else self._volume_up_color
|
||||
self._volume_down_color = down_color if down_color else self._volume_down_color
|
||||
self.run_script(f'''
|
||||
{self._chart_var}.volumeSeries.priceScale().applyOptions({{
|
||||
{self.id}.volumeSeries.priceScale().applyOptions({{
|
||||
scaleMargins: {{
|
||||
top: {scale_margin_top},
|
||||
bottom: {scale_margin_bottom},
|
||||
@ -450,40 +362,35 @@ class LWC:
|
||||
}})''')
|
||||
|
||||
def crosshair(self, mode: CROSSHAIR_MODE = 'normal', vert_width: int = 1, vert_color: str = None,
|
||||
vert_style: LINE_TYPE = None, vert_label_background_color: str = None, horz_width: int = 1,
|
||||
horz_color: str = None, horz_style: LINE_TYPE = None, horz_label_background_color: str = None):
|
||||
vert_style: LINE_STYLE = 'dashed', vert_label_background_color: str = 'rgb(46, 46, 46)', horz_width: int = 1,
|
||||
horz_color: str = None, horz_style: LINE_STYLE = 'dashed', horz_label_background_color: str = 'rgb(55, 55, 55)'):
|
||||
"""
|
||||
Crosshair formatting for its vertical and horizontal axes.
|
||||
"""
|
||||
if self._stored('crosshair', mode, vert_width, vert_color, vert_style, vert_label_background_color,
|
||||
horz_width, horz_color, horz_style, horz_label_background_color):
|
||||
return None
|
||||
|
||||
args = f"LightweightCharts.CrosshairMode.{_crosshair_mode(mode)}", \
|
||||
f"{vert_width}}}", f"'{vert_color}'}}", f"LightweightCharts.LineStyle.{_line_type(vert_style)}}}",\
|
||||
f"'{vert_label_background_color}'}}", \
|
||||
f"{horz_width}}}", f"'{horz_color}'}}", f"LightweightCharts.LineStyle.{_line_type(horz_style)}}}",\
|
||||
f"'{horz_label_background_color}'}}"
|
||||
for key, arg in zip(
|
||||
('mode', 'vertLine: {width', 'vertLine: {color', 'vertLine: {style', 'vertLine: {labelBackgroundColor',
|
||||
'horzLine: {width', 'horzLine: {color', 'horzLine: {style', 'horzLine: {labelBackgroundColor'), args):
|
||||
if 'None' in arg:
|
||||
continue
|
||||
self.run_script(f'''
|
||||
{self._chart_var}.chart.applyOptions({{
|
||||
{self.id}.chart.applyOptions({{
|
||||
crosshair: {{
|
||||
{key}: {arg}
|
||||
mode: {_crosshair_mode(mode)},
|
||||
vertLine: {{
|
||||
width: {vert_width},
|
||||
{f'color: "{vert_color}",' if vert_color else ''}
|
||||
style: {_line_style(vert_style)},
|
||||
labelBackgroundColor: "{vert_label_background_color}"
|
||||
}},
|
||||
horzLine: {{
|
||||
width: {horz_width},
|
||||
{f'color: "{horz_color}",' if horz_color else ''}
|
||||
style: {_line_style(horz_style)},
|
||||
labelBackgroundColor: "{horz_label_background_color}"
|
||||
}}
|
||||
}}}})''')
|
||||
|
||||
def watermark(self, text: str, font_size: int = 44, color: str = 'rgba(180, 180, 200, 0.5)'):
|
||||
"""
|
||||
Adds a watermark to the chart.
|
||||
"""
|
||||
if self._stored('watermark', text, font_size, color):
|
||||
return None
|
||||
|
||||
self.run_script(f'''
|
||||
{self._chart_var}.chart.applyOptions({{
|
||||
{self.id}.chart.applyOptions({{
|
||||
watermark: {{
|
||||
visible: true,
|
||||
fontSize: {font_size},
|
||||
@ -499,18 +406,15 @@ class LWC:
|
||||
"""
|
||||
Configures the legend of the chart.
|
||||
"""
|
||||
if self._stored('legend', visible, ohlc, percent, color, font_size, font_family):
|
||||
return None
|
||||
|
||||
if visible:
|
||||
self.run_script(f'''
|
||||
{f"{self._chart_var}.legend.style.color = '{color}'" if color else ''}
|
||||
{f"{self._chart_var}.legend.style.fontSize = {font_size}" if font_size else ''}
|
||||
{f"{self._chart_var}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
|
||||
{f"{self.id}.legend.style.color = '{color}'" if color else ''}
|
||||
{f"{self.id}.legend.style.fontSize = {font_size}" if font_size else ''}
|
||||
{f"{self.id}.legend.style.fontFamily = '{font_family}'" if font_family else ''}
|
||||
|
||||
{self._chart_var}.chart.subscribeCrosshairMove((param) => {{
|
||||
{self.id}.chart.subscribeCrosshairMove((param) => {{
|
||||
if (param.time){{
|
||||
const data = param.seriesPrices.get({self._chart_var}.series);
|
||||
const data = param.seriesData.get({self.id}.series);
|
||||
if (!data) {{return}}
|
||||
let percentMove = ((data.close-data.open)/data.open)*100
|
||||
let ohlc = `open: ${{legendItemFormat(data.open)}}
|
||||
@ -521,44 +425,38 @@ class LWC:
|
||||
let finalString = ''
|
||||
{'finalString += ohlc' if ohlc else ''}
|
||||
{'finalString += percent' if percent else ''}
|
||||
{self._chart_var}.legend.innerHTML = finalString
|
||||
{self.id}.legend.innerHTML = finalString
|
||||
}}
|
||||
else {{
|
||||
{self._chart_var}.legend.innerHTML = ''
|
||||
{self.id}.legend.innerHTML = ''
|
||||
}}
|
||||
}});''')
|
||||
|
||||
def subscribe_click(self, function: object):
|
||||
if self._stored('subscribe_click', function):
|
||||
return None
|
||||
|
||||
self._js_api.click_funcs[str(self.id)] = function
|
||||
var = self._rand.generate()
|
||||
"""
|
||||
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._chart_var}.chart.subscribeClick((param) => {{
|
||||
{self.id}.chart.subscribeClick((param) => {{
|
||||
if (!param.point) {{return}}
|
||||
let prices{var} = param.seriesPrices.get({self._chart_var}.series);
|
||||
let data{var} = {{
|
||||
let prices = param.seriesData.get({self.id}.series);
|
||||
let data = {{
|
||||
time: param.time,
|
||||
open: prices{var}.open,
|
||||
high: prices{var}.high,
|
||||
low: prices{var}.low,
|
||||
close: prices{var}.close,
|
||||
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{var})
|
||||
{self._js_api_code}(data)
|
||||
}})''')
|
||||
|
||||
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, UUID] = False):
|
||||
subchart = SubChart(self, volume_enabled, position, width, height, sync)
|
||||
self._subcharts[subchart.id] = subchart
|
||||
return subchart
|
||||
|
||||
def _pywebview_subchart(self, volume_enabled, position, width, height, sync, parent=None):
|
||||
subchart = PyWebViewSubChart(self if not parent else parent, volume_enabled, position, width, height, sync)
|
||||
self._subcharts[subchart.id] = subchart
|
||||
return subchart.id
|
||||
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False):
|
||||
return SubChart(self, volume_enabled, position, width, height, sync)
|
||||
|
||||
|
||||
class SubChart(LWC):
|
||||
@ -566,207 +464,96 @@ class SubChart(LWC):
|
||||
super().__init__(volume_enabled, width, height)
|
||||
self._chart = parent._chart if isinstance(parent, SubChart) else parent
|
||||
self._parent = parent
|
||||
|
||||
self._position = position
|
||||
self._rand = self._chart._rand
|
||||
self._chart_var = 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.position = position
|
||||
|
||||
self._create_panel(sync)
|
||||
|
||||
def _stored(self, func, *args, **kwargs):
|
||||
if self._chart.loaded:
|
||||
return False
|
||||
self._chart.js_queue.append((f'SUB{func}', (self.id,)+args, kwargs))
|
||||
return True
|
||||
self._create_chart()
|
||||
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)
|
||||
}});''')
|
||||
|
||||
def run_script(self, script): self._chart.run_script(script)
|
||||
|
||||
def _create_panel(self, sync):
|
||||
if self._stored('_create_panel', sync):
|
||||
return None
|
||||
|
||||
parent_div = 'chartsDiv' if self._parent._chart_var == 'chart' else self._parent._chart_var+'div'
|
||||
|
||||
sub_sync = ''
|
||||
if sync:
|
||||
sync_parent_var = self._chart._subcharts[sync]._chart_var if isinstance(sync, UUID) else self._parent._chart_var
|
||||
sub_sync = f'''
|
||||
{sync_parent_var}.chart.timeScale().subscribeVisibleLogicalRangeChange((timeRange) => {{
|
||||
{self._chart_var}.chart.timeScale().setVisibleLogicalRange(timeRange)
|
||||
}});
|
||||
'''
|
||||
self.run_script(f'''
|
||||
{self._chart_var}div = document.createElement('div')
|
||||
//{self._chart_var}div.style.position = 'relative'
|
||||
{self._chart_var}div.style.float = "{self.position}"
|
||||
|
||||
//chartsDiv.style.display = 'inline-block'
|
||||
chartsDiv.style.float = 'left'
|
||||
|
||||
{self._chart_var} = {{}}
|
||||
{self._chart_var}.scale = {{
|
||||
width: {self.inner_width},
|
||||
height: {self.inner_height}
|
||||
}}
|
||||
{self._chart_var}.chart = makeChart(window.innerWidth*{self._chart_var}.scale.width,
|
||||
window.innerHeight*{self._chart_var}.scale.height, {self._chart_var}div)
|
||||
{self._chart_var}.series = makeCandlestickSeries({self._chart_var}.chart)
|
||||
{self._chart_var}.volumeSeries = makeVolumeSeries({self._chart_var}.chart)
|
||||
|
||||
{self._chart_var}.legend = document.createElement('div')
|
||||
{self._chart_var}.legend.style.position = 'absolute'
|
||||
{self._chart_var}.legend.style.zIndex = 1000
|
||||
{self._chart_var}.legend.style.width = '{(self.inner_width*100)-8}vw'
|
||||
{self._chart_var}.legend.style.top = '10px'
|
||||
{self._chart_var}.legend.style.left = '10px'
|
||||
{self._chart_var}.legend.style.fontFamily = 'Monaco'
|
||||
{self._chart_var}.legend.style.fontSize = '11px'
|
||||
{self._chart_var}.legend.style.color = 'rgb(191, 195, 203)'
|
||||
{self._chart_var}div.appendChild({self._chart_var}.legend)
|
||||
|
||||
{parent_div}.parentNode.insertBefore({self._chart_var}div, {parent_div}.nextSibling)
|
||||
charts.push({self._chart_var})
|
||||
|
||||
{self._chart_var}.chart.priceScale('').applyOptions({{
|
||||
scaleMargins: {{
|
||||
top: 0.8,
|
||||
bottom: 0,
|
||||
}}
|
||||
}});
|
||||
{sub_sync}
|
||||
''')
|
||||
|
||||
|
||||
class PyWebViewSubChart(SubChart):
|
||||
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, UUID] = False):
|
||||
return self._chart._pywebview_subchart(volume_enabled, position, width, height, sync, parent=self)
|
||||
|
||||
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
|
||||
return super().create_line(color, width).id
|
||||
|
||||
|
||||
SCRIPT = """
|
||||
document.body.style.backgroundColor = '#000000'
|
||||
|
||||
const markers = []
|
||||
const horizontal_lines = []
|
||||
const lines = []
|
||||
const charts = []
|
||||
|
||||
const up = 'rgba(39, 157, 130, 100)'
|
||||
const down = 'rgba(200, 97, 100, 100)'
|
||||
|
||||
|
||||
function makeChart(width, height, div) {
|
||||
return LightweightCharts.createChart(div, {
|
||||
width: width,
|
||||
height: height,
|
||||
function makeChart(innerWidth, innerHeight) {
|
||||
let chart = {}
|
||||
chart.scale = {
|
||||
width: innerWidth,
|
||||
height: innerHeight
|
||||
}
|
||||
chart.div = document.createElement('div')
|
||||
chart.chart = LightweightCharts.createChart(chart.div, {
|
||||
width: window.innerWidth*innerWidth,
|
||||
height: window.innerHeight*innerHeight,
|
||||
layout: {
|
||||
textColor: '#d1d4dc',
|
||||
backgroundColor: '#000000',
|
||||
fontSize: 12,
|
||||
background: {
|
||||
color:'#000000',
|
||||
type: LightweightCharts.ColorType.Solid,
|
||||
},
|
||||
fontSize: 12
|
||||
},
|
||||
rightPriceScale: {
|
||||
scaleMargins: {
|
||||
top: 0.3,
|
||||
bottom: 0.25,
|
||||
},
|
||||
},
|
||||
timeScale: {
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
scaleMargins: {top: 0.3, bottom: 0.25},
|
||||
},
|
||||
timeScale: {timeVisible: true, secondsVisible: false},
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode.Normal,
|
||||
vertLine: {
|
||||
labelBackgroundColor: 'rgb(46, 46, 46)'
|
||||
},
|
||||
horzLine: {
|
||||
labelBackgroundColor: 'rgb(55, 55, 55)'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
vertLines: {
|
||||
color: 'rgba(29, 30, 38, 5)',
|
||||
},
|
||||
horzLines: {
|
||||
color: 'rgba(29, 30, 58, 5)',
|
||||
},
|
||||
},
|
||||
handleScroll: {
|
||||
vertTouchDrag: true,
|
||||
vertLines: {color: 'rgba(29, 30, 38, 5)'},
|
||||
horzLines: {color: 'rgba(29, 30, 58, 5)'},
|
||||
},
|
||||
handleScroll: {vertTouchDrag: true},
|
||||
})
|
||||
}
|
||||
function makeCandlestickSeries(chart){
|
||||
return chart.addCandlestickSeries({
|
||||
color: 'rgb(0, 120, 255)',
|
||||
upColor: up,
|
||||
borderUpColor: up,
|
||||
wickUpColor: up,
|
||||
|
||||
downColor: down,
|
||||
borderDownColor: down,
|
||||
wickDownColor: down,
|
||||
lineWidth: 2,
|
||||
chart.series = chart.chart.addCandlestickSeries({color: 'rgb(0, 120, 255)', upColor: up, borderUpColor: up, wickUpColor: up,
|
||||
downColor: down, borderDownColor: down, wickDownColor: down, lineWidth: 2,
|
||||
})
|
||||
}
|
||||
|
||||
function makeVolumeSeries(chart) {
|
||||
return chart.addHistogramSeries({
|
||||
chart.volumeSeries = chart.chart.addHistogramSeries({
|
||||
color: '#26a69a',
|
||||
priceFormat: {
|
||||
type: 'volume',
|
||||
},
|
||||
priceFormat: {type: 'volume'},
|
||||
priceScaleId: '',
|
||||
});
|
||||
})
|
||||
chart.legend = document.createElement('div')
|
||||
chart.legend.style.position = 'absolute'
|
||||
chart.legend.style.zIndex = 1000
|
||||
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
|
||||
chart.legend.style.top = '10px'
|
||||
chart.legend.style.left = '10px'
|
||||
chart.legend.style.fontFamily = 'Monaco'
|
||||
chart.legend.style.fontSize = '11px'
|
||||
chart.legend.style.color = 'rgb(191, 195, 203)'
|
||||
chart.div.appendChild(chart.legend)
|
||||
|
||||
chart.chart.priceScale('').applyOptions({
|
||||
scaleMargins: {top: 0.8, bottom: 0}
|
||||
});
|
||||
return chart
|
||||
}
|
||||
|
||||
const chartsDiv = document.createElement('div')
|
||||
|
||||
var chart = {}
|
||||
chart.scale = {
|
||||
width: __INNER_WIDTH__,
|
||||
height: __INNER_HEIGHT__
|
||||
}
|
||||
chart.chart = makeChart(window.innerWidth*chart.scale.width, window.innerHeight*chart.scale.height, chartsDiv)
|
||||
chart.series = makeCandlestickSeries(chart.chart)
|
||||
chart.volumeSeries = makeVolumeSeries(chart.chart)
|
||||
|
||||
|
||||
document.body.appendChild(chartsDiv)
|
||||
|
||||
chart.legend = document.createElement('div')
|
||||
chart.legend.style.position = 'absolute'
|
||||
chart.legend.style.zIndex = 1000
|
||||
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
|
||||
chart.legend.style.top = '10px'
|
||||
chart.legend.style.left = '10px'
|
||||
chart.legend.style.fontFamily = 'Monaco'
|
||||
chart.legend.style.fontSize = '11px'
|
||||
chart.legend.style.color = 'rgb(191, 195, 203)'
|
||||
document.body.appendChild(chart.legend)
|
||||
|
||||
chart.chart.priceScale('').applyOptions({
|
||||
scaleMargins: {
|
||||
top: 0.8,
|
||||
bottom: 0,
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function() {
|
||||
let width = window.innerWidth;
|
||||
let height = window.innerHeight;
|
||||
chart.chart.resize(width*chart.scale.width, height*chart.scale.height)
|
||||
|
||||
charts.forEach(function (subchart) {{
|
||||
subchart.chart.resize(width*subchart.scale.width, height*subchart.scale.height)
|
||||
}});
|
||||
|
||||
});
|
||||
|
||||
function legendItemFormat(num) {
|
||||
return num.toFixed(2).toString().padStart(8, ' ')
|
||||
return num.toFixed(2).toString().padStart(8, ' ')
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
HTML = f"""
|
||||
@ -774,7 +561,7 @@ HTML = f"""
|
||||
<html lang="">
|
||||
<head>
|
||||
<title>lightweight-charts-python</title>
|
||||
<script>{LWC_3_5_0}</script>
|
||||
<script>{LWC_4_0_1}</script>
|
||||
<meta name="viewport" content ="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {{
|
||||
@ -785,9 +572,8 @@ HTML = f"""
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="chart"></div>
|
||||
<script>
|
||||
{SCRIPT}
|
||||
</script>
|
||||
<script>
|
||||
{SCRIPT}
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,82 +0,0 @@
|
||||
from typing import Literal, Union
|
||||
from uuid import UUID
|
||||
import webview
|
||||
from multiprocessing import Queue
|
||||
|
||||
from lightweight_charts.js import LWC
|
||||
|
||||
_q = Queue()
|
||||
_result_q = Queue()
|
||||
|
||||
|
||||
class Webview(LWC):
|
||||
def __init__(self, chart):
|
||||
super().__init__(chart.volume_enabled, chart.inner_width, chart.inner_height)
|
||||
self.chart = chart
|
||||
self.started = False
|
||||
self._js_api_code = 'pywebview.api.onClick'
|
||||
|
||||
self.webview = webview.create_window('', html=self._html, on_top=chart.on_top, js_api=self._js_api,
|
||||
width=chart.width, height=chart.height, x=chart.x, y=chart.y)
|
||||
self.webview.events.loaded += self._on_js_load
|
||||
|
||||
def run_script(self, script): self.webview.evaluate_js(script)
|
||||
|
||||
def _on_js_load(self):
|
||||
self.loaded = True
|
||||
while len(self.js_queue) > 0:
|
||||
func, args, kwargs = self.js_queue[0]
|
||||
|
||||
if 'SUB' in func:
|
||||
c_id = args[0]
|
||||
args = args[1:]
|
||||
getattr(self._subcharts[c_id], func.replace('SUB', ''))(*args)
|
||||
else:
|
||||
getattr(self, func)(*args)
|
||||
del self.js_queue[0]
|
||||
|
||||
_loop(self.chart, controller=self)
|
||||
|
||||
def show(self):
|
||||
if self.loaded:
|
||||
self.webview.show()
|
||||
else:
|
||||
webview.start(debug=self.chart.debug)
|
||||
|
||||
def hide(self): self.webview.hide()
|
||||
|
||||
def exit(self):
|
||||
self.webview.destroy()
|
||||
del self
|
||||
|
||||
def create_line(self, color: str = 'rgba(214, 237, 255, 0.6)', width: int = 2):
|
||||
return super().create_line(color, width).id
|
||||
|
||||
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, UUID] = False):
|
||||
return super()._pywebview_subchart(volume_enabled, position, width, height, sync)
|
||||
|
||||
|
||||
def _loop(chart, controller=None):
|
||||
wv = Webview(chart) if not controller else controller
|
||||
chart._result_q.put(wv.id)
|
||||
while 1:
|
||||
obj = wv
|
||||
func, args = chart._q.get()
|
||||
|
||||
if 'SUB' in func:
|
||||
obj = obj._subcharts[args[0]]
|
||||
args = args[1:]
|
||||
func = func.replace('SUB', '')
|
||||
|
||||
try:
|
||||
result = getattr(obj, func)(*args)
|
||||
except KeyError as e:
|
||||
return
|
||||
if func == 'show':
|
||||
chart._exit.set()
|
||||
elif func == 'exit':
|
||||
chart._exit.set()
|
||||
|
||||
chart._result_q.put(result) if result is not None else None
|
||||
|
||||
@ -21,71 +21,56 @@ class ColorError(ValueError):
|
||||
return f'{self.msg}'
|
||||
|
||||
|
||||
class IDGen(list):
|
||||
def generate(self):
|
||||
var = ''.join(choices(ascii_lowercase, k=8))
|
||||
if var not in self:
|
||||
self.append(var)
|
||||
return var
|
||||
self.generate()
|
||||
|
||||
|
||||
def _valid_color(string):
|
||||
if string[:3] == 'rgb' or string[:4] == 'rgba' or string[0] == '#':
|
||||
return True
|
||||
raise ColorError('Colors must be in the format of either rgb, rgba or hex.')
|
||||
|
||||
|
||||
LINE_TYPE = Literal['solid', 'dotted', 'dashed', 'large_dashed', 'sparse_dotted']
|
||||
def _js_bool(b: bool): return 'true' if b is True else 'false' if b is False else None
|
||||
|
||||
POSITION = Literal['above', 'below', 'inside']
|
||||
|
||||
SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
|
||||
LINE_STYLE = Literal['solid', 'dotted', 'dashed', 'large_dashed', 'sparse_dotted']
|
||||
|
||||
MARKER_POSITION = Literal['above', 'below', 'inside']
|
||||
|
||||
MARKER_SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
|
||||
|
||||
CROSSHAIR_MODE = Literal['normal', 'magnet']
|
||||
|
||||
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
|
||||
|
||||
|
||||
def _line_type(lt: LINE_TYPE):
|
||||
return {
|
||||
'solid': 'Solid',
|
||||
'dotted': 'Dotted',
|
||||
'dashed': 'Dashed',
|
||||
'large_dashed': 'LargeDashed',
|
||||
'sparse_dotted': 'SparseDotted',
|
||||
None: None,
|
||||
}[lt]
|
||||
def _line_style(line: LINE_STYLE):
|
||||
js = 'LightweightCharts.LineStyle.'
|
||||
return js+line[:line.index('_')].title() + line[line.index('_') + 1:].title() if '_' in line else js+line.title()
|
||||
|
||||
|
||||
def _position(p: POSITION):
|
||||
def _crosshair_mode(mode: CROSSHAIR_MODE):
|
||||
return f'LightweightCharts.CrosshairMode.{mode.title()}' if mode else None
|
||||
|
||||
|
||||
def _price_scale_mode(mode: PRICE_SCALE_MODE):
|
||||
return f"LightweightCharts.PriceScaleMode.{'IndexedTo100' if mode == 'index100' else mode.title() if mode else None}"
|
||||
|
||||
|
||||
def _marker_shape(shape: MARKER_SHAPE):
|
||||
return shape[:shape.index('_')]+shape[shape.index('_')+1:].title() if '_' in shape else shape.title()
|
||||
|
||||
|
||||
def _marker_position(p: MARKER_POSITION):
|
||||
return {
|
||||
'above': 'aboveBar',
|
||||
'below': 'belowBar',
|
||||
'inside': 'inBar',
|
||||
None: None,
|
||||
}[p]
|
||||
|
||||
|
||||
def _shape(shape: SHAPE):
|
||||
return {
|
||||
'arrow_up': 'arrowUp',
|
||||
'arrow_down': 'arrowDown',
|
||||
'circle': 'Circle',
|
||||
'square': 'Square',
|
||||
None: None,
|
||||
}[shape]
|
||||
|
||||
|
||||
def _crosshair_mode(mode: CROSSHAIR_MODE): return mode.title() if mode else None
|
||||
|
||||
|
||||
def _js_bool(b: bool): return 'true' if b is True else 'false' if b is False else None
|
||||
|
||||
|
||||
def _price_scale_mode(mode: PRICE_SCALE_MODE):
|
||||
return 'IndexedTo100' if mode == 'index100' else mode.title() if mode else None
|
||||
|
||||
|
||||
class IDGen:
|
||||
def __init__(self):
|
||||
self.list = []
|
||||
|
||||
def generate(self):
|
||||
var = ''.join(choices(ascii_lowercase, k=8))
|
||||
if var in self.list:
|
||||
self.generate()
|
||||
else:
|
||||
self.list.append(var)
|
||||
return var
|
||||
|
||||
@ -21,24 +21,16 @@ class WxChart(LWC):
|
||||
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
|
||||
|
||||
self.webview.AddScriptMessageHandler('wx_msg')
|
||||
self._script_func = self.webview.RunScript
|
||||
self._js_api_code = 'window.wx_msg.postMessage'
|
||||
self.webview.Bind(wx.html2.EVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED, lambda e: self._js_api.onClick(eval(e.GetString())))
|
||||
|
||||
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 run_script(self, script): self.webview.RunScript(script)
|
||||
|
||||
def _on_js_load(self, e):
|
||||
self.loaded = True
|
||||
for func, args, kwargs in self.js_queue:
|
||||
if 'SUB' in func:
|
||||
c_id = args[0]
|
||||
args = args[1:]
|
||||
getattr(self._subcharts[c_id], func.replace('SUB', ''))(*args)
|
||||
else:
|
||||
getattr(self, func)(*args)
|
||||
def _on_js_load(self, e): super()._on_js_load()
|
||||
|
||||
def get_webview(self): return self.webview
|
||||
|
||||
@ -49,18 +41,13 @@ class QtChart(LWC):
|
||||
self.webview = QWebEngineView(widget)
|
||||
except NameError:
|
||||
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.')
|
||||
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height)
|
||||
|
||||
self._script_func = self.webview.page().runJavaScript
|
||||
|
||||
self.webview.loadFinished.connect(self._on_js_load)
|
||||
self.webview.page().setHtml(self._html)
|
||||
|
||||
def run_script(self, script): self.webview.page().runJavaScript(script)
|
||||
|
||||
def _on_js_load(self):
|
||||
self.loaded = True
|
||||
for func, args, kwargs in self.js_queue:
|
||||
getattr(super(), func)(*args, **kwargs)
|
||||
self._create_chart()
|
||||
|
||||
def get_webview(self): return self.webview
|
||||
|
||||
|
||||
Reference in New Issue
Block a user