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