test(01-02): add comprehensive unit tests for Tradier data layer
- 25 tests across 10 test classes covering all Phase 1 DATA requirements - TestGetExpirations (DATA-02): DTE filtering, single-item normalization (Pitfall 5) - TestGetOptionsChain (DATA-01): structure, values, single-contract normalization (Pitfall 2) - TestGreeksPresent (DATA-03): all 1st-order Greeks populated - TestGreeksAbsent (Pitfall 1): null Greeks handled gracefully - TestIVPresent (DATA-04): bid_iv, mid_iv, ask_iv, smv_vol values - TestDTEFilter (DATA-05): range filtering with expiration list update - TestVendorRegistration (DATA-08): VENDOR_LIST, TOOLS_CATEGORIES, VENDOR_METHODS - TestRateLimitDetection: HTTP 429 and X-Ratelimit-Available=0 - TestSessionCache: cache hit avoids redundant calls, clear forces re-fetch - TestSandboxURL: env var toggles production vs sandbox URL - All mocked, no real API calls; pytest added as dev dependency
This commit is contained in:
parent
18e1e99d46
commit
a249334f8d
|
|
@ -7,7 +7,7 @@ name = "tradingagents"
|
|||
version = "0.2.2"
|
||||
description = "TradingAgents: Multi-Agents LLM Financial Trading Framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"langchain-core>=0.3.81",
|
||||
"backtrader>=1.9.78.123",
|
||||
|
|
@ -40,3 +40,8 @@ include = ["tradingagents*", "cli*"]
|
|||
|
||||
[tool.setuptools.package-data]
|
||||
cli = ["static/*"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
import pytest
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,449 @@
|
|||
"""Comprehensive unit tests for Tradier data layer (Phase 1 requirements).
|
||||
|
||||
Covers: 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).
|
||||
|
||||
All tests use mocked API responses -- no real Tradier calls are made.
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import (
|
||||
_iso_days_out,
|
||||
MOCK_EXPIRATIONS_RESPONSE,
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_NO_GREEKS_RESPONSE,
|
||||
MOCK_SINGLE_CONTRACT_RESPONSE,
|
||||
)
|
||||
from tradingagents.dataflows.tradier import (
|
||||
get_options_expirations,
|
||||
get_options_chain,
|
||||
get_options_chain_structured,
|
||||
clear_options_cache,
|
||||
OptionsChain,
|
||||
OptionsContract,
|
||||
)
|
||||
from tradingagents.dataflows.tradier_common import (
|
||||
TradierRateLimitError,
|
||||
get_base_url,
|
||||
make_tradier_request,
|
||||
)
|
||||
from tradingagents.dataflows.interface import (
|
||||
TOOLS_CATEGORIES,
|
||||
VENDOR_LIST,
|
||||
VENDOR_METHODS,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetExpirations (DATA-02)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetExpirations:
|
||||
"""DATA-02: Options expirations retrieval with DTE filtering."""
|
||||
|
||||
def setup_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
def teardown_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_expirations_filtered_by_dte(self, mock_request):
|
||||
mock_request.return_value = MOCK_EXPIRATIONS_RESPONSE
|
||||
result = get_options_expirations("AAPL", 0, 50)
|
||||
assert isinstance(result, list)
|
||||
today = date.today()
|
||||
for d in result:
|
||||
from datetime import datetime
|
||||
exp = datetime.strptime(d, "%Y-%m-%d").date()
|
||||
dte = (exp - today).days
|
||||
assert 0 <= dte <= 50, f"Date {d} has DTE {dte}, outside 0-50 range"
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_expirations_returns_list_of_strings(self, mock_request):
|
||||
mock_request.return_value = MOCK_EXPIRATIONS_RESPONSE
|
||||
result = get_options_expirations("AAPL", 0, 50)
|
||||
assert len(result) == 5
|
||||
for d in result:
|
||||
assert isinstance(d, str)
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_single_expiration_normalized(self, mock_request):
|
||||
"""Pitfall 5: single-item response comes as string, not list."""
|
||||
mock_request.return_value = MOCK_SINGLE_EXPIRATION_RESPONSE
|
||||
result = get_options_expirations("AAPL", 0, 50)
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert result[0] == _iso_days_out(21)
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_expirations_narrow_dte_filter(self, mock_request):
|
||||
mock_request.return_value = MOCK_EXPIRATIONS_RESPONSE
|
||||
# Only dates between 10 and 25 DTE should pass
|
||||
result = get_options_expirations("AAPL", 10, 25)
|
||||
today = date.today()
|
||||
for d in result:
|
||||
from datetime import datetime
|
||||
exp = datetime.strptime(d, "%Y-%m-%d").date()
|
||||
dte = (exp - today).days
|
||||
assert 10 <= dte <= 25
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGetOptionsChain (DATA-01)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetOptionsChain:
|
||||
"""DATA-01: Options chain retrieval with correct structure."""
|
||||
|
||||
def setup_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
def teardown_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_chain_structure(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
MOCK_EXPIRATIONS_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
]
|
||||
chain = get_options_chain_structured("AAPL")
|
||||
assert chain.underlying == "AAPL"
|
||||
assert len(chain.contracts) > 0
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_chain_contract_values(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
MOCK_EXPIRATIONS_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
]
|
||||
chain = get_options_chain_structured("AAPL")
|
||||
c = chain.contracts[0]
|
||||
assert c.bid == 5.10
|
||||
assert c.volume == 1234
|
||||
assert c.open_interest == 5678
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_single_contract_normalization(self, mock_request):
|
||||
"""Pitfall 2: single contract comes as dict, not list."""
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_SINGLE_CONTRACT_RESPONSE,
|
||||
]
|
||||
chain = get_options_chain_structured("AAPL")
|
||||
assert len(chain.contracts) == 1
|
||||
assert chain.contracts[0].symbol == "AAPL260417C00170000"
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_chain_string_format(self, mock_request):
|
||||
"""get_options_chain returns string for LLM consumption."""
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
]
|
||||
result = get_options_chain("AAPL")
|
||||
assert isinstance(result, str)
|
||||
assert "AAPL" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGreeksPresent (DATA-03)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGreeksPresent:
|
||||
"""DATA-03: Greeks fields populated when present in API response."""
|
||||
|
||||
def setup_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
def teardown_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_greeks_values(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
]
|
||||
chain = get_options_chain_structured("AAPL")
|
||||
c = chain.contracts[0]
|
||||
assert c.delta == 0.55
|
||||
assert c.gamma == 0.04
|
||||
assert c.theta == -0.08
|
||||
assert c.vega == 0.25
|
||||
assert c.rho == 0.03
|
||||
assert c.greeks_updated_at == "2026-04-01 12:00:00"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestGreeksAbsent (Pitfall 1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGreeksAbsent:
|
||||
"""Pitfall 1: Greeks null in response should not crash."""
|
||||
|
||||
def setup_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
def teardown_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_no_greeks_yields_none(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_NO_GREEKS_RESPONSE,
|
||||
]
|
||||
chain = get_options_chain_structured("AAPL")
|
||||
c = chain.contracts[0]
|
||||
assert c.delta is None
|
||||
assert c.gamma is None
|
||||
assert c.theta is None
|
||||
assert c.vega is None
|
||||
assert c.rho is None
|
||||
assert c.greeks_updated_at is None
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_no_greeks_no_exception(self, mock_request):
|
||||
"""Parsing should succeed without raising."""
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_NO_GREEKS_RESPONSE,
|
||||
]
|
||||
# Should not raise
|
||||
chain = get_options_chain_structured("AAPL")
|
||||
assert len(chain.contracts) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestIVPresent (DATA-04)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestIVPresent:
|
||||
"""DATA-04: IV fields populated when present in API response."""
|
||||
|
||||
def setup_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
def teardown_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_iv_values(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
]
|
||||
chain = get_options_chain_structured("AAPL")
|
||||
c = chain.contracts[0]
|
||||
assert c.bid_iv == 0.28
|
||||
assert c.mid_iv == 0.29
|
||||
assert c.ask_iv == 0.30
|
||||
assert c.smv_vol == 0.285
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestDTEFilter (DATA-05)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDTEFilter:
|
||||
"""DATA-05: DTE-based filtering on OptionsChain."""
|
||||
|
||||
def test_filter_by_dte_range(self):
|
||||
today = date.today()
|
||||
contracts = [
|
||||
OptionsContract(
|
||||
symbol=f"TEST{i}", underlying="TEST", option_type="call",
|
||||
strike=100.0 + i * 5,
|
||||
expiration_date=(today + timedelta(days=dte)).isoformat(),
|
||||
bid=1.0, ask=1.5, last=1.25, volume=100, open_interest=500,
|
||||
)
|
||||
for i, dte in enumerate([5, 15, 25, 35, 55])
|
||||
]
|
||||
chain = OptionsChain(
|
||||
underlying="TEST",
|
||||
fetch_timestamp="2026-01-01T00:00:00",
|
||||
expirations=sorted({c.expiration_date for c in contracts}),
|
||||
contracts=contracts,
|
||||
)
|
||||
filtered = chain.filter_by_dte(10, 30)
|
||||
# Only 15 and 25 DTE should remain
|
||||
assert len(filtered.contracts) == 2
|
||||
for c in filtered.contracts:
|
||||
from datetime import datetime
|
||||
exp = datetime.strptime(c.expiration_date, "%Y-%m-%d").date()
|
||||
dte = (exp - today).days
|
||||
assert 10 <= dte <= 30
|
||||
|
||||
def test_filter_updates_expirations_list(self):
|
||||
today = date.today()
|
||||
exp_10 = (today + timedelta(days=10)).isoformat()
|
||||
exp_40 = (today + timedelta(days=40)).isoformat()
|
||||
contracts = [
|
||||
OptionsContract(
|
||||
symbol="A", underlying="TEST", option_type="call",
|
||||
strike=100.0, expiration_date=exp_10,
|
||||
bid=1.0, ask=1.5, last=1.25, volume=100, open_interest=500,
|
||||
),
|
||||
OptionsContract(
|
||||
symbol="B", underlying="TEST", option_type="call",
|
||||
strike=100.0, expiration_date=exp_40,
|
||||
bid=1.0, ask=1.5, last=1.25, volume=100, open_interest=500,
|
||||
),
|
||||
]
|
||||
chain = OptionsChain(
|
||||
underlying="TEST",
|
||||
fetch_timestamp="2026-01-01T00:00:00",
|
||||
expirations=[exp_10, exp_40],
|
||||
contracts=contracts,
|
||||
)
|
||||
filtered = chain.filter_by_dte(5, 20)
|
||||
assert exp_10 in filtered.expirations
|
||||
assert exp_40 not in filtered.expirations
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestVendorRegistration (DATA-08)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestVendorRegistration:
|
||||
"""DATA-08: Tradier registered in vendor routing system."""
|
||||
|
||||
def test_tradier_in_vendor_list(self):
|
||||
assert "tradier" in VENDOR_LIST
|
||||
|
||||
def test_options_chain_in_tools_categories(self):
|
||||
assert "options_chain" in TOOLS_CATEGORIES
|
||||
tools = TOOLS_CATEGORIES["options_chain"]["tools"]
|
||||
assert "get_options_chain" in tools
|
||||
assert "get_options_expirations" in tools
|
||||
|
||||
def test_get_options_chain_in_vendor_methods(self):
|
||||
assert "get_options_chain" in VENDOR_METHODS
|
||||
assert "tradier" in VENDOR_METHODS["get_options_chain"]
|
||||
|
||||
def test_get_options_expirations_in_vendor_methods(self):
|
||||
assert "get_options_expirations" in VENDOR_METHODS
|
||||
assert "tradier" in VENDOR_METHODS["get_options_expirations"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestRateLimitDetection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRateLimitDetection:
|
||||
"""Rate limit detection raises TradierRateLimitError."""
|
||||
|
||||
@patch("tradingagents.dataflows.tradier_common.get_api_key", return_value="test-key")
|
||||
@patch("tradingagents.dataflows.tradier_common.requests.get")
|
||||
def test_http_429_raises(self, mock_get, mock_key):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 429
|
||||
mock_response.headers = {}
|
||||
mock_get.return_value = mock_response
|
||||
with pytest.raises(TradierRateLimitError, match="429"):
|
||||
make_tradier_request("/v1/markets/options/chains")
|
||||
|
||||
@patch("tradingagents.dataflows.tradier_common.get_api_key", return_value="test-key")
|
||||
@patch("tradingagents.dataflows.tradier_common.requests.get")
|
||||
def test_ratelimit_available_zero_raises(self, mock_get, mock_key):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.headers = {
|
||||
"X-Ratelimit-Available": "0",
|
||||
"X-Ratelimit-Expiry": "1700000000",
|
||||
}
|
||||
mock_get.return_value = mock_response
|
||||
with pytest.raises(TradierRateLimitError, match="exhausted"):
|
||||
make_tradier_request("/v1/markets/options/chains")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestSessionCache
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSessionCache:
|
||||
"""Session cache avoids redundant API calls."""
|
||||
|
||||
def setup_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
def teardown_method(self):
|
||||
clear_options_cache()
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_cache_hit_avoids_second_call(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
# No more responses -- second call should hit cache
|
||||
]
|
||||
# First call
|
||||
get_options_chain_structured("AAPL")
|
||||
call_count_after_first = mock_request.call_count
|
||||
|
||||
# Second call (should be cache hit)
|
||||
get_options_chain_structured("AAPL")
|
||||
assert mock_request.call_count == call_count_after_first
|
||||
|
||||
@patch("tradingagents.dataflows.tradier.make_tradier_request_with_retry")
|
||||
def test_cache_clear_forces_new_call(self, mock_request):
|
||||
mock_request.side_effect = [
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
# After cache clear:
|
||||
MOCK_SINGLE_EXPIRATION_RESPONSE,
|
||||
MOCK_CHAIN_RESPONSE,
|
||||
]
|
||||
get_options_chain_structured("AAPL")
|
||||
call_count_before_clear = mock_request.call_count
|
||||
|
||||
clear_options_cache()
|
||||
get_options_chain_structured("AAPL")
|
||||
assert mock_request.call_count > call_count_before_clear
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TestSandboxURL
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSandboxURL:
|
||||
"""Sandbox URL configuration via TRADIER_SANDBOX env var."""
|
||||
|
||||
@patch.dict("os.environ", {"TRADIER_SANDBOX": "true"})
|
||||
def test_sandbox_true_returns_sandbox_url(self):
|
||||
assert get_base_url() == "https://sandbox.tradier.com"
|
||||
|
||||
@patch.dict("os.environ", {"TRADIER_SANDBOX": "false"})
|
||||
def test_sandbox_false_returns_production_url(self):
|
||||
assert get_base_url() == "https://api.tradier.com"
|
||||
|
||||
@patch.dict("os.environ", {}, clear=True)
|
||||
def test_unset_returns_production_url(self):
|
||||
assert get_base_url() == "https://api.tradier.com"
|
||||
Loading…
Reference in New Issue