1use std::{
19 collections::HashMap,
20 str::FromStr,
21 sync::{
22 Arc,
23 atomic::{AtomicBool, AtomicU64, Ordering},
24 },
25};
26
27use ahash::{AHashMap, AHashSet};
28use indexmap::IndexMap;
29use jiff::Timestamp;
30use nautilus_common::cache::InstrumentLookupError;
31use nautilus_core::{
32 AtomicMap, AtomicTime, Params, datetime::nanos_to_millis, nanos::UnixNanos,
33 time::get_atomic_clock_realtime,
34};
35use nautilus_model::{
36 data::{Bar, BarType, TradeTick},
37 enums::{AggregationSource, BarAggregation},
38 events::AccountState,
39 identifiers::{AccountId, InstrumentId, Symbol},
40 instruments::{Instrument, InstrumentAny},
41 orderbook::OrderBook,
42 reports::{FillReport, OrderStatusReport, PositionStatusReport},
43};
44use nautilus_network::{
45 http::{HttpClient, HttpRedirectPolicy, Method, create_standard_nautilus_headers},
46 ratelimiter::quota::Quota,
47 retry::{RetryConfig, RetryError, RetryManager},
48};
49use serde::{Serialize, de::DeserializeOwned};
50use serde_json::json;
51use strum::IntoEnumIterator;
52use tokio_util::sync::CancellationToken;
53use ustr::Ustr;
54
55use super::{
56 error::DeribitHttpError,
57 models::{
58 DeribitAccountSummariesResponse, DeribitBookSummaryRaw, DeribitCombo, DeribitCurrency,
59 DeribitExpirationsResponse, DeribitInstrument, DeribitJsonRpcRequest,
60 DeribitJsonRpcResponse, DeribitPosition, DeribitProductType, DeribitTicker,
61 DeribitUserTradesResponse,
62 },
63 query::{
64 DeribitExpirationKind, GetAccountSummariesParams, GetBookSummaryByCurrencyParams,
65 GetCombosParams, GetExpirationsParams, GetInstrumentParams, GetInstrumentsParams,
66 GetOpenOrdersByInstrumentParams, GetOpenOrdersParams, GetOrderHistoryByCurrencyParams,
67 GetOrderHistoryByInstrumentParams, GetOrderStateParams, GetPositionsParams,
68 GetTickerParams, GetUserTradesByCurrencyAndTimeParams,
69 GetUserTradesByInstrumentAndTimeParams,
70 },
71};
72use crate::{
73 common::{
74 consts::{
75 DERIBIT_ACCOUNT_RATE_KEY, DERIBIT_API_PATH, DERIBIT_GLOBAL_RATE_KEY,
76 DERIBIT_HTTP_ACCOUNT_QUOTA, DERIBIT_HTTP_ORDER_QUOTA, DERIBIT_HTTP_REST_QUOTA,
77 DERIBIT_ORDER_RATE_KEY, DERIBIT_VENUE, JSONRPC_VERSION,
78 },
79 credential::{Credential, credential_env_vars},
80 enums::DeribitEnvironment,
81 parse::{
82 extract_server_timestamp, parse_account_state, parse_bars,
83 parse_deribit_instrument_any, parse_order_book, parse_trade_tick,
84 use_cost_for_bar_volume,
85 },
86 urls::get_http_base_url,
87 },
88 http::{
89 models::{DeribitOrderBook, DeribitTradesResponse, DeribitTradingViewChartData},
90 query::{
91 GetLastTradesByCurrencyParams, GetLastTradesByInstrumentAndTimeParams,
92 GetOrderBookParams, GetTradingViewChartDataParams,
93 },
94 },
95 websocket::{
96 messages::{DeribitOrderMsg, DeribitUserTradeMsg},
97 parse::{parse_position_status_report, parse_user_order_msg, parse_user_trade_msg},
98 },
99};
100
101pub const DERIBIT_HISTORICAL_TRADES_MAX_COUNT: u32 = 1000;
105
106struct TradePaginator {
112 seen_ids: AHashSet<String>,
113 cursor: i64,
114 end: i64,
115}
116
117impl TradePaginator {
118 fn new(start: i64, end: i64) -> Self {
119 Self {
120 seen_ids: AHashSet::new(),
121 cursor: start,
122 end,
123 }
124 }
125
126 fn advance(
129 &mut self,
130 ids: &[String],
131 timestamps: &[i64],
132 has_more: bool,
133 ) -> Option<Vec<usize>> {
134 if ids.is_empty() {
135 return None;
136 }
137
138 let prev_seen = self.seen_ids.len();
139 let mut new_indices = Vec::new();
140 let mut last_ts = self.cursor;
141
142 for (i, id) in ids.iter().enumerate() {
143 last_ts = timestamps[i];
144
145 if self.seen_ids.insert(id.clone()) {
146 new_indices.push(i);
147 }
148 }
149
150 if !has_more {
151 return Some(new_indices);
152 }
153
154 let new_count = self.seen_ids.len() - prev_seen;
155
156 if new_count == 0 {
157 self.cursor = last_ts + 1;
158 } else {
159 self.cursor = last_ts;
160 }
161
162 Some(new_indices)
163 }
164
165 fn is_exhausted(&self) -> bool {
168 self.cursor > self.end
169 }
170
171 fn reset(&mut self, start: i64) {
172 self.seen_ids.clear();
173 self.cursor = start;
174 }
175}
176
177#[derive(Debug)]
182pub struct DeribitRawHttpClient {
183 base_url: String,
184 client: HttpClient,
185 credential: Option<Credential>,
186 retry_manager: RetryManager<DeribitHttpError>,
187 cancellation_token: CancellationToken,
188 request_id: AtomicU64,
189}
190
191impl DeribitRawHttpClient {
192 pub fn new(
198 base_url: Option<String>,
199 environment: DeribitEnvironment,
200 timeout_secs: u64,
201 max_retries: u32,
202 retry_delay_ms: u64,
203 retry_delay_max_ms: u64,
204 proxy_url: Option<String>,
205 ) -> Result<Self, DeribitHttpError> {
206 let base_url = base_url
207 .unwrap_or_else(|| format!("{}{}", get_http_base_url(environment), DERIBIT_API_PATH));
208 let retry_config = RetryConfig {
209 max_retries,
210 initial_delay_ms: retry_delay_ms,
211 max_delay_ms: retry_delay_max_ms,
212 backoff_factor: 2.0,
213 jitter_ms: 1000,
214 operation_timeout_ms: Some(60_000),
215 immediate_first: false,
216 max_elapsed_ms: Some(180_000),
217 };
218
219 let retry_manager = RetryManager::new(retry_config);
220
221 Ok(Self {
222 base_url,
223 client: HttpClient::builder()
224 .headers(create_standard_nautilus_headers().into_iter().collect())
225 .keyed_quotas(Self::rate_limiter_quotas())
226 .default_quota(*DERIBIT_HTTP_REST_QUOTA)
227 .timeout_secs(timeout_secs)
228 .maybe_proxy_url(proxy_url)
229 .build()
230 .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?,
231 credential: None,
232 retry_manager,
233 cancellation_token: CancellationToken::new(),
234 request_id: AtomicU64::new(1),
235 })
236 }
237
238 pub fn cancellation_token(&self) -> &CancellationToken {
240 &self.cancellation_token
241 }
242
243 #[must_use]
245 pub fn is_testnet(&self) -> bool {
246 self.base_url.contains("test.")
247 }
248
249 fn rate_limiter_quotas() -> Vec<(String, Quota)> {
256 vec![
257 (
258 DERIBIT_GLOBAL_RATE_KEY.to_string(),
259 *DERIBIT_HTTP_REST_QUOTA,
260 ),
261 (
262 DERIBIT_ORDER_RATE_KEY.to_string(),
263 *DERIBIT_HTTP_ORDER_QUOTA,
264 ),
265 (
266 DERIBIT_ACCOUNT_RATE_KEY.to_string(),
267 *DERIBIT_HTTP_ACCOUNT_QUOTA,
268 ),
269 ]
270 }
271
272 fn rate_limit_keys(method: &str) -> Vec<String> {
276 let mut keys = vec![DERIBIT_GLOBAL_RATE_KEY.to_string()];
277
278 if Self::is_order_method(method) {
280 keys.push(DERIBIT_ORDER_RATE_KEY.to_string());
281 } else if Self::is_account_method(method) {
282 keys.push(DERIBIT_ACCOUNT_RATE_KEY.to_string());
283 }
284
285 keys.push(format!("deribit:{method}"));
287
288 keys
289 }
290
291 fn is_order_method(method: &str) -> bool {
293 matches!(
294 method,
295 "private/buy"
296 | "private/sell"
297 | "private/edit"
298 | "private/cancel"
299 | "private/cancel_all"
300 | "private/cancel_all_by_currency"
301 | "private/cancel_all_by_instrument"
302 | "private/cancel_by_label"
303 | "private/close_position"
304 )
305 }
306
307 fn is_account_method(method: &str) -> bool {
309 matches!(
310 method,
311 "private/get_account_summaries"
312 | "private/get_account_summary"
313 | "private/get_positions"
314 | "private/get_position"
315 | "private/get_open_orders_by_currency"
316 | "private/get_open_orders_by_instrument"
317 | "private/get_order_state"
318 | "private/get_user_trades_by_currency"
319 | "private/get_user_trades_by_instrument"
320 )
321 }
322
323 #[expect(clippy::too_many_arguments)]
329 pub fn with_credentials(
330 api_key: String,
331 api_secret: String,
332 base_url: Option<String>,
333 environment: DeribitEnvironment,
334 timeout_secs: u64,
335 max_retries: u32,
336 retry_delay_ms: u64,
337 retry_delay_max_ms: u64,
338 proxy_url: Option<String>,
339 ) -> Result<Self, DeribitHttpError> {
340 let base_url = base_url
341 .unwrap_or_else(|| format!("{}{}", get_http_base_url(environment), DERIBIT_API_PATH));
342 let retry_config = RetryConfig {
343 max_retries,
344 initial_delay_ms: retry_delay_ms,
345 max_delay_ms: retry_delay_max_ms,
346 backoff_factor: 2.0,
347 jitter_ms: 1000,
348 operation_timeout_ms: Some(60_000),
349 immediate_first: false,
350 max_elapsed_ms: Some(180_000),
351 };
352
353 let retry_manager = RetryManager::new(retry_config);
354 let credential = Credential::new(api_key, api_secret);
355
356 Ok(Self {
357 base_url,
358 client: HttpClient::builder()
359 .redirect_policy(HttpRedirectPolicy::Reject)
360 .headers(create_standard_nautilus_headers().into_iter().collect())
361 .keyed_quotas(Self::rate_limiter_quotas())
362 .default_quota(*DERIBIT_HTTP_REST_QUOTA)
363 .timeout_secs(timeout_secs)
364 .maybe_proxy_url(proxy_url)
365 .build()
366 .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?,
367 credential: Some(credential),
368 retry_manager,
369 cancellation_token: CancellationToken::new(),
370 request_id: AtomicU64::new(1),
371 })
372 }
373
374 #[expect(clippy::too_many_arguments)]
386 pub fn new_with_env(
387 api_key: Option<String>,
388 api_secret: Option<String>,
389 base_url: Option<String>,
390 environment: DeribitEnvironment,
391 timeout_secs: u64,
392 max_retries: u32,
393 retry_delay_ms: u64,
394 retry_delay_max_ms: u64,
395 proxy_url: Option<String>,
396 ) -> Result<Self, DeribitHttpError> {
397 let (key_env, secret_env) = credential_env_vars(environment);
399
400 let api_key = nautilus_core::env::get_or_env_var_opt(api_key, key_env);
402 let api_secret = nautilus_core::env::get_or_env_var_opt(api_secret, secret_env);
403
404 if let (Some(key), Some(secret)) = (api_key, api_secret) {
406 Self::with_credentials(
407 key,
408 secret,
409 base_url,
410 environment,
411 timeout_secs,
412 max_retries,
413 retry_delay_ms,
414 retry_delay_max_ms,
415 proxy_url,
416 )
417 } else {
418 Self::new(
420 base_url,
421 environment,
422 timeout_secs,
423 max_retries,
424 retry_delay_ms,
425 retry_delay_max_ms,
426 proxy_url,
427 )
428 }
429 }
430
431 async fn send_request<T, P>(
433 &self,
434 method: &str,
435 params: P,
436 authenticate: bool,
437 ) -> Result<DeribitJsonRpcResponse<T>, DeribitHttpError>
438 where
439 T: DeserializeOwned,
440 P: Serialize,
441 {
442 let operation_id = format!("{}#{}", self.base_url, method);
444 let params_clone = serde_json::to_value(¶ms)?;
445
446 let operation = || {
447 let method = method.to_string();
448 let params_clone = params_clone.clone();
449
450 async move {
451 let id = self.request_id.fetch_add(1, Ordering::SeqCst);
453 let request = DeribitJsonRpcRequest {
454 jsonrpc: JSONRPC_VERSION,
455 id,
456 method: method.clone(),
457 params: params_clone.clone(),
458 };
459
460 let body = serde_json::to_vec(&request)?;
461
462 let mut headers = HashMap::new();
464 headers.insert("Content-Type".to_string(), "application/json".to_string());
465
466 if authenticate {
468 let credentials = self
469 .credential
470 .as_ref()
471 .ok_or(DeribitHttpError::MissingCredentials)?;
472 let auth_headers = credentials.sign_auth_headers("POST", "/api/v2", &body)?;
473 headers.extend(auth_headers);
474 }
475
476 let rate_limit_keys = Self::rate_limit_keys(&method);
477 let resp = self
478 .client
479 .request(
480 Method::POST,
481 self.base_url.clone(),
482 None,
483 Some(headers),
484 Some(body),
485 None,
486 Some(rate_limit_keys),
487 )
488 .await
489 .map_err(|e| DeribitHttpError::NetworkError(e.to_string()))?;
490
491 let json_rpc_response: DeribitJsonRpcResponse<T> = match serde_json::from_slice(
497 &resp.body,
498 ) {
499 Ok(response) => response,
500 Err(e) if e.is_data() => {
501 log::warn!(
502 "Failed to deserialize Deribit JSON-RPC response: method={method}, status={}, error={e}",
503 resp.status.as_u16()
504 );
505 log::debug!(
506 "Response JSON (first 2000 chars): {}",
507 String::from_utf8_lossy(&resp.body)
508 .chars()
509 .take(2000)
510 .collect::<String>()
511 );
512 return Err(DeribitHttpError::JsonError(e.to_string()));
513 }
514 Err(_) => {
515 let error_body = String::from_utf8_lossy(&resp.body);
517 log::warn!(
518 "Non-JSON response: method={method}, status={}, body={error_body}",
519 resp.status.as_u16()
520 );
521 return Err(DeribitHttpError::UnexpectedStatus {
522 status: resp.status.as_u16(),
523 body: error_body.to_string(),
524 });
525 }
526 };
527
528 if json_rpc_response.result.is_some() {
530 Ok(json_rpc_response)
531 } else if let Some(error) = &json_rpc_response.error {
532 log::warn!(
534 "Deribit RPC error response: method={method}, http_status={}, error_code={}, error_message={}, error_data={:?}",
535 resp.status.as_u16(),
536 error.code,
537 error.message,
538 error.data
539 );
540
541 Err(DeribitHttpError::from_jsonrpc_error(
543 error.code,
544 error.message.clone(),
545 error.data.as_ref(),
546 ))
547 } else {
548 log::warn!(
549 "Response contains neither result nor error field: method={method}, status={}, request_id={:?}",
550 resp.status.as_u16(),
551 json_rpc_response.id
552 );
553 Err(DeribitHttpError::JsonError(
554 "Response contains neither result nor error".to_string(),
555 ))
556 }
557 }
558 };
559
560 let should_retry = |error: &DeribitHttpError| -> bool { error.is_retryable() };
569
570 let create_error = |error: RetryError| -> DeribitHttpError {
571 match error {
572 RetryError::Canceled => {
573 DeribitHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
574 }
575 error => DeribitHttpError::NetworkError(error.to_string()),
576 }
577 };
578
579 let result = self
580 .retry_manager
581 .invocation(&operation_id, operation, should_retry, create_error)
582 .cancellation_token(&self.cancellation_token)
583 .execute()
584 .await;
585
586 if let Err(ref e) = result
587 && e.is_retryable()
588 {
589 log::warn!("Request exhausted retries: method={method}, error={e}");
590 }
591
592 result
593 }
594
595 pub async fn get_instruments(
601 &self,
602 params: GetInstrumentsParams,
603 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitInstrument>>, DeribitHttpError> {
604 self.send_request("public/get_instruments", params, false)
605 .await
606 }
607
608 pub async fn get_instrument(
614 &self,
615 params: GetInstrumentParams,
616 ) -> Result<DeribitJsonRpcResponse<DeribitInstrument>, DeribitHttpError> {
617 self.send_request("public/get_instrument", params, false)
618 .await
619 }
620
621 pub async fn get_combos(
627 &self,
628 params: GetCombosParams,
629 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitCombo>>, DeribitHttpError> {
630 self.send_request("public/get_combos", params, false).await
631 }
632
633 pub async fn get_last_trades_by_instrument_and_time(
639 &self,
640 params: GetLastTradesByInstrumentAndTimeParams,
641 ) -> Result<DeribitJsonRpcResponse<DeribitTradesResponse>, DeribitHttpError> {
642 self.send_request(
643 "public/get_last_trades_by_instrument_and_time",
644 params,
645 false,
646 )
647 .await
648 }
649
650 pub async fn get_last_trades_by_currency(
660 &self,
661 params: GetLastTradesByCurrencyParams,
662 ) -> Result<DeribitJsonRpcResponse<DeribitTradesResponse>, DeribitHttpError> {
663 self.send_request("public/get_last_trades_by_currency", params, false)
664 .await
665 }
666
667 pub async fn get_expirations(
673 &self,
674 params: GetExpirationsParams,
675 ) -> Result<DeribitJsonRpcResponse<DeribitExpirationsResponse>, DeribitHttpError> {
676 self.send_request("public/get_expirations", params, false)
677 .await
678 }
679
680 pub async fn get_tradingview_chart_data(
686 &self,
687 params: GetTradingViewChartDataParams,
688 ) -> Result<DeribitJsonRpcResponse<DeribitTradingViewChartData>, DeribitHttpError> {
689 self.send_request("public/get_tradingview_chart_data", params, false)
690 .await
691 }
692
693 pub async fn get_account_summaries(
702 &self,
703 params: GetAccountSummariesParams,
704 ) -> Result<DeribitJsonRpcResponse<DeribitAccountSummariesResponse>, DeribitHttpError> {
705 self.send_request("private/get_account_summaries", params, true)
706 .await
707 }
708
709 pub async fn get_order_book(
715 &self,
716 params: GetOrderBookParams,
717 ) -> Result<DeribitJsonRpcResponse<DeribitOrderBook>, DeribitHttpError> {
718 self.send_request("public/get_order_book", params, false)
719 .await
720 }
721
722 pub async fn get_order_state(
731 &self,
732 params: GetOrderStateParams,
733 ) -> Result<DeribitJsonRpcResponse<DeribitOrderMsg>, DeribitHttpError> {
734 self.send_request("private/get_order_state", params, true)
735 .await
736 }
737
738 pub async fn get_open_orders(
747 &self,
748 params: GetOpenOrdersParams,
749 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
750 self.send_request("private/get_open_orders", params, true)
751 .await
752 }
753
754 pub async fn get_open_orders_by_instrument(
763 &self,
764 params: GetOpenOrdersByInstrumentParams,
765 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
766 self.send_request("private/get_open_orders_by_instrument", params, true)
767 .await
768 }
769
770 pub async fn get_order_history_by_instrument(
779 &self,
780 params: GetOrderHistoryByInstrumentParams,
781 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
782 self.send_request("private/get_order_history_by_instrument", params, true)
783 .await
784 }
785
786 pub async fn get_order_history_by_currency(
795 &self,
796 params: GetOrderHistoryByCurrencyParams,
797 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
798 self.send_request("private/get_order_history_by_currency", params, true)
799 .await
800 }
801
802 pub async fn get_user_trades_by_instrument_and_time(
811 &self,
812 params: GetUserTradesByInstrumentAndTimeParams,
813 ) -> Result<DeribitJsonRpcResponse<DeribitUserTradesResponse>, DeribitHttpError> {
814 self.send_request(
815 "private/get_user_trades_by_instrument_and_time",
816 params,
817 true,
818 )
819 .await
820 }
821
822 pub async fn get_user_trades_by_currency_and_time(
831 &self,
832 params: GetUserTradesByCurrencyAndTimeParams,
833 ) -> Result<DeribitJsonRpcResponse<DeribitUserTradesResponse>, DeribitHttpError> {
834 self.send_request("private/get_user_trades_by_currency_and_time", params, true)
835 .await
836 }
837
838 pub async fn get_book_summary_by_currency(
844 &self,
845 params: GetBookSummaryByCurrencyParams,
846 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitBookSummaryRaw>>, DeribitHttpError> {
847 self.send_request("public/get_book_summary_by_currency", params, false)
848 .await
849 }
850
851 pub async fn get_ticker(
857 &self,
858 params: GetTickerParams,
859 ) -> Result<DeribitJsonRpcResponse<DeribitTicker>, DeribitHttpError> {
860 self.send_request("public/ticker", params, false).await
861 }
862
863 pub async fn get_positions(
872 &self,
873 params: GetPositionsParams,
874 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitPosition>>, DeribitHttpError> {
875 self.send_request("private/get_positions", params, true)
876 .await
877 }
878}
879
880#[derive(Debug)]
885#[cfg_attr(
886 feature = "python",
887 pyo3::pyclass(module = "nautilus_trader.adapters.deribit", from_py_object)
888)]
889#[cfg_attr(
890 feature = "python",
891 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.deribit")
892)]
893pub struct DeribitHttpClient {
894 pub(crate) inner: Arc<DeribitRawHttpClient>,
895 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
896 clock: &'static AtomicTime,
897 cache_initialized: AtomicBool,
898}
899
900impl Clone for DeribitHttpClient {
901 fn clone(&self) -> Self {
902 let cache_initialized = AtomicBool::new(false);
903
904 let is_initialized = self.cache_initialized.load(Ordering::Acquire);
905 if is_initialized {
906 cache_initialized.store(true, Ordering::Release);
907 }
908
909 Self {
910 inner: self.inner.clone(),
911 instruments_cache: self.instruments_cache.clone(),
912 cache_initialized,
913 clock: self.clock,
914 }
915 }
916}
917
918impl DeribitHttpClient {
919 #[must_use]
924 pub fn inner(&self) -> &DeribitRawHttpClient {
925 &self.inner
926 }
927
928 pub fn new(
938 base_url: Option<String>,
939 environment: DeribitEnvironment,
940 timeout_secs: u64,
941 max_retries: u32,
942 retry_delay_ms: u64,
943 retry_delay_max_ms: u64,
944 proxy_url: Option<String>,
945 ) -> anyhow::Result<Self> {
946 let raw_client = Arc::new(DeribitRawHttpClient::new(
947 base_url,
948 environment,
949 timeout_secs,
950 max_retries,
951 retry_delay_ms,
952 retry_delay_max_ms,
953 proxy_url,
954 )?);
955
956 Ok(Self {
957 inner: raw_client,
958 instruments_cache: Arc::new(AtomicMap::new()),
959 cache_initialized: AtomicBool::new(false),
960 clock: get_atomic_clock_realtime(),
961 })
962 }
963
964 #[expect(clippy::too_many_arguments)]
976 pub fn new_with_env(
977 api_key: Option<String>,
978 api_secret: Option<String>,
979 base_url: Option<String>,
980 environment: DeribitEnvironment,
981 timeout_secs: u64,
982 max_retries: u32,
983 retry_delay_ms: u64,
984 retry_delay_max_ms: u64,
985 proxy_url: Option<String>,
986 ) -> anyhow::Result<Self> {
987 let raw_client = Arc::new(DeribitRawHttpClient::new_with_env(
988 api_key,
989 api_secret,
990 base_url,
991 environment,
992 timeout_secs,
993 max_retries,
994 retry_delay_ms,
995 retry_delay_max_ms,
996 proxy_url,
997 )?);
998
999 Ok(Self {
1000 inner: raw_client,
1001 instruments_cache: Arc::new(AtomicMap::new()),
1002 cache_initialized: AtomicBool::new(false),
1003 clock: get_atomic_clock_realtime(),
1004 })
1005 }
1006
1007 pub async fn request_instruments(
1013 &self,
1014 currency: DeribitCurrency,
1015 product_type: Option<DeribitProductType>,
1016 ) -> anyhow::Result<Vec<InstrumentAny>> {
1017 let params = if let Some(pt) = product_type {
1019 GetInstrumentsParams::with_kind(currency, pt)
1020 } else {
1021 GetInstrumentsParams::new(currency)
1022 };
1023
1024 let full_response = self.inner.get_instruments(params).await?;
1026 let result = full_response
1027 .result
1028 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1029 let ts_event = extract_server_timestamp(full_response.us_out)?;
1030 let ts_init = self.generate_ts_init();
1031 let combo_by_id = self.combo_map_for_instruments(currency, &result).await;
1032
1033 let mut instruments = Vec::new();
1035 let mut skipped_count = 0;
1036 let mut error_count = 0;
1037
1038 for raw_instrument in result {
1039 match parse_deribit_instrument_any(&raw_instrument, ts_init, ts_event) {
1040 Ok(Some(mut instrument)) => {
1041 if let Some(combo) = combo_by_id.get(&raw_instrument.instrument_name) {
1042 Self::attach_combo_leg_info(&mut instrument, combo);
1043 }
1044 instruments.push(instrument);
1045 }
1046 Ok(None) => {
1047 skipped_count += 1;
1049 log::debug!(
1050 "Skipped unsupported instrument type: {} (kind: {:?})",
1051 raw_instrument.instrument_name,
1052 raw_instrument.kind
1053 );
1054 }
1055 Err(e) => {
1056 error_count += 1;
1057 log::warn!(
1058 "Failed to parse instrument {}: {}",
1059 raw_instrument.instrument_name,
1060 e
1061 );
1062 }
1063 }
1064 }
1065
1066 log::debug!(
1067 "Parsed {} instruments ({} skipped, {} errors)",
1068 instruments.len(),
1069 skipped_count,
1070 error_count
1071 );
1072
1073 Ok(instruments)
1074 }
1075
1076 pub async fn request_instrument(
1088 &self,
1089 instrument_id: InstrumentId,
1090 ) -> anyhow::Result<InstrumentAny> {
1091 let params = GetInstrumentParams {
1092 instrument_name: instrument_id.symbol.to_string(),
1093 };
1094
1095 let full_response = self.inner.get_instrument(params).await?;
1096 let response = full_response
1097 .result
1098 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1099 let ts_event = extract_server_timestamp(full_response.us_out)?;
1100 let ts_init = self.generate_ts_init();
1101
1102 match parse_deribit_instrument_any(&response, ts_init, ts_event)? {
1103 Some(mut instrument) => {
1104 if Self::is_combo_kind(response.kind) {
1105 let currency = DeribitCurrency::from_str(response.base_currency.as_str())
1106 .unwrap_or(DeribitCurrency::ANY);
1107 let combo_by_id = self
1108 .combo_map_for_instruments(currency, std::slice::from_ref(&response))
1109 .await;
1110
1111 if let Some(combo) = combo_by_id.get(&response.instrument_name) {
1112 Self::attach_combo_leg_info(&mut instrument, combo);
1113 }
1114 }
1115
1116 Ok(instrument)
1117 }
1118 None => anyhow::bail!(
1119 "Unsupported instrument type: {} (kind: {:?})",
1120 response.instrument_name,
1121 response.kind
1122 ),
1123 }
1124 }
1125
1126 async fn combo_map_for_instruments(
1127 &self,
1128 requested_currency: DeribitCurrency,
1129 raw_instruments: &[DeribitInstrument],
1130 ) -> AHashMap<Ustr, DeribitCombo> {
1131 if !raw_instruments
1132 .iter()
1133 .any(|instrument| Self::is_combo_kind(instrument.kind))
1134 {
1135 return AHashMap::new();
1136 }
1137
1138 let mut currencies = AHashSet::new();
1139
1140 if requested_currency == DeribitCurrency::ANY {
1141 for instrument in raw_instruments
1142 .iter()
1143 .filter(|instrument| Self::is_combo_kind(instrument.kind))
1144 {
1145 if let Ok(currency) = DeribitCurrency::from_str(instrument.base_currency.as_str()) {
1146 currencies.insert(currency);
1147 }
1148 }
1149 } else {
1150 currencies.insert(requested_currency);
1151 }
1152
1153 let mut combo_by_id = AHashMap::new();
1154
1155 for currency in currencies {
1156 match self.inner.get_combos(GetCombosParams::new(currency)).await {
1157 Ok(response) => {
1158 if let Some(combos) = response.result {
1159 for combo in combos {
1160 combo_by_id.insert(combo.id, combo);
1161 }
1162 }
1163 }
1164 Err(e) => {
1165 log::warn!("Failed to load Deribit combo definitions for {currency}: {e}");
1166 }
1167 }
1168 }
1169
1170 combo_by_id
1171 }
1172
1173 fn is_combo_kind(kind: DeribitProductType) -> bool {
1174 matches!(
1175 kind,
1176 DeribitProductType::FutureCombo | DeribitProductType::OptionCombo
1177 )
1178 }
1179
1180 fn attach_combo_leg_info(instrument: &mut InstrumentAny, combo: &DeribitCombo) {
1181 if let Some(info) = Self::combo_leg_info(instrument, combo) {
1182 match instrument {
1183 InstrumentAny::CryptoOptionSpread(spread) => spread.info = Some(info),
1184 InstrumentAny::CryptoFuturesSpread(spread) => spread.info = Some(info),
1185 _ => {}
1186 }
1187 }
1188 }
1189
1190 fn combo_leg_info(instrument: &InstrumentAny, combo: &DeribitCombo) -> Option<Params> {
1191 let existing_info = match instrument {
1192 InstrumentAny::CryptoOptionSpread(spread) => spread.info.as_ref(),
1193 InstrumentAny::CryptoFuturesSpread(spread) => spread.info.as_ref(),
1194 _ => return None,
1195 };
1196
1197 let mut info = existing_info
1198 .map(|info| IndexMap::clone(info))
1199 .unwrap_or_default();
1200
1201 let legs = combo
1202 .legs
1203 .iter()
1204 .map(|leg| {
1205 let instrument_id =
1206 InstrumentId::new(Symbol::new(leg.instrument_name), *DERIBIT_VENUE);
1207
1208 json!({
1209 "amount": leg.amount,
1210 "instrument_id": instrument_id.to_string(),
1211 "instrument_name": leg.instrument_name,
1212 })
1213 })
1214 .collect::<Vec<_>>();
1215
1216 info.insert("deribit_combo_id".to_string(), json!(combo.id));
1217 info.insert(
1218 "deribit_combo_state".to_string(),
1219 json!(combo.state.as_str()),
1220 );
1221 info.insert("deribit_combo_legs".to_string(), json!(legs));
1222
1223 Some(Params::new(info))
1224 }
1225
1226 pub async fn request_trades(
1250 &self,
1251 instrument_id: InstrumentId,
1252 start: Option<Timestamp>,
1253 end: Option<Timestamp>,
1254 limit: Option<u32>,
1255 ) -> anyhow::Result<Vec<TradeTick>> {
1256 let (price_precision, size_precision) =
1258 if let Some(instrument) = self.get_instrument(&instrument_id.symbol.inner()) {
1259 (instrument.price_precision(), instrument.size_precision())
1260 } else {
1261 log::warn!("Instrument {instrument_id} not in cache, skipping trades request");
1262 return Err(InstrumentLookupError::not_found(instrument_id).into());
1263 };
1264
1265 let now = Timestamp::now();
1267 let end_dt = end.unwrap_or(now);
1268 let start_dt = start.unwrap_or(end_dt - jiff::SignedDuration::from_hours(1));
1269
1270 if let (Some(s), Some(e)) = (start, end) {
1271 anyhow::ensure!(s < e, "Invalid time range: start={s:?} end={e:?}");
1272 }
1273
1274 let start_ms = start_dt.as_millisecond();
1275 let end_ms = end_dt.as_millisecond();
1276 let ts_init = self.generate_ts_init();
1277 let mut all_trades = Vec::new();
1278 let mut paginator = TradePaginator::new(start_ms, end_ms);
1279
1280 loop {
1281 let params = GetLastTradesByInstrumentAndTimeParams::new(
1282 instrument_id.symbol.to_string(),
1283 paginator.cursor,
1284 end_ms,
1285 Some(DERIBIT_HISTORICAL_TRADES_MAX_COUNT),
1286 Some("asc".to_string()),
1287 );
1288
1289 let full_response = self
1290 .inner
1291 .get_last_trades_by_instrument_and_time(params)
1292 .await
1293 .map_err(|e| anyhow::anyhow!(e))?;
1294
1295 let response_data = full_response
1296 .result
1297 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1298
1299 let ids: Vec<String> = response_data
1300 .trades
1301 .iter()
1302 .map(|t| t.trade_id.clone())
1303 .collect();
1304 let timestamps: Vec<i64> = response_data.trades.iter().map(|t| t.timestamp).collect();
1305
1306 let Some(new_indices) = paginator.advance(&ids, ×tamps, response_data.has_more)
1307 else {
1308 break;
1309 };
1310
1311 for i in &new_indices {
1312 let raw_trade = &response_data.trades[*i];
1313
1314 match parse_trade_tick(
1315 raw_trade,
1316 instrument_id,
1317 price_precision,
1318 size_precision,
1319 ts_init,
1320 ) {
1321 Ok(trade) => {
1322 all_trades.push(trade);
1323
1324 if let Some(max) = limit
1325 && all_trades.len() >= max as usize
1326 {
1327 return Ok(all_trades);
1328 }
1329 }
1330 Err(e) => {
1331 log::warn!(
1332 "Failed to parse trade {} for {}: {}",
1333 raw_trade.trade_id,
1334 instrument_id,
1335 e
1336 );
1337 }
1338 }
1339 }
1340
1341 if !response_data.has_more || paginator.is_exhausted() {
1342 break;
1343 }
1344 }
1345
1346 log::debug!(
1347 "Fetched {} historical trades for {} from {} to {}",
1348 all_trades.len(),
1349 instrument_id,
1350 start_dt,
1351 end_dt
1352 );
1353
1354 Ok(all_trades)
1355 }
1356
1357 pub async fn request_bars(
1373 &self,
1374 bar_type: BarType,
1375 start: Option<Timestamp>,
1376 end: Option<Timestamp>,
1377 limit: Option<u32>,
1378 ) -> anyhow::Result<Vec<Bar>> {
1379 anyhow::ensure!(
1380 bar_type.aggregation_source() == AggregationSource::External,
1381 "Only EXTERNAL aggregation is supported"
1382 );
1383
1384 let now = Timestamp::now();
1385
1386 let end_dt = end.unwrap_or(now);
1388 let start_dt = start.unwrap_or(end_dt - jiff::SignedDuration::from_hours(1));
1389
1390 if let (Some(s), Some(e)) = (start, end) {
1391 anyhow::ensure!(s < e, "Invalid time range: start={s:?} end={e:?}");
1392 }
1393
1394 let spec = bar_type.spec();
1396 let step = spec.step.get();
1397 let resolution = match spec.aggregation {
1398 BarAggregation::Minute => format!("{step}"),
1399 BarAggregation::Hour => format!("{}", step * 60),
1400 BarAggregation::Day => "1D".to_string(),
1401 a => anyhow::bail!("Deribit does not support {a:?} aggregation"),
1402 };
1403
1404 let supported_resolutions = [
1406 "1", "3", "5", "10", "15", "30", "60", "120", "180", "360", "720", "1D",
1407 ];
1408
1409 if !supported_resolutions.contains(&resolution.as_str()) {
1410 anyhow::bail!(
1411 "Deribit does not support resolution '{resolution}'. Supported: {supported_resolutions:?}"
1412 );
1413 }
1414
1415 let instrument_id = bar_type.instrument_id();
1416 let (price_precision, size_precision, use_cost_for_volume) =
1417 if let Some(instrument) = self.get_instrument(&instrument_id.symbol.inner()) {
1418 (
1419 instrument.price_precision(),
1420 instrument.size_precision(),
1421 use_cost_for_bar_volume(&instrument),
1422 )
1423 } else {
1424 log::warn!("Instrument {instrument_id} not in cache, skipping bars request");
1425 return Err(InstrumentLookupError::not_found(instrument_id).into());
1426 };
1427
1428 let instrument_name = instrument_id.symbol.to_string();
1429 let start_timestamp = start_dt.as_millisecond();
1430 let end_timestamp = end_dt.as_millisecond();
1431
1432 let params = GetTradingViewChartDataParams::new(
1433 instrument_name,
1434 start_timestamp,
1435 end_timestamp,
1436 resolution,
1437 );
1438
1439 let full_response = self.inner.get_tradingview_chart_data(params).await?;
1440 let chart_data = full_response
1441 .result
1442 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1443
1444 if chart_data.status == "no_data" {
1445 log::debug!("No bar data returned for {bar_type}");
1446 return Ok(Vec::new());
1447 }
1448
1449 let ts_init = self.generate_ts_init();
1450 let mut bars = parse_bars(
1451 &chart_data,
1452 bar_type,
1453 price_precision,
1454 size_precision,
1455 use_cost_for_volume,
1456 ts_init,
1457 )?;
1458
1459 if let Some(max) = limit {
1460 let max = max as usize;
1461 if bars.len() > max {
1462 bars.drain(..bars.len() - max);
1463 }
1464 }
1465
1466 log::debug!("Parsed {} bars for {}", bars.len(), bar_type);
1467
1468 Ok(bars)
1469 }
1470
1471 pub async fn request_book_snapshot(
1487 &self,
1488 instrument_id: InstrumentId,
1489 depth: Option<u32>,
1490 ) -> anyhow::Result<OrderBook> {
1491 let (price_precision, size_precision) =
1492 if let Some(instrument) = self.get_instrument(&instrument_id.symbol.inner()) {
1493 (instrument.price_precision(), instrument.size_precision())
1494 } else {
1495 return Err(InstrumentLookupError::not_found(instrument_id).into());
1496 };
1497
1498 let params = GetOrderBookParams::new(instrument_id.symbol.to_string(), depth);
1499 let full_response = self
1500 .inner
1501 .get_order_book(params)
1502 .await
1503 .map_err(|e| anyhow::anyhow!(e))?;
1504
1505 let order_book_data = full_response
1506 .result
1507 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1508
1509 let ts_init = self.generate_ts_init();
1510 let book = parse_order_book(
1511 &order_book_data,
1512 instrument_id,
1513 price_precision,
1514 size_precision,
1515 ts_init,
1516 )?;
1517
1518 log::debug!(
1519 "Fetched order book for {} with {} bids and {} asks",
1520 instrument_id,
1521 order_book_data.bids.len(),
1522 order_book_data.asks.len()
1523 );
1524
1525 Ok(book)
1526 }
1527
1528 pub async fn request_account_state(
1539 &self,
1540 account_id: AccountId,
1541 ) -> anyhow::Result<AccountState> {
1542 let params = GetAccountSummariesParams::default();
1543 let full_response = self
1544 .inner
1545 .get_account_summaries(params)
1546 .await
1547 .map_err(|e| anyhow::anyhow!(e))?;
1548 let response_data = full_response
1549 .result
1550 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1551 let ts_init = self.generate_ts_init();
1552 let ts_event = extract_server_timestamp(full_response.us_out)?;
1553
1554 parse_account_state(&response_data.summaries, account_id, ts_init, ts_event)
1555 }
1556
1557 fn generate_ts_init(&self) -> UnixNanos {
1559 self.clock.get_time_ns()
1560 }
1561
1562 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1564 self.instruments_cache.rcu(|m| {
1565 for inst in instruments {
1566 m.insert(inst.raw_symbol().inner(), inst.clone());
1567 }
1568 });
1569 self.cache_initialized.store(true, Ordering::Release);
1570 }
1571
1572 #[must_use]
1574 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1575 self.instruments_cache.get_cloned(symbol)
1576 }
1577
1578 #[must_use]
1580 pub fn is_cache_initialized(&self) -> bool {
1581 self.cache_initialized.load(Ordering::Acquire)
1582 }
1583
1584 #[must_use]
1586 pub fn is_testnet(&self) -> bool {
1587 self.inner.is_testnet()
1588 }
1589
1590 pub async fn request_order_status_reports(
1603 &self,
1604 account_id: AccountId,
1605 instrument_id: Option<InstrumentId>,
1606 start: Option<UnixNanos>,
1607 end: Option<UnixNanos>,
1608 open_only: bool,
1609 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1610 let ts_init = self.generate_ts_init();
1611 let mut reports = Vec::new();
1612 let mut seen_order_ids = AHashSet::new();
1613
1614 let mut parse_and_add = |order: &DeribitOrderMsg| {
1615 let symbol = order.instrument_name;
1616 if let Some(instrument) = self.get_instrument(&symbol) {
1617 match parse_user_order_msg(order, &instrument, account_id, ts_init) {
1618 Ok(report) => {
1619 let ts_last = report.ts_last;
1621 let in_range = match (start, end) {
1622 (Some(s), Some(e)) => ts_last >= s && ts_last <= e,
1623 (Some(s), None) => ts_last >= s,
1624 (None, Some(e)) => ts_last <= e,
1625 (None, None) => true,
1626 };
1627 if in_range && seen_order_ids.insert(order.order_id.clone()) {
1629 reports.push(report);
1630 }
1631 }
1632 Err(e) => {
1633 log::warn!(
1634 "Failed to parse order {} for {}: {}",
1635 order.order_id,
1636 order.instrument_name,
1637 e
1638 );
1639 }
1640 }
1641 } else {
1642 log::debug!(
1643 "Skipping order {} - instrument {} not in cache",
1644 order.order_id,
1645 order.instrument_name
1646 );
1647 }
1648 };
1649
1650 if let Some(instrument_id) = instrument_id {
1651 let instrument_name = instrument_id.symbol.to_string();
1653
1654 let open_params = GetOpenOrdersByInstrumentParams {
1656 instrument_name: instrument_name.clone(),
1657 r#type: None,
1658 };
1659
1660 if let Some(orders) = self
1661 .inner
1662 .get_open_orders_by_instrument(open_params)
1663 .await?
1664 .result
1665 {
1666 for order in &orders {
1667 parse_and_add(order);
1668 }
1669 }
1670
1671 if !open_only {
1672 const PAGE_SIZE: u32 = 100;
1673 let mut offset: u32 = 0;
1674
1675 loop {
1676 let history_params = GetOrderHistoryByInstrumentParams {
1677 instrument_name: instrument_name.clone(),
1678 count: Some(PAGE_SIZE),
1679 offset: Some(offset),
1680 include_old: Some(true),
1681 include_unfilled: Some(true),
1682 };
1683 let orders = self
1684 .inner
1685 .get_order_history_by_instrument(history_params)
1686 .await?
1687 .result
1688 .unwrap_or_default();
1689
1690 let count = orders.len() as u32;
1691 for order in &orders {
1692 parse_and_add(order);
1693 }
1694
1695 if count < PAGE_SIZE {
1696 break;
1697 }
1698 offset += count;
1699 }
1700 }
1701 } else {
1702 let open_params = GetOpenOrdersParams::default();
1704 if let Some(orders) = self.inner.get_open_orders(open_params).await?.result {
1705 for order in &orders {
1706 parse_and_add(order);
1707 }
1708 }
1709
1710 if !open_only {
1711 const PAGE_SIZE: u32 = 100;
1712
1713 for currency in DeribitCurrency::iter().filter(|c| *c != DeribitCurrency::ANY) {
1714 let mut offset: u32 = 0;
1715
1716 loop {
1717 let history_params = GetOrderHistoryByCurrencyParams {
1718 currency,
1719 kind: None,
1720 count: Some(PAGE_SIZE),
1721 offset: Some(offset),
1722 include_old: Some(true),
1723 include_unfilled: Some(true),
1724 };
1725 let orders = self
1726 .inner
1727 .get_order_history_by_currency(history_params)
1728 .await?
1729 .result
1730 .unwrap_or_default();
1731
1732 let count = orders.len() as u32;
1733 for order in &orders {
1734 parse_and_add(order);
1735 }
1736
1737 if count < PAGE_SIZE {
1738 break;
1739 }
1740 offset += count;
1741 }
1742 }
1743 }
1744 }
1745
1746 log::debug!("Generated {} order status reports", reports.len());
1747 Ok(reports)
1748 }
1749
1750 pub async fn request_fill_reports(
1763 &self,
1764 account_id: AccountId,
1765 instrument_id: Option<InstrumentId>,
1766 start: Option<UnixNanos>,
1767 end: Option<UnixNanos>,
1768 ) -> anyhow::Result<Vec<FillReport>> {
1769 let ts_init = self.generate_ts_init();
1770 let now_ms = Timestamp::now().as_millisecond();
1771
1772 let start_ms = start.map_or(0, |ns| nanos_to_millis(ns.as_u64()) as i64);
1774 let end_ms = end.map_or(now_ms, |ns| nanos_to_millis(ns.as_u64()) as i64);
1775 let mut reports = Vec::new();
1776
1777 let mut parse_and_add = |trade: &DeribitUserTradeMsg| {
1778 let symbol = trade.instrument_name;
1779 if let Some(instrument) = self.get_instrument(&symbol) {
1780 match parse_user_trade_msg(trade, &instrument, account_id, ts_init) {
1781 Ok(report) => reports.push(report),
1782 Err(e) => {
1783 log::warn!(
1784 "Failed to parse trade {} for {}: {}",
1785 trade.trade_id,
1786 trade.instrument_name,
1787 e
1788 );
1789 }
1790 }
1791 } else {
1792 log::debug!(
1793 "Skipping trade {} - instrument {} not in cache",
1794 trade.trade_id,
1795 trade.instrument_name
1796 );
1797 }
1798 };
1799
1800 let mut paginator = TradePaginator::new(start_ms, end_ms);
1801
1802 if let Some(instrument_id) = instrument_id {
1803 loop {
1804 let params = GetUserTradesByInstrumentAndTimeParams {
1805 instrument_name: instrument_id.symbol.to_string(),
1806 start_timestamp: paginator.cursor,
1807 end_timestamp: end_ms,
1808 count: Some(DERIBIT_HISTORICAL_TRADES_MAX_COUNT),
1809 sorting: Some("asc".to_string()),
1810 };
1811 let response = self
1812 .inner
1813 .get_user_trades_by_instrument_and_time(params)
1814 .await?;
1815
1816 let Some(data) = response.result else { break };
1817
1818 let ids: Vec<String> = data.trades.iter().map(|t| t.trade_id.clone()).collect();
1819 let timestamps: Vec<i64> = data.trades.iter().map(|t| t.timestamp as i64).collect();
1820
1821 let Some(new_indices) = paginator.advance(&ids, ×tamps, data.has_more) else {
1822 break;
1823 };
1824
1825 for i in &new_indices {
1826 parse_and_add(&data.trades[*i]);
1827 }
1828
1829 if !data.has_more || paginator.is_exhausted() {
1830 break;
1831 }
1832 }
1833 } else {
1834 for currency in DeribitCurrency::iter().filter(|c| *c != DeribitCurrency::ANY) {
1835 paginator.reset(start_ms);
1836
1837 loop {
1838 let params = GetUserTradesByCurrencyAndTimeParams {
1839 currency,
1840 start_timestamp: paginator.cursor,
1841 end_timestamp: end_ms,
1842 kind: None,
1843 count: Some(DERIBIT_HISTORICAL_TRADES_MAX_COUNT),
1844 sorting: Some("asc".to_string()),
1845 };
1846 let response = self
1847 .inner
1848 .get_user_trades_by_currency_and_time(params)
1849 .await?;
1850
1851 let Some(data) = response.result else { break };
1852
1853 let ids: Vec<String> = data.trades.iter().map(|t| t.trade_id.clone()).collect();
1854 let timestamps: Vec<i64> =
1855 data.trades.iter().map(|t| t.timestamp as i64).collect();
1856
1857 let Some(new_indices) = paginator.advance(&ids, ×tamps, data.has_more)
1858 else {
1859 break;
1860 };
1861
1862 for i in &new_indices {
1863 parse_and_add(&data.trades[*i]);
1864 }
1865
1866 if !data.has_more || paginator.is_exhausted() {
1867 break;
1868 }
1869 }
1870 }
1871 }
1872
1873 log::debug!("Generated {} fill reports", reports.len());
1874 Ok(reports)
1875 }
1876
1877 pub async fn request_ticker(&self, instrument_name: &str) -> anyhow::Result<DeribitTicker> {
1885 let params = GetTickerParams {
1886 instrument_name: instrument_name.to_string(),
1887 };
1888 let response = self
1889 .inner
1890 .get_ticker(params)
1891 .await
1892 .map_err(|e| anyhow::anyhow!(e))?;
1893 response
1894 .result
1895 .ok_or_else(|| anyhow::anyhow!("No result in ticker response"))
1896 }
1897
1898 pub async fn request_book_summaries(
1907 &self,
1908 currency: &str,
1909 ) -> anyhow::Result<Vec<DeribitBookSummaryRaw>> {
1910 self.request_book_summaries_kind(currency, Some("option"))
1911 .await
1912 }
1913
1914 pub async fn request_book_summaries_kind(
1922 &self,
1923 currency: &str,
1924 kind: Option<&str>,
1925 ) -> anyhow::Result<Vec<DeribitBookSummaryRaw>> {
1926 let params = GetBookSummaryByCurrencyParams {
1927 currency: currency.to_string(),
1928 kind: kind.map(str::to_string),
1929 };
1930 let full_response = self
1931 .inner
1932 .get_book_summary_by_currency(params)
1933 .await
1934 .map_err(|e| anyhow::anyhow!(e))?;
1935 full_response
1936 .result
1937 .ok_or_else(|| anyhow::anyhow!("No result in book summary response"))
1938 }
1939
1940 pub async fn request_option_expirations(
1946 &self,
1947 currency: DeribitCurrency,
1948 ) -> anyhow::Result<Vec<String>> {
1949 let params = GetExpirationsParams::new(currency.as_str(), DeribitExpirationKind::Option);
1950 let full_response = self
1951 .inner
1952 .get_expirations(params)
1953 .await
1954 .map_err(|e| anyhow::anyhow!(e))?;
1955 let response = full_response
1956 .result
1957 .ok_or_else(|| anyhow::anyhow!("No result in expirations response"))?;
1958 let expirations = response
1959 .expirations_for_currency(currency.as_str())
1960 .ok_or_else(|| anyhow::anyhow!("No option expirations for {currency}"))?;
1961
1962 Ok(expirations.option.clone())
1963 }
1964
1965 pub async fn request_position_status_reports(
1977 &self,
1978 account_id: AccountId,
1979 instrument_id: Option<InstrumentId>,
1980 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1981 let ts_init = self.generate_ts_init();
1982 let mut reports = Vec::new();
1983
1984 let params = GetPositionsParams {
1986 currency: DeribitCurrency::ANY,
1987 kind: None,
1988 };
1989
1990 if let Some(positions) = self.inner.get_positions(params).await?.result {
1991 for position in &positions {
1992 if position.size.is_zero() {
1994 continue;
1995 }
1996
1997 let symbol = position.instrument_name;
1998 if let Some(instrument) = self.get_instrument(&symbol) {
1999 let report =
2000 parse_position_status_report(position, &instrument, account_id, ts_init);
2001 reports.push(report);
2002 } else {
2003 log::debug!(
2004 "Skipping position - instrument {} not in cache",
2005 position.instrument_name
2006 );
2007 }
2008 }
2009 }
2010
2011 if let Some(instrument_id) = instrument_id {
2013 reports.retain(|r| r.instrument_id == instrument_id);
2014 }
2015
2016 log::debug!("Generated {} position status reports", reports.len());
2017 Ok(reports)
2018 }
2019}
2020
2021#[cfg(test)]
2022mod tests {
2023 use nautilus_testkit::http::assert_http_redirect_rejected;
2024 use rstest::rstest;
2025
2026 use super::*;
2027 use crate::common::consts::{
2028 DERIBIT_ACCOUNT_RATE_KEY, DERIBIT_GLOBAL_RATE_KEY, DERIBIT_ORDER_RATE_KEY,
2029 };
2030
2031 #[tokio::test]
2032 async fn test_authenticated_client_rejects_redirects() {
2033 let client = DeribitRawHttpClient::with_credentials(
2034 "key".into(),
2035 "secret".into(),
2036 None,
2037 DeribitEnvironment::Testnet,
2038 3,
2039 0,
2040 1,
2041 1,
2042 None,
2043 )
2044 .unwrap()
2045 .client;
2046 assert_http_redirect_rejected(|url| async move {
2047 client
2048 .get(url, None, None, Some(3), None)
2049 .await
2050 .unwrap()
2051 .status
2052 .as_u16()
2053 })
2054 .await;
2055 }
2056
2057 #[rstest]
2058 #[case("private/buy", true, false)]
2059 #[case("private/cancel", true, false)]
2060 #[case("private/get_account_summaries", false, true)]
2061 #[case("private/get_positions", false, true)]
2062 #[case("public/get_instruments", false, false)]
2063 fn test_method_classification(
2064 #[case] method: &str,
2065 #[case] is_order: bool,
2066 #[case] is_account: bool,
2067 ) {
2068 assert_eq!(DeribitRawHttpClient::is_order_method(method), is_order);
2069 assert_eq!(DeribitRawHttpClient::is_account_method(method), is_account);
2070 }
2071
2072 #[rstest]
2073 #[case("private/buy", vec![DERIBIT_GLOBAL_RATE_KEY, DERIBIT_ORDER_RATE_KEY])]
2074 #[case("private/get_account_summaries", vec![DERIBIT_GLOBAL_RATE_KEY, DERIBIT_ACCOUNT_RATE_KEY])]
2075 #[case("public/get_instruments", vec![DERIBIT_GLOBAL_RATE_KEY])]
2076 fn test_rate_limit_keys(#[case] method: &str, #[case] expected_keys: Vec<&str>) {
2077 let keys = DeribitRawHttpClient::rate_limit_keys(method);
2078
2079 for key in &expected_keys {
2080 assert!(keys.contains(&key.to_string()));
2081 }
2082 assert!(keys.contains(&format!("deribit:{method}")));
2083 }
2084
2085 #[rstest]
2086 fn test_paginator_empty_page_returns_none() {
2087 let mut p = TradePaginator::new(100, 200);
2088 assert!(p.advance(&[], &[], true).is_none());
2089 }
2090
2091 #[rstest]
2092 fn test_paginator_single_page_no_more() {
2093 let mut p = TradePaginator::new(100, 200);
2094 let ids = vec!["t1".into(), "t2".into()];
2095 let ts = vec![150, 160];
2096
2097 let result = p.advance(&ids, &ts, false);
2098 assert_eq!(result, Some(vec![0, 1]));
2099 }
2100
2101 #[rstest]
2102 fn test_paginator_dedup_across_pages() {
2103 let mut p = TradePaginator::new(100, 200);
2104
2105 let ids1 = vec!["t1".into(), "t2".into()];
2107 let ts1 = vec![150, 150];
2108 let r1 = p.advance(&ids1, &ts1, true);
2109 assert_eq!(r1, Some(vec![0, 1]));
2110 assert_eq!(p.cursor, 150);
2111
2112 let ids2 = vec!["t2".into(), "t3".into()];
2114 let ts2 = vec![150, 150];
2115 let r2 = p.advance(&ids2, &ts2, false);
2116 assert_eq!(r2, Some(vec![1])); }
2118
2119 #[rstest]
2120 fn test_paginator_all_duplicates_advances_past_timestamp() {
2121 let mut p = TradePaginator::new(100, 200);
2122
2123 let ids = vec!["t1".into(), "t2".into()];
2125 let ts = vec![150, 150];
2126 p.advance(&ids, &ts, true);
2127 assert_eq!(p.cursor, 150);
2128
2129 let r2 = p.advance(&ids, &ts, true);
2131 assert_eq!(r2, Some(vec![])); assert_eq!(p.cursor, 151); }
2134
2135 #[rstest]
2136 fn test_paginator_is_exhausted_strict_greater_than() {
2137 let mut p = TradePaginator::new(100, 150);
2138
2139 let ids = vec!["t1".into()];
2140 let ts = vec![150];
2141 p.advance(&ids, &ts, true);
2142
2143 assert_eq!(p.cursor, 150);
2145 assert!(!p.is_exhausted());
2146
2147 p.advance(&ids, &ts, true);
2149 assert_eq!(p.cursor, 151);
2150 assert!(p.is_exhausted());
2151 }
2152
2153 #[rstest]
2154 fn test_paginator_reset_clears_state() {
2155 let mut p = TradePaginator::new(100, 200);
2156
2157 let ids = vec!["t1".into()];
2158 let ts = vec![150];
2159 p.advance(&ids, &ts, true);
2160 assert_eq!(p.seen_ids.len(), 1);
2161
2162 p.reset(100);
2163 assert_eq!(p.cursor, 100);
2164 assert!(p.seen_ids.is_empty());
2165
2166 let r = p.advance(&ids, &ts, false);
2168 assert_eq!(r, Some(vec![0]));
2169 }
2170}