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