From 733a11bd0ae15b2ec7dc328f1095bfe49161538e Mon Sep 17 00:00:00 2001 From: Ahmet Guzererler Date: Sat, 28 Mar 2026 09:33:32 +0100 Subject: [PATCH] feat: wire gatekeeper into scanner graph --- docs/agent/CURRENT_STATE.md | 2 +- tests/unit/test_api_usage.py | 19 ++++++- tests/unit/test_global_search_scanners.py | 34 ++++++++++++ tests/unit/test_macro_synthesis.py | 15 ++++++ tests/unit/test_scanner_graph.py | 1 + tradingagents/agents/scanners/__init__.py | 1 + .../agents/scanners/drift_scanner.py | 15 +++--- .../agents/scanners/gatekeeper_scanner.py | 53 +++++++++++++++++++ .../agents/scanners/industry_deep_dive.py | 3 ++ .../agents/scanners/macro_synthesis.py | 9 +++- .../agents/scanners/market_movers_scanner.py | 17 +++--- tradingagents/agents/utils/scanner_states.py | 1 + tradingagents/api_usage.py | 36 +++++++++---- tradingagents/graph/scanner_graph.py | 5 +- tradingagents/graph/scanner_setup.py | 6 ++- 15 files changed, 185 insertions(+), 32 deletions(-) create mode 100644 tradingagents/agents/scanners/gatekeeper_scanner.py diff --git a/docs/agent/CURRENT_STATE.md b/docs/agent/CURRENT_STATE.md index 54bf972a..1b2f07f0 100644 --- a/docs/agent/CURRENT_STATE.md +++ b/docs/agent/CURRENT_STATE.md @@ -29,7 +29,7 @@ All storage, event, checkpoint, and phase re-run logic is now documented in ADR - Added live-tested `yfinance` gatekeeper universe query for US-listed liquid profitable mid-cap+ names - Added live-tested Finviz gap-subset path using the bounded gatekeeper-plus-gap filter - Narrowed Finviz usage to the gap/event layer instead of the full market-universe layer - - Next step is graph wiring so downstream candidate selection is hard-filtered by the gatekeeper universe + - Added graph wiring: dedicated gatekeeper scanner node, gatekeeper-aware drift context, and deterministic ranking that excludes names outside the gatekeeper universe # In Progress diff --git a/tests/unit/test_api_usage.py b/tests/unit/test_api_usage.py index feaef496..2f6275ac 100644 --- a/tests/unit/test_api_usage.py +++ b/tests/unit/test_api_usage.py @@ -23,8 +23,8 @@ from tradingagents.api_usage import ( class TestVendorEstimate: def test_total(self): - ve = VendorEstimate(yfinance=10, alpha_vantage=5, finnhub=2) - assert ve.total == 17 + ve = VendorEstimate(yfinance=10, alpha_vantage=5, finnhub=2, finviz=1) + assert ve.total == 18 def test_default_zeros(self): ve = VendorEstimate() @@ -157,6 +157,10 @@ class TestEstimateScan: 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() @@ -220,6 +224,15 @@ class TestFormatEstimate: 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): @@ -264,12 +277,14 @@ class TestFormatVendorBreakdown: "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 # ────────────────────────────────────────────────────────────────────────────── diff --git a/tests/unit/test_global_search_scanners.py b/tests/unit/test_global_search_scanners.py index a41a3609..b6e240a3 100644 --- a/tests/unit/test_global_search_scanners.py +++ b/tests/unit/test_global_search_scanners.py @@ -8,6 +8,9 @@ from tradingagents.agents.scanners.drift_scanner import create_drift_scanner from tradingagents.agents.scanners.factor_alignment_scanner import ( create_factor_alignment_scanner, ) +from tradingagents.agents.scanners.gatekeeper_scanner import ( + create_gatekeeper_scanner, +) class MockRunnable(Runnable): @@ -38,11 +41,42 @@ def _base_state(): return { "messages": [HumanMessage(content="Run the market scan.")], "scan_date": "2026-03-27", + "gatekeeper_universe_report": "| Symbol |\n| NVDA |\n| AAPL |", "sector_performance_report": "| Sector | 1-Month % |\n| Technology | +5.0% |", "market_movers_report": "| Symbol | Change % |\n| NVDA | +4.0% |", } +def test_gatekeeper_scanner_end_to_end(): + llm = MockLLM( + [ + AIMessage( + content="", + tool_calls=[ + {"name": "get_gatekeeper_universe", "args": {}, "id": "tc1"}, + ], + ), + AIMessage(content="Gatekeeper report with liquid profitable names."), + ] + ) + + gatekeeper_tool = SimpleNamespace( + name="get_gatekeeper_universe", + invoke=lambda args: "gatekeeper universe table", + ) + + with patch( + "tradingagents.agents.scanners.gatekeeper_scanner.get_gatekeeper_universe", + gatekeeper_tool, + ): + node = create_gatekeeper_scanner(llm) + result = node(_base_state()) + + assert "Gatekeeper report" in result["gatekeeper_universe_report"] + assert result["sender"] == "gatekeeper_scanner" + assert [tool.name for tool in llm.tools_bound] == ["get_gatekeeper_universe"] + + def test_factor_alignment_scanner_end_to_end(): llm = MockLLM( [ diff --git a/tests/unit/test_macro_synthesis.py b/tests/unit/test_macro_synthesis.py index 68ed5f4f..a227fd2e 100644 --- a/tests/unit/test_macro_synthesis.py +++ b/tests/unit/test_macro_synthesis.py @@ -26,6 +26,7 @@ def test_extract_rankable_tickers_filters_noise(): def test_build_candidate_rankings_rewards_overlap(): state = { + "gatekeeper_universe_report": "NVDA AAPL MSFT", "market_movers_report": "NVDA AAPL", "smart_money_report": "NVDA", "factor_alignment_report": "NVDA MSFT", @@ -36,3 +37,17 @@ def test_build_candidate_rankings_rewards_overlap(): assert ranked[0]["ticker"] == "NVDA" assert ranked[0]["score"] > ranked[1]["score"] + + +def test_build_candidate_rankings_excludes_names_outside_gatekeeper(): + state = { + "gatekeeper_universe_report": "NVDA AAPL", + "market_movers_report": "NVDA TSLA", + "drift_opportunities_report": "TSLA", + } + + ranked = _build_candidate_rankings(state) + + tickers = {row["ticker"] for row in ranked} + assert "NVDA" in tickers + assert "TSLA" not in tickers diff --git a/tests/unit/test_scanner_graph.py b/tests/unit/test_scanner_graph.py index 0f0b32ed..ea401a61 100644 --- a/tests/unit/test_scanner_graph.py +++ b/tests/unit/test_scanner_graph.py @@ -41,6 +41,7 @@ def test_scanner_setup_compiles_graph(): from tradingagents.graph.scanner_setup import ScannerGraphSetup mock_agents = { + "gatekeeper_scanner": MagicMock(), "geopolitical_scanner": MagicMock(), "market_movers_scanner": MagicMock(), "sector_scanner": MagicMock(), diff --git a/tradingagents/agents/scanners/__init__.py b/tradingagents/agents/scanners/__init__.py index e9dd6752..0da18df6 100644 --- a/tradingagents/agents/scanners/__init__.py +++ b/tradingagents/agents/scanners/__init__.py @@ -1,3 +1,4 @@ +from .gatekeeper_scanner import create_gatekeeper_scanner from .geopolitical_scanner import create_geopolitical_scanner from .market_movers_scanner import create_market_movers_scanner from .sector_scanner import create_sector_scanner diff --git a/tradingagents/agents/scanners/drift_scanner.py b/tradingagents/agents/scanners/drift_scanner.py index 04f7c7f1..17dbab0a 100644 --- a/tradingagents/agents/scanners/drift_scanner.py +++ b/tradingagents/agents/scanners/drift_scanner.py @@ -15,11 +15,14 @@ def create_drift_scanner(llm): scan_date = state["scan_date"] tools = [get_gap_candidates, get_topic_news, get_earnings_calendar] + gatekeeper_context = state.get("gatekeeper_universe_report", "") market_context = state.get("market_movers_report", "") sector_context = state.get("sector_performance_report", "") context_chunks = [] + if gatekeeper_context: + context_chunks.append(f"Gatekeeper universe:\n{gatekeeper_context}") if market_context: - context_chunks.append(f"Market movers context:\n{market_context}") + context_chunks.append(f"Market regime context:\n{market_context}") if sector_context: context_chunks.append(f"Sector rotation context:\n{sector_context}") context_section = f"\n\n{'\n\n'.join(context_chunks)}" if context_chunks else "" @@ -32,16 +35,16 @@ def create_drift_scanner(llm): system_message = ( "You are a drift-window scanner focused on 1-3 month continuation setups. " - "Stay global and bounded: use the existing market movers context, then confirm whether those moves " - "look like the start of a sustained drift rather than one-day noise.\n\n" + "Stay global and bounded: the gatekeeper universe defines the only admissible stock set, and the Finviz " + "gap scan provides the event subset within that universe.\n\n" "You MUST perform these bounded searches:\n" - "1. Call get_gap_candidates to retrieve real market-data gap candidates.\n" + "1. Call get_gap_candidates to retrieve Finviz gap candidates from the gatekeeper universe.\n" "2. Call get_topic_news for earnings beats, raised guidance, and positive post-event follow-through.\n" f"3. Call get_earnings_calendar from {start_date.isoformat()} to {end_date.isoformat()}.\n\n" "Then write a concise report covering:\n" - "(1) which current movers look most likely to sustain a 1-3 month drift,\n" + "(1) which gatekeeper names look most likely to sustain a 1-3 month drift,\n" "(2) which sectors show the cleanest drift setup rather than short-covering noise,\n" - "(3) 5-8 candidate tickers surfaced globally from the mover context plus catalyst confirmation,\n" + "(3) 5-8 candidate tickers surfaced from the gap subset plus catalyst confirmation,\n" "(4) the key evidence for continuation risk versus reversal risk." f"{context_section}" ) diff --git a/tradingagents/agents/scanners/gatekeeper_scanner.py b/tradingagents/agents/scanners/gatekeeper_scanner.py new file mode 100644 index 00000000..78fa7115 --- /dev/null +++ b/tradingagents/agents/scanners/gatekeeper_scanner.py @@ -0,0 +1,53 @@ +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder + +from tradingagents.agents.utils.scanner_tools import get_gatekeeper_universe +from tradingagents.agents.utils.tool_runner import run_tool_loop + + +def create_gatekeeper_scanner(llm): + def gatekeeper_scanner_node(state): + scan_date = state["scan_date"] + + tools = [get_gatekeeper_universe] + + system_message = ( + "You are the gatekeeper scanner for the market-wide search graph. " + "Your job is to define the only stock universe that downstream agents are allowed to consider.\n\n" + "You MUST call get_gatekeeper_universe before writing your report.\n" + "Then write a concise report covering:\n" + "(1) the size and quality of the eligible universe,\n" + "(2) which sectors dominate the gatekeeper set,\n" + "(3) 10-15 representative liquid names worth monitoring,\n" + "(4) any obvious universe concentration risks.\n\n" + "Do not introduce stocks outside the gatekeeper universe." + ) + + prompt = ChatPromptTemplate.from_messages( + [ + ( + "system", + "You are a helpful AI assistant, collaborating with other assistants." + " Use the provided tools to progress towards answering the question." + " If you are unable to fully answer, that's OK; another assistant with different tools" + " will help where you left off. Execute what you can to make progress." + " You have access to the following tools: {tool_names}.\n{system_message}" + " For your reference, the current date is {current_date}.", + ), + MessagesPlaceholder(variable_name="messages"), + ] + ) + + prompt = prompt.partial(system_message=system_message) + prompt = prompt.partial(tool_names=", ".join([tool.name for tool in tools])) + prompt = prompt.partial(current_date=scan_date) + + chain = prompt | llm.bind_tools(tools) + result = run_tool_loop(chain, state["messages"], tools) + + return { + "messages": [result], + "gatekeeper_universe_report": result.content or "", + "sender": "gatekeeper_scanner", + } + + return gatekeeper_scanner_node diff --git a/tradingagents/agents/scanners/industry_deep_dive.py b/tradingagents/agents/scanners/industry_deep_dive.py index c6480e14..2e99a1d7 100644 --- a/tradingagents/agents/scanners/industry_deep_dive.py +++ b/tradingagents/agents/scanners/industry_deep_dive.py @@ -136,6 +136,9 @@ def create_industry_deep_dive(llm): ### Geopolitical Report: {state.get("geopolitical_report", "Not available")} +### Gatekeeper Universe Report: +{state.get("gatekeeper_universe_report", "Not available")} + ### Market Movers Report: {state.get("market_movers_report", "Not available")} diff --git a/tradingagents/agents/scanners/macro_synthesis.py b/tradingagents/agents/scanners/macro_synthesis.py index 8d5931fe..9332c1cf 100644 --- a/tradingagents/agents/scanners/macro_synthesis.py +++ b/tradingagents/agents/scanners/macro_synthesis.py @@ -42,6 +42,7 @@ def _extract_rankable_tickers(text: str) -> set[str]: def _build_candidate_rankings(state: dict, limit: int = 15) -> list[dict[str, object]]: + allowed_tickers = _extract_rankable_tickers(state.get("gatekeeper_universe_report", "")) weighted_sources = [ ("market_movers_report", 2, "market_movers"), ("smart_money_report", 2, "smart_money"), @@ -56,6 +57,8 @@ def _build_candidate_rankings(state: dict, limit: int = 15) -> list[dict[str, ob for state_key, weight, label in weighted_sources: tickers = _extract_rankable_tickers(state.get(state_key, "")) for ticker in tickers: + if allowed_tickers and ticker not in allowed_tickers: + continue scores[ticker] += weight sources[ticker].append(label) @@ -92,6 +95,9 @@ def create_macro_synthesis(llm, max_scan_tickers: int = 10, scan_horizon_days: i ranking_section = "\n\n### Deterministic Candidate Ranking:\n" + "\n".join(ranking_lines) all_reports_context = f"""## All Scanner and Research Reports +### Gatekeeper Universe Report: +{state.get("gatekeeper_universe_report", "Not available")} + ### Geopolitical Report: {state.get("geopolitical_report", "Not available")} @@ -117,10 +123,11 @@ def create_macro_synthesis(llm, max_scan_tickers: int = 10, scan_horizon_days: i system_message = ( "You are a macro strategist synthesizing all scanner and research reports into a final investment thesis. " - "You have received: geopolitical analysis, market movers analysis, sector performance analysis, " + "You have received: gatekeeper universe analysis, geopolitical analysis, market regime analysis, sector performance analysis, " "smart money institutional screener results, and industry deep dive analysis. " "A deterministic candidate-ranking snapshot is also provided when available. Treat higher-ranked " "candidates as preferred because they appeared across more independent scanner streams. " + "Do not recommend stocks outside the gatekeeper universe. " "## THE GOLDEN OVERLAP (apply when Smart Money Report is available and not 'Not available'):\n" "Cross-reference the Smart Money tickers with your macro regime thesis. " "If a Smart Money ticker fits your top-down macro narrative (e.g., an Energy stock with heavy insider " diff --git a/tradingagents/agents/scanners/market_movers_scanner.py b/tradingagents/agents/scanners/market_movers_scanner.py index 1fb85244..89f5a576 100644 --- a/tradingagents/agents/scanners/market_movers_scanner.py +++ b/tradingagents/agents/scanners/market_movers_scanner.py @@ -1,5 +1,5 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder -from tradingagents.agents.utils.scanner_tools import get_market_movers, get_market_indices +from tradingagents.agents.utils.scanner_tools import get_market_indices from tradingagents.agents.utils.tool_runner import run_tool_loop @@ -7,18 +7,17 @@ def create_market_movers_scanner(llm): def market_movers_scanner_node(state): scan_date = state["scan_date"] - tools = [get_market_movers, get_market_indices] + tools = [get_market_indices] system_message = ( - "You are a market analyst scanning for unusual activity and momentum signals. " - "Use get_market_movers to fetch today's top gainers, losers, and most active stocks. " + "You are a market regime analyst scanning for broad index and risk-appetite conditions. " "Use get_market_indices to check major index performance. " "Analyze the results and write a report covering: " - "(1) Unusual movers and potential catalysts, " - "(2) Volume anomalies, " - "(3) Index trends and breadth, " - "(4) Sector concentration in movers. " - "Include a summary table of the most significant moves." + "(1) Index trends and breadth, " + "(2) Risk-on versus risk-off tone, " + "(3) Small-cap versus large-cap participation, " + "(4) Whether the broader tape is supportive for gap continuation trades. " + "Do not use this report to nominate stocks; the gatekeeper universe controls admissible names." ) prompt = ChatPromptTemplate.from_messages( diff --git a/tradingagents/agents/utils/scanner_states.py b/tradingagents/agents/utils/scanner_states.py index 630e0995..68a8075a 100644 --- a/tradingagents/agents/utils/scanner_states.py +++ b/tradingagents/agents/utils/scanner_states.py @@ -27,6 +27,7 @@ class ScannerState(MessagesState): scan_date: str # Phase 1: Parallel scanner outputs — each written by exactly one node + gatekeeper_universe_report: Annotated[str, _last_value] geopolitical_report: Annotated[str, _last_value] market_movers_report: Annotated[str, _last_value] sector_performance_report: Annotated[str, _last_value] diff --git a/tradingagents/api_usage.py b/tradingagents/api_usage.py index 31f220d2..2d4adc9e 100644 --- a/tradingagents/api_usage.py +++ b/tradingagents/api_usage.py @@ -56,10 +56,11 @@ class VendorEstimate: yfinance: int = 0 alpha_vantage: int = 0 finnhub: int = 0 + finviz: int = 0 @property def total(self) -> int: - return self.yfinance + self.alpha_vantage + self.finnhub + return self.yfinance + self.alpha_vantage + self.finnhub + self.finviz @dataclass @@ -94,6 +95,9 @@ def _resolve_vendor(config: dict, method: str) -> str: get_category_for_method, ) + if method == "get_gap_candidates": + return "finviz" + # Tool-level override first tool_vendors = config.get("tool_vendors", {}) if method in tool_vendors: @@ -220,6 +224,8 @@ def estimate_scan(config: dict | None = None) -> UsageEstimate: est.vendor_calls.alpha_vantage += count elif vendor == "finnhub": est.vendor_calls.finnhub += count + elif vendor == "finviz": + est.vendor_calls.finviz += count if vendor not in breakdown: breakdown[vendor] = {} breakdown[vendor][method] = breakdown[vendor].get(method, 0) + count @@ -230,25 +236,28 @@ def estimate_scan(config: dict | None = None) -> UsageEstimate: _add("get_topic_news") est.notes.append(f"Phase 1A (Geopolitical): ~{topic_news_calls} topic news calls") - # Phase 1B: Market Movers Scanner — 3 market_movers + 1 indices - _add("get_market_movers", 3) + # Phase 1B: Gatekeeper universe — 1 bounded yfinance query + _add("get_gatekeeper_universe") + est.notes.append("Phase 1B (Gatekeeper): 1 bounded yfinance universe query") + + # Phase 1C: Market regime scanner — 1 indices call _add("get_market_indices") - est.notes.append("Phase 1B (Market Movers): 3 screener calls + 1 indices call") + est.notes.append("Phase 1C (Market Regime): 1 indices call") - # Phase 1C: Sector Scanner — 1 sector performance + # Phase 1D: Sector Scanner — 1 sector performance _add("get_sector_performance") - est.notes.append("Phase 1C (Sector): 1 sector performance call") + est.notes.append("Phase 1D (Sector): 1 sector performance call") - # Phase 1D: Factor Alignment — bounded global revision/sentiment checks + # Phase 1E: Factor Alignment — bounded global revision/sentiment checks _add("get_topic_news", 2) _add("get_earnings_calendar") - est.notes.append("Phase 1D (Factor Alignment): ~2 topic news + 1 earnings calendar") + est.notes.append("Phase 1E (Factor Alignment): ~2 topic news + 1 earnings calendar") - # Phase 1E: Drift Scanner — bounded global continuation checks + # Phase 1F: Drift Scanner — bounded gap subset + continuation checks _add("get_gap_candidates") _add("get_topic_news") _add("get_earnings_calendar") - est.notes.append("Phase 1E (Drift): 1 live gap scan + ~1 topic news + 1 earnings calendar") + est.notes.append("Phase 1F (Drift): 1 Finviz gap scan + ~1 topic news + 1 earnings calendar") # Phase 2: Industry Deep Dive — ~3 industry perf + ~3 topic news industry_calls = 3 @@ -295,11 +304,13 @@ def estimate_pipeline( est.vendor_calls.yfinance += scan_est.vendor_calls.yfinance est.vendor_calls.alpha_vantage += scan_est.vendor_calls.alpha_vantage est.vendor_calls.finnhub += scan_est.vendor_calls.finnhub + est.vendor_calls.finviz += scan_est.vendor_calls.finviz # Analyze phase × num_tickers est.vendor_calls.yfinance += analyze_est.vendor_calls.yfinance * num_tickers est.vendor_calls.alpha_vantage += analyze_est.vendor_calls.alpha_vantage * num_tickers est.vendor_calls.finnhub += analyze_est.vendor_calls.finnhub * num_tickers + est.vendor_calls.finviz += analyze_est.vendor_calls.finviz * num_tickers # Merge breakdowns merged: dict[str, dict[str, int]] = {} @@ -342,6 +353,8 @@ def format_estimate(est: UsageEstimate) -> str: lines.append(f" Alpha Vantage: {vc.alpha_vantage:>3} calls (free tier: {AV_FREE_DAILY_LIMIT}/day)") if vc.finnhub: lines.append(f" Finnhub: {vc.finnhub:>3} calls (free tier: 60/min)") + if vc.finviz: + lines.append(f" Finviz: {vc.finviz:>3} calls (HTML scrape, bounded use)") lines.append(f" Total: {vc.total:>4} vendor API calls") # Alpha Vantage assessment @@ -382,7 +395,7 @@ def format_vendor_breakdown(summary: dict) -> str: return "" parts: list[str] = [] - for vendor in ("yfinance", "alpha_vantage", "finnhub"): + for vendor in ("yfinance", "alpha_vantage", "finnhub", "finviz"): counts = vendors_used.get(vendor) if counts: ok = counts.get("ok", 0) @@ -391,6 +404,7 @@ def format_vendor_breakdown(summary: dict) -> str: "yfinance": "yfinance", "alpha_vantage": "AV", "finnhub": "Finnhub", + "finviz": "Finviz", }.get(vendor, vendor) parts.append(f"{label}:{ok}ok/{fail}fail") diff --git a/tradingagents/graph/scanner_graph.py b/tradingagents/graph/scanner_graph.py index a535e8de..22cbaa1b 100644 --- a/tradingagents/graph/scanner_graph.py +++ b/tradingagents/graph/scanner_graph.py @@ -5,6 +5,7 @@ from typing import Any, List, Optional from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.llm_clients import create_llm_client from tradingagents.agents.scanners import ( + create_gatekeeper_scanner, create_geopolitical_scanner, create_market_movers_scanner, create_sector_scanner, @@ -20,7 +21,7 @@ from .scanner_setup import ScannerGraphSetup class ScannerGraph: """Orchestrates the macro scanner pipeline. - Phase 1a (parallel): geopolitical_scanner, market_movers_scanner, sector_scanner + Phase 1a (parallel): gatekeeper_scanner, geopolitical_scanner, market_movers_scanner, sector_scanner Phase 1b (bounded global follow-ons): factor_alignment_scanner, smart_money_scanner Phase 1c (after market + sector): drift_scanner Phase 2: industry_deep_dive (fan-in from all Phase 1 nodes) @@ -52,6 +53,7 @@ class ScannerGraph: scan_horizon_days = int(self.config.get("scan_horizon_days", 30)) agents = { + "gatekeeper_scanner": create_gatekeeper_scanner(quick_llm), "geopolitical_scanner": create_geopolitical_scanner(quick_llm), "market_movers_scanner": create_market_movers_scanner(quick_llm), "sector_scanner": create_sector_scanner(quick_llm), @@ -155,6 +157,7 @@ class ScannerGraph: initial_state: dict[str, Any] = { "scan_date": scan_date, "messages": [], + "gatekeeper_universe_report": "", "geopolitical_report": "", "market_movers_report": "", "sector_performance_report": "", diff --git a/tradingagents/graph/scanner_setup.py b/tradingagents/graph/scanner_setup.py index 7aa30056..421653ec 100644 --- a/tradingagents/graph/scanner_setup.py +++ b/tradingagents/graph/scanner_setup.py @@ -9,7 +9,7 @@ class ScannerGraphSetup: """Sets up the scanner graph with LLM agent nodes. Phase 1a (parallel from START): - geopolitical_scanner, market_movers_scanner, sector_scanner + gatekeeper_scanner, geopolitical_scanner, market_movers_scanner, sector_scanner Phase 1b (sequential after sector_scanner): factor_alignment_scanner, smart_money_scanner — bounded global follow-ons that use sector rotation context @@ -24,6 +24,7 @@ class ScannerGraphSetup: Args: agents: Dict mapping node names to agent node functions: - geopolitical_scanner + - gatekeeper_scanner - market_movers_scanner - sector_scanner - factor_alignment_scanner @@ -46,6 +47,7 @@ class ScannerGraphSetup: workflow.add_node(name, node_fn) # Phase 1a: parallel fan-out from START + workflow.add_edge(START, "gatekeeper_scanner") workflow.add_edge(START, "geopolitical_scanner") workflow.add_edge(START, "market_movers_scanner") workflow.add_edge(START, "sector_scanner") @@ -55,8 +57,10 @@ class ScannerGraphSetup: workflow.add_edge("sector_scanner", "smart_money_scanner") workflow.add_edge("sector_scanner", "drift_scanner") workflow.add_edge("market_movers_scanner", "drift_scanner") + workflow.add_edge("gatekeeper_scanner", "drift_scanner") # Fan-in: all Phase 1 nodes must complete before Phase 2 + workflow.add_edge("gatekeeper_scanner", "industry_deep_dive") workflow.add_edge("geopolitical_scanner", "industry_deep_dive") workflow.add_edge("market_movers_scanner", "industry_deep_dive") workflow.add_edge("factor_alignment_scanner", "industry_deep_dive")