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