PyAlgoEngine 0.7.4__py3-none-any.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.
Files changed (43) hide show
  1. PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
  2. PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
  3. PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
  4. PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
  5. PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
  6. algo_engine/__init__.py +41 -0
  7. algo_engine/apps/__init__.py +17 -0
  8. algo_engine/apps/backtest/__init__.py +20 -0
  9. algo_engine/apps/backtest/doc_server.py +331 -0
  10. algo_engine/apps/backtest/tester.py +254 -0
  11. algo_engine/apps/backtest/web_app.py +127 -0
  12. algo_engine/apps/bokeh_server.py +205 -0
  13. algo_engine/apps/demo/__init__.py +0 -0
  14. algo_engine/apps/demo/test.py +39 -0
  15. algo_engine/backtest/__init__.py +19 -0
  16. algo_engine/backtest/__main__.py +51 -0
  17. algo_engine/backtest/metrics.py +179 -0
  18. algo_engine/backtest/replay.py +261 -0
  19. algo_engine/backtest/sim_match.py +295 -0
  20. algo_engine/base/__init__.py +40 -0
  21. algo_engine/base/console_utils.py +1070 -0
  22. algo_engine/base/finance_decimal.py +258 -0
  23. algo_engine/base/market_buffer.py +571 -0
  24. algo_engine/base/market_utils.py +3092 -0
  25. algo_engine/base/market_utils_nt.py +188 -0
  26. algo_engine/base/market_utils_posix.py +3004 -0
  27. algo_engine/base/technical_analysis.py +406 -0
  28. algo_engine/base/telemetrics.py +78 -0
  29. algo_engine/base/trade_utils.py +709 -0
  30. algo_engine/engine/__init__.py +28 -0
  31. algo_engine/engine/algo_engine.py +901 -0
  32. algo_engine/engine/event_engine.py +53 -0
  33. algo_engine/engine/market_engine.py +370 -0
  34. algo_engine/engine/trade_engine.py +2037 -0
  35. algo_engine/monitor/__init__.py +15 -0
  36. algo_engine/monitor/advanced_data_interface.py +239 -0
  37. algo_engine/profile/__init__.py +121 -0
  38. algo_engine/profile/cn.py +175 -0
  39. algo_engine/strategy/__init__.py +44 -0
  40. algo_engine/strategy/strategy_engine.py +440 -0
  41. algo_engine/utils/__init__.py +3 -0
  42. algo_engine/utils/commit_regularizer.py +49 -0
  43. algo_engine/utils/data_utils.py +251 -0
@@ -0,0 +1,3092 @@
1
+ import abc
2
+ import datetime
3
+ import enum
4
+ import json
5
+ import math
6
+ import re
7
+ import warnings
8
+ from ctypes import c_ulong, c_double, c_wchar, c_int, c_longlong
9
+ from multiprocessing import RawValue, RawArray
10
+ from typing import overload, Literal, Self
11
+
12
+ import numpy as np
13
+
14
+ from . import LOGGER, PROFILE
15
+
16
+ LOGGER = LOGGER.getChild('MarketUtils')
17
+ __all__ = ['TransactionSide',
18
+ 'MarketData', 'OrderBook', 'BarData', 'DailyBar', 'CandleStick', 'TickData', 'TransactionData', 'TradeData',
19
+ 'MarketDataMemoryBuffer', 'OrderBookRawValue', 'BarDataRawValue', 'TickDataRawValue', 'TransactionDataRawValue']
20
+
21
+
22
+ class TransactionSide(enum.Enum):
23
+ """
24
+ Enumeration representing different sides of a financial transaction.
25
+
26
+ Attributes:
27
+ ShortOrder: Represents an order to short.
28
+ ShortOpen: Represents the opening of a short position.
29
+ ShortFilled: Represents a filled short order.
30
+ UNKNOWN: Represents an unknown transaction side. Normally a cancel order.
31
+ LongFilled: Represents a filled long order.
32
+ ShortClose: Represents the closing of a short position.
33
+ LongOrder: Represents an order to go long.
34
+ """
35
+
36
+ ShortOrder = AskOrder = Offer_to_Short = -3
37
+ ShortOpen = Sell_to_Short = -2
38
+ ShortFilled = LongClose = Sell_to_Unwind = ask = -1
39
+ UNKNOWN = CANCEL = 0
40
+ LongFilled = LongOpen = Buy_to_Long = bid = 1
41
+ ShortClose = Buy_to_Cover = 2
42
+ LongOrder = BidOrder = Bid_to_Long = 3
43
+
44
+ def __lt__(self, other):
45
+ """
46
+ Compare if this transaction side is less than another.
47
+
48
+ This comparison is deprecated.
49
+
50
+ Args:
51
+ other: The other TransactionSide to compare with.
52
+
53
+ Returns:
54
+ bool: True if this transaction side is less than the other, otherwise False.
55
+ """
56
+ warnings.warn(DeprecationWarning('Comparison of the <TransactionSide> deprecated!'))
57
+ if self.__class__ is other.__class__:
58
+ return self.value < other.value
59
+ else:
60
+ return self.value < other
61
+
62
+ def __gt__(self, other):
63
+ """
64
+ Compare if this transaction side is greater than another.
65
+
66
+ This comparison is deprecated.
67
+
68
+ Args:
69
+ other: The other TransactionSide to compare with.
70
+
71
+ Returns:
72
+ bool: True if this transaction side is greater than the other, otherwise False.
73
+ """
74
+ warnings.warn(DeprecationWarning('Comparison of the <TransactionSide> deprecated!'))
75
+ if self.__class__ is other.__class__:
76
+ return self.value > other.value
77
+ else:
78
+ return self.value > other
79
+
80
+ def __eq__(self, other):
81
+ """
82
+ Compare if this transaction side is equal to another.
83
+
84
+ Args:
85
+ other: The other TransactionSide to compare with.
86
+
87
+ Returns:
88
+ bool: True if this transaction side is equal to the other, otherwise False.
89
+ """
90
+ if self.__class__ is other.__class__:
91
+ return self.value == other.value
92
+ else:
93
+ return self.value == other
94
+
95
+ def __neg__(self) -> Self:
96
+ """
97
+ Get the opposite transaction side.
98
+
99
+ Returns:
100
+ TransactionSide: The opposite transaction side.
101
+ """
102
+ if self is self.LongOpen:
103
+ return self.LongClose
104
+ elif self is self.LongClose:
105
+ return self.LongOpen
106
+ elif self is self.ShortOpen:
107
+ return self.ShortClose
108
+ elif self is self.ShortClose:
109
+ return self.ShortOpen
110
+ elif self is self.BidOrder:
111
+ return self.AskOrder
112
+ elif self is self.AskOrder:
113
+ return self.BidOrder
114
+ else:
115
+ LOGGER.warning('No valid registered opposite trade side for {}'.format(self))
116
+ return self.UNKNOWN
117
+
118
+ def __hash__(self):
119
+ """
120
+ Get the hash value of this transaction side.
121
+
122
+ Returns:
123
+ int: The hash value of the transaction side.
124
+ """
125
+ return self.value
126
+
127
+ @classmethod
128
+ def from_offset(cls, direction: str, offset: str) -> Self:
129
+ """
130
+ Determine the transaction side from direction and offset.
131
+
132
+ Args:
133
+ direction (str): The trade direction (e.g., 'buy', 'sell').
134
+ offset (str): The trade offset (e.g., 'open', 'close').
135
+
136
+ Returns:
137
+ TransactionSide: The corresponding transaction side.
138
+
139
+ Raises:
140
+ ValueError: If the direction or offset is not recognized.
141
+ """
142
+ direction = direction.lower()
143
+ offset = offset.lower()
144
+
145
+ if direction in ['buy', 'long', 'b']:
146
+ if offset in ['open', 'wind']:
147
+ return cls.LongOpen
148
+ elif offset in ['close', 'cover', 'unwind']:
149
+ return cls.ShortOpen
150
+ else:
151
+ raise ValueError(f'Not recognized {direction} {offset}')
152
+ elif direction in ['sell', 'short', 's']:
153
+ if offset in ['open', 'wind']:
154
+ return cls.ShortOpen
155
+ elif offset in ['close', 'cover', 'unwind']:
156
+ return cls.LongClose
157
+ else:
158
+ raise ValueError(f'Not recognized {direction} {offset}')
159
+ else:
160
+ raise ValueError(f'Not recognized {direction} {offset}')
161
+
162
+ @classmethod
163
+ def _missing_(cls, value: str | int):
164
+ """
165
+ Handle missing values in the enumeration.
166
+
167
+ Args:
168
+ value (str | int): The value to resolve.
169
+
170
+ Returns:
171
+ TransactionSide: The resolved transaction side, or UNKNOWN if not recognized.
172
+ """
173
+ capital_str = str(value).capitalize()
174
+
175
+ if capital_str == 'Long' or capital_str == 'Buy' or capital_str == 'B':
176
+ trade_side = cls.LongOpen
177
+ elif capital_str == 'Short' or capital_str == 'Ss':
178
+ trade_side = cls.ShortOpen
179
+ elif capital_str == 'Close' or capital_str == 'Sell' or capital_str == 'S':
180
+ trade_side = cls.LongClose
181
+ elif capital_str == 'Cover' or capital_str == 'Bc':
182
+ trade_side = cls.ShortClose
183
+ elif capital_str == 'Ask':
184
+ trade_side = cls.AskOrder
185
+ elif capital_str == 'Bid':
186
+ trade_side = cls.BidOrder
187
+ else:
188
+ # noinspection PyBroadException
189
+ try:
190
+ trade_side = cls.__getitem__(value)
191
+ except Exception as _:
192
+ trade_side = cls.UNKNOWN
193
+ LOGGER.warning('{} is not recognized, return TransactionSide.UNKNOWN'.format(value))
194
+
195
+ return trade_side
196
+
197
+ @property
198
+ def sign(self) -> int:
199
+ """
200
+ Get the sign of the transaction side.
201
+
202
+ Returns:
203
+ int: 1 for buy/long, -1 for sell/short, 0 for unknown.
204
+ """
205
+ if self.value == self.Buy_to_Long.value or self.value == self.Buy_to_Cover.value:
206
+ return 1
207
+ elif self.value == self.Sell_to_Unwind.value or self.value == self.Sell_to_Short.value:
208
+ return -1
209
+ elif self.value == 0:
210
+ return 0
211
+ else:
212
+ LOGGER.warning(f'Requesting .sign of {self.name} is not recommended, use .order_sign instead')
213
+ return self.order_sign
214
+
215
+ @property
216
+ def order_sign(self) -> int:
217
+ """
218
+ Get the order sign of the transaction side.
219
+
220
+ Returns:
221
+ int: 1 for long orders, -1 for short orders, 0 for unknown.
222
+ """
223
+ if self.value == self.LongOrder.value:
224
+ return 1
225
+ elif self.value == self.ShortOrder.value:
226
+ return -1
227
+ elif self.value == 0:
228
+ return 0
229
+ else:
230
+ LOGGER.warning(f'Requesting .order_sign of {self.name} is not recommended, use .sign instead')
231
+ return self.sign
232
+
233
+ @property
234
+ def offset(self) -> int:
235
+ """
236
+ Get the offset of the transaction side.
237
+
238
+ Returns:
239
+ int: The offset value, equivalent to the sign.
240
+ """
241
+ return self.sign
242
+
243
+ @property
244
+ def side_name(self) -> str:
245
+ """
246
+ Get the name of the transaction side.
247
+
248
+ Returns:
249
+ str: 'Long', 'Short', 'ask', 'bid', or 'Unknown'.
250
+ """
251
+ if self.value == self.Buy_to_Long.value or self.value == self.Buy_to_Cover.value:
252
+ return 'Long'
253
+ elif self.value == self.Sell_to_Unwind.value or self.value == self.Sell_to_Short.value:
254
+ return 'Short'
255
+ elif self.value == self.Offer_to_Short.value:
256
+ return 'ask'
257
+ elif self.value == self.Bid_to_Long.value:
258
+ return 'bid'
259
+ else:
260
+ return 'Unknown'
261
+
262
+ @property
263
+ def offset_name(self) -> str:
264
+ """
265
+ Get the offset name of the transaction side.
266
+
267
+ Returns:
268
+ str: 'Open', 'Close', 'ask', 'bid', or 'Unknown'.
269
+ """
270
+ if self.value == self.Buy_to_Long.value or self.value == self.Sell_to_Short.value:
271
+ return 'Open'
272
+ elif self.value == self.Buy_to_Cover.value or self.value == self.Sell_to_Unwind.value:
273
+ return 'Close'
274
+ elif self.value == self.Offer_to_Short.value or self.value == self.Bid_to_Long.value:
275
+ LOGGER.warning(f'Requesting offset of {self.name} is not supported, returns {self.side_name}')
276
+ return self.side_name
277
+ else:
278
+ return 'Unknown'
279
+
280
+
281
+ class MarketData(dict, metaclass=abc.ABCMeta):
282
+ """
283
+ Abstract base class for representing market data in the form of a dictionary.
284
+
285
+ Properties:
286
+ ticker (str): The ticker symbol associated with the market data.
287
+ timestamp (float): The timestamp of the market data in seconds since the epoch.
288
+ additional (dict): A dictionary to store any additional attributes provided during initialization.
289
+
290
+ Methods:
291
+ __copy__(): Create a shallow copy of the MarketData instance.
292
+ copy(): Alias for __copy__().
293
+ to_json(fmt='str', **kwargs): Convert the MarketData instance to a JSON-compatible format.
294
+ from_json(json_message: str | bytes | bytearray | dict) -> MarketData: Create a MarketData instance from a JSON string or dictionary.
295
+ to_list() -> list[float | int | str | bool]: Abstract method to convert the MarketData instance to a list.
296
+ from_list(data_list: list[float | int | str | bool]) -> MarketData: Create a MarketData instance from a list.
297
+ """
298
+
299
+ def __init__(self, ticker: str, timestamp: float, **kwargs):
300
+ """
301
+ Initialize a MarketData instance.
302
+
303
+ Args:
304
+ ticker (str): The ticker symbol associated with the market data.
305
+ timestamp (float): The timestamp of the market data in seconds since the epoch.
306
+ **kwargs: Additional keyword arguments to store as extra attributes.
307
+ """
308
+ super().__init__(ticker=ticker, timestamp=timestamp)
309
+
310
+ if kwargs:
311
+ self['additional'] = dict(kwargs)
312
+
313
+ def __copy__(self):
314
+ """
315
+ Create a shallow copy of the MarketData instance.
316
+
317
+ Returns:
318
+ MarketData: A new instance of MarketData with the same data.
319
+ """
320
+ return self.__class__(**self)
321
+
322
+ def copy(self):
323
+ """
324
+ Alias for __copy__().
325
+
326
+ Returns:
327
+ MarketData: A new instance of MarketData with the same data.
328
+ """
329
+ return self.__copy__()
330
+
331
+ def to_json(self, fmt='str', **kwargs) -> str | dict:
332
+ """
333
+ Convert the MarketData instance to a JSON-compatible format.
334
+
335
+ Args:
336
+ fmt (str, optional): The output format. Can be 'str' to return a JSON string,
337
+ or 'dict' to return a dictionary. Defaults to 'str'.
338
+ **kwargs: Additional keyword arguments passed to the json.dumps function.
339
+
340
+ Returns:
341
+ str | dict: The MarketData instance as a JSON string or dictionary.
342
+ """
343
+ data_dict = dict(
344
+ dtype=self.__class__.__name__,
345
+ **self
346
+ )
347
+
348
+ if 'additional' in data_dict:
349
+ additional = data_dict.pop('additional')
350
+ data_dict.update(additional)
351
+
352
+ if fmt == 'dict':
353
+ return data_dict
354
+ elif fmt == 'str':
355
+ return json.dumps(data_dict, **kwargs)
356
+ else:
357
+ raise ValueError(f'Invalid format {fmt}, expected "dict" or "str".')
358
+
359
+ @classmethod
360
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
361
+ """
362
+ Create a MarketData instance from a JSON string or dictionary.
363
+
364
+ Args:
365
+ json_message (str | bytes | bytearray | dict): The JSON string or dictionary containing market data information.
366
+
367
+ Returns:
368
+ MarketData: An instance of a subclass of MarketData.
369
+
370
+ Raises:
371
+ TypeError: If the 'dtype' key in the JSON message is invalid or unrecognized.
372
+ """
373
+ if isinstance(json_message, dict):
374
+ json_dict = json_message
375
+ else:
376
+ json_dict = json.loads(json_message)
377
+
378
+ dtype = json_dict.pop('dtype', None)
379
+ if dtype == 'BarData':
380
+ return BarData.from_json(json_dict)
381
+ elif dtype == 'TickData':
382
+ return TickData.from_json(json_dict)
383
+ elif dtype == 'TransactionData':
384
+ return TransactionData.from_json(json_dict)
385
+ elif dtype == 'TradeData':
386
+ return TradeData.from_json(json_dict)
387
+ elif dtype == 'OrderBook':
388
+ return OrderBook.from_json(json_dict)
389
+ else:
390
+ raise TypeError(f'Invalid dtype {dtype}')
391
+
392
+ @abc.abstractmethod
393
+ def to_list(self) -> list[float | int | str | bool]:
394
+ """
395
+ Convert the MarketData instance to a list.
396
+
397
+ Returns:
398
+ list[float | int | str | bool]: A list representing the MarketData instance.
399
+
400
+ Note:
401
+ This method must be implemented by subclasses.
402
+ """
403
+ ...
404
+
405
+ @classmethod
406
+ def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
407
+ """
408
+ Create a MarketData instance from a list.
409
+
410
+ Args:
411
+ data_list (list[float | int | str | bool]): A list containing market data information.
412
+
413
+ Returns:
414
+ MarketData: An instance of a subclass of MarketData.
415
+
416
+ Raises:
417
+ TypeError: If the first element of the list (dtype) is invalid or unrecognized.
418
+ """
419
+ dtype = data_list[0]
420
+
421
+ if dtype == 'BarData':
422
+ return BarData.from_list(data_list)
423
+ elif dtype == 'TickData':
424
+ return TickData.from_list(data_list)
425
+ elif dtype == 'TransactionData':
426
+ return TransactionData.from_list(data_list)
427
+ elif dtype == 'TradeData':
428
+ return TradeData.from_list(data_list)
429
+ elif dtype == 'OrderBook':
430
+ return OrderBook.from_list(data_list)
431
+ else:
432
+ raise TypeError(f'Invalid dtype {dtype}')
433
+
434
+ @property
435
+ def ticker(self) -> str:
436
+ """
437
+ Get the ticker symbol of the market data.
438
+
439
+ Returns:
440
+ str: The ticker symbol.
441
+ """
442
+ return self['ticker']
443
+
444
+ @property
445
+ def timestamp(self) -> float:
446
+ """
447
+ Get the timestamp of the market data.
448
+
449
+ Returns:
450
+ float: The timestamp in seconds since the epoch.
451
+ """
452
+ return self['timestamp']
453
+
454
+ @property
455
+ def additional(self) -> dict:
456
+ """
457
+ Get the additional attributes stored in the market data.
458
+
459
+ Returns:
460
+ dict: A dictionary of additional attributes.
461
+ """
462
+ if 'additional' not in self:
463
+ self['additional'] = {}
464
+ return self['additional']
465
+
466
+ @property
467
+ def topic(self) -> str:
468
+ """
469
+ Get the topic string for the market data, used for messaging or logging.
470
+
471
+ Returns:
472
+ str: The topic string in the format 'ticker.MarketDataClassName'.
473
+ """
474
+ return f'{self.ticker}.{self.__class__.__name__}'
475
+
476
+ @property
477
+ def market_time(self) -> datetime.datetime | datetime.date:
478
+ """
479
+ Get the market time as a datetime object.
480
+
481
+ Returns:
482
+ datetime.datetime | datetime.date: The market time based on the timestamp.
483
+ """
484
+ return datetime.datetime.fromtimestamp(self.timestamp, tz=PROFILE.time_zone)
485
+
486
+ @property
487
+ @abc.abstractmethod
488
+ def market_price(self) -> float:
489
+ """
490
+ Abstract property to get the market price.
491
+
492
+ Returns:
493
+ float: The market price.
494
+
495
+ Note:
496
+ This property must be implemented by subclasses.
497
+ """
498
+ ...
499
+
500
+
501
+ class OrderBook(MarketData):
502
+ """
503
+ Class representing an order book, which tracks bid and ask orders for a financial instrument.
504
+
505
+ Nested Classes:
506
+ Book: Represents a side of the order book (either bid or ask), with methods for managing entries.
507
+ """
508
+
509
+ class Book(object):
510
+ """
511
+ Class representing a side of the order book (either bid or ask).
512
+
513
+ Attributes:
514
+ side (int): Indicates the side of the book; positive for bid, negative for ask.
515
+ _book (list[tuple[float, float, ...]]): A list of tuples representing (price, volume, order).
516
+ _dict (dict[float, tuple[float, float, ...]]): A dictionary mapping prices to order book entries.
517
+ sorted (bool): Indicates whether the book is sorted.
518
+ """
519
+
520
+ def __init__(self, side: int):
521
+ """
522
+ Initialize the order book for a specific side.
523
+
524
+ Args:
525
+ side (int): Side of the book; positive for bid, negative for ask.
526
+ """
527
+ self.side: int = side
528
+ # store the entry in order of (price, volume, order, etc...)
529
+ self._book: list[tuple[float, float, ...]] = []
530
+ self._dict: dict[float, tuple[float, float, ...]] = {}
531
+ self.sorted = False
532
+
533
+ def __iter__(self):
534
+ """
535
+ Iterate over the sorted order book.
536
+
537
+ Returns:
538
+ iterator: An iterator over the sorted book entries.
539
+ """
540
+ self.sort()
541
+ return self._book.__iter__()
542
+
543
+ def __getitem__(self, item):
544
+ """
545
+ Retrieve an entry by price or level.
546
+
547
+ Args:
548
+ item (int | float): Level number (int) or price (float).
549
+
550
+ Returns:
551
+ tuple[float, float, ...]: The order book entry at the specified level or price.
552
+
553
+ Raises:
554
+ KeyError: If the index value is ambiguous.
555
+ """
556
+ if isinstance(item, int) and item not in self._dict:
557
+ return self.at_level(item)
558
+ elif isinstance(item, float):
559
+ return self.at_price(item)
560
+ else:
561
+ raise KeyError(f'Ambiguous index value {item}, please use at_price or at_level specifically')
562
+
563
+ def __contains__(self, price: float):
564
+ """
565
+ Check if a price exists in the order book.
566
+
567
+ Args:
568
+ price (float): The price to check.
569
+
570
+ Returns:
571
+ bool: True if the price exists, False otherwise.
572
+ """
573
+ return self._dict.__contains__(price)
574
+
575
+ def __len__(self):
576
+ """
577
+ Get the number of entries in the order book.
578
+
579
+ Returns:
580
+ int: The number of entries.
581
+ """
582
+ return self._book.__len__()
583
+
584
+ def __repr__(self):
585
+ """
586
+ Get a string representation of the book.
587
+
588
+ Returns:
589
+ str: A string indicating whether the book is for bids or asks.
590
+ """
591
+ return f'<OrderBook.Book.{"Bid" if self.side > 0 else "Ask"}>'
592
+
593
+ def __bool__(self):
594
+ """
595
+ Check if the order book has any entries.
596
+
597
+ Returns:
598
+ bool: True if the book is not empty, False otherwise.
599
+ """
600
+ return bool(self._book)
601
+
602
+ def __sub__(self, other: Self) -> dict[float, float]:
603
+ """
604
+ Subtract another order book from this one to find the differences in volumes at matching prices.
605
+
606
+ Args:
607
+ other (OrderBook.Book): The other book to compare against.
608
+
609
+ Returns:
610
+ dict[float, float]: A dictionary of price differences.
611
+
612
+ Raises:
613
+ TypeError: If the other object is not of type OrderBook.Book.
614
+ ValueError: If the sides of the books do not match.
615
+ """
616
+ if not isinstance(other, self.__class__):
617
+ raise TypeError(f'Expect type {self.__class__.__name__}, got {type(other)}')
618
+
619
+ if self.side != other.side:
620
+ raise ValueError(f'Expect side {self.side}, got {other.side}')
621
+
622
+ diff = {}
623
+
624
+ # bid book
625
+ if (not self._dict) or (not other._dict):
626
+ pass
627
+ elif self.side > 0:
628
+ limit_0 = min(self._dict)
629
+ limit_1 = min(other._dict)
630
+ limit = max(limit_0, limit_1)
631
+ contain_limit = limit_0 == limit_1
632
+
633
+ for entry in self._book:
634
+ price, volume, *_ = entry
635
+
636
+ if price > limit or (price >= limit and contain_limit):
637
+ diff[price] = volume
638
+
639
+ for entry in other._book:
640
+ price, volume, *_ = entry
641
+
642
+ if price > limit or (price >= limit and contain_limit):
643
+ diff[price] = diff.get(price, 0.) - volume
644
+ # ask book
645
+ else:
646
+ limit_0 = max(self._dict)
647
+ limit_1 = max(other._dict)
648
+ limit = min(limit_0, limit_1)
649
+ contain_limit = limit_0 == limit_1
650
+
651
+ for entry in self._book:
652
+ price, volume, *_ = entry
653
+
654
+ if price < limit or (price <= limit and contain_limit):
655
+ diff[price] = volume
656
+
657
+ for entry in other._book:
658
+ price, volume, *_ = entry
659
+
660
+ if price < limit or (price <= limit and contain_limit):
661
+ diff[price] = diff.get(price, 0.) - volume
662
+
663
+ return diff
664
+
665
+ def get(self, item=None, **kwargs) -> tuple[float, float, ...] | None:
666
+ """
667
+ Retrieve an entry by price or level, with flexibility for keyword arguments.
668
+
669
+ Args:
670
+ item (int | float, optional): The level (int) or price (float) to retrieve.
671
+ **kwargs: Additional arguments for price or level.
672
+
673
+ Returns:
674
+ tuple[float, float, ...] | None: The entry at the specified price or level, or None if not found.
675
+
676
+ Raises:
677
+ ValueError: If both price and level are not provided or both are provided.
678
+ """
679
+ if item is None:
680
+ price = kwargs.pop('price', None)
681
+ level = kwargs.pop('level', None)
682
+ else:
683
+ if isinstance(item, int):
684
+ price = None
685
+ level = item
686
+ elif isinstance(item, float):
687
+ price = item
688
+ level = None
689
+ else:
690
+ raise ValueError(f'Invalid type {type(item)}, must be int or float')
691
+
692
+ if price is None and level is None:
693
+ raise ValueError('Must assign either price or level in kwargs')
694
+ elif price is None:
695
+ try:
696
+ return self.at_level(level=level)
697
+ except IndexError:
698
+ return None
699
+ elif level is None:
700
+ try:
701
+ return self.at_price(price=price)
702
+ except KeyError:
703
+ return None
704
+ else:
705
+ raise ValueError('Must NOT assign both price and level in kwargs')
706
+
707
+ def pop(self, price: float):
708
+ """
709
+ Remove and return an entry at the specified price.
710
+
711
+ Args:
712
+ price (float): The price of the entry to remove.
713
+
714
+ Returns:
715
+ tuple[float, float, ...]: The removed entry.
716
+
717
+ Raises:
718
+ KeyError: If the price does not exist in the order book.
719
+ """
720
+ entry = self._dict.pop(price, None)
721
+ if entry is not None:
722
+ self._book.remove(entry)
723
+ else:
724
+ raise KeyError(f'Price {price} does not exist in the order book')
725
+ return entry
726
+
727
+ def remove(self, entry: tuple[float, float, ...]):
728
+ """
729
+ Remove a specific entry from the order book.
730
+
731
+ Args:
732
+ entry (tuple[float, float, ...]): The entry to remove.
733
+
734
+ Raises:
735
+ ValueError: If the entry does not exist in the order book.
736
+ """
737
+ try:
738
+ self._book.remove(entry)
739
+ self._dict.pop(entry[0])
740
+ except ValueError:
741
+ raise ValueError(f'Entry {entry} does not exist in the order book')
742
+
743
+ def at_price(self, price: float):
744
+ """
745
+ Get the entry at a specific price.
746
+
747
+ Args:
748
+ price (float): The price to search for.
749
+
750
+ Returns:
751
+ tuple[float, float, ...]: The entry at the given price, or None if not found.
752
+ """
753
+ if price in self._dict:
754
+ return self._dict.__getitem__(price)
755
+ else:
756
+ return None
757
+
758
+ def at_level(self, level: int):
759
+ """
760
+ Get the entry at a specific level.
761
+
762
+ Args:
763
+ level (int): The level to search for.
764
+
765
+ Returns:
766
+ tuple[float, float, ...]: The entry at the given level.
767
+ """
768
+ return self._book.__getitem__(level)
769
+
770
+ def update(self, price: float, volume: float, order: int = None):
771
+ """
772
+ Update or add an entry in the order book.
773
+
774
+ Args:
775
+ price (float): The price of the entry to update.
776
+ volume (float): The new volume for the entry.
777
+ order (int, optional): The order number. Defaults to None.
778
+
779
+ Raises:
780
+ ValueError: If the volume is invalid.
781
+ """
782
+ if price in self._dict:
783
+ if volume == 0:
784
+ self.pop(price=price)
785
+ elif volume < 0:
786
+ LOGGER.warning(f'Invalid volume {volume}, expect a positive float.')
787
+ self.pop(price=price)
788
+ else:
789
+ entry = self._dict[price]
790
+ new_entry = list(entry)
791
+ new_entry[1] = volume
792
+ self._dict[price] = tuple(new_entry)
793
+ self._book[self._book.index(entry)] = tuple(new_entry)
794
+ else:
795
+ self.add(price=price, volume=volume, order=order)
796
+
797
+ def add(self, price: float, volume: float, order: int = None):
798
+ """
799
+ Add a new entry to the order book.
800
+
801
+ Args:
802
+ price (float): The price of the new entry.
803
+ volume (float): The volume of the new entry.
804
+ order (int, optional): The order number. Defaults to None.
805
+ """
806
+ entry = (price, volume, order if order else 0)
807
+ self._dict[price] = entry
808
+ self._book.append(entry)
809
+
810
+ def loc_volume(self, p0: float, p1: float) -> float:
811
+ """
812
+ Calculate the total volume between two price levels. Inclusive of the 2 given price.
813
+
814
+ Args:
815
+ p0 (float): The first price level.
816
+ p1 (float): The second price level.
817
+
818
+ Returns:
819
+ float: The total volume between the two prices.
820
+ """
821
+ volume = 0.0
822
+ p_min = min(p0, p1)
823
+ p_max = max(p0, p1)
824
+
825
+ for entry in self._book:
826
+ price, vol, *_ = entry
827
+ if p_min <= price <= p_max:
828
+ volume += vol
829
+
830
+ return volume
831
+
832
+ def sort(self):
833
+ """
834
+ Sort the order book by price in the appropriate order (descending for bids, ascending for asks).
835
+ """
836
+ if self.side > 0: # bid
837
+ self._book.sort(reverse=True, key=lambda x: x[0])
838
+ else: # ask
839
+ self._book.sort(key=lambda x: x[0])
840
+ self.sorted = True
841
+
842
+ @property
843
+ def price(self) -> list[float]:
844
+ """
845
+ Get a sorted list of all prices in the order book.
846
+
847
+ Returns:
848
+ list[float]: A list of all prices.
849
+ """
850
+ if not self.sorted:
851
+ self.sort()
852
+
853
+ return [entry[0] for entry in self._book]
854
+
855
+ @property
856
+ def volume(self) -> list[float]:
857
+ """
858
+ Get a sorted list of all volumes in the order book.
859
+
860
+ Returns:
861
+ list[float]: A list of all volumes.
862
+ """
863
+ if not self.sorted:
864
+ self.sort()
865
+
866
+ return [entry[1] for entry in self._book]
867
+
868
+ def __init__(self, *, ticker: str, timestamp: float, bid: list[list[float | int]] = None, ask: list[list[float | int]] = None, **kwargs):
869
+ """
870
+ Initialize an OrderBook instance with market data.
871
+
872
+ Args:
873
+ ticker (str): The ticker symbol of the financial instrument.
874
+ timestamp (float): The timestamp of the market data.
875
+ bid (list[list[float | int]], optional): A list of bid data, where each sublist contains price, volume, and optionally, order numbers. Defaults to None.
876
+ ask (list[list[float | int]], optional): A list of ask data. Defaults to None.
877
+ **kwargs: Additional key-value pairs for parsing extra data fields.
878
+ """
879
+ super().__init__(ticker=ticker, timestamp=timestamp)
880
+ self.update(
881
+ bid=[] if bid is None else bid,
882
+ ask=[] if ask is None else ask
883
+ )
884
+ self.parse(**kwargs)
885
+
886
+ def __getattr__(self, item: str):
887
+ """
888
+ Dynamically retrieve attributes like bid_price_X or ask_volume_Y.
889
+
890
+ Args:
891
+ item (str): The name of the attribute to retrieve.
892
+
893
+ Returns:
894
+ The value of the requested attribute.
895
+
896
+ Raises:
897
+ AttributeError: If the attribute is not found or the query level exceeds the maximum level.
898
+ """
899
+ if re.match('^((bid_)|(ask_))((price_)|(volume_))[0-9]+$', item):
900
+ side, key, level, *_ = item.split('_')
901
+ level = int(level)
902
+ book: OrderBook.Book = self.__getattribute__(f'{side}')
903
+ if 0 < level <= len(book):
904
+ return book[level - 1].__getattribute__(key)
905
+ else:
906
+ raise AttributeError(f'query level [{level}] exceed max level [{len(book)}]')
907
+ else:
908
+ raise AttributeError(f'{item} not found in {self.__class__}')
909
+
910
+ def __setattr__(self, key, value):
911
+ """
912
+ Dynamically set attributes like bid_price_X or ask_volume_Y.
913
+
914
+ Args:
915
+ key (str): The name of the attribute to set.
916
+ value: The value to set for the attribute.
917
+ """
918
+ if re.match('^((bid_)|(ask_))((price_)|(volume_))[0-9]+$', key):
919
+ self.update({key: value})
920
+ else:
921
+ super().__setattr__(key, value)
922
+
923
+ def __repr__(self):
924
+ """
925
+ String representation of the OrderBook instance.
926
+
927
+ Returns:
928
+ str: A string describing the OrderBook.
929
+ """
930
+ return f'<OrderBook>([{self.market_time:%Y-%m-%d %H:%M:%S}] {self.ticker}, bid={self.best_bid_price}, ask={self.best_ask_price})'
931
+
932
+ def __str__(self):
933
+ """
934
+ String representation for print output.
935
+
936
+ Returns:
937
+ str: A detailed string representation of the OrderBook.
938
+ """
939
+ return f'<OrderBook>([{self.market_time:%Y-%m-%d %H:%M:%S}] {self.ticker} {{Bid: {self.best_bid_price, self.best_bid_volume}, Ask: {self.best_ask_price, self.best_ask_volume}, Level: {self.max_level}}})'
940
+
941
+ def __bool__(self):
942
+ """
943
+ Boolean value of the OrderBook instance.
944
+
945
+ Returns:
946
+ bool: True if both bid and ask sides have entries, False otherwise.
947
+ """
948
+ return bool(self.bid) and bool(self.ask)
949
+
950
+ @classmethod
951
+ def _parse_entry_name(cls, name: str, validate: bool = False) -> tuple[str, str, int]:
952
+ """
953
+ Parse an entry name like bid_price_X into its components.
954
+
955
+ Args:
956
+ name (str): The entry name to parse.
957
+ validate (bool, optional): Whether to validate the entry name. Defaults to False.
958
+
959
+ Returns:
960
+ tuple[str, str, int]: The parsed side, key, and level.
961
+
962
+ Raises:
963
+ ValueError: If validation fails and the name is not parsable.
964
+ """
965
+ if validate:
966
+ if not re.match('^((bid_)|(ask_))((price_)|(volume_)|(order_))[0-9]+$', name):
967
+ raise ValueError(f'Cannot parse kwargs {name}.')
968
+
969
+ side, key, level = name.split('_')
970
+ level = int(level)
971
+
972
+ return side, key, level
973
+
974
+ @overload
975
+ def parse(self, data: dict[str, float] = None, /, bid_price_1: float = math.nan, bid_volume_1: float = math.nan, ask_price_1: float = math.nan, ask_volume_1: float = math.nan, **kwargs: float):
976
+ ...
977
+
978
+ def parse(self, data: dict[str, float] = None, validate: bool = False, **kwargs):
979
+ """
980
+ Parse bid and ask data into the OrderBook.
981
+
982
+ Args:
983
+ data (dict[str, float], optional): A dictionary of data entries to parse. Defaults to None.
984
+ validate (bool, optional): Whether to validate the entry names. Defaults to False.
985
+ **kwargs: Additional key-value pairs for parsing into the OrderBook.
986
+ """
987
+ if not data:
988
+ data = {}
989
+
990
+ data.update(kwargs)
991
+
992
+ for name, value in data.items():
993
+ side, key, level = self._parse_entry_name(name, validate)
994
+ book: list = self[side]
995
+
996
+ if level <= 0:
997
+ raise ValueError(f'Level of name [{name}] must be greater than zero!')
998
+
999
+ entry_idx = {'price': 0, 'volume': 1, 'order': 2}[key]
1000
+
1001
+ while level > len(book):
1002
+ book.append([math.nan, 0, 0])
1003
+
1004
+ book[level - 1][entry_idx] = value
1005
+
1006
+ @classmethod
1007
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
1008
+ """
1009
+ Create an OrderBook instance from a JSON message.
1010
+
1011
+ Args:
1012
+ json_message (str | bytes | bytearray | dict): The JSON message to parse.
1013
+
1014
+ Returns:
1015
+ OrderBook: An instance of the OrderBook class.
1016
+
1017
+ Raises:
1018
+ TypeError: If the dtype in the JSON message does not match the class name.
1019
+ """
1020
+ if isinstance(json_message, dict):
1021
+ json_dict = json_message
1022
+ else:
1023
+ json_dict = json.loads(json_message)
1024
+
1025
+ dtype = json_dict.pop('dtype', None)
1026
+ if dtype is not None and dtype != cls.__name__:
1027
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
1028
+
1029
+ self = cls(**json_dict)
1030
+ return self
1031
+
1032
+ def to_list(self) -> list[float | int | str | bool]:
1033
+ """
1034
+ Convert the OrderBook to a list format.
1035
+
1036
+ Returns:
1037
+ list[float | int | str | bool]: A list representation of the OrderBook.
1038
+ """
1039
+ min_length = min(len(self.bid), len(self.ask))
1040
+ r = ([self.__class__.__name__, self.ticker, self.timestamp]
1041
+ + [value for entry in self.bid[:min_length] for value in entry]
1042
+ + [value for entry in self.ask[:min_length] for value in entry])
1043
+
1044
+ return r
1045
+
1046
+ @classmethod
1047
+ def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
1048
+ """
1049
+ Create an OrderBook instance from a list format.
1050
+
1051
+ Args:
1052
+ data_list (list[float | int | str | bool]): A list representation of an OrderBook.
1053
+
1054
+ Returns:
1055
+ OrderBook: An instance of the OrderBook class.
1056
+
1057
+ Raises:
1058
+ TypeError: If the dtype in the list does not match the class name.
1059
+ """
1060
+ dtype, ticker, timestamp = data_list[:3]
1061
+ bid_data, ask_data = np.array(data_list[3:]).reshape(2, -1).tolist()
1062
+ bid = np.array(bid_data).reshape(-1, 3).tolist()
1063
+ ask = np.array(ask_data).reshape(-1, 3).tolist()
1064
+
1065
+ if dtype != cls.__name__:
1066
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
1067
+
1068
+ return cls(
1069
+ ticker=ticker,
1070
+ timestamp=timestamp,
1071
+ bid=bid,
1072
+ ask=ask
1073
+ )
1074
+
1075
+ @property
1076
+ def market_price(self):
1077
+ """
1078
+ Get the mid price of the order book.
1079
+
1080
+ Returns:
1081
+ float: The mid price, or NaN if not available.
1082
+ """
1083
+ return self.mid_price
1084
+
1085
+ @property
1086
+ def mid_price(self):
1087
+ """
1088
+ Calculate the mid price of the order book.
1089
+
1090
+ Returns:
1091
+ float: The mid price, or NaN if not available.
1092
+ """
1093
+ if math.isfinite(self.best_bid_price) and math.isfinite(self.best_ask_price):
1094
+ return (self.best_bid_price + self.best_ask_price) / 2
1095
+ else:
1096
+ return math.nan
1097
+
1098
+ @property
1099
+ def spread(self):
1100
+ """
1101
+ Calculate the bid-ask spread.
1102
+
1103
+ Returns:
1104
+ float: The spread, or NaN if not available.
1105
+ """
1106
+ if math.isfinite(self.best_bid_price) and math.isfinite(self.best_ask_price):
1107
+ return self.best_ask_price - self.best_bid_price
1108
+ else:
1109
+ return math.nan
1110
+
1111
+ @property
1112
+ def spread_pct(self):
1113
+ """
1114
+ Calculate the bid-ask spread as a percentage of the mid price.
1115
+
1116
+ Returns:
1117
+ float: The spread percentage, or infinity if mid price is zero.
1118
+ """
1119
+ if self.mid_price != 0:
1120
+ return self.spread / self.mid_price
1121
+ else:
1122
+ return np.inf
1123
+
1124
+ @property
1125
+ def bid(self) -> Book:
1126
+ """
1127
+ Get the bid side of the order book.
1128
+
1129
+ Returns:
1130
+ Book: An instance of the Book class representing the bid side.
1131
+ """
1132
+ book = self.Book(side=1)
1133
+ for price, volume, *_ in self['bid']:
1134
+ book.add(price=price, volume=volume)
1135
+ book.sort()
1136
+ return book
1137
+
1138
+ @property
1139
+ def ask(self) -> Book:
1140
+ """
1141
+ Get the ask side of the order book.
1142
+
1143
+ Returns:
1144
+ Book: An instance of the Book class representing the ask side.
1145
+ """
1146
+ book = self.Book(side=-1)
1147
+ for price, volume, *_ in self['ask']:
1148
+ book.add(price=price, volume=volume)
1149
+ book.sort()
1150
+ return book
1151
+
1152
+ @property
1153
+ def best_bid_price(self):
1154
+ """
1155
+ Get the best bid price in the order book.
1156
+
1157
+ Returns:
1158
+ float: The best bid price, or NaN if not available.
1159
+ """
1160
+ if book := self.bid:
1161
+ return book.at_level(0)[0]
1162
+ else:
1163
+ return math.nan
1164
+
1165
+ @property
1166
+ def best_ask_price(self):
1167
+ """
1168
+ Get the best ask price in the order book.
1169
+
1170
+ Returns:
1171
+ float: The best ask price, or NaN if not available.
1172
+ """
1173
+ if book := self.ask:
1174
+ return book.at_level(0)[0]
1175
+ else:
1176
+ return math.nan
1177
+
1178
+ @property
1179
+ def best_bid_volume(self):
1180
+ """
1181
+ Get the best bid volume in the order book.
1182
+
1183
+ Returns:
1184
+ float: The best bid volume, or NaN if not available.
1185
+ """
1186
+ if book := self.bid:
1187
+ return book[0][1]
1188
+ else:
1189
+ return math.nan
1190
+
1191
+ @property
1192
+ def best_ask_volume(self):
1193
+ """
1194
+ Get the best ask volume in the order book.
1195
+
1196
+ Returns:
1197
+ float: The best ask volume, or NaN if not available.
1198
+ """
1199
+ if book := self.ask:
1200
+ return book[0][1]
1201
+ else:
1202
+ return math.nan
1203
+
1204
+
1205
+ class BarData(MarketData):
1206
+ """
1207
+ Represents a single bar of market data for a specific ticker within a given time frame.
1208
+
1209
+ This class extends the `MarketData` class and includes attributes and methods relevant to a market bar,
1210
+ such as price, volume, and duration. It also provides functionality for data serialization and validation.
1211
+
1212
+ Methods:
1213
+ from_json(json_message: str | bytes | bytearray | dict) -> BarData:
1214
+ Creates a `BarData` instance from a JSON-encoded message or dictionary.
1215
+
1216
+ to_list() -> list[float | int | str | bool]:
1217
+ Converts the `BarData` instance to a list of its attributes.
1218
+
1219
+ from_list(data_list: list[float | int | str | bool]) -> BarData:
1220
+ Creates a `BarData` instance from a list of attributes.
1221
+
1222
+ is_valid(verbose=False) -> bool:
1223
+ Validates the `BarData` instance to ensure all required fields are set correctly.
1224
+
1225
+ Properties:
1226
+ high_price (float): The highest price during the bar.
1227
+ low_price (float): The lowest price during the bar.
1228
+ open_price (float): The opening price of the bar.
1229
+ close_price (float): The closing price of the bar.
1230
+ bar_span (datetime.timedelta): The duration of the bar.
1231
+ volume (float): The total volume of trades during the bar.
1232
+ notional (float): The total notional value of trades during the bar.
1233
+ trade_count (int): The number of trades that occurred during the bar.
1234
+ bar_start_time (datetime.datetime): The start time of the bar.
1235
+ vwap (float): The volume-weighted average price for the bar.
1236
+ market_price (float): The closing price of the bar.
1237
+ bar_type (Literal['Hourly-Plus', 'Hourly', 'Minute-Plus', 'Minute', 'Sub-Minute']): The type of the bar based on its span.
1238
+ bar_end_time (datetime.datetime | datetime.date): The end time of the bar.
1239
+ """
1240
+
1241
+ def __init__(
1242
+ self, *,
1243
+ ticker: str,
1244
+ timestamp: float, # The bar end timestamp
1245
+ start_timestamp: float = None,
1246
+ bar_span: datetime.timedelta | int | float = None,
1247
+ high_price: float = math.nan,
1248
+ low_price: float = math.nan,
1249
+ open_price: float = math.nan,
1250
+ close_price: float = math.nan,
1251
+ volume: float = 0.,
1252
+ notional: float = 0.,
1253
+ trade_count: int = 0,
1254
+ **kwargs
1255
+ ):
1256
+ """
1257
+ Initializes a new instance of `BarData`.
1258
+
1259
+ Args:
1260
+ ticker (str): The ticker symbol for the market data.
1261
+ timestamp (float): The timestamp marking the end of the bar.
1262
+ start_timestamp (float, optional): The timestamp marking the start of the bar. Required if `bar_span` is not provided.
1263
+ bar_span (datetime.timedelta | int | float, optional): The duration of the bar. Either this or `start_timestamp` must be provided.
1264
+ high_price (float, optional): The highest price during the bar. Defaults to NaN.
1265
+ low_price (float, optional): The lowest price during the bar. Defaults to NaN.
1266
+ open_price (float, optional): The opening price of the bar. Defaults to NaN.
1267
+ close_price (float, optional): The closing price of the bar. Defaults to NaN.
1268
+ volume (float, optional): The total volume of trades during the bar. Defaults to 0.0.
1269
+ notional (float, optional): The total notional value of trades during the bar. Defaults to 0.0.
1270
+ trade_count (int, optional): The number of trades that occurred during the bar. Defaults to 0.
1271
+ **kwargs: Additional keyword arguments passed to the parent `MarketData` class.
1272
+
1273
+ Raises:
1274
+ ValueError: If neither `start_timestamp` nor `bar_span` is provided or if `bar_span` is of invalid type.
1275
+ """
1276
+ super().__init__(ticker=ticker, timestamp=timestamp, **kwargs)
1277
+ self.update(
1278
+ high_price=high_price,
1279
+ low_price=low_price,
1280
+ open_price=open_price,
1281
+ close_price=close_price,
1282
+ volume=volume,
1283
+ notional=notional,
1284
+ trade_count=trade_count
1285
+ )
1286
+
1287
+ if bar_span is None and start_timestamp is None:
1288
+ raise ValueError('Must assign either start_timestamp or bar_span or both.')
1289
+ elif start_timestamp is None:
1290
+ # self['start_timestamp'] = timestamp - bar_span.total_seconds()
1291
+ if isinstance(bar_span, datetime.timedelta):
1292
+ self['bar_span'] = bar_span.total_seconds()
1293
+ elif isinstance(bar_span, (int, float)):
1294
+ self['bar_span'] = bar_span
1295
+ else:
1296
+ raise ValueError(f'Invalid bar_span, expect int, float or timedelta, got {bar_span}')
1297
+ elif bar_span is None:
1298
+ self['start_timestamp'] = start_timestamp
1299
+ else:
1300
+ self['start_timestamp'] = start_timestamp
1301
+
1302
+ if isinstance(bar_span, datetime.timedelta):
1303
+ self['bar_span'] = bar_span.total_seconds()
1304
+ elif isinstance(bar_span, (int, float)):
1305
+ self['bar_span'] = bar_span
1306
+ else:
1307
+ raise ValueError(f'Invalid bar_span, expect int, float or timedelta, got {bar_span}')
1308
+
1309
+ def __repr__(self):
1310
+ """
1311
+ Returns a string representation of the `BarData` instance.
1312
+
1313
+ The string representation includes the class name, market time, ticker symbol, and key price attributes.
1314
+
1315
+ Returns:
1316
+ str: A string representation of the `BarData` instance.
1317
+ """
1318
+ return f'<{self.__class__.__name__}>([{self.market_time:%Y-%m-%d %H:%M:%S}] {self.ticker}, open={self.open_price}, close={self.close_price}, high={self.high_price}, low={self.low_price})'
1319
+
1320
+ @classmethod
1321
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
1322
+ """
1323
+ Creates a `BarData` instance from a JSON-encoded message or dictionary.
1324
+
1325
+ Args:
1326
+ json_message (str | bytes | bytearray | dict): The JSON-encoded message or dictionary containing `BarData` attributes.
1327
+
1328
+ Returns:
1329
+ BarData: A `BarData` instance initialized with the data from the JSON message.
1330
+
1331
+ Raises:
1332
+ TypeError: If the dtype in the JSON does not match the class name.
1333
+ """
1334
+ if isinstance(json_message, dict):
1335
+ json_dict = json_message
1336
+ else:
1337
+ json_dict = json.loads(json_message)
1338
+
1339
+ dtype = json_dict.pop('dtype', None)
1340
+ if dtype is not None and dtype != cls.__name__:
1341
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
1342
+
1343
+ self = cls(**json_dict)
1344
+ return self
1345
+
1346
+ def to_list(self) -> list[float | int | str | bool]:
1347
+ """
1348
+ Converts the `BarData` instance to a list of its attributes.
1349
+
1350
+ Returns:
1351
+ list[float | int | str | bool]: A list containing the `BarData` instance's attributes.
1352
+ """
1353
+ return [self.__class__.__name__,
1354
+ self.ticker,
1355
+ self.timestamp,
1356
+ self.high_price,
1357
+ self.low_price,
1358
+ self.open_price,
1359
+ self.close_price,
1360
+ self.get('start_timestamp'),
1361
+ self.get('bar_span'),
1362
+ self.volume,
1363
+ self.notional,
1364
+ self.trade_count]
1365
+
1366
+ @classmethod
1367
+ def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
1368
+ """
1369
+ Creates a `BarData` instance from a list of attributes.
1370
+
1371
+ Args:
1372
+ data_list (list[float | int | str | bool]): A list of attributes representing a `BarData` instance.
1373
+
1374
+ Returns:
1375
+ BarData: A `BarData` instance initialized with the data from the list.
1376
+
1377
+ Raises:
1378
+ TypeError: If the dtype in the list does not match the class name.
1379
+ """
1380
+ (dtype, ticker, timestamp, high_price, low_price, open_price, close_price,
1381
+ start_timestamp, bar_span, volume, notional, trade_count) = data_list
1382
+
1383
+ if dtype != cls.__name__:
1384
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
1385
+
1386
+ return cls(
1387
+ ticker=ticker,
1388
+ timestamp=timestamp,
1389
+ high_price=high_price,
1390
+ low_price=low_price,
1391
+ open_price=open_price,
1392
+ close_price=close_price,
1393
+ start_timestamp=start_timestamp if start_timestamp else None,
1394
+ bar_span=datetime.timedelta(bar_span) if bar_span else None,
1395
+ volume=volume,
1396
+ notional=notional,
1397
+ trade_count=trade_count
1398
+ )
1399
+
1400
+ @property
1401
+ def high_price(self) -> float:
1402
+ """
1403
+ The highest price during the bar.
1404
+
1405
+ Returns:
1406
+ float: The highest price during the bar.
1407
+ """
1408
+ return self['high_price']
1409
+
1410
+ @property
1411
+ def low_price(self) -> float:
1412
+ """
1413
+ The lowest price during the bar.
1414
+
1415
+ Returns:
1416
+ float: The lowest price during the bar.
1417
+ """
1418
+ return self['low_price']
1419
+
1420
+ @property
1421
+ def open_price(self) -> float:
1422
+ """
1423
+ The opening price of the bar.
1424
+
1425
+ Returns:
1426
+ float: The opening price of the bar.
1427
+ """
1428
+ return self['open_price']
1429
+
1430
+ @property
1431
+ def close_price(self) -> float:
1432
+ """
1433
+ The closing price of the bar.
1434
+
1435
+ Returns:
1436
+ float: The closing price of the bar.
1437
+ """
1438
+ return self['close_price']
1439
+
1440
+ @property
1441
+ def bar_span(self) -> datetime.timedelta:
1442
+ """
1443
+ The duration of the bar.
1444
+
1445
+ Returns:
1446
+ datetime.timedelta: The duration of the bar.
1447
+ """
1448
+ if 'bar_span' in self:
1449
+ return datetime.timedelta(seconds=self['bar_span'])
1450
+ else:
1451
+ return datetime.timedelta(seconds=self['timestamp'] - self['start_timestamp'])
1452
+
1453
+ @property
1454
+ def volume(self) -> float:
1455
+ """
1456
+ The total volume of trades during the bar.
1457
+
1458
+ Returns:
1459
+ float: The total volume of trades during the bar.
1460
+ """
1461
+ return self['volume']
1462
+
1463
+ @property
1464
+ def notional(self) -> float:
1465
+ """
1466
+ The total notional value of trades during the bar.
1467
+
1468
+ Returns:
1469
+ float: The total notional value of trades during the bar.
1470
+ """
1471
+ return self['notional']
1472
+
1473
+ @property
1474
+ def trade_count(self) -> int:
1475
+ """
1476
+ The number of trades that occurred during the bar.
1477
+
1478
+ Returns:
1479
+ int: The number of trades that occurred during the bar.
1480
+ """
1481
+ return self['trade_count']
1482
+
1483
+ @property
1484
+ def bar_start_time(self) -> datetime.datetime:
1485
+ """
1486
+ The start time of the bar.
1487
+
1488
+ Returns:
1489
+ datetime.datetime: The start time of the bar.
1490
+ """
1491
+ if 'start_timestamp' in self:
1492
+ return datetime.datetime.fromtimestamp(self['start_timestamp'], tz=PROFILE.time_zone)
1493
+ else:
1494
+ return datetime.datetime.fromtimestamp(self['timestamp'] - self['bar_span'], tz=PROFILE.time_zone)
1495
+
1496
+ @property
1497
+ def vwap(self) -> float:
1498
+ """
1499
+ The volume-weighted average price for the bar.
1500
+
1501
+ Returns:
1502
+ float: The VWAP for the bar. Defaults to the closing price if volume is zero.
1503
+ """
1504
+ if self.volume != 0:
1505
+ return self.notional / self.volume
1506
+ else:
1507
+ LOGGER.warning(f'[{self.market_time}] {self.ticker} Volume data not available, using close_price as default VWAP value')
1508
+ return self.close_price
1509
+
1510
+ @property
1511
+ def is_valid(self, verbose=False) -> bool:
1512
+ """
1513
+ Validates the `BarData` instance to ensure all required fields are set correctly.
1514
+
1515
+ Args:
1516
+ verbose (bool, optional): If True, logs detailed validation errors. Defaults to False.
1517
+
1518
+ Returns:
1519
+ bool: True if the `BarData` instance is valid, False otherwise.
1520
+ """
1521
+ try:
1522
+ assert type(self.ticker) is str, '{} Invalid ticker'.format(str(self))
1523
+ assert math.isfinite(self.high_price), '{} Invalid high_price'.format(str(self))
1524
+ assert math.isfinite(self.low_price), '{} Invalid low_price'.format(str(self))
1525
+ assert math.isfinite(self.open_price), '{} Invalid open_price'.format(str(self))
1526
+ assert math.isfinite(self.close_price), '{} Invalid close_price'.format(str(self))
1527
+ assert math.isfinite(self.volume), '{} Invalid volume'.format(str(self))
1528
+ assert math.isfinite(self.notional), '{} Invalid notional'.format(str(self))
1529
+ assert math.isfinite(self.trade_count), '{} Invalid trade_count'.format(str(self))
1530
+ assert isinstance(self.bar_start_time, (datetime.datetime, datetime.date)), '{} Invalid bar_start_time'.format(str(self))
1531
+ assert isinstance(self.bar_span, datetime.timedelta), '{} Invalid bar_span'.format(str(self))
1532
+
1533
+ return True
1534
+ except AssertionError as e:
1535
+ if verbose:
1536
+ LOGGER.warning(str(e))
1537
+ return False
1538
+
1539
+ @property
1540
+ def market_price(self) -> float:
1541
+ """
1542
+ The closing price for the `BarData`.
1543
+
1544
+ Returns:
1545
+ float: The closing price of the bar.
1546
+ """
1547
+ return self.close_price
1548
+
1549
+ @property
1550
+ def bar_type(self) -> Literal['Hourly-Plus', 'Hourly', 'Minute-Plus', 'Minute', 'Sub-Minute']:
1551
+ """
1552
+ Determines the type of the bar based on its span.
1553
+
1554
+ Returns:
1555
+ Literal['Hourly-Plus', 'Hourly', 'Minute-Plus', 'Minute', 'Sub-Minute']: The type of the bar.
1556
+ """
1557
+ if self['bar_span'] > 3600:
1558
+ return 'Hourly-Plus'
1559
+ elif self['bar_span'] == 3600:
1560
+ return 'Hourly'
1561
+ elif self['bar_span'] > 60:
1562
+ return 'Minute-Plus'
1563
+ elif self['bar_span'] == 60:
1564
+ return 'Minute'
1565
+ else:
1566
+ return 'Sub-Minute'
1567
+
1568
+ @property
1569
+ def bar_end_time(self) -> datetime.datetime | datetime.date:
1570
+ """
1571
+ The end time of the bar.
1572
+
1573
+ Returns:
1574
+ datetime.datetime | datetime.date: The end time of the bar.
1575
+ """
1576
+ return self.market_time
1577
+
1578
+
1579
+ # alias of the BarData
1580
+ CandleStick = BarData
1581
+
1582
+
1583
+ class DailyBar(BarData):
1584
+ """
1585
+ Represents a daily bar of market data for a specific ticker.
1586
+
1587
+ This class extends the `BarData` class and focuses on daily bar data, which includes attributes and methods
1588
+ specific to daily market bars. It supports various ways to define the bar span and manage the market date.
1589
+
1590
+ Attributes:
1591
+ ...
1592
+
1593
+ Methods:
1594
+ __repr__() -> str:
1595
+ Returns a string representation of the `DailyBar` instance.
1596
+
1597
+ to_json(fmt='str', **kwargs) -> str | dict:
1598
+ Converts the `DailyBar` instance to a JSON string or dictionary.
1599
+
1600
+ to_list() -> list[float | int | str | bool]:
1601
+ Converts the `DailyBar` instance to a list of its attributes.
1602
+
1603
+ from_list(data_list: list[float | int | str | bool]) -> DailyBar:
1604
+ Creates a `DailyBar` instance from a list of attributes.
1605
+
1606
+ Properties:
1607
+ ticker (str): The ticker symbol for the market data.
1608
+ timestamp (float): The timestamp marking the end of the bar.
1609
+ start_date (datetime.date, optional): The start date of the bar period. Required if `bar_span` is not provided.
1610
+ bar_span (datetime.timedelta | int, optional): The duration of the bar in days. Either this or `start_date` must be provided.
1611
+ high_price (float): The highest price during the bar.
1612
+ low_price (float): The lowest price during the bar.
1613
+ open_price (float): The opening price of the bar.
1614
+ close_price (float): The closing price of the bar.
1615
+ volume (float): The total volume of trades during the bar.
1616
+ notional (float): The total notional value of trades during the bar.
1617
+ trade_count (int): The number of trades that occurred during the bar.
1618
+ bar_span (datetime.timedelta): The duration of the bar in days.
1619
+
1620
+ market_date (datetime.date): The market date of the bar.
1621
+ market_time (datetime.date): The market date of the bar (same as `market_date`).
1622
+ bar_start_time (datetime.date): The start date of the bar period.
1623
+ bar_end_time (datetime.date): The end date of the bar period.
1624
+ bar_type (Literal['Daily', 'Daily-Plus']): The type of the bar based on its span.
1625
+ """
1626
+
1627
+ def __init__(
1628
+ self, *,
1629
+ ticker: str,
1630
+ market_date: datetime.date | str, # The market date of the bar, if with 1D data, or the END date of the bar.
1631
+ timestamp: float = None,
1632
+ start_date: datetime.date = None,
1633
+ bar_span: datetime.timedelta | int = None, # Expect to be a timedelta for several days, or the number of days
1634
+ high_price: float = math.nan,
1635
+ low_price: float = math.nan,
1636
+ open_price: float = math.nan,
1637
+ close_price: float = math.nan,
1638
+ volume: float = 0.,
1639
+ notional: float = 0.,
1640
+ trade_count: int = 0,
1641
+ **kwargs
1642
+ ):
1643
+ """
1644
+ Initializes a new instance of `DailyBar`.
1645
+
1646
+ Args:
1647
+ ticker (str): The ticker symbol for the market data.
1648
+ market_date (datetime.date | str): The market date of the bar or the end date of the bar.
1649
+ timestamp (float, optional): The timestamp marking the end of the bar. Defaults to None.
1650
+ start_date (datetime.date, optional): The start date of the bar period. Required if `bar_span` is not provided.
1651
+ bar_span (datetime.timedelta | int, optional): The duration of the bar in days. Either this or `start_date` must be provided.
1652
+ high_price (float, optional): The highest price during the bar. Defaults to NaN.
1653
+ low_price (float, optional): The lowest price during the bar. Defaults to NaN.
1654
+ open_price (float, optional): The opening price of the bar. Defaults to NaN.
1655
+ close_price (float, optional): The closing price of the bar. Defaults to NaN.
1656
+ volume (float, optional): The total volume of trades during the bar. Defaults to 0.0.
1657
+ notional (float, optional): The total notional value of trades during the bar. Defaults to 0.0.
1658
+ trade_count (int, optional): The number of trades that occurred during the bar. Defaults to 0.
1659
+ **kwargs: Additional keyword arguments passed to the parent `BarData` class.
1660
+
1661
+ Raises:
1662
+ ValueError: If neither `start_date` nor `bar_span` is provided or if `bar_span` is of invalid type.
1663
+ """
1664
+ if isinstance(market_date, str):
1665
+ market_date = datetime.date.fromisoformat(market_date)
1666
+
1667
+ if bar_span is None and start_date is None:
1668
+ raise ValueError('Must assign either datetime.date or bar_span or both.')
1669
+ elif start_date is None:
1670
+ if isinstance(bar_span, datetime.timedelta):
1671
+ bar_span = bar_span.days
1672
+ elif isinstance(bar_span, int):
1673
+ pass
1674
+ else:
1675
+ raise ValueError(f'Invalid bar_span, expect int, float or timedelta, got {bar_span}')
1676
+ elif bar_span is None:
1677
+ bar_span = (market_date - start_date).days
1678
+ else:
1679
+ assert (market_date - start_date).days == bar_span.days
1680
+
1681
+ if timestamp is None:
1682
+ if PROFILE.session_end is None:
1683
+ timestamp = datetime.datetime.combine(market_date, datetime.time.min, tzinfo=PROFILE.time_zone).timestamp()
1684
+ else:
1685
+ timestamp = datetime.datetime.combine(market_date, PROFILE.session_end, tzinfo=PROFILE.time_zone).timestamp()
1686
+
1687
+ super().__init__(
1688
+ ticker=ticker,
1689
+ timestamp=timestamp,
1690
+ bar_span=bar_span,
1691
+ high_price=high_price,
1692
+ low_price=low_price,
1693
+ open_price=open_price,
1694
+ close_price=close_price,
1695
+ volume=volume,
1696
+ notional=notional,
1697
+ trade_count=trade_count,
1698
+ **kwargs
1699
+ )
1700
+
1701
+ self['market_date'] = market_date
1702
+
1703
+ def __repr__(self) -> str:
1704
+ """
1705
+ Returns a string representation of the `DailyBar` instance.
1706
+
1707
+ The string representation includes the class name, market date, ticker symbol, and key price attributes.
1708
+
1709
+ Returns:
1710
+ str: A string representation of the `DailyBar` instance.
1711
+ """
1712
+ return f'<{self.__class__.__name__}>([{self.market_time:%Y-%m-%d}] {self.ticker}, open={self.open_price}, close={self.close_price}, high={self.high_price}, low={self.low_price})'
1713
+
1714
+ def to_json(self, fmt='str', **kwargs) -> str | dict:
1715
+ """
1716
+ Converts the `DailyBar` instance to a JSON string or dictionary.
1717
+
1718
+ Args:
1719
+ fmt (str, optional): The format for the JSON output. Either 'dict' or 'str'. Defaults to 'str'.
1720
+ **kwargs: Additional keyword arguments passed to `json.dumps()` if `fmt='str'`.
1721
+
1722
+ Returns:
1723
+ str | dict: The JSON-encoded representation of the `DailyBar` instance, in the specified format.
1724
+
1725
+ Raises:
1726
+ ValueError: If an invalid format is specified.
1727
+ """
1728
+ data_dict = super().to_json(fmt='dict', **kwargs)
1729
+ data_dict['market_date'] = self.market_date.isoformat()
1730
+
1731
+ if fmt == 'dict':
1732
+ return data_dict
1733
+ elif fmt == 'str':
1734
+ return json.dumps(data_dict, **kwargs)
1735
+ else:
1736
+ raise ValueError(f'Invalid format {fmt}, expected "dict" or "str".')
1737
+
1738
+ def to_list(self) -> list[float | int | str | bool]:
1739
+ """
1740
+ Converts the `DailyBar` instance to a list of its attributes.
1741
+
1742
+ Returns:
1743
+ list[float | int | str | bool]: A list containing the `DailyBar` instance's attributes.
1744
+ """
1745
+ return [self.__class__.__name__,
1746
+ self.ticker,
1747
+ self.market_date.isoformat(),
1748
+ self.timestamp,
1749
+ self.high_price,
1750
+ self.low_price,
1751
+ self.open_price,
1752
+ self.close_price,
1753
+ self['bar_span'],
1754
+ self.volume,
1755
+ self.notional,
1756
+ self.trade_count]
1757
+
1758
+ @classmethod
1759
+ def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
1760
+ """
1761
+ Creates a `DailyBar` instance from a list of attributes.
1762
+
1763
+ Args:
1764
+ data_list (list[float | int | str | bool]): A list of attributes representing a `DailyBar` instance.
1765
+
1766
+ Returns:
1767
+ DailyBar: A `DailyBar` instance initialized with the data from the list.
1768
+
1769
+ Raises:
1770
+ TypeError: If the dtype in the list does not match the class name.
1771
+ """
1772
+ (dtype, ticker, market_date, timestamp, high_price, low_price, open_price, close_price,
1773
+ bar_span, volume, notional, trade_count) = data_list
1774
+
1775
+ if dtype != cls.__name__:
1776
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
1777
+
1778
+ return cls(
1779
+ ticker=ticker,
1780
+ market_date=market_date,
1781
+ timestamp=timestamp,
1782
+ high_price=high_price,
1783
+ low_price=low_price,
1784
+ open_price=open_price,
1785
+ close_price=close_price,
1786
+ bar_span=datetime.timedelta(days=bar_span) if bar_span else None,
1787
+ volume=volume,
1788
+ notional=notional,
1789
+ trade_count=trade_count
1790
+ )
1791
+
1792
+ @property
1793
+ def bar_span(self) -> datetime.timedelta:
1794
+ """
1795
+ The duration of the bar in days.
1796
+
1797
+ Returns:
1798
+ datetime.timedelta: The duration of the bar.
1799
+ """
1800
+ return datetime.timedelta(days=self['bar_span'])
1801
+
1802
+ @property
1803
+ def market_date(self) -> datetime.date:
1804
+ """
1805
+ The market date of the bar.
1806
+
1807
+ Returns:
1808
+ datetime.date: The market date of the bar.
1809
+ """
1810
+ return self['market_date']
1811
+
1812
+ @property
1813
+ def market_time(self) -> datetime.date:
1814
+ """
1815
+ The market date of the bar (same as `market_date`).
1816
+
1817
+ Returns:
1818
+ datetime.date: The market date of the bar.
1819
+ """
1820
+ return self.market_date
1821
+
1822
+ @property
1823
+ def bar_start_time(self) -> datetime.date:
1824
+ """
1825
+ The start date of the bar period.
1826
+
1827
+ Returns:
1828
+ datetime.date: The start date of the bar.
1829
+ """
1830
+ return self.market_date - self.bar_span
1831
+
1832
+ @property
1833
+ def bar_end_time(self) -> datetime.date:
1834
+ """
1835
+ The end date of the bar period.
1836
+
1837
+ Returns:
1838
+ datetime.date: The end date of the bar.
1839
+ """
1840
+ return self.market_date
1841
+
1842
+ @property
1843
+ def bar_type(self) -> Literal['Daily', 'Daily-Plus']:
1844
+ """
1845
+ Determines the type of the bar based on its span.
1846
+
1847
+ Returns:
1848
+ Literal['Daily', 'Daily-Plus']: The type of the bar.
1849
+
1850
+ Raises:
1851
+ ValueError: If `bar_span` is not valid for a daily bar.
1852
+ """
1853
+ if self['bar_span'] == 1:
1854
+ return 'Daily'
1855
+ elif self['bar_span'] > 1:
1856
+ return 'Daily-Plus'
1857
+ else:
1858
+ raise ValueError(f'Invalid bar_span for {self.__class__.__name__}! Expect an int greater or equal to 1, got {self["bar_span"]}')
1859
+
1860
+
1861
+ class TickData(MarketData):
1862
+ """
1863
+ Represents tick data for a specific ticker.
1864
+
1865
+ This class extends the `MarketData` class and focuses on tick-level market data, including last price, bid/ask prices,
1866
+ bid/ask volumes, and order book details.
1867
+
1868
+ Attributes:
1869
+ ...
1870
+
1871
+ Methods:
1872
+ __repr__() -> str:
1873
+ Returns a string representation of the `TickData` instance.
1874
+
1875
+ from_json(json_message: str | bytes | bytearray | dict) -> TickData:
1876
+ Creates a `TickData` instance from a JSON message.
1877
+
1878
+ to_list() -> list[float | int | str | bool]:
1879
+ Converts the `TickData` instance to a list of its attributes, excluding order book information.
1880
+
1881
+ from_list(data_list: list[float | int | str | bool]) -> TickData:
1882
+ Creates a `TickData` instance from a list of attributes.
1883
+
1884
+ Properties:
1885
+ ticker (str): The ticker symbol for the market data.
1886
+ timestamp (float): The timestamp of the tick data.
1887
+ bid (list[list[float | int]] | None): A list of bid prices and volumes. Optional, used to build the order book.
1888
+ ask (list[list[float | int]] | None): A list of ask prices and volumes. Optional, used to build the order book.
1889
+ level_2 (OrderBook | None): The level 2 order book created from the bid and ask data.
1890
+ order_book (OrderBook | None): Alias for `level_2`.
1891
+ last_price (float): The last traded price.
1892
+ bid_price (float | None): The bid price.
1893
+ ask_price (float | None): The ask price.
1894
+ bid_volume (float | None): The bid volume.
1895
+ ask_volume (float | None): The ask volume.
1896
+ total_traded_volume (float): The total traded volume.
1897
+ total_traded_notional (float): The total traded notional value.
1898
+ total_trade_count (float): The total number of trades.
1899
+ mid_price (float): The midpoint price calculated as the average of bid and ask prices.
1900
+ market_price (float): The last traded price.
1901
+ """
1902
+
1903
+ def __init__(
1904
+ self, *,
1905
+ ticker: str,
1906
+ timestamp: float,
1907
+ last_price: float,
1908
+ bid_price: float = None,
1909
+ bid_volume: float = None,
1910
+ ask_price: float = None,
1911
+ ask_volume: float = None,
1912
+ order_book: OrderBook = None,
1913
+ bid: list[list[float | int]] = None,
1914
+ ask: list[list[float | int]] = None,
1915
+ total_traded_volume: float = 0.,
1916
+ total_traded_notional: float = 0.,
1917
+ total_trade_count: int = 0,
1918
+ **kwargs
1919
+ ):
1920
+ """
1921
+ Initializes a new instance of `TickData`.
1922
+
1923
+ Args:
1924
+ ticker (str): The ticker symbol for the market data.
1925
+ timestamp (float): The timestamp of the tick data.
1926
+ last_price (float): The last traded price.
1927
+ bid_price (float, optional): The bid price. Defaults to None.
1928
+ bid_volume (float, optional): The bid volume. Defaults to None.
1929
+ ask_price (float, optional): The ask price. Defaults to None.
1930
+ ask_volume (float, optional): The ask volume. Defaults to None.
1931
+ order_book (OrderBook, optional): The order book containing bid and ask data. Defaults to None.
1932
+ bid (list[list[float | int]], optional): A list of bid prices and volumes. Defaults to None.
1933
+ ask (list[list[float | int]], optional): A list of ask prices and volumes. Defaults to None.
1934
+ total_traded_volume (float, optional): The total traded volume. Defaults to 0.0.
1935
+ total_traded_notional (float, optional): The total traded notional value. Defaults to 0.0.
1936
+ total_trade_count (int, optional): The total number of trades. Defaults to 0.
1937
+ **kwargs: Additional keyword arguments passed to the parent `MarketData` class.
1938
+ """
1939
+ super().__init__(ticker=ticker, timestamp=timestamp, **kwargs)
1940
+
1941
+ self.update(
1942
+ last_price=last_price,
1943
+ total_traded_volume=total_traded_volume,
1944
+ total_traded_notional=total_traded_notional,
1945
+ total_trade_count=total_trade_count,
1946
+ )
1947
+
1948
+ if order_book is not None:
1949
+ self['order_book'] = {'bid': order_book['bid'], 'ask': order_book['ask']}
1950
+ elif bid and ask:
1951
+ self['order_book'] = {
1952
+ 'bid': sorted(bid, key=lambda _: _[0], reverse=True),
1953
+ 'ask': sorted(ask, key=lambda _: _[0], reverse=False)
1954
+ }
1955
+
1956
+ if bid_price is None:
1957
+ bid_price, _, *_ = bid[0]
1958
+ if bid_volume is None:
1959
+ _, bid_volume, *_ = bid[0]
1960
+ if ask_price is None:
1961
+ ask_price, _, *_ = ask[0]
1962
+ if ask_volume is None:
1963
+ _, ask_volume, *_ = ask[0]
1964
+
1965
+ if bid_price is not None and math.isfinite(bid_price):
1966
+ self['bid_price'] = bid_price
1967
+
1968
+ if bid_volume is not None and math.isfinite(bid_volume):
1969
+ self['bid_volume'] = bid_volume
1970
+
1971
+ if ask_price is not None and math.isfinite(ask_price):
1972
+ self['ask_price'] = ask_price
1973
+
1974
+ if ask_volume is not None and math.isfinite(ask_volume):
1975
+ self['ask_volume'] = ask_volume
1976
+
1977
+ @property
1978
+ def level_2(self) -> OrderBook | None:
1979
+ """
1980
+ The level 2 order book created from the bid and ask data.
1981
+
1982
+ Returns:
1983
+ OrderBook | None: The `OrderBook` instance if available, otherwise `None`.
1984
+ """
1985
+ if 'order_book' in self:
1986
+ return OrderBook(ticker=self.ticker, timestamp=self.timestamp, **self['order_book'])
1987
+ else:
1988
+ return None
1989
+
1990
+ @property
1991
+ def order_book(self) -> OrderBook | None:
1992
+ """
1993
+ Alias for `level_2`.
1994
+
1995
+ Returns:
1996
+ OrderBook | None: The `OrderBook` instance if available, otherwise `None`.
1997
+ """
1998
+ return self.level_2
1999
+
2000
+ @property
2001
+ def last_price(self) -> float:
2002
+ """
2003
+ The last traded price.
2004
+
2005
+ Returns:
2006
+ float: The last traded price.
2007
+ """
2008
+ return self['last_price']
2009
+
2010
+ @property
2011
+ def bid_price(self) -> float | None:
2012
+ """
2013
+ The bid price.
2014
+
2015
+ Returns:
2016
+ float | None: The bid price if available, otherwise `None`.
2017
+ """
2018
+ return self.get('bid_price')
2019
+
2020
+ @property
2021
+ def ask_price(self) -> float | None:
2022
+ """
2023
+ The ask price.
2024
+
2025
+ Returns:
2026
+ float | None: The ask price if available, otherwise `None`.
2027
+ """
2028
+ return self.get('ask_price')
2029
+
2030
+ @property
2031
+ def bid_volume(self) -> float | None:
2032
+ """
2033
+ The bid volume.
2034
+
2035
+ Returns:
2036
+ float | None: The bid volume if available, otherwise `None`.
2037
+ """
2038
+ return self.get('bid_volume')
2039
+
2040
+ @property
2041
+ def ask_volume(self) -> float | None:
2042
+ """
2043
+ The ask volume.
2044
+
2045
+ Returns:
2046
+ float | None: The ask volume if available, otherwise `None`.
2047
+ """
2048
+ return self.get('ask_volume')
2049
+
2050
+ @property
2051
+ def total_traded_volume(self) -> float:
2052
+ """
2053
+ The total traded volume.
2054
+
2055
+ Returns:
2056
+ float: The total traded volume.
2057
+ """
2058
+ return self['total_traded_volume']
2059
+
2060
+ @property
2061
+ def total_traded_notional(self) -> float:
2062
+ """
2063
+ The total traded notional value.
2064
+
2065
+ Returns:
2066
+ float: The total traded notional value.
2067
+ """
2068
+ return self['total_traded_notional']
2069
+
2070
+ @property
2071
+ def total_trade_count(self) -> float:
2072
+ """
2073
+ The total number of trades.
2074
+
2075
+ Returns:
2076
+ float: The total number of trades.
2077
+ """
2078
+ return self['total_trade_count']
2079
+
2080
+ def __repr__(self) -> str:
2081
+ """
2082
+ Returns a string representation of the `TickData` instance.
2083
+
2084
+ The string representation includes the class name, market time, ticker symbol, and bid/ask prices.
2085
+
2086
+ Returns:
2087
+ str: A string representation of the `TickData` instance.
2088
+ """
2089
+ return f'<TickData>([{self.market_time:%Y-%m-%d %H:%M:%S}] {self.ticker}, bid={self.bid_price}, ask={self.ask_price})'
2090
+
2091
+ @classmethod
2092
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
2093
+ """
2094
+ Creates a `TickData` instance from a JSON message.
2095
+
2096
+ Args:
2097
+ json_message (str | bytes | bytearray | dict): The JSON message containing tick data.
2098
+
2099
+ Returns:
2100
+ TickData: A `TickData` instance.
2101
+
2102
+ Raises:
2103
+ TypeError: If the JSON message does not match the expected data type.
2104
+ """
2105
+ if isinstance(json_message, dict):
2106
+ json_dict = json_message
2107
+ else:
2108
+ json_dict = json.loads(json_message)
2109
+
2110
+ dtype = json_dict.pop('dtype', None)
2111
+ if dtype is not None and dtype != cls.__name__:
2112
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
2113
+
2114
+ self = cls(**json_dict)
2115
+ return self
2116
+
2117
+ def to_list(self) -> list[float | int | str | bool]:
2118
+ """
2119
+ Converts the `TickData` instance to a list of its attributes.
2120
+
2121
+ Note:
2122
+ This method does not retain order book information. To save all information, use `to_json` instead.
2123
+
2124
+ Returns:
2125
+ list[float | int | str | bool]: A list of attribute values.
2126
+ """
2127
+ return [self.__class__.__name__,
2128
+ self.ticker,
2129
+ self.timestamp,
2130
+ self.last_price,
2131
+ self.bid_price,
2132
+ self.bid_volume,
2133
+ self.ask_price,
2134
+ self.ask_volume,
2135
+ self.total_traded_volume,
2136
+ self.total_traded_notional,
2137
+ self.total_trade_count]
2138
+
2139
+ @classmethod
2140
+ def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
2141
+ """
2142
+ Creates a `TickData` instance from a list of attributes.
2143
+
2144
+ Args:
2145
+ data_list (list[float | int | str | bool]): A list of attributes in the order:
2146
+ - dtype (str)
2147
+ - ticker (str)
2148
+ - timestamp (float)
2149
+ - last_price (float)
2150
+ - bid_price (float | None)
2151
+ - bid_volume (float | None)
2152
+ - ask_price (float | None)
2153
+ - ask_volume (float | None)
2154
+ - total_traded_volume (float)
2155
+ - total_traded_notional (float)
2156
+ - total_trade_count (int)
2157
+
2158
+ Returns:
2159
+ TickData: A `TickData` instance.
2160
+
2161
+ Raises:
2162
+ TypeError: If the dtype in the list does not match the class name.
2163
+ """
2164
+ (dtype, ticker, timestamp, last_price,
2165
+ bid_price, bid_volume, ask_price, ask_volume,
2166
+ total_traded_volume, total_traded_notional, total_trade_count) = data_list
2167
+
2168
+ if dtype != cls.__name__:
2169
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
2170
+
2171
+ kwargs = {}
2172
+
2173
+ if bid_price is not None:
2174
+ kwargs['bid_price'] = bid_price
2175
+
2176
+ if ask_price is not None:
2177
+ kwargs['ask_price'] = ask_price
2178
+
2179
+ if bid_volume is not None:
2180
+ kwargs['bid_volume'] = bid_volume
2181
+
2182
+ if ask_volume is not None:
2183
+ kwargs['ask_volume'] = ask_volume
2184
+
2185
+ return cls(
2186
+ ticker=ticker,
2187
+ timestamp=timestamp,
2188
+ last_price=last_price,
2189
+ total_traded_volume=total_traded_volume,
2190
+ total_traded_notional=total_traded_notional,
2191
+ total_trade_count=total_trade_count,
2192
+ **kwargs
2193
+ )
2194
+
2195
+ @property
2196
+ def mid_price(self) -> float:
2197
+ """
2198
+ The midpoint price calculated as the average of bid and ask prices.
2199
+
2200
+ Returns:
2201
+ float: The midpoint price.
2202
+ """
2203
+ return (self.bid_price + self.ask_price) / 2
2204
+
2205
+ @property
2206
+ def market_price(self) -> float:
2207
+ """
2208
+ The last traded price.
2209
+
2210
+ Returns:
2211
+ float: The last traded price.
2212
+ """
2213
+ return self.last_price
2214
+
2215
+
2216
+ class TransactionData(MarketData):
2217
+ """
2218
+ Represents transaction data for a specific market.
2219
+
2220
+ This class extends the `MarketData` class to handle transaction-level data, including price, volume, side, and identifiers.
2221
+
2222
+ Attributes:
2223
+ ...
2224
+
2225
+ Methods:
2226
+ __repr__() -> str:
2227
+ Returns a string representation of the `TransactionData` instance.
2228
+
2229
+ from_json(json_message: str | bytes | bytearray | dict) -> TransactionData:
2230
+ Creates a `TransactionData` instance from a JSON message.
2231
+
2232
+ to_list() -> list[float | int | str | bool]:
2233
+ Converts the `TransactionData` instance to a list of its attributes.
2234
+
2235
+ from_list(data_list: list[float | int | str | bool]) -> TransactionData:
2236
+ Creates a `TransactionData` instance from a list of attributes.
2237
+
2238
+ merge(trade_data_list: list[TransactionData]) -> TransactionData | None:
2239
+ Merges multiple `TransactionData` instances into a single aggregated `TransactionData` instance.
2240
+
2241
+ Properties:
2242
+ ticker (str): The ticker symbol for the transaction.
2243
+ timestamp (float): The timestamp of the transaction.
2244
+
2245
+ price (float): The price at which the transaction occurred.
2246
+ volume (float): The volume of the transaction.
2247
+ side (TransactionSide): The side of the transaction (buy or sell).
2248
+ multiplier (float): The multiplier for the transaction.
2249
+ transaction_id (int | str | None): The identifier for the transaction.
2250
+ buy_id (int | str | None): The identifier for the buying transaction.
2251
+ sell_id (int | str | None): The identifier for the selling transaction.
2252
+ notional (float): The notional value of the transaction.
2253
+ market_price (float): Alias for `price`.
2254
+ flow (float): The flow of the transaction, calculated as side.sign * volume.
2255
+ """
2256
+
2257
+ def __init__(
2258
+ self, *,
2259
+ ticker: str,
2260
+ price: float,
2261
+ volume: float,
2262
+ timestamp: float,
2263
+ side: int | float | str | TransactionSide = 0,
2264
+ multiplier: float = None,
2265
+ notional: float = None,
2266
+ transaction_id: str | int = None,
2267
+ buy_id: str | int = None,
2268
+ sell_id: str | int = None,
2269
+ **kwargs
2270
+ ):
2271
+ """
2272
+ Initializes a new instance of `TransactionData`.
2273
+
2274
+ Args:
2275
+ ticker (str): The ticker symbol for the transaction.
2276
+ price (float): The price at which the transaction occurred.
2277
+ volume (float): The volume of the transaction.
2278
+ timestamp (float): The timestamp of the transaction.
2279
+ side (int | float | str | TransactionSide, optional): The side of the transaction (buy or sell). Defaults to 0.
2280
+ multiplier (float, optional): The multiplier for the transaction. Defaults to None.
2281
+ notional (float, optional): The notional value of the transaction. Defaults to None.
2282
+ transaction_id (str | int, optional): The identifier for the transaction. Defaults to None.
2283
+ buy_id (str | int, optional): The identifier for the buying transaction. Defaults to None.
2284
+ sell_id (str | int, optional): The identifier for the selling transaction. Defaults to None.
2285
+ **kwargs: Additional keyword arguments passed to the parent `MarketData` class.
2286
+ """
2287
+ super().__init__(ticker=ticker, timestamp=timestamp, **kwargs)
2288
+
2289
+ self['price'] = price
2290
+ self['volume'] = volume
2291
+ self['side'] = int(side) if isinstance(side, (int, float)) else TransactionSide(side).value
2292
+
2293
+ if multiplier is not None and math.isfinite(multiplier):
2294
+ self['multiplier'] = multiplier
2295
+
2296
+ if notional is not None and math.isfinite(notional):
2297
+ self['notional'] = notional
2298
+
2299
+ if transaction_id is not None:
2300
+ self['transaction_id'] = transaction_id
2301
+
2302
+ if buy_id is not None:
2303
+ self['buy_id'] = buy_id
2304
+
2305
+ if sell_id is not None:
2306
+ self['sell_id'] = sell_id
2307
+
2308
+ def __repr__(self) -> str:
2309
+ """
2310
+ Returns a string representation of the `TransactionData` instance.
2311
+
2312
+ The string representation includes the class name, market time, side, ticker symbol, price, and volume.
2313
+
2314
+ Returns:
2315
+ str: A string representation of the `TransactionData` instance.
2316
+ """
2317
+ return f'<TransactionData>([{self.market_time:%Y-%m-%d %H:%M:%S}] {self.side.side_name} {self.ticker}, price={self.price}, volume={self.volume})'
2318
+
2319
+ @classmethod
2320
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
2321
+ """
2322
+ Creates a `TransactionData` instance from a JSON message.
2323
+
2324
+ Args:
2325
+ json_message (str | bytes | bytearray | dict): The JSON message containing transaction data.
2326
+
2327
+ Returns:
2328
+ TransactionData: A `TransactionData` instance.
2329
+
2330
+ Raises:
2331
+ TypeError: If the JSON message does not match the expected data type.
2332
+ """
2333
+ if isinstance(json_message, dict):
2334
+ json_dict = json_message
2335
+ else:
2336
+ json_dict = json.loads(json_message)
2337
+
2338
+ dtype = json_dict.pop('dtype', None)
2339
+ if dtype is not None and dtype != cls.__name__:
2340
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
2341
+
2342
+ self = cls(**json_dict)
2343
+ return self
2344
+
2345
+ def to_list(self) -> list[float | int | str | bool]:
2346
+ """
2347
+ Converts the `TransactionData` instance to a list of its attributes.
2348
+
2349
+ Returns:
2350
+ list[float | int | str | bool]: A list of attribute values.
2351
+ """
2352
+ return [self.__class__.__name__,
2353
+ self.ticker,
2354
+ self.timestamp,
2355
+ self.price,
2356
+ self.volume,
2357
+ self['side'],
2358
+ self.get('multiplier'),
2359
+ self.get('notional'),
2360
+ self.get('transaction_id'),
2361
+ self.get('buy_id'),
2362
+ self.get('sell_id')]
2363
+
2364
+ @classmethod
2365
+ def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
2366
+ """
2367
+ Creates a `TransactionData` instance from a list of attributes.
2368
+
2369
+ Args:
2370
+ data_list (list[float | int | str | bool]): A list of attributes in the order:
2371
+ - dtype (str)
2372
+ - ticker (str)
2373
+ - timestamp (float)
2374
+ - price (float)
2375
+ - volume (float)
2376
+ - side (int | float | str)
2377
+ - multiplier (float | None)
2378
+ - notional (float | None)
2379
+ - transaction_id (str | int | None)
2380
+ - buy_id (str | int | None)
2381
+ - sell_id (str | int | None)
2382
+
2383
+ Returns:
2384
+ TransactionData: A `TransactionData` instance.
2385
+
2386
+ Raises:
2387
+ TypeError: If the dtype in the list does not match the class name.
2388
+ """
2389
+ (dtype, ticker, timestamp, price, volume, side,
2390
+ multiplier, notional, transaction_id, buy_id, sell_id) = data_list
2391
+
2392
+ if dtype != cls.__name__:
2393
+ raise TypeError(f'dtype mismatch, expect {cls.__name__}, got {dtype}.')
2394
+
2395
+ kwargs = {}
2396
+
2397
+ if multiplier is not None:
2398
+ kwargs['multiplier'] = multiplier
2399
+
2400
+ if notional is not None:
2401
+ kwargs['notional'] = notional
2402
+
2403
+ if transaction_id is not None:
2404
+ kwargs['transaction_id'] = transaction_id
2405
+
2406
+ if buy_id is not None:
2407
+ kwargs['buy_id'] = buy_id
2408
+
2409
+ if sell_id is not None:
2410
+ kwargs['sell_id'] = sell_id
2411
+
2412
+ return cls(
2413
+ ticker=ticker,
2414
+ timestamp=timestamp,
2415
+ price=price,
2416
+ volume=volume,
2417
+ side=side,
2418
+ **kwargs
2419
+ )
2420
+
2421
+ @classmethod
2422
+ def merge(cls, trade_data_list: list[Self]) -> Self | None:
2423
+ """
2424
+ Merges multiple `TransactionData` instances into a single aggregated `TransactionData` instance.
2425
+
2426
+ Args:
2427
+ trade_data_list (list[TransactionData]): A list of `TransactionData` instances to merge.
2428
+
2429
+ Returns:
2430
+ TransactionData | None: A merged `TransactionData` instance if the list is not empty, otherwise `None`.
2431
+
2432
+ Raises:
2433
+ AssertionError: If the list contains transaction data for multiple tickers.
2434
+ """
2435
+ if not trade_data_list:
2436
+ return None
2437
+
2438
+ ticker = trade_data_list[0].ticker
2439
+ assert all([trade.ticker == ticker for trade in trade_data_list]), 'input contains trade data of multiple ticker'
2440
+ timestamp = max([trade.timestamp for trade in trade_data_list])
2441
+ sum_volume = sum([trade.volume * trade.side.sign for trade in trade_data_list])
2442
+ sum_notional = sum([trade.notional * trade.side.sign for trade in trade_data_list])
2443
+ trade_side_sign = np.sign(sum_volume) if sum_volume != 0 else 1
2444
+
2445
+ if sum_notional == 0:
2446
+ trade_price = 0
2447
+ elif sum_volume == 0:
2448
+ trade_price = math.nan
2449
+ else:
2450
+ trade_price = sum_notional / sum_volume if sum_volume else math.inf * np.sign(sum_notional)
2451
+
2452
+ trade_side = TransactionSide(trade_side_sign)
2453
+ trade_volume = abs(sum_volume)
2454
+ trade_notional = abs(sum_notional)
2455
+
2456
+ merged_trade_data = cls(
2457
+ ticker=ticker,
2458
+ timestamp=timestamp,
2459
+ side=trade_side,
2460
+ price=trade_price,
2461
+ volume=trade_volume,
2462
+ notional=trade_notional
2463
+ )
2464
+
2465
+ return merged_trade_data
2466
+
2467
+ @property
2468
+ def price(self) -> float:
2469
+ """
2470
+ The price at which the transaction occurred.
2471
+
2472
+ Returns:
2473
+ float: The transaction price.
2474
+ """
2475
+ return self['price']
2476
+
2477
+ @property
2478
+ def volume(self) -> float:
2479
+ """
2480
+ The volume of the transaction.
2481
+
2482
+ Returns:
2483
+ float: The transaction volume.
2484
+ """
2485
+ return self['volume']
2486
+
2487
+ @property
2488
+ def side(self) -> TransactionSide:
2489
+ """
2490
+ The side of the transaction (buy or sell).
2491
+
2492
+ Returns:
2493
+ TransactionSide: The side of the transaction.
2494
+ """
2495
+ return TransactionSide(self['side'])
2496
+
2497
+ @property
2498
+ def multiplier(self) -> float:
2499
+ """
2500
+ The multiplier for the transaction. Defaults to 1 if not specified.
2501
+
2502
+ Returns:
2503
+ float: The transaction multiplier.
2504
+ """
2505
+ return self.get('multiplier', 1.)
2506
+
2507
+ @property
2508
+ def transaction_id(self) -> int | str | None:
2509
+ """
2510
+ The identifier for the transaction.
2511
+
2512
+ Returns:
2513
+ int | str | None: The transaction identifier.
2514
+ """
2515
+ return self.get('transaction_id', None)
2516
+
2517
+ @property
2518
+ def buy_id(self) -> int | str | None:
2519
+ """
2520
+ The identifier for the buying transaction.
2521
+
2522
+ Returns:
2523
+ int | str | None: The buying transaction identifier.
2524
+ """
2525
+ return self.get('buy_id', None)
2526
+
2527
+ @property
2528
+ def sell_id(self) -> int | str | None:
2529
+ """
2530
+ The identifier for the selling transaction.
2531
+
2532
+ Returns:
2533
+ int | str | None: The selling transaction identifier.
2534
+ """
2535
+ return self.get('sell_id', None)
2536
+
2537
+ @property
2538
+ def notional(self) -> float:
2539
+ """
2540
+ The notional value of the transaction. Calculated as price * volume * multiplier.
2541
+
2542
+ Returns:
2543
+ float: The transaction notional.
2544
+ """
2545
+ return self.get('notional', self.price * self.volume * self.multiplier)
2546
+
2547
+ @property
2548
+ def market_price(self) -> float:
2549
+ """
2550
+ Alias for the transaction price.
2551
+
2552
+ Returns:
2553
+ float: The transaction price.
2554
+ """
2555
+ return self.price
2556
+
2557
+ @property
2558
+ def flow(self) -> float:
2559
+ """
2560
+ The flow of the transaction, calculated as side.sign * volume.
2561
+
2562
+ Returns:
2563
+ float: The transaction flow.
2564
+ """
2565
+ return self.side.sign * self.volume
2566
+
2567
+
2568
+ class TradeData(TransactionData):
2569
+ """
2570
+ Alias for `TransactionData` with alternate property names for trade price and volume.
2571
+
2572
+ This class allows initialization with 'trade_price' instead of 'price' and 'trade_volume' instead of 'volume'.
2573
+ It provides additional properties for these alternate names.
2574
+
2575
+ Properties:
2576
+ trade_price (float): Alias for `price`.
2577
+ trade_volume (float): Alias for `volume`.
2578
+
2579
+ Methods:
2580
+ from_json(json_message: str | bytes | bytearray | dict) -> TradeData:
2581
+ Creates a `TradeData` instance from a JSON message.
2582
+
2583
+ from_list(data_list: list[float | int | str | bool]) -> TradeData:
2584
+ Creates a `TradeData` instance from a list of attributes.
2585
+ """
2586
+
2587
+ def __init__(self, **kwargs):
2588
+ """
2589
+ Initializes a new instance of `TradeData`.
2590
+
2591
+ Args:
2592
+ **kwargs: Keyword arguments passed to the parent `TransactionData` class.
2593
+ If 'trade_price' or 'trade_volume' are provided, they are converted to 'price' and 'volume'.
2594
+ """
2595
+ if 'trade_price' in kwargs:
2596
+ kwargs['price'] = kwargs.pop('trade_price')
2597
+
2598
+ if 'trade_volume' in kwargs:
2599
+ kwargs['volume'] = kwargs.pop('trade_volume')
2600
+
2601
+ super().__init__(**kwargs)
2602
+
2603
+ @property
2604
+ def trade_price(self) -> float:
2605
+ """
2606
+ Alias for the transaction price.
2607
+
2608
+ Returns:
2609
+ float: The transaction price.
2610
+ """
2611
+ return self['price']
2612
+
2613
+ @property
2614
+ def trade_volume(self) -> float:
2615
+ """
2616
+ Alias for the transaction volume.
2617
+
2618
+ Returns:
2619
+ float: The transaction volume.
2620
+ """
2621
+ return self['volume']
2622
+
2623
+ @classmethod
2624
+ def from_json(cls, json_message: str | bytes | bytearray | dict) -> Self:
2625
+ """
2626
+ Creates a `TradeData` instance from a JSON message.
2627
+
2628
+ Args:
2629
+ json_message (str | bytes | bytearray | dict): The JSON message containing trade data.
2630
+
2631
+ Returns:
2632
+ TradeData: A `TradeData` instance.
2633
+
2634
+ Raises:
2635
+ TypeError: If the JSON message does not match the expected data type.
2636
+ """
2637
+ return super(TradeData, cls).from_json(json_message=json_message)
2638
+
2639
+ @classmethod
2640
+ def from_list(cls, data_list: list[float | int | str | bool]) -> Self:
2641
+ """
2642
+ Creates a `TradeData` instance from a list of attributes.
2643
+
2644
+ Args:
2645
+ data_list (list[float | int | str | bool]): A list of attributes.
2646
+
2647
+ Returns:
2648
+ TradeData: A `TradeData` instance.
2649
+ """
2650
+ return super(TradeData, cls).from_list(data_list=data_list)
2651
+
2652
+
2653
+ class MarketDataMemoryBuffer(object, metaclass=abc.ABCMeta):
2654
+ """
2655
+ Abstract base class for a market data memory buffer.
2656
+
2657
+ Attributes:
2658
+ dtype (RawArray): Data type of the market data.
2659
+ ticker (RawArray): Ticker symbol.
2660
+ timestamp (RawValue): Timestamp of the market data.
2661
+
2662
+ Methods:
2663
+ update(market_data: MarketData):
2664
+ Updates the buffer with new market data.
2665
+
2666
+ to_market_data() -> MarketData:
2667
+ Abstract method to convert the buffer data to a `MarketData` instance.
2668
+ """
2669
+
2670
+ def __init__(self):
2671
+ """
2672
+ Initializes the memory buffer with default values.
2673
+ """
2674
+ LOGGER.warning(DeprecationWarning(f'{self.__class__.__name__} deprecated. Please use the module from algo_engine.base.market_buffer instead.'))
2675
+ self.dtype = RawArray(c_wchar, 16)
2676
+ self.ticker = RawArray(c_wchar, 32) # max length of ticker is 32
2677
+ self.timestamp = RawValue(c_double)
2678
+
2679
+ def update(self, market_data: MarketData):
2680
+ """
2681
+ Updates the buffer with new market data.
2682
+
2683
+ Args:
2684
+ market_data (MarketData): An instance of `MarketData` containing new data.
2685
+ """
2686
+ self.dtype.value = market_data.__class__.__name__
2687
+ self.ticker.value = market_data['ticker']
2688
+ self.timestamp.value = market_data['timestamp']
2689
+
2690
+ @abc.abstractmethod
2691
+ def to_market_data(self) -> MarketData:
2692
+ """
2693
+ Abstract method to convert the buffer data to a `MarketData` instance.
2694
+
2695
+ Returns:
2696
+ MarketData: A `MarketData` instance populated with buffer data.
2697
+ """
2698
+ ...
2699
+
2700
+
2701
+ class OrderBookRawValue(MarketDataMemoryBuffer):
2702
+ """
2703
+ Memory buffer for order book data.
2704
+
2705
+ Attributes:
2706
+ max_level (int): Maximum levels for bid and ask data.
2707
+ bid (list[tuple[RawValue, RawValue, RawValue]]): List of bid data buffers.
2708
+ ask (list[tuple[RawValue, RawValue, RawValue]]): List of ask data buffers.
2709
+
2710
+ Methods:
2711
+ update(market_data: OrderBook):
2712
+ Updates the buffer with new order book data.
2713
+
2714
+ to_market_data() -> OrderBook:
2715
+ Converts the buffer data to an `OrderBook` instance.
2716
+ """
2717
+
2718
+ def __init__(self, max_level: int = 20):
2719
+ """
2720
+ Initializes the order book buffer with the specified maximum levels.
2721
+
2722
+ Args:
2723
+ max_level (int): Maximum number of levels for bid and ask data. Defaults to 20.
2724
+ """
2725
+ LOGGER.warning(DeprecationWarning(f'{self.__class__.__name__} deprecated. Please use the module from algo_engine.base.market_buffer instead.'))
2726
+ super().__init__()
2727
+
2728
+ self.max_level = max_level
2729
+
2730
+ self.bid = [(RawValue(c_double), RawValue(c_double), RawValue(c_ulong)) for _ in range(self.max_level)]
2731
+ self.ask = [(RawValue(c_double), RawValue(c_double), RawValue(c_ulong)) for _ in range(self.max_level)]
2732
+
2733
+ def update(self, market_data: OrderBook):
2734
+ """
2735
+ Updates the buffer with new order book data.
2736
+
2737
+ Args:
2738
+ market_data (OrderBook): An instance of `OrderBook` containing new data.
2739
+ """
2740
+ super().update(market_data=market_data)
2741
+
2742
+ bid = market_data['bid']
2743
+ ask = market_data['ask']
2744
+
2745
+ for i in range(self.max_level):
2746
+ bid_memory_array = self.bid[i]
2747
+ ask_memory_array = self.ask[i]
2748
+
2749
+ if i < len(bid):
2750
+ for bid_entry_value, bid_memory in zip(bid[i], bid_memory_array):
2751
+ bid_memory.value = bid_entry_value
2752
+ else:
2753
+ for bid_memory in bid_memory_array:
2754
+ bid_memory.value = 0
2755
+
2756
+ if i < len(ask):
2757
+ for ask_entry_value, ask_memory in zip(ask[i], ask_memory_array):
2758
+ ask_memory.value = ask_entry_value
2759
+ else:
2760
+ for ask_memory in ask_memory_array:
2761
+ ask_memory.value = 0
2762
+
2763
+ def to_market_data(self) -> OrderBook:
2764
+ """
2765
+ Converts the buffer data to an `OrderBook` instance.
2766
+
2767
+ Returns:
2768
+ OrderBook: An `OrderBook` instance populated with buffer data.
2769
+ """
2770
+ bid, ask = [], []
2771
+
2772
+ for i in range(self.max_level):
2773
+ bid_memory_array = self.bid[i]
2774
+ ask_memory_array = self.ask[i]
2775
+
2776
+ bid_price, bid_volume, bid_n_orders = [_.value for _ in bid_memory_array]
2777
+ ask_price, ask_volume, ask_n_orders = [_.value for _ in ask_memory_array]
2778
+
2779
+ if bid_volume:
2780
+ bid.append([bid_price, bid_volume, bid_n_orders])
2781
+
2782
+ if ask_volume:
2783
+ ask.append([ask_price, ask_volume, ask_n_orders])
2784
+
2785
+ if bid_volume == ask_volume == 0:
2786
+ break
2787
+
2788
+ order_book = OrderBook(
2789
+ ticker=self.ticker.value,
2790
+ timestamp=self.timestamp.value,
2791
+ bid=bid,
2792
+ ask=ask,
2793
+ )
2794
+
2795
+ return order_book
2796
+
2797
+
2798
+ class BarDataRawValue(MarketDataMemoryBuffer):
2799
+ """
2800
+ Memory buffer for bar data.
2801
+
2802
+ Attributes:
2803
+ start_timestamp (RawValue): Start timestamp of the bar.
2804
+ bar_span (RawValue): Span of the bar.
2805
+ high_price (RawValue): Highest price during the bar.
2806
+ low_price (RawValue): Lowest price during the bar.
2807
+ open_price (RawValue): Opening price of the bar.
2808
+ close_price (RawValue): Closing price of the bar.
2809
+ volume (RawValue): Total volume during the bar.
2810
+ notional (RawValue): Total notional value during the bar.
2811
+ trade_count (RawValue): Number of trades during the bar.
2812
+
2813
+ Methods:
2814
+ update(market_data: BarData):
2815
+ Updates the buffer with new bar data.
2816
+
2817
+ to_market_data() -> BarData:
2818
+ Converts the buffer data to a `BarData` instance.
2819
+ """
2820
+
2821
+ def __init__(self):
2822
+ """
2823
+ Initializes the bar data buffer with default values.
2824
+ """
2825
+ LOGGER.warning(DeprecationWarning(f'{self.__class__.__name__} deprecated. Please use the module from algo_engine.base.market_buffer instead.'))
2826
+ super().__init__()
2827
+
2828
+ self.start_timestamp = RawValue(c_double)
2829
+ self.bar_span = RawValue(c_double)
2830
+ self.high_price = RawValue(c_double)
2831
+ self.low_price = RawValue(c_double)
2832
+ self.open_price = RawValue(c_double)
2833
+ self.close_price = RawValue(c_double)
2834
+ self.volume = RawValue(c_double)
2835
+ self.notional = RawValue(c_double)
2836
+ self.trade_count = RawValue(c_longlong)
2837
+
2838
+ def update(self, market_data: BarData):
2839
+ """
2840
+ Updates the buffer with new bar data.
2841
+
2842
+ Args:
2843
+ market_data (BarData): An instance of `BarData` containing new data.
2844
+ """
2845
+ super().update(market_data=market_data)
2846
+
2847
+ self.start_timestamp.value = market_data['start_timestamp'] if 'start_timestamp' in market_data else math.nan
2848
+ self.bar_span.value = market_data['bar_span'] if 'bar_span' in market_data else math.nan
2849
+
2850
+ self.high_price.value = market_data['high_price']
2851
+ self.low_price.value = market_data['low_price']
2852
+ self.open_price.value = market_data['open_price']
2853
+ self.close_price.value = market_data['close_price']
2854
+ self.volume.value = market_data['volume']
2855
+ self.notional.value = market_data['notional']
2856
+ self.trade_count.value = market_data['trade_count']
2857
+
2858
+ def to_market_data(self) -> BarData:
2859
+ """
2860
+ Converts the buffer data to a `BarData` instance.
2861
+
2862
+ Returns:
2863
+ BarData: A `BarData` instance populated with buffer data.
2864
+ """
2865
+ bar_data = BarData(
2866
+ ticker=self.ticker.value,
2867
+ timestamp=self.timestamp.value,
2868
+ start_timestamp=self.start_timestamp.value if math.isfinite(self.start_timestamp.value) else None,
2869
+ bar_span=self.bar_span.value if math.isfinite(self.bar_span.value) else None,
2870
+ high_price=self.high_price.value,
2871
+ low_price=self.low_price.value,
2872
+ open_price=self.open_price.value,
2873
+ close_price=self.close_price.value,
2874
+ volume=self.volume.value,
2875
+ notional=self.notional.value,
2876
+ trade_count=self.trade_count.value
2877
+ )
2878
+
2879
+ return bar_data
2880
+
2881
+
2882
+ class TickDataRawValue(MarketDataMemoryBuffer):
2883
+ """
2884
+ Memory buffer for tick data.
2885
+
2886
+ Attributes:
2887
+ last_price (RawValue): Last traded price.
2888
+ bid_price (RawValue): Current bid price.
2889
+ bid_volume (RawValue): Volume at the bid price.
2890
+ ask_price (RawValue): Current ask price.
2891
+ ask_volume (RawValue): Volume at the ask price.
2892
+ total_traded_volume (RawValue): Total traded volume.
2893
+ total_traded_notional (RawValue): Total traded notional value.
2894
+ total_trade_count (RawValue): Total number of trades.
2895
+
2896
+ Methods:
2897
+ update(market_data: TickData):
2898
+ Updates the buffer with new tick data.
2899
+
2900
+ to_market_data() -> TickData:
2901
+ Converts the buffer data to a `TickData` instance.
2902
+ """
2903
+
2904
+ def __init__(self):
2905
+ """
2906
+ Initializes the tick data buffer with default values.
2907
+ """
2908
+ LOGGER.warning(DeprecationWarning(f'{self.__class__.__name__} deprecated. Please use the module from algo_engine.base.market_buffer instead.'))
2909
+ super().__init__()
2910
+
2911
+ self.last_price = RawValue(c_double)
2912
+ self.bid_price = RawValue(c_double)
2913
+ self.bid_volume = RawValue(c_double)
2914
+ self.ask_price = RawValue(c_double)
2915
+ self.ask_volume = RawValue(c_double)
2916
+ self.total_traded_volume = RawValue(c_double)
2917
+ self.total_traded_notional = RawValue(c_double)
2918
+ self.total_trade_count = RawValue(c_longlong)
2919
+
2920
+ def update(self, market_data: TickData):
2921
+ """
2922
+ Updates the buffer with new tick data.
2923
+
2924
+ Args:
2925
+ market_data (TickData): An instance of `TickData` containing new data.
2926
+
2927
+ Notes:
2928
+ The order book is not stored in shared memory. Use `OrderBookShared` to store the level 2 data.
2929
+ """
2930
+ super().update(market_data=market_data)
2931
+
2932
+ self.last_price.value = market_data.last_price
2933
+
2934
+ self.bid_price.value = market_data['bid_price'] if 'bid_price' in market_data else math.nan
2935
+ self.bid_volume.value = market_data['bid_volume'] if 'bid_volume' in market_data else math.nan
2936
+ self.ask_price.value = market_data['ask_price'] if 'ask_price' in market_data else math.nan
2937
+ self.ask_volume.value = market_data['ask_volume'] if 'ask_volume' in market_data else math.nan
2938
+
2939
+ self.total_traded_volume.value = market_data['total_traded_volume']
2940
+ self.total_traded_notional.value = market_data['total_traded_notional']
2941
+ self.total_trade_count.value = market_data['total_trade_count']
2942
+
2943
+ def to_market_data(self) -> TickData:
2944
+ """
2945
+ Converts the buffer data to a `TickData` instance.
2946
+
2947
+ Returns:
2948
+ TickData: A `TickData` instance populated with buffer data.
2949
+ """
2950
+ tick_data = TickData(
2951
+ ticker=self.ticker.value,
2952
+ timestamp=self.timestamp.value,
2953
+ last_price=self.last_price.value,
2954
+ bid_price=self.bid_price.value,
2955
+ bid_volume=self.bid_volume.value,
2956
+ ask_price=self.ask_price.value,
2957
+ ask_volume=self.ask_volume.value,
2958
+ order_book=None,
2959
+ total_traded_volume=self.total_traded_volume.value,
2960
+ total_traded_notional=self.total_traded_notional.value,
2961
+ total_trade_count=self.total_trade_count.value,
2962
+ )
2963
+
2964
+ return tick_data
2965
+
2966
+
2967
+ class TransactionDataRawValue(MarketDataMemoryBuffer):
2968
+ """
2969
+ Memory buffer for transaction data.
2970
+
2971
+ Attributes:
2972
+ price (RawValue): Transaction price.
2973
+ volume (RawValue): Transaction volume.
2974
+ side (RawValue): Side of the transaction (buy/sell).
2975
+ multiplier (RawValue): Multiplier for the transaction.
2976
+ notional (RawValue): Notional value of the transaction.
2977
+ id_map (dict[str, tuple[RawValue, RawArray]]): Map for various IDs related to the transaction.
2978
+
2979
+ Methods:
2980
+ update(market_data: TradeData | TransactionData):
2981
+ Updates the buffer with new transaction data.
2982
+
2983
+ to_market_data() -> TradeData | TransactionData:
2984
+ Converts the buffer data to a `TradeData` or `TransactionData` instance.
2985
+ """
2986
+
2987
+ def __init__(self):
2988
+ """
2989
+ Initializes the transaction data buffer with default values.
2990
+ """
2991
+ LOGGER.warning(DeprecationWarning(f'{self.__class__.__name__} deprecated. Please use the module from algo_engine.base.market_buffer instead.'))
2992
+ super().__init__()
2993
+
2994
+ self.dtype = RawArray(c_wchar, 16)
2995
+ self.price = RawValue(c_double)
2996
+ self.volume = RawValue(c_double)
2997
+ self.side = RawValue(c_int)
2998
+
2999
+ self.multiplier = RawValue(c_double)
3000
+ self.notional = RawValue(c_double)
3001
+ self.id_map = dict(
3002
+ transaction_id=(RawValue(c_longlong), RawArray(c_wchar, 64)), # id can be an int, str or None
3003
+ buy_id=(RawValue(c_longlong), RawArray(c_wchar, 64)),
3004
+ sell_id=(RawValue(c_longlong), RawArray(c_wchar, 64)),
3005
+ )
3006
+
3007
+ def update(self, market_data: TradeData | TransactionData):
3008
+ """
3009
+ Updates the buffer with new transaction data.
3010
+
3011
+ Args:
3012
+ market_data (TradeData | TransactionData): An instance of `TradeData` or `TransactionData` containing new data.
3013
+ """
3014
+ super().update(market_data=market_data)
3015
+
3016
+ self.price.value = market_data['price']
3017
+ self.volume.value = market_data['volume']
3018
+ self.side.value = market_data['side']
3019
+
3020
+ if 'multiplier' in market_data:
3021
+ self.multiplier.value = market_data['multiplier']
3022
+ else:
3023
+ self.multiplier.value = math.nan
3024
+
3025
+ if 'notional' in market_data:
3026
+ self.notional.value = market_data['notional']
3027
+ else:
3028
+ self.notional.value = math.nan
3029
+
3030
+ for id_name in ['transaction_id', 'buy_id', 'sell_id']:
3031
+ if id_name in market_data:
3032
+ id_value = market_data[id_name]
3033
+ if isinstance(id_value, int):
3034
+ self.id_map[id_name][0].value = id_value
3035
+ self.id_map[id_name][1].value = ''
3036
+ elif isinstance(id_value, str):
3037
+ self.id_map[id_name][0].value = -1
3038
+ self.id_map[id_name][1].value = id_value
3039
+ else:
3040
+ raise TypeError(f'Invalid {id_name} type: {type(id_name)}, expect int or str.')
3041
+ else:
3042
+ self.id_map[id_name][0].value = -1
3043
+ self.id_map[id_name][1].value = ''
3044
+
3045
+ def to_market_data(self) -> TradeData | TransactionData:
3046
+ """
3047
+ Converts the buffer data to a `TradeData` or `TransactionData` instance.
3048
+
3049
+ Returns:
3050
+ TradeData | TransactionData: An instance of `TradeData` or `TransactionData` populated with buffer data.
3051
+ """
3052
+ if math.isnan(multiplier := self.multiplier.value):
3053
+ multiplier = None
3054
+
3055
+ if math.isnan(notional := self.notional.value):
3056
+ notional = None
3057
+
3058
+ id_map = {}
3059
+ for id_name in ['transaction_id', 'buy_id', 'sell_id']:
3060
+ id_int = self.id_map[id_name][0].value
3061
+ id_str = self.id_map[id_name][1].value
3062
+
3063
+ if id_int == -1 and not id_str:
3064
+ id_value = None
3065
+ elif not id_str:
3066
+ id_value = id_int
3067
+ elif id_int == -1:
3068
+ id_value = id_str
3069
+ else:
3070
+ raise ValueError(f'id_map can not contain both info of id_int={id_int}, id_str={id_str}.')
3071
+
3072
+ id_map[id_name] = id_value
3073
+
3074
+ if (dtype := self.dtype.value) == 'TransactionData':
3075
+ constructor = TransactionData
3076
+ elif dtype == 'TradeData':
3077
+ constructor = TradeData
3078
+ else:
3079
+ raise NotImplementedError(f'Constructor for market data {dtype} not implemented.')
3080
+
3081
+ td = constructor(
3082
+ ticker=self.ticker.value,
3083
+ price=self.price.value,
3084
+ volume=self.volume.value,
3085
+ timestamp=self.timestamp.value,
3086
+ side=self.side.value,
3087
+ multiplier=multiplier,
3088
+ notional=notional,
3089
+ **id_map
3090
+ )
3091
+
3092
+ return td