1use std::fmt::Display;
17
18use enum_dispatch::enum_dispatch;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22use super::{
23 Order, OrderCore, OrderError, limit::LimitOrder, limit_if_touched::LimitIfTouchedOrder,
24 market::MarketOrder, market_if_touched::MarketIfTouchedOrder,
25 market_to_limit::MarketToLimitOrder, stop_limit::StopLimitOrder, stop_market::StopMarketOrder,
26 trailing_stop_limit::TrailingStopLimitOrder, trailing_stop_market::TrailingStopMarketOrder,
27};
28use crate::{events::OrderEventAny, identifiers::OrderListId, types::Price};
29
30#[derive(Debug, Error)]
32pub enum OrderReplayError {
33 #[error("No order events provided to create OrderAny")]
35 EmptyInput,
36 #[error("First event must be `OrderInitialized`")]
38 WrongFirstEvent,
39 #[error("Invalid `OrderInitialized` event: {source}")]
41 InvalidInitialization {
42 #[source]
44 source: OrderError,
45 },
46 #[error("{source}")]
48 ApplyFailed {
49 #[source]
51 source: OrderError,
52 },
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize)]
56#[enum_dispatch(Order)]
57pub enum OrderAny {
58 Limit(LimitOrder),
59 LimitIfTouched(LimitIfTouchedOrder),
60 Market(MarketOrder),
61 MarketIfTouched(MarketIfTouchedOrder),
62 MarketToLimit(MarketToLimitOrder),
63 StopLimit(StopLimitOrder),
64 StopMarket(StopMarketOrder),
65 TrailingStopLimit(TrailingStopLimitOrder),
66 TrailingStopMarket(TrailingStopMarketOrder),
67}
68
69impl OrderAny {
70 pub fn from_events(events: Vec<OrderEventAny>) -> Result<Self, OrderReplayError> {
82 let Some(init_event) = events.first() else {
83 return Err(OrderReplayError::EmptyInput);
84 };
85
86 let OrderEventAny::Initialized(init) = init_event else {
87 return Err(OrderReplayError::WrongFirstEvent);
88 };
89
90 let mut order = Self::try_from(init.clone())
91 .map_err(|source| OrderReplayError::InvalidInitialization { source })?;
92
93 for event in events.into_iter().skip(1) {
94 order
95 .apply(event)
96 .map_err(|source| OrderReplayError::ApplyFailed { source })?;
97 }
98
99 Ok(order)
100 }
101
102 #[must_use]
110 pub fn init_event(&self) -> &crate::events::OrderInitialized {
111 match self
112 .events()
113 .first()
114 .expect("Order invariant violated: no events")
115 {
116 OrderEventAny::Initialized(init) => init,
117 _ => panic!("Order invariant violated: first event must be OrderInitialized"),
118 }
119 }
120
121 pub fn set_order_list_id(&mut self, id: OrderListId) {
127 let order: &mut OrderCore = match self {
128 Self::Limit(order) => order,
129 Self::LimitIfTouched(order) => order,
130 Self::Market(order) => order,
131 Self::MarketIfTouched(order) => order,
132 Self::MarketToLimit(order) => order,
133 Self::StopLimit(order) => order,
134 Self::StopMarket(order) => order,
135 Self::TrailingStopLimit(order) => order,
136 Self::TrailingStopMarket(order) => order,
137 };
138 let init = match order
139 .events
140 .first_mut()
141 .expect("Order invariant violated: no events")
142 {
143 OrderEventAny::Initialized(init) => init,
144 _ => panic!("Order invariant violated: first event must be OrderInitialized"),
145 };
146
147 order.order_list_id = Some(id);
148 init.order_list_id = Some(id);
149 }
150}
151
152impl PartialEq for OrderAny {
153 fn eq(&self, other: &Self) -> bool {
154 self.client_order_id() == other.client_order_id()
155 }
156}
157
158impl Eq for OrderAny {}
160
161impl Display for OrderAny {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 write!(
164 f,
165 "{}",
166 match self {
167 Self::Limit(order) => order.to_string(),
168 Self::LimitIfTouched(order) => order.to_string(),
169 Self::Market(order) => order.to_string(),
170 Self::MarketIfTouched(order) => order.to_string(),
171 Self::MarketToLimit(order) => order.to_string(),
172 Self::StopLimit(order) => order.to_string(),
173 Self::StopMarket(order) => order.to_string(),
174 Self::TrailingStopLimit(order) => order.to_string(),
175 Self::TrailingStopMarket(order) => order.to_string(),
176 }
177 )
178 }
179}
180
181impl TryFrom<OrderAny> for PassiveOrderAny {
182 type Error = String;
183
184 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
185 match order {
186 OrderAny::Limit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
187 OrderAny::LimitIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
188 OrderAny::MarketIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
189 OrderAny::StopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
190 OrderAny::StopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
191 OrderAny::TrailingStopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
192 OrderAny::TrailingStopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
193 OrderAny::MarketToLimit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
194 OrderAny::Market(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
195 }
196 }
197}
198
199impl From<PassiveOrderAny> for OrderAny {
200 fn from(order: PassiveOrderAny) -> Self {
201 match order {
202 PassiveOrderAny::Limit(order) => order.into(),
203 PassiveOrderAny::Stop(order) => order.into(),
204 }
205 }
206}
207
208impl TryFrom<OrderAny> for StopOrderAny {
209 type Error = String;
210
211 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
212 match order {
213 OrderAny::LimitIfTouched(order) => Ok(Self::LimitIfTouched(order)),
214 OrderAny::MarketIfTouched(order) => Ok(Self::MarketIfTouched(order)),
215 OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
216 OrderAny::StopMarket(order) => Ok(Self::StopMarket(order)),
217 OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
218 OrderAny::TrailingStopMarket(order) => Ok(Self::TrailingStopMarket(order)),
219 _ => Err(format!(
220 "Cannot convert {:?} order to StopOrderAny: order type does not have a stop/trigger price",
221 order.order_type()
222 )),
223 }
224 }
225}
226
227impl From<StopOrderAny> for OrderAny {
228 fn from(order: StopOrderAny) -> Self {
229 match order {
230 StopOrderAny::LimitIfTouched(order) => Self::LimitIfTouched(order),
231 StopOrderAny::MarketIfTouched(order) => Self::MarketIfTouched(order),
232 StopOrderAny::StopLimit(order) => Self::StopLimit(order),
233 StopOrderAny::StopMarket(order) => Self::StopMarket(order),
234 StopOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
235 StopOrderAny::TrailingStopMarket(order) => Self::TrailingStopMarket(order),
236 }
237 }
238}
239
240impl TryFrom<OrderAny> for LimitOrderAny {
241 type Error = String;
242
243 fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
244 match order {
245 OrderAny::Limit(order) => Ok(Self::Limit(order)),
246 OrderAny::MarketToLimit(order) => Ok(Self::MarketToLimit(order)),
247 OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
248 OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
249 OrderAny::Market(order) => Ok(Self::MarketOrderWithProtection(order)),
250 _ => Err(format!(
251 "Cannot convert {:?} order to LimitOrderAny: order type does not have a limit price",
252 order.order_type()
253 )),
254 }
255 }
256}
257
258impl From<LimitOrderAny> for OrderAny {
259 fn from(order: LimitOrderAny) -> Self {
260 match order {
261 LimitOrderAny::Limit(order) => Self::Limit(order),
262 LimitOrderAny::MarketToLimit(order) => Self::MarketToLimit(order),
263 LimitOrderAny::StopLimit(order) => Self::StopLimit(order),
264 LimitOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
265 LimitOrderAny::MarketOrderWithProtection(order) => Self::Market(order),
266 }
267 }
268}
269
270#[derive(Clone, Debug)]
271#[enum_dispatch(Order)]
272pub enum PassiveOrderAny {
273 Limit(LimitOrderAny),
274 Stop(StopOrderAny),
275}
276
277impl PassiveOrderAny {
278 #[must_use]
279 pub fn to_any(&self) -> OrderAny {
280 match self {
281 Self::Limit(order) => order.clone().into(),
282 Self::Stop(order) => order.clone().into(),
283 }
284 }
285}
286
287impl PartialEq for PassiveOrderAny {
289 fn eq(&self, rhs: &Self) -> bool {
290 match self {
291 Self::Limit(order) => order.client_order_id() == rhs.client_order_id(),
292 Self::Stop(order) => order.client_order_id() == rhs.client_order_id(),
293 }
294 }
295}
296
297#[derive(Clone, Debug)]
298#[enum_dispatch(Order)]
299pub enum LimitOrderAny {
300 Limit(LimitOrder),
301 MarketToLimit(MarketToLimitOrder),
302 StopLimit(StopLimitOrder),
303 TrailingStopLimit(TrailingStopLimitOrder),
304 MarketOrderWithProtection(MarketOrder),
305}
306
307impl LimitOrderAny {
308 #[must_use]
314 pub fn limit_px(&self) -> Price {
315 match self {
316 Self::Limit(order) => order.price,
317 Self::MarketToLimit(order) => order.price.expect("MarketToLimit order price not set"),
318 Self::StopLimit(order) => order.price,
319 Self::TrailingStopLimit(order) => {
320 order.price.expect("TrailingStopLimit order price not set")
321 }
322 Self::MarketOrderWithProtection(order) => {
323 order.protection_price.expect("No price for order")
324 }
325 }
326 }
327}
328
329impl PartialEq for LimitOrderAny {
330 fn eq(&self, rhs: &Self) -> bool {
331 match self {
332 Self::Limit(order) => order.client_order_id == rhs.client_order_id(),
333 Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(),
334 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
335 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
336 Self::MarketOrderWithProtection(order) => {
337 order.client_order_id == rhs.client_order_id()
338 }
339 }
340 }
341}
342
343#[derive(Clone, Debug)]
344#[enum_dispatch(Order)]
345pub enum StopOrderAny {
346 LimitIfTouched(LimitIfTouchedOrder),
347 MarketIfTouched(MarketIfTouchedOrder),
348 StopLimit(StopLimitOrder),
349 StopMarket(StopMarketOrder),
350 TrailingStopLimit(TrailingStopLimitOrder),
351 TrailingStopMarket(TrailingStopMarketOrder),
352}
353
354impl StopOrderAny {
355 #[must_use]
356 pub fn stop_px(&self) -> Option<Price> {
357 match self {
358 Self::LimitIfTouched(o) => Some(o.trigger_price),
359 Self::MarketIfTouched(o) => Some(o.trigger_price),
360 Self::StopLimit(o) => Some(o.trigger_price),
361 Self::StopMarket(o) => Some(o.trigger_price),
362 Self::TrailingStopLimit(o) => o.activation_price.or(o.trigger_price),
363 Self::TrailingStopMarket(o) => o.activation_price.or(o.trigger_price),
364 }
365 }
366}
367
368impl PartialEq for StopOrderAny {
370 fn eq(&self, rhs: &Self) -> bool {
371 match self {
372 Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(),
373 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
374 Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(),
375 Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(),
376 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
377 Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(),
378 }
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 use nautilus_core::{UUID4, UnixNanos};
385 use rstest::rstest;
386 use rust_decimal::Decimal;
387 use rust_decimal_macros::dec;
388
389 use super::*;
390 use crate::{
391 enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
392 events::{
393 OrderEventAny, OrderInitialized, OrderUpdated, order::spec::OrderInitializedSpec,
394 },
395 identifiers::{ClientOrderId, InstrumentId, StrategyId},
396 orders::{OrderError, builder::OrderTestBuilder},
397 types::{Price, Quantity},
398 };
399
400 #[rstest]
401 fn test_order_any_equality() {
402 let client_order_id = ClientOrderId::from("ORDER-001");
404
405 let market_order = OrderTestBuilder::new(OrderType::Market)
406 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
407 .quantity(Quantity::from(10))
408 .client_order_id(client_order_id)
409 .build();
410
411 let limit_order = OrderTestBuilder::new(OrderType::Limit)
412 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
413 .quantity(Quantity::from(10))
414 .price(Price::new(100.0, 2))
415 .client_order_id(client_order_id)
416 .build();
417
418 assert_eq!(market_order, limit_order);
420 }
421
422 #[rstest]
423 fn test_order_any_conversion_from_events() {
424 let init_event = OrderInitializedSpec::builder()
426 .order_type(OrderType::Market)
427 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
428 .quantity(Quantity::from(10))
429 .build();
430
431 let events = vec![OrderEventAny::Initialized(init_event.clone())];
433
434 let order = OrderAny::from_events(events).unwrap();
436
437 assert_eq!(order.order_type(), OrderType::Market);
439 assert_eq!(order.instrument_id(), init_event.instrument_id);
440 assert_eq!(order.quantity(), init_event.quantity);
441 }
442
443 #[rstest]
444 #[case::limit(OrderType::Limit)]
445 #[case::limit_if_touched(OrderType::LimitIfTouched)]
446 #[case::market(OrderType::Market)]
447 #[case::market_if_touched(OrderType::MarketIfTouched)]
448 #[case::market_to_limit(OrderType::MarketToLimit)]
449 #[case::stop_limit(OrderType::StopLimit)]
450 #[case::stop_market(OrderType::StopMarket)]
451 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
452 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
453 fn test_set_order_list_id_updates_init_event_and_replay(#[case] order_type: OrderType) {
454 let mut order = order_for_list_id_test(order_type);
455 let init_before = order.init_event().clone();
456 let event_count = order.event_count();
457 let order_list_id = OrderListId::from("OL-SET-LIST-001");
458
459 order.set_order_list_id(order_list_id);
460
461 let mut expected_init = init_before;
462 expected_init.order_list_id = Some(order_list_id);
463 assert_eq!(order.order_list_id(), Some(order_list_id));
464 assert_eq!(order.init_event(), &expected_init);
465 assert_eq!(order.event_count(), event_count);
466
467 let persisted_events: Vec<_> = order.events().into_iter().cloned().collect();
468 assert_eq!(
469 persisted_events,
470 vec![OrderEventAny::Initialized(expected_init.clone())]
471 );
472
473 let replayed = OrderAny::from_events(persisted_events).unwrap();
474 assert_eq!(replayed.order_list_id(), Some(order_list_id));
475 assert_eq!(replayed.init_event(), &expected_init);
476 }
477
478 fn order_for_list_id_test(order_type: OrderType) -> OrderAny {
479 OrderTestBuilder::new(order_type)
480 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
481 .client_order_id(ClientOrderId::from("ORDER-LIST-001"))
482 .quantity(Quantity::from(10))
483 .price(Price::from("101.00"))
484 .trigger_price(Price::from("100.00"))
485 .trigger_type(TriggerType::LastPrice)
486 .limit_offset(dec!(1))
487 .trailing_offset(dec!(1))
488 .trailing_offset_type(TrailingOffsetType::Price)
489 .init_id(UUID4::from("16578139-a945-4b65-b46c-bc131a15d8e7"))
490 .ts_init(UnixNanos::from(123_456_789))
491 .build()
492 }
493
494 #[rstest]
495 fn test_order_any_from_events_empty_error() {
496 let events: Vec<OrderEventAny> = vec![];
497 let err = OrderAny::from_events(events).expect_err("empty events should fail");
498
499 assert!(matches!(err, OrderReplayError::EmptyInput));
500 assert_eq!(
501 err.to_string(),
502 "No order events provided to create OrderAny"
503 );
504 }
505
506 #[rstest]
507 fn test_order_any_from_events_invalid_init_returns_error() {
508 let init_event = OrderInitializedSpec::builder()
511 .order_type(OrderType::Limit)
512 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
513 .quantity(Quantity::from(10))
514 .build();
515
516 let events = vec![OrderEventAny::Initialized(init_event)];
517 let err =
518 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
519
520 match &err {
521 OrderReplayError::InvalidInitialization { source } => {
522 assert_eq!(
523 source.to_string(),
524 "`price` is required for `LimitOrder` initialization",
525 );
526 }
527 _ => panic!("expected InvalidInitialization, was {err:?}"),
528 }
529 assert_eq!(
530 err.to_string(),
531 "Invalid `OrderInitialized` event: `price` is required for `LimitOrder` initialization",
532 );
533 }
534
535 #[rstest]
536 #[case::buy(
537 OrderSide::Buy,
538 Price::from("100.00"),
539 Price::from("101.00"),
540 "BUY Limit-If-Touched"
541 )]
542 #[case::sell(
543 OrderSide::Sell,
544 Price::from("100.00"),
545 Price::from("99.00"),
546 "SELL Limit-If-Touched"
547 )]
548 fn test_order_any_from_events_invalid_predicate_returns_error(
549 #[case] side: OrderSide,
550 #[case] price: Price,
551 #[case] trigger_price: Price,
552 #[case] expected_msg: &str,
553 ) {
554 let init_event = OrderInitializedSpec::builder()
558 .order_type(OrderType::LimitIfTouched)
559 .order_side(side)
560 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
561 .quantity(Quantity::from(10))
562 .price(price)
563 .trigger_price(trigger_price)
564 .trigger_type(TriggerType::LastPrice)
565 .build();
566
567 let events = vec![OrderEventAny::Initialized(init_event)];
568 let err =
569 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
570
571 assert!(matches!(
572 err,
573 OrderReplayError::InvalidInitialization { .. }
574 ));
575 let msg = err.to_string();
576 assert!(
577 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
578 "unexpected error message: {msg}"
579 );
580 }
581
582 fn make_init_with_optional_fields(
583 order_type: OrderType,
584 price: Option<Price>,
585 trigger_price: Option<Price>,
586 trigger_type: Option<TriggerType>,
587 limit_offset: Option<Decimal>,
588 trailing_offset: Option<Decimal>,
589 trailing_offset_type: Option<TrailingOffsetType>,
590 ) -> OrderInitialized {
591 OrderInitializedSpec::builder()
592 .order_type(order_type)
593 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
594 .quantity(Quantity::from(10))
595 .maybe_price(price)
596 .maybe_trigger_price(trigger_price)
597 .maybe_trigger_type(trigger_type)
598 .maybe_limit_offset(limit_offset)
599 .maybe_trailing_offset(trailing_offset)
600 .maybe_trailing_offset_type(trailing_offset_type)
601 .build()
602 }
603
604 #[rstest]
605 #[case::lit_missing_price(
606 make_init_with_optional_fields(
607 OrderType::LimitIfTouched,
608 None,
609 Some(Price::from("100.00")),
610 Some(TriggerType::LastPrice),
611 None,
612 None,
613 None,
614 ),
615 "`price` is required for `LimitIfTouchedOrder`"
616 )]
617 #[case::lit_missing_trigger_price(
618 make_init_with_optional_fields(
619 OrderType::LimitIfTouched,
620 Some(Price::from("100.00")),
621 None,
622 Some(TriggerType::LastPrice),
623 None,
624 None,
625 None,
626 ),
627 "`trigger_price` is required for `LimitIfTouchedOrder`"
628 )]
629 #[case::lit_missing_trigger_type(
630 make_init_with_optional_fields(
631 OrderType::LimitIfTouched,
632 Some(Price::from("100.00")),
633 Some(Price::from("99.00")),
634 None,
635 None,
636 None,
637 None,
638 ),
639 "`trigger_type` is required for `LimitIfTouchedOrder`"
640 )]
641 #[case::stop_limit_missing_price(
642 make_init_with_optional_fields(
643 OrderType::StopLimit,
644 None,
645 Some(Price::from("100.00")),
646 Some(TriggerType::LastPrice),
647 None,
648 None,
649 None,
650 ),
651 "`price` is required for `StopLimitOrder`"
652 )]
653 #[case::stop_limit_missing_trigger_price(
654 make_init_with_optional_fields(
655 OrderType::StopLimit,
656 Some(Price::from("100.00")),
657 None,
658 Some(TriggerType::LastPrice),
659 None,
660 None,
661 None,
662 ),
663 "`trigger_price` is required for `StopLimitOrder`"
664 )]
665 #[case::stop_limit_missing_trigger_type(
666 make_init_with_optional_fields(
667 OrderType::StopLimit,
668 Some(Price::from("100.00")),
669 Some(Price::from("99.00")),
670 None,
671 None,
672 None,
673 None,
674 ),
675 "`trigger_type` is required for `StopLimitOrder`"
676 )]
677 #[case::stop_market_missing_trigger_price(
678 make_init_with_optional_fields(
679 OrderType::StopMarket,
680 None,
681 None,
682 Some(TriggerType::LastPrice),
683 None,
684 None,
685 None,
686 ),
687 "`trigger_price` is required for `StopMarketOrder`"
688 )]
689 #[case::stop_market_missing_trigger_type(
690 make_init_with_optional_fields(
691 OrderType::StopMarket,
692 None,
693 Some(Price::from("100.00")),
694 None,
695 None,
696 None,
697 None,
698 ),
699 "`trigger_type` is required for `StopMarketOrder`"
700 )]
701 #[case::mit_missing_trigger_price(
702 make_init_with_optional_fields(
703 OrderType::MarketIfTouched,
704 None,
705 None,
706 Some(TriggerType::LastPrice),
707 None,
708 None,
709 None,
710 ),
711 "`trigger_price` is required for `MarketIfTouchedOrder`"
712 )]
713 #[case::mit_missing_trigger_type(
714 make_init_with_optional_fields(
715 OrderType::MarketIfTouched,
716 None,
717 Some(Price::from("100.00")),
718 None,
719 None,
720 None,
721 None,
722 ),
723 "`trigger_type` is required for `MarketIfTouchedOrder`"
724 )]
725 #[case::tsl_missing_trigger_type(
726 make_init_with_optional_fields(
727 OrderType::TrailingStopLimit,
728 Some(Price::from("100.00")), Some(Price::from("99.00")), None,
729 Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
730 ),
731 "`trigger_type` is required for `TrailingStopLimitOrder`",
732 )]
733 #[case::tsl_missing_limit_offset(
734 make_init_with_optional_fields(
735 OrderType::TrailingStopLimit,
736 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
737 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
738 ),
739 "`limit_offset` is required for `TrailingStopLimitOrder`",
740 )]
741 #[case::tsl_missing_trailing_offset(
742 make_init_with_optional_fields(
743 OrderType::TrailingStopLimit,
744 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
745 Some(dec!(1)), None, Some(TrailingOffsetType::Price),
746 ),
747 "`trailing_offset` is required for `TrailingStopLimitOrder`",
748 )]
749 #[case::tsl_missing_trailing_offset_type(
750 make_init_with_optional_fields(
751 OrderType::TrailingStopLimit,
752 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
753 Some(dec!(1)), Some(dec!(1)), None,
754 ),
755 "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
756 )]
757 #[case::tsm_missing_trigger_type(
758 make_init_with_optional_fields(
759 OrderType::TrailingStopMarket,
760 None, Some(Price::from("100.00")), None,
761 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
762 ),
763 "`trigger_type` is required for `TrailingStopMarketOrder`",
764 )]
765 #[case::tsm_missing_trailing_offset(
766 make_init_with_optional_fields(
767 OrderType::TrailingStopMarket,
768 None,
769 Some(Price::from("100.00")),
770 Some(TriggerType::LastPrice),
771 None,
772 None,
773 Some(TrailingOffsetType::Price),
774 ),
775 "`trailing_offset` is required for `TrailingStopMarketOrder`"
776 )]
777 #[case::tsm_missing_trailing_offset_type(
778 make_init_with_optional_fields(
779 OrderType::TrailingStopMarket,
780 None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
781 None, Some(dec!(1)), None,
782 ),
783 "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
784 )]
785 fn test_order_any_from_events_missing_required_field_returns_error(
786 #[case] init: OrderInitialized,
787 #[case] expected_field_msg: &str,
788 ) {
789 let events = vec![OrderEventAny::Initialized(init)];
792 let err =
793 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
794
795 assert!(matches!(
796 err,
797 OrderReplayError::InvalidInitialization { .. }
798 ));
799 let msg = err.to_string();
800 assert!(
801 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
802 "unexpected error message: {msg}"
803 );
804 }
805
806 #[rstest]
807 fn test_order_any_from_events_wrong_first_event() {
808 let client_order_id = ClientOrderId::from("ORDER-001");
810 let strategy_id = StrategyId::from("STRATEGY-001");
811
812 let update_event = OrderUpdated {
813 client_order_id,
814 strategy_id,
815 quantity: Quantity::from(20),
816 ..Default::default()
817 };
818
819 let events = vec![OrderEventAny::Updated(update_event)];
821
822 let err = OrderAny::from_events(events).expect_err("wrong first event should fail replay");
824 assert!(matches!(err, OrderReplayError::WrongFirstEvent));
825 assert_eq!(err.to_string(), "First event must be `OrderInitialized`");
826 }
827
828 #[rstest]
829 fn test_order_any_from_events_apply_failure() {
830 let init_event = OrderInitializedSpec::builder()
831 .order_type(OrderType::Market)
832 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
833 .quantity(Quantity::from(10))
834 .build();
835
836 let events = vec![
837 OrderEventAny::Initialized(init_event.clone()),
838 OrderEventAny::Initialized(init_event),
839 ];
840 let err =
841 OrderAny::from_events(events).expect_err("later invalid event should fail replay");
842
843 match &err {
844 OrderReplayError::ApplyFailed { source } => {
845 assert!(matches!(source, OrderError::AlreadyInitialized));
846 }
847 _ => panic!("expected ApplyFailed, was {err:?}"),
848 }
849 assert_eq!(err.to_string(), "Order was already initialized");
850 }
851
852 #[rstest]
853 fn test_passive_order_any_conversion() {
854 let limit_order = OrderTestBuilder::new(OrderType::Limit)
856 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
857 .quantity(Quantity::from(10))
858 .price(Price::new(100.0, 2))
859 .build();
860
861 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
863 let order_any: OrderAny = passive_order.into();
864
865 assert_eq!(order_any.order_type(), OrderType::Limit);
867 assert_eq!(order_any.quantity(), Quantity::from(10));
868 }
869
870 #[rstest]
871 fn test_stop_order_any_conversion() {
872 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
874 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
875 .quantity(Quantity::from(10))
876 .trigger_price(Price::new(100.0, 2))
877 .build();
878
879 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
881 let order_any: OrderAny = stop_order_any.into();
882
883 assert_eq!(order_any.order_type(), OrderType::StopMarket);
885 assert_eq!(order_any.quantity(), Quantity::from(10));
886 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
887 }
888
889 #[rstest]
890 fn test_limit_order_any_conversion() {
891 let limit_order = OrderTestBuilder::new(OrderType::Limit)
893 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
894 .quantity(Quantity::from(10))
895 .price(Price::new(100.0, 2))
896 .build();
897
898 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
900 let order_any: OrderAny = limit_order_any.into();
901
902 assert_eq!(order_any.order_type(), OrderType::Limit);
904 assert_eq!(order_any.quantity(), Quantity::from(10));
905 }
906
907 #[rstest]
908 fn test_limit_order_any_limit_price() {
909 let limit_order = OrderTestBuilder::new(OrderType::Limit)
911 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
912 .quantity(Quantity::from(10))
913 .price(Price::new(100.0, 2))
914 .build();
915
916 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
918
919 let limit_px = limit_order_any.limit_px();
921 assert_eq!(limit_px, Price::new(100.0, 2));
922 }
923
924 #[rstest]
925 fn test_stop_order_any_stop_price() {
926 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
928 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
929 .quantity(Quantity::from(10))
930 .trigger_price(Price::new(100.0, 2))
931 .build();
932
933 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
935
936 let stop_px = stop_order_any.stop_px();
938 assert_eq!(stop_px, Some(Price::new(100.0, 2)));
939 }
940
941 #[rstest]
942 fn test_trailing_stop_market_order_conversion() {
943 let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
945 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
946 .quantity(Quantity::from(10))
947 .trigger_price(Price::new(100.0, 2))
948 .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
950 .build();
951
952 let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
954
955 let order_any: OrderAny = stop_order_any.into();
957
958 assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
960 assert_eq!(order_any.quantity(), Quantity::from(10));
961 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
962 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
963 assert_eq!(
964 order_any.trailing_offset_type(),
965 Some(TrailingOffsetType::NoTrailingOffset)
966 );
967 }
968
969 #[rstest]
970 fn test_trailing_stop_limit_order_conversion() {
971 let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
973 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
974 .quantity(Quantity::from(10))
975 .price(Price::new(99.0, 2))
976 .trigger_price(Price::new(100.0, 2))
977 .limit_offset(Decimal::new(10, 1)) .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
980 .build();
981
982 let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
984
985 assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
987
988 let order_any: OrderAny = limit_order_any.into();
990
991 assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
993 assert_eq!(order_any.quantity(), Quantity::from(10));
994 assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
995 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
996 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
997 }
998
999 #[rstest]
1000 fn test_passive_order_any_to_any() {
1001 let limit_order = OrderTestBuilder::new(OrderType::Limit)
1003 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1004 .quantity(Quantity::from(10))
1005 .price(Price::new(100.0, 2))
1006 .build();
1007
1008 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
1010
1011 let order_any = passive_order.to_any();
1013
1014 assert_eq!(order_any.order_type(), OrderType::Limit);
1016 assert_eq!(order_any.quantity(), Quantity::from(10));
1017 assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
1018 }
1019}