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