TradingAgents/tests/integration/test_finnhub_live.py

447 lines
16 KiB
Python

"""Live integration tests for the Finnhub dataflow modules.
These tests make REAL HTTP requests to the Finnhub API and therefore require
a valid ``FINNHUB_API_KEY`` environment variable. When the key is absent the
entire module is skipped automatically.
## Free-tier vs paid-tier endpoints (confirmed by live testing 2026-03-18)
FREE TIER (60 calls/min):
/quote ✅ get_quote, market movers/indices/sectors
/stock/profile2 ✅ get_company_profile
/stock/metric ✅ get_basic_financials
/company-news ✅ get_company_news
/news ✅ get_market_news, get_topic_news
/stock/insider-transactions ✅ get_insider_transactions
PAID TIER (returns HTTP 403):
/stock/candle ❌ get_stock_candles
/financials-reported ❌ get_financial_statements (XBRL as-filed)
/indicator ❌ get_indicator_finnhub (SMA, EMA, MACD, RSI, BBANDS, ATR)
Run only the live tests:
FINNHUB_API_KEY=<your_key> pytest tests/test_finnhub_live_integration.py -v -m integration
Run only free-tier tests:
FINNHUB_API_KEY=<your_key> pytest tests/test_finnhub_live_integration.py -v -m "integration and not paid_tier"
Skip them in CI (default behaviour when the env var is not set):
pytest tests/ -v # live tests auto-skip
"""
import os
import pytest
# ---------------------------------------------------------------------------
# Global skip guard — skip every test in this file if no API key is present.
# ---------------------------------------------------------------------------
pytestmark = pytest.mark.integration
_FINNHUB_API_KEY = os.environ.get("FINNHUB_API_KEY", "")
_skip_if_no_key = pytest.mark.skipif(
not _FINNHUB_API_KEY,
reason="FINNHUB_API_KEY env var not set — skipping live Finnhub tests",
)
# Mark tests that require a paid Finnhub subscription (confirmed HTTP 403 on free tier)
_paid_tier = pytest.mark.paid_tier
# Stable, well-covered symbol used across all tests
_SYMBOL = "AAPL"
_START_DATE = "2024-01-02"
_END_DATE = "2024-01-05"
# ---------------------------------------------------------------------------
# 1. finnhub_common
# ---------------------------------------------------------------------------
@_skip_if_no_key
class TestLiveGetApiKey:
"""Live smoke tests for the key-retrieval helper."""
def test_returns_non_empty_string(self):
from tradingagents.dataflows.finnhub_common import get_api_key
key = get_api_key()
assert isinstance(key, str)
assert len(key) > 0
@_skip_if_no_key
class TestLiveMakeApiRequest:
"""Live smoke test for the HTTP request helper."""
def test_quote_endpoint_returns_dict(self):
from tradingagents.dataflows.finnhub_common import _make_api_request
result = _make_api_request("quote", {"symbol": _SYMBOL})
assert isinstance(result, dict)
# Finnhub quote always returns these keys
assert "c" in result # current price
assert "pc" in result # previous close
# ---------------------------------------------------------------------------
# 2. finnhub_stock
# ---------------------------------------------------------------------------
@_skip_if_no_key
@_paid_tier
@pytest.mark.skip(reason="Requires paid Finnhub tier — /stock/candle returns HTTP 403 on free tier")
class TestLiveGetStockCandles:
"""Live smoke tests for OHLCV candle retrieval (PAID TIER ONLY)."""
def test_returns_csv_string(self):
from tradingagents.dataflows.finnhub_stock import get_stock_candles
result = get_stock_candles(_SYMBOL, _START_DATE, _END_DATE)
assert isinstance(result, str)
def test_csv_has_header_row(self):
from tradingagents.dataflows.finnhub_stock import get_stock_candles
result = get_stock_candles(_SYMBOL, _START_DATE, _END_DATE)
first_line = result.strip().split("\n")[0]
assert first_line == "timestamp,open,high,low,close,volume"
def test_csv_contains_data_rows(self):
from tradingagents.dataflows.finnhub_stock import get_stock_candles
result = get_stock_candles(_SYMBOL, _START_DATE, _END_DATE)
lines = [l for l in result.strip().split("\n") if l]
# At minimum the header + at least one trading day
assert len(lines) >= 2
def test_invalid_symbol_raises_finnhub_error(self):
from tradingagents.dataflows.finnhub_common import FinnhubError
from tradingagents.dataflows.finnhub_stock import get_stock_candles
with pytest.raises(FinnhubError):
get_stock_candles("ZZZZZ_INVALID_TICKER", _START_DATE, _END_DATE)
@_skip_if_no_key
class TestLiveGetQuote:
"""Live smoke tests for real-time quote retrieval."""
def test_returns_dict_with_expected_keys(self):
from tradingagents.dataflows.finnhub_stock import get_quote
result = get_quote(_SYMBOL)
expected_keys = {
"symbol", "current_price", "change", "change_percent",
"high", "low", "open", "prev_close", "timestamp",
}
assert expected_keys == set(result.keys())
def test_symbol_field_matches_requested_symbol(self):
from tradingagents.dataflows.finnhub_stock import get_quote
result = get_quote(_SYMBOL)
assert result["symbol"] == _SYMBOL
def test_current_price_is_positive_float(self):
from tradingagents.dataflows.finnhub_stock import get_quote
result = get_quote(_SYMBOL)
assert isinstance(result["current_price"], float)
assert result["current_price"] > 0
# ---------------------------------------------------------------------------
# 3. finnhub_fundamentals
# ---------------------------------------------------------------------------
@_skip_if_no_key
class TestLiveGetCompanyProfile:
"""Live smoke tests for company profile retrieval."""
def test_returns_non_empty_string(self):
from tradingagents.dataflows.finnhub_fundamentals import get_company_profile
result = get_company_profile(_SYMBOL)
assert isinstance(result, str)
assert len(result) > 0
def test_output_contains_symbol(self):
from tradingagents.dataflows.finnhub_fundamentals import get_company_profile
result = get_company_profile(_SYMBOL)
assert _SYMBOL in result
def test_output_contains_company_name(self):
from tradingagents.dataflows.finnhub_fundamentals import get_company_profile
result = get_company_profile(_SYMBOL)
# Apple appears under various name variants; just check 'Apple' is present
assert "Apple" in result
@_skip_if_no_key
@_paid_tier
@pytest.mark.skip(reason="Requires paid Finnhub tier — /financials-reported returns HTTP 403 on free tier")
class TestLiveGetFinancialStatements:
"""Live smoke tests for XBRL as-filed financial statements (PAID TIER ONLY)."""
def test_income_statement_returns_non_empty_string(self):
from tradingagents.dataflows.finnhub_fundamentals import get_financial_statements
result = get_financial_statements(_SYMBOL, "income_statement", "quarterly")
assert isinstance(result, str)
assert len(result) > 0
def test_balance_sheet_returns_string(self):
from tradingagents.dataflows.finnhub_fundamentals import get_financial_statements
result = get_financial_statements(_SYMBOL, "balance_sheet", "quarterly")
assert isinstance(result, str)
def test_cash_flow_returns_string(self):
from tradingagents.dataflows.finnhub_fundamentals import get_financial_statements
result = get_financial_statements(_SYMBOL, "cash_flow", "quarterly")
assert isinstance(result, str)
def test_output_contains_symbol(self):
from tradingagents.dataflows.finnhub_fundamentals import get_financial_statements
result = get_financial_statements(_SYMBOL, "income_statement", "quarterly")
assert _SYMBOL in result
@_skip_if_no_key
class TestLiveGetBasicFinancials:
"""Live smoke tests for key financial metrics retrieval."""
def test_returns_non_empty_string(self):
from tradingagents.dataflows.finnhub_fundamentals import get_basic_financials
result = get_basic_financials(_SYMBOL)
assert isinstance(result, str)
assert len(result) > 0
def test_output_contains_valuation_section(self):
from tradingagents.dataflows.finnhub_fundamentals import get_basic_financials
result = get_basic_financials(_SYMBOL)
assert "Valuation" in result
def test_output_contains_pe_metric(self):
from tradingagents.dataflows.finnhub_fundamentals import get_basic_financials
result = get_basic_financials(_SYMBOL)
assert "P/E" in result
# ---------------------------------------------------------------------------
# 4. finnhub_news
# ---------------------------------------------------------------------------
@_skip_if_no_key
class TestLiveGetCompanyNews:
"""Live smoke tests for company-specific news retrieval."""
def test_returns_non_empty_string(self):
from tradingagents.dataflows.finnhub_news import get_company_news
result = get_company_news(_SYMBOL, _START_DATE, _END_DATE)
assert isinstance(result, str)
assert len(result) > 0
def test_output_contains_symbol(self):
from tradingagents.dataflows.finnhub_news import get_company_news
result = get_company_news(_SYMBOL, _START_DATE, _END_DATE)
assert _SYMBOL in result
@_skip_if_no_key
class TestLiveGetMarketNews:
"""Live smoke tests for broad market news retrieval."""
def test_general_news_returns_string(self):
from tradingagents.dataflows.finnhub_news import get_market_news
result = get_market_news("general")
assert isinstance(result, str)
assert len(result) > 0
def test_output_contains_market_news_header(self):
from tradingagents.dataflows.finnhub_news import get_market_news
result = get_market_news("general")
assert "Market News" in result
def test_crypto_category_accepted(self):
from tradingagents.dataflows.finnhub_news import get_market_news
result = get_market_news("crypto")
assert isinstance(result, str)
@_skip_if_no_key
class TestLiveGetInsiderTransactions:
"""Live smoke tests for insider transaction retrieval."""
def test_returns_non_empty_string(self):
from tradingagents.dataflows.finnhub_news import get_insider_transactions
result = get_insider_transactions(_SYMBOL)
assert isinstance(result, str)
assert len(result) > 0
def test_output_contains_symbol(self):
from tradingagents.dataflows.finnhub_news import get_insider_transactions
result = get_insider_transactions(_SYMBOL)
assert _SYMBOL in result
# ---------------------------------------------------------------------------
# 5. finnhub_scanner
# ---------------------------------------------------------------------------
@_skip_if_no_key
class TestLiveGetMarketMovers:
"""Live smoke tests for market movers (may be slow — fetches 50 quotes)."""
def test_gainers_returns_markdown_table(self):
from tradingagents.dataflows.finnhub_scanner import get_market_movers_finnhub
result = get_market_movers_finnhub("gainers")
assert isinstance(result, str)
assert "Symbol" in result or "symbol" in result.lower()
def test_losers_returns_markdown_table(self):
from tradingagents.dataflows.finnhub_scanner import get_market_movers_finnhub
result = get_market_movers_finnhub("losers")
assert isinstance(result, str)
def test_active_returns_markdown_table(self):
from tradingagents.dataflows.finnhub_scanner import get_market_movers_finnhub
result = get_market_movers_finnhub("active")
assert isinstance(result, str)
@_skip_if_no_key
class TestLiveGetMarketIndices:
"""Live smoke tests for market index levels."""
def test_returns_string_with_indices_header(self):
from tradingagents.dataflows.finnhub_scanner import get_market_indices_finnhub
result = get_market_indices_finnhub()
assert isinstance(result, str)
assert "Major Market Indices" in result
def test_output_contains_sp500_proxy(self):
from tradingagents.dataflows.finnhub_scanner import get_market_indices_finnhub
result = get_market_indices_finnhub()
assert "SPY" in result or "S&P 500" in result
@_skip_if_no_key
class TestLiveGetSectorPerformance:
"""Live smoke tests for sector performance."""
def test_returns_sector_performance_string(self):
from tradingagents.dataflows.finnhub_scanner import get_sector_performance_finnhub
result = get_sector_performance_finnhub()
assert isinstance(result, str)
assert "Sector Performance" in result
def test_output_contains_at_least_one_etf(self):
from tradingagents.dataflows.finnhub_scanner import get_sector_performance_finnhub
result = get_sector_performance_finnhub()
etf_tickers = {"XLK", "XLV", "XLF", "XLE", "XLY", "XLP", "XLI", "XLB", "XLRE", "XLU", "XLC"}
assert any(t in result for t in etf_tickers)
@_skip_if_no_key
class TestLiveGetTopicNews:
"""Live smoke tests for topic-based news."""
def test_market_topic_returns_string(self):
from tradingagents.dataflows.finnhub_scanner import get_topic_news_finnhub
result = get_topic_news_finnhub("market")
assert isinstance(result, str)
def test_crypto_topic_returns_string(self):
from tradingagents.dataflows.finnhub_scanner import get_topic_news_finnhub
result = get_topic_news_finnhub("crypto")
assert isinstance(result, str)
assert "crypto" in result.lower()
# ---------------------------------------------------------------------------
# 6. finnhub_indicators
# ---------------------------------------------------------------------------
@_skip_if_no_key
@_paid_tier
@pytest.mark.skip(reason="Requires paid Finnhub tier — /indicator returns HTTP 403 on free tier")
class TestLiveGetIndicatorFinnhub:
"""Live smoke tests for technical indicators (PAID TIER ONLY)."""
def test_rsi_returns_string(self):
from tradingagents.dataflows.finnhub_indicators import get_indicator_finnhub
result = get_indicator_finnhub(_SYMBOL, "rsi", "2023-10-01", _END_DATE)
assert isinstance(result, str)
assert "RSI" in result
def test_macd_returns_string_with_columns(self):
from tradingagents.dataflows.finnhub_indicators import get_indicator_finnhub
result = get_indicator_finnhub(_SYMBOL, "macd", "2023-10-01", _END_DATE)
assert isinstance(result, str)
assert "MACD" in result
assert "Signal" in result
def test_bbands_returns_string_with_band_columns(self):
from tradingagents.dataflows.finnhub_indicators import get_indicator_finnhub
result = get_indicator_finnhub(_SYMBOL, "bbands", "2023-10-01", _END_DATE)
assert isinstance(result, str)
assert "Upper" in result
assert "Lower" in result
def test_sma_returns_string(self):
from tradingagents.dataflows.finnhub_indicators import get_indicator_finnhub
result = get_indicator_finnhub(_SYMBOL, "sma", "2023-10-01", _END_DATE, time_period=20)
assert isinstance(result, str)
assert "SMA" in result
def test_ema_returns_string(self):
from tradingagents.dataflows.finnhub_indicators import get_indicator_finnhub
result = get_indicator_finnhub(_SYMBOL, "ema", "2023-10-01", _END_DATE, time_period=12)
assert isinstance(result, str)
assert "EMA" in result
def test_atr_returns_string(self):
from tradingagents.dataflows.finnhub_indicators import get_indicator_finnhub
result = get_indicator_finnhub(_SYMBOL, "atr", "2023-10-01", _END_DATE, time_period=14)
assert isinstance(result, str)
assert "ATR" in result