nautilus_model/orders/
trailing_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::ops::{Deref, DerefMut};
17
18use indexmap::IndexMap;
19use nautilus_core::{UUID4, UnixNanos};
20use rust_decimal::Decimal;
21use serde::{Deserialize, Serialize};
22use ustr::Ustr;
23
24use super::{Order, OrderAny, OrderCore, OrderError};
25use crate::{
26    enums::{
27        ContingencyType, LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSide,
28        TimeInForce, TrailingOffsetType, TriggerType,
29    },
30    events::{OrderEventAny, OrderInitialized, OrderUpdated},
31    identifiers::{
32        AccountId, ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, PositionId,
33        StrategyId, Symbol, TradeId, TraderId, Venue, VenueOrderId,
34    },
35    types::{Currency, Money, Price, Quantity},
36};
37
38#[derive(Clone, Debug, Serialize, Deserialize)]
39#[cfg_attr(
40    feature = "python",
41    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
42)]
43pub struct TrailingStopLimitOrder {
44    core: OrderCore,
45    pub price: Price,
46    pub trigger_price: Price,
47    pub trigger_type: TriggerType,
48    pub limit_offset: Decimal,
49    pub trailing_offset: Decimal,
50    pub trailing_offset_type: TrailingOffsetType,
51    pub expire_time: Option<UnixNanos>,
52    pub is_post_only: bool,
53    pub display_qty: Option<Quantity>,
54    pub trigger_instrument_id: Option<InstrumentId>,
55    pub is_triggered: bool,
56    pub ts_triggered: Option<UnixNanos>,
57}
58
59impl TrailingStopLimitOrder {
60    /// Creates a new [`TrailingStopLimitOrder`] 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        limit_offset: Decimal,
73        trailing_offset: Decimal,
74        trailing_offset_type: TrailingOffsetType,
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::TrailingStopLimit,
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            Some(limit_offset),
114            Some(trailing_offset),
115            Some(trailing_offset_type),
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            limit_offset,
135            trailing_offset,
136            trailing_offset_type,
137            expire_time,
138            is_post_only: post_only,
139            display_qty,
140            trigger_instrument_id,
141            is_triggered: false,
142            ts_triggered: None,
143        }
144    }
145}
146
147impl Deref for TrailingStopLimitOrder {
148    type Target = OrderCore;
149
150    fn deref(&self) -> &Self::Target {
151        &self.core
152    }
153}
154
155impl DerefMut for TrailingStopLimitOrder {
156    fn deref_mut(&mut self) -> &mut Self::Target {
157        &mut self.core
158    }
159}
160
161impl Order for TrailingStopLimitOrder {
162    fn into_any(self) -> OrderAny {
163        OrderAny::TrailingStopLimit(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        Some(self.limit_offset)
268    }
269
270    fn trailing_offset(&self) -> Option<Decimal> {
271        Some(self.trailing_offset)
272    }
273
274    fn trailing_offset_type(&self) -> Option<TrailingOffsetType> {
275        Some(self.trailing_offset_type)
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 trade_ids(&self) -> Vec<&TradeId> {
367        self.trade_ids.iter().collect()
368    }
369
370    fn commissions(&self) -> &IndexMap<Currency, Money> {
371        &self.commissions
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        if let Some(price) = event.price {
391            self.price = price;
392        }
393
394        if let Some(trigger_price) = event.trigger_price {
395            self.trigger_price = trigger_price;
396        }
397
398        self.quantity = event.quantity;
399        self.leaves_qty = self.quantity - self.filled_qty;
400    }
401
402    fn is_triggered(&self) -> Option<bool> {
403        Some(self.is_triggered)
404    }
405
406    fn set_position_id(&mut self, position_id: Option<PositionId>) {
407        self.position_id = position_id;
408    }
409
410    fn set_quantity(&mut self, quantity: Quantity) {
411        self.quantity = quantity;
412    }
413
414    fn set_leaves_qty(&mut self, leaves_qty: Quantity) {
415        self.leaves_qty = leaves_qty;
416    }
417
418    fn set_emulation_trigger(&mut self, emulation_trigger: Option<TriggerType>) {
419        self.emulation_trigger = emulation_trigger;
420    }
421
422    fn set_is_quote_quantity(&mut self, is_quote_quantity: bool) {
423        self.is_quote_quantity = is_quote_quantity;
424    }
425
426    fn set_liquidity_side(&mut self, liquidity_side: LiquiditySide) {
427        self.liquidity_side = Some(liquidity_side)
428    }
429
430    fn would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
431        self.core.would_reduce_only(side, position_qty)
432    }
433
434    fn previous_status(&self) -> Option<OrderStatus> {
435        self.core.previous_status
436    }
437}
438
439impl From<OrderInitialized> for TrailingStopLimitOrder {
440    fn from(event: OrderInitialized) -> Self {
441        Self::new(
442            event.trader_id,
443            event.strategy_id,
444            event.instrument_id,
445            event.client_order_id,
446            event.order_side,
447            event.quantity,
448            event
449                .price // TODO: Improve this error, model order domain errors
450                .expect("Error initializing order: `price` was `None` for `TrailingStopLimitOrder`"),
451            event
452                .trigger_price // TODO: Improve this error, model order domain errors
453                .expect(
454                    "Error initializing order: `trigger_price` was `None` for `TrailingStopLimitOrder`",
455                ),
456            event
457                .trigger_type
458                .expect("Error initializing order: `trigger_type` was `None` for `TrailingStopLimitOrder`"),
459            event.limit_offset.unwrap(),  // TODO
460            event.trailing_offset.unwrap(),  // TODO
461            event.trailing_offset_type.unwrap(),  // TODO
462            event.time_in_force,
463            event.expire_time,
464            event.post_only,
465            event.reduce_only,
466            event.quote_quantity,
467            event.display_qty,
468            event.emulation_trigger,
469            event.trigger_instrument_id,
470            event.contingency_type,
471            event.order_list_id,
472            event.linked_order_ids,
473            event.parent_order_id,
474            event.exec_algorithm_id,
475            event.exec_algorithm_params,
476            event.exec_spawn_id,
477            event.tags,
478            event.event_id,
479            event.ts_event,
480        )
481    }
482}