1use std::fmt::{self, Write};
10use std::os::raw::c_char;
11
12use crate::pretty;
13use crate::record::c_chars_to_str;
14use crate::{BidAskPair, ConsolidatedBidAskPair, FlagSet, RecordHeader};
15
16pub trait WritePyRepr {
18 const SHOULD_FLATTEN: bool = false;
20 fn write_py_repr(&self, s: &mut String) -> fmt::Result;
26}
27
28macro_rules! impl_write_py_repr_debug {
29 ($($ty:ty),+ $(,)?) => {
30 $(
31 impl WritePyRepr for $ty {
32 fn write_py_repr(&self, s: &mut String) -> fmt::Result {
33 write!(s, "{self:?}")
34 }
35 }
36 )+
37 };
38}
39
40impl_write_py_repr_debug! {
41 i64, u64, i32, u32, i16, u16, i8, u8, bool,
42 FlagSet,
43}
44
45impl WritePyRepr for RecordHeader {
46 const SHOULD_FLATTEN: bool = true;
47
48 fn write_py_repr(&self, s: &mut String) -> fmt::Result {
49 write!(s, "rtype=")?;
50 match self.rtype() {
51 Ok(rtype) => rtype.write_py_repr(s)?,
52 Err(_) => write!(s, "{}", self.rtype)?,
53 }
54 write!(s, ", publisher_id=")?;
55 match self.publisher() {
56 Ok(p) => p.write_py_repr(s)?,
57 Err(_) => write!(s, "{}", self.publisher_id)?,
58 }
59 write!(s, ", instrument_id={}, ", self.instrument_id)?;
60 fmt_ts(s, "ts_event", self.ts_event)
61 }
62}
63
64impl<const N: usize> WritePyRepr for [c_char; N] {
65 fn write_py_repr(&self, s: &mut String) -> fmt::Result {
66 match c_chars_to_str(self) {
67 Ok(v) => write!(s, "'{v}'"),
68 Err(_) => write!(s, "{self:?}"),
69 }
70 }
71}
72
73impl WritePyRepr for &str {
74 fn write_py_repr(&self, s: &mut String) -> fmt::Result {
75 write!(s, "'{self}'")
76 }
77}
78
79impl<const N: usize> WritePyRepr for [BidAskPair; N] {
80 const SHOULD_FLATTEN: bool = true;
81
82 fn write_py_repr(&self, s: &mut String) -> fmt::Result {
83 for (i, level) in self.iter().enumerate() {
85 if i > 0 {
86 write!(s, ", ")?;
87 }
88 write!(
90 s,
91 "bid_px_{i:02}={}, pretty_bid_px_{i:02}={}, ask_px_{i:02}={}, pretty_ask_px_{i:02}={}, bid_sz_{i:02}={}, ask_sz_{i:02}={}, bid_ct_{i:02}={}, ask_ct_{i:02}={}",
92 level.bid_px,
93 pretty::px_to_f64(level.bid_px),
94 level.ask_px,
95 pretty::px_to_f64(level.ask_px),
96 level.bid_sz,
97 level.ask_sz,
98 level.bid_ct,
99 level.ask_ct
100 )?;
101 }
102 Ok(())
103 }
104}
105
106impl<const N: usize> WritePyRepr for [ConsolidatedBidAskPair; N] {
107 const SHOULD_FLATTEN: bool = true;
108
109 fn write_py_repr(&self, s: &mut String) -> fmt::Result {
110 for (i, level) in self.iter().enumerate() {
112 if i > 0 {
113 write!(s, ", ")?;
114 }
115 write!(
116 s,
117 "bid_px_{i:02}={}, pretty_bid_px_{i:02}={}, ask_px_{i:02}={}, pretty_ask_px_{i:02}={}, bid_sz_{i:02}={}, ask_sz_{i:02}={}, bid_pb_{i:02}={}, ask_pb_{i:02}={}",
118 level.bid_px,
119 pretty::px_to_f64(level.bid_px),
120 level.ask_px,
121 pretty::px_to_f64(level.ask_px),
122 level.bid_sz,
123 level.ask_sz,
124 level.bid_pb,
125 level.ask_pb
126 )?;
127 }
128 Ok(())
129 }
130}
131
132pub fn fmt_px(s: &mut String, field_name: &str, px: i64) -> fmt::Result {
138 write!(s, "{field_name}={px}, ")?;
139 write!(s, "pretty_{field_name}={}", pretty::px_to_f64(px))
140}
141
142pub fn fmt_ts(s: &mut String, field_name: &str, ts: u64) -> fmt::Result {
148 write!(s, "{field_name}={ts}, ")?;
149 write!(s, "pretty_{field_name}='{}'", pretty::fmt_ts(ts))
150}
151
152pub fn fmt_c_char_enum<E, F>(
159 f: &mut String,
160 field_name: &str,
161 raw: c_char,
162 parser: F,
163) -> fmt::Result
164where
165 E: WritePyRepr,
166 F: FnOnce() -> crate::Result<E>,
167{
168 write!(f, "{field_name}=")?;
169 match parser() {
170 Ok(e) => e.write_py_repr(f),
171 Err(_) => write!(f, "'{}'", raw as u8 as char),
172 }
173}
174
175pub fn fmt_enum_method<E, F>(f: &mut String, field_name: &str, getter: F) -> fmt::Result
181where
182 E: WritePyRepr,
183 F: FnOnce() -> crate::Result<E>,
184{
185 write!(f, "{field_name}=")?;
186 match getter() {
187 Ok(e) => e.write_py_repr(f),
188 Err(_) => write!(f, "None"),
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::{CbboMsg, Mbp10Msg, Mbp1Msg, SType, StatMsg, SymbolMappingMsg, UNDEF_PRICE};
196
197 #[test]
198 fn test_fmt_px() {
199 let mut s = String::new();
200 fmt_px(&mut s, "price", 150_250_000_000).unwrap();
201 assert_eq!(s, "price=150250000000, pretty_price=150.25");
202 }
203
204 #[test]
205 fn test_fmt_px_undef() {
206 let mut s = String::new();
207 fmt_px(&mut s, "price", UNDEF_PRICE).unwrap();
208 assert_eq!(s, "price=9223372036854775807, pretty_price=NaN");
209 }
210
211 #[test]
212 fn test_flags_empty() {
213 let mut s = String::new();
214 FlagSet::empty().write_py_repr(&mut s).unwrap();
215 assert_eq!(s, "0");
216 }
217
218 #[test]
219 fn test_flags_set() {
220 let mut s = String::new();
221 let flags = FlagSet::empty().set_last().set_snapshot();
222 flags.write_py_repr(&mut s).unwrap();
223 assert_eq!(s, "LAST | SNAPSHOT (160)");
224 }
225
226 #[test]
227 fn test_bid_ask_pair_array_flattens() {
228 let levels = [BidAskPair {
229 bid_px: 100_250_000_000,
230 ask_px: 101_000_000_000,
231 bid_sz: 10,
232 ask_sz: 20,
233 bid_ct: 5,
234 ask_ct: 8,
235 }];
236 let mut s = String::new();
237 levels.write_py_repr(&mut s).unwrap();
238 assert_eq!(
239 s,
240 r"bid_px_00=100250000000, pretty_bid_px_00=100.25, ask_px_00=101000000000, pretty_ask_px_00=101, bid_sz_00=10, ask_sz_00=20, bid_ct_00=5, ask_ct_00=8"
241 );
242 }
243
244 #[test]
245 fn test_mbp1_msg_repr() {
246 let msg = Mbp1Msg {
247 hd: RecordHeader::new::<Mbp1Msg>(crate::rtype::MBP_1, 1, 12345, 1_000_000_000),
248 price: 150_250_000_000,
249 size: 100,
250 action: b'A' as i8,
251 side: b'B' as i8,
252 flags: FlagSet::empty().set_last(),
253 depth: 0,
254 ts_recv: 1_000_000_100,
255 ts_in_delta: 100,
256 sequence: 1,
257 levels: [BidAskPair {
258 bid_px: 150_000_000_000,
259 ask_px: 150_500_000_000,
260 bid_sz: 50,
261 ask_sz: 75,
262 bid_ct: 3,
263 ask_ct: 4,
264 }],
265 };
266 let mut s = String::new();
267 msg.write_py_repr(&mut s).unwrap();
268 assert_eq!(
269 s,
270 r"Mbp1Msg(ts_recv=1000000100, pretty_ts_recv='1970-01-01T00:00:01.000000100Z', rtype=<RType.MBP_1: 1>, publisher_id=GLBX.MDP3.GLBX (1), instrument_id=12345, ts_event=1000000000, pretty_ts_event='1970-01-01T00:00:01.000000000Z', action='A', side='B', depth=0, price=150250000000, pretty_price=150.25, size=100, flags=LAST (128), ts_in_delta=100, sequence=1, bid_px_00=150000000000, pretty_bid_px_00=150, ask_px_00=150500000000, pretty_ask_px_00=150.5, bid_sz_00=50, ask_sz_00=75, bid_ct_00=3, ask_ct_00=4)"
271 );
272 }
273
274 #[test]
275 fn test_symbol_mapping_msg_repr() {
276 let msg = SymbolMappingMsg::new(
277 12345,
278 1_704_067_200_000_000_000, SType::RawSymbol,
280 "AAPL",
281 SType::InstrumentId,
282 "AAPL.XNAS",
283 1_704_067_200_000_000_000,
284 1_704_153_600_000_000_000, )
286 .unwrap();
287 let mut s = String::new();
288 msg.write_py_repr(&mut s).unwrap();
289 assert!(s.starts_with("SymbolMappingMsg("));
291 assert!(s.contains("stype_in_symbol='AAPL'"));
292 assert!(s.contains("stype_out_symbol='AAPL.XNAS'"));
293 assert!(s.contains("start_ts="));
294 assert!(s.contains("pretty_start_ts="));
295 assert!(s.contains("end_ts="));
296 assert!(s.contains("pretty_end_ts="));
297 }
298
299 #[test]
300 fn test_stat_msg_repr() {
301 let stat = StatMsg {
302 hd: RecordHeader::new::<StatMsg>(crate::rtype::STATISTICS, 1, 12345, 1_000_000_000),
303 ts_recv: 1_000_000_100,
304 ts_ref: 1_000_000_000,
305 price: 150_250_000_000,
306 quantity: 1000,
307 sequence: 42,
308 ts_in_delta: 100,
309 stat_type: 1,
310 channel_id: 0,
311 update_action: 1,
312 stat_flags: 0,
313 ..Default::default()
314 };
315 let mut s = String::new();
316 stat.write_py_repr(&mut s).unwrap();
317 assert!(s.starts_with("StatMsg("));
318 assert!(s.contains("price=150250000000, pretty_price=150.25"));
319 assert!(s.contains("ts_recv="));
320 assert!(s.contains("pretty_ts_recv="));
321 assert!(s.contains("ts_ref="));
322 assert!(s.contains("pretty_ts_ref="));
323 }
324
325 #[test]
326 fn test_cbbo_msg_repr() {
327 let cbbo = CbboMsg {
328 hd: RecordHeader::new::<CbboMsg>(crate::rtype::CBBO_1S, 1, 12345, 1_000_000_000),
329 price: 150_250_000_000,
330 size: 100,
331 side: b'B' as i8,
332 flags: FlagSet::empty().set_last(),
333 ts_recv: 1_000_000_100,
334 levels: [ConsolidatedBidAskPair {
335 bid_px: 150_000_000_000,
336 ask_px: 150_500_000_000,
337 bid_sz: 50,
338 ask_sz: 75,
339 bid_pb: 1,
340 ask_pb: 2,
341 ..Default::default()
342 }],
343 ..CbboMsg::default_for_schema(crate::Schema::Cbbo1S)
344 };
345 let mut s = String::new();
346 cbbo.write_py_repr(&mut s).unwrap();
347 assert!(s.starts_with("CbboMsg("));
348 assert!(s.contains("side='B'"));
349 assert!(s.contains("bid_px_00="));
350 assert!(s.contains("pretty_bid_px_00="));
351 assert!(s.contains("bid_pb_00="));
352 assert!(s.contains("ask_pb_00="));
353 assert!(!s.contains("_reserved"));
355 }
356
357 #[test]
358 fn test_mbp10_msg_repr() {
359 let mut mbp = Mbp10Msg {
360 hd: RecordHeader::new::<Mbp10Msg>(crate::rtype::MBP_10, 1, 12345, 1_000_000_000),
361 price: 150_250_000_000,
362 size: 100,
363 action: b'A' as i8,
364 side: b'B' as i8,
365 flags: FlagSet::empty().set_last(),
366 depth: 0,
367 ts_recv: 1_000_000_100,
368 ts_in_delta: 100,
369 sequence: 1,
370 ..Default::default()
371 };
372 mbp.levels[0] = BidAskPair {
373 bid_px: 150_000_000_000,
374 ask_px: 150_500_000_000,
375 bid_sz: 50,
376 ask_sz: 75,
377 bid_ct: 3,
378 ask_ct: 4,
379 };
380 mbp.levels[1] = BidAskPair {
381 bid_px: 149_750_000_000,
382 ask_px: 150_750_000_000,
383 bid_sz: 100,
384 ask_sz: 120,
385 bid_ct: 5,
386 ask_ct: 6,
387 };
388 let mut s = String::new();
389 mbp.write_py_repr(&mut s).unwrap();
390 assert!(s.starts_with("Mbp10Msg("));
391 assert!(s.contains("bid_px_00=150000000000"));
392 assert!(s.contains("pretty_bid_px_00=150"));
393 assert!(s.contains("bid_px_01=149750000000"));
394 assert!(s.contains("pretty_bid_px_01=149.75"));
395 assert!(s.contains("bid_px_09="));
396 assert!(s.contains("ask_ct_09="));
397 }
398}