15 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-tradier-data-layer | 01 | execute | 1 |
|
true |
|
|
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.mdFrom 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:
-
Constants (per D-02):
TRADIER_PRODUCTION_URL = "https://api.tradier.com"TRADIER_SANDBOX_URL = "https://sandbox.tradier.com"
-
TradierRateLimitError exception class:
class TradierRateLimitError(Exception): pass
-
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
- Read
-
get_base_url() function (per D-02):
- Read
os.getenv("TRADIER_SANDBOX", "false") - If value
.lower()is in("true", "1", "yes"), returnTRADIER_SANDBOX_URL - Otherwise return
TRADIER_PRODUCTION_URL
- Read
-
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"). Ifavailable is not None, tryint(available). OnValueErrororTypeError, raiseTradierRateLimitErrorincluding the rawX-Ratelimit-Availablevalue andresponse.headers.get("X-Ratelimit-Expiry"). If conversion succeeds and the integer is<= 0, raiseTradierRateLimitErrorincludingX-Ratelimit-Expiry - Check
response.status_code == 429-- raiseTradierRateLimitError("Tradier rate limit exceeded (HTTP 429)") - Call
response.raise_for_status() - Return
response.json()
- Build URL:
-
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: ifattempt < max_retries - 1,time.sleep(2 ** attempt)then continue; else re-raise - Return result on success
- Loop
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.
-
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 -
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 listThe
filter_by_dtemethod must: calculate(exp_date - date.today()).daysfor each contract'sexpiration_date, keep contracts wheremin_dte <= dte <= max_dte, derive theexpirationslist from the filtered contracts' unique expiration_dates (sorted), return a new OptionsChain instance. -
_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")
- Extract
-
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()anddatetime.strptime(d, "%Y-%m-%d").date() - Return sorted list of qualifying date strings
- Call
-
_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]
- Call
-
Session cache (Claude's discretion -- in-memory dict):
- Module-level
_options_cache: dict[str, OptionsChain] = {} clear_options_cache()function to reset cache
- Module-level
-
get_options_chain(symbol: str, min_dte: int = 0, max_dte: int = 50) -> str function (per D-03: pre-fetch all expirations):
- Cache stores
OptionsChainonly (_options_cache: dict[str, OptionsChain]). On hit forcache_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)
- Cache stores
-
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.
<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>