Skip to main content

nautilus_deribit/python/
http.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 Deribit HTTP client.
17
18use jiff::Timestamp;
19use nautilus_core::{
20    python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
21    time::get_atomic_clock_realtime,
22};
23use nautilus_model::{
24    data::BarType,
25    identifiers::{AccountId, InstrumentId},
26    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
27};
28use pyo3::{conversion::IntoPyObjectExt, prelude::*, types::PyList};
29
30use crate::{
31    common::enums::DeribitEnvironment,
32    data_types::DeribitBookSummary,
33    http::{
34        client::DeribitHttpClient,
35        error::DeribitHttpError,
36        models::{DeribitCurrency, DeribitProductType},
37    },
38};
39
40#[pymethods]
41#[pyo3_stub_gen::derive::gen_stub_pymethods]
42impl DeribitHttpClient {
43    /// High-level Deribit HTTP client with domain-level abstractions.
44    ///
45    /// This client wraps the raw HTTP client and provides methods that use Nautilus
46    /// domain types. It maintains an instrument cache for efficient lookups.
47    #[new]
48    #[pyo3(signature = (
49        api_key=None,
50        api_secret=None,
51        base_url=None,
52        environment=DeribitEnvironment::Mainnet,
53        timeout_secs=10,
54        max_retries=3,
55        retry_delay_ms=1000,
56        retry_delay_max_ms=10_000,
57        proxy_url=None,
58    ))]
59    #[expect(clippy::too_many_arguments)]
60    #[allow(unused_variables)]
61    fn py_new(
62        api_key: Option<String>,
63        api_secret: Option<String>,
64        base_url: Option<String>,
65        environment: DeribitEnvironment,
66        timeout_secs: u64,
67        max_retries: u32,
68        retry_delay_ms: u64,
69        retry_delay_max_ms: u64,
70        proxy_url: Option<String>,
71    ) -> PyResult<Self> {
72        Self::new_with_env(
73            api_key,
74            api_secret,
75            base_url,
76            environment,
77            timeout_secs,
78            max_retries,
79            retry_delay_ms,
80            retry_delay_max_ms,
81            proxy_url,
82        )
83        .map_err(to_pyvalue_err)
84    }
85
86    /// Returns whether this client is connected to testnet.
87    #[getter]
88    #[pyo3(name = "is_testnet")]
89    #[must_use]
90    pub fn py_is_testnet(&self) -> bool {
91        self.is_testnet()
92    }
93
94    #[pyo3(name = "is_initialized")]
95    #[must_use]
96    pub fn py_is_initialized(&self) -> bool {
97        self.is_cache_initialized()
98    }
99
100    /// Caches instruments for later retrieval.
101    #[pyo3(name = "cache_instruments")]
102    pub fn py_cache_instruments(
103        &self,
104        py: Python<'_>,
105        instruments: Vec<Py<PyAny>>,
106    ) -> PyResult<()> {
107        let instruments: Result<Vec<_>, _> = instruments
108            .into_iter()
109            .map(|inst| pyobject_to_instrument_any(py, inst))
110            .collect();
111        self.cache_instruments(&instruments?);
112        Ok(())
113    }
114
115    /// # Errors
116    ///
117    /// Returns a Python exception if adding the instrument to the cache fails.
118    #[pyo3(name = "cache_instrument")]
119    pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
120        let inst = pyobject_to_instrument_any(py, instrument)?;
121        self.cache_instruments(std::slice::from_ref(&inst));
122        Ok(())
123    }
124
125    /// Requests instruments for a specific currency.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if the request fails or instruments cannot be parsed.
130    #[pyo3(name = "request_instruments")]
131    #[pyo3(signature = (currency, product_type=None))]
132    fn py_request_instruments<'py>(
133        &self,
134        py: Python<'py>,
135        currency: DeribitCurrency,
136        product_type: Option<DeribitProductType>,
137    ) -> PyResult<Bound<'py, PyAny>> {
138        let client = self.clone();
139
140        pyo3_async_runtimes::tokio::future_into_py(py, async move {
141            let instruments = client
142                .request_instruments(currency, product_type)
143                .await
144                .map_err(to_pyvalue_err)?;
145
146            Python::attach(|py| {
147                let py_instruments: PyResult<Vec<_>> = instruments
148                    .into_iter()
149                    .map(|inst| instrument_any_to_pyobject(py, inst))
150                    .collect();
151                let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
152                Ok(pylist)
153            })
154        })
155    }
156
157    /// Requests traded option expirations for a settlement currency.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if the request fails.
162    #[pyo3(name = "request_option_expirations")]
163    fn py_request_option_expirations<'py>(
164        &self,
165        py: Python<'py>,
166        currency: DeribitCurrency,
167    ) -> PyResult<Bound<'py, PyAny>> {
168        let client = self.clone();
169
170        pyo3_async_runtimes::tokio::future_into_py(py, async move {
171            let expirations = client
172                .request_option_expirations(currency)
173                .await
174                .map_err(to_pyvalue_err)?;
175
176            Python::attach(|py| {
177                let pylist = PyList::new(py, expirations)?.into_any().unbind();
178                Ok(pylist)
179            })
180        })
181    }
182
183    /// Requests a specific instrument by its Nautilus instrument ID.
184    ///
185    /// This is a high-level method that fetches the raw instrument data from Deribit
186    /// and converts it to a Nautilus `InstrumentAny` type.
187    ///
188    /// # Errors
189    ///
190    /// Returns an error if:
191    /// - The instrument name format is invalid (error code `-32602`)
192    /// - The instrument doesn't exist (error code `13020`)
193    /// - Network or API errors occur
194    #[pyo3(name = "request_instrument")]
195    fn py_request_instrument<'py>(
196        &self,
197        py: Python<'py>,
198        instrument_id: InstrumentId,
199    ) -> PyResult<Bound<'py, PyAny>> {
200        let client = self.clone();
201
202        pyo3_async_runtimes::tokio::future_into_py(py, async move {
203            let instrument = client
204                .request_instrument(instrument_id)
205                .await
206                .map_err(to_pyvalue_err)?;
207
208            Python::attach(|py| instrument_any_to_pyobject(py, instrument))
209        })
210    }
211
212    /// Requests account state for all currencies.
213    ///
214    /// Fetches account balance and margin information for all currencies from Deribit
215    /// and converts it to Nautilus `AccountState` event.
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if:
220    /// - The request fails
221    /// - Currency conversion fails
222    #[pyo3(name = "request_account_state")]
223    fn py_request_account_state<'py>(
224        &self,
225        py: Python<'py>,
226        account_id: AccountId,
227    ) -> PyResult<Bound<'py, PyAny>> {
228        let client = self.clone();
229
230        pyo3_async_runtimes::tokio::future_into_py(py, async move {
231            let account_state = client
232                .request_account_state(account_id)
233                .await
234                .map_err(to_pyvalue_err)?;
235
236            Python::attach(|py| account_state.into_py_any(py))
237        })
238    }
239
240    /// Requests historical trades for an instrument within a time range.
241    ///
242    /// Fetches trade ticks from Deribit and converts them to Nautilus `TradeTick` objects.
243    ///
244    /// # Arguments
245    ///
246    /// * `instrument_id` - The instrument to fetch trades for
247    /// * `start` - Optional start time filter
248    /// * `end` - Optional end time filter
249    /// * `limit` - Optional limit on number of trades (max 1000)
250    ///
251    /// # Errors
252    ///
253    /// Returns an error if:
254    /// - The instrument is not found in cache
255    /// - The request fails
256    /// - Trade parsing fails
257    ///
258    /// # Pagination
259    ///
260    /// When `limit` is `None`, this function automatically paginates through all available
261    /// trades in the time range using the `has_more` field from the API response.
262    /// When `limit` is specified, pagination stops once that many trades are collected.
263    #[pyo3(name = "request_trades")]
264    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
265    fn py_request_trades<'py>(
266        &self,
267        py: Python<'py>,
268        instrument_id: InstrumentId,
269        start: Option<Timestamp>,
270        end: Option<Timestamp>,
271        limit: Option<u32>,
272    ) -> PyResult<Bound<'py, PyAny>> {
273        let client = self.clone();
274
275        pyo3_async_runtimes::tokio::future_into_py(py, async move {
276            let trades = client
277                .request_trades(instrument_id, start, end, limit)
278                .await
279                .map_err(to_pyvalue_err)?;
280
281            Python::attach(|py| {
282                let py_trades = trades
283                    .into_iter()
284                    .map(|trade| trade.into_py_any(py))
285                    .collect::<PyResult<Vec<_>>>()?;
286                let pylist = PyList::new(py, py_trades)?;
287                Ok(pylist.into_py_any_unwrap(py))
288            })
289        })
290    }
291
292    /// Requests historical bars (OHLCV) for an instrument.
293    ///
294    /// Uses the `public/get_tradingview_chart_data` endpoint to fetch candlestick data.
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if:
299    /// - Aggregation source is not EXTERNAL
300    /// - Bar aggregation type is not supported by Deribit
301    /// - The instrument is not found in cache
302    /// - The request fails or response cannot be parsed
303    ///
304    /// # Supported Resolutions
305    ///
306    /// Deribit supports: 1, 3, 5, 10, 15, 30, 60, 120, 180, 360, 720 minutes, and 1D (daily)
307    #[pyo3(name = "request_bars")]
308    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
309    fn py_request_bars<'py>(
310        &self,
311        py: Python<'py>,
312        bar_type: BarType,
313        start: Option<Timestamp>,
314        end: Option<Timestamp>,
315        limit: Option<u32>,
316    ) -> PyResult<Bound<'py, PyAny>> {
317        let client = self.clone();
318
319        pyo3_async_runtimes::tokio::future_into_py(py, async move {
320            let bars = client
321                .request_bars(bar_type, start, end, limit)
322                .await
323                .map_err(to_pyvalue_err)?;
324
325            Python::attach(|py| {
326                let py_bars = bars
327                    .into_iter()
328                    .map(|bar| bar.into_py_any(py))
329                    .collect::<PyResult<Vec<_>>>()?;
330                let pylist = PyList::new(py, py_bars)?;
331                Ok(pylist.into_py_any_unwrap(py))
332            })
333        })
334    }
335
336    /// Requests a snapshot of the order book for an instrument.
337    ///
338    /// Fetches the order book from Deribit and converts it to a Nautilus `OrderBook`.
339    ///
340    /// # Arguments
341    ///
342    /// * `instrument_id` - The instrument to fetch the order book for
343    /// * `depth` - Optional depth limit (valid values: 1, 5, 10, 20, 50, 100, 1000, 10000)
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if:
348    /// - The instrument is not found in cache
349    /// - The request fails
350    /// - Order book parsing fails
351    #[pyo3(name = "request_book_snapshot")]
352    #[pyo3(signature = (instrument_id, depth=None))]
353    fn py_request_book_snapshot<'py>(
354        &self,
355        py: Python<'py>,
356        instrument_id: InstrumentId,
357        depth: Option<u32>,
358    ) -> PyResult<Bound<'py, PyAny>> {
359        let client = self.clone();
360
361        pyo3_async_runtimes::tokio::future_into_py(py, async move {
362            let book = client
363                .request_book_snapshot(instrument_id, depth)
364                .await
365                .map_err(to_pyvalue_err)?;
366
367            Python::attach(|py| book.into_py_any(py))
368        })
369    }
370
371    /// Requests order status reports for reconciliation.
372    ///
373    /// Fetches order statuses from Deribit and converts them to Nautilus `OrderStatusReport`.
374    ///
375    /// # Strategy
376    /// - Uses `/private/get_open_orders` for all open orders (single efficient API call)
377    /// - Uses `/private/get_open_orders_by_instrument` when specific instrument is provided
378    /// - For historical orders (when `open_only=false`), iterates over currencies
379    ///
380    /// # Errors
381    ///
382    /// Returns an error if the request fails or parsing fails.
383    #[pyo3(name = "request_order_status_reports")]
384    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=true))]
385    fn py_request_order_status_reports<'py>(
386        &self,
387        py: Python<'py>,
388        account_id: AccountId,
389        instrument_id: Option<InstrumentId>,
390        start: Option<u64>,
391        end: Option<u64>,
392        open_only: bool,
393    ) -> PyResult<Bound<'py, PyAny>> {
394        let client = self.clone();
395
396        pyo3_async_runtimes::tokio::future_into_py(py, async move {
397            let reports = client
398                .request_order_status_reports(
399                    account_id,
400                    instrument_id,
401                    start.map(nautilus_core::UnixNanos::from),
402                    end.map(nautilus_core::UnixNanos::from),
403                    open_only,
404                )
405                .await
406                .map_err(to_pyvalue_err)?;
407
408            Python::attach(|py| {
409                let py_reports: PyResult<Vec<_>> = reports
410                    .into_iter()
411                    .map(|report| report.into_py_any(py))
412                    .collect();
413                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
414                Ok(pylist)
415            })
416        })
417    }
418
419    /// Requests fill reports for reconciliation.
420    ///
421    /// Fetches user trades from Deribit and converts them to Nautilus `FillReport`.
422    /// Automatically paginates through all results using time-cursor advancement.
423    ///
424    /// # Strategy
425    /// - Uses `/private/get_user_trades_by_instrument_and_time` when instrument is provided
426    /// - Otherwise iterates over currencies using `/private/get_user_trades_by_currency_and_time`
427    ///
428    /// # Errors
429    ///
430    /// Returns an error if the request fails or parsing fails.
431    #[pyo3(name = "request_fill_reports")]
432    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
433    fn py_request_fill_reports<'py>(
434        &self,
435        py: Python<'py>,
436        account_id: AccountId,
437        instrument_id: Option<InstrumentId>,
438        start: Option<u64>,
439        end: Option<u64>,
440    ) -> PyResult<Bound<'py, PyAny>> {
441        let client = self.clone();
442
443        pyo3_async_runtimes::tokio::future_into_py(py, async move {
444            let reports = client
445                .request_fill_reports(
446                    account_id,
447                    instrument_id,
448                    start.map(nautilus_core::UnixNanos::from),
449                    end.map(nautilus_core::UnixNanos::from),
450                )
451                .await
452                .map_err(to_pyvalue_err)?;
453
454            Python::attach(|py| {
455                let py_reports: PyResult<Vec<_>> = reports
456                    .into_iter()
457                    .map(|report| report.into_py_any(py))
458                    .collect();
459                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
460                Ok(pylist)
461            })
462        })
463    }
464
465    /// Requests position status reports for reconciliation.
466    ///
467    /// Fetches positions from Deribit and converts them to Nautilus `PositionStatusReport`.
468    ///
469    /// # Strategy
470    /// - Uses `currency=any` to fetch all positions in one call
471    /// - Filters by instrument_id if provided
472    ///
473    /// # Errors
474    ///
475    /// Returns an error if the request fails or parsing fails.
476    #[pyo3(name = "request_position_status_reports")]
477    #[pyo3(signature = (account_id, instrument_id=None))]
478    fn py_request_position_status_reports<'py>(
479        &self,
480        py: Python<'py>,
481        account_id: AccountId,
482        instrument_id: Option<InstrumentId>,
483    ) -> PyResult<Bound<'py, PyAny>> {
484        let client = self.clone();
485
486        pyo3_async_runtimes::tokio::future_into_py(py, async move {
487            let reports = client
488                .request_position_status_reports(account_id, instrument_id)
489                .await
490                .map_err(to_pyvalue_err)?;
491
492            Python::attach(|py| {
493                let py_reports: PyResult<Vec<_>> = reports
494                    .into_iter()
495                    .map(|report| report.into_py_any(py))
496                    .collect();
497                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
498                Ok(pylist)
499            })
500        })
501    }
502
503    /// Requests book summaries for a currency via `public/get_book_summary_by_currency`.
504    ///
505    /// Defaults to product kind `option`.
506    /// Entries include mark/IV, bid-ask, volumes, and `underlying_price` (forward) when present.
507    ///
508    /// # Errors
509    ///
510    /// Returns an error if the request fails.
511    #[pyo3(name = "request_book_summaries")]
512    #[pyo3(signature = (currency, kind=None))]
513    fn py_request_book_summaries<'py>(
514        &self,
515        py: Python<'py>,
516        currency: String,
517        kind: Option<String>,
518    ) -> PyResult<Bound<'py, PyAny>> {
519        let client = self.clone();
520
521        pyo3_async_runtimes::tokio::future_into_py(py, async move {
522            let currency = currency.trim().to_ascii_uppercase();
523            if currency.is_empty() {
524                return Err(to_pyvalue_err(
525                    "request_book_summaries requires a non-empty currency",
526                ));
527            }
528            let kind = kind
529                .as_deref()
530                .map(str::trim)
531                .filter(|value| !value.is_empty())
532                .map_or_else(|| "option".to_string(), str::to_ascii_lowercase);
533            let summaries = client
534                .request_book_summaries_kind(&currency, Some(kind.as_str()))
535                .await
536                .map_err(to_pyvalue_err)?;
537            // Stamp observed time after the HTTP round-trip completes.
538            let ts = get_atomic_clock_realtime().get_time_ns();
539
540            Python::attach(|py| {
541                let py_items: PyResult<Vec<_>> = summaries
542                    .into_iter()
543                    .map(|raw| Py::new(py, DeribitBookSummary::from_raw(raw, ts)))
544                    .collect();
545                let pylist = PyList::new(py, py_items?)?.into_any().unbind();
546                Ok(pylist)
547            })
548        })
549    }
550}
551
552impl From<DeribitHttpError> for PyErr {
553    fn from(error: DeribitHttpError) -> Self {
554        match error {
555            // Runtime/operational errors
556            DeribitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
557            DeribitHttpError::NetworkError(msg) => {
558                to_pyruntime_err(format!("Network error: {msg}"))
559            }
560            DeribitHttpError::UnexpectedStatus { status, body } => {
561                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
562            }
563            DeribitHttpError::Timeout(msg) => to_pyruntime_err(format!("Request timeout: {msg}")),
564            // Validation/configuration errors
565            DeribitHttpError::MissingCredentials => {
566                to_pyvalue_err("Missing credentials for authenticated request")
567            }
568            DeribitHttpError::ValidationError(msg) => {
569                to_pyvalue_err(format!("Parameter validation error: {msg}"))
570            }
571            DeribitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
572            DeribitHttpError::DeribitError {
573                error_code,
574                message,
575            } => to_pyvalue_err(format!("Deribit error {error_code}: {message}")),
576        }
577    }
578}