NEW FEATURE: Trendlines, Rays and the Toolbox
- Added `trend_line` and `ray_line` to the Common Methods.
- Added the `toolbox` parameter to chart declaration. This allows horizontal lines, trend lines and rays to be drawn on the chart using hotkeys and buttons.
- cmd-Z will delete the last drawing.
- Drawings can be moved by clicking and dragging.
- Added the `render_drawings` parameter to `set`, which will keep and re-render the drawings displayed on the chart (useful for multiple timeframes!)
Horizontal Lines
- The `horizontal_line` method now returns a HorizontalLine object, containing the methods `update` and `delete`.
- Added the `interactive` parameter to `horizontal_line`, allowing for callbacks to be emitted to the `on_horizontal_line_move` callback method when the line is dragged to a new price (stop losses, limit orders, etc.).
Enhancements:
- added the `precision` method to the Common Methods, allowing for the number of decimal places shown on the price scale to be declared.
- Lines displayed on legends now have toggle switches, allowing for their visibility to be controlled directly within the chart window.
- when using `set`, the column names can now be capitalised, and the `date` column can be the index.
Changes:
- Merged the `title` method into the `price_line` method.
This commit is contained in:
26
README.md
26
README.md
@ -3,7 +3,7 @@
|
||||
# lightweight-charts-python
|
||||
|
||||
[](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://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
|
||||
|
||||
@ -24,10 +24,12 @@ ___
|
||||
1. Simple and easy to use.
|
||||
2. Blocking or non-blocking GUI.
|
||||
3. Streamlined for live data, with methods for updating directly from tick data.
|
||||
4. __Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
|
||||
5. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#callbacks) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more.
|
||||
6. Multi-Pane Charts using the [`SubChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart).
|
||||
4. Multi-Pane Charts using the [`SubChart`](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#subchart).
|
||||
5. The Toolbox, allowing for trendlines, rays and horizontal lines to be drawn directly onto charts.
|
||||
6. [Callbacks](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html#callbacks) allowing for timeframe (1min, 5min, 30min etc.) selectors, searching, and more.
|
||||
7. Direct integration of market data through [Polygon.io's](https://polygon.io) market data API.
|
||||
|
||||
__Supports:__ Jupyter Notebooks, PyQt, wxPython, Streamlit, and asyncio.
|
||||
___
|
||||
|
||||
### 1. Display data from a csv:
|
||||
@ -129,19 +131,20 @@ def calculate_sma(data: pd.DataFrame, period: int = 50):
|
||||
result = []
|
||||
for i in range(period - 1, len(data)):
|
||||
val = avg(data.iloc[i - period + 1:i])
|
||||
result.append({'time': data.iloc[i]['date'], 'value': val})
|
||||
result.append({'time': data.iloc[i]['date'], f'SMA {period}': val})
|
||||
return pd.DataFrame(result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
chart = Chart()
|
||||
chart.legend(visible=True)
|
||||
|
||||
df = pd.read_csv('ohlcv.csv')
|
||||
chart.set(df)
|
||||
|
||||
line = chart.create_line()
|
||||
sma_data = calculate_sma(df)
|
||||
line._set(sma_data)
|
||||
sma_data = calculate_sma(df, period=50)
|
||||
line.set(sma_data, name='SMA 50')
|
||||
|
||||
chart.show(block=True)
|
||||
|
||||
@ -219,11 +222,14 @@ class API:
|
||||
return
|
||||
self.chart.set(new_data)
|
||||
|
||||
async def on_horizontal_line_move(self, line_id, price):
|
||||
print(f'Horizontal line moved to: {price}')
|
||||
|
||||
|
||||
async def main():
|
||||
api = API()
|
||||
|
||||
chart = Chart(api=api, topbar=True, searchbox=True)
|
||||
chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True)
|
||||
chart.legend(True)
|
||||
|
||||
chart.topbar.textbox('corner', 'TSLA')
|
||||
@ -232,6 +238,8 @@ async def main():
|
||||
df = get_bar_data('TSLA', '5min')
|
||||
chart.set(df)
|
||||
|
||||
chart.horizontal_line(200, interactive=True)
|
||||
|
||||
await chart.show_async(block=True)
|
||||
|
||||
|
||||
@ -245,6 +253,8 @@ ___
|
||||
<div align="center">
|
||||
|
||||
[](https://lightweight-charts-python.readthedocs.io/en/latest/docs.html)
|
||||
|
||||
Inquiries: [shaders_worker_0e@icloud.com](mailto:shaders_worker_0e@icloud.com)
|
||||
___
|
||||
|
||||
_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.13.1'
|
||||
release = '1.0.14'
|
||||
|
||||
extensions = ["myst_parser"]
|
||||
|
||||
|
||||
@ -9,11 +9,11 @@
|
||||
___
|
||||
|
||||
## Common Methods
|
||||
These methods can be used within the [`Chart`](#chart), [`SubChart`](#subchart), [`QtChart`](#qtchart), [`WxChart`](#wxchart) and [`StreamlitChart`](#streamlitchart) objects.
|
||||
The methods below can be used within all chart objects.
|
||||
|
||||
___
|
||||
### `set`
|
||||
`data: pd.DataFrame`
|
||||
`data: pd.DataFrame` `render_drawings: bool`
|
||||
|
||||
Sets the initial data for the chart.
|
||||
|
||||
@ -21,7 +21,11 @@ The data must be given as a DataFrame, with the columns:
|
||||
|
||||
`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` or be the index, and the `volume` column can be omitted if volume is not enabled.
|
||||
|
||||
Column names are not case sensitive.
|
||||
|
||||
If `render_drawings` is `True`, any drawings made using the `toolbox` will be redrawn with the new data. This is designed to be used when switching to a different timeframe of the same symbol.
|
||||
|
||||
```{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.
|
||||
@ -53,7 +57,7 @@ As before, the `time` can also be named `date`, and the `volume` can be omitted
|
||||
The provided ticks do not need to be rounded to an interval (1 min, 5 min etc.), as the library handles this automatically.```````
|
||||
```
|
||||
|
||||
If `cumulative_volume` is used, the volume data given to this method will be added onto the latest bar of volume data.
|
||||
If `cumulative_volume` is used, the volume data given will be added onto the latest bar of volume data.
|
||||
___
|
||||
|
||||
### `create_line`
|
||||
@ -65,14 +69,29 @@ ___
|
||||
### `lines`
|
||||
`-> List[Line]`
|
||||
|
||||
Returns a list of all Line objects for the chart or subchart.
|
||||
Returns a list of all lines for the chart or subchart.
|
||||
___
|
||||
|
||||
### `trend_line`
|
||||
`start_time: str/datetime` | `start_value: float/int` | `end_time: str/datetime` | `end_value: float/int` | `color: str` | `width: int` | `-> Line`
|
||||
|
||||
Creates a trend line, drawn from the first point (`start_time`, `start_value`) to the last point (`end_time`, `end_value`).
|
||||
___
|
||||
|
||||
### `ray_line`
|
||||
`start_time: str/datetime` | `value: float/int` | `color: str` | `width: int` | `-> Line`
|
||||
|
||||
Creates a ray line, drawn from the first point (`start_time`, `value`) and onwards.
|
||||
___
|
||||
|
||||
### `marker`
|
||||
`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 id.
|
||||
|
||||
If the `time` parameter is not given, the marker will be placed at the latest bar.
|
||||
|
||||
When using multiple markers, they should be placed in chronological order or display bugs may be present.
|
||||
___
|
||||
|
||||
### `remove_marker`
|
||||
@ -88,9 +107,11 @@ chart.remove_marker(marker)
|
||||
___
|
||||
|
||||
### `horizontal_line`
|
||||
`price: float/int` | `color: str` | `width: int` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` | `text: str` | `axis_label_visible: bool`
|
||||
`price: float/int` | `color: str` | `width: int` | `style: 'solid'/'dotted'/'dashed'/'large_dashed'/'sparse_dotted'` | `text: str` | `axis_label_visible: bool` | `interactive: bool` | `-> HorizontalLine`
|
||||
|
||||
Places a horizontal line at the given price.
|
||||
Places a horizontal line at the given price, and returns a HorizontalLine object.
|
||||
|
||||
If `interactive` is set to `True`, this horizontal line can be edited on the chart. Upon its movement a callback will also be emitted to an `on_horizontal_line_move` method, containing its ID and price. The toolbox should be enabled during its usage. It is designed to be used to update an order (limit, stop, etc.) directly on the chart.
|
||||
___
|
||||
|
||||
### `remove_horizontal_line`
|
||||
@ -109,6 +130,12 @@ ___
|
||||
Clears the horizontal lines displayed on the data.
|
||||
___
|
||||
|
||||
### `precision`
|
||||
`precision: int`
|
||||
|
||||
Sets the precision of the chart based on the given number of decimal places.
|
||||
___
|
||||
|
||||
### `price_scale`
|
||||
`mode: 'normal'/'logarithmic'/'percentage'/'index100'` | `align_labels: bool` | `border_visible: bool` | `border_color: str` | `text_color: str` | `entire_text_only: bool` | `ticks_visible: bool` | `scale_margin_top: float` | `scale_margin_bottom: float`
|
||||
|
||||
@ -172,12 +199,6 @@ ___
|
||||
Overlays a watermark on top of the chart.
|
||||
___
|
||||
|
||||
### `title`
|
||||
`title: str`
|
||||
|
||||
Sets the title label for the chart.
|
||||
___
|
||||
|
||||
### `legend`
|
||||
`visible: bool` | `ohlc: bool` | `percent: bool` | `lines: bool` | `color: str` | `font_size: int` | `font_family: str`
|
||||
|
||||
@ -191,7 +212,7 @@ Shows a loading spinner on the chart, which can be used to visualise the loading
|
||||
___
|
||||
|
||||
### `price_line`
|
||||
`label_visible: bool` | `line_visible: bool`
|
||||
`label_visible: bool` | `line_visible: bool` | `title: str`
|
||||
|
||||
Configures the visibility of the last value price line and its label.
|
||||
___
|
||||
@ -212,7 +233,7 @@ Shows the hidden candles on the chart.
|
||||
___
|
||||
|
||||
### `polygon`
|
||||
Used to access Polygon.io's API (see [here](https://lightweight-charts-python.readthedocs.io/en/latest/polygon.html))
|
||||
Used to access Polygon.io's API (see [here](https://lightweight-charts-python.readthedocs.io/en/latest/polygon.html)).
|
||||
___
|
||||
|
||||
### `create_subchart`
|
||||
@ -227,7 +248,7 @@ Creates and returns a [SubChart](#subchart) object, placing it adjacent to the d
|
||||
`sync`: If given as `True`, the `SubChart`'s timescale and crosshair 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.
|
||||
`width` and `height` should be given as a number between 0 and 1.
|
||||
```
|
||||
|
||||
___
|
||||
@ -235,25 +256,29 @@ ___
|
||||
|
||||
## Chart
|
||||
`volume_enabled: bool` | `width: int` | `height: int` | `x: int` | `y: int` | `on_top: bool` | `maximize: bool` | `debug: bool` |
|
||||
`api: object` | `topbar: bool` | `searchbox: bool`
|
||||
`api: object` | `topbar: bool` | `searchbox: bool` | `toolbox: bool`
|
||||
|
||||
The main object used for the normal functionality of lightweight-charts-python, built on the pywebview library.
|
||||
|
||||
```{important}
|
||||
The `Chart` object should be defined within an `if __name__ == '__main__'` block.
|
||||
```
|
||||
___
|
||||
|
||||
### `show`
|
||||
`block: bool`
|
||||
|
||||
Shows the chart window. If `block` is enabled, the method will block code execution until the window is closed.
|
||||
Shows the chart window, blocking until the chart has loaded. If `block` is enabled, the method will block code execution until the window is closed.
|
||||
___
|
||||
|
||||
### `hide`
|
||||
|
||||
Hides the chart window, and can be later shown by calling `chart.show()`.
|
||||
Hides the chart window, which can be later shown by calling `chart.show()`.
|
||||
___
|
||||
|
||||
### `exit`
|
||||
|
||||
Exits and destroys the chart and window.
|
||||
Exits and destroys the chart window.
|
||||
|
||||
___
|
||||
|
||||
@ -280,7 +305,7 @@ if __name__ == '__main__':
|
||||
```
|
||||
|
||||
```{important}
|
||||
This method must be called after the chart window is open.
|
||||
This method should be called after the chart window has loaded.
|
||||
```
|
||||
|
||||
___
|
||||
@ -288,11 +313,10 @@ ___
|
||||
## 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:
|
||||
|
||||
[`title`](#title), [`marker`](#marker), [`horizontal_line`](#horizontal-line) [`hide_data`](#hide-data), [`show_data`](#show-data) and[`price_line`](#price-line) methods.
|
||||
[`title`](#title), [`marker`](#marker), [`horizontal_line`](#horizontal-line) [`hide_data`](#hide-data), [`show_data`](#show-data) and[`price_line`](#price-line).
|
||||
|
||||
```{important}
|
||||
The `line` object should only be accessed from the [`create_line`](#create-line) method of `Chart`.
|
||||
The `Line` object should only be accessed from the [`create_line`](#create-line) method of `Chart`.
|
||||
```
|
||||
___
|
||||
|
||||
@ -324,7 +348,28 @@ ___
|
||||
|
||||
### `delete`
|
||||
|
||||
Irreversibly deletes the line on the chart as well as the Line object.
|
||||
Irreversibly deletes the line.
|
||||
___
|
||||
|
||||
## HorizontalLine
|
||||
|
||||
The `HorizontalLine` object represents a `PriceLine` in Lightweight Charts.
|
||||
|
||||
```{important}
|
||||
The `HorizontalLine` object should only be accessed from the [`horizontal_line`](#horizontal-line) Common Method.
|
||||
```
|
||||
___
|
||||
|
||||
### `update`
|
||||
`price: float/int`
|
||||
|
||||
Updates the price of the horizontal line.
|
||||
|
||||
___
|
||||
|
||||
### `delete`
|
||||
|
||||
Irreversibly deletes the horizontal line.
|
||||
___
|
||||
|
||||
## SubChart
|
||||
@ -513,6 +558,19 @@ if __name__ == '__main__':
|
||||
```
|
||||
___
|
||||
|
||||
## Toolbox
|
||||
The Toolbox allows for trendlines, ray lines and horizontal lines to be drawn and edited directly on the chart.
|
||||
|
||||
It can be used within any Chart object, and is enabled by setting the `toolbox` parameter to `True` upon Chart declaration.
|
||||
|
||||
The following hotkeys can also be used when the Toolbox is enabled:
|
||||
* Alt+T: Trendline
|
||||
* Alt+H: Horizontal Line
|
||||
* Alt+R: Ray Line
|
||||
* Meta+Z: Undo
|
||||
|
||||
___
|
||||
|
||||
## QtChart
|
||||
`widget: QWidget` | `volume_enabled: bool`
|
||||
|
||||
|
||||
@ -8,19 +8,20 @@ def calculate_sma(data: pd.DataFrame, period: int = 50):
|
||||
result = []
|
||||
for i in range(period - 1, len(data)):
|
||||
val = avg(data.iloc[i - period + 1:i])
|
||||
result.append({'time': data.iloc[i]['date'], 'value': val})
|
||||
result.append({'time': data.iloc[i]['date'], f'SMA {period}': val})
|
||||
return pd.DataFrame(result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
chart = Chart()
|
||||
chart = Chart(debug=True)
|
||||
chart.legend(visible=True)
|
||||
|
||||
df = pd.read_csv('ohlcv.csv')
|
||||
chart.set(df)
|
||||
|
||||
line = chart.create_line()
|
||||
sma_data = calculate_sma(df)
|
||||
line.set(sma_data)
|
||||
sma_data = calculate_sma(df, period=50)
|
||||
line.set(sma_data, name='SMA 50')
|
||||
|
||||
chart.show(block=True)
|
||||
|
||||
@ -13,7 +13,7 @@ def get_bar_data(symbol, timeframe):
|
||||
|
||||
class API:
|
||||
def __init__(self):
|
||||
self.chart = None # Changes after each callback.
|
||||
self.chart: Chart = None # Changes after each callback.
|
||||
|
||||
async def on_search(self, searched_string): # Called when the user searches.
|
||||
new_data = get_bar_data(searched_string, self.chart.topbar['timeframe'].value)
|
||||
@ -26,13 +26,16 @@ class API:
|
||||
new_data = get_bar_data(self.chart.topbar['symbol'].value, self.chart.topbar['timeframe'].value)
|
||||
if new_data.empty:
|
||||
return
|
||||
self.chart.set(new_data)
|
||||
self.chart.set(new_data, render_drawings=True)
|
||||
|
||||
async def on_horizontal_line_move(self, line_id, price):
|
||||
print(f'Horizontal line moved to: {price}')
|
||||
|
||||
|
||||
async def main():
|
||||
api = API()
|
||||
|
||||
chart = Chart(api=api, topbar=True, searchbox=True)
|
||||
chart = Chart(api=api, topbar=True, searchbox=True, toolbox=True)
|
||||
chart.legend(True)
|
||||
|
||||
chart.topbar.textbox('symbol', 'TSLA')
|
||||
@ -41,6 +44,8 @@ async def main():
|
||||
df = get_bar_data('TSLA', '5min')
|
||||
chart.set(df)
|
||||
|
||||
chart.horizontal_line(200, interactive=True)
|
||||
|
||||
await chart.show_async(block=True)
|
||||
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ from lightweight_charts.util import LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, C
|
||||
|
||||
JS = {}
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
for file in ('pkg', 'funcs', 'callback'):
|
||||
for file in ('pkg', 'funcs', 'callback', 'toolbox'):
|
||||
with open(os.path.join(current_dir, 'js', f'{file}.js'), 'r', encoding='utf-8') as f:
|
||||
JS[file] = f.read()
|
||||
|
||||
@ -59,10 +59,15 @@ class SeriesCommon:
|
||||
}}
|
||||
''')
|
||||
|
||||
def _df_datetime_format(self, df: pd.DataFrame):
|
||||
def _df_datetime_format(self, df: pd.DataFrame, exclude_lowercase=None):
|
||||
df = df.copy()
|
||||
df.columns = df.columns.str.lower()
|
||||
if exclude_lowercase:
|
||||
df[exclude_lowercase] = df[exclude_lowercase.lower()]
|
||||
if 'date' in df.columns:
|
||||
df = df.rename(columns={'date': 'time'})
|
||||
elif 'time' not in df.columns:
|
||||
df['time'] = df.index
|
||||
self._set_interval(df)
|
||||
df['time'] = self._datetime_format(df['time'])
|
||||
return df
|
||||
@ -76,11 +81,14 @@ class SeriesCommon:
|
||||
|
||||
def _datetime_format(self, arg: Union[pd.Series, str]):
|
||||
arg = pd.to_datetime(arg)
|
||||
if self._interval != timedelta(days=1):
|
||||
arg = arg.astype('int64') // 10 ** 9 if isinstance(arg, pd.Series) else arg.timestamp()
|
||||
arg = self._interval.total_seconds() * (arg // self._interval.total_seconds())
|
||||
if self._interval < timedelta(days=1):
|
||||
if isinstance(arg, pd.Series):
|
||||
arg = arg.astype('int64') // 10 ** 9
|
||||
else:
|
||||
arg = self._interval.total_seconds() * (arg.timestamp() // self._interval.total_seconds())
|
||||
else:
|
||||
arg = arg.dt.strftime('%Y-%m-%d') if isinstance(arg, pd.Series) else arg.strftime('%Y-%m-%d')
|
||||
|
||||
return arg
|
||||
|
||||
def marker(self, time: datetime = None, position: MARKER_POSITION = 'below', shape: MARKER_SHAPE = 'arrow_up',
|
||||
@ -123,28 +131,21 @@ class SeriesCommon:
|
||||
}}
|
||||
}});''')
|
||||
|
||||
def horizontal_line(self, price: Union[float, int], color: str = 'rgb(122, 146, 202)', width: int = 1,
|
||||
style: LINE_STYLE = 'solid', text: str = '', axis_label_visible=True):
|
||||
def horizontal_line(self, price: Union[float, int], color: str = 'rgb(122, 146, 202)', width: int = 2,
|
||||
style: LINE_STYLE = 'solid', text: str = '', axis_label_visible: bool = True, interactive: bool = False) -> 'HorizontalLine':
|
||||
"""
|
||||
Creates a horizontal line at the given price.\n
|
||||
"""
|
||||
line_id = self._rand.generate()
|
||||
self.run_script(f"""
|
||||
makeHorizontalLine({self.id}, '{line_id}', {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
|
||||
""")
|
||||
return line_id
|
||||
return HorizontalLine(self, price, color, width, style, text, axis_label_visible, interactive)
|
||||
|
||||
def remove_horizontal_line(self, price: Union[float, int]):
|
||||
def remove_horizontal_line(self, price: Union[float, int] = None):
|
||||
"""
|
||||
Removes a horizontal line at the given price.
|
||||
"""
|
||||
self.run_script(f'''
|
||||
{self.id}.horizontal_lines.forEach(function (line) {{
|
||||
if ({price} === line.price) {{
|
||||
{self.id}.series.removePriceLine(line.line);
|
||||
{self.id}.horizontal_lines.splice({self.id}.horizontal_lines.indexOf(line), 1)
|
||||
}}
|
||||
}});''')
|
||||
if ({price} === line.price) line.deleteLine()
|
||||
}})''')
|
||||
|
||||
def clear_markers(self):
|
||||
"""
|
||||
@ -161,13 +162,22 @@ class SeriesCommon:
|
||||
{self.id}.horizontal_lines = [];
|
||||
''')
|
||||
|
||||
def title(self, title: str): self.run_script(f'{self.id}.series.applyOptions({{title: "{title}"}})')
|
||||
|
||||
def price_line(self, label_visible: bool = True, line_visible: bool = True):
|
||||
def price_line(self, label_visible: bool = True, line_visible: bool = True, title: str = ''):
|
||||
self.run_script(f'''
|
||||
{self.id}.series.applyOptions({{
|
||||
lastValueVisible: {_js_bool(label_visible)},
|
||||
priceLineVisible: {_js_bool(line_visible)},
|
||||
title: '{title}',
|
||||
}})''')
|
||||
|
||||
def precision(self, precision: int):
|
||||
"""
|
||||
Sets the precision and minMove.\n
|
||||
:param precision: The number of decimal places.
|
||||
"""
|
||||
self.run_script(f'''
|
||||
{self.id}.series.applyOptions({{
|
||||
priceFormat: {{precision: {precision}, minMove: {1 / (10 ** precision)}}}
|
||||
}})''')
|
||||
|
||||
def hide_data(self): self._toggle_data(False)
|
||||
@ -180,47 +190,79 @@ class SeriesCommon:
|
||||
{f'{self.id}.volumeSeries.applyOptions({{visible: {_js_bool(arg)}}})' if hasattr(self, 'volume_enabled') and self.volume_enabled else ''}
|
||||
''')
|
||||
|
||||
class HorizontalLine:
|
||||
def __init__(self, chart, price, color, width, style, text, axis_label_visible, interactive):
|
||||
self._chart = chart
|
||||
self.id = self._chart._rand.generate()
|
||||
self._chart.run_script(f'''
|
||||
{self.id} = new HorizontalLine({self._chart.id}, '{self.id}', {price}, '{color}', {width}, {_line_style(style)}, {_js_bool(axis_label_visible)}, '{text}')
|
||||
''')
|
||||
if not interactive: return
|
||||
self._chart.run_script(f'if ("toolBox" in {self._chart.id}) {self._chart.id}.toolBox.drawings.push({self.id})')
|
||||
|
||||
def update(self, price):
|
||||
"""
|
||||
Moves the horizontal line to the given price.
|
||||
"""
|
||||
self._chart.run_script(f'{self.id}.updatePrice({price})')
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Irreversibly deletes the horizontal line.
|
||||
"""
|
||||
self._chart.run_script(f'{self.id}.deleteLine()')
|
||||
del self
|
||||
|
||||
|
||||
class Line(SeriesCommon):
|
||||
def __init__(self, chart, color, width, price_line, price_label):
|
||||
def __init__(self, chart, color, width, price_line, price_label, crosshair_marker=True):
|
||||
self.color = color
|
||||
self.name = ''
|
||||
self._chart = chart
|
||||
self._rand = chart._rand
|
||||
self.id = f'window.{self._rand.generate()}'
|
||||
self.id = self._rand.generate()
|
||||
self.run_script = self._chart.run_script
|
||||
self.run_script(f'''
|
||||
{self.id} = {{
|
||||
series: {self._chart.id}.chart.addLineSeries({{
|
||||
color: '{color}',
|
||||
lineWidth: {width},
|
||||
lastValueVisible: {_js_bool(price_label)},
|
||||
priceLineVisible: {_js_bool(price_line)},
|
||||
{"""autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),""" if self._chart._scale_candles_only else ''}
|
||||
}}),
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
}}''')
|
||||
{self.id} = {{
|
||||
series: {self._chart.id}.chart.addLineSeries({{
|
||||
color: '{color}',
|
||||
lineWidth: {width},
|
||||
lastValueVisible: {_js_bool(price_label)},
|
||||
priceLineVisible: {_js_bool(price_line)},
|
||||
crosshairMarkerVisible: {_js_bool(crosshair_marker)},
|
||||
{"""autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),""" if self._chart._scale_candles_only else ''}
|
||||
}}),
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
name: '',
|
||||
color: '{color}',
|
||||
}}
|
||||
{self._chart.id}.lines.push({self.id})
|
||||
if ('legend' in {self._chart.id}) {{
|
||||
{self._chart.id}.legend.makeLines({self._chart.id})
|
||||
}}
|
||||
''')
|
||||
|
||||
def set(self, data: pd.DataFrame, name=None):
|
||||
|
||||
def set(self, data: pd.DataFrame, name=''):
|
||||
"""
|
||||
Sets the line data.\n
|
||||
:param data: If the name parameter is not used, the columns should be named: date/time, value.
|
||||
:param name: The column of the DataFrame to use as the line value. When used, the Line will be named after this column.
|
||||
"""
|
||||
df = self._df_datetime_format(data)
|
||||
df = self._df_datetime_format(data, exclude_lowercase=name)
|
||||
if name:
|
||||
if name not in data:
|
||||
raise NameError(f'No column named "{name}".')
|
||||
self.name = name
|
||||
df = df.rename(columns={name: 'value'})
|
||||
self._last_bar = df.iloc[-1]
|
||||
self.run_script(f'{self.id}.series.setData({df.to_dict("records")})')
|
||||
self.run_script(f'{self.id}.series.setData({df.to_dict("records")}); {self.id}.name = "{name}"')
|
||||
|
||||
def update(self, series: pd.Series):
|
||||
"""
|
||||
@ -231,11 +273,23 @@ class Line(SeriesCommon):
|
||||
self._last_bar = series
|
||||
self.run_script(f'{self.id}.series.update({series.to_dict()})')
|
||||
|
||||
def _set_trend(self, start_time, start_value, end_time, end_value, ray=False):
|
||||
def time_format(time_val):
|
||||
time_val = self._chart._datetime_format(time_val)
|
||||
return f"'{time_val}'" if isinstance(time_val, str) else time_val
|
||||
self.run_script(f'''
|
||||
let logical
|
||||
if ({_js_bool(ray)}) logical = {self._chart.id}.chart.timeScale().getVisibleLogicalRange()
|
||||
{self.id}.series.setData(calculateTrendLine({time_format(start_time)}, {start_value}, {time_format(end_time)}, {end_value},
|
||||
{self._chart._interval.total_seconds()*1000}, {self._chart.id}, {_js_bool(ray)}))
|
||||
if (logical) {self._chart.id}.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
''')
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
Irreversibly deletes the line, as well as the object that contains the line.
|
||||
"""
|
||||
self._chart._lines.remove(self)
|
||||
self._chart._lines.remove(self) if self in self._chart._lines else None
|
||||
self.run_script(f'''
|
||||
{self._chart.id}.chart.removeSeries({self.id}.series)
|
||||
delete {self.id}
|
||||
@ -253,7 +307,7 @@ class TextWidget(Widget):
|
||||
def __init__(self, topbar, initial_text):
|
||||
super().__init__(topbar)
|
||||
self.value = initial_text
|
||||
self.id = f"window.{self._chart._rand.generate()}"
|
||||
self.id = self._chart._rand.generate()
|
||||
self._chart.run_script(f'''{self.id} = makeTextBoxWidget({self._chart.id}, "{initial_text}")''')
|
||||
|
||||
def set(self, string):
|
||||
@ -267,9 +321,9 @@ class SwitcherWidget(Widget):
|
||||
self.value = default
|
||||
self._method = method.__name__
|
||||
self._chart.run_script(f'''
|
||||
makeSwitcher({self._chart.id}, {list(options)}, '{default}', {self._chart._js_api_code}, '{method.__name__}',
|
||||
makeSwitcher({self._chart.id}, {list(options)}, '{default}', '{method.__name__}',
|
||||
'{topbar.active_background_color}', '{topbar.active_text_color}', '{topbar.text_color}', '{topbar.hover_color}')
|
||||
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width}, (window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
|
||||
reSize({self._chart.id})
|
||||
''')
|
||||
|
||||
|
||||
@ -279,8 +333,7 @@ class TopBar:
|
||||
self._widgets: Dict[str, Widget] = {}
|
||||
self._chart.run_script(f'''
|
||||
makeTopBar({self._chart.id})
|
||||
{self._chart.id}.chart.resize(window.innerWidth*{self._chart._inner_width},
|
||||
(window.innerHeight*{self._chart._inner_height})-{self._chart.id}.topBar.offsetHeight)
|
||||
reSize({self._chart.id})
|
||||
''')
|
||||
self.active_background_color = 'rgba(0, 122, 255, 0.7)'
|
||||
self.active_text_color = 'rgb(240, 240, 240)'
|
||||
@ -302,15 +355,17 @@ class TopBar:
|
||||
|
||||
class LWC(SeriesCommon):
|
||||
def __init__(self, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False,
|
||||
scale_candles_only: bool = False):
|
||||
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False,
|
||||
_js_api_code: str = '""', autosize=True, _run_script=None):
|
||||
self.volume_enabled = volume_enabled
|
||||
self._scale_candles_only = scale_candles_only
|
||||
self._inner_width = inner_width
|
||||
self._inner_height = inner_height
|
||||
self._dynamic_loading = dynamic_loading
|
||||
|
||||
if _run_script:
|
||||
self.run_script = _run_script
|
||||
self._rand = IDGen()
|
||||
self.id = f'window.{self._rand.generate()}'
|
||||
self.id = self._rand.generate()
|
||||
self._position = 'left'
|
||||
self.loaded = False
|
||||
self._html = HTML
|
||||
@ -321,7 +376,7 @@ class LWC(SeriesCommon):
|
||||
self._interval = None
|
||||
self._charts = {self.id: self}
|
||||
self._lines = []
|
||||
self._js_api_code = None
|
||||
self._js_api_code = _js_api_code
|
||||
self._return_q = None
|
||||
|
||||
self._background_color = '#000000'
|
||||
@ -331,6 +386,21 @@ class LWC(SeriesCommon):
|
||||
from lightweight_charts.polygon import PolygonAPI
|
||||
self.polygon: PolygonAPI = PolygonAPI(self)
|
||||
|
||||
self.run_script(f'''
|
||||
{self.id} = makeChart({self._js_api_code}, {self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)})
|
||||
{self.id}.id = '{self.id}'
|
||||
{self.id}.wrapper.style.float = "{self._position}"
|
||||
''')
|
||||
if toolbox:
|
||||
self.run_script(JS['toolbox'])
|
||||
self.run_script(f'{self.id}.toolBox = new ToolBox({self.id})')
|
||||
if not topbar and not searchbox:
|
||||
return
|
||||
self.run_script(JS['callback'])
|
||||
self.run_script(f'makeSpinner({self.id})')
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self.run_script(f'{self.id}.search = makeSearchBox({self.id})') if searchbox else None
|
||||
|
||||
def _on_js_load(self):
|
||||
if self.loaded:
|
||||
return
|
||||
@ -338,16 +408,6 @@ class LWC(SeriesCommon):
|
||||
[self.run_script(script) for script in self._scripts]
|
||||
[self.run_script(script) for script in self._final_scripts]
|
||||
|
||||
def _create_chart(self, autosize=True):
|
||||
self.run_script(f'''
|
||||
{self.id} = makeChart({self._inner_width}, {self._inner_height}, autoSize={_js_bool(autosize)})
|
||||
{self.id}.id = '{self.id}'
|
||||
{self.id}.wrapper.style.float = "{self._position}"
|
||||
''')
|
||||
|
||||
def _make_search_box(self):
|
||||
self.run_script(f'{self.id}.search = makeSearchBox({self.id}, {self._js_api_code})')
|
||||
|
||||
def run_script(self, script, run_last=False):
|
||||
"""
|
||||
For advanced users; evaluates JavaScript within the Webview.
|
||||
@ -357,14 +417,17 @@ class LWC(SeriesCommon):
|
||||
return
|
||||
self._scripts.append(script) if not run_last else self._final_scripts.append(script)
|
||||
|
||||
def set(self, df: pd.DataFrame):
|
||||
def set(self, df: pd.DataFrame = None, render_drawings=False):
|
||||
"""
|
||||
Sets the initial data for the chart.\n
|
||||
:param df: columns: date/time, open, high, low, close, volume (if volume enabled).
|
||||
:param render_drawings: Re-renders any drawings made through the toolbox. Otherwise, they will be deleted.
|
||||
"""
|
||||
if df.empty:
|
||||
|
||||
if df.empty or df is None:
|
||||
self.run_script(f'{self.id}.series.setData([])')
|
||||
self.run_script(f'{self.id}.volumeSeries.setData([])')
|
||||
# self.run_script(f"if ('toolBox' in {self.id}) {self.id}.toolBox.{render_or_clear}()")
|
||||
return
|
||||
bars = self._df_datetime_format(df)
|
||||
self._last_bar = bars.iloc[-1]
|
||||
@ -416,6 +479,7 @@ class LWC(SeriesCommon):
|
||||
}}, 50);
|
||||
}});
|
||||
''') if self._dynamic_loading else self.run_script(f'{self.id}.candleData = {bars}; {self.id}.series.setData({self.id}.candleData)')
|
||||
self.run_script(f"if ('toolBox' in {self.id}) {self.id}.toolBox.{'clearDrawings' if not render_drawings else 'renderDrawings'}()")
|
||||
|
||||
def fit(self):
|
||||
"""
|
||||
@ -446,7 +510,6 @@ class LWC(SeriesCommon):
|
||||
let barsInfo = {self.id}.series.barsInLogicalRange(logicalRange);
|
||||
|
||||
if ({self.id}.candleData[{self.id}.candleData.length-1].time === {bar['time']}) {{
|
||||
|
||||
{self.id}.shownData[{self.id}.shownData.length-1] = {bar}
|
||||
{self.id}.candleData[{self.id}.candleData.length-1] = {bar}
|
||||
}}
|
||||
@ -461,7 +524,15 @@ class LWC(SeriesCommon):
|
||||
{self.id}.candleData.push({bar})
|
||||
}}
|
||||
{self.id}.series.update({self.id}.shownData[{self.id}.shownData.length-1])
|
||||
''') if self._dynamic_loading else self.run_script(f'{self.id}.series.update({bar})')
|
||||
''') if self._dynamic_loading else self.run_script(f'''
|
||||
if (chartTimeToDate({self.id}.candleData[{self.id}.candleData.length-1].time).getTime() === chartTimeToDate({bar['time']}).getTime()) {{
|
||||
{self.id}.candleData[{self.id}.candleData.length-1] = {bar}
|
||||
}}
|
||||
else {{
|
||||
{self.id}.candleData.push({bar})
|
||||
}}
|
||||
{self.id}.series.update({bar})
|
||||
''')
|
||||
|
||||
def update_from_tick(self, series, cumulative_volume=False):
|
||||
"""
|
||||
@ -504,6 +575,16 @@ class LWC(SeriesCommon):
|
||||
"""
|
||||
return self._lines.copy()
|
||||
|
||||
def trend_line(self, start_time, start_value, end_time, end_value, color: str = '#1E80F0', width: int = 2) -> Line:
|
||||
line = Line(self, color, width, price_line=False, price_label=False, crosshair_marker=False)
|
||||
line._set_trend(start_time, start_value, end_time, end_value, ray=False)
|
||||
return line
|
||||
|
||||
def ray_line(self, start_time, value, color: str = '#1E80F0', width: int = 2) -> Line:
|
||||
line = Line(self, color, width, price_line=False, price_label=False, crosshair_marker=False)
|
||||
line._set_trend(start_time, value, start_time, value, ray=True)
|
||||
return line
|
||||
|
||||
def price_scale(self, mode: PRICE_SCALE_MODE = 'normal', align_labels: bool = True, border_visible: bool = False,
|
||||
border_color: str = None, text_color: str = None, entire_text_only: bool = False,
|
||||
ticks_visible: bool = False, scale_margin_top: float = 0.2, scale_margin_bottom: float = 0.2):
|
||||
@ -657,48 +738,16 @@ class LWC(SeriesCommon):
|
||||
}}
|
||||
}})''')
|
||||
|
||||
def legend(self, visible: bool = False, ohlc: bool = True, percent: bool = True, lines: bool = True, color: str = None,
|
||||
font_size: int = None, font_family: str = None):
|
||||
def legend(self, visible: bool = False, ohlc: bool = True, percent: bool = True, lines: bool = True, color: str = 'rgb(191, 195, 203)',
|
||||
font_size: int = 11, font_family: str = 'Monaco'):
|
||||
"""
|
||||
Configures the legend of the chart.
|
||||
"""
|
||||
if not visible:
|
||||
return
|
||||
lines_code = ''
|
||||
for i, line in enumerate(self._lines):
|
||||
lines_code += f'''finalString += `<span style="color: {line.color};">▨</span>{f' {line.name}'} :
|
||||
${{legendItemFormat(param.seriesData.get({line.id}.series).value)}}<br>`;'''
|
||||
|
||||
self.run_script(f'''
|
||||
{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 ''}
|
||||
|
||||
legendItemFormat = (num) => num.toFixed(2).toString().padStart(8, ' ')
|
||||
|
||||
{self.id}.chart.subscribeCrosshairMove((param) => {{
|
||||
if (param.time){{
|
||||
let data = param.seriesData.get({self.id}.series);
|
||||
let finalString = '<span style="line-height: 1.8;">'
|
||||
if (data) {{
|
||||
let ohlc = `O ${{legendItemFormat(data.open)}}
|
||||
| H ${{legendItemFormat(data.high)}}
|
||||
| L ${{legendItemFormat(data.low)}}
|
||||
| C ${{legendItemFormat(data.close)}} `
|
||||
let percentMove = ((data.close-data.open)/data.open)*100
|
||||
let percent = `| ${{percentMove >= 0 ? '+' : ''}}${{percentMove.toFixed(2)}} %`
|
||||
|
||||
{'finalString += ohlc' if ohlc else ''}
|
||||
{'finalString += percent' if percent else ''}
|
||||
{'finalString += "<br>"' if ohlc or percent else ''}
|
||||
}}
|
||||
{lines_code if lines else ''}
|
||||
{self.id}.legend.innerHTML = finalString+'</span>'
|
||||
}}
|
||||
else {{
|
||||
{self.id}.legend.innerHTML = ''
|
||||
}}
|
||||
}});''')
|
||||
{self.id}.legend = new Legend({self.id}, {_js_bool(ohlc)}, {_js_bool(percent)}, {_js_bool(lines)}, '{color}', {font_size}, '{font_family}')
|
||||
''')
|
||||
|
||||
def spinner(self, visible): self.run_script(f"{self.id}.spinner.style.display = '{'block' if visible else 'none'}'")
|
||||
|
||||
@ -721,30 +770,24 @@ class LWC(SeriesCommon):
|
||||
return b64decode(serial_data.split(',')[1])
|
||||
|
||||
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,
|
||||
topbar: bool = False, searchbox: bool = False):
|
||||
subchart = SubChart(self, volume_enabled, position, width, height, sync, topbar, searchbox)
|
||||
width: float = 0.5, height: float = 0.5, sync: Union[bool, str] = False, dynamic_loading: bool = False,
|
||||
scale_candles_only: bool = False, topbar: bool = False, searchbox: bool = False, toolbox: bool = False):
|
||||
subchart = SubChart(self, volume_enabled, position, width, height, sync, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox)
|
||||
self._charts[subchart.id] = subchart
|
||||
return subchart
|
||||
|
||||
|
||||
class SubChart(LWC):
|
||||
def __init__(self, parent, volume_enabled, position, width, height, sync, topbar, searchbox):
|
||||
super().__init__(volume_enabled, width, height)
|
||||
def __init__(self, parent, volume_enabled, position, width, height, sync, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox):
|
||||
self._chart = parent._chart if isinstance(parent, SubChart) else parent
|
||||
super().__init__(volume_enabled, width, height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox,
|
||||
self._chart._js_api_code, _run_script=self._chart.run_script)
|
||||
self._parent = parent
|
||||
self._position = position
|
||||
self._rand = self._chart._rand
|
||||
self._js_api_code = self._chart._js_api_code
|
||||
self._return_q = self._chart._return_q
|
||||
self.run_script = self._chart.run_script
|
||||
self._charts = self._chart._charts
|
||||
self.id = f'window.{self._rand.generate()}'
|
||||
self.polygon = self._chart.polygon._subchart(self)
|
||||
|
||||
self._create_chart()
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
if not sync:
|
||||
return
|
||||
sync_parent_id = self._parent.id if isinstance(sync, bool) else sync
|
||||
@ -757,6 +800,3 @@ class SubChart(LWC):
|
||||
self.run_script(f'''
|
||||
{self.id}.chart.timeScale().setVisibleLogicalRange({sync_parent_id}.chart.timeScale().getVisibleLogicalRange())
|
||||
''', run_last=True)
|
||||
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import asyncio
|
||||
import multiprocessing as mp
|
||||
import webview
|
||||
|
||||
from lightweight_charts.abstract import LWC, JS, TopBar
|
||||
from lightweight_charts.abstract import LWC
|
||||
|
||||
|
||||
class CallbackAPI:
|
||||
@ -48,25 +48,17 @@ class PyWV:
|
||||
class Chart(LWC):
|
||||
def __init__(self, volume_enabled: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||
on_top: bool = False, maximize: bool = False, debug: bool = False,
|
||||
api: object = None, topbar: bool = False, searchbox: bool = False,
|
||||
api: object = None, topbar: bool = False, searchbox: bool = False, toolbox: bool = False,
|
||||
inner_width: float = 1.0, inner_height: float = 1.0, dynamic_loading: bool = False, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading, scale_candles_only)
|
||||
super().__init__(volume_enabled, inner_width, inner_height, dynamic_loading, scale_candles_only, topbar, searchbox, toolbox, 'pywebview.api.callback')
|
||||
self._q, self._emit_q, self._return_q = (mp.Queue() for _ in range(3))
|
||||
self._exit, self._loaded = mp.Event(), mp.Event()
|
||||
self._script_func = self._q.put
|
||||
self._api = api
|
||||
self._js_api_code = 'pywebview.api.callback'
|
||||
self._process = mp.Process(target=PyWV, args=(self._q, self._exit, self._loaded, self._html,
|
||||
width, height, x, y, on_top, maximize, debug,
|
||||
self._emit_q, self._return_q), daemon=True)
|
||||
self._process.start()
|
||||
self._create_chart()
|
||||
if not topbar and not searchbox:
|
||||
return
|
||||
self.run_script(JS['callback'])
|
||||
self.run_script(f'makeSpinner({self.id})')
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
|
||||
def show(self, block: bool = False):
|
||||
"""
|
||||
@ -101,7 +93,7 @@ class Chart(LWC):
|
||||
widget.value = arg
|
||||
await getattr(self._api, key)()
|
||||
else:
|
||||
await getattr(self._api, key)(arg)
|
||||
await getattr(self._api, key)(*arg.split(';;;'))
|
||||
continue
|
||||
value = self.polygon._q.get()
|
||||
func, args = value[0], value[1:]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
function makeSearchBox(chart, callbackFunction) {
|
||||
function makeSearchBox(chart) {
|
||||
let searchWindow = document.createElement('div')
|
||||
searchWindow.style.position = 'absolute'
|
||||
searchWindow.style.top = '0'
|
||||
@ -49,39 +49,28 @@ function makeSearchBox(chart, callbackFunction) {
|
||||
chart.wrapper.addEventListener('mouseout', (event) => {
|
||||
selectedChart = false
|
||||
})
|
||||
document.addEventListener('keydown', function(event) {
|
||||
if (!selectedChart) {return}
|
||||
if (event.altKey && event.code === 'KeyH') {
|
||||
let price = chart.series.coordinateToPrice(yPrice)
|
||||
|
||||
let colorList = [
|
||||
'rgba(228, 0, 16, 0.7)',
|
||||
'rgba(255, 133, 34, 0.7)',
|
||||
'rgba(164, 59, 176, 0.7)',
|
||||
'rgba(129, 59, 102, 0.7)',
|
||||
'rgba(91, 20, 248, 0.7)',
|
||||
'rgba(32, 86, 249, 0.7)',
|
||||
]
|
||||
let color = colorList[Math.floor(Math.random()*colorList.length)]
|
||||
|
||||
makeHorizontalLine(chart, 0, price, color, 2, LightweightCharts.LineStyle.Solid, true, '')
|
||||
}
|
||||
chart.commandFunctions.push((event) => {
|
||||
if (searchWindow.style.display === 'none') {
|
||||
if (/^[a-zA-Z0-9]$/.test(event.key)) {
|
||||
searchWindow.style.display = 'flex';
|
||||
sBox.focus();
|
||||
return true
|
||||
}
|
||||
else return false
|
||||
}
|
||||
else if (event.key === 'Enter') {
|
||||
callbackFunction(`on_search__${chart.id}__${sBox.value}`)
|
||||
chart.callbackFunction(`on_search__${chart.id}__${sBox.value}`)
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
return true
|
||||
}
|
||||
else if (event.key === 'Escape') {
|
||||
searchWindow.style.display = 'none'
|
||||
sBox.value = ''
|
||||
return true
|
||||
}
|
||||
});
|
||||
else return false
|
||||
})
|
||||
sBox.addEventListener('input', function() {
|
||||
sBox.value = sBox.value.toUpperCase();
|
||||
});
|
||||
@ -115,7 +104,7 @@ function makeSpinner(chart) {
|
||||
animateSpinner();
|
||||
}
|
||||
|
||||
function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
|
||||
function makeSwitcher(chart, items, activeItem, callbackName, activeBackgroundColor, activeColor, inactiveColor, hoverColor) {
|
||||
let switcherElement = document.createElement('div');
|
||||
switcherElement.style.margin = '4px 14px'
|
||||
switcherElement.style.zIndex = '1000'
|
||||
@ -155,7 +144,7 @@ function makeSwitcher(chart, items, activeItem, callbackFunction, callbackName,
|
||||
element.style.color = items[index] === item ? 'activeColor' : inactiveColor
|
||||
});
|
||||
activeItem = item;
|
||||
callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
||||
chart.callbackFunction(`${callbackName}__${chart.id}__${item}`);
|
||||
}
|
||||
chart.topBar.appendChild(switcherElement)
|
||||
makeSeperator(chart.topBar)
|
||||
|
||||
@ -1,14 +1,17 @@
|
||||
function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
function makeChart(callbackFunction, innerWidth, innerHeight, autoSize=true) {
|
||||
let chart = {
|
||||
markers: [],
|
||||
horizontal_lines: [],
|
||||
lines: [],
|
||||
wrapper: document.createElement('div'),
|
||||
div: document.createElement('div'),
|
||||
legend: document.createElement('div'),
|
||||
scale: {
|
||||
width: innerWidth,
|
||||
height: innerHeight
|
||||
height: innerHeight,
|
||||
},
|
||||
callbackFunction: callbackFunction,
|
||||
candleData: [],
|
||||
commandFunctions: []
|
||||
}
|
||||
chart.chart = LightweightCharts.createChart(chart.div, {
|
||||
width: window.innerWidth*innerWidth,
|
||||
@ -57,15 +60,6 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
chart.volumeSeries.priceScale().applyOptions({
|
||||
scaleMargins: {top: 0.8, bottom: 0},
|
||||
});
|
||||
chart.legend.style.position = 'absolute'
|
||||
chart.legend.style.zIndex = '1000'
|
||||
chart.legend.style.width = `${(chart.scale.width*100)-8}vw`
|
||||
chart.legend.style.top = '10px'
|
||||
chart.legend.style.left = '10px'
|
||||
chart.legend.style.fontFamily = 'Monaco'
|
||||
chart.legend.style.fontSize = '11px'
|
||||
chart.legend.style.color = 'rgb(191, 195, 203)'
|
||||
|
||||
chart.wrapper.style.width = `${100*innerWidth}%`
|
||||
chart.wrapper.style.height = `${100*innerHeight}%`
|
||||
chart.wrapper.style.display = 'flex'
|
||||
@ -73,35 +67,190 @@ function makeChart(innerWidth, innerHeight, autoSize=true) {
|
||||
|
||||
chart.div.style.position = 'relative'
|
||||
chart.div.style.display = 'flex'
|
||||
|
||||
chart.div.appendChild(chart.legend)
|
||||
chart.wrapper.appendChild(chart.div)
|
||||
document.getElementById('wrapper').append(chart.wrapper)
|
||||
|
||||
if (!autoSize) return chart
|
||||
document.addEventListener('keydown', (event) => {
|
||||
for (let i=0; i<chart.commandFunctions.length; i++) {
|
||||
if (chart.commandFunctions[i](event)) break
|
||||
}
|
||||
})
|
||||
|
||||
let topBarOffset = 0
|
||||
window.addEventListener('resize', function() {
|
||||
if ('topBar' in chart) topBarOffset = chart.topBar.offsetHeight
|
||||
chart.chart.resize(window.innerWidth*innerWidth, (window.innerHeight*innerHeight)-topBarOffset)
|
||||
});
|
||||
if (!autoSize) return chart
|
||||
window.addEventListener('resize', () => reSize(chart))
|
||||
return chart
|
||||
}
|
||||
function makeHorizontalLine(chart, lineId, price, color, width, style, axisLabelVisible, text) {
|
||||
let priceLine = {
|
||||
price: price,
|
||||
color: color,
|
||||
lineWidth: width,
|
||||
lineStyle: style,
|
||||
axisLabelVisible: axisLabelVisible,
|
||||
title: text,
|
||||
};
|
||||
let line = {
|
||||
line: chart.series.createPriceLine(priceLine),
|
||||
price: price,
|
||||
id: lineId,
|
||||
};
|
||||
chart.horizontal_lines.push(line)
|
||||
|
||||
function reSize(chart) {
|
||||
let topBarOffset = 'topBar' in chart ? chart.topBar.offsetHeight : 0
|
||||
chart.chart.resize(window.innerWidth*chart.scale.width, (window.innerHeight*chart.scale.height)-topBarOffset)
|
||||
}
|
||||
|
||||
if (!window.HorizontalLine) {
|
||||
class HorizontalLine {
|
||||
constructor(chart, lineId, price, color, width, style, axisLabelVisible, text) {
|
||||
this.updatePrice = this.updatePrice.bind(this)
|
||||
this.deleteLine = this.deleteLine.bind(this)
|
||||
this.chart = chart
|
||||
this.price = price
|
||||
this.id = lineId
|
||||
this.priceLine = {
|
||||
price: this.price,
|
||||
color: color,
|
||||
lineWidth: width,
|
||||
lineStyle: style,
|
||||
axisLabelVisible: axisLabelVisible,
|
||||
title: text,
|
||||
}
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
this.chart.horizontal_lines.push(this)
|
||||
}
|
||||
|
||||
updatePrice(price) {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.price = price
|
||||
this.priceLine.price = this.price
|
||||
this.line = this.chart.series.createPriceLine(this.priceLine)
|
||||
}
|
||||
|
||||
deleteLine() {
|
||||
this.chart.series.removePriceLine(this.line)
|
||||
this.chart.horizontal_lines.splice(this.chart.horizontal_lines.indexOf(this))
|
||||
delete this
|
||||
}
|
||||
}
|
||||
|
||||
window.HorizontalLine = HorizontalLine
|
||||
|
||||
class Legend {
|
||||
constructor(chart, ohlcEnabled = true, percentEnabled = true, linesEnabled = true,
|
||||
color = 'rgb(191, 195, 203)', fontSize = '11', fontFamily = 'Monaco') {
|
||||
this.div = document.createElement('div')
|
||||
this.div.style.position = 'absolute'
|
||||
this.div.style.zIndex = '3000'
|
||||
this.div.style.top = '10px'
|
||||
this.div.style.left = '10px'
|
||||
this.div.style.display = 'flex'
|
||||
this.div.style.flexDirection = 'column'
|
||||
this.div.style.width = `${(chart.scale.width * 100) - 8}vw`
|
||||
this.div.style.color = color
|
||||
this.div.style.fontSize = fontSize + 'px'
|
||||
this.div.style.fontFamily = fontFamily
|
||||
this.candle = document.createElement('div')
|
||||
|
||||
this.div.appendChild(this.candle)
|
||||
chart.div.appendChild(this.div)
|
||||
|
||||
this.color = color
|
||||
|
||||
this.linesEnabled = linesEnabled
|
||||
this.makeLines(chart)
|
||||
|
||||
let legendItemFormat = (num) => num.toFixed(2).toString().padStart(8, ' ')
|
||||
|
||||
chart.chart.subscribeCrosshairMove((param) => {
|
||||
if (param.time) {
|
||||
let data = param.seriesData.get(chart.series);
|
||||
let finalString = '<span style="line-height: 1.8;">'
|
||||
if (data) {
|
||||
this.candle.style.color = ''
|
||||
let ohlc = `O ${legendItemFormat(data.open)}
|
||||
| H ${legendItemFormat(data.high)}
|
||||
| L ${legendItemFormat(data.low)}
|
||||
| C ${legendItemFormat(data.close)} `
|
||||
let percentMove = ((data.close - data.open) / data.open) * 100
|
||||
let percent = `| ${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
|
||||
|
||||
finalString += ohlcEnabled ? ohlc : ''
|
||||
finalString += percentEnabled ? percent : ''
|
||||
|
||||
}
|
||||
this.candle.innerHTML = finalString + '</span>'
|
||||
this.lines.forEach((line) => {
|
||||
if (!param.seriesData.get(line.line.series)) return
|
||||
let price = legendItemFormat(param.seriesData.get(line.line.series).value)
|
||||
line.div.innerHTML = `<span style="color: ${line.line.color};">▨</span> ${line.line.name} : ${price}`
|
||||
})
|
||||
|
||||
} else {
|
||||
this.candle.style.color = 'transparent'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
makeLines(chart) {
|
||||
this.lines = []
|
||||
if (this.linesEnabled) {
|
||||
chart.lines.forEach((line) => {
|
||||
this.lines.push(this.makeLineRow(line))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
makeLineRow(line) {
|
||||
let openEye = `
|
||||
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 21.998437 12 C 21.998437 12 18.998437 18 12 18 C 5.001562 18 2.001562 12 2.001562 12 C 2.001562 12 5.001562 6 12 6 C 18.998437 6 21.998437 12 21.998437 12 Z M 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
|
||||
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 15 12 C 15 13.654687 13.654687 15 12 15 C 10.345312 15 9 13.654687 9 12 C 9 10.345312 10.345312 9 12 9 C 13.654687 9 15 10.345312 15 12 Z M 15 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>\`
|
||||
`
|
||||
let closedEye = `
|
||||
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${this.color};stroke-opacity:1;stroke-miterlimit:4;" d="M 20.001562 9 C 20.001562 9 19.678125 9.665625 18.998437 10.514062 M 12 14.001562 C 10.392187 14.001562 9.046875 13.589062 7.95 12.998437 M 12 14.001562 C 13.607812 14.001562 14.953125 13.589062 16.05 12.998437 M 12 14.001562 L 12 17.498437 M 3.998437 9 C 3.998437 9 4.354687 9.735937 5.104687 10.645312 M 7.95 12.998437 L 5.001562 15.998437 M 7.95 12.998437 C 6.689062 12.328125 5.751562 11.423437 5.104687 10.645312 M 16.05 12.998437 L 18.501562 15.998437 M 16.05 12.998437 C 17.38125 12.290625 18.351562 11.320312 18.998437 10.514062 M 5.104687 10.645312 L 2.001562 12 M 18.998437 10.514062 L 21.998437 12 " transform="matrix(0.833333,0,0,0.833333,0,0)"/>
|
||||
`
|
||||
|
||||
let row = document.createElement('div')
|
||||
row.style.display = 'flex'
|
||||
row.style.alignItems = 'center'
|
||||
let div = document.createElement('div')
|
||||
let toggle = document.createElement('div')
|
||||
toggle.style.borderRadius = '4px'
|
||||
toggle.style.marginLeft = '10px'
|
||||
|
||||
|
||||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("width", "22");
|
||||
svg.setAttribute("height", "16");
|
||||
|
||||
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.innerHTML = openEye
|
||||
|
||||
let on = true
|
||||
toggle.addEventListener('click', (event) => {
|
||||
if (on) {
|
||||
on = false
|
||||
group.innerHTML = closedEye
|
||||
line.series.applyOptions({
|
||||
visible: false
|
||||
})
|
||||
} else {
|
||||
on = true
|
||||
line.series.applyOptions({
|
||||
visible: true
|
||||
})
|
||||
group.innerHTML = openEye
|
||||
}
|
||||
})
|
||||
toggle.addEventListener('mouseover', (event) => {
|
||||
document.body.style.cursor = 'pointer'
|
||||
toggle.style.backgroundColor = 'rgba(50, 50, 50, 0.5)'
|
||||
})
|
||||
toggle.addEventListener('mouseleave', (event) => {
|
||||
document.body.style.cursor = 'default'
|
||||
toggle.style.backgroundColor = 'transparent'
|
||||
})
|
||||
svg.appendChild(group)
|
||||
toggle.appendChild(svg);
|
||||
row.appendChild(div)
|
||||
row.appendChild(toggle)
|
||||
this.div.appendChild(row)
|
||||
return {
|
||||
div: div,
|
||||
row: row,
|
||||
toggle: toggle,
|
||||
line: line,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.Legend = Legend
|
||||
}
|
||||
function syncCrosshairs(childChart, parentChart) {
|
||||
function crosshairHandler (e, thisChart, otherChart, otherHandler) {
|
||||
@ -140,3 +289,108 @@ function syncCrosshairs(childChart, parentChart) {
|
||||
parentChart.subscribeCrosshairMove(parentCrosshairHandler)
|
||||
childChart.subscribeCrosshairMove(childCrosshairHandler)
|
||||
}
|
||||
|
||||
function chartTimeToDate(stampOrBusiness) {
|
||||
if (typeof stampOrBusiness === 'number') {
|
||||
stampOrBusiness = new Date(stampOrBusiness*1000)
|
||||
}
|
||||
else if (typeof stampOrBusiness === 'string') {
|
||||
let [year, month, day] = stampOrBusiness.split('-').map(Number)
|
||||
stampOrBusiness = new Date(Date.UTC(year, month-1, day))
|
||||
}
|
||||
else {
|
||||
stampOrBusiness = new Date(Date.UTC(stampOrBusiness.year, stampOrBusiness.month - 1, stampOrBusiness.day))
|
||||
}
|
||||
return stampOrBusiness
|
||||
}
|
||||
|
||||
function dateToChartTime(date, interval) {
|
||||
if (interval >= 24*60*60*1000) {
|
||||
return {day: date.getUTCDate(), month: date.getUTCMonth()+1, year: date.getUTCFullYear()}
|
||||
}
|
||||
return Math.floor(date.getTime()/1000)
|
||||
}
|
||||
|
||||
function calculateTrendLine(startDate, startValue, endDate, endValue, interval, chart, ray=false) {
|
||||
let reversed = false
|
||||
if (chartTimeToDate(endDate).getTime() < chartTimeToDate(startDate).getTime()) {
|
||||
reversed = true;
|
||||
[startDate, endDate] = [endDate, startDate];
|
||||
}
|
||||
let startIndex
|
||||
if (chartTimeToDate(startDate).getTime() < chartTimeToDate(chart.candleData[0].time).getTime()) {
|
||||
startIndex = 0
|
||||
}
|
||||
else {
|
||||
startIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(startDate).getTime())
|
||||
}
|
||||
|
||||
if (startIndex === -1) {
|
||||
return []
|
||||
}
|
||||
let endIndex
|
||||
if (ray) {
|
||||
endIndex = chart.candleData.length+1000
|
||||
startValue = endValue
|
||||
}
|
||||
else {
|
||||
endIndex = chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(endDate).getTime())
|
||||
if (endIndex === -1) {
|
||||
let barsBetween = (chartTimeToDate(endDate)-chartTimeToDate(chart.candleData[chart.candleData.length-1].time))/interval
|
||||
endIndex = chart.candleData.length-1+barsBetween
|
||||
}
|
||||
}
|
||||
|
||||
let numBars = endIndex-startIndex
|
||||
const rate_of_change = (endValue - startValue) / numBars;
|
||||
const trendData = [];
|
||||
let currentDate = null
|
||||
let iPastData = 0
|
||||
for (let i = 0; i <= numBars; i++) {
|
||||
if (chart.candleData[startIndex+i]) {
|
||||
currentDate = chart.candleData[startIndex+i].time
|
||||
}
|
||||
else {
|
||||
iPastData ++
|
||||
currentDate = dateToChartTime(new Date(chartTimeToDate(chart.candleData[chart.candleData.length-1].time).getTime()+(iPastData*interval)), interval)
|
||||
|
||||
}
|
||||
|
||||
const currentValue = reversed ? startValue + rate_of_change * (numBars - i) : startValue + rate_of_change * i;
|
||||
trendData.push({ time: currentDate, value: currentValue });
|
||||
}
|
||||
return trendData;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
let customMenu = document.createElement('div')
|
||||
customMenu.style.position = 'absolute'
|
||||
customMenu.style.zIndex = '10000'
|
||||
customMenu.style.background = 'rgba(25, 25, 25, 0.7)'
|
||||
customMenu.style.color = 'lightgrey'
|
||||
customMenu.style.display = 'none'
|
||||
customMenu.style.borderRadius = '5px'
|
||||
customMenu.style.padding = '5px 10px'
|
||||
document.body.appendChild(customMenu)
|
||||
|
||||
function menuItem(text) {
|
||||
let elem = document.createElement('div')
|
||||
elem.innerText = text
|
||||
customMenu.appendChild(elem)
|
||||
}
|
||||
menuItem('Delete drawings')
|
||||
menuItem('Hide all indicators')
|
||||
menuItem('Save Chart State')
|
||||
|
||||
let closeMenu = (event) => {if (!customMenu.contains(event.target)) customMenu.style.display = 'none';}
|
||||
document.addEventListener('contextmenu', function (event) {
|
||||
event.preventDefault(); // Prevent default right-click menu
|
||||
customMenu.style.left = event.clientX + 'px';
|
||||
customMenu.style.top = event.clientY + 'px';
|
||||
customMenu.style.display = 'block';
|
||||
document.removeEventListener('click', closeMenu)
|
||||
document.addEventListener('click', closeMenu)
|
||||
});
|
||||
|
||||
*/
|
||||
|
||||
484
lightweight_charts/js/toolbox.js
Normal file
484
lightweight_charts/js/toolbox.js
Normal file
@ -0,0 +1,484 @@
|
||||
if (!window.ToolBox) {
|
||||
class ToolBox {
|
||||
constructor(chart) {
|
||||
this.onTrendSelect = this.onTrendSelect.bind(this)
|
||||
this.onHorzSelect = this.onHorzSelect.bind(this)
|
||||
this.onRaySelect = this.onRaySelect.bind(this)
|
||||
|
||||
this.chart = chart
|
||||
this.drawings = []
|
||||
this.chart.cursor = 'default'
|
||||
this.makingDrawing = false
|
||||
|
||||
this.interval = 24 * 60 * 60 * 1000
|
||||
this.activeBackgroundColor = 'rgba(0, 122, 255, 0.7)'
|
||||
this.activeIconColor = 'rgb(240, 240, 240)'
|
||||
this.iconColor = 'lightgrey'
|
||||
this.backgroundColor = 'transparent'
|
||||
this.hoverColor = 'rgba(60, 60, 60, 0.7)'
|
||||
|
||||
this.elem = this.makeToolBox()
|
||||
this.subscribeHoverMove()
|
||||
}
|
||||
|
||||
makeToolBox() {
|
||||
let toolBoxElem = document.createElement('div')
|
||||
toolBoxElem.style.position = 'absolute'
|
||||
toolBoxElem.style.zIndex = '2000'
|
||||
toolBoxElem.style.display = 'flex'
|
||||
toolBoxElem.style.alignItems = 'center'
|
||||
toolBoxElem.style.top = '25%'
|
||||
toolBoxElem.style.borderRight = '2px solid #3C434C'
|
||||
toolBoxElem.style.borderTop = '2px solid #3C434C'
|
||||
toolBoxElem.style.borderBottom = '2px solid #3C434C'
|
||||
toolBoxElem.style.borderTopRightRadius = '4px'
|
||||
toolBoxElem.style.borderBottomRightRadius = '4px'
|
||||
toolBoxElem.style.backgroundColor = 'rgba(25, 27, 30, 0.5)'
|
||||
toolBoxElem.style.flexDirection = 'column'
|
||||
|
||||
this.chart.activeIcon = null
|
||||
|
||||
let trend = this.makeToolBoxElement(this.onTrendSelect, 'KeyT', `<rect x="3.84" y="13.67" transform="matrix(0.7071 -0.7071 0.7071 0.7071 -5.9847 14.4482)" width="21.21" height="1.56"/><path d="M23,3.17L20.17,6L23,8.83L25.83,6L23,3.17z M23,7.41L21.59,6L23,4.59L24.41,6L23,7.41z"/><path d="M6,20.17L3.17,23L6,25.83L8.83,23L6,20.17z M6,24.41L4.59,23L6,21.59L7.41,23L6,24.41z"/>`)
|
||||
let horz = this.makeToolBoxElement(this.onHorzSelect, 'KeyH', `<rect x="4" y="14" width="9" height="1"/><rect x="16" y="14" width="9" height="1"/><path d="M11.67,14.5l2.83,2.83l2.83-2.83l-2.83-2.83L11.67,14.5z M15.91,14.5l-1.41,1.41l-1.41-1.41l1.41-1.41L15.91,14.5z"/>`)
|
||||
let ray = this.makeToolBoxElement(this.onRaySelect, 'KeyR', `<rect x="8" y="14" width="17" height="1"/><path d="M3.67,14.5l2.83,2.83l2.83-2.83L6.5,11.67L3.67,14.5z M7.91,14.5L6.5,15.91L5.09,14.5l1.41-1.41L7.91,14.5z"/>`)
|
||||
//let testB = this.makeToolBoxElement(this.onTrendSelect, `<rect x="8" y="6" width="12" height="1"/><rect x="9" y="22" width="11" height="1"/><path d="M3.67,6.5L6.5,9.33L9.33,6.5L6.5,3.67L3.67,6.5z M7.91,6.5L6.5,7.91L5.09,6.5L6.5,5.09L7.91,6.5z"/><path d="M19.67,6.5l2.83,2.83l2.83-2.83L22.5,3.67L19.67,6.5z M23.91,6.5L22.5,7.91L21.09,6.5l1.41-1.41L23.91,6.5z"/><path d="M19.67,22.5l2.83,2.83l2.83-2.83l-2.83-2.83L19.67,22.5z M23.91,22.5l-1.41,1.41l-1.41-1.41l1.41-1.41L23.91,22.5z"/><path d="M3.67,22.5l2.83,2.83l2.83-2.83L6.5,19.67L3.67,22.5z M7.91,22.5L6.5,23.91L5.09,22.5l1.41-1.41L7.91,22.5z"/><rect x="22" y="9" width="1" height="11"/><rect x="6" y="9" width="1" height="11"/>`)
|
||||
toolBoxElem.appendChild(trend)
|
||||
toolBoxElem.appendChild(horz)
|
||||
toolBoxElem.appendChild(ray)
|
||||
//toolBoxElem.appendChild(testB)
|
||||
|
||||
this.chart.div.append(toolBoxElem)
|
||||
|
||||
let commandZHandler = (toDelete) => {
|
||||
if (!toDelete) return
|
||||
if ('price' in toDelete) {
|
||||
if (toDelete.id !== 'toolBox') return commandZHandler(this.drawings[this.drawings.indexOf(toDelete) - 1])
|
||||
this.chart.series.removePriceLine(toDelete.line)
|
||||
} else {
|
||||
let logical
|
||||
if (toDelete.ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
this.chart.chart.removeSeries(toDelete.line);
|
||||
if (toDelete.ray) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
}
|
||||
this.drawings.splice(this.drawings.length - 1)
|
||||
}
|
||||
this.chart.commandFunctions.push((event) => {
|
||||
if (event.metaKey && event.code === 'KeyZ') {
|
||||
commandZHandler(this.drawings[this.drawings.length - 1])
|
||||
return true
|
||||
}
|
||||
});
|
||||
|
||||
return toolBoxElem
|
||||
}
|
||||
|
||||
makeToolBoxElement(action, keyCmd, paths) {
|
||||
let elem = document.createElement('div')
|
||||
elem.style.margin = '3px'
|
||||
elem.style.borderRadius = '4px'
|
||||
elem.style.display = 'flex'
|
||||
|
||||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.setAttribute("width", "29");
|
||||
svg.setAttribute("height", "29");
|
||||
|
||||
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||
group.innerHTML = paths
|
||||
group.setAttribute("fill", this.iconColor)
|
||||
|
||||
svg.appendChild(group)
|
||||
elem.appendChild(svg);
|
||||
|
||||
elem.addEventListener('mouseenter', () => {
|
||||
elem.style.backgroundColor = elem === this.chart.activeIcon ? this.activeBackgroundColor : this.hoverColor
|
||||
document.body.style.cursor = 'pointer'
|
||||
})
|
||||
elem.addEventListener('mouseleave', () => {
|
||||
elem.style.backgroundColor = elem === this.chart.activeIcon ? this.activeBackgroundColor : this.backgroundColor
|
||||
document.body.style.cursor = this.chart.cursor
|
||||
})
|
||||
elem.addEventListener('click', () => {
|
||||
if (this.chart.activeIcon) {
|
||||
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
|
||||
group.setAttribute("fill", this.iconColor)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
action(false)
|
||||
}
|
||||
if (this.chart.activeIcon === elem) {
|
||||
this.chart.activeIcon = null
|
||||
return
|
||||
}
|
||||
this.chart.activeIcon = elem
|
||||
group.setAttribute("fill", this.activeIconColor)
|
||||
elem.style.backgroundColor = this.activeBackgroundColor
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
action(true)
|
||||
})
|
||||
this.chart.commandFunctions.push((event) => {
|
||||
if (event.altKey && event.code === keyCmd) {
|
||||
if (this.chart.activeIcon) {
|
||||
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
|
||||
group.setAttribute("fill", this.iconColor)
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
action(false)
|
||||
}
|
||||
this.chart.activeIcon = elem
|
||||
group.setAttribute("fill", this.activeIconColor)
|
||||
elem.style.backgroundColor = this.activeBackgroundColor
|
||||
document.body.style.cursor = 'crosshair'
|
||||
this.chart.cursor = 'crosshair'
|
||||
action(true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
return elem
|
||||
}
|
||||
|
||||
onTrendSelect(toggle, ray = false) {
|
||||
let trendLine = {
|
||||
line: null,
|
||||
markers: null,
|
||||
data: null,
|
||||
from: null,
|
||||
to: null,
|
||||
ray: ray,
|
||||
}
|
||||
let firstTime = null
|
||||
let firstPrice = null
|
||||
let currentTime = null
|
||||
|
||||
|
||||
if (!toggle) {
|
||||
this.chart.chart.unsubscribeClick(this.chart.clickHandler)
|
||||
return
|
||||
}
|
||||
let crosshairHandlerTrend = (param) => {
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
|
||||
if (!this.makingDrawing) return
|
||||
|
||||
let logical
|
||||
let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time
|
||||
currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
|
||||
if (!currentTime) {
|
||||
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
|
||||
logical = barsToMove <= 0 ? null : this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
|
||||
} else if (chartTimeToDate(lastCandleTime).getTime() <= chartTimeToDate(currentTime).getTime()) {
|
||||
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
}
|
||||
|
||||
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
|
||||
|
||||
if (!currentTime) return this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
trendLine.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart, ray)
|
||||
trendLine.from = trendLine.data[0].time
|
||||
trendLine.to = trendLine.data[trendLine.data.length - 1].time
|
||||
|
||||
|
||||
if (ray) logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
|
||||
|
||||
trendLine.line.setData(trendLine.data)
|
||||
|
||||
if (logical) {
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
setTimeout(() => {
|
||||
this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
}, 1)
|
||||
setTimeout(() => {
|
||||
this.chart.chart.applyOptions({handleScroll: false})
|
||||
}, 50)
|
||||
}
|
||||
if (!ray) {
|
||||
trendLine.markers = [
|
||||
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
|
||||
{time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
|
||||
]
|
||||
trendLine.line.setMarkers(trendLine.markers)
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
}, 10);
|
||||
}
|
||||
|
||||
this.chart.clickHandler = (param) => {
|
||||
if (!this.makingDrawing) {
|
||||
this.makingDrawing = true
|
||||
trendLine.line = this.chart.chart.addLineSeries({
|
||||
lineWidth: 2,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
autoscaleInfoProvider: () => ({
|
||||
priceRange: {
|
||||
minValue: 1_000_000_000,
|
||||
maxValue: 0,
|
||||
},
|
||||
}),
|
||||
})
|
||||
firstPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
firstTime = !ray ? this.chart.chart.timeScale().coordinateToTime(param.point.x) : this.chart.candleData[this.chart.candleData.length - 1].time
|
||||
this.chart.chart.applyOptions({
|
||||
handleScroll: false
|
||||
})
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
} else {
|
||||
this.chart.chart.applyOptions({
|
||||
handleScroll: true
|
||||
})
|
||||
this.makingDrawing = false
|
||||
trendLine.line.setMarkers([])
|
||||
this.drawings.push(trendLine)
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
this.chart.chart.unsubscribeClick(this.chart.clickHandler)
|
||||
document.body.style.cursor = 'default'
|
||||
this.chart.cursor = 'default'
|
||||
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
|
||||
this.chart.activeIcon = null
|
||||
}
|
||||
}
|
||||
this.chart.chart.subscribeClick(this.chart.clickHandler)
|
||||
}
|
||||
|
||||
onHorzSelect(toggle) {
|
||||
let clickHandlerHorz = (param) => {
|
||||
let price = this.chart.series.coordinateToPrice(param.point.y)
|
||||
let lineStyle = LightweightCharts.LineStyle.Solid
|
||||
let line = new HorizontalLine(this.chart, 'toolBox', price, null, 2, lineStyle, true)
|
||||
this.drawings.push(line)
|
||||
this.chart.chart.unsubscribeClick(clickHandlerHorz)
|
||||
document.body.style.cursor = 'default'
|
||||
this.chart.cursor = 'default'
|
||||
this.chart.activeIcon.style.backgroundColor = this.backgroundColor
|
||||
this.chart.activeIcon = null
|
||||
}
|
||||
this.chart.chart.subscribeClick(clickHandlerHorz)
|
||||
}
|
||||
|
||||
onRaySelect(toggle) {
|
||||
this.onTrendSelect(toggle, true)
|
||||
}
|
||||
|
||||
subscribeHoverMove() {
|
||||
let hoveringOver = null
|
||||
let x, y
|
||||
let hoverOver = (param) => {
|
||||
if (!param.point || this.makingDrawing) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
|
||||
x = param.point.x
|
||||
y = param.point.y
|
||||
|
||||
this.drawings.forEach((drawing) => {
|
||||
let boundaryConditional
|
||||
let horizontal = false
|
||||
|
||||
if ('price' in drawing) {
|
||||
horizontal = true
|
||||
let priceCoordinate = this.chart.series.priceToCoordinate(drawing.price)
|
||||
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6
|
||||
} else {
|
||||
let trendData = param.seriesData.get(drawing.line);
|
||||
if (!trendData) return
|
||||
let priceCoordinate = this.chart.series.priceToCoordinate(trendData.value)
|
||||
let timeCoordinate = this.chart.chart.timeScale().timeToCoordinate(trendData.time)
|
||||
boundaryConditional = Math.abs(priceCoordinate - param.point.y) < 6 && Math.abs(timeCoordinate - param.point.x) < 6
|
||||
}
|
||||
|
||||
if (boundaryConditional) {
|
||||
if (hoveringOver === drawing) return
|
||||
|
||||
if (!horizontal && !drawing.ray) drawing.line.setMarkers(drawing.markers)
|
||||
document.body.style.cursor = 'pointer'
|
||||
document.addEventListener('mousedown', checkForClick)
|
||||
document.addEventListener('mouseup', checkForRelease)
|
||||
hoveringOver = drawing
|
||||
} else if (hoveringOver === drawing) {
|
||||
if (!horizontal && !drawing.ray) drawing.line.setMarkers([])
|
||||
document.body.style.cursor = this.chart.cursor
|
||||
hoveringOver = null
|
||||
}
|
||||
})
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
}
|
||||
let originalIndex
|
||||
let originalTime
|
||||
let originalPrice
|
||||
let mouseDown = false
|
||||
let clickedEnd = false
|
||||
let checkForClick = (event) => {
|
||||
//if (!hoveringOver) return
|
||||
mouseDown = true
|
||||
document.body.style.cursor = 'grabbing'
|
||||
this.chart.chart.applyOptions({
|
||||
handleScroll: false
|
||||
})
|
||||
|
||||
this.chart.chart.unsubscribeCrosshairMove(hoverOver)
|
||||
|
||||
// let [x, y] = [event.clientX, event.clientY]
|
||||
// if ('topBar' in this.chart) y = y - this.chart.topBar.offsetHeight
|
||||
if ('price' in hoveringOver) {
|
||||
originalPrice = hoveringOver.price
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
|
||||
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[0].time) - x) < 4 && !hoveringOver.ray) {
|
||||
clickedEnd = 'first'
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
} else if (Math.abs(this.chart.chart.timeScale().timeToCoordinate(hoveringOver.data[hoveringOver.data.length - 1].time) - x) < 4 && !hoveringOver.ray) {
|
||||
clickedEnd = 'last'
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
} else {
|
||||
originalPrice = this.chart.series.coordinateToPrice(y)
|
||||
originalTime = this.chart.chart.timeScale().coordinateToTime(x * this.chart.scale.width)
|
||||
this.chart.chart.subscribeCrosshairMove(checkForDrag)
|
||||
}
|
||||
originalIndex = this.chart.chart.timeScale().coordinateToLogical(x)
|
||||
this.chart.chart.unsubscribeClick(checkForClick)
|
||||
}
|
||||
let checkForRelease = (event) => {
|
||||
mouseDown = false
|
||||
document.body.style.cursor = 'pointer'
|
||||
|
||||
this.chart.chart.applyOptions({handleScroll: true})
|
||||
if (hoveringOver && 'price' in hoveringOver && hoveringOver.id !== 'toolBox') {
|
||||
this.chart.callbackFunction(`on_horizontal_line_move__${this.chart.id}__${hoveringOver.id};;;${hoveringOver.price.toFixed(8)}`);
|
||||
}
|
||||
hoveringOver = null
|
||||
document.removeEventListener('mousedown', checkForClick)
|
||||
document.removeEventListener('mouseup', checkForRelease)
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
}
|
||||
let checkForDrag = (param) => {
|
||||
if (!param.point) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(checkForDrag)
|
||||
if (!mouseDown) return
|
||||
|
||||
let priceAtCursor = this.chart.series.coordinateToPrice(param.point.y)
|
||||
|
||||
let priceDiff = priceAtCursor - originalPrice
|
||||
let barsToMove = param.logical - originalIndex
|
||||
|
||||
let startBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[0].time).getTime())
|
||||
let endBarIndex = this.chart.candleData.findIndex(item => chartTimeToDate(item.time).getTime() === chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime())
|
||||
|
||||
let startBar
|
||||
let endBar
|
||||
if (hoveringOver.ray) {
|
||||
endBar = this.chart.candleData[startBarIndex + barsToMove]
|
||||
startBar = hoveringOver.data[hoveringOver.data.length - 1]
|
||||
} else {
|
||||
startBar = this.chart.candleData[startBarIndex + barsToMove]
|
||||
endBar = endBarIndex === -1 ? null : this.chart.candleData[endBarIndex + barsToMove]
|
||||
}
|
||||
|
||||
let endDate = endBar ? endBar.time : dateToChartTime(new Date(chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
|
||||
let startDate = startBar.time
|
||||
let startValue = hoveringOver.data[0].value + priceDiff
|
||||
let endValue = hoveringOver.data[hoveringOver.data.length - 1].value + priceDiff
|
||||
hoveringOver.data = calculateTrendLine(startDate, startValue, endDate, endValue, this.interval, this.chart, hoveringOver.ray)
|
||||
|
||||
let logical
|
||||
if (chartTimeToDate(hoveringOver.data[hoveringOver.data.length - 1].time).getTime() >= chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime()) {
|
||||
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
}
|
||||
hoveringOver.from = hoveringOver.data[0].time
|
||||
hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time
|
||||
hoveringOver.line.setData(hoveringOver.data)
|
||||
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
|
||||
if (!hoveringOver.ray) {
|
||||
hoveringOver.markers = [
|
||||
{time: startDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
|
||||
{time: endDate, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
|
||||
]
|
||||
hoveringOver.line.setMarkers(hoveringOver.markers)
|
||||
}
|
||||
|
||||
originalIndex = param.logical
|
||||
originalPrice = priceAtCursor
|
||||
this.chart.chart.subscribeCrosshairMove(checkForDrag)
|
||||
}
|
||||
let crosshairHandlerTrend = (param) => {
|
||||
if (!param.point) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerTrend)
|
||||
if (!mouseDown) return
|
||||
|
||||
let currentPrice = this.chart.series.coordinateToPrice(param.point.y)
|
||||
let currentTime = this.chart.chart.timeScale().coordinateToTime(param.point.x)
|
||||
|
||||
let [firstTime, firstPrice] = [null, null]
|
||||
if (clickedEnd === 'last') {
|
||||
firstTime = hoveringOver.data[0].time
|
||||
firstPrice = hoveringOver.data[0].value
|
||||
} else if (clickedEnd === 'first') {
|
||||
firstTime = hoveringOver.data[hoveringOver.data.length - 1].time
|
||||
firstPrice = hoveringOver.data[hoveringOver.data.length - 1].value
|
||||
}
|
||||
|
||||
let logical
|
||||
let lastCandleTime = this.chart.candleData[this.chart.candleData.length - 1].time
|
||||
if (!currentTime) {
|
||||
let barsToMove = param.logical - this.chart.chart.timeScale().coordinateToLogical(this.chart.chart.timeScale().timeToCoordinate(lastCandleTime))
|
||||
logical = barsToMove <= 0 ? null : this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
currentTime = dateToChartTime(new Date(chartTimeToDate(this.chart.candleData[this.chart.candleData.length - 1].time).getTime() + (barsToMove * this.interval)), this.interval)
|
||||
} else if (chartTimeToDate(lastCandleTime).getTime() <= chartTimeToDate(currentTime).getTime()) {
|
||||
logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
}
|
||||
|
||||
hoveringOver.data = calculateTrendLine(firstTime, firstPrice, currentTime, currentPrice, this.interval, this.chart)
|
||||
hoveringOver.line.setData(hoveringOver.data)
|
||||
|
||||
hoveringOver.from = hoveringOver.data[0].time
|
||||
hoveringOver.to = hoveringOver.data[hoveringOver.data.length - 1].time
|
||||
if (logical) this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
|
||||
hoveringOver.markers = [
|
||||
{time: firstTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1},
|
||||
{time: currentTime, position: 'inBar', color: '#1E80F0', shape: 'circle', size: 0.1}
|
||||
]
|
||||
hoveringOver.line.setMarkers(hoveringOver.markers)
|
||||
|
||||
setTimeout(() => {
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerTrend)
|
||||
}, 10);
|
||||
}
|
||||
let crosshairHandlerHorz = (param) => {
|
||||
if (!param.point) return
|
||||
this.chart.chart.unsubscribeCrosshairMove(crosshairHandlerHorz)
|
||||
if (!mouseDown) return
|
||||
hoveringOver.updatePrice(this.chart.series.coordinateToPrice(param.point.y))
|
||||
setTimeout(() => {
|
||||
this.chart.chart.subscribeCrosshairMove(crosshairHandlerHorz)
|
||||
}, 10)
|
||||
}
|
||||
this.chart.chart.subscribeCrosshairMove(hoverOver)
|
||||
}
|
||||
|
||||
renderDrawings() {
|
||||
//let logical = this.chart.chart.timeScale().getVisibleLogicalRange()
|
||||
this.drawings.forEach((item) => {
|
||||
if ('price' in item) return
|
||||
let startDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.from).getTime() / this.interval) * this.interval), this.interval)
|
||||
let endDate = dateToChartTime(new Date(Math.round(chartTimeToDate(item.to).getTime() / this.interval) * this.interval), this.interval)
|
||||
let data = calculateTrendLine(startDate, item.data[0].value, endDate, item.data[item.data.length - 1].value, this.interval, this.chart, item.ray)
|
||||
if (data.length !== 0) item.data = data
|
||||
item.line.setData(data)
|
||||
})
|
||||
//this.chart.chart.timeScale().setVisibleLogicalRange(logical)
|
||||
}
|
||||
|
||||
clearDrawings() {
|
||||
this.drawings.forEach((item) => {
|
||||
if ('price' in item) this.chart.series.removePriceLine(item.line)
|
||||
else this.chart.chart.removeSeries(item.line)
|
||||
})
|
||||
this.drawings = []
|
||||
}
|
||||
}
|
||||
|
||||
window.ToolBox = ToolBox
|
||||
}
|
||||
@ -49,6 +49,7 @@ class PolygonAPI:
|
||||
self._using_live_data = False
|
||||
self._using_live = {'stocks': False, 'options': False, 'indices': False, 'crypto': False, 'forex': False}
|
||||
self._ws = {'stocks': None, 'options': None, 'indices': None, 'crypto': None, 'forex': None}
|
||||
self._tickers = {}
|
||||
|
||||
|
||||
def log(self, info: bool):
|
||||
@ -156,7 +157,9 @@ class PolygonAPI:
|
||||
columns.append('v')
|
||||
df = df[columns].rename(columns=rename)
|
||||
df['time'] = pd.to_datetime(df['time'], unit='ms')
|
||||
chart.set(df)
|
||||
|
||||
chart.set(df, render_drawings=self._tickers.get(chart) == ticker)
|
||||
self._tickers[chart] = ticker
|
||||
|
||||
if not live:
|
||||
return True
|
||||
@ -300,10 +303,10 @@ class PolygonChart(Chart):
|
||||
def __init__(self, api_key: str, live: bool = False, num_bars: int = 200, end_date: str = 'now', limit: int = 5_000,
|
||||
timeframe_options: tuple = ('1min', '5min', '30min', 'D', 'W'),
|
||||
security_options: tuple = ('Stock', 'Option', 'Index', 'Forex', 'Crypto'),
|
||||
width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||
toolbox: bool = True, width: int = 800, height: int = 600, x: int = None, y: int = None,
|
||||
on_top: bool = False, maximize: bool = False, debug: bool = False):
|
||||
super().__init__(volume_enabled=True, width=width, height=height, x=x, y=y, on_top=on_top, maximize=maximize, debug=debug,
|
||||
api=self, topbar=True, searchbox=True)
|
||||
api=self, topbar=True, searchbox=True, toolbox=toolbox)
|
||||
self.chart = self
|
||||
self.num_bars = num_bars
|
||||
self.end_date = end_date
|
||||
@ -337,7 +340,7 @@ class PolygonChart(Chart):
|
||||
|
||||
def _polygon(self, symbol):
|
||||
self.spinner(True)
|
||||
self.set(pd.DataFrame())
|
||||
self.set(pd.DataFrame(), True)
|
||||
self.crosshair(vert_visible=False, horz_visible=False)
|
||||
|
||||
mult, span = _convert_timeframe(self.topbar['timeframe'].value)
|
||||
|
||||
@ -27,7 +27,7 @@ class IDGen(list):
|
||||
var = ''.join(choices(ascii_lowercase, k=8))
|
||||
if var not in self:
|
||||
self.append(var)
|
||||
return var
|
||||
return f'window.{var}'
|
||||
self.generate()
|
||||
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ try:
|
||||
except ImportError:
|
||||
HTML = None
|
||||
|
||||
from lightweight_charts.abstract import LWC, TopBar, JS
|
||||
from lightweight_charts.abstract import LWC, JS
|
||||
|
||||
|
||||
def _widget_message(chart, string):
|
||||
@ -47,40 +47,41 @@ def _widget_message(chart, string):
|
||||
|
||||
class WxChart(LWC):
|
||||
def __init__(self, parent, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0,
|
||||
scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False):
|
||||
scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False,
|
||||
toolbox: bool = False):
|
||||
if wx is None:
|
||||
raise ModuleNotFoundError('wx.html2 was not found, and must be installed to use WxChart.')
|
||||
self.webview: wx.html2.WebView = wx.html2.WebView.New(parent)
|
||||
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height, scale_candles_only=scale_candles_only)
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height,
|
||||
scale_candles_only=scale_candles_only, topbar=topbar, searchbox=searchbox, toolbox=toolbox,
|
||||
_js_api_code='window.wx_msg.postMessage.bind(window.wx_msg)')
|
||||
self.api = api
|
||||
self._script_func = self.webview.RunScript
|
||||
self._js_api_code = 'window.wx_msg.postMessage.bind(window.wx_msg)'
|
||||
|
||||
self.webview.Bind(wx.html2.EVT_WEBVIEW_LOADED, lambda e: wx.CallLater(500, 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.SetPage(self._html, '')
|
||||
|
||||
self.webview.AddUserScript(JS['callback'])
|
||||
self._create_chart()
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
self.webview.AddUserScript(JS['callback']) if topbar or searchbox else None
|
||||
self.webview.AddUserScript(JS['toolbox']) if toolbox else None
|
||||
|
||||
def get_webview(self): return self.webview
|
||||
|
||||
|
||||
class QtChart(LWC):
|
||||
def __init__(self, widget=None, api: object = None, topbar: bool = False, searchbox: bool = False,
|
||||
volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0, scale_candles_only: bool = False):
|
||||
def __init__(self, widget=None, volume_enabled: bool = True, inner_width: float = 1.0, inner_height: float = 1.0,
|
||||
scale_candles_only: bool = False, api: object = None, topbar: bool = False, searchbox: bool = False,
|
||||
toolbox: bool = False):
|
||||
if QWebEngineView is None:
|
||||
raise ModuleNotFoundError('QWebEngineView was not found, and must be installed to use QtChart.')
|
||||
self.webview = QWebEngineView(widget)
|
||||
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height, scale_candles_only=scale_candles_only)
|
||||
super().__init__(volume_enabled, inner_width=inner_width, inner_height=inner_height,
|
||||
scale_candles_only=scale_candles_only, topbar=topbar, searchbox=searchbox, toolbox=toolbox,
|
||||
_js_api_code='window.pythonObject.callback')
|
||||
self.api = api
|
||||
self._script_func = self.webview.page().runJavaScript
|
||||
self._js_api_code = 'window.pythonObject.callback'
|
||||
|
||||
self.web_channel = QWebChannel()
|
||||
self.bridge = Bridge(self)
|
||||
@ -100,17 +101,12 @@ class QtChart(LWC):
|
||||
'''
|
||||
self.webview.page().setHtml(self._html)
|
||||
|
||||
self.run_script(JS['callback'])
|
||||
self._create_chart()
|
||||
self.topbar = TopBar(self) if topbar else None
|
||||
self._make_search_box() if searchbox else None
|
||||
|
||||
def get_webview(self): return self.webview
|
||||
|
||||
|
||||
class StaticLWC(LWC):
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, scale_candles_only=scale_candles_only)
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox=False, autosize=True):
|
||||
super().__init__(volume_enabled, inner_width, inner_height, scale_candles_only=scale_candles_only, toolbox=toolbox, autosize=autosize)
|
||||
self.width = width
|
||||
self.height = height
|
||||
self._html = self._html.replace('</script>\n</body>\n</html>', '')
|
||||
@ -133,9 +129,8 @@ class StaticLWC(LWC):
|
||||
|
||||
|
||||
class StreamlitChart(StaticLWC):
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
|
||||
self._create_chart()
|
||||
def __init__(self, volume_enabled=True, width=None, height=None, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox: bool = False):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only, toolbox)
|
||||
|
||||
def _load(self):
|
||||
if html is None:
|
||||
@ -144,11 +139,10 @@ class StreamlitChart(StaticLWC):
|
||||
|
||||
|
||||
class JupyterChart(StaticLWC):
|
||||
def __init__(self, volume_enabled=True, width=800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only)
|
||||
def __init__(self, volume_enabled=True, width: int = 800, height=350, inner_width=1, inner_height=1, scale_candles_only: bool = False, toolbox: bool = False):
|
||||
super().__init__(volume_enabled, width, height, inner_width, inner_height, scale_candles_only, toolbox, autosize=False)
|
||||
self._position = ""
|
||||
|
||||
self._create_chart(autosize=False)
|
||||
self.run_script(f'''
|
||||
for (var i = 0; i < document.getElementsByClassName("tv-lightweight-charts").length; i++) {{
|
||||
var element = document.getElementsByClassName("tv-lightweight-charts")[i];
|
||||
|
||||
Reference in New Issue
Block a user