TradingAgents/tests/integration/dataflows/test_fred_integration.py

561 lines
22 KiB
Python

"""
Test suite for FRED API Integration Tests.
This module tests:
1. End-to-end API integration with real fredapi library
2. Cache integration with real filesystem
3. Retry logic with simulated network failures
4. Rate limit handling with backoff
5. Multiple series retrieval in sequence
6. Data transformation and formatting
Test Coverage:
- Integration tests with mocked fredapi library (not real FRED API)
- Cache persistence and retrieval
- Error recovery and retry mechanisms
- Multi-function workflows
- Real-world usage patterns
"""
import pytest
import pandas as pd
import time
import os
import sys
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock, call
from datetime import datetime, timedelta
pytestmark = pytest.mark.integration
# Mock fredapi before importing FRED modules
if 'fredapi' in sys.modules:
del sys.modules['fredapi']
mock_fredapi = MagicMock()
sys.modules['fredapi'] = mock_fredapi
# ============================================================================
# Fixtures
# ============================================================================
@pytest.fixture
def mock_fred_api_key():
"""Mock FRED_API_KEY environment variable."""
with patch.dict('os.environ', {'FRED_API_KEY': 'integration_test_key'}):
yield 'integration_test_key'
@pytest.fixture
def integration_cache_dir(tmp_path):
"""Create temporary cache directory for integration tests."""
cache_dir = tmp_path / ".cache" / "fred"
cache_dir.mkdir(parents=True, exist_ok=True)
with patch('tradingagents.dataflows.fred_common.CACHE_DIR', cache_dir):
yield cache_dir
@pytest.fixture
def sample_fred_series():
"""Create sample FRED series data for multiple indicators."""
return {
'FEDFUNDS': pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=30, freq='D'),
'value': [5.33 + i * 0.01 for i in range(30)],
}),
'DGS10': pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=30, freq='D'),
'value': [4.25 + i * 0.01 for i in range(30)],
}),
'DGS2': pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=30, freq='D'),
'value': [4.05 + i * 0.01 for i in range(30)],
}),
'M2SL': pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=12, freq='ME'),
'value': [21000.0 + i * 50.0 for i in range(12)],
}),
'GDP': pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=4, freq='QE'),
'value': [27000.0 + i * 200.0 for i in range(4)],
}),
'CPIAUCSL': pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=12, freq='ME'),
'value': [308.0 + i for i in range(12)],
}),
'UNRATE': pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=12, freq='ME'),
'value': [3.7 + i * 0.1 for i in range(12)],
}),
}
@pytest.fixture
def mock_fred_client(sample_fred_series):
"""Mock fredapi.Fred client with sample data."""
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
def get_series_side_effect(series_id, observation_start=None, observation_end=None):
if series_id in sample_fred_series:
return sample_fred_series[series_id].copy()
else:
raise Exception(f"Series not found: {series_id}")
mock_client.get_series.side_effect = get_series_side_effect
mock_fred_class.return_value = mock_client
yield mock_client
# ============================================================================
# Test End-to-End Integration
# ============================================================================
class TestEndToEndIntegration:
"""Test complete end-to-end workflows."""
def test_get_interest_rates_end_to_end(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test complete interest rate retrieval workflow."""
from tradingagents.dataflows.fred import get_interest_rates
result = get_interest_rates()
assert isinstance(result, pd.DataFrame)
assert len(result) == 30
assert 'date' in result.columns
assert 'value' in result.columns
assert result['value'].iloc[0] == 5.33
# Verify API was called
mock_fred_client.get_series.assert_called_once()
def test_get_treasury_rates_end_to_end(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test complete treasury rate retrieval workflow."""
from tradingagents.dataflows.fred import get_treasury_rates
result = get_treasury_rates(maturity='10Y')
assert isinstance(result, pd.DataFrame)
assert len(result) == 30
assert result['value'].iloc[0] == 4.25
def test_get_money_supply_end_to_end(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test complete M2 money supply retrieval workflow."""
from tradingagents.dataflows.fred import get_money_supply
result = get_money_supply()
assert isinstance(result, pd.DataFrame)
assert len(result) == 12
assert result['value'].iloc[0] == 21000.0
def test_get_gdp_end_to_end(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test complete GDP retrieval workflow."""
from tradingagents.dataflows.fred import get_gdp
result = get_gdp()
assert isinstance(result, pd.DataFrame)
assert len(result) == 4
assert result['value'].iloc[0] == 27000.0
def test_get_inflation_end_to_end(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test complete CPI/inflation retrieval workflow."""
from tradingagents.dataflows.fred import get_inflation
result = get_inflation()
assert isinstance(result, pd.DataFrame)
assert len(result) == 12
assert result['value'].iloc[0] == 308.0
def test_get_unemployment_end_to_end(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test complete unemployment rate retrieval workflow."""
from tradingagents.dataflows.fred import get_unemployment
result = get_unemployment()
assert isinstance(result, pd.DataFrame)
assert len(result) == 12
assert result['value'].iloc[0] == 3.7
# ============================================================================
# Test Cache Integration
# ============================================================================
@pytest.mark.skip(reason="Cache integration not yet implemented in fred_common._make_fred_request")
class TestCacheIntegration:
"""Test cache persistence and retrieval in real filesystem."""
def test_cache_saves_to_disk(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that data is saved to cache on first request."""
from tradingagents.dataflows.fred import get_interest_rates
# First request should save to cache
result = get_interest_rates()
# Verify cache file exists
cache_file = integration_cache_dir / 'FEDFUNDS.parquet'
assert cache_file.exists()
# Verify cached data
cached_df = pd.read_parquet(cache_file)
assert len(cached_df) == 30
def test_cache_is_used_on_second_request(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that cached data is used on subsequent requests."""
from tradingagents.dataflows.fred import get_interest_rates
# First request
result1 = get_interest_rates()
call_count_after_first = mock_fred_client.get_series.call_count
# Second request should use cache
result2 = get_interest_rates()
call_count_after_second = mock_fred_client.get_series.call_count
# API should only be called once (first request)
assert call_count_after_second == call_count_after_first
# Results should be identical
pd.testing.assert_frame_equal(result1, result2)
def test_cache_respects_different_date_ranges(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that different date ranges create separate cache entries."""
from tradingagents.dataflows.fred import get_interest_rates
# Request with different date ranges
result1 = get_interest_rates(start_date='2024-01-01', end_date='2024-06-30')
result2 = get_interest_rates(start_date='2024-07-01', end_date='2024-12-31')
# Should make two separate API calls
assert mock_fred_client.get_series.call_count == 2
# Should create two separate cache files
cache_files = list(integration_cache_dir.glob('FEDFUNDS*.parquet'))
assert len(cache_files) >= 1 # At least one cache file
def test_expired_cache_triggers_refresh(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that expired cache is refreshed with new API call."""
from tradingagents.dataflows.fred import get_interest_rates
# First request
result1 = get_interest_rates()
# Make cache file old (25 hours)
cache_file = integration_cache_dir / 'FEDFUNDS.parquet'
old_time = time.time() - (25 * 3600)
os.utime(cache_file, (old_time, old_time))
# Reset mock call count
mock_fred_client.get_series.reset_mock()
# Second request should refresh cache
result2 = get_interest_rates()
# API should be called again
assert mock_fred_client.get_series.call_count == 1
def test_cache_directory_created_if_missing(self, mock_fred_api_key, mock_fred_client, tmp_path):
"""Test that cache directory is created if it doesn't exist."""
from tradingagents.dataflows.fred import get_interest_rates
new_cache_dir = tmp_path / "new_cache" / "fred"
with patch('tradingagents.dataflows.fred_common.CACHE_DIR', new_cache_dir):
result = get_interest_rates()
assert new_cache_dir.exists()
assert (new_cache_dir / 'FEDFUNDS.parquet').exists()
# ============================================================================
# Test Retry Logic Integration
# ============================================================================
class TestRetryIntegration:
"""Test retry logic with simulated failures."""
@patch('tradingagents.dataflows.fred_common.time.sleep')
def test_retry_on_temporary_network_error(self, mock_sleep, mock_fred_api_key, integration_cache_dir, sample_fred_series):
"""Test successful retry after temporary network error."""
from tradingagents.dataflows.fred import get_interest_rates
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
# First call fails, second succeeds
mock_client.get_series.side_effect = [
Exception("Connection timeout"),
sample_fred_series['FEDFUNDS']
]
mock_fred_class.return_value = mock_client
result = get_interest_rates()
assert isinstance(result, pd.DataFrame)
assert len(result) == 30
# Should have retried once
assert mock_client.get_series.call_count == 2
assert mock_sleep.call_count == 1
@patch('tradingagents.dataflows.fred_common.time.sleep')
def test_retry_exhaustion_returns_error(self, mock_sleep, mock_fred_api_key, integration_cache_dir):
"""Test that exhausted retries return error string."""
from tradingagents.dataflows.fred import get_interest_rates
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
mock_client.get_series.side_effect = Exception("Persistent error")
mock_fred_class.return_value = mock_client
result = get_interest_rates()
assert isinstance(result, str)
assert "error" in result.lower()
# Should retry max times
assert mock_client.get_series.call_count >= 3
@patch('tradingagents.dataflows.fred_common.time.sleep')
def test_exponential_backoff_timing(self, mock_sleep, mock_fred_api_key, integration_cache_dir):
"""Test that retry uses exponential backoff."""
from tradingagents.dataflows.fred import get_interest_rates
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
mock_client.get_series.side_effect = Exception("Error")
mock_fred_class.return_value = mock_client
result = get_interest_rates()
# Check exponential backoff: 1s, 2s, 4s
if mock_sleep.call_count >= 3:
sleep_times = [call.args[0] for call in mock_sleep.call_args_list[:3]]
assert sleep_times == [1, 2, 4]
# ============================================================================
# Test Rate Limit Handling
# ============================================================================
class TestRateLimitIntegration:
"""Test rate limit error handling and recovery."""
def test_rate_limit_error_returns_error_string(self, mock_fred_api_key, integration_cache_dir):
"""Test that rate limit error returns error string."""
from tradingagents.dataflows.fred import get_interest_rates
from tradingagents.dataflows.fred_common import FredRateLimitError
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
mock_client.get_series.side_effect = FredRateLimitError("Rate limit exceeded", retry_after=60)
mock_fred_class.return_value = mock_client
result = get_interest_rates()
assert isinstance(result, str)
assert "rate limit" in result.lower()
@patch('tradingagents.dataflows.fred_common.time.sleep')
def test_rate_limit_with_retry_after(self, mock_sleep, mock_fred_api_key, integration_cache_dir, sample_fred_series):
"""Test rate limit with retry-after header."""
from tradingagents.dataflows.fred import get_interest_rates
from tradingagents.dataflows.fred_common import FredRateLimitError
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
# First call rate limited, second succeeds
mock_client.get_series.side_effect = [
FredRateLimitError("Rate limit", retry_after=5),
sample_fred_series['FEDFUNDS']
]
mock_fred_class.return_value = mock_client
result = get_interest_rates()
# Should respect retry-after (5 seconds)
if mock_sleep.call_count > 0:
assert 5 in [call.args[0] for call in mock_sleep.call_args_list]
# ============================================================================
# Test Multi-Function Workflows
# ============================================================================
class TestMultiFunctionWorkflows:
"""Test realistic workflows using multiple functions."""
def test_retrieve_multiple_indicators(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test retrieving multiple economic indicators in sequence."""
from tradingagents.dataflows.fred import (
get_interest_rates,
get_treasury_rates,
get_money_supply,
get_gdp,
get_inflation,
get_unemployment
)
# Retrieve all indicators
fed_funds = get_interest_rates()
treasury_10y = get_treasury_rates(maturity='10Y')
m2_supply = get_money_supply()
gdp = get_gdp()
cpi = get_inflation()
unemployment = get_unemployment()
# All should succeed
assert isinstance(fed_funds, pd.DataFrame)
assert isinstance(treasury_10y, pd.DataFrame)
assert isinstance(m2_supply, pd.DataFrame)
assert isinstance(gdp, pd.DataFrame)
assert isinstance(cpi, pd.DataFrame)
assert isinstance(unemployment, pd.DataFrame)
# Verify data
assert len(fed_funds) == 30
assert len(treasury_10y) == 30
assert len(m2_supply) == 12
assert len(gdp) == 4
assert len(cpi) == 12
assert len(unemployment) == 12
def test_economic_dashboard_workflow(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test realistic economic dashboard data retrieval."""
from tradingagents.dataflows.fred import (
get_interest_rates,
get_treasury_rates,
get_inflation,
get_unemployment
)
# Dashboard data with date range
start = '2024-01-01'
end = '2024-12-31'
dashboard_data = {
'fed_funds': get_interest_rates(start_date=start, end_date=end),
'treasury_2y': get_treasury_rates(maturity='2Y', start_date=start, end_date=end),
'treasury_10y': get_treasury_rates(maturity='10Y', start_date=start, end_date=end),
'cpi': get_inflation(start_date=start, end_date=end),
'unemployment': get_unemployment(start_date=start, end_date=end),
}
# All should be DataFrames
for key, value in dashboard_data.items():
assert isinstance(value, pd.DataFrame), f"{key} should be DataFrame"
assert len(value) > 0, f"{key} should have data"
def test_time_series_analysis_workflow(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test workflow for time series analysis with multiple series."""
from tradingagents.dataflows.fred import get_fred_series
# Retrieve multiple custom series
series_ids = ['FEDFUNDS', 'DGS10', 'UNRATE']
results = {}
for series_id in series_ids:
results[series_id] = get_fred_series(series_id)
# Verify all retrieved successfully
for series_id, df in results.items():
assert isinstance(df, pd.DataFrame), f"{series_id} should be DataFrame"
assert len(df) > 0, f"{series_id} should have data"
# ============================================================================
# Test Error Recovery Integration
# ============================================================================
class TestErrorRecoveryIntegration:
"""Test error handling and recovery mechanisms."""
def test_invalid_series_recovers_gracefully(self, mock_fred_api_key, integration_cache_dir):
"""Test that invalid series returns error string without crashing."""
from tradingagents.dataflows.fred import get_fred_series
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
mock_client.get_series.side_effect = Exception("Series not found")
mock_fred_class.return_value = mock_client
result = get_fred_series('INVALID_SERIES')
assert isinstance(result, str)
assert "error" in result.lower()
def test_missing_api_key_returns_error(self, integration_cache_dir):
"""Test that missing API key returns error string."""
from tradingagents.dataflows.fred import get_interest_rates
with patch.dict('os.environ', {}, clear=True):
result = get_interest_rates()
assert isinstance(result, str)
assert "error" in result.lower() or "api key" in result.lower()
def test_empty_data_returns_empty_dataframe(self, mock_fred_api_key, integration_cache_dir):
"""Test that empty data is handled gracefully."""
from tradingagents.dataflows.fred import get_interest_rates
with patch('tradingagents.dataflows.fred_common.Fred') as mock_fred_class:
mock_client = MagicMock()
mock_client.get_series.return_value = pd.DataFrame()
mock_fred_class.return_value = mock_client
result = get_interest_rates()
assert isinstance(result, pd.DataFrame)
assert len(result) == 0
# ============================================================================
# Test Data Transformation Integration
# ============================================================================
class TestDataTransformationIntegration:
"""Test data formatting and transformation."""
def test_date_column_formatting(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that date column is properly formatted."""
from tradingagents.dataflows.fred import get_interest_rates
result = get_interest_rates()
assert 'date' in result.columns
assert pd.api.types.is_datetime64_any_dtype(result['date'])
def test_value_column_numeric(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that value column is numeric."""
from tradingagents.dataflows.fred import get_interest_rates
result = get_interest_rates()
assert 'value' in result.columns
assert pd.api.types.is_numeric_dtype(result['value'])
def test_no_null_values_in_valid_data(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that valid data has no null values."""
from tradingagents.dataflows.fred import get_interest_rates
result = get_interest_rates()
assert result['date'].notna().all()
assert result['value'].notna().all()
def test_data_sorted_by_date(self, mock_fred_api_key, mock_fred_client, integration_cache_dir):
"""Test that data is sorted by date."""
from tradingagents.dataflows.fred import get_interest_rates
result = get_interest_rates()
# Check if dates are in ascending order
assert result['date'].is_monotonic_increasing