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