Skip to main content

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}