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.
- PyAlgoEngine-0.7.4.dist-info/LICENSE +21 -0
- PyAlgoEngine-0.7.4.dist-info/METADATA +27 -0
- PyAlgoEngine-0.7.4.dist-info/RECORD +43 -0
- PyAlgoEngine-0.7.4.dist-info/WHEEL +5 -0
- PyAlgoEngine-0.7.4.dist-info/top_level.txt +1 -0
- algo_engine/__init__.py +41 -0
- algo_engine/apps/__init__.py +17 -0
- algo_engine/apps/backtest/__init__.py +20 -0
- algo_engine/apps/backtest/doc_server.py +331 -0
- algo_engine/apps/backtest/tester.py +254 -0
- algo_engine/apps/backtest/web_app.py +127 -0
- algo_engine/apps/bokeh_server.py +205 -0
- algo_engine/apps/demo/__init__.py +0 -0
- algo_engine/apps/demo/test.py +39 -0
- algo_engine/backtest/__init__.py +19 -0
- algo_engine/backtest/__main__.py +51 -0
- algo_engine/backtest/metrics.py +179 -0
- algo_engine/backtest/replay.py +261 -0
- algo_engine/backtest/sim_match.py +295 -0
- algo_engine/base/__init__.py +40 -0
- algo_engine/base/console_utils.py +1070 -0
- algo_engine/base/finance_decimal.py +258 -0
- algo_engine/base/market_buffer.py +571 -0
- algo_engine/base/market_utils.py +3092 -0
- algo_engine/base/market_utils_nt.py +188 -0
- algo_engine/base/market_utils_posix.py +3004 -0
- algo_engine/base/technical_analysis.py +406 -0
- algo_engine/base/telemetrics.py +78 -0
- algo_engine/base/trade_utils.py +709 -0
- algo_engine/engine/__init__.py +28 -0
- algo_engine/engine/algo_engine.py +901 -0
- algo_engine/engine/event_engine.py +53 -0
- algo_engine/engine/market_engine.py +370 -0
- algo_engine/engine/trade_engine.py +2037 -0
- algo_engine/monitor/__init__.py +15 -0
- algo_engine/monitor/advanced_data_interface.py +239 -0
- algo_engine/profile/__init__.py +121 -0
- algo_engine/profile/cn.py +175 -0
- algo_engine/strategy/__init__.py +44 -0
- algo_engine/strategy/strategy_engine.py +440 -0
- algo_engine/utils/__init__.py +3 -0
- algo_engine/utils/commit_regularizer.py +49 -0
- 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
|
+
]
|