Skip to main content

nautilus_deribit/python/
websocket.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 the Deribit WebSocket client.
17//!
18//! # Design Pattern: Clone and Share State
19//!
20//! The WebSocket client must be cloned for async operations because PyO3's `future_into_py`
21//! requires `'static` futures (cannot borrow from `self`). To ensure clones share the same
22//! connection state, key fields use `Arc<RwLock<T>>`:
23//!
24//! - Connection mode and signal are shared via Arc.
25//!
26//! ## Connection Flow
27//!
28//! 1. Clone the client for async operation.
29//! 2. Connect and populate shared state on the clone.
30//! 3. Spawn stream handler as background task.
31//! 4. Return immediately (non-blocking).
32//!
33//! ## Important Notes
34//!
35//! - Never use `block_on()` - it blocks the runtime.
36//! - Always clone before async blocks for lifetime requirements.
37
38use futures_util::StreamExt;
39use nautilus_common::live::get_runtime;
40use nautilus_core::python::{call_python_threadsafe, to_pyruntime_err, to_pyvalue_err};
41use nautilus_model::{
42    data::{BarType, Data, OrderBookDeltas_API},
43    enums::{OrderSide, OrderType, TimeInForce, TriggerType},
44    identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
45    python::{
46        data::data_to_pycapsule,
47        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
48    },
49    types::{Price, Quantity},
50};
51use nautilus_network::websocket::TransportBackend;
52use pyo3::{IntoPyObjectExt, prelude::*};
53
54use crate::{
55    common::{
56        enums::{DeribitEnvironment, DeribitTimeInForce, resolve_trigger_type},
57        parse::parse_instrument_kind_currency,
58    },
59    websocket::{
60        client::DeribitWebSocketClient,
61        enums::DeribitUpdateInterval,
62        messages::{DeribitOrderParams, NautilusWsMessage},
63    },
64};
65
66fn call_python_with_data<F>(call_soon: &Py<PyAny>, callback: &Py<PyAny>, data_converter: F)
67where
68    F: FnOnce(Python) -> PyResult<Py<PyAny>>,
69{
70    Python::attach(|py| match data_converter(py) {
71        Ok(py_obj) => call_python_threadsafe(py, call_soon, callback, py_obj),
72        Err(e) => log::error!("Failed to convert data to Python object: {e}"),
73    });
74}
75
76fn ws_data_to_pyobject(py: Python<'_>, data: Data) -> PyResult<Py<PyAny>> {
77    match data {
78        Data::Custom(custom) => Py::new(py, custom).map(|obj| obj.into_any()),
79        Data::OptionGreeks(greeks) => Py::new(py, greeks).map(|obj| obj.into_any()),
80        other => Ok(data_to_pycapsule(py, other)),
81    }
82}
83
84#[pymethods]
85#[pyo3_stub_gen::derive::gen_stub_pymethods]
86impl DeribitWebSocketClient {
87    /// WebSocket client for connecting to Deribit.
88    #[new]
89    #[pyo3(signature = (
90        url=None,
91        api_key=None,
92        api_secret=None,
93        heartbeat_interval=30,
94        environment=DeribitEnvironment::Mainnet,
95        proxy_url=None,
96    ))]
97    fn py_new(
98        url: Option<String>,
99        api_key: Option<String>,
100        api_secret: Option<String>,
101        heartbeat_interval: u64,
102        environment: DeribitEnvironment,
103        proxy_url: Option<String>,
104    ) -> PyResult<Self> {
105        Self::new(
106            url,
107            api_key,
108            api_secret,
109            heartbeat_interval,
110            environment,
111            TransportBackend::default(),
112            proxy_url,
113        )
114        .map_err(to_pyvalue_err)
115    }
116
117    /// Creates a new public (unauthenticated) client.
118    ///
119    /// Does NOT fall back to environment variables for credentials.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error if initialization fails.
124    #[staticmethod]
125    #[pyo3(name = "new_public", signature = (environment, proxy_url = None))]
126    fn py_new_public(environment: DeribitEnvironment, proxy_url: Option<String>) -> PyResult<Self> {
127        Self::new_public(environment, proxy_url).map_err(to_pyvalue_err)
128    }
129
130    /// Creates an authenticated client with credentials.
131    ///
132    /// Resolves each credential from the provided argument first, falling back
133    /// to the environment variable for the given `environment`:
134    /// - Testnet: `DERIBIT_TESTNET_API_KEY` and `DERIBIT_TESTNET_API_SECRET`
135    /// - Mainnet: `DERIBIT_API_KEY` and `DERIBIT_API_SECRET`
136    #[staticmethod]
137    #[pyo3(name = "with_credentials", signature = (environment, api_key = None, api_secret = None, account_id = None, proxy_url = None))]
138    fn py_with_credentials(
139        environment: DeribitEnvironment,
140        api_key: Option<String>,
141        api_secret: Option<String>,
142        account_id: Option<AccountId>,
143        proxy_url: Option<String>,
144    ) -> PyResult<Self> {
145        let mut client = Self::with_credentials(environment, api_key, api_secret, proxy_url)
146            .map_err(to_pyvalue_err)?;
147
148        if let Some(id) = account_id {
149            client.set_account_id(id);
150        }
151        Ok(client)
152    }
153
154    /// Returns the WebSocket URL.
155    #[getter]
156    #[pyo3(name = "url")]
157    #[must_use]
158    pub fn py_url(&self) -> String {
159        self.url().to_string()
160    }
161
162    #[getter]
163    #[pyo3(name = "is_testnet")]
164    #[must_use]
165    pub fn py_is_testnet(&self) -> bool {
166        self.environment() == DeribitEnvironment::Testnet
167    }
168
169    /// Returns whether the client is actively connected.
170    #[pyo3(name = "is_active")]
171    #[must_use]
172    fn py_is_active(&self) -> bool {
173        self.is_active()
174    }
175
176    /// Returns whether the client is closed.
177    #[pyo3(name = "is_closed")]
178    #[must_use]
179    fn py_is_closed(&self) -> bool {
180        self.is_closed()
181    }
182
183    /// Returns whether the client has credentials configured.
184    #[pyo3(name = "has_credentials")]
185    #[must_use]
186    fn py_has_credentials(&self) -> bool {
187        self.has_credentials()
188    }
189
190    /// Returns whether the client is authenticated.
191    #[pyo3(name = "is_authenticated")]
192    #[must_use]
193    fn py_is_authenticated(&self) -> bool {
194        self.is_authenticated()
195    }
196
197    /// Cancel all pending WebSocket requests.
198    #[pyo3(name = "cancel_all_requests")]
199    pub fn py_cancel_all_requests(&self) {
200        self.cancel_all_requests();
201    }
202
203    /// Caches instruments for use during message parsing.
204    #[pyo3(name = "cache_instruments")]
205    pub fn py_cache_instruments(
206        &self,
207        py: Python<'_>,
208        instruments: Vec<Py<PyAny>>,
209    ) -> PyResult<()> {
210        let instruments: Result<Vec<_>, _> = instruments
211            .into_iter()
212            .map(|inst| pyobject_to_instrument_any(py, inst))
213            .collect();
214        self.cache_instruments(&instruments?);
215        Ok(())
216    }
217
218    /// Caches a single instrument.
219    #[pyo3(name = "cache_instrument")]
220    pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
221        let inst = pyobject_to_instrument_any(py, instrument)?;
222        self.cache_instrument(inst);
223        Ok(())
224    }
225
226    /// Sets the account ID for order/fill reports.
227    #[pyo3(name = "set_account_id")]
228    pub fn py_set_account_id(&mut self, account_id: AccountId) {
229        self.set_account_id(account_id);
230    }
231
232    /// Sets whether bar timestamps should use the close time.
233    ///
234    /// When `true` (default), bar `ts_event` is set to the bar's close time.
235    #[pyo3(name = "set_bars_timestamp_on_close")]
236    pub fn py_set_bars_timestamp_on_close(&mut self, value: bool) {
237        self.set_bars_timestamp_on_close(value);
238    }
239
240    /// Connects to the Deribit WebSocket API.
241    #[pyo3(name = "connect")]
242    #[expect(clippy::needless_pass_by_value)]
243    fn py_connect<'py>(
244        &mut self,
245        py: Python<'py>,
246        loop_: Py<PyAny>,
247        instruments: Vec<Py<PyAny>>,
248        callback: Py<PyAny>,
249    ) -> PyResult<Bound<'py, PyAny>> {
250        let call_soon: Py<PyAny> = loop_.getattr(py, "call_soon_threadsafe")?;
251
252        let mut instruments_any = Vec::new();
253
254        for inst in instruments {
255            let inst_any = pyobject_to_instrument_any(py, inst)?;
256            instruments_any.push(inst_any);
257        }
258
259        self.cache_instruments(&instruments_any);
260
261        let mut client = self.clone();
262
263        pyo3_async_runtimes::tokio::future_into_py(py, async move {
264            client.connect().await.map_err(to_pyruntime_err)?;
265
266            let stream = client.stream().map_err(to_pyruntime_err)?;
267
268            // Keep client alive in the spawned task to prevent handler from dropping
269            get_runtime().spawn(async move {
270                let _client = client;
271                tokio::pin!(stream);
272
273                while let Some(msg) = stream.next().await {
274                    match msg {
275                        NautilusWsMessage::Instrument(msg) => {
276                            call_python_with_data(&call_soon, &callback, |py| {
277                                instrument_any_to_pyobject(py, *msg)
278                            });
279                        }
280                        NautilusWsMessage::Data(msg) => Python::attach(|py| {
281                            for data in msg {
282                                match ws_data_to_pyobject(py, data) {
283                                    Ok(py_obj) => {
284                                        call_python_threadsafe(py, &call_soon, &callback, py_obj);
285                                    }
286                                    Err(e) => {
287                                        log::error!(
288                                            "Failed to convert WebSocket data payload: {e}"
289                                        );
290                                    }
291                                }
292                            }
293                        }),
294                        NautilusWsMessage::Deltas(msg) => Python::attach(|py| {
295                            let py_obj =
296                                data_to_pycapsule(py, Data::Deltas(OrderBookDeltas_API::new(msg)));
297                            call_python_threadsafe(py, &call_soon, &callback, py_obj);
298                        }),
299                        NautilusWsMessage::Error(err) => {
300                            log::warn!("WebSocket error: {err}");
301                        }
302                        NautilusWsMessage::Reconnected => {
303                            log::info!("WebSocket reconnected");
304                        }
305                        NautilusWsMessage::Authenticated(auth_result) => {
306                            log::debug!("WebSocket authenticated (scope: {})", auth_result.scope);
307                        }
308                        NautilusWsMessage::InstrumentStatus(status) => {
309                            call_python_with_data(&call_soon, &callback, |py| {
310                                status.into_py_any(py)
311                            });
312                        }
313                        NautilusWsMessage::Raw(msg) => {
314                            log::debug!("Received raw message, skipping: {msg}");
315                        }
316                        NautilusWsMessage::FundingRates(funding_rates) => Python::attach(|py| {
317                            for funding_rate in funding_rates {
318                                match Py::new(py, funding_rate) {
319                                    Ok(py_obj) => call_python_threadsafe(
320                                        py,
321                                        &call_soon,
322                                        &callback,
323                                        py_obj.into_any(),
324                                    ),
325                                    Err(e) => {
326                                        log::error!("Failed to create FundingRateUpdate: {e}");
327                                    }
328                                }
329                            }
330                        }),
331                        NautilusWsMessage::OptionGreeks(greeks) => {
332                            call_python_with_data(&call_soon, &callback, |py| {
333                                Py::new(py, greeks).map(|obj| obj.into_any())
334                            });
335                        }
336                        // Execution events - route to Python callback
337                        NautilusWsMessage::OrderStatusReports(reports) => Python::attach(|py| {
338                            for report in reports {
339                                match Py::new(py, report) {
340                                    Ok(py_obj) => call_python_threadsafe(
341                                        py,
342                                        &call_soon,
343                                        &callback,
344                                        py_obj.into_any(),
345                                    ),
346                                    Err(e) => {
347                                        log::error!("Failed to create OrderStatusReport: {e}");
348                                    }
349                                }
350                            }
351                        }),
352                        NautilusWsMessage::FillReports(reports) => Python::attach(|py| {
353                            for report in reports {
354                                match Py::new(py, report) {
355                                    Ok(py_obj) => call_python_threadsafe(
356                                        py,
357                                        &call_soon,
358                                        &callback,
359                                        py_obj.into_any(),
360                                    ),
361                                    Err(e) => log::error!("Failed to create FillReport: {e}"),
362                                }
363                            }
364                        }),
365                        NautilusWsMessage::OrderRejected(msg) => {
366                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
367                        }
368                        NautilusWsMessage::OrderAccepted(msg) => {
369                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
370                        }
371                        NautilusWsMessage::OrderCanceled(msg) => {
372                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
373                        }
374                        NautilusWsMessage::OrderExpired(msg) => {
375                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
376                        }
377                        NautilusWsMessage::OrderUpdated(msg) => {
378                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
379                        }
380                        NautilusWsMessage::OrderCancelRejected(msg) => {
381                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
382                        }
383                        NautilusWsMessage::OrderModifyRejected(msg) => {
384                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
385                        }
386                        NautilusWsMessage::AccountState(msg) => {
387                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
388                        }
389                        NautilusWsMessage::AuthenticationFailed(reason) => {
390                            log::error!("Authentication failed: {reason}");
391                        }
392                    }
393                }
394            });
395
396            Ok(())
397        })
398    }
399
400    /// Waits until the client is active or timeout expires.
401    #[pyo3(name = "wait_until_active")]
402    fn py_wait_until_active<'py>(
403        &self,
404        py: Python<'py>,
405        timeout_secs: f64,
406    ) -> PyResult<Bound<'py, PyAny>> {
407        let client = self.clone();
408
409        pyo3_async_runtimes::tokio::future_into_py(py, async move {
410            client
411                .wait_until_active(timeout_secs)
412                .await
413                .map_err(to_pyruntime_err)?;
414            Ok(())
415        })
416    }
417
418    /// Closes the WebSocket connection.
419    ///
420    /// # Errors
421    ///
422    /// Returns an error if the close operation fails.
423    #[pyo3(name = "close")]
424    fn py_close<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
425        let client = self.clone();
426
427        pyo3_async_runtimes::tokio::future_into_py(py, async move {
428            if let Err(e) = client.close().await {
429                log::warn!("Error on close: {e}");
430            }
431            Ok(())
432        })
433    }
434
435    /// Authenticates the WebSocket session with Deribit.
436    ///
437    /// Uses the `client_signature` grant type with HMAC-SHA256 signature.
438    /// This must be called before subscribing to raw data streams.
439    ///
440    /// # Arguments
441    ///
442    /// * `session_name` - Optional session name for session-scoped authentication.
443    ///   When provided, uses `session:<name>` scope which allows skipping `access_token`
444    ///   in subsequent private requests. When `None`, uses default `connection` scope.
445    ///   Recommended to use session scope for order execution compatibility.
446    #[pyo3(name = "authenticate")]
447    #[pyo3(signature = (session_name=None))]
448    fn py_authenticate<'py>(
449        &self,
450        py: Python<'py>,
451        session_name: Option<String>,
452    ) -> PyResult<Bound<'py, PyAny>> {
453        let client = self.clone();
454
455        pyo3_async_runtimes::tokio::future_into_py(py, async move {
456            client
457                .authenticate(session_name.as_deref())
458                .await
459                .map_err(to_pyruntime_err)?;
460            Ok(())
461        })
462    }
463
464    /// Authenticates with session scope using the provided session name.
465    ///
466    /// Use `DERIBIT_DATA_SESSION_NAME` for data clients and
467    /// `DERIBIT_EXECUTION_SESSION_NAME` for execution clients.
468    #[pyo3(name = "authenticate_session")]
469    fn py_authenticate_session<'py>(
470        &self,
471        py: Python<'py>,
472        session_name: String,
473    ) -> PyResult<Bound<'py, PyAny>> {
474        let client = self.clone();
475
476        pyo3_async_runtimes::tokio::future_into_py(py, async move {
477            client
478                .authenticate_session(&session_name)
479                .await
480                .map_err(|e| {
481                    to_pyruntime_err(format!(
482                        "Failed to authenticate Deribit websocket session '{session_name}': {e}"
483                    ))
484                })?;
485            Ok(())
486        })
487    }
488
489    /// Subscribes to trade updates for an instrument.
490    ///
491    /// # Arguments
492    ///
493    /// * `instrument_id` - The instrument to subscribe to
494    /// * `interval` - Update interval. Defaults to `Ms100` (100ms). `Raw` requires authentication.
495    #[pyo3(name = "subscribe_trades")]
496    #[pyo3(signature = (instrument_id, interval=None))]
497    fn py_subscribe_trades<'py>(
498        &self,
499        py: Python<'py>,
500        instrument_id: InstrumentId,
501        interval: Option<DeribitUpdateInterval>,
502    ) -> PyResult<Bound<'py, PyAny>> {
503        let client = self.clone();
504
505        pyo3_async_runtimes::tokio::future_into_py(py, async move {
506            client
507                .subscribe_trades(instrument_id, interval)
508                .await
509                .map_err(to_pyvalue_err)
510        })
511    }
512
513    /// Unsubscribes from trade updates for an instrument.
514    #[pyo3(name = "unsubscribe_trades")]
515    #[pyo3(signature = (instrument_id, interval=None))]
516    fn py_unsubscribe_trades<'py>(
517        &self,
518        py: Python<'py>,
519        instrument_id: InstrumentId,
520        interval: Option<DeribitUpdateInterval>,
521    ) -> PyResult<Bound<'py, PyAny>> {
522        let client = self.clone();
523
524        pyo3_async_runtimes::tokio::future_into_py(py, async move {
525            client
526                .unsubscribe_trades(instrument_id, interval)
527                .await
528                .map_err(to_pyvalue_err)
529        })
530    }
531
532    /// Subscribes to order book updates for an instrument.
533    ///
534    /// # Arguments
535    ///
536    /// * `instrument_id` - The instrument to subscribe to
537    /// * `interval` - Update interval. Defaults to `Ms100` (100ms). `Raw` requires authentication.
538    #[pyo3(name = "subscribe_book")]
539    #[pyo3(signature = (instrument_id, interval=None, depth=None))]
540    fn py_subscribe_book<'py>(
541        &self,
542        py: Python<'py>,
543        instrument_id: InstrumentId,
544        interval: Option<DeribitUpdateInterval>,
545        depth: Option<u32>,
546    ) -> PyResult<Bound<'py, PyAny>> {
547        let client = self.clone();
548
549        pyo3_async_runtimes::tokio::future_into_py(py, async move {
550            if let Some(d) = depth {
551                client
552                    .subscribe_book_grouped(instrument_id, "none", d, interval)
553                    .await
554                    .map_err(to_pyvalue_err)
555            } else {
556                client
557                    .subscribe_book(instrument_id, interval)
558                    .await
559                    .map_err(to_pyvalue_err)
560            }
561        })
562    }
563
564    /// Unsubscribes from order book updates for an instrument.
565    #[pyo3(name = "unsubscribe_book")]
566    #[pyo3(signature = (instrument_id, interval=None, depth=None))]
567    fn py_unsubscribe_book<'py>(
568        &self,
569        py: Python<'py>,
570        instrument_id: InstrumentId,
571        interval: Option<DeribitUpdateInterval>,
572        depth: Option<u32>,
573    ) -> PyResult<Bound<'py, PyAny>> {
574        let client = self.clone();
575
576        pyo3_async_runtimes::tokio::future_into_py(py, async move {
577            if let Some(d) = depth {
578                client
579                    .unsubscribe_book_grouped(instrument_id, "none", d, interval)
580                    .await
581                    .map_err(to_pyvalue_err)
582            } else {
583                client
584                    .unsubscribe_book(instrument_id, interval)
585                    .await
586                    .map_err(to_pyvalue_err)
587            }
588        })
589    }
590
591    /// Subscribes to grouped (depth-limited) order book updates for an instrument.
592    ///
593    /// Uses the Deribit grouped book channel format: `book.{instrument}.{group}.{depth}.{interval}`
594    ///
595    /// Depth is normalized to Deribit supported values: 1, 10, or 20.
596    #[pyo3(name = "subscribe_book_grouped")]
597    #[pyo3(signature = (instrument_id, group, depth, interval=None))]
598    fn py_subscribe_book_grouped<'py>(
599        &self,
600        py: Python<'py>,
601        instrument_id: InstrumentId,
602        group: String,
603        depth: u32,
604        interval: Option<DeribitUpdateInterval>,
605    ) -> PyResult<Bound<'py, PyAny>> {
606        let client = self.clone();
607
608        pyo3_async_runtimes::tokio::future_into_py(py, async move {
609            client
610                .subscribe_book_grouped(instrument_id, &group, depth, interval)
611                .await
612                .map_err(to_pyvalue_err)
613        })
614    }
615
616    /// Unsubscribes from grouped (depth-limited) order book updates for an instrument.
617    ///
618    /// Depth is normalized to Deribit supported values: 1, 10, or 20.
619    #[pyo3(name = "unsubscribe_book_grouped")]
620    #[pyo3(signature = (instrument_id, group, depth, interval=None))]
621    fn py_unsubscribe_book_grouped<'py>(
622        &self,
623        py: Python<'py>,
624        instrument_id: InstrumentId,
625        group: String,
626        depth: u32,
627        interval: Option<DeribitUpdateInterval>,
628    ) -> PyResult<Bound<'py, PyAny>> {
629        let client = self.clone();
630
631        pyo3_async_runtimes::tokio::future_into_py(py, async move {
632            client
633                .unsubscribe_book_grouped(instrument_id, &group, depth, interval)
634                .await
635                .map_err(to_pyvalue_err)
636        })
637    }
638
639    /// Subscribes to ticker updates for an instrument.
640    ///
641    /// # Arguments
642    ///
643    /// * `instrument_id` - The instrument to subscribe to
644    /// * `interval` - Update interval. Defaults to `Ms100` (100ms). `Raw` requires authentication.
645    #[pyo3(name = "subscribe_ticker")]
646    #[pyo3(signature = (instrument_id, interval=None))]
647    fn py_subscribe_ticker<'py>(
648        &self,
649        py: Python<'py>,
650        instrument_id: InstrumentId,
651        interval: Option<DeribitUpdateInterval>,
652    ) -> PyResult<Bound<'py, PyAny>> {
653        let client = self.clone();
654
655        pyo3_async_runtimes::tokio::future_into_py(py, async move {
656            client
657                .subscribe_ticker(instrument_id, interval)
658                .await
659                .map_err(to_pyvalue_err)
660        })
661    }
662
663    /// Unsubscribes from ticker updates for an instrument.
664    #[pyo3(name = "unsubscribe_ticker")]
665    #[pyo3(signature = (instrument_id, interval=None))]
666    fn py_unsubscribe_ticker<'py>(
667        &self,
668        py: Python<'py>,
669        instrument_id: InstrumentId,
670        interval: Option<DeribitUpdateInterval>,
671    ) -> PyResult<Bound<'py, PyAny>> {
672        let client = self.clone();
673
674        pyo3_async_runtimes::tokio::future_into_py(py, async move {
675            client
676                .unsubscribe_ticker(instrument_id, interval)
677                .await
678                .map_err(to_pyvalue_err)
679        })
680    }
681
682    /// Subscribes to mark prices for the given instrument.
683    ///
684    /// Registers the instrument in the `mark_price_subs` set so the handler
685    /// emits `MarkPriceUpdate` from ticker messages, then subscribes to the ticker channel.
686    #[pyo3(name = "subscribe_mark_prices")]
687    #[pyo3(signature = (instrument_id, interval=None))]
688    fn py_subscribe_mark_prices<'py>(
689        &self,
690        py: Python<'py>,
691        instrument_id: InstrumentId,
692        interval: Option<DeribitUpdateInterval>,
693    ) -> PyResult<Bound<'py, PyAny>> {
694        self.add_mark_price_sub(instrument_id);
695        let client = self.clone();
696
697        pyo3_async_runtimes::tokio::future_into_py(py, async move {
698            client
699                .subscribe_ticker(instrument_id, interval)
700                .await
701                .map_err(to_pyvalue_err)
702        })
703    }
704
705    /// Unsubscribes from mark prices for the given instrument.
706    ///
707    /// Removes the instrument from the `mark_price_subs` set and unsubscribes
708    /// from the ticker channel.
709    #[pyo3(name = "unsubscribe_mark_prices")]
710    #[pyo3(signature = (instrument_id, interval=None))]
711    fn py_unsubscribe_mark_prices<'py>(
712        &self,
713        py: Python<'py>,
714        instrument_id: InstrumentId,
715        interval: Option<DeribitUpdateInterval>,
716    ) -> PyResult<Bound<'py, PyAny>> {
717        self.remove_mark_price_sub(&instrument_id);
718        let client = self.clone();
719
720        pyo3_async_runtimes::tokio::future_into_py(py, async move {
721            client
722                .unsubscribe_ticker(instrument_id, interval)
723                .await
724                .map_err(to_pyvalue_err)
725        })
726    }
727
728    /// Subscribes to index prices for the given instrument.
729    ///
730    /// Registers the instrument in the `index_price_subs` set so the handler
731    /// emits `IndexPriceUpdate` from ticker messages, then subscribes to the ticker channel.
732    #[pyo3(name = "subscribe_index_prices")]
733    #[pyo3(signature = (instrument_id, interval=None))]
734    fn py_subscribe_index_prices<'py>(
735        &self,
736        py: Python<'py>,
737        instrument_id: InstrumentId,
738        interval: Option<DeribitUpdateInterval>,
739    ) -> PyResult<Bound<'py, PyAny>> {
740        self.add_index_price_sub(instrument_id);
741        let client = self.clone();
742
743        pyo3_async_runtimes::tokio::future_into_py(py, async move {
744            client
745                .subscribe_ticker(instrument_id, interval)
746                .await
747                .map_err(to_pyvalue_err)
748        })
749    }
750
751    /// Unsubscribes from index prices for the given instrument.
752    ///
753    /// Removes the instrument from the `index_price_subs` set and unsubscribes
754    /// from the ticker channel.
755    #[pyo3(name = "unsubscribe_index_prices")]
756    #[pyo3(signature = (instrument_id, interval=None))]
757    fn py_unsubscribe_index_prices<'py>(
758        &self,
759        py: Python<'py>,
760        instrument_id: InstrumentId,
761        interval: Option<DeribitUpdateInterval>,
762    ) -> PyResult<Bound<'py, PyAny>> {
763        self.remove_index_price_sub(&instrument_id);
764        let client = self.clone();
765
766        pyo3_async_runtimes::tokio::future_into_py(py, async move {
767            client
768                .unsubscribe_ticker(instrument_id, interval)
769                .await
770                .map_err(to_pyvalue_err)
771        })
772    }
773
774    /// Subscribes to option greeks for the given instrument.
775    ///
776    /// Registers the instrument in the `option_greeks_subs` set so the handler
777    /// emits `OptionGreeks` from ticker messages, then subscribes to the ticker channel.
778    #[pyo3(name = "subscribe_option_greeks")]
779    #[pyo3(signature = (instrument_id, interval=None))]
780    fn py_subscribe_option_greeks<'py>(
781        &self,
782        py: Python<'py>,
783        instrument_id: InstrumentId,
784        interval: Option<DeribitUpdateInterval>,
785    ) -> PyResult<Bound<'py, PyAny>> {
786        self.add_option_greeks_sub(instrument_id);
787        let client = self.clone();
788
789        pyo3_async_runtimes::tokio::future_into_py(py, async move {
790            client
791                .subscribe_ticker(instrument_id, interval)
792                .await
793                .map_err(to_pyvalue_err)
794        })
795    }
796
797    /// Unsubscribes from option greeks for the given instrument.
798    ///
799    /// Removes the instrument from the `option_greeks_subs` set and unsubscribes
800    /// from the ticker channel.
801    #[pyo3(name = "unsubscribe_option_greeks")]
802    #[pyo3(signature = (instrument_id, interval=None))]
803    fn py_unsubscribe_option_greeks<'py>(
804        &self,
805        py: Python<'py>,
806        instrument_id: InstrumentId,
807        interval: Option<DeribitUpdateInterval>,
808    ) -> PyResult<Bound<'py, PyAny>> {
809        self.remove_option_greeks_sub(&instrument_id);
810        let client = self.clone();
811
812        pyo3_async_runtimes::tokio::future_into_py(py, async move {
813            client
814                .unsubscribe_ticker(instrument_id, interval)
815                .await
816                .map_err(to_pyvalue_err)
817        })
818    }
819
820    /// Subscribes to quote (best bid/ask) updates for an instrument.
821    ///
822    /// Note: Quote channel does not support interval parameter.
823    #[pyo3(name = "subscribe_quotes")]
824    fn py_subscribe_quotes<'py>(
825        &self,
826        py: Python<'py>,
827        instrument_id: InstrumentId,
828    ) -> PyResult<Bound<'py, PyAny>> {
829        let client = self.clone();
830
831        pyo3_async_runtimes::tokio::future_into_py(py, async move {
832            client
833                .subscribe_quotes(instrument_id)
834                .await
835                .map_err(to_pyvalue_err)
836        })
837    }
838
839    /// Unsubscribes from quote updates for an instrument.
840    #[pyo3(name = "unsubscribe_quotes")]
841    fn py_unsubscribe_quotes<'py>(
842        &self,
843        py: Python<'py>,
844        instrument_id: InstrumentId,
845    ) -> PyResult<Bound<'py, PyAny>> {
846        let client = self.clone();
847
848        pyo3_async_runtimes::tokio::future_into_py(py, async move {
849            client
850                .unsubscribe_quotes(instrument_id)
851                .await
852                .map_err(to_pyvalue_err)
853        })
854    }
855
856    /// Subscribes to user order updates for all instruments.
857    ///
858    /// Requires authentication. Subscribes to `user.orders.any.any.raw` channel.
859    ///
860    /// # Errors
861    ///
862    /// Returns an error if client is not authenticated or subscription fails.
863    #[pyo3(name = "subscribe_user_orders")]
864    fn py_subscribe_user_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
865        let client = self.clone();
866
867        pyo3_async_runtimes::tokio::future_into_py(py, async move {
868            client.subscribe_user_orders().await.map_err(to_pyvalue_err)
869        })
870    }
871
872    /// Unsubscribes from user order updates for all instruments.
873    ///
874    /// # Errors
875    ///
876    /// Returns an error if unsubscription fails.
877    #[pyo3(name = "unsubscribe_user_orders")]
878    fn py_unsubscribe_user_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
879        let client = self.clone();
880
881        pyo3_async_runtimes::tokio::future_into_py(py, async move {
882            client
883                .unsubscribe_user_orders()
884                .await
885                .map_err(to_pyvalue_err)
886        })
887    }
888
889    /// Subscribes to user trade/fill updates for all instruments.
890    ///
891    /// Requires authentication. Subscribes to `user.trades.any.any.raw` channel.
892    ///
893    /// # Errors
894    ///
895    /// Returns an error if client is not authenticated or subscription fails.
896    #[pyo3(name = "subscribe_user_trades")]
897    fn py_subscribe_user_trades<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
898        let client = self.clone();
899
900        pyo3_async_runtimes::tokio::future_into_py(py, async move {
901            client.subscribe_user_trades().await.map_err(to_pyvalue_err)
902        })
903    }
904
905    /// Unsubscribes from user trade/fill updates for all instruments.
906    ///
907    /// # Errors
908    ///
909    /// Returns an error if unsubscription fails.
910    #[pyo3(name = "unsubscribe_user_trades")]
911    fn py_unsubscribe_user_trades<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
912        let client = self.clone();
913
914        pyo3_async_runtimes::tokio::future_into_py(py, async move {
915            client
916                .unsubscribe_user_trades()
917                .await
918                .map_err(to_pyvalue_err)
919        })
920    }
921
922    /// Subscribes to user portfolio updates for all currencies.
923    ///
924    /// Requires authentication. Subscribes to `user.portfolio.any` channel which
925    /// provides real-time account balance and margin updates for all currencies
926    /// (BTC, ETH, USDC, USDT, etc.).
927    ///
928    /// # Errors
929    ///
930    /// Returns an error if client is not authenticated or subscription fails.
931    #[pyo3(name = "subscribe_user_portfolio")]
932    fn py_subscribe_user_portfolio<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
933        let client = self.clone();
934
935        pyo3_async_runtimes::tokio::future_into_py(py, async move {
936            client
937                .subscribe_user_portfolio()
938                .await
939                .map_err(to_pyvalue_err)
940        })
941    }
942
943    /// Unsubscribes from user portfolio updates for all currencies.
944    ///
945    /// # Errors
946    ///
947    /// Returns an error if unsubscription fails.
948    #[pyo3(name = "unsubscribe_user_portfolio")]
949    fn py_unsubscribe_user_portfolio<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
950        let client = self.clone();
951
952        pyo3_async_runtimes::tokio::future_into_py(py, async move {
953            client
954                .unsubscribe_user_portfolio()
955                .await
956                .map_err(to_pyvalue_err)
957        })
958    }
959
960    /// Subscribes to multiple channels at once.
961    #[pyo3(name = "subscribe")]
962    fn py_subscribe<'py>(
963        &self,
964        py: Python<'py>,
965        channels: Vec<String>,
966    ) -> PyResult<Bound<'py, PyAny>> {
967        let client = self.clone();
968
969        pyo3_async_runtimes::tokio::future_into_py(py, async move {
970            client.subscribe(channels).await.map_err(to_pyvalue_err)
971        })
972    }
973
974    /// Unsubscribes from multiple channels at once.
975    #[pyo3(name = "unsubscribe")]
976    fn py_unsubscribe<'py>(
977        &self,
978        py: Python<'py>,
979        channels: Vec<String>,
980    ) -> PyResult<Bound<'py, PyAny>> {
981        let client = self.clone();
982
983        pyo3_async_runtimes::tokio::future_into_py(py, async move {
984            client.unsubscribe(channels).await.map_err(to_pyvalue_err)
985        })
986    }
987
988    #[pyo3(name = "subscribe_perpetual_interest_rates")]
989    #[pyo3(signature = (instrument_id, interval=None))]
990    fn py_subscribe_perpetual_interest_rates<'py>(
991        &self,
992        py: Python<'py>,
993        instrument_id: InstrumentId,
994        interval: Option<DeribitUpdateInterval>,
995    ) -> PyResult<Bound<'py, PyAny>> {
996        let client = self.clone();
997
998        pyo3_async_runtimes::tokio::future_into_py(py, async move {
999            client
1000                .subscribe_perpetual_interests_rates_updates(instrument_id, interval)
1001                .await
1002                .map_err(to_pyvalue_err)
1003        })
1004    }
1005
1006    #[pyo3(name = "unsubscribe_perpetual_interest_rates")]
1007    #[pyo3(signature = (instrument_id, interval=None))]
1008    fn py_unsubscribe_perpetual_interest_rates<'py>(
1009        &self,
1010        py: Python<'py>,
1011        instrument_id: InstrumentId,
1012        interval: Option<DeribitUpdateInterval>,
1013    ) -> PyResult<Bound<'py, PyAny>> {
1014        let client = self.clone();
1015
1016        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1017            client
1018                .unsubscribe_perpetual_interest_rates_updates(instrument_id, interval)
1019                .await
1020                .map_err(to_pyvalue_err)
1021        })
1022    }
1023
1024    /// Subscribes to instrument status changes for lifecycle notifications.
1025    ///
1026    /// Channel format: `instrument.state.{kind}.{currency}`
1027    #[pyo3(name = "subscribe_instrument_status")]
1028    fn py_subscribe_instrument_status<'py>(
1029        &self,
1030        py: Python<'py>,
1031        instrument_id: InstrumentId,
1032    ) -> PyResult<Bound<'py, PyAny>> {
1033        let client = self.clone();
1034        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
1035
1036        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1037            client
1038                .subscribe_instrument_status(&kind, &currency)
1039                .await
1040                .map_err(to_pyvalue_err)
1041        })
1042    }
1043
1044    /// Unsubscribes from instrument status changes.
1045    #[pyo3(name = "unsubscribe_instrument_status")]
1046    fn py_unsubscribe_instrument_status<'py>(
1047        &self,
1048        py: Python<'py>,
1049        instrument_id: InstrumentId,
1050    ) -> PyResult<Bound<'py, PyAny>> {
1051        let client = self.clone();
1052        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
1053
1054        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1055            client
1056                .unsubscribe_instrument_status(&kind, &currency)
1057                .await
1058                .map_err(to_pyvalue_err)
1059        })
1060    }
1061
1062    /// Subscribes to volatility index updates for the given index name.
1063    ///
1064    /// Channel format: `deribit_volatility_index.{index_name}`
1065    #[pyo3(name = "subscribe_volatility_index")]
1066    fn py_subscribe_volatility_index<'py>(
1067        &self,
1068        py: Python<'py>,
1069        index_name: String,
1070    ) -> PyResult<Bound<'py, PyAny>> {
1071        let client = self.clone();
1072
1073        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1074            client
1075                .subscribe_volatility_index(&index_name)
1076                .await
1077                .map_err(to_pyvalue_err)
1078        })
1079    }
1080
1081    /// Unsubscribes from volatility index updates for the given index name.
1082    #[pyo3(name = "unsubscribe_volatility_index")]
1083    fn py_unsubscribe_volatility_index<'py>(
1084        &self,
1085        py: Python<'py>,
1086        index_name: String,
1087    ) -> PyResult<Bound<'py, PyAny>> {
1088        let client = self.clone();
1089
1090        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1091            client
1092                .unsubscribe_volatility_index(&index_name)
1093                .await
1094                .map_err(to_pyvalue_err)
1095        })
1096    }
1097
1098    /// Subscribes to chart/OHLC bar updates for an instrument.
1099    ///
1100    /// # Arguments
1101    ///
1102    /// * `instrument_id` - The instrument to subscribe to
1103    /// * `resolution` - Bar resolution: "1", "3", "5", "10", "15", "30", "60", "120", "180",
1104    ///   "360", "720", "1D" (minutes or 1D for daily)
1105    #[pyo3(name = "subscribe_chart")]
1106    fn py_subscribe_chart<'py>(
1107        &self,
1108        py: Python<'py>,
1109        instrument_id: InstrumentId,
1110        resolution: String,
1111    ) -> PyResult<Bound<'py, PyAny>> {
1112        let client = self.clone();
1113
1114        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1115            client
1116                .subscribe_chart(instrument_id, &resolution)
1117                .await
1118                .map_err(to_pyvalue_err)
1119        })
1120    }
1121
1122    /// Unsubscribes from chart/OHLC bar updates.
1123    #[pyo3(name = "unsubscribe_chart")]
1124    fn py_unsubscribe_chart<'py>(
1125        &self,
1126        py: Python<'py>,
1127        instrument_id: InstrumentId,
1128        resolution: String,
1129    ) -> PyResult<Bound<'py, PyAny>> {
1130        let client = self.clone();
1131
1132        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1133            client
1134                .unsubscribe_chart(instrument_id, &resolution)
1135                .await
1136                .map_err(to_pyvalue_err)
1137        })
1138    }
1139
1140    /// Subscribes to bar updates for an instrument using a BarType specification.
1141    ///
1142    /// Converts the BarType to the nearest supported Deribit resolution and subscribes
1143    /// to the chart channel.
1144    #[pyo3(name = "subscribe_bars")]
1145    fn py_subscribe_bars<'py>(
1146        &self,
1147        py: Python<'py>,
1148        bar_type: BarType,
1149    ) -> PyResult<Bound<'py, PyAny>> {
1150        let client = self.clone();
1151
1152        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1153            client
1154                .subscribe_bars(bar_type)
1155                .await
1156                .map_err(to_pyvalue_err)
1157        })
1158    }
1159
1160    /// Unsubscribes from bar updates for an instrument using a BarType specification.
1161    #[pyo3(name = "unsubscribe_bars")]
1162    fn py_unsubscribe_bars<'py>(
1163        &self,
1164        py: Python<'py>,
1165        bar_type: BarType,
1166    ) -> PyResult<Bound<'py, PyAny>> {
1167        let client = self.clone();
1168
1169        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1170            client
1171                .unsubscribe_bars(bar_type)
1172                .await
1173                .map_err(to_pyvalue_err)
1174        })
1175    }
1176
1177    /// Submits an order to Deribit via WebSocket.
1178    ///
1179    /// Routes to `private/buy` or `private/sell` JSON-RPC method based on order side.
1180    /// Requires authentication (call `authenticate_session()` first).
1181    #[pyo3(name = "submit_order")]
1182    #[pyo3(signature = (
1183        order_side,
1184        quantity,
1185        order_type,
1186        client_order_id,
1187        trader_id,
1188        strategy_id,
1189        instrument_id,
1190        price=None,
1191        time_in_force=None,
1192        post_only=false,
1193        reduce_only=false,
1194        trigger_price=None,
1195        trigger_type=None,
1196    ))]
1197    #[expect(clippy::too_many_arguments)]
1198    fn py_submit_order<'py>(
1199        &self,
1200        py: Python<'py>,
1201        order_side: OrderSide,
1202        quantity: Quantity,
1203        order_type: OrderType,
1204        client_order_id: ClientOrderId,
1205        trader_id: TraderId,
1206        strategy_id: StrategyId,
1207        instrument_id: InstrumentId,
1208        price: Option<Price>,
1209        time_in_force: Option<TimeInForce>,
1210        post_only: bool,
1211        reduce_only: bool,
1212        trigger_price: Option<Price>,
1213        trigger_type: Option<TriggerType>,
1214    ) -> PyResult<Bound<'py, PyAny>> {
1215        let client = self.clone();
1216        let instrument_name = instrument_id.symbol.to_string();
1217
1218        // Convert Nautilus TimeInForce to Deribit format
1219        let deribit_tif = time_in_force
1220            .map(|tif| {
1221                DeribitTimeInForce::try_from(tif)
1222                    .map(|deribit_tif| deribit_tif.as_str().to_string())
1223            })
1224            .transpose()
1225            .map_err(to_pyvalue_err)?;
1226
1227        let params = DeribitOrderParams {
1228            instrument_name,
1229            amount: quantity.as_decimal(),
1230            order_type: order_type.to_string().to_lowercase(),
1231            label: Some(client_order_id.to_string()),
1232            price: price.map(|p| p.as_decimal()),
1233            time_in_force: deribit_tif,
1234            post_only: if post_only { Some(true) } else { None },
1235            reject_post_only: if post_only { Some(true) } else { None },
1236            reduce_only: if reduce_only { Some(true) } else { None },
1237            trigger_price: trigger_price.map(|p| p.as_decimal()),
1238            trigger: resolve_trigger_type(trigger_type),
1239            max_show: None,
1240            valid_until: None,
1241        };
1242
1243        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1244            client
1245                .submit_order(
1246                    order_side,
1247                    params,
1248                    client_order_id,
1249                    trader_id,
1250                    strategy_id,
1251                    instrument_id,
1252                )
1253                .await
1254                .map_err(to_pyruntime_err)?;
1255            Ok(())
1256        })
1257    }
1258
1259    /// Modifies an existing order on Deribit via WebSocket.
1260    ///
1261    /// The order parameters are sent using the `private/edit` JSON-RPC method.
1262    /// Requires authentication (call `authenticate_session()` first).
1263    #[pyo3(name = "modify_order")]
1264    #[expect(clippy::too_many_arguments)]
1265    fn py_modify_order<'py>(
1266        &self,
1267        py: Python<'py>,
1268        order_id: String,
1269        quantity: Quantity,
1270        price: Price,
1271        client_order_id: ClientOrderId,
1272        trader_id: TraderId,
1273        strategy_id: StrategyId,
1274        instrument_id: InstrumentId,
1275    ) -> PyResult<Bound<'py, PyAny>> {
1276        let client = self.clone();
1277
1278        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1279            client
1280                .modify_order(
1281                    &order_id,
1282                    quantity,
1283                    price,
1284                    client_order_id,
1285                    trader_id,
1286                    strategy_id,
1287                    instrument_id,
1288                )
1289                .await
1290                .map_err(to_pyruntime_err)?;
1291            Ok(())
1292        })
1293    }
1294
1295    /// Cancels an existing order on Deribit via WebSocket.
1296    ///
1297    /// The order is cancelled using the `private/cancel` JSON-RPC method.
1298    /// Requires authentication (call `authenticate_session()` first).
1299    #[pyo3(name = "cancel_order")]
1300    fn py_cancel_order<'py>(
1301        &self,
1302        py: Python<'py>,
1303        order_id: String,
1304        client_order_id: ClientOrderId,
1305        trader_id: TraderId,
1306        strategy_id: StrategyId,
1307        instrument_id: InstrumentId,
1308    ) -> PyResult<Bound<'py, PyAny>> {
1309        let client = self.clone();
1310
1311        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1312            client
1313                .cancel_order(
1314                    &order_id,
1315                    client_order_id,
1316                    trader_id,
1317                    strategy_id,
1318                    instrument_id,
1319                )
1320                .await
1321                .map_err(to_pyruntime_err)?;
1322            Ok(())
1323        })
1324    }
1325
1326    /// Cancels all orders for a specific instrument on Deribit via WebSocket.
1327    ///
1328    /// Uses the `private/cancel_all_by_instrument` JSON-RPC method.
1329    /// Requires authentication (call `authenticate_session()` first).
1330    #[pyo3(name = "cancel_all_orders")]
1331    #[pyo3(signature = (instrument_id, order_type=None))]
1332    fn py_cancel_all_orders<'py>(
1333        &self,
1334        py: Python<'py>,
1335        instrument_id: InstrumentId,
1336        order_type: Option<String>,
1337    ) -> PyResult<Bound<'py, PyAny>> {
1338        let client = self.clone();
1339
1340        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1341            client
1342                .cancel_all_orders(instrument_id, order_type)
1343                .await
1344                .map_err(to_pyruntime_err)?;
1345            Ok(())
1346        })
1347    }
1348
1349    /// Queries the state of an order on Deribit via WebSocket.
1350    ///
1351    /// Uses the `private/get_order_state` JSON-RPC method.
1352    /// Requires authentication (call `authenticate_session()` first).
1353    #[pyo3(name = "query_order")]
1354    fn py_query_order<'py>(
1355        &self,
1356        py: Python<'py>,
1357        order_id: String,
1358        client_order_id: ClientOrderId,
1359        trader_id: TraderId,
1360        strategy_id: StrategyId,
1361        instrument_id: InstrumentId,
1362    ) -> PyResult<Bound<'py, PyAny>> {
1363        let client = self.clone();
1364
1365        pyo3_async_runtimes::tokio::future_into_py(py, async move {
1366            client
1367                .query_order(
1368                    &order_id,
1369                    client_order_id,
1370                    trader_id,
1371                    strategy_id,
1372                    instrument_id,
1373                )
1374                .await
1375                .map_err(to_pyruntime_err)?;
1376            Ok(())
1377        })
1378    }
1379}