TradingAgents/tests/unit/test_api_usage.py

325 lines
14 KiB
Python

"""Tests for tradingagents/api_usage.py — API consumption estimation."""
import pytest
from tradingagents.api_usage import (
AV_FREE_DAILY_LIMIT,
AV_PREMIUM_PER_MINUTE,
UsageEstimate,
VendorEstimate,
estimate_analyze,
estimate_pipeline,
estimate_scan,
format_av_assessment,
format_estimate,
format_vendor_breakdown,
)
# ──────────────────────────────────────────────────────────────────────────────
# VendorEstimate
# ──────────────────────────────────────────────────────────────────────────────
class TestVendorEstimate:
def test_total(self):
ve = VendorEstimate(yfinance=10, alpha_vantage=5, finnhub=2, finviz=1)
assert ve.total == 18
def test_default_zeros(self):
ve = VendorEstimate()
assert ve.total == 0
# ──────────────────────────────────────────────────────────────────────────────
# UsageEstimate
# ──────────────────────────────────────────────────────────────────────────────
class TestUsageEstimate:
def test_av_fits_free_tier_true(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=10),
)
assert est.av_fits_free_tier() is True
def test_av_fits_free_tier_false(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=100),
)
assert est.av_fits_free_tier() is False
def test_av_daily_runs_free(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=5),
)
assert est.av_daily_runs_free() == AV_FREE_DAILY_LIMIT // 5
def test_av_daily_runs_free_zero_av(self):
est = UsageEstimate(
command="test",
description="test",
vendor_calls=VendorEstimate(alpha_vantage=0),
)
assert est.av_daily_runs_free() == -1 # unlimited
# ──────────────────────────────────────────────────────────────────────────────
# estimate_analyze — default config (yfinance primary)
# ──────────────────────────────────────────────────────────────────────────────
class TestEstimateAnalyze:
def test_default_config_uses_yfinance(self):
"""Default analyze path should materially use yfinance."""
est = estimate_analyze()
assert est.vendor_calls.yfinance > 0
def test_explicit_yfinance_config_has_no_av_calls(self):
"""A pure yfinance config should keep Alpha Vantage at zero."""
cfg = {
"data_vendors": {
"core_stock_apis": "yfinance",
"technical_indicators": "yfinance",
"fundamental_data": "yfinance",
"news_data": "yfinance",
"scanner_data": "yfinance",
"calendar_data": "finnhub",
},
"tool_vendors": {
"get_insider_transactions": "finnhub",
},
}
est = estimate_analyze(config=cfg)
assert est.vendor_calls.alpha_vantage == 0
def test_all_analysts_nonzero_total(self):
est = estimate_analyze(selected_analysts=["market", "news", "fundamentals", "social"])
assert est.vendor_calls.total > 0
def test_market_only(self):
est = estimate_analyze(selected_analysts=["market"], num_indicators=4)
# 1 stock data + 4 indicators = 5 calls
assert est.vendor_calls.total >= 5
def test_fundamentals_includes_insider(self):
"""Fundamentals analyst should include insider_transactions (Finnhub default)."""
est = estimate_analyze(selected_analysts=["fundamentals"])
# insider_transactions defaults to finnhub
assert est.vendor_calls.finnhub >= 1
def test_num_indicators_varies_total(self):
est_low = estimate_analyze(selected_analysts=["market"], num_indicators=2)
est_high = estimate_analyze(selected_analysts=["market"], num_indicators=8)
assert est_high.vendor_calls.total > est_low.vendor_calls.total
def test_av_config_counts_av_calls(self):
"""When AV is configured as primary, calls should show up under alpha_vantage."""
av_config = {
"data_vendors": {
"core_stock_apis": "alpha_vantage",
"technical_indicators": "alpha_vantage",
"fundamental_data": "alpha_vantage",
"news_data": "alpha_vantage",
"scanner_data": "alpha_vantage",
"calendar_data": "finnhub",
},
"tool_vendors": {
"get_insider_transactions": "alpha_vantage",
},
}
est = estimate_analyze(config=av_config, selected_analysts=["market", "fundamentals"])
assert est.vendor_calls.alpha_vantage > 0
assert est.vendor_calls.yfinance == 0
def test_method_breakdown_has_entries(self):
est = estimate_analyze(selected_analysts=["market"])
assert len(est.method_breakdown) > 0
def test_notes_populated(self):
est = estimate_analyze()
assert len(est.notes) > 0
# ──────────────────────────────────────────────────────────────────────────────
# estimate_scan — default config (yfinance primary)
# ──────────────────────────────────────────────────────────────────────────────
class TestEstimateScan:
def test_default_config_uses_yfinance(self):
est = estimate_scan()
assert est.vendor_calls.yfinance > 0
def test_scan_uses_finviz_for_gap_subset(self):
est = estimate_scan()
assert est.vendor_calls.finviz >= 1
def test_finnhub_for_calendars(self):
"""Global bounded scanners should add Finnhub earnings-calendar usage."""
est = estimate_scan()
assert est.vendor_calls.finnhub >= 2
def test_scan_total_reasonable(self):
est = estimate_scan()
# Global-only scanner remains bounded despite added nodes.
assert 10 <= est.vendor_calls.total <= 50
def test_notes_have_phases(self):
est = estimate_scan()
phase_notes = [n for n in est.notes if "Phase" in n]
assert len(phase_notes) >= 5
def test_macro_synthesis_has_no_external_calls(self):
est = estimate_scan()
assert any("Macro Synthesis" in note and "no external tool calls" in note for note in est.notes)
# ──────────────────────────────────────────────────────────────────────────────
# estimate_pipeline
# ──────────────────────────────────────────────────────────────────────────────
class TestEstimatePipeline:
def test_pipeline_larger_than_scan(self):
scan_est = estimate_scan()
pipe_est = estimate_pipeline(num_tickers=3)
assert pipe_est.vendor_calls.total > scan_est.vendor_calls.total
def test_pipeline_scales_with_tickers(self):
est3 = estimate_pipeline(num_tickers=3)
est7 = estimate_pipeline(num_tickers=7)
assert est7.vendor_calls.total > est3.vendor_calls.total
def test_pipeline_av_config(self):
"""Pipeline with AV config should report AV calls."""
av_config = {
"data_vendors": {
"core_stock_apis": "alpha_vantage",
"technical_indicators": "alpha_vantage",
"fundamental_data": "alpha_vantage",
"news_data": "alpha_vantage",
"scanner_data": "alpha_vantage",
"calendar_data": "finnhub",
},
"tool_vendors": {},
}
est = estimate_pipeline(config=av_config, num_tickers=5)
assert est.vendor_calls.alpha_vantage > 0
# ──────────────────────────────────────────────────────────────────────────────
# format_estimate
# ──────────────────────────────────────────────────────────────────────────────
class TestFormatEstimate:
def test_contains_vendor_counts(self):
est = estimate_analyze()
text = format_estimate(est)
assert "yfinance" in text
def test_includes_finviz_when_present(self):
est = UsageEstimate(
command="scan",
description="scan",
vendor_calls=VendorEstimate(finviz=1),
)
text = format_estimate(est)
assert "Finviz" in text
assert "Total:" in text
def test_default_format_includes_av_assessment(self):
est = estimate_analyze()
text = format_estimate(est)
assert "Alpha Vantage Assessment" in text
def test_av_shows_assessment(self):
av_config = {
"data_vendors": {
"core_stock_apis": "alpha_vantage",
"technical_indicators": "alpha_vantage",
"fundamental_data": "alpha_vantage",
"news_data": "alpha_vantage",
"scanner_data": "alpha_vantage",
"calendar_data": "finnhub",
},
"tool_vendors": {},
}
est = estimate_analyze(config=av_config)
text = format_estimate(est)
assert "Alpha Vantage" in text
# ──────────────────────────────────────────────────────────────────────────────
# format_vendor_breakdown (actual run data)
# ──────────────────────────────────────────────────────────────────────────────
class TestFormatVendorBreakdown:
def test_empty_summary(self):
assert format_vendor_breakdown({}) == ""
def test_yfinance_only(self):
summary = {"vendors_used": {"yfinance": {"ok": 10, "fail": 0}}}
text = format_vendor_breakdown(summary)
assert "yfinance:10ok/0fail" in text
def test_multiple_vendors(self):
summary = {
"vendors_used": {
"yfinance": {"ok": 8, "fail": 1},
"alpha_vantage": {"ok": 3, "fail": 0},
"finnhub": {"ok": 2, "fail": 0},
"finviz": {"ok": 1, "fail": 0},
}
}
text = format_vendor_breakdown(summary)
assert "yfinance:8ok/1fail" in text
assert "AV:3ok/0fail" in text
assert "Finnhub:2ok/0fail" in text
assert "Finviz:1ok/0fail" in text
# ──────────────────────────────────────────────────────────────────────────────
# format_av_assessment (actual run data)
# ──────────────────────────────────────────────────────────────────────────────
class TestFormatAvAssessment:
def test_no_av_used(self):
summary = {"vendors_used": {"yfinance": {"ok": 10, "fail": 0}}}
text = format_av_assessment(summary)
assert "not used" in text
def test_av_within_free(self):
summary = {"vendors_used": {"alpha_vantage": {"ok": 5, "fail": 0}}}
text = format_av_assessment(summary)
assert "free tier" in text
assert "5 calls" in text
def test_av_exceeds_free(self):
summary = {"vendors_used": {"alpha_vantage": {"ok": 30, "fail": 0}}}
text = format_av_assessment(summary)
assert "exceeds" in text
assert "Premium" in text
# ──────────────────────────────────────────────────────────────────────────────
# Constants
# ──────────────────────────────────────────────────────────────────────────────
class TestConstants:
def test_av_free_daily_limit(self):
assert AV_FREE_DAILY_LIMIT == 25
def test_av_premium_per_minute(self):
assert AV_PREMIUM_PER_MINUTE == 75