Qubx 0.0.1__cp311-cp311-manylinux_2_35_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/core/series.pyx ADDED
@@ -0,0 +1,763 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+ cimport numpy as np
4
+ from cython cimport abs
5
+ from typing import Union
6
+ from qubx.core.utils import time_to_str, time_delta_to_str, recognize_timeframe
7
+
8
+
9
+ cdef extern from "math.h":
10
+ float INFINITY
11
+
12
+
13
+ cdef np.ndarray nans(int dims):
14
+ """
15
+ nans(n) is an n length array of NaNs.
16
+
17
+ :param dims: array size
18
+ :return: nans matrix
19
+ """
20
+ return np.nan * np.ones(dims)
21
+
22
+
23
+ cdef inline long long floor_t64(long long time, long long dt):
24
+ """
25
+ Floor timestamp by dt
26
+ """
27
+ return time - time % dt
28
+
29
+
30
+ cpdef long long time_as_nsec(time):
31
+ """
32
+ Tries to recognize input time and convert it to nanosec
33
+ """
34
+ if isinstance(time, np.datetime64):
35
+ return time.astype('<M8[ns]').item()
36
+ elif isinstance(time, pd.Timestamp):
37
+ return time.asm8
38
+ elif isinstance(time, str):
39
+ return np.datetime64(time).astype('<M8[ns]').item()
40
+ return time
41
+
42
+
43
+ cdef class RollingSum:
44
+ """
45
+ Rolling fast summator
46
+ """
47
+
48
+ def __init__(self, int period):
49
+ self.period = period
50
+ self.__s = np.zeros(period)
51
+ self.__i = 0
52
+ self.rsum = 0.0
53
+ self.is_init_stage = 1
54
+
55
+ cpdef double update(self, double value, short new_item_started):
56
+ if np.isnan(value):
57
+ return np.nan
58
+ sub = self.__s[self.__i]
59
+ if new_item_started:
60
+ self.__i += 1
61
+ if self.__i >= self.period:
62
+ self.__i = 0
63
+ self.is_init_stage = 0
64
+ sub = self.__s[self.__i]
65
+ self.__s[self.__i] = value
66
+ self.rsum -= sub
67
+ self.rsum += value
68
+ return self.rsum
69
+
70
+ def __str__(self):
71
+ return f"rs[{self.period}] = {self.__s} @ {self.__i} -> {self.is_init_stage}"
72
+
73
+
74
+ cdef class Indexed:
75
+
76
+ def __init__(self, max_series_length=INFINITY):
77
+ self.max_series_length = max_series_length
78
+ self.values = list()
79
+ self._is_empty = 1
80
+
81
+ def __len__(self) -> int:
82
+ return len(self.values)
83
+
84
+ def empty(self) -> bool:
85
+ return self._is_empty
86
+
87
+ def __getitem__(self, idx):
88
+ if isinstance(idx, slice):
89
+ return [self.values[self._get_index(i)] for i in range(*idx.indices(len(self.values)))]
90
+ return self.values[self._get_index(idx)]
91
+
92
+ def _get_index(self, idx: int) -> int:
93
+ n_len = len(self)
94
+ if n_len == 0 or (idx > 0 and idx > (n_len - 1)) or (idx < 0 and abs(idx) > n_len):
95
+ raise IndexError(f"Can't find record at index {idx}")
96
+ return (n_len - idx - 1) if idx >= 0 else abs(1 + idx)
97
+
98
+ def add(self, v):
99
+ self.values.append(v)
100
+ self._is_empty = 0
101
+ if len(self.values) >= self.max_series_length:
102
+ self.values.pop(0)
103
+
104
+ def update_last(self, v):
105
+ if self.values:
106
+ self.values[-1] = v
107
+ else:
108
+ self.append(v)
109
+ self._is_empty = 0
110
+
111
+ def set_values(self, new_values: list):
112
+ self._is_empty = False
113
+ self.values = new_values
114
+
115
+ def clear(self):
116
+ self.values.clear()
117
+ self._is_empty = 1
118
+
119
+
120
+ global _plot_func
121
+
122
+
123
+ cdef class TimeSeries:
124
+
125
+ def __init__(self, str name, timeframe, max_series_length=INFINITY) -> None:
126
+ self.name = name
127
+ self.max_series_length = max_series_length
128
+ self.timeframe = recognize_timeframe(timeframe)
129
+ self.times = Indexed(max_series_length)
130
+ self.values = Indexed(max_series_length)
131
+ self.indicators = dict()
132
+ self.calculation_order = []
133
+
134
+ def __len__(self) -> int:
135
+ return len(self.times)
136
+
137
+ def _lift_up(self, indicator: Indicator, indicator_input: TimeSeries):
138
+ # print(f"> Received: {indicator_input.name}[{id(indicator_input)}] -> {indicator.name}[{id(indicator)}]")
139
+ # collect indicators calculation order as list: [ (input_id, indicator_obj, indicator_id) ]
140
+ self.calculation_order.append((
141
+ id(indicator_input), indicator, id(indicator)
142
+ ))
143
+
144
+ def __getitem__(self, idx):
145
+ return self.values[idx]
146
+
147
+ def _add_new_item(self, long long time, double value):
148
+ self.times.add(time)
149
+ self.values.add(value)
150
+ self._is_new_item = True
151
+
152
+ def _update_last_item(self, long long time, double value):
153
+ self.times.update_last(time)
154
+ self.values.update_last(value)
155
+ self._is_new_item = False
156
+
157
+ def update(self, long long time, double value) -> bool:
158
+ item_start_time = floor_t64(time, self.timeframe)
159
+
160
+ if not self.times:
161
+ self._add_new_item(item_start_time, value)
162
+
163
+ # - disable first notification because first item may be incomplete
164
+ self._is_new_item = False
165
+
166
+ elif time - self.times[0] >= self.timeframe:
167
+ # - add new item
168
+ self._add_new_item(item_start_time, value)
169
+
170
+ # - update indicators
171
+ self._update_indicators(item_start_time, value, True)
172
+
173
+ return self._is_new_item
174
+ else:
175
+ self._update_last_item(item_start_time, value)
176
+
177
+ # - update indicators by new data
178
+ self._update_indicators(item_start_time, value, False)
179
+
180
+ return self._is_new_item
181
+
182
+ cdef _update_indicators(self, long long time, value, short new_item_started):
183
+ mem = dict() # store calculated values during this update
184
+ mem[id(self)] = value # initail value - new data from itself
185
+ for input, indicator, iid in self.calculation_order:
186
+ if input not in mem:
187
+ raise ValueError("> No input data - something wrong in calculation order !")
188
+ mem[iid] = indicator.update(time, mem[input], new_item_started)
189
+
190
+ def shift(self, int period):
191
+ """
192
+ Returns shifted series by period
193
+ """
194
+ if period < 0:
195
+ raise ValueError("Only positive shift (from past) period is allowed !")
196
+ return lag(self, period)
197
+
198
+ def __add__(self, other: Union[TimeSeries, float, int]):
199
+ return plus(self, other)
200
+
201
+ def __sub__(self, other: Union[TimeSeries, float, int]):
202
+ return minus(self, other)
203
+
204
+ def __mul__(self, other: Union[TimeSeries, float, int]):
205
+ return mult(self, other)
206
+
207
+ def __truediv__(self, other: Union[TimeSeries, float, int]):
208
+ return divide(self, other)
209
+
210
+ def __lt__(self, other: Union[TimeSeries, float, int]):
211
+ return lt(self, other)
212
+
213
+ def __le__(self, other: Union[TimeSeries, float, int]):
214
+ return le(self, other)
215
+
216
+ def __gt__(self, other: Union[TimeSeries, float, int]):
217
+ return gt(self, other)
218
+
219
+ def __ge__(self, other: Union[TimeSeries, float, int]):
220
+ return ge(self, other)
221
+
222
+ def __eq__(self, other: Union[TimeSeries, float, int]):
223
+ return eq(self, other)
224
+
225
+ def __ne__(self, other: Union[TimeSeries, float, int]):
226
+ return ne(self, other)
227
+
228
+ def __neg__(self):
229
+ return neg(self)
230
+
231
+ def __abs__(self):
232
+ return series_abs(self)
233
+
234
+ def to_records(self) -> dict:
235
+ ts = [np.datetime64(t, 'ns') for t in self.times[::-1]]
236
+ return dict(zip(ts, self.values[::-1]))
237
+
238
+ def to_series(self):
239
+ return pd.Series(self.values.values, index=pd.DatetimeIndex(self.times.values), name=self.name, dtype=float)
240
+ # return pd.Series(self.to_records(), name=self.name, dtype=float)
241
+
242
+ def pd(self):
243
+ return self.to_series()
244
+
245
+ def get_indicators(self) -> dict:
246
+ return self.indicators
247
+
248
+ def plot(self, *args, **kwargs):
249
+ _timeseries_plot_func(self, *args, **kwargs)
250
+
251
+ def __str__(self):
252
+ nl = len(self)
253
+ r = f"{self.name}[{time_delta_to_str(self.timeframe)}] | {nl} records\n"
254
+ hd, tl = 3, 3
255
+ if nl <= hd + tl:
256
+ hd, tl = nl, 0
257
+
258
+ for n in range(hd):
259
+ r += f" {time_to_str(self.times[n], 'ns')} {str(self[n])}\n"
260
+
261
+ if tl > 0:
262
+ r += " .......... \n"
263
+ for n in range(-tl, 0):
264
+ r += f" {time_to_str(self.times[n], 'ns')} {str(self[n])}\n"
265
+
266
+ return r
267
+
268
+ def __repr__(self):
269
+ return repr(self.pd())
270
+
271
+
272
+ def _wrap_indicator(series: TimeSeries, clz, *args, **kwargs):
273
+ aw = ','.join([a.name if isinstance(a, TimeSeries) else str(a) for a in args])
274
+ if kwargs:
275
+ aw += ',' + ','.join([f"{k}={str(v)}" for k,v in kwargs.items()])
276
+ nn = clz.__name__.lower() + "(" + aw + ")"
277
+ inds = series.get_indicators()
278
+ if nn in inds:
279
+ return inds[nn]
280
+ return clz(nn, series, *args, **kwargs)
281
+
282
+
283
+ cdef class Indicator(TimeSeries):
284
+
285
+ def __init__(self, str name, TimeSeries series):
286
+ if not name:
287
+ raise ValueError(f" > Name must not be empty for {self.__class__.__name__}!")
288
+ super().__init__(name, series.timeframe, series.max_series_length)
289
+ series.indicators[name] = self
290
+ self.name = name
291
+
292
+ # - we need to make a empty copy and fill it
293
+ self.series = TimeSeries(series.name, series.timeframe, series.max_series_length)
294
+ self.parent = series
295
+ self._lift_up(self, series)
296
+
297
+ # - recalculate indicator on data as if it would being streamed
298
+ self._initial_data_recalculate(series)
299
+
300
+ def _lift_up(self, indicator: Indicator, indicator_input: TimeSeries):
301
+ self.parent._lift_up(indicator, indicator_input)
302
+
303
+ def _initial_data_recalculate(self, TimeSeries series):
304
+ for t, v in zip(series.times[::-1], series.values[::-1]):
305
+ self.update(t, v, True)
306
+
307
+ def update(self, long long time, value, short new_item_started) -> object:
308
+ if new_item_started or len(self) == 0:
309
+ self.series._add_new_item(time, value)
310
+ iv = self.calculate(time, value, new_item_started)
311
+ self._add_new_item(time, iv)
312
+ else:
313
+ self.series._update_last_item(time, value)
314
+ iv = self.calculate(time, value, new_item_started)
315
+ self._update_last_item(time, iv)
316
+
317
+ return iv
318
+
319
+ def calculate(self, long long time, value, short new_item_started) -> object:
320
+ raise ValueError("Indicator must implement calculate() method")
321
+
322
+ @classmethod
323
+ def wrap(clz, series:TimeSeries, *args, **kwargs):
324
+ return _wrap_indicator(series, clz, *args, **kwargs)
325
+
326
+
327
+ cdef class Lag(Indicator):
328
+ cdef int period
329
+
330
+ def __init__(self, str name, TimeSeries series, int period):
331
+ self.period = period
332
+ super().__init__(name, series)
333
+
334
+ cpdef double calculate(self, long long time, double value, short new_item_started):
335
+ if len(self.series) <= self.period:
336
+ return np.nan
337
+ return self.series[self.period]
338
+
339
+
340
+ def lag(series:TimeSeries, period: int):
341
+ return Lag.wrap(series, period)
342
+
343
+
344
+ cdef class Abs(Indicator):
345
+
346
+ def __init__(self, str name, TimeSeries series):
347
+ super().__init__(name, series)
348
+
349
+ cpdef double calculate(self, long long time, double value, short new_item_started):
350
+ return abs(self.series[0])
351
+
352
+
353
+ def series_abs(series:TimeSeries):
354
+ return Abs.wrap(series)
355
+
356
+
357
+ cdef class Compare(Indicator):
358
+ cdef TimeSeries to_compare
359
+ cdef double comparable_scalar
360
+ cdef short _cmp_to_series
361
+
362
+ def __init__(self, name: str, original: TimeSeries, comparable: Union[TimeSeries, float, int]):
363
+ if isinstance(comparable, TimeSeries):
364
+ if comparable.timeframe != original.timeframe:
365
+ raise ValueError("Series must be of the same timeframe for performing operation !")
366
+ self.to_compare = comparable
367
+ self._cmp_to_series = 1
368
+ else:
369
+ self.comparable_scalar = comparable
370
+ self._cmp_to_series = 0
371
+ super().__init__(name, original)
372
+
373
+ cdef double _operation(self, double a, double b):
374
+ if np.isnan(a) or np.isnan(b):
375
+ return np.nan
376
+ return +1 if a > b else -1 if a < b else 0
377
+
378
+ def _initial_data_recalculate(self, TimeSeries series):
379
+ if self._cmp_to_series:
380
+ r = pd.concat((series.to_series(), self.to_compare.to_series()), axis=1)
381
+ for t, (a, b) in zip(r.index, r.values):
382
+ self.series._add_new_item(t.asm8, a)
383
+ self._add_new_item(t.asm8, self._operation(a, b))
384
+ else:
385
+ r = series.to_series()
386
+ for t, a in zip(r.index, r.values):
387
+ self.series._add_new_item(t.asm8, a)
388
+ self._add_new_item(t.asm8, self._operation(a, self.comparable_scalar))
389
+
390
+ cpdef double calculate(self, long long time, double value, short new_item_started):
391
+ if self._cmp_to_series:
392
+ if len(self.to_compare) == 0 or len(self.series) == 0 or time != self.to_compare.times[0]:
393
+ return np.nan
394
+ return self._operation(value, self.to_compare[0])
395
+ else:
396
+ if len(self.series) == 0:
397
+ return np.nan
398
+ return self._operation(value, self.comparable_scalar)
399
+
400
+
401
+ def compare(series0:TimeSeries, series1:TimeSeries):
402
+ return Compare.wrap(series0, series1)
403
+
404
+
405
+ cdef class Plus(Compare):
406
+
407
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
408
+ super().__init__(name, original, comparable)
409
+
410
+ cdef double _operation(self, double a, double b):
411
+ return a + b
412
+
413
+
414
+ cdef class Minus(Compare):
415
+
416
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
417
+ super().__init__(name, original, comparable)
418
+
419
+ cdef double _operation(self, double a, double b):
420
+ return a - b
421
+
422
+
423
+ cdef class Mult(Compare):
424
+
425
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
426
+ super().__init__(name, original, comparable)
427
+
428
+ cdef double _operation(self, double a, double b):
429
+ return a * b
430
+
431
+
432
+ cdef class Divide(Compare):
433
+
434
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
435
+ super().__init__(name, original, comparable)
436
+
437
+ cdef double _operation(self, double a, double b):
438
+ return a / b
439
+
440
+
441
+ cdef class EqualTo(Compare):
442
+
443
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
444
+ super().__init__(name, original, comparable)
445
+
446
+ cdef double _operation(self, double a, double b):
447
+ return a == b
448
+
449
+
450
+ cdef class NotEqualTo(Compare):
451
+
452
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
453
+ super().__init__(name, original, comparable)
454
+
455
+ cdef double _operation(self, double a, double b):
456
+ return a != b
457
+
458
+
459
+ cdef class LessThan(Compare):
460
+
461
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
462
+ super().__init__(name, original, comparable)
463
+
464
+ cdef double _operation(self, double a, double b):
465
+ return a < b
466
+
467
+
468
+ cdef class LessEqualThan(Compare):
469
+
470
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
471
+ super().__init__(name, original, comparable)
472
+
473
+ cdef double _operation(self, double a, double b):
474
+ return a <= b
475
+
476
+
477
+ cdef class GreaterThan(Compare):
478
+
479
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
480
+ super().__init__(name, original, comparable)
481
+
482
+ cdef double _operation(self, double a, double b):
483
+ return a > b
484
+
485
+
486
+ cdef class GreaterEqualThan(Compare):
487
+
488
+ def __init__(self, name: str, original:TimeSeries, comparable: Union[TimeSeries, float, int]):
489
+ super().__init__(name, original, comparable)
490
+
491
+ cdef double _operation(self, double a, double b):
492
+ return a >= b
493
+
494
+
495
+ cdef class Neg(Indicator):
496
+
497
+ def __init__(self, name: str, series:TimeSeries):
498
+ super().__init__(name, series)
499
+
500
+ cpdef double calculate(self, long long time, double value, short new_item_started):
501
+ return -value
502
+
503
+
504
+ def plus(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
505
+ return Plus.wrap(series0, series1)
506
+
507
+
508
+ def minus(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
509
+ return Minus.wrap(series0, series1)
510
+
511
+
512
+ def mult(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
513
+ return Mult.wrap(series0, series1)
514
+
515
+
516
+ def divide(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
517
+ return Divide.wrap(series0, series1)
518
+
519
+
520
+ def eq(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
521
+ return EqualTo.wrap(series0, series1)
522
+
523
+
524
+ def ne(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
525
+ return NotEqualTo.wrap(series0, series1)
526
+
527
+
528
+ def lt(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
529
+ return LessThan.wrap(series0, series1)
530
+
531
+
532
+ def le(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
533
+ return LessEqualThan.wrap(series0, series1)
534
+
535
+
536
+ def gt(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
537
+ return GreaterThan.wrap(series0, series1)
538
+
539
+
540
+ def ge(series0:TimeSeries, series1:Union[TimeSeries, float, int]):
541
+ return GreaterEqualThan.wrap(series0, series1)
542
+
543
+
544
+ def neg(series: TimeSeries):
545
+ return Neg.wrap(series)
546
+
547
+
548
+ cdef class Trade:
549
+ def __init__(self, time, double price, double size, short taker=-1):
550
+ self.time = time_as_nsec(time)
551
+ self.price = price
552
+ self.size = size
553
+ self.taker = taker
554
+
555
+ def __repr__(self):
556
+ return "[%s]\t%.5f (%.1f) <%s>" % (
557
+ time_to_str(self.time, 'ns'), self.price, self.size,
558
+ 'take' if self.taker == 1 else 'make' if self.taker == 0 else '???'
559
+ )
560
+
561
+
562
+ cdef class Quote:
563
+ def __init__(self, time, double bid, double ask, double bid_size, double ask_size):
564
+ self.time = time_as_nsec(time)
565
+ self.bid = bid
566
+ self.ask = ask
567
+ self.bid_size = bid_size
568
+ self.ask_size = ask_size
569
+
570
+ cpdef double mid_price(self):
571
+ return 0.5 * (self.ask + self.bid)
572
+
573
+ def __repr__(self):
574
+ return "[%s]\t%.5f (%.1f) | %.5f (%.1f)" % (
575
+ time_to_str(self.time, 'ns'), self.bid, self.bid_size, self.ask, self.ask_size
576
+ )
577
+
578
+
579
+ cdef class Bar:
580
+
581
+ def __init__(self, long long time, double open, double high, double low, double close, double volume) -> None:
582
+ self.open = open
583
+ self.high = high
584
+ self.low = low
585
+ self.close = close
586
+ self.volume = volume
587
+
588
+ cpdef Bar update(self, double price, double volume):
589
+ self.close = price
590
+ self.high = max(price, self.high)
591
+ self.low = min(price, self.low)
592
+ self.volume += volume
593
+ return self
594
+
595
+ cpdef dict to_dict(self, unsigned short skip_time=0):
596
+ if skip_time:
597
+ return {
598
+ 'open': self.open, 'high': self.high, 'low': self.low, 'close': self.close, 'volume': self.volume,
599
+ }
600
+ return {
601
+ 'timestamp': np.datetime64(self.time, 'ns'), 'open': self.open, 'high': self.high, 'low': self.low, 'close': self.close, 'volume': self.volume,
602
+ }
603
+
604
+ def __repr__(self):
605
+ return "{o:%f | h:%f | l:%f | c:%f | v:%f}" % (self.open, self.high, self.low, self.close, self.volume)
606
+
607
+
608
+ cdef class OHLCV(TimeSeries):
609
+
610
+ def __init__(self, str name, timeframe, max_series_length=INFINITY) -> None:
611
+ super().__init__(name, timeframe, max_series_length)
612
+ self.open = TimeSeries('open', timeframe, max_series_length)
613
+ self.high = TimeSeries('high', timeframe, max_series_length)
614
+ self.low = TimeSeries('low', timeframe, max_series_length)
615
+ self.close = TimeSeries('close', timeframe, max_series_length)
616
+ self.volume = TimeSeries('volume', timeframe, max_series_length)
617
+
618
+ cpdef object append_data(self,
619
+ np.ndarray times,
620
+ np.ndarray opens,
621
+ np.ndarray highs,
622
+ np.ndarray lows,
623
+ np.ndarray closes,
624
+ np.ndarray volumes,
625
+ ):
626
+ cdef long long t
627
+ cdef short _conv
628
+ cdef short _upd_inds, _has_vol
629
+ cdef Bar b
630
+
631
+ # - check if volume data presented
632
+ _has_vol = len(volumes) > 0
633
+
634
+ # - check if need to convert time to nanosec
635
+ _conv = 0
636
+ if not isinstance(times[0].item(), long):
637
+ _conv = 1
638
+
639
+ # - check if need to update any indicators
640
+ _upd_inds = 0
641
+ if (
642
+ len(self.indicators) > 0 or
643
+ len(self.open.indicators) > 0 or
644
+ len(self.high.indicators) > 0 or
645
+ len(self.low.indicators) > 0 or
646
+ len(self.close.indicators) > 0 or
647
+ len(self.volume.indicators) > 0
648
+ ):
649
+ _upd_inds = 1
650
+
651
+ for i in range(len(times)):
652
+ if _conv:
653
+ t = times[i].astype('datetime64[ns]').item()
654
+ else:
655
+ t = times[i].item()
656
+
657
+ b = Bar(t, opens[i], highs[i], lows[i], closes[i], volumes[i] if _has_vol else 0)
658
+ self._add_new_item(t, b)
659
+
660
+ if _upd_inds:
661
+ self._update_indicators(t, b, True)
662
+
663
+ return self
664
+
665
+ def _add_new_item(self, long long time, Bar value):
666
+ self.times.add(time)
667
+ self.values.add(value)
668
+ self.open._add_new_item(time, value.open)
669
+ self.high._add_new_item(time, value.high)
670
+ self.low._add_new_item(time, value.low)
671
+ self.close._add_new_item(time, value.close)
672
+ self.volume._add_new_item(time, value.volume)
673
+ self._is_new_item = True
674
+
675
+ def _update_last_item(self, long long time, Bar value):
676
+ self.times.update_last(time)
677
+ self.values.update_last(value)
678
+ self.open._update_last_item(time, value.open)
679
+ self.high._update_last_item(time, value.high)
680
+ self.low._update_last_item(time, value.low)
681
+ self.close._update_last_item(time, value.close)
682
+ self.volume._update_last_item(time, value.volume)
683
+ self._is_new_item = False
684
+
685
+ cpdef short update(self, long long time, double price, double volume=0.0):
686
+ cdef Bar b
687
+ bar_start_time = floor_t64(time, self.timeframe)
688
+
689
+ if not self.times:
690
+ self._add_new_item(bar_start_time, Bar(bar_start_time, price, price, price, price, volume))
691
+
692
+ # Here we disable first notification because first item may be incomplete
693
+ self._is_new_item = False
694
+
695
+ elif time - self.times[0] >= self.timeframe:
696
+ b = Bar(bar_start_time, price, price, price, price, volume)
697
+
698
+ # - add new item
699
+ self._add_new_item(bar_start_time, b)
700
+
701
+ # - update indicators
702
+ self._update_indicators(bar_start_time, b, True)
703
+
704
+ return self._is_new_item
705
+ else:
706
+ self._update_last_item(bar_start_time, self[0].update(price, volume))
707
+
708
+ # - update indicators by new data
709
+ self._update_indicators(bar_start_time, self[0], False)
710
+
711
+ return self._is_new_item
712
+
713
+ # - TODO: need to check if it's safe to drop value series (series of Bar) to avoid duplicating data
714
+ # def __getitem__(self, idx):
715
+ # if isinstance(idx, slice):
716
+ # return [
717
+ # Bar(self.times[i], self.open[i], self.high[i], self.low[i], self.close[i], self.volume[i])
718
+ # for i in range(*idx.indices(len(self.times)))
719
+ # ]
720
+ # return Bar(self.times[idx], self.open[idx], self.high[idx], self.low[idx], self.close[idx], self.volume[idx])
721
+
722
+ cpdef _update_indicators(self, long long time, value, short new_item_started):
723
+ TimeSeries._update_indicators(self, time, value, new_item_started)
724
+ if new_item_started:
725
+ self.open._update_indicators(time, value.open, new_item_started)
726
+ self.close._update_indicators(time, value.close, new_item_started)
727
+ self.high._update_indicators(time, value.high, new_item_started)
728
+ self.low._update_indicators(time, value.low, new_item_started)
729
+ self.volume._update_indicators(time, value.volume, new_item_started)
730
+
731
+ def to_series(self) -> pd.DataFrame:
732
+ df = pd.DataFrame({
733
+ 'open': self.open.to_series(),
734
+ 'high': self.high.to_series(),
735
+ 'low': self.low.to_series(),
736
+ 'close': self.close.to_series(),
737
+ 'volume': self.volume.to_series(),
738
+ })
739
+ df.index.name = 'timestamp'
740
+ return df
741
+
742
+ def to_records(self) -> dict:
743
+ ts = [np.datetime64(t, 'ns') for t in self.times[::-1]]
744
+ bs = [v.to_dict(skip_time=True) for v in self.values[::-1]]
745
+ return dict(zip(ts, bs))
746
+
747
+
748
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
749
+ # - this should be done in separate module -
750
+ def _plot_mpl(series: TimeSeries, *args, **kwargs):
751
+ import matplotlib.pyplot as plt
752
+ include_indicators = kwargs.pop('with_indicators', False)
753
+ no_labels = kwargs.pop('no_labels', False)
754
+
755
+ plt.plot(series.pd(), *args, **kwargs, label=series.name)
756
+ if include_indicators:
757
+ for k, vi in series.get_indicators().items():
758
+ plt.plot(vi.pd(), label=k)
759
+ if not no_labels:
760
+ plt.legend(loc=2)
761
+
762
+ _timeseries_plot_func = _plot_mpl
763
+ # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -