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, 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    /// - Any event has an invalid state transition when applied to the order.
52    ///
53    /// # Panics
54    ///
55    /// Panics if `events` is empty (after the check, but before .unwrap()).
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::from(init.clone());
66                // Apply the rest of the events
67                for event in events.into_iter().skip(1) {
68                    // Apply event to order
69                    order.apply(event)?;
70                }
71                Ok(order)
72            }
73            _ => {
74                anyhow::bail!("First event must be `OrderInitialized`");
75            }
76        }
77    }
78
79    /// Returns a reference to the [`crate::events::OrderInitialized`] event.
80    ///
81    /// This is always the first event in the order's event list (invariant).
82    ///
83    /// # Panics
84    ///
85    /// Panics if the first event is not `OrderInitialized` (violates invariant).
86    #[must_use]
87    pub fn init_event(&self) -> &crate::events::OrderInitialized {
88        // SAFETY: Order specification guarantees at least one event (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
100impl PartialEq for OrderAny {
101    fn eq(&self, other: &Self) -> bool {
102        self.client_order_id() == other.client_order_id()
103    }
104}
105
106// TODO: fix equality
107impl Eq for OrderAny {}
108
109impl Display for OrderAny {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        write!(
112            f,
113            "{}",
114            match self {
115                Self::Limit(order) => order.to_string(),
116                Self::LimitIfTouched(order) => order.to_string(),
117                Self::Market(order) => order.to_string(),
118                Self::MarketIfTouched(order) => order.to_string(),
119                Self::MarketToLimit(order) => order.to_string(),
120                Self::StopLimit(order) => order.to_string(),
121                Self::StopMarket(order) => order.to_string(),
122                Self::TrailingStopLimit(order) => order.to_string(),
123                Self::TrailingStopMarket(order) => order.to_string(),
124            }
125        )
126    }
127}
128
129impl TryFrom<OrderAny> for PassiveOrderAny {
130    type Error = String;
131
132    fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
133        match order {
134            OrderAny::Limit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
135            OrderAny::LimitIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
136            OrderAny::MarketIfTouched(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
137            OrderAny::StopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
138            OrderAny::StopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
139            OrderAny::TrailingStopLimit(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
140            OrderAny::TrailingStopMarket(_) => Ok(Self::Stop(StopOrderAny::try_from(order)?)),
141            OrderAny::MarketToLimit(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
142            OrderAny::Market(_) => Ok(Self::Limit(LimitOrderAny::try_from(order)?)),
143        }
144    }
145}
146
147impl From<PassiveOrderAny> for OrderAny {
148    fn from(order: PassiveOrderAny) -> Self {
149        match order {
150            PassiveOrderAny::Limit(order) => order.into(),
151            PassiveOrderAny::Stop(order) => order.into(),
152        }
153    }
154}
155
156impl TryFrom<OrderAny> for StopOrderAny {
157    type Error = String;
158
159    fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
160        match order {
161            OrderAny::LimitIfTouched(order) => Ok(Self::LimitIfTouched(order)),
162            OrderAny::MarketIfTouched(order) => Ok(Self::MarketIfTouched(order)),
163            OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
164            OrderAny::StopMarket(order) => Ok(Self::StopMarket(order)),
165            OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
166            OrderAny::TrailingStopMarket(order) => Ok(Self::TrailingStopMarket(order)),
167            _ => Err(format!(
168                "Cannot convert {:?} order to StopOrderAny: order type does not have a stop/trigger price",
169                order.order_type()
170            )),
171        }
172    }
173}
174
175impl From<StopOrderAny> for OrderAny {
176    fn from(order: StopOrderAny) -> Self {
177        match order {
178            StopOrderAny::LimitIfTouched(order) => Self::LimitIfTouched(order),
179            StopOrderAny::MarketIfTouched(order) => Self::MarketIfTouched(order),
180            StopOrderAny::StopLimit(order) => Self::StopLimit(order),
181            StopOrderAny::StopMarket(order) => Self::StopMarket(order),
182            StopOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
183            StopOrderAny::TrailingStopMarket(order) => Self::TrailingStopMarket(order),
184        }
185    }
186}
187
188impl TryFrom<OrderAny> for LimitOrderAny {
189    type Error = String;
190
191    fn try_from(order: OrderAny) -> Result<Self, Self::Error> {
192        match order {
193            OrderAny::Limit(order) => Ok(Self::Limit(order)),
194            OrderAny::MarketToLimit(order) => Ok(Self::MarketToLimit(order)),
195            OrderAny::StopLimit(order) => Ok(Self::StopLimit(order)),
196            OrderAny::TrailingStopLimit(order) => Ok(Self::TrailingStopLimit(order)),
197            OrderAny::Market(order) => Ok(Self::MarketOrderWithProtection(order)),
198            _ => Err(format!(
199                "Cannot convert {:?} order to LimitOrderAny: order type does not have a limit price",
200                order.order_type()
201            )),
202        }
203    }
204}
205
206impl From<LimitOrderAny> for OrderAny {
207    fn from(order: LimitOrderAny) -> Self {
208        match order {
209            LimitOrderAny::Limit(order) => Self::Limit(order),
210            LimitOrderAny::MarketToLimit(order) => Self::MarketToLimit(order),
211            LimitOrderAny::StopLimit(order) => Self::StopLimit(order),
212            LimitOrderAny::TrailingStopLimit(order) => Self::TrailingStopLimit(order),
213            LimitOrderAny::MarketOrderWithProtection(order) => Self::Market(order),
214        }
215    }
216}
217
218#[derive(Clone, Debug)]
219#[enum_dispatch(Order)]
220pub enum PassiveOrderAny {
221    Limit(LimitOrderAny),
222    Stop(StopOrderAny),
223}
224
225impl PassiveOrderAny {
226    #[must_use]
227    pub fn to_any(&self) -> OrderAny {
228        match self {
229            Self::Limit(order) => order.clone().into(),
230            Self::Stop(order) => order.clone().into(),
231        }
232    }
233}
234
235// TODO: Derive equality
236impl PartialEq for PassiveOrderAny {
237    fn eq(&self, rhs: &Self) -> bool {
238        match self {
239            Self::Limit(order) => order.client_order_id() == rhs.client_order_id(),
240            Self::Stop(order) => order.client_order_id() == rhs.client_order_id(),
241        }
242    }
243}
244
245#[derive(Clone, Debug)]
246#[enum_dispatch(Order)]
247pub enum LimitOrderAny {
248    Limit(LimitOrder),
249    MarketToLimit(MarketToLimitOrder),
250    StopLimit(StopLimitOrder),
251    TrailingStopLimit(TrailingStopLimitOrder),
252    MarketOrderWithProtection(MarketOrder),
253}
254
255impl LimitOrderAny {
256    /// Returns the limit price for this order.
257    ///
258    /// # Panics
259    ///
260    /// Panics if the MarketToLimit order price is not set.
261    #[must_use]
262    pub fn limit_px(&self) -> Price {
263        match self {
264            Self::Limit(order) => order.price,
265            Self::MarketToLimit(order) => order.price.expect("MarketToLimit order price not set"),
266            Self::StopLimit(order) => order.price,
267            Self::TrailingStopLimit(order) => order.price,
268            Self::MarketOrderWithProtection(order) => {
269                order.protection_price.expect("No price for order")
270            }
271        }
272    }
273}
274
275impl PartialEq for LimitOrderAny {
276    fn eq(&self, rhs: &Self) -> bool {
277        match self {
278            Self::Limit(order) => order.client_order_id == rhs.client_order_id(),
279            Self::MarketToLimit(order) => order.client_order_id == rhs.client_order_id(),
280            Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
281            Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
282            Self::MarketOrderWithProtection(order) => {
283                order.client_order_id == rhs.client_order_id()
284            }
285        }
286    }
287}
288
289#[derive(Clone, Debug)]
290#[enum_dispatch(Order)]
291pub enum StopOrderAny {
292    LimitIfTouched(LimitIfTouchedOrder),
293    MarketIfTouched(MarketIfTouchedOrder),
294    StopLimit(StopLimitOrder),
295    StopMarket(StopMarketOrder),
296    TrailingStopLimit(TrailingStopLimitOrder),
297    TrailingStopMarket(TrailingStopMarketOrder),
298}
299
300impl StopOrderAny {
301    #[must_use]
302    pub fn stop_px(&self) -> Price {
303        match self {
304            Self::LimitIfTouched(o) => o.trigger_price,
305            Self::MarketIfTouched(o) => o.trigger_price,
306            Self::StopLimit(o) => o.trigger_price,
307            Self::StopMarket(o) => o.trigger_price,
308            Self::TrailingStopLimit(o) => o.activation_price.unwrap_or(o.trigger_price),
309            Self::TrailingStopMarket(o) => o.activation_price.unwrap_or(o.trigger_price),
310        }
311    }
312}
313
314// TODO: Derive equality
315impl PartialEq for StopOrderAny {
316    fn eq(&self, rhs: &Self) -> bool {
317        match self {
318            Self::LimitIfTouched(order) => order.client_order_id == rhs.client_order_id(),
319            Self::StopLimit(order) => order.client_order_id == rhs.client_order_id(),
320            Self::StopMarket(order) => order.client_order_id == rhs.client_order_id(),
321            Self::MarketIfTouched(order) => order.client_order_id == rhs.client_order_id(),
322            Self::TrailingStopLimit(order) => order.client_order_id == rhs.client_order_id(),
323            Self::TrailingStopMarket(order) => order.client_order_id == rhs.client_order_id(),
324        }
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use rstest::rstest;
331    use rust_decimal::Decimal;
332    use rust_decimal_macros::dec;
333
334    use super::*;
335    use crate::{
336        enums::{OrderType, TrailingOffsetType},
337        events::{OrderEventAny, OrderUpdated, order::initialized::OrderInitializedBuilder},
338        identifiers::{ClientOrderId, InstrumentId, StrategyId},
339        orders::builder::OrderTestBuilder,
340        types::{Price, Quantity},
341    };
342
343    #[rstest]
344    fn test_order_any_equality() {
345        // Create two orders with different types but same client_order_id
346        let client_order_id = ClientOrderId::from("ORDER-001");
347
348        let market_order = OrderTestBuilder::new(OrderType::Market)
349            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
350            .quantity(Quantity::from(10))
351            .client_order_id(client_order_id)
352            .build();
353
354        let limit_order = OrderTestBuilder::new(OrderType::Limit)
355            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
356            .quantity(Quantity::from(10))
357            .price(Price::new(100.0, 2))
358            .client_order_id(client_order_id)
359            .build();
360
361        // They should be equal because they have the same client_order_id
362        assert_eq!(market_order, limit_order);
363    }
364
365    #[rstest]
366    fn test_order_any_conversion_from_events() {
367        // Create an OrderInitialized event
368        let init_event = OrderInitializedBuilder::default()
369            .order_type(OrderType::Market)
370            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
371            .quantity(Quantity::from(10))
372            .build()
373            .unwrap();
374
375        // Create a vector of events
376        let events = vec![OrderEventAny::Initialized(init_event.clone())];
377
378        // Create OrderAny from events
379        let order = OrderAny::from_events(events).unwrap();
380
381        // Verify the order was created properly
382        assert_eq!(order.order_type(), OrderType::Market);
383        assert_eq!(order.instrument_id(), init_event.instrument_id);
384        assert_eq!(order.quantity(), init_event.quantity);
385    }
386
387    #[rstest]
388    fn test_order_any_from_events_empty_error() {
389        let events: Vec<OrderEventAny> = vec![];
390        let result = OrderAny::from_events(events);
391
392        assert!(result.is_err());
393        assert_eq!(
394            result.unwrap_err().to_string(),
395            "No order events provided to create OrderAny"
396        );
397    }
398
399    #[rstest]
400    fn test_order_any_from_events_wrong_first_event() {
401        // Create an event that is not OrderInitialized
402        let client_order_id = ClientOrderId::from("ORDER-001");
403        let strategy_id = StrategyId::from("STRATEGY-001");
404
405        let update_event = OrderUpdated {
406            client_order_id,
407            strategy_id,
408            quantity: Quantity::from(20),
409            ..Default::default()
410        };
411
412        // Create a vector with a non-initialization event first
413        let events = vec![OrderEventAny::Updated(update_event)];
414
415        // Attempt to create order should fail
416        let result = OrderAny::from_events(events);
417        assert!(result.is_err());
418        assert_eq!(
419            result.unwrap_err().to_string(),
420            "First event must be `OrderInitialized`"
421        );
422    }
423
424    #[rstest]
425    fn test_passive_order_any_conversion() {
426        // Create a limit order
427        let limit_order = OrderTestBuilder::new(OrderType::Limit)
428            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
429            .quantity(Quantity::from(10))
430            .price(Price::new(100.0, 2))
431            .build();
432
433        // Convert to PassiveOrderAny and back
434        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
435        let order_any: OrderAny = passive_order.into();
436
437        // Verify it maintained its properties
438        assert_eq!(order_any.order_type(), OrderType::Limit);
439        assert_eq!(order_any.quantity(), Quantity::from(10));
440    }
441
442    #[rstest]
443    fn test_stop_order_any_conversion() {
444        // Create a stop market order
445        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
446            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
447            .quantity(Quantity::from(10))
448            .trigger_price(Price::new(100.0, 2))
449            .build();
450
451        // Convert to StopOrderAny and back
452        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
453        let order_any: OrderAny = stop_order_any.into();
454
455        // Verify it maintained its properties
456        assert_eq!(order_any.order_type(), OrderType::StopMarket);
457        assert_eq!(order_any.quantity(), Quantity::from(10));
458        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
459    }
460
461    #[rstest]
462    fn test_limit_order_any_conversion() {
463        // Create a limit order
464        let limit_order = OrderTestBuilder::new(OrderType::Limit)
465            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
466            .quantity(Quantity::from(10))
467            .price(Price::new(100.0, 2))
468            .build();
469
470        // Convert to LimitOrderAny and back
471        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
472        let order_any: OrderAny = limit_order_any.into();
473
474        // Verify it maintained its properties
475        assert_eq!(order_any.order_type(), OrderType::Limit);
476        assert_eq!(order_any.quantity(), Quantity::from(10));
477    }
478
479    #[rstest]
480    fn test_limit_order_any_limit_price() {
481        // Create a limit order
482        let limit_order = OrderTestBuilder::new(OrderType::Limit)
483            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
484            .quantity(Quantity::from(10))
485            .price(Price::new(100.0, 2))
486            .build();
487
488        // Convert to LimitOrderAny
489        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
490
491        // Check limit price accessor
492        let limit_px = limit_order_any.limit_px();
493        assert_eq!(limit_px, Price::new(100.0, 2));
494    }
495
496    #[rstest]
497    fn test_stop_order_any_stop_price() {
498        // Create a stop market order
499        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
500            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
501            .quantity(Quantity::from(10))
502            .trigger_price(Price::new(100.0, 2))
503            .build();
504
505        // Convert to StopOrderAny
506        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
507
508        // Check stop price accessor
509        let stop_px = stop_order_any.stop_px();
510        assert_eq!(stop_px, Price::new(100.0, 2));
511    }
512
513    #[rstest]
514    fn test_trailing_stop_market_order_conversion() {
515        // Create a trailing stop market order
516        let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
517            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
518            .quantity(Quantity::from(10))
519            .trigger_price(Price::new(100.0, 2))
520            .trailing_offset(Decimal::new(5, 1)) // 0.5
521            .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
522            .build();
523
524        // Convert to StopOrderAny
525        let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
526
527        // And back to OrderAny
528        let order_any: OrderAny = stop_order_any.into();
529
530        // Verify properties are preserved
531        assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
532        assert_eq!(order_any.quantity(), Quantity::from(10));
533        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
534        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
535        assert_eq!(
536            order_any.trailing_offset_type(),
537            Some(TrailingOffsetType::NoTrailingOffset)
538        );
539    }
540
541    #[rstest]
542    fn test_trailing_stop_limit_order_conversion() {
543        // Create a trailing stop limit order
544        let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
545            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
546            .quantity(Quantity::from(10))
547            .price(Price::new(99.0, 2))
548            .trigger_price(Price::new(100.0, 2))
549            .limit_offset(Decimal::new(10, 1)) // 1.0
550            .trailing_offset(Decimal::new(5, 1)) // 0.5
551            .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
552            .build();
553
554        // Convert to LimitOrderAny
555        let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
556
557        // Check limit price
558        assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
559
560        // Convert back to OrderAny
561        let order_any: OrderAny = limit_order_any.into();
562
563        // Verify properties are preserved
564        assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
565        assert_eq!(order_any.quantity(), Quantity::from(10));
566        assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
567        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
568        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
569    }
570
571    #[rstest]
572    fn test_passive_order_any_to_any() {
573        // Create a limit order
574        let limit_order = OrderTestBuilder::new(OrderType::Limit)
575            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
576            .quantity(Quantity::from(10))
577            .price(Price::new(100.0, 2))
578            .build();
579
580        // Convert to PassiveOrderAny
581        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
582
583        // Use to_any method
584        let order_any = passive_order.to_any();
585
586        // Verify it maintained its properties
587        assert_eq!(order_any.order_type(), OrderType::Limit);
588        assert_eq!(order_any.quantity(), Quantity::from(10));
589        assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
590    }
591}