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