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
qubx/ta/indicators.pyx
ADDED
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
cimport numpy as np
|
|
4
|
+
from scipy.special.cython_special import ndtri, stdtrit, gamma
|
|
5
|
+
from collections import deque
|
|
6
|
+
|
|
7
|
+
from qubx.core.series cimport TimeSeries, Indicator, IndicatorOHLC, RollingSum, nans, OHLCV, Bar
|
|
8
|
+
from qubx.core.utils import time_to_str
|
|
9
|
+
from qubx.pandaz.utils import scols, srows
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
cdef extern from "math.h":
|
|
13
|
+
float INFINITY
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
cdef class Sma(Indicator):
|
|
17
|
+
"""
|
|
18
|
+
Simple Moving Average indicator
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, str name, TimeSeries series, int period):
|
|
22
|
+
self.period = period
|
|
23
|
+
self.summator = RollingSum(period)
|
|
24
|
+
super().__init__(name, series)
|
|
25
|
+
|
|
26
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
27
|
+
cdef double r = self.summator.update(value, new_item_started)
|
|
28
|
+
return np.nan if self.summator.is_init_stage else r / self.period
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sma(series:TimeSeries, period: int):
|
|
32
|
+
return Sma.wrap(series, period)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
cdef class Ema(Indicator):
|
|
36
|
+
"""
|
|
37
|
+
Exponential moving average
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, str name, TimeSeries series, int period, init_mean=True):
|
|
41
|
+
self.period = period
|
|
42
|
+
|
|
43
|
+
# when it's required to initialize this ema by mean on first period
|
|
44
|
+
self.init_mean = init_mean
|
|
45
|
+
if init_mean:
|
|
46
|
+
self.__s = nans(period)
|
|
47
|
+
self.__i = 0
|
|
48
|
+
|
|
49
|
+
self._init_stage = 1
|
|
50
|
+
self.alpha = 2.0 / (1.0 + period)
|
|
51
|
+
self.alpha_1 = (1 - self.alpha)
|
|
52
|
+
super().__init__(name, series)
|
|
53
|
+
|
|
54
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
55
|
+
cdef int prev_bar_idx = 0 if new_item_started else 1
|
|
56
|
+
|
|
57
|
+
if self._init_stage:
|
|
58
|
+
if np.isnan(value): return np.nan
|
|
59
|
+
|
|
60
|
+
if new_item_started:
|
|
61
|
+
self.__i += 1
|
|
62
|
+
if self.__i > self.period - 1:
|
|
63
|
+
self._init_stage = False
|
|
64
|
+
return self.alpha * value + self.alpha_1 * self[prev_bar_idx]
|
|
65
|
+
|
|
66
|
+
if self.__i == self.period - 1:
|
|
67
|
+
self.__s[self.__i] = value
|
|
68
|
+
return np.nansum(self.__s) / self.period
|
|
69
|
+
|
|
70
|
+
self.__s[self.__i] = value
|
|
71
|
+
return np.nan
|
|
72
|
+
|
|
73
|
+
if len(self) == 0:
|
|
74
|
+
return value
|
|
75
|
+
|
|
76
|
+
return self.alpha * value + self.alpha_1 * self[prev_bar_idx]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def ema(series:TimeSeries, period: int, init_mean: bool = True):
|
|
80
|
+
return Ema.wrap(series, period, init_mean=init_mean)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
cdef class Tema(Indicator):
|
|
84
|
+
|
|
85
|
+
def __init__(self, str name, TimeSeries series, int period, init_mean=True):
|
|
86
|
+
self.period = period
|
|
87
|
+
self.init_mean = init_mean
|
|
88
|
+
self.ser0 = TimeSeries('ser0', series.timeframe, series.max_series_length)
|
|
89
|
+
self.ema1 = ema(self.ser0, period, init_mean)
|
|
90
|
+
self.ema2 = ema(self.ema1, period, init_mean)
|
|
91
|
+
self.ema3 = ema(self.ema2, period, init_mean)
|
|
92
|
+
super().__init__(name, series)
|
|
93
|
+
|
|
94
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
95
|
+
self.ser0.update(time, value)
|
|
96
|
+
return 3 * self.ema1[0] - 3 * self.ema2[0] + self.ema3[0]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def tema(series:TimeSeries, period: int, init_mean: bool = True):
|
|
100
|
+
return Tema.wrap(series, period, init_mean=init_mean)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
cdef class Dema(Indicator):
|
|
104
|
+
|
|
105
|
+
def __init__(self, str name, TimeSeries series, int period, init_mean=True):
|
|
106
|
+
self.period = period
|
|
107
|
+
self.init_mean = init_mean
|
|
108
|
+
self.ser0 = TimeSeries('ser0', series.timeframe, series.max_series_length)
|
|
109
|
+
self.ema1 = ema(self.ser0, period, init_mean)
|
|
110
|
+
self.ema2 = ema(self.ema1, period, init_mean)
|
|
111
|
+
super().__init__(name, series)
|
|
112
|
+
|
|
113
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
114
|
+
self.ser0.update(time, value)
|
|
115
|
+
return 2 * self.ema1[0] - self.ema2[0]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def dema(series:TimeSeries, period: int, init_mean: bool = True):
|
|
119
|
+
return Dema.wrap(series, period, init_mean=init_mean)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
cdef class Kama(Indicator):
|
|
123
|
+
# cdef int period
|
|
124
|
+
# cdef int fast_span
|
|
125
|
+
# cdef int slow_span
|
|
126
|
+
# cdef double _S1
|
|
127
|
+
# cdef double _K1
|
|
128
|
+
# cdef _x_past
|
|
129
|
+
# cdef RollingSum summator
|
|
130
|
+
|
|
131
|
+
def __init__(self, str name, TimeSeries series, int period, int fast_span=2, int slow_span=30):
|
|
132
|
+
self.period = period
|
|
133
|
+
self.fast_span = fast_span
|
|
134
|
+
self.slow_span = slow_span
|
|
135
|
+
self._S1 = 2.0 / (slow_span + 1)
|
|
136
|
+
self._K1 = 2.0 / (fast_span + 1) - self._S1
|
|
137
|
+
self._x_past = deque(nans(period+1), period+1)
|
|
138
|
+
self.summator = RollingSum(period)
|
|
139
|
+
super().__init__(name, series)
|
|
140
|
+
|
|
141
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
142
|
+
if new_item_started:
|
|
143
|
+
self._x_past.append(value)
|
|
144
|
+
else:
|
|
145
|
+
self._x_past[-1] = value
|
|
146
|
+
|
|
147
|
+
cdef double rs = self.summator.update(abs(value - self._x_past[-2]), new_item_started)
|
|
148
|
+
cdef double er = (abs(value - self._x_past[0]) / rs) if rs != 0.0 else 1.0
|
|
149
|
+
cdef double sc = (er * self._K1 + self._S1) ** 2
|
|
150
|
+
|
|
151
|
+
if self.summator.is_init_stage:
|
|
152
|
+
if not np.isnan(self._x_past[1]):
|
|
153
|
+
return value
|
|
154
|
+
return np.nan
|
|
155
|
+
|
|
156
|
+
return sc * value + (1 - sc) * self[0 if new_item_started else 1]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def kama(series:TimeSeries, period: int, fast_span:int=2, slow_span:int=30):
|
|
160
|
+
return Kama.wrap(series, period, fast_span, slow_span)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
cdef class Highest(Indicator):
|
|
164
|
+
|
|
165
|
+
def __init__(self, str name, TimeSeries series, int period):
|
|
166
|
+
self.period = period
|
|
167
|
+
self.queue = deque([np.nan] * period, maxlen=period)
|
|
168
|
+
super().__init__(name, series)
|
|
169
|
+
|
|
170
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
171
|
+
"""
|
|
172
|
+
Not a most effictive algo but simplest and can handle updated last value
|
|
173
|
+
"""
|
|
174
|
+
cdef float r = np.nan
|
|
175
|
+
|
|
176
|
+
if not np.isnan(value):
|
|
177
|
+
if new_item_started:
|
|
178
|
+
self.queue.append(value)
|
|
179
|
+
else:
|
|
180
|
+
self.queue[-1] = value
|
|
181
|
+
|
|
182
|
+
if not np.isnan(self.queue[0]):
|
|
183
|
+
r = max(self.queue)
|
|
184
|
+
|
|
185
|
+
return r
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def highest(series:TimeSeries, period:int):
|
|
189
|
+
return Highest.wrap(series, period)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
cdef class Lowest(Indicator):
|
|
193
|
+
|
|
194
|
+
def __init__(self, str name, TimeSeries series, int period):
|
|
195
|
+
self.period = period
|
|
196
|
+
self.queue = deque([np.nan] * period, maxlen=period)
|
|
197
|
+
super().__init__(name, series)
|
|
198
|
+
|
|
199
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
200
|
+
"""
|
|
201
|
+
Not a most effictive algo but simplest and can handle updated last value
|
|
202
|
+
"""
|
|
203
|
+
cdef float r = np.nan
|
|
204
|
+
|
|
205
|
+
if not np.isnan(value):
|
|
206
|
+
if new_item_started:
|
|
207
|
+
self.queue.append(value)
|
|
208
|
+
else:
|
|
209
|
+
self.queue[-1] = value
|
|
210
|
+
|
|
211
|
+
if not np.isnan(self.queue[0]):
|
|
212
|
+
r = min(self.queue)
|
|
213
|
+
|
|
214
|
+
return r
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def lowest(series:TimeSeries, period:int):
|
|
218
|
+
return Lowest.wrap(series, period)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
cdef class Std(Indicator):
|
|
222
|
+
"""
|
|
223
|
+
Streaming Standard Deviation indicator
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def __init__(self, str name, TimeSeries series, int period):
|
|
227
|
+
self.period = period
|
|
228
|
+
self.rolling_sum = RollingSum(period)
|
|
229
|
+
self.variance_sum = RollingSum(period)
|
|
230
|
+
super().__init__(name, series)
|
|
231
|
+
|
|
232
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
233
|
+
# Update the rolling sum with the new value
|
|
234
|
+
cdef double _sum = self.rolling_sum.update(value, new_item_started)
|
|
235
|
+
|
|
236
|
+
# If we're still in the initialization stage, return NaN
|
|
237
|
+
if self.rolling_sum.is_init_stage:
|
|
238
|
+
return np.nan
|
|
239
|
+
|
|
240
|
+
# Calculate the mean from the rolling sum
|
|
241
|
+
cdef double _mean = _sum / self.period
|
|
242
|
+
|
|
243
|
+
# Update the variance sum with the squared deviation from the mean
|
|
244
|
+
cdef double _var_sum = self.variance_sum.update((value - _mean) ** 2, new_item_started)
|
|
245
|
+
|
|
246
|
+
# If the variance sum is still in the initialization stage, return NaN
|
|
247
|
+
if self.variance_sum.is_init_stage:
|
|
248
|
+
return np.nan
|
|
249
|
+
|
|
250
|
+
# Return the square root of the variance (standard deviation)
|
|
251
|
+
return np.sqrt(_var_sum / self.period)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def std(series: TimeSeries, period: int, mean=0):
|
|
255
|
+
return Std.wrap(series, period)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
cdef double norm_pdf(double x):
|
|
259
|
+
return np.exp(-x ** 2 / 2) / np.sqrt(2 * np.pi)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
cdef double lognorm_pdf(double x, double s):
|
|
263
|
+
return np.exp(-np.log(x) ** 2 / (2 * s ** 2)) / (x * s * np.sqrt(2 * np.pi))
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
cdef double student_t_pdf(double x, double df):
|
|
267
|
+
"""Compute the PDF of the Student's t-distribution."""
|
|
268
|
+
gamma_df = gamma(df / 2.0)
|
|
269
|
+
gamma_df_plus_1 = gamma((df + 1) / 2.0)
|
|
270
|
+
|
|
271
|
+
# Normalization constant
|
|
272
|
+
normalization = gamma_df_plus_1 / (np.sqrt(df * np.pi) * gamma_df)
|
|
273
|
+
|
|
274
|
+
# PDF calculation
|
|
275
|
+
term = (1 + (x ** 2) / df) ** (-(df + 1) / 2.0)
|
|
276
|
+
pdf_value = normalization * term
|
|
277
|
+
|
|
278
|
+
return pdf_value
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
cdef class Pewma(Indicator):
|
|
282
|
+
|
|
283
|
+
def __init__(self, str name, TimeSeries series, double alpha, double beta, int T):
|
|
284
|
+
self.alpha = alpha
|
|
285
|
+
self.beta = beta
|
|
286
|
+
self.T = T
|
|
287
|
+
|
|
288
|
+
# - local variables
|
|
289
|
+
self._i = 0
|
|
290
|
+
self.std = TimeSeries('std', series.timeframe, series.max_series_length)
|
|
291
|
+
super().__init__(name, series)
|
|
292
|
+
|
|
293
|
+
def _store(self):
|
|
294
|
+
self.mean = self._mean
|
|
295
|
+
self.vstd = self._vstd
|
|
296
|
+
self.var = self._var
|
|
297
|
+
|
|
298
|
+
def _restore(self):
|
|
299
|
+
self._mean = self.mean
|
|
300
|
+
self._vstd = self.vstd
|
|
301
|
+
self._var = self.var
|
|
302
|
+
|
|
303
|
+
def _get_alpha(self, p_t):
|
|
304
|
+
if self._i - 1 > self.T:
|
|
305
|
+
return self.alpha * (1.0 - self.beta * p_t)
|
|
306
|
+
return 1.0 - 1.0 / self._i
|
|
307
|
+
|
|
308
|
+
cpdef double calculate(self, long long time, double x, short new_item_started):
|
|
309
|
+
cdef double diff, p_t, a_t, incr
|
|
310
|
+
|
|
311
|
+
if len(self.series) <= 1:
|
|
312
|
+
self._mean = x
|
|
313
|
+
self._vstd = 0.0
|
|
314
|
+
self._var = 0.0
|
|
315
|
+
self._store()
|
|
316
|
+
self.std.update(time, self.vstd)
|
|
317
|
+
return self.mean
|
|
318
|
+
|
|
319
|
+
if new_item_started:
|
|
320
|
+
self._i += 1
|
|
321
|
+
self._restore()
|
|
322
|
+
else:
|
|
323
|
+
self._store()
|
|
324
|
+
|
|
325
|
+
diff = x - self.mean
|
|
326
|
+
# prob of observing diff
|
|
327
|
+
p_t = norm_pdf(diff / self.vstd) if self.vstd != 0.0 else 0.0
|
|
328
|
+
|
|
329
|
+
# weight to give to this point
|
|
330
|
+
a_t = self._get_alpha(p_t)
|
|
331
|
+
incr = (1.0 - a_t) * diff
|
|
332
|
+
self.mean += incr
|
|
333
|
+
self.var = a_t * (self.var + diff * incr)
|
|
334
|
+
self.vstd = np.sqrt(self.var)
|
|
335
|
+
self.std.update(time, self.vstd)
|
|
336
|
+
|
|
337
|
+
return self.mean
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def pewma(series:TimeSeries, alpha: float, beta: float, T:int=30):
|
|
341
|
+
"""
|
|
342
|
+
Implementation of probabilistic exponential weighted ma (https://sci-hub.shop/10.1109/SSP.2012.6319708)
|
|
343
|
+
See pandas version here: qubx.pandaz.ta::pwma
|
|
344
|
+
"""
|
|
345
|
+
return Pewma.wrap(series, alpha, beta, T)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
cdef class PewmaOutliersDetector(Indicator):
|
|
349
|
+
|
|
350
|
+
def __init__(
|
|
351
|
+
self,
|
|
352
|
+
str name,
|
|
353
|
+
TimeSeries series,
|
|
354
|
+
double alpha,
|
|
355
|
+
double beta,
|
|
356
|
+
int T,
|
|
357
|
+
double threshold,
|
|
358
|
+
str dist = "normal",
|
|
359
|
+
double student_t_df = 3.0
|
|
360
|
+
):
|
|
361
|
+
self.alpha = alpha
|
|
362
|
+
self.beta = beta
|
|
363
|
+
self.T = T
|
|
364
|
+
self.threshold = threshold
|
|
365
|
+
self.dist = dist
|
|
366
|
+
self.student_t_df = student_t_df
|
|
367
|
+
|
|
368
|
+
# - series
|
|
369
|
+
self.upper = TimeSeries('uba', series.timeframe, series.max_series_length)
|
|
370
|
+
self.lower = TimeSeries('lba', series.timeframe, series.max_series_length)
|
|
371
|
+
self.std = TimeSeries('std', series.timeframe, series.max_series_length)
|
|
372
|
+
self.outliers = TimeSeries('outliers', series.timeframe, series.max_series_length)
|
|
373
|
+
|
|
374
|
+
# - local variables
|
|
375
|
+
self._i = 0
|
|
376
|
+
self._z_thr = self._get_z_thr()
|
|
377
|
+
|
|
378
|
+
super().__init__(name, series)
|
|
379
|
+
|
|
380
|
+
def _store(self):
|
|
381
|
+
self.mean = self._mean
|
|
382
|
+
self.vstd = self._vstd
|
|
383
|
+
self.variance = self._variance
|
|
384
|
+
|
|
385
|
+
def _restore(self):
|
|
386
|
+
self._mean = self.mean
|
|
387
|
+
self._vstd = self.vstd
|
|
388
|
+
self._variance = self.variance
|
|
389
|
+
|
|
390
|
+
cdef double _get_z_thr(self):
|
|
391
|
+
if self.dist == 'normal':
|
|
392
|
+
return ndtri(1 - self.threshold / 2)
|
|
393
|
+
elif self.dist == 'student_t':
|
|
394
|
+
return stdtrit(self.student_t_df, 1 - self.threshold / 2)
|
|
395
|
+
else:
|
|
396
|
+
raise ValueError('Invalid distribution type')
|
|
397
|
+
|
|
398
|
+
cdef double _get_alpha(self, double p_t):
|
|
399
|
+
if self._i + 1 >= self.T:
|
|
400
|
+
return self.alpha * (1.0 - self.beta * p_t)
|
|
401
|
+
return 1.0 - 1.0 / (self._i + 1.0)
|
|
402
|
+
|
|
403
|
+
cdef double _get_mean(self, double x, double alpha_t):
|
|
404
|
+
return alpha_t * self.mean + (1.0 - alpha_t) * x
|
|
405
|
+
|
|
406
|
+
cdef double _get_variance(self, double x, double alpha_t):
|
|
407
|
+
return alpha_t * self.variance + (1.0 - alpha_t) * np.square(x)
|
|
408
|
+
|
|
409
|
+
cdef double _get_std(self, double variance, double mean):
|
|
410
|
+
return np.sqrt(max(variance - np.square(mean), 0.0))
|
|
411
|
+
|
|
412
|
+
cdef double _get_p(self, double x):
|
|
413
|
+
cdef double z_t = ((x - self.mean) / self.vstd) if (self.vstd != 0 and not np.isnan(x)) else 0.0
|
|
414
|
+
if self.dist == 'normal':
|
|
415
|
+
p_t = norm_pdf(z_t)
|
|
416
|
+
elif self.dist == 'student_t':
|
|
417
|
+
p_t = student_t_pdf(z_t, self.student_t_df)
|
|
418
|
+
# elif self.dist == 'cauchy':
|
|
419
|
+
# p_t = (1 / (np.pi * (1 + np.square(z_t))))
|
|
420
|
+
else:
|
|
421
|
+
raise ValueError('Invalid distribution type')
|
|
422
|
+
return p_t
|
|
423
|
+
|
|
424
|
+
cpdef double calculate(self, long long time, double x, short new_item_started):
|
|
425
|
+
# - first bar - just use it as initial value
|
|
426
|
+
if len(self.series) <= 1:
|
|
427
|
+
self._mean = x
|
|
428
|
+
self._variance = x ** 2
|
|
429
|
+
self._vstd = 0.0
|
|
430
|
+
self._store()
|
|
431
|
+
self.std.update(time, self.vstd)
|
|
432
|
+
self.upper.update(time, x)
|
|
433
|
+
self.lower.update(time, x)
|
|
434
|
+
return self._mean
|
|
435
|
+
|
|
436
|
+
# - new bar is started use n-1 values for calculate innovations
|
|
437
|
+
if new_item_started:
|
|
438
|
+
self._i += 1
|
|
439
|
+
self._restore()
|
|
440
|
+
else:
|
|
441
|
+
self._store()
|
|
442
|
+
|
|
443
|
+
cdef double p_t = self._get_p(x)
|
|
444
|
+
cdef double alpha_t = self._get_alpha(p_t)
|
|
445
|
+
self.mean = self._get_mean(x, alpha_t)
|
|
446
|
+
self.variance = self._get_variance(x, alpha_t)
|
|
447
|
+
self.vstd = self._get_std(self.variance, self.mean)
|
|
448
|
+
cdef double ub = self.mean + self._z_thr * self.vstd
|
|
449
|
+
cdef double lb = self.mean - self._z_thr * self.vstd
|
|
450
|
+
|
|
451
|
+
self.upper.update(time, ub)
|
|
452
|
+
self.lower.update(time, lb)
|
|
453
|
+
self.std.update(time, self.vstd)
|
|
454
|
+
|
|
455
|
+
# - check if it's outlier
|
|
456
|
+
if p_t < self.threshold:
|
|
457
|
+
self.outliers.update(time, x)
|
|
458
|
+
else:
|
|
459
|
+
self.outliers.update(time, np.nan)
|
|
460
|
+
return self.mean
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def pewma_outliers_detector(
|
|
464
|
+
series: TimeSeries,
|
|
465
|
+
alpha: float,
|
|
466
|
+
beta: float,
|
|
467
|
+
T:int=30,
|
|
468
|
+
threshold=0.05,
|
|
469
|
+
dist: str = "normal",
|
|
470
|
+
**kwargs
|
|
471
|
+
):
|
|
472
|
+
"""
|
|
473
|
+
Outliers detector based on pwma
|
|
474
|
+
"""
|
|
475
|
+
return PewmaOutliersDetector.wrap(series, alpha, beta, T, threshold, dist=dist, **kwargs)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
cdef class Psar(IndicatorOHLC):
|
|
479
|
+
|
|
480
|
+
def __init__(self, name, series, iaf, maxaf):
|
|
481
|
+
self.iaf = iaf
|
|
482
|
+
self.maxaf = maxaf
|
|
483
|
+
self.upper = TimeSeries('upper', series.timeframe, series.max_series_length)
|
|
484
|
+
self.lower = TimeSeries('lower', series.timeframe, series.max_series_length)
|
|
485
|
+
super().__init__(name, series)
|
|
486
|
+
|
|
487
|
+
cdef _store(self):
|
|
488
|
+
self.bull = self._bull
|
|
489
|
+
self.af = self._af
|
|
490
|
+
self.psar = self._psar
|
|
491
|
+
self.lp = self._lp
|
|
492
|
+
self.hp = self._hp
|
|
493
|
+
|
|
494
|
+
cdef _restore(self):
|
|
495
|
+
self._bull = self.bull
|
|
496
|
+
self._af = self.af
|
|
497
|
+
self._psar = self.psar
|
|
498
|
+
self._lp = self.lp
|
|
499
|
+
self._hp = self.hp
|
|
500
|
+
|
|
501
|
+
cpdef double calculate(self, long long time, Bar bar, short new_item_started):
|
|
502
|
+
cdef short reverse = 1
|
|
503
|
+
|
|
504
|
+
if len(self.series) <= 2:
|
|
505
|
+
self._bull = 1
|
|
506
|
+
self._af = self.iaf
|
|
507
|
+
self._psar = bar.close
|
|
508
|
+
|
|
509
|
+
if len(self.series) == 1:
|
|
510
|
+
self._lp = bar.low
|
|
511
|
+
self._hp = bar.high
|
|
512
|
+
self._store()
|
|
513
|
+
return self._psar
|
|
514
|
+
|
|
515
|
+
if not new_item_started:
|
|
516
|
+
self._store()
|
|
517
|
+
else:
|
|
518
|
+
self._restore()
|
|
519
|
+
|
|
520
|
+
bar1 = self.series[1]
|
|
521
|
+
bar2 = self.series[2]
|
|
522
|
+
cdef double h0 = bar.high
|
|
523
|
+
cdef double l0 = bar.low
|
|
524
|
+
cdef double h1 = bar1.high
|
|
525
|
+
cdef double l1 = bar1.low
|
|
526
|
+
cdef double h2 = bar2.high
|
|
527
|
+
cdef double l2 = bar2.low
|
|
528
|
+
|
|
529
|
+
if self.bull:
|
|
530
|
+
self.psar += self.af * (self.hp - self.psar)
|
|
531
|
+
else:
|
|
532
|
+
self.psar += self.af * (self.lp - self.psar)
|
|
533
|
+
|
|
534
|
+
reverse = 0
|
|
535
|
+
if self.bull:
|
|
536
|
+
if l0 < self.psar:
|
|
537
|
+
self.bull = 0
|
|
538
|
+
reverse = 1
|
|
539
|
+
self.psar = self.hp
|
|
540
|
+
self.lp = l0
|
|
541
|
+
self.af = self.iaf
|
|
542
|
+
else:
|
|
543
|
+
if h0 > self.psar:
|
|
544
|
+
self.bull = 1
|
|
545
|
+
reverse = 1
|
|
546
|
+
self.psar = self.lp
|
|
547
|
+
self.hp = h0
|
|
548
|
+
self.af = self.iaf
|
|
549
|
+
|
|
550
|
+
if not reverse:
|
|
551
|
+
if self.bull:
|
|
552
|
+
if h0 > self.hp:
|
|
553
|
+
self.hp = h0
|
|
554
|
+
self.af = min(self.af + self.iaf, self.maxaf)
|
|
555
|
+
if l1 < self.psar:
|
|
556
|
+
self.psar = l1
|
|
557
|
+
if l2 < self.psar:
|
|
558
|
+
self.psar = l2
|
|
559
|
+
else:
|
|
560
|
+
if l0 < self.lp:
|
|
561
|
+
self.lp = l0
|
|
562
|
+
self.af = min(self.af + self.iaf, self.maxaf)
|
|
563
|
+
if h1 > self.psar:
|
|
564
|
+
self.psar = h1
|
|
565
|
+
if h2 > self.psar:
|
|
566
|
+
self.psar = h2
|
|
567
|
+
|
|
568
|
+
if self.bull:
|
|
569
|
+
self.lower.update(time, self.psar)
|
|
570
|
+
self.upper.update(time, np.nan)
|
|
571
|
+
else:
|
|
572
|
+
self.upper.update(time, self.psar)
|
|
573
|
+
self.lower.update(time, np.nan)
|
|
574
|
+
|
|
575
|
+
return self.psar
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def psar(series: OHLCV, iaf: float=0.02, maxaf: float=0.2):
|
|
579
|
+
if not isinstance(series, OHLCV):
|
|
580
|
+
raise ValueError('Series must be OHLCV !')
|
|
581
|
+
|
|
582
|
+
return Psar.wrap(series, iaf, maxaf)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
# List of smoothing functions
|
|
586
|
+
_smoothers = {f.__name__: f for f in [pewma, ema, sma, kama, tema, dema]}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def smooth(TimeSeries series, str smoother, *args, **kwargs) -> Indicator:
|
|
590
|
+
"""
|
|
591
|
+
Handy utility function to smooth series
|
|
592
|
+
"""
|
|
593
|
+
_sfn = _smoothers.get(smoother)
|
|
594
|
+
if _sfn is None:
|
|
595
|
+
raise ValueError(f"Smoother {smoother} not found!")
|
|
596
|
+
return _sfn(series, *args, **kwargs)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
cdef class Atr(IndicatorOHLC):
|
|
600
|
+
|
|
601
|
+
def __init__(self, str name, OHLCV series, int period, str smoother, short percentage):
|
|
602
|
+
self.percentage = percentage
|
|
603
|
+
self.tr = TimeSeries("tr", series.timeframe, series.max_series_length)
|
|
604
|
+
self.ma = smooth(self.tr, smoother, period)
|
|
605
|
+
super().__init__(name, series)
|
|
606
|
+
|
|
607
|
+
cpdef double calculate(self, long long time, Bar bar, short new_item_started):
|
|
608
|
+
if len(self.series) <= 1:
|
|
609
|
+
return np.nan
|
|
610
|
+
|
|
611
|
+
cdef double c1 = self.series[1].close
|
|
612
|
+
cdef double h_l = abs(bar.high - bar.low)
|
|
613
|
+
cdef double h_pc = abs(bar.high - c1)
|
|
614
|
+
cdef double l_pc = abs(bar.low - c1)
|
|
615
|
+
self.tr.update(time, max(h_l, h_pc, l_pc))
|
|
616
|
+
return (100 * self.ma[0] / c1) if self.percentage else self.ma[0]
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def atr(series: OHLCV, period: int = 14, smoother="sma", percentage: bool = False):
|
|
620
|
+
if not isinstance(series, OHLCV):
|
|
621
|
+
raise ValueError("Series must be OHLCV !")
|
|
622
|
+
return Atr.wrap(series, period, smoother, percentage)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
cdef class Zscore(Indicator):
|
|
626
|
+
"""
|
|
627
|
+
Z-score normalization using rolling SMA and Std
|
|
628
|
+
"""
|
|
629
|
+
|
|
630
|
+
def __init__(self, str name, TimeSeries series, int period, str smoother):
|
|
631
|
+
self.tr = TimeSeries("tr", series.timeframe, series.max_series_length)
|
|
632
|
+
self.ma = smooth(self.tr, smoother, period)
|
|
633
|
+
self.std = std(self.tr, period)
|
|
634
|
+
super().__init__(name, series)
|
|
635
|
+
|
|
636
|
+
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
637
|
+
self.tr.update(time, value)
|
|
638
|
+
if len(self.ma) < 1 or len(self.std) < 1 or np.isnan(self.ma[0]) or np.isnan(self.std[0]) or self.std[0] == 0:
|
|
639
|
+
return np.nan
|
|
640
|
+
return (value - self.ma[0]) / self.std[0]
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def zscore(series: TimeSeries, period: int = 20, smoother="sma"):
|
|
644
|
+
return Zscore.wrap(series, period, smoother)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
cdef class Swings(IndicatorOHLC):
|
|
648
|
+
|
|
649
|
+
def __init__(self, str name, OHLCV series, trend_indicator, **indicator_args):
|
|
650
|
+
self.base = OHLCV("base", series.timeframe, series.max_series_length)
|
|
651
|
+
self.trend = trend_indicator(self.base, **indicator_args)
|
|
652
|
+
|
|
653
|
+
self.tops = TimeSeries("tops", series.timeframe, series.max_series_length)
|
|
654
|
+
self.tops_detection_lag = TimeSeries("tops_lag", series.timeframe, series.max_series_length)
|
|
655
|
+
|
|
656
|
+
self.bottoms = TimeSeries("bottoms", series.timeframe, series.max_series_length)
|
|
657
|
+
self.bottoms_detection_lag = TimeSeries("bottoms_lag", series.timeframe, series.max_series_length)
|
|
658
|
+
|
|
659
|
+
self.middles = TimeSeries("middles", series.timeframe, series.max_series_length)
|
|
660
|
+
self.deltas = TimeSeries("deltas", series.timeframe, series.max_series_length)
|
|
661
|
+
|
|
662
|
+
# - store parameters for copying
|
|
663
|
+
self._trend_indicator = trend_indicator
|
|
664
|
+
self._indicator_args = indicator_args
|
|
665
|
+
|
|
666
|
+
self._min_l = +np.inf
|
|
667
|
+
self._max_h = -np.inf
|
|
668
|
+
self._max_t = 0
|
|
669
|
+
self._min_t = 0
|
|
670
|
+
super().__init__(name, series)
|
|
671
|
+
|
|
672
|
+
cpdef double calculate(self, long long time, Bar bar, short new_item_started):
|
|
673
|
+
self.base.update_by_bar(time, bar.open, bar.high, bar.low, bar.close, bar.volume)
|
|
674
|
+
cdef int _t = 0
|
|
675
|
+
|
|
676
|
+
if len(self.trend.upper) > 0:
|
|
677
|
+
_u = self.trend.upper[0]
|
|
678
|
+
_d = self.trend.lower[0]
|
|
679
|
+
|
|
680
|
+
if not np.isnan(_u):
|
|
681
|
+
if self._max_t > 0:
|
|
682
|
+
self.tops.update(self._max_t, self._max_h)
|
|
683
|
+
self.tops_detection_lag.update(self._max_t, time - self._max_t)
|
|
684
|
+
if len(self.bottoms) > 0:
|
|
685
|
+
self.middles.update(time, (self.tops[0] + self.bottoms[0]) / 2)
|
|
686
|
+
self.deltas.update(time, self.tops[0] - self.bottoms[0])
|
|
687
|
+
|
|
688
|
+
if bar.low <= self._min_l:
|
|
689
|
+
self._min_l = bar.low
|
|
690
|
+
self._min_t = time
|
|
691
|
+
|
|
692
|
+
self._max_h = -np.inf
|
|
693
|
+
self._max_t = 0
|
|
694
|
+
_t = -1
|
|
695
|
+
elif not np.isnan(_d):
|
|
696
|
+
if self._min_t > 0:
|
|
697
|
+
self.bottoms.update(self._min_t, self._min_l)
|
|
698
|
+
self.bottoms_detection_lag.update(self._min_t, time - self._min_t)
|
|
699
|
+
if len(self.tops) > 0:
|
|
700
|
+
self.middles.update(time, (self.tops[0] + self.bottoms[0]) / 2)
|
|
701
|
+
self.deltas.update(time, self.tops[0] - self.bottoms[0])
|
|
702
|
+
|
|
703
|
+
if bar.high >= self._max_h:
|
|
704
|
+
self._max_h = bar.high
|
|
705
|
+
self._max_t = time
|
|
706
|
+
|
|
707
|
+
self._min_l = +np.inf
|
|
708
|
+
self._min_t = 0
|
|
709
|
+
_t = +1
|
|
710
|
+
|
|
711
|
+
return _t
|
|
712
|
+
|
|
713
|
+
def get_current_trend_end(self):
|
|
714
|
+
if np.isfinite(self._min_l):
|
|
715
|
+
return pd.Timestamp(self._min_t, 'ns'), self._min_l
|
|
716
|
+
elif np.isfinite(self._max_h):
|
|
717
|
+
return pd.Timestamp(self._max_t, 'ns'), self._max_h
|
|
718
|
+
return (None, None)
|
|
719
|
+
|
|
720
|
+
def copy(self, int start, int stop):
|
|
721
|
+
n_ts = Swings(self.name, OHLCV("base", self.series.timeframe), self._trend_indicator, **self._indicator_args)
|
|
722
|
+
|
|
723
|
+
# - copy main series
|
|
724
|
+
for i in range(start, stop):
|
|
725
|
+
n_ts._add_new_item(self.times.values[i], self.values.values[i])
|
|
726
|
+
n_ts.trend._add_new_item(self.trend.times.values[i], self.trend.values.values[i])
|
|
727
|
+
|
|
728
|
+
# - copy internal series
|
|
729
|
+
(
|
|
730
|
+
n_ts.tops,
|
|
731
|
+
n_ts.tops_detection_lag,
|
|
732
|
+
n_ts.bottoms,
|
|
733
|
+
n_ts.bottoms_detection_lag,
|
|
734
|
+
n_ts.middles,
|
|
735
|
+
n_ts.deltas
|
|
736
|
+
) = self._copy_internal_series(start, stop,
|
|
737
|
+
self.tops,
|
|
738
|
+
self.tops_detection_lag,
|
|
739
|
+
self.bottoms,
|
|
740
|
+
self.bottoms_detection_lag,
|
|
741
|
+
self.middles,
|
|
742
|
+
self.deltas
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
return n_ts
|
|
746
|
+
|
|
747
|
+
def pd(self) -> pd.DataFrame:
|
|
748
|
+
_t, _d = self.get_current_trend_end()
|
|
749
|
+
tps, bts = self.tops.pd(), self.bottoms.pd()
|
|
750
|
+
tpl, btl = self.tops_detection_lag.pd(), self.bottoms_detection_lag.pd()
|
|
751
|
+
if _t is not None:
|
|
752
|
+
if bts.index[-1] < tps.index[-1]:
|
|
753
|
+
bts = srows(bts, pd.Series({_t: _d}))
|
|
754
|
+
btl = srows(btl, pd.Series({_t: 0})) # last lag is 0
|
|
755
|
+
else:
|
|
756
|
+
tps = srows(tps, pd.Series({_t: _d}))
|
|
757
|
+
tpl = srows(tpl, pd.Series({_t: 0})) # last lag is 0
|
|
758
|
+
|
|
759
|
+
# - convert tpl / btl to timedeltas
|
|
760
|
+
tpl, btl = tpl.apply(lambda x: pd.Timedelta(x, unit='ns')), btl.apply(lambda x: pd.Timedelta(x, unit='ns'))
|
|
761
|
+
|
|
762
|
+
eid = pd.Series(tps.index, tps.index)
|
|
763
|
+
mx = scols(bts, tps, eid, names=["start_price", "end_price", "end"])
|
|
764
|
+
dt = scols(mx["start_price"], mx["end_price"].shift(-1), mx["end"].shift(-1)) # .dropna()
|
|
765
|
+
dt = dt.assign(
|
|
766
|
+
delta = dt["end_price"] - dt["start_price"],
|
|
767
|
+
spotted = pd.Series(bts.index + btl, bts.index)
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
eid = pd.Series(bts.index, bts.index)
|
|
771
|
+
mx = scols(tps, bts, eid, names=["start_price", "end_price", "end"])
|
|
772
|
+
ut = scols(mx["start_price"], mx["end_price"].shift(-1), mx["end"].shift(-1)) # .dropna()
|
|
773
|
+
ut = ut.assign(
|
|
774
|
+
delta = ut["end_price"] - ut["start_price"],
|
|
775
|
+
spotted = pd.Series(tps.index + tpl, tps.index)
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
return scols(ut, dt, keys=["DownTrends", "UpTrends"])
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
def swings(series: OHLCV, trend_indicator, **indicator_args):
|
|
782
|
+
"""
|
|
783
|
+
Swing detector based on provided trend indicator.
|
|
784
|
+
"""
|
|
785
|
+
if not isinstance(series, OHLCV):
|
|
786
|
+
raise ValueError("Series must be OHLCV !")
|
|
787
|
+
return Swings.wrap(series, trend_indicator, **indicator_args)
|