itchfeed 1.0.5__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 +8 -2
- itch/messages.py +2 -4
- itch/parser.py +105 -69
- {itchfeed-1.0.5.dist-info → itchfeed-1.0.6.dist-info}/METADATA +16 -38
- itchfeed-1.0.6.dist-info/RECORD +9 -0
- itchfeed-1.0.5.dist-info/RECORD +0 -9
- {itchfeed-1.0.5.dist-info → itchfeed-1.0.6.dist-info}/WHEEL +0 -0
- {itchfeed-1.0.5.dist-info → itchfeed-1.0.6.dist-info}/licenses/LICENSE +0 -0
- {itchfeed-1.0.5.dist-info → itchfeed-1.0.6.dist-info}/top_level.txt +0 -0
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@
|
|
7
|
+
__email__ = "bertin@bbs-trading.com"
|
|
8
8
|
__license__ = "MIT"
|
|
9
|
-
|
|
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
|
@@ -46,7 +46,7 @@ class MarketMessage(object):
|
|
|
46
46
|
def __bytes__(self) -> bytes:
|
|
47
47
|
return self.to_bytes()
|
|
48
48
|
|
|
49
|
-
def to_bytes(self) -> bytes:
|
|
49
|
+
def to_bytes(self) -> bytes: # type: ignore
|
|
50
50
|
"""
|
|
51
51
|
Packs the message into bytes using the defined message_pack_format.
|
|
52
52
|
This method should be overridden by subclasses to include specific fields.
|
|
@@ -109,9 +109,7 @@ class MarketMessage(object):
|
|
|
109
109
|
ts1 = self.timestamp >> 32
|
|
110
110
|
ts2 = self.timestamp - (ts1 << 32)
|
|
111
111
|
return (ts1, ts2)
|
|
112
|
-
|
|
113
|
-
ts2 = self.timestamp - (ts1 << 32)
|
|
114
|
-
return (ts1, ts2)
|
|
112
|
+
|
|
115
113
|
|
|
116
114
|
def decode_price(self, price_attr: str) -> float:
|
|
117
115
|
precision = getattr(self, "price_precision")
|
itch/parser.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import IO, BinaryIO, Iterator
|
|
1
|
+
from typing import IO, BinaryIO, Callable, Iterator, Optional, Tuple
|
|
2
2
|
|
|
3
3
|
from itch.messages import MESSAGES, MarketMessage
|
|
4
4
|
from itch.messages import messages as msgs
|
|
@@ -13,8 +13,70 @@ class MessageParser(object):
|
|
|
13
13
|
def __init__(self, message_type: bytes = MESSAGES):
|
|
14
14
|
self.message_type = message_type
|
|
15
15
|
|
|
16
|
-
def
|
|
17
|
-
|
|
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
|
|
18
80
|
) -> Iterator[MarketMessage]:
|
|
19
81
|
"""
|
|
20
82
|
Reads and parses market messages from a binary file-like object.
|
|
@@ -55,7 +117,7 @@ class MessageParser(object):
|
|
|
55
117
|
>>> with gzip.open(data_file, "rb") as itch_file:
|
|
56
118
|
>>> message_count = 0
|
|
57
119
|
>>> start_time = time.time()
|
|
58
|
-
>>> for message in parser.
|
|
120
|
+
>>> for message in parser.parse_file(itch_file):
|
|
59
121
|
>>> message_count += 1
|
|
60
122
|
>>> if message_count <= 5:
|
|
61
123
|
>>> print(message)
|
|
@@ -66,46 +128,24 @@ class MessageParser(object):
|
|
|
66
128
|
if not file.readable():
|
|
67
129
|
raise ValueError("file must be opened in binary read mode")
|
|
68
130
|
|
|
69
|
-
if save_file is not None:
|
|
70
|
-
|
|
71
|
-
raise ValueError("save_file must be opened in binary write mode")
|
|
131
|
+
if save_file is not None and not save_file.writable():
|
|
132
|
+
raise ValueError("save_file must be opened in binary write mode")
|
|
72
133
|
|
|
73
134
|
data_buffer = b""
|
|
74
135
|
offset = 0
|
|
75
136
|
|
|
76
137
|
while True:
|
|
77
|
-
|
|
138
|
+
parsed = self._parse_message_from_buffer(memoryview(data_buffer), offset)
|
|
139
|
+
if parsed is None:
|
|
78
140
|
data_buffer = data_buffer[offset:]
|
|
79
141
|
offset = 0
|
|
80
|
-
new_data = file.read(cachesize)
|
|
81
|
-
if not new_data:
|
|
82
|
-
break
|
|
83
|
-
data_buffer += new_data
|
|
84
|
-
|
|
85
|
-
if len(data_buffer) < 2:
|
|
86
|
-
break
|
|
87
|
-
|
|
88
|
-
if data_buffer[offset : offset + 1] != b"\x00":
|
|
89
|
-
raise ValueError(
|
|
90
|
-
"Unexpected byte: "
|
|
91
|
-
+ str(data_buffer[offset : offset + 1], encoding="ascii")
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
message_len = data_buffer[offset + 1]
|
|
95
|
-
total_len = 2 + message_len
|
|
96
|
-
|
|
97
|
-
if len(data_buffer) - offset < total_len:
|
|
98
|
-
data_buffer = data_buffer[offset:]
|
|
99
|
-
offset = 0
|
|
100
|
-
|
|
101
142
|
new_data = file.read(cachesize)
|
|
102
143
|
if not new_data:
|
|
103
144
|
break
|
|
104
145
|
data_buffer += new_data
|
|
105
146
|
continue
|
|
106
147
|
|
|
107
|
-
|
|
108
|
-
message = self.get_message_type(message_data)
|
|
148
|
+
message, total_len = parsed
|
|
109
149
|
|
|
110
150
|
if message.message_type in self.message_type:
|
|
111
151
|
if save_file is not None:
|
|
@@ -113,13 +153,16 @@ class MessageParser(object):
|
|
|
113
153
|
save_file.write(b"\x00" + msg_len_to_bytes + message.to_bytes())
|
|
114
154
|
yield message
|
|
115
155
|
|
|
116
|
-
if
|
|
117
|
-
|
|
118
|
-
|
|
156
|
+
if (
|
|
157
|
+
message.message_type == b"S"
|
|
158
|
+
and getattr(message, "event_code", b"") == b"C"
|
|
159
|
+
):
|
|
160
|
+
break
|
|
161
|
+
|
|
119
162
|
offset += total_len
|
|
120
163
|
|
|
121
|
-
def
|
|
122
|
-
self, data: bytes, save_file: IO = None
|
|
164
|
+
def parse_stream(
|
|
165
|
+
self, data: bytes, save_file: Optional[IO] = None
|
|
123
166
|
) -> Iterator[MarketMessage]:
|
|
124
167
|
"""
|
|
125
168
|
Process one or multiple ITCH binary messages from a raw bytes input.
|
|
@@ -139,30 +182,20 @@ class MessageParser(object):
|
|
|
139
182
|
- No buffering is done here — this is meant for real-time decoding.
|
|
140
183
|
"""
|
|
141
184
|
if not isinstance(data, (bytes, bytearray)):
|
|
142
|
-
raise TypeError("data must be bytes or bytearray not " + str(type(data)))
|
|
185
|
+
raise TypeError("data must be bytes or bytearray, not " + str(type(data)))
|
|
143
186
|
|
|
144
|
-
if save_file is not None:
|
|
145
|
-
|
|
146
|
-
raise ValueError("save_file must be opened in binary write mode")
|
|
187
|
+
if save_file is not None and not save_file.writable():
|
|
188
|
+
raise ValueError("save_file must be opened in binary write mode")
|
|
147
189
|
|
|
148
190
|
offset = 0
|
|
149
191
|
data_view = memoryview(data)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if
|
|
154
|
-
raise ValueError(
|
|
155
|
-
f"Unexpected start byte at offset {offset:offset+1}: "
|
|
156
|
-
f"{data_view[offset : offset + 1].tobytes()}"
|
|
157
|
-
)
|
|
158
|
-
msg_len = data_view[offset + 1]
|
|
159
|
-
total_len = 2 + msg_len
|
|
160
|
-
|
|
161
|
-
if offset + total_len > data_len:
|
|
192
|
+
|
|
193
|
+
while True:
|
|
194
|
+
parsed = self._parse_message_from_buffer(data_view, offset)
|
|
195
|
+
if parsed is None:
|
|
162
196
|
break
|
|
163
197
|
|
|
164
|
-
|
|
165
|
-
message = self.get_message_type(raw_msg.tobytes())
|
|
198
|
+
message, total_len = parsed
|
|
166
199
|
|
|
167
200
|
if message.message_type in self.message_type:
|
|
168
201
|
if save_file is not None:
|
|
@@ -170,23 +203,26 @@ class MessageParser(object):
|
|
|
170
203
|
save_file.write(b"\x00" + msg_len_to_bytes + message.to_bytes())
|
|
171
204
|
yield message
|
|
172
205
|
|
|
173
|
-
if
|
|
174
|
-
|
|
175
|
-
|
|
206
|
+
if (
|
|
207
|
+
message.message_type == b"S"
|
|
208
|
+
and getattr(message, "event_code", b"") == b"C"
|
|
209
|
+
):
|
|
210
|
+
break
|
|
176
211
|
|
|
177
212
|
offset += total_len
|
|
178
213
|
|
|
179
|
-
def
|
|
214
|
+
def parse_messages(
|
|
215
|
+
self,
|
|
216
|
+
data: BinaryIO | bytes | bytearray,
|
|
217
|
+
callback: Callable[[MarketMessage], None],
|
|
218
|
+
) -> None:
|
|
180
219
|
"""
|
|
181
|
-
|
|
182
|
-
instance based on the message type indicator (first byte of the message).
|
|
183
|
-
|
|
184
|
-
All message type indicators are single ASCII characters.
|
|
220
|
+
Parses messages from data and invokes a callback for each message.
|
|
185
221
|
"""
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
)
|
|
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.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: Simple parser for ITCH messages
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
[](https://pypi.org/project/itchfeed/)
|
|
@@ -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 `
|
|
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
|
-
#
|
|
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
|
|
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
|
|
|
@@ -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
|
-
#
|
|
196
|
-
|
|
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
|
|
|
@@ -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 `
|
|
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
|
-
|
|
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,,
|
itchfeed-1.0.5.dist-info/RECORD
DELETED
|
@@ -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=UWhpFzgfaDwAah-R0p24LtpP7-G5pP1jnOMakBc33t4,64935
|
|
4
|
-
itch/parser.py,sha256=glYjyeqY9841UqcOD8Wck-Az70AUwQxTt9SckYoxOg0,7422
|
|
5
|
-
itchfeed-1.0.5.dist-info/licenses/LICENSE,sha256=f2u79rUzh-UcYH0RN0Ph0VvVYHBkYlVxtguhKmrHqsw,1089
|
|
6
|
-
itchfeed-1.0.5.dist-info/METADATA,sha256=gUXQ25Q5Cvw6EHGVLUDuTXFOTdUSp8VQEIyKUOd8LU8,33853
|
|
7
|
-
itchfeed-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
-
itchfeed-1.0.5.dist-info/top_level.txt,sha256=xwsOYShvy3gc1rfyitCTgSxBZDGG1y6bfQxkdhIGmEM,5
|
|
9
|
-
itchfeed-1.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|