Skip to main content

nautilus_model/python/orders/
trailing_stop_limit.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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 indexmap::IndexMap;
17use nautilus_core::{
18    UUID4, UnixNanos,
19    python::{
20        IntoPyObjectNautilusExt,
21        parsing::{
22            get_optional, get_optional_parsed, get_required, get_required_parsed,
23            get_required_string,
24        },
25        to_pyruntime_err, to_pyvalue_err,
26    },
27};
28use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
29use rust_decimal::Decimal;
30use ustr::Ustr;
31
32use crate::{
33    enums::{
34        ContingencyType, OrderSide, OrderStatus, OrderType, PositionSide, TimeInForce,
35        TrailingOffsetType, TriggerType,
36    },
37    events::order::initialized::OrderInitialized,
38    identifiers::{
39        ClientOrderId, ExecAlgorithmId, InstrumentId, OrderListId, StrategyId, TraderId,
40    },
41    orders::{Order, OrderCore, TrailingStopLimitOrder, str_indexmap_to_ustr},
42    python::{
43        common::commissions_from_indexmap,
44        events::order::{order_event_to_pyobject, pyobject_to_order_event},
45    },
46    types::{Currency, Money, Price, Quantity},
47};
48
49#[pymethods]
50#[pyo3_stub_gen::derive::gen_stub_pymethods]
51impl TrailingStopLimitOrder {
52    /// Creates a new `TrailingStopLimitOrder` instance.
53    #[new]
54    #[expect(clippy::too_many_arguments)]
55    #[pyo3(signature = (trader_id, strategy_id, instrument_id, client_order_id, order_side, quantity, price, trigger_price, trigger_type, limit_offset, trailing_offset, trailing_offset_type, time_in_force, post_only, reduce_only, quote_quantity, init_id, ts_init, activation_price=None, expire_time=None, display_qty=None, emulation_trigger=None, trigger_instrument_id=None, contingency_type=None, order_list_id=None, linked_order_ids=None, parent_order_id=None, exec_algorithm_id=None, exec_algorithm_params=None, exec_spawn_id=None, tags=None))]
56    fn py_new(
57        trader_id: TraderId,
58        strategy_id: StrategyId,
59        instrument_id: InstrumentId,
60        client_order_id: ClientOrderId,
61        order_side: OrderSide,
62        quantity: Quantity,
63        price: Option<Price>,
64        trigger_price: Option<Price>,
65        trigger_type: TriggerType,
66        limit_offset: Decimal,
67        trailing_offset: Decimal,
68        trailing_offset_type: TrailingOffsetType,
69        time_in_force: TimeInForce,
70        post_only: bool,
71        reduce_only: bool,
72        quote_quantity: bool,
73        init_id: UUID4,
74        ts_init: u64,
75        activation_price: Option<Price>,
76        expire_time: Option<u64>,
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<String, String>>,
86        exec_spawn_id: Option<ClientOrderId>,
87        tags: Option<Vec<String>>,
88    ) -> PyResult<Self> {
89        Self::new_checked(
90            trader_id,
91            strategy_id,
92            instrument_id,
93            client_order_id,
94            order_side,
95            quantity,
96            activation_price,
97            price,
98            trigger_price,
99            trigger_type,
100            limit_offset,
101            trailing_offset,
102            trailing_offset_type,
103            time_in_force,
104            expire_time.map(std::convert::Into::into),
105            post_only,
106            reduce_only,
107            quote_quantity,
108            display_qty,
109            emulation_trigger,
110            trigger_instrument_id,
111            contingency_type,
112            order_list_id,
113            linked_order_ids,
114            parent_order_id,
115            exec_algorithm_id,
116            exec_algorithm_params.map(str_indexmap_to_ustr),
117            exec_spawn_id,
118            tags.map(|vec| vec.into_iter().map(|s| Ustr::from(s.as_str())).collect()),
119            init_id,
120            ts_init.into(),
121        )
122        .map_err(to_pyvalue_err)
123    }
124
125    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
126        match op {
127            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
128            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
129            _ => py.NotImplemented(),
130        }
131    }
132
133    fn __repr__(&self) -> String {
134        self.to_string()
135    }
136
137    fn __str__(&self) -> String {
138        self.to_string()
139    }
140
141    #[staticmethod]
142    #[pyo3(name = "create")]
143    fn py_create(init: OrderInitialized) -> PyResult<Self> {
144        Self::try_from(init).map_err(to_pyvalue_err)
145    }
146
147    #[staticmethod]
148    #[pyo3(name = "opposite_side")]
149    fn py_opposite_side(side: OrderSide) -> OrderSide {
150        OrderCore::opposite_side(side)
151    }
152
153    #[staticmethod]
154    #[pyo3(name = "closing_side")]
155    fn py_closing_side(side: PositionSide) -> Option<OrderSide> {
156        OrderCore::closing_side(side)
157    }
158
159    #[getter]
160    #[pyo3(name = "status")]
161    fn py_status(&self) -> OrderStatus {
162        self.status
163    }
164
165    #[getter]
166    #[pyo3(name = "trader_id")]
167    fn py_trader_id(&self) -> TraderId {
168        self.trader_id
169    }
170
171    #[getter]
172    #[pyo3(name = "strategy_id")]
173    fn py_strategy_id(&self) -> StrategyId {
174        self.strategy_id
175    }
176
177    #[getter]
178    #[pyo3(name = "instrument_id")]
179    fn py_instrument_id(&self) -> InstrumentId {
180        self.instrument_id
181    }
182
183    #[getter]
184    #[pyo3(name = "client_order_id")]
185    fn py_client_order_id(&self) -> ClientOrderId {
186        self.client_order_id
187    }
188
189    #[getter]
190    #[pyo3(name = "side")]
191    fn py_order_side(&self) -> OrderSide {
192        self.side
193    }
194
195    #[getter]
196    #[pyo3(name = "quantity")]
197    fn py_quantity(&self) -> Quantity {
198        self.quantity
199    }
200
201    #[getter]
202    #[pyo3(name = "price")]
203    fn py_price(&self) -> Option<Price> {
204        self.price
205    }
206
207    #[getter]
208    #[pyo3(name = "trigger_price")]
209    fn py_trigger_price(&self) -> Option<Price> {
210        self.trigger_price
211    }
212
213    #[getter]
214    #[pyo3(name = "trigger_type")]
215    fn py_trigger_type(&self) -> TriggerType {
216        self.trigger_type
217    }
218
219    #[getter]
220    #[pyo3(name = "limit_offset")]
221    fn py_limit_offset(&self) -> Decimal {
222        self.limit_offset
223    }
224
225    #[getter]
226    #[pyo3(name = "trailing_offset")]
227    fn py_trailing_offset(&self) -> Decimal {
228        self.trailing_offset
229    }
230
231    #[getter]
232    #[pyo3(name = "trailing_offset_type")]
233    fn py_trailing_offset_type(&self) -> TrailingOffsetType {
234        self.trailing_offset_type
235    }
236
237    #[getter]
238    #[pyo3(name = "activation_price")]
239    fn py_activation_price(&self) -> Option<Price> {
240        self.activation_price
241    }
242
243    #[getter]
244    #[pyo3(name = "order_type")]
245    fn py_order_type(&self) -> OrderType {
246        self.order_type
247    }
248
249    #[getter]
250    #[pyo3(name = "time_in_force")]
251    fn py_time_in_force(&self) -> TimeInForce {
252        self.time_in_force
253    }
254
255    #[getter]
256    #[pyo3(name = "expire_time")]
257    fn py_expire_time(&self) -> Option<u64> {
258        self.expire_time.map(std::convert::Into::into)
259    }
260
261    #[getter]
262    #[pyo3(name = "init_id")]
263    fn py_init_id(&self) -> UUID4 {
264        self.init_id
265    }
266
267    #[getter]
268    #[pyo3(name = "ts_init")]
269    fn py_ts_init(&self) -> u64 {
270        self.ts_init.as_u64()
271    }
272
273    #[getter]
274    #[pyo3(name = "init_event")]
275    fn py_init_event(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
276        match self.init_event() {
277            Some(event) => order_event_to_pyobject(py, event),
278            None => Ok(py.None()),
279        }
280    }
281
282    #[getter]
283    #[pyo3(name = "has_price")]
284    fn py_has_price(&self) -> bool {
285        self.price.is_some()
286    }
287
288    #[getter]
289    #[pyo3(name = "has_trigger_price")]
290    fn py_has_trigger_price(&self) -> bool {
291        true
292    }
293
294    #[getter]
295    #[pyo3(name = "is_passive")]
296    fn py_is_passive(&self) -> bool {
297        self.is_passive()
298    }
299
300    #[getter]
301    #[pyo3(name = "is_aggressive")]
302    fn py_is_aggressive(&self) -> bool {
303        self.is_aggressive()
304    }
305
306    #[getter]
307    #[pyo3(name = "is_closed")]
308    fn py_is_closed(&self) -> bool {
309        self.is_closed()
310    }
311
312    #[getter]
313    #[pyo3(name = "is_open")]
314    fn py_is_open(&self) -> bool {
315        self.is_open()
316    }
317
318    #[getter]
319    #[pyo3(name = "is_post_only")]
320    fn py_post_only(&self) -> bool {
321        self.is_post_only
322    }
323
324    #[getter]
325    #[pyo3(name = "is_reduce_only")]
326    fn py_reduce_only(&self) -> bool {
327        self.is_reduce_only
328    }
329
330    #[getter]
331    #[pyo3(name = "is_quote_quantity")]
332    fn py_quote_quantity(&self) -> bool {
333        self.is_quote_quantity
334    }
335
336    #[getter]
337    #[pyo3(name = "display_qty")]
338    fn py_display_qty(&self) -> Option<Quantity> {
339        self.display_qty
340    }
341
342    #[getter]
343    #[pyo3(name = "emulation_trigger")]
344    fn py_emulation_trigger(&self) -> Option<TriggerType> {
345        self.emulation_trigger
346    }
347
348    #[getter]
349    #[pyo3(name = "trigger_instrument_id")]
350    fn py_trigger_instrument_id(&self) -> Option<InstrumentId> {
351        self.trigger_instrument_id
352    }
353
354    #[getter]
355    #[pyo3(name = "contingency_type")]
356    fn py_contingency_type(&self) -> Option<ContingencyType> {
357        self.contingency_type
358    }
359
360    #[getter]
361    #[pyo3(name = "order_list_id")]
362    fn py_order_list_id(&self) -> Option<OrderListId> {
363        self.order_list_id
364    }
365
366    #[getter]
367    #[pyo3(name = "linked_order_ids")]
368    fn py_linked_order_ids(&self) -> Option<Vec<ClientOrderId>> {
369        self.linked_order_ids.clone()
370    }
371
372    #[getter]
373    #[pyo3(name = "parent_order_id")]
374    fn py_parent_order_id(&self) -> Option<ClientOrderId> {
375        self.parent_order_id
376    }
377
378    #[getter]
379    #[pyo3(name = "exec_algorithm_id")]
380    fn py_exec_algorithm_id(&self) -> Option<ExecAlgorithmId> {
381        self.exec_algorithm_id
382    }
383
384    #[getter]
385    #[pyo3(name = "exec_algorithm_params")]
386    fn py_exec_algorithm_params(&self) -> Option<IndexMap<&str, &str>> {
387        self.exec_algorithm_params
388            .as_ref()
389            .map(|x| x.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect())
390    }
391
392    #[getter]
393    #[pyo3(name = "exec_spawn_id")]
394    fn py_exec_spawn_id(&self) -> Option<ClientOrderId> {
395        self.exec_spawn_id
396    }
397
398    #[getter]
399    #[pyo3(name = "account_id")]
400    fn py_account_id(&self) -> Option<crate::identifiers::AccountId> {
401        self.account_id
402    }
403
404    #[getter]
405    #[pyo3(name = "tags")]
406    fn py_tags(&self) -> Option<Vec<&str>> {
407        self.tags
408            .as_ref()
409            .map(|vec| vec.iter().map(Ustr::as_str).collect())
410    }
411
412    #[pyo3(name = "commission")]
413    fn py_commission(&self, currency: &Currency) -> Option<Money> {
414        self.commission(currency)
415    }
416
417    #[pyo3(name = "commissions")]
418    fn py_commissions(&self) -> IndexMap<Currency, Money> {
419        self.commissions().clone()
420    }
421
422    #[pyo3(name = "events")]
423    fn py_events(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
424        self.events()
425            .into_iter()
426            .map(|event| order_event_to_pyobject(py, event.clone()))
427            .collect()
428    }
429
430    #[pyo3(name = "signed_decimal_qty")]
431    fn py_signed_decimal_qty(&self) -> Decimal {
432        self.signed_decimal_qty()
433    }
434
435    #[pyo3(name = "would_reduce_only")]
436    fn py_would_reduce_only(&self, side: PositionSide, position_qty: Quantity) -> bool {
437        self.would_reduce_only(side, position_qty)
438    }
439
440    #[pyo3(name = "apply")]
441    fn py_apply(&mut self, event: Py<PyAny>, py: Python<'_>) -> PyResult<()> {
442        let event_any = pyobject_to_order_event(py, event)?;
443        self.apply(event_any).map_err(to_pyruntime_err)
444    }
445
446    #[staticmethod]
447    #[pyo3(name = "from_dict")]
448    fn py_from_dict(values: &Bound<'_, PyDict>) -> PyResult<Self> {
449        let trader_id = TraderId::from(get_required_string(values, "trader_id")?.as_str());
450        let strategy_id = StrategyId::from(get_required_string(values, "strategy_id")?.as_str());
451        let instrument_id = InstrumentId::from(get_required_string(values, "instrument_id")?);
452        let client_order_id =
453            ClientOrderId::from(get_required_string(values, "client_order_id")?.as_str());
454        let order_side = get_required_parsed(values, "side", |s| {
455            s.parse::<OrderSide>().map_err(|e| e.to_string())
456        })?;
457        let quantity = Quantity::from(get_required_string(values, "quantity")?.as_str());
458        let activation_price =
459            get_optional_parsed(values, "activation_price", |s| Ok(Price::from(s.as_str())))?;
460        let price = get_optional_parsed(values, "price", |s| Ok(Price::from(s.as_str())))?;
461        let trigger_price =
462            get_optional_parsed(values, "trigger_price", |s| Ok(Price::from(s.as_str())))?;
463        let trigger_type = get_required_parsed(values, "trigger_type", |s| {
464            s.parse::<TriggerType>().map_err(|e| e.to_string())
465        })?;
466        let limit_offset = get_required_parsed(values, "limit_offset", |s| {
467            s.parse::<Decimal>().map_err(|e| e.to_string())
468        })?;
469        let trailing_offset = get_required_parsed(values, "trailing_offset", |s| {
470            s.parse::<Decimal>().map_err(|e| e.to_string())
471        })?;
472        let trailing_offset_type = get_required_parsed(values, "trailing_offset_type", |s| {
473            s.parse::<TrailingOffsetType>().map_err(|e| e.to_string())
474        })?;
475        let time_in_force = get_required_parsed(values, "time_in_force", |s| {
476            s.parse::<TimeInForce>().map_err(|e| e.to_string())
477        })?;
478        let post_only = get_required::<bool>(values, "is_post_only")?;
479        let reduce_only = get_required::<bool>(values, "is_reduce_only")?;
480        let quote_quantity = get_required::<bool>(values, "is_quote_quantity")?;
481        let expire_time = get_optional::<u64>(values, "expire_time_ns")?.map(UnixNanos::from);
482        let display_quantity =
483            get_optional_parsed(values, "display_qty", |s| Ok(Quantity::from(s.as_str())))?;
484        let emulation_trigger = get_optional_parsed(values, "emulation_trigger", |s| {
485            s.parse::<TriggerType>().map_err(|e| e.to_string())
486        })?;
487        let trigger_instrument_id = get_optional_parsed(values, "trigger_instrument_id", |s| {
488            s.parse::<InstrumentId>().map_err(|e| e.to_string())
489        })?;
490        let contingency_type = get_optional_parsed(values, "contingency_type", |s| {
491            s.parse::<ContingencyType>().map_err(|e| e.to_string())
492        })?;
493        let order_list_id = get_optional_parsed(values, "order_list_id", |s| {
494            Ok(OrderListId::from(s.as_str()))
495        })?;
496        let linked_order_ids =
497            get_optional::<Vec<String>>(values, "linked_order_ids")?.map(|vec| {
498                vec.iter()
499                    .map(|s| ClientOrderId::from(s.as_str()))
500                    .collect()
501            });
502        let parent_order_id = get_optional_parsed(values, "parent_order_id", |s| {
503            Ok(ClientOrderId::from(s.as_str()))
504        })?;
505        let exec_algorithm_id = get_optional_parsed(values, "exec_algorithm_id", |s| {
506            Ok(ExecAlgorithmId::from(s.as_str()))
507        })?;
508        let exec_algorithm_params =
509            get_optional::<IndexMap<String, String>>(values, "exec_algorithm_params")?
510                .map(str_indexmap_to_ustr);
511        let exec_spawn_id = get_optional_parsed(values, "exec_spawn_id", |s| {
512            Ok(ClientOrderId::from(s.as_str()))
513        })?;
514        let tags = get_optional::<Vec<String>>(values, "tags")?
515            .map(|vec| vec.iter().map(|s| Ustr::from(s)).collect());
516        let init_id = get_required_parsed(values, "init_id", |s| s.parse::<UUID4>())?;
517        let ts_init = get_required::<u64>(values, "ts_init")?;
518        let order = Self::new_checked(
519            trader_id,
520            strategy_id,
521            instrument_id,
522            client_order_id,
523            order_side,
524            quantity,
525            activation_price,
526            price,
527            trigger_price,
528            trigger_type,
529            limit_offset,
530            trailing_offset,
531            trailing_offset_type,
532            time_in_force,
533            expire_time,
534            post_only,
535            reduce_only,
536            quote_quantity,
537            display_quantity,
538            emulation_trigger,
539            trigger_instrument_id,
540            contingency_type,
541            order_list_id,
542            linked_order_ids,
543            parent_order_id,
544            exec_algorithm_id,
545            exec_algorithm_params,
546            exec_spawn_id,
547            tags,
548            init_id,
549            ts_init.into(),
550        )
551        .map_err(to_pyvalue_err)?;
552        Ok(order)
553    }
554
555    #[pyo3(name = "to_dict")]
556    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
557        let dict = PyDict::new(py);
558        dict.set_item("trader_id", self.trader_id.to_string())?;
559        dict.set_item("strategy_id", self.strategy_id.to_string())?;
560        dict.set_item("instrument_id", self.instrument_id.to_string())?;
561        dict.set_item("client_order_id", self.client_order_id.to_string())?;
562        dict.set_item("side", self.side.to_string())?;
563        dict.set_item("type", self.order_type.to_string())?;
564        dict.set_item("quantity", self.quantity.to_string())?;
565        dict.set_item("status", self.status.to_string())?;
566        self.price.map_or_else(
567            || dict.set_item("price", py.None()),
568            |x| dict.set_item("price", x.to_string()),
569        )?;
570        self.trigger_price.map_or_else(
571            || dict.set_item("trigger_price", py.None()),
572            |x| dict.set_item("trigger_price", x.to_string()),
573        )?;
574        dict.set_item("trigger_type", self.trigger_type.to_string())?;
575        dict.set_item("limit_offset", self.limit_offset.to_string())?;
576        dict.set_item("trailing_offset", self.trailing_offset.to_string())?;
577        dict.set_item(
578            "trailing_offset_type",
579            self.trailing_offset_type.to_string(),
580        )?;
581        dict.set_item("filled_qty", self.filled_qty.to_string())?;
582        dict.set_item("time_in_force", self.time_in_force.to_string())?;
583        dict.set_item("is_post_only", self.is_post_only)?;
584        dict.set_item("is_reduce_only", self.is_reduce_only)?;
585        dict.set_item("is_quote_quantity", self.is_quote_quantity)?;
586        dict.set_item("init_id", self.init_id.to_string())?;
587        dict.set_item(
588            "expire_time_ns",
589            self.expire_time.filter(|&t| t != 0).map(|t| t.as_u64()),
590        )?;
591        dict.set_item("ts_init", self.ts_init.as_u64())?;
592        dict.set_item("ts_last", self.ts_last.as_u64())?;
593        dict.set_item(
594            "commissions",
595            commissions_from_indexmap(py, self.commissions())?,
596        )?;
597        self.activation_price.map_or_else(
598            || dict.set_item("activation_price", py.None()),
599            |x| dict.set_item("activation_price", x.to_string()),
600        )?;
601        self.last_trade_id.map_or_else(
602            || dict.set_item("last_trade_id", py.None()),
603            |x| dict.set_item("last_trade_id", x.to_string()),
604        )?;
605        self.avg_px.map_or_else(
606            || dict.set_item("avg_px", py.None()),
607            |x| dict.set_item("avg_px", x.to_string()),
608        )?;
609        self.position_id.map_or_else(
610            || dict.set_item("position_id", py.None()),
611            |x| dict.set_item("position_id", x.to_string()),
612        )?;
613        self.liquidity_side.map_or_else(
614            || dict.set_item("liquidity_side", py.None()),
615            |x| dict.set_item("liquidity_side", x.to_string()),
616        )?;
617        self.slippage.map_or_else(
618            || dict.set_item("slippage", py.None()),
619            |x| dict.set_item("slippage", x.to_string()),
620        )?;
621        self.account_id.map_or_else(
622            || dict.set_item("account_id", py.None()),
623            |x| dict.set_item("account_id", x.to_string()),
624        )?;
625        self.venue_order_id.map_or_else(
626            || dict.set_item("venue_order_id", py.None()),
627            |x| dict.set_item("venue_order_id", x.to_string()),
628        )?;
629        self.display_qty.map_or_else(
630            || dict.set_item("display_qty", py.None()),
631            |x| dict.set_item("display_qty", x.to_string()),
632        )?;
633        self.emulation_trigger.map_or_else(
634            || dict.set_item("emulation_trigger", py.None()),
635            |x| dict.set_item("emulation_trigger", x.to_string()),
636        )?;
637        self.trigger_instrument_id.map_or_else(
638            || dict.set_item("trigger_instrument_id", py.None()),
639            |x| dict.set_item("trigger_instrument_id", x.to_string()),
640        )?;
641        self.contingency_type.map_or_else(
642            || dict.set_item("contingency_type", py.None()),
643            |x| dict.set_item("contingency_type", x.to_string()),
644        )?;
645        self.order_list_id.map_or_else(
646            || dict.set_item("order_list_id", py.None()),
647            |x| dict.set_item("order_list_id", x.to_string()),
648        )?;
649        dict.set_item(
650            "linked_order_ids",
651            self.linked_order_ids
652                .as_ref()
653                .map(|x| x.iter().map(ToString::to_string).collect::<Vec<String>>()),
654        )?;
655        self.parent_order_id.map_or_else(
656            || dict.set_item("parent_order_id", py.None()),
657            |x| dict.set_item("parent_order_id", x.to_string()),
658        )?;
659        self.exec_algorithm_id.map_or_else(
660            || dict.set_item("exec_algorithm_id", py.None()),
661            |x| dict.set_item("exec_algorithm_id", x.to_string()),
662        )?;
663        dict.set_item(
664            "exec_algorithm_params",
665            self.exec_algorithm_params.as_ref().map(|x| {
666                x.iter()
667                    .map(|(k, v)| (k.to_string(), v.to_string()))
668                    .collect::<IndexMap<String, String>>()
669            }),
670        )?;
671        self.exec_spawn_id.map_or_else(
672            || dict.set_item("exec_spawn_id", py.None()),
673            |x| dict.set_item("exec_spawn_id", x.to_string()),
674        )?;
675        dict.set_item(
676            "tags",
677            self.tags
678                .as_ref()
679                .map(|vec| vec.iter().map(ToString::to_string).collect::<Vec<String>>()),
680        )?;
681        Ok(dict.into())
682    }
683}