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,241 @@
1
+ import asyncio
2
+ import json
3
+ import multiprocessing as mp
4
+ import typing
5
+
6
+ from . import abstract
7
+ from .util import parse_event_message, FLOAT
8
+
9
+ import os
10
+ import threading
11
+
12
+
13
+ class CallbackAPI:
14
+ def __init__(self, emit_queue):
15
+ self.emit_queue = emit_queue
16
+
17
+ def callback(self, message: str):
18
+ self.emit_queue.put(message)
19
+
20
+
21
+ class PyWV:
22
+
23
+ def __init__(self, q, emit_q, return_q, loaded_event):
24
+ self.queue = q
25
+ self.return_queue = return_q
26
+ self.emit_queue = emit_q
27
+ self.loaded_event = loaded_event
28
+
29
+ self.is_alive = True
30
+
31
+ import webview
32
+
33
+ self.callback_api = CallbackAPI(emit_q)
34
+ self.windows: typing.List[webview.Window] = []
35
+ self.loop()
36
+
37
+
38
+ def create_window(
39
+ self, width, height, x, y, screen=None, on_top=False,
40
+ maximize=False, title=''
41
+ ):
42
+ import webview
43
+
44
+ screen = webview.screens[screen] if screen is not None else None
45
+ if maximize:
46
+ if screen is None:
47
+ active_screen = webview.screens[0]
48
+ width, height = active_screen.width, active_screen.height
49
+ else:
50
+ width, height = screen.width, screen.height
51
+
52
+ self.windows.append(webview.create_window(
53
+ title,
54
+ url=abstract.INDEX,
55
+ js_api=self.callback_api,
56
+ width=width,
57
+ height=height,
58
+ x=x,
59
+ y=y,
60
+ screen=screen,
61
+ on_top=on_top,
62
+ background_color='#000000')
63
+ )
64
+
65
+ self.windows[-1].events.loaded += lambda: self.loaded_event.set()
66
+
67
+
68
+ def loop(self):
69
+ import webview
70
+ from webview.errors import JavascriptException
71
+
72
+ # self.loaded_event.set()
73
+ while self.is_alive:
74
+ i, arg = self.queue.get()
75
+
76
+ if i == 'start':
77
+ webview.start(debug=arg, func=self.loop)
78
+ self.is_alive = False
79
+ self.emit_queue.put('exit')
80
+ return
81
+ if i == 'create_window':
82
+ self.create_window(*arg)
83
+ continue
84
+
85
+ window = self.windows[i]
86
+ if arg == 'show':
87
+ window.show()
88
+ elif arg == 'hide':
89
+ window.hide()
90
+ else:
91
+ try:
92
+ if '_~_~RETURN~_~_' in arg:
93
+ self.return_queue.put(window.evaluate_js(arg[14:]))
94
+ else:
95
+ window.evaluate_js(arg)
96
+ except KeyError as e:
97
+ return
98
+ except JavascriptException as e:
99
+ msg = eval(str(e))
100
+ raise JavascriptException(f"\n\nscript -> '{arg}',\nerror -> {msg['name']}[{msg['line']}:{msg['column']}]\n{msg['message']}")
101
+
102
+
103
+ class WebviewHandler():
104
+ def __init__(self) -> None:
105
+ self._reset()
106
+ self.debug = False
107
+
108
+ def _reset(self):
109
+ self.loaded_event = mp.Event()
110
+ self.return_queue = mp.Queue()
111
+ self.function_call_queue = mp.Queue()
112
+ self.emit_queue = mp.Queue()
113
+ self.wv_process = mp.Process(
114
+ target=PyWV, args=(
115
+ self.function_call_queue, self.emit_queue,
116
+ self.return_queue, self.loaded_event
117
+ ),
118
+ daemon=True
119
+ )
120
+ self.max_window_num = -1
121
+
122
+ def create_window(
123
+ self, width, height, x, y, screen=None, on_top=False,
124
+ maximize=False, title=''
125
+ ):
126
+ self.function_call_queue.put((
127
+ 'create_window', (width, height, x, y, screen, on_top, maximize, title)
128
+ ))
129
+ self.max_window_num += 1
130
+ return self.max_window_num
131
+
132
+ def start(self):
133
+ self.loaded_event.clear()
134
+ self.wv_process.start()
135
+ self.function_call_queue.put(('start', self.debug))
136
+ self.loaded_event.wait()
137
+
138
+ def show(self, window_num):
139
+ self.function_call_queue.put((window_num, 'show'))
140
+
141
+ def hide(self, window_num):
142
+ self.function_call_queue.put((window_num, 'hide'))
143
+
144
+ def evaluate_js(self, window_num, script):
145
+ self.function_call_queue.put((window_num, script))
146
+
147
+ def exit(self):
148
+ if self.wv_process.is_alive():
149
+ self.wv_process.terminate()
150
+ self.wv_process.join()
151
+ self._reset()
152
+
153
+
154
+ class Chart(abstract.AbstractChart):
155
+ _main_window_handlers = None
156
+ WV: WebviewHandler = WebviewHandler()
157
+
158
+ def __init__(
159
+ self,
160
+ width: int = 800,
161
+ height: int = 600,
162
+ x: int = None,
163
+ y: int = None,
164
+ title: str = '',
165
+ screen: int = None,
166
+ on_top: bool = False,
167
+ maximize: bool = False,
168
+ debug: bool = False,
169
+ toolbox: bool = False,
170
+ inner_width: float = 1.0,
171
+ inner_height: float = 1.0,
172
+ scale_candles_only: bool = False,
173
+ position: FLOAT = 'left'
174
+ ):
175
+ Chart.WV.debug = debug
176
+ self._i = Chart.WV.create_window(
177
+ width, height, x, y, screen, on_top, maximize, title
178
+ )
179
+
180
+ window = abstract.Window(
181
+ script_func=lambda s: Chart.WV.evaluate_js(self._i, s),
182
+ js_api_code='pywebview.api.callback'
183
+ )
184
+
185
+ abstract.Window._return_q = Chart.WV.return_queue
186
+
187
+ self.is_alive = True
188
+
189
+ if Chart._main_window_handlers is None:
190
+ super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
191
+ Chart._main_window_handlers = self.win.handlers
192
+ else:
193
+ window.handlers = Chart._main_window_handlers
194
+ super().__init__(window, inner_width, inner_height, scale_candles_only, toolbox, position=position)
195
+
196
+ def show(self, block: bool = False):
197
+ """
198
+ Shows the chart window.\n
199
+ :param block: blocks execution until the chart is closed.
200
+ """
201
+ if not self.win.loaded:
202
+ Chart.WV.start()
203
+ self.win.on_js_load()
204
+ else:
205
+ Chart.WV.show(self._i)
206
+ if block:
207
+ asyncio.run(self.show_async())
208
+
209
+ async def show_async(self):
210
+ self.show(block=False)
211
+ try:
212
+ from . import polygon
213
+ [asyncio.create_task(self.polygon.async_set(*args)) for args in polygon._set_on_load]
214
+ while 1:
215
+ while Chart.WV.emit_queue.empty() and self.is_alive:
216
+ await asyncio.sleep(0.05)
217
+ if not self.is_alive:
218
+ return
219
+ response = Chart.WV.emit_queue.get()
220
+ if response == 'exit':
221
+ Chart.WV.exit()
222
+ self.is_alive = False
223
+ return
224
+ else:
225
+ func, args = parse_event_message(self.win, response)
226
+ await func(*args) if asyncio.iscoroutinefunction(func) else func(*args)
227
+ except KeyboardInterrupt:
228
+ return
229
+
230
+ def hide(self):
231
+ """
232
+ Hides the chart window.\n
233
+ """
234
+ self._q.put((self._i, 'hide'))
235
+
236
+ def exit(self):
237
+ """
238
+ Exits and destroys the chart window.\n
239
+ """
240
+ Chart.WV.exit()
241
+ self.is_alive = False
@@ -0,0 +1,278 @@
1
+ import asyncio
2
+ import json
3
+ import pandas as pd
4
+
5
+ from typing import Union, Optional
6
+
7
+ from .util import NUM, Pane, as_enum, LINE_STYLE, TIME, snake_to_camel, js_json
8
+
9
+ def make_js_point(chart, time, price):
10
+ formatted_time = chart._single_datetime_format(time)
11
+ return f'''{{
12
+ "time": {formatted_time},
13
+ "logical": {chart.id}.chart.timeScale()
14
+ .coordinateToLogical(
15
+ {chart.id}.chart.timeScale()
16
+ .timeToCoordinate({formatted_time})
17
+ ),
18
+ "price": {price}
19
+ }}'''
20
+
21
+ class Drawing(Pane):
22
+ def __init__(self, chart, func=None):
23
+ super().__init__(chart.win)
24
+ self.chart = chart
25
+
26
+ def update(self, *points):
27
+ formatted_points = []
28
+ for i in range(0, len(points), 2):
29
+ formatted_points.append(make_js_point(self.chart, points[i], points[i + 1]))
30
+ self.run_script(f'{self.id}.updatePoints({", ".join(formatted_points)})')
31
+ print(f'{self.id}.updatePoints({", ".join(formatted_points)})')
32
+
33
+ def delete(self):
34
+ """
35
+ Irreversibly deletes the drawing.
36
+ """
37
+ self.run_script(f'{self.id}.detach()')
38
+
39
+ def options(self, color='#1E80F0', style='solid', width=4):
40
+ self.run_script(f'''{self.id}.applyOptions({{
41
+ lineColor: '{color}',
42
+ lineStyle: {as_enum(style, LINE_STYLE)},
43
+ width: {width},
44
+ }})''')
45
+
46
+ class TwoPointDrawing(Drawing):
47
+ def __init__(
48
+ self,
49
+ drawing_type,
50
+ chart,
51
+ start_time: TIME,
52
+ start_value: NUM,
53
+ end_time: TIME,
54
+ end_value: NUM,
55
+ round: bool,
56
+ options: dict,
57
+ func=None
58
+ ):
59
+ super().__init__(chart, func)
60
+
61
+
62
+
63
+ options_string = '\n'.join(f'{key}: {val},' for key, val in options.items())
64
+
65
+ self.run_script(f'''
66
+ {self.id} = new Lib.{drawing_type}(
67
+ {make_js_point(self.chart, start_time, start_value)},
68
+ {make_js_point(self.chart, end_time, end_value)},
69
+ {{
70
+ {options_string}
71
+ }}
72
+ )
73
+ {chart.id}.series.attachPrimitive({self.id})
74
+ ''')
75
+
76
+
77
+ class HorizontalLine(Drawing):
78
+ def __init__(self, chart, price, color, width, style, text, axis_label_visible, func):
79
+ super().__init__(chart, func)
80
+ self.price = price
81
+ self.run_script(f'''
82
+
83
+ {self.id} = new Lib.HorizontalLine(
84
+ {{price: {price}}},
85
+ {{
86
+ lineColor: '{color}',
87
+ lineStyle: {as_enum(style, LINE_STYLE)},
88
+ width: {width},
89
+ text: `{text}`,
90
+ }},
91
+ callbackName={f"'{self.id}'" if func else 'null'}
92
+ )
93
+ {chart.id}.series.attachPrimitive({self.id})
94
+ ''')
95
+ if not func:
96
+ return
97
+
98
+ def wrapper(p):
99
+ self.price = float(p)
100
+ func(chart, self)
101
+
102
+ async def wrapper_async(p):
103
+ self.price = float(p)
104
+ await func(chart, self)
105
+
106
+ self.win.handlers[self.id] = wrapper_async if asyncio.iscoroutinefunction(func) else wrapper
107
+ self.run_script(f'{chart.id}.toolBox?.addNewDrawing({self.id})')
108
+
109
+ def update(self, price: float):
110
+ """
111
+ Moves the horizontal line to the given price.
112
+ """
113
+ self.run_script(f'{self.id}.updatePoints({{price: {price}}})')
114
+ # self.run_script(f'{self.id}.updatePrice({price})')
115
+ self.price = price
116
+
117
+ def options(self, color='#1E80F0', style='solid', width=4, text=''):
118
+ super().options(color, style, width)
119
+ self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
120
+
121
+
122
+
123
+ class VerticalLine(Drawing):
124
+ def __init__(self, chart, time, color, width, style, text, func=None):
125
+ super().__init__(chart, func)
126
+ self.time = time
127
+ self.run_script(f'''
128
+
129
+ {self.id} = new Lib.VerticalLine(
130
+ {{time: {self.chart._single_datetime_format(time)}}},
131
+ {{
132
+ lineColor: '{color}',
133
+ lineStyle: {as_enum(style, LINE_STYLE)},
134
+ width: {width},
135
+ text: `{text}`,
136
+ }},
137
+ callbackName={f"'{self.id}'" if func else 'null'}
138
+ )
139
+ {chart.id}.series.attachPrimitive({self.id})
140
+ ''')
141
+
142
+ def update(self, time: TIME):
143
+ self.run_script(f'{self.id}.updatePoints({{time: {time}}})')
144
+ # self.run_script(f'{self.id}.updatePrice({price})')
145
+ self.price = price
146
+
147
+ def options(self, color='#1E80F0', style='solid', width=4, text=''):
148
+ super().options(color, style, width)
149
+ self.run_script(f'{self.id}.applyOptions({{text: `{text}`}})')
150
+
151
+
152
+ class RayLine(Drawing):
153
+ def __init__(self,
154
+ chart,
155
+ start_time: TIME,
156
+ value: NUM,
157
+ round: bool = False,
158
+ color: str = '#1E80F0',
159
+ width: int = 2,
160
+ style: LINE_STYLE = 'solid',
161
+ text: str = '',
162
+ func = None,
163
+ ):
164
+ super().__init__(chart, func)
165
+ self.run_script(f'''
166
+ {self.id} = new Lib.RayLine(
167
+ {{time: {self.chart._single_datetime_format(start_time)}, price: {value}}},
168
+ {{
169
+ lineColor: '{color}',
170
+ lineStyle: {as_enum(style, LINE_STYLE)},
171
+ width: {width},
172
+ text: `{text}`,
173
+ }},
174
+ callbackName={f"'{self.id}'" if func else 'null'}
175
+ )
176
+ {chart.id}.series.attachPrimitive({self.id})
177
+ ''')
178
+
179
+
180
+
181
+
182
+ class Box(TwoPointDrawing):
183
+ def __init__(self,
184
+ chart,
185
+ start_time: TIME,
186
+ start_value: NUM,
187
+ end_time: TIME,
188
+ end_value: NUM,
189
+ round: bool,
190
+ line_color: str,
191
+ fill_color: str,
192
+ width: int,
193
+ style: LINE_STYLE,
194
+ func=None):
195
+
196
+ super().__init__(
197
+ "Box",
198
+ chart,
199
+ start_time,
200
+ start_value,
201
+ end_time,
202
+ end_value,
203
+ round,
204
+ {
205
+ "lineColor": f'"{line_color}"',
206
+ "fillColor": f'"{fill_color}"',
207
+ "width": width,
208
+ "lineStyle": as_enum(style, LINE_STYLE)
209
+ },
210
+ func
211
+ )
212
+
213
+
214
+ class TrendLine(TwoPointDrawing):
215
+ def __init__(self,
216
+ chart,
217
+ start_time: TIME,
218
+ start_value: NUM,
219
+ end_time: TIME,
220
+ end_value: NUM,
221
+ round: bool,
222
+ line_color: str,
223
+ width: int,
224
+ style: LINE_STYLE,
225
+ func=None):
226
+
227
+ super().__init__(
228
+ "TrendLine",
229
+ chart,
230
+ start_time,
231
+ start_value,
232
+ end_time,
233
+ end_value,
234
+ round,
235
+ {
236
+ "lineColor": f'"{line_color}"',
237
+ "width": width,
238
+ "lineStyle": as_enum(style, LINE_STYLE)
239
+ },
240
+ func
241
+ )
242
+
243
+ # TODO reimplement/fix
244
+ class VerticalSpan(Pane):
245
+ def __init__(self, series: 'SeriesCommon', start_time: Union[TIME, tuple, list], end_time: Optional[TIME] = None,
246
+ color: str = 'rgba(252, 219, 3, 0.2)'):
247
+ self._chart = series._chart
248
+ super().__init__(self._chart.win)
249
+ start_time, end_time = pd.to_datetime(start_time), pd.to_datetime(end_time)
250
+ self.run_script(f'''
251
+ {self.id} = {self._chart.id}.chart.addHistogramSeries({{
252
+ color: '{color}',
253
+ priceFormat: {{type: 'volume'}},
254
+ priceScaleId: 'vertical_line',
255
+ lastValueVisible: false,
256
+ priceLineVisible: false,
257
+ }})
258
+ {self.id}.priceScale('').applyOptions({{
259
+ scaleMargins: {{top: 0, bottom: 0}}
260
+ }})
261
+ ''')
262
+ if end_time is None:
263
+ if isinstance(start_time, pd.DatetimeIndex):
264
+ data = [{'time': time.timestamp(), 'value': 1} for time in start_time]
265
+ else:
266
+ data = [{'time': start_time.timestamp(), 'value': 1}]
267
+ self.run_script(f'{self.id}.setData({data})')
268
+ else:
269
+ self.run_script(f'''
270
+ {self.id}.setData(calculateTrendLine(
271
+ {start_time.timestamp()}, 1, {end_time.timestamp()}, 1, {series.id}))
272
+ ''')
273
+
274
+ def delete(self):
275
+ """
276
+ Irreversibly deletes the vertical span.
277
+ """
278
+ self.run_script(f'{self._chart.id}.chart.removeSeries({self.id})')