nautilus_model/orders/
any.rs

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