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 fn test_order_any_equality() {
401 let client_order_id = ClientOrderId::from("ORDER-001");
403
404 let market_order = OrderTestBuilder::new(OrderType::Market)
405 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
406 .quantity(Quantity::from(10))
407 .client_order_id(client_order_id)
408 .build();
409
410 let limit_order = OrderTestBuilder::new(OrderType::Limit)
411 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
412 .quantity(Quantity::from(10))
413 .price(Price::new(100.0, 2))
414 .client_order_id(client_order_id)
415 .build();
416
417 assert_eq!(market_order, limit_order);
419 }
420
421 #[rstest]
422 fn test_order_any_conversion_from_events() {
423 let init_event = OrderInitializedSpec::builder()
425 .order_type(OrderType::Market)
426 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
427 .quantity(Quantity::from(10))
428 .build();
429
430 let events = vec![OrderEventAny::Initialized(init_event.clone())];
432
433 let order = OrderAny::from_events(events).unwrap();
435
436 assert_eq!(order.order_type(), OrderType::Market);
438 assert_eq!(order.instrument_id(), init_event.instrument_id);
439 assert_eq!(order.quantity(), init_event.quantity);
440 }
441
442 #[rstest]
443 #[case::limit(OrderType::Limit)]
444 #[case::limit_if_touched(OrderType::LimitIfTouched)]
445 #[case::market(OrderType::Market)]
446 #[case::market_if_touched(OrderType::MarketIfTouched)]
447 #[case::market_to_limit(OrderType::MarketToLimit)]
448 #[case::stop_limit(OrderType::StopLimit)]
449 #[case::stop_market(OrderType::StopMarket)]
450 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
451 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
452 fn test_set_order_list_id_updates_init_event_and_replay(#[case] order_type: OrderType) {
453 let mut order = order_for_list_id_test(order_type);
454 let init_before = order.init_event().clone();
455 let event_count = order.event_count();
456 let order_list_id = OrderListId::from("OL-SET-LIST-001");
457
458 order.set_order_list_id(order_list_id);
459
460 let mut expected_init = init_before;
461 expected_init.order_list_id = Some(order_list_id);
462 assert_eq!(order.order_list_id(), Some(order_list_id));
463 assert_eq!(order.init_event(), &expected_init);
464 assert_eq!(order.event_count(), event_count);
465
466 let persisted_events: Vec<_> = order.events().into_iter().cloned().collect();
467 assert_eq!(
468 persisted_events,
469 vec![OrderEventAny::Initialized(expected_init.clone())]
470 );
471
472 let replayed = OrderAny::from_events(persisted_events).unwrap();
473 assert_eq!(replayed.order_list_id(), Some(order_list_id));
474 assert_eq!(replayed.init_event(), &expected_init);
475 }
476
477 fn order_for_list_id_test(order_type: OrderType) -> OrderAny {
478 OrderTestBuilder::new(order_type)
479 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
480 .client_order_id(ClientOrderId::from("ORDER-LIST-001"))
481 .quantity(Quantity::from(10))
482 .price(Price::from("101.00"))
483 .trigger_price(Price::from("100.00"))
484 .trigger_type(TriggerType::LastPrice)
485 .limit_offset(dec!(1))
486 .trailing_offset(dec!(1))
487 .trailing_offset_type(TrailingOffsetType::Price)
488 .init_id(UUID4::from("16578139-a945-4b65-b46c-bc131a15d8e7"))
489 .ts_init(UnixNanos::from(123_456_789))
490 .build()
491 }
492
493 #[rstest]
494 fn test_order_any_from_events_empty_error() {
495 let events: Vec<OrderEventAny> = vec![];
496 let err = OrderAny::from_events(events).expect_err("empty events should fail");
497
498 assert!(matches!(err, OrderReplayError::EmptyInput));
499 assert_eq!(
500 err.to_string(),
501 "No order events provided to create OrderAny"
502 );
503 }
504
505 #[rstest]
506 fn test_order_any_from_events_invalid_init_returns_error() {
507 let init_event = OrderInitializedSpec::builder()
510 .order_type(OrderType::Limit)
511 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
512 .quantity(Quantity::from(10))
513 .build();
514
515 let events = vec![OrderEventAny::Initialized(init_event)];
516 let err =
517 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
518
519 match &err {
520 OrderReplayError::InvalidInitialization { source } => {
521 assert_eq!(
522 source.to_string(),
523 "`price` is required for `LimitOrder` initialization",
524 );
525 }
526 _ => panic!("expected InvalidInitialization, was {err:?}"),
527 }
528 assert_eq!(
529 err.to_string(),
530 "Invalid `OrderInitialized` event: `price` is required for `LimitOrder` initialization",
531 );
532 }
533
534 #[rstest]
535 #[case::buy(
536 OrderSide::Buy,
537 Price::from("100.00"),
538 Price::from("101.00"),
539 "BUY Limit-If-Touched"
540 )]
541 #[case::sell(
542 OrderSide::Sell,
543 Price::from("100.00"),
544 Price::from("99.00"),
545 "SELL Limit-If-Touched"
546 )]
547 fn test_order_any_from_events_invalid_predicate_returns_error(
548 #[case] side: OrderSide,
549 #[case] price: Price,
550 #[case] trigger_price: Price,
551 #[case] expected_msg: &str,
552 ) {
553 let init_event = OrderInitializedSpec::builder()
557 .order_type(OrderType::LimitIfTouched)
558 .order_side(side)
559 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
560 .quantity(Quantity::from(10))
561 .price(price)
562 .trigger_price(trigger_price)
563 .trigger_type(TriggerType::LastPrice)
564 .build();
565
566 let events = vec![OrderEventAny::Initialized(init_event)];
567 let err =
568 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
569
570 assert!(matches!(
571 err,
572 OrderReplayError::InvalidInitialization { .. }
573 ));
574 let msg = err.to_string();
575 assert!(
576 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
577 "unexpected error message: {msg}"
578 );
579 }
580
581 fn make_init_with_optional_fields(
582 order_type: OrderType,
583 price: Option<Price>,
584 trigger_price: Option<Price>,
585 trigger_type: Option<TriggerType>,
586 limit_offset: Option<Decimal>,
587 trailing_offset: Option<Decimal>,
588 trailing_offset_type: Option<TrailingOffsetType>,
589 ) -> OrderInitialized {
590 OrderInitializedSpec::builder()
591 .order_type(order_type)
592 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
593 .quantity(Quantity::from(10))
594 .maybe_price(price)
595 .maybe_trigger_price(trigger_price)
596 .maybe_trigger_type(trigger_type)
597 .maybe_limit_offset(limit_offset)
598 .maybe_trailing_offset(trailing_offset)
599 .maybe_trailing_offset_type(trailing_offset_type)
600 .build()
601 }
602
603 #[rstest]
604 #[case::lit_missing_price(
605 make_init_with_optional_fields(
606 OrderType::LimitIfTouched,
607 None,
608 Some(Price::from("100.00")),
609 Some(TriggerType::LastPrice),
610 None,
611 None,
612 None,
613 ),
614 "`price` is required for `LimitIfTouchedOrder`"
615 )]
616 #[case::lit_missing_trigger_price(
617 make_init_with_optional_fields(
618 OrderType::LimitIfTouched,
619 Some(Price::from("100.00")),
620 None,
621 Some(TriggerType::LastPrice),
622 None,
623 None,
624 None,
625 ),
626 "`trigger_price` is required for `LimitIfTouchedOrder`"
627 )]
628 #[case::lit_missing_trigger_type(
629 make_init_with_optional_fields(
630 OrderType::LimitIfTouched,
631 Some(Price::from("100.00")),
632 Some(Price::from("99.00")),
633 None,
634 None,
635 None,
636 None,
637 ),
638 "`trigger_type` is required for `LimitIfTouchedOrder`"
639 )]
640 #[case::stop_limit_missing_price(
641 make_init_with_optional_fields(
642 OrderType::StopLimit,
643 None,
644 Some(Price::from("100.00")),
645 Some(TriggerType::LastPrice),
646 None,
647 None,
648 None,
649 ),
650 "`price` is required for `StopLimitOrder`"
651 )]
652 #[case::stop_limit_missing_trigger_price(
653 make_init_with_optional_fields(
654 OrderType::StopLimit,
655 Some(Price::from("100.00")),
656 None,
657 Some(TriggerType::LastPrice),
658 None,
659 None,
660 None,
661 ),
662 "`trigger_price` is required for `StopLimitOrder`"
663 )]
664 #[case::stop_limit_missing_trigger_type(
665 make_init_with_optional_fields(
666 OrderType::StopLimit,
667 Some(Price::from("100.00")),
668 Some(Price::from("99.00")),
669 None,
670 None,
671 None,
672 None,
673 ),
674 "`trigger_type` is required for `StopLimitOrder`"
675 )]
676 #[case::stop_market_missing_trigger_price(
677 make_init_with_optional_fields(
678 OrderType::StopMarket,
679 None,
680 None,
681 Some(TriggerType::LastPrice),
682 None,
683 None,
684 None,
685 ),
686 "`trigger_price` is required for `StopMarketOrder`"
687 )]
688 #[case::stop_market_missing_trigger_type(
689 make_init_with_optional_fields(
690 OrderType::StopMarket,
691 None,
692 Some(Price::from("100.00")),
693 None,
694 None,
695 None,
696 None,
697 ),
698 "`trigger_type` is required for `StopMarketOrder`"
699 )]
700 #[case::mit_missing_trigger_price(
701 make_init_with_optional_fields(
702 OrderType::MarketIfTouched,
703 None,
704 None,
705 Some(TriggerType::LastPrice),
706 None,
707 None,
708 None,
709 ),
710 "`trigger_price` is required for `MarketIfTouchedOrder`"
711 )]
712 #[case::mit_missing_trigger_type(
713 make_init_with_optional_fields(
714 OrderType::MarketIfTouched,
715 None,
716 Some(Price::from("100.00")),
717 None,
718 None,
719 None,
720 None,
721 ),
722 "`trigger_type` is required for `MarketIfTouchedOrder`"
723 )]
724 #[case::tsl_missing_trigger_type(
725 make_init_with_optional_fields(
726 OrderType::TrailingStopLimit,
727 Some(Price::from("100.00")), Some(Price::from("99.00")), None,
728 Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
729 ),
730 "`trigger_type` is required for `TrailingStopLimitOrder`",
731 )]
732 #[case::tsl_missing_limit_offset(
733 make_init_with_optional_fields(
734 OrderType::TrailingStopLimit,
735 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
736 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
737 ),
738 "`limit_offset` is required for `TrailingStopLimitOrder`",
739 )]
740 #[case::tsl_missing_trailing_offset(
741 make_init_with_optional_fields(
742 OrderType::TrailingStopLimit,
743 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
744 Some(dec!(1)), None, Some(TrailingOffsetType::Price),
745 ),
746 "`trailing_offset` is required for `TrailingStopLimitOrder`",
747 )]
748 #[case::tsl_missing_trailing_offset_type(
749 make_init_with_optional_fields(
750 OrderType::TrailingStopLimit,
751 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
752 Some(dec!(1)), Some(dec!(1)), None,
753 ),
754 "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
755 )]
756 #[case::tsm_missing_trigger_type(
757 make_init_with_optional_fields(
758 OrderType::TrailingStopMarket,
759 None, Some(Price::from("100.00")), None,
760 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
761 ),
762 "`trigger_type` is required for `TrailingStopMarketOrder`",
763 )]
764 #[case::tsm_missing_trailing_offset(
765 make_init_with_optional_fields(
766 OrderType::TrailingStopMarket,
767 None,
768 Some(Price::from("100.00")),
769 Some(TriggerType::LastPrice),
770 None,
771 None,
772 Some(TrailingOffsetType::Price),
773 ),
774 "`trailing_offset` is required for `TrailingStopMarketOrder`"
775 )]
776 #[case::tsm_missing_trailing_offset_type(
777 make_init_with_optional_fields(
778 OrderType::TrailingStopMarket,
779 None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
780 None, Some(dec!(1)), None,
781 ),
782 "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
783 )]
784 fn test_order_any_from_events_missing_required_field_returns_error(
785 #[case] init: OrderInitialized,
786 #[case] expected_field_msg: &str,
787 ) {
788 let events = vec![OrderEventAny::Initialized(init)];
791 let err =
792 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
793
794 assert!(matches!(
795 err,
796 OrderReplayError::InvalidInitialization { .. }
797 ));
798 let msg = err.to_string();
799 assert!(
800 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
801 "unexpected error message: {msg}"
802 );
803 }
804
805 #[rstest]
806 fn test_order_any_from_events_wrong_first_event() {
807 let client_order_id = ClientOrderId::from("ORDER-001");
809 let strategy_id = StrategyId::from("STRATEGY-001");
810
811 let update_event = OrderUpdated {
812 client_order_id,
813 strategy_id,
814 quantity: Quantity::from(20),
815 ..Default::default()
816 };
817
818 let events = vec![OrderEventAny::Updated(update_event)];
820
821 let err = OrderAny::from_events(events).expect_err("wrong first event should fail replay");
823 assert!(matches!(err, OrderReplayError::WrongFirstEvent));
824 assert_eq!(err.to_string(), "First event must be `OrderInitialized`");
825 }
826
827 #[rstest]
828 fn test_order_any_from_events_apply_failure() {
829 let init_event = OrderInitializedSpec::builder()
830 .order_type(OrderType::Market)
831 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
832 .quantity(Quantity::from(10))
833 .build();
834
835 let events = vec![
836 OrderEventAny::Initialized(init_event.clone()),
837 OrderEventAny::Initialized(init_event),
838 ];
839 let err =
840 OrderAny::from_events(events).expect_err("later invalid event should fail replay");
841
842 match &err {
843 OrderReplayError::ApplyFailed { source } => {
844 assert!(matches!(source, OrderError::AlreadyInitialized));
845 }
846 _ => panic!("expected ApplyFailed, was {err:?}"),
847 }
848 assert_eq!(err.to_string(), "Order was already initialized");
849 }
850
851 #[rstest]
852 fn test_passive_order_any_conversion() {
853 let limit_order = OrderTestBuilder::new(OrderType::Limit)
855 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
856 .quantity(Quantity::from(10))
857 .price(Price::new(100.0, 2))
858 .build();
859
860 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
862 let order_any: OrderAny = passive_order.into();
863
864 assert_eq!(order_any.order_type(), OrderType::Limit);
866 assert_eq!(order_any.quantity(), Quantity::from(10));
867 }
868
869 #[rstest]
870 fn test_stop_order_any_conversion() {
871 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
873 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
874 .quantity(Quantity::from(10))
875 .trigger_price(Price::new(100.0, 2))
876 .build();
877
878 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
880 let order_any: OrderAny = stop_order_any.into();
881
882 assert_eq!(order_any.order_type(), OrderType::StopMarket);
884 assert_eq!(order_any.quantity(), Quantity::from(10));
885 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
886 }
887
888 #[rstest]
889 fn test_limit_order_any_conversion() {
890 let limit_order = OrderTestBuilder::new(OrderType::Limit)
892 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
893 .quantity(Quantity::from(10))
894 .price(Price::new(100.0, 2))
895 .build();
896
897 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
899 let order_any: OrderAny = limit_order_any.into();
900
901 assert_eq!(order_any.order_type(), OrderType::Limit);
903 assert_eq!(order_any.quantity(), Quantity::from(10));
904 }
905
906 #[rstest]
907 fn test_limit_order_any_limit_price() {
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
918 let limit_px = limit_order_any.limit_px();
920 assert_eq!(limit_px, Price::new(100.0, 2));
921 }
922
923 #[rstest]
924 fn test_stop_order_any_stop_price() {
925 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
927 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
928 .quantity(Quantity::from(10))
929 .trigger_price(Price::new(100.0, 2))
930 .build();
931
932 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
934
935 let stop_px = stop_order_any.stop_px();
937 assert_eq!(stop_px, Some(Price::new(100.0, 2)));
938 }
939
940 #[rstest]
941 fn test_trailing_stop_market_order_conversion() {
942 let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
944 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
945 .quantity(Quantity::from(10))
946 .trigger_price(Price::new(100.0, 2))
947 .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::Price)
949 .build();
950
951 let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
953
954 let order_any: OrderAny = stop_order_any.into();
956
957 assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
959 assert_eq!(order_any.quantity(), Quantity::from(10));
960 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
961 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
962 assert_eq!(
963 order_any.trailing_offset_type(),
964 Some(TrailingOffsetType::Price)
965 );
966 }
967
968 #[rstest]
969 fn test_trailing_stop_limit_order_conversion() {
970 let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
972 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
973 .quantity(Quantity::from(10))
974 .price(Price::new(99.0, 2))
975 .trigger_price(Price::new(100.0, 2))
976 .limit_offset(Decimal::new(10, 1)) .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::Price)
979 .build();
980
981 let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
983
984 assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
986
987 let order_any: OrderAny = limit_order_any.into();
989
990 assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
992 assert_eq!(order_any.quantity(), Quantity::from(10));
993 assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
994 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
995 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
996 }
997
998 #[rstest]
999 fn test_passive_order_any_to_any() {
1000 let limit_order = OrderTestBuilder::new(OrderType::Limit)
1002 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1003 .quantity(Quantity::from(10))
1004 .price(Price::new(100.0, 2))
1005 .build();
1006
1007 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
1009
1010 let order_any = passive_order.to_any();
1012
1013 assert_eq!(order_any.order_type(), OrderType::Limit);
1015 assert_eq!(order_any.quantity(), Quantity::from(10));
1016 assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
1017 }
1018}