bn-lightweight-charts 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bn_lightweight_charts/__init__.py +7 -0
- bn_lightweight_charts/abstract.py +1001 -0
- bn_lightweight_charts/chart.py +241 -0
- bn_lightweight_charts/drawings.py +278 -0
- bn_lightweight_charts/js/bundle.dev.js +2472 -0
- bn_lightweight_charts/js/bundle.js +1 -0
- bn_lightweight_charts/js/index.html +25 -0
- bn_lightweight_charts/js/index_bn.html +144 -0
- bn_lightweight_charts/js/lightweight-charts.js +15475 -0
- bn_lightweight_charts/js/lightweight-charts.standalone.development.js +15475 -0
- bn_lightweight_charts/js/styles.css +257 -0
- bn_lightweight_charts/polygon.py +470 -0
- bn_lightweight_charts/table.py +138 -0
- bn_lightweight_charts/toolbox.py +45 -0
- bn_lightweight_charts/topbar.py +128 -0
- bn_lightweight_charts/util.py +227 -0
- bn_lightweight_charts/widgets.py +357 -0
- bn_lightweight_charts-0.2.0.dist-info/METADATA +317 -0
- bn_lightweight_charts-0.2.0.dist-info/RECORD +21 -0
- bn_lightweight_charts-0.2.0.dist-info/WHEEL +4 -0
- bn_lightweight_charts-0.2.0.dist-info/licenses/LICENSE +22 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Dict, Literal
|
|
3
|
+
|
|
4
|
+
from .util import jbool, Pane
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
ALIGN = Literal['left', 'right']
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Widget(Pane):
|
|
11
|
+
def __init__(self, topbar, value, func: callable = None, convert_boolean=False):
|
|
12
|
+
super().__init__(topbar.win)
|
|
13
|
+
self.value = value
|
|
14
|
+
|
|
15
|
+
def wrapper(v):
|
|
16
|
+
if convert_boolean:
|
|
17
|
+
self.value = False if v == 'false' else True
|
|
18
|
+
else:
|
|
19
|
+
self.value = v
|
|
20
|
+
func(topbar._chart)
|
|
21
|
+
|
|
22
|
+
async def async_wrapper(v):
|
|
23
|
+
self.value = v
|
|
24
|
+
await func(topbar._chart)
|
|
25
|
+
|
|
26
|
+
self.win.handlers[self.id] = async_wrapper if asyncio.iscoroutinefunction(func) else wrapper
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TextWidget(Widget):
|
|
30
|
+
def __init__(self, topbar, initial_text, align, func):
|
|
31
|
+
super().__init__(topbar, value=initial_text, func=func)
|
|
32
|
+
|
|
33
|
+
callback_name = f'"{self.id}"' if func else ''
|
|
34
|
+
|
|
35
|
+
self.run_script(f'{self.id} = {topbar.id}.makeTextBoxWidget("{initial_text}", "{align}", {callback_name})')
|
|
36
|
+
|
|
37
|
+
def set(self, string):
|
|
38
|
+
self.value = string
|
|
39
|
+
self.run_script(f'{self.id}.innerText = "{string}"')
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SwitcherWidget(Widget):
|
|
43
|
+
def __init__(self, topbar, options, default, align, func):
|
|
44
|
+
super().__init__(topbar, value=default, func=func)
|
|
45
|
+
self.options = list(options)
|
|
46
|
+
self.run_script(f'{self.id} = {topbar.id}.makeSwitcher({self.options}, "{default}", "{self.id}", "{align}")')
|
|
47
|
+
|
|
48
|
+
def set(self, option):
|
|
49
|
+
if option not in self.options:
|
|
50
|
+
raise ValueError(f"option '{option}' does not exist within {self.options}.")
|
|
51
|
+
self.run_script(f'{self.id}.onItemClicked("{option}")')
|
|
52
|
+
self.value = option
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MenuWidget(Widget):
|
|
56
|
+
def __init__(self, topbar, options, default, separator, align, func):
|
|
57
|
+
super().__init__(topbar, value=default, func=func)
|
|
58
|
+
self.options = list(options)
|
|
59
|
+
self.run_script(f'''
|
|
60
|
+
{self.id} = {topbar.id}.makeMenu({list(options)}, "{default}", {jbool(separator)}, "{self.id}", "{align}")
|
|
61
|
+
''')
|
|
62
|
+
|
|
63
|
+
# TODO this will probably need to be fixed
|
|
64
|
+
def set(self, option):
|
|
65
|
+
if option not in self.options:
|
|
66
|
+
raise ValueError(f"Option {option} not in menu options ({self.options})")
|
|
67
|
+
self.value = option
|
|
68
|
+
self.run_script(f'''
|
|
69
|
+
{self.id}._clickHandler("{option}")
|
|
70
|
+
''')
|
|
71
|
+
# self.win.handlers[self.id](option)
|
|
72
|
+
|
|
73
|
+
def update_items(self, *items: str):
|
|
74
|
+
self.options = list(items)
|
|
75
|
+
self.run_script(f'{self.id}.updateMenuItems({self.options})')
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ButtonWidget(Widget):
|
|
79
|
+
def __init__(self, topbar, button, separator, align, toggle, func):
|
|
80
|
+
super().__init__(topbar, value=False, func=func, convert_boolean=toggle)
|
|
81
|
+
self.run_script(
|
|
82
|
+
f'{self.id} = {topbar.id}.makeButton("{button}", "{self.id}", {jbool(separator)}, true, "{align}", {jbool(toggle)})')
|
|
83
|
+
|
|
84
|
+
def set(self, string):
|
|
85
|
+
# self.value = string
|
|
86
|
+
self.run_script(f'{self.id}.elem.innerText = "{string}"')
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class TopBar(Pane):
|
|
90
|
+
def __init__(self, chart):
|
|
91
|
+
super().__init__(chart.win)
|
|
92
|
+
self._chart = chart
|
|
93
|
+
self._widgets: Dict[str, Widget] = {}
|
|
94
|
+
self._created = False
|
|
95
|
+
|
|
96
|
+
def _create(self):
|
|
97
|
+
if self._created:
|
|
98
|
+
return
|
|
99
|
+
self._created = True
|
|
100
|
+
self.run_script(f'{self.id} = {self._chart.id}.createTopBar()')
|
|
101
|
+
|
|
102
|
+
def __getitem__(self, item):
|
|
103
|
+
if widget := self._widgets.get(item):
|
|
104
|
+
return widget
|
|
105
|
+
raise KeyError(f'Topbar widget "{item}" not found.')
|
|
106
|
+
|
|
107
|
+
def get(self, widget_name):
|
|
108
|
+
return self._widgets.get(widget_name)
|
|
109
|
+
|
|
110
|
+
def switcher(self, name, options: tuple, default: str = None,
|
|
111
|
+
align: ALIGN = 'left', func: callable = None):
|
|
112
|
+
self._create()
|
|
113
|
+
self._widgets[name] = SwitcherWidget(self, options, default if default else options[0], align, func)
|
|
114
|
+
|
|
115
|
+
def menu(self, name, options: tuple, default: str = None, separator: bool = True,
|
|
116
|
+
align: ALIGN = 'left', func: callable = None):
|
|
117
|
+
self._create()
|
|
118
|
+
self._widgets[name] = MenuWidget(self, options, default if default else options[0], separator, align, func)
|
|
119
|
+
|
|
120
|
+
def textbox(self, name: str, initial_text: str = '',
|
|
121
|
+
align: ALIGN = 'left', func: callable = None):
|
|
122
|
+
self._create()
|
|
123
|
+
self._widgets[name] = TextWidget(self, initial_text, align, func)
|
|
124
|
+
|
|
125
|
+
def button(self, name, button_text: str, separator: bool = True,
|
|
126
|
+
align: ALIGN = 'left', toggle: bool = False, func: callable = None):
|
|
127
|
+
self._create()
|
|
128
|
+
self._widgets[name] = ButtonWidget(self, button_text, separator, align, toggle, func)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from zoneinfo import ZoneInfo
|
|
5
|
+
from tzlocal import get_localzone_name
|
|
6
|
+
from random import choices
|
|
7
|
+
from typing import Literal, Union
|
|
8
|
+
from numpy import isin
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Pane:
|
|
13
|
+
def __init__(self, window):
|
|
14
|
+
from .abstract import Window
|
|
15
|
+
self.win: Window = window
|
|
16
|
+
self.run_script = window.run_script
|
|
17
|
+
self.bulk_run = window.bulk_run
|
|
18
|
+
if hasattr(self, 'id'):
|
|
19
|
+
return
|
|
20
|
+
self.id = Window._id_gen.generate()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class IDGen(list):
|
|
24
|
+
ascii = 'abcdefghijklmnopqrstuvwxyz'
|
|
25
|
+
|
|
26
|
+
def generate(self) -> str:
|
|
27
|
+
var = ''.join(choices(self.ascii, k=8))
|
|
28
|
+
if var not in self:
|
|
29
|
+
self.append(var)
|
|
30
|
+
return f'window.{var}'
|
|
31
|
+
self.generate()
|
|
32
|
+
|
|
33
|
+
def format_datetime(dt: datetime, tz: Union[str, ZoneInfo] = None) -> str:
|
|
34
|
+
if tz is None:
|
|
35
|
+
# tz = ZoneInfo(get_localzone_name())
|
|
36
|
+
return dt.strftime('%Y-%m-%d %H:%M')
|
|
37
|
+
elif isinstance(tz, str):
|
|
38
|
+
tz = ZoneInfo(tz)
|
|
39
|
+
# If dt does not contain tzinfo, assume it is in the specified zone
|
|
40
|
+
if dt.tzinfo is None:
|
|
41
|
+
dt = dt.replace(tzinfo=tz)
|
|
42
|
+
else:
|
|
43
|
+
# Convert datetime to the required timezone
|
|
44
|
+
dt = dt.astimezone(tz)
|
|
45
|
+
return dt.strftime('%Y-%m-%d %H:%M GMT%z')
|
|
46
|
+
|
|
47
|
+
def parse_event_message(window, string):
|
|
48
|
+
name, args = string.split('_~_')
|
|
49
|
+
args = args.split(';;;')
|
|
50
|
+
func = window.handlers[name]
|
|
51
|
+
return func, args
|
|
52
|
+
|
|
53
|
+
def df_data(data: Union[pd.DataFrame, pd.Series]):
|
|
54
|
+
if isinstance(data, pd.DataFrame):
|
|
55
|
+
d = data.to_dict(orient='records')
|
|
56
|
+
filtered_records = [{k: v for k, v in record.items() if v is not None and not pd.isna(v)} for record in d]
|
|
57
|
+
else:
|
|
58
|
+
d = data.to_dict()
|
|
59
|
+
filtered_records = {k: v for k, v in d.items()}
|
|
60
|
+
return filtered_records
|
|
61
|
+
|
|
62
|
+
def series_data(data: Union[pd.DataFrame, pd.Series]):
|
|
63
|
+
filtered_records = []
|
|
64
|
+
for idx, val in data.items():
|
|
65
|
+
if isinstance(val, float):
|
|
66
|
+
val_str = f'{val:.4f}'
|
|
67
|
+
else:
|
|
68
|
+
val_str = str(val)
|
|
69
|
+
filtered_records.append({'index': idx, 'value': val_str})
|
|
70
|
+
return filtered_records
|
|
71
|
+
|
|
72
|
+
def js_data(data: Union[pd.DataFrame, pd.Series]):
|
|
73
|
+
if isinstance(data, pd.DataFrame):
|
|
74
|
+
d = data.to_dict(orient='records')
|
|
75
|
+
filtered_records = [{k: v for k, v in record.items() if v is not None and not pd.isna(v)} for record in d]
|
|
76
|
+
else:
|
|
77
|
+
d = data.to_dict()
|
|
78
|
+
filtered_records = {k: v for k, v in d.items()}
|
|
79
|
+
return json.dumps(filtered_records)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def snake_to_camel(s: str):
|
|
83
|
+
components = s.split('_')
|
|
84
|
+
return components[0] + ''.join(x.title() for x in components[1:])
|
|
85
|
+
|
|
86
|
+
def js_json(d: dict):
|
|
87
|
+
filtered_dict = {}
|
|
88
|
+
for key, val in d.items():
|
|
89
|
+
if key in ('self') or val in (None,):
|
|
90
|
+
continue
|
|
91
|
+
if '_' in key:
|
|
92
|
+
key = snake_to_camel(key)
|
|
93
|
+
filtered_dict[key] = val
|
|
94
|
+
return f"JSON.parse('{json.dumps(filtered_dict)}')"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def jbool(b: bool): return 'true' if b is True else 'false' if b is False else None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
LINE_STYLE = Literal['solid', 'dotted', 'dashed', 'large_dashed', 'sparse_dotted']
|
|
101
|
+
|
|
102
|
+
MARKER_POSITION = Literal['above', 'below', 'inside', 'atPriceMiddle', 'atPriceTop', 'atPriceBottom']
|
|
103
|
+
|
|
104
|
+
MARKER_SHAPE = Literal['arrow_up', 'arrow_down', 'circle', 'square']
|
|
105
|
+
|
|
106
|
+
CROSSHAIR_MODE = Literal['normal', 'magnet', 'hidden']
|
|
107
|
+
|
|
108
|
+
PRICE_SCALE_MODE = Literal['normal', 'logarithmic', 'percentage', 'index100']
|
|
109
|
+
|
|
110
|
+
TIME = Union[datetime, pd.Timestamp, str, float]
|
|
111
|
+
|
|
112
|
+
NUM = Union[float, int]
|
|
113
|
+
|
|
114
|
+
FLOAT = Literal['left', 'right', 'top', 'bottom']
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def as_enum(value, string_types):
|
|
118
|
+
types = string_types.__args__
|
|
119
|
+
return -1 if value not in types else types.index(value)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def marker_shape(shape: MARKER_SHAPE):
|
|
123
|
+
return {
|
|
124
|
+
'arrow_up': 'arrowUp',
|
|
125
|
+
'arrow_down': 'arrowDown',
|
|
126
|
+
}.get(shape) or shape
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def marker_position(p: MARKER_POSITION):
|
|
130
|
+
return {
|
|
131
|
+
'above' : 'aboveBar',
|
|
132
|
+
'below' : 'belowBar',
|
|
133
|
+
'inside': 'inBar',
|
|
134
|
+
'atPriceMiddle': 'atPriceMiddle',
|
|
135
|
+
'atPriceTop' : 'atPriceTop',
|
|
136
|
+
'atPriceBottom': 'atPriceBottom',
|
|
137
|
+
}.get(p)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Emitter:
|
|
141
|
+
def __init__(self):
|
|
142
|
+
self._callable = None
|
|
143
|
+
|
|
144
|
+
def __iadd__(self, other):
|
|
145
|
+
self._callable = other
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def _emit(self, *args):
|
|
149
|
+
if self._callable:
|
|
150
|
+
if asyncio.iscoroutinefunction(self._callable):
|
|
151
|
+
asyncio.create_task(self._callable(*args))
|
|
152
|
+
else:
|
|
153
|
+
self._callable(*args)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class JSEmitter:
|
|
157
|
+
def __init__(self, chart, name, on_iadd, wrapper=None):
|
|
158
|
+
self._on_iadd = on_iadd
|
|
159
|
+
self._chart = chart
|
|
160
|
+
self._name = name
|
|
161
|
+
self._wrapper = wrapper
|
|
162
|
+
|
|
163
|
+
def __iadd__(self, other):
|
|
164
|
+
def final_wrapper(*arg):
|
|
165
|
+
other(self._chart, *arg) if not self._wrapper else self._wrapper(other, self._chart, *arg)
|
|
166
|
+
async def final_async_wrapper(*arg):
|
|
167
|
+
await other(self._chart, *arg) if not self._wrapper else await self._wrapper(other, self._chart, *arg)
|
|
168
|
+
|
|
169
|
+
self._chart.win.handlers[self._name] = final_async_wrapper if asyncio.iscoroutinefunction(other) else final_wrapper
|
|
170
|
+
self._on_iadd(other)
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Events:
|
|
175
|
+
def __init__(self, chart):
|
|
176
|
+
self.new_bar = Emitter()
|
|
177
|
+
self.search = JSEmitter(chart, f'search{chart.id}',
|
|
178
|
+
lambda o: chart.run_script(f'''
|
|
179
|
+
Lib.Handler.makeSpinner({chart.id})
|
|
180
|
+
{chart.id}.search = Lib.Handler.makeSearchBox({chart.id})
|
|
181
|
+
''')
|
|
182
|
+
)
|
|
183
|
+
salt = chart.id[chart.id.index('.')+1:]
|
|
184
|
+
self.range_change = JSEmitter(chart, f'range_change{salt}',
|
|
185
|
+
lambda o: chart.run_script(f'''
|
|
186
|
+
let checkLogicalRange{salt} = (logical) => {{
|
|
187
|
+
{chart.id}.chart.timeScale().unsubscribeVisibleLogicalRangeChange(checkLogicalRange{salt})
|
|
188
|
+
|
|
189
|
+
let barsInfo = {chart.id}.series.barsInLogicalRange(logical)
|
|
190
|
+
if (barsInfo) window.callbackFunction(`range_change{salt}_~_${{barsInfo.barsBefore}};;;${{barsInfo.barsAfter}}`)
|
|
191
|
+
|
|
192
|
+
setTimeout(() => {chart.id}.chart.timeScale().subscribeVisibleLogicalRangeChange(checkLogicalRange{salt}), 50)
|
|
193
|
+
}}
|
|
194
|
+
{chart.id}.chart.timeScale().subscribeVisibleLogicalRangeChange(checkLogicalRange{salt})
|
|
195
|
+
'''),
|
|
196
|
+
wrapper=lambda o, c, *arg: o(c, *[float(a) for a in arg])
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
self.click = JSEmitter(chart, f'subscribe_click{salt}',
|
|
200
|
+
lambda o: chart.run_script(f'''
|
|
201
|
+
let clickHandler{salt} = (param) => {{
|
|
202
|
+
if (!param.point) return;
|
|
203
|
+
const time = {chart.id}.chart.timeScale().coordinateToTime(param.point.x)
|
|
204
|
+
const price = {chart.id}.series.coordinateToPrice(param.point.y);
|
|
205
|
+
window.callbackFunction(`subscribe_click{salt}_~_${{time}};;;${{price}}`)
|
|
206
|
+
}}
|
|
207
|
+
{chart.id}.chart.subscribeClick(clickHandler{salt})
|
|
208
|
+
'''),
|
|
209
|
+
wrapper=lambda func, c, *args: func(c, *[float(a) if a != 'null' else None for a in args])
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
class BulkRunScript:
|
|
213
|
+
def __init__(self, script_func):
|
|
214
|
+
self.enabled = False
|
|
215
|
+
self.scripts = []
|
|
216
|
+
self.script_func = script_func
|
|
217
|
+
|
|
218
|
+
def __enter__(self):
|
|
219
|
+
self.enabled = True
|
|
220
|
+
|
|
221
|
+
def __exit__(self, *args):
|
|
222
|
+
self.enabled = False
|
|
223
|
+
self.script_func('\n'.join(self.scripts))
|
|
224
|
+
self.scripts = []
|
|
225
|
+
|
|
226
|
+
def add_script(self, script):
|
|
227
|
+
self.scripts.append(script)
|