291 lines
15 KiB
Markdown
291 lines
15 KiB
Markdown
---
|
|
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", "make_tradier_request_with_retry", "TradierRateLimitError"]
|
|
- path: "tradingagents/dataflows/tradier.py"
|
|
provides: "Tradier vendor module with options chain retrieval"
|
|
exports: ["OptionsContract", "OptionsChain", "get_options_expirations", "get_options_chain", "get_options_chain_structured", "clear_options_cache"]
|
|
key_links:
|
|
- from: "tradingagents/dataflows/tradier.py"
|
|
to: "tradingagents/dataflows/tradier_common.py"
|
|
via: "imports make_tradier_request, TradierRateLimitError"
|
|
pattern: "from .tradier_common import"
|
|
---
|
|
|
|
<objective>
|
|
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.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<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
|
|
|
|
<interfaces>
|
|
<!-- Existing patterns the executor must follow -->
|
|
|
|
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."""
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Create Tradier common module (auth, HTTP helper, rate limit error)</name>
|
|
<files>tradingagents/dataflows/tradier_common.py</files>
|
|
<read_first>
|
|
- tradingagents/dataflows/alpha_vantage_common.py
|
|
- tradingagents/dataflows/config.py
|
|
</read_first>
|
|
<action>
|
|
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`
|
|
</action>
|
|
<verify>
|
|
<automated>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')"</automated>
|
|
</verify>
|
|
<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>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Create Tradier vendor module with typed dataclasses and chain retrieval</name>
|
|
<files>tradingagents/dataflows/tradier.py</files>
|
|
<read_first>
|
|
- tradingagents/dataflows/tradier_common.py
|
|
- tradingagents/dataflows/y_finance.py
|
|
- tradingagents/dataflows/alpha_vantage.py
|
|
</read_first>
|
|
<action>
|
|
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`
|
|
</action>
|
|
<verify>
|
|
<automated>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')"</automated>
|
|
</verify>
|
|
<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>
|
|
<done>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.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
- `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')"`
|
|
</verification>
|
|
|
|
<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>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/01-tradier-data-layer/01-01-SUMMARY.md`
|
|
</output>
|