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