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