feat(01-01): create Tradier common module with auth, HTTP helper, and rate limit handling
- Add TradierRateLimitError exception for vendor fallback integration - Add get_api_key() reading TRADIER_API_KEY env var - Add get_base_url() with TRADIER_SANDBOX env var toggle - Add make_tradier_request() with X-Ratelimit-Available header and HTTP 429 detection - Add make_tradier_request_with_retry() with exponential backoff Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ba32a41f39
commit
246c2b7c4d
|
|
@ -0,0 +1,123 @@
|
|||
"""Tradier API common utilities: authentication, HTTP helpers, and rate limit handling.
|
||||
|
||||
Mirrors the pattern established by alpha_vantage_common.py for vendor-abstracted
|
||||
data access with automatic rate limit detection and retry logic.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
TRADIER_PRODUCTION_URL = "https://api.tradier.com"
|
||||
TRADIER_SANDBOX_URL = "https://sandbox.tradier.com"
|
||||
|
||||
|
||||
class TradierRateLimitError(Exception):
|
||||
"""Exception raised when Tradier API rate limit is exceeded."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def get_api_key() -> str:
|
||||
"""Retrieve the Tradier API key from environment variables.
|
||||
|
||||
Returns:
|
||||
The API key string.
|
||||
|
||||
Raises:
|
||||
ValueError: If TRADIER_API_KEY is not set.
|
||||
"""
|
||||
api_key = os.getenv("TRADIER_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError("TRADIER_API_KEY environment variable is not set.")
|
||||
return api_key
|
||||
|
||||
|
||||
def get_base_url() -> str:
|
||||
"""Return the Tradier base URL based on sandbox configuration.
|
||||
|
||||
Reads TRADIER_SANDBOX env var. If set to 'true', '1', or 'yes' (case-insensitive),
|
||||
returns the sandbox URL; otherwise returns production URL.
|
||||
|
||||
Returns:
|
||||
The base URL string for Tradier API requests.
|
||||
"""
|
||||
sandbox = os.getenv("TRADIER_SANDBOX", "false")
|
||||
if sandbox.lower() in ("true", "1", "yes"):
|
||||
return TRADIER_SANDBOX_URL
|
||||
return TRADIER_PRODUCTION_URL
|
||||
|
||||
|
||||
def make_tradier_request(path: str, params: dict | None = None) -> dict:
|
||||
"""Make an authenticated GET request to the Tradier API.
|
||||
|
||||
Args:
|
||||
path: API endpoint path (e.g. '/v1/markets/options/chains').
|
||||
params: Optional query parameters.
|
||||
|
||||
Returns:
|
||||
Parsed JSON response as a dict.
|
||||
|
||||
Raises:
|
||||
TradierRateLimitError: On HTTP 429 or exhausted X-Ratelimit-Available.
|
||||
requests.HTTPError: On other non-2xx responses.
|
||||
"""
|
||||
url = f"{get_base_url()}{path}"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {get_api_key()}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
|
||||
response = requests.get(url, headers=headers, params=params or {})
|
||||
|
||||
# Check rate limit headers before status code
|
||||
available = response.headers.get("X-Ratelimit-Available")
|
||||
if available is not None:
|
||||
try:
|
||||
available_int = int(available)
|
||||
except (ValueError, TypeError):
|
||||
expiry = response.headers.get("X-Ratelimit-Expiry")
|
||||
raise TradierRateLimitError(
|
||||
f"Tradier rate limit: non-numeric X-Ratelimit-Available={available!r}, "
|
||||
f"X-Ratelimit-Expiry={expiry}"
|
||||
)
|
||||
if available_int <= 0:
|
||||
expiry = response.headers.get("X-Ratelimit-Expiry")
|
||||
raise TradierRateLimitError(
|
||||
f"Tradier rate limit exhausted: X-Ratelimit-Available=0, "
|
||||
f"X-Ratelimit-Expiry={expiry}"
|
||||
)
|
||||
|
||||
if response.status_code == 429:
|
||||
raise TradierRateLimitError("Tradier rate limit exceeded (HTTP 429)")
|
||||
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
def make_tradier_request_with_retry(
|
||||
path: str, params: dict | None = None, max_retries: int = 3
|
||||
) -> dict:
|
||||
"""Make a Tradier API request with exponential backoff retry on rate limits.
|
||||
|
||||
Args:
|
||||
path: API endpoint path.
|
||||
params: Optional query parameters.
|
||||
max_retries: Maximum number of attempts (default 3).
|
||||
|
||||
Returns:
|
||||
Parsed JSON response as a dict.
|
||||
|
||||
Raises:
|
||||
TradierRateLimitError: If all retries are exhausted.
|
||||
"""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
return make_tradier_request(path, params)
|
||||
except TradierRateLimitError:
|
||||
if attempt < max_retries - 1:
|
||||
time.sleep(2**attempt)
|
||||
continue
|
||||
raise
|
||||
Loading…
Reference in New Issue