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