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