Compare commits
48 Commits
3a859c97fc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c72818dae | |||
| 92acc2b96a | |||
| ada85883c7 | |||
| 3adf0b9ce3 | |||
| 1551f6f904 | |||
| 33c2d47858 | |||
| cab85bb4f8 | |||
| 37af631a3e | |||
| f1024d551f | |||
| 2b607f96be | |||
| 81996a1891 | |||
| e526940717 | |||
| 0e88137927 | |||
| 5407e22bd6 | |||
| ef192e82f9 | |||
| 1f4aa4fa8e | |||
| 1deb397e28 | |||
| 2c656fa640 | |||
| 6393e618ce | |||
| b336857832 | |||
| e752ef8fdd | |||
| 1c2afbf93b | |||
| 3f2a484cd7 | |||
| 7b0acec3e6 | |||
| a9cb8da66e | |||
| 9fca26db4b | |||
| dfe1eafba9 | |||
| 8b9f3ad61f | |||
| 0da6839bcb | |||
| c4356bef3a | |||
| 7986aa9195 | |||
| 35f029714b | |||
| 2b9f238a42 | |||
| bbbd37520d | |||
| 51fc616f82 | |||
| 197936d69f | |||
| 789d27c54b | |||
| f90e7789ab | |||
| eb2c24e157 | |||
| c27401a59d | |||
| da029fcbb7 | |||
| ea2f53b794 | |||
| 19b1834a6d | |||
| 6de9ba9c33 | |||
| 105ea66731 | |||
| 9b8e43e1e8 | |||
| 9f9f22eb93 | |||
| eb56a889a9 |
137
README.md
@ -1,17 +1,140 @@
|
|||||||
Fork of lightweight-charts with enhancements and supporting proprietary workflow
|
Fork of the original [lightweight-charts](louisnw01/lightweight-charts-python) with enhancements and supporting `vectorbtpro` workflow
|
||||||
* colored legends
|
* legend color matching each line color
|
||||||
* self picking colors
|
* automatic color assignment if not provided
|
||||||
* support for left price scale
|
* support for multiple scales (right, left, middle1, middle2, histogram)
|
||||||
* support from pd series as input to set series
|
* 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
|
* new markers_set method allowing to set pd.series or dataframe as markers input
|
||||||
* allows vbt indicators as input for set() method (extracting real values series from that)
|
* 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.
|
||||||
|
|
||||||
|
|
||||||
<img width="1005" alt="image" src="https://github.com/drew2323/lightweight-charts-python/assets/28433232/856c32aa-e0ff-4de0-b4a2-befc34adb571">
|
<img width="1005" alt="image" src="https://github.com/drew2323/lightweight-charts-python/assets/28433232/856c32aa-e0ff-4de0-b4a2-befc34adb571">
|
||||||
|
|
||||||
|
## Instalation
|
||||||
|
Directly from the repository
|
||||||
|
|
||||||
|
```pip install git+https://github.com/drew2323/lightweight-charts-python.git```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lightweight_charts import chart, Panel, PlotSRAccessor, PlotDFAccessor
|
||||||
|
|
||||||
|
#one liner, displays close series as line on single Panel
|
||||||
|
close_sr.lw.plot()
|
||||||
|
```
|
||||||
|

|
||||||
|
```python
|
||||||
|
close_sr.lw.plot(size="m") #on medium panesize
|
||||||
|
close_se.lw.plot(histogram=(trade_series, "trades")) #number of trades as histogram is displayed on top of that
|
||||||
|
```
|
||||||
|

|
||||||
|
```python
|
||||||
|
#one liner to display OHLCV
|
||||||
|
ohlcv_df.lw.plot() #if dataframe contains other columns they are displayed too - based on standard settings (rsi-on the left, vwap - right etc.)
|
||||||
|
```
|
||||||
|

|
||||||
|
```python
|
||||||
|
ohlcv_df.lw.plot(left=[(angle_series, "angle_momentum")]) #Another line is displayed on top of OHLCV on left scale
|
||||||
|
```
|
||||||
|

|
||||||
|
```python
|
||||||
|
ohlcv_complex_df.lw.plot() #df containing ohlcv and other columns
|
||||||
|
```
|
||||||
|

|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
```
|
||||||
|

|
||||||
|
```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")
|
||||||
|
```
|
||||||
|

|
||||||
|
```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
|
||||||
|
pane1 = Panel(
|
||||||
|
ohlcv=(t1data.data["BAC"],), #(series, entries, exits, other_markers)
|
||||||
|
histogram=[(order_imbalance_allvolume, "oivol",None, 0.3)], # [(series, name, "rgba(53, 94, 59, 0.6)", opacity)]
|
||||||
|
|
||||||
|
#following attributes assign lineseries series to different priceScaleIds, format: # [(series, name, entries, exits, other_markers)]
|
||||||
|
right=[(bbands)], #multioutput indicator, outputs are autom.extracted
|
||||||
|
left=[(sma, "sma", short_signals, short_exits) #simple vbt indicator with markers in and outs
|
||||||
|
(supertrend.trend, "ST_trend") #explicitly just one output of multioutput indicator
|
||||||
|
],
|
||||||
|
middle1=[],
|
||||||
|
middle2=[],
|
||||||
|
)
|
||||||
|
#on second panel displays also ohlcv data, and sma on the left pricescale and histogram
|
||||||
|
pane2 = Panel(
|
||||||
|
ohlcv=(t1data.data["BAC"],),
|
||||||
|
right=[],
|
||||||
|
left=[(sma, "sma_below", short_signals, short_exits)],
|
||||||
|
middle1=[],
|
||||||
|
middle2=[],
|
||||||
|
histogram=[(order_imbalance_sma, "oisma")],
|
||||||
|
)
|
||||||
|
|
||||||
|
#display both Panels, sync them and pick size, use xloc
|
||||||
|
ch = chart([pane1, pane2], sync=True, title="Title", size="l", xloc=slice("1-1-2024","1-2-2024"))
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example with markers
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
#assume i want to display simple entries or exits on series or ohlcv
|
||||||
|
#based on tuple positions it determines entries or exits (and set colors and shape accordingly)
|
||||||
|
pane1 = Panel(
|
||||||
|
ohlcv=(ohlcv_df, clean_long_entries, clean_short_entries)
|
||||||
|
)
|
||||||
|
ch = chart([pane1], title="Chart with Entry/Exit Markers", session=None, size="s")
|
||||||
|
```
|
||||||
|
<img width="798" alt="image" src="https://github.com/drew2323/lightweight-charts-python/assets/28433232/0cdc3930-b8b1-40f8-af95-55467879148b">
|
||||||
|
|
||||||
|
```python
|
||||||
|
#if you want to display more entries or exits, use tuples with their colors
|
||||||
|
|
||||||
|
# Create Panel with OHLC data and entry signals
|
||||||
|
pane1 = Panel(
|
||||||
|
ohlcv=(data.ohlcv.get(),
|
||||||
|
[(clean_long_entries, "yellow"), (clean_short_entries, "pink")], #list of entries tuples with color
|
||||||
|
[(clean_long_exits, "yellow"), (clean_short_exits, "pink")]), #list of exits tuples with color
|
||||||
|
)
|
||||||
|
|
||||||
|
# # Create the chart with the panel
|
||||||
|
ch = chart([pane1], title="Chart with EntryShort/ExitShort (yellow) and EntryLong/ExitLong markers (pink)", session=None, size="s")
|
||||||
|
```
|
||||||
|
<img width="800" alt="image" src="https://github.com/drew2323/lightweight-charts-python/assets/28433232/0acde2bf-600e-4f45-8db0-5077822d6993">
|
||||||
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
# lightweight-charts-python
|
# lightweight-charts-python - forked from
|
||||||
|
|
||||||
[](https://pypi.org/project/lightweight-charts/)
|
[](https://pypi.org/project/lightweight-charts/)
|
||||||
[](https://python.org "Go to Python homepage")
|
[](https://python.org "Go to Python homepage")
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 555 KiB After Width: | Height: | Size: 555 KiB |
|
Before Width: | Height: | Size: 5.0 MiB After Width: | Height: | Size: 5.0 MiB |
|
Before Width: | Height: | Size: 349 KiB After Width: | Height: | Size: 349 KiB |
|
Before Width: | Height: | Size: 612 KiB After Width: | Height: | Size: 612 KiB |
|
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 510 KiB |
|
Before Width: | Height: | Size: 475 KiB After Width: | Height: | Size: 475 KiB |
BIN
image-1.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
image-2.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
image-3.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
image-4.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
image-5.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
image-6.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
image-7.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
@ -2,4 +2,4 @@ from .abstract import AbstractChart, Window
|
|||||||
from .chart import Chart
|
from .chart import Chart
|
||||||
from .widgets import JupyterChart
|
from .widgets import JupyterChart
|
||||||
from .polygon import PolygonChart
|
from .polygon import PolygonChart
|
||||||
from .helpers import chart, Panel, PlotAccessor
|
from .helpers import chart, Panel, PlotSRAccessor, PlotDFAccessor
|
||||||
@ -202,7 +202,7 @@ class SeriesCommon(Pane):
|
|||||||
self._set_interval(df)
|
self._set_interval(df)
|
||||||
if not pd.api.types.is_datetime64_any_dtype(df['time']):
|
if not pd.api.types.is_datetime64_any_dtype(df['time']):
|
||||||
df['time'] = pd.to_datetime(df['time'])
|
df['time'] = pd.to_datetime(df['time'])
|
||||||
df['time'] = df['time'].astype('int64') / 10 ** 9 #removed integer divison // 10 ** 9 to keep subseconds precision
|
df['time'] = (df['time'].astype('int64') / 10 ** 9).astype(float)
|
||||||
|
|
||||||
# if 'updated' in df.columns:
|
# if 'updated' in df.columns:
|
||||||
# df['updated'] = pd.to_datetime(df['updated']).astype('int64') / 10**9
|
# df['updated'] = pd.to_datetime(df['updated']).astype('int64') / 10**9
|
||||||
@ -213,7 +213,7 @@ class SeriesCommon(Pane):
|
|||||||
if col == 'time' or not pd.api.types.is_datetime64_any_dtype(df[col]):
|
if col == 'time' or not pd.api.types.is_datetime64_any_dtype(df[col]):
|
||||||
continue
|
continue
|
||||||
# Convert datetime to timestamp with nanosecond precision after the decimal
|
# Convert datetime to timestamp with nanosecond precision after the decimal
|
||||||
df[col] = pd.to_datetime(df[col]).astype('int64') / 10**9
|
df[col] = (pd.to_datetime(df[col]).astype('int64') / 10**9).astype(float)
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
@ -245,9 +245,12 @@ class SeriesCommon(Pane):
|
|||||||
if format_cols:
|
if format_cols:
|
||||||
df = self._df_datetime_format(df, exclude_lowercase=self.name)
|
df = self._df_datetime_format(df, exclude_lowercase=self.name)
|
||||||
if 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}".')
|
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.data = df.copy()
|
||||||
self._last_bar = df.iloc[-1]
|
self._last_bar = df.iloc[-1]
|
||||||
self.run_script(f'{self.id}.series.setData({js_data(df)}); ')
|
self.run_script(f'{self.id}.series.setData({js_data(df)}); ')
|
||||||
@ -646,7 +649,7 @@ class Candlestick(SeriesCommon):
|
|||||||
super().__init__(chart)
|
super().__init__(chart)
|
||||||
self._volume_up_color = 'rgba(83,141,131,0.8)'
|
self._volume_up_color = 'rgba(83,141,131,0.8)'
|
||||||
self._volume_down_color = 'rgba(200,127,130,0.8)'
|
self._volume_down_color = 'rgba(200,127,130,0.8)'
|
||||||
|
self.num_decimals = 2
|
||||||
self.candle_data = pd.DataFrame()
|
self.candle_data = pd.DataFrame()
|
||||||
|
|
||||||
# self.run_script(f'{self.id}.makeCandlestickSeries()')
|
# self.run_script(f'{self.id}.makeCandlestickSeries()')
|
||||||
@ -750,11 +753,25 @@ class Candlestick(SeriesCommon):
|
|||||||
text_color: Optional[str] = None,
|
text_color: Optional[str] = None,
|
||||||
entire_text_only: bool = False,
|
entire_text_only: bool = False,
|
||||||
visible: bool = True,
|
visible: bool = True,
|
||||||
ticks_visible: bool = False,
|
ticks_visible: bool = True,
|
||||||
minimum_width: int = 0
|
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.run_script(f'''
|
||||||
{self.id}.series.priceScale().applyOptions({{
|
{self.id}.series.priceScale().applyOptions({{
|
||||||
|
priceFormat: {{ precision: 3, minMove: 0.005 }},
|
||||||
autoScale: {jbool(auto_scale)},
|
autoScale: {jbool(auto_scale)},
|
||||||
mode: {as_enum(mode, PRICE_SCALE_MODE)},
|
mode: {as_enum(mode, PRICE_SCALE_MODE)},
|
||||||
invertScale: {jbool(invert_scale)},
|
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,
|
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',
|
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.
|
Configures the legend of the chart.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -1,18 +1,89 @@
|
|||||||
|
from ast import parse
|
||||||
from .widgets import JupyterChart
|
from .widgets import JupyterChart
|
||||||
from .util import (
|
from .util import (
|
||||||
is_vbt_indicator, get_next_color
|
is_vbt_indicator, get_next_color
|
||||||
)
|
)
|
||||||
import pandas as pd
|
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, auto_scale, kwargs):
|
||||||
|
"""
|
||||||
|
Mutate lists based on kwargs for accessor.
|
||||||
|
Used when user added additional series to kwargs when using accessor.
|
||||||
|
"""
|
||||||
|
if 'ohlcv' in kwargs:
|
||||||
|
ohlcv = kwargs['ohlcv'] #ohlcv is only a tuple
|
||||||
|
if 'left' in kwargs:
|
||||||
|
append_or_extend(left, kwargs['left'])
|
||||||
|
if 'right' in kwargs:
|
||||||
|
append_or_extend(right, kwargs['right'])
|
||||||
|
if 'histogram' in kwargs:
|
||||||
|
append_or_extend(histogram, kwargs['histogram'])
|
||||||
|
if 'middle1' in kwargs:
|
||||||
|
append_or_extend(middle1, kwargs['middle1'])
|
||||||
|
if 'middle2' in kwargs:
|
||||||
|
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
|
||||||
|
|
||||||
# Register the custom accessor
|
# Register the custom accessor
|
||||||
@pd.api.extensions.register_series_accessor("lw")
|
@pd.api.extensions.register_series_accessor("lw")
|
||||||
class PlotAccessor:
|
class PlotSRAccessor:
|
||||||
"""
|
"""
|
||||||
Custom plot accessor for pandas series.
|
Custom plot accessor for pandas series. Quickly displays series values as line on the single pane.
|
||||||
|
|
||||||
|
Also additional priceseries can be added on top of them. They can be added
|
||||||
|
for each scale in the correct format - either as tuple(OHLCV) or as list of tuple (others)
|
||||||
|
|
||||||
|
# input parameter / expected format:
|
||||||
|
# ohlcv=(), #(series, entries, exits, other_markers)
|
||||||
|
# histogram=[], # [(series, name, "rgba(53, 94, 59, 0.6)", opacity)]
|
||||||
|
# right=[],
|
||||||
|
# left=[], #[(series, name, entries, exits, other_markers)]
|
||||||
|
# middle1=[],
|
||||||
|
# middle2=[],
|
||||||
|
|
||||||
|
|
||||||
Usage: s
|
Usage: s
|
||||||
series.lw.plot()
|
series.lw.plot() #plot series as line
|
||||||
series.lw.plot(size="m")
|
series.lw.plot(size="m") #on medium panesize
|
||||||
|
series.lw.plot(histogram=(trade_series, "trades")) #plot histogram with trades on top of that
|
||||||
"""
|
"""
|
||||||
def __init__(self, pandas_obj):
|
def __init__(self, pandas_obj):
|
||||||
self._obj = pandas_obj
|
self._obj = pandas_obj
|
||||||
@ -20,11 +91,123 @@ class PlotAccessor:
|
|||||||
def plot(self, **kwargs):
|
def plot(self, **kwargs):
|
||||||
if "size" not in kwargs:
|
if "size" not in kwargs:
|
||||||
kwargs["size"] = "xs"
|
kwargs["size"] = "xs"
|
||||||
|
name = kwargs["name"] if "name" in kwargs else "line"
|
||||||
|
|
||||||
|
ohlcv = ()
|
||||||
|
right = []
|
||||||
|
left = []
|
||||||
|
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, auto_scale, kwargs)
|
||||||
|
|
||||||
|
right.append((self._obj,name))
|
||||||
|
|
||||||
pane1 = Panel(
|
pane1 = Panel(
|
||||||
right=[(self._obj, "line")],
|
auto_scale=auto_scale,
|
||||||
)
|
ohlcv=ohlcv,
|
||||||
|
histogram=histogram,
|
||||||
|
right=right,
|
||||||
|
left=left,
|
||||||
|
middle1=middle1,
|
||||||
|
middle2=middle2
|
||||||
|
)
|
||||||
|
|
||||||
ch = chart([pane1], **kwargs)
|
ch = chart([pane1], **kwargs)
|
||||||
|
|
||||||
|
@pd.api.extensions.register_dataframe_accessor("lw")
|
||||||
|
class PlotDFAccessor:
|
||||||
|
"""
|
||||||
|
Custom plot accessor for dataframe. Quickly displays all columns on the single pane.
|
||||||
|
|
||||||
|
Series type is automatically extracted for each column based on following setting:
|
||||||
|
scale / columns
|
||||||
|
ohlcv = ['close', 'volume', 'open', 'high', 'low']
|
||||||
|
right = ['vwap']
|
||||||
|
left = ['rsi']
|
||||||
|
middle1 = []
|
||||||
|
middle2 = []
|
||||||
|
histogram = ['buyvolume', 'sellvolume', 'trades']
|
||||||
|
|
||||||
|
Also additional priceseries can be added on top of them as parameters. They can be added
|
||||||
|
for each scale in the correct format - either as tuple(OHLCV) or as list of tuple (others)
|
||||||
|
|
||||||
|
# input parameter / expected format:
|
||||||
|
# ohlcv=(), #(series, entries, exits, other_markers)
|
||||||
|
# histogram=[], # [(series, name, "rgba(53, 94, 59, 0.6)", opacity)]
|
||||||
|
# right=[],
|
||||||
|
# left=[], #[(series, name, entries, exits, other_markers)]
|
||||||
|
# middle1=[],
|
||||||
|
# middle2=[],
|
||||||
|
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ohlcv_df.lw.plot()
|
||||||
|
ohlcv_df.lw.plot(size="m")
|
||||||
|
ohlcv_df.lw.plot(right=(rsi_series, "rsi"))
|
||||||
|
ohlcv_df.lw.plot(right=[(rsi_series, "rsi"), (angle_series, "angle")])
|
||||||
|
basic_data.data[SYMBOL].lw.plot(histogram=(basic_data.data[SYMBOL].close, "close"), size="m")
|
||||||
|
"""
|
||||||
|
def __init__(self, pandas_obj):
|
||||||
|
self._obj = pandas_obj
|
||||||
|
|
||||||
|
def plot(self, **kwargs):
|
||||||
|
if "size" not in kwargs:
|
||||||
|
kwargs["size"] = "xs"
|
||||||
|
|
||||||
|
ohlcv = ()
|
||||||
|
right = []
|
||||||
|
left = []
|
||||||
|
middle1 = []
|
||||||
|
middle2 = []
|
||||||
|
histogram = []
|
||||||
|
auto_scale = []
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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, auto_scale, kwargs)
|
||||||
|
|
||||||
|
pane1 = Panel(
|
||||||
|
auto_scale=auto_scale,
|
||||||
|
ohlcv=ohlcv,
|
||||||
|
histogram=histogram,
|
||||||
|
right=right,
|
||||||
|
left=left,
|
||||||
|
middle1=middle1,
|
||||||
|
middle2=middle2
|
||||||
|
)
|
||||||
|
|
||||||
|
ch = chart([pane1], **kwargs)
|
||||||
|
|
||||||
|
# pane1 = Panel(
|
||||||
|
# ohlcv=(), #(series, entries, exits, other_markers)
|
||||||
|
# histogram=[], # [(series, name, "rgba(53, 94, 59, 0.6)", opacity)]
|
||||||
|
# right=[],
|
||||||
|
# left=[], #[(series, name, entries, exits, other_markers)]
|
||||||
|
# middle1=[],
|
||||||
|
# middle2=[],
|
||||||
|
# )
|
||||||
|
|
||||||
class Panel:
|
class Panel:
|
||||||
"""
|
"""
|
||||||
A class to represent a panel in a chart.
|
A class to represent a panel in a chart.
|
||||||
@ -44,6 +227,7 @@ class Panel:
|
|||||||
* left : list of tuples, optional
|
* left : list of tuples, optional
|
||||||
* middle1 : list of tuples, optional
|
* middle1 : list of tuples, optional
|
||||||
* middle2 : 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.
|
* 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.
|
* precision: int, optional. The number of digits after the decimal point. Apply to all lines on this pane. Default is None.
|
||||||
|
|
||||||
@ -62,6 +246,18 @@ class Panel:
|
|||||||
|
|
||||||
ch = chart([pane1])
|
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
|
# Synced example
|
||||||
pane1 = Panel(
|
pane1 = Panel(
|
||||||
ohlcv=(t1data.data["BAC"],), #(series, entries, exits, other_markers)
|
ohlcv=(t1data.data["BAC"],), #(series, entries, exits, other_markers)
|
||||||
@ -75,6 +271,7 @@ class Panel:
|
|||||||
)
|
)
|
||||||
|
|
||||||
pane2 = Panel(
|
pane2 = Panel(
|
||||||
|
auto_scale=[macd_vbt_ind],
|
||||||
ohlcv=(t1data.data["BAC"],),
|
ohlcv=(t1data.data["BAC"],),
|
||||||
right=[],
|
right=[],
|
||||||
left=[(sma, "sma_below", short_signals, short_exits)],
|
left=[(sma, "sma_below", short_signals, short_exits)],
|
||||||
@ -106,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")
|
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.ohlcv = ohlcv if ohlcv is not None else ()
|
||||||
self.right = right if right is not None else []
|
self.right = right if right is not None else []
|
||||||
self.left = left if left is not None else []
|
self.left = left if left is not None else []
|
||||||
@ -117,8 +315,10 @@ class Panel:
|
|||||||
self.xloc = xloc
|
self.xloc = xloc
|
||||||
self.precision = precision
|
self.precision = precision
|
||||||
|
|
||||||
|
def chart(self, **kwargs):
|
||||||
def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session: str="9:30:00", precision=None):
|
chart([self], **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
|
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
|
output or interfaces with an external framework to display charts with synchronized
|
||||||
@ -144,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.
|
* 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.
|
* xloc (str): xloc advanced filtering of vbt.xloc accessor. Defaults to None. Applies to all panes.
|
||||||
Might be overriden by pane-specific xloc.
|
Might be overriden by pane-specific xloc.
|
||||||
|
|
||||||
@ -230,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)
|
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 != ():
|
if pane.ohlcv != ():
|
||||||
series, entries, exits, markers = (pane.ohlcv + (None,) * 4)[:4]
|
series, entries, exits, markers = (pane.ohlcv + (None,) * 4)[:4]
|
||||||
if series is None:
|
if series is None:
|
||||||
@ -252,8 +527,19 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
|
|||||||
kwargs['color'] = color
|
kwargs['color'] = color
|
||||||
if opacity is not None:
|
if opacity is not None:
|
||||||
kwargs['opacity'] = opacity
|
kwargs['opacity'] = opacity
|
||||||
tmp = active_chart.create_histogram(**kwargs) #green transparent "rgba(53, 94, 59, 0.6)"
|
if isinstance(series, pd.DataFrame) and isinstance(series.columns, pd.MultiIndex): #multiindex handling
|
||||||
tmp.set(xloc_me(series, xloc))
|
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:
|
if pane.title is not None:
|
||||||
active_chart.topbar.textbox("title",pane.title)
|
active_chart.topbar.textbox("title",pane.title)
|
||||||
@ -261,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
|
#iterate over keys - they are all priceScaleId except of these
|
||||||
for att_name, att_value_tuple in vars(pane).items():
|
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
|
continue
|
||||||
for tup in att_value_tuple:
|
for tup in att_value_tuple:
|
||||||
series, name, entries, exits, markers = (tup + (None, None, None, None, None))[:5]
|
series, name, entries, exits, markers = (tup + (None, None, None, None, None))[:5]
|
||||||
@ -273,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
|
series = series.xloc[xloc] if xloc is not None else series
|
||||||
for output in series.output_names:
|
for output in series.output_names:
|
||||||
output_series = getattr(series, output)
|
output_series = getattr(series, output)
|
||||||
output = name + ':' + output if name is not None else output
|
output = name + ':' + output if name is not None else series.short_name + ":" + output
|
||||||
tmp = active_chart.create_line(name=output, priceScaleId=att_name)#, color="blue")
|
#if output_series is multiindex - create aline for each combination
|
||||||
tmp.set(output_series)
|
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:
|
else:
|
||||||
if name is None:
|
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 = active_chart.create_line(name=name, priceScaleId=att_name)#, color="blue")
|
||||||
tmp.set(xloc_me(series, xloc))
|
tmp.set(xloc_me(series, xloc))
|
||||||
@ -296,8 +641,22 @@ def chart(panes: list[Panel], sync=False, title='', size="m", xloc=None, session
|
|||||||
active_chart.legend(True)
|
active_chart.legend(True)
|
||||||
active_chart.fit()
|
active_chart.fit()
|
||||||
if session is not None and session:
|
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
|
try:
|
||||||
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)")
|
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:
|
if not main_title_set:
|
||||||
chartX.topbar.textbox("title",title)
|
chartX.topbar.textbox("title",title)
|
||||||
|
|||||||
@ -113,7 +113,7 @@ def is_vbt_indicator(variable):
|
|||||||
# Get the module path of the variable's type
|
# Get the module path of the variable's type
|
||||||
module_path = variable.__class__.__module__
|
module_path = variable.__class__.__module__
|
||||||
# Check if it starts with 'vectorbtpro.indicators'
|
# 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:
|
class Pane:
|
||||||
def __init__(self, window):
|
def __init__(self, window):
|
||||||
|
|||||||
2
setup.py
@ -5,7 +5,7 @@ with open('README.md', 'r', encoding='utf-8') as f:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='lightweight_charts',
|
name='lightweight_charts',
|
||||||
version='2.1.4',
|
version='2.2.29',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
python_requires='>=3.8',
|
python_requires='>=3.8',
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export class Handler {
|
|||||||
|
|
||||||
public chart: IChartApi;
|
public chart: IChartApi;
|
||||||
public scale: Scale;
|
public scale: Scale;
|
||||||
public precision: number = 2;
|
public precision: number = 3;
|
||||||
|
|
||||||
public series: ISeriesApi<SeriesType>;
|
public series: ISeriesApi<SeriesType>;
|
||||||
public volumeSeries: ISeriesApi<SeriesType>;
|
public volumeSeries: ISeriesApi<SeriesType>;
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts";
|
import { ISeriesApi, LineData, Logical, MouseEventParams, PriceFormatBuiltIn, SeriesType } from "lightweight-charts";
|
||||||
import { Handler } from "./handler";
|
import { Handler } from "./handler";
|
||||||
|
|
||||||
|
|
||||||
interface LineElement {
|
interface LineElement {
|
||||||
name: string;
|
name: string;
|
||||||
div: HTMLDivElement;
|
div: HTMLDivElement;
|
||||||
@ -14,6 +13,11 @@ interface LineElement {
|
|||||||
export class Legend {
|
export class Legend {
|
||||||
private handler: Handler;
|
private handler: Handler;
|
||||||
public div: HTMLDivElement;
|
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 ohlcEnabled: boolean = false;
|
||||||
private percentEnabled: boolean = false;
|
private percentEnabled: boolean = false;
|
||||||
@ -24,74 +28,214 @@ export class Legend {
|
|||||||
private candle: HTMLDivElement;
|
private candle: HTMLDivElement;
|
||||||
public _lines: LineElement[] = [];
|
public _lines: LineElement[] = [];
|
||||||
|
|
||||||
|
|
||||||
constructor(handler: Handler) {
|
constructor(handler: Handler) {
|
||||||
this.legendHandler = this.legendHandler.bind(this)
|
this.legendHandler = this.legendHandler.bind(this)
|
||||||
|
|
||||||
this.handler = handler;
|
this.handler = handler;
|
||||||
this.ohlcEnabled = false;
|
this.ohlcEnabled = false;
|
||||||
this.percentEnabled = false
|
this.percentEnabled = false
|
||||||
this.linesEnabled = false
|
this.linesEnabled = false
|
||||||
this.colorBasedOnCandle = false
|
this.colorBasedOnCandle = false
|
||||||
|
|
||||||
this.div = document.createElement('div');
|
this.div = document.createElement('div');
|
||||||
this.div.classList.add('legend');
|
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.display = 'none';
|
||||||
|
this.div.style.pointerEvents = 'all';
|
||||||
|
|
||||||
this.text = document.createElement('span')
|
const buttonsContainer = document.createElement('div');
|
||||||
this.text.style.lineHeight = '1.8'
|
buttonsContainer.style.position = 'absolute';
|
||||||
this.candle = document.createElement('div')
|
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.collapseButton = document.createElement('div');
|
||||||
this.div.appendChild(this.candle)
|
this.collapseButton.style.cursor = 'pointer';
|
||||||
handler.div.appendChild(this.div)
|
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)
|
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() {
|
toJSON() {
|
||||||
// Exclude the chart attribute from serialization
|
|
||||||
const {_lines, handler, ...serialized} = this;
|
const {_lines, handler, ...serialized} = this;
|
||||||
return serialized;
|
return serialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeSeriesRows(handler: Handler) {
|
|
||||||
// if (this.linesEnabled) handler._seriesList.forEach(s => this.makeSeriesRow(s))
|
|
||||||
// }
|
|
||||||
|
|
||||||
makeSeriesRow(name: string, series: ISeriesApi<SeriesType>) {
|
makeSeriesRow(name: string, series: ISeriesApi<SeriesType>) {
|
||||||
const strokeColor = series.options().color;
|
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')
|
let row = document.createElement('div')
|
||||||
row.style.display = 'flex'
|
row.style.display = 'flex'
|
||||||
row.style.alignItems = 'center'
|
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')
|
let div = document.createElement('div')
|
||||||
|
div.style.flex = '1'
|
||||||
let toggle = document.createElement('div')
|
let toggle = document.createElement('div')
|
||||||
toggle.classList.add('legend-toggle-switch');
|
toggle.classList.add('legend-toggle-switch');
|
||||||
|
|
||||||
|
|
||||||
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
svg.setAttribute("width", "22");
|
svg.setAttribute("width", "22");
|
||||||
svg.setAttribute("height", "16");
|
svg.setAttribute("height", "16");
|
||||||
|
|
||||||
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
let group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
||||||
group.innerHTML = openEye
|
group.innerHTML = this.openEyeSvg(strokeColor);
|
||||||
|
|
||||||
let on = true
|
let on = true
|
||||||
toggle.addEventListener('click', () => {
|
toggle.addEventListener('click', () => {
|
||||||
if (on) {
|
if (on) {
|
||||||
on = false
|
on = false
|
||||||
group.innerHTML = closedEye
|
group.innerHTML = this.closedEyeSvg(strokeColor);
|
||||||
series.applyOptions({
|
series.applyOptions({
|
||||||
visible: false
|
visible: false
|
||||||
})
|
})
|
||||||
@ -100,7 +244,7 @@ export class Legend {
|
|||||||
series.applyOptions({
|
series.applyOptions({
|
||||||
visible: true
|
visible: true
|
||||||
})
|
})
|
||||||
group.innerHTML = openEye
|
group.innerHTML = this.openEyeSvg(strokeColor);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -108,7 +252,7 @@ export class Legend {
|
|||||||
toggle.appendChild(svg);
|
toggle.appendChild(svg);
|
||||||
row.appendChild(div)
|
row.appendChild(div)
|
||||||
row.appendChild(toggle)
|
row.appendChild(toggle)
|
||||||
this.div.appendChild(row)
|
this.contentWrapper.appendChild(row)
|
||||||
|
|
||||||
const color = series.options().color;
|
const color = series.options().color;
|
||||||
this._lines.push({
|
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) {
|
shorthandFormat(num: number) {
|
||||||
const absNum = Math.abs(num)
|
const absNum = Math.abs(num)
|
||||||
@ -132,20 +278,19 @@ export class Legend {
|
|||||||
}
|
}
|
||||||
return num.toString().padStart(8, ' ');
|
return num.toString().padStart(8, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
legendHandler(param: MouseEventParams, usingPoint= false) {
|
legendHandler(param: MouseEventParams, usingPoint= false) {
|
||||||
if (!this.ohlcEnabled && !this.linesEnabled && !this.percentEnabled) return;
|
if (!this.ohlcEnabled && !this.linesEnabled && !this.percentEnabled) return;
|
||||||
const options: any = this.handler.series.options()
|
const options: any = this.handler.series.options()
|
||||||
|
|
||||||
if (!param.time) {
|
if (!param.time) {
|
||||||
this.candle.style.color = 'transparent'
|
this.candle.style.color = 'transparent'
|
||||||
this.candle.innerHTML = this.candle.innerHTML.replace(options['upColor'], '').replace(options['downColor'], '')
|
this.candle.innerHTML = this.candle.innerHTML.replace(options['upColor'], '').replace(options['downColor'], '')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let data: any;
|
let data: any;
|
||||||
let logical: Logical | null = null;
|
let logical: Logical | null = null;
|
||||||
|
|
||||||
if (usingPoint) {
|
if (usingPoint) {
|
||||||
const timeScale = this.handler.chart.timeScale();
|
const timeScale = this.handler.chart.timeScale();
|
||||||
let coordinate = timeScale.timeToCoordinate(param.time)
|
let coordinate = timeScale.timeToCoordinate(param.time)
|
||||||
@ -157,9 +302,9 @@ export class Legend {
|
|||||||
else {
|
else {
|
||||||
data = param.seriesData.get(this.handler.series);
|
data = param.seriesData.get(this.handler.series);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.candle.style.color = ''
|
this.candle.style.color = ''
|
||||||
let str = '<span style="line-height: 1.8;">'
|
let str = '<span style="line-height: 1.0;">'
|
||||||
if (data) {
|
if (data) {
|
||||||
if (this.ohlcEnabled) {
|
if (this.ohlcEnabled) {
|
||||||
str += `O ${this.legendItemFormat(data.open, this.handler.precision)} `
|
str += `O ${this.legendItemFormat(data.open, this.handler.precision)} `
|
||||||
@ -167,19 +312,19 @@ export class Legend {
|
|||||||
str += `| L ${this.legendItemFormat(data.low, this.handler.precision)} `
|
str += `| L ${this.legendItemFormat(data.low, this.handler.precision)} `
|
||||||
str += `| C ${this.legendItemFormat(data.close, this.handler.precision)} `
|
str += `| C ${this.legendItemFormat(data.close, this.handler.precision)} `
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.percentEnabled) {
|
if (this.percentEnabled) {
|
||||||
let percentMove = ((data.close - data.open) / data.open) * 100
|
let percentMove = ((data.close - data.open) / data.open) * 100
|
||||||
let color = percentMove > 0 ? options['upColor'] : options['downColor']
|
let color = percentMove > 0 ? options['upColor'] : options['downColor']
|
||||||
let percentStr = `${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
|
let percentStr = `${percentMove >= 0 ? '+' : ''}${percentMove.toFixed(2)} %`
|
||||||
|
|
||||||
if (this.colorBasedOnCandle) {
|
if (this.colorBasedOnCandle) {
|
||||||
str += `| <span style="color: ${color};">${percentStr}</span>`
|
str += `| <span style="color: ${color};">${percentStr}</span>`
|
||||||
} else {
|
} else {
|
||||||
str += '| ' + percentStr
|
str += '| ' + percentStr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.handler.volumeSeries) {
|
if (this.handler.volumeSeries) {
|
||||||
let volumeData: any;
|
let volumeData: any;
|
||||||
if (logical) {
|
if (logical) {
|
||||||
@ -194,14 +339,14 @@ export class Legend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.candle.innerHTML = str + '</span>'
|
this.candle.innerHTML = str + '</span>'
|
||||||
|
|
||||||
this._lines.forEach((e) => {
|
this._lines.forEach((e) => {
|
||||||
if (!this.linesEnabled) {
|
if (!this.linesEnabled) {
|
||||||
e.row.style.display = 'none'
|
e.row.style.display = 'none'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.row.style.display = 'flex'
|
e.row.style.display = 'flex'
|
||||||
|
|
||||||
let data
|
let data
|
||||||
if (usingPoint && logical) {
|
if (usingPoint && logical) {
|
||||||
data = e.series.dataByIndex(logical) as LineData
|
data = e.series.dataByIndex(logical) as LineData
|
||||||
@ -209,15 +354,15 @@ export class Legend {
|
|||||||
else {
|
else {
|
||||||
data = param.seriesData.get(e.series) as LineData
|
data = param.seriesData.get(e.series) as LineData
|
||||||
}
|
}
|
||||||
if (!data?.value) return;
|
if (data === undefined || data.value === undefined) return;
|
||||||
let price;
|
let price;
|
||||||
if (e.series.seriesType() == 'Histogram') {
|
if (e.series.seriesType() == 'Histogram') {
|
||||||
price = this.shorthandFormat(data.value)
|
price = this.shorthandFormat(data.value)
|
||||||
} else {
|
} else {
|
||||||
const format = e.series.options().priceFormat as PriceFormatBuiltIn
|
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>`
|
e.div.innerHTML = `<span style="color: ${e.solid};">▨ ${e.name} : ${price}</span>`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||