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