""" Rate Limiter for API Calls OrbiQuant IA Trading Platform Token bucket rate limiter with: - Configurable calls per minute - Automatic waiting when limit reached - Metrics tracking """ import asyncio from datetime import datetime, timedelta from typing import Optional import logging logger = logging.getLogger(__name__) class RateLimiter: """ Token bucket rate limiter for API calls. Features: - Configurable calls per window - Automatic waiting when limit reached - Metrics tracking """ def __init__( self, calls_per_minute: int = 5, window_seconds: int = 60 ): """ Initialize rate limiter. Args: calls_per_minute: Maximum calls allowed per window window_seconds: Window duration in seconds """ self.limit = calls_per_minute self.window_seconds = window_seconds self._calls = 0 self._window_start = datetime.utcnow() self._lock = asyncio.Lock() self._total_waits = 0 self._total_calls = 0 async def acquire(self) -> float: """ Acquire a rate limit token. Waits if limit is reached until the window resets. Returns: Wait time in seconds (0 if no wait needed) """ async with self._lock: now = datetime.utcnow() elapsed = (now - self._window_start).total_seconds() # Reset window if expired if elapsed >= self.window_seconds: self._calls = 0 self._window_start = now elapsed = 0 # Check if we need to wait wait_time = 0 if self._calls >= self.limit: wait_time = self.window_seconds - elapsed if wait_time > 0: logger.info( f"Rate limit reached ({self._calls}/{self.limit}), " f"waiting {wait_time:.1f}s" ) self._total_waits += 1 await asyncio.sleep(wait_time) # Reset after wait self._calls = 0 self._window_start = datetime.utcnow() self._calls += 1 self._total_calls += 1 return wait_time def get_remaining(self) -> int: """Get remaining calls in current window""" now = datetime.utcnow() elapsed = (now - self._window_start).total_seconds() if elapsed >= self.window_seconds: return self.limit return max(0, self.limit - self._calls) def get_reset_time(self) -> datetime: """Get when the current window resets""" return self._window_start + timedelta(seconds=self.window_seconds) def get_stats(self) -> dict: """Get rate limiter statistics""" return { "limit": self.limit, "window_seconds": self.window_seconds, "current_calls": self._calls, "remaining": self.get_remaining(), "reset_at": self.get_reset_time().isoformat(), "total_calls": self._total_calls, "total_waits": self._total_waits, } def reset(self): """Reset the rate limiter state""" self._calls = 0 self._window_start = datetime.utcnow()