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