feat: wire gatekeeper into scanner graph
This commit is contained in:
parent
7aa76d0061
commit
733a11bd0a
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in New Issue