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