Skip to main content

deribit_http/
rate_limit.rs

1//! Rate limiting implementation for Deribit HTTP client
2//!
3//! This module provides automatic rate limiting to comply with Deribit API limits.
4//! It implements a token bucket algorithm with different limits for different
5//! endpoint categories.
6
7use crate::sleep_compat::sleep;
8use crate::sync_compat::Mutex;
9use crate::time_compat::Instant;
10use std::collections::HashMap;
11use std::sync::Arc;
12use std::time::Duration;
13
14/// Rate limiter for different endpoint categories
15#[derive(Debug, Clone)]
16pub struct RateLimiter {
17    limiters: Arc<Mutex<HashMap<RateLimitCategory, TokenBucket>>>,
18}
19
20/// Categories of rate limits based on Deribit API documentation
21#[derive(Debug, Clone, Hash, Eq, PartialEq)]
22pub enum RateLimitCategory {
23    /// Trading endpoints (buy, sell, cancel, etc.)
24    Trading,
25    /// Market data endpoints (ticker, orderbook, etc.)
26    MarketData,
27    /// Account management endpoints (summary, positions, etc.)
28    Account,
29    /// Authentication endpoints
30    Auth,
31    /// General/other endpoints
32    General,
33}
34
35/// Token bucket implementation for rate limiting
36#[derive(Debug)]
37struct TokenBucket {
38    /// Maximum number of tokens
39    capacity: u32,
40    /// Current number of tokens
41    tokens: u32,
42    /// Rate of token refill (tokens per second)
43    refill_rate: u32,
44    /// Last refill time
45    last_refill: Instant,
46}
47
48impl TokenBucket {
49    /// Create a new token bucket
50    fn new(capacity: u32, refill_rate: u32) -> Self {
51        Self {
52            capacity,
53            tokens: capacity,
54            refill_rate,
55            last_refill: Instant::now(),
56        }
57    }
58
59    /// Try to consume a token, returns true if successful
60    fn try_consume(&mut self) -> bool {
61        self.refill();
62        if self.tokens > 0 {
63            self.tokens -= 1;
64            true
65        } else {
66            false
67        }
68    }
69
70    /// Get time until next token is available
71    fn time_until_token(&self) -> Duration {
72        if self.tokens > 0 {
73            Duration::from_secs(0)
74        } else {
75            Duration::from_secs_f64(1.0 / self.refill_rate as f64)
76        }
77    }
78
79    /// Refill tokens based on elapsed time
80    fn refill(&mut self) {
81        let now = Instant::now();
82        let elapsed = now.duration_since(self.last_refill);
83        let tokens_to_add = (elapsed.as_secs_f64() * self.refill_rate as f64) as u32;
84
85        if tokens_to_add > 0 {
86            self.tokens = (self.tokens + tokens_to_add).min(self.capacity);
87            self.last_refill = now;
88        }
89    }
90}
91
92impl RateLimiter {
93    /// Create a new rate limiter with default Deribit limits
94    pub fn new() -> Self {
95        let mut limiters = HashMap::new();
96
97        // Based on Deribit API documentation
98        // Trading: 200 requests per second with burst of 250
99        limiters.insert(RateLimitCategory::Trading, TokenBucket::new(250, 200));
100
101        // Market data: Higher limits for public endpoints
102        limiters.insert(RateLimitCategory::MarketData, TokenBucket::new(500, 400));
103
104        // Account: Moderate limits
105        limiters.insert(RateLimitCategory::Account, TokenBucket::new(200, 150));
106
107        // Auth: Lower limits to prevent abuse
108        limiters.insert(RateLimitCategory::Auth, TokenBucket::new(50, 30));
109
110        // General: Default limits
111        limiters.insert(RateLimitCategory::General, TokenBucket::new(300, 200));
112
113        Self {
114            limiters: Arc::new(Mutex::new(limiters)),
115        }
116    }
117
118    /// Wait for rate limit permission for the given category
119    pub async fn wait_for_permission(&self, category: RateLimitCategory) {
120        loop {
121            let wait_time = {
122                let mut limiters = self.limiters.lock().await;
123                let bucket = limiters
124                    .get_mut(&category)
125                    .expect("Rate limit category should exist");
126
127                if bucket.try_consume() {
128                    return; // Permission granted
129                } else {
130                    bucket.time_until_token()
131                }
132            };
133
134            // Wait before trying again
135            sleep(wait_time.max(Duration::from_millis(10))).await;
136        }
137    }
138
139    /// Check if permission is available without waiting
140    pub async fn check_permission(&self, category: RateLimitCategory) -> bool {
141        let mut limiters = self.limiters.lock().await;
142        let bucket = limiters
143            .get_mut(&category)
144            .expect("Rate limit category should exist");
145        bucket.try_consume()
146    }
147
148    /// Get current token count for a category (for monitoring)
149    pub async fn get_tokens(&self, category: RateLimitCategory) -> u32 {
150        let mut limiters = self.limiters.lock().await;
151        let bucket = limiters
152            .get_mut(&category)
153            .expect("Rate limit category should exist");
154        bucket.refill();
155        bucket.tokens
156    }
157}
158
159impl Default for RateLimiter {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165/// Helper function to categorize endpoints
166pub fn categorize_endpoint(endpoint: &str) -> RateLimitCategory {
167    if endpoint.contains("/private/buy")
168        || endpoint.contains("/private/sell")
169        || endpoint.contains("/private/cancel")
170        || endpoint.contains("/private/edit")
171    {
172        RateLimitCategory::Trading
173    } else if endpoint.contains("/public/ticker")
174        || endpoint.contains("/public/get_order_book")
175        || endpoint.contains("/public/get_last_trades")
176        || endpoint.contains("/public/get_instruments")
177    {
178        RateLimitCategory::MarketData
179    } else if endpoint.contains("/private/get_account_summary")
180        || endpoint.contains("/private/get_positions")
181        || endpoint.contains("/private/get_subaccounts")
182    {
183        RateLimitCategory::Account
184    } else if endpoint.contains("/public/auth") || endpoint.contains("/private/logout") {
185        RateLimitCategory::Auth
186    } else {
187        RateLimitCategory::General
188    }
189}
190
191#[cfg(all(test, not(target_arch = "wasm32")))]
192mod tests {
193    use super::*;
194    use crate::sleep_compat::sleep;
195
196    #[tokio::test]
197    async fn test_token_bucket_basic() {
198        let mut bucket = TokenBucket::new(10, 5);
199
200        // Should be able to consume initial tokens
201        for _ in 0..10 {
202            assert!(bucket.try_consume());
203        }
204
205        // Should be empty now
206        assert!(!bucket.try_consume());
207    }
208
209    #[tokio::test]
210    async fn test_token_bucket_refill() {
211        let mut bucket = TokenBucket::new(5, 10); // 10 tokens per second
212
213        // Consume all tokens
214        for _ in 0..5 {
215            assert!(bucket.try_consume());
216        }
217        assert!(!bucket.try_consume());
218
219        // Wait for refill (100ms should give us 1 token at 10/sec rate)
220        sleep(Duration::from_millis(200)).await;
221
222        // Should have at least 1 token now
223        assert!(bucket.try_consume());
224    }
225
226    #[tokio::test]
227    async fn test_rate_limiter() {
228        let limiter = RateLimiter::new();
229
230        // Should be able to get permission initially
231        assert!(limiter.check_permission(RateLimitCategory::Trading).await);
232
233        // Test waiting for permission
234        limiter
235            .wait_for_permission(RateLimitCategory::MarketData)
236            .await;
237        // If we get here, the wait succeeded
238    }
239
240    #[test]
241    fn test_endpoint_categorization() {
242        assert_eq!(
243            categorize_endpoint("/private/buy"),
244            RateLimitCategory::Trading
245        );
246        assert_eq!(
247            categorize_endpoint("/public/ticker"),
248            RateLimitCategory::MarketData
249        );
250        assert_eq!(
251            categorize_endpoint("/private/get_account_summary"),
252            RateLimitCategory::Account
253        );
254        assert_eq!(categorize_endpoint("/public/auth"), RateLimitCategory::Auth);
255        assert_eq!(
256            categorize_endpoint("/public/get_time"),
257            RateLimitCategory::General
258        );
259    }
260}