trading-platform-data-servi.../src/providers/rate_limiter.py
rckrdmrd 62a9f3e1d9 feat: Initial commit - Data Service
Data aggregation and distribution service:
- Market data collection
- OHLCV aggregation
- Real-time data feeds
- Data API endpoints

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 04:30:42 -06:00

119 lines
3.3 KiB
Python

"""
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()