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 traded option expirations for a settlement currency.
158    #[pyo3(name = "request_option_expirations")]
159    fn py_request_option_expirations<'py>(
160        &self,
161        py: Python<'py>,
162        currency: DeribitCurrency,
163    ) -> PyResult<Bound<'py, PyAny>> {
164        let client = self.clone();
165
166        pyo3_async_runtimes::tokio::future_into_py(py, async move {
167            let expirations = client
168                .request_option_expirations(currency)
169                .await
170                .map_err(to_pyvalue_err)?;
171
172            Python::attach(|py| {
173                let pylist = PyList::new(py, expirations)?.into_any().unbind();
174                Ok(pylist)
175            })
176        })
177    }
178
179    /// Requests a specific instrument by its Nautilus instrument ID.
180    ///
181    /// This is a high-level method that fetches the raw instrument data from Deribit
182    /// and converts it to a Nautilus `InstrumentAny` type.
183    #[pyo3(name = "request_instrument")]
184    fn py_request_instrument<'py>(
185        &self,
186        py: Python<'py>,
187        instrument_id: InstrumentId,
188    ) -> PyResult<Bound<'py, PyAny>> {
189        let client = self.clone();
190
191        pyo3_async_runtimes::tokio::future_into_py(py, async move {
192            let instrument = client
193                .request_instrument(instrument_id)
194                .await
195                .map_err(to_pyvalue_err)?;
196
197            Python::attach(|py| instrument_any_to_pyobject(py, instrument))
198        })
199    }
200
201    /// Requests account state for all currencies.
202    ///
203    /// Fetches account balance and margin information for all currencies from Deribit
204    /// and converts it to Nautilus `AccountState` event.
205    #[pyo3(name = "request_account_state")]
206    fn py_request_account_state<'py>(
207        &self,
208        py: Python<'py>,
209        account_id: AccountId,
210    ) -> PyResult<Bound<'py, PyAny>> {
211        let client = self.clone();
212
213        pyo3_async_runtimes::tokio::future_into_py(py, async move {
214            let account_state = client
215                .request_account_state(account_id)
216                .await
217                .map_err(to_pyvalue_err)?;
218
219            Python::attach(|py| Ok(account_state.into_py_any_unwrap(py)))
220        })
221    }
222
223    /// Requests historical trades for an instrument within a time range.
224    ///
225    /// Fetches trade ticks from Deribit and converts them to Nautilus `TradeTick` objects.
226    ///
227    /// # Arguments
228    ///
229    /// * `instrument_id` - The instrument to fetch trades for
230    /// * `start` - Optional start time filter
231    /// * `end` - Optional end time filter
232    /// * `limit` - Optional limit on number of trades (max 1000)
233    ///
234    /// # Pagination
235    ///
236    /// When `limit` is `None`, this function automatically paginates through all available
237    /// trades in the time range using the `has_more` field from the API response.
238    /// When `limit` is specified, pagination stops once that many trades are collected.
239    #[pyo3(name = "request_trades")]
240    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
241    fn py_request_trades<'py>(
242        &self,
243        py: Python<'py>,
244        instrument_id: InstrumentId,
245        start: Option<DateTime<Utc>>,
246        end: Option<DateTime<Utc>>,
247        limit: Option<u32>,
248    ) -> PyResult<Bound<'py, PyAny>> {
249        let client = self.clone();
250
251        pyo3_async_runtimes::tokio::future_into_py(py, async move {
252            let trades = client
253                .request_trades(instrument_id, start, end, limit)
254                .await
255                .map_err(to_pyvalue_err)?;
256
257            Python::attach(|py| {
258                let pylist = PyList::new(
259                    py,
260                    trades.into_iter().map(|trade| trade.into_py_any_unwrap(py)),
261                )?;
262                Ok(pylist.into_py_any_unwrap(py))
263            })
264        })
265    }
266
267    /// Requests historical bars (OHLCV) for an instrument.
268    ///
269    /// Uses the `public/get_tradingview_chart_data` endpoint to fetch candlestick data.
270    ///
271    /// # Supported Resolutions
272    ///
273    /// Deribit supports: 1, 3, 5, 10, 15, 30, 60, 120, 180, 360, 720 minutes, and 1D (daily)
274    #[pyo3(name = "request_bars")]
275    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
276    fn py_request_bars<'py>(
277        &self,
278        py: Python<'py>,
279        bar_type: BarType,
280        start: Option<DateTime<Utc>>,
281        end: Option<DateTime<Utc>>,
282        limit: Option<u32>,
283    ) -> PyResult<Bound<'py, PyAny>> {
284        let client = self.clone();
285
286        pyo3_async_runtimes::tokio::future_into_py(py, async move {
287            let bars = client
288                .request_bars(bar_type, start, end, limit)
289                .await
290                .map_err(to_pyvalue_err)?;
291
292            Python::attach(|py| {
293                let pylist =
294                    PyList::new(py, bars.into_iter().map(|bar| bar.into_py_any_unwrap(py)))?;
295                Ok(pylist.into_py_any_unwrap(py))
296            })
297        })
298    }
299
300    /// Requests a snapshot of the order book for an instrument.
301    ///
302    /// Fetches the order book from Deribit and converts it to a Nautilus `OrderBook`.
303    ///
304    /// # Arguments
305    ///
306    /// * `instrument_id` - The instrument to fetch the order book for
307    /// * `depth` - Optional depth limit (valid values: 1, 5, 10, 20, 50, 100, 1000, 10000)
308    #[pyo3(name = "request_book_snapshot")]
309    #[pyo3(signature = (instrument_id, depth=None))]
310    fn py_request_book_snapshot<'py>(
311        &self,
312        py: Python<'py>,
313        instrument_id: InstrumentId,
314        depth: Option<u32>,
315    ) -> PyResult<Bound<'py, PyAny>> {
316        let client = self.clone();
317
318        pyo3_async_runtimes::tokio::future_into_py(py, async move {
319            let book = client
320                .request_book_snapshot(instrument_id, depth)
321                .await
322                .map_err(to_pyvalue_err)?;
323
324            Python::attach(|py| Ok(book.into_py_any_unwrap(py)))
325        })
326    }
327
328    /// Requests order status reports for reconciliation.
329    ///
330    /// Fetches order statuses from Deribit and converts them to Nautilus `OrderStatusReport`.
331    ///
332    /// # Strategy
333    /// - Uses `/private/get_open_orders` for all open orders (single efficient API call)
334    /// - Uses `/private/get_open_orders_by_instrument` when specific instrument is provided
335    /// - For historical orders (when `open_only=false`), iterates over currencies
336    #[pyo3(name = "request_order_status_reports")]
337    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=true))]
338    fn py_request_order_status_reports<'py>(
339        &self,
340        py: Python<'py>,
341        account_id: AccountId,
342        instrument_id: Option<InstrumentId>,
343        start: Option<u64>,
344        end: Option<u64>,
345        open_only: bool,
346    ) -> PyResult<Bound<'py, PyAny>> {
347        let client = self.clone();
348
349        pyo3_async_runtimes::tokio::future_into_py(py, async move {
350            let reports = client
351                .request_order_status_reports(
352                    account_id,
353                    instrument_id,
354                    start.map(nautilus_core::UnixNanos::from),
355                    end.map(nautilus_core::UnixNanos::from),
356                    open_only,
357                )
358                .await
359                .map_err(to_pyvalue_err)?;
360
361            Python::attach(|py| {
362                let py_reports: PyResult<Vec<_>> = reports
363                    .into_iter()
364                    .map(|report| report.into_py_any(py))
365                    .collect();
366                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
367                Ok(pylist)
368            })
369        })
370    }
371
372    /// Requests fill reports for reconciliation.
373    ///
374    /// Fetches user trades from Deribit and converts them to Nautilus `FillReport`.
375    /// Automatically paginates through all results using time-cursor advancement.
376    ///
377    /// # Strategy
378    /// - Uses `/private/get_user_trades_by_instrument_and_time` when instrument is provided
379    /// - Otherwise iterates over currencies using `/private/get_user_trades_by_currency_and_time`
380    #[pyo3(name = "request_fill_reports")]
381    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
382    fn py_request_fill_reports<'py>(
383        &self,
384        py: Python<'py>,
385        account_id: AccountId,
386        instrument_id: Option<InstrumentId>,
387        start: Option<u64>,
388        end: Option<u64>,
389    ) -> PyResult<Bound<'py, PyAny>> {
390        let client = self.clone();
391
392        pyo3_async_runtimes::tokio::future_into_py(py, async move {
393            let reports = client
394                .request_fill_reports(
395                    account_id,
396                    instrument_id,
397                    start.map(nautilus_core::UnixNanos::from),
398                    end.map(nautilus_core::UnixNanos::from),
399                )
400                .await
401                .map_err(to_pyvalue_err)?;
402
403            Python::attach(|py| {
404                let py_reports: PyResult<Vec<_>> = reports
405                    .into_iter()
406                    .map(|report| report.into_py_any(py))
407                    .collect();
408                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
409                Ok(pylist)
410            })
411        })
412    }
413
414    /// Requests position status reports for reconciliation.
415    ///
416    /// Fetches positions from Deribit and converts them to Nautilus `PositionStatusReport`.
417    ///
418    /// # Strategy
419    /// - Uses `currency=any` to fetch all positions in one call
420    /// - Filters by instrument_id if provided
421    #[pyo3(name = "request_position_status_reports")]
422    #[pyo3(signature = (account_id, instrument_id=None))]
423    fn py_request_position_status_reports<'py>(
424        &self,
425        py: Python<'py>,
426        account_id: AccountId,
427        instrument_id: Option<InstrumentId>,
428    ) -> PyResult<Bound<'py, PyAny>> {
429        let client = self.clone();
430
431        pyo3_async_runtimes::tokio::future_into_py(py, async move {
432            let reports = client
433                .request_position_status_reports(account_id, instrument_id)
434                .await
435                .map_err(to_pyvalue_err)?;
436
437            Python::attach(|py| {
438                let py_reports: PyResult<Vec<_>> = reports
439                    .into_iter()
440                    .map(|report| report.into_py_any(py))
441                    .collect();
442                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
443                Ok(pylist)
444            })
445        })
446    }
447
448    /// Request forward prices for option chain ATM determination.
449    ///
450    /// Single-instrument path (1 HTTP call) if `instrument_id` is provided,
451    /// otherwise bulk path via book summaries.
452    #[pyo3(name = "request_forward_prices")]
453    #[pyo3(signature = (currency, instrument_id=None))]
454    fn py_request_forward_prices<'py>(
455        &self,
456        py: Python<'py>,
457        currency: String,
458        instrument_id: Option<InstrumentId>,
459    ) -> PyResult<Bound<'py, PyAny>> {
460        let client = self.clone();
461
462        pyo3_async_runtimes::tokio::future_into_py(py, async move {
463            let forward_prices = if let Some(inst_id) = instrument_id {
464                // Single-instrument path: 1 HTTP call to public/ticker
465                let instrument_name = inst_id.symbol.to_string();
466                let ticker = client
467                    .request_ticker(&instrument_name)
468                    .await
469                    .map_err(to_pyvalue_err)?;
470
471                let ts = UnixNanos::default();
472                ticker
473                    .underlying_price
474                    .map(|up| {
475                        vec![ForwardPrice::new(
476                            inst_id,
477                            up,
478                            ticker.underlying_index.filter(|s| !s.is_empty()),
479                            ts,
480                            ts,
481                        )]
482                    })
483                    .unwrap_or_default()
484            } else {
485                // Bulk path: fetch all book summaries
486                let summaries = client
487                    .request_book_summaries(&currency)
488                    .await
489                    .map_err(to_pyvalue_err)?;
490
491                let ts = nautilus_core::UnixNanos::default();
492                let mut seen_indices = HashSet::new();
493                summaries
494                    .into_iter()
495                    .filter_map(|s| {
496                        let up = s.underlying_price?;
497                        let idx = s.underlying_index.clone().unwrap_or_default();
498                        if !seen_indices.insert(idx.clone()) {
499                            return None;
500                        }
501                        Some(ForwardPrice::new(
502                            InstrumentId::new(Symbol::new(&s.instrument_name), *DERIBIT_VENUE),
503                            up,
504                            Some(idx).filter(|s| !s.is_empty()),
505                            ts,
506                            ts,
507                        ))
508                    })
509                    .collect()
510            };
511
512            Python::attach(|py| {
513                let py_prices: PyResult<Vec<_>> = forward_prices
514                    .into_iter()
515                    .map(|fp| Py::new(py, fp))
516                    .collect();
517                let pylist = PyList::new(py, py_prices?)?.into_any().unbind();
518                Ok(pylist)
519            })
520        })
521    }
522}
523
524impl From<DeribitHttpError> for PyErr {
525    fn from(error: DeribitHttpError) -> Self {
526        match error {
527            // Runtime/operational errors
528            DeribitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
529            DeribitHttpError::NetworkError(msg) => {
530                to_pyruntime_err(format!("Network error: {msg}"))
531            }
532            DeribitHttpError::UnexpectedStatus { status, body } => {
533                to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
534            }
535            DeribitHttpError::Timeout(msg) => to_pyruntime_err(format!("Request timeout: {msg}")),
536            // Validation/configuration errors
537            DeribitHttpError::MissingCredentials => {
538                to_pyvalue_err("Missing credentials for authenticated request")
539            }
540            DeribitHttpError::ValidationError(msg) => {
541                to_pyvalue_err(format!("Parameter validation error: {msg}"))
542            }
543            DeribitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
544            DeribitHttpError::DeribitError {
545                error_code,
546                message,
547            } => to_pyvalue_err(format!("Deribit error {error_code}: {message}")),
548        }
549    }
550}