25 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-tradier-data-layer | 02 | execute | 2 |
|
|
true |
|
|
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/unit/data/test_tradier.py.
<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 @.planning/phases/01-tradier-data-layer/01-01-SUMMARY.mdFrom tradingagents/dataflows/tradier.py (created in Plan 01):
@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):
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):
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):
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):
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`:**
-
Add imports at top of file (after existing vendor imports):
from .tradier import ( get_options_chain as get_tradier_options_chain, get_options_expirations as get_tradier_options_expirations, ) from .tradier_common import TradierRateLimitError -
Add
"options_chain"category toTOOLS_CATEGORIESdict:"options_chain": { "description": "Options chain data with Greeks and IV", "tools": [ "get_options_chain", "get_options_expirations", ] } -
Add
"tradier"toVENDOR_LIST:VENDOR_LIST = ["yfinance", "alpha_vantage", "tradier"] -
Add options methods to
VENDOR_METHODSdict:"get_options_chain": { "tradier": get_tradier_options_chain, }, "get_options_expirations": { "tradier": get_tradier_options_expirations, }, -
Update
route_to_vendor()fallback exception catch to also catchTradierRateLimitError. Change line:except AlphaVantageRateLimitError:to:
except (AlphaVantageRateLimitError, TradierRateLimitError):
B. Update tradingagents/default_config.py:
Add to the "data_vendors" dict inside DEFAULT_CONFIG:
"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:
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')
"
<acceptance_criteria>
- 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
</acceptance_criteria>
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.
B. Create tests/conftest.py with shared Tradier API mock fixtures:
import pytest
from datetime import date, timedelta
from unittest.mock import patch, MagicMock
def _iso_days_out(days: int) -> str:
"""Expiration string relative to today so DTE assertions never go stale."""
return (date.today() + timedelta(days=days)).isoformat()
MOCK_EXPIRATIONS_RESPONSE = {
"expirations": {
"date": [_iso_days_out(d) for d in (7, 14, 21, 28, 45)]
}
}
MOCK_SINGLE_EXPIRATION_RESPONSE = {
"expirations": {
"date": _iso_days_out(21)
}
}
MOCK_CHAIN_RESPONSE = {
"options": {
"option": [
{
"symbol": "AAPL260417C00170000",
"underlying": "AAPL",
"option_type": "call",
"strike": 170.0,
"expiration_date": _iso_days_out(20),
"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": _iso_days_out(20),
"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": _iso_days_out(20),
"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": _iso_days_out(20),
"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": _iso_days_out(20),
"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/unit/data/test_tradier.py with test classes:
-
TestGetExpirations(DATA-02): patchtradingagents.dataflows.tradier_common.make_tradier_requestto returnMOCK_EXPIRATIONS_RESPONSE. Callget_options_expirations("AAPL", 0, 50). Assert result is a list of strings. Assert all dates are within 0-50 DTE of today. Test withMOCK_SINGLE_EXPIRATION_RESPONSEto verify Pitfall 5 normalization. -
TestGetOptionsChain(DATA-01): patchmake_tradier_request_with_retryto return mock expirations then mock chain. Callget_options_chain_structured("AAPL"). Assert.underlying == "AAPL". Assertlen(.contracts) == 3. Assert.contracts[0].bid == 5.10. Assert.contracts[0].volume == 1234. Assert.contracts[0].open_interest == 5678. Test single-contract normalization withMOCK_SINGLE_CONTRACT_RESPONSE. -
TestGreeksPresent(DATA-03): use contract from mock chain response. Assertcontract.delta == 0.55. Assertcontract.gamma == 0.04. Assertcontract.theta == -0.08. Assertcontract.vega == 0.25. Assertcontract.rho == 0.03. Assertcontract.greeks_updated_at == "2026-04-01 12:00:00". -
TestGreeksAbsent(Pitfall 1): useMOCK_CHAIN_NO_GREEKS_RESPONSE. Assertcontract.delta is None. Assertcontract.gamma is None. Assert no exception raised. -
TestIVPresent(DATA-04): use contract from mock chain. Assertcontract.bid_iv == 0.28. Assertcontract.mid_iv == 0.29. Assertcontract.ask_iv == 0.30. Assertcontract.smv_vol == 0.285. -
TestDTEFilter(DATA-05): create OptionsChain with contracts at various known expiration_dates. Callchain.filter_by_dte(10, 30). Assert only contracts within 10-30 DTE remain. Assert returned OptionsChain.expirations matches filtered contracts. -
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"]. -
TestRateLimitDetection: mockrequests.getto return response with status 429. VerifyTradierRateLimitErrorraised. Mock response withX-Ratelimit-Available: 0header. VerifyTradierRateLimitErrorraised. -
TestSessionCache: as in behavior — after twoget_options_chain_structured("AAPL")calls, assert underlying mockcall_count == 1; afterclear_options_cache()and a thirdget_options_chain_structured("AAPL"), assertcall_count == 2. -
TestSandboxURL: patchos.environwithTRADIER_SANDBOX=true. Assertget_base_url()returns"https://sandbox.tradier.com". Unset it. Assert returns"https://api.tradier.com".
Isolation pattern: every test class should clear the module cache in setUp and tearDown, e.g.:
class TestGetExpirations:
def setup_method(self):
from tradingagents.dataflows import tradier as t
t.clear_options_cache()
def teardown_method(self):
from tradingagents.dataflows import tradier as t
t.clear_options_cache()
def test_expirations_filtered_by_dte(self):
...
Apply the same pattern to other classes touching get_options_chain / get_options_chain_structured.
uv run python -m pytest tests/unit/data/test_tradier.py -x -v --timeout=30
<acceptance_criteria>
- 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/unit/data/test_tradier.py exists and defines at least 10 test classes (one per behavior bullet: TestGetExpirations, TestGetOptionsChain, …), each with one or more test_* methods
- tests/unit/data/test_tradier.py contains class TestGetExpirations
- tests/unit/data/test_tradier.py contains class TestGetOptionsChain
- tests/unit/data/test_tradier.py contains class TestGreeksPresent
- tests/unit/data/test_tradier.py contains class TestGreeksAbsent
- tests/unit/data/test_tradier.py contains class TestIVPresent
- tests/unit/data/test_tradier.py contains class TestDTEFilter
- tests/unit/data/test_tradier.py contains class TestVendorRegistration
- tests/unit/data/test_tradier.py contains class TestRateLimitDetection
- tests/unit/data/test_tradier.py contains class TestSessionCache
- tests/unit/data/test_tradier.py contains class TestSandboxURL
- tests/unit/data/test_tradier.py contains clear_options_cache calls for test isolation
- uv run python -m pytest tests/unit/data/test_tradier.py -x --timeout=30 exits 0 with all tests passing
</acceptance_criteria>
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.
<success_criteria>
- 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) </success_criteria>