Skip to content

Commit de8870c

Browse files
authored
Merge pull request #55 from vgreg/25-add-support-for-itch-41-format-2
2 parents fed1a21 + cdfaf74 commit de8870c

25 files changed

Lines changed: 4191 additions & 355 deletions
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Listing Symbols in an ITCH 4.1 File
2+
3+
This script shows how to extract all available symbols from an ITCH 4.1 file.
4+
Based on the pattern from the MeatPy documentation notebooks.
5+
"""
6+
7+
from pathlib import Path
8+
from meatpy.itch41 import ITCH41MessageReader
9+
10+
# Define the path to our sample data file
11+
script_dir = Path(__file__).parent
12+
data_dir = script_dir / "data"
13+
file_path = data_dir / "S083012-v41.txt.gz"
14+
15+
print(f"Reading ITCH 4.1 file: {file_path}")
16+
if file_path.exists():
17+
file_size_mb = file_path.stat().st_size / (1024**2)
18+
print(f"File size: {file_size_mb:.2f} MB")
19+
else:
20+
print("⚠️ Sample file not found - this is expected in most setups")
21+
print("You can download ITCH 4.1 sample files or use your own data")
22+
exit(1)
23+
24+
symbols = set()
25+
message_count = 0
26+
27+
print("Reading ITCH 4.1 file to extract symbols...")
28+
29+
with ITCH41MessageReader(file_path) as reader:
30+
for message in reader:
31+
message_count += 1
32+
33+
# Stock Directory messages (type 'R') contain symbol information
34+
if message.type == b"R":
35+
symbol = message.stock.decode().strip()
36+
symbols.add(symbol)
37+
38+
# For ITCH 4.1, we can break early since stock directory messages
39+
# typically appear at the beginning of the file
40+
if message_count >= 50000:
41+
break
42+
43+
print(f"Found {len(symbols)} symbols after processing {message_count:,} messages")
44+
45+
symbols = sorted(symbols)
46+
47+
# Display first 20 symbols
48+
print("\nFirst 20 symbols:")
49+
for symbol in symbols[:20]:
50+
print(f" {symbol}")
51+
52+
if len(symbols) > 20:
53+
print(f" ... and {len(symbols) - 20} more")
54+
55+
# Save symbols to file
56+
output_file = data_dir / "itch41_symbols.txt"
57+
with open(output_file, "w") as f:
58+
for symbol in symbols:
59+
f.write(f"{symbol}\n")
60+
61+
print(f"\n✅ Symbols saved to: {output_file}")

samples/itch41/Step1_Parsing.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Extracting Specific Symbols from ITCH 4.1 File
2+
3+
This script demonstrates how to create a new ITCH 4.1 file containing only data
4+
for specific symbols of interest. Based on the pattern from MeatPy documentation.
5+
"""
6+
7+
from pathlib import Path
8+
9+
from meatpy.itch41 import ITCH41MessageReader, ITCH41Writer
10+
11+
# Define paths
12+
data_dir = Path("data")
13+
input_file = data_dir / "S083012-v41.txt.gz"
14+
output_file = data_dir / "S083012-v41-AAPL-SPY.itch41.gz"
15+
16+
# Symbols we want to extract
17+
target_symbols = ["AAPL", "SPY"]
18+
19+
print(f"Input file: {input_file}")
20+
if input_file.exists():
21+
input_size_mb = input_file.stat().st_size / (1024**2)
22+
print(f"Input file size: {input_size_mb:.2f} MB")
23+
else:
24+
print("⚠️ Sample file not found - this is expected in most setups")
25+
print("You can download ITCH 4.1 sample files or use your own data")
26+
exit(1)
27+
28+
print(f"Extracting symbols: {target_symbols}")
29+
print(f"Output file: {output_file}")
30+
31+
# Process the file and filter for target symbols
32+
message_count = 0
33+
with (
34+
ITCH41MessageReader(input_file) as reader,
35+
ITCH41Writer(output_file, symbols=target_symbols) as writer,
36+
):
37+
for message in reader:
38+
message_count += 1
39+
writer.process_message(message)
40+
41+
# Progress update every 10,000 messages
42+
if message_count % 10000 == 0:
43+
print(f"Processed {message_count:,} messages...")
44+
45+
print(f"Total messages processed: {message_count:,}")
46+
47+
# # Check the filtered file
48+
# if output_file.exists():
49+
# new_message_count = 0
50+
# with ITCH41MessageReader(output_file) as reader:
51+
# for message in reader:
52+
# print(message)
53+
# new_message_count += 1
54+
55+
# print(f"Total messages in filtered file: {new_message_count:,}")
56+
# output_size_mb = output_file.stat().st_size / (1024**2)
57+
# print(f"Output file size: {output_size_mb:.2f} MB")
58+
59+
# size_reduction = (1 - output_size_mb / input_size_mb) * 100
60+
# print(f"Size reduction: {size_reduction:.1f}%")
61+
62+
# print(f"\n✅ Filtered file created: {output_file}")
63+
# else:
64+
# print("❌ Failed to create output file")

samples/itch41/Step2_Processing.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import datetime
2+
from pathlib import Path
3+
4+
from meatpy.event_handlers.lob_recorder import LOBRecorder
5+
from meatpy.itch41 import ITCH41MarketProcessor, ITCH41MessageReader
6+
from meatpy.lob import ExecutionPriorityExceptionList
7+
from meatpy.writers.parquet_writer import ParquetWriter
8+
9+
# Define paths and parameters
10+
data_dir = Path("data")
11+
12+
file_path = data_dir / "S083012-v41-AAPL-SPY.itch41.gz"
13+
outfile_path = data_dir / "spy_tob.parquet"
14+
book_date = datetime.datetime(2012, 8, 30)
15+
16+
17+
with ITCH41MessageReader(file_path) as reader, ParquetWriter(outfile_path) as writer:
18+
processor = ITCH41MarketProcessor("SPY", book_date)
19+
20+
# We only care about the top of book
21+
tob_recorder = LOBRecorder(writer=writer, max_depth=1)
22+
# Generate a list of timedeltas from 9:30 to 16:00 (inclusive) in 1-minute increments
23+
market_open = book_date + datetime.timedelta(hours=9, minutes=30)
24+
market_close = book_date + datetime.timedelta(hours=16, minutes=0)
25+
record_timestamps = [
26+
market_open + datetime.timedelta(minutes=i)
27+
for i in range(int((market_close - market_open).total_seconds() // 60) + 1)
28+
]
29+
tob_recorder.record_timestamps = record_timestamps
30+
31+
# Attach the recorders to the processor
32+
processor.handlers.append(tob_recorder)
33+
34+
message_types = set()
35+
36+
for i, message in enumerate(reader):
37+
# if i % 10_000 == 0:
38+
# print(f"Processing message {i:,}...")
39+
if message.type != b"T":
40+
print(message)
41+
try:
42+
processor.process_message(message)
43+
except ExecutionPriorityExceptionList as e:
44+
print(f"Execution priority exception: {e}")
45+
message_types.add(message.type)
46+
47+
print(f"Processed {i + 1:,} messages.")
48+
print(f"Message types: {message_types}")

src/meatpy/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,18 @@
6262
"TradeRef",
6363
"Qualifiers",
6464
]
65+
66+
# ITCH format imports (available when format-specific modules are imported)
67+
try:
68+
from . import itch41 # noqa: F401
69+
70+
__all__.extend(["itch41"])
71+
except ImportError:
72+
pass
73+
74+
try:
75+
from . import itch50 # noqa: F401
76+
77+
__all__.extend(["itch50"])
78+
except ImportError:
79+
pass

src/meatpy/itch41/__init__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""ITCH 4.1 market data subpackage.
2+
3+
This package provides message types, parsers, processors, and recorders for handling ITCH 4.1 market data in MeatPy.
4+
"""
5+
6+
from .itch41_exec_trade_recorder import ITCH41ExecTradeRecorder
7+
from .itch41_market_message import (
8+
AddOrderMessage,
9+
AddOrderMPIDMessage,
10+
BrokenTradeMessage,
11+
CrossTradeMessage,
12+
MarketParticipantPositionMessage,
13+
OrderCancelMessage,
14+
OrderDeleteMessage,
15+
OrderExecutedMessage,
16+
OrderExecutedPriceMessage,
17+
OrderReplaceMessage,
18+
RegSHOMessage,
19+
SecondsMessage,
20+
StockDirectoryMessage,
21+
StockTradingActionMessage,
22+
SystemEventMessage,
23+
TradeMessage,
24+
)
25+
from .itch41_market_processor import ITCH41MarketProcessor
26+
from .itch41_message_reader import ITCH41MessageReader
27+
from .itch41_ofi_recorder import ITCH41OFIRecorder
28+
from .itch41_order_event_recorder import ITCH41OrderEventRecorder
29+
from .itch41_top_of_book_message_recorder import (
30+
ITCH41TopOfBookMessageRecorder,
31+
)
32+
from .itch41_writer import ITCH41Writer
33+
34+
__all__ = [
35+
"ITCH41ExecTradeRecorder",
36+
"ITCH41MarketMessage",
37+
"ITCH41MarketProcessor",
38+
"ITCH41MessageParser",
39+
"ITCH41MessageReader",
40+
"ITCH41Writer",
41+
"ITCH41OFIRecorder",
42+
"ITCH41OrderEventRecorder",
43+
"ITCH41TopOfBookMessageRecorder",
44+
"AddOrderMessage",
45+
"AddOrderMPIDMessage",
46+
"BrokenTradeMessage",
47+
"CrossTradeMessage",
48+
"MarketParticipantPositionMessage",
49+
"OrderCancelMessage",
50+
"OrderDeleteMessage",
51+
"OrderExecutedMessage",
52+
"OrderExecutedPriceMessage",
53+
"OrderReplaceMessage",
54+
"RegSHOMessage",
55+
"SecondsMessage",
56+
"StockDirectoryMessage",
57+
"StockTradingActionMessage",
58+
"SystemEventMessage",
59+
"TradeMessage",
60+
]
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"""ITCH 4.1 execution trade recorder for limit order books.
2+
3+
This module provides the ITCH41ExecTradeRecorder class, which records trade
4+
executions from ITCH 4.1 market data and exports them to CSV files.
5+
"""
6+
7+
from typing import Any
8+
9+
from ..market_event_handler import MarketEventHandler
10+
from .itch41_market_message import (
11+
OrderExecutedMessage,
12+
OrderExecutedPriceMessage,
13+
TradeMessage,
14+
)
15+
16+
17+
class ITCH41ExecTradeRecorder(MarketEventHandler):
18+
"""Records trade executions from ITCH 4.1 market data.
19+
20+
This recorder detects and records trade executions, including both
21+
visible and hidden trades, and exports them to CSV format.
22+
23+
Attributes:
24+
records: List of recorded trade execution records
25+
"""
26+
27+
def __init__(self) -> None:
28+
"""Initialize the ITCH41ExecTradeRecorder."""
29+
self.records: list[Any] = []
30+
31+
def message_event(
32+
self,
33+
market_processor,
34+
timestamp,
35+
message,
36+
) -> None:
37+
"""Detect messages that represent trade executions and record them.
38+
39+
Args:
40+
market_processor: The market processor instance
41+
timestamp: The timestamp of the message
42+
message: The market message to process
43+
"""
44+
lob = market_processor.lob
45+
if lob is None:
46+
return
47+
48+
if isinstance(message, OrderExecutedMessage):
49+
# An executed order will ALWAYS be against top of book
50+
# because of price priority, so record.
51+
if lob.ask_order_on_book(message.order_ref):
52+
record = {
53+
"MessageType": "Exec",
54+
"Volume": message.shares,
55+
"OrderID": message.order_ref,
56+
}
57+
record["Queue"] = "Ask"
58+
record["Price"] = lob.ask_levels[0].price
59+
try:
60+
(queue, i, j) = lob.find_order(message.order_ref)
61+
record["OrderTimestamp"] = queue[i].queue[j].timestamp
62+
except Exception:
63+
record["OrderTimestamp"] = ""
64+
self.records.append((timestamp, record))
65+
elif lob.bid_order_on_book(message.order_ref):
66+
record = {
67+
"MessageType": "Exec",
68+
"Volume": message.shares,
69+
"OrderID": message.order_ref,
70+
}
71+
record["Queue"] = "Bid"
72+
record["Price"] = lob.bid_levels[0].price
73+
try:
74+
(queue, i, j) = lob.find_order(message.order_ref)
75+
record["OrderTimestamp"] = queue[i].queue[j].timestamp
76+
except Exception:
77+
record["OrderTimestamp"] = ""
78+
self.records.append((timestamp, record))
79+
elif isinstance(message, TradeMessage):
80+
if message.side == b"S":
81+
record = {
82+
"MessageType": "ExecHid",
83+
"Volume": message.shares,
84+
"OrderID": "",
85+
"OrderTimestamp": "",
86+
}
87+
record["Queue"] = "Ask"
88+
record["Price"] = message.price
89+
self.records.append((timestamp, record))
90+
elif message.side == b"B":
91+
record = {
92+
"MessageType": "ExecHid",
93+
"Volume": message.shares,
94+
"OrderID": "",
95+
"OrderTimestamp": "",
96+
}
97+
record["Queue"] = "Bid"
98+
record["Price"] = message.price
99+
self.records.append((timestamp, record))
100+
elif isinstance(message, OrderExecutedPriceMessage):
101+
if len(lob.ask_levels) > 0 and lob.ask_levels[0].order_on_book(
102+
message.order_ref
103+
):
104+
record = {
105+
"MessageType": "ExecPrice",
106+
"Queue": "Ask",
107+
"Volume": message.shares,
108+
"OrderID": message.order_ref,
109+
"Price": message.price,
110+
}
111+
try:
112+
(queue, i, j) = lob.find_order(message.order_ref)
113+
record["OrderTimestamp"] = queue[i].queue[j].timestamp
114+
except Exception:
115+
record["OrderTimestamp"] = ""
116+
self.records.append((timestamp, record))
117+
elif len(lob.bid_levels) > 0 and lob.bid_levels[0].order_on_book(
118+
message.order_ref
119+
):
120+
record = {
121+
"MessageType": "ExecPrice",
122+
"Queue": "Bid",
123+
"Volume": message.shares,
124+
"OrderID": message.order_ref,
125+
"Price": message.price,
126+
}
127+
try:
128+
(queue, i, j) = lob.find_order(message.order_ref)
129+
record["OrderTimestamp"] = queue[i].queue[j].timestamp
130+
except Exception:
131+
record["OrderTimestamp"] = ""
132+
self.records.append((timestamp, record))

0 commit comments

Comments
 (0)