Qubx 0.5.7__cp312-cp312-manylinux_2_39_x86_64.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.

Potentially problematic release.


This version of Qubx might be problematic. Click here for more details.

Files changed (100) hide show
  1. qubx/__init__.py +207 -0
  2. qubx/_nb_magic.py +100 -0
  3. qubx/backtester/__init__.py +5 -0
  4. qubx/backtester/account.py +145 -0
  5. qubx/backtester/broker.py +87 -0
  6. qubx/backtester/data.py +296 -0
  7. qubx/backtester/management.py +378 -0
  8. qubx/backtester/ome.py +296 -0
  9. qubx/backtester/optimization.py +201 -0
  10. qubx/backtester/simulated_data.py +558 -0
  11. qubx/backtester/simulator.py +362 -0
  12. qubx/backtester/utils.py +780 -0
  13. qubx/cli/__init__.py +0 -0
  14. qubx/cli/commands.py +67 -0
  15. qubx/connectors/ccxt/__init__.py +0 -0
  16. qubx/connectors/ccxt/account.py +495 -0
  17. qubx/connectors/ccxt/broker.py +132 -0
  18. qubx/connectors/ccxt/customizations.py +193 -0
  19. qubx/connectors/ccxt/data.py +612 -0
  20. qubx/connectors/ccxt/exceptions.py +17 -0
  21. qubx/connectors/ccxt/factory.py +93 -0
  22. qubx/connectors/ccxt/utils.py +307 -0
  23. qubx/core/__init__.py +0 -0
  24. qubx/core/account.py +251 -0
  25. qubx/core/basics.py +850 -0
  26. qubx/core/context.py +420 -0
  27. qubx/core/exceptions.py +38 -0
  28. qubx/core/helpers.py +480 -0
  29. qubx/core/interfaces.py +1150 -0
  30. qubx/core/loggers.py +514 -0
  31. qubx/core/lookups.py +475 -0
  32. qubx/core/metrics.py +1512 -0
  33. qubx/core/mixins/__init__.py +13 -0
  34. qubx/core/mixins/market.py +94 -0
  35. qubx/core/mixins/processing.py +428 -0
  36. qubx/core/mixins/subscription.py +203 -0
  37. qubx/core/mixins/trading.py +88 -0
  38. qubx/core/mixins/universe.py +270 -0
  39. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  40. qubx/core/series.pxd +125 -0
  41. qubx/core/series.pyi +118 -0
  42. qubx/core/series.pyx +988 -0
  43. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  44. qubx/core/utils.pyi +6 -0
  45. qubx/core/utils.pyx +62 -0
  46. qubx/data/__init__.py +25 -0
  47. qubx/data/helpers.py +416 -0
  48. qubx/data/readers.py +1562 -0
  49. qubx/data/tardis.py +100 -0
  50. qubx/gathering/simplest.py +88 -0
  51. qubx/math/__init__.py +3 -0
  52. qubx/math/stats.py +129 -0
  53. qubx/pandaz/__init__.py +23 -0
  54. qubx/pandaz/ta.py +2757 -0
  55. qubx/pandaz/utils.py +638 -0
  56. qubx/resources/instruments/symbols-binance.cm.json +1 -0
  57. qubx/resources/instruments/symbols-binance.json +1 -0
  58. qubx/resources/instruments/symbols-binance.um.json +1 -0
  59. qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
  60. qubx/resources/instruments/symbols-bitfinex.json +1 -0
  61. qubx/resources/instruments/symbols-kraken.f.json +1 -0
  62. qubx/resources/instruments/symbols-kraken.json +1 -0
  63. qubx/ta/__init__.py +0 -0
  64. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  65. qubx/ta/indicators.pxd +149 -0
  66. qubx/ta/indicators.pyi +41 -0
  67. qubx/ta/indicators.pyx +787 -0
  68. qubx/trackers/__init__.py +3 -0
  69. qubx/trackers/abvanced.py +236 -0
  70. qubx/trackers/composite.py +146 -0
  71. qubx/trackers/rebalancers.py +129 -0
  72. qubx/trackers/riskctrl.py +641 -0
  73. qubx/trackers/sizers.py +235 -0
  74. qubx/utils/__init__.py +5 -0
  75. qubx/utils/_pyxreloader.py +281 -0
  76. qubx/utils/charting/lookinglass.py +1057 -0
  77. qubx/utils/charting/mpl_helpers.py +1183 -0
  78. qubx/utils/marketdata/binance.py +284 -0
  79. qubx/utils/marketdata/ccxt.py +90 -0
  80. qubx/utils/marketdata/dukas.py +130 -0
  81. qubx/utils/misc.py +541 -0
  82. qubx/utils/ntp.py +63 -0
  83. qubx/utils/numbers_utils.py +7 -0
  84. qubx/utils/orderbook.py +491 -0
  85. qubx/utils/plotting/__init__.py +0 -0
  86. qubx/utils/plotting/dashboard.py +150 -0
  87. qubx/utils/plotting/data.py +137 -0
  88. qubx/utils/plotting/interfaces.py +25 -0
  89. qubx/utils/plotting/renderers/__init__.py +0 -0
  90. qubx/utils/plotting/renderers/plotly.py +0 -0
  91. qubx/utils/runner/__init__.py +1 -0
  92. qubx/utils/runner/_jupyter_runner.pyt +60 -0
  93. qubx/utils/runner/accounts.py +88 -0
  94. qubx/utils/runner/configs.py +65 -0
  95. qubx/utils/runner/runner.py +470 -0
  96. qubx/utils/time.py +312 -0
  97. qubx-0.5.7.dist-info/METADATA +105 -0
  98. qubx-0.5.7.dist-info/RECORD +100 -0
  99. qubx-0.5.7.dist-info/WHEEL +4 -0
  100. qubx-0.5.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1057 @@
1
+ import matplotlib.pyplot as plt
2
+ import pandas as pd
3
+ import plotly.graph_objects as go
4
+ from plotly.graph_objs.graph_objs import FigureWidget
5
+ from plotly.subplots import make_subplots
6
+
7
+ from qubx.core.series import OHLCV, TimeSeries
8
+ from qubx.utils import Struct
9
+ from qubx.utils.charting.mpl_helpers import ohlc_plot, plot_trends, subplot
10
+
11
+
12
+ def install_plotly_helpers():
13
+ try:
14
+
15
+ def rline_(look, x, y, c="red", lw=1):
16
+ """
17
+ Ray line
18
+ """
19
+ return look.update_layout(
20
+ shapes=(
21
+ dict(
22
+ type="line",
23
+ xref="x1",
24
+ yref="y1",
25
+ x0=pd.Timestamp(x),
26
+ y0=y,
27
+ x1=look.data[0]["x"][-1],
28
+ y1=y,
29
+ fillcolor=c,
30
+ opacity=1,
31
+ line=dict(color=c, width=lw),
32
+ )
33
+ ),
34
+ overwrite=False,
35
+ )
36
+
37
+ def rline(look, x, y, c="red", lw=1, ls=None):
38
+ return look.add_shape(
39
+ go.layout.Shape(
40
+ type="line",
41
+ x0=pd.Timestamp(x),
42
+ x1=look.data[0]["x"][-1],
43
+ y0=y,
44
+ y1=y,
45
+ xref="x1",
46
+ yref="y1",
47
+ line=dict(width=lw, color=c, dash=ls),
48
+ )
49
+ )
50
+
51
+ def rlinex(look, x0, x1, y, c="red", lw=1, ls=None):
52
+ return look.add_shape(
53
+ go.layout.Shape(
54
+ type="line",
55
+ x0=pd.Timestamp(x0),
56
+ x1=pd.Timestamp(x1),
57
+ y0=y,
58
+ y1=y,
59
+ xref="x1",
60
+ yref="y1",
61
+ line=dict(width=lw, color=c, dash=ls),
62
+ )
63
+ )
64
+
65
+ def dliney(look, x0, y0, y1, c="red", lw=1, ls=None):
66
+ return look.add_shape(
67
+ go.layout.Shape(
68
+ type="line",
69
+ x0=pd.Timestamp(x0),
70
+ x1=pd.Timestamp(x0),
71
+ y0=y0,
72
+ y1=y1,
73
+ xref="x1",
74
+ yref="y1",
75
+ line=dict(width=lw, color=c, dash=ls),
76
+ )
77
+ )
78
+
79
+ def vline(look, x, c="yellow", lw=1, ls="dot"):
80
+ return look.add_shape(
81
+ go.layout.Shape(
82
+ type="line",
83
+ x0=pd.Timestamp(x),
84
+ x1=pd.Timestamp(x),
85
+ y0=0,
86
+ y1=1,
87
+ xref="x1",
88
+ yref="paper",
89
+ line=dict(width=lw, color=c, dash=ls),
90
+ )
91
+ )
92
+
93
+ def hline(look, y, c="yellow", lw=1, ls="dot"):
94
+ return look.add_shape(
95
+ go.layout.Shape(
96
+ type="line",
97
+ x0=0,
98
+ x1=1,
99
+ y0=y,
100
+ y1=y,
101
+ xref="paper",
102
+ yref="y1",
103
+ line=dict(width=lw, color=c, dash=ls),
104
+ )
105
+ )
106
+
107
+ def arrow(look, x2, y2, x1, y1, c="red", text="", lw=1, font=dict(size=8), head=1):
108
+ return look.add_annotation(
109
+ x=x1,
110
+ y=y1,
111
+ ax=x2,
112
+ ay=y2,
113
+ xref="x",
114
+ yref="y",
115
+ axref="x",
116
+ ayref="y",
117
+ text=text,
118
+ font=font,
119
+ showarrow=True,
120
+ arrowhead=head,
121
+ arrowsize=1,
122
+ arrowwidth=lw,
123
+ arrowcolor=c,
124
+ )
125
+
126
+ def hover(v, h=600, n=2, legend=False, show_info=True):
127
+ return (
128
+ v.update_traces(xaxis="x1")
129
+ .update_layout(
130
+ height=h,
131
+ hovermode="x unified",
132
+ showlegend=legend,
133
+ hoverdistance=1 if show_info else 0,
134
+ xaxis={"hoverformat": "%d-%b-%y %H:%M"},
135
+ yaxis={"hoverformat": f".{n}f"},
136
+ dragmode="zoom",
137
+ newshape=dict(line_color="yellow", line_width=1.0),
138
+ modebar_add=["drawline", "drawopenpath", "drawrect", "eraseshape"],
139
+ # hoversubplots="axis",
140
+ hoverlabel=dict(align="auto", bgcolor="rgba(10, 10, 10, 0.5)"),
141
+ )
142
+ .update_xaxes(
143
+ showspikes=True,
144
+ spikemode="across",
145
+ spikesnap="cursor",
146
+ spikecolor="#306020",
147
+ spikethickness=1,
148
+ spikedash="dot",
149
+ )
150
+ .update_yaxes(
151
+ spikesnap="cursor",
152
+ spikecolor="#306020",
153
+ tickformat=f".{n}f",
154
+ spikethickness=1,
155
+ )
156
+ )
157
+
158
+ FigureWidget.hover = hover # type: ignore
159
+ FigureWidget.rline = rline # type: ignore
160
+ FigureWidget.rlinex = rlinex # type: ignore
161
+ FigureWidget.rline_ = rline_ # type: ignore
162
+ FigureWidget.vline = vline # type: ignore
163
+ FigureWidget.hline = hline # type: ignore
164
+ FigureWidget.dliney = dliney # type: ignore
165
+ FigureWidget.arrow = arrow # type: ignore
166
+ except: # noqa: E722
167
+ print(" >>> Cant attach helpers to plotly::FigureWidget - probably it isn't installed !")
168
+
169
+
170
+ # - install plotly helpers
171
+ install_plotly_helpers()
172
+
173
+
174
+ class AbstractLookingGlass:
175
+ """
176
+ Handy utility for plotting data
177
+ """
178
+
179
+ def __init__(self, master, studies: dict | None, title=""):
180
+ self.m = master
181
+ self.s = {} if studies is None else studies
182
+ self._title = title
183
+
184
+ def look(self, *args, title=None, **kwargs):
185
+ _vert_bar = None
186
+ zoom = None
187
+
188
+ if len(args) == 1:
189
+ zoom = args[0]
190
+ elif len(args) > 1:
191
+ zoom = args
192
+
193
+ if zoom and not isinstance(zoom, slice):
194
+ zoom = zoom if isinstance(zoom, (list, tuple)) else [zoom]
195
+
196
+ if len(zoom) == 2:
197
+ z0, is_d_0 = self.__as_time(zoom[0])
198
+ z1, is_d_1 = self.__as_time(zoom[1])
199
+
200
+ if is_d_0:
201
+ if is_d_1:
202
+ raise ValueError("At least one of zoom values must be timestamp !")
203
+ zoom = slice(z1 - z0, z1)
204
+ else:
205
+ zoom = slice(z0, (z0 + z1) if is_d_1 else z1)
206
+ elif len(zoom) > 2:
207
+ z0, is_d_0 = self.__as_time(zoom[0])
208
+ z1, is_d_1 = self.__as_time(zoom[1])
209
+ z2, is_d_2 = self.__as_time(zoom[2])
210
+
211
+ if is_d_1:
212
+ raise ValueError("Second argument must be timestamp !")
213
+ if not is_d_0:
214
+ raise ValueError("First argument must be timedelta !")
215
+ if not is_d_2:
216
+ raise ValueError("Third argument must be timedelta !")
217
+
218
+ zoom = slice(z1 - z0, z1 + z2)
219
+ _vert_bar = z1
220
+ elif len(zoom) == 1:
221
+ import datetime
222
+
223
+ z1, is_d = self.__as_time(zoom[0])
224
+ if is_d:
225
+ raise ValueError("Argument must be date time not timedelta !")
226
+
227
+ if z1.time() == datetime.time(0, 0):
228
+ shift = pd.Timedelta(kwargs.get("shift", "24h"))
229
+ zoom = slice(z1, z1 + shift)
230
+ else:
231
+ shift = pd.Timedelta(kwargs.get("shift", "12h"))
232
+ zoom = slice(z1 - shift, z1 + shift)
233
+ _vert_bar = z1
234
+ else:
235
+ raise ValueError("Don't know how to interpret '%s'" % str(zoom))
236
+
237
+ return self._show_plot(_vert_bar, title, zoom)
238
+
239
+ def _frame_has_cols(self, df, cols):
240
+ return isinstance(df, pd.DataFrame) and all(x in df.columns for x in cols)
241
+
242
+ def __as_time(self, z):
243
+ _is_delta = False
244
+ if isinstance(z, str):
245
+ try:
246
+ z = pd.Timedelta(z)
247
+ _is_delta = True
248
+ except:
249
+ try:
250
+ z = pd.Timestamp(z)
251
+ except:
252
+ raise ValueError("Value '%s' can't be recognized" % z)
253
+ else:
254
+ _is_delta = isinstance(z, pd.Timedelta)
255
+ return z, _is_delta
256
+
257
+ def _show_plot(self, _vert_bar, title, zoom):
258
+ raise NotImplementedError("Must be implemented in child class %s", self.__class__.__name__)
259
+
260
+
261
+ class LookingGlass:
262
+ def __init__(self, master, studies: dict | None = None, title="", backend="plotly", **kwargs):
263
+ if backend in ["matplotlib", "mpl"]:
264
+ self.__instance = LookingGlassMatplotLib(master=master, studies=studies, title=title, **kwargs)
265
+ elif backend in ["plotly", "ply", "plt"]:
266
+ self.__instance = LookingGlassPlotly(master=master, studies=studies, title=title, **kwargs)
267
+ else:
268
+ raise ValueError("Backend %s is not recognized" % backend)
269
+
270
+ def look(self, *args, **kwargs):
271
+ return self.__instance.look(*args, **kwargs)
272
+
273
+
274
+ class LookingGlassMatplotLib(AbstractLookingGlass):
275
+ def __init__(
276
+ self,
277
+ master,
278
+ studies: dict | None = None,
279
+ master_size=3,
280
+ study_size=1,
281
+ title="",
282
+ legend_loc="upper left",
283
+ fmt="%H:%M",
284
+ ohlc_width=0,
285
+ ):
286
+ super().__init__(master, studies, title)
287
+ self.s_size = study_size
288
+ self.m_size = master_size
289
+ self.legend_loc = legend_loc
290
+ self._fmt = fmt
291
+ self._ohlc_width = ohlc_width
292
+ self._n_style = "-"
293
+
294
+ def __plt_series(self, y, zoom, study_name, k, plot_style="line"):
295
+ _forced_limits = []
296
+ if isinstance(y, (int, float)):
297
+ try:
298
+ plt.axhline(y, lw=0.5, ls=self._n_style)
299
+ except:
300
+ plt.axhline(y, lw=0.5, ls="--")
301
+ else:
302
+ _lbl = y.name if hasattr(y, "name") and y.name else ("%s_%d" % (study_name, k))
303
+
304
+ if isinstance(y, (pd.DataFrame, OHLCV)):
305
+ y = y.pd() if isinstance(y, OHLCV) else y
306
+
307
+ yy = y[zoom] if zoom else y
308
+
309
+ # reversal points
310
+ if self._frame_has_cols(y, ["start_price", "delta"]):
311
+ _lo_pts = yy[yy.delta > 0].rename(columns={"start_price": "Bottom"})
312
+ _hi_pts = yy[yy.delta < 0].rename(columns={"start_price": "Top"})
313
+ plt.plot(
314
+ _lo_pts.index,
315
+ _lo_pts.Bottom,
316
+ marker=6,
317
+ markersize=5,
318
+ ls="",
319
+ c="#10aa10",
320
+ )
321
+ plt.plot(_hi_pts.index, _hi_pts.Top, marker=7, markersize=5, ls="", c="r")
322
+
323
+ # trends
324
+ elif self._frame_has_cols(y, ["UpTrends", "DownTrends"]):
325
+ plot_trends(yy, fmt=self._fmt)
326
+
327
+ # tracks
328
+ elif self._frame_has_cols(y, ["Type", "Time", "Price", "PriceOccured"]):
329
+ _bot = yy[yy.Type == "-"]
330
+ _top = yy[yy.Type == "+"]
331
+ plt.plot(
332
+ _bot.index,
333
+ _bot.PriceOccured.rename("B"),
334
+ marker=5,
335
+ markersize=8,
336
+ ls="",
337
+ c="#1080ff",
338
+ )
339
+ plt.plot(
340
+ _top.index,
341
+ _top.PriceOccured.rename("T"),
342
+ marker=5,
343
+ markersize=8,
344
+ ls="",
345
+ c="#909010",
346
+ )
347
+
348
+ # executed signals from signal tester
349
+ elif self._frame_has_cols(y, ["exec_price", "quantity"]):
350
+ _b_ords = yy[yy.quantity > 0]
351
+ _s_ords = yy[yy.quantity < 0]
352
+ plt.plot(
353
+ _b_ords.index,
354
+ _b_ords.exec_price.rename("BOT"),
355
+ marker="2",
356
+ markersize=10,
357
+ ls="",
358
+ c="#1080ff",
359
+ )
360
+ plt.plot(
361
+ _s_ords.index,
362
+ _s_ords.exec_price.rename("SLD"),
363
+ marker="1",
364
+ markersize=10,
365
+ ls="",
366
+ c="#909010",
367
+ )
368
+
369
+ # order executions from simulator
370
+ elif self._frame_has_cols(y, ["side", "fill_avg_price", "quantity", "status"]):
371
+ _b_ords = yy[yy.side == "BUY"]
372
+ _s_ords = yy[yy.side == "SELL"]
373
+ plt.plot(
374
+ _b_ords.index,
375
+ _b_ords.fill_avg_price.rename("BOT"),
376
+ marker="2",
377
+ markersize=10,
378
+ ls="",
379
+ c="#1080ff",
380
+ )
381
+ plt.plot(
382
+ _s_ords.index,
383
+ _s_ords.fill_avg_price.rename("SLD"),
384
+ marker="1",
385
+ markersize=10,
386
+ ls="",
387
+ c="#909010",
388
+ )
389
+
390
+ # experimental tester view of trading log
391
+ elif self._frame_has_cols(y, ["Action", "Price", "Info", "PnL_ticks"]):
392
+ _b_ords = yy[yy.Action == "Long"]
393
+ _s_ords = yy[yy.Action == "Short"]
394
+ _t_ords = yy[yy.Action == "Take"]
395
+ _l_ords = yy[yy.Action == "Stop"]
396
+ _e_ords = yy[(yy.Action == "Expired") | (yy.Action == "Flat")]
397
+ plt.plot(
398
+ _b_ords.index,
399
+ _b_ords.Price.rename("BOT"),
400
+ marker=6,
401
+ markersize=10,
402
+ ls="",
403
+ c="#3cfa00",
404
+ )
405
+ plt.plot(
406
+ _s_ords.index,
407
+ _s_ords.Price.rename("SLD"),
408
+ marker=7,
409
+ markersize=10,
410
+ ls="",
411
+ c="#20ffff",
412
+ )
413
+ plt.plot(
414
+ _t_ords.index,
415
+ _t_ords.CurrentPrice.rename("Take"),
416
+ marker="P",
417
+ markersize=10,
418
+ ls="",
419
+ c="#fffb00",
420
+ )
421
+ plt.plot(
422
+ _l_ords.index,
423
+ _l_ords.CurrentPrice.rename("Stop"),
424
+ marker="8",
425
+ markersize=10,
426
+ ls="",
427
+ c="#fffb00",
428
+ )
429
+ plt.plot(
430
+ _e_ords.index,
431
+ _e_ords.CurrentPrice.rename("Exp"),
432
+ marker="X",
433
+ markersize=10,
434
+ ls="",
435
+ c="#b0b0b0",
436
+ )
437
+
438
+ elif self._frame_has_cols(y, ["open", "high", "low", "close"]):
439
+ ohlc_plot(yy, width=self._ohlc_width, fmt=self._fmt)
440
+ _forced_limits = 0.999 * min(yy["low"]), 1.001 * max(yy["high"])
441
+ # temp hack to aling scales
442
+ plt.plot(yy["close"], lw=0, label=_lbl)
443
+ else:
444
+ for _col in yy.columns:
445
+ self.__plot_as_type(yy[_col], plot_style, self._n_style, _col)
446
+ else:
447
+ y = y.pd() if isinstance(y, TimeSeries) else y
448
+ yy = y[zoom] if zoom else y
449
+ self.__plot_as_type(yy, plot_style, self._n_style, _lbl)
450
+
451
+ # we want to see OHLC at maximal scale
452
+ return _forced_limits
453
+
454
+ def __plot_as_type(self, y, plot_style, line_style: str, label):
455
+ __clr = line_style[0] if len(line_style) > 0 and line_style[0].isalpha() else None
456
+ if plot_style == "line":
457
+ plt.plot(y, line_style, label=label)
458
+ elif plot_style == "area":
459
+ plt.fill_between(y.index, y, color=__clr, label=label)
460
+ elif plot_style.startswith("step"):
461
+ _where = "post" if "post" in plot_style else "pre"
462
+ plt.step(y.index, y, color=__clr, where=_where, label=label)
463
+ elif plot_style.startswith("bar"):
464
+ _bw = pd.Series(y.index).diff().mean().total_seconds() / 24 / 60 / 60
465
+ plt.bar(y.index, y, lw=0.4, width=_bw, edgecolor=__clr, color=__clr, label=label)
466
+
467
+ def _show_plot(self, vert_bar, title, zoom):
468
+ # plot all master series
469
+ shape = (self.s_size * len(self.s) + self.m_size, 1)
470
+ subplot(shape, 1, rowspan=self.m_size)
471
+ ms = self.m if isinstance(self.m, (tuple, list)) else [self.m]
472
+ _limits_to_set = None
473
+
474
+ for j, m in enumerate(ms):
475
+ # if style description
476
+ if isinstance(m, str):
477
+ self._n_style = m
478
+ else:
479
+ _lims = self.__plt_series(m, zoom, "Master", j)
480
+ self._n_style = "-"
481
+ if _limits_to_set is None and _lims:
482
+ _limits_to_set = _lims
483
+
484
+ # special case
485
+ if _limits_to_set:
486
+ plt.ylim(*_limits_to_set)
487
+
488
+ if self.legend_loc:
489
+ plt.legend(loc=self.legend_loc)
490
+
491
+ if vert_bar:
492
+ plt.axvline(vert_bar, ls="-.", lw=0.5)
493
+ if title is None:
494
+ plt.title("%s %s" % (self._title, str(vert_bar)))
495
+
496
+ if title is not None:
497
+ plt.title("%s %s" % (self._title, str(title)))
498
+
499
+ # plot studies
500
+ i = 1 + self.m_size
501
+ for k, vs in self.s.items():
502
+ subplot(shape, i, rowspan=self.s_size)
503
+ vs = vs if isinstance(vs, (tuple, list)) else [vs]
504
+ self._n_style = "-"
505
+ plot_style = "line"
506
+
507
+ wait_for_limits = False
508
+ for j, v in enumerate(vs):
509
+ # if we need to read limits
510
+ if wait_for_limits:
511
+ if isinstance(v, (list, tuple)) and len(v) > 1:
512
+ plt.ylim(*v)
513
+ wait_for_limits = False
514
+ continue
515
+
516
+ # if style description
517
+ if isinstance(v, str):
518
+ vl = v.lower()
519
+ if vl.startswith("lim"):
520
+ wait_for_limits = True
521
+ elif any([vl.startswith(x) for x in ["line", "bar", "step", "stem", "area"]]):
522
+ plot_style = vl
523
+ else:
524
+ self._n_style = v
525
+ else:
526
+ self.__plt_series(v, zoom, k, j, plot_style=plot_style)
527
+
528
+ if vert_bar:
529
+ plt.axvline(vert_bar, ls="-.", lw=0.5)
530
+
531
+ i += self.s_size
532
+ plt.legend(loc=self.legend_loc)
533
+
534
+ self._n_style = "-"
535
+
536
+
537
+ class LookingGlassPlotly(AbstractLookingGlass):
538
+ TREND_COLORS = Struct(
539
+ uline="#ffffff",
540
+ dline="#ffffff",
541
+ udot="rgb(10,168,10)",
542
+ ddot="rgb(168,10,10)",
543
+ )
544
+
545
+ def __init__(
546
+ self,
547
+ master,
548
+ studies: dict | None = None,
549
+ master_plot_height=400,
550
+ study_plot_height=100,
551
+ title="",
552
+ ):
553
+ super().__init__(master, studies, title)
554
+ self.mph = master_plot_height
555
+ self.sph = study_plot_height
556
+ self._n_style = "-"
557
+
558
+ def __plt_series(self, y, zoom, study_name, k, row, col, plot_style="line"):
559
+ _lbl = y.name if hasattr(y, "name") and y.name else ("%s_%d" % (study_name, k))
560
+
561
+ def _scatter(
562
+ xs: pd.Series, comments: pd.Series | None, name: str, marker: str, color: str, size: int = 11
563
+ ) -> None:
564
+ _args = dict(
565
+ x=xs.index,
566
+ y=xs,
567
+ mode="markers",
568
+ name=name,
569
+ text=comments,
570
+ marker={
571
+ "symbol": marker,
572
+ "size": size,
573
+ "color": color,
574
+ },
575
+ )
576
+ self.fig.add_trace(go.Scatter(**_args), row=row, col=col)
577
+
578
+ if isinstance(y, (pd.DataFrame, OHLCV)):
579
+ y = y.pd() if isinstance(y, OHLCV) else y
580
+ yy = y[zoom] if zoom else y
581
+
582
+ # candlesticks
583
+ if self._frame_has_cols(y, ["open", "high", "low", "close"]):
584
+ self.fig.add_trace(
585
+ go.Candlestick(
586
+ x=yy.index,
587
+ open=yy["open"],
588
+ high=yy["high"],
589
+ low=yy["low"],
590
+ close=yy["close"],
591
+ name=_lbl,
592
+ line={"width": 1},
593
+ ),
594
+ row=row,
595
+ col=col,
596
+ )
597
+
598
+ # trends
599
+ elif self._frame_has_cols(y, ["UpTrends", "DownTrends"]):
600
+ u, d = yy.UpTrends.dropna(), yy.DownTrends.dropna()
601
+ for i, r in enumerate(u.iterrows()):
602
+ self.fig.add_trace(
603
+ go.Scatter(
604
+ x=[r[0], r[1].end],
605
+ y=[r[1].start_price, r[1].end_price],
606
+ mode="lines+markers",
607
+ name="UpTrends",
608
+ line={
609
+ "color": LookingGlassPlotly.TREND_COLORS.uline,
610
+ "width": 1,
611
+ "dash": "dot",
612
+ },
613
+ marker={"color": LookingGlassPlotly.TREND_COLORS.udot},
614
+ showlegend=i == 0,
615
+ legendgroup="trends_UpTrends",
616
+ ),
617
+ row=row,
618
+ col=col,
619
+ )
620
+
621
+ for i, r in enumerate(d.iterrows()):
622
+ self.fig.add_trace(
623
+ go.Scatter(
624
+ x=[r[0], r[1].end],
625
+ y=[r[1].start_price, r[1].end_price],
626
+ mode="lines+markers",
627
+ name="DownTrends",
628
+ line={
629
+ "color": LookingGlassPlotly.TREND_COLORS.dline,
630
+ "width": 1,
631
+ "dash": "dot",
632
+ },
633
+ marker={"color": LookingGlassPlotly.TREND_COLORS.ddot},
634
+ showlegend=i == 0,
635
+ legendgroup="trends_DownTrends",
636
+ ),
637
+ row=row,
638
+ col=col,
639
+ )
640
+
641
+ # order executions from simulator
642
+ elif self._frame_has_cols(y, ["side", "fill_avg_price", "quantity", "status"]):
643
+ yy = yy[yy.status == "FILLED"]
644
+ _b_ords = yy[yy.side == "BUY"]
645
+ _s_ords = yy[yy.side == "SELL"]
646
+ _info_b = "INFO : " + _b_ords["user_description"]
647
+ _info_s = "INFO : " + _s_ords["user_description"]
648
+
649
+ self.fig.add_trace(
650
+ go.Scatter(
651
+ x=_b_ords.index,
652
+ y=_b_ords.fill_avg_price,
653
+ mode="markers",
654
+ name="BOT",
655
+ text=_info_b,
656
+ marker={
657
+ "symbol": "triangle-up",
658
+ "size": 13,
659
+ "color": "#3cfa00",
660
+ },
661
+ ),
662
+ row=row,
663
+ col=col,
664
+ )
665
+ self.fig.add_trace(
666
+ go.Scatter(
667
+ x=_s_ords.index,
668
+ y=_s_ords.fill_avg_price,
669
+ mode="markers",
670
+ name="SLD",
671
+ text=_info_s,
672
+ marker={
673
+ "symbol": "triangle-down",
674
+ "size": 13,
675
+ "color": "#20ffff",
676
+ },
677
+ ),
678
+ row=row,
679
+ col=col,
680
+ )
681
+
682
+ # experimental tester view of trading log
683
+ elif self._frame_has_cols(y, ["Action", "Price", "Info", "PnL_ticks"]):
684
+ _b_ords = yy[yy.Action == "Long"]
685
+ _s_ords = yy[yy.Action == "Short"]
686
+ _t_ords = yy[yy.Action == "Take"]
687
+ _l_ords = yy[yy.Action == "Stop"]
688
+ _e_ords = yy[(yy.Action == "Expired") | (yy.Action == "Flat")]
689
+ _info = "INFO : " + yy["Info"] + "<br>PnL Ticks: " + yy["PnL_ticks"].astype(str)
690
+
691
+ self.fig.add_trace(
692
+ go.Scatter(
693
+ x=_b_ords.index,
694
+ y=_b_ords.Price,
695
+ mode="markers",
696
+ name="BOT",
697
+ text=_info,
698
+ marker={
699
+ "symbol": "triangle-up",
700
+ "size": 13,
701
+ "color": "#3cfa00",
702
+ },
703
+ ),
704
+ row=row,
705
+ col=col,
706
+ )
707
+
708
+ self.fig.add_trace(
709
+ go.Scatter(
710
+ x=_s_ords.index,
711
+ y=_s_ords.Price,
712
+ mode="markers",
713
+ name="SLD",
714
+ text=_info,
715
+ marker={
716
+ "symbol": "triangle-down",
717
+ "size": 13,
718
+ "color": "#20ffff",
719
+ },
720
+ ),
721
+ row=row,
722
+ col=col,
723
+ )
724
+
725
+ self.fig.add_trace(
726
+ go.Scatter(
727
+ x=_t_ords.index,
728
+ y=_t_ords.CurrentPrice,
729
+ mode="markers",
730
+ name="Take",
731
+ text=_info,
732
+ marker={"symbol": "cross", "size": 13},
733
+ ),
734
+ row=row,
735
+ col=col,
736
+ )
737
+
738
+ self.fig.add_trace(
739
+ go.Scatter(
740
+ x=_l_ords.index,
741
+ y=_l_ords.CurrentPrice,
742
+ mode="markers",
743
+ name="Stop",
744
+ text=_info,
745
+ marker={"symbol": "circle", "size": 13},
746
+ ),
747
+ row=row,
748
+ col=col,
749
+ )
750
+
751
+ self.fig.add_trace(
752
+ go.Scatter(
753
+ x=_e_ords.index,
754
+ y=_e_ords.CurrentPrice,
755
+ mode="markers",
756
+ name="Exp",
757
+ text=_info,
758
+ marker={"symbol": "x", "size": 13},
759
+ ),
760
+ row=row,
761
+ col=col,
762
+ )
763
+
764
+ # reversal points
765
+ elif self._frame_has_cols(y, ["start_price", "delta"]):
766
+ _lo_pts = yy[yy.delta > 0]
767
+ _hi_pts = yy[yy.delta < 0]
768
+ self.fig.add_trace(
769
+ go.Scatter(
770
+ x=_lo_pts.index,
771
+ y=_lo_pts.start_price,
772
+ mode="markers",
773
+ name="Bottom",
774
+ marker={"symbol": "triangle-up"},
775
+ ),
776
+ row=row,
777
+ col=col,
778
+ )
779
+ self.fig.add_trace(
780
+ go.Scatter(
781
+ x=_hi_pts.index,
782
+ y=_hi_pts.start_price,
783
+ mode="markers",
784
+ name="Top",
785
+ marker={"symbol": "triangle-down"},
786
+ ),
787
+ row=row,
788
+ col=col,
789
+ )
790
+
791
+ # tracks
792
+ elif self._frame_has_cols(y, ["Type", "Time", "Price", "PriceOccured"]):
793
+ _bot = yy[yy.Type == "-"]
794
+ _top = yy[yy.Type == "+"]
795
+
796
+ _scatter(_bot.PriceOccured, None, "B", "triangle-right", "#3cfa00", 12)
797
+ _scatter(_top.PriceOccured, None, "T", "triangle-right", "#3cfa00", 12)
798
+
799
+ # executions from executor's log
800
+ elif self._frame_has_cols(y, ["exec_price", "quantity"]):
801
+ _b_ords = yy[yy.quantity > 0]
802
+ _s_ords = yy[yy.quantity < 0]
803
+
804
+ # - how much was traded
805
+ _b_info = "<i>bought</i> " + _b_ords.quantity.astype(str)
806
+ _s_info = "<i>sold</i> " + _s_ords.quantity.astype(str)
807
+
808
+ _scatter(_b_ords.exec_price, _b_info, "BOT", "triangle-up", "#25ff00", 12)
809
+ _scatter(_s_ords.exec_price, _s_info, "SLD", "triangle-down", "#f0d", 12)
810
+ # _scatter(_s_ords.exec_price, _s_info, "SLD", "triangle-down", "#20ffff", 12)
811
+
812
+ # 27-aug-2024: show generated signals from Qubx backtester
813
+ elif self._frame_has_cols(y, ["signal", "reference_price", "price", "take", "stop", "group", "comment"]):
814
+ _sel_price = lambda x: x["reference_price"].where(x["price"].isna(), x["price"]) # noqa: E731
815
+ _b_sigs = yy[yy.signal > 0]
816
+ _z_sigs = yy[yy.signal == 0]
817
+ _s_sigs = yy[yy.signal < 0]
818
+ # - Longs
819
+ _scatter(_sel_price(_b_sigs), _b_sigs["comment"], "LONG", "triangle-up-open-dot", "#3cfa00")
820
+ _scatter(_b_sigs[~_b_sigs["take"].isna()]["take"], None, "Take", "line-ew-open", "#3cfa00")
821
+ _scatter(_b_sigs[~_b_sigs["stop"].isna()]["stop"], None, "Stop", "line-ew-open", "#fa3c00")
822
+
823
+ # - Shorts
824
+ _scatter(_sel_price(_s_sigs), _s_sigs["comment"], "SHORTS", "triangle-down-open-dot", "#20ffff")
825
+ _scatter(_s_sigs[~_s_sigs["take"].isna()]["take"], None, "Take", "line-ew-open", "#3cfa00")
826
+ _scatter(_s_sigs[~_s_sigs["stop"].isna()]["stop"], None, "Stop", "line-ew-open", "#fa3c00")
827
+
828
+ # - Exits
829
+ _scatter(_sel_price(_z_sigs), _z_sigs["comment"], "EXITS", "circle-open-dot", "#ffffff")
830
+
831
+ else:
832
+ for _col in yy.columns:
833
+ self.__plot_as_type(yy[_col], row, col, plot_style, _col)
834
+ else:
835
+ y = y.pd() if isinstance(y, TimeSeries) else y
836
+ yy = y[zoom] if zoom else y
837
+ self.__plot_as_type(yy, row, col, plot_style, _lbl)
838
+
839
+ def __plot_as_type(self, y, row, col, plot_style, label):
840
+ style, color = self.__line_style_to_color_dash(self._n_style)
841
+ if plot_style == "line":
842
+ self.fig.add_trace(
843
+ go.Scatter(
844
+ x=y.index, y=y, mode="lines", line={"width": 0.5, "dash": style, "color": color}, name=label
845
+ ),
846
+ row=row,
847
+ col=col,
848
+ )
849
+ elif plot_style == "area":
850
+ self.fig.add_trace(
851
+ go.Scatter(x=y.index, y=y, mode="lines", line={"width": 1, "color": color}, fill="tozeroy", name=label),
852
+ row=row,
853
+ col=col,
854
+ )
855
+ elif plot_style.startswith("step"):
856
+ self.fig.add_trace(
857
+ go.Scatter(x=y.index, y=y, mode="lines", line={"shape": "hv", "color": color}, name=label),
858
+ row=row,
859
+ col=col,
860
+ )
861
+ elif plot_style.startswith("bar"):
862
+ self.fig.add_trace(go.Bar(x=y.index, y=y, name=label, marker_color=color), row=row, col=col)
863
+ elif plot_style.startswith("dots") or plot_style.startswith("point"):
864
+ self.fig.add_trace(
865
+ go.Scatter(
866
+ x=y.index,
867
+ y=y,
868
+ mode="markers",
869
+ name=label,
870
+ marker_color=color,
871
+ marker={"symbol": "circle", "size": 4},
872
+ ),
873
+ row=row,
874
+ col=col,
875
+ )
876
+ elif plot_style.startswith("arrow"):
877
+ self.fig.add_trace(
878
+ go.Scatter(
879
+ x=y.index,
880
+ y=y,
881
+ mode="markers",
882
+ name=label,
883
+ marker_color=color,
884
+ marker={"symbol": plot_style, "size": 12},
885
+ ),
886
+ row=row,
887
+ col=col,
888
+ )
889
+
890
+ def __line_style_to_color_dash(self, style):
891
+ """
892
+ Convert mpl format color-style to plotly format
893
+ :param style: style in format 'color style'. Example: 'r --' means red color and dash style line.
894
+ :return style, color:
895
+ """
896
+ plotly_styles = ["dash", "dot", "dashdot", "solid"]
897
+ splitted = style.split(" ")
898
+ if len(splitted) > 1:
899
+ mpl_color, mpl_style = splitted[0], splitted[1]
900
+ else:
901
+ mpl_color, mpl_style = None, splitted[0]
902
+
903
+ plotly_style, plotly_color = None, None
904
+
905
+ if mpl_style in plotly_styles: # specified plotly style line
906
+ plotly_style = mpl_style
907
+ elif mpl_style == ":":
908
+ plotly_style = "dot"
909
+ elif mpl_style == "--":
910
+ plotly_style = "dash"
911
+ elif mpl_style == "-.":
912
+ plotly_style = "dashdot"
913
+ elif mpl_style == "-":
914
+ plotly_style = "solid"
915
+
916
+ if plotly_style is None and mpl_color is None: # it looks only color is specified
917
+ mpl_color = mpl_style
918
+
919
+ if mpl_color == "r":
920
+ plotly_color = "red"
921
+ elif mpl_color == "g":
922
+ plotly_color = "green"
923
+ elif mpl_color == "w":
924
+ plotly_color = "white"
925
+ else:
926
+ plotly_color = mpl_color
927
+
928
+ return plotly_style, plotly_color
929
+
930
+ # def __mpl_color_to_plotly(self, color):
931
+ # if color == "r":
932
+ # color = "red"
933
+ # elif color == "g":
934
+ # color = "green"
935
+ # elif color == "w":
936
+ # color = "white"
937
+ # return color
938
+
939
+ def _show_plot(self, vert_bar, title, zoom):
940
+ # plot all master series
941
+ master_fraction = self.mph / (self.mph + self.sph * len(self.s)) if len(self.s) else 1.0
942
+ row_heights = [master_fraction]
943
+ axis_rules = {}
944
+ if len(self.s):
945
+ row_heights.extend([(1 - master_fraction) / len(self.s)] * len(self.s))
946
+
947
+ self.fig = make_subplots(
948
+ rows=len(self.s) + 1, cols=1, shared_xaxes=True, vertical_spacing=0.01, row_heights=row_heights
949
+ )
950
+
951
+ ms = self.m if isinstance(self.m, (tuple, list)) else [self.m]
952
+ plot_style = "line"
953
+ for j, m in enumerate(ms):
954
+ if isinstance(m, str):
955
+ if any(
956
+ [
957
+ m.startswith(x)
958
+ for x in [
959
+ "line",
960
+ "bar",
961
+ "step",
962
+ "stem",
963
+ "area",
964
+ "dots",
965
+ "point",
966
+ "arrow-up",
967
+ "arrow-down",
968
+ ]
969
+ ]
970
+ ):
971
+ plot_style = m
972
+ else:
973
+ self._n_style = m
974
+ else:
975
+ self.__plt_series(m, zoom, "Master", j, 1, 1, plot_style=plot_style)
976
+ self._n_style = "-"
977
+
978
+ if vert_bar:
979
+ self.__add_vline("x", vert_bar)
980
+ i = 1
981
+ for k, vs in self.s.items():
982
+ wait_for_limits = False
983
+ i += 1
984
+ vs = vs if isinstance(vs, (tuple, list)) else [vs]
985
+ plot_style = "line"
986
+ self._n_style = "-"
987
+ for j, v in enumerate(vs):
988
+ if wait_for_limits and isinstance(v, (list, tuple)) and len(v) > 1:
989
+ axis_rules["yaxis%d" % i] = {"range": v}
990
+ wait_for_limits = False
991
+
992
+ elif isinstance(v, str):
993
+ vl = v.lower()
994
+ if vl.startswith("lim"):
995
+ wait_for_limits = True
996
+ continue
997
+ elif any(
998
+ [
999
+ vl.startswith(x)
1000
+ for x in [
1001
+ "line",
1002
+ "bar",
1003
+ "step",
1004
+ "stem",
1005
+ "area",
1006
+ "dots",
1007
+ "point",
1008
+ "arrow-up",
1009
+ "arrow-down",
1010
+ ]
1011
+ ]
1012
+ ):
1013
+ plot_style = vl
1014
+ else:
1015
+ self._n_style = v
1016
+ elif isinstance(v, (float, int)):
1017
+ self.__add_hline("y%d" % i, v)
1018
+ else:
1019
+ self.__plt_series(v, zoom, k, j, i, 1, plot_style=plot_style)
1020
+
1021
+ plot_title = title or self._title or ""
1022
+ plot_title += str(vert_bar) if vert_bar else "" # add title to plot
1023
+ if plot_title:
1024
+ self.fig.update_layout(
1025
+ title={"text": plot_title, "y": 0.9, "x": 0.5, "xanchor": "center", "yanchor": "top"}
1026
+ )
1027
+
1028
+ self.fig.update_layout(axis_rules)
1029
+ self.fig.update_layout(
1030
+ xaxis_rangeslider_visible=False, margin=dict(l=5, r=5, t=35, b=5), height=self.mph + self.sph * len(self.s)
1031
+ )
1032
+
1033
+ return go.FigureWidget(self.fig)
1034
+
1035
+ def __add_vline(self, xref, x):
1036
+ # Line Vertical
1037
+ self.fig.add_shape(
1038
+ go.layout.Shape(
1039
+ type="line", x0=x, x1=x, xref=xref, yref="paper", y0=0, y1=1, line=dict(width=1, dash="dot")
1040
+ )
1041
+ )
1042
+
1043
+ def __add_hline(self, yref, y):
1044
+ # Line Horizontal
1045
+ style, color = self.__line_style_to_color_dash(self._n_style)
1046
+ self.fig.add_shape(
1047
+ go.layout.Shape(
1048
+ type="line",
1049
+ x0=0,
1050
+ x1=1,
1051
+ xref="paper",
1052
+ yref=yref,
1053
+ y0=y,
1054
+ y1=y,
1055
+ line=dict(width=1, dash=style, color=color),
1056
+ )
1057
+ )