1use std::{mem, ops::RangeInclusive, os::raw::c_char, ptr::NonNull};
3
4use crate::enums::SecurityUpdateAction;
5
6#[repr(C)]
8#[derive(Clone, Debug, PartialEq, Eq)]
9#[cfg_attr(feature = "trivial_copy", derive(Copy))]
10#[cfg_attr(feature = "serde", derive(serde::Serialize))]
11pub struct RecordHeader {
12 #[cfg_attr(feature = "serde", serde(skip))]
14 pub length: u8,
15 pub rtype: u8,
19 pub publisher_id: u16,
21 pub product_id: u32,
23 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
25 pub ts_event: u64,
26}
27
28pub const TICK_MSG_TYPE_ID: u8 = 0xA0;
29#[repr(C)]
32#[derive(Clone, Debug, PartialEq, Eq)]
33#[cfg_attr(feature = "trivial_copy", derive(Copy))]
34#[cfg_attr(feature = "serde", derive(serde::Serialize))]
35pub struct MboMsg {
36 pub hd: RecordHeader,
38 pub order_id: u64,
40 pub price: i64,
43 pub size: u32,
45 pub flags: u8,
47 pub channel_id: u8,
49 pub action: c_char,
52 pub side: c_char,
54 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
56 pub ts_recv: u64,
57 pub ts_in_delta: i32,
59 pub sequence: u32,
61}
62
63#[repr(C)]
66#[derive(Clone, Debug, PartialEq, Eq)]
67#[cfg_attr(feature = "trivial_copy", derive(Copy))]
68#[cfg_attr(feature = "serde", derive(serde::Serialize))]
69pub struct BidAskPair {
70 pub bid_px: i64,
72 pub ask_px: i64,
74 pub bid_sz: u32,
76 pub ask_sz: u32,
78 pub bid_ct: u32,
80 pub ask_ct: u32,
82}
83
84pub const MAX_UA_BOOK_LEVEL: usize = 0xF;
85pub const MBP_MSG_TYPE_ID_RANGE: RangeInclusive<u8> = 0x00..=(MAX_UA_BOOK_LEVEL as u8);
86
87#[repr(C)]
90#[derive(Clone, Debug, PartialEq, Eq)]
91#[cfg_attr(feature = "trivial_copy", derive(Copy))]
92#[cfg_attr(feature = "serde", derive(serde::Serialize))]
93pub struct TradeMsg {
94 pub hd: RecordHeader,
96 pub price: i64,
99 pub size: u32,
101 pub action: c_char,
104 pub side: c_char,
106 pub flags: u8,
108 pub depth: u8,
110 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
112 pub ts_recv: u64,
113 pub ts_in_delta: i32,
115 pub sequence: u32,
117 #[cfg_attr(feature = "serde", serde(skip))]
118 pub booklevel: [BidAskPair; 0],
119}
120
121#[repr(C)]
123#[derive(Clone, Debug, PartialEq, Eq)]
124#[cfg_attr(feature = "trivial_copy", derive(Copy))]
125#[cfg_attr(feature = "serde", derive(serde::Serialize))]
126pub struct Mbp1Msg {
127 pub hd: RecordHeader,
129 pub price: i64,
132 pub size: u32,
134 pub action: c_char,
137 pub side: c_char,
139 pub flags: u8,
141 pub depth: u8,
143 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
145 pub ts_recv: u64,
146 pub ts_in_delta: i32,
148 pub sequence: u32,
150 pub booklevel: [BidAskPair; 1],
151}
152
153#[repr(C)]
155#[derive(Clone, Debug, PartialEq, Eq)]
156#[cfg_attr(feature = "trivial_copy", derive(Copy))]
157#[cfg_attr(feature = "serde", derive(serde::Serialize))]
158pub struct Mbp10Msg {
159 pub hd: RecordHeader,
161 pub price: i64,
164 pub size: u32,
166 pub action: c_char,
169 pub side: c_char,
171 pub flags: u8,
173 pub depth: u8,
175 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
177 pub ts_recv: u64,
178 pub ts_in_delta: i32,
180 pub sequence: u32,
182 pub booklevel: [BidAskPair; 10],
183}
184
185pub type TbboMsg = Mbp1Msg;
186
187pub const OHLCV_TYPE_ID: u8 = 0x11;
188#[repr(C)]
190#[derive(Clone, Debug, PartialEq, Eq)]
191#[cfg_attr(feature = "trivial_copy", derive(Copy))]
192#[cfg_attr(feature = "serde", derive(serde::Serialize))]
193pub struct OhlcvMsg {
194 pub hd: RecordHeader,
196 pub open: i64,
198 pub high: i64,
200 pub low: i64,
202 pub close: i64,
204 pub volume: u64,
206}
207
208pub const STATUS_MSG_TYPE_ID: u8 = 0x12;
209#[repr(C)]
212#[derive(Clone, Debug, PartialEq, Eq)]
213#[cfg_attr(feature = "trivial_copy", derive(Copy))]
214#[cfg_attr(feature = "serde", derive(serde::Serialize))]
215pub struct StatusMsg {
216 pub hd: RecordHeader,
218 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
220 pub ts_recv: u64,
221 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
222 pub group: [c_char; 21],
223 pub trading_status: u8,
224 pub halt_reason: u8,
225 pub trading_event: u8,
226}
227
228pub const INSTRUMENT_DEF_MSG_TYPE_ID: u8 = 0x13;
229#[repr(C)]
232#[derive(Clone, Debug, PartialEq, Eq)]
233#[cfg_attr(feature = "trivial_copy", derive(Copy))]
234#[cfg_attr(feature = "serde", derive(serde::Serialize))]
235#[doc(hidden)]
236pub struct InstrumentDefMsg {
237 pub hd: RecordHeader,
239 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
241 pub ts_recv: u64,
242 pub min_price_increment: i64,
243 pub display_factor: i64,
244 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
245 pub expiration: u64,
246 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
247 pub activation: u64,
248 pub high_limit_price: i64,
249 pub low_limit_price: i64,
250 pub max_price_variation: i64,
251 pub trading_reference_price: i64,
252 pub unit_of_measure_qty: i64,
253 pub min_price_increment_amount: i64,
254 pub price_ratio: i64,
255 pub inst_attrib_value: i32,
256 pub underlying_id: u32,
257 pub cleared_volume: i32,
258 pub market_depth_implied: i32,
259 pub market_depth: i32,
260 pub market_segment_id: u32,
261 pub max_trade_vol: u32,
262 pub min_lot_size: i32,
263 pub min_lot_size_block: i32,
264 pub min_lot_size_round_lot: i32,
265 pub min_trade_vol: u32,
266 pub open_interest_qty: i32,
267 pub contract_multiplier: i32,
268 pub decay_quantity: i32,
269 pub original_contract_size: i32,
270 pub related_security_id: u32,
271 pub trading_reference_date: u16,
272 pub appl_id: i16,
273 pub maturity_year: u16,
274 pub decay_start_date: u16,
275 pub channel_id: u16,
276 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
277 pub currency: [c_char; 4],
278 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
279 pub settl_currency: [c_char; 4],
280 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
281 pub secsubtype: [c_char; 6],
282 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
283 pub symbol: [c_char; 22],
284 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
285 pub group: [c_char; 21],
286 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
287 pub exchange: [c_char; 5],
288 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
289 pub asset: [c_char; 7],
290 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
291 pub cfi: [c_char; 7],
292 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
293 pub security_type: [c_char; 7],
294 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
295 pub unit_of_measure: [c_char; 31],
296 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
297 pub underlying: [c_char; 21],
298 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
299 pub related: [c_char; 21],
300 pub match_algorithm: c_char,
301 pub md_security_trading_status: u8,
302 pub main_fraction: u8,
303 pub price_display_format: u8,
304 pub settl_price_type: u8,
305 pub sub_fraction: u8,
306 pub underlying_product: u8,
307 #[cfg_attr(
308 feature = "serde",
309 serde(serialize_with = "serialize_enum_as_char_repr")
310 )]
311 pub security_update_action: SecurityUpdateAction,
312 pub maturity_month: u8,
313 pub maturity_day: u8,
314 pub maturity_week: u8,
315 pub user_defined_instrument: c_char,
316 pub contract_multiplier_unit: i8,
317 pub flow_schedule_type: i8,
318 pub tick_rule: u8,
319 #[cfg_attr(feature = "serde", serde(skip))]
321 pub _dummy: [c_char; 3],
322}
323
324pub const IMBALANCE_TYPE_ID: u8 = 0x14;
325#[repr(C)]
327#[derive(Clone, Debug, PartialEq, Eq)]
328#[cfg_attr(feature = "trivial_copy", derive(Copy))]
329#[cfg_attr(feature = "serde", derive(serde::Serialize))]
330#[doc(hidden)]
331pub struct Imbalance {
332 pub hd: RecordHeader,
333 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
334 pub ts_recv: u64,
335 pub ref_price: i64,
336 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_large_u64"))]
337 pub auction_time: u64,
338 pub cont_book_clr_price: i64,
340 pub auct_interest_clr_price: i64,
342 pub ssr_filling_price: i64,
344 pub ind_match_price: i64,
346 pub upper_collar: i64,
347 pub lower_collar: i64,
348 pub paired_qty: u32,
349 pub total_imbalance_qty: u32,
350 pub market_imbalance_qty: u32,
351 pub auction_type: c_char,
352 pub side: c_char,
353 pub auction_status: u8,
354 pub freeze_status: u8,
355 pub num_extensions: u8,
356 pub unpaired_qty: u8,
357 pub unpaired_side: c_char,
358 pub significant_imbalance: c_char,
359 #[cfg_attr(feature = "serde", serde(skip))]
360 pub _dummy: [c_char; 4],
361}
362
363pub const GATEWAY_ERROR_MSG_TYPE_ID: u8 = 0x15;
364#[repr(C)]
367#[derive(Clone, Debug, PartialEq, Eq)]
368#[cfg_attr(feature = "trivial_copy", derive(Copy))]
369#[cfg_attr(feature = "serde", derive(serde::Serialize))]
370pub struct GatewayErrorMsg {
371 pub hd: RecordHeader,
372 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
373 pub err: [c_char; 64],
374}
375
376pub const SYMBOL_MAPPING_MSG_TYPE_ID: u8 = 0x16;
377#[repr(C)]
380#[derive(Clone, Debug, PartialEq, Eq)]
381#[cfg_attr(feature = "trivial_copy", derive(Copy))]
382#[cfg_attr(feature = "serde", derive(serde::Serialize))]
383pub struct SymbolMappingMsg {
384 pub hd: RecordHeader,
385 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
386 pub stype_in_symbol: [c_char; 22],
387 #[cfg_attr(feature = "serde", serde(serialize_with = "serialize_c_char_arr"))]
388 pub stype_out_symbol: [c_char; 22],
389 #[cfg_attr(feature = "serde", serde(skip))]
390 pub _dummy: [c_char; 4],
391 pub start_ts: u64,
392 pub end_ts: u64,
393}
394
395#[cfg(feature = "serde")]
396fn serialize_c_char_arr<S: serde::Serializer, const N: usize>(
397 arr: &[c_char; N],
398 serializer: S,
399) -> Result<S::Ok, S::Error> {
400 let cstr = unsafe { std::ffi::CStr::from_ptr(&arr[0]) };
401 let str = cstr.to_str().unwrap_or("<invalid UTF-8>");
402 serializer.serialize_str(str)
403}
404
405#[cfg(feature = "serde")]
407fn serialize_large_u64<S: serde::Serializer>(num: &u64, serializer: S) -> Result<S::Ok, S::Error> {
408 serializer.serialize_str(&num.to_string())
409}
410
411#[cfg(feature = "serde")]
413fn serialize_enum_as_char_repr<S: serde::Serializer, T: Copy + Into<u8>>(
414 val: &T,
415 serializer: S,
416) -> Result<S::Ok, S::Error> {
417 serializer.serialize_char(Into::<u8>::into(*val) as char)
418}
419
420pub trait ConstTypeId {
422 const TYPE_ID: u8;
424}
425
426pub unsafe fn transmute_into_header<T: ConstTypeId>(record: &T) -> &RecordHeader {
435 let non_null = NonNull::from(record);
438 non_null.cast::<RecordHeader>().as_ref()
439}
440
441pub unsafe fn transmute_record_bytes<T: ConstTypeId>(bytes: &[u8]) -> Option<&T> {
450 assert!(
451 bytes.len() >= mem::size_of::<T>(),
452 concat!(
453 "Passing a slice smaller than `",
454 stringify!(T),
455 "` to `transmute_record_bytes` is invalid"
456 )
457 );
458 let non_null = NonNull::new_unchecked(bytes.as_ptr() as *mut u8);
459 if non_null.cast::<RecordHeader>().as_ref().rtype == T::TYPE_ID {
460 Some(non_null.cast::<T>().as_ref())
461 } else {
462 None
463 }
464}
465
466pub unsafe fn transmute_header_bytes(bytes: &[u8]) -> Option<&RecordHeader> {
475 assert!(
476 bytes.len() >= mem::size_of::<RecordHeader>(),
477 concat!(
478 "Passing a slice smaller than `",
479 stringify!(RecordHeader),
480 "` to `transmute_header_bytes` is invalid"
481 )
482 );
483 let non_null = NonNull::new_unchecked(bytes.as_ptr() as *mut u8);
484 let header = non_null.cast::<RecordHeader>().as_ref();
485 if header.length as usize * 4 > bytes.len() {
486 None
487 } else {
488 Some(header)
489 }
490}
491
492pub unsafe fn transmute_record<T: ConstTypeId>(header: &RecordHeader) -> Option<&T> {
500 if header.rtype == T::TYPE_ID {
501 let non_null = NonNull::from(header);
504 Some(non_null.cast::<T>().as_ref())
505 } else {
506 None
507 }
508}
509
510pub unsafe fn transmute_record_mut<T: ConstTypeId>(header: &mut RecordHeader) -> Option<&mut T> {
518 if header.rtype == T::TYPE_ID {
519 let non_null = NonNull::from(header);
522 Some(non_null.cast::<T>().as_mut())
523 } else {
524 None
525 }
526}
527
528impl ConstTypeId for MboMsg {
529 const TYPE_ID: u8 = TICK_MSG_TYPE_ID;
530}
531
532impl ConstTypeId for TradeMsg {
535 const TYPE_ID: u8 = 0;
536}
537
538impl ConstTypeId for Mbp1Msg {
540 const TYPE_ID: u8 = 1;
541}
542
543impl ConstTypeId for Mbp10Msg {
545 const TYPE_ID: u8 = 10;
546}
547
548impl ConstTypeId for OhlcvMsg {
549 const TYPE_ID: u8 = OHLCV_TYPE_ID;
550}
551
552impl ConstTypeId for StatusMsg {
553 const TYPE_ID: u8 = STATUS_MSG_TYPE_ID;
554}
555
556impl ConstTypeId for InstrumentDefMsg {
557 const TYPE_ID: u8 = INSTRUMENT_DEF_MSG_TYPE_ID;
558}
559
560impl ConstTypeId for Imbalance {
561 const TYPE_ID: u8 = IMBALANCE_TYPE_ID;
562}
563
564impl ConstTypeId for GatewayErrorMsg {
565 const TYPE_ID: u8 = GATEWAY_ERROR_MSG_TYPE_ID;
566}
567
568impl ConstTypeId for SymbolMappingMsg {
569 const TYPE_ID: u8 = SYMBOL_MAPPING_MSG_TYPE_ID;
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 const OHLCV_MSG: OhlcvMsg = OhlcvMsg {
577 hd: RecordHeader {
578 length: 56,
579 rtype: 17,
580 publisher_id: 1,
581 product_id: 5482,
582 ts_event: 1609160400000000000,
583 },
584 open: 372025000000000,
585 high: 372050000000000,
586 low: 372025000000000,
587 close: 372050000000000,
588 volume: 57,
589 };
590
591 #[test]
592 fn test_transmute_record_bytes() {
593 unsafe {
594 let ohlcv_bytes = std::slice::from_raw_parts(
595 &OHLCV_MSG as *const OhlcvMsg as *const u8,
596 mem::size_of::<OhlcvMsg>(),
597 )
598 .to_vec();
599 let ohlcv = transmute_record_bytes::<OhlcvMsg>(ohlcv_bytes.as_slice()).unwrap();
600 assert_eq!(*ohlcv, OHLCV_MSG);
601 };
602 }
603
604 #[test]
605 #[should_panic]
606 fn test_transmute_record_bytes_small_buffer() {
607 let source = OHLCV_MSG;
608 unsafe {
609 let slice = std::slice::from_raw_parts(
610 &source as *const OhlcvMsg as *const u8,
611 mem::size_of::<OhlcvMsg>() - 5,
612 );
613 transmute_record_bytes::<OhlcvMsg>(slice);
614 };
615 }
616
617 #[test]
618 fn test_transmute_record() {
619 let source = Box::new(OHLCV_MSG);
620 let ohlcv_ref: &OhlcvMsg = unsafe { transmute_record(&source.hd) }.unwrap();
621 assert_eq!(*ohlcv_ref, OHLCV_MSG);
622 }
623
624 #[test]
625 fn test_transmute_record_mut() {
626 let mut source = Box::new(OHLCV_MSG);
627 let ohlcv_ref: &OhlcvMsg = unsafe { transmute_record_mut(&mut source.hd) }.unwrap();
628 assert_eq!(*ohlcv_ref, OHLCV_MSG);
629 }
630
631 #[test]
632 fn test_symbol_mapping_size() {
633 assert_eq!(mem::size_of::<SymbolMappingMsg>(), 80);
634 }
635}