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