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:
@ -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) {
|
||||
@ -139,4 +288,109 @@ 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