1use 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 let summaries = client
465 .request_book_summaries(¤cy)
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 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 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}