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.
- qubx/__init__.py +207 -0
- qubx/_nb_magic.py +100 -0
- qubx/backtester/__init__.py +5 -0
- qubx/backtester/account.py +145 -0
- qubx/backtester/broker.py +87 -0
- qubx/backtester/data.py +296 -0
- qubx/backtester/management.py +378 -0
- qubx/backtester/ome.py +296 -0
- qubx/backtester/optimization.py +201 -0
- qubx/backtester/simulated_data.py +558 -0
- qubx/backtester/simulator.py +362 -0
- qubx/backtester/utils.py +780 -0
- qubx/cli/__init__.py +0 -0
- qubx/cli/commands.py +67 -0
- qubx/connectors/ccxt/__init__.py +0 -0
- qubx/connectors/ccxt/account.py +495 -0
- qubx/connectors/ccxt/broker.py +132 -0
- qubx/connectors/ccxt/customizations.py +193 -0
- qubx/connectors/ccxt/data.py +612 -0
- qubx/connectors/ccxt/exceptions.py +17 -0
- qubx/connectors/ccxt/factory.py +93 -0
- qubx/connectors/ccxt/utils.py +307 -0
- qubx/core/__init__.py +0 -0
- qubx/core/account.py +251 -0
- qubx/core/basics.py +850 -0
- qubx/core/context.py +420 -0
- qubx/core/exceptions.py +38 -0
- qubx/core/helpers.py +480 -0
- qubx/core/interfaces.py +1150 -0
- qubx/core/loggers.py +514 -0
- qubx/core/lookups.py +475 -0
- qubx/core/metrics.py +1512 -0
- qubx/core/mixins/__init__.py +13 -0
- qubx/core/mixins/market.py +94 -0
- qubx/core/mixins/processing.py +428 -0
- qubx/core/mixins/subscription.py +203 -0
- qubx/core/mixins/trading.py +88 -0
- qubx/core/mixins/universe.py +270 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +125 -0
- qubx/core/series.pyi +118 -0
- qubx/core/series.pyx +988 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyi +6 -0
- qubx/core/utils.pyx +62 -0
- qubx/data/__init__.py +25 -0
- qubx/data/helpers.py +416 -0
- qubx/data/readers.py +1562 -0
- qubx/data/tardis.py +100 -0
- qubx/gathering/simplest.py +88 -0
- qubx/math/__init__.py +3 -0
- qubx/math/stats.py +129 -0
- qubx/pandaz/__init__.py +23 -0
- qubx/pandaz/ta.py +2757 -0
- qubx/pandaz/utils.py +638 -0
- qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx/resources/instruments/symbols-binance.json +1 -0
- qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx/ta/__init__.py +0 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +149 -0
- qubx/ta/indicators.pyi +41 -0
- qubx/ta/indicators.pyx +787 -0
- qubx/trackers/__init__.py +3 -0
- qubx/trackers/abvanced.py +236 -0
- qubx/trackers/composite.py +146 -0
- qubx/trackers/rebalancers.py +129 -0
- qubx/trackers/riskctrl.py +641 -0
- qubx/trackers/sizers.py +235 -0
- qubx/utils/__init__.py +5 -0
- qubx/utils/_pyxreloader.py +281 -0
- qubx/utils/charting/lookinglass.py +1057 -0
- qubx/utils/charting/mpl_helpers.py +1183 -0
- qubx/utils/marketdata/binance.py +284 -0
- qubx/utils/marketdata/ccxt.py +90 -0
- qubx/utils/marketdata/dukas.py +130 -0
- qubx/utils/misc.py +541 -0
- qubx/utils/ntp.py +63 -0
- qubx/utils/numbers_utils.py +7 -0
- qubx/utils/orderbook.py +491 -0
- qubx/utils/plotting/__init__.py +0 -0
- qubx/utils/plotting/dashboard.py +150 -0
- qubx/utils/plotting/data.py +137 -0
- qubx/utils/plotting/interfaces.py +25 -0
- qubx/utils/plotting/renderers/__init__.py +0 -0
- qubx/utils/plotting/renderers/plotly.py +0 -0
- qubx/utils/runner/__init__.py +1 -0
- qubx/utils/runner/_jupyter_runner.pyt +60 -0
- qubx/utils/runner/accounts.py +88 -0
- qubx/utils/runner/configs.py +65 -0
- qubx/utils/runner/runner.py +470 -0
- qubx/utils/time.py +312 -0
- qubx-0.5.7.dist-info/METADATA +105 -0
- qubx-0.5.7.dist-info/RECORD +100 -0
- qubx-0.5.7.dist-info/WHEEL +4 -0
- 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
|
+
)
|