itchfeed 1.0.4__py3-none-any.whl → 1.0.6__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.
itch/__init__.py CHANGED
@@ -4,8 +4,14 @@ Nasdaq TotalView-ITCH 5.0 Parser
4
4
 
5
5
  __author__ = "Bertin Balouki SIMYELI"
6
6
  __copyright__ = "2025 Bertin Balouki SIMYELI"
7
- __email__ = "bertin@bbstrader.com"
7
+ __email__ = "bertin@bbs-trading.com"
8
8
  __license__ = "MIT"
9
- __version__ = "1.0.4"
9
+
10
+ from importlib.metadata import version, PackageNotFoundError
11
+
12
+ try:
13
+ __version__ = version("itchfeed")
14
+ except PackageNotFoundError:
15
+ __version__ = "unknown"
10
16
 
11
17
 
itch/messages.py CHANGED
@@ -13,13 +13,21 @@ class MarketMessage(object):
13
13
 
14
14
  All Message have the following attributes:
15
15
  - message_type: A single letter that identify the message
16
+ - timestamp: Time at which the message was generated (Nanoseconds past midnight)
17
+ - stock_locate: Locate code identifying the security
18
+ - tracking_number: Nasdaq internal tracking number
19
+
20
+ The following attributes are not part of the message, but are used to describe the message:
16
21
  - description: Describe the message
17
22
  - message_format: string format using to unpack the message
18
23
  - message_pack_format: string format using to pack the message
19
24
  - message_size: The size in bytes of the message
20
- - timestamp: Time at which the message was generated (Nanoseconds past midnight)
21
- - stock_locate: Locate code identifying the security
22
- - tracking_number: Nasdaq internal tracking number
25
+
26
+ # NOTE:
27
+ Prices are integers fields supplied with an associated precision. When converted to a decimal format, prices are in
28
+ fixed point format, where the precision defines the number of decimal places. For example, a field flagged as Price
29
+ (4) has an implied 4 decimal places. The maximum value of price (4) in TotalView ITCH is 200,000.0000 (decimal,
30
+ 77359400 hex). ``price_precision`` is 4 for all messages except MWCBDeclineLeveMessage where ``price_precision`` is 8.
23
31
  """
24
32
 
25
33
  message_type: bytes
@@ -32,13 +40,24 @@ class MarketMessage(object):
32
40
  tracking_number: int
33
41
  price_precision: int = 4
34
42
 
35
- def __repr__(self):
43
+ def __repr__(self) -> str:
36
44
  return repr(self.decode())
37
45
 
38
- def pack(self) -> bytes:
46
+ def __bytes__(self) -> bytes:
47
+ return self.to_bytes()
48
+
49
+ def to_bytes(self) -> bytes: # type: ignore
39
50
  """
40
51
  Packs the message into bytes using the defined message_pack_format.
41
52
  This method should be overridden by subclasses to include specific fields.
53
+
54
+ Note:
55
+ All packed messages do not include
56
+ - ``description``,
57
+ - ``message_format``,
58
+ - ``message_pack_format``,
59
+ - ``message_size``
60
+ - ``price_precision``
42
61
  """
43
62
  pass
44
63
 
@@ -90,9 +109,7 @@ class MarketMessage(object):
90
109
  ts1 = self.timestamp >> 32
91
110
  ts2 = self.timestamp - (ts1 << 32)
92
111
  return (ts1, ts2)
93
- ts1 = self.timestamp >> 32
94
- ts2 = self.timestamp - (ts1 << 32)
95
- return (ts1, ts2)
112
+
96
113
 
97
114
  def decode_price(self, price_attr: str) -> float:
98
115
  precision = getattr(self, "price_precision")
@@ -166,7 +183,7 @@ class SystemEventMessage(MarketMessage):
166
183
  ) = struct.unpack(self.message_format, message[1:])
167
184
  self.set_timestamp(timestamp1, timestamp2)
168
185
 
169
- def pack(self):
186
+ def to_bytes(self):
170
187
  (timestamp1, timestamp2) = self.split_timestamp()
171
188
  message = struct.pack(
172
189
  self.message_pack_format,
@@ -288,7 +305,7 @@ class StockDirectoryMessage(MarketMessage):
288
305
  ) = struct.unpack(self.message_format, message[1:])
289
306
  self.set_timestamp(timestamp1, timestamp2)
290
307
 
291
- def pack(self):
308
+ def to_bytes(self):
292
309
  (timestamp1, timestamp2) = self.split_timestamp()
293
310
  message = struct.pack(
294
311
  self.message_pack_format,
@@ -368,7 +385,7 @@ class StockTradingActionMessage(MarketMessage):
368
385
  ) = struct.unpack(self.message_format, message[1:])
369
386
  self.set_timestamp(timestamp1, timestamp2)
370
387
 
371
- def pack(self):
388
+ def to_bytes(self):
372
389
  (timestamp1, timestamp2) = self.split_timestamp()
373
390
  message = struct.pack(
374
391
  self.message_pack_format,
@@ -429,7 +446,7 @@ class RegSHOMessage(MarketMessage):
429
446
  ) = struct.unpack(self.message_format, message[1:])
430
447
  self.set_timestamp(timestamp1, timestamp2)
431
448
 
432
- def pack(self):
449
+ def to_bytes(self):
433
450
  (timestamp1, timestamp2) = self.split_timestamp()
434
451
  message = struct.pack(
435
452
  self.message_pack_format,
@@ -491,7 +508,7 @@ class MarketParticipantPositionMessage(MarketMessage):
491
508
  ) = struct.unpack(self.message_format, message[1:])
492
509
  self.set_timestamp(timestamp1, timestamp2)
493
510
 
494
- def pack(self):
511
+ def to_bytes(self):
495
512
  (timestamp1, timestamp2) = self.split_timestamp()
496
513
  message = struct.pack(
497
514
  self.message_pack_format,
@@ -542,7 +559,7 @@ class MWCBDeclineLeveMessage(MarketMessage):
542
559
  ) = struct.unpack(self.message_format, message[1:])
543
560
  self.set_timestamp(timestamp1, timestamp2)
544
561
 
545
- def pack(self):
562
+ def to_bytes(self):
546
563
  (timestamp1, timestamp2) = self.split_timestamp()
547
564
  message = struct.pack(
548
565
  self.message_pack_format,
@@ -587,7 +604,7 @@ class MWCBStatusMessage(MarketMessage):
587
604
  ) = struct.unpack(self.message_format, message[1:])
588
605
  self.set_timestamp(timestamp1, timestamp2)
589
606
 
590
- def pack(self):
607
+ def to_bytes(self):
591
608
  (timestamp1, timestamp2) = self.split_timestamp()
592
609
  message = struct.pack(
593
610
  self.message_pack_format,
@@ -648,7 +665,7 @@ class IPOQuotingPeriodUpdateMessage(MarketMessage):
648
665
  ) = struct.unpack(self.message_format, message[1:])
649
666
  self.set_timestamp(timestamp1, timestamp2)
650
667
 
651
- def pack(self):
668
+ def to_bytes(self):
652
669
  (timestamp1, timestamp2) = self.split_timestamp()
653
670
  message = struct.pack(
654
671
  self.message_pack_format,
@@ -704,7 +721,7 @@ class LULDAuctionCollarMessage(MarketMessage):
704
721
  ) = struct.unpack(self.message_format, message[1:])
705
722
  self.set_timestamp(timestamp1, timestamp2)
706
723
 
707
- def pack(self):
724
+ def to_bytes(self):
708
725
  (timestamp1, timestamp2) = self.split_timestamp()
709
726
  message = struct.pack(
710
727
  self.message_pack_format,
@@ -769,7 +786,7 @@ class OperationalHaltMessage(MarketMessage):
769
786
  ) = struct.unpack(self.message_format, message[1:])
770
787
  self.set_timestamp(timestamp1, timestamp2)
771
788
 
772
- def pack(self):
789
+ def to_bytes(self):
773
790
  (timestamp1, timestamp2) = self.split_timestamp()
774
791
  message = struct.pack(
775
792
  self.message_pack_format,
@@ -832,7 +849,7 @@ class AddOrderNoMPIAttributionMessage(AddOrderMessage):
832
849
  ) = struct.unpack(self.message_format, message[1:])
833
850
  self.set_timestamp(timestamp1, timestamp2)
834
851
 
835
- def pack(self):
852
+ def to_bytes(self):
836
853
  (timestamp1, timestamp2) = self.split_timestamp()
837
854
  message = struct.pack(
838
855
  self.message_pack_format,
@@ -886,7 +903,7 @@ class AddOrderMPIDAttribution(AddOrderMessage):
886
903
  ) = struct.unpack(self.message_format, message[1:])
887
904
  self.set_timestamp(timestamp1, timestamp2)
888
905
 
889
- def pack(self):
906
+ def to_bytes(self):
890
907
  (timestamp1, timestamp2) = self.split_timestamp()
891
908
  message = struct.pack(
892
909
  self.message_pack_format,
@@ -957,7 +974,7 @@ class OrderExecutedMessage(ModifyOrderMessage):
957
974
  ) = struct.unpack(self.message_format, message[1:])
958
975
  self.set_timestamp(timestamp1, timestamp2)
959
976
 
960
- def pack(self):
977
+ def to_bytes(self):
961
978
  (timestamp1, timestamp2) = self.split_timestamp()
962
979
  message = struct.pack(
963
980
  self.message_pack_format,
@@ -1025,7 +1042,7 @@ class OrderExecutedWithPriceMessage(ModifyOrderMessage):
1025
1042
  ) = struct.unpack(self.message_format, message[1:])
1026
1043
  self.set_timestamp(timestamp1, timestamp2)
1027
1044
 
1028
- def pack(self):
1045
+ def to_bytes(self):
1029
1046
  (timestamp1, timestamp2) = self.split_timestamp()
1030
1047
  message = struct.pack(
1031
1048
  self.message_pack_format,
@@ -1071,7 +1088,7 @@ class OrderCancelMessage(ModifyOrderMessage):
1071
1088
  ) = struct.unpack(self.message_format, message[1:])
1072
1089
  self.set_timestamp(timestamp1, timestamp2)
1073
1090
 
1074
- def pack(self):
1091
+ def to_bytes(self):
1075
1092
  (timestamp1, timestamp2) = self.split_timestamp()
1076
1093
  message = struct.pack(
1077
1094
  self.message_pack_format,
@@ -1111,7 +1128,7 @@ class OrderDeleteMessage(ModifyOrderMessage):
1111
1128
  ) = struct.unpack(self.message_format, message[1:])
1112
1129
  self.set_timestamp(timestamp1, timestamp2)
1113
1130
 
1114
- def pack(self):
1131
+ def to_bytes(self):
1115
1132
  (timestamp1, timestamp2) = self.split_timestamp()
1116
1133
  message = struct.pack(
1117
1134
  self.message_pack_format,
@@ -1164,7 +1181,7 @@ class OrderReplaceMessage(ModifyOrderMessage):
1164
1181
  ) = struct.unpack(self.message_format, message[1:])
1165
1182
  self.set_timestamp(timestamp1, timestamp2)
1166
1183
 
1167
- def pack(self):
1184
+ def to_bytes(self):
1168
1185
  (timestamp1, timestamp2) = self.split_timestamp()
1169
1186
  message = struct.pack(
1170
1187
  self.message_pack_format,
@@ -1238,7 +1255,7 @@ class NonCrossTradeMessage(TradeMessage):
1238
1255
  ) = struct.unpack(self.message_format, message[1:])
1239
1256
  self.set_timestamp(timestamp1, timestamp2)
1240
1257
 
1241
- def pack(self):
1258
+ def to_bytes(self):
1242
1259
  (timestamp1, timestamp2) = self.split_timestamp()
1243
1260
  message = struct.pack(
1244
1261
  self.message_pack_format,
@@ -1309,7 +1326,7 @@ class CrossTradeMessage(TradeMessage):
1309
1326
  ) = struct.unpack(self.message_format, message[1:])
1310
1327
  self.set_timestamp(timestamp1, timestamp2)
1311
1328
 
1312
- def pack(self):
1329
+ def to_bytes(self):
1313
1330
  (timestamp1, timestamp2) = self.split_timestamp()
1314
1331
  message = struct.pack(
1315
1332
  self.message_pack_format,
@@ -1359,7 +1376,7 @@ class BrokenTradeMessage(TradeMessage):
1359
1376
  ) = struct.unpack(self.message_format, message[1:])
1360
1377
  self.set_timestamp(timestamp1, timestamp2)
1361
1378
 
1362
- def pack(self):
1379
+ def to_bytes(self):
1363
1380
  (timestamp1, timestamp2) = self.split_timestamp()
1364
1381
  message = struct.pack(
1365
1382
  self.message_pack_format,
@@ -1441,7 +1458,7 @@ class NOIIMessage(MarketMessage):
1441
1458
  ) = struct.unpack(self.message_format, message[1:])
1442
1459
  self.set_timestamp(timestamp1, timestamp2)
1443
1460
 
1444
- def pack(self):
1461
+ def to_bytes(self):
1445
1462
  (timestamp1, timestamp2) = self.split_timestamp()
1446
1463
  message = struct.pack(
1447
1464
  self.message_pack_format,
@@ -1496,7 +1513,7 @@ class RetailPriceImprovementIndicator(MarketMessage):
1496
1513
  ) = struct.unpack(self.message_format, message[1:])
1497
1514
  self.set_timestamp(timestamp1, timestamp2)
1498
1515
 
1499
- def pack(self):
1516
+ def to_bytes(self):
1500
1517
  (timestamp1, timestamp2) = self.split_timestamp()
1501
1518
  message = struct.pack(
1502
1519
  self.message_pack_format,
@@ -1559,7 +1576,7 @@ class DLCRMessage(MarketMessage):
1559
1576
  ) = struct.unpack(self.message_format, message[1:])
1560
1577
  self.set_timestamp(timestamp1, timestamp2)
1561
1578
 
1562
- def pack(self):
1579
+ def to_bytes(self):
1563
1580
  (timestamp1, timestamp2) = self.split_timestamp()
1564
1581
  message = struct.pack(
1565
1582
  self.message_pack_format,
@@ -1579,8 +1596,8 @@ class DLCRMessage(MarketMessage):
1579
1596
  )
1580
1597
  return message
1581
1598
 
1582
-
1583
- messages: Dict[bytes, Type[MarketMessage]] = {
1599
+ messages: Dict[bytes, Type[MarketMessage]]
1600
+ messages = {
1584
1601
  b"S": SystemEventMessage,
1585
1602
  b"R": StockDirectoryMessage,
1586
1603
  b"H": StockTradingActionMessage,
@@ -1607,7 +1624,7 @@ messages: Dict[bytes, Type[MarketMessage]] = {
1607
1624
  }
1608
1625
 
1609
1626
 
1610
- def create_message(message_type: bytes, **kwargs) -> Type[MarketMessage]:
1627
+ def create_message(message_type: bytes, **kwargs) -> MarketMessage:
1611
1628
  """
1612
1629
  Creates a new message of a given type with specified attributes.
1613
1630
 
itch/parser.py CHANGED
@@ -1,5 +1,4 @@
1
- from queue import Queue
2
- from typing import BinaryIO, List, Type
1
+ from typing import IO, BinaryIO, Callable, Iterator, Optional, Tuple
3
2
 
4
3
  from itch.messages import MESSAGES, MarketMessage
5
4
  from itch.messages import messages as msgs
@@ -14,160 +13,216 @@ class MessageParser(object):
14
13
  def __init__(self, message_type: bytes = MESSAGES):
15
14
  self.message_type = message_type
16
15
 
17
- def read_message_from_file(
18
- self,
19
- file: BinaryIO,
20
- cachesize: int = 4096,
21
- ) -> List[MarketMessage]:
16
+ def get_message_type(self, message: bytes) -> MarketMessage:
17
+ """
18
+ Take an entire bytearray and return the appropriate ITCH message
19
+ instance based on the message type indicator (first byte of the message).
20
+
21
+ All message type indicators are single ASCII characters.
22
+ """
23
+ message_type = message[0:1]
24
+ try:
25
+ return msgs[message_type](message) # type: ignore
26
+ except Exception:
27
+ raise ValueError(
28
+ f"Unknown message type: {message_type.decode(encoding='ascii')}"
29
+ )
30
+
31
+ def _parse_message_from_buffer(
32
+ self, buffer: memoryview, offset: int
33
+ ) -> Optional[Tuple[MarketMessage, int]]:
34
+ """
35
+ Parses a single ITCH message from a memory buffer.
36
+
37
+ This method checks for a 2-byte header (a null byte and a length byte),
38
+ determines the full message size, and extracts the message if the
39
+ complete message is present in the buffer.
40
+
41
+ Args:
42
+ buffer (memoryview):
43
+ The buffer containing the binary data.
44
+ offset (int):
45
+ The starting position in the buffer to begin parsing.
46
+
47
+ Returns:
48
+ Optional[Tuple[MarketMessage, int]]:
49
+ A tuple containing the parsed MarketMessage and the total length
50
+ of the message including the header. Returns None if a complete
51
+ message could not be parsed.
52
+
53
+ Raises:
54
+ ValueError:
55
+ If the data at the current offset does not start with the
56
+ expected 0x00 byte.
57
+ """
58
+ buffer_len = len(buffer)
59
+ if offset + 2 > buffer_len:
60
+ return None
61
+
62
+ if buffer[offset : offset + 1] != b"\x00":
63
+ raise ValueError(
64
+ f"Unexpected start byte at offset {offset}: "
65
+ f"{buffer[offset : offset + 1].tobytes()}"
66
+ )
67
+
68
+ msg_len = buffer[offset + 1]
69
+ total_len = 2 + msg_len
70
+
71
+ if offset + total_len > buffer_len:
72
+ return None
73
+
74
+ raw_msg = buffer[offset + 2 : offset + total_len]
75
+ message = self.get_message_type(raw_msg.tobytes())
76
+ return message, total_len
77
+
78
+ def parse_file(
79
+ self, file: BinaryIO, cachesize: int = 65_536, save_file: Optional[IO] = None
80
+ ) -> Iterator[MarketMessage]:
22
81
  """
23
82
  Reads and parses market messages from a binary file-like object.
24
83
 
25
84
  This method processes binary data in chunks, extracts individual messages
26
- according to a specific format, and returns a list of successfully decoded
27
- MarketMessage objects. Parsing stops either when the end of the file is
85
+ according to a specific format, and returns a list of successfully decoded
86
+ MarketMessage objects. Parsing stops either when the end of the file is
28
87
  reached or when a system message with an end-of-messages event code is encountered.
29
88
 
30
89
  Args:
31
- file (BinaryIO):
90
+ file (BinaryIO):
32
91
  A binary file-like object (opened in binary mode) from which market messages are read.
33
- cachesize (int, optional):
34
- The size (in bytes) of each data chunk read from the file. Defaults to 4096 bytes.
92
+ cachesize (int, optional):
93
+ The size of each data chunk to read. Defaults to 65536 bytes (64KB).
94
+ save_file (IO, optional):
95
+ A binary file-like object (opened in binary write mode) where filtered messages are saved.
35
96
 
36
- Returns:
37
- List[MarketMessage]:
38
- A list of parsed MarketMessage objects that match the allowed message types
39
- defined in self.message_type.
97
+ Yields:
98
+ MarketMessage:
99
+ The next parsed MarketMessage object from the file.
40
100
 
41
101
  Raises:
42
- ValueError:
43
- If a message does not start with the expected 0x00 byte, indicating
102
+ ValueError:
103
+ If a message does not start with the expected 0x00 byte, indicating
44
104
  an unexpected file format or possible corruption.
45
105
 
46
- Message Format:
106
+ Notes:
47
107
  - Each message starts with a 0x00 byte.
48
108
  - The following byte specifies the message length.
49
109
  - The complete message consists of the first 2 bytes and 'message length' bytes of body.
50
- - If a system message (message_type == b'S') with event_code == b'C' is encountered,
51
- parsing stops immediately.
110
+ - If a system message (message_type == b'S') with event_code == b'C' is encountered,
111
+ parsing stops immediately.
52
112
 
53
113
  Example:
54
- >>> with open('market_data.bin', 'rb') as binary_file:
55
- >>> messages = reader.read_message_from_file(binary_file, cachesize=4096)
56
- >>> for message in messages:
114
+ >>> data_file = "01302020.NASDAQ_ITCH50.gz"
115
+ >>> message_type = b"AFE" # Example message type to filter
116
+ >>> parser = MessageParser(message_type=message_type)
117
+ >>> with gzip.open(data_file, "rb") as itch_file:
118
+ >>> message_count = 0
119
+ >>> start_time = time.time()
120
+ >>> for message in parser.parse_file(itch_file):
121
+ >>> message_count += 1
122
+ >>> if message_count <= 5:
57
123
  >>> print(message)
58
- """
59
- max_message_size = 52
60
- file_end_reached = False
61
-
62
- data_buffer = file.read(cachesize)
63
- buffer_len = len(data_buffer)
64
- messages: List[MarketMessage] = []
65
-
66
- while not file_end_reached:
67
- if buffer_len < 2:
68
- new_data = file.read(cachesize)
69
- if not new_data:
70
- break
71
- data_buffer += new_data
72
- buffer_len = len(data_buffer)
73
- continue
124
+ >>> end_time = time.time()
125
+ >>> print(f"Processed {message_count} messages in {end_time - start_time:.2f} seconds")
126
+ >>> print(f"Average time per message: {(end_time - start_time) / message_count:.6f} seconds")
127
+ """
128
+ if not file.readable():
129
+ raise ValueError("file must be opened in binary read mode")
74
130
 
75
- if data_buffer[0:1] != b"\x00":
76
- raise ValueError(
77
- "Unexpected byte: " + str(data_buffer[0:1], encoding="ascii")
78
- )
131
+ if save_file is not None and not save_file.writable():
132
+ raise ValueError("save_file must be opened in binary write mode")
79
133
 
80
- message_len = data_buffer[1]
81
- total_len = 2 + message_len
134
+ data_buffer = b""
135
+ offset = 0
82
136
 
83
- if buffer_len < total_len:
84
- # Wait for more data if message is incomplete
137
+ while True:
138
+ parsed = self._parse_message_from_buffer(memoryview(data_buffer), offset)
139
+ if parsed is None:
140
+ data_buffer = data_buffer[offset:]
141
+ offset = 0
85
142
  new_data = file.read(cachesize)
86
143
  if not new_data:
87
144
  break
88
145
  data_buffer += new_data
89
- buffer_len = len(data_buffer)
90
146
  continue
91
- message_data = data_buffer[2:total_len]
92
- message = self.get_message_type(message_data)
93
-
94
- if message.message_type in self.message_type:
95
- messages.append(message)
96
147
 
97
- if message.message_type == b"S": # System message
98
- if message.event_code == b"C": # End of messages
99
- break
100
-
101
- # Update buffer
102
- data_buffer = data_buffer[total_len:]
103
- buffer_len = len(data_buffer)
148
+ message, total_len = parsed
104
149
 
105
- if buffer_len < max_message_size and not file_end_reached:
106
- new_data = file.read(cachesize)
107
- if not new_data:
108
- file_end_reached = True
109
- else:
110
- data_buffer += new_data
111
- buffer_len = len(data_buffer)
150
+ if message.message_type in self.message_type:
151
+ if save_file is not None:
152
+ msg_len_to_bytes = message.message_size.to_bytes()
153
+ save_file.write(b"\x00" + msg_len_to_bytes + message.to_bytes())
154
+ yield message
155
+
156
+ if (
157
+ message.message_type == b"S"
158
+ and getattr(message, "event_code", b"") == b"C"
159
+ ):
160
+ break
112
161
 
113
- return messages
162
+ offset += total_len
114
163
 
115
- def read_message_from_bytes(self, data: bytes):
164
+ def parse_stream(
165
+ self, data: bytes, save_file: Optional[IO] = None
166
+ ) -> Iterator[MarketMessage]:
116
167
  """
117
168
  Process one or multiple ITCH binary messages from a raw bytes input.
118
169
 
119
170
  Args:
120
171
  data (bytes): Binary blob containing one or more ITCH messages.
121
172
 
122
- Returns:
123
- Queue: A queue containing parsed ITCH message objects.
173
+ save_file (IO, optional):
174
+ A binary file-like object (opened in binary write mode) where filtered messages are saved.
175
+
176
+ Yields:
177
+ MarketMessage:
178
+ The next parsed MarketMessage object from the bytes input.
124
179
 
125
180
  Notes:
126
181
  - Each message must be prefixed with a 0x00 header and a length byte.
127
182
  - No buffering is done here — this is meant for real-time decoding.
128
183
  """
184
+ if not isinstance(data, (bytes, bytearray)):
185
+ raise TypeError("data must be bytes or bytearray, not " + str(type(data)))
186
+
187
+ if save_file is not None and not save_file.writable():
188
+ raise ValueError("save_file must be opened in binary write mode")
129
189
 
130
190
  offset = 0
131
- messages = Queue()
132
- while offset + 2 <= len(data):
133
- # Each message starts with: 1-byte header (0x00) 1-byte length
134
- if data[offset : offset + 1] != b"\x00":
135
- raise ValueError(
136
- f"Unexpected start byte at offset {offset}: "
137
- f"{str(data[offset : offset + 1], encoding='ascii')}"
138
- )
139
-
140
- msg_len = data[offset + 1]
141
- total_len = 2 + msg_len
142
-
143
- if offset + total_len > len(data):
191
+ data_view = memoryview(data)
192
+
193
+ while True:
194
+ parsed = self._parse_message_from_buffer(data_view, offset)
195
+ if parsed is None:
144
196
  break
145
197
 
146
- raw_msg = data[offset + 2 : offset + total_len]
147
- message = self.get_message_type(raw_msg)
198
+ message, total_len = parsed
148
199
 
149
200
  if message.message_type in self.message_type:
150
- messages.put(message)
151
-
152
- if message.message_type == b"S": # System message
153
- if message.event_code == b"C": # End of messages
154
- break
201
+ if save_file is not None:
202
+ msg_len_to_bytes = message.message_size.to_bytes()
203
+ save_file.write(b"\x00" + msg_len_to_bytes + message.to_bytes())
204
+ yield message
205
+
206
+ if (
207
+ message.message_type == b"S"
208
+ and getattr(message, "event_code", b"") == b"C"
209
+ ):
210
+ break
155
211
 
156
212
  offset += total_len
157
213
 
158
- return messages
159
-
160
- def get_message_type(self, message: bytes) -> Type[MarketMessage]:
214
+ def parse_messages(
215
+ self,
216
+ data: BinaryIO | bytes | bytearray,
217
+ callback: Callable[[MarketMessage], None],
218
+ ) -> None:
161
219
  """
162
- Take an entire bytearray and return the appropriate ITCH message
163
- instance based on the message type indicator (first byte of the message).
164
-
165
- All message type indicators are single ASCII characters.
220
+ Parses messages from data and invokes a callback for each message.
166
221
  """
167
- message_type = message[0:1]
168
- try:
169
- return msgs[message_type](message)
170
- except Exception:
171
- raise ValueError(
172
- f"Unknown message type: {message_type.decode(encoding='ascii')}"
173
- )
222
+ parser_func = (
223
+ self.parse_stream
224
+ if isinstance(data, (bytes, bytearray))
225
+ else self.parse_file
226
+ )
227
+ for message in parser_func(data):
228
+ callback(message)
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: itchfeed
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: Simple parser for ITCH messages
5
- Home-page: https://github.com/bbalouki/itch
6
- Download-URL: https://pypi.org/project/itchfeed/
7
- Author: Bertin Balouki SIMYELI
8
- Author-email: <bertin@bbstrader.com>
9
- Maintainer: Bertin Balouki SIMYELI
10
- License: The MIT License (MIT)
5
+ Author-email: Bertin Balouki SIMYELI <bertin@bbs-trading.com>
6
+ Maintainer-email: Bertin Balouki SIMYELI <bertin@bbs-trading.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/bbalouki/itch
9
+ Project-URL: Download, https://pypi.org/project/itchfeed/
11
10
  Project-URL: Source Code, https://github.com/bbalouki/itch
12
11
  Keywords: Finance,Financial,Quantitative,Equities,Totalview-ITCH,Totalview,Nasdaq-ITCH,Nasdaq,ITCH,Data,Feed,ETFs,Funds,Trading,Investing
13
12
  Classifier: Development Status :: 5 - Production/Stable
@@ -19,20 +18,7 @@ Classifier: Operating System :: OS Independent
19
18
  Description-Content-Type: text/markdown
20
19
  License-File: LICENSE
21
20
  Requires-Dist: pytest
22
- Dynamic: author
23
- Dynamic: author-email
24
- Dynamic: classifier
25
- Dynamic: description
26
- Dynamic: description-content-type
27
- Dynamic: download-url
28
- Dynamic: home-page
29
- Dynamic: keywords
30
- Dynamic: license
31
21
  Dynamic: license-file
32
- Dynamic: maintainer
33
- Dynamic: project-url
34
- Dynamic: requires-dist
35
- Dynamic: summary
36
22
 
37
23
  # Nasdaq TotalView-ITCH 5.0 Parser
38
24
  [![PYPI Version](https://img.shields.io/pypi/v/itchfeed)](https://pypi.org/project/itchfeed/)
@@ -59,7 +45,7 @@ Dynamic: summary
59
45
  * [Data Representation](#data-representation)
60
46
  * [Common Attributes of `MarketMessage`](#common-attributes-of-marketmessage)
61
47
  * [Common Methods of `MarketMessage`](#common-methods-of-marketmessage)
62
- * [Serializing Messages with `pack()`](#serializing-messages-with-pack)
48
+ * [Serializing Messages with `to_bytes()`](#serializing-messages-with-to_bytes)
63
49
  * [Data Types in Parsed Messages](#data-types-in-parsed-messages)
64
50
  * [Error Handling](#error-handling)
65
51
  * [Handling Strategies](#handling-strategies)
@@ -111,6 +97,8 @@ After installation (typically via pip), import the necessary modules directly in
111
97
 
112
98
  ## Usage
113
99
 
100
+ Download some sample data [here](https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/)
101
+
114
102
  ### Parsing from a Binary File
115
103
 
116
104
  This is useful for processing historical ITCH data stored in files. The `MessageParser` handles buffering efficiently.
@@ -131,9 +119,8 @@ parser = MessageParser() # Parses all messages by default
131
119
 
132
120
  # Path to your ITCH 5.0 data file
133
121
  itch_file_path = 'path/to/your/data'
134
- # you can find sample data [here](https://emi.nasdaq.com/ITCH/Nasdaq%20ITCH/)
135
122
 
136
- # The `read_message_from_file()` method reads the ITCH data in chunks.
123
+ # The `parse_file()` method reads the ITCH data in chunks.
137
124
  # - `cachesize` (optional, default: 65536 bytes): This parameter determines the size of data chunks
138
125
  # read from the file at a time. Adjusting this might impact performance for very large files
139
126
  # or memory usage, but the default is generally suitable.
@@ -144,13 +131,9 @@ itch_file_path = 'path/to/your/data'
144
131
 
145
132
  try:
146
133
  with open(itch_file_path, 'rb') as itch_file:
147
- # read_message_from_file returns a list of parsed message objects
148
- parsed_messages = parser.read_message_from_file(itch_file) # You can also pass cachesize here, e.g., parser.read_message_from_file(itch_file, cachesize=131072)
149
-
150
- print(f"Parsed {len(parsed_messages)} messages.")
151
-
134
+ # parse_file returns an Iterator of parsed message objects
152
135
  # Process the messages
153
- for message in parsed_messages:
136
+ for message in parser.parse_file(itch_file):
154
137
  # Access attributes directly
155
138
  print(f"Type: {message.message_type.decode()}, Timestamp: {message.timestamp}")
156
139
 
@@ -170,8 +153,8 @@ try:
170
153
 
171
154
  except FileNotFoundError:
172
155
  print(f"Error: File not found at {itch_file_path}")
173
- except Exception as e:
174
- print(f"An error occurred: {e}")
156
+ except ValueError as e:
157
+ print(f"An error occurred during parsing: {e}")
175
158
 
176
159
  ```
177
160
 
@@ -192,14 +175,8 @@ parser = MessageParser()
192
175
  # Example: \x00\x0bS...\x00\x25R...\x00\x27F...
193
176
  raw_binary_data: bytes = b"..." # Your raw ITCH 5.0 data chunk
194
177
 
195
- # read_message_from_bytes returns a queue of parsed message objects
196
- message_queue: Queue = parser.read_message_from_bytes(raw_binary_data)
197
-
198
- print(f"Parsed {message_queue.qsize()} messages from the byte chunk.")
199
-
200
- # Process messages from the queue
201
- while not message_queue.empty():
202
- message = message_queue.get()
178
+ # parse_stream returns an Iterator of parsed message objects
179
+ for message in parser.parse_stream(raw_binary_data)
203
180
 
204
181
  print(f"Type: {message.message_type.decode()}, Timestamp: {message.timestamp}")
205
182
 
@@ -276,10 +253,10 @@ for message_type, sample_data in TEST_DATA.items():
276
253
  print(f"Creating message of type {message_type}")
277
254
  message = create_message(message_type, **sample_data)
278
255
  print(f"Created message: {message}")
279
- print(f"Packed message: {message.pack()}")
256
+ print(f"Packed message: {message.to_bytes()}")
280
257
  print(f"Message size: {message.message_size}")
281
258
  print(f"Message Attributes: {message.get_attributes()}")
282
- assert len(message.pack()) == message.message_size
259
+ assert len(message.to_bytes()) == message.message_size
283
260
  print()
284
261
  ```
285
262
 
@@ -388,14 +365,14 @@ The `MarketMessage` base class, and therefore all specific message classes, prov
388
365
  * Returns a dictionary of all attributes (fields) of the message instance, along with their current values.
389
366
  * This can be useful for generic inspection or logging of message contents without needing to know the specific type of the message beforehand.
390
367
 
391
- ### Serializing Messages with `pack()`
368
+ ### Serializing Messages with `to_bytes()`
392
369
 
393
- Each specific message class (e.g., `SystemEventMessage`, `AddOrderNoMPIAttributionMessage`) also provides a `pack()` method. This method is the inverse of the parsing process.
370
+ Each specific message class (e.g., `SystemEventMessage`, `AddOrderNoMPIAttributionMessage`) also provides a `to_bytes()` method. This method is the inverse of the parsing process.
394
371
 
395
372
  * **Purpose:** It serializes the message object, with its current attribute values, back into its raw ITCH 5.0 binary format. The output is a `bytes` object representing the exact byte sequence that would appear in an ITCH data feed for that message.
396
373
  * **Usefulness:**
397
374
  * **Generating Test Data:** Create custom ITCH messages for testing your own ITCH processing applications.
398
- * **Modifying Messages:** Parse an existing message, modify some of its attributes, and then `pack()` it back into binary form.
375
+ * **Modifying Messages:** Parse an existing message, modify some of its attributes, and then `to_bytes()` it back into binary form.
399
376
  * **Creating Custom ITCH Feeds:** While more involved, you could use this to construct sequences of ITCH messages for specialized scenarios.
400
377
 
401
378
  **Example:**
@@ -415,21 +392,21 @@ import time
415
392
  # for packing requires setting attributes manually if not using raw bytes for construction)
416
393
 
417
394
  event_msg = SystemEventMessage.__new__(SystemEventMessage) # Create instance without calling __init__
418
- event_msg.message_type = b'S' # Must be set for pack() to know its type
395
+ event_msg.message_type = b'S' # Must be set for to_bytes() to know its type
419
396
  event_msg.stock_locate = 0 # Placeholder or actual value
420
397
  event_msg.tracking_number = 0 # Placeholder or actual value
421
398
  event_msg.event_code = b'O' # Example: Start of Messages
422
399
 
423
400
  # 2. Set the timestamp.
424
401
  # The `timestamp` attribute (nanoseconds since midnight) must be set.
425
- # The `pack()` method will internally use `split_timestamp()` to get the parts.
402
+ # The `to_bytes()` method will internally use `split_timestamp()` to get the parts.
426
403
  current_nanoseconds = int(time.time() * 1e9) % (24 * 60 * 60 * int(1e9))
427
404
  event_msg.timestamp = current_nanoseconds # Directly set the nanosecond timestamp
428
405
 
429
406
  # 3. Pack the message into binary format.
430
- # The pack() method prepends the message type and then packs stock_locate,
407
+ # The to_bytes() method prepends the message type and then packs stock_locate,
431
408
  # tracking_number, the split timestamp, and then the message-specific fields.
432
- packed_bytes = event_msg.pack()
409
+ packed_bytes = event_msg.to_bytes()
433
410
 
434
411
  # 4. The result is a bytes object
435
412
  print(f"Packed {len(packed_bytes)} bytes: {packed_bytes.hex().upper()}")
@@ -462,11 +439,12 @@ Common scenarios that can lead to a `ValueError` include:
462
439
 
463
440
  It's crucial to anticipate these errors in your application:
464
441
 
465
- * **Use `try-except` Blocks:** Wrap your parsing calls (especially `read_message_from_file` or `read_message_from_bytes`) in `try-except ValueError as e:` blocks.
442
+ * **Use `try-except` Blocks:** Wrap your parsing calls (especially `parse_file` or `parse_stream`) in `try-except ValueError as e:` blocks.
466
443
  ```python
467
444
  try:
468
445
  # ... parsing operations ...
469
- messages = parser.read_message_from_file(itch_file)
446
+ for message in parser.parse_file(itch_file):
447
+ ...
470
448
  except ValueError as e:
471
449
  print(f"An error occurred during parsing: {e}")
472
450
  # Log the error, problematic data chunk, or take other actions
@@ -0,0 +1,9 @@
1
+ itch/__init__.py,sha256=ykG85u8WbMZnQt78wg-a4Zqm6dTGNf3lYRsqh5FOuPI,348
2
+ itch/indicators.py,sha256=-Ed2M8I60xGQ1bIPZCGCKGb8ayT87JAnIaosfiBimXI,6542
3
+ itch/messages.py,sha256=bXEcD9h0ZsD9IXVMfQa7FWZTpgiQoyeSdxZ9hrp3ukQ,64847
4
+ itch/parser.py,sha256=1Skl8SBQz_SDqHEDkFHydJw0TEt3M_xNcUMAE19yquo,8442
5
+ itchfeed-1.0.6.dist-info/licenses/LICENSE,sha256=f2u79rUzh-UcYH0RN0Ph0VvVYHBkYlVxtguhKmrHqsw,1089
6
+ itchfeed-1.0.6.dist-info/METADATA,sha256=ZPgCxVsjBIAXxdkllrx57IrP-wE0oxkgafIZaSsxm0Q,33174
7
+ itchfeed-1.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ itchfeed-1.0.6.dist-info/top_level.txt,sha256=xwsOYShvy3gc1rfyitCTgSxBZDGG1y6bfQxkdhIGmEM,5
9
+ itchfeed-1.0.6.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- itch/__init__.py,sha256=M9Jirj4-XXdaCoTcU2_g89z7JHK8mDtJflTFf9HnU-k,205
2
- itch/indicators.py,sha256=-Ed2M8I60xGQ1bIPZCGCKGb8ayT87JAnIaosfiBimXI,6542
3
- itch/messages.py,sha256=lm5pKQ00ETMbTij9HqaYVrN08JJdQe-mZKB_C7IAetU,63934
4
- itch/parser.py,sha256=BOrkGsmRkcYnXSf5S9yqrwzP0jqtkH5mGCpWCeiWNTg,6155
5
- itchfeed-1.0.4.dist-info/licenses/LICENSE,sha256=f2u79rUzh-UcYH0RN0Ph0VvVYHBkYlVxtguhKmrHqsw,1089
6
- itchfeed-1.0.4.dist-info/METADATA,sha256=HYjteeevesrGUcrjvuNlW2P-0B9k8jtL0qdovoaLZT0,33793
7
- itchfeed-1.0.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- itchfeed-1.0.4.dist-info/top_level.txt,sha256=xwsOYShvy3gc1rfyitCTgSxBZDGG1y6bfQxkdhIGmEM,5
9
- itchfeed-1.0.4.dist-info/RECORD,,