"""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