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>
119 lines
3.3 KiB
Python
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()
|