diff --git a/agent_os/frontend/src/components/AgentGraph.tsx b/agent_os/frontend/src/components/AgentGraph.tsx
index c51a5d80..35a8ffc4 100644
--- a/agent_os/frontend/src/components/AgentGraph.tsx
+++ b/agent_os/frontend/src/components/AgentGraph.tsx
@@ -127,9 +127,15 @@ const AgentNode = ({ data }: NodeProps) => {
{data.metrics?.model && data.metrics.model !== 'unknown' && (
-
- {data.metrics.model}
-
+
+
+ {data.metrics.model}
+
+
)}
{/* Running shimmer */}
diff --git a/docs/agent/CURRENT_STATE.md b/docs/agent/CURRENT_STATE.md
index 613f1f42..39bd992e 100644
--- a/docs/agent/CURRENT_STATE.md
+++ b/docs/agent/CURRENT_STATE.md
@@ -1,32 +1,26 @@
# Current Milestone
-AgentOS visual observability layer shipped. Portfolio Manager fully implemented (Phases 1–10). All 725 tests passing (14 skipped). Stop-loss / take-profit fields added to trades.
+Smart Money Scanner added to scanner pipeline (Phase 1b). `finvizfinance` integration with Golden Overlap strategy in macro_synthesis. 18 agent factories. All tests passing (2 pre-existing failures excluded).
# Recent Progress
-- **Stop-loss & Take-profit on trades**: Added `stop_loss` and `take_profit` optional fields to the `Trade` model, SQL migration, PM agent prompt, trade executor, repository, API route, and frontend Trade History tab.
-- **AgentOS (current PR)**: Full-stack visual observability layer for agent execution
- - `agent_os/backend/` — FastAPI backend (port 8088) with REST + WebSocket streaming
- - `agent_os/frontend/` — React + Vite 8 + Chakra UI + ReactFlow dashboard
- - `agent_os/backend/services/langgraph_engine.py` — LangGraph event mapping engine (4 run types: scan, pipeline, portfolio, auto)
- - `agent_os/backend/routes/websocket.py` — WebSocket streaming endpoint (`/ws/stream/{run_id}`)
- - `agent_os/backend/routes/runs.py` — REST run triggers (`POST /api/run/{type}`)
- - `agent_os/backend/routes/portfolios.py` — Portfolio REST API with field mapping (backend models → frontend shape)
- - `agent_os/frontend/src/Dashboard.tsx` — 2-page layout (dashboard + portfolio), agent graph + terminal + controls
- - `agent_os/frontend/src/components/AgentGraph.tsx` — ReactFlow live graph visualization
- - `agent_os/frontend/src/components/PortfolioViewer.tsx` — Holdings, trade history, summary views
- - `agent_os/frontend/src/components/MetricHeader.tsx` — Top-3 metrics (Sharpe, regime, drawdown)
- - `agent_os/frontend/src/hooks/useAgentStream.ts` — WebSocket hook with status tracking
- - `tests/unit/test_langgraph_engine_extraction.py` — 14 tests for event mapping
- - Pipeline recursion limit fix: passes `config={"recursion_limit": propagator.max_recur_limit}` to `astream_events()`
- - Portfolio field mapping fix: shares→quantity, portfolio_id→id, cash→cash_balance, trade_date→executed_at
-- **PR #32 merged**: Portfolio Manager data foundation — models, SQL schema, module scaffolding
-- **Portfolio Manager Phases 2-5** (implemented): risk_evaluator, candidate_prioritizer, trade_executor, holding_reviewer, pm_decision_agent, portfolio_states, portfolio_setup, portfolio_graph
-- **Portfolio CLI integration**: `portfolio`, `check-portfolio`, `auto` commands in `cli/main.py`
+- **Smart Money Scanner (current branch)**: 4th scanner node added to macro pipeline
+ - `tradingagents/agents/scanners/smart_money_scanner.py` — Phase 1b node, runs sequentially after sector_scanner
+ - `tradingagents/agents/utils/scanner_tools.py` — 3 zero-parameter Finviz tools: `get_insider_buying_stocks`, `get_unusual_volume_stocks`, `get_breakout_accumulation_stocks`
+ - `tradingagents/agents/utils/scanner_states.py` — Added `smart_money_report` field with `_last_value` reducer
+ - `tradingagents/graph/scanner_setup.py` — Topology: sector_scanner → smart_money_scanner → industry_deep_dive
+ - `tradingagents/graph/scanner_graph.py` — Instantiates smart_money_scanner with quick_llm
+ - `tradingagents/agents/scanners/macro_synthesis.py` — Golden Overlap instructions + smart_money_report in context
+ - `pyproject.toml` — Added `finvizfinance>=0.14.0` dependency
+ - `docs/agent/decisions/014-finviz-smart-money-scanner.md` — ADR documenting all design decisions
+ - Tests: 6 new mocked tests in `tests/unit/test_scanner_mocked.py`, 1 fix in `tests/unit/test_scanner_graph.py`
+- **AgentOS**: Full-stack visual observability layer (FastAPI + React + ReactFlow)
+- **Portfolio Manager**: Phases 1–10 fully implemented (models, agents, CLI integration, stop-loss/take-profit)
+- **PR #32 merged**: Portfolio Manager data foundation
# In Progress
-- None — PR ready for merge
+- None — branch ready for PR
# Active Blockers
diff --git a/docs/agent/context/ARCHITECTURE.md b/docs/agent/context/ARCHITECTURE.md
index bf92ee63..7b022063 100644
--- a/docs/agent/context/ARCHITECTURE.md
+++ b/docs/agent/context/ARCHITECTURE.md
@@ -1,8 +1,8 @@
-
+
# Architecture
-TradingAgents v0.2.1 is a multi-agent LLM framework using LangGraph. It has 17 agent factory functions, 3 data vendors (yfinance, Alpha Vantage, Finnhub), and 6 LLM providers (OpenAI, Anthropic, Google, xAI, OpenRouter, Ollama).
+TradingAgents v0.2.2 is a multi-agent LLM framework using LangGraph. It has 18 agent factory functions, 4 data vendors (yfinance, Alpha Vantage, Finnhub, Finviz), and 6 LLM providers (OpenAI, Anthropic, Google, xAI, OpenRouter, Ollama).
## 3-Tier LLM System
@@ -36,6 +36,7 @@ Source: `tradingagents/llm_clients/`
| yfinance | Primary (free) | OHLCV, fundamentals, news, screener, sector/industry, indices |
| Alpha Vantage | Fallback | OHLCV, fundamentals, news, sector ETF proxies, market movers |
| Finnhub | Specialized | Insider transactions (primary), earnings calendar, economic calendar |
+| Finviz | Smart Money (best-effort) | Institutional screeners via `finvizfinance` web scraper — insider buys, unusual volume, breakout accumulation; graceful degradation on failure |
Routing: 2-level dispatch — category-level (`data_vendors` config) + tool-level (`tool_vendors` config). Fail-fast by default; only 5 methods in `FALLBACK_ALLOWED` get cross-vendor fallback (ADR 011).
@@ -65,12 +66,16 @@ Source: `tradingagents/graph/trading_graph.py`, `tradingagents/graph/setup.py`
## Scanner Pipeline
```
-START ──┬── Geopolitical Scanner (quick) ──┐
- ├── Market Movers Scanner (quick) ──┼── Industry Deep Dive (mid) ── Macro Synthesis (deep) ── END
- └── Sector Scanner (quick) ─────────┘
+START ──┬── Geopolitical Scanner (quick) ──────────────────────┐
+ ├── Market Movers Scanner (quick) ─────────────────────┤
+ └── Sector Scanner (quick) ── Smart Money Scanner ─────┴── Industry Deep Dive (mid) ── Macro Synthesis (deep) ── END
+ (quick, Finviz)
```
-Phase 1: 3 scanners run in parallel. Phase 2: Deep dive cross-references all outputs, calls `get_industry_performance` per sector. Phase 3: Macro synthesis produces top-10 watchlist as JSON.
+- **Phase 1a** (parallel): geopolitical, market_movers, sector scanners
+- **Phase 1b** (sequential after sector): smart_money_scanner — uses sector rotation context when running Finviz screeners (insider buys, unusual volume, breakout accumulation)
+- **Phase 2**: Industry deep dive cross-references all 4 Phase 1 outputs
+- **Phase 3**: Macro synthesis applies **Golden Overlap** — cross-references smart money tickers with top-down macro thesis to assign high/medium/low conviction; produces top 8-10 watchlist as JSON
Source: `tradingagents/graph/scanner_graph.py`, `tradingagents/graph/scanner_setup.py`
@@ -158,7 +163,7 @@ Full-stack web UI for monitoring and controlling agent execution in real-time.
| Type | REST Trigger | WebSocket Executor | Description |
|------|-------------|-------------------|-------------|
-| `scan` | `POST /api/run/scan` | `run_scan()` | 3-phase macro scanner |
+| `scan` | `POST /api/run/scan` | `run_scan()` | 4-node macro scanner (3 parallel + smart money) |
| `pipeline` | `POST /api/run/pipeline` | `run_pipeline()` | Per-ticker trading analysis |
| `portfolio` | `POST /api/run/portfolio` | `run_portfolio()` | Portfolio manager workflow |
| `auto` | `POST /api/run/auto` | `run_auto()` | Sequential: scan → pipeline → portfolio |
diff --git a/docs/agent/context/COMPONENTS.md b/docs/agent/context/COMPONENTS.md
index c367f6e1..f7071317 100644
--- a/docs/agent/context/COMPONENTS.md
+++ b/docs/agent/context/COMPONENTS.md
@@ -1,4 +1,4 @@
-
+
# Components
@@ -34,6 +34,7 @@ tradingagents/
│ │ ├── geopolitical_scanner.py # create_geopolitical_scanner(llm)
│ │ ├── market_movers_scanner.py # create_market_movers_scanner(llm)
│ │ ├── sector_scanner.py # create_sector_scanner(llm)
+│ │ ├── smart_money_scanner.py # create_smart_money_scanner(llm) ← NEW
│ │ ├── industry_deep_dive.py # create_industry_deep_dive(llm)
│ │ └── macro_synthesis.py # create_macro_synthesis(llm)
│ ├── trader/
@@ -47,7 +48,7 @@ tradingagents/
│ ├── memory.py # FinancialSituationMemory
│ ├── news_data_tools.py # get_news, get_global_news, get_insider_transactions
│ ├── scanner_states.py # ScannerState, _last_value reducer
-│ ├── scanner_tools.py # Scanner @tool definitions (7 tools)
+│ ├── scanner_tools.py # Scanner @tool definitions (10 tools: 7 routed + 3 Finviz direct)
│ ├── technical_indicators_tools.py
│ └── tool_runner.py # run_tool_loop(), MAX_TOOL_ROUNDS, MIN_REPORT_LENGTH
├── dataflows/
@@ -130,7 +131,7 @@ agent_os/
└── PortfolioViewer.tsx # Holdings table, trade history, snapshot view
```
-## Agent Factory Inventory (17 factories + 1 utility)
+## Agent Factory Inventory (18 factories + 1 utility)
| Factory | File | LLM Tier | Extra Params |
|---------|------|----------|-------------|
@@ -149,6 +150,7 @@ agent_os/
| `create_geopolitical_scanner` | `agents/scanners/geopolitical_scanner.py` | quick | — |
| `create_market_movers_scanner` | `agents/scanners/market_movers_scanner.py` | quick | — |
| `create_sector_scanner` | `agents/scanners/sector_scanner.py` | quick | — |
+| `create_smart_money_scanner` | `agents/scanners/smart_money_scanner.py` | quick | — |
| `create_industry_deep_dive` | `agents/scanners/industry_deep_dive.py` | mid | — |
| `create_macro_synthesis` | `agents/scanners/macro_synthesis.py` | deep | — |
| `create_msg_delete` | `agents/utils/agent_utils.py` | — | No LLM param |
@@ -193,7 +195,7 @@ agent_os/
| Command | Function | Description |
|---------|----------|-------------|
| `analyze` | `run_analysis()` | Interactive per-ticker multi-agent analysis with Rich live UI |
-| `scan` | `run_scan(date)` | 3-phase macro scanner, saves 5 report files |
+| `scan` | `run_scan(date)` | Macro scanner (4 Phase-1 nodes + deep dive + synthesis), saves 5 report files |
| `pipeline` | `run_pipeline()` | Full pipeline: scan JSON → filter by conviction → per-ticker deep dive |
## AgentOS Frontend Components
diff --git a/docs/agent/context/GLOSSARY.md b/docs/agent/context/GLOSSARY.md
index 085ab5e4..dd849927 100644
--- a/docs/agent/context/GLOSSARY.md
+++ b/docs/agent/context/GLOSSARY.md
@@ -1,4 +1,4 @@
-
+
# Glossary
@@ -7,7 +7,9 @@
| Term | Definition | Source |
|------|-----------|--------|
| Trading Graph | Full per-ticker analysis pipeline: analysts → debate → trader → risk → decision | `graph/trading_graph.py` |
-| Scanner Graph | 3-phase macro scanner: parallel scanners → deep dive → synthesis | `graph/scanner_graph.py` |
+| Scanner Graph | 4-phase macro scanner: Phase 1a parallel scanners → Phase 1b smart money → deep dive → synthesis | `graph/scanner_graph.py` |
+| Smart Money Scanner | Phase 1b scanner node, runs sequentially after sector_scanner. Runs 3 Finviz screeners (insider buying, unusual volume, breakout accumulation) to surface institutional footprints. Output feeds industry_deep_dive and macro_synthesis. | `agents/scanners/smart_money_scanner.py` |
+| Golden Overlap | Cross-reference strategy in macro_synthesis: if a bottom-up Smart Money ticker also fits the top-down macro thesis, label it high-conviction. Provides dual evidence confirmation for stock selection. | `agents/scanners/macro_synthesis.py` |
| Agent Factory | Closure pattern `create_X(llm)` → returns `_node(state)` function | `agents/analysts/*.py`, `agents/scanners/*.py` |
| ToolNode | LangGraph-native tool executor — used in trading graph for analyst tools | `langgraph.prebuilt`, wired in `graph/setup.py` |
| run_tool_loop | Inline tool executor for scanner agents — iterates up to `MAX_TOOL_ROUNDS` | `agents/utils/tool_runner.py` |
@@ -18,6 +20,11 @@
| Term | Definition | Source |
|------|-----------|--------|
| route_to_vendor | Central dispatch: resolves vendor for a method, calls it, handles fallback for `FALLBACK_ALLOWED` methods | `dataflows/interface.py` |
+| Finviz / finvizfinance | Web scraper library (not official API) for Finviz screener data. Used only in Smart Money Scanner tools. Graceful fallback on any exception — returns error string, never raises. | `agents/utils/scanner_tools.py` |
+| _run_finviz_screen | Shared private helper that runs a Finviz screener with hardcoded filters. Catches all exceptions, returns "Smart money scan unavailable" on failure. All 3 smart money tools delegate to this helper. | `agents/utils/scanner_tools.py` |
+| Insider Buying Screen | Finviz filter: Mid+ market cap, InsiderPurchases > 0%, Volume > 1M. Surfaces stocks where corporate insiders are making open-market purchases. | `agents/utils/scanner_tools.py` |
+| Unusual Volume Screen | Finviz filter: Relative Volume > 2x, Price > $10. Surfaces institutional accumulation/distribution footprints. | `agents/utils/scanner_tools.py` |
+| Breakout Accumulation Screen | Finviz filter: 52-Week High, Relative Volume > 2x, Price > $10. O'Neil CAN SLIM institutional accumulation pattern. | `agents/utils/scanner_tools.py` |
| VENDOR_METHODS | Dict mapping method name → vendor → function reference. Direct function refs, not module paths. | `dataflows/interface.py` |
| FALLBACK_ALLOWED | Set of 5 method names that get cross-vendor fallback: `get_stock_data`, `get_market_indices`, `get_sector_performance`, `get_market_movers`, `get_industry_performance` | `dataflows/interface.py` |
| TOOLS_CATEGORIES | Dict mapping category name → `{"description": str, "tools": list}`. 6 categories: `core_stock_apis`, `technical_indicators`, `fundamental_data`, `news_data`, `scanner_data`, `calendar_data` | `dataflows/interface.py` |
diff --git a/docs/agent/decisions/014-finviz-smart-money-scanner.md b/docs/agent/decisions/014-finviz-smart-money-scanner.md
new file mode 100644
index 00000000..c19ed53e
--- /dev/null
+++ b/docs/agent/decisions/014-finviz-smart-money-scanner.md
@@ -0,0 +1,78 @@
+# ADR 014: Finviz Smart Money Scanner — Phase 1b Bottom-Up Signal Layer
+
+## Status
+
+Accepted
+
+## Context
+
+The macro scanner pipeline produced top-down qualitative analysis (geopolitical events, market movers, sector rotation) but selected stocks entirely from macro reasoning. There was no bottom-up quantitative signal layer to cross-validate candidates. Adding institutional footprint detection (insider buying, unusual volume, breakout accumulation) via `finvizfinance` creates a "Golden Overlap" — stocks confirmed by both top-down macro themes and bottom-up institutional signals carry higher conviction.
+
+Key constraints considered during design:
+
+1. `run_tool_loop()` has `MAX_TOOL_ROUNDS=5`. The market_movers_scanner already uses ~4 rounds. Adding Finviz tools to it would silently truncate at round 5.
+2. `finvizfinance` is a web scraper, not an official API — it can be blocked or rate-limited at any time.
+3. LLMs can hallucinate string parameter values when calling parameterized tools.
+4. Sector rotation context is available from sector_scanner output and should inform smart money interpretation.
+
+## Decisions
+
+### 1. Separate Phase 1b Node (not bolted onto market_movers_scanner)
+
+A dedicated `smart_money_scanner` node avoids the `MAX_TOOL_ROUNDS=5` truncation risk entirely. It runs sequentially after `sector_scanner` (not in the Phase 1a parallel fan-out), giving it access to `sector_performance_report` in state. This context lets the LLM cross-reference institutional footprints against leading/lagging sectors.
+
+Final topology:
+```
+Phase 1a (parallel): START → geopolitical_scanner
+ START → market_movers_scanner
+ START → sector_scanner
+Phase 1b (sequential): sector_scanner → smart_money_scanner
+Phase 2: geopolitical_scanner, market_movers_scanner, smart_money_scanner → industry_deep_dive
+Phase 3: industry_deep_dive → macro_synthesis → END
+```
+
+### 2. Three Zero-Parameter Tools (not one parameterized tool)
+
+Original proposal: `get_smart_money_anomalies(scan_type: str)` with values like `"insider_buying"`.
+
+Problem: LLMs hallucinate string parameter values. The LLM might call `get_smart_money_anomalies("insider_buys")` or `get_smart_money_anomalies("volume_spike")` — strings that have no corresponding filter set.
+
+Solution: Three separate zero-parameter tools:
+- `get_insider_buying_stocks()` — hardcoded insider purchase filters
+- `get_unusual_volume_stocks()` — hardcoded volume anomaly filters
+- `get_breakout_accumulation_stocks()` — hardcoded 52-week high + volume filters
+
+With zero parameters, there is nothing to hallucinate. The LLM selects tools by name from its schema — unambiguous. All three share a `_run_finviz_screen(filters_dict, label)` private helper to keep the implementation DRY.
+
+### 3. Graceful Degradation (never raise)
+
+`finvizfinance` wraps a web scraper that can fail at any time (rate limiting, Finviz HTML changes, network errors). `_run_finviz_screen()` catches all exceptions and returns a string starting with `"Smart money scan unavailable (Finviz error): "`. The pipeline never hard-fails due to Finviz unavailability. `macro_synthesis` is instructed to note the absence and proceed on remaining reports.
+
+### 4. `breakout_accumulation` over `oversold_bounces`
+
+Original proposal included an `oversold_bounces` scan (RSI < 30). This was rejected: RSI < 30 bounces are retail contrarian signals, not smart money signals. Institutions don't systematically buy at RSI < 30. Replaced with `breakout_accumulation` (52-week highs on 2x+ volume) — the O'Neil CAN SLIM institutional accumulation pattern, where institutional buying drives price to new highs on above-average volume.
+
+### 5. Golden Overlap in macro_synthesis
+
+`macro_synthesis` now receives `smart_money_report` alongside the 4 existing reports. The system prompt includes explicit Golden Overlap instructions: if a smart money ticker fits the top-down macro narrative (e.g., an energy stock with heavy insider buying during a supply shock), assign it `"high"` conviction. If no smart money tickers align, proceed on remaining reports. The JSON output schema is unchanged.
+
+## Consequences
+
+- **Pro**: Dual evidence layer — top-down macro + bottom-up institutional signals improve conviction quality
+- **Pro**: Zero hallucination risk — no string parameters in any Finviz tool
+- **Pro**: Pipeline never fails due to Finviz — graceful degradation preserves all other outputs
+- **Pro**: Sector context injection — smart money interpretation is informed by rotation context from sector_scanner
+- **Con**: `finvizfinance` is a web scraper — brittle to Finviz HTML changes; requires periodic maintenance
+- **Con**: Finviz screener results lag real-time institutional data (data is end-of-day); not suitable for intraday signals
+- **Con**: Adds ~620 tokens to scanner pipeline token budget (quick_llm tier, acceptable)
+
+## Source Files
+
+- `tradingagents/agents/scanners/smart_money_scanner.py` (new)
+- `tradingagents/agents/utils/scanner_tools.py` (3 new tools + `_run_finviz_screen` helper)
+- `tradingagents/agents/utils/scanner_states.py` (`smart_money_report` field)
+- `tradingagents/graph/scanner_setup.py` (Phase 1b topology)
+- `tradingagents/graph/scanner_graph.py` (agent instantiation)
+- `tradingagents/agents/scanners/macro_synthesis.py` (Golden Overlap prompt)
+- `pyproject.toml` (`finvizfinance>=0.14.0`)
+- `tests/unit/test_scanner_mocked.py` (6 new tests for Finviz tools)
diff --git a/docs/agent_dataflow.md b/docs/agent_dataflow.md
index b4ea991b..110f2ab5 100644
--- a/docs/agent_dataflow.md
+++ b/docs/agent_dataflow.md
@@ -30,8 +30,9 @@ used by every agent.
- [4.13 Geopolitical Scanner](#413-geopolitical-scanner)
- [4.14 Market Movers Scanner](#414-market-movers-scanner)
- [4.15 Sector Scanner](#415-sector-scanner)
- - [4.16 Industry Deep Dive](#416-industry-deep-dive)
- - [4.17 Macro Synthesis](#417-macro-synthesis)
+ - [4.16 Smart Money Scanner](#416-smart-money-scanner)
+ - [4.17 Industry Deep Dive](#417-industry-deep-dive)
+ - [4.18 Macro Synthesis](#418-macro-synthesis)
5. [Tool → Data-Source Mapping](#5-tool--data-source-mapping)
6. [Memory System](#6-memory-system)
7. [Tool Data Formats & Sizes](#7-tool-data-formats--sizes)
@@ -73,8 +74,9 @@ All are overridable via `TRADINGAGENTS_` env vars.
| 13 | Geopolitical Scanner | **Quick** | ✅ | — | `run_tool_loop()` |
| 14 | Market Movers Scanner | **Quick** | ✅ | — | `run_tool_loop()` |
| 15 | Sector Scanner | **Quick** | ✅ | — | `run_tool_loop()` |
-| 16 | Industry Deep Dive | **Mid** | ✅ | — | `run_tool_loop()` |
-| 17 | Macro Synthesis | **Deep** | — | — | — |
+| 16 | Smart Money Scanner | **Quick** | ✅ | — | `run_tool_loop()` |
+| 17 | Industry Deep Dive | **Mid** | ✅ | — | `run_tool_loop()` |
+| 18 | Macro Synthesis | **Deep** | — | — | — |
---
@@ -186,68 +188,97 @@ All are overridable via `TRADINGAGENTS_` env vars.
## 3. Scanner Pipeline Flow
```
- ┌─────────────────────────┐
- │ START │
- │ (scan_date) │
- └────────────┬─────────────┘
+ ┌─────────────────────────┐
+ │ START │
+ │ (scan_date) │
+ └────────────┬─────────────┘
+ │
+ ┌─────────────────────────────────┼──────────────────────────────────┐
+ ▼ ▼ ▼
+┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
+│ Geopolitical │ │ Market Movers │ │ Sector Scanner │
+│ Scanner │ │ Scanner │ │ │
+│ (quick_think) │ │ (quick_think) │ │ (quick_think) │
+│ │ │ │ │ │
+│ Tools: │ │ Tools: │ │ Tools: │
+│ • get_topic_news │ │ • get_market_ │ │ • get_sector_ │
+│ │ │ movers │ │ performance │
+│ Output: │ │ • get_market_ │ │ │
+│ geopolitical_rpt │ │ indices │ │ Output: │
+│ │ │ │ │ sector_perf_rpt │
+│ │ │ Output: │ │ │
+│ │ │ market_movers_rpt│ │ │ │
+└────────┬─────────┘ └────────┬─────────┘ └────────┼─────────┘
+ │ │ │
+ │ │ ┌───────────────┘
+ │ │ ▼ (sector data available)
+ │ │ ┌──────────────────────────┐
+ │ │ │ Smart Money Scanner │
+ │ │ │ (quick_think) │
+ │ │ │ │
+ │ │ │ Context: sector_perf_rpt │
+ │ │ │ │
+ │ │ │ Tools (no params): │
+ │ │ │ • get_insider_buying_ │
+ │ │ │ stocks │
+ │ │ │ • get_unusual_volume_ │
+ │ │ │ stocks │
+ │ │ │ • get_breakout_ │
+ │ │ │ accumulation_stocks │
+ │ │ │ │
+ │ │ │ Output: │
+ │ │ │ smart_money_report │
+ │ │ └──────────┬───────────────┘
+ │ │ │
+ └─────────────────────────────┼──────────────┘
+ │ (Phase 1 → Phase 2, all 4 reports)
+ ▼
+ ┌─────────────────────────────┐
+ │ Industry Deep Dive │
+ │ (mid_think) │
+ │ │
+ │ Reads: all 4 Phase-1 reports │
+ │ Auto-extracts top 3 sectors │
+ │ │
+ │ Tools: │
+ │ • get_industry_performance │
+ │ (called per top sector) │
+ │ • get_topic_news │
+ │ (sector-specific searches) │
+ │ │
+ │ Output: │
+ │ industry_deep_dive_report │
+ └──────────────┬───────────────┘
+ │ (Phase 2 → Phase 3)
+ ▼
+ ┌─────────────────────────────┐
+ │ Macro Synthesis │
+ │ (deep_think) │
+ │ │
+ │ Reads: all 5 prior reports │
+ │ Golden Overlap: cross-refs │
+ │ smart money tickers with │
+ │ top-down macro thesis │
+ │ No tools – pure LLM reasoning│
+ │ │
+ │ Output: │
+ │ macro_scan_summary (JSON) │
+ │ Top 8-10 stock candidates │
+ │ with conviction & catalysts │
+ └──────────────┬───────────────┘
│
- ┌────────────────────────────┼────────────────────────────┐
- ▼ ▼ ▼
-┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
-│ Geopolitical │ │ Market Movers │ │ Sector Scanner │
-│ Scanner │ │ Scanner │ │ │
-│ (quick_think) │ │ (quick_think) │ │ (quick_think) │
-│ │ │ │ │ │
-│ Tools: │ │ Tools: │ │ Tools: │
-│ • get_topic_news │ │ • get_market_ │ │ • get_sector_ │
-│ │ │ movers │ │ performance │
-│ Output: │ │ • get_market_ │ │ │
-│ geopolitical_rpt │ │ indices │ │ Output: │
-│ │ │ │ │ sector_perf_rpt │
-│ │ │ Output: │ │ │
-│ │ │ market_movers_rpt│ │ │
-└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
- │ │ │
- └────────────────────────┼─────────────────────────┘
- │ (Phase 1 → Phase 2)
- ▼
- ┌─────────────────────────────┐
- │ Industry Deep Dive │
- │ (mid_think) │
- │ │
- │ Reads: all 3 Phase-1 reports │
- │ Auto-extracts top 3 sectors │
- │ │
- │ Tools: │
- │ • get_industry_performance │
- │ (called per top sector) │
- │ • get_topic_news │
- │ (sector-specific searches) │
- │ │
- │ Output: │
- │ industry_deep_dive_report │
- └──────────────┬───────────────┘
- │ (Phase 2 → Phase 3)
- ▼
- ┌─────────────────────────────┐
- │ Macro Synthesis │
- │ (deep_think) │
- │ │
- │ Reads: all 4 prior reports │
- │ No tools – pure LLM reasoning│
- │ │
- │ Output: │
- │ macro_scan_summary (JSON) │
- │ Top 8-10 stock candidates │
- │ with conviction & catalysts │
- └──────────────┬───────────────┘
- │
- ▼
- ┌───────────────┐
- │ END │
- └───────────────┘
+ ▼
+ ┌───────────────┐
+ │ END │
+ └───────────────┘
```
+**Graph Topology Notes:**
+- **Phase 1a** (parallel from START): geopolitical, market_movers, sector scanners
+- **Phase 1b** (sequential after sector): smart_money_scanner — runs after sector data is available so it can use sector rotation context when interpreting Finviz signals
+- **Phase 2** (fan-in from all 4 Phase 1 nodes): industry_deep_dive
+- **Phase 3**: macro_synthesis with Golden Overlap strategy
+
---
## 4. Per-Agent Data Flows
@@ -1072,7 +1103,75 @@ Round 1 ≈ 17 KB (~4,250 tokens), Round 2 ≈ 31 KB (~7,800 tokens).
---
-### 4.16 Industry Deep Dive
+### 4.16 Smart Money Scanner
+
+| | |
+|---|---|
+| **File** | `agents/scanners/smart_money_scanner.py` |
+| **Factory** | `create_smart_money_scanner(llm)` |
+| **Thinking Modality** | **Quick** (`quick_think_llm`, default `gpt-5-mini`) |
+| **Tool Execution** | `run_tool_loop()` |
+| **Graph position** | Sequential after `sector_scanner` (Phase 1b) |
+
+**Data Flow:**
+
+```
+ State Input: scan_date
+ + sector_performance_report ← injected from sector_scanner (available
+ because this node runs after it)
+ │
+ ▼
+ Tool calls via run_tool_loop():
+ (All three tools have NO parameters — filters are hardcoded.
+ The LLM calls each tool by name; nothing to hallucinate.)
+
+ 1. get_insider_buying_stocks()
+ → Mid/Large cap stocks with positive insider purchases, volume > 1M
+ → Filters: InsiderPurchases=Positive, MarketCap=+Mid, Volume=Over 1M
+ Data source: Finviz screener (web scraper, graceful fallback on error)
+
+ 2. get_unusual_volume_stocks()
+ → Stocks trading at 2x+ normal volume today, price > $10
+ → Filters: RelativeVolume=Over 2, Price=Over $10
+ Data source: Finviz screener
+
+ 3. get_breakout_accumulation_stocks()
+ → Stocks at 52-week highs on 2x+ volume (O'Neil CAN SLIM pattern)
+ → Filters: Performance2=52-Week High, RelativeVolume=Over 2, Price=Over $10
+ Data source: Finviz screener
+ │
+ ▼
+ LLM Prompt (quick_think):
+ "Hunt for Smart Money institutional footprints. Call all three tools.
+ Use sector rotation context to prioritize tickers from leading sectors.
+ Flag signals that confirm or contradict sector trends.
+ Report: 5-8 tickers with scan source, sector, and anomaly explanation."
+
+ Context: sector_performance_report + 3 Finviz tool results
+ │
+ ▼
+ Output: smart_money_report
+```
+
+**Hallucination Safety:** Each Finviz tool is a zero-parameter `@tool`. Filters
+are hardcoded inside the helper `_run_finviz_screen()`. If Finviz is unavailable
+(rate-limited, scraped HTML changed), each tool returns
+`"Smart money scan unavailable (Finviz error): "` — the pipeline never fails.
+
+**Prompt Size Budget:**
+
+| Component | Data Type | Format | Avg Size | Avg Tokens |
+|-----------|-----------|--------|----------|------------|
+| System prompt | Text | Instructions | ~0.7 KB | ~175 |
+| Sector performance report (injected) | Markdown | Table (11 sectors) | ~0.9 KB | ~220 |
+| `get_insider_buying_stocks` result | Markdown | 5-row ticker list | ~0.3 KB | ~75 |
+| `get_unusual_volume_stocks` result | Markdown | 5-row ticker list | ~0.3 KB | ~75 |
+| `get_breakout_accumulation_stocks` result | Markdown | 5-row ticker list | ~0.3 KB | ~75 |
+| **Total prompt** | | | **~2.5 KB** | **~620** |
+
+---
+
+### 4.17 Industry Deep Dive
| | |
|---|---|
@@ -1087,9 +1186,10 @@ Round 1 ≈ 17 KB (~4,250 tokens), Round 2 ≈ 31 KB (~7,800 tokens).
┌─────────────────────────────────────────────────────┐
│ State Input: │
│ • scan_date │
- │ • geopolitical_report (Phase 1) │
- │ • market_movers_report (Phase 1) │
- │ • sector_performance_report (Phase 1) │
+ │ • geopolitical_report (Phase 1a) │
+ │ • market_movers_report (Phase 1a) │
+ │ • sector_performance_report (Phase 1a) │
+ │ • smart_money_report (Phase 1b) │
└────────────────────────┬────────────────────────────┘
│
▼
@@ -1139,14 +1239,14 @@ Round 1 ≈ 17 KB (~4,250 tokens), Round 2 ≈ 31 KB (~7,800 tokens).
| Component | Data Type | Format | Avg Size | Avg Tokens |
|-----------|-----------|--------|----------|------------|
| System prompt | Text | Instructions + sector list | ~1 KB | ~250 |
-| Phase 1 context (3 reports) | Text | Concatenated Markdown | ~6 KB | ~1,500 |
+| Phase 1 context (4 reports) | Text | Concatenated Markdown | ~8 KB | ~2,000 |
| `get_industry_performance` × 3 | Markdown | Tables (10–15 companies each) | ~7.5 KB | ~1,875 |
| `get_topic_news` × 2 | Markdown | Article lists (10 articles each) | ~5 KB | ~1,250 |
-| **Total prompt** | | | **~20 KB** | **~4,875** |
+| **Total prompt** | | | **~21.5 KB** | **~5,375** |
---
-### 4.17 Macro Synthesis
+### 4.18 Macro Synthesis
| | |
|---|---|
@@ -1160,15 +1260,20 @@ Round 1 ≈ 17 KB (~4,250 tokens), Round 2 ≈ 31 KB (~7,800 tokens).
```
┌─────────────────────────────────────────────────────┐
│ State Input: │
- │ • geopolitical_report (Phase 1) │
- │ • market_movers_report (Phase 1) │
- │ • sector_performance_report (Phase 1) │
+ │ • geopolitical_report (Phase 1a) │
+ │ • market_movers_report (Phase 1a) │
+ │ • sector_performance_report (Phase 1a) │
+ │ • smart_money_report (Phase 1b) ← NEW │
│ • industry_deep_dive_report (Phase 2) │
└────────────────────────┬────────────────────────────┘
│
▼
LLM Prompt (deep_think):
"Synthesize all reports into final investment thesis.
+ GOLDEN OVERLAP: Cross-reference Smart Money tickers with macro thesis.
+ If a Smart Money ticker fits the top-down narrative (e.g., Energy stock
+ with heavy insider buying during an oil shortage) → label conviction 'high'.
+ If no Smart Money tickers fit → select best from other reports.
Output ONLY valid JSON (no markdown, no preamble).
Structure:
{
@@ -1180,7 +1285,7 @@ Round 1 ≈ 17 KB (~4,250 tokens), Round 2 ≈ 31 KB (~7,800 tokens).
risk_factors
}"
- Context: all 4 prior reports concatenated
+ Context: all 5 prior reports concatenated
│
▼
Post-processing (Python, no LLM):
@@ -1194,12 +1299,13 @@ Round 1 ≈ 17 KB (~4,250 tokens), Round 2 ≈ 31 KB (~7,800 tokens).
| Component | Data Type | Format | Avg Size | Avg Tokens |
|-----------|-----------|--------|----------|------------|
-| System prompt | Text | Instructions + JSON schema | ~1.3 KB | ~325 |
-| Geopolitical report (Phase 1) | Text | Markdown report | ~3 KB | ~750 |
-| Market movers report (Phase 1) | Text | Markdown report | ~3 KB | ~750 |
-| Sector performance report (Phase 1) | Text | Markdown report | ~2 KB | ~500 |
+| System prompt | Text | Instructions + JSON schema + Golden Overlap | ~1.5 KB | ~375 |
+| Geopolitical report (Phase 1a) | Text | Markdown report | ~3 KB | ~750 |
+| Market movers report (Phase 1a) | Text | Markdown report | ~3 KB | ~750 |
+| Sector performance report (Phase 1a) | Text | Markdown report | ~2 KB | ~500 |
+| Smart money report (Phase 1b) | Text | Markdown report | ~2 KB | ~500 |
| Industry deep dive report (Phase 2) | Text | Markdown report | ~8 KB | ~2,000 |
-| **Total prompt** | | | **~17 KB** | **~4,325** |
+| **Total prompt** | | | **~19.5 KB** | **~4,875** |
**Output:** Valid JSON (~3–5 KB, ~750–1,250 tokens).
@@ -1239,9 +1345,17 @@ dispatches to the configured vendor.
| `get_topic_news` | scanner_data | yfinance | — | Topic news |
| `get_earnings_calendar` | calendar_data | **Finnhub** | — | Earnings cal. |
| `get_economic_calendar` | calendar_data | **Finnhub** | — | Econ cal. |
+| `get_insider_buying_stocks` | *(Finviz direct)* | **Finviz** | graceful string | Insider buys |
+| `get_unusual_volume_stocks` | *(Finviz direct)* | **Finviz** | graceful string | Vol anomalies |
+| `get_breakout_accumulation_stocks` | *(Finviz direct)* | **Finviz** | graceful string | Breakouts |
> **Fallback rules** (ADR 011): Only 5 methods in `FALLBACK_ALLOWED` get
> cross-vendor fallback. All others fail-fast on error.
+>
+> **Finviz tools** bypass `route_to_vendor()` — they call `finvizfinance` directly
+> via the shared `_run_finviz_screen()` helper. Errors return a graceful string
+> starting with `"Smart money scan unavailable"` so the pipeline never hard-fails.
+> `finvizfinance` is a web scraper, not an official API — treat it as best-effort.
---
@@ -1326,6 +1440,9 @@ typical size, and any truncation limits for each tool.
| `get_topic_news` | Markdown (article list) | ~2.5 KB | ~625 | 10 articles (default) | Configurable limit |
| `get_earnings_calendar` | Markdown (table) | ~3 KB | ~750 | 20–50+ events | All events in date range |
| `get_economic_calendar` | Markdown (table) | ~2.5 KB | ~625 | 5–15 events | All events in date range |
+| `get_insider_buying_stocks` | Markdown (list) | ~0.3 KB | ~75 | Top 5 stocks | Hard limit: top 5 by volume; returns error string on Finviz failure |
+| `get_unusual_volume_stocks` | Markdown (list) | ~0.3 KB | ~75 | Top 5 stocks | Hard limit: top 5 by volume; returns error string on Finviz failure |
+| `get_breakout_accumulation_stocks` | Markdown (list) | ~0.3 KB | ~75 | Top 5 stocks | Hard limit: top 5 by volume; returns error string on Finviz failure |
### Non-Tool Data Injected into Prompts
@@ -1377,8 +1494,9 @@ the context windows of popular models to identify potential overflow risks.
| 13 | Geopolitical Scanner | Quick | ~2,150 tok | ~3,000 tok | 2% | ✅ Safe |
| 14 | Market Movers Scanner | Quick | ~1,525 tok | ~2,000 tok | 1–2% | ✅ Safe |
| 15 | Sector Scanner | Quick | ~345 tok | ~500 tok | <1% | ✅ Safe |
-| 16 | Industry Deep Dive | Mid | ~4,875 tok | ~7,000 tok | 4–5% | ✅ Safe |
-| 17 | Macro Synthesis | Deep | ~4,325 tok | ~6,500 tok | 3–5% | ✅ Safe |
+| 16 | Smart Money Scanner | Quick | ~620 tok | ~900 tok | <1% | ✅ Safe |
+| 17 | Industry Deep Dive | Mid | ~5,375 tok | ~7,500 tok | 4–6% | ✅ Safe |
+| 18 | Macro Synthesis | Deep | ~4,875 tok | ~7,000 tok | 4–5% | ✅ Safe |
> **†Peak Prompt** = estimate with `max_debate_rounds=3` or maximum optional
> tool calls. All agents are well within the 128K context window.
@@ -1451,37 +1569,41 @@ TOTAL INPUT TOKENS (single company): ~98,400
```
Phase Calls Avg Tokens (per call) Subtotal
─────────────────────────────────────────────────────────────────────────
-1. PHASE 1 SCANNERS (parallel)
+1a. PHASE 1 SCANNERS (parallel from START)
Geopolitical Scanner 1 ~2,150 ~2,150
Market Movers Scanner 1 ~1,525 ~1,525
Sector Scanner 1 ~345 ~345
- Phase 1: ~4,020
+ Phase 1a: ~4,020
+
+1b. SMART MONEY (sequential after Sector Scanner)
+ Smart Money Scanner 1 ~620 ~620
+ Phase 1b: ~620
2. PHASE 2
- Industry Deep Dive 1 ~4,875 ~4,875
- Phase 2: ~4,875
+ Industry Deep Dive 1 ~5,375 ~5,375
+ Phase 2: ~5,375
3. PHASE 3
- Macro Synthesis 1 ~4,325 ~4,325
- Phase 3: ~4,325
+ Macro Synthesis 1 ~4,875 ~4,875
+ Phase 3: ~4,875
═══════════════════════════════════════════════════════════════════════════
-TOTAL INPUT TOKENS (market scan): ~13,220
+TOTAL INPUT TOKENS (market scan): ~14,890
═══════════════════════════════════════════════════════════════════════════
```
-> Scanner output tokens ≈ 5,000–8,000 additional.
-> **Grand total (input + output) ≈ 18,000–21,000 tokens per scan.**
+> Scanner output tokens ≈ 6,000–9,000 additional.
+> **Grand total (input + output) ≈ 21,000–24,000 tokens per scan.**
### Full Pipeline (Scan → Per-Ticker Deep Dives)
When running the `pipeline` command (scan + per-ticker analysis for top picks):
```
-Scanner pipeline: ~13,220 input tokens
+Scanner pipeline: ~14,890 input tokens
+ N company analyses (N = 8–10 picks): ~98,400 × N input tokens
───────────────────────────────────────────────────────────────────
-Example (10 companies): ~997,220 input tokens
+Example (10 companies): ~998,890 input tokens
≈ 1.0M total tokens (input + output)
```
@@ -1502,5 +1624,5 @@ Example (10 companies): ~997,220 input tokens
cannot accommodate debate agents beyond round 1. Use
`max_debate_rounds=1` for such models.
-5. **Cost optimization**: The scanner pipeline uses ~13K tokens total —
- roughly 7× cheaper than a single company analysis.
+5. **Cost optimization**: The scanner pipeline uses ~15K tokens total —
+ roughly 6-7× cheaper than a single company analysis.
diff --git a/pyproject.toml b/pyproject.toml
index 1e5ac176..f56ed56d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,7 @@ dependencies = [
"tqdm>=4.67.1",
"typing-extensions>=4.14.0",
"yfinance>=0.2.63",
+ "finvizfinance>=0.14.0",
"psycopg2-binary>=2.9.11",
"fastapi>=0.115.9",
"uvicorn>=0.34.3",
diff --git a/tests/unit/test_scanner_graph.py b/tests/unit/test_scanner_graph.py
index 6d7a5ad8..6c75d581 100644
--- a/tests/unit/test_scanner_graph.py
+++ b/tests/unit/test_scanner_graph.py
@@ -44,6 +44,7 @@ def test_scanner_setup_compiles_graph():
"geopolitical_scanner": MagicMock(),
"market_movers_scanner": MagicMock(),
"sector_scanner": MagicMock(),
+ "smart_money_scanner": MagicMock(),
"industry_deep_dive": MagicMock(),
"macro_synthesis": MagicMock(),
}
diff --git a/tests/unit/test_scanner_mocked.py b/tests/unit/test_scanner_mocked.py
index 39a21751..6db33dfe 100644
--- a/tests/unit/test_scanner_mocked.py
+++ b/tests/unit/test_scanner_mocked.py
@@ -727,3 +727,94 @@ class TestScannerRouting:
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"
diff --git a/tradingagents/agents/scanners/__init__.py b/tradingagents/agents/scanners/__init__.py
index 1279e61e..1fa350eb 100644
--- a/tradingagents/agents/scanners/__init__.py
+++ b/tradingagents/agents/scanners/__init__.py
@@ -1,5 +1,6 @@
from .geopolitical_scanner import create_geopolitical_scanner
from .market_movers_scanner import create_market_movers_scanner
from .sector_scanner import create_sector_scanner
+from .smart_money_scanner import create_smart_money_scanner
from .industry_deep_dive import create_industry_deep_dive
from .macro_synthesis import create_macro_synthesis
diff --git a/tradingagents/agents/scanners/macro_synthesis.py b/tradingagents/agents/scanners/macro_synthesis.py
index b29d517f..2ae76162 100644
--- a/tradingagents/agents/scanners/macro_synthesis.py
+++ b/tradingagents/agents/scanners/macro_synthesis.py
@@ -13,6 +13,7 @@ def create_macro_synthesis(llm):
scan_date = state["scan_date"]
# Inject all previous reports for synthesis — no tools, pure LLM reasoning
+ smart_money = state.get("smart_money_report", "") or "Not available"
all_reports_context = f"""## All Scanner and Research Reports
### Geopolitical Report:
@@ -24,6 +25,9 @@ def create_macro_synthesis(llm):
### Sector Performance Report:
{state.get("sector_performance_report", "Not available")}
+### Smart Money Report (Finviz institutional screeners):
+{smart_money}
+
### Industry Deep Dive Report:
{state.get("industry_deep_dive_report", "Not available")}
"""
@@ -31,8 +35,13 @@ def create_macro_synthesis(llm):
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, "
- "and industry deep dive analysis. "
- "Synthesize these into a structured output with: "
+ "smart money institutional screener results, and industry deep dive analysis. "
+ "## 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 "
+ "buying during an oil shortage), prioritize it as a top candidate and label its conviction as 'high'. "
+ "If no Smart Money tickers fit the macro narrative, proceed with the best candidates from other reports.\n\n"
+ "Synthesize all reports into a structured output with: "
"(1) Executive summary of the macro environment, "
"(2) Top macro themes with conviction levels, "
"(3) A list of 8-10 specific stocks worth investigating with ticker, name, sector, rationale, "
diff --git a/tradingagents/agents/scanners/smart_money_scanner.py b/tradingagents/agents/scanners/smart_money_scanner.py
new file mode 100644
index 00000000..79522509
--- /dev/null
+++ b/tradingagents/agents/scanners/smart_money_scanner.py
@@ -0,0 +1,81 @@
+"""Smart Money Scanner — runs sequentially after sector_scanner.
+
+Runs three Finviz screeners to find institutional footprints:
+ 1. Insider buying (open-market purchases by insiders)
+ 2. Unusual volume (2x+ normal, price > $10)
+ 3. Breakout accumulation (52-week highs on 2x+ volume)
+
+Positioned after sector_scanner so it can use sector rotation data as context
+when interpreting and prioritizing Finviz signals. Each screener tool has no
+parameters — filters are hardcoded to prevent LLM hallucinations.
+"""
+
+from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
+
+from tradingagents.agents.utils.scanner_tools import (
+ get_breakout_accumulation_stocks,
+ get_insider_buying_stocks,
+ get_unusual_volume_stocks,
+)
+from tradingagents.agents.utils.tool_runner import run_tool_loop
+
+
+def create_smart_money_scanner(llm):
+ def smart_money_scanner_node(state):
+ scan_date = state["scan_date"]
+ tools = [
+ get_insider_buying_stocks,
+ get_unusual_volume_stocks,
+ get_breakout_accumulation_stocks,
+ ]
+
+ # Inject sector rotation context — available because this node runs
+ # after sector_scanner completes.
+ sector_context = state.get("sector_performance_report", "")
+ sector_section = (
+ f"\n\nSector rotation context from the Sector Scanner:\n{sector_context}"
+ if sector_context
+ else ""
+ )
+
+ system_message = (
+ "You are a quantitative analyst hunting for 'Smart Money' institutional footprints in today's market. "
+ "You MUST call all three of these tools exactly once each:\n"
+ "1. `get_insider_buying_stocks` — insider open-market purchases\n"
+ "2. `get_unusual_volume_stocks` — stocks trading at 2x+ normal volume\n"
+ "3. `get_breakout_accumulation_stocks` — institutional breakout accumulation pattern\n\n"
+ "After running all three scans, write a concise report highlighting the best 5 to 8 specific tickers "
+ "you found. For each ticker, state: which scan flagged it, its sector, and why it is anomalous "
+ "(e.g., 'XYZ has heavy insider buying in a sector that is showing strong rotation momentum'). "
+ "Use the sector rotation context below to prioritize tickers from leading sectors and flag any "
+ "smart money signals that confirm or contradict the sector trend. "
+ "If any scan returned unavailable or empty, note it briefly and focus on the remaining results. "
+ "This report will be used by the Macro Strategist to identify high-conviction candidates via the "
+ "Golden Overlap (bottom-up smart money signals cross-referenced with top-down macro themes)."
+ f"{sector_section}"
+ )
+
+ prompt = ChatPromptTemplate.from_messages(
+ [
+ (
+ "system",
+ "You are a helpful AI assistant, collaborating with other assistants.\n{system_message}"
+ "\nFor your reference, the current date is {current_date}.",
+ ),
+ MessagesPlaceholder(variable_name="messages"),
+ ]
+ )
+ prompt = prompt.partial(system_message=system_message)
+ prompt = prompt.partial(current_date=scan_date)
+
+ chain = prompt | llm.bind_tools(tools)
+ result = run_tool_loop(chain, state["messages"], tools)
+ report = result.content or ""
+
+ return {
+ "messages": [result],
+ "smart_money_report": report,
+ "sender": "smart_money_scanner",
+ }
+
+ return smart_money_scanner_node
diff --git a/tradingagents/agents/utils/scanner_states.py b/tradingagents/agents/utils/scanner_states.py
index 07795a6a..3dd60930 100644
--- a/tradingagents/agents/utils/scanner_states.py
+++ b/tradingagents/agents/utils/scanner_states.py
@@ -31,6 +31,7 @@ class ScannerState(MessagesState):
geopolitical_report: Annotated[str, _last_value]
market_movers_report: Annotated[str, _last_value]
sector_performance_report: Annotated[str, _last_value]
+ smart_money_report: Annotated[str, _last_value]
# Phase 2: Deep dive output
industry_deep_dive_report: Annotated[str, _last_value]
diff --git a/tradingagents/agents/utils/scanner_tools.py b/tradingagents/agents/utils/scanner_tools.py
index c3d9f9ac..b00738ae 100644
--- a/tradingagents/agents/utils/scanner_tools.py
+++ b/tradingagents/agents/utils/scanner_tools.py
@@ -1,9 +1,14 @@
"""Scanner tools for market-wide analysis."""
-from langchain_core.tools import tool
+import logging
from typing import Annotated
+
+from langchain_core.tools import tool
+
from tradingagents.dataflows.interface import route_to_vendor
+logger = logging.getLogger(__name__)
+
@tool
def get_market_movers(
@@ -111,3 +116,87 @@ def get_economic_calendar(
Unique Finnhub capability not available in Alpha Vantage.
"""
return route_to_vendor("get_economic_calendar", from_date, to_date)
+
+
+# ---------------------------------------------------------------------------
+# Finviz smart-money screener tools
+# Each tool has NO parameters — filters are hardcoded to prevent LLM
+# hallucinating invalid Finviz filter strings.
+# ---------------------------------------------------------------------------
+
+
+def _run_finviz_screen(filters_dict: dict, label: str) -> str:
+ """Shared helper — runs a Finviz Overview screener with hardcoded filters."""
+ try:
+ from finvizfinance.screener.overview import Overview # lazy import
+
+ foverview = Overview()
+ foverview.set_filter(filters_dict=filters_dict)
+ df = foverview.screener_view()
+
+ if df is None or df.empty:
+ return f"No stocks matched the {label} criteria today."
+
+ if "Volume" in df.columns:
+ df = df.sort_values(by="Volume", ascending=False)
+
+ cols = [c for c in ["Ticker", "Sector", "Price", "Volume"] if c in df.columns]
+ top_results = df.head(5)[cols].to_dict("records")
+
+ report = f"Top 5 stocks for {label}:\n"
+ for row in top_results:
+ report += f"- {row.get('Ticker', 'N/A')} ({row.get('Sector', 'N/A')}) @ ${row.get('Price', 'N/A')}\n"
+ return report
+
+ except Exception as e:
+ logger.error("Finviz screener error (%s): %s", label, e)
+ return f"Smart money scan unavailable (Finviz error): {e}"
+
+
+@tool
+def get_insider_buying_stocks() -> str:
+ """
+ Finds Mid/Large cap stocks with positive insider purchases and volume > 1M today.
+ Insider open-market buys are a strong smart money signal — insiders know their
+ company's prospects better than the market.
+ """
+ return _run_finviz_screen(
+ {
+ "InsiderPurchases": "Positive (>0%)",
+ "Market Cap.": "+Mid (over $2bln)",
+ "Current Volume": "Over 1M",
+ },
+ label="insider_buying",
+ )
+
+
+@tool
+def get_unusual_volume_stocks() -> str:
+ """
+ Finds stocks trading at 2x+ their normal volume today, priced above $10.
+ Unusual volume is a footprint of institutional accumulation or distribution.
+ """
+ return _run_finviz_screen(
+ {
+ "Relative Volume": "Over 2",
+ "Price": "Over $10",
+ },
+ label="unusual_volume",
+ )
+
+
+@tool
+def get_breakout_accumulation_stocks() -> str:
+ """
+ Finds stocks hitting 52-week highs on 2x+ normal volume, priced above $10.
+ This is the classic institutional accumulation-before-breakout pattern
+ (O'Neil CAN SLIM). Price strength combined with volume confirms institutional buying.
+ """
+ return _run_finviz_screen(
+ {
+ "Performance 2": "52-Week High",
+ "Relative Volume": "Over 2",
+ "Price": "Over $10",
+ },
+ label="breakout_accumulation",
+ )
diff --git a/tradingagents/graph/scanner_graph.py b/tradingagents/graph/scanner_graph.py
index c35890c5..3d610d4b 100644
--- a/tradingagents/graph/scanner_graph.py
+++ b/tradingagents/graph/scanner_graph.py
@@ -1,4 +1,4 @@
-"""Scanner graph — orchestrates the 3-phase macro scanner pipeline."""
+"""Scanner graph — orchestrates the 4-phase macro scanner pipeline."""
from typing import Any, List, Optional
@@ -8,6 +8,7 @@ from tradingagents.agents.scanners import (
create_geopolitical_scanner,
create_market_movers_scanner,
create_sector_scanner,
+ create_smart_money_scanner,
create_industry_deep_dive,
create_macro_synthesis,
)
@@ -15,10 +16,11 @@ from .scanner_setup import ScannerGraphSetup
class ScannerGraph:
- """Orchestrates the 3-phase macro scanner pipeline.
+ """Orchestrates the macro scanner pipeline.
- Phase 1 (parallel): geopolitical_scanner, market_movers_scanner, sector_scanner
- Phase 2: industry_deep_dive (fan-in from Phase 1)
+ Phase 1a (parallel): geopolitical_scanner, market_movers_scanner, sector_scanner
+ Phase 1b (sequential after sector): smart_money_scanner
+ Phase 2: industry_deep_dive (fan-in from all Phase 1 nodes)
Phase 3: macro_synthesis -> END
"""
@@ -47,6 +49,7 @@ class ScannerGraph:
"geopolitical_scanner": create_geopolitical_scanner(quick_llm),
"market_movers_scanner": create_market_movers_scanner(quick_llm),
"sector_scanner": create_sector_scanner(quick_llm),
+ "smart_money_scanner": create_smart_money_scanner(quick_llm),
"industry_deep_dive": create_industry_deep_dive(mid_llm),
"macro_synthesis": create_macro_synthesis(deep_llm),
}
@@ -143,6 +146,7 @@ class ScannerGraph:
"geopolitical_report": "",
"market_movers_report": "",
"sector_performance_report": "",
+ "smart_money_report": "",
"industry_deep_dive_report": "",
"macro_scan_summary": "",
"sender": "",
diff --git a/tradingagents/graph/scanner_setup.py b/tradingagents/graph/scanner_setup.py
index c4f8302b..b8b48d6e 100644
--- a/tradingagents/graph/scanner_setup.py
+++ b/tradingagents/graph/scanner_setup.py
@@ -6,10 +6,14 @@ from tradingagents.agents.utils.scanner_states import ScannerState
class ScannerGraphSetup:
- """Sets up the 3-phase scanner graph with LLM agent nodes.
+ """Sets up the scanner graph with LLM agent nodes.
- Phase 1: geopolitical_scanner, market_movers_scanner, sector_scanner (parallel fan-out)
- Phase 2: industry_deep_dive (fan-in from all three Phase 1 nodes)
+ Phase 1a (parallel from START):
+ geopolitical_scanner, market_movers_scanner, sector_scanner
+ Phase 1b (sequential after sector_scanner):
+ smart_money_scanner — runs after sector data is available so it can
+ use sector rotation context when interpreting Finviz signals
+ Phase 2: industry_deep_dive (fan-in from all Phase 1 nodes)
Phase 3: macro_synthesis -> END
"""
@@ -20,6 +24,7 @@ class ScannerGraphSetup:
- geopolitical_scanner
- market_movers_scanner
- sector_scanner
+ - smart_money_scanner
- industry_deep_dive
- macro_synthesis
"""
@@ -36,15 +41,18 @@ class ScannerGraphSetup:
for name, node_fn in self.agents.items():
workflow.add_node(name, node_fn)
- # Phase 1: parallel fan-out from START
+ # Phase 1a: parallel fan-out from START
workflow.add_edge(START, "geopolitical_scanner")
workflow.add_edge(START, "market_movers_scanner")
workflow.add_edge(START, "sector_scanner")
- # Fan-in: all three Phase 1 nodes must complete before Phase 2
+ # Phase 1b: smart_money runs after sector (gets sector rotation context)
+ workflow.add_edge("sector_scanner", "smart_money_scanner")
+
+ # Fan-in: all Phase 1 nodes must complete before Phase 2
workflow.add_edge("geopolitical_scanner", "industry_deep_dive")
workflow.add_edge("market_movers_scanner", "industry_deep_dive")
- workflow.add_edge("sector_scanner", "industry_deep_dive")
+ workflow.add_edge("smart_money_scanner", "industry_deep_dive")
# Phase 2 -> Phase 3 -> END
workflow.add_edge("industry_deep_dive", "macro_synthesis")