1use std::fmt::Display;
17
18use enum_dispatch::enum_dispatch;
19use serde::{Deserialize, Serialize};
20
21use super::{
22 Order, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder, market::MarketOrder,
23 market_if_touched::MarketIfTouchedOrder, market_to_limit::MarketToLimitOrder,
24 stop_limit::StopLimitOrder, stop_market::StopMarketOrder,
25 trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder,
26};
27use crate::{events::OrderEventAny, identifiers::OrderListId, types::Price};
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
30#[enum_dispatch(Order)]
31pub enum OrderAny {
32 Limit(LimitOrder),
33 LimitIfTouched(LimitIfTouchedOrder),
34 Market(MarketOrder),
35 MarketIfTouched(MarketIfTouchedOrder),
36 MarketToLimit(MarketToLimitOrder),
37 StopLimit(StopLimitOrder),
38 StopMarket(StopMarketOrder),
39 TrailingStopLimit(TrailingStopLimitOrder),
40 TrailingStopMarket(TrailingStopMarketOrder),
41}
42
43impl OrderAny {
44 #[expect(clippy::missing_panics_doc)] pub fn from_events(events: Vec<OrderEventAny>) -> anyhow::Result<Self> {
57 if events.is_empty() {
58 anyhow::bail!("No order events provided to create OrderAny");
59 }
60
61 let init_event = events.first().unwrap();
63 match init_event {
64 OrderEventAny::Initialized(init) => {
65 let mut order = Self::try_from(init.clone())
66 .map_err(|e| anyhow::anyhow!("Invalid `OrderInitialized` event: {e}"))?;
67 for event in events.into_iter().skip(1) {
69 order.apply(event)?;
71 }
72 Ok(order)
73 }
74 _ => {
75 anyhow::bail!("First event must be `OrderInitialized`");
76 }
77 }
78 }
79
80 #[must_use]
88 pub fn init_event(&self) -> &crate::events::OrderInitialized {
89 match self
90 .events()
91 .first()
92 .expect("Order invariant violated: no events")
93 {
94 OrderEventAny::Initialized(init) => init,
95 _ => panic!("Order invariant violated: first event must be OrderInitialized"),
96 }
97 }
98
99 pub fn set_order_list_id(&mut self, id: OrderListId) {
103 match self {
104 Self::Limit(o) => o.order_list_id = Some(id),
105 Self::LimitIfTouched(o) => o.order_list_id = Some(id),
106 Self::Market(o) => o.order_list_id = Some(id),
107 Self::MarketIfTouched(o) => o.order_list_id = Some(id),
108 Self::MarketToLimit(o) => o.order_list_id = Some(id),
109 Self::StopLimit(o) => o.order_list_id = Some(id),
110 Self::StopMarket(o) => o.order_list_id = Some(id),
111 Self::TrailingStopLimit(o) => o.order_list_id = Some(id),
112 Self::TrailingStopMarket(o) => o.order_list_id = Some(id),
113 }
114 }
115}
116
117impl PartialEq for OrderAny {
118 fn eq(&self, other: &Self) -> bool {
119 self.client_order_id() == other.client_order_id()
120 }
121}
122
123impl Eq for OrderAny {}
125
126impl Display for OrderAny {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 write!(
129 f,
130 "{}",
131 match self {
132 Self::Limit(order) => order.to_string(),
133 Self::LimitIfTouched(order) => order.to_string(),
134 Self::Market(order) => order.to_string(),
135 Self::MarketIfTouched(order) => order.to_string(),
136 Self::MarketToLimit(order) => order.to_string(),
137 Self::StopLimit(order) => order.to_string(),
138 Self::StopMarket(order) => order.to_string(),
139 Self::TrailingStopLimit(order) => order.to_string(),
140 Self::TrailingStopMarket(order) => order.to_string(),
141 }
142 )
143 }
144}
145
146impl TryFrom<OrderAny> for PassiveOrderAny {
147 type Error = String;
148
149 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
150 match order {
151 OrderAny::Limit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
152 OrderAny::LimitIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
153 OrderAny::MarketIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
154 OrderAny::StopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
155 OrderAny::StopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
156 OrderAny::TrailingStopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
157 OrderAny::TrailingStopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
158 OrderAny::MarketToLimit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
159 OrderAny::Market(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
160 }
161 }
162}
163
164impl From<PassiveOrderAny> for OrderAny {
165 fn from(order: PassiveOrderAny) -> Self {
166 match order {
167 PassiveOrderAny::Limit(order) => order.into(),
168 PassiveOrderAny::Stop(order) => order.into(),
169 }
170 }
171}
172
173impl TryFrom<OrderAny> for StopOrderAny {
174 type Error = String;
175
176 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
177 match order {
178 OrderAny::LimitIfTouched(order) => Ok(Self::LimitIfTouched(order)),
179 OrderAny::MarketIfTouched(order) => Ok(Self::MarketIfTouched(order)),
180 OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
181 OrderAny::StopMarket(order) => Ok(Self::StopMarket(order)),
182 OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
183 OrderAny::TrailingStopMarket(order) => Ok(Self::TrailingStopMarket(order)),
184 _ => Err(format!(
185 "Cannot convert {:?} order to StopOrderAny: order type does not have a stop/trigger price",
186 order.order_type()
187 )),
188 }
189 }
190}
191
192impl From<StopOrderAny> for OrderAny {
193 fn from(order: StopOrderAny) -> Self {
194 match order {
195 StopOrderAny::LimitIfTouched(order) => Self::LimitIfTouched(order),
196 StopOrderAny::MarketIfTouched(order) => Self::MarketIfTouched(order),
197 StopOrderAny::StopLimit(order) => Self::StopLimit(order),
198 StopOrderAny::StopMarket(order) => Self::StopMarket(order),
199 StopOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
200 StopOrderAny::TrailingStopMarket(order) => Self::TrailingStopMarket(order),
201 }
202 }
203}
204
205impl TryFrom<OrderAny> for LimitOrderAny {
206 type Error = String;
207
208 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
209 match order {
210 OrderAny::Limit(order) => Ok(Self::Limit(order)),
211 OrderAny::MarketToLimit(order) => Ok(Self::MarketToLimit(order)),
212 OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
213 OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
214 OrderAny::Market(order) => Ok(Self::MarketOrderWithProtection(order)),
215 _ => Err(format!(
216 "Cannot convert {:?} order to LimitOrderAny: order type does not have a limit price",
217 order.order_type()
218 )),
219 }
220 }
221}
222
223impl From<LimitOrderAny> for OrderAny {
224 fn from(order: LimitOrderAny) -> Self {
225 match order {
226 LimitOrderAny::Limit(order) => Self::Limit(order),
227 LimitOrderAny::MarketToLimit(order) => Self::MarketToLimit(order),
228 LimitOrderAny::StopLimit(order) => Self::StopLimit(order),
229 LimitOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
230 LimitOrderAny::MarketOrderWithProtection(order) => Self::Market(order),
231 }
232 }
233}
234
235#[derive(Clone, Debug)]
236#[enum_dispatch(Order)]
237pub enum PassiveOrderAny {
238 Limit(LimitOrderAny),
239 Stop(StopOrderAny),
240}
241
242impl PassiveOrderAny {
243 #[must_use]
244 pub fn to_any(&self) -> OrderAny {
245 match self {
246 Self::Limit(order) => order.clone().into(),
247 Self::Stop(order) => order.clone().into(),
248 }
249 }
250}
251
252impl PartialEq for PassiveOrderAny {
254 fn eq(&self, rhs: &Self) -> bool {
255 match self {
256 Self::Limit(order) => order.client_order_id() == rhs.client_order_id(),
257 Self::Stop(order) => order.client_order_id() == rhs.client_order_id(),
258 }
259 }
260}
261
262#[derive(Clone, Debug)]
263#[enum_dispatch(Order)]
264pub enum LimitOrderAny {
265 Limit(LimitOrder),
266 MarketToLimit(MarketToLimitOrder),
267 StopLimit(StopLimitOrder),
268 TrailingStopLimit(TrailingStopLimitOrder),
269 MarketOrderWithProtection(MarketOrder),
270}
271
272impl LimitOrderAny {
273 #[must_use]
279 pub fn limit_px(&self) -> Price {
280 match self {
281 Self::Limit(order) => order.price,
282 Self::MarketToLimit(order) => order.price.expect("MarketToLimit order price not set"),
283 Self::StopLimit(order) => order.price,
284 Self::TrailingStopLimit(order) => order.price,
285 Self::MarketOrderWithProtection(order) => {
286 order.protection_price.expect("No price for order")
287 }
288 }
289 }
290}
291
292impl PartialEq for LimitOrderAny {
293 fn eq(&self, rhs: &Self) -> bool {
294 match self {
295 Self::Limit(order) => order.client_order_id == rhs.client_order_id(),
296 Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(),
297 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
298 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
299 Self::MarketOrderWithProtection(order) => {
300 order.client_order_id == rhs.client_order_id()
301 }
302 }
303 }
304}
305
306#[derive(Clone, Debug)]
307#[enum_dispatch(Order)]
308pub enum StopOrderAny {
309 LimitIfTouched(LimitIfTouchedOrder),
310 MarketIfTouched(MarketIfTouchedOrder),
311 StopLimit(StopLimitOrder),
312 StopMarket(StopMarketOrder),
313 TrailingStopLimit(TrailingStopLimitOrder),
314 TrailingStopMarket(TrailingStopMarketOrder),
315}
316
317impl StopOrderAny {
318 #[must_use]
319 pub fn stop_px(&self) -> Price {
320 match self {
321 Self::LimitIfTouched(o) => o.trigger_price,
322 Self::MarketIfTouched(o) => o.trigger_price,
323 Self::StopLimit(o) => o.trigger_price,
324 Self::StopMarket(o) => o.trigger_price,
325 Self::TrailingStopLimit(o) => o.activation_price.unwrap_or(o.trigger_price),
326 Self::TrailingStopMarket(o) => o.activation_price.unwrap_or(o.trigger_price),
327 }
328 }
329}
330
331impl PartialEq for StopOrderAny {
333 fn eq(&self, rhs: &Self) -> bool {
334 match self {
335 Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(),
336 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
337 Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(),
338 Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(),
339 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
340 Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(),
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use rstest::rstest;
348 use rust_decimal::Decimal;
349 use rust_decimal_macros::dec;
350
351 use super::*;
352 use crate::{
353 enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
354 events::{
355 OrderEventAny, OrderInitialized, OrderUpdated, order::spec::OrderInitializedSpec,
356 },
357 identifiers::{ClientOrderId, InstrumentId, StrategyId},
358 orders::builder::OrderTestBuilder,
359 types::{Price, Quantity},
360 };
361
362 #[rstest]
363 fn test_order_any_equality() {
364 let client_order_id = ClientOrderId::from("ORDER-001");
366
367 let market_order = OrderTestBuilder::new(OrderType::Market)
368 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
369 .quantity(Quantity::from(10))
370 .client_order_id(client_order_id)
371 .build();
372
373 let limit_order = OrderTestBuilder::new(OrderType::Limit)
374 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
375 .quantity(Quantity::from(10))
376 .price(Price::new(100.0, 2))
377 .client_order_id(client_order_id)
378 .build();
379
380 assert_eq!(market_order, limit_order);
382 }
383
384 #[rstest]
385 fn test_order_any_conversion_from_events() {
386 let init_event = OrderInitializedSpec::builder()
388 .order_type(OrderType::Market)
389 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
390 .quantity(Quantity::from(10))
391 .build();
392
393 let events = vec![OrderEventAny::Initialized(init_event.clone())];
395
396 let order = OrderAny::from_events(events).unwrap();
398
399 assert_eq!(order.order_type(), OrderType::Market);
401 assert_eq!(order.instrument_id(), init_event.instrument_id);
402 assert_eq!(order.quantity(), init_event.quantity);
403 }
404
405 #[rstest]
406 fn test_order_any_from_events_empty_error() {
407 let events: Vec<OrderEventAny> = vec![];
408 let result = OrderAny::from_events(events);
409
410 assert!(result.is_err());
411 assert_eq!(
412 result.unwrap_err().to_string(),
413 "No order events provided to create OrderAny"
414 );
415 }
416
417 #[rstest]
418 fn test_order_any_from_events_invalid_init_returns_error() {
419 let init_event = OrderInitializedSpec::builder()
422 .order_type(OrderType::Limit)
423 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
424 .quantity(Quantity::from(10))
425 .build();
426
427 let events = vec![OrderEventAny::Initialized(init_event)];
428 let result = OrderAny::from_events(events);
429
430 assert!(result.is_err());
431 let msg = result.unwrap_err().to_string();
432 assert!(
433 msg.contains("Invalid `OrderInitialized` event")
434 && msg.contains("`price` is required for `LimitOrder`"),
435 "unexpected error message: {msg}"
436 );
437 }
438
439 #[rstest]
440 #[case::buy(
441 OrderSide::Buy,
442 Price::from("100.00"),
443 Price::from("101.00"),
444 "BUY Limit-If-Touched"
445 )]
446 #[case::sell(
447 OrderSide::Sell,
448 Price::from("100.00"),
449 Price::from("99.00"),
450 "SELL Limit-If-Touched"
451 )]
452 fn test_order_any_from_events_invalid_predicate_returns_error(
453 #[case] side: OrderSide,
454 #[case] price: Price,
455 #[case] trigger_price: Price,
456 #[case] expected_msg: &str,
457 ) {
458 let init_event = OrderInitializedSpec::builder()
462 .order_type(OrderType::LimitIfTouched)
463 .order_side(side)
464 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
465 .quantity(Quantity::from(10))
466 .price(price)
467 .trigger_price(trigger_price)
468 .trigger_type(TriggerType::LastPrice)
469 .build();
470
471 let events = vec![OrderEventAny::Initialized(init_event)];
472 let result = OrderAny::from_events(events);
473
474 assert!(result.is_err());
475 let msg = result.unwrap_err().to_string();
476 assert!(
477 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
478 "unexpected error message: {msg}"
479 );
480 }
481
482 #[allow(clippy::too_many_arguments)]
483 fn make_init_with_optional_fields(
484 order_type: OrderType,
485 price: Option<Price>,
486 trigger_price: Option<Price>,
487 trigger_type: Option<TriggerType>,
488 limit_offset: Option<Decimal>,
489 trailing_offset: Option<Decimal>,
490 trailing_offset_type: Option<TrailingOffsetType>,
491 ) -> OrderInitialized {
492 OrderInitializedSpec::builder()
493 .order_type(order_type)
494 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
495 .quantity(Quantity::from(10))
496 .maybe_price(price)
497 .maybe_trigger_price(trigger_price)
498 .maybe_trigger_type(trigger_type)
499 .maybe_limit_offset(limit_offset)
500 .maybe_trailing_offset(trailing_offset)
501 .maybe_trailing_offset_type(trailing_offset_type)
502 .build()
503 }
504
505 #[rstest]
506 #[case::lit_missing_price(
507 make_init_with_optional_fields(
508 OrderType::LimitIfTouched,
509 None,
510 Some(Price::from("100.00")),
511 Some(TriggerType::LastPrice),
512 None,
513 None,
514 None,
515 ),
516 "`price` is required for `LimitIfTouchedOrder`"
517 )]
518 #[case::lit_missing_trigger_price(
519 make_init_with_optional_fields(
520 OrderType::LimitIfTouched,
521 Some(Price::from("100.00")),
522 None,
523 Some(TriggerType::LastPrice),
524 None,
525 None,
526 None,
527 ),
528 "`trigger_price` is required for `LimitIfTouchedOrder`"
529 )]
530 #[case::lit_missing_trigger_type(
531 make_init_with_optional_fields(
532 OrderType::LimitIfTouched,
533 Some(Price::from("100.00")),
534 Some(Price::from("99.00")),
535 None,
536 None,
537 None,
538 None,
539 ),
540 "`trigger_type` is required for `LimitIfTouchedOrder`"
541 )]
542 #[case::stop_limit_missing_price(
543 make_init_with_optional_fields(
544 OrderType::StopLimit,
545 None,
546 Some(Price::from("100.00")),
547 Some(TriggerType::LastPrice),
548 None,
549 None,
550 None,
551 ),
552 "`price` is required for `StopLimitOrder`"
553 )]
554 #[case::stop_limit_missing_trigger_price(
555 make_init_with_optional_fields(
556 OrderType::StopLimit,
557 Some(Price::from("100.00")),
558 None,
559 Some(TriggerType::LastPrice),
560 None,
561 None,
562 None,
563 ),
564 "`trigger_price` is required for `StopLimitOrder`"
565 )]
566 #[case::stop_limit_missing_trigger_type(
567 make_init_with_optional_fields(
568 OrderType::StopLimit,
569 Some(Price::from("100.00")),
570 Some(Price::from("99.00")),
571 None,
572 None,
573 None,
574 None,
575 ),
576 "`trigger_type` is required for `StopLimitOrder`"
577 )]
578 #[case::stop_market_missing_trigger_price(
579 make_init_with_optional_fields(
580 OrderType::StopMarket,
581 None,
582 None,
583 Some(TriggerType::LastPrice),
584 None,
585 None,
586 None,
587 ),
588 "`trigger_price` is required for `StopMarketOrder`"
589 )]
590 #[case::stop_market_missing_trigger_type(
591 make_init_with_optional_fields(
592 OrderType::StopMarket,
593 None,
594 Some(Price::from("100.00")),
595 None,
596 None,
597 None,
598 None,
599 ),
600 "`trigger_type` is required for `StopMarketOrder`"
601 )]
602 #[case::mit_missing_trigger_price(
603 make_init_with_optional_fields(
604 OrderType::MarketIfTouched,
605 None,
606 None,
607 Some(TriggerType::LastPrice),
608 None,
609 None,
610 None,
611 ),
612 "`trigger_price` is required for `MarketIfTouchedOrder`"
613 )]
614 #[case::mit_missing_trigger_type(
615 make_init_with_optional_fields(
616 OrderType::MarketIfTouched,
617 None,
618 Some(Price::from("100.00")),
619 None,
620 None,
621 None,
622 None,
623 ),
624 "`trigger_type` is required for `MarketIfTouchedOrder`"
625 )]
626 #[case::tsl_missing_price(
627 make_init_with_optional_fields(
628 OrderType::TrailingStopLimit,
629 None, Some(Price::from("99.00")), Some(TriggerType::LastPrice),
630 Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
631 ),
632 "`price` is required for `TrailingStopLimitOrder`",
633 )]
634 #[case::tsl_missing_trigger_price(
635 make_init_with_optional_fields(
636 OrderType::TrailingStopLimit,
637 Some(Price::from("100.00")), None, Some(TriggerType::LastPrice),
638 Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
639 ),
640 "`trigger_price` is required for `TrailingStopLimitOrder`",
641 )]
642 #[case::tsl_missing_trigger_type(
643 make_init_with_optional_fields(
644 OrderType::TrailingStopLimit,
645 Some(Price::from("100.00")), Some(Price::from("99.00")), None,
646 Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
647 ),
648 "`trigger_type` is required for `TrailingStopLimitOrder`",
649 )]
650 #[case::tsl_missing_limit_offset(
651 make_init_with_optional_fields(
652 OrderType::TrailingStopLimit,
653 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
654 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
655 ),
656 "`limit_offset` is required for `TrailingStopLimitOrder`",
657 )]
658 #[case::tsl_missing_trailing_offset(
659 make_init_with_optional_fields(
660 OrderType::TrailingStopLimit,
661 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
662 Some(dec!(1)), None, Some(TrailingOffsetType::Price),
663 ),
664 "`trailing_offset` is required for `TrailingStopLimitOrder`",
665 )]
666 #[case::tsl_missing_trailing_offset_type(
667 make_init_with_optional_fields(
668 OrderType::TrailingStopLimit,
669 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
670 Some(dec!(1)), Some(dec!(1)), None,
671 ),
672 "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
673 )]
674 #[case::tsm_missing_trigger_price(
675 make_init_with_optional_fields(
676 OrderType::TrailingStopMarket,
677 None, None, Some(TriggerType::LastPrice),
678 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
679 ),
680 "`trigger_price` is required for `TrailingStopMarketOrder`",
681 )]
682 #[case::tsm_missing_trigger_type(
683 make_init_with_optional_fields(
684 OrderType::TrailingStopMarket,
685 None, Some(Price::from("100.00")), None,
686 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
687 ),
688 "`trigger_type` is required for `TrailingStopMarketOrder`",
689 )]
690 #[case::tsm_missing_trailing_offset(
691 make_init_with_optional_fields(
692 OrderType::TrailingStopMarket,
693 None,
694 Some(Price::from("100.00")),
695 Some(TriggerType::LastPrice),
696 None,
697 None,
698 Some(TrailingOffsetType::Price),
699 ),
700 "`trailing_offset` is required for `TrailingStopMarketOrder`"
701 )]
702 #[case::tsm_missing_trailing_offset_type(
703 make_init_with_optional_fields(
704 OrderType::TrailingStopMarket,
705 None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
706 None, Some(dec!(1)), None,
707 ),
708 "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
709 )]
710 fn test_order_any_from_events_missing_required_field_returns_error(
711 #[case] init: OrderInitialized,
712 #[case] expected_field_msg: &str,
713 ) {
714 let events = vec![OrderEventAny::Initialized(init)];
717 let result = OrderAny::from_events(events);
718
719 assert!(result.is_err());
720 let msg = result.unwrap_err().to_string();
721 assert!(
722 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
723 "unexpected error message: {msg}"
724 );
725 }
726
727 #[rstest]
728 fn test_order_any_from_events_wrong_first_event() {
729 let client_order_id = ClientOrderId::from("ORDER-001");
731 let strategy_id = StrategyId::from("STRATEGY-001");
732
733 let update_event = OrderUpdated {
734 client_order_id,
735 strategy_id,
736 quantity: Quantity::from(20),
737 ..Default::default()
738 };
739
740 let events = vec![OrderEventAny::Updated(update_event)];
742
743 let result = OrderAny::from_events(events);
745 assert!(result.is_err());
746 assert_eq!(
747 result.unwrap_err().to_string(),
748 "First event must be `OrderInitialized`"
749 );
750 }
751
752 #[rstest]
753 fn test_passive_order_any_conversion() {
754 let limit_order = OrderTestBuilder::new(OrderType::Limit)
756 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
757 .quantity(Quantity::from(10))
758 .price(Price::new(100.0, 2))
759 .build();
760
761 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
763 let order_any: OrderAny = passive_order.into();
764
765 assert_eq!(order_any.order_type(), OrderType::Limit);
767 assert_eq!(order_any.quantity(), Quantity::from(10));
768 }
769
770 #[rstest]
771 fn test_stop_order_any_conversion() {
772 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
774 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
775 .quantity(Quantity::from(10))
776 .trigger_price(Price::new(100.0, 2))
777 .build();
778
779 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
781 let order_any: OrderAny = stop_order_any.into();
782
783 assert_eq!(order_any.order_type(), OrderType::StopMarket);
785 assert_eq!(order_any.quantity(), Quantity::from(10));
786 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
787 }
788
789 #[rstest]
790 fn test_limit_order_any_conversion() {
791 let limit_order = OrderTestBuilder::new(OrderType::Limit)
793 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
794 .quantity(Quantity::from(10))
795 .price(Price::new(100.0, 2))
796 .build();
797
798 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
800 let order_any: OrderAny = limit_order_any.into();
801
802 assert_eq!(order_any.order_type(), OrderType::Limit);
804 assert_eq!(order_any.quantity(), Quantity::from(10));
805 }
806
807 #[rstest]
808 fn test_limit_order_any_limit_price() {
809 let limit_order = OrderTestBuilder::new(OrderType::Limit)
811 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
812 .quantity(Quantity::from(10))
813 .price(Price::new(100.0, 2))
814 .build();
815
816 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
818
819 let limit_px = limit_order_any.limit_px();
821 assert_eq!(limit_px, Price::new(100.0, 2));
822 }
823
824 #[rstest]
825 fn test_stop_order_any_stop_price() {
826 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
828 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
829 .quantity(Quantity::from(10))
830 .trigger_price(Price::new(100.0, 2))
831 .build();
832
833 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
835
836 let stop_px = stop_order_any.stop_px();
838 assert_eq!(stop_px, Price::new(100.0, 2));
839 }
840
841 #[rstest]
842 fn test_trailing_stop_market_order_conversion() {
843 let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
845 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
846 .quantity(Quantity::from(10))
847 .trigger_price(Price::new(100.0, 2))
848 .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
850 .build();
851
852 let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
854
855 let order_any: OrderAny = stop_order_any.into();
857
858 assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
860 assert_eq!(order_any.quantity(), Quantity::from(10));
861 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
862 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
863 assert_eq!(
864 order_any.trailing_offset_type(),
865 Some(TrailingOffsetType::NoTrailingOffset)
866 );
867 }
868
869 #[rstest]
870 fn test_trailing_stop_limit_order_conversion() {
871 let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
873 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
874 .quantity(Quantity::from(10))
875 .price(Price::new(99.0, 2))
876 .trigger_price(Price::new(100.0, 2))
877 .limit_offset(Decimal::new(10, 1)) .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
880 .build();
881
882 let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
884
885 assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
887
888 let order_any: OrderAny = limit_order_any.into();
890
891 assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
893 assert_eq!(order_any.quantity(), Quantity::from(10));
894 assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
895 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
896 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
897 }
898
899 #[rstest]
900 fn test_passive_order_any_to_any() {
901 let limit_order = OrderTestBuilder::new(OrderType::Limit)
903 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
904 .quantity(Quantity::from(10))
905 .price(Price::new(100.0, 2))
906 .build();
907
908 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
910
911 let order_any = passive_order.to_any();
913
914 assert_eq!(order_any.order_type(), OrderType::Limit);
916 assert_eq!(order_any.quantity(), Quantity::from(10));
917 assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
918 }
919}