TradingAgents/tradingagents/dataflows/tradier_common.py

124 lines
3.7 KiB
Python

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