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,571 @@
1
+ import abc
2
+ import ctypes
3
+ import math
4
+ from multiprocessing import RawArray, RawValue, Condition
5
+ from typing import overload
6
+
7
+ import numpy as np
8
+
9
+ from .market_utils import MarketData, OrderBook, BarData, TickData, TransactionData, TradeData
10
+
11
+
12
+ class MarketDataPointer(object, metaclass=abc.ABCMeta):
13
+ def __init__(self, **kwargs):
14
+ self._ptr_dtype = kwargs.get('dtype')
15
+ self._ptr_ticker = kwargs.get('ticker')
16
+ self._ptr_timestamp = kwargs.get('timestamp')
17
+
18
+ def update(self, market_data: MarketData, encoding='utf-8'):
19
+ self._ptr_dtype.contents.value = market_data.__class__.__name__.encode(encoding).ljust(ctypes.sizeof(self._ptr_dtype.contents), b'\x00')
20
+ self._ptr_ticker.contents.value = market_data['ticker'].encode(encoding).ljust(ctypes.sizeof(self._ptr_ticker.contents), b'\x00')
21
+ self._ptr_timestamp.contents.value = float(market_data['timestamp'])
22
+
23
+ @abc.abstractmethod
24
+ def to_market_data(self, encoding='utf-8') -> MarketData:
25
+ """
26
+ Abstract method to convert the buffer data to a `MarketData` instance.
27
+
28
+ Returns:
29
+ MarketData: A `MarketData` instance populated with buffer data.
30
+ """
31
+ ...
32
+
33
+ @property
34
+ def contents(self) -> MarketData:
35
+ return self.to_market_data()
36
+
37
+
38
+ class MarketDataMemoryBuffer(object, metaclass=abc.ABCMeta):
39
+ def __init__(self, size: int, **kwargs):
40
+ self.size = size
41
+ self.block = kwargs.get('block', False)
42
+ self.condition_put = kwargs.get('condition_put', Condition())
43
+ self.condition_get = kwargs.get('condition_get', Condition())
44
+ self._dtype_size = kwargs.get('dtype_size', 16)
45
+ self._ticker_size = kwargs.get('ticker_size', 32)
46
+
47
+ self._ticker_c_arr = ctypes.c_char * self._ticker_size
48
+ self._dtype_c_arr = ctypes.c_char * self._dtype_size
49
+
50
+ self.dtype = RawValue(self._dtype_c_arr)
51
+ self.ticker = RawArray(self._ticker_c_arr, self.size)
52
+ self.timestamp = RawArray(ctypes.c_double, self.size)
53
+ self._index = RawValue(ctypes.c_int * 2)
54
+
55
+ @overload
56
+ def __getitem__(self, index: slice) -> list[MarketDataPointer]:
57
+ ...
58
+
59
+ @overload
60
+ def __getitem__(self, index: int) -> MarketDataPointer:
61
+ ...
62
+
63
+ def __getitem__(self, index):
64
+ """
65
+ based on the virtual index, not the internal (actual) index.
66
+ """
67
+ if isinstance(index, slice):
68
+ return self._get_slice(index)
69
+ elif isinstance(index, int):
70
+ return self._get(index)
71
+ else:
72
+ raise TypeError(f'Invalid index {index}. Expected int or slice!')
73
+
74
+ def __len__(self) -> int:
75
+ return self.tail - self.head
76
+
77
+ def _get_slice(self, index: slice) -> list[MarketDataPointer]:
78
+ start, stop, step = index.start, index.stop, index.step
79
+ return [self._get(i) for i in range(start, stop, step if step is not None else 1)]
80
+
81
+ def _get(self, index: int) -> MarketDataPointer:
82
+ """
83
+ the internal method of get will not increase the index
84
+ """
85
+ valid_length = self.__len__()
86
+ if -valid_length <= index < valid_length:
87
+ index = index % valid_length
88
+ else:
89
+ raise IndexError(f'Index {index} is out of bounds!')
90
+
91
+ internal_index = (index + self.head) % self.size
92
+ return self.at(internal_index)
93
+
94
+ def get(self, raise_on_empty: bool = False, encoding='utf-8') -> MarketData | None:
95
+ while self.is_empty():
96
+ if raise_on_empty:
97
+ raise ValueError(f'Buffer {self.__class__.__name__} is empty!')
98
+
99
+ if not self.block:
100
+ return None
101
+
102
+ with self.condition_get:
103
+ self.condition_get.wait()
104
+
105
+ md_ptr = self.at(index=self.head)
106
+ md = md_ptr.to_market_data(encoding=encoding)
107
+
108
+ if self.is_full() and self.block:
109
+ self.head += 1
110
+ self.condition_put.notify_all()
111
+ else:
112
+ self.head += 1
113
+
114
+ return md
115
+
116
+ def _put(self, market_data: MarketData, encoding='utf-8'):
117
+ """
118
+ the internal method of put will not increase the index
119
+ """
120
+ md_ptr = self.at(index=self.tail)
121
+ md_ptr.update(market_data=market_data, encoding=encoding)
122
+
123
+ def put(self, market_data: MarketData, raise_on_full: bool = False, encoding='utf-8'):
124
+ """
125
+ the put method is not thread safe, and should only be called in one single thread.
126
+ """
127
+ while self.is_full():
128
+ if raise_on_full:
129
+ raise ValueError(f'Buffer {self.__class__.__name__} is full!')
130
+
131
+ if not self.block:
132
+ continue
133
+
134
+ with self.condition_put:
135
+ self.condition_put.wait()
136
+
137
+ self._put(market_data=market_data, encoding=encoding)
138
+
139
+ if self.is_empty() and self.block:
140
+ with self.condition_get:
141
+ self.tail += 1
142
+ self.condition_get.notify_all()
143
+ else:
144
+ self.tail += 1
145
+
146
+ def at(self, index: int) -> MarketDataPointer:
147
+ return self._at(index % self.size)
148
+
149
+ @abc.abstractmethod
150
+ def _at(self, index: int) -> MarketDataPointer:
151
+ """
152
+ get pointer by actual index
153
+ """
154
+ ...
155
+
156
+ def is_full(self) -> bool:
157
+ _tail_next = self.tail + 1
158
+ _head_next_circle = self.head + self.size
159
+
160
+ # to be more generic
161
+ # _tail_next = _tail_next % self.size
162
+ # _head_next_circle = _head_next_circle % self.size
163
+
164
+ return _tail_next == _head_next_circle
165
+
166
+ def is_empty(self) -> bool:
167
+ idx_tail = self.tail
168
+ idx_head = self.head
169
+
170
+ # to be more generic
171
+ # idx_tail = idx_tail % self.size
172
+ # idx_head = idx_head % self.size
173
+
174
+ return idx_head == idx_tail
175
+
176
+ @property
177
+ def head(self) -> int:
178
+ return self._index[0]
179
+
180
+ @property
181
+ def tail(self) -> int:
182
+ return self._index[1]
183
+
184
+ @head.setter
185
+ def head(self, value: int):
186
+ self._index[0] = value
187
+
188
+ @tail.setter
189
+ def tail(self, value: int):
190
+ self._index[1] = value
191
+
192
+
193
+ class OrderBookPointer(MarketDataPointer):
194
+ def __init__(self, max_level: int, **kwargs):
195
+ super().__init__(
196
+ dtype=kwargs.get('dtype'),
197
+ ticker=kwargs.get('ticker'),
198
+ timestamp=kwargs.get('timestamp')
199
+ )
200
+
201
+ self.max_level = max_level
202
+
203
+ self._ptr_bid_price = kwargs.get('bid_price')
204
+ self._ptr_ask_price = kwargs.get('ask_price')
205
+
206
+ self._ptr_bid_volume = kwargs.get('bid_volume')
207
+ self._ptr_ask_volume = kwargs.get('ask_volume')
208
+
209
+ self._ptr_bid_n_orders = kwargs.get('bid_n_orders')
210
+ self._ptr_ask_n_orders = kwargs.get('ask_n_orders')
211
+
212
+ def update(self, market_data: OrderBook, encoding='utf-8'):
213
+ super().update(market_data, encoding=encoding)
214
+
215
+ bid = market_data['bid']
216
+ ask = market_data['ask']
217
+
218
+ bid_px, bid_vol, *bid_n = zip(*bid)
219
+ ask_px, ask_vol, *ask_n = zip(*ask)
220
+
221
+ self._ptr_bid_price.contents[:] = bid_px[:self.max_level] + [0] * (self.max_level - len(bid_px))
222
+ self._ptr_ask_price.contents[:] = ask_px[:self.max_level] + [0] * (self.max_level - len(ask_px))
223
+
224
+ self._ptr_bid_volume.contents[:] = bid_vol[:self.max_level] + [0] * (self.max_level - len(bid_vol))
225
+ self._ptr_ask_volume.contents[:] = ask_vol[:self.max_level] + [0] * (self.max_level - len(ask_vol))
226
+
227
+ if bid_n:
228
+ bid_n = bid_n[0]
229
+ self._ptr_bid_n_orders.contents[:] = bid_n[:self.max_level] + [0] * (self.max_level - len(bid_n))
230
+
231
+ if ask_n:
232
+ ask_n = ask_n[0]
233
+ self._ptr_ask_n_orders.contents[:] = ask_n[:self.max_level] + [0] * (self.max_level - len(ask_n))
234
+
235
+ def to_market_data(self, encoding='utf-8') -> MarketData:
236
+ bid_px = self._ptr_bid_price.contents[:]
237
+ bid_vol = self._ptr_bid_volume.contents[:]
238
+ bid_n = self._ptr_bid_n_orders.contents[:]
239
+
240
+ ask_px = self._ptr_ask_price.contents[:]
241
+ ask_vol = self._ptr_ask_volume.contents[:]
242
+ ask_n = self._ptr_ask_n_orders.contents[:]
243
+
244
+ if any(bid_n):
245
+ bid = zip(bid_px, bid_vol, bid_n)
246
+ else:
247
+ bid = zip(bid_px, bid_vol)
248
+
249
+ if any(ask_n):
250
+ ask = zip(ask_px, ask_vol, ask_n)
251
+ else:
252
+ ask = zip(ask_px, ask_vol)
253
+
254
+ order_book = OrderBook(
255
+ ticker=self._ptr_ticker.contents.value.decode(encoding),
256
+ timestamp=self._ptr_timestamp.contents.value,
257
+ bid=[list(_) for _ in bid],
258
+ ask=[list(_) for _ in ask]
259
+ )
260
+
261
+ return order_book
262
+
263
+
264
+ class OrderBookBuffer(MarketDataMemoryBuffer):
265
+ def __init__(self, size: int, max_level: int = 20, **kwargs):
266
+ """
267
+ Initializes the order book buffer with the specified maximum levels.
268
+
269
+ Args:
270
+ max_level (int): Maximum number of levels for bid and ask data. Defaults to 20.
271
+ """
272
+ super().__init__(size=size, **kwargs)
273
+ self.max_level = max_level
274
+
275
+ self._book_c_arr = ctypes.c_double * self.max_level
276
+ self._order_c_arr = ctypes.c_int * self.max_level
277
+
278
+ self.bid_price = RawArray(self._book_c_arr, self.size)
279
+ self.ask_price = RawArray(self._book_c_arr, self.size)
280
+
281
+ self.bid_volume = RawArray(self._book_c_arr, self.size)
282
+ self.ask_volume = RawArray(self._book_c_arr, self.size)
283
+
284
+ self.bid_n_orders = RawArray(self._order_c_arr, self.size)
285
+ self.ask_n_orders = RawArray(self._order_c_arr, self.size)
286
+
287
+ def _at(self, index: int) -> OrderBookPointer:
288
+ ptr = OrderBookPointer(
289
+ dtype=ctypes.pointer(self.dtype),
290
+ ticker=ctypes.cast(ctypes.addressof(self.ticker) + index * ctypes.sizeof(self._ticker_c_arr), ctypes.POINTER(self._ticker_c_arr)),
291
+ timestamp=ctypes.cast(ctypes.addressof(self.timestamp) + index * ctypes.sizeof(ctypes.c_double), ctypes.POINTER(ctypes.c_double)),
292
+ max_level=self.max_level
293
+ )
294
+
295
+ ptr._ptr_bid_price = ctypes.cast(ctypes.addressof(self.bid_price) + index * ctypes.sizeof(self._book_c_arr), ctypes.POINTER(self._book_c_arr))
296
+ ptr._ptr_ask_price = ctypes.cast(ctypes.addressof(self.ask_price) + index * ctypes.sizeof(self._book_c_arr), ctypes.POINTER(self._book_c_arr))
297
+
298
+ ptr._ptr_bid_volume = ctypes.cast(ctypes.addressof(self.bid_volume) + index * ctypes.sizeof(self._book_c_arr), ctypes.POINTER(self._book_c_arr))
299
+ ptr._ptr_ask_volume = ctypes.cast(ctypes.addressof(self.ask_volume) + index * ctypes.sizeof(self._book_c_arr), ctypes.POINTER(self._book_c_arr))
300
+
301
+ ptr._ptr_bid_n_orders = ctypes.cast(ctypes.addressof(self.bid_n_orders) + index * ctypes.sizeof(self._order_c_arr), ctypes.POINTER(self._order_c_arr))
302
+ ptr._ptr_ask_n_orders = ctypes.cast(ctypes.addressof(self.ask_n_orders) + index * ctypes.sizeof(self._order_c_arr), ctypes.POINTER(self._order_c_arr))
303
+
304
+ return ptr
305
+
306
+
307
+ class BarDataPointer(MarketDataPointer):
308
+ def __init__(self, **kwargs):
309
+ super().__init__(
310
+ dtype=kwargs.get('dtype'),
311
+ ticker=kwargs.get('ticker'),
312
+ timestamp=kwargs.get('timestamp')
313
+ )
314
+
315
+ self.data = kwargs.get('data')
316
+
317
+ def update(self, market_data: BarData, encoding='utf-8'):
318
+ super().update(market_data, encoding=encoding)
319
+
320
+ data = [
321
+ market_data['start_timestamp'] if 'start_timestamp' in market_data else math.nan,
322
+ market_data['bar_span'] if 'bar_span' in market_data else math.nan,
323
+ market_data['high_price'],
324
+ market_data['low_price'],
325
+ market_data['open_price'],
326
+ market_data['close_price'],
327
+ market_data['volume'],
328
+ market_data['notional'],
329
+ float(market_data['trade_count'])
330
+ ]
331
+
332
+ self.data.contents[:] = data
333
+
334
+ def to_market_data(self, encoding='utf-8') -> BarData:
335
+ data = self.data.contents[:]
336
+
337
+ (start_timestamp, bar_span,
338
+ high_price, low_price, open_price, close_price,
339
+ volume, notional,
340
+ trade_count) = data
341
+
342
+ bar_data = BarData(
343
+ ticker=self._ptr_ticker.contents.value.decode(encoding),
344
+ timestamp=self._ptr_timestamp.contents.value,
345
+ start_timestamp=start_timestamp if math.isfinite(start_timestamp) else None,
346
+ bar_span=bar_span if math.isfinite(bar_span) else None,
347
+ high_price=high_price,
348
+ low_price=low_price,
349
+ open_price=open_price,
350
+ close_price=close_price,
351
+ volume=volume,
352
+ notional=notional,
353
+ trade_count=int(trade_count)
354
+ )
355
+
356
+ return bar_data
357
+
358
+
359
+ class BarDataBuffer(MarketDataMemoryBuffer):
360
+ _c_data_arr = ctypes.c_double * 9
361
+
362
+ def __init__(self, size: int, **kwargs):
363
+ super().__init__(size=size, **kwargs)
364
+ # the data should store the info in following orders
365
+ # start_timestamp, bar_span
366
+ # high_price, low_price, open_price, close_price
367
+ # volume, notional
368
+ # trade_count
369
+ self.data = RawArray(self._c_data_arr, self.size)
370
+
371
+ # self.start_timestamp = RawArray(c_double)
372
+ # self.bar_span = RawArray(c_double)
373
+ # self.high_price = RawArray(c_double)
374
+ # self.low_price = RawArray(c_double)
375
+ # self.open_price = RawArray(c_double)
376
+ # self.close_price = RawArray(c_double)
377
+ # self.volume = RawArray(c_double)
378
+ # self.notional = RawArray(c_double)
379
+ # self.trade_count = RawArray(ctypes.c_int8)
380
+
381
+ def _at(self, index: int) -> BarDataPointer:
382
+ ptr = BarDataPointer(
383
+ dtype=ctypes.pointer(self.dtype),
384
+ ticker=ctypes.cast(ctypes.addressof(self.ticker) + index * ctypes.sizeof(self._ticker_c_arr), ctypes.POINTER(self._ticker_c_arr)),
385
+ timestamp=ctypes.cast(ctypes.addressof(self.timestamp) + index * ctypes.sizeof(ctypes.c_double), ctypes.POINTER(ctypes.c_double)),
386
+ data=ctypes.cast(ctypes.addressof(self.data) + index * ctypes.sizeof(self._c_data_arr), ctypes.POINTER(self._c_data_arr))
387
+ )
388
+
389
+ return ptr
390
+
391
+
392
+ class TickDataPointer(MarketDataPointer):
393
+ def __init__(self, **kwargs):
394
+ super().__init__(
395
+ dtype=kwargs.get('dtype'),
396
+ ticker=kwargs.get('ticker'),
397
+ timestamp=kwargs.get('timestamp')
398
+ )
399
+
400
+ self.data = kwargs.get('data')
401
+
402
+ def update(self, market_data: TickData, encoding='utf-8'):
403
+ super().update(market_data, encoding=encoding)
404
+
405
+ data = [
406
+ market_data.last_price,
407
+ market_data['bid_price'] if 'bid_price' in market_data else math.nan,
408
+ market_data['bid_volume'] if 'bid_volume' in market_data else math.nan,
409
+ market_data['ask_price'] if 'ask_price' in market_data else math.nan,
410
+ market_data['ask_volume'] if 'ask_volume' in market_data else math.nan,
411
+ market_data['total_traded_volume'],
412
+ market_data['total_traded_notional'],
413
+ float(market_data['total_trade_count'])
414
+ ]
415
+
416
+ self.data.contents[:] = data
417
+
418
+ def to_market_data(self, encoding='utf-8') -> TickData:
419
+ data = self.data.contents[:]
420
+
421
+ (last_price,
422
+ bid_price, bid_volume,
423
+ ask_price, ask_volume,
424
+ total_traded_volume, total_traded_notional,
425
+ total_trade_count) = data
426
+
427
+ tick_data = TickData(
428
+ ticker=self._ptr_ticker.contents.value.decode(encoding),
429
+ timestamp=self._ptr_timestamp.contents.value,
430
+ last_price=last_price,
431
+ bid_price=bid_price,
432
+ bid_volume=bid_volume,
433
+ ask_price=ask_price,
434
+ ask_volume=ask_volume,
435
+ order_book=None,
436
+ total_traded_volume=total_traded_volume,
437
+ total_traded_notional=total_traded_notional,
438
+ total_trade_count=int(total_trade_count),
439
+ )
440
+
441
+ return tick_data
442
+
443
+
444
+ class TickDataBuffer(MarketDataMemoryBuffer):
445
+ _c_data_arr = ctypes.c_double * 8
446
+
447
+ def __init__(self, size: int, **kwargs):
448
+ super().__init__(size=size, **kwargs)
449
+
450
+ self.data = RawArray(self._dtype_c_arr, self.size)
451
+
452
+ def _at(self, index: int) -> TickDataPointer:
453
+ ptr = TickDataPointer(
454
+ dtype=ctypes.pointer(self.dtype),
455
+ ticker=ctypes.cast(ctypes.addressof(self.ticker) + index * ctypes.sizeof(self._ticker_c_arr), ctypes.POINTER(self._ticker_c_arr)),
456
+ timestamp=ctypes.cast(ctypes.addressof(self.timestamp) + index * ctypes.sizeof(ctypes.c_double), ctypes.POINTER(ctypes.c_double)),
457
+ data=ctypes.cast(ctypes.addressof(self.data) + index * ctypes.sizeof(self._c_data_arr), ctypes.POINTER(self._c_data_arr))
458
+ )
459
+
460
+ return ptr
461
+
462
+
463
+ class TransactionDataPointer(MarketDataPointer):
464
+ def __init__(self, **kwargs):
465
+ super().__init__(
466
+ dtype=kwargs.get('dtype'),
467
+ ticker=kwargs.get('ticker'),
468
+ timestamp=kwargs.get('timestamp')
469
+ )
470
+
471
+ self.data = kwargs.get('data')
472
+ self.id_type = kwargs.get('id_type')
473
+ self.transaction_id = kwargs.get('transaction_id')
474
+ self.buy_id = kwargs.get('buy_id')
475
+ self.sell_id = kwargs.get('sell_id')
476
+
477
+ def update(self, market_data: TransactionData | TradeData, encoding='utf-8'):
478
+ super().update(market_data, encoding=encoding)
479
+
480
+ data = [
481
+ market_data['price'],
482
+ market_data['volume'],
483
+ market_data['side'],
484
+ market_data['multiplier'] if 'multiplier' in market_data else math.nan,
485
+ market_data['notional'] if 'notional' in market_data else math.nan,
486
+ ]
487
+
488
+ for i, (id_name, id_ptr) in enumerate(zip(['transaction_id', 'buy_id', 'sell_id'], [self.transaction_id, self.buy_id, self.sell_id])):
489
+ if id_name not in market_data:
490
+ id_ptr.contents[:] = b''.ljust(ctypes.sizeof(id_ptr.contents), b'\x00')
491
+ self.id_type.contents[i] = 0
492
+ elif isinstance(_id := market_data[id_name], str):
493
+ if len(_id) > 16:
494
+ raise ValueError(f'{id_name} too long, expect 16 bytes.')
495
+ id_ptr.contents[:] = _id.encode(encoding).ljust(ctypes.sizeof(id_ptr.contents), b'\x00')
496
+ self.id_type.contents[i] = 1
497
+ elif isinstance(_id, int):
498
+ id_ptr.contents[:] = _id.to_bytes(length=ctypes.sizeof(id_ptr.contents), byteorder='big')
499
+ self.id_type.contents[i] = 0
500
+ else:
501
+ raise TypeError(f'Invalid {id_name} {_id} type: {type(_id)}, expect int or str.')
502
+
503
+ self.data.contents[:] = data
504
+
505
+ def to_market_data(self, encoding='utf-8') -> TransactionData | TradeData:
506
+ data = self.data.contents[:]
507
+ data_id_type = self.id_type.contents[:]
508
+
509
+ price, volume, side, multiplier, notional = data
510
+ transaction_id_type, buy_id_type, sell_id_type = data_id_type
511
+
512
+ if (dtype := self._ptr_dtype.contents.value) == b'TransactionData':
513
+ constructor = TransactionData
514
+ elif dtype == b'TradeData':
515
+ constructor = TradeData
516
+ else:
517
+ raise NotImplementedError(f'Constructor for market data {dtype} not implemented.')
518
+
519
+ transaction_data = constructor(
520
+ ticker=self._ptr_ticker.contents.value.decode(encoding),
521
+ timestamp=self._ptr_timestamp.contents.value,
522
+ price=price,
523
+ volume=volume,
524
+ side=int(side),
525
+ multiplier=None if np.isnan(multiplier) else multiplier,
526
+ notional=None if np.isnan(notional) else notional,
527
+ transaction_id=self.transaction_id.contents.value.decode(encoding) if transaction_id_type else int.from_bytes(self.transaction_id.contents),
528
+ buy_id=self.buy_id.contents.value.decode(encoding) if buy_id_type else int.from_bytes(self.buy_id.contents),
529
+ sell_id=self.sell_id.contents.value.decode(encoding) if sell_id_type else int.from_bytes(self.sell_id.contents)
530
+ )
531
+
532
+ return transaction_data
533
+
534
+
535
+ class TransactionDataBuffer(MarketDataMemoryBuffer):
536
+ _c_data_arr = ctypes.c_double * 5
537
+ _c_id_type_arr = ctypes.c_int * 3
538
+
539
+ def __init__(self, size: int, **kwargs):
540
+ super().__init__(size=size, **kwargs)
541
+ self._id_size = kwargs.get('id_size', 16)
542
+ self._c_id_arr = ctypes.c_char * self._id_size
543
+
544
+ self.data = RawArray(self._c_data_arr, self.size)
545
+ self.data_id_type = RawArray(self._c_id_type_arr, self.size)
546
+ self.transaction_id = RawArray(self._c_id_arr, self.size)
547
+ self.buy_id = RawArray(self._c_id_arr, self.size)
548
+ self.sell_id = RawArray(self._c_id_arr, self.size)
549
+
550
+ def _at(self, index: int) -> TransactionDataPointer:
551
+ ptr = TransactionDataPointer(
552
+ dtype=ctypes.pointer(self.dtype),
553
+ ticker=ctypes.cast(ctypes.addressof(self.ticker) + index * ctypes.sizeof(self._ticker_c_arr), ctypes.POINTER(self._ticker_c_arr)),
554
+ timestamp=ctypes.cast(ctypes.addressof(self.timestamp) + index * ctypes.sizeof(ctypes.c_double), ctypes.POINTER(ctypes.c_double)),
555
+ data=ctypes.cast(ctypes.addressof(self.data) + index * ctypes.sizeof(self._c_data_arr), ctypes.POINTER(self._c_data_arr)),
556
+ id_type=ctypes.cast(ctypes.addressof(self.data_id_type) + index * ctypes.sizeof(self._c_id_type_arr), ctypes.POINTER(self._c_id_type_arr)),
557
+ transaction_id=ctypes.cast(ctypes.addressof(self.transaction_id) + index * ctypes.sizeof(self._c_id_arr), ctypes.POINTER(self._c_id_arr)),
558
+ buy_id=ctypes.cast(ctypes.addressof(self.buy_id) + index * ctypes.sizeof(self._c_id_arr), ctypes.POINTER(self._c_id_arr)),
559
+ sell_id=ctypes.cast(ctypes.addressof(self.sell_id) + index * ctypes.sizeof(self._c_id_arr), ctypes.POINTER(self._c_id_arr))
560
+ )
561
+
562
+ return ptr
563
+
564
+
565
+ __all__ = [
566
+ 'MarketDataPointer', 'MarketDataMemoryBuffer',
567
+ 'OrderBookPointer', 'OrderBookBuffer',
568
+ 'BarDataPointer', 'BarDataBuffer',
569
+ 'TickDataPointer', 'TickDataBuffer',
570
+ 'TransactionDataPointer', 'TransactionDataBuffer'
571
+ ]