"""Offline mocked tests for the market-wide scanner layer. Covers both yfinance and Alpha Vantage scanner functions, plus the route_to_vendor scanner routing. All external calls are mocked so these tests run without a network connection or API key. """ import json import pandas as pd import pytest from datetime import date, datetime from unittest.mock import patch, MagicMock # --------------------------------------------------------------------------- # Helpers — mock data factories # --------------------------------------------------------------------------- def _av_response(payload: dict | str) -> MagicMock: """Build a mock requests.Response wrapping a JSON dict or raw string.""" resp = MagicMock() resp.status_code = 200 resp.text = json.dumps(payload) if isinstance(payload, dict) else payload resp.raise_for_status = MagicMock() return resp def _global_quote(symbol: str, price: float = 480.0, change: float = 2.5, change_pct: str = "0.52%") -> dict: return { "Global Quote": { "01. symbol": symbol, "05. price": str(price), "09. change": str(change), "10. change percent": change_pct, } } def _time_series_daily(symbol: str) -> dict: """Return a minimal TIME_SERIES_DAILY JSON payload.""" return { "Meta Data": {"2. Symbol": symbol}, "Time Series (Daily)": { "2024-01-08": {"4. close": "482.00"}, "2024-01-05": {"4. close": "480.00"}, "2024-01-04": {"4. close": "475.00"}, }, } _TOP_GAINERS_LOSERS = { "top_gainers": [ {"ticker": "NVDA", "price": "620.00", "change_percentage": "5.10%", "volume": "45000000"}, {"ticker": "AMD", "price": "175.00", "change_percentage": "3.20%", "volume": "32000000"}, ], "top_losers": [ {"ticker": "INTC", "price": "31.00", "change_percentage": "-4.50%", "volume": "28000000"}, ], "most_actively_traded": [ {"ticker": "TSLA", "price": "240.00", "change_percentage": "1.80%", "volume": "90000000"}, ], } _NEWS_SENTIMENT = { "feed": [ { "title": "AI Stocks Rally on Positive Earnings", "summary": "Tech stocks continued their upward climb.", "source": "Reuters", "url": "https://example.com/news/1", "time_published": "20240108T130000", "overall_sentiment_score": 0.35, } ] } # --------------------------------------------------------------------------- # yfinance scanner — get_market_movers_yfinance # --------------------------------------------------------------------------- class TestYfinanceScannerMarketMovers: """Offline tests for get_market_movers_yfinance.""" def _screener_data(self, category: str = "day_gainers") -> dict: return { "quotes": [ { "symbol": "NVDA", "shortName": "NVIDIA Corp", "regularMarketPrice": 620.00, "regularMarketChangePercent": 5.10, "regularMarketVolume": 45_000_000, "marketCap": 1_500_000_000_000, }, { "symbol": "AMD", "shortName": "Advanced Micro Devices", "regularMarketPrice": 175.00, "regularMarketChangePercent": 3.20, "regularMarketVolume": 32_000_000, "marketCap": 280_000_000_000, }, ] } def test_returns_markdown_table_for_day_gainers(self): from tradingagents.dataflows.yfinance_scanner import get_market_movers_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.screener.screen", return_value=self._screener_data()): result = get_market_movers_yfinance("day_gainers") assert isinstance(result, str) assert "Market Movers" in result assert "NVDA" in result assert "5.10%" in result assert "|" in result # markdown table def test_returns_markdown_table_for_day_losers(self): from tradingagents.dataflows.yfinance_scanner import get_market_movers_yfinance data = {"quotes": [{"symbol": "INTC", "shortName": "Intel", "regularMarketPrice": 31.00, "regularMarketChangePercent": -4.5, "regularMarketVolume": 28_000_000, "marketCap": 130_000_000_000}]} with patch("tradingagents.dataflows.yfinance_scanner.yf.screener.screen", return_value=data): result = get_market_movers_yfinance("day_losers") assert "Market Movers" in result assert "INTC" in result def test_invalid_category_returns_error_string(self): from tradingagents.dataflows.yfinance_scanner import get_market_movers_yfinance result = get_market_movers_yfinance("not_a_category") assert "Invalid category" in result def test_empty_quotes_returns_no_data_message(self): from tradingagents.dataflows.yfinance_scanner import get_market_movers_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.screener.screen", return_value={"quotes": []}): result = get_market_movers_yfinance("day_gainers") assert "No quotes found" in result def test_api_error_returns_error_string(self): from tradingagents.dataflows.yfinance_scanner import get_market_movers_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.screener.screen", side_effect=Exception("network failure")): result = get_market_movers_yfinance("day_gainers") assert "Error" in result # --------------------------------------------------------------------------- # yfinance scanner — get_market_indices_yfinance # --------------------------------------------------------------------------- class TestYfinanceScannerMarketIndices: """Offline tests for get_market_indices_yfinance.""" def _make_multi_etf_df(self) -> pd.DataFrame: """Build a minimal multi-ticker Close DataFrame as yf.download returns.""" symbols = ["^GSPC", "^DJI", "^IXIC", "^VIX", "^RUT"] idx = pd.date_range("2024-01-04", periods=3, freq="B", tz="UTC") closes = pd.DataFrame( {s: [4800.0 + i * 10, 4810.0 + i * 10, 4820.0 + i * 10] for i, s in enumerate(symbols)}, index=idx, ) return pd.DataFrame({"Close": closes}) def test_returns_markdown_table_with_indices(self): from tradingagents.dataflows.yfinance_scanner import get_market_indices_yfinance # Multi-symbol download returns a MultiIndex DataFrame symbols = ["^GSPC", "^DJI", "^IXIC", "^VIX", "^RUT"] idx = pd.date_range("2024-01-04", periods=5, freq="B") close_data = {s: [4800.0 + i for i in range(5)] for s in symbols} # yf.download with multiple symbols returns DataFrame with MultiIndex columns multi_df = pd.DataFrame(close_data, index=idx) multi_df.columns = pd.MultiIndex.from_product([["Close"], symbols]) with patch("tradingagents.dataflows.yfinance_scanner.yf.download", return_value=multi_df): result = get_market_indices_yfinance() assert isinstance(result, str) assert "Market Indices" in result or "Index" in result.split("\n")[0] def test_returns_string_on_download_error(self): from tradingagents.dataflows.yfinance_scanner import get_market_indices_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.download", side_effect=Exception("network error")): result = get_market_indices_yfinance() assert isinstance(result, str) # --------------------------------------------------------------------------- # yfinance scanner — get_sector_performance_yfinance # --------------------------------------------------------------------------- class TestYfinanceScannerSectorPerformance: """Offline tests for get_sector_performance_yfinance.""" def _make_sector_df(self) -> pd.DataFrame: """Multi-symbol ETF DataFrame covering 6 months of daily closes.""" etfs = ["XLK", "XLV", "XLF", "XLE", "XLY", "XLP", "XLI", "XLB", "XLRE", "XLU", "XLC"] # 130 trading days ~ 6 months idx = pd.date_range("2023-07-01", periods=130, freq="B") data = {e: [100.0 + i * 0.01 for i in range(130)] for e in etfs} df = pd.DataFrame(data, index=idx) df.columns = pd.MultiIndex.from_product([["Close"], etfs]) return df def test_returns_sector_performance_table(self): from tradingagents.dataflows.yfinance_scanner import get_sector_performance_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.download", return_value=self._make_sector_df()): result = get_sector_performance_yfinance() assert isinstance(result, str) assert "Sector Performance Overview" in result assert "|" in result def test_contains_all_sectors(self): from tradingagents.dataflows.yfinance_scanner import get_sector_performance_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.download", return_value=self._make_sector_df()): result = get_sector_performance_yfinance() # 11 GICS sectors should all appear for sector in ["Technology", "Healthcare", "Financials", "Energy"]: assert sector in result def test_download_error_returns_error_string(self): from tradingagents.dataflows.yfinance_scanner import get_sector_performance_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.download", side_effect=Exception("connection refused")): result = get_sector_performance_yfinance() assert "Error" in result # --------------------------------------------------------------------------- # yfinance scanner — get_industry_performance_yfinance # --------------------------------------------------------------------------- class TestYfinanceScannerIndustryPerformance: """Offline tests for get_industry_performance_yfinance.""" def _mock_sector_with_companies(self) -> MagicMock: top_companies = pd.DataFrame( { "name": ["Apple Inc.", "Microsoft Corp", "NVIDIA Corp"], "rating": [4.5, 4.8, 4.2], "market weight": [0.072, 0.065, 0.051], }, index=pd.Index(["AAPL", "MSFT", "NVDA"], name="symbol"), ) mock_sector = MagicMock() mock_sector.top_companies = top_companies return mock_sector def test_returns_industry_table_for_valid_sector(self): from tradingagents.dataflows.yfinance_scanner import get_industry_performance_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.Sector", return_value=self._mock_sector_with_companies()): result = get_industry_performance_yfinance("technology") assert isinstance(result, str) assert "Industry Performance" in result assert "AAPL" in result assert "Apple Inc." in result def test_empty_top_companies_returns_no_data_message(self): from tradingagents.dataflows.yfinance_scanner import get_industry_performance_yfinance mock_sector = MagicMock() mock_sector.top_companies = pd.DataFrame() with patch("tradingagents.dataflows.yfinance_scanner.yf.Sector", return_value=mock_sector): result = get_industry_performance_yfinance("technology") assert "No industry data found" in result def test_none_top_companies_returns_no_data_message(self): from tradingagents.dataflows.yfinance_scanner import get_industry_performance_yfinance mock_sector = MagicMock() mock_sector.top_companies = None with patch("tradingagents.dataflows.yfinance_scanner.yf.Sector", return_value=mock_sector): result = get_industry_performance_yfinance("healthcare") assert "No industry data found" in result def test_sector_error_returns_error_string(self): from tradingagents.dataflows.yfinance_scanner import get_industry_performance_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.Sector", side_effect=Exception("yfinance unavailable")): result = get_industry_performance_yfinance("technology") assert "Error" in result # --------------------------------------------------------------------------- # yfinance scanner — get_topic_news_yfinance # --------------------------------------------------------------------------- class TestYfinanceScannerTopicNews: """Offline tests for get_topic_news_yfinance.""" def _mock_search(self, title: str = "AI Revolution in Tech") -> MagicMock: mock_search = MagicMock() mock_search.news = [ { "title": title, "publisher": "TechCrunch", "link": "https://techcrunch.com/story", "summary": "Artificial intelligence is transforming the industry.", } ] return mock_search def test_returns_formatted_news_for_topic(self): from tradingagents.dataflows.yfinance_scanner import get_topic_news_yfinance with patch("tradingagents.dataflows.yfinance_scanner.yf.Search", return_value=self._mock_search()): result = get_topic_news_yfinance("artificial intelligence") assert isinstance(result, str) assert "AI Revolution in Tech" in result assert "News for Topic" in result def test_no_results_returns_no_news_message(self): from tradingagents.dataflows.yfinance_scanner import get_topic_news_yfinance mock_search = MagicMock() mock_search.news = [] with patch("tradingagents.dataflows.yfinance_scanner.yf.Search", return_value=mock_search): result = get_topic_news_yfinance("obscure_topic") assert "No news found" in result def test_handles_nested_content_structure(self): from tradingagents.dataflows.yfinance_scanner import get_topic_news_yfinance mock_search = MagicMock() mock_search.news = [ { "content": { "title": "Semiconductor Demand Surges", "summary": "Chip makers report record orders.", "provider": {"displayName": "Bloomberg"}, "canonicalUrl": {"url": "https://bloomberg.com/chips"}, } } ] with patch("tradingagents.dataflows.yfinance_scanner.yf.Search", return_value=mock_search): result = get_topic_news_yfinance("semiconductors") assert "Semiconductor Demand Surges" in result # --------------------------------------------------------------------------- # Alpha Vantage scanner — get_market_movers_alpha_vantage # --------------------------------------------------------------------------- class TestAVScannerMarketMovers: """Offline mocked tests for get_market_movers_alpha_vantage.""" def test_day_gainers_returns_markdown_table(self): from tradingagents.dataflows.alpha_vantage_scanner import get_market_movers_alpha_vantage with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", return_value=json.dumps(_TOP_GAINERS_LOSERS)): result = get_market_movers_alpha_vantage("day_gainers") assert "Market Movers" in result assert "NVDA" in result assert "5.10%" in result assert "|" in result def test_day_losers_returns_markdown_table(self): from tradingagents.dataflows.alpha_vantage_scanner import get_market_movers_alpha_vantage with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", return_value=json.dumps(_TOP_GAINERS_LOSERS)): result = get_market_movers_alpha_vantage("day_losers") assert "INTC" in result def test_most_actives_returns_markdown_table(self): from tradingagents.dataflows.alpha_vantage_scanner import get_market_movers_alpha_vantage with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", return_value=json.dumps(_TOP_GAINERS_LOSERS)): result = get_market_movers_alpha_vantage("most_actives") assert "TSLA" in result def test_invalid_category_raises_value_error(self): from tradingagents.dataflows.alpha_vantage_scanner import get_market_movers_alpha_vantage with pytest.raises(ValueError, match="Invalid category"): get_market_movers_alpha_vantage("not_valid") def test_rate_limit_error_propagates(self): from tradingagents.dataflows.alpha_vantage_scanner import get_market_movers_alpha_vantage from tradingagents.dataflows.alpha_vantage_common import RateLimitError with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=RateLimitError("rate limited")): with pytest.raises(RateLimitError): get_market_movers_alpha_vantage("day_gainers") # --------------------------------------------------------------------------- # Alpha Vantage scanner — get_market_indices_alpha_vantage # --------------------------------------------------------------------------- class TestAVScannerMarketIndices: """Offline mocked tests for get_market_indices_alpha_vantage.""" def test_returns_markdown_table_with_index_names(self): from tradingagents.dataflows.alpha_vantage_scanner import get_market_indices_alpha_vantage def fake_request(function_name, params, **kwargs): symbol = params.get("symbol", "SPY") return json.dumps(_global_quote(symbol)) with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=fake_request): result = get_market_indices_alpha_vantage() assert "Market Indices" in result assert "|" in result assert any(name in result for name in ["S&P 500", "Dow Jones", "NASDAQ"]) def test_all_proxies_appear_in_output(self): from tradingagents.dataflows.alpha_vantage_scanner import get_market_indices_alpha_vantage def fake_request(function_name, params, **kwargs): return json.dumps(_global_quote(params.get("symbol", "SPY"))) with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=fake_request): result = get_market_indices_alpha_vantage() # All 4 ETF proxies should appear for proxy in ["SPY", "DIA", "QQQ", "IWM"]: assert proxy in result # --------------------------------------------------------------------------- # Alpha Vantage scanner — get_sector_performance_alpha_vantage # --------------------------------------------------------------------------- class TestAVScannerSectorPerformance: """Offline mocked tests for get_sector_performance_alpha_vantage.""" def _make_fake_request(self): """Return a side_effect function handling both GLOBAL_QUOTE and TIME_SERIES_DAILY.""" def fake(function_name, params, **kwargs): if function_name == "GLOBAL_QUOTE": symbol = params.get("symbol", "XLK") return json.dumps(_global_quote(symbol)) elif function_name == "TIME_SERIES_DAILY": symbol = params.get("symbol", "XLK") return json.dumps(_time_series_daily(symbol)) return json.dumps({}) return fake def test_returns_sector_table_with_percentages(self): from tradingagents.dataflows.alpha_vantage_scanner import get_sector_performance_alpha_vantage with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=self._make_fake_request()): result = get_sector_performance_alpha_vantage() assert "Sector Performance Overview" in result assert "|" in result def test_all_eleven_sectors_in_output(self): from tradingagents.dataflows.alpha_vantage_scanner import get_sector_performance_alpha_vantage with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=self._make_fake_request()): result = get_sector_performance_alpha_vantage() for sector in ["Technology", "Healthcare", "Financials", "Energy"]: assert sector in result def test_all_errors_raises_alpha_vantage_error(self): """If ALL sector ETF requests fail, AlphaVantageError is raised for fallback.""" from tradingagents.dataflows.alpha_vantage_scanner import get_sector_performance_alpha_vantage from tradingagents.dataflows.alpha_vantage_common import AlphaVantageError, RateLimitError with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=RateLimitError("rate limited")): with pytest.raises(AlphaVantageError): get_sector_performance_alpha_vantage() # --------------------------------------------------------------------------- # Alpha Vantage scanner — get_industry_performance_alpha_vantage # --------------------------------------------------------------------------- class TestAVScannerIndustryPerformance: """Offline mocked tests for get_industry_performance_alpha_vantage.""" def test_returns_table_for_technology_sector(self): from tradingagents.dataflows.alpha_vantage_scanner import get_industry_performance_alpha_vantage def fake_request(function_name, params, **kwargs): symbol = params.get("symbol", "AAPL") return json.dumps(_global_quote(symbol, price=185.0, change_pct="+1.20%")) with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=fake_request): result = get_industry_performance_alpha_vantage("technology") assert "Industry Performance" in result assert "|" in result assert any(t in result for t in ["AAPL", "MSFT", "NVDA"]) def test_invalid_sector_raises_value_error(self): from tradingagents.dataflows.alpha_vantage_scanner import get_industry_performance_alpha_vantage with pytest.raises(ValueError, match="Unknown sector"): get_industry_performance_alpha_vantage("not_a_real_sector") def test_sorted_by_change_percent_descending(self): """Results should be sorted by change % descending.""" from tradingagents.dataflows.alpha_vantage_scanner import get_industry_performance_alpha_vantage # Alternate high/low changes to verify sort order prices = {"AAPL": ("180.00", "+5.00%"), "MSFT": ("380.00", "+1.00%"), "NVDA": ("620.00", "+8.00%"), "GOOGL": ("140.00", "+2.50%"), "META": ("350.00", "+3.10%"), "AVGO": ("850.00", "+0.50%"), "ADBE": ("550.00", "+4.20%"), "CRM": ("275.00", "+1.80%"), "AMD": ("170.00", "+6.30%"), "INTC": ("31.00", "-2.10%")} def fake_request(function_name, params, **kwargs): symbol = params.get("symbol", "AAPL") p, c = prices.get(symbol, ("100.00", "0.00%")) return json.dumps({ "Global Quote": {"01. symbol": symbol, "05. price": p, "09. change": "1.00", "10. change percent": c} }) with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=fake_request): result = get_industry_performance_alpha_vantage("technology") # NVDA (+8%) should appear before INTC (-2.1%) nvda_pos = result.find("NVDA") intc_pos = result.find("INTC") assert nvda_pos != -1 and intc_pos != -1 assert nvda_pos < intc_pos # --------------------------------------------------------------------------- # Alpha Vantage scanner — get_topic_news_alpha_vantage # --------------------------------------------------------------------------- class TestAVScannerTopicNews: """Offline mocked tests for get_topic_news_alpha_vantage.""" def test_returns_news_articles_for_known_topic(self): from tradingagents.dataflows.alpha_vantage_scanner import get_topic_news_alpha_vantage with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", return_value=json.dumps(_NEWS_SENTIMENT)): result = get_topic_news_alpha_vantage("market", limit=5) assert "News for Topic" in result assert "AI Stocks Rally on Positive Earnings" in result def test_known_topic_is_mapped_to_av_value(self): """Topic strings like 'market' are remapped to AV-specific topic keys.""" from tradingagents.dataflows.alpha_vantage_scanner import get_topic_news_alpha_vantage captured = {} def capture_request(function_name, params, **kwargs): captured.update(params) return json.dumps(_NEWS_SENTIMENT) with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=capture_request): get_topic_news_alpha_vantage("market", limit=5) # "market" maps to "financial_markets" in _TOPIC_MAP assert captured.get("topics") == "financial_markets" def test_unknown_topic_passed_through(self): """Topics not in the map are forwarded to the API as-is.""" from tradingagents.dataflows.alpha_vantage_scanner import get_topic_news_alpha_vantage captured = {} def capture_request(function_name, params, **kwargs): captured.update(params) return json.dumps(_NEWS_SENTIMENT) with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=capture_request): get_topic_news_alpha_vantage("custom_topic", limit=3) assert captured.get("topics") == "custom_topic" def test_empty_feed_returns_no_articles_message(self): from tradingagents.dataflows.alpha_vantage_scanner import get_topic_news_alpha_vantage empty = {"feed": []} with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", return_value=json.dumps(empty)): result = get_topic_news_alpha_vantage("earnings", limit=5) assert "No articles" in result def test_rate_limit_error_propagates(self): from tradingagents.dataflows.alpha_vantage_scanner import get_topic_news_alpha_vantage from tradingagents.dataflows.alpha_vantage_common import RateLimitError with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=RateLimitError("rate limited")): with pytest.raises(RateLimitError): get_topic_news_alpha_vantage("technology") # --------------------------------------------------------------------------- # Scanner routing — route_to_vendor for scanner methods # --------------------------------------------------------------------------- class TestScannerRouting: """End-to-end routing tests for scanner_data methods via route_to_vendor.""" def test_get_market_movers_routes_to_yfinance_by_default(self): """Default config uses yfinance for scanner_data.""" from tradingagents.dataflows.interface import route_to_vendor screener_data = { "quotes": [{"symbol": "NVDA", "shortName": "NVIDIA", "regularMarketPrice": 620.0, "regularMarketChangePercent": 5.1, "regularMarketVolume": 45_000_000, "marketCap": 1_500_000_000_000}] } with patch("tradingagents.dataflows.yfinance_scanner.yf.screener.screen", return_value=screener_data): result = route_to_vendor("get_market_movers", "day_gainers") assert isinstance(result, str) assert "NVDA" in result def test_get_sector_performance_routes_to_yfinance_by_default(self): from tradingagents.dataflows.interface import route_to_vendor etfs = ["XLK", "XLV", "XLF", "XLE", "XLY", "XLP", "XLI", "XLB", "XLRE", "XLU", "XLC"] idx = pd.date_range("2023-07-01", periods=130, freq="B") close_data = {e: [100.0 + i * 0.01 for i in range(130)] for e in etfs} df = pd.DataFrame(close_data, index=idx) df.columns = pd.MultiIndex.from_product([["Close"], etfs]) with patch("tradingagents.dataflows.yfinance_scanner.yf.download", return_value=df): result = route_to_vendor("get_sector_performance") assert isinstance(result, str) assert "Sector Performance Overview" in result def test_get_market_movers_falls_back_to_yfinance_when_av_fails(self): """When AV scanner raises AlphaVantageError, fallback to yfinance is used.""" from tradingagents.dataflows.interface import route_to_vendor from tradingagents.dataflows.config import get_config from tradingagents.dataflows.alpha_vantage_common import AlphaVantageError original_config = get_config() patched_config = { **original_config, "data_vendors": {**original_config.get("data_vendors", {}), "scanner_data": "alpha_vantage"}, } screener_data = { "quotes": [{"symbol": "AMD", "shortName": "AMD", "regularMarketPrice": 175.0, "regularMarketChangePercent": 3.2, "regularMarketVolume": 32_000_000, "marketCap": 280_000_000_000}] } with patch("tradingagents.dataflows.interface.get_config", return_value=patched_config): # AV market movers raises → fallback to yfinance with patch("tradingagents.dataflows.alpha_vantage_scanner._rate_limited_request", side_effect=AlphaVantageError("rate limited")): with patch("tradingagents.dataflows.yfinance_scanner.yf.screener.screen", return_value=screener_data): result = route_to_vendor("get_market_movers", "day_gainers") assert isinstance(result, str) assert "AMD" in result def test_get_topic_news_routes_correctly(self): from tradingagents.dataflows.interface import route_to_vendor mock_search = MagicMock() mock_search.news = [{"title": "Fed Signals Rate Cut", "publisher": "Reuters", "link": "https://example.com", "summary": "Fed news."}] with patch("tradingagents.dataflows.yfinance_scanner.yf.Search", return_value=mock_search): result = route_to_vendor("get_topic_news", "economy") assert isinstance(result, str) # --------------------------------------------------------------------------- # Finviz smart-money screener tools # --------------------------------------------------------------------------- def _make_finviz_df(): """Minimal DataFrame matching what finvizfinance screener_view() returns.""" return pd.DataFrame([ {"Ticker": "NVDA", "Sector": "Technology", "Price": "620.00", "Volume": "45000000"}, {"Ticker": "AMD", "Sector": "Technology", "Price": "175.00", "Volume": "32000000"}, {"Ticker": "XOM", "Sector": "Energy", "Price": "115.00", "Volume": "18000000"}, ]) class TestFinvizSmartMoneyTools: """Mocked unit tests for Finviz screener tools — no network required.""" def _mock_overview(self, df): """Return a patched Overview instance whose screener_view() yields df.""" mock_inst = MagicMock() mock_inst.screener_view.return_value = df mock_cls = MagicMock(return_value=mock_inst) return mock_cls def test_get_insider_buying_stocks_returns_report(self): from tradingagents.agents.utils.scanner_tools import get_insider_buying_stocks with patch("tradingagents.agents.utils.scanner_tools._run_finviz_screen", wraps=None) as _: pass # use full stack — patch Overview only mock_cls = self._mock_overview(_make_finviz_df()) with patch("finvizfinance.screener.overview.Overview", mock_cls): result = get_insider_buying_stocks.invoke({}) assert "insider_buying" in result assert "NVDA" in result or "AMD" in result or "XOM" in result def test_get_unusual_volume_stocks_returns_report(self): from tradingagents.agents.utils.scanner_tools import get_unusual_volume_stocks mock_cls = self._mock_overview(_make_finviz_df()) with patch("finvizfinance.screener.overview.Overview", mock_cls): result = get_unusual_volume_stocks.invoke({}) assert "unusual_volume" in result def test_get_breakout_accumulation_stocks_returns_report(self): from tradingagents.agents.utils.scanner_tools import get_breakout_accumulation_stocks mock_cls = self._mock_overview(_make_finviz_df()) with patch("finvizfinance.screener.overview.Overview", mock_cls): result = get_breakout_accumulation_stocks.invoke({}) assert "breakout_accumulation" in result def test_empty_dataframe_returns_no_match_message(self): from tradingagents.agents.utils.scanner_tools import get_insider_buying_stocks mock_cls = self._mock_overview(pd.DataFrame()) with patch("finvizfinance.screener.overview.Overview", mock_cls): result = get_insider_buying_stocks.invoke({}) assert "No stocks matched" in result def test_exception_returns_graceful_unavailable_message(self): from tradingagents.agents.utils.scanner_tools import get_breakout_accumulation_stocks mock_inst = MagicMock() mock_inst.screener_view.side_effect = ConnectionError("timeout") mock_cls = MagicMock(return_value=mock_inst) with patch("finvizfinance.screener.overview.Overview", mock_cls): result = get_breakout_accumulation_stocks.invoke({}) assert "Smart money scan unavailable" in result assert "timeout" in result def test_all_three_tools_sort_by_volume(self): """Verify the top result is the highest-volume ticker.""" from tradingagents.agents.utils.scanner_tools import get_unusual_volume_stocks # NVDA has highest volume (45M) — should appear first in report mock_cls = self._mock_overview(_make_finviz_df()) with patch("finvizfinance.screener.overview.Overview", mock_cls): result = get_unusual_volume_stocks.invoke({}) nvda_pos = result.find("NVDA") amd_pos = result.find("AMD") assert nvda_pos < amd_pos, "NVDA (higher volume) should appear before AMD"