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