diff --git a/tradingagents/dataflows/tradier_common.py b/tradingagents/dataflows/tradier_common.py new file mode 100644 index 00000000..c75d4067 --- /dev/null +++ b/tradingagents/dataflows/tradier_common.py @@ -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