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
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)