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.
@@ -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)