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    fn test_order_any_equality() {
401        // Create two orders with different types but same client_order_id
402        let client_order_id = ClientOrderId::from("ORDER-001");
403
404        let market_order = OrderTestBuilder::new(OrderType::Market)
405            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
406            .quantity(Quantity::from(10))
407            .client_order_id(client_order_id)
408            .build();
409
410        let limit_order = OrderTestBuilder::new(OrderType::Limit)
411            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
412            .quantity(Quantity::from(10))
413            .price(Price::new(100.0, 2))
414            .client_order_id(client_order_id)
415            .build();
416
417        // They should be equal because they have the same client_order_id
418        assert_eq!(market_order, limit_order);
419    }
420
421    #[rstest]
422    fn test_order_any_conversion_from_events() {
423        // Create an OrderInitialized event
424        let init_event = OrderInitializedSpec::builder()
425            .order_type(OrderType::Market)
426            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
427            .quantity(Quantity::from(10))
428            .build();
429
430        // Create a vector of events
431        let events = vec![OrderEventAny::Initialized(init_event.clone())];
432
433        // Create OrderAny from events
434        let order = OrderAny::from_events(events).unwrap();
435
436        // Verify the order was created properly
437        assert_eq!(order.order_type(), OrderType::Market);
438        assert_eq!(order.instrument_id(), init_event.instrument_id);
439        assert_eq!(order.quantity(), init_event.quantity);
440    }
441
442    #[rstest]
443    #[case::limit(OrderType::Limit)]
444    #[case::limit_if_touched(OrderType::LimitIfTouched)]
445    #[case::market(OrderType::Market)]
446    #[case::market_if_touched(OrderType::MarketIfTouched)]
447    #[case::market_to_limit(OrderType::MarketToLimit)]
448    #[case::stop_limit(OrderType::StopLimit)]
449    #[case::stop_market(OrderType::StopMarket)]
450    #[case::trailing_stop_limit(OrderType::TrailingStopLimit)]
451    #[case::trailing_stop_market(OrderType::TrailingStopMarket)]
452    fn test_set_order_list_id_updates_init_event_and_replay(#[case] order_type: OrderType) {
453        let mut order = order_for_list_id_test(order_type);
454        let init_before = order.init_event().clone();
455        let event_count = order.event_count();
456        let order_list_id = OrderListId::from("OL-SET-LIST-001");
457
458        order.set_order_list_id(order_list_id);
459
460        let mut expected_init = init_before;
461        expected_init.order_list_id = Some(order_list_id);
462        assert_eq!(order.order_list_id(), Some(order_list_id));
463        assert_eq!(order.init_event(), &expected_init);
464        assert_eq!(order.event_count(), event_count);
465
466        let persisted_events: Vec<_> = order.events().into_iter().cloned().collect();
467        assert_eq!(
468            persisted_events,
469            vec![OrderEventAny::Initialized(expected_init.clone())]
470        );
471
472        let replayed = OrderAny::from_events(persisted_events).unwrap();
473        assert_eq!(replayed.order_list_id(), Some(order_list_id));
474        assert_eq!(replayed.init_event(), &expected_init);
475    }
476
477    fn order_for_list_id_test(order_type: OrderType) -> OrderAny {
478        OrderTestBuilder::new(order_type)
479            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
480            .client_order_id(ClientOrderId::from("ORDER-LIST-001"))
481            .quantity(Quantity::from(10))
482            .price(Price::from("101.00"))
483            .trigger_price(Price::from("100.00"))
484            .trigger_type(TriggerType::LastPrice)
485            .limit_offset(dec!(1))
486            .trailing_offset(dec!(1))
487            .trailing_offset_type(TrailingOffsetType::Price)
488            .init_id(UUID4::from("16578139-a945-4b65-b46c-bc131a15d8e7"))
489            .ts_init(UnixNanos::from(123_456_789))
490            .build()
491    }
492
493    #[rstest]
494    fn test_order_any_from_events_empty_error() {
495        let events: Vec<OrderEventAny> = vec![];
496        let err = OrderAny::from_events(events).expect_err("empty events should fail");
497
498        assert!(matches!(err, OrderReplayError::EmptyInput));
499        assert_eq!(
500            err.to_string(),
501            "No order events provided to create OrderAny"
502        );
503    }
504
505    #[rstest]
506    fn test_order_any_from_events_invalid_init_returns_error() {
507        // Limit order with `price = None` violates `LimitOrder` invariants. Previously this
508        // panicked inside `From<OrderInitialized>`; `from_events` must now surface it as `Err`.
509        let init_event = OrderInitializedSpec::builder()
510            .order_type(OrderType::Limit)
511            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
512            .quantity(Quantity::from(10))
513            .build();
514
515        let events = vec![OrderEventAny::Initialized(init_event)];
516        let err =
517            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
518
519        match &err {
520            OrderReplayError::InvalidInitialization { source } => {
521                assert_eq!(
522                    source.to_string(),
523                    "`price` is required for `LimitOrder` initialization",
524                );
525            }
526            _ => panic!("expected InvalidInitialization, was {err:?}"),
527        }
528        assert_eq!(
529            err.to_string(),
530            "Invalid `OrderInitialized` event: `price` is required for `LimitOrder` initialization",
531        );
532    }
533
534    #[rstest]
535    #[case::buy(
536        OrderSide::Buy,
537        Price::from("100.00"),
538        Price::from("101.00"),
539        "BUY Limit-If-Touched"
540    )]
541    #[case::sell(
542        OrderSide::Sell,
543        Price::from("100.00"),
544        Price::from("99.00"),
545        "SELL Limit-If-Touched"
546    )]
547    fn test_order_any_from_events_invalid_predicate_returns_error(
548        #[case] side: OrderSide,
549        #[case] price: Price,
550        #[case] trigger_price: Price,
551        #[case] expected_msg: &str,
552    ) {
553        // LimitIfTouched enforces `trigger_price <= price` for BUY and `trigger_price >= price`
554        // for SELL inside `new_checked`. Reconciliation must see violations as `Err` rather
555        // than panicking, on either side.
556        let init_event = OrderInitializedSpec::builder()
557            .order_type(OrderType::LimitIfTouched)
558            .order_side(side)
559            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
560            .quantity(Quantity::from(10))
561            .price(price)
562            .trigger_price(trigger_price)
563            .trigger_type(TriggerType::LastPrice)
564            .build();
565
566        let events = vec![OrderEventAny::Initialized(init_event)];
567        let err =
568            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
569
570        assert!(matches!(
571            err,
572            OrderReplayError::InvalidInitialization { .. }
573        ));
574        let msg = err.to_string();
575        assert!(
576            msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_msg),
577            "unexpected error message: {msg}"
578        );
579    }
580
581    fn make_init_with_optional_fields(
582        order_type: OrderType,
583        price: Option<Price>,
584        trigger_price: Option<Price>,
585        trigger_type: Option<TriggerType>,
586        limit_offset: Option<Decimal>,
587        trailing_offset: Option<Decimal>,
588        trailing_offset_type: Option<TrailingOffsetType>,
589    ) -> OrderInitialized {
590        OrderInitializedSpec::builder()
591            .order_type(order_type)
592            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
593            .quantity(Quantity::from(10))
594            .maybe_price(price)
595            .maybe_trigger_price(trigger_price)
596            .maybe_trigger_type(trigger_type)
597            .maybe_limit_offset(limit_offset)
598            .maybe_trailing_offset(trailing_offset)
599            .maybe_trailing_offset_type(trailing_offset_type)
600            .build()
601    }
602
603    #[rstest]
604    #[case::lit_missing_price(
605        make_init_with_optional_fields(
606            OrderType::LimitIfTouched,
607            None,
608            Some(Price::from("100.00")),
609            Some(TriggerType::LastPrice),
610            None,
611            None,
612            None,
613        ),
614        "`price` is required for `LimitIfTouchedOrder`"
615    )]
616    #[case::lit_missing_trigger_price(
617        make_init_with_optional_fields(
618            OrderType::LimitIfTouched,
619            Some(Price::from("100.00")),
620            None,
621            Some(TriggerType::LastPrice),
622            None,
623            None,
624            None,
625        ),
626        "`trigger_price` is required for `LimitIfTouchedOrder`"
627    )]
628    #[case::lit_missing_trigger_type(
629        make_init_with_optional_fields(
630            OrderType::LimitIfTouched,
631            Some(Price::from("100.00")),
632            Some(Price::from("99.00")),
633            None,
634            None,
635            None,
636            None,
637        ),
638        "`trigger_type` is required for `LimitIfTouchedOrder`"
639    )]
640    #[case::stop_limit_missing_price(
641        make_init_with_optional_fields(
642            OrderType::StopLimit,
643            None,
644            Some(Price::from("100.00")),
645            Some(TriggerType::LastPrice),
646            None,
647            None,
648            None,
649        ),
650        "`price` is required for `StopLimitOrder`"
651    )]
652    #[case::stop_limit_missing_trigger_price(
653        make_init_with_optional_fields(
654            OrderType::StopLimit,
655            Some(Price::from("100.00")),
656            None,
657            Some(TriggerType::LastPrice),
658            None,
659            None,
660            None,
661        ),
662        "`trigger_price` is required for `StopLimitOrder`"
663    )]
664    #[case::stop_limit_missing_trigger_type(
665        make_init_with_optional_fields(
666            OrderType::StopLimit,
667            Some(Price::from("100.00")),
668            Some(Price::from("99.00")),
669            None,
670            None,
671            None,
672            None,
673        ),
674        "`trigger_type` is required for `StopLimitOrder`"
675    )]
676    #[case::stop_market_missing_trigger_price(
677        make_init_with_optional_fields(
678            OrderType::StopMarket,
679            None,
680            None,
681            Some(TriggerType::LastPrice),
682            None,
683            None,
684            None,
685        ),
686        "`trigger_price` is required for `StopMarketOrder`"
687    )]
688    #[case::stop_market_missing_trigger_type(
689        make_init_with_optional_fields(
690            OrderType::StopMarket,
691            None,
692            Some(Price::from("100.00")),
693            None,
694            None,
695            None,
696            None,
697        ),
698        "`trigger_type` is required for `StopMarketOrder`"
699    )]
700    #[case::mit_missing_trigger_price(
701        make_init_with_optional_fields(
702            OrderType::MarketIfTouched,
703            None,
704            None,
705            Some(TriggerType::LastPrice),
706            None,
707            None,
708            None,
709        ),
710        "`trigger_price` is required for `MarketIfTouchedOrder`"
711    )]
712    #[case::mit_missing_trigger_type(
713        make_init_with_optional_fields(
714            OrderType::MarketIfTouched,
715            None,
716            Some(Price::from("100.00")),
717            None,
718            None,
719            None,
720            None,
721        ),
722        "`trigger_type` is required for `MarketIfTouchedOrder`"
723    )]
724    #[case::tsl_missing_trigger_type(
725        make_init_with_optional_fields(
726            OrderType::TrailingStopLimit,
727            Some(Price::from("100.00")), Some(Price::from("99.00")), None,
728            Some(dec!(1)), Some(dec!(1)), Some(TrailingOffsetType::Price),
729        ),
730        "`trigger_type` is required for `TrailingStopLimitOrder`",
731    )]
732    #[case::tsl_missing_limit_offset(
733        make_init_with_optional_fields(
734            OrderType::TrailingStopLimit,
735            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
736            None, Some(dec!(1)), Some(TrailingOffsetType::Price),
737        ),
738        "`limit_offset` is required for `TrailingStopLimitOrder`",
739    )]
740    #[case::tsl_missing_trailing_offset(
741        make_init_with_optional_fields(
742            OrderType::TrailingStopLimit,
743            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
744            Some(dec!(1)), None, Some(TrailingOffsetType::Price),
745        ),
746        "`trailing_offset` is required for `TrailingStopLimitOrder`",
747    )]
748    #[case::tsl_missing_trailing_offset_type(
749        make_init_with_optional_fields(
750            OrderType::TrailingStopLimit,
751            Some(Price::from("100.00")), Some(Price::from("99.00")), Some(TriggerType::LastPrice),
752            Some(dec!(1)), Some(dec!(1)), None,
753        ),
754        "`trailing_offset_type` is required for `TrailingStopLimitOrder`",
755    )]
756    #[case::tsm_missing_trigger_type(
757        make_init_with_optional_fields(
758            OrderType::TrailingStopMarket,
759            None, Some(Price::from("100.00")), None,
760            None, Some(dec!(1)), Some(TrailingOffsetType::Price),
761        ),
762        "`trigger_type` is required for `TrailingStopMarketOrder`",
763    )]
764    #[case::tsm_missing_trailing_offset(
765        make_init_with_optional_fields(
766            OrderType::TrailingStopMarket,
767            None,
768            Some(Price::from("100.00")),
769            Some(TriggerType::LastPrice),
770            None,
771            None,
772            Some(TrailingOffsetType::Price),
773        ),
774        "`trailing_offset` is required for `TrailingStopMarketOrder`"
775    )]
776    #[case::tsm_missing_trailing_offset_type(
777        make_init_with_optional_fields(
778            OrderType::TrailingStopMarket,
779            None, Some(Price::from("100.00")), Some(TriggerType::LastPrice),
780            None, Some(dec!(1)), None,
781        ),
782        "`trailing_offset_type` is required for `TrailingStopMarketOrder`",
783    )]
784    fn test_order_any_from_events_missing_required_field_returns_error(
785        #[case] init: OrderInitialized,
786        #[case] expected_field_msg: &str,
787    ) {
788        // Each case omits exactly one required field for its order type. `from_events` must
789        // surface the per-type `TryFrom` error rather than panicking inside `OrderAny::from`.
790        let events = vec![OrderEventAny::Initialized(init)];
791        let err =
792            OrderAny::from_events(events).expect_err("invalid initialization should fail replay");
793
794        assert!(matches!(
795            err,
796            OrderReplayError::InvalidInitialization { .. }
797        ));
798        let msg = err.to_string();
799        assert!(
800            msg.contains("Invalid `OrderInitialized` event") && msg.contains(expected_field_msg),
801            "unexpected error message: {msg}"
802        );
803    }
804
805    #[rstest]
806    fn test_order_any_from_events_wrong_first_event() {
807        // Create an event that is not OrderInitialized
808        let client_order_id = ClientOrderId::from("ORDER-001");
809        let strategy_id = StrategyId::from("STRATEGY-001");
810
811        let update_event = OrderUpdated {
812            client_order_id,
813            strategy_id,
814            quantity: Quantity::from(20),
815            ..Default::default()
816        };
817
818        // Create a vector with a non-initialization event first
819        let events = vec![OrderEventAny::Updated(update_event)];
820
821        // Attempt to create order should fail
822        let err = OrderAny::from_events(events).expect_err("wrong first event should fail replay");
823        assert!(matches!(err, OrderReplayError::WrongFirstEvent));
824        assert_eq!(err.to_string(), "First event must be `OrderInitialized`");
825    }
826
827    #[rstest]
828    fn test_order_any_from_events_apply_failure() {
829        let init_event = OrderInitializedSpec::builder()
830            .order_type(OrderType::Market)
831            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
832            .quantity(Quantity::from(10))
833            .build();
834
835        let events = vec![
836            OrderEventAny::Initialized(init_event.clone()),
837            OrderEventAny::Initialized(init_event),
838        ];
839        let err =
840            OrderAny::from_events(events).expect_err("later invalid event should fail replay");
841
842        match &err {
843            OrderReplayError::ApplyFailed { source } => {
844                assert!(matches!(source, OrderError::AlreadyInitialized));
845            }
846            _ => panic!("expected ApplyFailed, was {err:?}"),
847        }
848        assert_eq!(err.to_string(), "Order was already initialized");
849    }
850
851    #[rstest]
852    fn test_passive_order_any_conversion() {
853        // Create a limit order
854        let limit_order = OrderTestBuilder::new(OrderType::Limit)
855            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
856            .quantity(Quantity::from(10))
857            .price(Price::new(100.0, 2))
858            .build();
859
860        // Convert to PassiveOrderAny and back
861        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
862        let order_any: OrderAny = passive_order.into();
863
864        // Verify it maintained its properties
865        assert_eq!(order_any.order_type(), OrderType::Limit);
866        assert_eq!(order_any.quantity(), Quantity::from(10));
867    }
868
869    #[rstest]
870    fn test_stop_order_any_conversion() {
871        // Create a stop market order
872        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
873            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
874            .quantity(Quantity::from(10))
875            .trigger_price(Price::new(100.0, 2))
876            .build();
877
878        // Convert to StopOrderAny and back
879        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
880        let order_any: OrderAny = stop_order_any.into();
881
882        // Verify it maintained its properties
883        assert_eq!(order_any.order_type(), OrderType::StopMarket);
884        assert_eq!(order_any.quantity(), Quantity::from(10));
885        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
886    }
887
888    #[rstest]
889    fn test_limit_order_any_conversion() {
890        // Create a limit order
891        let limit_order = OrderTestBuilder::new(OrderType::Limit)
892            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
893            .quantity(Quantity::from(10))
894            .price(Price::new(100.0, 2))
895            .build();
896
897        // Convert to LimitOrderAny and back
898        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
899        let order_any: OrderAny = limit_order_any.into();
900
901        // Verify it maintained its properties
902        assert_eq!(order_any.order_type(), OrderType::Limit);
903        assert_eq!(order_any.quantity(), Quantity::from(10));
904    }
905
906    #[rstest]
907    fn test_limit_order_any_limit_price() {
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
916        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
917
918        // Check limit price accessor
919        let limit_px = limit_order_any.limit_px();
920        assert_eq!(limit_px, Price::new(100.0, 2));
921    }
922
923    #[rstest]
924    fn test_stop_order_any_stop_price() {
925        // Create a stop market order
926        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
927            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
928            .quantity(Quantity::from(10))
929            .trigger_price(Price::new(100.0, 2))
930            .build();
931
932        // Convert to StopOrderAny
933        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
934
935        // Check stop price accessor
936        let stop_px = stop_order_any.stop_px();
937        assert_eq!(stop_px, Some(Price::new(100.0, 2)));
938    }
939
940    #[rstest]
941    fn test_trailing_stop_market_order_conversion() {
942        // Create a trailing stop market order
943        let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
944            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
945            .quantity(Quantity::from(10))
946            .trigger_price(Price::new(100.0, 2))
947            .trailing_offset(Decimal::new(5, 1)) // 0.5
948            .trailing_offset_type(TrailingOffsetType::Price)
949            .build();
950
951        // Convert to StopOrderAny
952        let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
953
954        // And back to OrderAny
955        let order_any: OrderAny = stop_order_any.into();
956
957        // Verify properties are preserved
958        assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
959        assert_eq!(order_any.quantity(), Quantity::from(10));
960        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
961        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
962        assert_eq!(
963            order_any.trailing_offset_type(),
964            Some(TrailingOffsetType::Price)
965        );
966    }
967
968    #[rstest]
969    fn test_trailing_stop_limit_order_conversion() {
970        // Create a trailing stop limit order
971        let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
972            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
973            .quantity(Quantity::from(10))
974            .price(Price::new(99.0, 2))
975            .trigger_price(Price::new(100.0, 2))
976            .limit_offset(Decimal::new(10, 1)) // 1.0
977            .trailing_offset(Decimal::new(5, 1)) // 0.5
978            .trailing_offset_type(TrailingOffsetType::Price)
979            .build();
980
981        // Convert to LimitOrderAny
982        let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
983
984        // Check limit price
985        assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
986
987        // Convert back to OrderAny
988        let order_any: OrderAny = limit_order_any.into();
989
990        // Verify properties are preserved
991        assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
992        assert_eq!(order_any.quantity(), Quantity::from(10));
993        assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
994        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
995        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
996    }
997
998    #[rstest]
999    fn test_passive_order_any_to_any() {
1000        // Create a limit order
1001        let limit_order = OrderTestBuilder::new(OrderType::Limit)
1002            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
1003            .quantity(Quantity::from(10))
1004            .price(Price::new(100.0, 2))
1005            .build();
1006
1007        // Convert to PassiveOrderAny
1008        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
1009
1010        // Use to_any method
1011        let order_any = passive_order.to_any();
1012
1013        // Verify it maintained its properties
1014        assert_eq!(order_any.order_type(), OrderType::Limit);
1015        assert_eq!(order_any.quantity(), Quantity::from(10));
1016        assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
1017    }
1018}