1use ibapi::contracts::{OptionComputation, tick_types::TickType};
19use nautilus_core::UnixNanos;
20use nautilus_model::{
21 data::{
22 Bar, BarType, IndexPriceUpdate, QuoteTick, TradeTick, greeks::OptionGreekValues,
23 option_chain::OptionGreeks,
24 },
25 enums::{AggressorSide, BookAction, GreeksConvention},
26 identifiers::{InstrumentId, TradeId},
27 types::{Price, Quantity},
28};
29use thiserror::Error;
30
31use super::convert::IbPriceConverter;
32
33#[derive(Debug, PartialEq, Error)]
34#[error("quantity size={size} cannot be represented with precision={precision}")]
35struct QuantityPrecisionError {
36 size: f64,
37 precision: u8,
38}
39
40fn checked_quantity(size: f64, precision: u8) -> anyhow::Result<Quantity> {
41 let quantity = Quantity::new_checked(size, precision)
42 .map_err(|e| anyhow::anyhow!("invalid quantity size={size}: {e}"))?;
43 if !is_representable_size(quantity, size, precision) {
44 anyhow::bail!(QuantityPrecisionError { size, precision });
45 }
46
47 Ok(quantity)
48}
49
50pub(super) fn is_representable_size(quantity: Quantity, size: f64, precision: u8) -> bool {
56 let tolerance =
57 (10_f64.powi(-i32::from(precision)) * 1e-9).max(size.abs() * 4.0 * f64::EPSILON);
58 (quantity.as_f64() - size).abs() <= tolerance
59}
60
61pub(crate) fn generate_ib_trade_id(ts_event: UnixNanos, price: f64, size: f64) -> TradeId {
63 let ts_secs = ts_event.as_i64() / 1_000_000_000;
64 TradeId::new(format!("{ts_secs}-{price}-{size}"))
65}
66
67#[allow(clippy::too_many_arguments)]
76pub fn parse_quote_tick(
77 instrument_id: InstrumentId,
78 bid_price: Option<f64>,
79 ask_price: Option<f64>,
80 bid_size: Option<f64>,
81 ask_size: Option<f64>,
82 prices: IbPriceConverter,
83 size_precision: u8,
84 ts_event: UnixNanos,
85 ts_init: UnixNanos,
86) -> anyhow::Result<QuoteTick> {
87 let bid = bid_price.map(|p| prices.bid_price(p)).transpose()?;
88 let ask = ask_price.map(|p| prices.ask_price(p)).transpose()?;
89 let bid_qty = bid_size
90 .map(|s| checked_quantity(s, size_precision))
91 .transpose()?;
92 let ask_qty = ask_size
93 .map(|s| checked_quantity(s, size_precision))
94 .transpose()?;
95
96 Ok(QuoteTick::new(
97 instrument_id,
98 bid.unwrap_or_else(|| Price::zero(prices.precision())),
99 ask.unwrap_or_else(|| Price::zero(prices.precision())),
100 bid_qty.unwrap_or_else(|| Quantity::zero(size_precision)),
101 ask_qty.unwrap_or_else(|| Quantity::zero(size_precision)),
102 ts_event,
103 ts_init,
104 ))
105}
106
107#[allow(clippy::too_many_arguments)]
113pub fn parse_trade_tick(
114 instrument_id: InstrumentId,
115 price: f64,
116 size: f64,
117 prices: IbPriceConverter,
118 size_precision: u8,
119 ts_event: UnixNanos,
120 ts_init: UnixNanos,
121 trade_id: Option<TradeId>,
122) -> anyhow::Result<TradeTick> {
123 let trade_price = prices
124 .price(price)
125 .map_err(|e| anyhow::anyhow!("invalid trade price={price}: {e}"))?;
126 let trade_size = checked_quantity(size, size_precision)?;
127 let aggressor_side = AggressorSide::NoAggressor; let trade_id = trade_id.unwrap_or_else(|| generate_ib_trade_id(ts_event, price, size));
129
130 TradeTick::new_checked(
131 instrument_id,
132 trade_price,
133 trade_size,
134 aggressor_side,
135 trade_id,
136 ts_event,
137 ts_init,
138 )
139 .map_err(|e| anyhow::anyhow!("invalid trade tick: {e}"))
140}
141
142pub(super) fn log_tick_parse_error(kind: &str, instrument_id: InstrumentId, e: &anyhow::Error) {
143 if e.is::<QuantityPrecisionError>() {
144 tracing::debug!("Dropping IB {kind} tick for {instrument_id}: {e}");
145 } else {
146 tracing::warn!("Failed to parse IB {kind} tick for {instrument_id}: {e}");
147 }
148}
149
150pub fn parse_index_price(
156 instrument_id: InstrumentId,
157 price: f64,
158 prices: IbPriceConverter,
159 ts_event: UnixNanos,
160 ts_init: UnixNanos,
161) -> anyhow::Result<IndexPriceUpdate> {
162 Ok(IndexPriceUpdate::new(
163 instrument_id,
164 prices.price(price)?,
165 ts_event,
166 ts_init,
167 ))
168}
169
170#[must_use]
172pub fn parse_option_computation_to_option_greeks(
173 instrument_id: InstrumentId,
174 computation: &OptionComputation,
175 ts_event: UnixNanos,
176 ts_init: UnixNanos,
177) -> Option<OptionGreeks> {
178 match computation.field {
179 TickType::ModelOption | TickType::DelayedModelOption => Some(OptionGreeks {
180 instrument_id,
181 greeks: OptionGreekValues {
182 delta: computation.delta.unwrap_or_default(),
183 gamma: computation.gamma.unwrap_or_default(),
184 vega: computation.vega.unwrap_or_default(),
185 theta: computation.theta.unwrap_or_default(),
186 rho: 0.0, },
188 convention: GreeksConvention::BlackScholes,
189 mark_iv: computation.implied_volatility,
190 bid_iv: None,
191 ask_iv: None,
192 underlying_price: computation.underlying_price,
193 open_interest: None,
194 ts_event,
195 ts_init,
196 }),
197 _ => None,
198 }
199}
200
201#[must_use]
203pub fn parse_option_open_interest(tick_type: &TickType, value: f64) -> Option<f64> {
204 match tick_type {
205 TickType::OpenInterest
206 | TickType::OptionCallOpenInterest
207 | TickType::OptionPutOpenInterest => Some(value),
208 _ => None,
209 }
210}
211
212#[allow(clippy::too_many_arguments)]
218pub fn parse_realtime_bar(
219 bar_type: BarType,
220 open: f64,
221 high: f64,
222 low: f64,
223 close: f64,
224 volume: f64,
225 prices: IbPriceConverter,
226 size_precision: u8,
227 ts_event: UnixNanos,
228 ts_init: UnixNanos,
229) -> anyhow::Result<Bar> {
230 let price_type = bar_type.spec().price_type;
231 let open_price = prices.bar_price(price_type, open)?;
232 let high_price = prices.bar_price(price_type, high)?;
233 let low_price = prices.bar_price(price_type, low)?;
234 let close_price = prices.bar_price(price_type, close)?;
235 let bar_volume = Quantity::new(volume, size_precision);
236
237 Ok(Bar::new(
238 bar_type,
239 open_price,
240 high_price,
241 low_price,
242 close_price,
243 bar_volume,
244 ts_event,
245 ts_init,
246 ))
247}
248
249#[must_use]
251pub fn parse_market_depth_operation(operation: i32) -> Option<BookAction> {
252 match operation {
253 0 => Some(BookAction::Add),
254 1 => Some(BookAction::Update),
255 2 => Some(BookAction::Delete),
256 _ => None,
257 }
258}
259
260#[cfg(test)]
263mod tests {
264 use ibapi::contracts::{OptionComputation, tick_types::TickType};
265 use nautilus_core::UnixNanos;
266 use nautilus_model::{
267 data::{BarSpecification, BarType},
268 enums::{AggregationSource, BarAggregation, PriceType},
269 identifiers::{InstrumentId, Symbol, Venue},
270 instruments::Instrument,
271 };
272 use rstest::rstest;
273 use rust_decimal::Decimal;
274
275 use super::*;
276 use crate::stubs::currency_pair;
277
278 fn create_test_instrument_id() -> InstrumentId {
279 InstrumentId::new(Symbol::from("AAPL"), Venue::from("NASDAQ"))
280 }
281
282 #[rstest]
283 fn test_parse_quote_tick_with_all_fields() {
284 let instrument_id = create_test_instrument_id();
285 let result = parse_quote_tick(
286 instrument_id,
287 Some(150.25),
288 Some(150.30),
289 Some(100.0),
290 Some(200.0),
291 IbPriceConverter::new(2),
292 0,
293 UnixNanos::new(0),
294 UnixNanos::new(0),
295 );
296 assert!(result.is_ok());
297 let quote = result.unwrap();
298 assert_eq!(quote.bid_price.as_f64(), 150.25);
299 assert_eq!(quote.ask_price.as_f64(), 150.30);
300 assert_eq!(quote.bid_size.as_f64(), 100.0);
301 assert_eq!(quote.ask_size.as_f64(), 200.0);
302 }
303
304 #[rstest]
305 fn test_parse_quote_tick_with_partial_fields() {
306 let instrument_id = create_test_instrument_id();
307 let result = parse_quote_tick(
308 instrument_id,
309 Some(150.25),
310 None,
311 Some(100.0),
312 None,
313 IbPriceConverter::new(2),
314 0,
315 UnixNanos::new(0),
316 UnixNanos::new(0),
317 );
318 assert!(result.is_ok());
319 let quote = result.unwrap();
320 assert_eq!(quote.bid_price.as_f64(), 150.25);
321 assert_eq!(quote.ask_price.as_f64(), 0.0); assert_eq!(quote.bid_size.as_f64(), 100.0);
323 assert_eq!(quote.ask_size.as_f64(), 0.0); }
325
326 #[rstest]
327 fn test_parse_quote_tick_with_no_fields() {
328 let instrument_id = create_test_instrument_id();
329 let result = parse_quote_tick(
330 instrument_id,
331 None,
332 None,
333 None,
334 None,
335 IbPriceConverter::new(2),
336 0,
337 UnixNanos::new(0),
338 UnixNanos::new(0),
339 );
340 assert!(result.is_ok());
341 let quote = result.unwrap();
342 assert_eq!(quote.bid_price.as_f64(), 0.0);
343 assert_eq!(quote.ask_price.as_f64(), 0.0);
344 }
345
346 #[rstest]
347 fn test_parse_trade_tick_with_trade_id() {
348 let instrument_id = create_test_instrument_id();
349 let trade_id = TradeId::from("TRADE-001");
350 let result = parse_trade_tick(
351 instrument_id,
352 150.25,
353 100.0,
354 IbPriceConverter::new(2),
355 0,
356 UnixNanos::new(0),
357 UnixNanos::new(0),
358 Some(trade_id),
359 );
360 assert!(result.is_ok());
361 let trade = result.unwrap();
362 assert_eq!(trade.price.as_f64(), 150.25);
363 assert_eq!(trade.size.as_f64(), 100.0);
364 assert_eq!(trade.trade_id, trade_id);
365 }
366
367 #[rstest]
368 fn test_parse_trade_tick_without_trade_id() {
369 let instrument_id = create_test_instrument_id();
370 let result = parse_trade_tick(
371 instrument_id,
372 150.25,
373 100.0,
374 IbPriceConverter::new(2),
375 0,
376 UnixNanos::new(1000),
377 UnixNanos::new(1000),
378 None,
379 );
380 assert!(result.is_ok());
381 let trade = result.unwrap();
382 assert_eq!(trade.price.as_f64(), 150.25);
383 assert_eq!(trade.size.as_f64(), 100.0);
384 assert!(!trade.trade_id.to_string().is_empty());
386 }
387
388 #[rstest]
389 fn test_parse_trade_tick_rejects_unrepresentable_size() {
390 let instrument_id = create_test_instrument_id();
391 let result = parse_trade_tick(
392 instrument_id,
393 150.25,
394 0.5,
395 IbPriceConverter::new(2),
396 0,
397 UnixNanos::new(1000),
398 UnixNanos::new(1000),
399 None,
400 );
401
402 let e = result.unwrap_err();
403 assert_eq!(
404 e.downcast_ref::<QuantityPrecisionError>(),
405 Some(&QuantityPrecisionError {
406 size: 0.5,
407 precision: 0
408 })
409 );
410 }
411
412 #[rstest]
413 #[case(0.63462, 0)]
414 #[case(1.025, 2)]
415 #[case(1_975_139.005, 2)]
416 fn test_quantity_precision_error_preserves_size(#[case] size: f64, #[case] precision: u8) {
417 let e = checked_quantity(size, precision).unwrap_err();
418 assert_eq!(
419 e.downcast_ref::<QuantityPrecisionError>(),
420 Some(&QuantityPrecisionError { size, precision })
421 );
422 }
423
424 #[rstest]
425 #[case(1_975_139.0, 2)]
426 #[case(1_975_139.25, 2)]
427 #[case(123_456_789.0, 0)]
428 fn test_checked_quantity_accepts_large_sizes(#[case] size: f64, #[case] precision: u8) {
429 let quantity = checked_quantity(size, precision).unwrap();
430
431 assert_eq!(quantity, Quantity::new(size, precision));
432 }
433
434 #[rstest]
435 #[case(-1.0)]
436 #[case(f64::NAN)]
437 #[case(f64::INFINITY)]
438 fn test_invalid_quantities_are_not_expected_precision_drops(#[case] size: f64) {
439 let e = checked_quantity(size, 0).unwrap_err();
440 assert!(!e.is::<QuantityPrecisionError>());
441 }
442
443 #[rstest]
444 fn test_parse_trade_tick_rejects_zero_size() {
445 let result = parse_trade_tick(
446 create_test_instrument_id(),
447 150.25,
448 0.0,
449 IbPriceConverter::new(2),
450 0,
451 UnixNanos::new(1000),
452 UnixNanos::new(1000),
453 None,
454 );
455
456 assert_eq!(
457 result.unwrap_err().to_string(),
458 "invalid trade tick: invalid `Quantity` for 'size' not positive, was 0",
459 );
460 }
461
462 #[rstest]
463 fn test_parse_quote_tick_rejects_unrepresentable_size() {
464 let instrument_id = create_test_instrument_id();
465 let result = parse_quote_tick(
466 instrument_id,
467 Some(150.25),
468 Some(150.30),
469 Some(0.5),
470 Some(200.0),
471 IbPriceConverter::new(2),
472 0,
473 UnixNanos::new(0),
474 UnixNanos::new(0),
475 );
476
477 assert!(result.is_err());
478 }
479
480 #[rstest]
481 fn test_parse_index_price_with_price_magnifier() {
482 let instrument_id = create_test_instrument_id();
483 let result = parse_index_price(
484 instrument_id,
485 452525.0,
486 IbPriceConverter::new(2).with_price_magnifier(100),
487 UnixNanos::new(0),
488 UnixNanos::new(0),
489 );
490 assert!(result.is_ok());
491 let index_price = result.unwrap();
492 assert_eq!(index_price.value.as_f64(), 4525.25);
493 }
494
495 #[rstest]
496 fn test_parse_index_price_without_price_magnifier() {
497 let instrument_id = create_test_instrument_id();
498 let result = parse_index_price(
499 instrument_id,
500 4525.25,
501 IbPriceConverter::new(2),
502 UnixNanos::new(0),
503 UnixNanos::new(0),
504 );
505 assert!(result.is_ok());
506 let index_price = result.unwrap();
507 assert_eq!(index_price.value.as_f64(), 4525.25);
508 }
509
510 #[rstest]
511 fn test_parse_realtime_bar() {
512 let instrument_id = create_test_instrument_id();
513
514 let bar_type = BarType::new(
515 instrument_id,
516 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
517 AggregationSource::External,
518 );
519 let result = parse_realtime_bar(
520 bar_type,
521 150.0,
522 151.0,
523 149.0,
524 150.5,
525 1000.0,
526 IbPriceConverter::new(2),
527 0,
528 UnixNanos::new(0),
529 UnixNanos::new(0),
530 );
531 assert!(result.is_ok());
532 let bar = result.unwrap();
533 assert_eq!(bar.open.as_f64(), 150.0);
534 assert_eq!(bar.high.as_f64(), 151.0);
535 assert_eq!(bar.low.as_f64(), 149.0);
536 assert_eq!(bar.close.as_f64(), 150.5);
537 assert_eq!(bar.volume.as_f64(), 1000.0);
538 }
539
540 #[rstest]
541 fn test_parse_market_depth_operation_insert() {
542 let action = parse_market_depth_operation(0);
543 assert_eq!(action, Some(BookAction::Add));
544 }
545
546 #[rstest]
547 fn test_parse_market_depth_operation_update() {
548 let action = parse_market_depth_operation(1);
549 assert_eq!(action, Some(BookAction::Update));
550 }
551
552 #[rstest]
553 fn test_parse_market_depth_operation_delete() {
554 let action = parse_market_depth_operation(2);
555 assert_eq!(action, Some(BookAction::Delete));
556 }
557
558 #[rstest]
559 fn test_parse_market_depth_operation_unknown() {
560 let action = parse_market_depth_operation(99);
561 assert_eq!(action, None);
562 }
563
564 #[rstest]
565 fn test_parse_quote_tick_precision() {
566 let instrument_id = create_test_instrument_id();
567 let result = parse_quote_tick(
568 instrument_id,
569 Some(150.255),
570 Some(150.305),
571 Some(100.0),
572 Some(200.0),
573 IbPriceConverter::new(2), 0, UnixNanos::new(0),
576 UnixNanos::new(0),
577 );
578 assert!(result.is_ok());
579 let quote = result.unwrap();
580 assert_eq!(quote.bid_price.as_f64(), 150.26); assert_eq!(quote.ask_price.as_f64(), 150.31); }
584
585 #[rstest]
586 fn test_parse_option_computation_to_option_greeks_from_model_tick() {
587 let instrument_id = create_test_instrument_id();
588
589 let greeks = parse_option_computation_to_option_greeks(
590 instrument_id,
591 &OptionComputation {
592 field: TickType::ModelOption,
593 implied_volatility: Some(0.25),
594 delta: Some(0.55),
595 gamma: Some(0.02),
596 vega: Some(0.15),
597 theta: Some(-0.05),
598 underlying_price: Some(155.0),
599 ..Default::default()
600 },
601 UnixNanos::new(10),
602 UnixNanos::new(11),
603 )
604 .unwrap();
605
606 assert_eq!(greeks.instrument_id, instrument_id);
607 assert_eq!(greeks.delta, 0.55);
608 assert_eq!(greeks.gamma, 0.02);
609 assert_eq!(greeks.vega, 0.15);
610 assert_eq!(greeks.theta, -0.05);
611 assert_eq!(greeks.rho, 0.0);
612 assert_eq!(greeks.mark_iv, Some(0.25));
613 assert_eq!(greeks.underlying_price, Some(155.0));
614 assert_eq!(greeks.open_interest, None);
615 assert_eq!(greeks.ts_event, UnixNanos::new(10));
616 assert_eq!(greeks.ts_init, UnixNanos::new(11));
617 }
618
619 #[rstest]
620 fn test_parse_option_computation_to_option_greeks_ignores_non_model_tick() {
621 let instrument_id = create_test_instrument_id();
622
623 let greeks = parse_option_computation_to_option_greeks(
624 instrument_id,
625 &OptionComputation {
626 field: TickType::BidOption,
627 implied_volatility: Some(0.24),
628 ..Default::default()
629 },
630 UnixNanos::new(10),
631 UnixNanos::new(11),
632 );
633
634 assert!(greeks.is_none());
635 }
636
637 #[rstest]
638 fn test_parse_option_open_interest_supports_option_interest_tick_types() {
639 assert_eq!(
640 parse_option_open_interest(&TickType::OptionCallOpenInterest, 1234.0),
641 Some(1234.0)
642 );
643 assert_eq!(
644 parse_option_open_interest(&TickType::OptionPutOpenInterest, 5678.0),
645 Some(5678.0)
646 );
647 assert_eq!(
648 parse_option_open_interest(&TickType::OpenInterest, 42.0),
649 Some(42.0)
650 );
651 assert_eq!(parse_option_open_interest(&TickType::Bid, 42.0), None);
652 }
653
654 #[rstest]
655 fn test_parse_quote_tick_rounds_fx_prices_to_increment() {
656 let instrument = currency_pair("EUR", "USD", 5, "0.00005");
657
658 let quote = parse_quote_tick(
659 instrument.id(),
660 Some(1.12097),
661 Some(1.12098),
662 Some(1_000_000.0),
663 Some(2_000_000.0),
664 IbPriceConverter::for_instrument(&instrument, 1),
665 0,
666 UnixNanos::new(0),
667 UnixNanos::new(0),
668 )
669 .unwrap();
670
671 assert_eq!(quote.bid_price, Price::from("1.12095"));
672 assert_eq!(quote.ask_price, Price::from("1.12100"));
673 }
674
675 #[rstest]
676 #[case(1.12098, 1.12099, "1.12095", "1.12100")]
677 #[case(1.12097, 1.12097, "1.12095", "1.12100")]
678 #[case(1.12100, 1.12105, "1.12100", "1.12105")]
679 fn test_parse_quote_tick_rounds_fx_bid_down_and_ask_up(
680 #[case] ib_bid: f64,
681 #[case] ib_ask: f64,
682 #[case] bid: &str,
683 #[case] ask: &str,
684 ) {
685 let instrument = currency_pair("EUR", "USD", 5, "0.00005");
686
687 let quote = parse_quote_tick(
688 instrument.id(),
689 Some(ib_bid),
690 Some(ib_ask),
691 Some(1_000_000.0),
692 Some(2_000_000.0),
693 IbPriceConverter::for_instrument(&instrument, 1),
694 0,
695 UnixNanos::new(0),
696 UnixNanos::new(0),
697 )
698 .unwrap();
699
700 assert_eq!(quote.bid_price, Price::from(bid));
701 assert_eq!(quote.ask_price, Price::from(ask));
702 }
703
704 #[rstest]
705 fn test_parse_quote_tick_never_locks_or_crosses_fx_quote_across_sub_pip_offsets() {
706 let instrument = currency_pair("EUR", "USD", 5, "0.00005");
707 let increment = Price::from("0.00005").as_decimal();
708 let to_ib = |units: i64| -> f64 {
709 format!("{}.{:05}", units / 100_000, units % 100_000)
710 .parse()
711 .unwrap()
712 };
713
714 for bid_units in 112_000_i64..=112_040 {
716 for ask_units in (bid_units + 1)..=112_041 {
717 let quote = parse_quote_tick(
718 instrument.id(),
719 Some(to_ib(bid_units)),
720 Some(to_ib(ask_units)),
721 Some(1.0),
722 Some(1.0),
723 IbPriceConverter::for_instrument(&instrument, 1),
724 0,
725 UnixNanos::new(0),
726 UnixNanos::new(0),
727 )
728 .unwrap();
729
730 let (bid, ask) = (quote.bid_price.as_decimal(), quote.ask_price.as_decimal());
731 let (ib_bid, ib_ask) = (Decimal::new(bid_units, 5), Decimal::new(ask_units, 5));
732 let case = format!("bid={ib_bid} ask={ib_ask} published {bid}/{ask}");
733
734 assert!((bid % increment).is_zero(), "bid off grid: {case}");
735 assert!((ask % increment).is_zero(), "ask off grid: {case}");
736 assert!(bid <= ib_bid && ib_bid - bid < increment, "{case}");
737 assert!(ask >= ib_ask && ask - ib_ask < increment, "{case}");
738 assert!(bid < ask, "locked or crossed: {case}");
739 }
740 }
741 }
742
743 #[rstest]
744 fn test_parse_quote_tick_leaves_equity_quote_prices_unrounded() {
745 let quote = parse_quote_tick(
746 create_test_instrument_id(),
747 Some(150.25),
748 Some(150.31),
749 Some(100.0),
750 Some(200.0),
751 IbPriceConverter::new(2),
752 0,
753 UnixNanos::new(0),
754 UnixNanos::new(0),
755 )
756 .unwrap();
757
758 assert_eq!(quote.bid_price, Price::from("150.25"));
759 assert_eq!(quote.ask_price, Price::from("150.31"));
760 }
761
762 #[rstest]
763 #[case(PriceType::Bid, "1.12095", "1.12105", "1.12090", "1.12095")]
764 #[case(PriceType::Ask, "1.12100", "1.12110", "1.12095", "1.12100")]
765 #[case(PriceType::Mid, "1.12095", "1.12110", "1.12090", "1.12095")]
766 fn test_parse_realtime_bar_rounds_fx_prices_by_price_type(
767 #[case] price_type: PriceType,
768 #[case] open: &str,
769 #[case] high: &str,
770 #[case] low: &str,
771 #[case] close: &str,
772 ) {
773 let instrument = currency_pair("EUR", "USD", 5, "0.00005");
774 let bar_type = BarType::new(
775 instrument.id(),
776 BarSpecification::new(5, BarAggregation::Second, price_type),
777 AggregationSource::External,
778 );
779
780 let bar = parse_realtime_bar(
781 bar_type,
782 1.12097,
783 1.12109,
784 1.12091,
785 1.12096,
786 0.0,
787 IbPriceConverter::for_instrument(&instrument, 1),
788 0,
789 UnixNanos::new(0),
790 UnixNanos::new(0),
791 )
792 .unwrap();
793
794 assert_eq!(bar.open, Price::from(open));
795 assert_eq!(bar.high, Price::from(high));
796 assert_eq!(bar.low, Price::from(low));
797 assert_eq!(bar.close, Price::from(close));
798 }
799
800 #[rstest]
801 fn test_parse_quote_tick_divides_magnified_prices() {
802 let quote = parse_quote_tick(
803 create_test_instrument_id(),
804 Some(425.25),
805 Some(425.5),
806 Some(10.0),
807 Some(20.0),
808 IbPriceConverter::new(4).with_price_magnifier(100),
809 0,
810 UnixNanos::new(0),
811 UnixNanos::new(0),
812 )
813 .unwrap();
814
815 assert_eq!(quote.bid_price, Price::from("4.2525"));
816 assert_eq!(quote.ask_price, Price::from("4.2550"));
817 }
818
819 #[rstest]
820 fn test_parse_trade_tick_divides_magnified_price() {
821 let trade = parse_trade_tick(
822 create_test_instrument_id(),
823 425.25,
824 3.0,
825 IbPriceConverter::new(4).with_price_magnifier(100),
826 0,
827 UnixNanos::new(0),
828 UnixNanos::new(0),
829 Some(TradeId::from("TRADE-001")),
830 )
831 .unwrap();
832
833 assert_eq!(trade.price, Price::from("4.2525"));
834 }
835
836 #[rstest]
837 fn test_parse_realtime_bar_divides_magnified_prices() {
838 let bar_type = BarType::new(
839 create_test_instrument_id(),
840 BarSpecification::new(5, BarAggregation::Second, PriceType::Last),
841 AggregationSource::External,
842 );
843
844 let bar = parse_realtime_bar(
845 bar_type,
846 425.25,
847 430.0,
848 424.5,
849 428.5,
850 10.0,
851 IbPriceConverter::new(4).with_price_magnifier(100),
852 0,
853 UnixNanos::new(0),
854 UnixNanos::new(0),
855 )
856 .unwrap();
857
858 assert_eq!(bar.open, Price::from("4.2525"));
859 assert_eq!(bar.close, Price::from("4.2850"));
860 }
861}