nautilus_model/orders/
market_to_limit.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::{
17    fmt::Display,
18    ops::{Deref, DerefMut},
19};
20
21use indexmap::IndexMap;
22use nautilus_core::{UUID4, UnixNanos, correctness::FAILED};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{Order, OrderAny, OrderCore, check_display_qty, check_time_in_force};
28use crate::{
29    enums::{
30        ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
31        TimeInForce, TrailingOffsetType, TriggerType,
32    },
33    events::{OrderEventAny, OrderInitialized, OrderUpdated},
34    identifiers::{
35        AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
36        StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
37    },
38    orders::OrderError,
39    types::{Currency, Money, Price, Quantity, quantity::check_positive_quantity},
40};
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
43#[cfg_attr(
44    feature = "python",
45    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
46)]
47pub struct MarketToLimitOrder {
48    core: OrderCore,
49    pub price: Option<Price>,
50    pub expire_time: Option<UnixNanos>,
51    pub is_post_only: bool,
52    pub display_qty: Option<Quantity>,
53}
54
55impl MarketToLimitOrder {
56    /// Creates a new [`MarketToLimitOrder`] instance.
57    ///
58    /// # Errors
59    ///
60    /// Returns an error if:
61    /// - The `quantity` is not positive.
62    /// - The `display_qty` (when provided) exceeds `quantity`.
63    /// - The `time_in_force` is `GTD` **and** `expire_time` is `None` or zero.
64    #[allow(clippy::too_many_arguments)]
65    pub fn new_checked(
66        trader_id: TraderId,
67        strategy_id: StrategyId,
68        instrument_id: InstrumentId,
69        client_order_id: ClientOrderId,
70        order_side: OrderSide,
71        quantity: Quantity,
72        time_in_force: TimeInForce,
73        expire_time: Option<UnixNanos>,
74        post_only: bool,
75        reduce_only: bool,
76        quote_quantity: bool,
77        display_qty: Option<Quantity>,
78        contingency_type: Option<ContingencyType>,
79        order_list_id: Option<OrderListId>,
80        linked_order_ids: Option<Vec<ClientOrderId>>,
81        parent_order_id: Option<ClientOrderId>,
82        exec_algorithm_id: Option<ExecAlgorithmId>,
83        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
84        exec_spawn_id: Option<ClientOrderId>,
85        tags: Option<Vec<Ustr>>,
86        init_id: UUID4,
87        ts_init: UnixNanos,
88    ) -> anyhow::Result<Self> {
89        check_positive_quantity(quantity, stringify!(quantity))?;
90        check_display_qty(display_qty, quantity)?;
91        check_time_in_force(time_in_force, expire_time)?;
92
93        let init_order = OrderInitialized::new(
94            trader_id,
95            strategy_id,
96            instrument_id,
97            client_order_id,
98            order_side,
99            OrderType::MarketToLimit,
100            quantity,
101            time_in_force,
102            post_only,
103            reduce_only,
104            quote_quantity,
105            false,
106            init_id,
107            ts_init,
108            ts_init,
109            None,
110            None,
111            None,
112            None,
113            None,
114            None,
115            expire_time,
116            display_qty,
117            Some(TriggerType::NoTrigger),
118            None,
119            contingency_type,
120            order_list_id,
121            linked_order_ids,
122            parent_order_id,
123            exec_algorithm_id,
124            exec_algorithm_params,
125            exec_spawn_id,
126            tags,
127        );
128
129        Ok(Self {
130            core: OrderCore::new(init_order),
131            price: None, // Price will be determined on fill
132            expire_time,
133            is_post_only: post_only,
134            display_qty,
135        })
136    }
137
138    /// Creates a new [`MarketToLimitOrder`] instance.
139    ///
140    /// # Panics
141    ///
142    /// Panics if any order validation fails (see [`MarketToLimitOrder::new_checked`]).
143    #[allow(clippy::too_many_arguments)]
144    pub fn new(
145        trader_id: TraderId,
146        strategy_id: StrategyId,
147        instrument_id: InstrumentId,
148        client_order_id: ClientOrderId,
149        order_side: OrderSide,
150        quantity: Quantity,
151        time_in_force: TimeInForce,
152        expire_time: Option<UnixNanos>,
153        post_only: bool,
154        reduce_only: bool,
155        quote_quantity: bool,
156        display_qty: Option<Quantity>,
157        contingency_type: Option<ContingencyType>,
158        order_list_id: Option<OrderListId>,
159        linked_order_ids: Option<Vec<ClientOrderId>>,
160        parent_order_id: Option<ClientOrderId>,
161        exec_algorithm_id: Option<ExecAlgorithmId>,
162        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
163        exec_spawn_id: Option<ClientOrderId>,
164        tags: Option<Vec<Ustr>>,
165        init_id: UUID4,
166        ts_init: UnixNanos,
167    ) -> Self {
168        Self::new_checked(
169            trader_id,
170            strategy_id,
171            instrument_id,
172            client_order_id,
173            order_side,
174            quantity,
175            time_in_force,
176            expire_time,
177            post_only,
178            reduce_only,
179            quote_quantity,
180            display_qty,
181            contingency_type,
182            order_list_id,
183            linked_order_ids,
184            parent_order_id,
185            exec_algorithm_id,
186            exec_algorithm_params,
187            exec_spawn_id,
188            tags,
189            init_id,
190            ts_init,
191        )
192        .expect(FAILED)
193    }
194}
195
196impl Deref for MarketToLimitOrder {
197    type Target = OrderCore;
198
199    fn deref(&self) -> &Self::Target {
200        &self.core
201    }
202}
203
204impl DerefMut for MarketToLimitOrder {
205    fn deref_mut(&mut self) -> &mut Self::Target {
206        &mut self.core
207    }
208}
209
210impl Order for MarketToLimitOrder {
211    fn into_any(self) -> OrderAny {
212        OrderAny::MarketToLimit(self)
213    }
214
215    fn status(&self) -> OrderStatus {
216        self.status
217    }
218
219    fn trader_id(&self) -> TraderId {
220        self.trader_id
221    }
222
223    fn strategy_id(&self) -> StrategyId {
224        self.strategy_id
225    }
226
227    fn instrument_id(&self) -> InstrumentId {
228        self.instrument_id
229    }
230
231    fn symbol(&self) -> Symbol {
232        self.instrument_id.symbol
233    }
234
235    fn venue(&self) -> Venue {
236        self.instrument_id.venue
237    }
238
239    fn client_order_id(&self) -> ClientOrderId {
240        self.client_order_id
241    }
242
243    fn venue_order_id(&self) -> Option<VenueOrderId> {
244        self.venue_order_id
245    }
246
247    fn position_id(&self) -> Option<PositionId> {
248        self.position_id
249    }
250
251    fn account_id(&self) -> Option<AccountId> {
252        self.account_id
253    }
254
255    fn last_trade_id(&self) -> Option<TradeId> {
256        self.last_trade_id
257    }
258
259    fn order_side(&self) -> OrderSide {
260        self.side
261    }
262
263    fn order_type(&self) -> OrderType {
264        self.order_type
265    }
266
267    fn quantity(&self) -> Quantity {
268        self.quantity
269    }
270
271    fn time_in_force(&self) -> TimeInForce {
272        self.time_in_force
273    }
274
275    fn expire_time(&self) -> Option<UnixNanos> {
276        self.expire_time
277    }
278
279    fn price(&self) -> Option<Price> {
280        self.price
281    }
282
283    fn trigger_price(&self) -> Option<Price> {
284        None
285    }
286
287    fn trigger_type(&self) -> Option<TriggerType> {
288        None
289    }
290
291    fn liquidity_side(&self) -> Option<LiquiditySide> {
292        self.liquidity_side
293    }
294
295    fn is_post_only(&self) -> bool {
296        self.is_post_only
297    }
298
299    fn is_reduce_only(&self) -> bool {
300        self.is_reduce_only
301    }
302
303    fn is_quote_quantity(&self) -> bool {
304        self.is_quote_quantity
305    }
306
307    fn has_price(&self) -> bool {
308        self.price.is_some()
309    }
310
311    fn display_qty(&self) -> Option<Quantity> {
312        self.display_qty
313    }
314
315    fn limit_offset(&self) -> Option<Decimal> {
316        None
317    }
318
319    fn trailing_offset(&self) -> Option<Decimal> {
320        None
321    }
322
323    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
324        None
325    }
326
327    fn emulation_trigger(&self) -> Option<TriggerType> {
328        None
329    }
330
331    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
332        None
333    }
334
335    fn contingency_type(&self) -> Option<ContingencyType> {
336        self.contingency_type
337    }
338
339    fn order_list_id(&self) -> Option<OrderListId> {
340        self.order_list_id
341    }
342
343    fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
344        self.linked_order_ids.as_deref()
345    }
346
347    fn parent_order_id(&self) -> Option<ClientOrderId> {
348        self.parent_order_id
349    }
350
351    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
352        self.exec_algorithm_id
353    }
354
355    fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
356        self.exec_algorithm_params.as_ref()
357    }
358
359    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
360        self.exec_spawn_id
361    }
362
363    fn tags(&self) -> Option<&[Ustr]> {
364        self.tags.as_deref()
365    }
366
367    fn filled_qty(&self) -> Quantity {
368        self.filled_qty
369    }
370
371    fn leaves_qty(&self) -> Quantity {
372        self.leaves_qty
373    }
374
375    fn avg_px(&self) -> Option<f64> {
376        self.avg_px
377    }
378
379    fn slippage(&self) -> Option<f64> {
380        self.slippage
381    }
382
383    fn init_id(&self) -> UUID4 {
384        self.init_id
385    }
386
387    fn ts_init(&self) -> UnixNanos {
388        self.ts_init
389    }
390
391    fn ts_submitted(&self) -> Option<UnixNanos> {
392        self.ts_submitted
393    }
394
395    fn ts_accepted(&self) -> Option<UnixNanos> {
396        self.ts_accepted
397    }
398
399    fn ts_closed(&self) -> Option<UnixNanos> {
400        self.ts_closed
401    }
402
403    fn ts_last(&self) -> UnixNanos {
404        self.ts_last
405    }
406
407    fn events(&self) -> Vec<&OrderEventAny> {
408        self.events.iter().collect()
409    }
410
411    fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
412        self.venue_order_ids.iter().collect()
413    }
414
415    fn trade_ids(&self) -> Vec<&TradeId> {
416        self.trade_ids.iter().collect()
417    }
418
419    fn commissions(&self) -> &IndexMap<Currency, Money> {
420        &self.commissions
421    }
422
423    fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
424        if let OrderEventAny::Updated(ref event) = event {
425            self.update(event);
426        };
427        let is_order_filled = matches!(event, OrderEventAny::Filled(_));
428
429        self.core.apply(event)?;
430
431        if is_order_filled && self.price.is_some() {
432            self.core.set_slippage(self.price.unwrap());
433        };
434
435        Ok(())
436    }
437
438    fn update(&mut self, event: &OrderUpdated) {
439        assert!(
440            event.trigger_price.is_none(),
441            "{}",
442            OrderError::InvalidOrderEvent
443        );
444
445        if let Some(price) = event.price {
446            self.price = Some(price);
447        }
448
449        self.quantity = event.quantity;
450        self.leaves_qty = self.quantity.saturating_sub(self.filled_qty);
451    }
452
453    fn is_triggered(&self) -> Option<bool> {
454        None
455    }
456
457    fn set_position_id(&mut self, position_id: Option<PositionId>) {
458        self.position_id = position_id;
459    }
460
461    fn set_quantity(&mut self, quantity: Quantity) {
462        self.quantity = quantity;
463    }
464
465    fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
466        self.leaves_qty = leaves_qty;
467    }
468
469    fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
470        self.emulation_trigger = emulation_trigger;
471    }
472
473    fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
474        self.is_quote_quantity = is_quote_quantity;
475    }
476
477    fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
478        self.liquidity_side = Some(liquidity_side);
479    }
480
481    fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
482        self.core.would_reduce_only(side, position_qty)
483    }
484
485    fn previous_status(&self) -> Option<OrderStatus> {
486        self.core.previous_status
487    }
488}
489
490impl Display for MarketToLimitOrder {
491    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
492        write!(
493            f,
494            "MarketToLimitOrder(\
495            {} {} {} {} {}, \
496            status={}, \
497            client_order_id={}, \
498            venue_order_id={}, \
499            position_id={}, \
500            exec_algorithm_id={}, \
501            exec_spawn_id={}, \
502            tags={:?}\
503            )",
504            self.side,
505            self.quantity.to_formatted_string(),
506            self.instrument_id,
507            self.order_type,
508            self.time_in_force,
509            self.status,
510            self.client_order_id,
511            self.venue_order_id.map_or_else(
512                || "None".to_string(),
513                |venue_order_id| format!("{venue_order_id}")
514            ),
515            self.position_id.map_or_else(
516                || "None".to_string(),
517                |position_id| format!("{position_id}")
518            ),
519            self.exec_algorithm_id
520                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
521            self.exec_spawn_id
522                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
523            self.tags
524        )
525    }
526}
527
528impl From<OrderInitialized> for MarketToLimitOrder {
529    fn from(event: OrderInitialized) -> Self {
530        Self::new(
531            event.trader_id,
532            event.strategy_id,
533            event.instrument_id,
534            event.client_order_id,
535            event.order_side,
536            event.quantity,
537            event.time_in_force,
538            event.expire_time,
539            event.post_only,
540            event.reduce_only,
541            event.quote_quantity,
542            event.display_qty,
543            event.contingency_type,
544            event.order_list_id,
545            event.linked_order_ids,
546            event.parent_order_id,
547            event.exec_algorithm_id,
548            event.exec_algorithm_params,
549            event.exec_spawn_id,
550            event.tags,
551            event.event_id,
552            event.ts_event,
553        )
554    }
555}
556
557////////////////////////////////////////////////////////////////////////////////
558// Tests
559////////////////////////////////////////////////////////////////////////////////
560#[cfg(test)]
561mod tests {
562    use rstest::rstest;
563
564    use super::*;
565    use crate::{
566        enums::{OrderSide, OrderType, TimeInForce},
567        events::order::{filled::OrderFilledBuilder, initialized::OrderInitializedBuilder},
568        identifiers::{InstrumentId, TradeId, VenueOrderId},
569        instruments::{CurrencyPair, stubs::*},
570        orders::{builder::OrderTestBuilder, stubs::TestOrderStubs},
571        types::{Price, Quantity},
572    };
573
574    #[rstest]
575    fn test_initialize(_audusd_sim: CurrencyPair) {
576        let order = OrderTestBuilder::new(OrderType::MarketToLimit)
577            .instrument_id(_audusd_sim.id)
578            .side(OrderSide::Buy)
579            .price(Price::from("0.68000"))
580            .quantity(Quantity::from(1))
581            .build();
582
583        assert_eq!(order.price(), None);
584        assert_eq!(order.is_triggered(), None);
585        assert_eq!(order.time_in_force(), TimeInForce::Gtc);
586        assert_eq!(order.is_triggered(), None);
587        assert_eq!(order.filled_qty(), Quantity::from(0));
588        assert_eq!(order.leaves_qty(), Quantity::from(1));
589        assert_eq!(order.trigger_instrument_id(), None);
590        assert_eq!(order.order_list_id(), None);
591    }
592
593    #[rstest]
594    fn test_display(audusd_sim: CurrencyPair) {
595        let order = OrderTestBuilder::new(OrderType::MarketToLimit)
596            .instrument_id(audusd_sim.id)
597            .side(OrderSide::Buy)
598            .quantity(Quantity::from(1))
599            .build();
600
601        assert_eq!(
602            order.to_string(),
603            "MarketToLimitOrder(BUY 1 AUD/USD.SIM MARKET_TO_LIMIT GTC, status=INITIALIZED, client_order_id=O-19700101-000000-001-001-1, venue_order_id=None, position_id=None, exec_algorithm_id=None, exec_spawn_id=None, tags=None)"
604        );
605    }
606
607    #[rstest]
608    #[should_panic(
609        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
610    )]
611    fn test_quantity_zero(audusd_sim: CurrencyPair) {
612        let _ = OrderTestBuilder::new(OrderType::MarketToLimit)
613            .instrument_id(audusd_sim.id)
614            .side(OrderSide::Buy)
615            .quantity(Quantity::from(0))
616            .build();
617    }
618
619    #[rstest]
620    #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
621    fn test_gtd_without_expire(audusd_sim: CurrencyPair) {
622        let _ = OrderTestBuilder::new(OrderType::MarketToLimit)
623            .instrument_id(audusd_sim.id)
624            .side(OrderSide::Buy)
625            .quantity(Quantity::from(1))
626            .time_in_force(TimeInForce::Gtd) // Missing expire_time
627            .build();
628    }
629
630    #[rstest]
631    #[should_panic(expected = "`display_qty` may not exceed `quantity`")]
632    fn test_display_qty_gt_quantity(audusd_sim: CurrencyPair) {
633        let _ = OrderTestBuilder::new(OrderType::MarketToLimit)
634            .instrument_id(audusd_sim.id)
635            .side(OrderSide::Buy)
636            .quantity(Quantity::from(1))
637            .display_qty(Quantity::from(2)) // Invalid: display > quantity
638            .build();
639    }
640
641    #[rstest]
642    fn test_market_to_limit_order_update() {
643        // Create and accept a basic MarketToLimitOrder
644        let order = OrderTestBuilder::new(OrderType::MarketToLimit)
645            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
646            .quantity(Quantity::from(10))
647            .build();
648
649        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
650
651        // Update with new values
652        let updated_price = Price::new(95.0, 2);
653        let updated_quantity = Quantity::from(5);
654
655        let event = OrderUpdated {
656            client_order_id: accepted_order.client_order_id(),
657            strategy_id: accepted_order.strategy_id(),
658            price: Some(updated_price),
659            quantity: updated_quantity,
660            ..Default::default()
661        };
662
663        accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
664
665        // Verify updates were applied correctly
666        assert_eq!(accepted_order.quantity(), updated_quantity);
667        assert_eq!(accepted_order.price(), Some(updated_price));
668    }
669
670    #[rstest]
671    fn test_market_to_limit_order_expire_time() {
672        // Create a new MarketToLimitOrder with an expire time
673        let expire_time = UnixNanos::from(1234567890);
674        let order = OrderTestBuilder::new(OrderType::MarketToLimit)
675            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
676            .quantity(Quantity::from(10))
677            .expire_time(expire_time)
678            .build();
679
680        // Assert that the expire time is set correctly
681        assert_eq!(order.expire_time(), Some(expire_time));
682    }
683
684    #[rstest]
685    fn test_market_to_limit_order_from_order_initialized() {
686        // Create an OrderInitialized event with all required fields for a MarketToLimitOrder
687        let order_initialized = OrderInitializedBuilder::default()
688            .order_type(OrderType::MarketToLimit)
689            .build()
690            .unwrap();
691
692        // Convert the OrderInitialized event into a MarketToLimitOrder
693        let order: MarketToLimitOrder = order_initialized.clone().into();
694
695        // Assert essential fields match the OrderInitialized fields
696        assert_eq!(order.trader_id(), order_initialized.trader_id);
697        assert_eq!(order.strategy_id(), order_initialized.strategy_id);
698        assert_eq!(order.instrument_id(), order_initialized.instrument_id);
699        assert_eq!(order.client_order_id(), order_initialized.client_order_id);
700        assert_eq!(order.order_side(), order_initialized.order_side);
701        assert_eq!(order.quantity(), order_initialized.quantity);
702    }
703
704    #[rstest]
705    fn test_market_to_limit_order_sets_slippage_when_filled() {
706        // Create a MarketToLimitOrder
707        let order = OrderTestBuilder::new(OrderType::MarketToLimit)
708            .instrument_id(InstrumentId::from("BTC-USDT.BINANCE"))
709            .quantity(Quantity::from(10))
710            .side(OrderSide::Buy)
711            .build();
712
713        // Accept the order first
714        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
715
716        // Update the order with a price
717        let price = Price::new(90.0, 2);
718        let update_event = OrderUpdated {
719            client_order_id: accepted_order.client_order_id(),
720            strategy_id: accepted_order.strategy_id(),
721            price: Some(price),
722            quantity: accepted_order.quantity(),
723            ..Default::default()
724        };
725
726        // Apply the update event to set the price
727        accepted_order
728            .apply(OrderEventAny::Updated(update_event))
729            .unwrap();
730
731        // Verify the price was set correctly
732        assert_eq!(accepted_order.price(), Some(price));
733
734        // Create a filled event with the correct quantity
735        let fill_quantity = accepted_order.quantity();
736        let fill_price = Price::new(98.50, 2);
737
738        let order_filled_event = OrderFilledBuilder::default()
739            .client_order_id(accepted_order.client_order_id())
740            .strategy_id(accepted_order.strategy_id())
741            .instrument_id(accepted_order.instrument_id())
742            .order_side(accepted_order.order_side())
743            .last_qty(fill_quantity)
744            .last_px(fill_price)
745            .venue_order_id(VenueOrderId::from("TEST-001"))
746            .trade_id(TradeId::from("TRADE-001"))
747            .build()
748            .unwrap();
749
750        // Apply the fill event
751        accepted_order
752            .apply(OrderEventAny::Filled(order_filled_event))
753            .unwrap();
754
755        // The slippage calculation should be triggered by the filled event
756        assert!(accepted_order.slippage().is_some());
757
758        // Additionally, verify the actual slippage value is correct
759        // The slippage would be 98.50 - 90.0 = 8.50 for a Buy order
760        assert_eq!(accepted_order.slippage().unwrap(), 8.50);
761    }
762}