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 `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
|
- 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
|
- 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
|
# In Progress
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ from tradingagents.api_usage import (
|
||||||
|
|
||||||
class TestVendorEstimate:
|
class TestVendorEstimate:
|
||||||
def test_total(self):
|
def test_total(self):
|
||||||
ve = VendorEstimate(yfinance=10, alpha_vantage=5, finnhub=2)
|
ve = VendorEstimate(yfinance=10, alpha_vantage=5, finnhub=2, finviz=1)
|
||||||
assert ve.total == 17
|
assert ve.total == 18
|
||||||
|
|
||||||
def test_default_zeros(self):
|
def test_default_zeros(self):
|
||||||
ve = VendorEstimate()
|
ve = VendorEstimate()
|
||||||
|
|
@ -157,6 +157,10 @@ class TestEstimateScan:
|
||||||
est = estimate_scan()
|
est = estimate_scan()
|
||||||
assert est.vendor_calls.yfinance > 0
|
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):
|
def test_finnhub_for_calendars(self):
|
||||||
"""Global bounded scanners should add Finnhub earnings-calendar usage."""
|
"""Global bounded scanners should add Finnhub earnings-calendar usage."""
|
||||||
est = estimate_scan()
|
est = estimate_scan()
|
||||||
|
|
@ -220,6 +224,15 @@ class TestFormatEstimate:
|
||||||
est = estimate_analyze()
|
est = estimate_analyze()
|
||||||
text = format_estimate(est)
|
text = format_estimate(est)
|
||||||
assert "yfinance" in text
|
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
|
assert "Total:" in text
|
||||||
|
|
||||||
def test_default_format_includes_av_assessment(self):
|
def test_default_format_includes_av_assessment(self):
|
||||||
|
|
@ -264,12 +277,14 @@ class TestFormatVendorBreakdown:
|
||||||
"yfinance": {"ok": 8, "fail": 1},
|
"yfinance": {"ok": 8, "fail": 1},
|
||||||
"alpha_vantage": {"ok": 3, "fail": 0},
|
"alpha_vantage": {"ok": 3, "fail": 0},
|
||||||
"finnhub": {"ok": 2, "fail": 0},
|
"finnhub": {"ok": 2, "fail": 0},
|
||||||
|
"finviz": {"ok": 1, "fail": 0},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
text = format_vendor_breakdown(summary)
|
text = format_vendor_breakdown(summary)
|
||||||
assert "yfinance:8ok/1fail" in text
|
assert "yfinance:8ok/1fail" in text
|
||||||
assert "AV:3ok/0fail" in text
|
assert "AV:3ok/0fail" in text
|
||||||
assert "Finnhub:2ok/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 (
|
from tradingagents.agents.scanners.factor_alignment_scanner import (
|
||||||
create_factor_alignment_scanner,
|
create_factor_alignment_scanner,
|
||||||
)
|
)
|
||||||
|
from tradingagents.agents.scanners.gatekeeper_scanner import (
|
||||||
|
create_gatekeeper_scanner,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MockRunnable(Runnable):
|
class MockRunnable(Runnable):
|
||||||
|
|
@ -38,11 +41,42 @@ def _base_state():
|
||||||
return {
|
return {
|
||||||
"messages": [HumanMessage(content="Run the market scan.")],
|
"messages": [HumanMessage(content="Run the market scan.")],
|
||||||
"scan_date": "2026-03-27",
|
"scan_date": "2026-03-27",
|
||||||
|
"gatekeeper_universe_report": "| Symbol |\n| NVDA |\n| AAPL |",
|
||||||
"sector_performance_report": "| Sector | 1-Month % |\n| Technology | +5.0% |",
|
"sector_performance_report": "| Sector | 1-Month % |\n| Technology | +5.0% |",
|
||||||
"market_movers_report": "| Symbol | Change % |\n| NVDA | +4.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():
|
def test_factor_alignment_scanner_end_to_end():
|
||||||
llm = MockLLM(
|
llm = MockLLM(
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ def test_extract_rankable_tickers_filters_noise():
|
||||||
|
|
||||||
def test_build_candidate_rankings_rewards_overlap():
|
def test_build_candidate_rankings_rewards_overlap():
|
||||||
state = {
|
state = {
|
||||||
|
"gatekeeper_universe_report": "NVDA AAPL MSFT",
|
||||||
"market_movers_report": "NVDA AAPL",
|
"market_movers_report": "NVDA AAPL",
|
||||||
"smart_money_report": "NVDA",
|
"smart_money_report": "NVDA",
|
||||||
"factor_alignment_report": "NVDA MSFT",
|
"factor_alignment_report": "NVDA MSFT",
|
||||||
|
|
@ -36,3 +37,17 @@ def test_build_candidate_rankings_rewards_overlap():
|
||||||
|
|
||||||
assert ranked[0]["ticker"] == "NVDA"
|
assert ranked[0]["ticker"] == "NVDA"
|
||||||
assert ranked[0]["score"] > ranked[1]["score"]
|
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
|
from tradingagents.graph.scanner_setup import ScannerGraphSetup
|
||||||
|
|
||||||
mock_agents = {
|
mock_agents = {
|
||||||
|
"gatekeeper_scanner": MagicMock(),
|
||||||
"geopolitical_scanner": MagicMock(),
|
"geopolitical_scanner": MagicMock(),
|
||||||
"market_movers_scanner": MagicMock(),
|
"market_movers_scanner": MagicMock(),
|
||||||
"sector_scanner": MagicMock(),
|
"sector_scanner": MagicMock(),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
from .gatekeeper_scanner import create_gatekeeper_scanner
|
||||||
from .geopolitical_scanner import create_geopolitical_scanner
|
from .geopolitical_scanner import create_geopolitical_scanner
|
||||||
from .market_movers_scanner import create_market_movers_scanner
|
from .market_movers_scanner import create_market_movers_scanner
|
||||||
from .sector_scanner import create_sector_scanner
|
from .sector_scanner import create_sector_scanner
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,14 @@ def create_drift_scanner(llm):
|
||||||
scan_date = state["scan_date"]
|
scan_date = state["scan_date"]
|
||||||
tools = [get_gap_candidates, get_topic_news, get_earnings_calendar]
|
tools = [get_gap_candidates, get_topic_news, get_earnings_calendar]
|
||||||
|
|
||||||
|
gatekeeper_context = state.get("gatekeeper_universe_report", "")
|
||||||
market_context = state.get("market_movers_report", "")
|
market_context = state.get("market_movers_report", "")
|
||||||
sector_context = state.get("sector_performance_report", "")
|
sector_context = state.get("sector_performance_report", "")
|
||||||
context_chunks = []
|
context_chunks = []
|
||||||
|
if gatekeeper_context:
|
||||||
|
context_chunks.append(f"Gatekeeper universe:\n{gatekeeper_context}")
|
||||||
if market_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:
|
if sector_context:
|
||||||
context_chunks.append(f"Sector rotation context:\n{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 ""
|
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 = (
|
system_message = (
|
||||||
"You are a drift-window scanner focused on 1-3 month continuation setups. "
|
"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 "
|
"Stay global and bounded: the gatekeeper universe defines the only admissible stock set, and the Finviz "
|
||||||
"look like the start of a sustained drift rather than one-day noise.\n\n"
|
"gap scan provides the event subset within that universe.\n\n"
|
||||||
"You MUST perform these bounded searches:\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"
|
"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"
|
f"3. Call get_earnings_calendar from {start_date.isoformat()} to {end_date.isoformat()}.\n\n"
|
||||||
"Then write a concise report covering:\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"
|
"(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."
|
"(4) the key evidence for continuation risk versus reversal risk."
|
||||||
f"{context_section}"
|
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:
|
### Geopolitical Report:
|
||||||
{state.get("geopolitical_report", "Not available")}
|
{state.get("geopolitical_report", "Not available")}
|
||||||
|
|
||||||
|
### Gatekeeper Universe Report:
|
||||||
|
{state.get("gatekeeper_universe_report", "Not available")}
|
||||||
|
|
||||||
### Market Movers Report:
|
### Market Movers Report:
|
||||||
{state.get("market_movers_report", "Not available")}
|
{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]]:
|
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 = [
|
weighted_sources = [
|
||||||
("market_movers_report", 2, "market_movers"),
|
("market_movers_report", 2, "market_movers"),
|
||||||
("smart_money_report", 2, "smart_money"),
|
("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:
|
for state_key, weight, label in weighted_sources:
|
||||||
tickers = _extract_rankable_tickers(state.get(state_key, ""))
|
tickers = _extract_rankable_tickers(state.get(state_key, ""))
|
||||||
for ticker in tickers:
|
for ticker in tickers:
|
||||||
|
if allowed_tickers and ticker not in allowed_tickers:
|
||||||
|
continue
|
||||||
scores[ticker] += weight
|
scores[ticker] += weight
|
||||||
sources[ticker].append(label)
|
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)
|
ranking_section = "\n\n### Deterministic Candidate Ranking:\n" + "\n".join(ranking_lines)
|
||||||
all_reports_context = f"""## All Scanner and Research Reports
|
all_reports_context = f"""## All Scanner and Research Reports
|
||||||
|
|
||||||
|
### Gatekeeper Universe Report:
|
||||||
|
{state.get("gatekeeper_universe_report", "Not available")}
|
||||||
|
|
||||||
### Geopolitical Report:
|
### Geopolitical Report:
|
||||||
{state.get("geopolitical_report", "Not available")}
|
{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 = (
|
system_message = (
|
||||||
"You are a macro strategist synthesizing all scanner and research reports into a final investment thesis. "
|
"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. "
|
"smart money institutional screener results, and industry deep dive analysis. "
|
||||||
"A deterministic candidate-ranking snapshot is also provided when available. Treat higher-ranked "
|
"A deterministic candidate-ranking snapshot is also provided when available. Treat higher-ranked "
|
||||||
"candidates as preferred because they appeared across more independent scanner streams. "
|
"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"
|
"## 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. "
|
"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 "
|
"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 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
|
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):
|
def market_movers_scanner_node(state):
|
||||||
scan_date = state["scan_date"]
|
scan_date = state["scan_date"]
|
||||||
|
|
||||||
tools = [get_market_movers, get_market_indices]
|
tools = [get_market_indices]
|
||||||
|
|
||||||
system_message = (
|
system_message = (
|
||||||
"You are a market analyst scanning for unusual activity and momentum signals. "
|
"You are a market regime analyst scanning for broad index and risk-appetite conditions. "
|
||||||
"Use get_market_movers to fetch today's top gainers, losers, and most active stocks. "
|
|
||||||
"Use get_market_indices to check major index performance. "
|
"Use get_market_indices to check major index performance. "
|
||||||
"Analyze the results and write a report covering: "
|
"Analyze the results and write a report covering: "
|
||||||
"(1) Unusual movers and potential catalysts, "
|
"(1) Index trends and breadth, "
|
||||||
"(2) Volume anomalies, "
|
"(2) Risk-on versus risk-off tone, "
|
||||||
"(3) Index trends and breadth, "
|
"(3) Small-cap versus large-cap participation, "
|
||||||
"(4) Sector concentration in movers. "
|
"(4) Whether the broader tape is supportive for gap continuation trades. "
|
||||||
"Include a summary table of the most significant moves."
|
"Do not use this report to nominate stocks; the gatekeeper universe controls admissible names."
|
||||||
)
|
)
|
||||||
|
|
||||||
prompt = ChatPromptTemplate.from_messages(
|
prompt = ChatPromptTemplate.from_messages(
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ class ScannerState(MessagesState):
|
||||||
scan_date: str
|
scan_date: str
|
||||||
|
|
||||||
# Phase 1: Parallel scanner outputs — each written by exactly one node
|
# Phase 1: Parallel scanner outputs — each written by exactly one node
|
||||||
|
gatekeeper_universe_report: Annotated[str, _last_value]
|
||||||
geopolitical_report: Annotated[str, _last_value]
|
geopolitical_report: Annotated[str, _last_value]
|
||||||
market_movers_report: Annotated[str, _last_value]
|
market_movers_report: Annotated[str, _last_value]
|
||||||
sector_performance_report: Annotated[str, _last_value]
|
sector_performance_report: Annotated[str, _last_value]
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,11 @@ class VendorEstimate:
|
||||||
yfinance: int = 0
|
yfinance: int = 0
|
||||||
alpha_vantage: int = 0
|
alpha_vantage: int = 0
|
||||||
finnhub: int = 0
|
finnhub: int = 0
|
||||||
|
finviz: int = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self) -> int:
|
def total(self) -> int:
|
||||||
return self.yfinance + self.alpha_vantage + self.finnhub
|
return self.yfinance + self.alpha_vantage + self.finnhub + self.finviz
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|
@ -94,6 +95,9 @@ def _resolve_vendor(config: dict, method: str) -> str:
|
||||||
get_category_for_method,
|
get_category_for_method,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if method == "get_gap_candidates":
|
||||||
|
return "finviz"
|
||||||
|
|
||||||
# Tool-level override first
|
# Tool-level override first
|
||||||
tool_vendors = config.get("tool_vendors", {})
|
tool_vendors = config.get("tool_vendors", {})
|
||||||
if method in 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
|
est.vendor_calls.alpha_vantage += count
|
||||||
elif vendor == "finnhub":
|
elif vendor == "finnhub":
|
||||||
est.vendor_calls.finnhub += count
|
est.vendor_calls.finnhub += count
|
||||||
|
elif vendor == "finviz":
|
||||||
|
est.vendor_calls.finviz += count
|
||||||
if vendor not in breakdown:
|
if vendor not in breakdown:
|
||||||
breakdown[vendor] = {}
|
breakdown[vendor] = {}
|
||||||
breakdown[vendor][method] = breakdown[vendor].get(method, 0) + count
|
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")
|
_add("get_topic_news")
|
||||||
est.notes.append(f"Phase 1A (Geopolitical): ~{topic_news_calls} topic news calls")
|
est.notes.append(f"Phase 1A (Geopolitical): ~{topic_news_calls} topic news calls")
|
||||||
|
|
||||||
# Phase 1B: Market Movers Scanner — 3 market_movers + 1 indices
|
# Phase 1B: Gatekeeper universe — 1 bounded yfinance query
|
||||||
_add("get_market_movers", 3)
|
_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")
|
_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")
|
_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_topic_news", 2)
|
||||||
_add("get_earnings_calendar")
|
_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_gap_candidates")
|
||||||
_add("get_topic_news")
|
_add("get_topic_news")
|
||||||
_add("get_earnings_calendar")
|
_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
|
# Phase 2: Industry Deep Dive — ~3 industry perf + ~3 topic news
|
||||||
industry_calls = 3
|
industry_calls = 3
|
||||||
|
|
@ -295,11 +304,13 @@ def estimate_pipeline(
|
||||||
est.vendor_calls.yfinance += scan_est.vendor_calls.yfinance
|
est.vendor_calls.yfinance += scan_est.vendor_calls.yfinance
|
||||||
est.vendor_calls.alpha_vantage += scan_est.vendor_calls.alpha_vantage
|
est.vendor_calls.alpha_vantage += scan_est.vendor_calls.alpha_vantage
|
||||||
est.vendor_calls.finnhub += scan_est.vendor_calls.finnhub
|
est.vendor_calls.finnhub += scan_est.vendor_calls.finnhub
|
||||||
|
est.vendor_calls.finviz += scan_est.vendor_calls.finviz
|
||||||
|
|
||||||
# Analyze phase × num_tickers
|
# Analyze phase × num_tickers
|
||||||
est.vendor_calls.yfinance += analyze_est.vendor_calls.yfinance * 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.alpha_vantage += analyze_est.vendor_calls.alpha_vantage * num_tickers
|
||||||
est.vendor_calls.finnhub += analyze_est.vendor_calls.finnhub * 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
|
# Merge breakdowns
|
||||||
merged: dict[str, dict[str, int]] = {}
|
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)")
|
lines.append(f" Alpha Vantage: {vc.alpha_vantage:>3} calls (free tier: {AV_FREE_DAILY_LIMIT}/day)")
|
||||||
if vc.finnhub:
|
if vc.finnhub:
|
||||||
lines.append(f" Finnhub: {vc.finnhub:>3} calls (free tier: 60/min)")
|
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")
|
lines.append(f" Total: {vc.total:>4} vendor API calls")
|
||||||
|
|
||||||
# Alpha Vantage assessment
|
# Alpha Vantage assessment
|
||||||
|
|
@ -382,7 +395,7 @@ def format_vendor_breakdown(summary: dict) -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
for vendor in ("yfinance", "alpha_vantage", "finnhub"):
|
for vendor in ("yfinance", "alpha_vantage", "finnhub", "finviz"):
|
||||||
counts = vendors_used.get(vendor)
|
counts = vendors_used.get(vendor)
|
||||||
if counts:
|
if counts:
|
||||||
ok = counts.get("ok", 0)
|
ok = counts.get("ok", 0)
|
||||||
|
|
@ -391,6 +404,7 @@ def format_vendor_breakdown(summary: dict) -> str:
|
||||||
"yfinance": "yfinance",
|
"yfinance": "yfinance",
|
||||||
"alpha_vantage": "AV",
|
"alpha_vantage": "AV",
|
||||||
"finnhub": "Finnhub",
|
"finnhub": "Finnhub",
|
||||||
|
"finviz": "Finviz",
|
||||||
}.get(vendor, vendor)
|
}.get(vendor, vendor)
|
||||||
parts.append(f"{label}:{ok}ok/{fail}fail")
|
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.default_config import DEFAULT_CONFIG
|
||||||
from tradingagents.llm_clients import create_llm_client
|
from tradingagents.llm_clients import create_llm_client
|
||||||
from tradingagents.agents.scanners import (
|
from tradingagents.agents.scanners import (
|
||||||
|
create_gatekeeper_scanner,
|
||||||
create_geopolitical_scanner,
|
create_geopolitical_scanner,
|
||||||
create_market_movers_scanner,
|
create_market_movers_scanner,
|
||||||
create_sector_scanner,
|
create_sector_scanner,
|
||||||
|
|
@ -20,7 +21,7 @@ from .scanner_setup import ScannerGraphSetup
|
||||||
class ScannerGraph:
|
class ScannerGraph:
|
||||||
"""Orchestrates the macro scanner pipeline.
|
"""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 1b (bounded global follow-ons): factor_alignment_scanner, smart_money_scanner
|
||||||
Phase 1c (after market + sector): drift_scanner
|
Phase 1c (after market + sector): drift_scanner
|
||||||
Phase 2: industry_deep_dive (fan-in from all Phase 1 nodes)
|
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))
|
scan_horizon_days = int(self.config.get("scan_horizon_days", 30))
|
||||||
|
|
||||||
agents = {
|
agents = {
|
||||||
|
"gatekeeper_scanner": create_gatekeeper_scanner(quick_llm),
|
||||||
"geopolitical_scanner": create_geopolitical_scanner(quick_llm),
|
"geopolitical_scanner": create_geopolitical_scanner(quick_llm),
|
||||||
"market_movers_scanner": create_market_movers_scanner(quick_llm),
|
"market_movers_scanner": create_market_movers_scanner(quick_llm),
|
||||||
"sector_scanner": create_sector_scanner(quick_llm),
|
"sector_scanner": create_sector_scanner(quick_llm),
|
||||||
|
|
@ -155,6 +157,7 @@ class ScannerGraph:
|
||||||
initial_state: dict[str, Any] = {
|
initial_state: dict[str, Any] = {
|
||||||
"scan_date": scan_date,
|
"scan_date": scan_date,
|
||||||
"messages": [],
|
"messages": [],
|
||||||
|
"gatekeeper_universe_report": "",
|
||||||
"geopolitical_report": "",
|
"geopolitical_report": "",
|
||||||
"market_movers_report": "",
|
"market_movers_report": "",
|
||||||
"sector_performance_report": "",
|
"sector_performance_report": "",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class ScannerGraphSetup:
|
||||||
"""Sets up the scanner graph with LLM agent nodes.
|
"""Sets up the scanner graph with LLM agent nodes.
|
||||||
|
|
||||||
Phase 1a (parallel from START):
|
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):
|
Phase 1b (sequential after sector_scanner):
|
||||||
factor_alignment_scanner, smart_money_scanner — bounded global follow-ons
|
factor_alignment_scanner, smart_money_scanner — bounded global follow-ons
|
||||||
that use sector rotation context
|
that use sector rotation context
|
||||||
|
|
@ -24,6 +24,7 @@ class ScannerGraphSetup:
|
||||||
Args:
|
Args:
|
||||||
agents: Dict mapping node names to agent node functions:
|
agents: Dict mapping node names to agent node functions:
|
||||||
- geopolitical_scanner
|
- geopolitical_scanner
|
||||||
|
- gatekeeper_scanner
|
||||||
- market_movers_scanner
|
- market_movers_scanner
|
||||||
- sector_scanner
|
- sector_scanner
|
||||||
- factor_alignment_scanner
|
- factor_alignment_scanner
|
||||||
|
|
@ -46,6 +47,7 @@ class ScannerGraphSetup:
|
||||||
workflow.add_node(name, node_fn)
|
workflow.add_node(name, node_fn)
|
||||||
|
|
||||||
# Phase 1a: parallel fan-out from START
|
# Phase 1a: parallel fan-out from START
|
||||||
|
workflow.add_edge(START, "gatekeeper_scanner")
|
||||||
workflow.add_edge(START, "geopolitical_scanner")
|
workflow.add_edge(START, "geopolitical_scanner")
|
||||||
workflow.add_edge(START, "market_movers_scanner")
|
workflow.add_edge(START, "market_movers_scanner")
|
||||||
workflow.add_edge(START, "sector_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", "smart_money_scanner")
|
||||||
workflow.add_edge("sector_scanner", "drift_scanner")
|
workflow.add_edge("sector_scanner", "drift_scanner")
|
||||||
workflow.add_edge("market_movers_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
|
# 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("geopolitical_scanner", "industry_deep_dive")
|
||||||
workflow.add_edge("market_movers_scanner", "industry_deep_dive")
|
workflow.add_edge("market_movers_scanner", "industry_deep_dive")
|
||||||
workflow.add_edge("factor_alignment_scanner", "industry_deep_dive")
|
workflow.add_edge("factor_alignment_scanner", "industry_deep_dive")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue