nautilus_model/orders/
stop_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::{UUID4, UnixNanos};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Serialize};
25use ustr::Ustr;
26
27use super::{
28    any::OrderAny,
29    base::{Order, OrderCore, OrderError},
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    types::{Price, Quantity},
42};
43
44#[derive(Clone, Debug, Serialize, Deserialize)]
45#[cfg_attr(
46    feature = "python",
47    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
48)]
49pub struct StopLimitOrder {
50    pub price: Price,
51    pub trigger_price: Price,
52    pub trigger_type: TriggerType,
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    pub is_triggered: bool,
58    pub ts_triggered: Option<UnixNanos>,
59    core: OrderCore,
60}
61
62impl StopLimitOrder {
63    /// Creates a new [`StopLimitOrder`] instance.
64    #[allow(clippy::too_many_arguments)]
65    pub fn new(
66        trader_id: TraderId,
67        strategy_id: StrategyId,
68        instrument_id: InstrumentId,
69        client_order_id: ClientOrderId,
70        order_side: OrderSide,
71        quantity: Quantity,
72        price: Price,
73        trigger_price: Price,
74        trigger_type: TriggerType,
75        time_in_force: TimeInForce,
76        expire_time: Option<UnixNanos>,
77        post_only: bool,
78        reduce_only: bool,
79        quote_quantity: bool,
80        display_qty: Option<Quantity>,
81        emulation_trigger: Option<TriggerType>,
82        trigger_instrument_id: Option<InstrumentId>,
83        contingency_type: Option<ContingencyType>,
84        order_list_id: Option<OrderListId>,
85        linked_order_ids: Option<Vec<ClientOrderId>>,
86        parent_order_id: Option<ClientOrderId>,
87        exec_algorithm_id: Option<ExecAlgorithmId>,
88        exec_algorithm_params: Option<IndexMap<Ustr, Ustr>>,
89        exec_spawn_id: Option<ClientOrderId>,
90        tags: Option<Vec<Ustr>>,
91        init_id: UUID4,
92        ts_init: UnixNanos,
93    ) -> Self {
94        let init_order = OrderInitialized::new(
95            trader_id,
96            strategy_id,
97            instrument_id,
98            client_order_id,
99            order_side,
100            OrderType::StopLimit,
101            quantity,
102            time_in_force,
103            post_only,
104            reduce_only,
105            quote_quantity,
106            false,
107            init_id,
108            ts_init,
109            ts_init,
110            Some(price),
111            Some(trigger_price),
112            Some(trigger_type),
113            None,
114            None,
115            None,
116            expire_time,
117            display_qty,
118            emulation_trigger,
119            trigger_instrument_id,
120            contingency_type,
121            order_list_id,
122            linked_order_ids,
123            parent_order_id,
124            exec_algorithm_id,
125            exec_algorithm_params,
126            exec_spawn_id,
127            tags,
128        );
129        Self {
130            core: OrderCore::new(init_order),
131            price,
132            trigger_price,
133            trigger_type,
134            expire_time,
135            is_post_only: post_only,
136            display_qty,
137            trigger_instrument_id,
138            is_triggered: false,
139            ts_triggered: None,
140        }
141    }
142}
143
144impl Deref for StopLimitOrder {
145    type Target = OrderCore;
146
147    fn deref(&self) -> &Self::Target {
148        &self.core
149    }
150}
151
152impl DerefMut for StopLimitOrder {
153    fn deref_mut(&mut self) -> &mut Self::Target {
154        &mut self.core
155    }
156}
157
158impl PartialEq for StopLimitOrder {
159    fn eq(&self, other: &Self) -> bool {
160        self.client_order_id == other.client_order_id
161    }
162}
163
164impl Order for StopLimitOrder {
165    fn into_any(self) -> OrderAny {
166        OrderAny::StopLimit(self)
167    }
168
169    fn status(&self) -> OrderStatus {
170        self.status
171    }
172
173    fn trader_id(&self) -> TraderId {
174        self.trader_id
175    }
176
177    fn strategy_id(&self) -> StrategyId {
178        self.strategy_id
179    }
180
181    fn instrument_id(&self) -> InstrumentId {
182        self.instrument_id
183    }
184
185    fn symbol(&self) -> Symbol {
186        self.instrument_id.symbol
187    }
188
189    fn venue(&self) -> Venue {
190        self.instrument_id.venue
191    }
192
193    fn client_order_id(&self) -> ClientOrderId {
194        self.client_order_id
195    }
196
197    fn venue_order_id(&self) -> Option<VenueOrderId> {
198        self.venue_order_id
199    }
200
201    fn position_id(&self) -> Option<PositionId> {
202        self.position_id
203    }
204
205    fn account_id(&self) -> Option<AccountId> {
206        self.account_id
207    }
208
209    fn last_trade_id(&self) -> Option<TradeId> {
210        self.last_trade_id
211    }
212
213    fn side(&self) -> OrderSide {
214        self.side
215    }
216
217    fn order_type(&self) -> OrderType {
218        self.order_type
219    }
220
221    fn quantity(&self) -> Quantity {
222        self.quantity
223    }
224
225    fn time_in_force(&self) -> TimeInForce {
226        self.time_in_force
227    }
228
229    fn expire_time(&self) -> Option<UnixNanos> {
230        self.expire_time
231    }
232
233    fn price(&self) -> Option<Price> {
234        Some(self.price)
235    }
236
237    fn trigger_price(&self) -> Option<Price> {
238        Some(self.trigger_price)
239    }
240
241    fn trigger_type(&self) -> Option<TriggerType> {
242        Some(self.trigger_type)
243    }
244
245    fn liquidity_side(&self) -> Option<LiquiditySide> {
246        self.liquidity_side
247    }
248
249    fn is_post_only(&self) -> bool {
250        self.is_post_only
251    }
252
253    fn is_reduce_only(&self) -> bool {
254        self.is_reduce_only
255    }
256
257    fn is_quote_quantity(&self) -> bool {
258        self.is_quote_quantity
259    }
260
261    fn display_qty(&self) -> Option<Quantity> {
262        self.display_qty
263    }
264
265    fn limit_offset(&self) -> Option<Decimal> {
266        None
267    }
268
269    fn trailing_offset(&self) -> Option<Decimal> {
270        None
271    }
272
273    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
274        None
275    }
276
277    fn emulation_trigger(&self) -> Option<TriggerType> {
278        self.emulation_trigger
279    }
280
281    fn trigger_instrument_id(&self) -> Option<InstrumentId> {
282        self.trigger_instrument_id
283    }
284
285    fn contingency_type(&self) -> Option<ContingencyType> {
286        self.contingency_type
287    }
288
289    fn order_list_id(&self) -> Option<OrderListId> {
290        self.order_list_id
291    }
292
293    fn linked_order_ids(&self) -> Option<&[ClientOrderId]> {
294        self.linked_order_ids.as_deref()
295    }
296
297    fn parent_order_id(&self) -> Option<ClientOrderId> {
298        self.parent_order_id
299    }
300
301    fn exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
302        self.exec_algorithm_id
303    }
304
305    fn exec_algorithm_params(&self) -> Option<&IndexMap<Ustr, Ustr>> {
306        self.exec_algorithm_params.as_ref()
307    }
308
309    fn exec_spawn_id(&self) -> Option<ClientOrderId> {
310        self.exec_spawn_id
311    }
312
313    fn tags(&self) -> Option<&[Ustr]> {
314        self.tags.as_deref()
315    }
316
317    fn filled_qty(&self) -> Quantity {
318        self.filled_qty
319    }
320
321    fn leaves_qty(&self) -> Quantity {
322        self.leaves_qty
323    }
324
325    fn avg_px(&self) -> Option<f64> {
326        self.avg_px
327    }
328
329    fn slippage(&self) -> Option<f64> {
330        self.slippage
331    }
332
333    fn init_id(&self) -> UUID4 {
334        self.init_id
335    }
336
337    fn ts_init(&self) -> UnixNanos {
338        self.ts_init
339    }
340
341    fn ts_last(&self) -> UnixNanos {
342        self.ts_last
343    }
344
345    fn events(&self) -> Vec<&OrderEventAny> {
346        self.events.iter().collect()
347    }
348
349    fn venue_order_ids(&self) -> Vec<&VenueOrderId> {
350        self.venue_order_ids.iter().collect()
351    }
352
353    fn trade_ids(&self) -> Vec<&TradeId> {
354        self.trade_ids.iter().collect()
355    }
356
357    fn apply(&mut self, event: OrderEventAny) -> Result<(), OrderError> {
358        if let OrderEventAny::Updated(ref event) = event {
359            self.update(event);
360        };
361        let is_order_filled = matches!(event, OrderEventAny::Filled(_));
362
363        self.core.apply(event)?;
364
365        if is_order_filled {
366            self.core.set_slippage(self.price);
367        };
368
369        Ok(())
370    }
371
372    fn update(&mut self, event: &OrderUpdated) {
373        self.quantity = event.quantity;
374
375        if let Some(price) = event.price {
376            self.price = price;
377        }
378
379        if let Some(trigger_price) = event.trigger_price {
380            self.trigger_price = trigger_price;
381        }
382
383        self.quantity = event.quantity;
384        self.leaves_qty = self.quantity - self.filled_qty;
385    }
386}
387
388impl From<OrderAny> for StopLimitOrder {
389    fn from(order: OrderAny) -> StopLimitOrder {
390        match order {
391            OrderAny::StopLimit(order) => order,
392            _ => {
393                panic!(
394                    "Invalid `OrderAny` not `{}`, was {order:?}",
395                    stringify!(StopLimitOrder),
396                )
397            }
398        }
399    }
400}
401
402impl From<OrderInitialized> for StopLimitOrder {
403    fn from(event: OrderInitialized) -> Self {
404        Self::new(
405            event.trader_id,
406            event.strategy_id,
407            event.instrument_id,
408            event.client_order_id,
409            event.order_side,
410            event.quantity,
411            event
412                .price // TODO: Improve this error, model order domain errors
413                .expect("Error initializing order: `price` was `None` for `StopLimitOrder"),
414            event
415                .trigger_price // TODO: Improve this error, model order domain errors
416                .expect("Error initializing order: `trigger_price` was `None` for `StopLimitOrder"),
417            event
418                .trigger_type
419                .expect("Error initializing order: `trigger_type` was `None`"),
420            event.time_in_force,
421            event.expire_time,
422            event.post_only,
423            event.reduce_only,
424            event.quote_quantity,
425            event.display_qty,
426            event.emulation_trigger,
427            event.trigger_instrument_id,
428            event.contingency_type,
429            event.order_list_id,
430            event.linked_order_ids,
431            event.parent_order_id,
432            event.exec_algorithm_id,
433            event.exec_algorithm_params,
434            event.exec_spawn_id,
435            event.tags,
436            event.event_id,
437            event.ts_event,
438        )
439    }
440}
441
442impl Display for StopLimitOrder {
443    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444        write!(
445            f,
446            "StopLimitOrder({} {} {} {} @ {}-STOP[{}] {}-LIMIT {}, status={}, client_order_id={}, venue_order_id={}, position_id={}, tags={})",
447            self.side,
448            self.quantity.to_formatted_string(),
449            self.instrument_id,
450            self.order_type,
451            self.trigger_price,
452            self.trigger_type,
453            self.price,
454            self.time_in_force,
455            self.status,
456            self.client_order_id,
457            self.venue_order_id
458                .map_or("None".to_string(), |venue_order_id| format!(
459                    "{venue_order_id}"
460                )),
461            self.position_id
462                .map_or("None".to_string(), |position_id| format!("{position_id}")),
463            self.tags.clone().map_or("None".to_string(), |tags| tags
464                .iter()
465                .map(|s| s.to_string())
466                .collect::<Vec<String>>()
467                .join(", ")),
468        )
469    }
470}