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