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 assert_last_event_views(order: &OrderAny) {
496 fn generic_last_event<T: Order + ?Sized>(order: &T) -> &OrderEventAny {
497 order.last_event()
498 }
499
500 let events = order.events();
501 let expected = *events.last().unwrap();
502 assert!(std::ptr::eq(order.last_event(), expected));
503 assert!(std::ptr::eq(generic_last_event(order), expected));
504
505 let concrete: &dyn Order = match order {
506 OrderAny::Limit(order) => order,
507 OrderAny::LimitIfTouched(order) => order,
508 OrderAny::Market(order) => order,
509 OrderAny::MarketIfTouched(order) => order,
510 OrderAny::MarketToLimit(order) => order,
511 OrderAny::StopLimit(order) => order,
512 OrderAny::StopMarket(order) => order,
513 OrderAny::TrailingStopLimit(order) => order,
514 OrderAny::TrailingStopMarket(order) => order,
515 };
516 assert!(std::ptr::eq(concrete.last_event(), expected));
517 }
518
519 #[rstest]
520 #[case::limit(OrderType::Limit)]
521 #[case::limit_if_touched(OrderType::LimitIfTouched)]
522 #[case::market(OrderType::Market)]
523 #[case::market_if_touched(OrderType::MarketIfTouched)]
524 #[case::market_to_limit(OrderType::MarketToLimit)]
525 #[case::stop_limit(OrderType::StopLimit)]
526 #[case::stop_market(OrderType::StopMarket)]
527 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
528 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
529 fn test_last_event_borrows_latest_native_event(#[case] order_type: OrderType) {
530 let mut order = order_for_list_id_test(order_type);
531 assert_last_event_views(&order);
532
533 let denied = crate::events::OrderDenied::new(
534 order.trader_id(),
535 order.strategy_id(),
536 order.instrument_id(),
537 order.client_order_id(),
538 "Risk denied".into(),
539 UUID4::new(),
540 UnixNanos::from(1),
541 UnixNanos::from(1),
542 );
543 order.apply(OrderEventAny::Denied(denied)).unwrap();
544 assert_last_event_views(&order);
545 }
546
547 #[rstest]
548 #[case::limit(OrderType::Limit)]
549 #[case::limit_if_touched(OrderType::LimitIfTouched)]
550 #[case::market(OrderType::Market)]
551 #[case::market_if_touched(OrderType::MarketIfTouched)]
552 #[case::market_to_limit(OrderType::MarketToLimit)]
553 #[case::stop_limit(OrderType::StopLimit)]
554 #[case::stop_market(OrderType::StopMarket)]
555 #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
556 #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
557 #[should_panic(expected = "Order invariant violated: no events")]
558 fn test_last_event_preserves_empty_history_panic(#[case] order_type: OrderType) {
559 let mut order = order_for_list_id_test(order_type);
560 let core: &mut OrderCore = match &mut order {
561 OrderAny::Limit(order) => order,
562 OrderAny::LimitIfTouched(order) => order,
563 OrderAny::Market(order) => order,
564 OrderAny::MarketIfTouched(order) => order,
565 OrderAny::MarketToLimit(order) => order,
566 OrderAny::StopLimit(order) => order,
567 OrderAny::StopMarket(order) => order,
568 OrderAny::TrailingStopLimit(order) => order,
569 OrderAny::TrailingStopMarket(order) => order,
570 };
571 core.events.clear();
572
573 let _ = Order::last_event(&order);
574 }
575
576 fn order_for_list_id_test(order_type: OrderType) -> OrderAny {
577 OrderTestBuilder::new(order_type)
578 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
579 .client_order_id(ClientOrderId::from("ORDER-LIST-001"))
580 .quantity(Quantity::from(10))
581 .price(Price::from("101.00"))
582 .trigger_price(Price::from("100.00"))
583 .trigger_type(TriggerType::LastPrice)
584 .limit_offset(dec!(1))
585 .trailing_offset(dec!(1))
586 .trailing_offset_type(TrailingOffsetType::Price)
587 .init_id(UUID4::from("16578139-a945-4b65-b46c-bc131a15d8e7"))
588 .ts_init(UnixNanos::from(123_456_789))
589 .build()
590 }
591
592 #[rstest]
593 fn test_order_any_from_events_empty_error() {
594 let events: Vec<OrderEventAny> = vec![];
595 let err = OrderAny::from_events(events).expect_err("empty events should fail");
596
597 assert!(matches!(err, OrderReplayError::EmptyInput));
598 assert_eq!(
599 err.to_string(),
600 "No order events provided to create OrderAny"
601 );
602 }
603
604 #[rstest]
605 fn test_order_any_from_events_invalid_init_returns_error() {
606 let init_event = OrderInitializedSpec::builder()
609 .order_type(OrderType::Limit)
610 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
611 .quantity(Quantity::from(10))
612 .build();
613
614 let events = vec![OrderEventAny::Initialized(init_event)];
615 let err =
616 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
617
618 match &err {
619 OrderReplayError::InvalidInitialization { source } => {
620 assert_eq!(
621 source.to_string(),
622 "`price` is required for `LimitOrder` initialization",
623 );
624 }
625 _ => panic!("expected InvalidInitialization, was {err:?}"),
626 }
627 assert_eq!(
628 err.to_string(),
629 "Invalid `OrderInitialized` event: `price` is required for `LimitOrder` initialization",
630 );
631 }
632
633 #[rstest]
634 #[case::buy(
635 OrderSide::Buy,
636 Price::from("100.00"),
637 Price::from("101.00"),
638 "BUY Limit-If-Touched"
639 )]
640 #[case::sell(
641 OrderSide::Sell,
642 Price::from("100.00"),
643 Price::from("99.00"),
644 "SELL Limit-If-Touched"
645 )]
646 fn test_order_any_from_events_invalid_predicate_returns_error(
647 #[case] side: OrderSide,
648 #[case] price: Price,
649 #[case] trigger_price: Price,
650 #[case] expected_msg: &str,
651 ) {
652 let init_event = OrderInitializedSpec::builder()
656 .order_type(OrderType::LimitIfTouched)
657 .order_side(side)
658 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
659 .quantity(Quantity::from(10))
660 .price(price)
661 .trigger_price(trigger_price)
662 .trigger_type(TriggerType::LastPrice)
663 .build();
664
665 let events = vec![OrderEventAny::Initialized(init_event)];
666 let err =
667 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
668
669 assert!(matches!(
670 err,
671 OrderReplayError::InvalidInitialization { .. }
672 ));
673 let msg = err.to_string();
674 assert!(
675 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
676 "unexpected error message: {msg}"
677 );
678 }
679
680 fn make_init_with_optional_fields(
681 order_type: OrderType,
682 price: Option<Price>,
683 trigger_price: Option<Price>,
684 trigger_type: Option<TriggerType>,
685 limit_offset: Option<Decimal>,
686 trailing_offset: Option<Decimal>,
687 trailing_offset_type: Option<TrailingOffsetType>,
688 ) -> OrderInitialized {
689 OrderInitializedSpec::builder()
690 .order_type(order_type)
691 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
692 .quantity(Quantity::from(10))
693 .maybe_price(price)
694 .maybe_trigger_price(trigger_price)
695 .maybe_trigger_type(trigger_type)
696 .maybe_limit_offset(limit_offset)
697 .maybe_trailing_offset(trailing_offset)
698 .maybe_trailing_offset_type(trailing_offset_type)
699 .build()
700 }
701
702 #[rstest]
703 #[case::lit_missing_price(
704 make_init_with_optional_fields(
705 OrderType::LimitIfTouched,
706 None,
707 Some(Price::from("100.00")),
708 Some(TriggerType::LastPrice),
709 None,
710 None,
711 None,
712 ),
713 "`price` is required for `LimitIfTouchedOrder`"
714 )]
715 #[case::lit_missing_trigger_price(
716 make_init_with_optional_fields(
717 OrderType::LimitIfTouched,
718 Some(Price::from("100.00")),
719 None,
720 Some(TriggerType::LastPrice),
721 None,
722 None,
723 None,
724 ),
725 "`trigger_price` is required for `LimitIfTouchedOrder`"
726 )]
727 #[case::lit_missing_trigger_type(
728 make_init_with_optional_fields(
729 OrderType::LimitIfTouched,
730 Some(Price::from("100.00")),
731 Some(Price::from("99.00")),
732 None,
733 None,
734 None,
735 None,
736 ),
737 "`trigger_type` is required for `LimitIfTouchedOrder`"
738 )]
739 #[case::stop_limit_missing_price(
740 make_init_with_optional_fields(
741 OrderType::StopLimit,
742 None,
743 Some(Price::from("100.00")),
744 Some(TriggerType::LastPrice),
745 None,
746 None,
747 None,
748 ),
749 "`price` is required for `StopLimitOrder`"
750 )]
751 #[case::stop_limit_missing_trigger_price(
752 make_init_with_optional_fields(
753 OrderType::StopLimit,
754 Some(Price::from("100.00")),
755 None,
756 Some(TriggerType::LastPrice),
757 None,
758 None,
759 None,
760 ),
761 "`trigger_price` is required for `StopLimitOrder`"
762 )]
763 #[case::stop_limit_missing_trigger_type(
764 make_init_with_optional_fields(
765 OrderType::StopLimit,
766 Some(Price::from("100.00")),
767 Some(Price::from("99.00")),
768 None,
769 None,
770 None,
771 None,
772 ),
773 "`trigger_type` is required for `StopLimitOrder`"
774 )]
775 #[case::stop_market_missing_trigger_price(
776 make_init_with_optional_fields(
777 OrderType::StopMarket,
778 None,
779 None,
780 Some(TriggerType::LastPrice),
781 None,
782 None,
783 None,
784 ),
785 "`trigger_price` is required for `StopMarketOrder`"
786 )]
787 #[case::stop_market_missing_trigger_type(
788 make_init_with_optional_fields(
789 OrderType::StopMarket,
790 None,
791 Some(Price::from("100.00")),
792 None,
793 None,
794 None,
795 None,
796 ),
797 "`trigger_type` is required for `StopMarketOrder`"
798 )]
799 #[case::mit_missing_trigger_price(
800 make_init_with_optional_fields(
801 OrderType::MarketIfTouched,
802 None,
803 None,
804 Some(TriggerType::LastPrice),
805 None,
806 None,
807 None,
808 ),
809 "`trigger_price` is required for `MarketIfTouchedOrder`"
810 )]
811 #[case::mit_missing_trigger_type(
812 make_init_with_optional_fields(
813 OrderType::MarketIfTouched,
814 None,
815 Some(Price::from("100.00")),
816 None,
817 None,
818 None,
819 None,
820 ),
821 "`trigger_type` is required for `MarketIfTouchedOrder`"
822 )]
823 #[case::tsl_missing_trigger_type(
824 make_init_with_optional_fields(
825 OrderType::TrailingStopLimit,
826 Some(Price::from("100.00")), Some(Price::from("99.00")), None,
827 Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
828 ),
829 "`trigger_type` is required for `TrailingStopLimitOrder`",
830 )]
831 #[case::tsl_missing_limit_offset(
832 make_init_with_optional_fields(
833 OrderType::TrailingStopLimit,
834 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
835 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
836 ),
837 "`limit_offset` is required for `TrailingStopLimitOrder`",
838 )]
839 #[case::tsl_missing_trailing_offset(
840 make_init_with_optional_fields(
841 OrderType::TrailingStopLimit,
842 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
843 Some(dec!(1)), None, Some(TrailingOffsetType::Price),
844 ),
845 "`trailing_offset` is required for `TrailingStopLimitOrder`",
846 )]
847 #[case::tsl_missing_trailing_offset_type(
848 make_init_with_optional_fields(
849 OrderType::TrailingStopLimit,
850 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
851 Some(dec!(1)), Some(dec!(1)), None,
852 ),
853 "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
854 )]
855 #[case::tsm_missing_trigger_type(
856 make_init_with_optional_fields(
857 OrderType::TrailingStopMarket,
858 None, Some(Price::from("100.00")), None,
859 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
860 ),
861 "`trigger_type` is required for `TrailingStopMarketOrder`",
862 )]
863 #[case::tsm_missing_trailing_offset(
864 make_init_with_optional_fields(
865 OrderType::TrailingStopMarket,
866 None,
867 Some(Price::from("100.00")),
868 Some(TriggerType::LastPrice),
869 None,
870 None,
871 Some(TrailingOffsetType::Price),
872 ),
873 "`trailing_offset` is required for `TrailingStopMarketOrder`"
874 )]
875 #[case::tsm_missing_trailing_offset_type(
876 make_init_with_optional_fields(
877 OrderType::TrailingStopMarket,
878 None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
879 None, Some(dec!(1)), None,
880 ),
881 "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
882 )]
883 fn test_order_any_from_events_missing_required_field_returns_error(
884 #[case] init: OrderInitialized,
885 #[case] expected_field_msg: &str,
886 ) {
887 let events = vec![OrderEventAny::Initialized(init)];
890 let err =
891 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
892
893 assert!(matches!(
894 err,
895 OrderReplayError::InvalidInitialization { .. }
896 ));
897 let msg = err.to_string();
898 assert!(
899 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
900 "unexpected error message: {msg}"
901 );
902 }
903
904 #[rstest]
905 fn test_order_any_from_events_wrong_first_event() {
906 let client_order_id = ClientOrderId::from("ORDER-001");
908 let strategy_id = StrategyId::from("STRATEGY-001");
909
910 let update_event = OrderUpdated {
911 client_order_id,
912 strategy_id,
913 quantity: Quantity::from(20),
914 ..Default::default()
915 };
916
917 let events = vec![OrderEventAny::Updated(update_event)];
919
920 let err = OrderAny::from_events(events).expect_err("wrong first event should fail replay");
922 assert!(matches!(err, OrderReplayError::WrongFirstEvent));
923 assert_eq!(err.to_string(), "First event must be `OrderInitialized`");
924 }
925
926 #[rstest]
927 fn test_order_any_from_events_apply_failure() {
928 let init_event = OrderInitializedSpec::builder()
929 .order_type(OrderType::Market)
930 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
931 .quantity(Quantity::from(10))
932 .build();
933
934 let events = vec![
935 OrderEventAny::Initialized(init_event.clone()),
936 OrderEventAny::Initialized(init_event),
937 ];
938 let err =
939 OrderAny::from_events(events).expect_err("later invalid event should fail replay");
940
941 match &err {
942 OrderReplayError::ApplyFailed { source } => {
943 assert!(matches!(source, OrderError::AlreadyInitialized));
944 }
945 _ => panic!("expected ApplyFailed, was {err:?}"),
946 }
947 assert_eq!(err.to_string(), "Order was already initialized");
948 }
949
950 #[rstest]
951 fn test_passive_order_any_conversion() {
952 let limit_order = OrderTestBuilder::new(OrderType::Limit)
954 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
955 .quantity(Quantity::from(10))
956 .price(Price::new(100.0, 2))
957 .build();
958
959 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
961 let order_any: OrderAny = passive_order.into();
962
963 assert_eq!(order_any.order_type(), OrderType::Limit);
965 assert_eq!(order_any.quantity(), Quantity::from(10));
966 }
967
968 #[rstest]
969 fn test_stop_order_any_conversion() {
970 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
972 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
973 .quantity(Quantity::from(10))
974 .trigger_price(Price::new(100.0, 2))
975 .build();
976
977 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
979 let order_any: OrderAny = stop_order_any.into();
980
981 assert_eq!(order_any.order_type(), OrderType::StopMarket);
983 assert_eq!(order_any.quantity(), Quantity::from(10));
984 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
985 }
986
987 #[rstest]
988 fn test_limit_order_any_conversion() {
989 let limit_order = OrderTestBuilder::new(OrderType::Limit)
991 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
992 .quantity(Quantity::from(10))
993 .price(Price::new(100.0, 2))
994 .build();
995
996 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
998 let order_any: OrderAny = limit_order_any.into();
999
1000 assert_eq!(order_any.order_type(), OrderType::Limit);
1002 assert_eq!(order_any.quantity(), Quantity::from(10));
1003 }
1004
1005 #[rstest]
1006 fn test_limit_order_any_limit_price() {
1007 let limit_order = OrderTestBuilder::new(OrderType::Limit)
1009 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1010 .quantity(Quantity::from(10))
1011 .price(Price::new(100.0, 2))
1012 .build();
1013
1014 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
1016
1017 let limit_px = limit_order_any.limit_px();
1019 assert_eq!(limit_px, Price::new(100.0, 2));
1020 }
1021
1022 #[rstest]
1023 fn test_stop_order_any_stop_price() {
1024 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
1026 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1027 .quantity(Quantity::from(10))
1028 .trigger_price(Price::new(100.0, 2))
1029 .build();
1030
1031 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
1033
1034 let stop_px = stop_order_any.stop_px();
1036 assert_eq!(stop_px, Some(Price::new(100.0, 2)));
1037 }
1038
1039 #[rstest]
1040 fn test_trailing_stop_market_order_conversion() {
1041 let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
1043 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1044 .quantity(Quantity::from(10))
1045 .trigger_price(Price::new(100.0, 2))
1046 .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::Price)
1048 .build();
1049
1050 let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
1052
1053 let order_any: OrderAny = stop_order_any.into();
1055
1056 assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
1058 assert_eq!(order_any.quantity(), Quantity::from(10));
1059 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
1060 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
1061 assert_eq!(
1062 order_any.trailing_offset_type(),
1063 Some(TrailingOffsetType::Price)
1064 );
1065 }
1066
1067 #[rstest]
1068 fn test_trailing_stop_limit_order_conversion() {
1069 let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
1071 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1072 .quantity(Quantity::from(10))
1073 .price(Price::new(99.0, 2))
1074 .trigger_price(Price::new(100.0, 2))
1075 .limit_offset(Decimal::new(10, 1)) .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::Price)
1078 .build();
1079
1080 let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
1082
1083 assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
1085
1086 let order_any: OrderAny = limit_order_any.into();
1088
1089 assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
1091 assert_eq!(order_any.quantity(), Quantity::from(10));
1092 assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
1093 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
1094 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
1095 }
1096
1097 #[rstest]
1098 fn test_passive_order_any_to_any() {
1099 let limit_order = OrderTestBuilder::new(OrderType::Limit)
1101 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1102 .quantity(Quantity::from(10))
1103 .price(Price::new(100.0, 2))
1104 .build();
1105
1106 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
1108
1109 let order_any = passive_order.to_any();
1111
1112 assert_eq!(order_any.order_type(), OrderType::Limit);
1114 assert_eq!(order_any.quantity(), Quantity::from(10));
1115 assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
1116 }
1117}