diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 3a0bcbc6..19dec70e 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -35,7 +35,10 @@ Decimal phases appear between their surrounding integers in numeric order.
3. User can see implied volatility per contract (bid_iv, mid_iv, ask_iv, smv_vol) from Tradier
4. User can filter the options chain by DTE range (e.g., 30-60 DTE)
5. Tradier is registered as a new vendor in the existing data routing layer following the established provider pattern
-**Plans**: TBD
+**Plans:** 2 plans
+Plans:
+- [ ] 01-01-PLAN.md -- Tradier common module and vendor module with typed dataclasses and chain retrieval
+- [ ] 01-02-PLAN.md -- Vendor routing integration, @tool functions, and comprehensive unit tests
### Phase 2: Deterministic Math Core
**Goal**: All deterministic financial math lives in a pure Python module with comprehensive tests, never as LLM tool calls
@@ -148,7 +151,7 @@ Note: Phases 2, 3, and 4 can execute in parallel after Phase 1. Phase 6 depends
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
-| 1. Tradier Data Layer | 0/TBD | Not started | - |
+| 1. Tradier Data Layer | 0/2 | Planning complete | - |
| 2. Deterministic Math Core | 0/TBD | Not started | - |
| 3. Volatility Metrics | 0/TBD | Not started | - |
| 4. GEX & Market Microstructure | 0/TBD | Not started | - |
diff --git a/.planning/phases/01-tradier-data-layer/01-01-PLAN.md b/.planning/phases/01-tradier-data-layer/01-01-PLAN.md
new file mode 100644
index 00000000..27832657
--- /dev/null
+++ b/.planning/phases/01-tradier-data-layer/01-01-PLAN.md
@@ -0,0 +1,290 @@
+---
+phase: 01-tradier-data-layer
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - tradingagents/dataflows/tradier_common.py
+ - tradingagents/dataflows/tradier.py
+autonomous: true
+requirements:
+ - DATA-01
+ - DATA-02
+ - DATA-03
+ - DATA-04
+ - DATA-05
+
+must_haves:
+ truths:
+ - "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"
+ artifacts:
+ - path: "tradingagents/dataflows/tradier_common.py"
+ provides: "Auth, base URL, rate limit error, HTTP helper"
+ exports: ["get_api_key", "get_base_url", "make_tradier_request", "TradierRateLimitError"]
+ - path: "tradingagents/dataflows/tradier.py"
+ provides: "Tradier vendor module with options chain retrieval"
+ exports: ["OptionsContract", "OptionsChain", "get_options_expirations", "get_options_chain"]
+ key_links:
+ - from: "tradingagents/dataflows/tradier.py"
+ to: "tradingagents/dataflows/tradier_common.py"
+ via: "imports make_tradier_request, TradierRateLimitError"
+ pattern: "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.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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:
+```python
+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:
+```python
+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 {})`
+ - Check `response.headers.get("X-Ratelimit-Available")` -- if not None and `int(remaining) <= 0`, raise `TradierRateLimitError` with message including `X-Ratelimit-Expiry` header value
+ - 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')"
+
+
+ - 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
+
+ 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):
+ - Check cache: `cache_key = f"{symbol.upper()}:{min_dte}:{max_dte}"`; if in `_options_cache`, return its `.to_dataframe().to_string()`
+ - 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')"
+
+
+ - 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
+
+ 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')"`
+
+
+
+- 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
+
+
+
diff --git a/.planning/phases/01-tradier-data-layer/01-02-PLAN.md b/.planning/phases/01-tradier-data-layer/01-02-PLAN.md
new file mode 100644
index 00000000..55c330df
--- /dev/null
+++ b/.planning/phases/01-tradier-data-layer/01-02-PLAN.md
@@ -0,0 +1,552 @@
+---
+phase: 01-tradier-data-layer
+plan: 02
+type: execute
+wave: 2
+depends_on:
+ - 01-01
+files_modified:
+ - tradingagents/dataflows/interface.py
+ - tradingagents/default_config.py
+ - tradingagents/agents/utils/options_tools.py
+ - .env.example
+ - tests/test_tradier.py
+ - tests/conftest.py
+autonomous: true
+requirements:
+ - DATA-08
+
+must_haves:
+ truths:
+ - "Tradier is registered as a vendor in VENDOR_LIST"
+ - "options_chain category exists in TOOLS_CATEGORIES with get_options_chain and get_options_expirations tools"
+ - "VENDOR_METHODS maps get_options_chain and get_options_expirations to Tradier implementations"
+ - "DEFAULT_CONFIG data_vendors includes options_chain: tradier"
+ - "route_to_vendor catches TradierRateLimitError for vendor fallback"
+ - "@tool decorated functions exist for options chain retrieval"
+ - "All unit tests pass with mocked Tradier API responses"
+ artifacts:
+ - path: "tradingagents/dataflows/interface.py"
+ provides: "Vendor routing with Tradier and options_chain category"
+ contains: "options_chain"
+ - path: "tradingagents/default_config.py"
+ provides: "Default config with options_chain vendor"
+ contains: "options_chain"
+ - path: "tradingagents/agents/utils/options_tools.py"
+ provides: "LangChain @tool functions for options data"
+ exports: ["get_options_chain", "get_options_expirations"]
+ - path: "tests/test_tradier.py"
+ provides: "Unit tests for all DATA requirements"
+ min_lines: 100
+ key_links:
+ - from: "tradingagents/dataflows/interface.py"
+ to: "tradingagents/dataflows/tradier.py"
+ via: "import get_options_chain, get_options_expirations"
+ pattern: "from .tradier import"
+ - from: "tradingagents/agents/utils/options_tools.py"
+ to: "tradingagents/dataflows/interface.py"
+ via: "route_to_vendor call"
+ pattern: "route_to_vendor"
+ - from: "tradingagents/dataflows/interface.py"
+ to: "tradingagents/dataflows/tradier_common.py"
+ via: "import TradierRateLimitError for fallback catch"
+ pattern: "TradierRateLimitError"
+---
+
+
+Integrate Tradier into the existing vendor routing system, create @tool functions for LLM agents, and write comprehensive unit tests covering all Phase 1 requirements.
+
+Purpose: Without vendor registration, no agent can access Tradier data. Without tests, we cannot verify correctness. This plan wires the Tradier module into the system and proves it works.
+Output: Updated interface.py and default_config.py, new options_tools.py, comprehensive test suite in tests/test_tradier.py.
+
+
+
+@$HOME/.claude/get-shit-done/workflows/execute-plan.md
+@$HOME/.claude/get-shit-done/templates/summary.md
+
+
+
+@.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
+@.planning/phases/01-tradier-data-layer/01-01-SUMMARY.md
+
+
+
+
+From tradingagents/dataflows/tradier.py (created in Plan 01):
+```python
+@dataclass
+class OptionsContract:
+ symbol: str; underlying: str; option_type: str; strike: float
+ expiration_date: str; bid: float; ask: float; last: float
+ volume: int; open_interest: int
+ delta: float | None; gamma: float | None; theta: float | None
+ vega: float | None; rho: float | None; phi: float | None
+ bid_iv: float | None; mid_iv: float | None; ask_iv: float | None
+ smv_vol: float | None; greeks_updated_at: str | None
+
+@dataclass
+class OptionsChain:
+ underlying: str; fetch_timestamp: str; expirations: list[str]
+ contracts: list[OptionsContract]
+ def to_dataframe(self) -> pd.DataFrame: ...
+ def filter_by_dte(self, min_dte: int = 0, max_dte: int = 50) -> "OptionsChain": ...
+
+def get_options_expirations(symbol: str, min_dte: int = 0, max_dte: int = 50) -> list[str]: ...
+def get_options_chain(symbol: str, min_dte: int = 0, max_dte: int = 50) -> str: ...
+def get_options_chain_structured(symbol: str, min_dte: int = 0, max_dte: int = 50) -> OptionsChain: ...
+def clear_options_cache(): ...
+```
+
+From tradingagents/dataflows/tradier_common.py (created in Plan 01):
+```python
+class TradierRateLimitError(Exception): pass
+def get_api_key() -> str: ...
+def get_base_url() -> str: ...
+def make_tradier_request(path: str, params: dict | None = None) -> dict: ...
+def make_tradier_request_with_retry(path: str, params: dict | None = None, max_retries: int = 3) -> dict: ...
+```
+
+From tradingagents/dataflows/interface.py (existing):
+```python
+TOOLS_CATEGORIES = { "core_stock_apis": ..., "technical_indicators": ..., "fundamental_data": ..., "news_data": ... }
+VENDOR_LIST = ["yfinance", "alpha_vantage"]
+VENDOR_METHODS = { "get_stock_data": {...}, ... }
+def route_to_vendor(method: str, *args, **kwargs): ...
+# Currently catches only AlphaVantageRateLimitError in fallback loop
+```
+
+From tradingagents/default_config.py (existing):
+```python
+DEFAULT_CONFIG = {
+ "data_vendors": {
+ "core_stock_apis": "yfinance",
+ "technical_indicators": "yfinance",
+ "fundamental_data": "yfinance",
+ "news_data": "yfinance",
+ },
+ "tool_vendors": {},
+}
+```
+
+From tradingagents/agents/utils/core_stock_tools.py (pattern reference):
+```python
+from langchain_core.tools import tool
+from typing import Annotated
+from tradingagents.dataflows.interface import route_to_vendor
+
+@tool
+def get_stock_data(symbol: Annotated[str, "ticker symbol"], ...) -> str:
+ return route_to_vendor("get_stock_data", symbol, start_date, end_date)
+```
+
+
+
+
+
+
+ Task 1: Register Tradier in vendor routing and update default config
+ tradingagents/dataflows/interface.py, tradingagents/default_config.py, .env.example, tradingagents/agents/utils/options_tools.py
+
+ - tradingagents/dataflows/interface.py
+ - tradingagents/default_config.py
+ - tradingagents/agents/utils/core_stock_tools.py
+ - tradingagents/dataflows/tradier.py
+ - tradingagents/dataflows/tradier_common.py
+ - .env.example
+
+
+**A. Update `tradingagents/dataflows/interface.py`:**
+
+1. Add imports at top of file (after existing vendor imports):
+ ```python
+ from .tradier import (
+ get_options_chain as get_tradier_options_chain,
+ get_options_expirations as get_tradier_options_expirations,
+ )
+ from .tradier_common import TradierRateLimitError
+ ```
+
+2. Add `"options_chain"` category to `TOOLS_CATEGORIES` dict:
+ ```python
+ "options_chain": {
+ "description": "Options chain data with Greeks and IV",
+ "tools": [
+ "get_options_chain",
+ "get_options_expirations",
+ ]
+ }
+ ```
+
+3. Add `"tradier"` to `VENDOR_LIST`:
+ ```python
+ VENDOR_LIST = ["yfinance", "alpha_vantage", "tradier"]
+ ```
+
+4. Add options methods to `VENDOR_METHODS` dict:
+ ```python
+ "get_options_chain": {
+ "tradier": get_tradier_options_chain,
+ },
+ "get_options_expirations": {
+ "tradier": get_tradier_options_expirations,
+ },
+ ```
+
+5. Update `route_to_vendor()` fallback exception catch to also catch `TradierRateLimitError`. Change line:
+ ```python
+ except AlphaVantageRateLimitError:
+ ```
+ to:
+ ```python
+ except (AlphaVantageRateLimitError, TradierRateLimitError):
+ ```
+
+**B. Update `tradingagents/default_config.py`:**
+
+Add to the `"data_vendors"` dict inside `DEFAULT_CONFIG`:
+```python
+"options_chain": "tradier", # Options: tradier
+```
+
+**C. Update `.env.example`:**
+
+Add after the existing API keys section:
+```
+# Options Data Providers
+TRADIER_API_KEY=
+TRADIER_SANDBOX=false
+```
+
+**D. Create `tradingagents/agents/utils/options_tools.py`:**
+
+Following the `core_stock_tools.py` pattern exactly:
+
+```python
+from langchain_core.tools import tool
+from typing import Annotated
+from tradingagents.dataflows.interface import route_to_vendor
+
+
+@tool
+def get_options_chain(
+ symbol: Annotated[str, "ticker symbol of the company"],
+ min_dte: Annotated[int, "minimum days to expiration"] = 0,
+ max_dte: Annotated[int, "maximum days to expiration"] = 50,
+) -> str:
+ """
+ Retrieve options chain data with Greeks and IV for a given ticker symbol.
+ Returns strikes, expirations, bid/ask, volume, OI, 1st-order Greeks
+ (Delta, Gamma, Theta, Vega, Rho), and implied volatility (bid_iv,
+ mid_iv, ask_iv, smv_vol) filtered by DTE range.
+
+ Args:
+ symbol (str): Ticker symbol of the company, e.g. AAPL, TSLA
+ min_dte (int): Minimum days to expiration (default 0)
+ max_dte (int): Maximum days to expiration (default 50)
+ Returns:
+ str: A formatted dataframe containing options chain data with Greeks and IV.
+ """
+ return route_to_vendor("get_options_chain", symbol, min_dte, max_dte)
+
+
+@tool
+def get_options_expirations(
+ symbol: Annotated[str, "ticker symbol of the company"],
+ min_dte: Annotated[int, "minimum days to expiration"] = 0,
+ max_dte: Annotated[int, "maximum days to expiration"] = 50,
+) -> str:
+ """
+ Retrieve available options expiration dates for a given ticker symbol,
+ filtered by DTE range.
+
+ Args:
+ symbol (str): Ticker symbol of the company, e.g. AAPL, TSLA
+ min_dte (int): Minimum days to expiration (default 0)
+ max_dte (int): Maximum days to expiration (default 50)
+ Returns:
+ str: Comma-separated list of expiration dates (YYYY-MM-DD format).
+ """
+ result = route_to_vendor("get_options_expirations", symbol, min_dte, max_dte)
+ if isinstance(result, list):
+ return ", ".join(result)
+ return str(result)
+```
+
+Default DTE range 0-50 per D-04. Docstrings are LLM-readable per project conventions.
+
+
+ uv run python -c "
+from tradingagents.dataflows.interface import TOOLS_CATEGORIES, VENDOR_LIST, VENDOR_METHODS
+assert 'options_chain' in TOOLS_CATEGORIES, 'options_chain not in TOOLS_CATEGORIES'
+assert 'tradier' in VENDOR_LIST, 'tradier not in VENDOR_LIST'
+assert 'get_options_chain' in VENDOR_METHODS, 'get_options_chain not in VENDOR_METHODS'
+assert 'get_options_expirations' in VENDOR_METHODS, 'get_options_expirations not in VENDOR_METHODS'
+from tradingagents.default_config import DEFAULT_CONFIG
+assert DEFAULT_CONFIG['data_vendors'].get('options_chain') == 'tradier', 'options_chain not in default config'
+from tradingagents.agents.utils.options_tools import get_options_chain, get_options_expirations
+print('ALL CHECKS PASSED')
+"
+
+
+ - tradingagents/dataflows/interface.py contains `"options_chain"` in TOOLS_CATEGORIES
+ - tradingagents/dataflows/interface.py contains `"tradier"` in VENDOR_LIST
+ - tradingagents/dataflows/interface.py contains `"get_options_chain"` in VENDOR_METHODS
+ - tradingagents/dataflows/interface.py contains `"get_options_expirations"` in VENDOR_METHODS
+ - tradingagents/dataflows/interface.py contains `TradierRateLimitError` in the except clause of route_to_vendor
+ - tradingagents/dataflows/interface.py contains `from .tradier import`
+ - tradingagents/dataflows/interface.py contains `from .tradier_common import TradierRateLimitError`
+ - tradingagents/default_config.py contains `"options_chain": "tradier"`
+ - .env.example contains `TRADIER_API_KEY=`
+ - .env.example contains `TRADIER_SANDBOX=false`
+ - tradingagents/agents/utils/options_tools.py exists
+ - tradingagents/agents/utils/options_tools.py contains `@tool` (at least twice)
+ - tradingagents/agents/utils/options_tools.py contains `route_to_vendor("get_options_chain"`
+ - tradingagents/agents/utils/options_tools.py contains `route_to_vendor("get_options_expirations"`
+ - tradingagents/agents/utils/options_tools.py contains `min_dte` and `max_dte` parameters with default 0 and 50 (D-04)
+ - `uv run python -c "from tradingagents.agents.utils.options_tools import get_options_chain"` exits 0
+
+ Tradier registered in vendor routing (VENDOR_LIST, TOOLS_CATEGORIES, VENDOR_METHODS). DEFAULT_CONFIG has options_chain: tradier. route_to_vendor catches TradierRateLimitError. Two @tool functions created following core_stock_tools.py pattern. .env.example updated with TRADIER_API_KEY and TRADIER_SANDBOX.
+
+
+
+ Task 2: Create comprehensive unit tests for all Phase 1 requirements
+ tests/test_tradier.py, tests/conftest.py
+
+ - tradingagents/dataflows/tradier.py
+ - tradingagents/dataflows/tradier_common.py
+ - tradingagents/dataflows/interface.py
+ - tradingagents/agents/utils/options_tools.py
+ - tests/test_ticker_symbol_handling.py
+
+
+ - TestGetExpirations (DATA-02): mock Tradier /expirations response with 5 dates, verify DTE filter returns only dates within range. Test single-date string normalization (Pitfall 5).
+ - TestGetOptionsChain (DATA-01): mock Tradier /chains response with 3 contracts, verify OptionsChain has correct underlying, expirations, and contract count. Test single-contract dict normalization (Pitfall 2).
+ - TestGreeksPresent (DATA-03): mock response with greeks object, verify OptionsContract has delta, gamma, theta, vega, rho values and greeks_updated_at timestamp.
+ - TestGreeksAbsent (Pitfall 1): mock response with greeks: null, verify OptionsContract has None for all Greeks fields without crashing.
+ - TestIVPresent (DATA-04): mock response with greeks object, verify OptionsContract has bid_iv, mid_iv, ask_iv, smv_vol values.
+ - TestDTEFilter (DATA-05): create OptionsChain with contracts at various DTEs, call filter_by_dte(30, 60), verify only contracts in range remain.
+ - TestVendorRegistration (DATA-08): verify "tradier" in VENDOR_LIST, "options_chain" in TOOLS_CATEGORIES, get_options_chain and get_options_expirations in VENDOR_METHODS.
+ - TestRateLimitDetection: mock 429 response, verify TradierRateLimitError raised. Mock response with X-Ratelimit-Available: 0, verify TradierRateLimitError raised.
+ - TestSessionCache: call get_options_chain twice with mock, verify only one API call made.
+ - TestSandboxURL: set TRADIER_SANDBOX=true, verify get_base_url returns sandbox URL.
+
+
+**A. Install pytest if not present:**
+```bash
+uv add --dev pytest>=8.0
+```
+
+**B. Create `tests/conftest.py`** with shared Tradier API mock fixtures:
+
+```python
+import pytest
+from unittest.mock import patch, MagicMock
+
+MOCK_EXPIRATIONS_RESPONSE = {
+ "expirations": {
+ "date": ["2026-04-03", "2026-04-10", "2026-04-17", "2026-04-24", "2026-05-15", "2026-07-17"]
+ }
+}
+
+MOCK_SINGLE_EXPIRATION_RESPONSE = {
+ "expirations": {
+ "date": "2026-04-17"
+ }
+}
+
+MOCK_CHAIN_RESPONSE = {
+ "options": {
+ "option": [
+ {
+ "symbol": "AAPL260417C00170000",
+ "underlying": "AAPL",
+ "option_type": "call",
+ "strike": 170.0,
+ "expiration_date": "2026-04-17",
+ "bid": 5.10,
+ "ask": 5.30,
+ "last": 5.20,
+ "volume": 1234,
+ "open_interest": 5678,
+ "greeks": {
+ "delta": 0.55,
+ "gamma": 0.04,
+ "theta": -0.08,
+ "vega": 0.25,
+ "rho": 0.03,
+ "phi": -0.02,
+ "bid_iv": 0.28,
+ "mid_iv": 0.29,
+ "ask_iv": 0.30,
+ "smv_vol": 0.285,
+ "updated_at": "2026-04-01 12:00:00"
+ }
+ },
+ {
+ "symbol": "AAPL260417P00170000",
+ "underlying": "AAPL",
+ "option_type": "put",
+ "strike": 170.0,
+ "expiration_date": "2026-04-17",
+ "bid": 3.40,
+ "ask": 3.60,
+ "last": 3.50,
+ "volume": 890,
+ "open_interest": 2345,
+ "greeks": {
+ "delta": -0.45,
+ "gamma": 0.04,
+ "theta": -0.07,
+ "vega": 0.25,
+ "rho": -0.02,
+ "phi": 0.02,
+ "bid_iv": 0.27,
+ "mid_iv": 0.28,
+ "ask_iv": 0.29,
+ "smv_vol": 0.280,
+ "updated_at": "2026-04-01 12:00:00"
+ }
+ },
+ {
+ "symbol": "AAPL260417C00175000",
+ "underlying": "AAPL",
+ "option_type": "call",
+ "strike": 175.0,
+ "expiration_date": "2026-04-17",
+ "bid": 2.80,
+ "ask": 3.00,
+ "last": 2.90,
+ "volume": 567,
+ "open_interest": 1234,
+ "greeks": {
+ "delta": 0.40,
+ "gamma": 0.05,
+ "theta": -0.09,
+ "vega": 0.24,
+ "rho": 0.02,
+ "phi": -0.01,
+ "bid_iv": 0.30,
+ "mid_iv": 0.31,
+ "ask_iv": 0.32,
+ "smv_vol": 0.305,
+ "updated_at": "2026-04-01 12:00:00"
+ }
+ }
+ ]
+ }
+}
+
+MOCK_CHAIN_NO_GREEKS_RESPONSE = {
+ "options": {
+ "option": [
+ {
+ "symbol": "AAPL260417C00170000",
+ "underlying": "AAPL",
+ "option_type": "call",
+ "strike": 170.0,
+ "expiration_date": "2026-04-17",
+ "bid": 5.10,
+ "ask": 5.30,
+ "last": 5.20,
+ "volume": 1234,
+ "open_interest": 5678,
+ "greeks": None
+ }
+ ]
+ }
+}
+
+MOCK_SINGLE_CONTRACT_RESPONSE = {
+ "options": {
+ "option": {
+ "symbol": "AAPL260417C00170000",
+ "underlying": "AAPL",
+ "option_type": "call",
+ "strike": 170.0,
+ "expiration_date": "2026-04-17",
+ "bid": 5.10,
+ "ask": 5.30,
+ "last": 5.20,
+ "volume": 1234,
+ "open_interest": 5678,
+ "greeks": {
+ "delta": 0.55, "gamma": 0.04, "theta": -0.08,
+ "vega": 0.25, "rho": 0.03, "phi": -0.02,
+ "bid_iv": 0.28, "mid_iv": 0.29, "ask_iv": 0.30,
+ "smv_vol": 0.285, "updated_at": "2026-04-01 12:00:00"
+ }
+ }
+ }
+}
+```
+
+**C. Create `tests/test_tradier.py`** with test classes:
+
+1. `TestGetExpirations` (DATA-02): patch `tradingagents.dataflows.tradier_common.make_tradier_request` to return `MOCK_EXPIRATIONS_RESPONSE`. Call `get_options_expirations("AAPL", 0, 50)`. Assert result is a list of strings. Assert all dates are within 0-50 DTE of today. Test with `MOCK_SINGLE_EXPIRATION_RESPONSE` to verify Pitfall 5 normalization.
+
+2. `TestGetOptionsChain` (DATA-01): patch `make_tradier_request_with_retry` to return mock expirations then mock chain. Call `get_options_chain_structured("AAPL")`. Assert `.underlying == "AAPL"`. Assert `len(.contracts) == 3`. Assert `.contracts[0].bid == 5.10`. Assert `.contracts[0].volume == 1234`. Assert `.contracts[0].open_interest == 5678`. Test single-contract normalization with `MOCK_SINGLE_CONTRACT_RESPONSE`.
+
+3. `TestGreeksPresent` (DATA-03): use contract from mock chain response. Assert `contract.delta == 0.55`. Assert `contract.gamma == 0.04`. Assert `contract.theta == -0.08`. Assert `contract.vega == 0.25`. Assert `contract.rho == 0.03`. Assert `contract.greeks_updated_at == "2026-04-01 12:00:00"`.
+
+4. `TestGreeksAbsent` (Pitfall 1): use `MOCK_CHAIN_NO_GREEKS_RESPONSE`. Assert `contract.delta is None`. Assert `contract.gamma is None`. Assert no exception raised.
+
+5. `TestIVPresent` (DATA-04): use contract from mock chain. Assert `contract.bid_iv == 0.28`. Assert `contract.mid_iv == 0.29`. Assert `contract.ask_iv == 0.30`. Assert `contract.smv_vol == 0.285`.
+
+6. `TestDTEFilter` (DATA-05): create OptionsChain with contracts at various known expiration_dates. Call `chain.filter_by_dte(10, 30)`. Assert only contracts within 10-30 DTE remain. Assert returned OptionsChain.expirations matches filtered contracts.
+
+7. `TestVendorRegistration` (DATA-08): import TOOLS_CATEGORIES, VENDOR_LIST, VENDOR_METHODS from interface. Assert `"tradier" in VENDOR_LIST`. Assert `"options_chain" in TOOLS_CATEGORIES`. Assert `"get_options_chain" in VENDOR_METHODS`. Assert `"tradier" in VENDOR_METHODS["get_options_chain"]`.
+
+8. `TestRateLimitDetection`: mock `requests.get` to return response with status 429. Verify `TradierRateLimitError` raised. Mock response with `X-Ratelimit-Available: 0` header. Verify `TradierRateLimitError` raised.
+
+9. `TestSessionCache`: patch API, call `get_options_chain_structured("AAPL")` twice. Assert mock was called only the number of times for first call (not doubled). Call `clear_options_cache()` and verify next call triggers API again.
+
+10. `TestSandboxURL`: patch `os.environ` with `TRADIER_SANDBOX=true`. Assert `get_base_url()` returns `"https://sandbox.tradier.com"`. Unset it. Assert returns `"https://api.tradier.com"`.
+
+Important: Each test must call `clear_options_cache()` in setUp/teardown to prevent cross-test cache pollution.
+
+
+ uv run python -m pytest tests/test_tradier.py -x -v --timeout=30
+
+
+ - tests/conftest.py exists and contains MOCK_EXPIRATIONS_RESPONSE, MOCK_CHAIN_RESPONSE, MOCK_CHAIN_NO_GREEKS_RESPONSE, MOCK_SINGLE_CONTRACT_RESPONSE, MOCK_SINGLE_EXPIRATION_RESPONSE
+ - tests/test_tradier.py exists and contains at least 10 test methods
+ - tests/test_tradier.py contains class TestGetExpirations
+ - tests/test_tradier.py contains class TestGetOptionsChain
+ - tests/test_tradier.py contains class TestGreeksPresent
+ - tests/test_tradier.py contains class TestGreeksAbsent
+ - tests/test_tradier.py contains class TestIVPresent
+ - tests/test_tradier.py contains class TestDTEFilter
+ - tests/test_tradier.py contains class TestVendorRegistration
+ - tests/test_tradier.py contains class TestRateLimitDetection
+ - tests/test_tradier.py contains class TestSessionCache
+ - tests/test_tradier.py contains class TestSandboxURL
+ - tests/test_tradier.py contains `clear_options_cache` calls for test isolation
+ - `uv run python -m pytest tests/test_tradier.py -x --timeout=30` exits 0 with all tests passing
+
+ All tests pass. Tests cover: DATA-01 (chain retrieval), DATA-02 (expirations), DATA-03 (Greeks present), DATA-04 (IV present), DATA-05 (DTE filtering), DATA-08 (vendor registration), plus edge cases (no Greeks, single contract, single expiration, rate limits, caching, sandbox URL). No real API calls -- all mocked.
+
+
+
+
+
+- `uv run python -m pytest tests/test_tradier.py -x -v --timeout=30` -- all tests pass
+- `uv run python -c "from tradingagents.dataflows.interface import TOOLS_CATEGORIES, VENDOR_LIST; assert 'tradier' in VENDOR_LIST; assert 'options_chain' in TOOLS_CATEGORIES; print('ROUTING OK')"` -- vendor registered
+- `uv run python -c "from tradingagents.agents.utils.options_tools import get_options_chain, get_options_expirations; print('TOOLS OK')"` -- tool functions importable
+
+
+
+- Tradier fully registered in vendor routing (VENDOR_LIST, TOOLS_CATEGORIES, VENDOR_METHODS, DEFAULT_CONFIG)
+- route_to_vendor catches both AlphaVantageRateLimitError and TradierRateLimitError
+- Two @tool functions exist in options_tools.py following core_stock_tools.py pattern
+- .env.example documents TRADIER_API_KEY and TRADIER_SANDBOX
+- All unit tests pass covering DATA-01 through DATA-05 and DATA-08
+- No real API calls in tests (all mocked)
+
+
+