1use std::collections::HashSet;
19
20use jiff::Timestamp;
21use nautilus_core::{
22 UnixNanos,
23 python::{IntoPyObjectNautilusExt, to_pyruntime_err, to_pyvalue_err},
24 time::get_atomic_clock_realtime,
25};
26use nautilus_model::{
27 data::{BarType, forward::ForwardPrice},
28 identifiers::{AccountId, InstrumentId, Symbol},
29 python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
30};
31use pyo3::{conversion::IntoPyObjectExt, prelude::*, types::PyList};
32
33use crate::{
34 common::{consts::DERIBIT_VENUE, enums::DeribitEnvironment},
35 data_types::DeribitBookSummary,
36 http::{
37 client::DeribitHttpClient,
38 error::DeribitHttpError,
39 models::{DeribitCurrency, DeribitProductType},
40 },
41};
42
43#[pymethods]
44#[pyo3_stub_gen::derive::gen_stub_pymethods]
45impl DeribitHttpClient {
46 #[new]
51 #[pyo3(signature = (
52 api_key=None,
53 api_secret=None,
54 base_url=None,
55 environment=DeribitEnvironment::Mainnet,
56 timeout_secs=10,
57 max_retries=3,
58 retry_delay_ms=1000,
59 retry_delay_max_ms=10_000,
60 proxy_url=None,
61 ))]
62 #[expect(clippy::too_many_arguments)]
63 #[allow(unused_variables)]
64 fn py_new(
65 api_key: Option<String>,
66 api_secret: Option<String>,
67 base_url: Option<String>,
68 environment: DeribitEnvironment,
69 timeout_secs: u64,
70 max_retries: u32,
71 retry_delay_ms: u64,
72 retry_delay_max_ms: u64,
73 proxy_url: Option<String>,
74 ) -> PyResult<Self> {
75 Self::new_with_env(
76 api_key,
77 api_secret,
78 base_url,
79 environment,
80 timeout_secs,
81 max_retries,
82 retry_delay_ms,
83 retry_delay_max_ms,
84 proxy_url,
85 )
86 .map_err(to_pyvalue_err)
87 }
88
89 #[getter]
91 #[pyo3(name = "is_testnet")]
92 #[must_use]
93 pub fn py_is_testnet(&self) -> bool {
94 self.is_testnet()
95 }
96
97 #[pyo3(name = "is_initialized")]
98 #[must_use]
99 pub fn py_is_initialized(&self) -> bool {
100 self.is_cache_initialized()
101 }
102
103 #[pyo3(name = "cache_instruments")]
105 pub fn py_cache_instruments(
106 &self,
107 py: Python<'_>,
108 instruments: Vec<Py<PyAny>>,
109 ) -> PyResult<()> {
110 let instruments: Result<Vec<_>, _> = instruments
111 .into_iter()
112 .map(|inst| pyobject_to_instrument_any(py, inst))
113 .collect();
114 self.cache_instruments(&instruments?);
115 Ok(())
116 }
117
118 #[pyo3(name = "cache_instrument")]
122 pub fn py_cache_instrument(&self, py: Python<'_>, instrument: Py<PyAny>) -> PyResult<()> {
123 let inst = pyobject_to_instrument_any(py, instrument)?;
124 self.cache_instruments(std::slice::from_ref(&inst));
125 Ok(())
126 }
127
128 #[pyo3(name = "request_instruments")]
134 #[pyo3(signature = (currency, product_type=None))]
135 fn py_request_instruments<'py>(
136 &self,
137 py: Python<'py>,
138 currency: DeribitCurrency,
139 product_type: Option<DeribitProductType>,
140 ) -> PyResult<Bound<'py, PyAny>> {
141 let client = self.clone();
142
143 pyo3_async_runtimes::tokio::future_into_py(py, async move {
144 let instruments = client
145 .request_instruments(currency, product_type)
146 .await
147 .map_err(to_pyvalue_err)?;
148
149 Python::attach(|py| {
150 let py_instruments: PyResult<Vec<_>> = instruments
151 .into_iter()
152 .map(|inst| instrument_any_to_pyobject(py, inst))
153 .collect();
154 let pylist = PyList::new(py, py_instruments?)?.into_any().unbind();
155 Ok(pylist)
156 })
157 })
158 }
159
160 #[pyo3(name = "request_option_expirations")]
166 fn py_request_option_expirations<'py>(
167 &self,
168 py: Python<'py>,
169 currency: DeribitCurrency,
170 ) -> PyResult<Bound<'py, PyAny>> {
171 let client = self.clone();
172
173 pyo3_async_runtimes::tokio::future_into_py(py, async move {
174 let expirations = client
175 .request_option_expirations(currency)
176 .await
177 .map_err(to_pyvalue_err)?;
178
179 Python::attach(|py| {
180 let pylist = PyList::new(py, expirations)?.into_any().unbind();
181 Ok(pylist)
182 })
183 })
184 }
185
186 #[pyo3(name = "request_instrument")]
198 fn py_request_instrument<'py>(
199 &self,
200 py: Python<'py>,
201 instrument_id: InstrumentId,
202 ) -> PyResult<Bound<'py, PyAny>> {
203 let client = self.clone();
204
205 pyo3_async_runtimes::tokio::future_into_py(py, async move {
206 let instrument = client
207 .request_instrument(instrument_id)
208 .await
209 .map_err(to_pyvalue_err)?;
210
211 Python::attach(|py| instrument_any_to_pyobject(py, instrument))
212 })
213 }
214
215 #[pyo3(name = "request_account_state")]
226 fn py_request_account_state<'py>(
227 &self,
228 py: Python<'py>,
229 account_id: AccountId,
230 ) -> PyResult<Bound<'py, PyAny>> {
231 let client = self.clone();
232
233 pyo3_async_runtimes::tokio::future_into_py(py, async move {
234 let account_state = client
235 .request_account_state(account_id)
236 .await
237 .map_err(to_pyvalue_err)?;
238
239 Python::attach(|py| account_state.into_py_any(py))
240 })
241 }
242
243 #[pyo3(name = "request_trades")]
267 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
268 fn py_request_trades<'py>(
269 &self,
270 py: Python<'py>,
271 instrument_id: InstrumentId,
272 start: Option<Timestamp>,
273 end: Option<Timestamp>,
274 limit: Option<u32>,
275 ) -> PyResult<Bound<'py, PyAny>> {
276 let client = self.clone();
277
278 pyo3_async_runtimes::tokio::future_into_py(py, async move {
279 let trades = client
280 .request_trades(instrument_id, start, end, limit)
281 .await
282 .map_err(to_pyvalue_err)?;
283
284 Python::attach(|py| {
285 let py_trades = trades
286 .into_iter()
287 .map(|trade| trade.into_py_any(py))
288 .collect::<PyResult<Vec<_>>>()?;
289 let pylist = PyList::new(py, py_trades)?;
290 Ok(pylist.into_py_any_unwrap(py))
291 })
292 })
293 }
294
295 #[pyo3(name = "request_bars")]
311 #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
312 fn py_request_bars<'py>(
313 &self,
314 py: Python<'py>,
315 bar_type: BarType,
316 start: Option<Timestamp>,
317 end: Option<Timestamp>,
318 limit: Option<u32>,
319 ) -> PyResult<Bound<'py, PyAny>> {
320 let client = self.clone();
321
322 pyo3_async_runtimes::tokio::future_into_py(py, async move {
323 let bars = client
324 .request_bars(bar_type, start, end, limit)
325 .await
326 .map_err(to_pyvalue_err)?;
327
328 Python::attach(|py| {
329 let py_bars = bars
330 .into_iter()
331 .map(|bar| bar.into_py_any(py))
332 .collect::<PyResult<Vec<_>>>()?;
333 let pylist = PyList::new(py, py_bars)?;
334 Ok(pylist.into_py_any_unwrap(py))
335 })
336 })
337 }
338
339 #[pyo3(name = "request_book_snapshot")]
355 #[pyo3(signature = (instrument_id, depth=None))]
356 fn py_request_book_snapshot<'py>(
357 &self,
358 py: Python<'py>,
359 instrument_id: InstrumentId,
360 depth: Option<u32>,
361 ) -> PyResult<Bound<'py, PyAny>> {
362 let client = self.clone();
363
364 pyo3_async_runtimes::tokio::future_into_py(py, async move {
365 let book = client
366 .request_book_snapshot(instrument_id, depth)
367 .await
368 .map_err(to_pyvalue_err)?;
369
370 Python::attach(|py| book.into_py_any(py))
371 })
372 }
373
374 #[pyo3(name = "request_order_status_reports")]
387 #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=true))]
388 fn py_request_order_status_reports<'py>(
389 &self,
390 py: Python<'py>,
391 account_id: AccountId,
392 instrument_id: Option<InstrumentId>,
393 start: Option<u64>,
394 end: Option<u64>,
395 open_only: bool,
396 ) -> PyResult<Bound<'py, PyAny>> {
397 let client = self.clone();
398
399 pyo3_async_runtimes::tokio::future_into_py(py, async move {
400 let reports = client
401 .request_order_status_reports(
402 account_id,
403 instrument_id,
404 start.map(nautilus_core::UnixNanos::from),
405 end.map(nautilus_core::UnixNanos::from),
406 open_only,
407 )
408 .await
409 .map_err(to_pyvalue_err)?;
410
411 Python::attach(|py| {
412 let py_reports: PyResult<Vec<_>> = reports
413 .into_iter()
414 .map(|report| report.into_py_any(py))
415 .collect();
416 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
417 Ok(pylist)
418 })
419 })
420 }
421
422 #[pyo3(name = "request_fill_reports")]
435 #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
436 fn py_request_fill_reports<'py>(
437 &self,
438 py: Python<'py>,
439 account_id: AccountId,
440 instrument_id: Option<InstrumentId>,
441 start: Option<u64>,
442 end: Option<u64>,
443 ) -> PyResult<Bound<'py, PyAny>> {
444 let client = self.clone();
445
446 pyo3_async_runtimes::tokio::future_into_py(py, async move {
447 let reports = client
448 .request_fill_reports(
449 account_id,
450 instrument_id,
451 start.map(nautilus_core::UnixNanos::from),
452 end.map(nautilus_core::UnixNanos::from),
453 )
454 .await
455 .map_err(to_pyvalue_err)?;
456
457 Python::attach(|py| {
458 let py_reports: PyResult<Vec<_>> = reports
459 .into_iter()
460 .map(|report| report.into_py_any(py))
461 .collect();
462 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
463 Ok(pylist)
464 })
465 })
466 }
467
468 #[pyo3(name = "request_position_status_reports")]
480 #[pyo3(signature = (account_id, instrument_id=None))]
481 fn py_request_position_status_reports<'py>(
482 &self,
483 py: Python<'py>,
484 account_id: AccountId,
485 instrument_id: Option<InstrumentId>,
486 ) -> PyResult<Bound<'py, PyAny>> {
487 let client = self.clone();
488
489 pyo3_async_runtimes::tokio::future_into_py(py, async move {
490 let reports = client
491 .request_position_status_reports(account_id, instrument_id)
492 .await
493 .map_err(to_pyvalue_err)?;
494
495 Python::attach(|py| {
496 let py_reports: PyResult<Vec<_>> = reports
497 .into_iter()
498 .map(|report| report.into_py_any(py))
499 .collect();
500 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
501 Ok(pylist)
502 })
503 })
504 }
505
506 #[pyo3(name = "request_book_summaries")]
515 #[pyo3(signature = (currency, kind=None))]
516 fn py_request_book_summaries<'py>(
517 &self,
518 py: Python<'py>,
519 currency: String,
520 kind: Option<String>,
521 ) -> PyResult<Bound<'py, PyAny>> {
522 let client = self.clone();
523
524 pyo3_async_runtimes::tokio::future_into_py(py, async move {
525 let currency = currency.trim().to_ascii_uppercase();
526 if currency.is_empty() {
527 return Err(to_pyvalue_err(
528 "request_book_summaries requires a non-empty currency",
529 ));
530 }
531 let kind = kind
532 .as_deref()
533 .map(str::trim)
534 .filter(|value| !value.is_empty())
535 .map_or_else(|| "option".to_string(), str::to_ascii_lowercase);
536 let summaries = client
537 .request_book_summaries_kind(¤cy, Some(kind.as_str()))
538 .await
539 .map_err(to_pyvalue_err)?;
540 let ts = get_atomic_clock_realtime().get_time_ns();
542
543 Python::attach(|py| {
544 let py_items: PyResult<Vec<_>> = summaries
545 .into_iter()
546 .map(|raw| Py::new(py, DeribitBookSummary::from_raw(raw, ts)))
547 .collect();
548 let pylist = PyList::new(py, py_items?)?.into_any().unbind();
549 Ok(pylist)
550 })
551 })
552 }
553
554 #[pyo3(name = "request_forward_prices")]
559 #[pyo3(signature = (currency, instrument_id=None))]
560 fn py_request_forward_prices<'py>(
561 &self,
562 py: Python<'py>,
563 currency: String,
564 instrument_id: Option<InstrumentId>,
565 ) -> PyResult<Bound<'py, PyAny>> {
566 let client = self.clone();
567
568 pyo3_async_runtimes::tokio::future_into_py(py, async move {
569 let forward_prices = if let Some(inst_id) = instrument_id {
570 let instrument_name = inst_id.symbol.to_string();
572 let ticker = client
573 .request_ticker(&instrument_name)
574 .await
575 .map_err(to_pyvalue_err)?;
576
577 let ts = UnixNanos::default();
578 ticker
579 .underlying_price
580 .map(|up| {
581 vec![ForwardPrice::new(
582 inst_id,
583 up,
584 ticker.underlying_index.filter(|s| !s.is_empty()),
585 ts,
586 ts,
587 )]
588 })
589 .unwrap_or_default()
590 } else {
591 let summaries = client
593 .request_book_summaries(¤cy)
594 .await
595 .map_err(to_pyvalue_err)?;
596
597 let ts = nautilus_core::UnixNanos::default();
598 let mut seen_indices = HashSet::new();
599 summaries
600 .into_iter()
601 .filter_map(|s| {
602 let up = s.underlying_price?;
603 let idx = s.underlying_index.clone().unwrap_or_default();
604 if !seen_indices.insert(idx.clone()) {
605 return None;
606 }
607 Some(ForwardPrice::new(
608 InstrumentId::new(Symbol::new(&s.instrument_name), *DERIBIT_VENUE),
609 up,
610 Some(idx).filter(|s| !s.is_empty()),
611 ts,
612 ts,
613 ))
614 })
615 .collect()
616 };
617
618 Python::attach(|py| {
619 let py_prices: PyResult<Vec<_>> = forward_prices
620 .into_iter()
621 .map(|fp| Py::new(py, fp))
622 .collect();
623 let pylist = PyList::new(py, py_prices?)?.into_any().unbind();
624 Ok(pylist)
625 })
626 })
627 }
628}
629
630impl From<DeribitHttpError> for PyErr {
631 fn from(error: DeribitHttpError) -> Self {
632 match error {
633 DeribitHttpError::Canceled(msg) => to_pyruntime_err(format!("Request canceled: {msg}")),
635 DeribitHttpError::NetworkError(msg) => {
636 to_pyruntime_err(format!("Network error: {msg}"))
637 }
638 DeribitHttpError::UnexpectedStatus { status, body } => {
639 to_pyruntime_err(format!("Unexpected HTTP status code {status}: {body}"))
640 }
641 DeribitHttpError::Timeout(msg) => to_pyruntime_err(format!("Request timeout: {msg}")),
642 DeribitHttpError::MissingCredentials => {
644 to_pyvalue_err("Missing credentials for authenticated request")
645 }
646 DeribitHttpError::ValidationError(msg) => {
647 to_pyvalue_err(format!("Parameter validation error: {msg}"))
648 }
649 DeribitHttpError::JsonError(msg) => to_pyvalue_err(format!("JSON error: {msg}")),
650 DeribitHttpError::DeribitError {
651 error_code,
652 message,
653 } => to_pyvalue_err(format!("Deribit error {error_code}: {message}")),
654 }
655 }
656}