nautilus_model/orders/
market.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 = "posei_trader.core.nautilus_pyo3.model")
49)]
50pub struct MarketOrder {
51    core: OrderCore,
52}
53
54impl MarketOrder {
55    /// Creates a new [`MarketOrder`] instance.
56    ///
57    /// # Errors
58    ///
59    /// Returns an error if:
60    /// - The `quantity` is not positive.
61    /// - The `time_in_force` is GTD (invalid for market orders).
62    #[allow(clippy::too_many_arguments)]
63    pub fn new_checked(
64        trader_id: TraderId,
65        strategy_id: StrategyId,
66        instrument_id: InstrumentId,
67        client_order_id: ClientOrderId,
68        order_side: OrderSide,
69        quantity: Quantity,
70        time_in_force: TimeInForce,
71        init_id: UUID4,
72        ts_init: UnixNanos,
73        reduce_only: bool,
74        quote_quantity: bool,
75        contingency_type: Option<ContingencyType>,
76        order_list_id: Option<OrderListId>,
77        linked_order_ids: Option<Vec<ClientOrderId>>,
78        parent_order_id: Option<ClientOrderId>,
79        exec_algorithm_id: Option<ExecAlgorithmId>,
80        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
81        exec_spawn_id: Option<ClientOrderId>,
82        tags: Option<Vec<Ustr>>,
83    ) -> anyhow::Result<Self> {
84        check_positive_quantity(quantity, stringify!(quantity))?;
85        check_predicate_false(
86            time_in_force == TimeInForce::Gtd,
87            "GTD not supported for Market orders",
88        )?;
89
90        let init_order = OrderInitialized::new(
91            trader_id,
92            strategy_id,
93            instrument_id,
94            client_order_id,
95            order_side,
96            OrderType::Market,
97            quantity,
98            time_in_force,
99            false,
100            reduce_only,
101            quote_quantity,
102            false,
103            init_id,
104            ts_init,
105            ts_init,
106            None,
107            None,
108            Some(TriggerType::NoTrigger),
109            None,
110            None,
111            None,
112            None,
113            None,
114            None,
115            None,
116            contingency_type,
117            order_list_id,
118            linked_order_ids,
119            parent_order_id,
120            exec_algorithm_id,
121            exec_algorithm_params,
122            exec_spawn_id,
123            tags,
124        );
125
126        Ok(Self {
127            core: OrderCore::new(init_order),
128        })
129    }
130
131    /// Creates a new [`MarketOrder`] instance.
132    ///
133    /// # Panics
134    ///
135    /// Panics if any order validation fails (see [`MarketOrder::new_checked`]).
136    #[allow(clippy::too_many_arguments)]
137    pub fn new(
138        trader_id: TraderId,
139        strategy_id: StrategyId,
140        instrument_id: InstrumentId,
141        client_order_id: ClientOrderId,
142        order_side: OrderSide,
143        quantity: Quantity,
144        time_in_force: TimeInForce,
145        init_id: UUID4,
146        ts_init: UnixNanos,
147        reduce_only: bool,
148        quote_quantity: bool,
149        contingency_type: Option<ContingencyType>,
150        order_list_id: Option<OrderListId>,
151        linked_order_ids: Option<Vec<ClientOrderId>>,
152        parent_order_id: Option<ClientOrderId>,
153        exec_algorithm_id: Option<ExecAlgorithmId>,
154        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
155        exec_spawn_id: Option<ClientOrderId>,
156        tags: Option<Vec<Ustr>>,
157    ) -> Self {
158        Self::new_checked(
159            trader_id,
160            strategy_id,
161            instrument_id,
162            client_order_id,
163            order_side,
164            quantity,
165            time_in_force,
166            init_id,
167            ts_init,
168            reduce_only,
169            quote_quantity,
170            contingency_type,
171            order_list_id,
172            linked_order_ids,
173            parent_order_id,
174            exec_algorithm_id,
175            exec_algorithm_params,
176            exec_spawn_id,
177            tags,
178        )
179        .expect(FAILED)
180    }
181}
182
183impl Deref for MarketOrder {
184    type Target = OrderCore;
185
186    fn deref(&self) -> &Self::Target {
187        &self.core
188    }
189}
190
191impl DerefMut for MarketOrder {
192    fn deref_mut(&mut self) -> &mut Self::Target {
193        &mut self.core
194    }
195}
196
197impl PartialEq for MarketOrder {
198    fn eq(&self, other: &Self) -> bool {
199        self.client_order_id == other.client_order_id
200    }
201}
202
203impl Order for MarketOrder {
204    fn into_any(self) -> OrderAny {
205        OrderAny::Market(self)
206    }
207
208    fn status(&self) -> OrderStatus {
209        self.status
210    }
211
212    fn trader_id(&self) -> TraderId {
213        self.trader_id
214    }
215
216    fn strategy_id(&self) -> StrategyId {
217        self.strategy_id
218    }
219
220    fn instrument_id(&self) -> InstrumentId {
221        self.instrument_id
222    }
223
224    fn symbol(&self) -> Symbol {
225        self.instrument_id.symbol
226    }
227
228    fn venue(&self) -> Venue {
229        self.instrument_id.venue
230    }
231
232    fn client_order_id(&self) -> ClientOrderId {
233        self.client_order_id
234    }
235
236    fn venue_order_id(&self) -> Option<VenueOrderId> {
237        self.venue_order_id
238    }
239
240    fn position_id(&self) -> Option<PositionId> {
241        self.position_id
242    }
243
244    fn account_id(&self) -> Option<AccountId> {
245        self.account_id
246    }
247
248    fn last_trade_id(&self) -> Option<TradeId> {
249        self.last_trade_id
250    }
251
252    fn order_side(&self) -> OrderSide {
253        self.side
254    }
255
256    fn order_type(&self) -> OrderType {
257        self.order_type
258    }
259
260    fn quantity(&self) -> Quantity {
261        self.quantity
262    }
263
264    fn time_in_force(&self) -> TimeInForce {
265        self.time_in_force
266    }
267
268    fn expire_time(&self) -> Option<UnixNanos> {
269        None
270    }
271
272    fn price(&self) -> Option<Price> {
273        None
274    }
275
276    fn trigger_price(&self) -> Option<Price> {
277        None
278    }
279
280    fn trigger_type(&self) -> Option<TriggerType> {
281        None
282    }
283
284    fn liquidity_side(&self) -> Option<LiquiditySide> {
285        self.liquidity_side
286    }
287
288    fn is_post_only(&self) -> bool {
289        false
290    }
291
292    fn is_reduce_only(&self) -> bool {
293        self.is_reduce_only
294    }
295
296    fn is_quote_quantity(&self) -> bool {
297        self.is_quote_quantity
298    }
299
300    fn has_price(&self) -> bool {
301        false
302    }
303
304    fn display_qty(&self) -> Option<Quantity> {
305        None
306    }
307
308    fn limit_offset(&self) -> Option<Decimal> {
309        None
310    }
311
312    fn trailing_offset(&self) -> Option<Decimal> {
313        None
314    }
315
316    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
317        None
318    }
319
320    fn emulation_trigger(&self) -> Option<TriggerType> {
321        None
322    }
323
324    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
325        None
326    }
327
328    fn contingency_type(&self) -> Option<ContingencyType> {
329        self.contingency_type
330    }
331
332    fn order_list_id(&self) -> Option<OrderListId> {
333        self.order_list_id
334    }
335
336    fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
337        self.linked_order_ids.as_deref()
338    }
339
340    fn parent_order_id(&self) -> Option<ClientOrderId> {
341        self.parent_order_id
342    }
343
344    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
345        self.exec_algorithm_id
346    }
347
348    fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
349        self.exec_algorithm_params.as_ref()
350    }
351
352    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
353        self.exec_spawn_id
354    }
355
356    fn tags(&self) -> Option<&[Ustr]> {
357        self.tags.as_deref()
358    }
359
360    fn filled_qty(&self) -> Quantity {
361        self.filled_qty
362    }
363
364    fn leaves_qty(&self) -> Quantity {
365        self.leaves_qty
366    }
367
368    fn avg_px(&self) -> Option<f64> {
369        self.avg_px
370    }
371
372    fn slippage(&self) -> Option<f64> {
373        self.slippage
374    }
375
376    fn init_id(&self) -> UUID4 {
377        self.init_id
378    }
379
380    fn ts_init(&self) -> UnixNanos {
381        self.ts_init
382    }
383
384    fn ts_submitted(&self) -> Option<UnixNanos> {
385        self.ts_submitted
386    }
387
388    fn ts_accepted(&self) -> Option<UnixNanos> {
389        self.ts_accepted
390    }
391
392    fn ts_closed(&self) -> Option<UnixNanos> {
393        self.ts_closed
394    }
395
396    fn ts_last(&self) -> UnixNanos {
397        self.ts_last
398    }
399
400    fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
401        if let OrderEventAny::Updated(ref event) = event {
402            self.update(event);
403        };
404
405        self.core.apply(event)?;
406
407        Ok(())
408    }
409
410    fn update(&mut self, event: &OrderUpdated) {
411        assert!(event.price.is_none(), "{}", OrderError::InvalidOrderEvent);
412        assert!(
413            event.trigger_price.is_none(),
414            "{}",
415            OrderError::InvalidOrderEvent
416        );
417
418        self.quantity = event.quantity;
419        self.leaves_qty = self.quantity - self.filled_qty;
420    }
421
422    fn events(&self) -> Vec<&OrderEventAny> {
423        self.events.iter().collect()
424    }
425
426    fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
427        self.venue_order_ids.iter().collect()
428    }
429
430    fn trade_ids(&self) -> Vec<&TradeId> {
431        self.trade_ids.iter().collect()
432    }
433
434    fn commissions(&self) -> &IndexMap<Currency, Money> {
435        &self.commissions
436    }
437
438    fn is_triggered(&self) -> Option<bool> {
439        None
440    }
441
442    fn set_position_id(&mut self, position_id: Option<PositionId>) {
443        self.position_id = position_id;
444    }
445
446    fn set_quantity(&mut self, quantity: Quantity) {
447        self.quantity = quantity;
448    }
449
450    fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
451        self.leaves_qty = leaves_qty;
452    }
453
454    fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
455        self.emulation_trigger = emulation_trigger;
456    }
457
458    fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
459        self.is_quote_quantity = is_quote_quantity;
460    }
461
462    fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
463        self.liquidity_side = Some(liquidity_side)
464    }
465
466    fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
467        self.core.would_reduce_only(side, position_qty)
468    }
469
470    fn previous_status(&self) -> Option<OrderStatus> {
471        self.core.previous_status
472    }
473}
474
475impl Display for MarketOrder {
476    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
477        write!(
478            f,
479            "MarketOrder(\
480            {} {} {} @ {} {}, \
481            status={}, \
482            client_order_id={}, \
483            venue_order_id={}, \
484            position_id={}, \
485            exec_algorithm_id={}, \
486            exec_spawn_id={}, \
487            tags={:?}\
488            )",
489            self.side,
490            self.quantity.to_formatted_string(),
491            self.instrument_id,
492            self.order_type,
493            self.time_in_force,
494            self.status,
495            self.client_order_id,
496            self.venue_order_id.map_or_else(
497                || "None".to_string(),
498                |venue_order_id| format!("{venue_order_id}")
499            ),
500            self.position_id.map_or_else(
501                || "None".to_string(),
502                |position_id| format!("{position_id}")
503            ),
504            self.exec_algorithm_id
505                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
506            self.exec_spawn_id
507                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
508            self.tags
509        )
510    }
511}
512
513impl From<OrderInitialized> for MarketOrder {
514    fn from(event: OrderInitialized) -> Self {
515        Self::new(
516            event.trader_id,
517            event.strategy_id,
518            event.instrument_id,
519            event.client_order_id,
520            event.order_side,
521            event.quantity,
522            event.time_in_force,
523            event.event_id,
524            event.ts_event,
525            event.reduce_only,
526            event.quote_quantity,
527            event.contingency_type,
528            event.order_list_id,
529            event.linked_order_ids,
530            event.parent_order_id,
531            event.exec_algorithm_id,
532            event.exec_algorithm_params,
533            event.exec_spawn_id,
534            event.tags,
535        )
536    }
537}
538
539////////////////////////////////////////////////////////////////////////////////
540// Tests
541////////////////////////////////////////////////////////////////////////////////
542#[cfg(test)]
543mod tests {
544    use rstest::rstest;
545
546    use crate::{
547        enums::{OrderSide, OrderType, TimeInForce},
548        events::{OrderEventAny, OrderUpdated, order::initialized::OrderInitializedBuilder},
549        instruments::{CurrencyPair, stubs::*},
550        orders::{MarketOrder, Order, builder::OrderTestBuilder, stubs::TestOrderStubs},
551        types::Quantity,
552    };
553
554    #[rstest]
555    #[should_panic(
556        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
557    )]
558    fn test_positive_quantity_condition(audusd_sim: CurrencyPair) {
559        let _ = OrderTestBuilder::new(OrderType::Market)
560            .instrument_id(audusd_sim.id)
561            .side(OrderSide::Buy)
562            .quantity(Quantity::from(0))
563            .build();
564    }
565
566    #[rstest]
567    #[should_panic(expected = "GTD not supported for Market orders")]
568    fn test_gtd_condition(audusd_sim: CurrencyPair) {
569        let _ = OrderTestBuilder::new(OrderType::Market)
570            .instrument_id(audusd_sim.id)
571            .side(OrderSide::Buy)
572            .quantity(Quantity::from(100))
573            .time_in_force(TimeInForce::Gtd)
574            .build();
575    }
576    #[rstest]
577    fn test_market_order_creation(audusd_sim: CurrencyPair) {
578        // Create a MarketOrder with specific parameters
579        let order = OrderTestBuilder::new(OrderType::Market)
580            .instrument_id(audusd_sim.id)
581            .quantity(Quantity::from(10))
582            .side(OrderSide::Buy)
583            .time_in_force(TimeInForce::Ioc)
584            .build();
585
586        // Assert that the MarketOrder-specific fields are correctly set
587        assert_eq!(order.time_in_force(), TimeInForce::Ioc);
588        assert_eq!(order.order_type(), OrderType::Market);
589        assert!(order.price().is_none());
590    }
591
592    #[rstest]
593    fn test_market_order_update(audusd_sim: CurrencyPair) {
594        // Create and accept a basic MarketOrder
595        let order = OrderTestBuilder::new(OrderType::Market)
596            .instrument_id(audusd_sim.id)
597            .quantity(Quantity::from(10))
598            .side(OrderSide::Buy)
599            .build();
600
601        let mut accepted_order = TestOrderStubs::make_accepted_order(&order);
602
603        // Update with new values
604        let updated_quantity = Quantity::from(5);
605
606        let event = OrderUpdated {
607            client_order_id: accepted_order.client_order_id(),
608            strategy_id: accepted_order.strategy_id(),
609            quantity: updated_quantity,
610            ..Default::default()
611        };
612
613        accepted_order.apply(OrderEventAny::Updated(event)).unwrap();
614
615        // Verify updates were applied correctly
616        assert_eq!(accepted_order.quantity(), updated_quantity);
617    }
618
619    #[rstest]
620    fn test_market_order_from_order_initialized(audusd_sim: CurrencyPair) {
621        // Create an OrderInitialized event with all required fields for a MarketOrder
622        let order_initialized = OrderInitializedBuilder::default()
623            .order_type(OrderType::Market)
624            .instrument_id(audusd_sim.id)
625            .quantity(Quantity::from(10))
626            .order_side(OrderSide::Buy)
627            .build()
628            .unwrap();
629
630        // Convert the OrderInitialized event into a MarketOrder
631        let order: MarketOrder = order_initialized.clone().into();
632
633        // Assert essential fields match the OrderInitialized fields
634        assert_eq!(order.trader_id(), order_initialized.trader_id);
635        assert_eq!(order.strategy_id(), order_initialized.strategy_id);
636        assert_eq!(order.instrument_id(), order_initialized.instrument_id);
637        assert_eq!(order.client_order_id(), order_initialized.client_order_id);
638        assert_eq!(order.order_side(), order_initialized.order_side);
639        assert_eq!(order.quantity(), order_initialized.quantity);
640    }
641
642    #[rstest]
643    #[should_panic(
644        expected = "Condition failed: invalid `Quantity` for 'quantity' not positive, was 0"
645    )]
646    fn test_market_order_invalid_quantity(audusd_sim: CurrencyPair) {
647        let _ = OrderTestBuilder::new(OrderType::Market)
648            .instrument_id(audusd_sim.id)
649            .quantity(Quantity::from(0))
650            .side(OrderSide::Buy)
651            .build();
652    }
653
654    #[rstest]
655    fn test_display(audusd_sim: CurrencyPair) {
656        let order = OrderTestBuilder::new(OrderType::Market)
657            .instrument_id(audusd_sim.id)
658            .quantity(Quantity::from(10))
659            .side(OrderSide::Buy)
660            .build();
661
662        // Assert that the display method returns a string representation of the order
663        assert_eq!(
664            order.to_string(),
665            format!(
666                "MarketOrder({} {} {} @ {} {}, status=INITIALIZED, client_order_id={}, venue_order_id=None, position_id=None, exec_algorithm_id=None, exec_spawn_id=None, tags=None)",
667                order.order_side(),
668                order.quantity().to_formatted_string(),
669                order.instrument_id(),
670                order.order_type(),
671                order.time_in_force(),
672                order.client_order_id()
673            )
674        );
675    }
676}