use crate::{Event, OrderId, Price, Side, TimeInForce};
use std::io::{Read, Result};
#[derive(Debug, Clone, PartialEq)]
pub enum ItchMessage {
AddOrder {
timestamp: u64,
order_ref: u64,
side: Side,
shares: u32,
stock: String,
price: u32,
},
OrderExecuted {
timestamp: u64,
order_ref: u64,
shares: u32,
match_number: u64,
},
OrderExecutedWithPrice {
timestamp: u64,
order_ref: u64,
shares: u32,
match_number: u64,
printable: bool,
price: u32,
},
OrderCancel {
timestamp: u64,
order_ref: u64,
shares: u32,
},
OrderDelete {
timestamp: u64,
order_ref: u64,
},
OrderReplace {
timestamp: u64,
old_order_ref: u64,
new_order_ref: u64,
shares: u32,
price: u32,
},
Trade {
timestamp: u64,
side: Side,
shares: u32,
stock: String,
price: u32,
match_number: u64,
},
StockDirectory {
stock: String,
locate: u16,
},
Other(char),
}
pub struct ItchParser<R: Read> {
reader: R,
stock_locates: std::collections::HashMap<u16, String>,
}
impl<R: Read> ItchParser<R> {
pub fn new(reader: R) -> Self {
Self {
reader,
stock_locates: std::collections::HashMap::new(),
}
}
pub fn next_message(&mut self) -> Result<Option<ItchMessage>> {
let mut len_buf = [0u8; 2];
if self.reader.read_exact(&mut len_buf).is_err() {
return Ok(None);
}
let len = u16::from_be_bytes(len_buf) as usize;
if len == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"ITCH message length is 0",
));
}
let mut msg_buf = vec![0u8; len];
self.reader.read_exact(&mut msg_buf)?;
let msg_type = msg_buf[0] as char;
let payload = &msg_buf[1..];
let min_payload = match msg_type {
'A' | 'F' => 35, 'E' => 30, 'C' => 35, 'X' => 22, 'D' => 18, 'U' => 34, 'P' => 43, 'R' => 10, _ => 0,
};
if payload.len() < min_payload {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"ITCH '{}' message too short: {} bytes, need {}",
msg_type,
payload.len(),
min_payload,
),
));
}
match msg_type {
'A' | 'F' => {
let timestamp = read_u48_be(&payload[4..10]);
let order_ref = u64::from_be_bytes(payload[10..18].try_into().unwrap());
let side = if payload[18] == b'B' {
Side::Buy
} else {
Side::Sell
};
let shares = u32::from_be_bytes(payload[19..23].try_into().unwrap());
let stock = String::from_utf8_lossy(&payload[23..31]).trim().to_string();
let price = u32::from_be_bytes(payload[31..35].try_into().unwrap());
Ok(Some(ItchMessage::AddOrder {
timestamp,
order_ref,
side,
shares,
stock,
price,
}))
}
'E' => {
let timestamp = read_u48_be(&payload[4..10]);
let order_ref = u64::from_be_bytes(payload[10..18].try_into().unwrap());
let shares = u32::from_be_bytes(payload[18..22].try_into().unwrap());
let match_number = u64::from_be_bytes(payload[22..30].try_into().unwrap());
Ok(Some(ItchMessage::OrderExecuted {
timestamp,
order_ref,
shares,
match_number,
}))
}
'C' => {
let timestamp = read_u48_be(&payload[4..10]);
let order_ref = u64::from_be_bytes(payload[10..18].try_into().unwrap());
let shares = u32::from_be_bytes(payload[18..22].try_into().unwrap());
let match_number = u64::from_be_bytes(payload[22..30].try_into().unwrap());
let printable = payload[30] == b'Y';
let price = u32::from_be_bytes(payload[31..35].try_into().unwrap());
Ok(Some(ItchMessage::OrderExecutedWithPrice {
timestamp,
order_ref,
shares,
match_number,
printable,
price,
}))
}
'X' => {
let timestamp = read_u48_be(&payload[4..10]);
let order_ref = u64::from_be_bytes(payload[10..18].try_into().unwrap());
let shares = u32::from_be_bytes(payload[18..22].try_into().unwrap());
Ok(Some(ItchMessage::OrderCancel {
timestamp,
order_ref,
shares,
}))
}
'D' => {
let timestamp = read_u48_be(&payload[4..10]);
let order_ref = u64::from_be_bytes(payload[10..18].try_into().unwrap());
Ok(Some(ItchMessage::OrderDelete {
timestamp,
order_ref,
}))
}
'U' => {
let timestamp = read_u48_be(&payload[4..10]);
let old_order_ref = u64::from_be_bytes(payload[10..18].try_into().unwrap());
let new_order_ref = u64::from_be_bytes(payload[18..26].try_into().unwrap());
let shares = u32::from_be_bytes(payload[26..30].try_into().unwrap());
let price = u32::from_be_bytes(payload[30..34].try_into().unwrap());
Ok(Some(ItchMessage::OrderReplace {
timestamp,
old_order_ref,
new_order_ref,
shares,
price,
}))
}
'P' => {
let timestamp = read_u48_be(&payload[4..10]);
let side = match payload[18] {
b'B' => Side::Buy,
_ => Side::Sell,
};
let shares = u32::from_be_bytes(payload[19..23].try_into().unwrap());
let stock = String::from_utf8_lossy(&payload[23..31]).trim().to_string();
let price = u32::from_be_bytes(payload[31..35].try_into().unwrap());
let match_number = u64::from_be_bytes(payload[35..43].try_into().unwrap());
Ok(Some(ItchMessage::Trade {
timestamp,
side,
shares,
stock,
price,
match_number,
}))
}
'R' => {
let locate = u16::from_be_bytes(payload[0..2].try_into().unwrap());
let stock = String::from_utf8_lossy(&payload[2..10]).trim().to_string();
self.stock_locates.insert(locate, stock.clone());
Ok(Some(ItchMessage::StockDirectory { stock, locate }))
}
_ => Ok(Some(ItchMessage::Other(msg_type))),
}
}
}
fn read_u48_be(buf: &[u8]) -> u64 {
let mut extended = [0u8; 8];
extended[2..8].copy_from_slice(buf);
u64::from_be_bytes(extended)
}
pub fn itch_to_event(msg: ItchMessage) -> Option<(String, Event)> {
match msg {
ItchMessage::AddOrder {
side,
shares,
stock,
price,
..
} => {
let nb_price = (price / 100) as i64;
Some((
stock,
Event::SubmitLimit {
side,
price: Price(nb_price),
quantity: shares as u64,
time_in_force: TimeInForce::GTC,
},
))
}
ItchMessage::OrderCancel { order_ref, .. } | ItchMessage::OrderDelete { order_ref, .. } => {
Some((
"".to_string(),
Event::Cancel {
order_id: OrderId(order_ref),
},
))
}
ItchMessage::OrderReplace {
old_order_ref,
shares,
price,
..
} => {
let nb_price = (price / 100) as i64;
Some((
"".to_string(),
Event::Modify {
order_id: OrderId(old_order_ref),
new_price: Price(nb_price),
new_quantity: shares as u64,
},
))
}
_ => None,
}
}