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    ///
81    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    /// Returns a reference to the [`crate::events::OrderInitialized`] event.
103    ///
104    /// This is always the first event in the order's event list (invariant).
105    ///
106    /// # Panics
107    ///
108    /// Panics if the first event is not `OrderInitialized` (violates invariant).
109    #[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    /// Assigns the order to the list identified by `id`.
122    ///
123    /// # Panics
124    ///
125    /// Panics if the order has no events or its first event is not `OrderInitialized`.
126    pub fn set_order_list_id(&mut self, id: OrderListId) {
127        let order: &mut OrderCore = match self {
128            Self::Limit(order) => order,
129            Self::LimitIfTouched(order) => order,
130            Self::Market(order) => order,
131            Self::MarketIfTouched(order) => order,
132            Self::MarketToLimit(order) => order,
133            Self::StopLimit(order) => order,
134            Self::StopMarket(order) => order,
135            Self::TrailingStopLimit(order) => order,
136            Self::TrailingStopMarket(order) => order,
137        };
138        let init = match order
139            .events
140            .first_mut()
141            .expect("Order invariant violated: no events")
142        {
143            OrderEventAny::Initialized(init) => init,
144            _ => panic!("Order invariant violated: first event must be OrderInitialized"),
145        };
146
147        order.order_list_id = Some(id);
148        init.order_list_id = Some(id);
149    }
150}
151
152impl PartialEq for OrderAny {
153    fn eq(&self, other: &Self) -> bool {
154        self.client_order_id() == other.client_order_id()
155    }
156}
157
158// TODO: fix equality
159impl Eq for OrderAny {}
160
161impl Display for OrderAny {
162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163        write!(
164            f,
165            "{}",
166            match self {
167                Self::Limit(order) => order.to_string(),
168                Self::LimitIfTouched(order) => order.to_string(),
169                Self::Market(order) => order.to_string(),
170                Self::MarketIfTouched(order) => order.to_string(),
171                Self::MarketToLimit(order) => order.to_string(),
172                Self::StopLimit(order) => order.to_string(),
173                Self::StopMarket(order) => order.to_string(),
174                Self::TrailingStopLimit(order) => order.to_string(),
175                Self::TrailingStopMarket(order) => order.to_string(),
176            }
177        )
178    }
179}
180
181impl TryFrom<OrderAny> for PassiveOrderAny {
182    type Error = String;
183
184    fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
185        match order {
186            OrderAny::Limit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
187            OrderAny::LimitIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
188            OrderAny::MarketIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
189            OrderAny::StopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
190            OrderAny::StopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
191            OrderAny::TrailingStopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
192            OrderAny::TrailingStopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
193            OrderAny::MarketToLimit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
194            OrderAny::Market(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
195        }
196    }
197}
198
199impl From<PassiveOrderAny> for OrderAny {
200    fn from(order: PassiveOrderAny) -> Self {
201        match order {
202            PassiveOrderAny::Limit(order) => order.into(),
203            PassiveOrderAny::Stop(order) => order.into(),
204        }
205    }
206}
207
208impl TryFrom<OrderAny> for StopOrderAny {
209    type Error = String;
210
211    fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
212        match order {
213            OrderAny::LimitIfTouched(order) => Ok(Self::LimitIfTouched(order)),
214            OrderAny::MarketIfTouched(order) => Ok(Self::MarketIfTouched(order)),
215            OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
216            OrderAny::StopMarket(order) => Ok(Self::StopMarket(order)),
217            OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
218            OrderAny::TrailingStopMarket(order) => Ok(Self::TrailingStopMarket(order)),
219            _ => Err(format!(
220                "Cannot convert {:?} order to StopOrderAny: order type does not have a stop/trigger price",
221                order.order_type()
222            )),
223        }
224    }
225}
226
227impl From<StopOrderAny> for OrderAny {
228    fn from(order: StopOrderAny) -> Self {
229        match order {
230            StopOrderAny::LimitIfTouched(order) => Self::LimitIfTouched(order),
231            StopOrderAny::MarketIfTouched(order) => Self::MarketIfTouched(order),
232            StopOrderAny::StopLimit(order) => Self::StopLimit(order),
233            StopOrderAny::StopMarket(order) => Self::StopMarket(order),
234            StopOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
235            StopOrderAny::TrailingStopMarket(order) => Self::TrailingStopMarket(order),
236        }
237    }
238}
239
240impl TryFrom<OrderAny> for LimitOrderAny {
241    type Error = String;
242
243    fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
244        match order {
245            OrderAny::Limit(order) => Ok(Self::Limit(order)),
246            OrderAny::MarketToLimit(order) => Ok(Self::MarketToLimit(order)),
247            OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
248            OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
249            OrderAny::Market(order) => Ok(Self::MarketOrderWithProtection(order)),
250            _ => Err(format!(
251                "Cannot convert {:?} order to LimitOrderAny: order type does not have a limit price",
252                order.order_type()
253            )),
254        }
255    }
256}
257
258impl From<LimitOrderAny> for OrderAny {
259    fn from(order: LimitOrderAny) -> Self {
260        match order {
261            LimitOrderAny::Limit(order) => Self::Limit(order),
262            LimitOrderAny::MarketToLimit(order) => Self::MarketToLimit(order),
263            LimitOrderAny::StopLimit(order) => Self::StopLimit(order),
264            LimitOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
265            LimitOrderAny::MarketOrderWithProtection(order) => Self::Market(order),
266        }
267    }
268}
269
270#[derive(Clone, Debug)]
271#[enum_dispatch(Order)]
272pub enum PassiveOrderAny {
273    Limit(LimitOrderAny),
274    Stop(StopOrderAny),
275}
276
277impl PassiveOrderAny {
278    #[must_use]
279    pub fn to_any(&self) -> OrderAny {
280        match self {
281            Self::Limit(order) => order.clone().into(),
282            Self::Stop(order) => order.clone().into(),
283        }
284    }
285}
286
287// TODO: Derive equality
288impl PartialEq for PassiveOrderAny {
289    fn eq(&self, rhs: &Self) -> bool {
290        match self {
291            Self::Limit(order) => order.client_order_id() == rhs.client_order_id(),
292            Self::Stop(order) => order.client_order_id() == rhs.client_order_id(),
293        }
294    }
295}
296
297#[derive(Clone, Debug)]
298#[enum_dispatch(Order)]
299pub enum LimitOrderAny {
300    Limit(LimitOrder),
301    MarketToLimit(MarketToLimitOrder),
302    StopLimit(StopLimitOrder),
303    TrailingStopLimit(TrailingStopLimitOrder),
304    MarketOrderWithProtection(MarketOrder),
305}
306
307impl LimitOrderAny {
308    /// Returns the limit price for this order.
309    ///
310    /// # Panics
311    ///
312    /// Panics if the `MarketToLimit` order price is not set.
313    #[must_use]
314    pub fn limit_px(&self) -> Price {
315        match self {
316            Self::Limit(order) => order.price,
317            Self::MarketToLimit(order) => order.price.expect("MarketToLimit order price not set"),
318            Self::StopLimit(order) => order.price,
319            Self::TrailingStopLimit(order) => {
320                order.price.expect("TrailingStopLimit order price not set")
321            }
322            Self::MarketOrderWithProtection(order) => {
323                order.protection_price.expect("No price for order")
324            }
325        }
326    }
327}
328
329impl PartialEq for LimitOrderAny {
330    fn eq(&self, rhs: &Self) -> bool {
331        match self {
332            Self::Limit(order) => order.client_order_id == rhs.client_order_id(),
333            Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(),
334            Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
335            Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
336            Self::MarketOrderWithProtection(order) => {
337                order.client_order_id == rhs.client_order_id()
338            }
339        }
340    }
341}
342
343#[derive(Clone, Debug)]
344#[enum_dispatch(Order)]
345pub enum StopOrderAny {
346    LimitIfTouched(LimitIfTouchedOrder),
347    MarketIfTouched(MarketIfTouchedOrder),
348    StopLimit(StopLimitOrder),
349    StopMarket(StopMarketOrder),
350    TrailingStopLimit(TrailingStopLimitOrder),
351    TrailingStopMarket(TrailingStopMarketOrder),
352}
353
354impl StopOrderAny {
355    #[must_use]
356    pub fn stop_px(&self) -> Option<Price> {
357        match self {
358            Self::LimitIfTouched(o) => Some(o.trigger_price),
359            Self::MarketIfTouched(o) => Some(o.trigger_price),
360            Self::StopLimit(o) => Some(o.trigger_price),
361            Self::StopMarket(o) => Some(o.trigger_price),
362            Self::TrailingStopLimit(o) => o.activation_price.or(o.trigger_price),
363            Self::TrailingStopMarket(o) => o.activation_price.or(o.trigger_price),
364        }
365    }
366}
367
368// TODO: Derive equality
369impl PartialEq for StopOrderAny {
370    fn eq(&self, rhs: &Self) -> bool {
371        match self {
372            Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(),
373            Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
374            Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(),
375            Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(),
376            Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
377            Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(),
378        }
379    }
380}
381
382#[cfg(test)]
383mod tests {
384    use nautilus_core::{UUID4, UnixNanos};
385    use rstest::rstest;
386    use rust_decimal::Decimal;
387    use rust_decimal_macros::dec;
388
389    use super::*;
390    use crate::{
391        enums::{OrderSide, OrderType, TrailingOffsetType, TriggerType},
392        events::{
393            OrderEventAny, OrderInitialized, OrderUpdated, order::spec::OrderInitializedSpec,
394        },
395        identifiers::{ClientOrderId, InstrumentId, StrategyId},
396        orders::{OrderError, builder::OrderTestBuilder},
397        types::{Price, Quantity},
398    };
399
400    #[rstest]
401    fn test_order_any_equality() {
402        // Create two orders with different types but same client_order_id
403        let client_order_id = ClientOrderId::from("ORDER-001");
404
405        let market_order = OrderTestBuilder::new(OrderType::Market)
406            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
407            .quantity(Quantity::from(10))
408            .client_order_id(client_order_id)
409            .build();
410
411        let limit_order = OrderTestBuilder::new(OrderType::Limit)
412            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
413            .quantity(Quantity::from(10))
414            .price(Price::new(100.0, 2))
415            .client_order_id(client_order_id)
416            .build();
417
418        // They should be equal because they have the same client_order_id
419        assert_eq!(market_order, limit_order);
420    }
421
422    #[rstest]
423    fn test_order_any_conversion_from_events() {
424        // Create an OrderInitialized event
425        let init_event = OrderInitializedSpec::builder()
426            .order_type(OrderType::Market)
427            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
428            .quantity(Quantity::from(10))
429            .build();
430
431        // Create a vector of events
432        let events = vec![OrderEventAny::Initialized(init_event.clone())];
433
434        // Create OrderAny from events
435        let order = OrderAny::from_events(events).unwrap();
436
437        // Verify the order was created properly
438        assert_eq!(order.order_type(), OrderType::Market);
439        assert_eq!(order.instrument_id(), init_event.instrument_id);
440        assert_eq!(order.quantity(), init_event.quantity);
441    }
442
443    #[rstest]
444    #[case::limit(OrderType::Limit)]
445    #[case::limit_if_touched(OrderType::LimitIfTouched)]
446    #[case::market(OrderType::Market)]
447    #[case::market_if_touched(OrderType::MarketIfTouched)]
448    #[case::market_to_limit(OrderType::MarketToLimit)]
449    #[case::stop_limit(OrderType::StopLimit)]
450    #[case::stop_market(OrderType::StopMarket)]
451    #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
452    #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
453    fn test_set_order_list_id_updates_init_event_and_replay(#[case] order_type: OrderType) {
454        let mut order = order_for_list_id_test(order_type);
455        let init_before = order.init_event().clone();
456        let event_count = order.event_count();
457        let order_list_id = OrderListId::from("OL-SET-LIST-001");
458
459        order.set_order_list_id(order_list_id);
460
461        let mut expected_init = init_before;
462        expected_init.order_list_id = Some(order_list_id);
463        assert_eq!(order.order_list_id(), Some(order_list_id));
464        assert_eq!(order.init_event(), &expected_init);
465        assert_eq!(order.event_count(), event_count);
466
467        let persisted_events: Vec<_> = order.events().into_iter().cloned().collect();
468        assert_eq!(
469            persisted_events,
470            vec![OrderEventAny::Initialized(expected_init.clone())]
471        );
472
473        let replayed = OrderAny::from_events(persisted_events).unwrap();
474        assert_eq!(replayed.order_list_id(), Some(order_list_id));
475        assert_eq!(replayed.init_event(), &expected_init);
476    }
477
478    fn order_for_list_id_test(order_type: OrderType) -> OrderAny {
479        OrderTestBuilder::new(order_type)
480            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
481            .client_order_id(ClientOrderId::from("ORDER-LIST-001"))
482            .quantity(Quantity::from(10))
483            .price(Price::from("101.00"))
484            .trigger_price(Price::from("100.00"))
485            .trigger_type(TriggerType::LastPrice)
486            .limit_offset(dec!(1))
487            .trailing_offset(dec!(1))
488            .trailing_offset_type(TrailingOffsetType::Price)
489            .init_id(UUID4::from("16578139-a945-4b65-b46c-bc131a15d8e7"))
490            .ts_init(UnixNanos::from(123_456_789))
491            .build()
492    }
493
494    #[rstest]
495    fn test_order_any_from_events_empty_error() {
496        let events: Vec<OrderEventAny> = vec![];
497        let err = OrderAny::from_events(events).expect_err("empty events should fail");
498
499        assert!(matches!(err, OrderReplayError::EmptyInput));
500        assert_eq!(
501            err.to_string(),
502            "No order events provided to create OrderAny"
503        );
504    }
505
506    #[rstest]
507    fn test_order_any_from_events_invalid_init_returns_error() {
508        // Limit order with `price = None` violates `LimitOrder` invariants. Previously this
509        // panicked inside `From<OrderInitialized>`; `from_events` must now surface it as `Err`.
510        let init_event = OrderInitializedSpec::builder()
511            .order_type(OrderType::Limit)
512            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
513            .quantity(Quantity::from(10))
514            .build();
515
516        let events = vec![OrderEventAny::Initialized(init_event)];
517        let err =
518            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
519
520        match &err {
521            OrderReplayError::InvalidInitialization { source } => {
522                assert_eq!(
523                    source.to_string(),
524                    "`price` is required for `LimitOrder` initialization",
525                );
526            }
527            _ => panic!("expected InvalidInitialization, was {err:?}"),
528        }
529        assert_eq!(
530            err.to_string(),
531            "Invalid `OrderInitialized` event: `price` is required for `LimitOrder` initialization",
532        );
533    }
534
535    #[rstest]
536    #[case::buy(
537        OrderSide::Buy,
538        Price::from("100.00"),
539        Price::from("101.00"),
540        "BUY Limit-If-Touched"
541    )]
542    #[case::sell(
543        OrderSide::Sell,
544        Price::from("100.00"),
545        Price::from("99.00"),
546        "SELL Limit-If-Touched"
547    )]
548    fn test_order_any_from_events_invalid_predicate_returns_error(
549        #[case] side: OrderSide,
550        #[case] price: Price,
551        #[case] trigger_price: Price,
552        #[case] expected_msg: &str,
553    ) {
554        // LimitIfTouched enforces `trigger_price <= price` for BUY and `trigger_price >= price`
555        // for SELL inside `new_checked`. Reconciliation must see violations as `Err` rather
556        // than panicking, on either side.
557        let init_event = OrderInitializedSpec::builder()
558            .order_type(OrderType::LimitIfTouched)
559            .order_side(side)
560            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
561            .quantity(Quantity::from(10))
562            .price(price)
563            .trigger_price(trigger_price)
564            .trigger_type(TriggerType::LastPrice)
565            .build();
566
567        let events = vec![OrderEventAny::Initialized(init_event)];
568        let err =
569            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
570
571        assert!(matches!(
572            err,
573            OrderReplayError::InvalidInitialization { .. }
574        ));
575        let msg = err.to_string();
576        assert!(
577            msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
578            "unexpected error message: {msg}"
579        );
580    }
581
582    fn make_init_with_optional_fields(
583        order_type: OrderType,
584        price: Option<Price>,
585        trigger_price: Option<Price>,
586        trigger_type: Option<TriggerType>,
587        limit_offset: Option<Decimal>,
588        trailing_offset: Option<Decimal>,
589        trailing_offset_type: Option<TrailingOffsetType>,
590    ) -> OrderInitialized {
591        OrderInitializedSpec::builder()
592            .order_type(order_type)
593            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
594            .quantity(Quantity::from(10))
595            .maybe_price(price)
596            .maybe_trigger_price(trigger_price)
597            .maybe_trigger_type(trigger_type)
598            .maybe_limit_offset(limit_offset)
599            .maybe_trailing_offset(trailing_offset)
600            .maybe_trailing_offset_type(trailing_offset_type)
601            .build()
602    }
603
604    #[rstest]
605    #[case::lit_missing_price(
606        make_init_with_optional_fields(
607            OrderType::LimitIfTouched,
608            None,
609            Some(Price::from("100.00")),
610            Some(TriggerType::LastPrice),
611            None,
612            None,
613            None,
614        ),
615        "`price` is required for `LimitIfTouchedOrder`"
616    )]
617    #[case::lit_missing_trigger_price(
618        make_init_with_optional_fields(
619            OrderType::LimitIfTouched,
620            Some(Price::from("100.00")),
621            None,
622            Some(TriggerType::LastPrice),
623            None,
624            None,
625            None,
626        ),
627        "`trigger_price` is required for `LimitIfTouchedOrder`"
628    )]
629    #[case::lit_missing_trigger_type(
630        make_init_with_optional_fields(
631            OrderType::LimitIfTouched,
632            Some(Price::from("100.00")),
633            Some(Price::from("99.00")),
634            None,
635            None,
636            None,
637            None,
638        ),
639        "`trigger_type` is required for `LimitIfTouchedOrder`"
640    )]
641    #[case::stop_limit_missing_price(
642        make_init_with_optional_fields(
643            OrderType::StopLimit,
644            None,
645            Some(Price::from("100.00")),
646            Some(TriggerType::LastPrice),
647            None,
648            None,
649            None,
650        ),
651        "`price` is required for `StopLimitOrder`"
652    )]
653    #[case::stop_limit_missing_trigger_price(
654        make_init_with_optional_fields(
655            OrderType::StopLimit,
656            Some(Price::from("100.00")),
657            None,
658            Some(TriggerType::LastPrice),
659            None,
660            None,
661            None,
662        ),
663        "`trigger_price` is required for `StopLimitOrder`"
664    )]
665    #[case::stop_limit_missing_trigger_type(
666        make_init_with_optional_fields(
667            OrderType::StopLimit,
668            Some(Price::from("100.00")),
669            Some(Price::from("99.00")),
670            None,
671            None,
672            None,
673            None,
674        ),
675        "`trigger_type` is required for `StopLimitOrder`"
676    )]
677    #[case::stop_market_missing_trigger_price(
678        make_init_with_optional_fields(
679            OrderType::StopMarket,
680            None,
681            None,
682            Some(TriggerType::LastPrice),
683            None,
684            None,
685            None,
686        ),
687        "`trigger_price` is required for `StopMarketOrder`"
688    )]
689    #[case::stop_market_missing_trigger_type(
690        make_init_with_optional_fields(
691            OrderType::StopMarket,
692            None,
693            Some(Price::from("100.00")),
694            None,
695            None,
696            None,
697            None,
698        ),
699        "`trigger_type` is required for `StopMarketOrder`"
700    )]
701    #[case::mit_missing_trigger_price(
702        make_init_with_optional_fields(
703            OrderType::MarketIfTouched,
704            None,
705            None,
706            Some(TriggerType::LastPrice),
707            None,
708            None,
709            None,
710        ),
711        "`trigger_price` is required for `MarketIfTouchedOrder`"
712    )]
713    #[case::mit_missing_trigger_type(
714        make_init_with_optional_fields(
715            OrderType::MarketIfTouched,
716            None,
717            Some(Price::from("100.00")),
718            None,
719            None,
720            None,
721            None,
722        ),
723        "`trigger_type` is required for `MarketIfTouchedOrder`"
724    )]
725    #[case::tsl_missing_trigger_type(
726        make_init_with_optional_fields(
727            OrderType::TrailingStopLimit,
728            Some(Price::from("100.00")), Some(Price::from("99.00")), None,
729            Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
730        ),
731        "`trigger_type` is required for `TrailingStopLimitOrder`",
732    )]
733    #[case::tsl_missing_limit_offset(
734        make_init_with_optional_fields(
735            OrderType::TrailingStopLimit,
736            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
737            None, Some(dec!(1)), Some(TrailingOffsetType::Price),
738        ),
739        "`limit_offset` is required for `TrailingStopLimitOrder`",
740    )]
741    #[case::tsl_missing_trailing_offset(
742        make_init_with_optional_fields(
743            OrderType::TrailingStopLimit,
744            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
745            Some(dec!(1)), None, Some(TrailingOffsetType::Price),
746        ),
747        "`trailing_offset` is required for `TrailingStopLimitOrder`",
748    )]
749    #[case::tsl_missing_trailing_offset_type(
750        make_init_with_optional_fields(
751            OrderType::TrailingStopLimit,
752            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
753            Some(dec!(1)), Some(dec!(1)), None,
754        ),
755        "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
756    )]
757    #[case::tsm_missing_trigger_type(
758        make_init_with_optional_fields(
759            OrderType::TrailingStopMarket,
760            None, Some(Price::from("100.00")), None,
761            None, Some(dec!(1)), Some(TrailingOffsetType::Price),
762        ),
763        "`trigger_type` is required for `TrailingStopMarketOrder`",
764    )]
765    #[case::tsm_missing_trailing_offset(
766        make_init_with_optional_fields(
767            OrderType::TrailingStopMarket,
768            None,
769            Some(Price::from("100.00")),
770            Some(TriggerType::LastPrice),
771            None,
772            None,
773            Some(TrailingOffsetType::Price),
774        ),
775        "`trailing_offset` is required for `TrailingStopMarketOrder`"
776    )]
777    #[case::tsm_missing_trailing_offset_type(
778        make_init_with_optional_fields(
779            OrderType::TrailingStopMarket,
780            None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
781            None, Some(dec!(1)), None,
782        ),
783        "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
784    )]
785    fn test_order_any_from_events_missing_required_field_returns_error(
786        #[case] init: OrderInitialized,
787        #[case] expected_field_msg: &str,
788    ) {
789        // Each case omits exactly one required field for its order type. `from_events` must
790        // surface the per-type `TryFrom` error rather than panicking inside `OrderAny::from`.
791        let events = vec![OrderEventAny::Initialized(init)];
792        let err =
793            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
794
795        assert!(matches!(
796            err,
797            OrderReplayError::InvalidInitialization { .. }
798        ));
799        let msg = err.to_string();
800        assert!(
801            msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
802            "unexpected error message: {msg}"
803        );
804    }
805
806    #[rstest]
807    fn test_order_any_from_events_wrong_first_event() {
808        // Create an event that is not OrderInitialized
809        let client_order_id = ClientOrderId::from("ORDER-001");
810        let strategy_id = StrategyId::from("STRATEGY-001");
811
812        let update_event = OrderUpdated {
813            client_order_id,
814            strategy_id,
815            quantity: Quantity::from(20),
816            ..Default::default()
817        };
818
819        // Create a vector with a non-initialization event first
820        let events = vec![OrderEventAny::Updated(update_event)];
821
822        // Attempt to create order should fail
823        let err = OrderAny::from_events(events).expect_err("wrong first event should fail replay");
824        assert!(matches!(err, OrderReplayError::WrongFirstEvent));
825        assert_eq!(err.to_string(), "First event must be `OrderInitialized`");
826    }
827
828    #[rstest]
829    fn test_order_any_from_events_apply_failure() {
830        let init_event = OrderInitializedSpec::builder()
831            .order_type(OrderType::Market)
832            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
833            .quantity(Quantity::from(10))
834            .build();
835
836        let events = vec![
837            OrderEventAny::Initialized(init_event.clone()),
838            OrderEventAny::Initialized(init_event),
839        ];
840        let err =
841            OrderAny::from_events(events).expect_err("later invalid event should fail replay");
842
843        match &err {
844            OrderReplayError::ApplyFailed { source } => {
845                assert!(matches!(source, OrderError::AlreadyInitialized));
846            }
847            _ => panic!("expected ApplyFailed, was {err:?}"),
848        }
849        assert_eq!(err.to_string(), "Order was already initialized");
850    }
851
852    #[rstest]
853    fn test_passive_order_any_conversion() {
854        // Create a limit order
855        let limit_order = OrderTestBuilder::new(OrderType::Limit)
856            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
857            .quantity(Quantity::from(10))
858            .price(Price::new(100.0, 2))
859            .build();
860
861        // Convert to PassiveOrderAny and back
862        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
863        let order_any: OrderAny = passive_order.into();
864
865        // Verify it maintained its properties
866        assert_eq!(order_any.order_type(), OrderType::Limit);
867        assert_eq!(order_any.quantity(), Quantity::from(10));
868    }
869
870    #[rstest]
871    fn test_stop_order_any_conversion() {
872        // Create a stop market order
873        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
874            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
875            .quantity(Quantity::from(10))
876            .trigger_price(Price::new(100.0, 2))
877            .build();
878
879        // Convert to StopOrderAny and back
880        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
881        let order_any: OrderAny = stop_order_any.into();
882
883        // Verify it maintained its properties
884        assert_eq!(order_any.order_type(), OrderType::StopMarket);
885        assert_eq!(order_any.quantity(), Quantity::from(10));
886        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
887    }
888
889    #[rstest]
890    fn test_limit_order_any_conversion() {
891        // Create a limit order
892        let limit_order = OrderTestBuilder::new(OrderType::Limit)
893            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
894            .quantity(Quantity::from(10))
895            .price(Price::new(100.0, 2))
896            .build();
897
898        // Convert to LimitOrderAny and back
899        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
900        let order_any: OrderAny = limit_order_any.into();
901
902        // Verify it maintained its properties
903        assert_eq!(order_any.order_type(), OrderType::Limit);
904        assert_eq!(order_any.quantity(), Quantity::from(10));
905    }
906
907    #[rstest]
908    fn test_limit_order_any_limit_price() {
909        // Create a limit order
910        let limit_order = OrderTestBuilder::new(OrderType::Limit)
911            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
912            .quantity(Quantity::from(10))
913            .price(Price::new(100.0, 2))
914            .build();
915
916        // Convert to LimitOrderAny
917        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
918
919        // Check limit price accessor
920        let limit_px = limit_order_any.limit_px();
921        assert_eq!(limit_px, Price::new(100.0, 2));
922    }
923
924    #[rstest]
925    fn test_stop_order_any_stop_price() {
926        // Create a stop market order
927        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
928            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
929            .quantity(Quantity::from(10))
930            .trigger_price(Price::new(100.0, 2))
931            .build();
932
933        // Convert to StopOrderAny
934        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
935
936        // Check stop price accessor
937        let stop_px = stop_order_any.stop_px();
938        assert_eq!(stop_px, Some(Price::new(100.0, 2)));
939    }
940
941    #[rstest]
942    fn test_trailing_stop_market_order_conversion() {
943        // Create a trailing stop market order
944        let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
945            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
946            .quantity(Quantity::from(10))
947            .trigger_price(Price::new(100.0, 2))
948            .trailing_offset(Decimal::new(5, 1)) // 0.5
949            .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
950            .build();
951
952        // Convert to StopOrderAny
953        let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
954
955        // And back to OrderAny
956        let order_any: OrderAny = stop_order_any.into();
957
958        // Verify properties are preserved
959        assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
960        assert_eq!(order_any.quantity(), Quantity::from(10));
961        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
962        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
963        assert_eq!(
964            order_any.trailing_offset_type(),
965            Some(TrailingOffsetType::NoTrailingOffset)
966        );
967    }
968
969    #[rstest]
970    fn test_trailing_stop_limit_order_conversion() {
971        // Create a trailing stop limit order
972        let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
973            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
974            .quantity(Quantity::from(10))
975            .price(Price::new(99.0, 2))
976            .trigger_price(Price::new(100.0, 2))
977            .limit_offset(Decimal::new(10, 1)) // 1.0
978            .trailing_offset(Decimal::new(5, 1)) // 0.5
979            .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
980            .build();
981
982        // Convert to LimitOrderAny
983        let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
984
985        // Check limit price
986        assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
987
988        // Convert back to OrderAny
989        let order_any: OrderAny = limit_order_any.into();
990
991        // Verify properties are preserved
992        assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
993        assert_eq!(order_any.quantity(), Quantity::from(10));
994        assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
995        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
996        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
997    }
998
999    #[rstest]
1000    fn test_passive_order_any_to_any() {
1001        // Create a limit order
1002        let limit_order = OrderTestBuilder::new(OrderType::Limit)
1003            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1004            .quantity(Quantity::from(10))
1005            .price(Price::new(100.0, 2))
1006            .build();
1007
1008        // Convert to PassiveOrderAny
1009        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
1010
1011        // Use to_any method
1012        let order_any = passive_order.to_any();
1013
1014        // Verify it maintained its properties
1015        assert_eq!(order_any.order_type(), OrderType::Limit);
1016        assert_eq!(order_any.quantity(), Quantity::from(10));
1017        assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
1018    }
1019}