bn-lightweight-charts 0.2.2__tar.gz → 1.1.0__tar.gz

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.
Files changed (21) hide show
  1. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/PKG-INFO +2 -2
  2. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/abstract.py +58 -2
  3. bn_lightweight_charts-1.1.0/bn_lightweight_charts/js/index.html +53 -0
  4. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/js/index_bn.html +60 -0
  5. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/util.py +14 -0
  6. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/widgets.py +27 -8
  7. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/pyproject.toml +2 -2
  8. bn_lightweight_charts-0.2.2/bn_lightweight_charts/js/index.html +0 -25
  9. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/.gitignore +0 -0
  10. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/LICENSE +0 -0
  11. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/README.md +0 -0
  12. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/__init__.py +0 -0
  13. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/chart.py +0 -0
  14. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/drawings.py +0 -0
  15. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/js/bundle.js +0 -0
  16. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/js/lightweight-charts.js +0 -0
  17. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/js/styles.css +0 -0
  18. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/polygon.py +0 -0
  19. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/table.py +0 -0
  20. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/toolbox.py +0 -0
  21. {bn_lightweight_charts-0.2.2 → bn_lightweight_charts-1.1.0}/bn_lightweight_charts/topbar.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bn-lightweight-charts
3
- Version: 0.2.2
3
+ Version: 1.1.0
4
4
  Summary: Python framework for TradingView's Lightweight Charts JavaScript library.
5
5
  Project-URL: Homepage, https://github.com/smalinin/bn_lightweight-charts-python
6
6
  Project-URL: Source, https://github.com/smalinin/bn_lightweight-charts-python
7
7
  Author: smalinin
8
8
  License: MIT
9
9
  License-File: LICENSE
10
- Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: License :: OSI Approved :: MIT License
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3
@@ -13,7 +13,7 @@ from .topbar import TopBar
13
13
  from .util import (
14
14
  BulkRunScript, Pane, Events, IDGen, as_enum, jbool, js_json, TIME, NUM, FLOAT,
15
15
  LINE_STYLE, MARKER_POSITION, MARKER_SHAPE, CROSSHAIR_MODE,
16
- PRICE_SCALE_MODE, marker_position, marker_shape, js_data,
16
+ PRICE_SCALE_MODE, marker_position, marker_shape, js_data, js_zipdata
17
17
  )
18
18
 
19
19
  current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -56,7 +56,7 @@ class Window:
56
56
  self.scripts.extend(self.final_scripts)
57
57
  for script in self.scripts:
58
58
  initial_script += f'\n{script}'
59
- self.script_func(initial_script)
59
+ self.script_func(f'(async ()=> {{ {initial_script} }})();')
60
60
 
61
61
  def run_script(self, script: str, run_last: bool = False):
62
62
  """
@@ -217,6 +217,21 @@ class SeriesCommon(Pane):
217
217
  arg = self._interval * (arg.timestamp() // self._interval)+self.offset
218
218
  return arg
219
219
 
220
+ def set_zipped(self, df: Optional[pd.DataFrame] = None, format_cols: bool = True):
221
+ if df is None or df.empty:
222
+ self.run_script(f'{self.id}.series.setData([])')
223
+ self.data = pd.DataFrame()
224
+ return
225
+ if format_cols:
226
+ df = self._df_datetime_format(df, exclude_lowercase=self.name)
227
+ if self.name:
228
+ if self.name not in df:
229
+ raise NameError(f'No column named "{self.name}".')
230
+ df = df.rename(columns={self.name: 'value'})
231
+ self.data = df.copy()
232
+ self._last_bar = df.iloc[-1]
233
+ self.run_script(f'{self.id}.series.setData(await decodeGzJSON("{js_zipdata(df)}")); ')
234
+
220
235
  def set(self, df: Optional[pd.DataFrame] = None, format_cols: bool = True):
221
236
  if df is None or df.empty:
222
237
  self.run_script(f'{self.id}.series.setData([])')
@@ -594,6 +609,47 @@ class Candlestick(SeriesCommon):
594
609
  else:
595
610
  self.run_script(f"{self._chart.id}.toolBox?.clearDrawings()")
596
611
 
612
+ def set_zipped(self, df: Optional[pd.DataFrame] = None, keep_drawings=False):
613
+ """
614
+ Sets the initial data for the chart.\n
615
+ :param df: columns: date/time, open, high, low, close, volume (if volume enabled).
616
+ :param keep_drawings: keeps any drawings made through the toolbox. Otherwise, they will be deleted.
617
+ """
618
+ if df is None or df.empty:
619
+ self.run_script(f'{self.id}.series.setData([])')
620
+ self.run_script(f'{self.id}.volumeSeries.setData([])')
621
+ self.candle_data = pd.DataFrame()
622
+ return
623
+ df = self._df_datetime_format(df)
624
+ df_copy = df.copy()
625
+ self.candle_data = df_copy[['time', 'open', 'high', 'low', 'close']]
626
+ self._last_bar = df.iloc[-1]
627
+ candle_js_data = js_zipdata(df[['time', 'open', 'high', 'low', 'close']])
628
+ self.run_script(f'{self.id}.series.setData(await decodeGzJSON("{candle_js_data}"))')
629
+
630
+ if 'volume' not in df:
631
+ return
632
+ volume = df[['time', 'volume']].rename(columns={'volume': 'value'})
633
+ volume['color'] = self._volume_down_color
634
+ volume.loc[df['close'] > df['open'], 'color'] = self._volume_up_color
635
+ volume_js_data = js_zipdata(volume)
636
+ self.run_script(f'{self.id}.volumeSeries.setData(await decodeGzJSON("{volume_js_data}"))')
637
+
638
+ for line in self._lines:
639
+ if line.name not in df.columns:
640
+ continue
641
+ line.set(df[['time', line.name]], format_cols=False)
642
+ # set autoScale to true in case the user has dragged the price scale
643
+ self.run_script(f'''
644
+ if (!{self.id}.chart.priceScale("right").options.autoScale)
645
+ {self.id}.chart.priceScale("right").applyOptions({{autoScale: true}})
646
+ ''')
647
+ # TODO keep drawings doesn't work consistenly w
648
+ if keep_drawings:
649
+ self.run_script(f'{self._chart.id}.toolBox?._drawingTool.repositionOnTime()')
650
+ else:
651
+ self.run_script(f"{self._chart.id}.toolBox?.clearDrawings()")
652
+
597
653
  def update(self, series: pd.Series, _from_tick=False):
598
654
  """
599
655
  Updates the data from a bar;
@@ -0,0 +1,53 @@
1
+ <!DOCTYPE html>
2
+ <html lang="">
3
+ <head>
4
+ <title>bn_lightweight-charts-python</title>
5
+ <link rel="stylesheet" href="styles.css">
6
+ <script src="./lightweight-charts.js"></script>
7
+ <meta name="viewport" content ="width=device-width, initial-scale=1">
8
+ <meta charset="UTF-8">
9
+ <style>
10
+ body {
11
+ margin: 0;
12
+ padding: 0;
13
+ /*overflow: hidden;*/
14
+ overflow-y: auto;
15
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
16
+ Cantarell, "Helvetica Neue", sans-serif;
17
+ }
18
+ </style>
19
+ </head>
20
+ <body>
21
+ <div id="container"></div>
22
+ <script src="./bundle.js"></script>
23
+ <script>
24
+ async function decodeGzipBase64(b64) {
25
+ // base64 -> Uint8Array
26
+ const bytes = Uint8Array.from(
27
+ atob(b64),
28
+ c => c.charCodeAt(0)
29
+ );
30
+
31
+ // gunzip
32
+ const stream = new DecompressionStream("gzip");
33
+ const decompressedStream = new Blob([bytes])
34
+ .stream()
35
+ .pipeThrough(stream);
36
+
37
+ // Uint8Array -> string
38
+ const decompressed = await new Response(decompressedStream).arrayBuffer();
39
+ return new TextDecoder("utf-8").decode(decompressed);
40
+ }
41
+
42
+ async function decodeGzJSON(b64) {
43
+ let s = await decodeGzipBase64(b64)
44
+ try {
45
+ return JSON.parse(s)
46
+ } catch {
47
+ return []
48
+ }
49
+ }
50
+ </script>
51
+ </body>
52
+ </html>
53
+
@@ -19,6 +19,14 @@
19
19
  Cantarell, "Helvetica Neue", sans-serif;
20
20
  }
21
21
 
22
+ td.dt0, td.dt1, td.dt {
23
+ cursor: pointer;
24
+ text-decoration: underline;
25
+ }
26
+ td.dt0, td.dt1{
27
+ color: blue;
28
+ }
29
+
22
30
  .tableview {
23
31
  background: darkgrey;
24
32
  color: black;
@@ -138,6 +146,58 @@
138
146
  </div>
139
147
 
140
148
  <script src="./bundle.js"></script>
149
+ <script>
150
+ async function decodeGzipBase64(b64) {
151
+ // base64 -> Uint8Array
152
+ const bytes = Uint8Array.from(
153
+ atob(b64),
154
+ c => c.charCodeAt(0)
155
+ );
156
+
157
+ // gunzip
158
+ const stream = new DecompressionStream("gzip");
159
+ const decompressedStream = new Blob([bytes])
160
+ .stream()
161
+ .pipeThrough(stream);
162
+
163
+ // Uint8Array -> string
164
+ const decompressed = await new Response(decompressedStream).arrayBuffer();
165
+ return new TextDecoder("utf-8").decode(decompressed);
166
+ }
167
+
168
+ async function decodeGzJSON(b64) {
169
+ let s = await decodeGzipBase64(b64)
170
+ try {
171
+ return JSON.parse(s)
172
+ } catch {
173
+ return []
174
+ }
175
+ }
176
+
177
+ function scrollToTime(chart, stm0, stm1)
178
+ {
179
+ const tm0 = (new Date((stm0+":00").replace(' ','T')+'Z')).getTime()/1000
180
+ const tm1 = stm1 ? (new Date((stm1+":00").replace(' ','T')+'Z')).getTime()/1000 : null
181
+
182
+ const scale = chart.timeScale()
183
+ const tc0 = scale.timeToCoordinate(tm0)
184
+ if (tc0) {
185
+ const idx0 = scale.coordinateToLogical(tc0)
186
+ if (tm1) {
187
+ const tc1 = scale.timeToCoordinate(tm1)
188
+ if (tc1) {
189
+ const idx1 = scale.coordinateToLogical(tc1)
190
+ scale.setVisibleLogicalRange({ from: idx0 - 5, to: idx1 + 5 });
191
+ document.querySelector("#nav-home-tab").click();
192
+ }
193
+ }
194
+ else {
195
+ scale.setVisibleLogicalRange({ from: idx0 - 5, to: idx0 + 200 + 5 });
196
+ document.querySelector("#nav-home-tab").click();
197
+ }
198
+ }
199
+ }
200
+ </script>
141
201
  </body>
142
202
  </html>
143
203
 
@@ -1,5 +1,7 @@
1
1
  import asyncio
2
2
  import json
3
+ import gzip
4
+ import base64
3
5
  from datetime import datetime
4
6
  from zoneinfo import ZoneInfo
5
7
  from tzlocal import get_localzone_name
@@ -79,6 +81,18 @@ def js_data(data: Union[pd.DataFrame, pd.Series]):
79
81
  return json.dumps(filtered_records)
80
82
 
81
83
 
84
+ def js_zipdata(data: Union[pd.DataFrame, pd.Series]):
85
+ if isinstance(data, pd.DataFrame):
86
+ d = data.to_dict(orient='records')
87
+ filtered_records = [{k: v for k, v in record.items() if v is not None and not pd.isna(v)} for record in d]
88
+ else:
89
+ d = data.to_dict()
90
+ filtered_records = {k: v for k, v in d.items()}
91
+ raw = json.dumps(filtered_records, ensure_ascii=False).encode("utf-8")
92
+ compressed = gzip.compress(raw)
93
+ return base64.b64encode(compressed).decode("ascii")
94
+
95
+
82
96
  def snake_to_camel(s: str):
83
97
  components = s.split('_')
84
98
  return components[0] + ''.join(x.title() for x in components[1:])
@@ -177,7 +177,7 @@ class StreamlitChart(StaticLWC):
177
177
  def _load(self):
178
178
  if sthtml is None:
179
179
  raise ModuleNotFoundError('streamlit.components.v1.html was not found, and must be installed to use StreamlitChart.')
180
- sthtml(f'{self._html_init} {self._html}</script></body></html>', width=self.width, height=self.height)
180
+ sthtml(f'{self._html_init} (async ()=> {{\n{self._html}\n}})();\n </script></body></html>', width=self.width, height=self.height)
181
181
 
182
182
 
183
183
  class JupyterChart(StaticLWC):
@@ -188,7 +188,7 @@ class JupyterChart(StaticLWC):
188
188
  def _load(self):
189
189
  if HTML is None:
190
190
  raise ModuleNotFoundError('IPython.display.HTML was not found, and must be installed to use JupyterChart.')
191
- html_code = html.escape(f"{self._html_init} {self._html}</script></body></html>")
191
+ html_code = html.escape(f"{self._html_init} (async ()=> {{\n{self._html}\n}})();\n </script></body></html>")
192
192
  iframe = f'<iframe width="{self.width}" height="{self.height}" frameBorder="0" srcdoc="{html_code}"></iframe>'
193
193
  display(HTML(iframe))
194
194
 
@@ -200,13 +200,13 @@ class HTMLChart(StaticLWC):
200
200
  self.filename = filename
201
201
 
202
202
  def _load(self):
203
- html_code = f"{self._html_init} {self._html}</script></body></html>"
203
+ html_code = f"{self._html_init} (async ()=> {{\n {self._html}\n}})();\n </script></body></html>"
204
204
  with open(self.filename, 'w') as file:
205
205
  file.write(html_code)
206
206
 
207
207
 
208
208
  class HTMLChart_BN(StaticLWC):
209
- def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1,
209
+ def __init__(self, width: int = 800, height=350, inner_width=1, inner_height=1,
210
210
  scale_candles_only: bool = False, toolbox: bool = False, filename = "bn_charts.html"):
211
211
  super().__init__(width=width, height=height, inner_width=inner_width, inner_height=inner_height,
212
212
  scale_candles_only=scale_candles_only, toolbox=toolbox, autosize=True,
@@ -237,7 +237,7 @@ class HTMLChart_BN(StaticLWC):
237
237
  const perf_metrics = {json.dumps(self.performance)};
238
238
  const strategy_titles = {json.dumps(self.strat_titles)};
239
239
 
240
- function updateChart(id){{
240
+ async function updateChart(id){{
241
241
  document.querySelector('#nav-home-tab')?.click();
242
242
  {func_code}
243
243
  }}
@@ -259,8 +259,8 @@ class HTMLChart_BN(StaticLWC):
259
259
  }}
260
260
  const pos = v.size > 0 ? 'Long' : 'Short';
261
261
  _html += `<tr><td>${{v.ref}}</td> <td>${{pos}}</td><td>${{v.tradeid}}</td>`
262
- +`<td>${{v.dateopen}}</td><td>${{v.priceopen}}</td>`
263
- +`<td>${{v.dateclose}}</td><td>${{v.priceclose}}</td>`
262
+ +`<td class="dt0">${{v.dateopen}}</td><td>${{v.priceopen}}</td>`
263
+ +`<td class="dt1">${{v.dateclose}}</td><td>${{v.priceclose}}</td>`
264
264
  +`<td>${{v.pnlcomm}}</td><td>${{v.return_pct}}</td><td>${{v.commission}}</td>`
265
265
  +`<td>${{v.barlen}}</td></tr>`;
266
266
  }}
@@ -274,7 +274,7 @@ class HTMLChart_BN(StaticLWC):
274
274
  state = 1;
275
275
  }}
276
276
  const pos = v.o_ordtype===0 ? 'Buy' : 'Sell';
277
- _html += `<tr><td>${{v.o_ref}}</td><td>${{v.o_datetime}}</td><td>${{pos}}</td>`
277
+ _html += `<tr><td>${{v.o_ref}}</td><td class="dt">${{v.o_datetime}}</td><td>${{pos}}</td>`
278
278
  +`<td>${{v.o_price}}</td><td>${{v.o_size}}</td></tr>`;
279
279
  }}
280
280
  }}
@@ -314,6 +314,25 @@ class HTMLChart_BN(StaticLWC):
314
314
  }}
315
315
  }}
316
316
  document.querySelectorAll('#slist a')[0]?.click()
317
+
318
+ document.querySelector('#trades tbody').ondblclick = (e) => {{
319
+ const n = e.target;
320
+ const chart = {self.id}.chart;
321
+ if (n.nodeName==='TD' && n.classList.contains('dt0')) {{
322
+ const dt0 = n.innerText
323
+ const dt1 = n.parentNode.querySelector('td.dt1')?.innerText
324
+ scrollToTime(chart, dt0, dt1)
325
+ }}
326
+ else if (n.nodeName==='TD' && n.classList.contains('dt1')) {{
327
+ const dt1 = n.innerText
328
+ const dt0 = n.parentNode.querySelector('td.dt0')?.innerText
329
+ scrollToTime(chart, dt0, dt1)
330
+ }}
331
+ else if (n.nodeName==='TD' && n.classList.contains('dt')) {{
332
+ const dt0 = n.innerText
333
+ scrollToTime(chart, dt0)
334
+ }}
335
+ }}
317
336
  \n</script></body></html>
318
337
  '''
319
338
  return html_code
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bn-lightweight-charts"
3
- version = "0.2.2"
3
+ version = "1.1.0"
4
4
  description = "Python framework for TradingView's Lightweight Charts JavaScript library."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -8,7 +8,7 @@ authors = [
8
8
  { name = "smalinin"}
9
9
  ]
10
10
  classifiers = [
11
- "Development Status :: 3 - Alpha",
11
+ "Development Status :: 5 - Production/Stable",
12
12
  "Programming Language :: Python :: 3",
13
13
  "License :: OSI Approved :: MIT License",
14
14
  "Operating System :: OS Independent",
@@ -1,25 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="">
3
- <head>
4
- <title>bn_lightweight-charts-python</title>
5
- <link rel="stylesheet" href="styles.css">
6
- <script src="./lightweight-charts.js"></script>
7
- <meta name="viewport" content ="width=device-width, initial-scale=1">
8
- <meta charset="UTF-8">
9
- <style>
10
- body {
11
- margin: 0;
12
- padding: 0;
13
- /*overflow: hidden;*/
14
- overflow-y: auto;
15
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
16
- Cantarell, "Helvetica Neue", sans-serif;
17
- }
18
- </style>
19
- </head>
20
- <body>
21
- <div id="container"></div>
22
- <script src="./bundle.js"></script>
23
- </body>
24
- </html>
25
-