Skip to main content

nautilus_model/orders/
market.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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::{
23    UUID4, UnixNanos,
24    correctness::{FAILED, check_predicate_false},
25};
26use rust_decimal::Decimal;
27use serde::{Deserialize, Serialize};
28use ustr::Ustr;
29
30use super::{Order, OrderAny, OrderCore};
31use crate::{
32    enums::{
33        ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
34        TimeInForce, TrailingOffsetType, TriggerType,
35    },
36    events::{OrderEventAny, OrderInitialized, OrderUpdated},
37    identifiers::{
38        AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
39        StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
40    },
41    orders::OrderError,
42    types::{Currency, Money, Price, Quantity, quantity::check_positive_quantity},
43};
44
45#[derive(Clone, Debug, Serialize, Deserialize)]
46#[cfg_attr(
47    feature = "python",
48    pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
49)]
50#[cfg_attr(
51    feature = "python",
52    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
53)]
54pub struct MarketOrder {
55    core: OrderCore,
56    pub protection_price: Option<Price>,
57}
58
59impl MarketOrder {
60    /// Creates a new [`MarketOrder`] instance.
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if:
65    /// - The `quantity` is not positive.
66    /// - The `time_in_force` is GTD (invalid for market orders).
67    /// - The order metadata violates an [`OrderInitialized::new_checked`] invariant.
68    #[expect(clippy::too_many_arguments)]
69    pub fn new_checked(
70        trader_id: TraderId,
71        strategy_id: StrategyId,
72        instrument_id: InstrumentId,
73        client_order_id: ClientOrderId,
74        order_side: OrderSide,
75        quantity: Quantity,
76        time_in_force: TimeInForce,
77        init_id: UUID4,
78        ts_init: UnixNanos,
79        reduce_only: bool,
80        quote_quantity: bool,
81        contingency_type: Option<ContingencyType>,
82        order_list_id: Option<OrderListId>,
83        linked_order_ids: Option<Vec<ClientOrderId>>,
84        parent_order_id: Option<ClientOrderId>,
85        exec_algorithm_id: Option<ExecAlgorithmId>,
86        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
87        exec_spawn_id: Option<ClientOrderId>,
88        tags: Option<Vec<Ustr>>,
89    ) -> Result<Self, OrderError> {
90        check_positive_quantity(quantity, stringify!(quantity))?;
91        check_predicate_false(
92            time_in_force == TimeInForce::Gtd,
93            "GTD not supported for Market orders",
94        )?;
95
96        let init_order = OrderInitialized::new_checked(
97            trader_id,
98            strategy_id,
99            instrument_id,
100            client_order_id,
101            order_side,
102            OrderType::Market,
103            quantity,
104            time_in_force,
105            false,
106            reduce_only,
107            quote_quantity,
108            false,
109            init_id,
110            ts_init,
111            ts_init,
112            None,
113            None,
114            None,
115            None,
116            None,
117            None,
118            None,
119            None,
120            None,
121            None,
122            None,
123            contingency_type,
124            order_list_id,
125            linked_order_ids,
126            parent_order_id,
127            exec_algorithm_id,
128            exec_algorithm_params,
129            exec_spawn_id,
130            tags,
131        )?;
132
133        Ok(Self {
134            core: OrderCore::new(init_order),
135            protection_price: None,
136        })
137    }
138
139    /// Creates a new [`MarketOrder`] instance.
140    ///
141    /// # Panics
142    ///
143    /// Panics if any order validation fails (see [`MarketOrder::new_checked`]).
144    #[expect(clippy::too_many_arguments)]
145    #[must_use]
146    pub fn new(
147        trader_id: TraderId,
148        strategy_id: StrategyId,
149        instrument_id: InstrumentId,
150        client_order_id: ClientOrderId,
151        order_side: OrderSide,
152        quantity: Quantity,
153        time_in_force: TimeInForce,
154        init_id: UUID4,
155        ts_init: UnixNanos,
156        reduce_only: bool,
157        quote_quantity: bool,
158        contingency_type: Option<ContingencyType>,
159        order_list_id: Option<OrderListId>,
160        linked_order_ids: Option<Vec<ClientOrderId>>,
161        parent_order_id: Option<ClientOrderId>,
162        exec_algorithm_id: Option<ExecAlgorithmId>,
163        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
164        exec_spawn_id: Option<ClientOrderId>,
165        tags: Option<Vec<Ustr>>,
166    ) -> Self {
167        Self::new_checked(
168            trader_id,
169            strategy_id,
170            instrument_id,
171            client_order_id,
172            order_side,
173            quantity,
174            time_in_force,
175            init_id,
176            ts_init,
177            reduce_only,
178            quote_quantity,
179            contingency_type,
180            order_list_id,
181            linked_order_ids,
182            parent_order_id,
183            exec_algorithm_id,
184            exec_algorithm_params,
185            exec_spawn_id,
186            tags,
187        )
188        .unwrap_or_else(|e| panic!("{FAILED}: {e}"))
189    }
190}
191
192impl Deref for MarketOrder {
193    type Target = OrderCore;
194
195    fn deref(&self) -> &Self::Target {
196        &self.core
197    }
198}
199
200impl DerefMut for MarketOrder {
201    fn deref_mut(&mut self) -> &mut Self::Target {
202        &mut self.core
203    }
204}
205
206impl PartialEq for MarketOrder {
207    fn eq(&self, other: &Self) -> bool {
208        self.client_order_id == other.client_order_id
209    }
210}
211
212impl Order for MarketOrder {
213    fn into_any(self) -> OrderAny {
214        OrderAny::Market(self)
215    }
216
217    fn status(&self) -> OrderStatus {
218        self.status
219    }
220
221    fn trader_id(&self) -> TraderId {
222        self.trader_id
223    }
224
225    fn strategy_id(&self) -> StrategyId {
226        self.strategy_id
227    }
228
229    fn instrument_id(&self) -> InstrumentId {
230        self.instrument_id
231    }
232
233    fn symbol(&self) -> Symbol {
234        self.instrument_id.symbol
235    }
236
237    fn venue(&self) -> Venue {
238        self.instrument_id.venue
239    }
240
241    fn client_order_id(&self) -> ClientOrderId {
242        self.client_order_id
243    }
244
245    fn venue_order_id(&self) -> Option<VenueOrderId> {
246        self.venue_order_id
247    }
248
249    fn position_id(&self) -> Option<PositionId> {
250        self.position_id
251    }
252
253    fn account_id(&self) -> Option<AccountId> {
254        self.account_id
255    }
256
257    fn last_trade_id(&self) -> Option<TradeId> {
258        self.last_trade_id
259    }
260
261    fn order_side(&self) -> OrderSide {
262        self.side
263    }
264
265    fn order_type(&self) -> OrderType {
266        self.order_type
267    }
268
269    fn quantity(&self) -> Quantity {
270        self.quantity
271    }
272
273    fn time_in_force(&self) -> TimeInForce {
274        self.time_in_force
275    }
276
277    fn expire_time(&self) -> Option<UnixNanos> {
278        None
279    }
280
281    fn price(&self) -> Option<Price> {
282        self.protection_price
283    }
284
285    fn trigger_price(&self) -> Option<Price> {
286        None
287    }
288
289    fn trigger_type(&self) -> Option<TriggerType> {
290        None
291    }
292
293    fn liquidity_side(&self) -> Option<LiquiditySide> {
294        self.liquidity_side
295    }
296
297    fn is_post_only(&self) -> bool {
298        false
299    }
300
301    fn is_reduce_only(&self) -> bool {
302        self.is_reduce_only
303    }
304
305    fn is_quote_quantity(&self) -> bool {
306        self.is_quote_quantity
307    }
308
309    fn has_price(&self) -> bool {
310        self.protection_price.is_some()
311    }
312
313    fn display_qty(&self) -> Option<Quantity> {
314        None
315    }
316
317    fn limit_offset(&self) -> Option<Decimal> {
318        None
319    }
320
321    fn trailing_offset(&self) -> Option<Decimal> {
322        None
323    }
324
325    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
326        None
327    }
328
329    fn emulation_trigger(&self) -> Option<TriggerType> {
330        None
331    }
332
333    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
334        None
335    }
336
337    fn contingency_type(&self) -> Option<ContingencyType> {
338        self.contingency_type
339    }
340
341    fn order_list_id(&self) -> Option<OrderListId> {
342        self.order_list_id
343    }
344
345    fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
346        self.linked_order_ids.as_deref()
347    }
348
349    fn parent_order_id(&self) -> Option<ClientOrderId> {
350        self.parent_order_id
351    }
352
353    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
354        self.exec_algorithm_id
355    }
356
357    fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
358        self.exec_algorithm_params.as_ref()
359    }
360
361    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
362        self.exec_spawn_id
363    }
364
365    fn tags(&self) -> Option<&[Ustr]> {
366        self.tags.as_deref()
367    }
368
369    fn filled_qty(&self) -> Quantity {
370        self.filled_qty
371    }
372
373    fn voided_qty(&self) -> Quantity {
374        self.voided_qty
375    }
376
377    fn non_reopened_voided_qty(&self) -> Quantity {
378        self.non_reopened_voided_qty
379    }
380
381    fn leaves_qty(&self) -> Quantity {
382        self.leaves_qty
383    }
384
385    fn overfill_qty(&self) -> Quantity {
386        self.overfill_qty
387    }
388
389    fn avg_px(&self) -> Option<Decimal> {
390        self.avg_px
391    }
392
393    fn slippage(&self) -> Option<Decimal> {
394        self.slippage
395    }
396
397    fn init_id(&self) -> UUID4 {
398        self.init_id
399    }
400
401    fn ts_init(&self) -> UnixNanos {
402        self.ts_init
403    }
404
405    fn ts_submitted(&self) -> Option<UnixNanos> {
406        self.ts_submitted
407    }
408
409    fn ts_accepted(&self) -> Option<UnixNanos> {
410        self.ts_accepted
411    }
412
413    fn ts_closed(&self) -> Option<UnixNanos> {
414        self.ts_closed
415    }
416
417    fn ts_last(&self) -> UnixNanos {
418        self.ts_last
419    }
420
421    fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
422        self.core.apply(event.clone())?;
423
424        if let OrderEventAny::Updated(ref event) = event {
425            self.update(event);
426        }
427
428        Ok(())
429    }
430
431    fn update(&mut self, event: &OrderUpdated) {
432        assert!(event.price.is_none(), "{}", OrderError::InvalidOrderEvent);
433        assert!(
434            event.trigger_price.is_none(),
435            "{}",
436            OrderError::InvalidOrderEvent
437        );
438
439        if let Some(protection_price) = event.protection_price {
440            self.protection_price = Some(protection_price);
441        }
442        self.core.apply_updated_quantity(event.quantity);
443    }
444
445    fn events(&self) -> Vec<&OrderEventAny> {
446        self.events.iter().collect()
447    }
448
449    fn last_event(&self) -> &OrderEventAny {
450        self.events
451            .last()
452            .expect("Order invariant violated: no events")
453    }
454
455    fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
456        self.venue_order_ids.iter().collect()
457    }
458
459    fn trade_ids(&self) -> Vec<&TradeId> {
460        self.trade_ids.iter().collect()
461    }
462
463    fn commissions(&self) -> &IndexMap<Currency, Money> {
464        &self.commissions
465    }
466
467    fn is_triggered(&self) -> Option<bool> {
468        None
469    }
470
471    fn set_position_id(&mut self, position_id: Option<PositionId>) {
472        self.position_id = position_id;
473    }
474
475    fn set_quantity(&mut self, quantity: Quantity) {
476        self.quantity = quantity;
477    }
478
479    fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
480        self.leaves_qty = leaves_qty;
481    }
482
483    fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
484        self.emulation_trigger = emulation_trigger;
485    }
486
487    fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
488        self.is_quote_quantity = is_quote_quantity;
489    }
490
491    fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
492        self.liquidity_side = Some(liquidity_side);
493    }
494
495    fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
496        self.core.would_reduce_only(side, position_qty)
497    }
498
499    fn previous_status(&self) -> Option<OrderStatus> {
500        self.core.previous_status
501    }
502}
503
504impl Display for MarketOrder {
505    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
506        write!(
507            f,
508            "MarketOrder(\
509            {} {} {} @ {} {}, \
510            status={}, \
511            client_order_id={}, \
512            venue_order_id={}, \
513            position_id={}, \
514            exec_algorithm_id={}, \
515            exec_spawn_id={}, \
516            tags={:?}\
517            )",
518            self.side,
519            self.quantity.to_formatted_string(),
520            self.instrument_id,
521            self.order_type,
522            self.time_in_force,
523            self.status,
524            self.client_order_id,
525            self.venue_order_id.map_or_else(
526                || "None".to_string(),
527                |venue_order_id| format!("{venue_order_id}")
528            ),
529            self.position_id.map_or_else(
530                || "None".to_string(),
531                |position_id| format!("{position_id}")
532            ),
533            self.exec_algorithm_id
534                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
535            self.exec_spawn_id
536                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
537            self.tags
538        )
539    }
540}
541
542impl TryFrom<OrderInitialized> for MarketOrder {
543    type Error = OrderError;
544
545    fn try_from(event: OrderInitialized) -> Result<Self, Self::Error> {
546        Self::new_checked(
547            event.trader_id,
548            event.strategy_id,
549            event.instrument_id,
550            event.client_order_id,
551            event.order_side,
552            event.quantity,
553            event.time_in_force,
554            event.event_id,
555            event.ts_event,
556            event.reduce_only,
557            event.quote_quantity,
558            event.contingency_type,
559            event.order_list_id,
560            event.linked_order_ids,
561            event.parent_order_id,
562            event.exec_algorithm_id,
563            event.exec_algorithm_params,
564            event.exec_spawn_id,
565            event.tags,
566        )
567    }
568}
569
570#[cfg(test)]
571mod tests {
572    use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessError};
573    use rstest::rstest;
574
575    use crate::{
576        enums::{ContingencyType, OrderSide, OrderType, TimeInForce},
577        events::{
578            OrderEventAny, OrderInitialized, OrderUpdated, order::spec::OrderInitializedSpec,
579        },
580        identifiers::{ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId},
581        instruments::{CurrencyPair, stubs::*},
582        orders::{
583            MarketOrder, Order, OrderError, builder::OrderTestBuilder, stubs::TestOrderStubs,
584        },
585        types::{Price, Quantity},
586    };
587
588    #[rstest]
589    #[should_panic(
590        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
591    )]
592    fn test_positive_quantity_condition(audusd_sim: CurrencyPair) {
593        let _ = OrderTestBuilder::new(OrderType::Market)
594            .instrument_id(audusd_sim.id)
595            .side(OrderSide::Buy)
596            .quantity(Quantity::from(0))
597            .build();
598    }
599
600    #[rstest]
601    #[should_panic(expected = "GTD not supported for Market orders")]
602    fn test_gtd_condition(audusd_sim: CurrencyPair) {
603        let _ = OrderTestBuilder::new(OrderType::Market)
604            .instrument_id(audusd_sim.id)
605            .side(OrderSide::Buy)
606            .quantity(Quantity::from(100))
607            .time_in_force(TimeInForce::Gtd)
608            .build();
609    }
610    #[rstest]
611    fn test_market_order_creation(audusd_sim: CurrencyPair) {
612        // Create a MarketOrder with specific parameters
613        let order = OrderTestBuilder::new(OrderType::Market)
614            .instrument_id(audusd_sim.id)
615            .quantity(Quantity::from(10))
616            .side(OrderSide::Buy)
617            .time_in_force(TimeInForce::Ioc)
618            .build();
619
620        // Assert that the MarketOrder-specific fields are correctly set
621        assert_eq!(order.time_in_force(), TimeInForce::Ioc);
622        assert_eq!(order.order_type(), OrderType::Market);
623        assert!(order.price().is_none());
624    }
625
626    #[rstest]
627    fn test_direct_market_order_rejects_contingency_without_linked_orders(
628        audusd_sim: CurrencyPair,
629    ) {
630        let result =
631            market_order_with_metadata(audusd_sim.id, Some(ContingencyType::Oco), None, None, None);
632
633        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
634        else {
635            panic!("expected a predicate violation, was {result:?}");
636        };
637        assert_eq!(
638            message,
639            "`linked_order_ids` is required for contingent orders"
640        );
641    }
642
643    #[rstest]
644    fn test_direct_market_order_rejects_exec_algorithm_without_spawn(audusd_sim: CurrencyPair) {
645        let result = market_order_with_metadata(
646            audusd_sim.id,
647            None,
648            None,
649            Some(ExecAlgorithmId::from("TWAP")),
650            None,
651        );
652
653        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
654        else {
655            panic!("expected a predicate violation, was {result:?}");
656        };
657        assert_eq!(
658            message,
659            "`exec_spawn_id` is required when `exec_algorithm_id` is set"
660        );
661    }
662
663    #[rstest]
664    #[case(
665        OrderInitialized {
666            contingency_type: Some(ContingencyType::Oco),
667            linked_order_ids: None,
668            ..OrderInitialized::default()
669        },
670        "`linked_order_ids` is required for contingent orders"
671    )]
672    #[case(
673        OrderInitialized {
674            exec_algorithm_id: Some(ExecAlgorithmId::from("TWAP")),
675            exec_spawn_id: None,
676            ..OrderInitialized::default()
677        },
678        "`exec_spawn_id` is required when `exec_algorithm_id` is set"
679    )]
680    fn test_market_order_reconstruction_rejects_invalid_metadata(
681        #[case] event: OrderInitialized,
682        #[case] expected: &str,
683    ) {
684        let result = MarketOrder::try_from(event);
685
686        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
687        else {
688            panic!("expected a predicate violation, was {result:?}");
689        };
690        assert_eq!(message, expected);
691    }
692
693    #[rstest]
694    fn test_market_order_update(audusd_sim: CurrencyPair) {
695        // Create and accept a basic MarketOrder
696        let order = OrderTestBuilder::new(OrderType::Market)
697            .instrument_id(audusd_sim.id)
698            .quantity(Quantity::from(10))
699            .side(OrderSide::Buy)
700            .build();
701
702        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
703
704        // Update with new values
705        let updated_quantity = Quantity::from(5);
706
707        let event = OrderUpdated {
708            client_order_id: accepted_order.client_order_id(),
709            strategy_id: accepted_order.strategy_id(),
710            quantity: updated_quantity,
711            ..Default::default()
712        };
713
714        accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
715
716        // Verify updates were applied correctly
717        assert_eq!(accepted_order.quantity(), updated_quantity);
718    }
719
720    #[rstest]
721    #[case(Some(Price::new(95.0, 2)), None)]
722    #[case(None, Some(Price::new(95.0, 2)))]
723    fn test_market_order_rejects_invalid_update_atomically(
724        audusd_sim: CurrencyPair,
725        #[case] price: Option<Price>,
726        #[case] trigger_price: Option<Price>,
727    ) {
728        let order = OrderTestBuilder::new(OrderType::Market)
729            .instrument_id(audusd_sim.id)
730            .quantity(Quantity::from(10))
731            .side(OrderSide::Buy)
732            .build();
733        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
734        let state = (
735            accepted_order.status(),
736            accepted_order.previous_status(),
737            accepted_order.ts_last(),
738            accepted_order.events().len(),
739        );
740        let event = OrderUpdated {
741            client_order_id: accepted_order.client_order_id(),
742            strategy_id: accepted_order.strategy_id(),
743            price,
744            trigger_price,
745            ..Default::default()
746        };
747
748        let result = accepted_order.apply(OrderEventAny::Updated(event));
749
750        assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
751        assert_eq!(accepted_order.status(), state.0);
752        assert_eq!(accepted_order.previous_status(), state.1);
753        assert_eq!(accepted_order.ts_last(), state.2);
754        assert_eq!(accepted_order.events().len(), state.3);
755    }
756
757    #[rstest]
758    fn test_market_order_from_order_initialized(audusd_sim: CurrencyPair) {
759        // Create an OrderInitialized event with all required fields for a MarketOrder
760        let order_initialized = OrderInitializedSpec::builder()
761            .order_type(OrderType::Market)
762            .instrument_id(audusd_sim.id)
763            .quantity(Quantity::from(10))
764            .order_side(OrderSide::Buy)
765            .build();
766
767        // Convert the OrderInitialized event into a MarketOrder
768        let order: MarketOrder = order_initialized.clone().try_into().unwrap();
769
770        // Assert essential fields match the OrderInitialized fields
771        assert_eq!(order.trader_id(), order_initialized.trader_id);
772        assert_eq!(order.strategy_id(), order_initialized.strategy_id);
773        assert_eq!(order.instrument_id(), order_initialized.instrument_id);
774        assert_eq!(order.client_order_id(), order_initialized.client_order_id);
775        assert_eq!(order.order_side(), order_initialized.order_side);
776        assert_eq!(order.quantity(), order_initialized.quantity);
777    }
778
779    #[rstest]
780    #[should_panic(
781        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
782    )]
783    fn test_market_order_invalid_quantity(audusd_sim: CurrencyPair) {
784        let _ = OrderTestBuilder::new(OrderType::Market)
785            .instrument_id(audusd_sim.id)
786            .quantity(Quantity::from(0))
787            .side(OrderSide::Buy)
788            .build();
789    }
790
791    #[rstest]
792    fn test_display(audusd_sim: CurrencyPair) {
793        let order = OrderTestBuilder::new(OrderType::Market)
794            .instrument_id(audusd_sim.id)
795            .quantity(Quantity::from(10))
796            .side(OrderSide::Buy)
797            .build();
798
799        // Assert that the display method returns a string representation of the order
800        assert_eq!(
801            order.to_string(),
802            format!(
803                "MarketOrder({} {} {} @ {} {}, status=INITIALIZED, client_order_id={}, venue_order_id=None, position_id=None, exec_algorithm_id=None, exec_spawn_id=None, tags=None)",
804                order.order_side(),
805                order.quantity().to_formatted_string(),
806                order.instrument_id(),
807                order.order_type(),
808                order.time_in_force(),
809                order.client_order_id()
810            )
811        );
812    }
813
814    #[rstest]
815    fn test_stop_market_order_protection_price_update(audusd_sim: CurrencyPair) {
816        // Create and accept a basic MarketOrder
817        let order = OrderTestBuilder::new(OrderType::Market)
818            .instrument_id(audusd_sim.id)
819            .quantity(Quantity::from(10))
820            .side(OrderSide::Buy)
821            .build();
822
823        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
824
825        // Update with new values
826        let calculated_protection_price = Price::new(95.0, 2);
827
828        let event = OrderUpdated {
829            client_order_id: accepted_order.client_order_id(),
830            strategy_id: accepted_order.strategy_id(),
831            protection_price: Some(calculated_protection_price),
832            ..Default::default()
833        };
834
835        assert_eq!(accepted_order.price(), None);
836        assert!(!accepted_order.has_price());
837
838        accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
839
840        // Verify updates were applied correctly
841        assert_eq!(accepted_order.price(), Some(calculated_protection_price));
842        assert!(accepted_order.has_price());
843    }
844
845    fn market_order_with_metadata(
846        instrument_id: InstrumentId,
847        contingency_type: Option<ContingencyType>,
848        linked_order_ids: Option<Vec<ClientOrderId>>,
849        exec_algorithm_id: Option<ExecAlgorithmId>,
850        exec_spawn_id: Option<ClientOrderId>,
851    ) -> Result<MarketOrder, OrderError> {
852        MarketOrder::new_checked(
853            TraderId::from("TRADER-001"),
854            StrategyId::from("S-001"),
855            instrument_id,
856            ClientOrderId::from("O-001"),
857            OrderSide::Buy,
858            Quantity::from(1),
859            TimeInForce::Gtc,
860            UUID4::new(),
861            UnixNanos::default(),
862            false,
863            false,
864            contingency_type,
865            None,
866            linked_order_ids,
867            None,
868            exec_algorithm_id,
869            None,
870            exec_spawn_id,
871            None,
872        )
873    }
874}