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) => {
307 order.price.expect("TrailingStopLimit order price not set")
308 }
309 Self::MarketOrderWithProtection(order) => {
310 order.protection_price.expect("No price for order")
311 }
312 }
313 }
314}
315
316impl PartialEq for LimitOrderAny {
317 fn eq(&self, rhs: &Self) -> bool {
318 match self {
319 Self::Limit(order) => order.client_order_id == rhs.client_order_id(),
320 Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(),
321 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
322 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
323 Self::MarketOrderWithProtection(order) => {
324 order.client_order_id == rhs.client_order_id()
325 }
326 }
327 }
328}
329
330#[derive(Clone, Debug)]
331#[enum_dispatch(Order)]
332pub enum StopOrderAny {
333 LimitIfTouched(LimitIfTouchedOrder),
334 MarketIfTouched(MarketIfTouchedOrder),
335 StopLimit(StopLimitOrder),
336 StopMarket(StopMarketOrder),
337 TrailingStopLimit(TrailingStopLimitOrder),
338 TrailingStopMarket(TrailingStopMarketOrder),
339}
340
341impl StopOrderAny {
342 #[must_use]
343 pub fn stop_px(&self) -> Option<Price> {
344 match self {
345 Self::LimitIfTouched(o) => Some(o.trigger_price),
346 Self::MarketIfTouched(o) => Some(o.trigger_price),
347 Self::StopLimit(o) => Some(o.trigger_price),
348 Self::StopMarket(o) => Some(o.trigger_price),
349 Self::TrailingStopLimit(o) => o.activation_price.or(o.trigger_price),
350 Self::TrailingStopMarket(o) => o.activation_price.or(o.trigger_price),
351 }
352 }
353}
354
355impl PartialEq for StopOrderAny {
357 fn eq(&self, rhs: &Self) -> bool {
358 match self {
359 Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(),
360 Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
361 Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(),
362 Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(),
363 Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
364 Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(),
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use rstest::rstest;
372 use rust_decimal::Decimal;
373 use rust_decimal_macros::dec;
374
375 use super::*;
376 use crate::{
377 enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
378 events::{
379 OrderEventAny, OrderInitialized, OrderUpdated, order::spec::OrderInitializedSpec,
380 },
381 identifiers::{ClientOrderId, InstrumentId, StrategyId},
382 orders::{OrderError, builder::OrderTestBuilder},
383 types::{Price, Quantity},
384 };
385
386 #[rstest]
387 fn test_order_any_equality() {
388 let client_order_id = ClientOrderId::from("ORDER-001");
390
391 let market_order = OrderTestBuilder::new(OrderType::Market)
392 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
393 .quantity(Quantity::from(10))
394 .client_order_id(client_order_id)
395 .build();
396
397 let limit_order = OrderTestBuilder::new(OrderType::Limit)
398 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
399 .quantity(Quantity::from(10))
400 .price(Price::new(100.0, 2))
401 .client_order_id(client_order_id)
402 .build();
403
404 assert_eq!(market_order, limit_order);
406 }
407
408 #[rstest]
409 fn test_order_any_conversion_from_events() {
410 let init_event = OrderInitializedSpec::builder()
412 .order_type(OrderType::Market)
413 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
414 .quantity(Quantity::from(10))
415 .build();
416
417 let events = vec![OrderEventAny::Initialized(init_event.clone())];
419
420 let order = OrderAny::from_events(events).unwrap();
422
423 assert_eq!(order.order_type(), OrderType::Market);
425 assert_eq!(order.instrument_id(), init_event.instrument_id);
426 assert_eq!(order.quantity(), init_event.quantity);
427 }
428
429 #[rstest]
430 fn test_order_any_from_events_empty_error() {
431 let events: Vec<OrderEventAny> = vec![];
432 let err = OrderAny::from_events(events).expect_err("empty events should fail");
433
434 assert!(matches!(err, OrderReplayError::EmptyInput));
435 assert_eq!(
436 err.to_string(),
437 "No order events provided to create OrderAny"
438 );
439 }
440
441 #[rstest]
442 fn test_order_any_from_events_invalid_init_returns_error() {
443 let init_event = OrderInitializedSpec::builder()
446 .order_type(OrderType::Limit)
447 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
448 .quantity(Quantity::from(10))
449 .build();
450
451 let events = vec![OrderEventAny::Initialized(init_event)];
452 let err =
453 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
454
455 match &err {
456 OrderReplayError::InvalidInitialization { source } => {
457 assert_eq!(
458 source.to_string(),
459 "`price` is required for `LimitOrder` initialization",
460 );
461 }
462 _ => panic!("expected InvalidInitialization, was {err:?}"),
463 }
464 assert_eq!(
465 err.to_string(),
466 "Invalid `OrderInitialized` event: `price` is required for `LimitOrder` initialization",
467 );
468 }
469
470 #[rstest]
471 #[case::buy(
472 OrderSide::Buy,
473 Price::from("100.00"),
474 Price::from("101.00"),
475 "BUY Limit-If-Touched"
476 )]
477 #[case::sell(
478 OrderSide::Sell,
479 Price::from("100.00"),
480 Price::from("99.00"),
481 "SELL Limit-If-Touched"
482 )]
483 fn test_order_any_from_events_invalid_predicate_returns_error(
484 #[case] side: OrderSide,
485 #[case] price: Price,
486 #[case] trigger_price: Price,
487 #[case] expected_msg: &str,
488 ) {
489 let init_event = OrderInitializedSpec::builder()
493 .order_type(OrderType::LimitIfTouched)
494 .order_side(side)
495 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
496 .quantity(Quantity::from(10))
497 .price(price)
498 .trigger_price(trigger_price)
499 .trigger_type(TriggerType::LastPrice)
500 .build();
501
502 let events = vec![OrderEventAny::Initialized(init_event)];
503 let err =
504 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
505
506 assert!(matches!(
507 err,
508 OrderReplayError::InvalidInitialization { .. }
509 ));
510 let msg = err.to_string();
511 assert!(
512 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
513 "unexpected error message: {msg}"
514 );
515 }
516
517 fn make_init_with_optional_fields(
518 order_type: OrderType,
519 price: Option<Price>,
520 trigger_price: Option<Price>,
521 trigger_type: Option<TriggerType>,
522 limit_offset: Option<Decimal>,
523 trailing_offset: Option<Decimal>,
524 trailing_offset_type: Option<TrailingOffsetType>,
525 ) -> OrderInitialized {
526 OrderInitializedSpec::builder()
527 .order_type(order_type)
528 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
529 .quantity(Quantity::from(10))
530 .maybe_price(price)
531 .maybe_trigger_price(trigger_price)
532 .maybe_trigger_type(trigger_type)
533 .maybe_limit_offset(limit_offset)
534 .maybe_trailing_offset(trailing_offset)
535 .maybe_trailing_offset_type(trailing_offset_type)
536 .build()
537 }
538
539 #[rstest]
540 #[case::lit_missing_price(
541 make_init_with_optional_fields(
542 OrderType::LimitIfTouched,
543 None,
544 Some(Price::from("100.00")),
545 Some(TriggerType::LastPrice),
546 None,
547 None,
548 None,
549 ),
550 "`price` is required for `LimitIfTouchedOrder`"
551 )]
552 #[case::lit_missing_trigger_price(
553 make_init_with_optional_fields(
554 OrderType::LimitIfTouched,
555 Some(Price::from("100.00")),
556 None,
557 Some(TriggerType::LastPrice),
558 None,
559 None,
560 None,
561 ),
562 "`trigger_price` is required for `LimitIfTouchedOrder`"
563 )]
564 #[case::lit_missing_trigger_type(
565 make_init_with_optional_fields(
566 OrderType::LimitIfTouched,
567 Some(Price::from("100.00")),
568 Some(Price::from("99.00")),
569 None,
570 None,
571 None,
572 None,
573 ),
574 "`trigger_type` is required for `LimitIfTouchedOrder`"
575 )]
576 #[case::stop_limit_missing_price(
577 make_init_with_optional_fields(
578 OrderType::StopLimit,
579 None,
580 Some(Price::from("100.00")),
581 Some(TriggerType::LastPrice),
582 None,
583 None,
584 None,
585 ),
586 "`price` is required for `StopLimitOrder`"
587 )]
588 #[case::stop_limit_missing_trigger_price(
589 make_init_with_optional_fields(
590 OrderType::StopLimit,
591 Some(Price::from("100.00")),
592 None,
593 Some(TriggerType::LastPrice),
594 None,
595 None,
596 None,
597 ),
598 "`trigger_price` is required for `StopLimitOrder`"
599 )]
600 #[case::stop_limit_missing_trigger_type(
601 make_init_with_optional_fields(
602 OrderType::StopLimit,
603 Some(Price::from("100.00")),
604 Some(Price::from("99.00")),
605 None,
606 None,
607 None,
608 None,
609 ),
610 "`trigger_type` is required for `StopLimitOrder`"
611 )]
612 #[case::stop_market_missing_trigger_price(
613 make_init_with_optional_fields(
614 OrderType::StopMarket,
615 None,
616 None,
617 Some(TriggerType::LastPrice),
618 None,
619 None,
620 None,
621 ),
622 "`trigger_price` is required for `StopMarketOrder`"
623 )]
624 #[case::stop_market_missing_trigger_type(
625 make_init_with_optional_fields(
626 OrderType::StopMarket,
627 None,
628 Some(Price::from("100.00")),
629 None,
630 None,
631 None,
632 None,
633 ),
634 "`trigger_type` is required for `StopMarketOrder`"
635 )]
636 #[case::mit_missing_trigger_price(
637 make_init_with_optional_fields(
638 OrderType::MarketIfTouched,
639 None,
640 None,
641 Some(TriggerType::LastPrice),
642 None,
643 None,
644 None,
645 ),
646 "`trigger_price` is required for `MarketIfTouchedOrder`"
647 )]
648 #[case::mit_missing_trigger_type(
649 make_init_with_optional_fields(
650 OrderType::MarketIfTouched,
651 None,
652 Some(Price::from("100.00")),
653 None,
654 None,
655 None,
656 None,
657 ),
658 "`trigger_type` is required for `MarketIfTouchedOrder`"
659 )]
660 #[case::tsl_missing_trigger_type(
661 make_init_with_optional_fields(
662 OrderType::TrailingStopLimit,
663 Some(Price::from("100.00")), Some(Price::from("99.00")), None,
664 Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
665 ),
666 "`trigger_type` is required for `TrailingStopLimitOrder`",
667 )]
668 #[case::tsl_missing_limit_offset(
669 make_init_with_optional_fields(
670 OrderType::TrailingStopLimit,
671 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
672 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
673 ),
674 "`limit_offset` is required for `TrailingStopLimitOrder`",
675 )]
676 #[case::tsl_missing_trailing_offset(
677 make_init_with_optional_fields(
678 OrderType::TrailingStopLimit,
679 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
680 Some(dec!(1)), None, Some(TrailingOffsetType::Price),
681 ),
682 "`trailing_offset` is required for `TrailingStopLimitOrder`",
683 )]
684 #[case::tsl_missing_trailing_offset_type(
685 make_init_with_optional_fields(
686 OrderType::TrailingStopLimit,
687 Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
688 Some(dec!(1)), Some(dec!(1)), None,
689 ),
690 "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
691 )]
692 #[case::tsm_missing_trigger_type(
693 make_init_with_optional_fields(
694 OrderType::TrailingStopMarket,
695 None, Some(Price::from("100.00")), None,
696 None, Some(dec!(1)), Some(TrailingOffsetType::Price),
697 ),
698 "`trigger_type` is required for `TrailingStopMarketOrder`",
699 )]
700 #[case::tsm_missing_trailing_offset(
701 make_init_with_optional_fields(
702 OrderType::TrailingStopMarket,
703 None,
704 Some(Price::from("100.00")),
705 Some(TriggerType::LastPrice),
706 None,
707 None,
708 Some(TrailingOffsetType::Price),
709 ),
710 "`trailing_offset` is required for `TrailingStopMarketOrder`"
711 )]
712 #[case::tsm_missing_trailing_offset_type(
713 make_init_with_optional_fields(
714 OrderType::TrailingStopMarket,
715 None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
716 None, Some(dec!(1)), None,
717 ),
718 "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
719 )]
720 fn test_order_any_from_events_missing_required_field_returns_error(
721 #[case] init: OrderInitialized,
722 #[case] expected_field_msg: &str,
723 ) {
724 let events = vec![OrderEventAny::Initialized(init)];
727 let err =
728 OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
729
730 assert!(matches!(
731 err,
732 OrderReplayError::InvalidInitialization { .. }
733 ));
734 let msg = err.to_string();
735 assert!(
736 msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
737 "unexpected error message: {msg}"
738 );
739 }
740
741 #[rstest]
742 fn test_order_any_from_events_wrong_first_event() {
743 let client_order_id = ClientOrderId::from("ORDER-001");
745 let strategy_id = StrategyId::from("STRATEGY-001");
746
747 let update_event = OrderUpdated {
748 client_order_id,
749 strategy_id,
750 quantity: Quantity::from(20),
751 ..Default::default()
752 };
753
754 let events = vec![OrderEventAny::Updated(update_event)];
756
757 let err = OrderAny::from_events(events).expect_err("wrong first event should fail replay");
759 assert!(matches!(err, OrderReplayError::WrongFirstEvent));
760 assert_eq!(err.to_string(), "First event must be `OrderInitialized`");
761 }
762
763 #[rstest]
764 fn test_order_any_from_events_apply_failure() {
765 let init_event = OrderInitializedSpec::builder()
766 .order_type(OrderType::Market)
767 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
768 .quantity(Quantity::from(10))
769 .build();
770
771 let events = vec![
772 OrderEventAny::Initialized(init_event.clone()),
773 OrderEventAny::Initialized(init_event),
774 ];
775 let err =
776 OrderAny::from_events(events).expect_err("later invalid event should fail replay");
777
778 match &err {
779 OrderReplayError::ApplyFailed { source } => {
780 assert!(matches!(source, OrderError::AlreadyInitialized));
781 }
782 _ => panic!("expected ApplyFailed, was {err:?}"),
783 }
784 assert_eq!(err.to_string(), "Order was already initialized");
785 }
786
787 #[rstest]
788 fn test_passive_order_any_conversion() {
789 let limit_order = OrderTestBuilder::new(OrderType::Limit)
791 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
792 .quantity(Quantity::from(10))
793 .price(Price::new(100.0, 2))
794 .build();
795
796 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
798 let order_any: OrderAny = passive_order.into();
799
800 assert_eq!(order_any.order_type(), OrderType::Limit);
802 assert_eq!(order_any.quantity(), Quantity::from(10));
803 }
804
805 #[rstest]
806 fn test_stop_order_any_conversion() {
807 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
809 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
810 .quantity(Quantity::from(10))
811 .trigger_price(Price::new(100.0, 2))
812 .build();
813
814 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
816 let order_any: OrderAny = stop_order_any.into();
817
818 assert_eq!(order_any.order_type(), OrderType::StopMarket);
820 assert_eq!(order_any.quantity(), Quantity::from(10));
821 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
822 }
823
824 #[rstest]
825 fn test_limit_order_any_conversion() {
826 let limit_order = OrderTestBuilder::new(OrderType::Limit)
828 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
829 .quantity(Quantity::from(10))
830 .price(Price::new(100.0, 2))
831 .build();
832
833 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
835 let order_any: OrderAny = limit_order_any.into();
836
837 assert_eq!(order_any.order_type(), OrderType::Limit);
839 assert_eq!(order_any.quantity(), Quantity::from(10));
840 }
841
842 #[rstest]
843 fn test_limit_order_any_limit_price() {
844 let limit_order = OrderTestBuilder::new(OrderType::Limit)
846 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
847 .quantity(Quantity::from(10))
848 .price(Price::new(100.0, 2))
849 .build();
850
851 let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
853
854 let limit_px = limit_order_any.limit_px();
856 assert_eq!(limit_px, Price::new(100.0, 2));
857 }
858
859 #[rstest]
860 fn test_stop_order_any_stop_price() {
861 let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
863 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
864 .quantity(Quantity::from(10))
865 .trigger_price(Price::new(100.0, 2))
866 .build();
867
868 let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
870
871 let stop_px = stop_order_any.stop_px();
873 assert_eq!(stop_px, Some(Price::new(100.0, 2)));
874 }
875
876 #[rstest]
877 fn test_trailing_stop_market_order_conversion() {
878 let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
880 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
881 .quantity(Quantity::from(10))
882 .trigger_price(Price::new(100.0, 2))
883 .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
885 .build();
886
887 let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
889
890 let order_any: OrderAny = stop_order_any.into();
892
893 assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
895 assert_eq!(order_any.quantity(), Quantity::from(10));
896 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
897 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
898 assert_eq!(
899 order_any.trailing_offset_type(),
900 Some(TrailingOffsetType::NoTrailingOffset)
901 );
902 }
903
904 #[rstest]
905 fn test_trailing_stop_limit_order_conversion() {
906 let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
908 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
909 .quantity(Quantity::from(10))
910 .price(Price::new(99.0, 2))
911 .trigger_price(Price::new(100.0, 2))
912 .limit_offset(Decimal::new(10, 1)) .trailing_offset(Decimal::new(5, 1)) .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
915 .build();
916
917 let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
919
920 assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
922
923 let order_any: OrderAny = limit_order_any.into();
925
926 assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
928 assert_eq!(order_any.quantity(), Quantity::from(10));
929 assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
930 assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
931 assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
932 }
933
934 #[rstest]
935 fn test_passive_order_any_to_any() {
936 let limit_order = OrderTestBuilder::new(OrderType::Limit)
938 .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
939 .quantity(Quantity::from(10))
940 .price(Price::new(100.0, 2))
941 .build();
942
943 let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
945
946 let order_any = passive_order.to_any();
948
949 assert_eq!(order_any.order_type(), OrderType::Limit);
951 assert_eq!(order_any.quantity(), Quantity::from(10));
952 assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
953 }
954}