deribit_http/
rate_limit.rs1use std::collections::HashMap;
8use std::sync::Arc;
9use std::time::{Duration, Instant};
10use tokio::sync::Mutex;
11use tokio::time::sleep;
12
13#[derive(Debug, Clone)]
15pub struct RateLimiter {
16 limiters: Arc<Mutex<HashMap<RateLimitCategory, TokenBucket>>>,
17}
18
19#[derive(Debug, Clone, Hash, Eq, PartialEq)]
21pub enum RateLimitCategory {
22 Trading,
24 MarketData,
26 Account,
28 Auth,
30 General,
32}
33
34#[derive(Debug)]
36struct TokenBucket {
37 capacity: u32,
39 tokens: u32,
41 refill_rate: u32,
43 last_refill: Instant,
45}
46
47impl TokenBucket {
48 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 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 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 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 pub fn new() -> Self {
94 let mut limiters = HashMap::new();
95
96 limiters.insert(RateLimitCategory::Trading, TokenBucket::new(250, 200));
99
100 limiters.insert(RateLimitCategory::MarketData, TokenBucket::new(500, 400));
102
103 limiters.insert(RateLimitCategory::Account, TokenBucket::new(200, 150));
105
106 limiters.insert(RateLimitCategory::Auth, TokenBucket::new(50, 30));
108
109 limiters.insert(RateLimitCategory::General, TokenBucket::new(300, 200));
111
112 Self {
113 limiters: Arc::new(Mutex::new(limiters)),
114 }
115 }
116
117 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; } else {
129 bucket.time_until_token()
130 }
131 };
132
133 if wait_time > Duration::from_secs(0) {
135 sleep(wait_time).await;
136 } else {
137 sleep(Duration::from_millis(10)).await;
139 }
140 }
141 }
142
143 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 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
169pub 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 for _ in 0..10 {
206 assert!(bucket.try_consume());
207 }
208
209 assert!(!bucket.try_consume());
211 }
212
213 #[tokio::test]
214 async fn test_token_bucket_refill() {
215 let mut bucket = TokenBucket::new(5, 10); for _ in 0..5 {
219 assert!(bucket.try_consume());
220 }
221 assert!(!bucket.try_consume());
222
223 sleep(Duration::from_millis(200)).await;
225
226 assert!(bucket.try_consume());
228 }
229
230 #[tokio::test]
231 async fn test_rate_limiter() {
232 let limiter = RateLimiter::new();
233
234 assert!(limiter.check_permission(RateLimitCategory::Trading).await);
236
237 limiter
239 .wait_for_permission(RateLimitCategory::MarketData)
240 .await;
241 }
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}