TradingAgents/.planning/phases/01-tradier-data-layer/01-01-PLAN.md

15 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
01-tradier-data-layer 01 execute 1
tradingagents/dataflows/tradier_common.py
tradingagents/dataflows/tradier.py
true
DATA-01
DATA-02
DATA-03
DATA-04
DATA-05
truths artifacts key_links
get_options_expirations() returns a list of YYYY-MM-DD date strings filtered by DTE range
get_options_chain() returns an OptionsChain with OptionsContract dataclasses containing Greeks and IV
OptionsChain.to_dataframe() returns a pandas DataFrame with all contract fields
OptionsChain.filter_by_dte() returns a filtered OptionsChain within the specified DTE range
Tradier API auth uses TRADIER_API_KEY env var and TRADIER_SANDBOX toggles base URL
Rate limit detection raises TradierRateLimitError on 429 or exhausted X-Ratelimit-Available
path provides exports
tradingagents/dataflows/tradier_common.py Auth, base URL, rate limit error, HTTP helper
get_api_key
get_base_url
make_tradier_request
make_tradier_request_with_retry
TradierRateLimitError
path provides exports
tradingagents/dataflows/tradier.py Tradier vendor module with options chain retrieval
OptionsContract
OptionsChain
get_options_expirations
get_options_chain
get_options_chain_structured
clear_options_cache
from to via pattern
tradingagents/dataflows/tradier.py tradingagents/dataflows/tradier_common.py imports make_tradier_request, TradierRateLimitError from .tradier_common import
Create the Tradier vendor module with typed data structures, API integration, and options chain retrieval with Greeks and IV.

Purpose: Establish the core data retrieval layer that all downstream options analysis depends on. This is the foundation -- typed dataclasses (OptionsContract, OptionsChain) define the contract for every subsequent phase. Output: Two new files (tradier_common.py, tradier.py) providing complete options chain retrieval with 1st-order Greeks, IV, DTE filtering, session caching, and rate limit handling.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-tradier-data-layer/01-CONTEXT.md @.planning/phases/01-tradier-data-layer/01-RESEARCH.md

From tradingagents/dataflows/alpha_vantage_common.py:

class AlphaVantageRateLimitError(Exception):
    """Exception raised when Alpha Vantage API rate limit is exceeded."""
    pass

def get_api_key() -> str:
    api_key = os.getenv("ALPHA_VANTAGE_API_KEY")
    if not api_key:
        raise ValueError("ALPHA_VANTAGE_API_KEY environment variable is not set.")
    return api_key

From tradingagents/dataflows/config.py:

def get_config() -> Dict:
    """Get the current configuration."""
Task 1: Create Tradier common module (auth, HTTP helper, rate limit error) tradingagents/dataflows/tradier_common.py - tradingagents/dataflows/alpha_vantage_common.py - tradingagents/dataflows/config.py Create `tradingagents/dataflows/tradier_common.py` with:
  1. Constants (per D-02):

    • TRADIER_PRODUCTION_URL = "https://api.tradier.com"
    • TRADIER_SANDBOX_URL = "https://sandbox.tradier.com"
  2. TradierRateLimitError exception class:

    • class TradierRateLimitError(Exception): pass
  3. get_api_key() function (per D-01):

    • Read os.getenv("TRADIER_API_KEY")
    • If not set, raise ValueError("TRADIER_API_KEY environment variable is not set.")
    • Return the key string
  4. get_base_url() function (per D-02):

    • Read os.getenv("TRADIER_SANDBOX", "false")
    • If value .lower() is in ("true", "1", "yes"), return TRADIER_SANDBOX_URL
    • Otherwise return TRADIER_PRODUCTION_URL
  5. make_tradier_request(path: str, params: dict | None = None) -> dict function:

    • Build URL: f"{get_base_url()}{path}"
    • Headers: {"Authorization": f"Bearer {get_api_key()}", "Accept": "application/json"}
    • Execute requests.get(url, headers=headers, params=params or {})
    • Read available = response.headers.get("X-Ratelimit-Available"). If available is not None, try int(available). On ValueError or TypeError, raise TradierRateLimitError including the raw X-Ratelimit-Available value and response.headers.get("X-Ratelimit-Expiry"). If conversion succeeds and the integer is <= 0, raise TradierRateLimitError including X-Ratelimit-Expiry
    • Check response.status_code == 429 -- raise TradierRateLimitError("Tradier rate limit exceeded (HTTP 429)")
    • Call response.raise_for_status()
    • Return response.json()
  6. make_tradier_request_with_retry(path: str, params: dict | None = None, max_retries: int = 3) -> dict function:

    • Loop for attempt in range(max_retries):
    • Try make_tradier_request(path, params)
    • On TradierRateLimitError: if attempt < max_retries - 1, time.sleep(2 ** attempt) then continue; else re-raise
    • Return result on success

Imports needed: os, time, requests uv run python -c "from tradingagents.dataflows.tradier_common import get_api_key, get_base_url, make_tradier_request, make_tradier_request_with_retry, TradierRateLimitError; print('imports OK')" <acceptance_criteria> - tradingagents/dataflows/tradier_common.py exists - File contains TRADIER_PRODUCTION_URL = "https://api.tradier.com" - File contains TRADIER_SANDBOX_URL = "https://sandbox.tradier.com" - File contains class TradierRateLimitError(Exception): - File contains def get_api_key() -> str: - File contains def get_base_url() -> str: - File contains def make_tradier_request(path: str, params: dict | None = None) -> dict: - File contains def make_tradier_request_with_retry( - File contains os.getenv("TRADIER_API_KEY") - File contains os.getenv("TRADIER_SANDBOX", "false") - File contains X-Ratelimit-Available - File contains Bearer {get_api_key()} - uv run python -c "from tradingagents.dataflows.tradier_common import TradierRateLimitError" exits 0 </acceptance_criteria> tradier_common.py exports TradierRateLimitError, get_api_key, get_base_url, make_tradier_request, make_tradier_request_with_retry. Auth reads TRADIER_API_KEY env var, sandbox detection reads TRADIER_SANDBOX env var, rate limit detection checks both headers and HTTP 429.

Task 2: Create Tradier vendor module with typed dataclasses and chain retrieval tradingagents/dataflows/tradier.py - tradingagents/dataflows/tradier_common.py - tradingagents/dataflows/y_finance.py - tradingagents/dataflows/alpha_vantage.py Create `tradingagents/dataflows/tradier.py` with:
  1. OptionsContract dataclass (per D-06):

    @dataclass
    class OptionsContract:
        symbol: str              # OCC symbol e.g. AAPL220617C00270000
        underlying: str          # e.g. AAPL
        option_type: str         # "call" or "put"
        strike: float
        expiration_date: str     # YYYY-MM-DD
        bid: float
        ask: float
        last: float
        volume: int
        open_interest: int
        # Greeks (from ORATS via Tradier)
        delta: float | None = None
        gamma: float | None = None
        theta: float | None = None
        vega: float | None = None
        rho: float | None = None
        phi: float | None = None
        # IV
        bid_iv: float | None = None
        mid_iv: float | None = None
        ask_iv: float | None = None
        smv_vol: float | None = None
        greeks_updated_at: str | None = None
    
  2. OptionsChain dataclass (per D-06):

    @dataclass
    class OptionsChain:
        underlying: str
        fetch_timestamp: str
        expirations: list[str]
        contracts: list[OptionsContract] = field(default_factory=list)
    
        def to_dataframe(self) -> pd.DataFrame:
            return pd.DataFrame([vars(c) for c in self.contracts])
    
        def filter_by_dte(self, min_dte: int = 0, max_dte: int = 50) -> "OptionsChain":
            # Calculate DTE for each contract, keep those in range
            # Return new OptionsChain with filtered contracts and updated expirations list
    

    The filter_by_dte method must: calculate (exp_date - date.today()).days for each contract's expiration_date, keep contracts where min_dte <= dte <= max_dte, derive the expirations list from the filtered contracts' unique expiration_dates (sorted), return a new OptionsChain instance.

  3. _parse_contract(raw: dict) -> OptionsContract private function:

    • Extract greeks = raw.get("greeks") or {}
    • Map all fields from Tradier response dict to OptionsContract
    • Use float(raw.get("bid", 0) or 0) pattern for nullable numeric fields (bid, ask, last)
    • Use int(raw.get("volume", 0) or 0) for volume, open_interest
    • Greeks and IV fields use greeks.get("delta") etc. (None if missing -- per Pitfall 1: sandbox has no Greeks)
    • greeks_updated_at = greeks.get("updated_at")
  4. get_options_expirations(symbol: str, min_dte: int = 0, max_dte: int = 50) -> list[str] function:

    • Call make_tradier_request_with_retry("/v1/markets/options/expirations", {"symbol": symbol.upper(), "includeAllRoots": "false", "strikes": "false"})
    • Extract dates: data.get("expirations", {}).get("date", [])
    • Normalize single-item response: if isinstance(dates, str): dates = [dates] (Pitfall 5)
    • Filter by DTE range using date.today() and datetime.strptime(d, "%Y-%m-%d").date()
    • Return sorted list of qualifying date strings
  5. _fetch_chain_for_expiration(symbol: str, expiration: str) -> list[OptionsContract] private function:

    • Call make_tradier_request_with_retry("/v1/markets/options/chains", {"symbol": symbol.upper(), "expiration": expiration, "greeks": "true"}) (per D-05: always greeks=true)
    • Extract: options = data.get("options", {}).get("option", [])
    • Normalize single contract: if isinstance(options, dict): options = [options] (Pitfall 2)
    • Return [_parse_contract(opt) for opt in options]
  6. Session cache (Claude's discretion -- in-memory dict):

    • Module-level _options_cache: dict[str, OptionsChain] = {}
    • clear_options_cache() function to reset cache
  7. get_options_chain(symbol: str, min_dte: int = 0, max_dte: int = 50) -> str function (per D-03: pre-fetch all expirations):

    • Cache stores OptionsChain only (_options_cache: dict[str, OptionsChain]). On hit for cache_key, return _options_cache[cache_key].to_dataframe().to_string() (do not pre-serialize strings in the cache)
    • Call get_options_expirations(symbol, min_dte, max_dte) to get qualifying dates
    • For each expiration, call _fetch_chain_for_expiration(symbol, expiration) and accumulate all OptionsContract instances
    • Build OptionsChain(underlying=symbol.upper(), fetch_timestamp=datetime.now().isoformat(), expirations=expirations, contracts=all_contracts)
    • Store in _options_cache[cache_key]
    • Return chain.to_dataframe().to_string() (string return matches vendor function pattern for LLM tool consumption)
  8. get_options_chain_structured(symbol: str, min_dte: int = 0, max_dte: int = 50) -> OptionsChain function:

    • Same as get_options_chain but returns the OptionsChain dataclass directly
    • This is for programmatic access by downstream computation modules (not LLM tools)
    • Uses same cache

Imports: from dataclasses import dataclass, field, from datetime import datetime, date, import pandas as pd, from .tradier_common import make_tradier_request_with_retry, TradierRateLimitError uv run python -c "from tradingagents.dataflows.tradier import OptionsContract, OptionsChain, get_options_expirations, get_options_chain, get_options_chain_structured, clear_options_cache; print('imports OK')" <acceptance_criteria> - tradingagents/dataflows/tradier.py exists - File contains @dataclass (at least twice, for OptionsContract and OptionsChain) - File contains class OptionsContract: with fields: symbol, underlying, option_type, strike, expiration_date, bid, ask, last, volume, open_interest, delta, gamma, theta, vega, rho, phi, bid_iv, mid_iv, ask_iv, smv_vol, greeks_updated_at - File contains class OptionsChain: with fields: underlying, fetch_timestamp, expirations, contracts - File contains def to_dataframe(self) -> pd.DataFrame: - File contains def filter_by_dte(self, min_dte: int = 0, max_dte: int = 50) - File contains def get_options_expirations(symbol: str, min_dte: int = 0, max_dte: int = 50) -> list[str]: - File contains def get_options_chain(symbol: str, min_dte: int = 0, max_dte: int = 50) -> str: - File contains def get_options_chain_structured(symbol: str, min_dte: int = 0, max_dte: int = 50) -> OptionsChain: - File contains def clear_options_cache(): - File contains "greeks": "true" (D-05) - File contains if isinstance(options, dict): (Pitfall 2 normalization) - File contains if isinstance(dates, str): (Pitfall 5 normalization) - File contains _options_cache (session cache) - File contains from .tradier_common import - uv run python -c "from tradingagents.dataflows.tradier import OptionsContract, OptionsChain" exits 0 </acceptance_criteria> tradier.py exports OptionsContract, OptionsChain (with to_dataframe and filter_by_dte), get_options_expirations, get_options_chain (string return), get_options_chain_structured (dataclass return), clear_options_cache. Pre-fetches all expirations in DTE range per D-03, always requests greeks=true per D-05, handles Pitfall 2 and Pitfall 5 response normalization, uses session cache per Claude's discretion.

- `uv run python -c "from tradingagents.dataflows.tradier_common import TradierRateLimitError, get_api_key, get_base_url, make_tradier_request, make_tradier_request_with_retry; print('common OK')"` - `uv run python -c "from tradingagents.dataflows.tradier import OptionsContract, OptionsChain, get_options_expirations, get_options_chain, get_options_chain_structured, clear_options_cache; print('tradier OK')"`

<success_criteria>

  • Two new files exist: tradier_common.py (auth + HTTP) and tradier.py (dataclasses + retrieval)
  • All exports importable without errors
  • OptionsContract has all 21 fields (symbol through greeks_updated_at)
  • OptionsChain has to_dataframe() and filter_by_dte() methods
  • Rate limit detection covers both header check and HTTP 429
  • Sandbox/production URL switching driven by TRADIER_SANDBOX env var </success_criteria>
After completion, create `.planning/phases/01-tradier-data-layer/01-01-SUMMARY.md`