nautilus_model/orders/
limit.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{
17    fmt::Display,
18    ops::{Deref, DerefMut},
19};
20
21use indexmap::IndexMap;
22use nautilus_core::{UnixNanos, UUID4};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{
28    any::OrderAny,
29    base::{Order, OrderCore},
30};
31use crate::{
32    enums::{
33        ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, TimeInForce,
34        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::{quantity::check_quantity_positive, Price, Quantity},
43};
44
45#[derive(Clone, Debug, Serialize, Deserialize)]
46#[cfg_attr(
47    feature = "python",
48    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
49)]
50pub struct LimitOrder {
51    core: OrderCore,
52    pub price: Price,
53    pub expire_time: Option<UnixNanos>,
54    pub is_post_only: bool,
55    pub display_qty: Option<Quantity>,
56    pub trigger_instrument_id: Option<InstrumentId>,
57}
58
59impl LimitOrder {
60    /// Creates a new [`LimitOrder`] instance.
61    #[allow(clippy::too_many_arguments)]
62    pub fn new(
63        trader_id: TraderId,
64        strategy_id: StrategyId,
65        instrument_id: InstrumentId,
66        client_order_id: ClientOrderId,
67        order_side: OrderSide,
68        quantity: Quantity,
69        price: Price,
70        time_in_force: TimeInForce,
71        expire_time: Option<UnixNanos>,
72        post_only: bool,
73        reduce_only: bool,
74        quote_quantity: bool,
75        display_qty: Option<Quantity>,
76        emulation_trigger: Option<TriggerType>,
77        trigger_instrument_id: Option<InstrumentId>,
78        contingency_type: Option<ContingencyType>,
79        order_list_id: Option<OrderListId>,
80        linked_order_ids: Option<Vec<ClientOrderId>>,
81        parent_order_id: Option<ClientOrderId>,
82        exec_algorithm_id: Option<ExecAlgorithmId>,
83        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
84        exec_spawn_id: Option<ClientOrderId>,
85        tags: Option<Vec<Ustr>>,
86        init_id: UUID4,
87        ts_init: UnixNanos,
88    ) -> anyhow::Result<Self> {
89        check_quantity_positive(quantity)?;
90        if time_in_force == TimeInForce::Gtd {
91            if expire_time.is_none() {
92                anyhow::bail!("Condition failed: `expire_time` is required for `GTD` order")
93            }
94            if let Some(time) = expire_time {
95                if time == 0 {
96                    anyhow::bail!("`expire_time` for `GTD` Limit order should be higher then 0")
97                }
98            }
99        }
100        let init_order = OrderInitialized::new(
101            trader_id,
102            strategy_id,
103            instrument_id,
104            client_order_id,
105            order_side,
106            OrderType::Limit,
107            quantity,
108            time_in_force,
109            post_only,
110            reduce_only,
111            quote_quantity,
112            false,
113            init_id,
114            ts_init, // ts_event timestamp identical to ts_init
115            ts_init,
116            Some(price),
117            None,
118            None,
119            None,
120            None,
121            None,
122            expire_time,
123            display_qty,
124            emulation_trigger,
125            trigger_instrument_id,
126            contingency_type,
127            order_list_id,
128            linked_order_ids,
129            parent_order_id,
130            exec_algorithm_id,
131            exec_algorithm_params,
132            exec_spawn_id,
133            tags,
134        );
135
136        Ok(Self {
137            core: OrderCore::new(init_order),
138            price,
139            expire_time: expire_time.or(Some(UnixNanos::default())),
140            is_post_only: post_only,
141            display_qty,
142            trigger_instrument_id,
143        })
144    }
145}
146
147impl Deref for LimitOrder {
148    type Target = OrderCore;
149
150    fn deref(&self) -> &Self::Target {
151        &self.core
152    }
153}
154
155impl DerefMut for LimitOrder {
156    fn deref_mut(&mut self) -> &mut Self::Target {
157        &mut self.core
158    }
159}
160
161impl PartialEq for LimitOrder {
162    fn eq(&self, other: &Self) -> bool {
163        self.client_order_id == other.client_order_id
164    }
165}
166
167impl Order for LimitOrder {
168    fn into_any(self) -> OrderAny {
169        OrderAny::Limit(self)
170    }
171
172    fn status(&self) -> OrderStatus {
173        self.status
174    }
175
176    fn trader_id(&self) -> TraderId {
177        self.trader_id
178    }
179
180    fn strategy_id(&self) -> StrategyId {
181        self.strategy_id
182    }
183
184    fn instrument_id(&self) -> InstrumentId {
185        self.instrument_id
186    }
187
188    fn symbol(&self) -> Symbol {
189        self.instrument_id.symbol
190    }
191
192    fn venue(&self) -> Venue {
193        self.instrument_id.venue
194    }
195
196    fn client_order_id(&self) -> ClientOrderId {
197        self.client_order_id
198    }
199
200    fn venue_order_id(&self) -> Option<VenueOrderId> {
201        self.venue_order_id
202    }
203
204    fn position_id(&self) -> Option<PositionId> {
205        self.position_id
206    }
207
208    fn account_id(&self) -> Option<AccountId> {
209        self.account_id
210    }
211
212    fn last_trade_id(&self) -> Option<TradeId> {
213        self.last_trade_id
214    }
215
216    fn side(&self) -> OrderSide {
217        self.side
218    }
219
220    fn order_type(&self) -> OrderType {
221        self.order_type
222    }
223
224    fn quantity(&self) -> Quantity {
225        self.quantity
226    }
227
228    fn time_in_force(&self) -> TimeInForce {
229        self.time_in_force
230    }
231
232    fn expire_time(&self) -> Option<UnixNanos> {
233        self.expire_time
234    }
235
236    fn price(&self) -> Option<Price> {
237        Some(self.price)
238    }
239
240    fn trigger_price(&self) -> Option<Price> {
241        None
242    }
243
244    fn trigger_type(&self) -> Option<TriggerType> {
245        None
246    }
247
248    fn liquidity_side(&self) -> Option<LiquiditySide> {
249        self.liquidity_side
250    }
251
252    fn is_post_only(&self) -> bool {
253        self.is_post_only
254    }
255
256    fn is_reduce_only(&self) -> bool {
257        self.is_reduce_only
258    }
259
260    fn is_quote_quantity(&self) -> bool {
261        self.is_quote_quantity
262    }
263
264    fn display_qty(&self) -> Option<Quantity> {
265        self.display_qty
266    }
267
268    fn limit_offset(&self) -> Option<Decimal> {
269        None
270    }
271
272    fn trailing_offset(&self) -> Option<Decimal> {
273        None
274    }
275
276    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
277        None
278    }
279
280    fn emulation_trigger(&self) -> Option<TriggerType> {
281        self.emulation_trigger
282    }
283
284    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
285        self.trigger_instrument_id
286    }
287
288    fn contingency_type(&self) -> Option<ContingencyType> {
289        self.contingency_type
290    }
291
292    fn order_list_id(&self) -> Option<OrderListId> {
293        self.order_list_id
294    }
295
296    fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
297        self.linked_order_ids.as_deref()
298    }
299
300    fn parent_order_id(&self) -> Option<ClientOrderId> {
301        self.parent_order_id
302    }
303
304    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
305        self.exec_algorithm_id
306    }
307
308    fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
309        self.exec_algorithm_params.as_ref()
310    }
311
312    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
313        self.exec_spawn_id
314    }
315
316    fn tags(&self) -> Option<&[Ustr]> {
317        self.tags.as_deref()
318    }
319
320    fn filled_qty(&self) -> Quantity {
321        self.filled_qty
322    }
323
324    fn leaves_qty(&self) -> Quantity {
325        self.leaves_qty
326    }
327
328    fn avg_px(&self) -> Option<f64> {
329        self.avg_px
330    }
331
332    fn slippage(&self) -> Option<f64> {
333        self.slippage
334    }
335
336    fn init_id(&self) -> UUID4 {
337        self.init_id
338    }
339
340    fn ts_init(&self) -> UnixNanos {
341        self.ts_init
342    }
343
344    fn ts_last(&self) -> UnixNanos {
345        self.ts_last
346    }
347
348    fn events(&self) -> Vec<&OrderEventAny> {
349        self.events.iter().collect()
350    }
351
352    fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
353        self.venue_order_ids.iter().collect()
354    }
355
356    fn trade_ids(&self) -> Vec<&TradeId> {
357        self.trade_ids.iter().collect()
358    }
359
360    fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
361        if let OrderEventAny::Updated(ref event) = event {
362            self.update(event);
363        };
364        let is_order_filled = matches!(event, OrderEventAny::Filled(_));
365
366        self.core.apply(event)?;
367
368        if is_order_filled {
369            self.core.set_slippage(self.price);
370        };
371
372        Ok(())
373    }
374
375    fn update(&mut self, event: &OrderUpdated) {
376        assert!(
377            event.trigger_price.is_none(),
378            "{}",
379            OrderError::InvalidOrderEvent
380        );
381
382        if let Some(price) = event.price {
383            self.price = price;
384        }
385
386        self.quantity = event.quantity;
387        self.leaves_qty = self.quantity - self.filled_qty;
388    }
389}
390
391impl Display for LimitOrder {
392    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
393        write!(
394            f,
395            "LimitOrder(\
396            {} {} {} {} @ {} {}, \
397            status={}, \
398            client_order_id={}, \
399            venue_order_id={}, \
400            position_id={}, \
401            exec_algorithm_id={}, \
402            exec_spawn_id={}, \
403            tags={:?}\
404            )",
405            self.side,
406            self.quantity.to_formatted_string(),
407            self.instrument_id,
408            self.order_type,
409            self.price,
410            self.time_in_force,
411            self.status,
412            self.client_order_id,
413            self.venue_order_id.map_or_else(
414                || "None".to_string(),
415                |venue_order_id| format!("{venue_order_id}")
416            ),
417            self.position_id.map_or_else(
418                || "None".to_string(),
419                |position_id| format!("{position_id}")
420            ),
421            self.exec_algorithm_id
422                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
423            self.exec_spawn_id
424                .map_or_else(|| "None".to_string(), |id| format!("{id}")),
425            self.tags
426        )
427    }
428}
429
430impl From<OrderAny> for LimitOrder {
431    fn from(order: OrderAny) -> LimitOrder {
432        match order {
433            OrderAny::Limit(order) => order,
434            _ => panic!(
435                "Invalid `OrderAny` not `{}`, was {order:?}",
436                stringify!(LimitOrder),
437            ),
438        }
439    }
440}
441
442impl From<OrderInitialized> for LimitOrder {
443    fn from(event: OrderInitialized) -> Self {
444        Self::new(
445            event.trader_id,
446            event.strategy_id,
447            event.instrument_id,
448            event.client_order_id,
449            event.order_side,
450            event.quantity,
451            event
452                .price // TODO: Improve this error, model order domain errors
453                .expect("Error initializing order: `price` was `None` for `LimitOrder"),
454            event.time_in_force,
455            event.expire_time,
456            event.post_only,
457            event.reduce_only,
458            event.quote_quantity,
459            event.display_qty,
460            event.emulation_trigger,
461            event.trigger_instrument_id,
462            event.contingency_type,
463            event.order_list_id,
464            event.linked_order_ids,
465            event.parent_order_id,
466            event.exec_algorithm_id,
467            event.exec_algorithm_params,
468            event.exec_spawn_id,
469            event.tags,
470            event.event_id,
471            event.ts_event,
472        )
473        .unwrap()
474    }
475}
476
477////////////////////////////////////////////////////////////////////////////////
478// Tests
479////////////////////////////////////////////////////////////////////////////////
480#[cfg(test)]
481mod tests {
482    use rstest::rstest;
483
484    use crate::{
485        enums::{OrderSide, OrderType, TimeInForce},
486        instruments::{stubs::*, CurrencyPair},
487        orders::OrderTestBuilder,
488        types::{Price, Quantity},
489    };
490
491    #[rstest]
492    fn test_display(audusd_sim: CurrencyPair) {
493        let order = OrderTestBuilder::new(OrderType::Limit)
494            .instrument_id(audusd_sim.id)
495            .side(OrderSide::Buy)
496            .price(Price::from("1.00000"))
497            .quantity(Quantity::from(100_000))
498            .build();
499
500        assert_eq!(
501            order.to_string(),
502            "LimitOrder(BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, \
503            status=INITIALIZED, client_order_id=O-19700101-000000-001-001-1, \
504            venue_order_id=None, position_id=None, exec_algorithm_id=None, \
505            exec_spawn_id=None, tags=None)"
506        );
507    }
508
509    #[rstest]
510    #[should_panic(expected = "Condition failed: invalid `Quantity`, should be positive and was 0")]
511    fn test_positive_quantity_condition(audusd_sim: CurrencyPair) {
512        let _ = OrderTestBuilder::new(OrderType::Limit)
513            .instrument_id(audusd_sim.id)
514            .side(OrderSide::Buy)
515            .price(Price::from("0.8"))
516            .quantity(Quantity::from(0))
517            .build();
518    }
519
520    #[rstest]
521    #[should_panic(expected = "Condition failed: `expire_time` is required for `GTD` order")]
522    fn test_correct_expiration_with_time_in_force_gtd(audusd_sim: CurrencyPair) {
523        let _ = OrderTestBuilder::new(OrderType::Limit)
524            .instrument_id(audusd_sim.id)
525            .side(OrderSide::Buy)
526            .price(Price::from("0.8"))
527            .quantity(Quantity::from(1))
528            .time_in_force(TimeInForce::Gtd)
529            .build();
530    }
531}