nautilus_dydx/python/encoder.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
16//! Python bindings for dYdX ClientOrderId encoder.
17
18#![allow(clippy::missing_errors_doc)]
19
20use std::sync::Arc;
21
22use nautilus_core::python::to_pyruntime_err;
23use nautilus_model::identifiers::ClientOrderId;
24use pyo3::prelude::*;
25
26use crate::execution::encoder::ClientOrderIdEncoder;
27
28/// Python wrapper for the ClientOrderIdEncoder.
29///
30/// Provides bidirectional encoding of Nautilus ClientOrderId strings to
31/// dYdX's (client_id, client_metadata) u32 pair.
32#[pyclass(name = "DydxClientOrderIdEncoder")]
33#[derive(Debug)]
34pub struct PyDydxClientOrderIdEncoder {
35 inner: Arc<ClientOrderIdEncoder>,
36}
37
38impl PyDydxClientOrderIdEncoder {
39 /// Creates a Python encoder wrapping an existing shared `Arc<ClientOrderIdEncoder>`.
40 pub fn from_arc(inner: Arc<ClientOrderIdEncoder>) -> Self {
41 Self { inner }
42 }
43}
44
45#[pymethods]
46impl PyDydxClientOrderIdEncoder {
47 /// Create a new ClientOrderIdEncoder.
48 #[new]
49 fn new() -> Self {
50 Self {
51 inner: Arc::new(ClientOrderIdEncoder::new()),
52 }
53 }
54
55 /// Encode a ClientOrderId string to (client_id, client_metadata) tuple.
56 ///
57 /// # Encoding Rules
58 ///
59 /// 1. Numeric IDs (e.g., "12345"): Returns `(12345, 4)` for backward compatibility
60 /// 2. O-format IDs (e.g., "O-20260131-174827-001-001-1"): Deterministically encoded
61 /// 3. Other formats: Sequential allocation with in-memory mapping
62 ///
63 /// # Errors
64 ///
65 /// Returns an error if the encoder's sequential counter overflows.
66 #[pyo3(name = "encode")]
67 fn py_encode(&self, client_order_id: &str) -> PyResult<(u32, u32)> {
68 let id = ClientOrderId::from(client_order_id);
69 let encoded = self.inner.encode(id).map_err(to_pyruntime_err)?;
70 Ok((encoded.client_id, encoded.client_metadata))
71 }
72
73 /// Decode (client_id, client_metadata) back to the original ClientOrderId string.
74 ///
75 /// # Decoding Rules
76 ///
77 /// 1. If `client_metadata == 4`: Returns numeric string (legacy format)
78 /// 2. If sequential allocation marker: Looks up in reverse mapping
79 /// 3. Otherwise: Decodes as O-format using timestamp + identity bits
80 ///
81 /// Returns `None` if decoding fails (e.g., sequential ID not in cache after restart).
82 #[pyo3(name = "decode")]
83 fn py_decode(&self, client_id: u32, client_metadata: u32) -> Option<String> {
84 self.inner
85 .decode(client_id, client_metadata)
86 .map(|id| id.to_string())
87 }
88
89 /// Get the encoded pair for a ClientOrderId without allocating a new mapping.
90 ///
91 /// Returns `None` if the ID is not in the cache and is not a deterministic format
92 /// (numeric or O-format).
93 #[pyo3(name = "get")]
94 fn py_get(&self, client_order_id: &str) -> Option<(u32, u32)> {
95 let id = ClientOrderId::from(client_order_id);
96 self.inner
97 .get(&id)
98 .map(|encoded| (encoded.client_id, encoded.client_metadata))
99 }
100
101 /// Remove the mapping for a given encoded pair.
102 ///
103 /// Returns the original ClientOrderId string if it was mapped.
104 /// For deterministic formats, this returns the decoded value but doesn't
105 /// actually remove anything (since they don't use in-memory mappings).
106 #[pyo3(name = "remove")]
107 fn py_remove(&self, client_id: u32, client_metadata: u32) -> Option<String> {
108 self.inner
109 .remove(client_id, client_metadata)
110 .map(|id| id.to_string())
111 }
112
113 /// Update the mapping for a ClientOrderId after order modification.
114 ///
115 /// Returns the current sequential counter value (for debugging/monitoring).
116 #[pyo3(name = "current_counter")]
117 fn py_current_counter(&self) -> u32 {
118 self.inner.current_counter()
119 }
120
121 /// Returns the number of non-deterministic mappings currently stored.
122 #[pyo3(name = "len")]
123 fn py_len(&self) -> usize {
124 self.inner.len()
125 }
126
127 /// Returns true if no non-deterministic mappings are stored.
128 #[pyo3(name = "is_empty")]
129 fn py_is_empty(&self) -> bool {
130 self.inner.is_empty()
131 }
132
133 fn __repr__(&self) -> String {
134 format!(
135 "DydxClientOrderIdEncoder(counter={}, mappings={})",
136 self.inner.current_counter(),
137 self.inner.len()
138 )
139 }
140}