Compare commits

...

29 Commits

Author SHA1 Message Date
1c72818dae fixed 0 doesnt update in legend 2024-11-21 13:43:28 +01:00
92acc2b96a fix 2024-11-15 09:41:16 +01:00
ada85883c7 fix 2024-11-15 09:35:55 +01:00
3adf0b9ce3 fix 2024-11-15 09:33:50 +01:00
1551f6f904 fix 2024-11-15 09:23:35 +01:00
33c2d47858 fix 2024-11-15 09:21:52 +01:00
cab85bb4f8 fix 2024-11-15 09:18:40 +01:00
37af631a3e fix 2024-11-15 09:12:26 +01:00
f1024d551f fix 2024-11-15 09:04:27 +01:00
2b607f96be fix 2024-11-15 09:01:02 +01:00
81996a1891 fix 2024-11-15 08:43:46 +01:00
e526940717 fix 2024-11-15 08:33:39 +01:00
0e88137927 fix 2024-11-15 07:54:00 +01:00
5407e22bd6 fix 2024-11-15 07:46:43 +01:00
ef192e82f9 fix 2024-11-15 07:38:46 +01:00
1f4aa4fa8e fix 2024-11-15 07:25:08 +01:00
1deb397e28 fix 2024-11-15 07:10:41 +01:00
2c656fa640 fix 2024-11-15 07:00:58 +01:00
6393e618ce fix 2024-11-15 06:13:54 +01:00
b336857832 fix 2024-10-20 15:11:22 +02:00
e752ef8fdd fix 2024-10-20 14:55:37 +02:00
1c2afbf93b update 2024-10-17 09:28:38 +02:00
3f2a484cd7 fix 2024-10-13 15:39:54 +02:00
7b0acec3e6 added shorter syntax for one pane 2024-10-13 14:21:05 +02:00
a9cb8da66e readme udpate 2024-10-09 16:11:46 +02:00
9fca26db4b auto scale support 2024-10-09 16:06:19 +02:00
dfe1eafba9 multindex indicator support 2024-10-04 21:58:03 +02:00
8b9f3ad61f update main session marker 2024-10-04 13:52:32 +02:00
0da6839bcb examples moved to archive 2024-10-04 12:04:55 +02:00
38 changed files with 491 additions and 99 deletions

View File

@ -1,10 +1,10 @@
Fork of the original [lightweight-charts](louisnw01/lightweight-charts-python) with enhancements and supporting vectorbtpro workflow
Fork of the original [lightweight-charts](louisnw01/lightweight-charts-python) with enhancements and supporting `vectorbtpro` workflow
* legend color matching each line color
* automatic color assignment if not provided
* support for multiple scales (right, left, middle1, middle2, histogram)
* accepts df,pd.series or vectorbtpro indicator object (including unpacking multi outputs)
* accepts df,pd.series or `vectorbtpro indicator` object (including unpacking multi outputs)
* new markers_set method allowing to set pd.series or dataframe as markers input
* for quick display supports simple df/sr accessors `close.lw.plot()` for quick visualization of single panel chart
* supports simple df/sr accessors `close.lw.plot()` for quick visualization of single panel chart
* df accessor unpacks dataframe columns and display them accordingly (ohlcv as ohlcv, vwap on the right, rsi on the left scale etc.)
* multipanes support `ch = chart([pane1, pane2], sync=True, title="Title", size="m")` to quickly display chart with N panes (`Panels`). Also supports syncing the Panels `sync=True` or using xloc.
@ -44,13 +44,35 @@ ohlcv_complex_df.lw.plot() #df containing ohlcv and other columns
```
![alt text](image-5.png)
```python
#quick few liner, displays close series with label "close" on right pricescale and rsi on left price scale, all on single Panel
t1data.ohlcv.data["BAC"].lw.plot(left=[(t1mom,"mom"),(t1mom_tt.mom.loc[:, (20,"1T")],),(t1mom_tt.mom.loc[:, (20,"5T")],)]) #display ohlcv 1m data along with 1min momentum ind and 2 multiindexed indicators on 1M and 5m resolution on 5M (calculated as multiindex)
#quickly plot vectorbtpro indicator on top of OHLCV data (with automatic unpacking)
macd = vbt.indicator("talib:MACD").run(t1data.data["BAC"].close)
t1data.ohlcv.data["BAC"].lw.plot(auto_scale=macd)
```
![alt text](image-6.png)
```python
#ONE PANEL - quick few liner, displays close series with label "close" on right pricescale and rsi on left price scale, all on single Panel
pane1 = Panel(
right=[(close, "close")],
left=[(rsi,"rsi")]
)
ch = chart([pane1])
##ONE PANEL - quicker
Panel(
auto_scale=[cdlbreakaway],
ohlcv=(t1data.ohlcv.data["BAC"],entries),
histogram=[],
right=[],
left=[],
middle1=[],
middle2=[]
).chart(size="s")
```
![alt text](image-7.png)
```python
# display two Panels
# on first displays ohlcv data, orderimbalance volume as histogram with opacity, bbands on the right pricescale and
# sma with short_signals and short_exits on the left pricescale

View File

Before

Width:  |  Height:  |  Size: 555 KiB

After

Width:  |  Height:  |  Size: 555 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 MiB

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

Before

Width:  |  Height:  |  Size: 349 KiB

After

Width:  |  Height:  |  Size: 349 KiB

View File

Before

Width:  |  Height:  |  Size: 612 KiB

After

Width:  |  Height:  |  Size: 612 KiB

View File

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 510 KiB

View File

Before

Width:  |  Height:  |  Size: 475 KiB

After

Width:  |  Height:  |  Size: 475 KiB

BIN
image-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
image-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -245,9 +245,12 @@ class SeriesCommon(Pane):
if format_cols:
df = self._df_datetime_format(df, exclude_lowercase=self.name)
if self.name:
if self.name not in df:
if self.name and len(df.columns) == 1: #if only one col rename it
df.columns = ['value']
elif self.name not in df:
raise NameError(f'No column named "{self.name}".')
df = df.rename(columns={self.name: 'value'})
else:
df = df.rename(columns={self.name: 'value'})
self.data = df.copy()
self._last_bar = df.iloc[-1]
self.run_script(f'{self.id}.series.setData({js_data(df)}); ')
@ -646,7 +649,7 @@ class Candlestick(SeriesCommon):
super().__init__(chart)
self._volume_up_color = 'rgba(83,141,131,0.8)'
self._volume_down_color = 'rgba(200,127,130,0.8)'
self.num_decimals = 2
self.candle_data = pd.DataFrame()
# self.run_script(f'{self.id}.makeCandlestickSeries()')
@ -750,11 +753,25 @@ class Candlestick(SeriesCommon):
text_color: Optional[str] = None,
entire_text_only: bool = False,
visible: bool = True,
ticks_visible: bool = False,
ticks_visible: bool = True,
minimum_width: int = 0
):
# def precision(self, precision: int):
# """
# Sets the precision and minMove.\n
# :param precision: The number of decimal places.
# """
# min_move = 1 / (10**precision)
# self.run_script(f'''
# {self.id}.series.applyOptions({{
# priceFormat: {{precision: {precision}, minMove: {min_move}}}
# }})''')
# self.num_decimals = precision
self.run_script(f'''
{self.id}.series.priceScale().applyOptions({{
priceFormat: {{ precision: 3, minMove: 0.005 }},
autoScale: {jbool(auto_scale)},
mode: {as_enum(mode, PRICE_SCALE_MODE)},
invertScale: {jbool(invert_scale)},
@ -985,7 +1002,7 @@ class AbstractChart(Candlestick, Pane):
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',
text: str = '', color_based_on_candle: bool = False):
text: str = '', color_based_on_candle: bool = True):
"""
Configures the legend of the chart.
"""

View File

@ -1,16 +1,46 @@
from ast import parse
from .widgets import JupyterChart
from .util import (
is_vbt_indicator, get_next_color
)
import pandas as pd
#default settings for each pricescale
ohlcv_cols = ['close', 'volume', 'open', 'high', 'low']
right_cols = ['vwap']
left_cols = ['rsi', 'cci', 'macd', 'macdsignal', "chopiness", "chopiness_ma"]
middle1_cols = ["mom"]
middle2_cols = ["updated", "integer"]
histogram_cols = ['buyvolume', 'sellvolume', 'trades', 'macdhist']
def append_scales(df, right, histogram, left, middle1, middle2, name = ""):
if isinstance(df, pd.DataFrame):
for col in df.columns:
match col:
case c if c.lower() in ohlcv_cols:
continue
case c if c.lower() in right_cols:
right.append((df[c],name+c,))
case c if c.lower() in histogram_cols:
histogram.append((df[c],name+c,))
case c if c.lower() in left_cols:
left.append((df[c],name+c,))
case c if c.lower() in middle1_cols:
middle1.append((df[c],name+c,))
case c if c.lower() in middle2_cols:
middle2.append((df[c],name+c,))
case _:
right.append((df[c],name+c,))
else: #it is series (as df multiindex can be just envelope for series)
right.append((df,str(df.name),))
def append_or_extend(target_list, value):
if isinstance(value, list):
target_list.extend(value) # Extend if it's a list
else:
target_list.append(value) # Append if it's a single value
def extend_kwargs(ohlcv, right, left, middle1, middle2, histogram, kwargs):
def extend_kwargs(ohlcv, right, left, middle1, middle2, histogram, auto_scale, kwargs):
"""
Mutate lists based on kwargs for accessor.
Used when user added additional series to kwargs when using accessor.
@ -26,7 +56,9 @@ def extend_kwargs(ohlcv, right, left, middle1, middle2, histogram, kwargs):
if 'middle1' in kwargs:
append_or_extend(middle1, kwargs['middle1'])
if 'middle2' in kwargs:
append_or_extend(middle1, kwargs['middle2'])
append_or_extend(middle2, kwargs['middle2'])
if 'auto_scale' in kwargs:
append_or_extend(auto_scale, kwargs['auto_scale'])
return ohlcv #as tuple is immutable
@ -59,6 +91,7 @@ class PlotSRAccessor:
def plot(self, **kwargs):
if "size" not in kwargs:
kwargs["size"] = "xs"
name = kwargs["name"] if "name" in kwargs else "line"
ohlcv = ()
right = []
@ -66,14 +99,16 @@ class PlotSRAccessor:
middle1 = []
middle2 = []
histogram = []
auto_scale = []
#if there are additional series in kwargs add them too
#ohlcv is returned as it is tuple thus immutable
ohlcv = extend_kwargs(ohlcv, right, left, middle1, middle2, histogram, kwargs)
ohlcv = extend_kwargs(ohlcv, right, left, middle1, middle2, histogram, auto_scale, kwargs)
right.append((self._obj,"line"))
right.append((self._obj,name))
pane1 = Panel(
auto_scale=auto_scale,
ohlcv=ohlcv,
histogram=histogram,
right=right,
@ -124,39 +159,36 @@ class PlotDFAccessor:
if "size" not in kwargs:
kwargs["size"] = "xs"
#default settings for each pricescale
ohlcv_cols = ['close', 'volume', 'open', 'high', 'low']
right_cols = ['vwap']
left_cols = ['rsi']
middle1_cols = []
middle2_cols = []
histogram_cols = ['buyvolume', 'sellvolume', 'trades']
ohlcv = ()
right = []
left = []
middle1 = []
middle2 = []
histogram = []
auto_scale = []
for col in self._obj.columns:
if col in right_cols:
right.append((self._obj[col],col,))
if col in histogram_cols:
histogram.append((self._obj[col],col,))
if col in left_cols:
left.append((self._obj[col],col,))
if col in middle1_cols:
middle1_cols.append((self._obj[col],col,))
if col in middle2_cols:
middle2_cols.append((self._obj[col],col,))
if isinstance(self._obj.columns, pd.MultiIndex):
for col_tuple in self._obj.columns:
# Access the data for each column tuple dynamically
df = self._obj.loc[:, col_tuple]
name = str(col_tuple)+" "
append_scales(df, right, histogram, left, middle1, middle2, name)
first_column_df = self._obj.loc[:, self._obj.columns[0]]
ohlcv = (first_column_df[ohlcv_cols],) if isinstance(first_column_df, pd.DataFrame) and first_column_df.columns in ohlcv else () #in case of multiindex only the first ohlcv is display only one ohlcv is allowed on the pane
ohlcv = (self._obj[ohlcv_cols],)
else:
append_scales(self._obj, right, histogram, left, middle1, middle2)
#add ohlcv if all columns ohlcv_cols
#column mapping enables either both lowercase and first upper
column_mapping = {key: next((col for col in self._obj.columns if col.lower() == key), None) for key in ohlcv_cols}
mapped_columns = [column_mapping[key] for key in ohlcv_cols if column_mapping[key] is not None]
ohlcv = (self._obj[mapped_columns],) if isinstance(self._obj, pd.DataFrame) and all(col in self._obj.columns.str.lower() for col in ohlcv_cols) else ()
#if there are additional series in kwargs add them too
ohlcv = extend_kwargs(ohlcv, right, left, middle1, middle2, histogram, kwargs)
ohlcv = extend_kwargs(ohlcv, right, left, middle1, middle2, histogram, auto_scale, kwargs)
pane1 = Panel(
auto_scale=auto_scale,
ohlcv=ohlcv,
histogram=histogram,
right=right,
@ -195,6 +227,7 @@ class Panel:
* left : list of tuples, optional
* middle1 : list of tuples, optional
* middle2 : list of tuples, optional
* auto_scale: list of objects, optional - external objects (vbt indicators) that can be automatically parsed to given scaleID
* xloc : str or slice, optional. Vectorbt indexing. Default is None.
* precision: int, optional. The number of digits after the decimal point. Apply to all lines on this pane. Default is None.
@ -213,6 +246,18 @@ class Panel:
ch = chart([pane1])
# or simply:
Panel(
auto_scale=[cdlbreakaway],
ohlcv=(t1data.ohlcv.data["BAC"],entries),
histogram=[],
right=[],
left=[],
middle1=[],
middle2=[]
).chart(size="s")
# Synced example
pane1 = Panel(
ohlcv=(t1data.data["BAC"],), #(series, entries, exits, other_markers)
@ -226,6 +271,7 @@ class Panel:
)
pane2 = Panel(
auto_scale=[macd_vbt_ind],
ohlcv=(t1data.data["BAC"],),
right=[],
left=[(sma, "sma_below", short_signals, short_exits)],
@ -257,7 +303,8 @@ class Panel:
ch = chart([pane1], title="Chart with EntryShort/ExitShort (yellow) and EntryLong/ExitLong markers (pink)", sync=True, session=None, size="s")
```
"""
def __init__(self, ohlcv=None, right=None, left=None, middle1=None, middle2=None, histogram=None, title=None, xloc=None, precision=None):
def __init__(self, auto_scale=[],ohlcv=None, right=None, left=None, middle1=None, middle2=None, histogram=None, title=None, xloc=None, precision=None):
self.auto_scale = auto_scale
self.ohlcv = ohlcv if ohlcv is not None else ()
self.right = right if right is not None else []
self.left = left if left is not None else []
@ -268,8 +315,10 @@ class Panel:
self.xloc = xloc
self.precision = precision
def chart(self, **kwargs):
chart([self], **kwargs)
def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session: str="9:30:00, 09:30:05", precision=None, **kwargs):
def chart(panes: list[Panel], sync=False, title='', size="s", xloc=None, session = slice("09:30:00","9:30:05"), precision=None, params_detail=False, **kwargs):
"""
Function to fast render a chart with multiple panes. This function manipulates graphical
output or interfaces with an external framework to display charts with synchronized
@ -295,6 +344,8 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
* precision (int): The number of digits after the decimal point. Defaults to None. Applies to lines on all panes, if not overriden by pane-specific precision.
* params_detail (bool): If True displays in the legend full names of multiindex columns.
* xloc (str): xloc advanced filtering of vbt.xloc accessor. Defaults to None. Applies to all panes.
Might be overriden by pane-specific xloc.
@ -381,6 +432,79 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
active_chart.markers_set(markers=xloc_me(markers, xloc), type=type, color=color if color is not None else None)
def add_to_scale(series, right, histogram, left, middle1, middle2, column,name = None):
"""
Assigns a series to a scaleId based on its name and pre-defined col names.
Args:
-----
series (pd.Series): The series to be added to a scaleId
right (list): The right scale to add to
histogram (list): The histogram scale to add to
left (list): The left scale to add to
middle1 (list): The middle1 scale to add to
middle2 (list): The middle2 scale to add to
name (str): The name of the series
Returns:
-------
None
Notes:
-----
The function checks if the series name is in the pre-defined column names
(e.g. ohlcv_cols, right_cols, histogram_cols, etc.) and assigns the series to
the corresponding scaleId. If the name is not found in any of the pre-defined
column names, the series is added to the right scale by default.
"""
if name is None:
name = column
if column.lower() in ohlcv_cols:
return
elif column.lower() in right_cols:
right.append((series, name,))
elif column.lower() in histogram_cols:
histogram.append((series, name))
elif column.lower() in left_cols:
left.append((series, name))
elif column.lower() in middle1_cols:
middle1.append((series, name))
elif column.lower() in middle2_cols:
middle2.append((series, name))
else:
right.append((series, name,))
# automatic scale assignment
if len(pane.auto_scale) > 0:
for obj in pane.auto_scale:
if is_vbt_indicator(obj): #for vbt indicators
for output in obj.output_names:
output_series = getattr(obj, output)
output_name = obj.short_name + ':' + output
output = obj.short_name if output == "real" else output
#if output_series is multiindex - add each combination to respective scaleId
if isinstance(output_series, pd.DataFrame) and isinstance(output_series.columns, pd.MultiIndex):
for col_tuple in output_series.columns:
name=output_name + " " + str(col_tuple)
series_copy = output_series.loc[:, col_tuple].copy(deep=True)
add_to_scale(series_copy, pane.right, pane.histogram, pane.left, pane.middle1, pane.middle2, output, name)
elif isinstance(output_series, pd.DataFrame): #in case of multicolumns
for col in output_series.columns:
name=output_name + " " + col
series_copy = output_series.loc[:, col].copy(deep=True)
add_to_scale(series_copy, pane.right, pane.histogram, pane.left, pane.middle1, pane.middle2, output, name)
# elif isinstance(output_series, pd.DataFrame): #it df with multiple columns (probably symbols)
# for col in output_series.columns:
# name=output_name + " " + col
# series_copy = output_series[col].squeeze()
# add_to_scale(series_copy, pane.right, pane.histogram, pane.left, pane.middle1, pane.middle2, output, name)
else: #add output to respective scale
series_copy = output_series.copy(deep=True)
add_to_scale(series_copy, pane.right, pane.histogram, pane.left, pane.middle1, pane.middle2, output, output_name)
# zde jsem skoncil
#vbt ind
if pane.ohlcv != ():
series, entries, exits, markers = (pane.ohlcv + (None,) * 4)[:4]
if series is None:
@ -403,8 +527,19 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
kwargs['color'] = color
if opacity is not None:
kwargs['opacity'] = opacity
tmp = active_chart.create_histogram(**kwargs) #green transparent "rgba(53, 94, 59, 0.6)"
tmp.set(xloc_me(series, xloc))
if isinstance(series, pd.DataFrame) and isinstance(series.columns, pd.MultiIndex): #multiindex handling
for col_tuple in series.columns:
kwargs = {'name': name + str(col_tuple)}
tmp = active_chart.create_histogram(**kwargs) #green transparent "rgba(53, 94, 59, 0.6)"
tmp.set(xloc_me(series.loc[:, col_tuple], xloc))
elif isinstance(series, pd.DataFrame): #it df with multiple columns (probably symbols)
for col in series.columns:
kwargs = {'name': name + str(col)}
tmp = active_chart.create_histogram(**kwargs) #green transparent "rgba(53, 94, 59, 0.6)"
tmp.set(xloc_me(series[col], xloc))
else:
tmp = active_chart.create_histogram(**kwargs) #green transparent "rgba(53, 94, 59, 0.6)"
tmp.set(xloc_me(series, xloc))
if pane.title is not None:
active_chart.topbar.textbox("title",pane.title)
@ -412,7 +547,7 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
#iterate over keys - they are all priceScaleId except of these
for att_name, att_value_tuple in vars(pane).items():
if att_name in ["ohlcv","histogram","title","xloc","precision"]:
if att_name in ["ohlcv","histogram","title","xloc","precision", "auto_scale"]:
continue
for tup in att_value_tuple:
series, name, entries, exits, markers = (tup + (None, None, None, None, None))[:5]
@ -424,12 +559,71 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
series = series.xloc[xloc] if xloc is not None else series
for output in series.output_names:
output_series = getattr(series, output)
output = name + ':' + output if name is not None else output
tmp = active_chart.create_line(name=output, priceScaleId=att_name)#, color="blue")
tmp.set(output_series)
output = name + ':' + output if name is not None else series.short_name + ":" + output
#if output_series is multiindex - create aline for each combination
if isinstance(output_series, pd.DataFrame) and isinstance(output_series.columns, pd.MultiIndex):
for col_tuple in output_series.columns:
tmp = active_chart.create_line(name=output + " " + str(col_tuple), priceScaleId=att_name)#, color="blue")
tmp.set(output_series.loc[:, col_tuple])
elif isinstance(output_series, pd.DataFrame): #it df with multiple columns (probably symbols)
for col in output_series.columns:
tmp = active_chart.create_line(name=output + " " + str(col), priceScaleId=att_name)#, color="blue")
tmp.set(output_series[col])
else:
tmp = active_chart.create_line(name=output, priceScaleId=att_name)#, color="blue")
tmp.set(output_series)
#if multiindex then unpack them all with tuple as names
elif isinstance(series, pd.DataFrame) and isinstance(series.columns, pd.MultiIndex):
for col_tuple in series.columns:
#if required show all multiindex names
if params_detail:
# Access MultiIndex level names
index_names = series.columns.names
# Build the name string by combining level names and their corresponding values
col_name_str = " ".join([f"{index_names[i]}: {col_val}" for i, col_val in enumerate(col_tuple)])
# Use this name in the chart, adding the provided name if it exists
final_name = col_name_str if name is None else f"{name} {col_name_str}"
else:
final_name = str(col_tuple) if name is None else name+" "+str(col_tuple)
tmp = active_chart.create_line(name=final_name, priceScaleId=att_name)#, color="blue")
tmp.set(xloc_me(series.loc[:, col_tuple], xloc))
elif isinstance(series, pd.DataFrame): #it df with multiple columns (probably symbols)
#recursive df handling - but make sure it
def traverse_dataframe(series, att_name, xloc, active_chart, name=""):
nonlocal tmp
# Check if the input is a DataFrame
if isinstance(series, pd.DataFrame):
for col in series.columns:
col_name = name + " " + col if name else col
# Recursively call the function for each column
traverse_dataframe(series[col], att_name, xloc, active_chart, col_name)
elif isinstance(series, pd.Series):
# Once we hit the series level, create the result_name and call the active_chart method
result_name = name.strip() # Remove any leading/trailing spaces from column name
result_series = series.squeeze() # Extract the series data
# Now call the `active_chart.create_line()` as per your requirement
tmp = active_chart.create_line(name=result_name, priceScaleId=att_name)
tmp.set(xloc_me(result_series, xloc)) # Call the xloc_me function for setting xloc
else:
raise ValueError(f"Unexpected type {type(series)} encountered")
if name is None:
name = "no_name" if not hasattr(series, 'name') or series.name is None else str(series.name)
traverse_dataframe(series, att_name, xloc, active_chart, name)
# for col in series.columns:
# name=name + " " + col
# series_copy = output_series[col].squeeze()
# tmp = active_chart.create_line(name=name, priceScaleId=att_name)#, color="blue")
# tmp.set(xloc_me(series_copy, xloc))
else:
if name is None:
name = "no_name"
name = "no_name" if not hasattr(series, 'name') or series.name is None else str(series.name)
tmp = active_chart.create_line(name=name, priceScaleId=att_name)#, color="blue")
tmp.set(xloc_me(series, xloc))
@ -447,8 +641,22 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
active_chart.legend(True)
active_chart.fit()
if session is not None and session:
last_used_series = output_series if is_vbt_indicator(series) else series #pokud byl posledni series vbt, pak pouzijeme jeho outputy
active_chart.vertical_span(start_time=xloc_me(last_used_series, xloc).vbt.xloc[session].obj.index.to_list(), color="rgba(252, 255, 187, 0.42)")
try:
last_used_series = output_series.loc[:, col_tuple] if isinstance(output_series, pd.DataFrame) and isinstance(output_series.columns, pd.MultiIndex) else output_series if is_vbt_indicator(series) else series #pokud byl posledni series vbt, pak pouzijeme jeho outputy
last_used_series = last_used_series.iloc[:,0] if isinstance(last_used_series, pd.DataFrame) else last_used_series #if df then use just first column
t1 = xloc_me(last_used_series, xloc)
t1 = t1.vbt.xloc[session]
target_data = t1.obj
#we dont know the exact time of market start +- 3 seconds thus we find mark first row after 9:30
# Resample the data to daily frequency and get the first entry of each day
first_row_indexes = target_data.resample('D').apply(lambda x: x.index[0] if not x.empty else None).dropna()
# Convert the indexes to a list
session_starts = first_row_indexes.to_list()
active_chart.vertical_span(start_time=session_starts, color="rgba(252, 255, 187, 0.42)")
except Exception as e:
print("Error fetching main session")
if not main_title_set:
chartX.topbar.textbox("title",title)

File diff suppressed because one or more lines are too long

View File

@ -113,7 +113,7 @@ def is_vbt_indicator(variable):
# Get the module path of the variable's type
module_path = variable.__class__.__module__
# Check if it starts with 'vectorbtpro.indicators'
return module_path.startswith('vectorbtpro.indicators')
return module_path.startswith('vectorbtpro.indicators') or module_path.startswith('indicators.')
class Pane:
def __init__(self, window):

View File

@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
setup(
name='lightweight_charts',
version='2.2.0',
version='2.2.29',
packages=find_packages(),
python_requires='>=3.8',
install_requires=[

View File

@ -40,7 +40,7 @@ export class Handler {
public chart: IChartApi;
public scale: Scale;
public precision: number = 2;
public precision: number = 3;
public series: ISeriesApi<SeriesType>;
public volumeSeries: ISeriesApi<SeriesType>;

View File

@ -1,7 +1,6 @@
import { ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts";
import { Handler } from "./handler";
interface LineElement {
name: string;
div: HTMLDivElement;
@ -14,6 +13,11 @@ interface LineElement {
export class Legend {
private handler: Handler;
public div: HTMLDivElement;
public contentWrapper: HTMLDivElement;
private collapseButton: HTMLDivElement;
private toggleAllButton: HTMLDivElement;
private isCollapsed: boolean = false;
private allVisible: boolean = true;
private ohlcEnabled: boolean = false;
private percentEnabled: boolean = false;
@ -24,7 +28,6 @@ export class Legend {
private candle: HTMLDivElement;
public _lines: LineElement[] = [];
constructor(handler: Handler) {
this.legendHandler = this.legendHandler.bind(this)
@ -36,62 +39,203 @@ export class Legend {
this.div = document.createElement('div');
this.div.classList.add('legend');
this.div.style.maxWidth = `${(handler.scale.width * 100) - 8}vw`
this.div.style.maxWidth = '300px';
this.div.style.minWidth = '200px';
this.div.style.maxHeight = '300px';
this.div.style.overflowY = 'auto';
this.div.style.overflowX = 'hidden';
this.div.style.position = 'absolute';
this.div.style.backgroundColor = 'rgba(19, 23, 34, 0)';
this.div.style.color = '#D1D4DC';
this.div.style.padding = '1px';
this.div.style.borderRadius = '4px';
//this.div.style.border = '1px solid rgba(42, 46, 57, 0.85)';
this.div.style.boxShadow = '0 2px 5px rgba(0,0,0,0.3)';
this.div.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
this.div.style.fontSize = '12px';
this.div.style.zIndex = '5';
this.div.style.display = 'none';
this.div.style.pointerEvents = 'all';
this.text = document.createElement('span')
this.text.style.lineHeight = '1.8'
this.candle = document.createElement('div')
const buttonsContainer = document.createElement('div');
buttonsContainer.style.position = 'absolute';
buttonsContainer.style.right = '8px';
buttonsContainer.style.top = '8px';
buttonsContainer.style.display = 'flex';
buttonsContainer.style.gap = '2px';
buttonsContainer.style.zIndex = '6';
buttonsContainer.style.pointerEvents = 'all';
this.div.appendChild(this.text)
this.div.appendChild(this.candle)
handler.div.appendChild(this.div)
this.collapseButton = document.createElement('div');
this.collapseButton.style.cursor = 'pointer';
this.collapseButton.style.width = '20px';
this.collapseButton.style.height = '20px';
this.collapseButton.style.display = 'flex';
this.collapseButton.style.alignItems = 'center';
this.collapseButton.style.justifyContent = 'center';
this.collapseButton.style.color = '#D1D4DC';
this.collapseButton.style.fontSize = '16px';
this.collapseButton.style.userSelect = 'none';
this.collapseButton.style.pointerEvents = 'all';
this.collapseButton.innerHTML = '';
this.collapseButton.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleCollapse();
});
// this.makeSeriesRows(handler);
this.toggleAllButton = document.createElement('div');
this.toggleAllButton.style.cursor = 'pointer';
this.toggleAllButton.style.width = '20px';
this.toggleAllButton.style.height = '20px';
this.toggleAllButton.style.display = 'flex';
this.toggleAllButton.style.alignItems = 'center';
this.toggleAllButton.style.justifyContent = 'center';
this.toggleAllButton.style.color = '#D1D4DC';
this.toggleAllButton.style.fontSize = '14px';
this.toggleAllButton.style.userSelect = 'none';
this.toggleAllButton.style.pointerEvents = 'all';
this.toggleAllButton.innerHTML = '👁️';
this.toggleAllButton.title = 'Toggle all';
this.toggleAllButton.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleAll();
});
buttonsContainer.appendChild(this.toggleAllButton);
buttonsContainer.appendChild(this.collapseButton);
const style = document.createElement('style');
style.textContent = `
.legend::-webkit-scrollbar {
width: 6px;
}
.legend::-webkit-scrollbar-track {
background: #2A2E39;
}
.legend::-webkit-scrollbar-thumb {
background: #434651;
border-radius: 3px;
}
.legend::-webkit-scrollbar-thumb:hover {
background: #545861;
}
.legend-toggle-switch {
cursor: pointer;
margin-left: 8px;
}
`;
document.head.appendChild(style);
this.contentWrapper = document.createElement('div');
this.contentWrapper.style.minHeight = '100%';
this.contentWrapper.style.width = '100%';
this.contentWrapper.style.display = 'flex';
this.contentWrapper.style.flexDirection = 'column';
this.contentWrapper.style.gap = '1px';
this.contentWrapper.style.marginTop = '20px';
this.contentWrapper.style.pointerEvents = 'all';
this.text = document.createElement('span');
this.text.style.lineHeight = '1';
this.text.style.display = 'block';
this.text.style.color = '#D1D4DC';
this.candle = document.createElement('div');
this.candle.style.color = '#D1D4DC';
this.candle.style.width = '100%';
this.contentWrapper.appendChild(this.text);
this.contentWrapper.appendChild(this.candle);
this.div.appendChild(buttonsContainer);
this.div.appendChild(this.contentWrapper);
handler.div.appendChild(this.div);
handler.chart.subscribeCrosshairMove(this.legendHandler)
}
private toggleCollapse() {
this.isCollapsed = !this.isCollapsed;
if (this.isCollapsed) {
this.contentWrapper.style.display = 'none';
this.div.style.maxHeight = '40px'; // Changed from 'auto' to fixed height for buttons
this.div.style.height = '40px'; // Added fixed height
this.collapseButton.innerHTML = '+';
} else {
this.contentWrapper.style.display = 'flex';
this.div.style.maxHeight = '300px';
this.div.style.height = 'auto'; // Reset height to auto
this.collapseButton.innerHTML = '';
}
}
private toggleAll() {
this.allVisible = !this.allVisible;
this._lines.forEach(line => {
line.series.applyOptions({
visible: this.allVisible
});
const group = line.toggle.querySelector('g');
if (group) {
if (this.allVisible) {
group.innerHTML = this.openEyeSvg(line.solid);
} else {
group.innerHTML = this.closedEyeSvg(line.solid);
}
}
});
this.toggleAllButton.style.opacity = this.allVisible ? '1' : '0.5';
}
private openEyeSvg(strokeColor: string): string {
return `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${strokeColor};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:${strokeColor};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)"/>
`;
}
private closedEyeSvg(strokeColor: string): string {
return `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${strokeColor};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)"/>
`;
}
toJSON() {
// Exclude the chart attribute from serialization
const {_lines, handler, ...serialized} = this;
return serialized;
}
// makeSeriesRows(handler: Handler) {
// if (this.linesEnabled) handler._seriesList.forEach(s => this.makeSeriesRow(s))
// }
makeSeriesRow(name: string, series: ISeriesApi<SeriesType>) {
const strokeColor = series.options().color;
let openEye = `
<path style="fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke:${strokeColor};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:${strokeColor};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:${strokeColor};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'
row.style.padding = '0px 0'
row.style.color = '#D1D4DC'
row.style.width = '100%'
row.style.pointerEvents = 'all'
row.style.cursor = 'default'
let div = document.createElement('div')
div.style.flex = '1'
let toggle = document.createElement('div')
toggle.classList.add('legend-toggle-switch');
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
group.innerHTML = this.openEyeSvg(strokeColor);
let on = true
toggle.addEventListener('click', () => {
if (on) {
on = false
group.innerHTML = closedEye
group.innerHTML = this.closedEyeSvg(strokeColor);
series.applyOptions({
visible: false
})
@ -100,7 +244,7 @@ export class Legend {
series.applyOptions({
visible: true
})
group.innerHTML = openEye
group.innerHTML = this.openEyeSvg(strokeColor);
}
})
@ -108,7 +252,7 @@ export class Legend {
toggle.appendChild(svg);
row.appendChild(div)
row.appendChild(toggle)
this.div.appendChild(row)
this.contentWrapper.appendChild(row)
const color = series.options().color;
this._lines.push({
@ -121,7 +265,9 @@ export class Legend {
});
}
legendItemFormat(num: number, decimal: number) { return num.toFixed(decimal).toString().padStart(8, ' ') }
legendItemFormat(num: number, decimal: number) {
return num.toFixed(decimal).toString().padStart(8, ' ')
}
shorthandFormat(num: number) {
const absNum = Math.abs(num)
@ -132,7 +278,6 @@ export class Legend {
}
return num.toString().padStart(8, ' ');
}
legendHandler(param: MouseEventParams, usingPoint= false) {
if (!this.ohlcEnabled && !this.linesEnabled && !this.percentEnabled) return;
const options: any = this.handler.series.options()
@ -159,7 +304,7 @@ export class Legend {
}
this.candle.style.color = ''
let str = '<span style="line-height: 1.8;">'
let str = '<span style="line-height: 1.0;">'
if (data) {
if (this.ohlcEnabled) {
str += `O ${this.legendItemFormat(data.open, this.handler.precision)} `
@ -209,13 +354,13 @@ export class Legend {
else {
data = param.seriesData.get(e.series) as LineData
}
if (!data?.value) return;
if (data === undefined || data.value === undefined) return;
let price;
if (e.series.seriesType() == 'Histogram') {
price = this.shorthandFormat(data.value)
} else {
const format = e.series.options().priceFormat as PriceFormatBuiltIn
price = this.legendItemFormat(data.value, format.precision) // couldn't this just be line.options().precision?
price = this.legendItemFormat(data.value, format.precision)
}
e.div.innerHTML = `<span style="color: ${e.solid};">▨ ${e.name} : ${price}</span>`
})