Skip to main content

nautilus_model/orders/
any.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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/// Error returned when [`OrderAny::from_events`] cannot replay order events.
31#[derive(Debug, Error)]
32pub enum OrderReplayError {
33    /// No events were supplied.
34    #[error("No order events provided to create OrderAny")]
35    EmptyInput,
36    /// The first event was not an initialization event.
37    #[error("First event must be `OrderInitialized`")]
38    WrongFirstEvent,
39    /// The initialization event could not be converted into an order.
40    #[error("Invalid `OrderInitialized` event: {source}")]
41    InvalidInitialization {
42        /// The source order conversion error.
43        #[source]
44        source: OrderError,
45    },
46    /// A later event could not be applied to the initialized order.
47    #[error("{source}")]
48    ApplyFailed {
49        /// The source event application error.
50        #[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    /// Creates a new [`OrderAny`] instance from the given `events`.
71    ///
72    /// # Errors
73    ///
74    /// Returns an error if:
75    /// - The `events` is empty.
76    /// - The first event is not `OrderInitialized`.
77    /// - The initialization event violates an order invariant
78    ///   (e.g. missing required price/trigger fields, invalid quantity, invalid TIF/expire combo).
79    /// - Any subsequent event has an invalid state transition when applied to the order.
80    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    /// Returns a reference to the [`crate::events::OrderInitialized`] event.
102    ///
103    /// This is always the first event in the order's event list (invariant).
104    ///
105    /// # Panics
106    ///
107    /// Panics if the first event is not `OrderInitialized` (violates invariant).
108    #[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    /// Assigns the order to the list identified by `id`.
121    ///
122    /// # Panics
123    ///
124    /// Panics if the order has no events or its first event is not `OrderInitialized`.
125    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
157// TODO: fix equality
158impl 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
286// TODO: Derive equality
287impl 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    /// Returns the limit price for this order.
308    ///
309    /// # Panics
310    ///
311    /// Panics if the `MarketToLimit` order price is not set.
312    #[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
367// TODO: Derive equality
368impl 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        // Create two orders with different types but same client_order_id
420        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        // They should be equal because they have the same client_order_id
436        assert_eq!(market_order, limit_order);
437    }
438
439    #[rstest]
440    fn test_order_any_conversion_from_events() {
441        // Create an OrderInitialized event
442        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        // Create a vector of events
449        let events = vec![OrderEventAny::Initialized(init_event.clone())];
450
451        // Create OrderAny from events
452        let order = OrderAny::from_events(events).unwrap();
453
454        // Verify the order was created properly
455        assert_eq!(order.order_type(), OrderType::Market);
456        assert_eq!(order.instrument_id(), init_event.instrument_id);
457        assert_eq!(order.quantity(), init_event.quantity);
458    }
459
460    #[rstest]
461    #[case::limit(OrderType::Limit)]
462    #[case::limit_if_touched(OrderType::LimitIfTouched)]
463    #[case::market(OrderType::Market)]
464    #[case::market_if_touched(OrderType::MarketIfTouched)]
465    #[case::market_to_limit(OrderType::MarketToLimit)]
466    #[case::stop_limit(OrderType::StopLimit)]
467    #[case::stop_market(OrderType::StopMarket)]
468    #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
469    #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
470    fn test_set_order_list_id_updates_init_event_and_replay(#[case] order_type: OrderType) {
471        let mut order = order_for_list_id_test(order_type);
472        let init_before = order.init_event().clone();
473        let event_count = order.event_count();
474        let order_list_id = OrderListId::from("OL-SET-LIST-001");
475
476        order.set_order_list_id(order_list_id);
477
478        let mut expected_init = init_before;
479        expected_init.order_list_id = Some(order_list_id);
480        assert_eq!(order.order_list_id(), Some(order_list_id));
481        assert_eq!(order.init_event(), &expected_init);
482        assert_eq!(order.event_count(), event_count);
483
484        let persisted_events: Vec<_> = order.events().into_iter().cloned().collect();
485        assert_eq!(
486            persisted_events,
487            vec![OrderEventAny::Initialized(expected_init.clone())]
488        );
489
490        let replayed = OrderAny::from_events(persisted_events).unwrap();
491        assert_eq!(replayed.order_list_id(), Some(order_list_id));
492        assert_eq!(replayed.init_event(), &expected_init);
493    }
494
495    fn order_for_list_id_test(order_type: OrderType) -> OrderAny {
496        OrderTestBuilder::new(order_type)
497            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
498            .client_order_id(ClientOrderId::from("ORDER-LIST-001"))
499            .quantity(Quantity::from(10))
500            .price(Price::from("101.00"))
501            .trigger_price(Price::from("100.00"))
502            .trigger_type(TriggerType::LastPrice)
503            .limit_offset(dec!(1))
504            .trailing_offset(dec!(1))
505            .trailing_offset_type(TrailingOffsetType::Price)
506            .init_id(UUID4::from("16578139-a945-4b65-b46c-bc131a15d8e7"))
507            .ts_init(UnixNanos::from(123_456_789))
508            .build()
509    }
510
511    #[rstest]
512    fn test_order_any_from_events_empty_error() {
513        let events: Vec<OrderEventAny> = vec![];
514        let err = OrderAny::from_events(events).expect_err("empty events should fail");
515
516        assert!(matches!(err, OrderReplayError::EmptyInput));
517        assert_eq!(
518            err.to_string(),
519            "No order events provided to create OrderAny"
520        );
521    }
522
523    #[rstest]
524    fn test_order_any_from_events_invalid_init_returns_error() {
525        // Limit order with `price = None` violates `LimitOrder` invariants. Previously this
526        // panicked inside `From<OrderInitialized>`; `from_events` must now surface it as `Err`.
527        let init_event = OrderInitializedSpec::builder()
528            .order_type(OrderType::Limit)
529            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
530            .quantity(Quantity::from(10))
531            .build();
532
533        let events = vec![OrderEventAny::Initialized(init_event)];
534        let err =
535            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
536
537        match &err {
538            OrderReplayError::InvalidInitialization { source } => {
539                assert_eq!(
540                    source.to_string(),
541                    "`price` is required for `LimitOrder` initialization",
542                );
543            }
544            _ => panic!("expected InvalidInitialization, was {err:?}"),
545        }
546        assert_eq!(
547            err.to_string(),
548            "Invalid `OrderInitialized` event: `price` is required for `LimitOrder` initialization",
549        );
550    }
551
552    #[rstest]
553    #[case::buy(
554        OrderSide::Buy,
555        Price::from("100.00"),
556        Price::from("101.00"),
557        "BUY Limit-If-Touched"
558    )]
559    #[case::sell(
560        OrderSide::Sell,
561        Price::from("100.00"),
562        Price::from("99.00"),
563        "SELL Limit-If-Touched"
564    )]
565    fn test_order_any_from_events_invalid_predicate_returns_error(
566        #[case] side: OrderSide,
567        #[case] price: Price,
568        #[case] trigger_price: Price,
569        #[case] expected_msg: &str,
570    ) {
571        // LimitIfTouched enforces `trigger_price <= price` for BUY and `trigger_price >= price`
572        // for SELL inside `new_checked`. Reconciliation must see violations as `Err` rather
573        // than panicking, on either side.
574        let init_event = OrderInitializedSpec::builder()
575            .order_type(OrderType::LimitIfTouched)
576            .order_side(side)
577            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
578            .quantity(Quantity::from(10))
579            .price(price)
580            .trigger_price(trigger_price)
581            .trigger_type(TriggerType::LastPrice)
582            .build();
583
584        let events = vec![OrderEventAny::Initialized(init_event)];
585        let err =
586            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
587
588        assert!(matches!(
589            err,
590            OrderReplayError::InvalidInitialization { .. }
591        ));
592        let msg = err.to_string();
593        assert!(
594            msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
595            "unexpected error message: {msg}"
596        );
597    }
598
599    fn make_init_with_optional_fields(
600        order_type: OrderType,
601        price: Option<Price>,
602        trigger_price: Option<Price>,
603        trigger_type: Option<TriggerType>,
604        limit_offset: Option<Decimal>,
605        trailing_offset: Option<Decimal>,
606        trailing_offset_type: Option<TrailingOffsetType>,
607    ) -> OrderInitialized {
608        OrderInitializedSpec::builder()
609            .order_type(order_type)
610            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
611            .quantity(Quantity::from(10))
612            .maybe_price(price)
613            .maybe_trigger_price(trigger_price)
614            .maybe_trigger_type(trigger_type)
615            .maybe_limit_offset(limit_offset)
616            .maybe_trailing_offset(trailing_offset)
617            .maybe_trailing_offset_type(trailing_offset_type)
618            .build()
619    }
620
621    #[rstest]
622    #[case::lit_missing_price(
623        make_init_with_optional_fields(
624            OrderType::LimitIfTouched,
625            None,
626            Some(Price::from("100.00")),
627            Some(TriggerType::LastPrice),
628            None,
629            None,
630            None,
631        ),
632        "`price` is required for `LimitIfTouchedOrder`"
633    )]
634    #[case::lit_missing_trigger_price(
635        make_init_with_optional_fields(
636            OrderType::LimitIfTouched,
637            Some(Price::from("100.00")),
638            None,
639            Some(TriggerType::LastPrice),
640            None,
641            None,
642            None,
643        ),
644        "`trigger_price` is required for `LimitIfTouchedOrder`"
645    )]
646    #[case::lit_missing_trigger_type(
647        make_init_with_optional_fields(
648            OrderType::LimitIfTouched,
649            Some(Price::from("100.00")),
650            Some(Price::from("99.00")),
651            None,
652            None,
653            None,
654            None,
655        ),
656        "`trigger_type` is required for `LimitIfTouchedOrder`"
657    )]
658    #[case::stop_limit_missing_price(
659        make_init_with_optional_fields(
660            OrderType::StopLimit,
661            None,
662            Some(Price::from("100.00")),
663            Some(TriggerType::LastPrice),
664            None,
665            None,
666            None,
667        ),
668        "`price` is required for `StopLimitOrder`"
669    )]
670    #[case::stop_limit_missing_trigger_price(
671        make_init_with_optional_fields(
672            OrderType::StopLimit,
673            Some(Price::from("100.00")),
674            None,
675            Some(TriggerType::LastPrice),
676            None,
677            None,
678            None,
679        ),
680        "`trigger_price` is required for `StopLimitOrder`"
681    )]
682    #[case::stop_limit_missing_trigger_type(
683        make_init_with_optional_fields(
684            OrderType::StopLimit,
685            Some(Price::from("100.00")),
686            Some(Price::from("99.00")),
687            None,
688            None,
689            None,
690            None,
691        ),
692        "`trigger_type` is required for `StopLimitOrder`"
693    )]
694    #[case::stop_market_missing_trigger_price(
695        make_init_with_optional_fields(
696            OrderType::StopMarket,
697            None,
698            None,
699            Some(TriggerType::LastPrice),
700            None,
701            None,
702            None,
703        ),
704        "`trigger_price` is required for `StopMarketOrder`"
705    )]
706    #[case::stop_market_missing_trigger_type(
707        make_init_with_optional_fields(
708            OrderType::StopMarket,
709            None,
710            Some(Price::from("100.00")),
711            None,
712            None,
713            None,
714            None,
715        ),
716        "`trigger_type` is required for `StopMarketOrder`"
717    )]
718    #[case::mit_missing_trigger_price(
719        make_init_with_optional_fields(
720            OrderType::MarketIfTouched,
721            None,
722            None,
723            Some(TriggerType::LastPrice),
724            None,
725            None,
726            None,
727        ),
728        "`trigger_price` is required for `MarketIfTouchedOrder`"
729    )]
730    #[case::mit_missing_trigger_type(
731        make_init_with_optional_fields(
732            OrderType::MarketIfTouched,
733            None,
734            Some(Price::from("100.00")),
735            None,
736            None,
737            None,
738            None,
739        ),
740        "`trigger_type` is required for `MarketIfTouchedOrder`"
741    )]
742    #[case::tsl_missing_trigger_type(
743        make_init_with_optional_fields(
744            OrderType::TrailingStopLimit,
745            Some(Price::from("100.00")), Some(Price::from("99.00")), None,
746            Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
747        ),
748        "`trigger_type` is required for `TrailingStopLimitOrder`",
749    )]
750    #[case::tsl_missing_limit_offset(
751        make_init_with_optional_fields(
752            OrderType::TrailingStopLimit,
753            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
754            None, Some(dec!(1)), Some(TrailingOffsetType::Price),
755        ),
756        "`limit_offset` is required for `TrailingStopLimitOrder`",
757    )]
758    #[case::tsl_missing_trailing_offset(
759        make_init_with_optional_fields(
760            OrderType::TrailingStopLimit,
761            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
762            Some(dec!(1)), None, Some(TrailingOffsetType::Price),
763        ),
764        "`trailing_offset` is required for `TrailingStopLimitOrder`",
765    )]
766    #[case::tsl_missing_trailing_offset_type(
767        make_init_with_optional_fields(
768            OrderType::TrailingStopLimit,
769            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
770            Some(dec!(1)), Some(dec!(1)), None,
771        ),
772        "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
773    )]
774    #[case::tsm_missing_trigger_type(
775        make_init_with_optional_fields(
776            OrderType::TrailingStopMarket,
777            None, Some(Price::from("100.00")), None,
778            None, Some(dec!(1)), Some(TrailingOffsetType::Price),
779        ),
780        "`trigger_type` is required for `TrailingStopMarketOrder`",
781    )]
782    #[case::tsm_missing_trailing_offset(
783        make_init_with_optional_fields(
784            OrderType::TrailingStopMarket,
785            None,
786            Some(Price::from("100.00")),
787            Some(TriggerType::LastPrice),
788            None,
789            None,
790            Some(TrailingOffsetType::Price),
791        ),
792        "`trailing_offset` is required for `TrailingStopMarketOrder`"
793    )]
794    #[case::tsm_missing_trailing_offset_type(
795        make_init_with_optional_fields(
796            OrderType::TrailingStopMarket,
797            None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
798            None, Some(dec!(1)), None,
799        ),
800        "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
801    )]
802    fn test_order_any_from_events_missing_required_field_returns_error(
803        #[case] init: OrderInitialized,
804        #[case] expected_field_msg: &str,
805    ) {
806        // Each case omits exactly one required field for its order type. `from_events` must
807        // surface the per-type `TryFrom` error rather than panicking inside `OrderAny::from`.
808        let events = vec![OrderEventAny::Initialized(init)];
809        let err =
810            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
811
812        assert!(matches!(
813            err,
814            OrderReplayError::InvalidInitialization { .. }
815        ));
816        let msg = err.to_string();
817        assert!(
818            msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
819            "unexpected error message: {msg}"
820        );
821    }
822
823    #[rstest]
824    fn test_order_any_from_events_wrong_first_event() {
825        // Create an event that is not OrderInitialized
826        let client_order_id = ClientOrderId::from("ORDER-001");
827        let strategy_id = StrategyId::from("STRATEGY-001");
828
829        let update_event = OrderUpdated {
830            client_order_id,
831            strategy_id,
832            quantity: Quantity::from(20),
833            ..Default::default()
834        };
835
836        // Create a vector with a non-initialization event first
837        let events = vec![OrderEventAny::Updated(update_event)];
838
839        // Attempt to create order should fail
840        let err = OrderAny::from_events(events).expect_err("wrong first event should fail replay");
841        assert!(matches!(err, OrderReplayError::WrongFirstEvent));
842        assert_eq!(err.to_string(), "First event must be `OrderInitialized`");
843    }
844
845    #[rstest]
846    fn test_order_any_from_events_apply_failure() {
847        let init_event = OrderInitializedSpec::builder()
848            .order_type(OrderType::Market)
849            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
850            .quantity(Quantity::from(10))
851            .build();
852
853        let events = vec![
854            OrderEventAny::Initialized(init_event.clone()),
855            OrderEventAny::Initialized(init_event),
856        ];
857        let err =
858            OrderAny::from_events(events).expect_err("later invalid event should fail replay");
859
860        match &err {
861            OrderReplayError::ApplyFailed { source } => {
862                assert!(matches!(source, OrderError::AlreadyInitialized));
863            }
864            _ => panic!("expected ApplyFailed, was {err:?}"),
865        }
866        assert_eq!(err.to_string(), "Order was already initialized");
867    }
868
869    #[rstest]
870    fn test_passive_order_any_conversion() {
871        // Create a limit order
872        let limit_order = OrderTestBuilder::new(OrderType::Limit)
873            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
874            .quantity(Quantity::from(10))
875            .price(Price::new(100.0, 2))
876            .build();
877
878        // Convert to PassiveOrderAny and back
879        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
880        let order_any: OrderAny = passive_order.into();
881
882        // Verify it maintained its properties
883        assert_eq!(order_any.order_type(), OrderType::Limit);
884        assert_eq!(order_any.quantity(), Quantity::from(10));
885    }
886
887    #[rstest]
888    fn test_stop_order_any_conversion() {
889        // Create a stop market order
890        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
891            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
892            .quantity(Quantity::from(10))
893            .trigger_price(Price::new(100.0, 2))
894            .build();
895
896        // Convert to StopOrderAny and back
897        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
898        let order_any: OrderAny = stop_order_any.into();
899
900        // Verify it maintained its properties
901        assert_eq!(order_any.order_type(), OrderType::StopMarket);
902        assert_eq!(order_any.quantity(), Quantity::from(10));
903        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
904    }
905
906    #[rstest]
907    fn test_limit_order_any_conversion() {
908        // Create a limit order
909        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        // Convert to LimitOrderAny and back
916        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
917        let order_any: OrderAny = limit_order_any.into();
918
919        // Verify it maintained its properties
920        assert_eq!(order_any.order_type(), OrderType::Limit);
921        assert_eq!(order_any.quantity(), Quantity::from(10));
922    }
923
924    #[rstest]
925    fn test_limit_order_any_limit_price() {
926        // Create a limit order
927        let limit_order = OrderTestBuilder::new(OrderType::Limit)
928            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
929            .quantity(Quantity::from(10))
930            .price(Price::new(100.0, 2))
931            .build();
932
933        // Convert to LimitOrderAny
934        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
935
936        // Check limit price accessor
937        let limit_px = limit_order_any.limit_px();
938        assert_eq!(limit_px, Price::new(100.0, 2));
939    }
940
941    #[rstest]
942    fn test_stop_order_any_stop_price() {
943        // Create a stop market order
944        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
945            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
946            .quantity(Quantity::from(10))
947            .trigger_price(Price::new(100.0, 2))
948            .build();
949
950        // Convert to StopOrderAny
951        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
952
953        // Check stop price accessor
954        let stop_px = stop_order_any.stop_px();
955        assert_eq!(stop_px, Some(Price::new(100.0, 2)));
956    }
957
958    #[rstest]
959    fn test_trailing_stop_market_order_conversion() {
960        // Create a trailing stop market order
961        let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
962            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
963            .quantity(Quantity::from(10))
964            .trigger_price(Price::new(100.0, 2))
965            .trailing_offset(Decimal::new(5, 1)) // 0.5
966            .trailing_offset_type(TrailingOffsetType::Price)
967            .build();
968
969        // Convert to StopOrderAny
970        let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
971
972        // And back to OrderAny
973        let order_any: OrderAny = stop_order_any.into();
974
975        // Verify properties are preserved
976        assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
977        assert_eq!(order_any.quantity(), Quantity::from(10));
978        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
979        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
980        assert_eq!(
981            order_any.trailing_offset_type(),
982            Some(TrailingOffsetType::Price)
983        );
984    }
985
986    #[rstest]
987    fn test_trailing_stop_limit_order_conversion() {
988        // Create a trailing stop limit order
989        let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
990            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
991            .quantity(Quantity::from(10))
992            .price(Price::new(99.0, 2))
993            .trigger_price(Price::new(100.0, 2))
994            .limit_offset(Decimal::new(10, 1)) // 1.0
995            .trailing_offset(Decimal::new(5, 1)) // 0.5
996            .trailing_offset_type(TrailingOffsetType::Price)
997            .build();
998
999        // Convert to LimitOrderAny
1000        let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
1001
1002        // Check limit price
1003        assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
1004
1005        // Convert back to OrderAny
1006        let order_any: OrderAny = limit_order_any.into();
1007
1008        // Verify properties are preserved
1009        assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
1010        assert_eq!(order_any.quantity(), Quantity::from(10));
1011        assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
1012        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
1013        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
1014    }
1015
1016    #[rstest]
1017    fn test_passive_order_any_to_any() {
1018        // Create a limit order
1019        let limit_order = OrderTestBuilder::new(OrderType::Limit)
1020            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1021            .quantity(Quantity::from(10))
1022            .price(Price::new(100.0, 2))
1023            .build();
1024
1025        // Convert to PassiveOrderAny
1026        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
1027
1028        // Use to_any method
1029        let order_any = passive_order.to_any();
1030
1031        // Verify it maintained its properties
1032        assert_eq!(order_any.order_type(), OrderType::Limit);
1033        assert_eq!(order_any.quantity(), Quantity::from(10));
1034        assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
1035    }
1036}