use std::collections::HashMap;
use std::time::{Duration, Instant};
use crate::rate_limit::limits::trading;
use crate::rate_limit::TtlCache;
#[derive(Debug, Clone)]
pub struct OrderTrackingInfo {
pub created_at: Instant,
pub pair: String,
pub client_order_id: Option<String>,
}
impl OrderTrackingInfo {
pub fn new(pair: impl Into<String>) -> Self {
Self {
created_at: Instant::now(),
pair: pair.into(),
client_order_id: None,
}
}
pub fn with_client_id(pair: impl Into<String>, client_order_id: impl Into<String>) -> Self {
Self {
created_at: Instant::now(),
pair: pair.into(),
client_order_id: Some(client_order_id.into()),
}
}
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
}
#[derive(Debug)]
pub struct TradingRateLimiter {
orders: TtlCache<String, OrderTrackingInfo>,
counter: i64,
max_counter: i64,
decay_rate: i64,
last_update: Instant,
}
impl TradingRateLimiter {
pub fn new(max_counter: u32, decay_rate_per_sec: f64) -> Self {
Self {
orders: TtlCache::new(Duration::from_secs(300)), counter: 0,
max_counter: (max_counter as i64) * 100,
decay_rate: (decay_rate_per_sec * 100.0) as i64,
last_update: Instant::now(),
}
}
fn update_counter(&mut self) {
let elapsed = self.last_update.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
let decay = (elapsed_secs * self.decay_rate as f64) as i64;
self.counter = (self.counter - decay).max(0);
self.last_update = Instant::now();
}
pub fn try_place_order(&mut self, order_id: &str, info: OrderTrackingInfo) -> Result<(), Duration> {
self.update_counter();
let cost = 100;
if self.counter + cost <= self.max_counter {
self.counter += cost;
self.orders.insert(order_id.to_string(), info);
Ok(())
} else {
let excess = self.counter + cost - self.max_counter;
let wait_secs = excess as f64 / self.decay_rate as f64;
Err(Duration::from_secs_f64(wait_secs))
}
}
pub fn track_order(&mut self, order_id: impl Into<String>, info: OrderTrackingInfo) {
self.orders.insert(order_id.into(), info);
}
pub fn cancel_penalty(age: Duration) -> u32 {
let secs = age.as_secs();
if secs < 5 {
trading::CANCEL_PENALTY_UNDER_5S
} else if secs < 10 {
trading::CANCEL_PENALTY_5_TO_10S
} else if secs < 15 {
trading::CANCEL_PENALTY_10_TO_15S
} else if secs < 45 {
trading::CANCEL_PENALTY_15_TO_45S
} else if secs < 90 {
trading::CANCEL_PENALTY_45_TO_90S
} else {
trading::CANCEL_PENALTY_OVER_90S
}
}
pub fn try_cancel_order(&mut self, order_id: &str) -> Result<u32, Duration> {
self.update_counter();
let penalty = if let Some((_, age)) = self.orders.remove_with_age(&order_id.to_string()) {
Self::cancel_penalty(age)
} else {
trading::CANCEL_PENALTY_UNDER_5S
};
let cost = (penalty as i64) * 100;
if self.counter + cost <= self.max_counter {
self.counter += cost;
Ok(penalty)
} else {
let excess = self.counter + cost - self.max_counter;
let wait_secs = excess as f64 / self.decay_rate as f64;
Err(Duration::from_secs_f64(wait_secs))
}
}
pub fn order_cancelled(&mut self, order_id: &str) {
self.orders.remove(&order_id.to_string());
}
pub fn order_filled(&mut self, order_id: &str) {
self.orders.remove(&order_id.to_string());
}
pub fn current_counter(&self) -> f64 {
let elapsed = self.last_update.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
let decay = elapsed_secs * self.decay_rate as f64;
let counter = (self.counter as f64 - decay).max(0.0);
counter / 100.0
}
pub fn available_capacity(&self) -> f64 {
(self.max_counter as f64 / 100.0) - self.current_counter()
}
pub fn would_allow_place(&self) -> bool {
let current = (self.current_counter() * 100.0) as i64;
current + 100 <= self.max_counter
}
pub fn tracked_orders(&self) -> usize {
self.orders.active_count()
}
pub fn cleanup(&mut self) {
self.orders.cleanup();
}
}
impl Default for TradingRateLimiter {
fn default() -> Self {
Self::new(20, 1.0)
}
}
#[derive(Debug, Default)]
pub struct PerPairTradingLimiter {
limiters: HashMap<String, TradingRateLimiter>,
max_counter: u32,
decay_rate: f64,
}
impl PerPairTradingLimiter {
pub fn new(max_counter: u32, decay_rate: f64) -> Self {
Self {
limiters: HashMap::new(),
max_counter,
decay_rate,
}
}
pub fn limiter_for(&mut self, pair: &str) -> &mut TradingRateLimiter {
self.limiters
.entry(pair.to_string())
.or_insert_with(|| TradingRateLimiter::new(self.max_counter, self.decay_rate))
}
pub fn cleanup(&mut self) {
for limiter in self.limiters.values_mut() {
limiter.cleanup();
}
}
pub fn tracked_pairs(&self) -> usize {
self.limiters.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_cancel_penalty_calculation() {
assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(2)), 8);
assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(5)), 6);
assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(12)), 5);
assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(30)), 4);
assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(60)), 2);
assert_eq!(TradingRateLimiter::cancel_penalty(Duration::from_secs(100)), 0);
}
#[test]
fn test_place_order_tracking() {
let mut limiter = TradingRateLimiter::new(20, 1.0);
let info = OrderTrackingInfo::new("BTC/USD");
assert!(limiter.try_place_order("order1", info).is_ok());
assert_eq!(limiter.tracked_orders(), 1);
}
#[test]
fn test_cancel_penalty_applied() {
let mut limiter = TradingRateLimiter::new(20, 1.0);
let info = OrderTrackingInfo::new("BTC/USD");
limiter.try_place_order("order1", info).ok();
let result = limiter.try_cancel_order("order1");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 8); }
#[test]
fn test_decay_over_time() {
let mut limiter = TradingRateLimiter::new(20, 10.0);
for i in 0..20 {
let info = OrderTrackingInfo::new("BTC/USD");
limiter.try_place_order(&format!("order{}", i), info).ok();
}
let initial = limiter.current_counter();
thread::sleep(Duration::from_millis(200));
let after = limiter.current_counter();
assert!(after < initial);
}
#[test]
fn test_order_info_age() {
let info = OrderTrackingInfo::new("BTC/USD");
thread::sleep(Duration::from_millis(50));
let age = info.age();
assert!(age >= Duration::from_millis(50));
}
}