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_rpc_response: DeribitJsonRpcResponse<T> = match serde_json::from_slice(
496 &resp.body,
497 ) {
498 Ok(response) => response,
499 Err(e) if e.is_data() => {
500 log::warn!(
501 "Failed to deserialize Deribit JSON-RPC response: method={method}, status={}, error={e}",
502 resp.status.as_u16()
503 );
504 log::debug!(
505 "Response JSON (first 2000 chars): {}",
506 String::from_utf8_lossy(&resp.body)
507 .chars()
508 .take(2000)
509 .collect::<String>()
510 );
511 return Err(DeribitHttpError::JsonError(e.to_string()));
512 }
513 Err(_) => {
514 let error_body = String::from_utf8_lossy(&resp.body);
516 log::warn!(
517 "Non-JSON response: method={method}, status={}, body={error_body}",
518 resp.status.as_u16()
519 );
520 return Err(DeribitHttpError::UnexpectedStatus {
521 status: resp.status.as_u16(),
522 body: error_body.to_string(),
523 });
524 }
525 };
526
527 if json_rpc_response.result.is_some() {
529 Ok(json_rpc_response)
530 } else if let Some(error) = &json_rpc_response.error {
531 log::warn!(
533 "Deribit RPC error response: method={method}, http_status={}, error_code={}, error_message={}, error_data={:?}",
534 resp.status.as_u16(),
535 error.code,
536 error.message,
537 error.data
538 );
539
540 Err(DeribitHttpError::from_jsonrpc_error(
542 error.code,
543 error.message.clone(),
544 error.data.as_ref(),
545 ))
546 } else {
547 log::warn!(
548 "Response contains neither result nor error field: method={method}, status={}, request_id={:?}",
549 resp.status.as_u16(),
550 json_rpc_response.id
551 );
552 Err(DeribitHttpError::JsonError(
553 "Response contains neither result nor error".to_string(),
554 ))
555 }
556 }
557 };
558
559 let should_retry = |error: &DeribitHttpError| -> bool { error.is_retryable() };
568
569 let create_error = |error: RetryError| -> DeribitHttpError {
570 match error {
571 RetryError::Canceled => {
572 DeribitHttpError::Canceled("Adapter disconnecting or shutting down".to_string())
573 }
574 error => DeribitHttpError::NetworkError(error.to_string()),
575 }
576 };
577
578 let result = self
579 .retry_manager
580 .invocation(&operation_id, operation, should_retry, create_error)
581 .cancellation_token(&self.cancellation_token)
582 .execute()
583 .await;
584
585 if let Err(ref e) = result
586 && e.is_retryable()
587 {
588 log::warn!("Request exhausted retries: method={method}, error={e}");
589 }
590
591 result
592 }
593
594 pub async fn get_instruments(
600 &self,
601 params: GetInstrumentsParams,
602 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitInstrument>>, DeribitHttpError> {
603 self.send_request("public/get_instruments", params, false)
604 .await
605 }
606
607 pub async fn get_instrument(
613 &self,
614 params: GetInstrumentParams,
615 ) -> Result<DeribitJsonRpcResponse<DeribitInstrument>, DeribitHttpError> {
616 self.send_request("public/get_instrument", params, false)
617 .await
618 }
619
620 pub async fn get_combos(
626 &self,
627 params: GetCombosParams,
628 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitCombo>>, DeribitHttpError> {
629 self.send_request("public/get_combos", params, false).await
630 }
631
632 pub async fn get_last_trades_by_instrument_and_time(
638 &self,
639 params: GetLastTradesByInstrumentAndTimeParams,
640 ) -> Result<DeribitJsonRpcResponse<DeribitTradesResponse>, DeribitHttpError> {
641 self.send_request(
642 "public/get_last_trades_by_instrument_and_time",
643 params,
644 false,
645 )
646 .await
647 }
648
649 pub async fn get_last_trades_by_currency(
659 &self,
660 params: GetLastTradesByCurrencyParams,
661 ) -> Result<DeribitJsonRpcResponse<DeribitTradesResponse>, DeribitHttpError> {
662 self.send_request("public/get_last_trades_by_currency", params, false)
663 .await
664 }
665
666 pub async fn get_expirations(
672 &self,
673 params: GetExpirationsParams,
674 ) -> Result<DeribitJsonRpcResponse<DeribitExpirationsResponse>, DeribitHttpError> {
675 self.send_request("public/get_expirations", params, false)
676 .await
677 }
678
679 pub async fn get_tradingview_chart_data(
685 &self,
686 params: GetTradingViewChartDataParams,
687 ) -> Result<DeribitJsonRpcResponse<DeribitTradingViewChartData>, DeribitHttpError> {
688 self.send_request("public/get_tradingview_chart_data", params, false)
689 .await
690 }
691
692 pub async fn get_account_summaries(
701 &self,
702 params: GetAccountSummariesParams,
703 ) -> Result<DeribitJsonRpcResponse<DeribitAccountSummariesResponse>, DeribitHttpError> {
704 self.send_request("private/get_account_summaries", params, true)
705 .await
706 }
707
708 pub async fn get_order_book(
714 &self,
715 params: GetOrderBookParams,
716 ) -> Result<DeribitJsonRpcResponse<DeribitOrderBook>, DeribitHttpError> {
717 self.send_request("public/get_order_book", params, false)
718 .await
719 }
720
721 pub async fn get_order_state(
730 &self,
731 params: GetOrderStateParams,
732 ) -> Result<DeribitJsonRpcResponse<DeribitOrderMsg>, DeribitHttpError> {
733 self.send_request("private/get_order_state", params, true)
734 .await
735 }
736
737 pub async fn get_open_orders(
746 &self,
747 params: GetOpenOrdersParams,
748 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
749 self.send_request("private/get_open_orders", params, true)
750 .await
751 }
752
753 pub async fn get_open_orders_by_instrument(
762 &self,
763 params: GetOpenOrdersByInstrumentParams,
764 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
765 self.send_request("private/get_open_orders_by_instrument", params, true)
766 .await
767 }
768
769 pub async fn get_order_history_by_instrument(
778 &self,
779 params: GetOrderHistoryByInstrumentParams,
780 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
781 self.send_request("private/get_order_history_by_instrument", params, true)
782 .await
783 }
784
785 pub async fn get_order_history_by_currency(
794 &self,
795 params: GetOrderHistoryByCurrencyParams,
796 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitOrderMsg>>, DeribitHttpError> {
797 self.send_request("private/get_order_history_by_currency", params, true)
798 .await
799 }
800
801 pub async fn get_user_trades_by_instrument_and_time(
810 &self,
811 params: GetUserTradesByInstrumentAndTimeParams,
812 ) -> Result<DeribitJsonRpcResponse<DeribitUserTradesResponse>, DeribitHttpError> {
813 self.send_request(
814 "private/get_user_trades_by_instrument_and_time",
815 params,
816 true,
817 )
818 .await
819 }
820
821 pub async fn get_user_trades_by_currency_and_time(
830 &self,
831 params: GetUserTradesByCurrencyAndTimeParams,
832 ) -> Result<DeribitJsonRpcResponse<DeribitUserTradesResponse>, DeribitHttpError> {
833 self.send_request("private/get_user_trades_by_currency_and_time", params, true)
834 .await
835 }
836
837 pub async fn get_book_summary_by_currency(
843 &self,
844 params: GetBookSummaryByCurrencyParams,
845 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitBookSummaryRaw>>, DeribitHttpError> {
846 self.send_request("public/get_book_summary_by_currency", params, false)
847 .await
848 }
849
850 pub async fn get_ticker(
856 &self,
857 params: GetTickerParams,
858 ) -> Result<DeribitJsonRpcResponse<DeribitTicker>, DeribitHttpError> {
859 self.send_request("public/ticker", params, false).await
860 }
861
862 pub async fn get_positions(
871 &self,
872 params: GetPositionsParams,
873 ) -> Result<DeribitJsonRpcResponse<Vec<DeribitPosition>>, DeribitHttpError> {
874 self.send_request("private/get_positions", params, true)
875 .await
876 }
877}
878
879#[derive(Debug)]
884#[cfg_attr(
885 feature = "python",
886 pyo3::pyclass(module = "nautilus_trader.adapters.deribit", from_py_object)
887)]
888#[cfg_attr(
889 feature = "python",
890 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.deribit")
891)]
892pub struct DeribitHttpClient {
893 pub(crate) inner: Arc<DeribitRawHttpClient>,
894 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
895 clock: &'static AtomicTime,
896 cache_initialized: AtomicBool,
897}
898
899impl Clone for DeribitHttpClient {
900 fn clone(&self) -> Self {
901 let cache_initialized = AtomicBool::new(false);
902
903 let is_initialized = self.cache_initialized.load(Ordering::Acquire);
904 if is_initialized {
905 cache_initialized.store(true, Ordering::Release);
906 }
907
908 Self {
909 inner: self.inner.clone(),
910 instruments_cache: self.instruments_cache.clone(),
911 cache_initialized,
912 clock: self.clock,
913 }
914 }
915}
916
917impl DeribitHttpClient {
918 #[must_use]
923 pub fn inner(&self) -> &DeribitRawHttpClient {
924 &self.inner
925 }
926
927 pub fn new(
937 base_url: Option<String>,
938 environment: DeribitEnvironment,
939 timeout_secs: u64,
940 max_retries: u32,
941 retry_delay_ms: u64,
942 retry_delay_max_ms: u64,
943 proxy_url: Option<String>,
944 ) -> anyhow::Result<Self> {
945 let raw_client = Arc::new(DeribitRawHttpClient::new(
946 base_url,
947 environment,
948 timeout_secs,
949 max_retries,
950 retry_delay_ms,
951 retry_delay_max_ms,
952 proxy_url,
953 )?);
954
955 Ok(Self {
956 inner: raw_client,
957 instruments_cache: Arc::new(AtomicMap::new()),
958 cache_initialized: AtomicBool::new(false),
959 clock: get_atomic_clock_realtime(),
960 })
961 }
962
963 #[expect(clippy::too_many_arguments)]
975 pub fn new_with_env(
976 api_key: Option<String>,
977 api_secret: Option<String>,
978 base_url: Option<String>,
979 environment: DeribitEnvironment,
980 timeout_secs: u64,
981 max_retries: u32,
982 retry_delay_ms: u64,
983 retry_delay_max_ms: u64,
984 proxy_url: Option<String>,
985 ) -> anyhow::Result<Self> {
986 let raw_client = Arc::new(DeribitRawHttpClient::new_with_env(
987 api_key,
988 api_secret,
989 base_url,
990 environment,
991 timeout_secs,
992 max_retries,
993 retry_delay_ms,
994 retry_delay_max_ms,
995 proxy_url,
996 )?);
997
998 Ok(Self {
999 inner: raw_client,
1000 instruments_cache: Arc::new(AtomicMap::new()),
1001 cache_initialized: AtomicBool::new(false),
1002 clock: get_atomic_clock_realtime(),
1003 })
1004 }
1005
1006 pub async fn request_instruments(
1012 &self,
1013 currency: DeribitCurrency,
1014 product_type: Option<DeribitProductType>,
1015 ) -> anyhow::Result<Vec<InstrumentAny>> {
1016 let params = if let Some(pt) = product_type {
1018 GetInstrumentsParams::with_kind(currency, pt)
1019 } else {
1020 GetInstrumentsParams::new(currency)
1021 };
1022
1023 let full_response = self.inner.get_instruments(params).await?;
1025 let result = full_response
1026 .result
1027 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1028 let ts_event = extract_server_timestamp(full_response.us_out)?;
1029 let ts_init = self.generate_ts_init();
1030 let combo_by_id = self.combo_map_for_instruments(currency, &result).await;
1031
1032 let mut instruments = Vec::new();
1034 let mut skipped_count = 0;
1035 let mut error_count = 0;
1036
1037 for raw_instrument in result {
1038 match parse_deribit_instrument_any(&raw_instrument, ts_init, ts_event) {
1039 Ok(Some(mut instrument)) => {
1040 if let Some(combo) = combo_by_id.get(&raw_instrument.instrument_name) {
1041 Self::attach_combo_leg_info(&mut instrument, combo);
1042 }
1043 instruments.push(instrument);
1044 }
1045 Ok(None) => {
1046 skipped_count += 1;
1048 log::debug!(
1049 "Skipped unsupported instrument type: {} (kind: {:?})",
1050 raw_instrument.instrument_name,
1051 raw_instrument.kind
1052 );
1053 }
1054 Err(e) => {
1055 error_count += 1;
1056 log::warn!(
1057 "Failed to parse instrument {}: {}",
1058 raw_instrument.instrument_name,
1059 e
1060 );
1061 }
1062 }
1063 }
1064
1065 log::debug!(
1066 "Parsed {} instruments ({} skipped, {} errors)",
1067 instruments.len(),
1068 skipped_count,
1069 error_count
1070 );
1071
1072 Ok(instruments)
1073 }
1074
1075 pub async fn request_instrument(
1087 &self,
1088 instrument_id: InstrumentId,
1089 ) -> anyhow::Result<InstrumentAny> {
1090 let params = GetInstrumentParams {
1091 instrument_name: instrument_id.symbol.to_string(),
1092 };
1093
1094 let full_response = self.inner.get_instrument(params).await?;
1095 let response = full_response
1096 .result
1097 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1098 let ts_event = extract_server_timestamp(full_response.us_out)?;
1099 let ts_init = self.generate_ts_init();
1100
1101 match parse_deribit_instrument_any(&response, ts_init, ts_event)? {
1102 Some(mut instrument) => {
1103 if Self::is_combo_kind(response.kind) {
1104 let currency = DeribitCurrency::from_str(response.base_currency.as_str())
1105 .unwrap_or(DeribitCurrency::ANY);
1106 let combo_by_id = self
1107 .combo_map_for_instruments(currency, std::slice::from_ref(&response))
1108 .await;
1109
1110 if let Some(combo) = combo_by_id.get(&response.instrument_name) {
1111 Self::attach_combo_leg_info(&mut instrument, combo);
1112 }
1113 }
1114
1115 Ok(instrument)
1116 }
1117 None => anyhow::bail!(
1118 "Unsupported instrument type: {} (kind: {:?})",
1119 response.instrument_name,
1120 response.kind
1121 ),
1122 }
1123 }
1124
1125 async fn combo_map_for_instruments(
1126 &self,
1127 requested_currency: DeribitCurrency,
1128 raw_instruments: &[DeribitInstrument],
1129 ) -> AHashMap<Ustr, DeribitCombo> {
1130 if !raw_instruments
1131 .iter()
1132 .any(|instrument| Self::is_combo_kind(instrument.kind))
1133 {
1134 return AHashMap::new();
1135 }
1136
1137 let mut currencies = AHashSet::new();
1138
1139 if requested_currency == DeribitCurrency::ANY {
1140 for instrument in raw_instruments
1141 .iter()
1142 .filter(|instrument| Self::is_combo_kind(instrument.kind))
1143 {
1144 if let Ok(currency) = DeribitCurrency::from_str(instrument.base_currency.as_str()) {
1145 currencies.insert(currency);
1146 }
1147 }
1148 } else {
1149 currencies.insert(requested_currency);
1150 }
1151
1152 let mut combo_by_id = AHashMap::new();
1153
1154 for currency in currencies {
1155 match self.inner.get_combos(GetCombosParams::new(currency)).await {
1156 Ok(response) => {
1157 if let Some(combos) = response.result {
1158 for combo in combos {
1159 combo_by_id.insert(combo.id, combo);
1160 }
1161 }
1162 }
1163 Err(e) => {
1164 log::warn!("Failed to load Deribit combo definitions for {currency}: {e}");
1165 }
1166 }
1167 }
1168
1169 combo_by_id
1170 }
1171
1172 fn is_combo_kind(kind: DeribitProductType) -> bool {
1173 matches!(
1174 kind,
1175 DeribitProductType::FutureCombo | DeribitProductType::OptionCombo
1176 )
1177 }
1178
1179 fn attach_combo_leg_info(instrument: &mut InstrumentAny, combo: &DeribitCombo) {
1180 if let Some(info) = Self::combo_leg_info(instrument, combo) {
1181 match instrument {
1182 InstrumentAny::CryptoOptionSpread(spread) => spread.info = Some(info),
1183 InstrumentAny::CryptoFuturesSpread(spread) => spread.info = Some(info),
1184 _ => {}
1185 }
1186 }
1187 }
1188
1189 fn combo_leg_info(instrument: &InstrumentAny, combo: &DeribitCombo) -> Option<Params> {
1190 let existing_info = match instrument {
1191 InstrumentAny::CryptoOptionSpread(spread) => spread.info.clone(),
1192 InstrumentAny::CryptoFuturesSpread(spread) => spread.info.clone(),
1193 _ => return None,
1194 };
1195
1196 let mut info = existing_info.unwrap_or_default();
1197 let legs = combo
1198 .legs
1199 .iter()
1200 .map(|leg| {
1201 let instrument_id =
1202 InstrumentId::new(Symbol::new(leg.instrument_name), *DERIBIT_VENUE);
1203
1204 json!({
1205 "amount": leg.amount,
1206 "instrument_id": instrument_id.to_string(),
1207 "instrument_name": leg.instrument_name,
1208 })
1209 })
1210 .collect::<Vec<_>>();
1211
1212 info.insert("deribit_combo_id".to_string(), json!(combo.id));
1213 info.insert(
1214 "deribit_combo_state".to_string(),
1215 json!(combo.state.as_str()),
1216 );
1217 info.insert("deribit_combo_legs".to_string(), json!(legs));
1218
1219 Some(info)
1220 }
1221
1222 pub async fn request_trades(
1246 &self,
1247 instrument_id: InstrumentId,
1248 start: Option<Timestamp>,
1249 end: Option<Timestamp>,
1250 limit: Option<u32>,
1251 ) -> anyhow::Result<Vec<TradeTick>> {
1252 let (price_precision, size_precision) =
1254 if let Some(instrument) = self.get_instrument(&instrument_id.symbol.inner()) {
1255 (instrument.price_precision(), instrument.size_precision())
1256 } else {
1257 log::warn!("Instrument {instrument_id} not in cache, skipping trades request");
1258 return Err(InstrumentLookupError::not_found(instrument_id).into());
1259 };
1260
1261 let now = Timestamp::now();
1263 let end_dt = end.unwrap_or(now);
1264 let start_dt = start.unwrap_or(end_dt - jiff::SignedDuration::from_hours(1));
1265
1266 if let (Some(s), Some(e)) = (start, end) {
1267 anyhow::ensure!(s < e, "Invalid time range: start={s:?} end={e:?}");
1268 }
1269
1270 let start_ms = start_dt.as_millisecond();
1271 let end_ms = end_dt.as_millisecond();
1272 let ts_init = self.generate_ts_init();
1273 let mut all_trades = Vec::new();
1274 let mut paginator = TradePaginator::new(start_ms, end_ms);
1275
1276 loop {
1277 let params = GetLastTradesByInstrumentAndTimeParams::new(
1278 instrument_id.symbol.to_string(),
1279 paginator.cursor,
1280 end_ms,
1281 Some(DERIBIT_HISTORICAL_TRADES_MAX_COUNT),
1282 Some("asc".to_string()),
1283 );
1284
1285 let full_response = self
1286 .inner
1287 .get_last_trades_by_instrument_and_time(params)
1288 .await
1289 .map_err(|e| anyhow::anyhow!(e))?;
1290
1291 let response_data = full_response
1292 .result
1293 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1294
1295 let ids: Vec<String> = response_data
1296 .trades
1297 .iter()
1298 .map(|t| t.trade_id.clone())
1299 .collect();
1300 let timestamps: Vec<i64> = response_data.trades.iter().map(|t| t.timestamp).collect();
1301
1302 let Some(new_indices) = paginator.advance(&ids, ×tamps, response_data.has_more)
1303 else {
1304 break;
1305 };
1306
1307 for i in &new_indices {
1308 let raw_trade = &response_data.trades[*i];
1309
1310 match parse_trade_tick(
1311 raw_trade,
1312 instrument_id,
1313 price_precision,
1314 size_precision,
1315 ts_init,
1316 ) {
1317 Ok(trade) => {
1318 all_trades.push(trade);
1319
1320 if let Some(max) = limit
1321 && all_trades.len() >= max as usize
1322 {
1323 return Ok(all_trades);
1324 }
1325 }
1326 Err(e) => {
1327 log::warn!(
1328 "Failed to parse trade {} for {}: {}",
1329 raw_trade.trade_id,
1330 instrument_id,
1331 e
1332 );
1333 }
1334 }
1335 }
1336
1337 if !response_data.has_more || paginator.is_exhausted() {
1338 break;
1339 }
1340 }
1341
1342 log::debug!(
1343 "Fetched {} historical trades for {} from {} to {}",
1344 all_trades.len(),
1345 instrument_id,
1346 start_dt,
1347 end_dt
1348 );
1349
1350 Ok(all_trades)
1351 }
1352
1353 pub async fn request_bars(
1369 &self,
1370 bar_type: BarType,
1371 start: Option<Timestamp>,
1372 end: Option<Timestamp>,
1373 limit: Option<u32>,
1374 ) -> anyhow::Result<Vec<Bar>> {
1375 anyhow::ensure!(
1376 bar_type.aggregation_source() == AggregationSource::External,
1377 "Only EXTERNAL aggregation is supported"
1378 );
1379
1380 let now = Timestamp::now();
1381
1382 let end_dt = end.unwrap_or(now);
1384 let start_dt = start.unwrap_or(end_dt - jiff::SignedDuration::from_hours(1));
1385
1386 if let (Some(s), Some(e)) = (start, end) {
1387 anyhow::ensure!(s < e, "Invalid time range: start={s:?} end={e:?}");
1388 }
1389
1390 let spec = bar_type.spec();
1392 let step = spec.step.get();
1393 let resolution = match spec.aggregation {
1394 BarAggregation::Minute => format!("{step}"),
1395 BarAggregation::Hour => format!("{}", step * 60),
1396 BarAggregation::Day => "1D".to_string(),
1397 a => anyhow::bail!("Deribit does not support {a:?} aggregation"),
1398 };
1399
1400 let supported_resolutions = [
1402 "1", "3", "5", "10", "15", "30", "60", "120", "180", "360", "720", "1D",
1403 ];
1404
1405 if !supported_resolutions.contains(&resolution.as_str()) {
1406 anyhow::bail!(
1407 "Deribit does not support resolution '{resolution}'. Supported: {supported_resolutions:?}"
1408 );
1409 }
1410
1411 let instrument_id = bar_type.instrument_id();
1412 let (price_precision, size_precision, use_cost_for_volume) =
1413 if let Some(instrument) = self.get_instrument(&instrument_id.symbol.inner()) {
1414 (
1415 instrument.price_precision(),
1416 instrument.size_precision(),
1417 use_cost_for_bar_volume(&instrument),
1418 )
1419 } else {
1420 log::warn!("Instrument {instrument_id} not in cache, skipping bars request");
1421 return Err(InstrumentLookupError::not_found(instrument_id).into());
1422 };
1423
1424 let instrument_name = instrument_id.symbol.to_string();
1425 let start_timestamp = start_dt.as_millisecond();
1426 let end_timestamp = end_dt.as_millisecond();
1427
1428 let params = GetTradingViewChartDataParams::new(
1429 instrument_name,
1430 start_timestamp,
1431 end_timestamp,
1432 resolution,
1433 );
1434
1435 let full_response = self.inner.get_tradingview_chart_data(params).await?;
1436 let chart_data = full_response
1437 .result
1438 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1439
1440 if chart_data.status == "no_data" {
1441 log::debug!("No bar data returned for {bar_type}");
1442 return Ok(Vec::new());
1443 }
1444
1445 let ts_init = self.generate_ts_init();
1446 let mut bars = parse_bars(
1447 &chart_data,
1448 bar_type,
1449 price_precision,
1450 size_precision,
1451 use_cost_for_volume,
1452 ts_init,
1453 )?;
1454
1455 if let Some(max) = limit {
1456 let max = max as usize;
1457 if bars.len() > max {
1458 bars.drain(..bars.len() - max);
1459 }
1460 }
1461
1462 log::debug!("Parsed {} bars for {}", bars.len(), bar_type);
1463
1464 Ok(bars)
1465 }
1466
1467 pub async fn request_book_snapshot(
1483 &self,
1484 instrument_id: InstrumentId,
1485 depth: Option<u32>,
1486 ) -> anyhow::Result<OrderBook> {
1487 let (price_precision, size_precision) =
1488 if let Some(instrument) = self.get_instrument(&instrument_id.symbol.inner()) {
1489 (instrument.price_precision(), instrument.size_precision())
1490 } else {
1491 return Err(InstrumentLookupError::not_found(instrument_id).into());
1492 };
1493
1494 let params = GetOrderBookParams::new(instrument_id.symbol.to_string(), depth);
1495 let full_response = self
1496 .inner
1497 .get_order_book(params)
1498 .await
1499 .map_err(|e| anyhow::anyhow!(e))?;
1500
1501 let order_book_data = full_response
1502 .result
1503 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1504
1505 let ts_init = self.generate_ts_init();
1506 let book = parse_order_book(
1507 &order_book_data,
1508 instrument_id,
1509 price_precision,
1510 size_precision,
1511 ts_init,
1512 )?;
1513
1514 log::debug!(
1515 "Fetched order book for {} with {} bids and {} asks",
1516 instrument_id,
1517 order_book_data.bids.len(),
1518 order_book_data.asks.len()
1519 );
1520
1521 Ok(book)
1522 }
1523
1524 pub async fn request_account_state(
1535 &self,
1536 account_id: AccountId,
1537 ) -> anyhow::Result<AccountState> {
1538 let params = GetAccountSummariesParams::default();
1539 let full_response = self
1540 .inner
1541 .get_account_summaries(params)
1542 .await
1543 .map_err(|e| anyhow::anyhow!(e))?;
1544 let response_data = full_response
1545 .result
1546 .ok_or_else(|| anyhow::anyhow!("No result in response"))?;
1547 let ts_init = self.generate_ts_init();
1548 let ts_event = extract_server_timestamp(full_response.us_out)?;
1549
1550 parse_account_state(&response_data.summaries, account_id, ts_init, ts_event)
1551 }
1552
1553 fn generate_ts_init(&self) -> UnixNanos {
1555 self.clock.get_time_ns()
1556 }
1557
1558 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1560 self.instruments_cache.rcu(|m| {
1561 for inst in instruments {
1562 m.insert(inst.raw_symbol().inner(), inst.clone());
1563 }
1564 });
1565 self.cache_initialized.store(true, Ordering::Release);
1566 }
1567
1568 #[must_use]
1570 pub fn get_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1571 self.instruments_cache.get_cloned(symbol)
1572 }
1573
1574 #[must_use]
1576 pub fn is_cache_initialized(&self) -> bool {
1577 self.cache_initialized.load(Ordering::Acquire)
1578 }
1579
1580 #[must_use]
1582 pub fn is_testnet(&self) -> bool {
1583 self.inner.is_testnet()
1584 }
1585
1586 pub async fn request_order_status_reports(
1599 &self,
1600 account_id: AccountId,
1601 instrument_id: Option<InstrumentId>,
1602 start: Option<UnixNanos>,
1603 end: Option<UnixNanos>,
1604 open_only: bool,
1605 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1606 let ts_init = self.generate_ts_init();
1607 let mut reports = Vec::new();
1608 let mut seen_order_ids = AHashSet::new();
1609
1610 let mut parse_and_add = |order: &DeribitOrderMsg| {
1611 let symbol = order.instrument_name;
1612 if let Some(instrument) = self.get_instrument(&symbol) {
1613 match parse_user_order_msg(order, &instrument, account_id, ts_init) {
1614 Ok(report) => {
1615 let ts_last = report.ts_last;
1617 let in_range = match (start, end) {
1618 (Some(s), Some(e)) => ts_last >= s && ts_last <= e,
1619 (Some(s), None) => ts_last >= s,
1620 (None, Some(e)) => ts_last <= e,
1621 (None, None) => true,
1622 };
1623 if in_range && seen_order_ids.insert(order.order_id.clone()) {
1625 reports.push(report);
1626 }
1627 }
1628 Err(e) => {
1629 log::warn!(
1630 "Failed to parse order {} for {}: {}",
1631 order.order_id,
1632 order.instrument_name,
1633 e
1634 );
1635 }
1636 }
1637 } else {
1638 log::debug!(
1639 "Skipping order {} - instrument {} not in cache",
1640 order.order_id,
1641 order.instrument_name
1642 );
1643 }
1644 };
1645
1646 if let Some(instrument_id) = instrument_id {
1647 let instrument_name = instrument_id.symbol.to_string();
1649
1650 let open_params = GetOpenOrdersByInstrumentParams {
1652 instrument_name: instrument_name.clone(),
1653 r#type: None,
1654 };
1655
1656 if let Some(orders) = self
1657 .inner
1658 .get_open_orders_by_instrument(open_params)
1659 .await?
1660 .result
1661 {
1662 for order in &orders {
1663 parse_and_add(order);
1664 }
1665 }
1666
1667 if !open_only {
1668 const PAGE_SIZE: u32 = 100;
1669 let mut offset: u32 = 0;
1670
1671 loop {
1672 let history_params = GetOrderHistoryByInstrumentParams {
1673 instrument_name: instrument_name.clone(),
1674 count: Some(PAGE_SIZE),
1675 offset: Some(offset),
1676 include_old: Some(true),
1677 include_unfilled: Some(true),
1678 };
1679 let orders = self
1680 .inner
1681 .get_order_history_by_instrument(history_params)
1682 .await?
1683 .result
1684 .unwrap_or_default();
1685
1686 let count = orders.len() as u32;
1687 for order in &orders {
1688 parse_and_add(order);
1689 }
1690
1691 if count < PAGE_SIZE {
1692 break;
1693 }
1694 offset += count;
1695 }
1696 }
1697 } else {
1698 let open_params = GetOpenOrdersParams::default();
1700 if let Some(orders) = self.inner.get_open_orders(open_params).await?.result {
1701 for order in &orders {
1702 parse_and_add(order);
1703 }
1704 }
1705
1706 if !open_only {
1707 const PAGE_SIZE: u32 = 100;
1708
1709 for currency in DeribitCurrency::iter().filter(|c| *c != DeribitCurrency::ANY) {
1710 let mut offset: u32 = 0;
1711
1712 loop {
1713 let history_params = GetOrderHistoryByCurrencyParams {
1714 currency,
1715 kind: None,
1716 count: Some(PAGE_SIZE),
1717 offset: Some(offset),
1718 include_old: Some(true),
1719 include_unfilled: Some(true),
1720 };
1721 let orders = self
1722 .inner
1723 .get_order_history_by_currency(history_params)
1724 .await?
1725 .result
1726 .unwrap_or_default();
1727
1728 let count = orders.len() as u32;
1729 for order in &orders {
1730 parse_and_add(order);
1731 }
1732
1733 if count < PAGE_SIZE {
1734 break;
1735 }
1736 offset += count;
1737 }
1738 }
1739 }
1740 }
1741
1742 log::debug!("Generated {} order status reports", reports.len());
1743 Ok(reports)
1744 }
1745
1746 pub async fn request_fill_reports(
1759 &self,
1760 account_id: AccountId,
1761 instrument_id: Option<InstrumentId>,
1762 start: Option<UnixNanos>,
1763 end: Option<UnixNanos>,
1764 ) -> anyhow::Result<Vec<FillReport>> {
1765 let ts_init = self.generate_ts_init();
1766 let now_ms = Timestamp::now().as_millisecond();
1767
1768 let start_ms = start.map_or(0, |ns| nanos_to_millis(ns.as_u64()) as i64);
1770 let end_ms = end.map_or(now_ms, |ns| nanos_to_millis(ns.as_u64()) as i64);
1771 let mut reports = Vec::new();
1772
1773 let mut parse_and_add = |trade: &DeribitUserTradeMsg| {
1774 let symbol = trade.instrument_name;
1775 if let Some(instrument) = self.get_instrument(&symbol) {
1776 match parse_user_trade_msg(trade, &instrument, account_id, ts_init) {
1777 Ok(report) => reports.push(report),
1778 Err(e) => {
1779 log::warn!(
1780 "Failed to parse trade {} for {}: {}",
1781 trade.trade_id,
1782 trade.instrument_name,
1783 e
1784 );
1785 }
1786 }
1787 } else {
1788 log::debug!(
1789 "Skipping trade {} - instrument {} not in cache",
1790 trade.trade_id,
1791 trade.instrument_name
1792 );
1793 }
1794 };
1795
1796 let mut paginator = TradePaginator::new(start_ms, end_ms);
1797
1798 if let Some(instrument_id) = instrument_id {
1799 loop {
1800 let params = GetUserTradesByInstrumentAndTimeParams {
1801 instrument_name: instrument_id.symbol.to_string(),
1802 start_timestamp: paginator.cursor,
1803 end_timestamp: end_ms,
1804 count: Some(DERIBIT_HISTORICAL_TRADES_MAX_COUNT),
1805 sorting: Some("asc".to_string()),
1806 };
1807 let response = self
1808 .inner
1809 .get_user_trades_by_instrument_and_time(params)
1810 .await?;
1811
1812 let Some(data) = response.result else { break };
1813
1814 let ids: Vec<String> = data.trades.iter().map(|t| t.trade_id.clone()).collect();
1815 let timestamps: Vec<i64> = data.trades.iter().map(|t| t.timestamp as i64).collect();
1816
1817 let Some(new_indices) = paginator.advance(&ids, ×tamps, data.has_more) else {
1818 break;
1819 };
1820
1821 for i in &new_indices {
1822 parse_and_add(&data.trades[*i]);
1823 }
1824
1825 if !data.has_more || paginator.is_exhausted() {
1826 break;
1827 }
1828 }
1829 } else {
1830 for currency in DeribitCurrency::iter().filter(|c| *c != DeribitCurrency::ANY) {
1831 paginator.reset(start_ms);
1832
1833 loop {
1834 let params = GetUserTradesByCurrencyAndTimeParams {
1835 currency,
1836 start_timestamp: paginator.cursor,
1837 end_timestamp: end_ms,
1838 kind: None,
1839 count: Some(DERIBIT_HISTORICAL_TRADES_MAX_COUNT),
1840 sorting: Some("asc".to_string()),
1841 };
1842 let response = self
1843 .inner
1844 .get_user_trades_by_currency_and_time(params)
1845 .await?;
1846
1847 let Some(data) = response.result else { break };
1848
1849 let ids: Vec<String> = data.trades.iter().map(|t| t.trade_id.clone()).collect();
1850 let timestamps: Vec<i64> =
1851 data.trades.iter().map(|t| t.timestamp as i64).collect();
1852
1853 let Some(new_indices) = paginator.advance(&ids, ×tamps, data.has_more)
1854 else {
1855 break;
1856 };
1857
1858 for i in &new_indices {
1859 parse_and_add(&data.trades[*i]);
1860 }
1861
1862 if !data.has_more || paginator.is_exhausted() {
1863 break;
1864 }
1865 }
1866 }
1867 }
1868
1869 log::debug!("Generated {} fill reports", reports.len());
1870 Ok(reports)
1871 }
1872
1873 pub async fn request_ticker(&self, instrument_name: &str) -> anyhow::Result<DeribitTicker> {
1881 let params = GetTickerParams {
1882 instrument_name: instrument_name.to_string(),
1883 };
1884 let response = self
1885 .inner
1886 .get_ticker(params)
1887 .await
1888 .map_err(|e| anyhow::anyhow!(e))?;
1889 response
1890 .result
1891 .ok_or_else(|| anyhow::anyhow!("No result in ticker response"))
1892 }
1893
1894 pub async fn request_book_summaries(
1903 &self,
1904 currency: &str,
1905 ) -> anyhow::Result<Vec<DeribitBookSummaryRaw>> {
1906 self.request_book_summaries_kind(currency, Some("option"))
1907 .await
1908 }
1909
1910 pub async fn request_book_summaries_kind(
1918 &self,
1919 currency: &str,
1920 kind: Option<&str>,
1921 ) -> anyhow::Result<Vec<DeribitBookSummaryRaw>> {
1922 let params = GetBookSummaryByCurrencyParams {
1923 currency: currency.to_string(),
1924 kind: kind.map(str::to_string),
1925 };
1926 let full_response = self
1927 .inner
1928 .get_book_summary_by_currency(params)
1929 .await
1930 .map_err(|e| anyhow::anyhow!(e))?;
1931 full_response
1932 .result
1933 .ok_or_else(|| anyhow::anyhow!("No result in book summary response"))
1934 }
1935
1936 pub async fn request_option_expirations(
1942 &self,
1943 currency: DeribitCurrency,
1944 ) -> anyhow::Result<Vec<String>> {
1945 let params = GetExpirationsParams::new(currency.as_str(), DeribitExpirationKind::Option);
1946 let full_response = self
1947 .inner
1948 .get_expirations(params)
1949 .await
1950 .map_err(|e| anyhow::anyhow!(e))?;
1951 let response = full_response
1952 .result
1953 .ok_or_else(|| anyhow::anyhow!("No result in expirations response"))?;
1954 let expirations = response
1955 .expirations_for_currency(currency.as_str())
1956 .ok_or_else(|| anyhow::anyhow!("No option expirations for {currency}"))?;
1957
1958 Ok(expirations.option.clone())
1959 }
1960
1961 pub async fn request_position_status_reports(
1973 &self,
1974 account_id: AccountId,
1975 instrument_id: Option<InstrumentId>,
1976 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1977 let ts_init = self.generate_ts_init();
1978 let mut reports = Vec::new();
1979
1980 let params = GetPositionsParams {
1982 currency: DeribitCurrency::ANY,
1983 kind: None,
1984 };
1985
1986 if let Some(positions) = self.inner.get_positions(params).await?.result {
1987 for position in &positions {
1988 if position.size.is_zero() {
1990 continue;
1991 }
1992
1993 let symbol = position.instrument_name;
1994 if let Some(instrument) = self.get_instrument(&symbol) {
1995 let report =
1996 parse_position_status_report(position, &instrument, account_id, ts_init);
1997 reports.push(report);
1998 } else {
1999 log::debug!(
2000 "Skipping position - instrument {} not in cache",
2001 position.instrument_name
2002 );
2003 }
2004 }
2005 }
2006
2007 if let Some(instrument_id) = instrument_id {
2009 reports.retain(|r| r.instrument_id == instrument_id);
2010 }
2011
2012 log::debug!("Generated {} position status reports", reports.len());
2013 Ok(reports)
2014 }
2015}
2016
2017#[cfg(test)]
2018mod tests {
2019 use nautilus_testkit::http::assert_http_redirect_rejected;
2020 use rstest::rstest;
2021
2022 use super::*;
2023 use crate::common::consts::{
2024 DERIBIT_ACCOUNT_RATE_KEY, DERIBIT_GLOBAL_RATE_KEY, DERIBIT_ORDER_RATE_KEY,
2025 };
2026
2027 #[tokio::test]
2028 async fn test_authenticated_client_rejects_redirects() {
2029 let client = DeribitRawHttpClient::with_credentials(
2030 "key".into(),
2031 "secret".into(),
2032 None,
2033 DeribitEnvironment::Testnet,
2034 3,
2035 0,
2036 1,
2037 1,
2038 None,
2039 )
2040 .unwrap()
2041 .client;
2042 assert_http_redirect_rejected(|url| async move {
2043 client
2044 .get(url, None, None, Some(3), None)
2045 .await
2046 .unwrap()
2047 .status
2048 .as_u16()
2049 })
2050 .await;
2051 }
2052
2053 #[rstest]
2054 #[case("private/buy", true, false)]
2055 #[case("private/cancel", true, false)]
2056 #[case("private/get_account_summaries", false, true)]
2057 #[case("private/get_positions", false, true)]
2058 #[case("public/get_instruments", false, false)]
2059 fn test_method_classification(
2060 #[case] method: &str,
2061 #[case] is_order: bool,
2062 #[case] is_account: bool,
2063 ) {
2064 assert_eq!(DeribitRawHttpClient::is_order_method(method), is_order);
2065 assert_eq!(DeribitRawHttpClient::is_account_method(method), is_account);
2066 }
2067
2068 #[rstest]
2069 #[case("private/buy", vec![DERIBIT_GLOBAL_RATE_KEY, DERIBIT_ORDER_RATE_KEY])]
2070 #[case("private/get_account_summaries", vec![DERIBIT_GLOBAL_RATE_KEY, DERIBIT_ACCOUNT_RATE_KEY])]
2071 #[case("public/get_instruments", vec![DERIBIT_GLOBAL_RATE_KEY])]
2072 fn test_rate_limit_keys(#[case] method: &str, #[case] expected_keys: Vec<&str>) {
2073 let keys = DeribitRawHttpClient::rate_limit_keys(method);
2074
2075 for key in &expected_keys {
2076 assert!(keys.contains(&key.to_string()));
2077 }
2078 assert!(keys.contains(&format!("deribit:{method}")));
2079 }
2080
2081 #[rstest]
2082 fn test_paginator_empty_page_returns_none() {
2083 let mut p = TradePaginator::new(100, 200);
2084 assert!(p.advance(&[], &[], true).is_none());
2085 }
2086
2087 #[rstest]
2088 fn test_paginator_single_page_no_more() {
2089 let mut p = TradePaginator::new(100, 200);
2090 let ids = vec!["t1".into(), "t2".into()];
2091 let ts = vec![150, 160];
2092
2093 let result = p.advance(&ids, &ts, false);
2094 assert_eq!(result, Some(vec![0, 1]));
2095 }
2096
2097 #[rstest]
2098 fn test_paginator_dedup_across_pages() {
2099 let mut p = TradePaginator::new(100, 200);
2100
2101 let ids1 = vec!["t1".into(), "t2".into()];
2103 let ts1 = vec![150, 150];
2104 let r1 = p.advance(&ids1, &ts1, true);
2105 assert_eq!(r1, Some(vec![0, 1]));
2106 assert_eq!(p.cursor, 150);
2107
2108 let ids2 = vec!["t2".into(), "t3".into()];
2110 let ts2 = vec![150, 150];
2111 let r2 = p.advance(&ids2, &ts2, false);
2112 assert_eq!(r2, Some(vec![1])); }
2114
2115 #[rstest]
2116 fn test_paginator_all_duplicates_advances_past_timestamp() {
2117 let mut p = TradePaginator::new(100, 200);
2118
2119 let ids = vec!["t1".into(), "t2".into()];
2121 let ts = vec![150, 150];
2122 p.advance(&ids, &ts, true);
2123 assert_eq!(p.cursor, 150);
2124
2125 let r2 = p.advance(&ids, &ts, true);
2127 assert_eq!(r2, Some(vec![])); assert_eq!(p.cursor, 151); }
2130
2131 #[rstest]
2132 fn test_paginator_is_exhausted_strict_greater_than() {
2133 let mut p = TradePaginator::new(100, 150);
2134
2135 let ids = vec!["t1".into()];
2136 let ts = vec![150];
2137 p.advance(&ids, &ts, true);
2138
2139 assert_eq!(p.cursor, 150);
2141 assert!(!p.is_exhausted());
2142
2143 p.advance(&ids, &ts, true);
2145 assert_eq!(p.cursor, 151);
2146 assert!(p.is_exhausted());
2147 }
2148
2149 #[rstest]
2150 fn test_paginator_reset_clears_state() {
2151 let mut p = TradePaginator::new(100, 200);
2152
2153 let ids = vec!["t1".into()];
2154 let ts = vec![150];
2155 p.advance(&ids, &ts, true);
2156 assert_eq!(p.seen_ids.len(), 1);
2157
2158 p.reset(100);
2159 assert_eq!(p.cursor, 100);
2160 assert!(p.seen_ids.is_empty());
2161
2162 let r = p.advance(&ids, &ts, false);
2164 assert_eq!(r, Some(vec![0]));
2165 }
2166}