1use std::str::FromStr;
19
20use anyhow::Context;
21use ibapi::orders::{Execution, OrderStatus};
22use nautilus_core::UnixNanos;
23use nautilus_model::{
24 enums::{
25 LiquiditySide, OrderSide, OrderStatus as NautilusOrderStatus, OrderType, TimeInForce,
26 TrailingOffsetType,
27 },
28 identifiers::{AccountId, ClientOrderId, InstrumentId, TradeId, VenueOrderId},
29 instruments::Instrument,
30 reports::{FillReport, OrderStatusReport},
31 types::{Currency, Money, Price, Quantity},
32};
33use rust_decimal::Decimal;
34use time::{PrimitiveDateTime, macros::format_description};
35
36use crate::{
37 common::parse::is_spread_instrument_id,
38 providers::instruments::InteractiveBrokersInstrumentProvider,
39};
40
41pub(crate) fn should_use_avg_fill_price(avg_fill_price: f64, instrument_id: &InstrumentId) -> bool {
42 avg_fill_price.is_finite()
43 && avg_fill_price != f64::MAX
44 && avg_fill_price != 0.0
45 && (avg_fill_price > 0.0 || is_spread_instrument_id(instrument_id))
46}
47
48#[allow(clippy::too_many_arguments)]
71pub fn parse_execution_to_fill_report(
72 execution: &Execution,
73 _contract: &ibapi::contracts::Contract,
74 commission: f64,
75 commission_currency: &str,
76 instrument_id: InstrumentId,
77 account_id: AccountId,
78 instrument_provider: &InteractiveBrokersInstrumentProvider,
79 ts_init: UnixNanos,
80 avg_px: Option<Price>,
81) -> anyhow::Result<FillReport> {
82 let price_magnifier = instrument_provider.get_price_magnifier(&instrument_id) as f64;
84
85 let execution_price = execution.price * price_magnifier;
87
88 let order_side = match execution.side.as_str() {
90 "BUY" | "BOT" => OrderSide::Buy,
91 "SELL" | "SLD" => OrderSide::Sell,
92 _ => anyhow::bail!("Unknown order side: {}", execution.side),
93 };
94
95 let instrument = instrument_provider
97 .find(&instrument_id)
98 .context("Instrument not found")?;
99
100 let last_qty = Quantity::new(execution.shares, instrument.size_precision());
102 let last_px = Price::new(execution_price, instrument.price_precision());
103
104 let commission_money = Money::new(commission, Currency::from_str(commission_currency)?);
106
107 let ts_event = parse_execution_time(&execution.time)?;
109
110 let trade_id = TradeId::new(&execution.execution_id);
112
113 let venue_order_id = VenueOrderId::new(execution.order_id.to_string());
115
116 let client_order_id = if !execution.order_reference.is_empty() {
118 Some(ClientOrderId::new(&execution.order_reference))
119 } else {
120 None
121 };
122
123 let mut report = FillReport::new(
124 account_id,
125 instrument_id,
126 venue_order_id,
127 trade_id,
128 order_side,
129 last_qty,
130 last_px,
131 commission_money,
132 LiquiditySide::NoLiquiditySide,
133 client_order_id,
134 None, ts_event,
136 ts_init,
137 Some(nautilus_core::UUID4::new()),
138 );
139 report.avg_px = avg_px.map(|price: Price| price.as_decimal());
140
141 Ok(report)
142}
143
144pub fn parse_order_status_to_report(
159 order_status: &OrderStatus,
160 order: Option<&ibapi::orders::Order>,
161 instrument_id: InstrumentId,
162 account_id: AccountId,
163 instrument_provider: &InteractiveBrokersInstrumentProvider,
164 ts_init: UnixNanos,
165) -> anyhow::Result<OrderStatusReport> {
166 let price_magnifier = instrument_provider.get_price_magnifier(&instrument_id) as f64;
168
169 let nautilus_status = match order_status.status.as_str() {
171 "ApiPending" | "PendingSubmit" | "PreSubmitted" => NautilusOrderStatus::Submitted,
172 "Submitted" => NautilusOrderStatus::Accepted,
173 "PendingCancel" => NautilusOrderStatus::PendingCancel,
174 "ApiCancelled" | "Cancelled" => NautilusOrderStatus::Canceled,
175 "Filled" => NautilusOrderStatus::Filled,
176 "Inactive" => NautilusOrderStatus::Rejected,
177 _ => {
178 tracing::warn!(
179 "Unknown order status: {}, defaulting to SUBMITTED",
180 order_status.status
181 );
182 NautilusOrderStatus::Submitted
183 }
184 };
185
186 let order_side = if let Some(order) = order {
188 match order.action {
189 ibapi::orders::Action::Buy => OrderSide::Buy,
190 ibapi::orders::Action::Sell => OrderSide::Sell,
191 ibapi::orders::Action::SellShort => OrderSide::Sell,
192 ibapi::orders::Action::SellLong => OrderSide::Sell,
193 }
194 } else {
195 OrderSide::Buy
197 };
198
199 let instrument = instrument_provider.find(&instrument_id);
200
201 let size_precision = instrument
203 .as_ref()
204 .map_or(0, |instr| instr.size_precision());
205 let price_precision = instrument
206 .as_ref()
207 .map_or(0, |instr| instr.price_precision());
208
209 let quantity = if let Some(order) = order {
211 Quantity::new(order.total_quantity, size_precision)
212 } else {
213 Quantity::zero(size_precision)
214 };
215
216 let filled_qty = Quantity::new(order_status.filled, size_precision);
218
219 let include_avg_px = should_use_avg_fill_price(order_status.average_fill_price, &instrument_id);
221 let avg_px_value = if include_avg_px {
222 order_status.average_fill_price * price_magnifier
223 } else {
224 0.0
225 };
226
227 let venue_order_id = VenueOrderId::new(order_status.order_id.to_string());
229
230 let client_order_id = if let Some(order) = order {
232 if order.order_ref.is_empty() {
233 None
234 } else {
235 Some(ClientOrderId::new(&order.order_ref))
236 }
237 } else {
238 None
239 };
240
241 let order_type = order
243 .map(|order| map_ib_order_type(&order.order_type))
244 .unwrap_or(OrderType::Market);
245
246 let time_in_force = if let Some(order) = order {
248 let tif_str = order.tif.to_string();
249 match tif_str.as_str() {
250 "DAY" => TimeInForce::Day,
251 "GTC" => TimeInForce::Gtc,
252 "IOC" => TimeInForce::Ioc,
253 "FOK" => TimeInForce::Fok,
254 _ => {
255 if tif_str.starts_with("GTD") || !order.good_till_date.is_empty() {
257 TimeInForce::Gtd
258 } else {
259 TimeInForce::Day }
261 }
262 }
263 } else {
264 TimeInForce::Day };
266
267 let mut report = OrderStatusReport::new(
269 account_id,
270 instrument_id,
271 client_order_id,
272 venue_order_id,
273 order_side,
274 order_type,
275 time_in_force,
276 nautilus_status,
277 quantity,
278 filled_qty,
279 ts_init, ts_init, ts_init,
282 Some(nautilus_core::UUID4::new()), );
284
285 if let Some(order) = order {
287 if let Some(limit_price) = order.limit_price {
288 let converted = limit_price * price_magnifier;
289 report = report.with_price(Price::new(converted, price_precision));
290 }
291
292 let (trigger_price, limit_offset, trailing_offset, trailing_offset_type) =
293 parse_ib_order_pricing_fields(order, order_type, price_magnifier, price_precision)?;
294
295 if let Some(trigger_price) = trigger_price {
296 report = report.with_trigger_price(trigger_price);
297 }
298
299 if let Some(limit_offset) = limit_offset {
300 report = report.with_limit_offset(limit_offset);
301 }
302
303 if let Some(trailing_offset) = trailing_offset {
304 report = report.with_trailing_offset(trailing_offset);
305 }
306
307 if let Some(trailing_offset_type) = trailing_offset_type {
308 report = report.with_trailing_offset_type(trailing_offset_type);
309 }
310 }
311
312 if include_avg_px {
313 report = report.with_avg_px(avg_px_value)?;
314 }
315
316 Ok(report)
317}
318
319fn map_ib_order_type(order_type: &str) -> OrderType {
320 match order_type {
321 "MKT" | "MOC" => OrderType::Market,
322 "LMT" | "LOC" => OrderType::Limit,
323 "STP" => OrderType::StopMarket,
324 "STP LMT" => OrderType::StopLimit,
325 "TRAIL" => OrderType::TrailingStopMarket,
326 "TRAIL LIMIT" => OrderType::TrailingStopLimit,
327 "MIT" => OrderType::MarketIfTouched,
328 "LIT" => OrderType::LimitIfTouched,
329 "MTL" => OrderType::MarketToLimit,
330 _ => OrderType::Market,
331 }
332}
333
334fn parse_ib_order_pricing_fields(
335 order: &ibapi::orders::Order,
336 order_type: OrderType,
337 price_magnifier: f64,
338 price_precision: u8,
339) -> anyhow::Result<(
340 Option<Price>,
341 Option<Decimal>,
342 Option<Decimal>,
343 Option<TrailingOffsetType>,
344)> {
345 let mut trigger_price = None;
346 let mut limit_offset = None;
347 let mut trailing_offset = None;
348 let mut trailing_offset_type = None;
349
350 if matches!(
351 order_type,
352 OrderType::TrailingStopMarket | OrderType::TrailingStopLimit
353 ) {
354 if let Some(trail_stop_price) = order.trail_stop_price {
355 trigger_price = Some(Price::new(
356 trail_stop_price * price_magnifier,
357 price_precision,
358 ));
359 }
360
361 if let Some(aux_price) = order.aux_price {
362 trailing_offset = Some(decimal_from_f64(aux_price)?);
363 trailing_offset_type = Some(TrailingOffsetType::Price);
364 } else if let Some(trailing_percent) = order.trailing_percent {
365 trailing_offset = Some(decimal_from_f64(trailing_percent)? * Decimal::from(100));
366 trailing_offset_type = Some(TrailingOffsetType::BasisPoints);
367 }
368
369 if order_type == OrderType::TrailingStopLimit
370 && let Some(limit_price_offset) = order.limit_price_offset
371 {
372 limit_offset = Some(decimal_from_f64(limit_price_offset)?);
373 trailing_offset_type = Some(trailing_offset_type.unwrap_or(TrailingOffsetType::Price));
374 }
375
376 return Ok((
377 trigger_price,
378 limit_offset,
379 trailing_offset,
380 trailing_offset_type,
381 ));
382 }
383
384 if let Some(aux_price) = order.aux_price {
385 trigger_price = Some(Price::new(aux_price * price_magnifier, price_precision));
386 }
387
388 Ok((
389 trigger_price,
390 limit_offset,
391 trailing_offset,
392 trailing_offset_type,
393 ))
394}
395
396fn decimal_from_f64(value: f64) -> anyhow::Result<Decimal> {
397 Decimal::from_str(&value.to_string())
398 .with_context(|| format!("Failed to convert IB floating-point value {value} to Decimal"))
399}
400
401pub fn parse_execution_time(time_str: &str) -> anyhow::Result<UnixNanos> {
419 fn parse_utc(
420 time_str: &str,
421 format: &[time::format_description::FormatItem<'_>],
422 ) -> anyhow::Result<UnixNanos> {
423 let dt = PrimitiveDateTime::parse(time_str, format).map_err(|e| {
424 anyhow::anyhow!("Failed to parse execution timestamp '{time_str}': {e}")
425 })?;
426 let nanos: u64 = dt
427 .assume_utc()
428 .unix_timestamp_nanos()
429 .try_into()
430 .map_err(|_| {
431 anyhow::anyhow!("Execution timestamp '{time_str}' was before Unix epoch")
432 })?;
433 Ok(UnixNanos::new(nanos))
434 }
435
436 if time_str.contains('-') && !time_str.contains(' ') {
437 let format = format_description!("[year][month][day]-[hour]:[minute]:[second]");
438 return parse_utc(time_str, format);
439 }
440
441 let parts: Vec<&str> = time_str.split(' ').collect();
442
443 if parts.len() < 2 {
444 anyhow::bail!("Invalid execution time format: {time_str}");
445 }
446
447 let format = format_description!("[year][month][day] [hour]:[minute]:[second]");
448 let date_str = format!("{} {}", parts[0], parts[1]);
449
450 if parts.len() == 2 {
451 return parse_utc(&date_str, format);
452 }
453
454 let timezone = parts[2];
455 if !matches!(timezone, "Universal" | "UTC" | "Etc/UTC" | "GMT" | "Z") {
456 anyhow::bail!(
457 "Unsupported non-UTC execution timezone '{timezone}' in '{time_str}'. Configure TWS / IB Gateway to emit UTC timestamps"
458 );
459 }
460
461 parse_utc(&date_str, format)
462}
463
464#[cfg(test)]
465mod tests {
466 use ibapi::{
467 contracts::Contract,
468 orders::{Action, Liquidity, Order},
469 };
470 use nautilus_model::{
471 enums::TrailingOffsetType,
472 identifiers::{Symbol, Venue},
473 };
474 use rust_decimal::Decimal;
475
476 use super::*;
477 use crate::{
478 config::InteractiveBrokersInstrumentProviderConfig,
479 providers::instruments::InteractiveBrokersInstrumentProvider,
480 };
481
482 fn create_test_instrument_provider() -> InteractiveBrokersInstrumentProvider {
483 let config = InteractiveBrokersInstrumentProviderConfig::default();
484 InteractiveBrokersInstrumentProvider::new(config)
485 }
486
487 fn create_test_instrument_id() -> InstrumentId {
488 InstrumentId::new(Symbol::from("AAPL"), Venue::from("NASDAQ"))
489 }
490
491 use rstest::rstest;
492
493 #[rstest]
494 fn test_parse_execution_time_hyphenated_format() {
495 let time_str = "20250225-15:15:00";
496 let result = parse_execution_time(time_str);
497 assert!(result.is_ok());
498 let timestamp = result.unwrap();
499 assert!(timestamp.as_i64() > 0);
500 }
501
502 #[rstest]
503 fn test_parse_execution_time_with_unsupported_non_utc_timezone() {
504 let time_str = "20230223 00:43:36 America/New_York";
505 let result = parse_execution_time(time_str);
506 assert!(result.is_err());
507 }
508
509 #[rstest]
510 fn test_parse_execution_time_utc() {
511 let time_str = "20230223 00:43:36 Universal";
512 let result = parse_execution_time(time_str);
513 assert!(result.is_ok());
514 let timestamp = result.unwrap();
515 assert!(timestamp.as_i64() > 0);
516 }
517
518 #[rstest]
519 fn test_parse_execution_time_no_timezone_assumes_utc() {
520 let time_str = "20230223 00:43:36";
521 let result = parse_execution_time(time_str);
522 assert!(result.is_ok());
523 let timestamp = result.unwrap();
524 assert!(timestamp.as_i64() > 0);
525 }
526
527 #[rstest]
528 fn test_parse_execution_time_invalid_format() {
529 let time_str = "invalid format";
530 let result = parse_execution_time(time_str);
531 assert!(result.is_err());
532 }
533
534 #[rstest]
535 fn test_parse_execution_time_short_format() {
536 let time_str = "20230223 00:43";
537 let result = parse_execution_time(time_str);
538 assert!(result.is_err());
539 }
540
541 #[rstest]
542 fn test_parse_order_status_to_report_submitted() {
543 let instrument_provider = create_test_instrument_provider();
544 let instrument_id = create_test_instrument_id();
545 let account_id = AccountId::from("IB-001");
546
547 let order_status = OrderStatus {
548 order_id: 12345,
549 status: String::from("Submitted"),
550 filled: 0.0,
551 remaining: 100.0,
552 average_fill_price: 0.0,
553 perm_id: 0,
554 parent_id: 0,
555 last_fill_price: 0.0,
556 client_id: 0,
557 why_held: String::new(),
558 market_cap_price: 0.0,
559 };
560
561 let result = parse_order_status_to_report(
562 &order_status,
563 None,
564 instrument_id,
565 account_id,
566 &instrument_provider,
567 UnixNanos::new(0),
568 );
569
570 if let Err(e) = result {
572 let error_msg = e.to_string();
573 assert!(
574 error_msg.contains("not found") || error_msg.contains("instrument"),
575 "Unexpected error: {}",
576 error_msg
577 );
578 }
579 }
580
581 #[rstest]
582 fn test_parse_order_status_to_report_filled() {
583 let instrument_provider = create_test_instrument_provider();
584 let instrument_id = create_test_instrument_id();
585 let account_id = AccountId::from("IB-001");
586
587 let order_status = OrderStatus {
588 order_id: 12345,
589 status: String::from("Filled"),
590 filled: 100.0,
591 remaining: 0.0,
592 average_fill_price: 150.25,
593 perm_id: 0,
594 parent_id: 0,
595 last_fill_price: 150.25,
596 client_id: 0,
597 why_held: String::new(),
598 market_cap_price: 0.0,
599 };
600
601 let result = parse_order_status_to_report(
602 &order_status,
603 None,
604 instrument_id,
605 account_id,
606 &instrument_provider,
607 UnixNanos::new(0),
608 );
609
610 if let Err(e) = result {
612 let error_msg = e.to_string();
613 assert!(
614 error_msg.contains("not found") || error_msg.contains("instrument"),
615 "Unexpected error: {}",
616 error_msg
617 );
618 }
619 }
620
621 #[rstest]
622 fn test_parse_order_status_to_report_spread_allows_negative_avg_fill_price() {
623 let instrument_provider = create_test_instrument_provider();
624 let instrument_id = InstrumentId::new(
625 Symbol::from("(1)SPY C400_((1))SPY C410"),
626 Venue::from("SMART"),
627 );
628 let account_id = AccountId::from("IB-001");
629
630 let order_status = OrderStatus {
631 order_id: 12345,
632 status: String::from("Filled"),
633 filled: 1.0,
634 remaining: 0.0,
635 average_fill_price: -2.25,
636 perm_id: 0,
637 parent_id: 0,
638 last_fill_price: -2.25,
639 client_id: 0,
640 why_held: String::new(),
641 market_cap_price: 0.0,
642 };
643
644 let report = parse_order_status_to_report(
645 &order_status,
646 None,
647 instrument_id,
648 account_id,
649 &instrument_provider,
650 UnixNanos::new(0),
651 )
652 .unwrap();
653
654 assert_eq!(report.avg_px, Some(Decimal::from_str("-2.25").unwrap()));
655 }
656
657 #[rstest]
658 fn test_parse_order_status_to_report_inactive_maps_to_rejected() {
659 let instrument_provider = create_test_instrument_provider();
660 let instrument_id = create_test_instrument_id();
661 let account_id = AccountId::from("IB-001");
662
663 let order_status = OrderStatus {
664 order_id: 12345,
665 status: String::from("Inactive"),
666 filled: 0.0,
667 remaining: 100.0,
668 average_fill_price: 0.0,
669 perm_id: 0,
670 parent_id: 0,
671 last_fill_price: 0.0,
672 client_id: 0,
673 why_held: String::new(),
674 market_cap_price: 0.0,
675 };
676
677 let report = parse_order_status_to_report(
678 &order_status,
679 None,
680 instrument_id,
681 account_id,
682 &instrument_provider,
683 UnixNanos::new(0),
684 )
685 .unwrap();
686
687 assert_eq!(report.order_status, NautilusOrderStatus::Rejected);
688 }
689
690 #[rstest]
691 #[case(
692 "MKT",
693 None,
694 None,
695 None,
696 None,
697 OrderType::Market,
698 None,
699 None,
700 None,
701 None,
702 TrailingOffsetType::NoTrailingOffset
703 )]
704 #[case(
705 "LMT",
706 Some(185.0),
707 None,
708 None,
709 None,
710 OrderType::Limit,
711 Some(Price::new(185.0, 0)),
712 None,
713 None,
714 None,
715 TrailingOffsetType::NoTrailingOffset
716 )]
717 #[case(
718 "MIT",
719 None,
720 Some(180.0),
721 None,
722 None,
723 OrderType::MarketIfTouched,
724 None,
725 Some(Price::new(180.0, 0)),
726 None,
727 None,
728 TrailingOffsetType::NoTrailingOffset
729 )]
730 #[case(
731 "LIT",
732 Some(179.0),
733 Some(180.0),
734 None,
735 None,
736 OrderType::LimitIfTouched,
737 Some(Price::new(179.0, 0)),
738 Some(Price::new(180.0, 0)),
739 None,
740 None,
741 TrailingOffsetType::NoTrailingOffset
742 )]
743 #[case(
744 "STP",
745 None,
746 Some(180.0),
747 None,
748 None,
749 OrderType::StopMarket,
750 None,
751 Some(Price::new(180.0, 0)),
752 None,
753 None,
754 TrailingOffsetType::NoTrailingOffset
755 )]
756 #[case(
757 "STP LMT",
758 Some(179.0),
759 Some(180.0),
760 None,
761 None,
762 OrderType::StopLimit,
763 Some(Price::new(179.0, 0)),
764 Some(Price::new(180.0, 0)),
765 None,
766 None,
767 TrailingOffsetType::NoTrailingOffset
768 )]
769 #[case(
770 "TRAIL LIMIT",
771 None,
772 Some(2.5),
773 Some(185.0),
774 Some(0.25),
775 OrderType::TrailingStopLimit,
776 None,
777 Some(Price::new(185.0, 0)),
778 Some(Decimal::from_str("0.25").unwrap()),
779 Some(Decimal::from_str("2.5").unwrap()),
780 TrailingOffsetType::Price,
781 )]
782 fn test_parse_order_status_to_report_maps_pricing_fields_by_order_type(
783 #[case] ib_order_type: &str,
784 #[case] limit_price: Option<f64>,
785 #[case] aux_price: Option<f64>,
786 #[case] trail_stop_price: Option<f64>,
787 #[case] limit_price_offset: Option<f64>,
788 #[case] expected_order_type: OrderType,
789 #[case] expected_price: Option<Price>,
790 #[case] expected_trigger_price: Option<Price>,
791 #[case] expected_limit_offset: Option<Decimal>,
792 #[case] expected_trailing_offset: Option<Decimal>,
793 #[case] expected_trailing_offset_type: TrailingOffsetType,
794 ) {
795 let instrument_provider = create_test_instrument_provider();
796 let instrument_id = create_test_instrument_id();
797 let account_id = AccountId::from("IB-001");
798
799 let order_status = OrderStatus {
800 order_id: 12345,
801 status: String::from("Submitted"),
802 filled: 0.0,
803 remaining: 5.0,
804 average_fill_price: 0.0,
805 perm_id: 0,
806 parent_id: 0,
807 last_fill_price: 0.0,
808 client_id: 0,
809 why_held: String::new(),
810 market_cap_price: 0.0,
811 };
812
813 let order = Order {
814 action: Action::Buy,
815 total_quantity: 5.0,
816 order_type: ib_order_type.to_string(),
817 limit_price,
818 aux_price,
819 trail_stop_price,
820 limit_price_offset,
821 tif: ibapi::orders::TimeInForce::GoodTilCanceled,
822 ..Default::default()
823 };
824
825 let report = parse_order_status_to_report(
826 &order_status,
827 Some(&order),
828 instrument_id,
829 account_id,
830 &instrument_provider,
831 UnixNanos::new(0),
832 )
833 .unwrap();
834
835 assert_eq!(report.order_type, expected_order_type);
836 assert_eq!(report.price, expected_price);
837 assert_eq!(report.trigger_price, expected_trigger_price);
838 assert_eq!(report.limit_offset, expected_limit_offset);
839 assert_eq!(report.trailing_offset, expected_trailing_offset);
840 assert_eq!(report.trailing_offset_type, expected_trailing_offset_type);
841 }
842
843 #[rstest]
844 fn test_parse_order_status_to_report_maps_trailing_percent_to_basis_points() {
845 let instrument_provider = create_test_instrument_provider();
846 let instrument_id = create_test_instrument_id();
847 let account_id = AccountId::from("IB-001");
848
849 let order_status = OrderStatus {
850 order_id: 12345,
851 status: String::from("Submitted"),
852 filled: 0.0,
853 remaining: 5.0,
854 average_fill_price: 0.0,
855 perm_id: 0,
856 parent_id: 0,
857 last_fill_price: 0.0,
858 client_id: 0,
859 why_held: String::new(),
860 market_cap_price: 0.0,
861 };
862
863 let order = Order {
864 action: Action::Buy,
865 total_quantity: 5.0,
866 order_type: "TRAIL".to_string(),
867 trail_stop_price: Some(185.0),
868 trailing_percent: Some(2.5),
869 tif: ibapi::orders::TimeInForce::GoodTilCanceled,
870 ..Default::default()
871 };
872
873 let report = parse_order_status_to_report(
874 &order_status,
875 Some(&order),
876 instrument_id,
877 account_id,
878 &instrument_provider,
879 UnixNanos::new(0),
880 )
881 .unwrap();
882
883 assert_eq!(report.order_type, OrderType::TrailingStopMarket);
884 assert_eq!(report.trigger_price, Some(Price::new(185.0, 0)));
885 assert_eq!(
886 report.trailing_offset,
887 Some(Decimal::from_str("250").unwrap())
888 );
889 assert_eq!(report.trailing_offset_type, TrailingOffsetType::BasisPoints);
890 assert_eq!(report.limit_offset, None);
891 }
892
893 #[rstest]
894 fn test_parse_execution_to_fill_report_buy() {
895 let instrument_provider = create_test_instrument_provider();
896 let instrument_id = create_test_instrument_id();
897 let account_id = AccountId::from("IB-001");
898
899 let execution = Execution {
900 order_id: 12345,
901 client_id: 0,
902 execution_id: String::from("EXEC-001"),
903 time: String::from("20230223 00:43:36 Universal"),
904 account_number: String::new(),
905 exchange: String::new(),
906 side: String::from("BOT"),
907 shares: 100.0,
908 price: 150.25,
909 perm_id: 0,
910 liquidation: 0,
911 cumulative_quantity: 100.0,
912 average_price: 150.25,
913 order_reference: String::from("ORDER-REF-001"),
914 ev_rule: String::new(),
915 ev_multiplier: None,
916 model_code: String::new(),
917 last_liquidity: Liquidity::None,
918 pending_price_revision: false,
919 submitter: String::new(),
920 };
921
922 let contract = Contract::default();
923 let result = parse_execution_to_fill_report(
924 &execution,
925 &contract,
926 1.0,
927 "USD",
928 instrument_id,
929 account_id,
930 &instrument_provider,
931 UnixNanos::new(0),
932 None, );
934
935 match result {
937 Err(e) => {
938 let error_msg = e.to_string();
939 assert!(
940 error_msg.contains("not found") || error_msg.contains("instrument"),
941 "Unexpected error: {}",
942 error_msg
943 );
944 }
945 Ok(fill) => {
946 assert_eq!(fill.order_side, OrderSide::Buy);
947 assert_eq!(fill.trade_id.to_string(), "EXEC-001");
948 }
949 }
950 }
951
952 #[rstest]
953 fn test_parse_execution_to_fill_report_sell() {
954 let instrument_provider = create_test_instrument_provider();
955 let instrument_id = create_test_instrument_id();
956 let account_id = AccountId::from("IB-001");
957
958 let execution = Execution {
959 order_id: 12345,
960 client_id: 0,
961 execution_id: String::from("EXEC-002"),
962 time: String::from("20230223 00:43:36 Universal"),
963 account_number: String::new(),
964 exchange: String::new(),
965 side: String::from("SLD"),
966 shares: 50.0,
967 price: 151.0,
968 perm_id: 0,
969 liquidation: 0,
970 cumulative_quantity: 50.0,
971 average_price: 151.0,
972 order_reference: String::new(),
973 ev_rule: String::new(),
974 ev_multiplier: None,
975 model_code: String::new(),
976 last_liquidity: Liquidity::None,
977 pending_price_revision: false,
978 submitter: String::new(),
979 };
980
981 let contract = Contract::default();
982 let result = parse_execution_to_fill_report(
983 &execution,
984 &contract,
985 0.5,
986 "USD",
987 instrument_id,
988 account_id,
989 &instrument_provider,
990 UnixNanos::new(0),
991 None, );
993
994 match result {
996 Err(e) => {
997 let error_msg = e.to_string();
998 assert!(
999 error_msg.contains("not found") || error_msg.contains("instrument"),
1000 "Unexpected error: {}",
1001 error_msg
1002 );
1003 }
1004 Ok(fill) => {
1005 assert_eq!(fill.order_side, OrderSide::Sell);
1006 }
1007 }
1008 }
1009}