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},
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 pyo3::{IntoPyObjectExt, prelude::*};
52
53use crate::{
54    common::{enums::DeribitTimeInForce, parse::parse_instrument_kind_currency},
55    websocket::{
56        client::DeribitWebSocketClient,
57        enums::DeribitUpdateInterval,
58        messages::{DeribitOrderParams, NautilusWsMessage},
59    },
60};
61
62fn call_python_with_data<F>(call_soon: &Py<PyAny>, callback: &Py<PyAny>, data_converter: F)
63where
64    F: FnOnce(Python) -> PyResult<Py<PyAny>>,
65{
66    Python::attach(|py| match data_converter(py) {
67        Ok(py_obj) => call_python_threadsafe(py, call_soon, callback, py_obj),
68        Err(e) => log::error!("Failed to convert data to Python object: {e}"),
69    });
70}
71
72#[pymethods]
73impl DeribitWebSocketClient {
74    #[new]
75    #[pyo3(signature = (
76        url=None,
77        api_key=None,
78        api_secret=None,
79        heartbeat_interval=None,
80        is_testnet=false,
81    ))]
82    fn py_new(
83        url: Option<String>,
84        api_key: Option<String>,
85        api_secret: Option<String>,
86        heartbeat_interval: Option<u64>,
87        is_testnet: bool,
88    ) -> PyResult<Self> {
89        Self::new(url, api_key, api_secret, heartbeat_interval, is_testnet).map_err(to_pyvalue_err)
90    }
91
92    #[staticmethod]
93    #[pyo3(name = "new_public")]
94    fn py_new_public(is_testnet: bool) -> PyResult<Self> {
95        Self::new_public(is_testnet).map_err(to_pyvalue_err)
96    }
97
98    #[staticmethod]
99    #[pyo3(name = "with_credentials", signature = (is_testnet, account_id = None))]
100    fn py_with_credentials(is_testnet: bool, account_id: Option<AccountId>) -> PyResult<Self> {
101        let mut client = Self::with_credentials(is_testnet).map_err(to_pyvalue_err)?;
102
103        if let Some(id) = account_id {
104            client.set_account_id(id);
105        }
106        Ok(client)
107    }
108
109    #[getter]
110    #[pyo3(name = "url")]
111    #[must_use]
112    pub fn py_url(&self) -> String {
113        self.url().to_string()
114    }
115
116    #[getter]
117    #[pyo3(name = "is_testnet")]
118    #[must_use]
119    pub fn py_is_testnet(&self) -> bool {
120        // Check if the URL contains "test"
121        self.url().contains("test")
122    }
123
124    #[pyo3(name = "is_active")]
125    #[must_use]
126    fn py_is_active(&self) -> bool {
127        self.is_active()
128    }
129
130    #[pyo3(name = "is_closed")]
131    #[must_use]
132    fn py_is_closed(&self) -> bool {
133        self.is_closed()
134    }
135
136    #[pyo3(name = "has_credentials")]
137    #[must_use]
138    fn py_has_credentials(&self) -> bool {
139        self.has_credentials()
140    }
141
142    #[pyo3(name = "is_authenticated")]
143    #[must_use]
144    fn py_is_authenticated(&self) -> bool {
145        self.is_authenticated()
146    }
147
148    #[pyo3(name = "cancel_all_requests")]
149    pub fn py_cancel_all_requests(&self) {
150        self.cancel_all_requests();
151    }
152
153    /// # Errors
154    ///
155    /// Returns an error if instrument conversion fails.
156    #[pyo3(name = "cache_instruments")]
157    pub fn py_cache_instruments(
158        &self,
159        py: Python<'_>,
160        instruments: Vec<Py<PyAny>>,
161    ) -> PyResult<()> {
162        let instruments: Result<Vec<_>, _> = instruments
163            .into_iter()
164            .map(|inst| pyobject_to_instrument_any(py, inst))
165            .collect();
166        self.cache_instruments(instruments?);
167        Ok(())
168    }
169
170    /// # Errors
171    ///
172    /// Returns an error if instrument conversion fails.
173    #[pyo3(name = "cache_instrument")]
174    pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
175        let inst = pyobject_to_instrument_any(py, instrument)?;
176        self.cache_instrument(inst);
177        Ok(())
178    }
179
180    #[pyo3(name = "set_account_id")]
181    pub fn py_set_account_id(&mut self, account_id: AccountId) {
182        self.set_account_id(account_id);
183    }
184
185    #[pyo3(name = "set_bars_timestamp_on_close")]
186    pub fn py_set_bars_timestamp_on_close(&mut self, value: bool) {
187        self.set_bars_timestamp_on_close(value);
188    }
189
190    #[pyo3(name = "connect")]
191    fn py_connect<'py>(
192        &mut self,
193        py: Python<'py>,
194        loop_: Py<PyAny>,
195        instruments: Vec<Py<PyAny>>,
196        callback: Py<PyAny>,
197    ) -> PyResult<Bound<'py, PyAny>> {
198        let call_soon: Py<PyAny> = loop_.getattr(py, "call_soon_threadsafe")?;
199
200        let mut instruments_any = Vec::new();
201        for inst in instruments {
202            let inst_any = pyobject_to_instrument_any(py, inst)?;
203            instruments_any.push(inst_any);
204        }
205
206        self.cache_instruments(instruments_any);
207
208        let mut client = self.clone();
209
210        pyo3_async_runtimes::tokio::future_into_py(py, async move {
211            client.connect().await.map_err(to_pyruntime_err)?;
212
213            let stream = client.stream().map_err(to_pyruntime_err)?;
214
215            // Keep client alive in the spawned task to prevent handler from dropping
216            get_runtime().spawn(async move {
217                let _client = client;
218                tokio::pin!(stream);
219
220                while let Some(msg) = stream.next().await {
221                    match msg {
222                        NautilusWsMessage::Instrument(msg) => {
223                            call_python_with_data(&call_soon, &callback, |py| {
224                                instrument_any_to_pyobject(py, *msg)
225                            });
226                        }
227                        NautilusWsMessage::Data(msg) => Python::attach(|py| {
228                            for data in msg {
229                                let py_obj = data_to_pycapsule(py, data);
230                                call_python_threadsafe(py, &call_soon, &callback, py_obj);
231                            }
232                        }),
233                        NautilusWsMessage::Deltas(msg) => Python::attach(|py| {
234                            let py_obj =
235                                data_to_pycapsule(py, Data::Deltas(OrderBookDeltas_API::new(msg)));
236                            call_python_threadsafe(py, &call_soon, &callback, py_obj);
237                        }),
238                        NautilusWsMessage::Error(err) => {
239                            log::error!("WebSocket error: {err}");
240                        }
241                        NautilusWsMessage::Reconnected => {
242                            log::info!("WebSocket reconnected");
243                        }
244                        NautilusWsMessage::Authenticated(auth_result) => {
245                            log::info!("WebSocket authenticated (scope: {})", auth_result.scope);
246                        }
247                        NautilusWsMessage::InstrumentStatus(status) => {
248                            call_python_with_data(&call_soon, &callback, |py| {
249                                status.into_py_any(py)
250                            });
251                        }
252                        NautilusWsMessage::Raw(msg) => {
253                            log::debug!("Received raw message, skipping: {msg}");
254                        }
255                        NautilusWsMessage::FundingRates(funding_rates) => Python::attach(|py| {
256                            for funding_rate in funding_rates {
257                                match Py::new(py, funding_rate) {
258                                    Ok(py_obj) => call_python_threadsafe(
259                                        py,
260                                        &call_soon,
261                                        &callback,
262                                        py_obj.into_any(),
263                                    ),
264                                    Err(e) => {
265                                        log::error!("Failed to create FundingRateUpdate: {e}");
266                                    }
267                                }
268                            }
269                        }),
270                        // Execution events - route to Python callback
271                        NautilusWsMessage::OrderStatusReports(reports) => Python::attach(|py| {
272                            for report in reports {
273                                match Py::new(py, report) {
274                                    Ok(py_obj) => call_python_threadsafe(
275                                        py,
276                                        &call_soon,
277                                        &callback,
278                                        py_obj.into_any(),
279                                    ),
280                                    Err(e) => {
281                                        log::error!("Failed to create OrderStatusReport: {e}");
282                                    }
283                                }
284                            }
285                        }),
286                        NautilusWsMessage::FillReports(reports) => Python::attach(|py| {
287                            for report in reports {
288                                match Py::new(py, report) {
289                                    Ok(py_obj) => call_python_threadsafe(
290                                        py,
291                                        &call_soon,
292                                        &callback,
293                                        py_obj.into_any(),
294                                    ),
295                                    Err(e) => log::error!("Failed to create FillReport: {e}"),
296                                }
297                            }
298                        }),
299                        NautilusWsMessage::OrderRejected(msg) => {
300                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
301                        }
302                        NautilusWsMessage::OrderAccepted(msg) => {
303                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
304                        }
305                        NautilusWsMessage::OrderCanceled(msg) => {
306                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
307                        }
308                        NautilusWsMessage::OrderExpired(msg) => {
309                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
310                        }
311                        NautilusWsMessage::OrderUpdated(msg) => {
312                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
313                        }
314                        NautilusWsMessage::OrderCancelRejected(msg) => {
315                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
316                        }
317                        NautilusWsMessage::OrderModifyRejected(msg) => {
318                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
319                        }
320                        NautilusWsMessage::AccountState(msg) => {
321                            call_python_with_data(&call_soon, &callback, |py| msg.into_py_any(py));
322                        }
323                        NautilusWsMessage::AuthenticationFailed(reason) => {
324                            log::error!("Authentication failed: {reason}");
325                        }
326                    }
327                }
328            });
329
330            Ok(())
331        })
332    }
333
334    #[pyo3(name = "wait_until_active")]
335    fn py_wait_until_active<'py>(
336        &self,
337        py: Python<'py>,
338        timeout_secs: f64,
339    ) -> PyResult<Bound<'py, PyAny>> {
340        let client = self.clone();
341
342        pyo3_async_runtimes::tokio::future_into_py(py, async move {
343            client
344                .wait_until_active(timeout_secs)
345                .await
346                .map_err(to_pyruntime_err)?;
347            Ok(())
348        })
349    }
350
351    #[pyo3(name = "close")]
352    fn py_close<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
353        let client = self.clone();
354
355        pyo3_async_runtimes::tokio::future_into_py(py, async move {
356            if let Err(e) = client.close().await {
357                log::error!("Error on close: {e}");
358            }
359            Ok(())
360        })
361    }
362
363    #[pyo3(name = "authenticate")]
364    #[pyo3(signature = (session_name=None))]
365    fn py_authenticate<'py>(
366        &self,
367        py: Python<'py>,
368        session_name: Option<String>,
369    ) -> PyResult<Bound<'py, PyAny>> {
370        let client = self.clone();
371
372        pyo3_async_runtimes::tokio::future_into_py(py, async move {
373            client
374                .authenticate(session_name.as_deref())
375                .await
376                .map_err(to_pyruntime_err)?;
377            Ok(())
378        })
379    }
380
381    #[pyo3(name = "authenticate_session")]
382    fn py_authenticate_session<'py>(
383        &self,
384        py: Python<'py>,
385        session_name: String,
386    ) -> PyResult<Bound<'py, PyAny>> {
387        let client = self.clone();
388
389        pyo3_async_runtimes::tokio::future_into_py(py, async move {
390            client
391                .authenticate_session(&session_name)
392                .await
393                .map_err(|e| {
394                    to_pyruntime_err(format!(
395                        "Failed to authenticate Deribit websocket session '{session_name}': {e}"
396                    ))
397                })?;
398            Ok(())
399        })
400    }
401
402    #[pyo3(name = "subscribe_trades")]
403    #[pyo3(signature = (instrument_id, interval=None))]
404    fn py_subscribe_trades<'py>(
405        &self,
406        py: Python<'py>,
407        instrument_id: InstrumentId,
408        interval: Option<DeribitUpdateInterval>,
409    ) -> PyResult<Bound<'py, PyAny>> {
410        let client = self.clone();
411
412        pyo3_async_runtimes::tokio::future_into_py(py, async move {
413            client
414                .subscribe_trades(instrument_id, interval)
415                .await
416                .map_err(to_pyvalue_err)
417        })
418    }
419
420    #[pyo3(name = "unsubscribe_trades")]
421    #[pyo3(signature = (instrument_id, interval=None))]
422    fn py_unsubscribe_trades<'py>(
423        &self,
424        py: Python<'py>,
425        instrument_id: InstrumentId,
426        interval: Option<DeribitUpdateInterval>,
427    ) -> PyResult<Bound<'py, PyAny>> {
428        let client = self.clone();
429
430        pyo3_async_runtimes::tokio::future_into_py(py, async move {
431            client
432                .unsubscribe_trades(instrument_id, interval)
433                .await
434                .map_err(to_pyvalue_err)
435        })
436    }
437
438    #[pyo3(name = "subscribe_book")]
439    #[pyo3(signature = (instrument_id, interval=None, depth=None))]
440    fn py_subscribe_book<'py>(
441        &self,
442        py: Python<'py>,
443        instrument_id: InstrumentId,
444        interval: Option<DeribitUpdateInterval>,
445        depth: Option<u32>,
446    ) -> PyResult<Bound<'py, PyAny>> {
447        let client = self.clone();
448
449        pyo3_async_runtimes::tokio::future_into_py(py, async move {
450            if let Some(d) = depth {
451                client
452                    .subscribe_book_grouped(instrument_id, "none", d, interval)
453                    .await
454                    .map_err(to_pyvalue_err)
455            } else {
456                client
457                    .subscribe_book(instrument_id, interval)
458                    .await
459                    .map_err(to_pyvalue_err)
460            }
461        })
462    }
463
464    #[pyo3(name = "unsubscribe_book")]
465    #[pyo3(signature = (instrument_id, interval=None, depth=None))]
466    fn py_unsubscribe_book<'py>(
467        &self,
468        py: Python<'py>,
469        instrument_id: InstrumentId,
470        interval: Option<DeribitUpdateInterval>,
471        depth: Option<u32>,
472    ) -> PyResult<Bound<'py, PyAny>> {
473        let client = self.clone();
474
475        pyo3_async_runtimes::tokio::future_into_py(py, async move {
476            if let Some(d) = depth {
477                client
478                    .unsubscribe_book_grouped(instrument_id, "none", d, interval)
479                    .await
480                    .map_err(to_pyvalue_err)
481            } else {
482                client
483                    .unsubscribe_book(instrument_id, interval)
484                    .await
485                    .map_err(to_pyvalue_err)
486            }
487        })
488    }
489
490    #[pyo3(name = "subscribe_book_grouped")]
491    #[pyo3(signature = (instrument_id, group, depth, interval=None))]
492    fn py_subscribe_book_grouped<'py>(
493        &self,
494        py: Python<'py>,
495        instrument_id: InstrumentId,
496        group: String,
497        depth: u32,
498        interval: Option<DeribitUpdateInterval>,
499    ) -> PyResult<Bound<'py, PyAny>> {
500        let client = self.clone();
501
502        pyo3_async_runtimes::tokio::future_into_py(py, async move {
503            client
504                .subscribe_book_grouped(instrument_id, &group, depth, interval)
505                .await
506                .map_err(to_pyvalue_err)
507        })
508    }
509
510    #[pyo3(name = "unsubscribe_book_grouped")]
511    #[pyo3(signature = (instrument_id, group, depth, interval=None))]
512    fn py_unsubscribe_book_grouped<'py>(
513        &self,
514        py: Python<'py>,
515        instrument_id: InstrumentId,
516        group: String,
517        depth: u32,
518        interval: Option<DeribitUpdateInterval>,
519    ) -> PyResult<Bound<'py, PyAny>> {
520        let client = self.clone();
521
522        pyo3_async_runtimes::tokio::future_into_py(py, async move {
523            client
524                .unsubscribe_book_grouped(instrument_id, &group, depth, interval)
525                .await
526                .map_err(to_pyvalue_err)
527        })
528    }
529
530    #[pyo3(name = "subscribe_ticker")]
531    #[pyo3(signature = (instrument_id, interval=None))]
532    fn py_subscribe_ticker<'py>(
533        &self,
534        py: Python<'py>,
535        instrument_id: InstrumentId,
536        interval: Option<DeribitUpdateInterval>,
537    ) -> PyResult<Bound<'py, PyAny>> {
538        let client = self.clone();
539
540        pyo3_async_runtimes::tokio::future_into_py(py, async move {
541            client
542                .subscribe_ticker(instrument_id, interval)
543                .await
544                .map_err(to_pyvalue_err)
545        })
546    }
547
548    #[pyo3(name = "unsubscribe_ticker")]
549    #[pyo3(signature = (instrument_id, interval=None))]
550    fn py_unsubscribe_ticker<'py>(
551        &self,
552        py: Python<'py>,
553        instrument_id: InstrumentId,
554        interval: Option<DeribitUpdateInterval>,
555    ) -> PyResult<Bound<'py, PyAny>> {
556        let client = self.clone();
557
558        pyo3_async_runtimes::tokio::future_into_py(py, async move {
559            client
560                .unsubscribe_ticker(instrument_id, interval)
561                .await
562                .map_err(to_pyvalue_err)
563        })
564    }
565
566    #[pyo3(name = "subscribe_quotes")]
567    fn py_subscribe_quotes<'py>(
568        &self,
569        py: Python<'py>,
570        instrument_id: InstrumentId,
571    ) -> PyResult<Bound<'py, PyAny>> {
572        let client = self.clone();
573
574        pyo3_async_runtimes::tokio::future_into_py(py, async move {
575            client
576                .subscribe_quotes(instrument_id)
577                .await
578                .map_err(to_pyvalue_err)
579        })
580    }
581
582    #[pyo3(name = "unsubscribe_quotes")]
583    fn py_unsubscribe_quotes<'py>(
584        &self,
585        py: Python<'py>,
586        instrument_id: InstrumentId,
587    ) -> PyResult<Bound<'py, PyAny>> {
588        let client = self.clone();
589
590        pyo3_async_runtimes::tokio::future_into_py(py, async move {
591            client
592                .unsubscribe_quotes(instrument_id)
593                .await
594                .map_err(to_pyvalue_err)
595        })
596    }
597
598    #[pyo3(name = "subscribe_user_orders")]
599    fn py_subscribe_user_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
600        let client = self.clone();
601
602        pyo3_async_runtimes::tokio::future_into_py(py, async move {
603            client.subscribe_user_orders().await.map_err(to_pyvalue_err)
604        })
605    }
606
607    #[pyo3(name = "unsubscribe_user_orders")]
608    fn py_unsubscribe_user_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
609        let client = self.clone();
610
611        pyo3_async_runtimes::tokio::future_into_py(py, async move {
612            client
613                .unsubscribe_user_orders()
614                .await
615                .map_err(to_pyvalue_err)
616        })
617    }
618
619    #[pyo3(name = "subscribe_user_trades")]
620    fn py_subscribe_user_trades<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
621        let client = self.clone();
622
623        pyo3_async_runtimes::tokio::future_into_py(py, async move {
624            client.subscribe_user_trades().await.map_err(to_pyvalue_err)
625        })
626    }
627
628    #[pyo3(name = "unsubscribe_user_trades")]
629    fn py_unsubscribe_user_trades<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
630        let client = self.clone();
631
632        pyo3_async_runtimes::tokio::future_into_py(py, async move {
633            client
634                .unsubscribe_user_trades()
635                .await
636                .map_err(to_pyvalue_err)
637        })
638    }
639
640    #[pyo3(name = "subscribe_user_portfolio")]
641    fn py_subscribe_user_portfolio<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
642        let client = self.clone();
643
644        pyo3_async_runtimes::tokio::future_into_py(py, async move {
645            client
646                .subscribe_user_portfolio()
647                .await
648                .map_err(to_pyvalue_err)
649        })
650    }
651
652    #[pyo3(name = "unsubscribe_user_portfolio")]
653    fn py_unsubscribe_user_portfolio<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
654        let client = self.clone();
655
656        pyo3_async_runtimes::tokio::future_into_py(py, async move {
657            client
658                .unsubscribe_user_portfolio()
659                .await
660                .map_err(to_pyvalue_err)
661        })
662    }
663
664    #[pyo3(name = "subscribe")]
665    fn py_subscribe<'py>(
666        &self,
667        py: Python<'py>,
668        channels: Vec<String>,
669    ) -> PyResult<Bound<'py, PyAny>> {
670        let client = self.clone();
671
672        pyo3_async_runtimes::tokio::future_into_py(py, async move {
673            client.subscribe(channels).await.map_err(to_pyvalue_err)
674        })
675    }
676
677    #[pyo3(name = "unsubscribe")]
678    fn py_unsubscribe<'py>(
679        &self,
680        py: Python<'py>,
681        channels: Vec<String>,
682    ) -> PyResult<Bound<'py, PyAny>> {
683        let client = self.clone();
684
685        pyo3_async_runtimes::tokio::future_into_py(py, async move {
686            client.unsubscribe(channels).await.map_err(to_pyvalue_err)
687        })
688    }
689
690    #[pyo3(name = "subscribe_perpetual_interest_rates")]
691    #[pyo3(signature = (instrument_id, interval=None))]
692    fn py_subscribe_perpetual_interest_rates<'py>(
693        &self,
694        py: Python<'py>,
695        instrument_id: InstrumentId,
696        interval: Option<DeribitUpdateInterval>,
697    ) -> PyResult<Bound<'py, PyAny>> {
698        let client = self.clone();
699
700        pyo3_async_runtimes::tokio::future_into_py(py, async move {
701            client
702                .subscribe_perpetual_interests_rates_updates(instrument_id, interval)
703                .await
704                .map_err(to_pyvalue_err)
705        })
706    }
707
708    #[pyo3(name = "unsubscribe_perpetual_interest_rates")]
709    #[pyo3(signature = (instrument_id, interval=None))]
710    fn py_unsubscribe_perpetual_interest_rates<'py>(
711        &self,
712        py: Python<'py>,
713        instrument_id: InstrumentId,
714        interval: Option<DeribitUpdateInterval>,
715    ) -> PyResult<Bound<'py, PyAny>> {
716        let client = self.clone();
717
718        pyo3_async_runtimes::tokio::future_into_py(py, async move {
719            client
720                .unsubscribe_perpetual_interest_rates_updates(instrument_id, interval)
721                .await
722                .map_err(to_pyvalue_err)
723        })
724    }
725
726    #[pyo3(name = "subscribe_instrument_status")]
727    fn py_subscribe_instrument_status<'py>(
728        &self,
729        py: Python<'py>,
730        instrument_id: InstrumentId,
731    ) -> PyResult<Bound<'py, PyAny>> {
732        let client = self.clone();
733        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
734
735        pyo3_async_runtimes::tokio::future_into_py(py, async move {
736            client
737                .subscribe_instrument_status(&kind, &currency)
738                .await
739                .map_err(to_pyvalue_err)
740        })
741    }
742
743    #[pyo3(name = "unsubscribe_instrument_status")]
744    fn py_unsubscribe_instrument_status<'py>(
745        &self,
746        py: Python<'py>,
747        instrument_id: InstrumentId,
748    ) -> PyResult<Bound<'py, PyAny>> {
749        let client = self.clone();
750        let (kind, currency) = parse_instrument_kind_currency(&instrument_id);
751
752        pyo3_async_runtimes::tokio::future_into_py(py, async move {
753            client
754                .unsubscribe_instrument_status(&kind, &currency)
755                .await
756                .map_err(to_pyvalue_err)
757        })
758    }
759
760    #[pyo3(name = "subscribe_chart")]
761    fn py_subscribe_chart<'py>(
762        &self,
763        py: Python<'py>,
764        instrument_id: InstrumentId,
765        resolution: String,
766    ) -> PyResult<Bound<'py, PyAny>> {
767        let client = self.clone();
768
769        pyo3_async_runtimes::tokio::future_into_py(py, async move {
770            client
771                .subscribe_chart(instrument_id, &resolution)
772                .await
773                .map_err(to_pyvalue_err)
774        })
775    }
776
777    #[pyo3(name = "unsubscribe_chart")]
778    fn py_unsubscribe_chart<'py>(
779        &self,
780        py: Python<'py>,
781        instrument_id: InstrumentId,
782        resolution: String,
783    ) -> PyResult<Bound<'py, PyAny>> {
784        let client = self.clone();
785
786        pyo3_async_runtimes::tokio::future_into_py(py, async move {
787            client
788                .unsubscribe_chart(instrument_id, &resolution)
789                .await
790                .map_err(to_pyvalue_err)
791        })
792    }
793
794    #[pyo3(name = "subscribe_bars")]
795    fn py_subscribe_bars<'py>(
796        &self,
797        py: Python<'py>,
798        bar_type: BarType,
799    ) -> PyResult<Bound<'py, PyAny>> {
800        let client = self.clone();
801
802        pyo3_async_runtimes::tokio::future_into_py(py, async move {
803            client
804                .subscribe_bars(bar_type)
805                .await
806                .map_err(to_pyvalue_err)
807        })
808    }
809
810    #[pyo3(name = "unsubscribe_bars")]
811    fn py_unsubscribe_bars<'py>(
812        &self,
813        py: Python<'py>,
814        bar_type: BarType,
815    ) -> PyResult<Bound<'py, PyAny>> {
816        let client = self.clone();
817
818        pyo3_async_runtimes::tokio::future_into_py(py, async move {
819            client
820                .unsubscribe_bars(bar_type)
821                .await
822                .map_err(to_pyvalue_err)
823        })
824    }
825
826    #[pyo3(name = "submit_order")]
827    #[pyo3(signature = (
828        order_side,
829        quantity,
830        order_type,
831        client_order_id,
832        trader_id,
833        strategy_id,
834        instrument_id,
835        price=None,
836        time_in_force=None,
837        post_only=false,
838        reduce_only=false,
839        trigger_price=None,
840        trigger=None,
841    ))]
842    #[allow(clippy::too_many_arguments)]
843    fn py_submit_order<'py>(
844        &self,
845        py: Python<'py>,
846        order_side: OrderSide,
847        quantity: Quantity,
848        order_type: OrderType,
849        client_order_id: ClientOrderId,
850        trader_id: TraderId,
851        strategy_id: StrategyId,
852        instrument_id: InstrumentId,
853        price: Option<Price>,
854        time_in_force: Option<TimeInForce>,
855        post_only: bool,
856        reduce_only: bool,
857        trigger_price: Option<Price>,
858        trigger: Option<String>,
859    ) -> PyResult<Bound<'py, PyAny>> {
860        let client = self.clone();
861        let instrument_name = instrument_id.symbol.to_string();
862
863        // Convert Nautilus TimeInForce to Deribit format
864        let deribit_tif = time_in_force
865            .map(|tif| {
866                DeribitTimeInForce::try_from(tif)
867                    .map(|deribit_tif| deribit_tif.as_str().to_string())
868            })
869            .transpose()
870            .map_err(to_pyvalue_err)?;
871
872        let params = DeribitOrderParams {
873            instrument_name,
874            amount: quantity.as_decimal(),
875            order_type: order_type.to_string().to_lowercase(),
876            label: Some(client_order_id.to_string()),
877            price: price.map(|p| p.as_decimal()),
878            time_in_force: deribit_tif,
879            post_only: if post_only { Some(true) } else { None },
880            reject_post_only: if post_only { Some(true) } else { None },
881            reduce_only: if reduce_only { Some(true) } else { None },
882            trigger_price: trigger_price.map(|p| p.as_decimal()),
883            trigger,
884            max_show: None,
885            valid_until: None,
886        };
887
888        pyo3_async_runtimes::tokio::future_into_py(py, async move {
889            client
890                .submit_order(
891                    order_side,
892                    params,
893                    client_order_id,
894                    trader_id,
895                    strategy_id,
896                    instrument_id,
897                )
898                .await
899                .map_err(to_pyruntime_err)?;
900            Ok(())
901        })
902    }
903
904    #[pyo3(name = "modify_order")]
905    #[allow(clippy::too_many_arguments)]
906    fn py_modify_order<'py>(
907        &self,
908        py: Python<'py>,
909        order_id: String,
910        quantity: Quantity,
911        price: Price,
912        client_order_id: ClientOrderId,
913        trader_id: TraderId,
914        strategy_id: StrategyId,
915        instrument_id: InstrumentId,
916    ) -> PyResult<Bound<'py, PyAny>> {
917        let client = self.clone();
918
919        pyo3_async_runtimes::tokio::future_into_py(py, async move {
920            client
921                .modify_order(
922                    &order_id,
923                    quantity,
924                    price,
925                    client_order_id,
926                    trader_id,
927                    strategy_id,
928                    instrument_id,
929                )
930                .await
931                .map_err(to_pyruntime_err)?;
932            Ok(())
933        })
934    }
935
936    #[pyo3(name = "cancel_order")]
937    fn py_cancel_order<'py>(
938        &self,
939        py: Python<'py>,
940        order_id: String,
941        client_order_id: ClientOrderId,
942        trader_id: TraderId,
943        strategy_id: StrategyId,
944        instrument_id: InstrumentId,
945    ) -> PyResult<Bound<'py, PyAny>> {
946        let client = self.clone();
947
948        pyo3_async_runtimes::tokio::future_into_py(py, async move {
949            client
950                .cancel_order(
951                    &order_id,
952                    client_order_id,
953                    trader_id,
954                    strategy_id,
955                    instrument_id,
956                )
957                .await
958                .map_err(to_pyruntime_err)?;
959            Ok(())
960        })
961    }
962
963    #[pyo3(name = "cancel_all_orders")]
964    #[pyo3(signature = (instrument_id, order_type=None))]
965    fn py_cancel_all_orders<'py>(
966        &self,
967        py: Python<'py>,
968        instrument_id: InstrumentId,
969        order_type: Option<String>,
970    ) -> PyResult<Bound<'py, PyAny>> {
971        let client = self.clone();
972
973        pyo3_async_runtimes::tokio::future_into_py(py, async move {
974            client
975                .cancel_all_orders(instrument_id, order_type)
976                .await
977                .map_err(to_pyruntime_err)?;
978            Ok(())
979        })
980    }
981
982    #[pyo3(name = "query_order")]
983    fn py_query_order<'py>(
984        &self,
985        py: Python<'py>,
986        order_id: String,
987        client_order_id: ClientOrderId,
988        trader_id: TraderId,
989        strategy_id: StrategyId,
990        instrument_id: InstrumentId,
991    ) -> PyResult<Bound<'py, PyAny>> {
992        let client = self.clone();
993
994        pyo3_async_runtimes::tokio::future_into_py(py, async move {
995            client
996                .query_order(
997                    &order_id,
998                    client_order_id,
999                    trader_id,
1000                    strategy_id,
1001                    instrument_id,
1002                )
1003                .await
1004                .map_err(to_pyruntime_err)?;
1005            Ok(())
1006        })
1007    }
1008}