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