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 venue_order_ids(&self) -> Vec<&VenueOrderId> {
450        self.venue_order_ids.iter().collect()
451    }
452
453    fn trade_ids(&self) -> Vec<&TradeId> {
454        self.trade_ids.iter().collect()
455    }
456
457    fn commissions(&self) -> &IndexMap<Currency, Money> {
458        &self.commissions
459    }
460
461    fn is_triggered(&self) -> Option<bool> {
462        None
463    }
464
465    fn set_position_id(&mut self, position_id: Option<PositionId>) {
466        self.position_id = position_id;
467    }
468
469    fn set_quantity(&mut self, quantity: Quantity) {
470        self.quantity = quantity;
471    }
472
473    fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
474        self.leaves_qty = leaves_qty;
475    }
476
477    fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
478        self.emulation_trigger = emulation_trigger;
479    }
480
481    fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
482        self.is_quote_quantity = is_quote_quantity;
483    }
484
485    fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
486        self.liquidity_side = Some(liquidity_side);
487    }
488
489    fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
490        self.core.would_reduce_only(side, position_qty)
491    }
492
493    fn previous_status(&self) -> Option<OrderStatus> {
494        self.core.previous_status
495    }
496}
497
498impl Display for MarketOrder {
499    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
500        write!(
501            f,
502            "MarketOrder(\
503            {} {} {} @ {} {}, \
504            status={}, \
505            client_order_id={}, \
506            venue_order_id={}, \
507            position_id={}, \
508            exec_algorithm_id={}, \
509            exec_spawn_id={}, \
510            tags={:?}\
511            )",
512            self.side,
513            self.quantity.to_formatted_string(),
514            self.instrument_id,
515            self.order_type,
516            self.time_in_force,
517            self.status,
518            self.client_order_id,
519            self.venue_order_id.map_or_else(
520                || "None".to_string(),
521                |venue_order_id| format!("{venue_order_id}")
522            ),
523            self.position_id.map_or_else(
524                || "None".to_string(),
525                |position_id| format!("{position_id}")
526            ),
527            self.exec_algorithm_id
528                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
529            self.exec_spawn_id
530                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
531            self.tags
532        )
533    }
534}
535
536impl TryFrom<OrderInitialized> for MarketOrder {
537    type Error = OrderError;
538
539    fn try_from(event: OrderInitialized) -> Result<Self, Self::Error> {
540        Self::new_checked(
541            event.trader_id,
542            event.strategy_id,
543            event.instrument_id,
544            event.client_order_id,
545            event.order_side,
546            event.quantity,
547            event.time_in_force,
548            event.event_id,
549            event.ts_event,
550            event.reduce_only,
551            event.quote_quantity,
552            event.contingency_type,
553            event.order_list_id,
554            event.linked_order_ids,
555            event.parent_order_id,
556            event.exec_algorithm_id,
557            event.exec_algorithm_params,
558            event.exec_spawn_id,
559            event.tags,
560        )
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use nautilus_core::{UUID4, UnixNanos, correctness::CorrectnessError};
567    use rstest::rstest;
568
569    use crate::{
570        enums::{ContingencyType, OrderSide, OrderType, TimeInForce},
571        events::{
572            OrderEventAny, OrderInitialized, OrderUpdated, order::spec::OrderInitializedSpec,
573        },
574        identifiers::{ClientOrderId, ExecAlgorithmId, InstrumentId, StrategyId, TraderId},
575        instruments::{CurrencyPair, stubs::*},
576        orders::{
577            MarketOrder, Order, OrderError, builder::OrderTestBuilder, stubs::TestOrderStubs,
578        },
579        types::{Price, Quantity},
580    };
581
582    #[rstest]
583    #[should_panic(
584        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
585    )]
586    fn test_positive_quantity_condition(audusd_sim: CurrencyPair) {
587        let _ = OrderTestBuilder::new(OrderType::Market)
588            .instrument_id(audusd_sim.id)
589            .side(OrderSide::Buy)
590            .quantity(Quantity::from(0))
591            .build();
592    }
593
594    #[rstest]
595    #[should_panic(expected = "GTD not supported for Market orders")]
596    fn test_gtd_condition(audusd_sim: CurrencyPair) {
597        let _ = OrderTestBuilder::new(OrderType::Market)
598            .instrument_id(audusd_sim.id)
599            .side(OrderSide::Buy)
600            .quantity(Quantity::from(100))
601            .time_in_force(TimeInForce::Gtd)
602            .build();
603    }
604    #[rstest]
605    fn test_market_order_creation(audusd_sim: CurrencyPair) {
606        // Create a MarketOrder with specific parameters
607        let order = OrderTestBuilder::new(OrderType::Market)
608            .instrument_id(audusd_sim.id)
609            .quantity(Quantity::from(10))
610            .side(OrderSide::Buy)
611            .time_in_force(TimeInForce::Ioc)
612            .build();
613
614        // Assert that the MarketOrder-specific fields are correctly set
615        assert_eq!(order.time_in_force(), TimeInForce::Ioc);
616        assert_eq!(order.order_type(), OrderType::Market);
617        assert!(order.price().is_none());
618    }
619
620    #[rstest]
621    fn test_direct_market_order_rejects_contingency_without_linked_orders(
622        audusd_sim: CurrencyPair,
623    ) {
624        let result =
625            market_order_with_metadata(audusd_sim.id, Some(ContingencyType::Oco), None, None, None);
626
627        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
628        else {
629            panic!("expected a predicate violation, was {result:?}");
630        };
631        assert_eq!(
632            message,
633            "`linked_order_ids` is required for contingent orders"
634        );
635    }
636
637    #[rstest]
638    fn test_direct_market_order_rejects_exec_algorithm_without_spawn(audusd_sim: CurrencyPair) {
639        let result = market_order_with_metadata(
640            audusd_sim.id,
641            None,
642            None,
643            Some(ExecAlgorithmId::from("TWAP")),
644            None,
645        );
646
647        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
648        else {
649            panic!("expected a predicate violation, was {result:?}");
650        };
651        assert_eq!(
652            message,
653            "`exec_spawn_id` is required when `exec_algorithm_id` is set"
654        );
655    }
656
657    #[rstest]
658    #[case(
659        OrderInitialized {
660            contingency_type: Some(ContingencyType::Oco),
661            linked_order_ids: None,
662            ..OrderInitialized::default()
663        },
664        "`linked_order_ids` is required for contingent orders"
665    )]
666    #[case(
667        OrderInitialized {
668            exec_algorithm_id: Some(ExecAlgorithmId::from("TWAP")),
669            exec_spawn_id: None,
670            ..OrderInitialized::default()
671        },
672        "`exec_spawn_id` is required when `exec_algorithm_id` is set"
673    )]
674    fn test_market_order_reconstruction_rejects_invalid_metadata(
675        #[case] event: OrderInitialized,
676        #[case] expected: &str,
677    ) {
678        let result = MarketOrder::try_from(event);
679
680        let Err(OrderError::Invariant(CorrectnessError::PredicateViolation { message })) = result
681        else {
682            panic!("expected a predicate violation, was {result:?}");
683        };
684        assert_eq!(message, expected);
685    }
686
687    #[rstest]
688    fn test_market_order_update(audusd_sim: CurrencyPair) {
689        // Create and accept a basic MarketOrder
690        let order = OrderTestBuilder::new(OrderType::Market)
691            .instrument_id(audusd_sim.id)
692            .quantity(Quantity::from(10))
693            .side(OrderSide::Buy)
694            .build();
695
696        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
697
698        // Update with new values
699        let updated_quantity = Quantity::from(5);
700
701        let event = OrderUpdated {
702            client_order_id: accepted_order.client_order_id(),
703            strategy_id: accepted_order.strategy_id(),
704            quantity: updated_quantity,
705            ..Default::default()
706        };
707
708        accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
709
710        // Verify updates were applied correctly
711        assert_eq!(accepted_order.quantity(), updated_quantity);
712    }
713
714    #[rstest]
715    #[case(Some(Price::new(95.0, 2)), None)]
716    #[case(None, Some(Price::new(95.0, 2)))]
717    fn test_market_order_rejects_invalid_update_atomically(
718        audusd_sim: CurrencyPair,
719        #[case] price: Option<Price>,
720        #[case] trigger_price: Option<Price>,
721    ) {
722        let order = OrderTestBuilder::new(OrderType::Market)
723            .instrument_id(audusd_sim.id)
724            .quantity(Quantity::from(10))
725            .side(OrderSide::Buy)
726            .build();
727        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
728        let state = (
729            accepted_order.status(),
730            accepted_order.previous_status(),
731            accepted_order.ts_last(),
732            accepted_order.events().len(),
733        );
734        let event = OrderUpdated {
735            client_order_id: accepted_order.client_order_id(),
736            strategy_id: accepted_order.strategy_id(),
737            price,
738            trigger_price,
739            ..Default::default()
740        };
741
742        let result = accepted_order.apply(OrderEventAny::Updated(event));
743
744        assert!(matches!(result, Err(OrderError::InvalidOrderEvent)));
745        assert_eq!(accepted_order.status(), state.0);
746        assert_eq!(accepted_order.previous_status(), state.1);
747        assert_eq!(accepted_order.ts_last(), state.2);
748        assert_eq!(accepted_order.events().len(), state.3);
749    }
750
751    #[rstest]
752    fn test_market_order_from_order_initialized(audusd_sim: CurrencyPair) {
753        // Create an OrderInitialized event with all required fields for a MarketOrder
754        let order_initialized = OrderInitializedSpec::builder()
755            .order_type(OrderType::Market)
756            .instrument_id(audusd_sim.id)
757            .quantity(Quantity::from(10))
758            .order_side(OrderSide::Buy)
759            .build();
760
761        // Convert the OrderInitialized event into a MarketOrder
762        let order: MarketOrder = order_initialized.clone().try_into().unwrap();
763
764        // Assert essential fields match the OrderInitialized fields
765        assert_eq!(order.trader_id(), order_initialized.trader_id);
766        assert_eq!(order.strategy_id(), order_initialized.strategy_id);
767        assert_eq!(order.instrument_id(), order_initialized.instrument_id);
768        assert_eq!(order.client_order_id(), order_initialized.client_order_id);
769        assert_eq!(order.order_side(), order_initialized.order_side);
770        assert_eq!(order.quantity(), order_initialized.quantity);
771    }
772
773    #[rstest]
774    #[should_panic(
775        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
776    )]
777    fn test_market_order_invalid_quantity(audusd_sim: CurrencyPair) {
778        let _ = OrderTestBuilder::new(OrderType::Market)
779            .instrument_id(audusd_sim.id)
780            .quantity(Quantity::from(0))
781            .side(OrderSide::Buy)
782            .build();
783    }
784
785    #[rstest]
786    fn test_display(audusd_sim: CurrencyPair) {
787        let order = OrderTestBuilder::new(OrderType::Market)
788            .instrument_id(audusd_sim.id)
789            .quantity(Quantity::from(10))
790            .side(OrderSide::Buy)
791            .build();
792
793        // Assert that the display method returns a string representation of the order
794        assert_eq!(
795            order.to_string(),
796            format!(
797                "MarketOrder({} {} {} @ {} {}, status=INITIALIZED, client_order_id={}, venue_order_id=None, position_id=None, exec_algorithm_id=None, exec_spawn_id=None, tags=None)",
798                order.order_side(),
799                order.quantity().to_formatted_string(),
800                order.instrument_id(),
801                order.order_type(),
802                order.time_in_force(),
803                order.client_order_id()
804            )
805        );
806    }
807
808    #[rstest]
809    fn test_stop_market_order_protection_price_update(audusd_sim: CurrencyPair) {
810        // Create and accept a basic MarketOrder
811        let order = OrderTestBuilder::new(OrderType::Market)
812            .instrument_id(audusd_sim.id)
813            .quantity(Quantity::from(10))
814            .side(OrderSide::Buy)
815            .build();
816
817        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
818
819        // Update with new values
820        let calculated_protection_price = Price::new(95.0, 2);
821
822        let event = OrderUpdated {
823            client_order_id: accepted_order.client_order_id(),
824            strategy_id: accepted_order.strategy_id(),
825            protection_price: Some(calculated_protection_price),
826            ..Default::default()
827        };
828
829        assert_eq!(accepted_order.price(), None);
830        assert!(!accepted_order.has_price());
831
832        accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
833
834        // Verify updates were applied correctly
835        assert_eq!(accepted_order.price(), Some(calculated_protection_price));
836        assert!(accepted_order.has_price());
837    }
838
839    fn market_order_with_metadata(
840        instrument_id: InstrumentId,
841        contingency_type: Option<ContingencyType>,
842        linked_order_ids: Option<Vec<ClientOrderId>>,
843        exec_algorithm_id: Option<ExecAlgorithmId>,
844        exec_spawn_id: Option<ClientOrderId>,
845    ) -> Result<MarketOrder, OrderError> {
846        MarketOrder::new_checked(
847            TraderId::from("TRADER-001"),
848            StrategyId::from("S-001"),
849            instrument_id,
850            ClientOrderId::from("O-001"),
851            OrderSide::Buy,
852            Quantity::from(1),
853            TimeInForce::Gtc,
854            UUID4::new(),
855            UnixNanos::default(),
856            false,
857            false,
858            contingency_type,
859            None,
860            linked_order_ids,
861            None,
862            exec_algorithm_id,
863            None,
864            exec_spawn_id,
865            None,
866        )
867    }
868}