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