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    /// # 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    // 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::{OrderType, TrailingOffsetType},
354        events::{OrderEventAny, OrderUpdated, order::initialized::OrderInitializedBuilder},
355        identifiers::{ClientOrderId, InstrumentId, StrategyId},
356        orders::builder::OrderTestBuilder,
357        types::{Price, Quantity},
358    };
359
360    #[rstest]
361    fn test_order_any_equality() {
362        // Create two orders with different types but same client_order_id
363        let client_order_id = ClientOrderId::from("ORDER-001");
364
365        let market_order = OrderTestBuilder::new(OrderType::Market)
366            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
367            .quantity(Quantity::from(10))
368            .client_order_id(client_order_id)
369            .build();
370
371        let limit_order = OrderTestBuilder::new(OrderType::Limit)
372            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
373            .quantity(Quantity::from(10))
374            .price(Price::new(100.0, 2))
375            .client_order_id(client_order_id)
376            .build();
377
378        // They should be equal because they have the same client_order_id
379        assert_eq!(market_order, limit_order);
380    }
381
382    #[rstest]
383    fn test_order_any_conversion_from_events() {
384        // Create an OrderInitialized event
385        let init_event = OrderInitializedBuilder::default()
386            .order_type(OrderType::Market)
387            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
388            .quantity(Quantity::from(10))
389            .build()
390            .unwrap();
391
392        // Create a vector of events
393        let events = vec![OrderEventAny::Initialized(init_event.clone())];
394
395        // Create OrderAny from events
396        let order = OrderAny::from_events(events).unwrap();
397
398        // Verify the order was created properly
399        assert_eq!(order.order_type(), OrderType::Market);
400        assert_eq!(order.instrument_id(), init_event.instrument_id);
401        assert_eq!(order.quantity(), init_event.quantity);
402    }
403
404    #[rstest]
405    fn test_order_any_from_events_empty_error() {
406        let events: Vec<OrderEventAny> = vec![];
407        let result = OrderAny::from_events(events);
408
409        assert!(result.is_err());
410        assert_eq!(
411            result.unwrap_err().to_string(),
412            "No order events provided to create OrderAny"
413        );
414    }
415
416    #[rstest]
417    fn test_order_any_from_events_wrong_first_event() {
418        // Create an event that is not OrderInitialized
419        let client_order_id = ClientOrderId::from("ORDER-001");
420        let strategy_id = StrategyId::from("STRATEGY-001");
421
422        let update_event = OrderUpdated {
423            client_order_id,
424            strategy_id,
425            quantity: Quantity::from(20),
426            ..Default::default()
427        };
428
429        // Create a vector with a non-initialization event first
430        let events = vec![OrderEventAny::Updated(update_event)];
431
432        // Attempt to create order should fail
433        let result = OrderAny::from_events(events);
434        assert!(result.is_err());
435        assert_eq!(
436            result.unwrap_err().to_string(),
437            "First event must be `OrderInitialized`"
438        );
439    }
440
441    #[rstest]
442    fn test_passive_order_any_conversion() {
443        // Create a limit order
444        let limit_order = OrderTestBuilder::new(OrderType::Limit)
445            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
446            .quantity(Quantity::from(10))
447            .price(Price::new(100.0, 2))
448            .build();
449
450        // Convert to PassiveOrderAny and back
451        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
452        let order_any: OrderAny = passive_order.into();
453
454        // Verify it maintained its properties
455        assert_eq!(order_any.order_type(), OrderType::Limit);
456        assert_eq!(order_any.quantity(), Quantity::from(10));
457    }
458
459    #[rstest]
460    fn test_stop_order_any_conversion() {
461        // Create a stop market order
462        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
463            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
464            .quantity(Quantity::from(10))
465            .trigger_price(Price::new(100.0, 2))
466            .build();
467
468        // Convert to StopOrderAny and back
469        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
470        let order_any: OrderAny = stop_order_any.into();
471
472        // Verify it maintained its properties
473        assert_eq!(order_any.order_type(), OrderType::StopMarket);
474        assert_eq!(order_any.quantity(), Quantity::from(10));
475        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
476    }
477
478    #[rstest]
479    fn test_limit_order_any_conversion() {
480        // Create a limit order
481        let limit_order = OrderTestBuilder::new(OrderType::Limit)
482            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
483            .quantity(Quantity::from(10))
484            .price(Price::new(100.0, 2))
485            .build();
486
487        // Convert to LimitOrderAny and back
488        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
489        let order_any: OrderAny = limit_order_any.into();
490
491        // Verify it maintained its properties
492        assert_eq!(order_any.order_type(), OrderType::Limit);
493        assert_eq!(order_any.quantity(), Quantity::from(10));
494    }
495
496    #[rstest]
497    fn test_limit_order_any_limit_price() {
498        // Create a limit order
499        let limit_order = OrderTestBuilder::new(OrderType::Limit)
500            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
501            .quantity(Quantity::from(10))
502            .price(Price::new(100.0, 2))
503            .build();
504
505        // Convert to LimitOrderAny
506        let limit_order_any = LimitOrderAny::try_from(limit_order).unwrap();
507
508        // Check limit price accessor
509        let limit_px = limit_order_any.limit_px();
510        assert_eq!(limit_px, Price::new(100.0, 2));
511    }
512
513    #[rstest]
514    fn test_stop_order_any_stop_price() {
515        // Create a stop market order
516        let stop_order = OrderTestBuilder::new(OrderType::StopMarket)
517            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
518            .quantity(Quantity::from(10))
519            .trigger_price(Price::new(100.0, 2))
520            .build();
521
522        // Convert to StopOrderAny
523        let stop_order_any = StopOrderAny::try_from(stop_order).unwrap();
524
525        // Check stop price accessor
526        let stop_px = stop_order_any.stop_px();
527        assert_eq!(stop_px, Price::new(100.0, 2));
528    }
529
530    #[rstest]
531    fn test_trailing_stop_market_order_conversion() {
532        // Create a trailing stop market order
533        let trailing_stop_order = OrderTestBuilder::new(OrderType::TrailingStopMarket)
534            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
535            .quantity(Quantity::from(10))
536            .trigger_price(Price::new(100.0, 2))
537            .trailing_offset(Decimal::new(5, 1)) // 0.5
538            .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
539            .build();
540
541        // Convert to StopOrderAny
542        let stop_order_any = StopOrderAny::try_from(trailing_stop_order).unwrap();
543
544        // And back to OrderAny
545        let order_any: OrderAny = stop_order_any.into();
546
547        // Verify properties are preserved
548        assert_eq!(order_any.order_type(), OrderType::TrailingStopMarket);
549        assert_eq!(order_any.quantity(), Quantity::from(10));
550        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
551        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
552        assert_eq!(
553            order_any.trailing_offset_type(),
554            Some(TrailingOffsetType::NoTrailingOffset)
555        );
556    }
557
558    #[rstest]
559    fn test_trailing_stop_limit_order_conversion() {
560        // Create a trailing stop limit order
561        let trailing_stop_limit = OrderTestBuilder::new(OrderType::TrailingStopLimit)
562            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
563            .quantity(Quantity::from(10))
564            .price(Price::new(99.0, 2))
565            .trigger_price(Price::new(100.0, 2))
566            .limit_offset(Decimal::new(10, 1)) // 1.0
567            .trailing_offset(Decimal::new(5, 1)) // 0.5
568            .trailing_offset_type(TrailingOffsetType::NoTrailingOffset)
569            .build();
570
571        // Convert to LimitOrderAny
572        let limit_order_any = LimitOrderAny::try_from(trailing_stop_limit).unwrap();
573
574        // Check limit price
575        assert_eq!(limit_order_any.limit_px(), Price::new(99.0, 2));
576
577        // Convert back to OrderAny
578        let order_any: OrderAny = limit_order_any.into();
579
580        // Verify properties are preserved
581        assert_eq!(order_any.order_type(), OrderType::TrailingStopLimit);
582        assert_eq!(order_any.quantity(), Quantity::from(10));
583        assert_eq!(order_any.price(), Some(Price::new(99.0, 2)));
584        assert_eq!(order_any.trigger_price(), Some(Price::new(100.0, 2)));
585        assert_eq!(order_any.trailing_offset(), Some(dec!(0.5)));
586    }
587
588    #[rstest]
589    fn test_passive_order_any_to_any() {
590        // Create a limit order
591        let limit_order = OrderTestBuilder::new(OrderType::Limit)
592            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
593            .quantity(Quantity::from(10))
594            .price(Price::new(100.0, 2))
595            .build();
596
597        // Convert to PassiveOrderAny
598        let passive_order = PassiveOrderAny::try_from(limit_order).unwrap();
599
600        // Use to_any method
601        let order_any = passive_order.to_any();
602
603        // Verify it maintained its properties
604        assert_eq!(order_any.order_type(), OrderType::Limit);
605        assert_eq!(order_any.quantity(), Quantity::from(10));
606        assert_eq!(order_any.price(), Some(Price::new(100.0, 2)));
607    }
608}