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