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