kraken_api_client/rate_limit/
trading.rs1use std::collections::HashMap;
19use std::time::{Duration, Instant};
20
21use crate::rate_limit::limits::trading;
22use crate::rate_limit::TtlCache;
23
24#[derive(Debug, Clone)]
26pub struct OrderTrackingInfo {
27 pub created_at: Instant,
29 pub pair: String,
31 pub client_order_id: Option<String>,
33}
34
35impl OrderTrackingInfo {
36 pub fn new(pair: impl Into<String>) -> Self {
38 Self {
39 created_at: Instant::now(),
40 pair: pair.into(),
41 client_order_id: None,
42 }
43 }
44
45 pub fn with_client_id(pair: impl Into<String>, client_order_id: impl Into<String>) -> Self {
47 Self {
48 created_at: Instant::now(),
49 pair: pair.into(),
50 client_order_id: Some(client_order_id.into()),
51 }
52 }
53
54 pub fn age(&self) -> Duration {
56 self.created_at.elapsed()
57 }
58}
59
60#[derive(Debug)]
65pub struct TradingRateLimiter {
66 orders: TtlCache<String, OrderTrackingInfo>,
68 counter: i64,
70 max_counter: i64,
72 decay_rate: i64,
74 last_update: Instant,
76}
77
78impl TradingRateLimiter {
79 pub fn new(max_counter: u32, decay_rate_per_sec: f64) -> Self {
86 Self {
87 orders: TtlCache::new(Duration::from_secs(300)), counter: 0,
89 max_counter: (max_counter as i64) * 100,
90 decay_rate: (decay_rate_per_sec * 100.0) as i64,
91 last_update: Instant::now(),
92 }
93 }
94
95 fn update_counter(&mut self) {
97 let elapsed = self.last_update.elapsed();
98 let elapsed_secs = elapsed.as_secs_f64();
99 let decay = (elapsed_secs * self.decay_rate as f64) as i64;
100 self.counter = (self.counter - decay).max(0);
101 self.last_update = Instant::now();
102 }
103
104 pub fn try_place_order(&mut self, order_id: &str, info: OrderTrackingInfo) -> Result<(), Duration> {
108 self.update_counter();
109
110 let cost = 100;
112
113 if self.counter + cost <= self.max_counter {
114 self.counter += cost;
115 self.orders.insert(order_id.to_string(), info);
116 Ok(())
117 } else {
118 let excess = self.counter + cost - self.max_counter;
120 let wait_secs = excess as f64 / self.decay_rate as f64;
121 Err(Duration::from_secs_f64(wait_secs))
122 }
123 }
124
125 pub fn track_order(&mut self, order_id: impl Into<String>, info: OrderTrackingInfo) {
129 self.orders.insert(order_id.into(), info);
130 }
131
132 pub fn cancel_penalty(age: Duration) -> u32 {
136 let secs = age.as_secs();
137
138 if secs < 5 {
139 trading::CANCEL_PENALTY_UNDER_5S
140 } else if secs < 10 {
141 trading::CANCEL_PENALTY_5_TO_10S
142 } else if secs < 15 {
143 trading::CANCEL_PENALTY_10_TO_15S
144 } else if secs < 45 {
145 trading::CANCEL_PENALTY_15_TO_45S
146 } else if secs < 90 {
147 trading::CANCEL_PENALTY_45_TO_90S
148 } else {
149 trading::CANCEL_PENALTY_OVER_90S
150 }
151 }
152
153 pub fn try_cancel_order(&mut self, order_id: &str) -> Result<u32, Duration> {
158 self.update_counter();
159
160 let penalty = if let Some((_, age)) = self.orders.remove_with_age(&order_id.to_string()) {
162 Self::cancel_penalty(age)
163 } else {
164 trading::CANCEL_PENALTY_UNDER_5S
166 };
167
168 let cost = (penalty as i64) * 100;
169
170 if self.counter + cost <= self.max_counter {
171 self.counter += cost;
172 Ok(penalty)
173 } else {
174 let excess = self.counter + cost - self.max_counter;
176 let wait_secs = excess as f64 / self.decay_rate as f64;
177 Err(Duration::from_secs_f64(wait_secs))
178 }
179 }
180
181 pub fn order_cancelled(&mut self, order_id: &str) {
185 self.orders.remove(&order_id.to_string());
186 }
187
188 pub fn order_filled(&mut self, order_id: &str) {
192 self.orders.remove(&order_id.to_string());
193 }
194
195 pub fn current_counter(&self) -> f64 {
197 let elapsed = self.last_update.elapsed();
198 let elapsed_secs = elapsed.as_secs_f64();
199 let decay = elapsed_secs * self.decay_rate as f64;
200 let counter = (self.counter as f64 - decay).max(0.0);
201 counter / 100.0
202 }
203
204 pub fn available_capacity(&self) -> f64 {
206 (self.max_counter as f64 / 100.0) - self.current_counter()
207 }
208
209 pub fn would_allow_place(&self) -> bool {
211 let current = (self.current_counter() * 100.0) as i64;
212 current + 100 <= self.max_counter
213 }
214
215 pub fn tracked_orders(&self) -> usize {
217 self.orders.active_count()
218 }
219
220 pub fn cleanup(&mut self) {
222 self.orders.cleanup();
223 }
224}
225
226impl Default for TradingRateLimiter {
227 fn default() -> Self {
228 Self::new(20, 1.0)
230 }
231}
232
233#[derive(Debug, Default)]
237pub struct PerPairTradingLimiter {
238 limiters: HashMap<String, TradingRateLimiter>,
239 max_counter: u32,
240 decay_rate: f64,
241}
242
243impl PerPairTradingLimiter {
244 pub fn new(max_counter: u32, decay_rate: f64) -> Self {
246 Self {
247 limiters: HashMap::new(),
248 max_counter,
249 decay_rate,
250 }
251 }
252
253 pub fn limiter_for(&mut self, pair: &str) -> &mut TradingRateLimiter {
255 self.limiters
256 .entry(pair.to_string())
257 .or_insert_with(|| TradingRateLimiter::new(self.max_counter, self.decay_rate))
258 }
259
260 pub fn cleanup(&mut self) {
262 for limiter in self.limiters.values_mut() {
263 limiter.cleanup();
264 }
265 }
266
267 pub fn tracked_pairs(&self) -> usize {
269 self.limiters.len()
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use std::thread;
277
278 #[test]
279 fn test_cancel_penalty_calculation() {
280 assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(2)), 8);
281 assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(5)), 6);
282 assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(12)), 5);
283 assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(30)), 4);
284 assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(60)), 2);
285 assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(100)), 0);
286 }
287
288 #[test]
289 fn test_place_order_tracking() {
290 let mut limiter = TradingRateLimiter::new(20, 1.0);
291
292 let info = OrderTrackingInfo::new("BTC/USD");
293 assert!(limiter.try_place_order("order1", info).is_ok());
294 assert_eq!(limiter.tracked_orders(), 1);
295 }
296
297 #[test]
298 fn test_cancel_penalty_applied() {
299 let mut limiter = TradingRateLimiter::new(20, 1.0);
300
301 let info = OrderTrackingInfo::new("BTC/USD");
302 limiter.try_place_order("order1", info).ok();
303
304 let result = limiter.try_cancel_order("order1");
306 assert!(result.is_ok());
307 assert_eq!(result.unwrap(), 8); }
309
310 #[test]
311 fn test_decay_over_time() {
312 let mut limiter = TradingRateLimiter::new(20, 10.0); for i in 0..20 {
316 let info = OrderTrackingInfo::new("BTC/USD");
317 limiter.try_place_order(&format!("order{}", i), info).ok();
318 }
319
320 let initial = limiter.current_counter();
321 thread::sleep(Duration::from_millis(200));
322 let after = limiter.current_counter();
323
324 assert!(after < initial);
325 }
326
327 #[test]
328 fn test_order_info_age() {
329 let info = OrderTrackingInfo::new("BTC/USD");
330 thread::sleep(Duration::from_millis(50));
331 let age = info.age();
332 assert!(age >= Duration::from_millis(50));
333 }
334}