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