feat(scanner): Finviz smart money scanner + Golden Overlap strategy

## Summary
- Adds `smart_money_scanner` as a new Phase 1b node that runs sequentially after `sector_scanner`, surfacing institutional footprints via Finviz screeners
- Introduces the **Golden Overlap** strategy in `macro_synthesis`: stocks confirmed by both top-down macro themes and bottom-up Finviz signals are labelled high-conviction
- Fixes model-name badge overflow in AgentGraph (long model IDs like OpenRouter paths were visually spilling into adjacent nodes)
- Completes all documentation: ADR-014, dataflow, architecture, components, glossary, current-state

## Key Decisions (see ADR-014)
- 3 zero-parameter tools (`get_insider_buying_stocks`, `get_unusual_volume_stocks`, `get_breakout_accumulation_stocks`) instead of 1 parameterised tool — prevents LLM hallucinations on string args
- Sequential after `sector_scanner` (not parallel fan-out) — gives access to `sector_performance_report` context and avoids `MAX_TOOL_ROUNDS=5` truncation in market_movers_scanner
- Graceful fallback: `_run_finviz_screen()` catches all exceptions and returns an error string — pipeline never hard-fails on web-scraper failure
- `breakout_accumulation` (52-wk high + 2x vol = O'Neil CAN SLIM institutional signal) replaces `oversold_bounces` (RSI<30 = retail contrarian, not smart money)

## Test Plan
- [x] 6 new mocked tests in `tests/unit/test_scanner_mocked.py` (happy path, empty DF, exception, sort order)
- [x] Fixed `tests/unit/test_scanner_graph.py` — added `smart_money_scanner` mock to compilation test
- [x] 2 pre-existing test failures excluded (verified baseline before changes)
- [x] AgentGraph badge: visually verified truncation with long OpenRouter model identifiers

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
ahmet guzererler 2026-03-24 16:03:17 +01:00 committed by GitHub
parent 6adf12eee6
commit 4c14080d73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 644 additions and 144 deletions

View File

@ -127,9 +127,15 @@ const AgentNode = ({ data }: NodeProps) => {
</Flex>
{data.metrics?.model && data.metrics.model !== 'unknown' && (
<Badge variant="outline" fontSize="2xs" colorScheme="blue" alignSelf="flex-start">
{data.metrics.model}
</Badge>
<Tooltip label={data.metrics.model} placement="top" hasArrow openDelay={300}>
<Badge
variant="outline" fontSize="2xs" colorScheme="blue"
display="block" maxW="100%"
overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap"
>
{data.metrics.model}
</Badge>
</Tooltip>
)}
{/* Running shimmer */}

View File

@ -1,32 +1,26 @@
# Current Milestone
AgentOS visual observability layer shipped. Portfolio Manager fully implemented (Phases 110). 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 110 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

View File

@ -1,8 +1,8 @@
<!-- Last verified: 2026-03-23 -->
<!-- Last verified: 2026-03-24 -->
# 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 |

View File

@ -1,4 +1,4 @@
<!-- Last verified: 2026-03-23 -->
<!-- Last verified: 2026-03-24 -->
# 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

View File

@ -1,4 +1,4 @@
<!-- Last verified: 2026-03-23 -->
<!-- Last verified: 2026-03-24 -->
# 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` |

View File

@ -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): <message>"`. 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)

View File

@ -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_<KEY>` 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_<KEY>` 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): <reason>"` — 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 (1015 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 (~35 KB, ~7501,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 | 2050+ events | All events in date range |
| `get_economic_calendar` | Markdown (table) | ~2.5 KB | ~625 | 515 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 | 12% | ✅ Safe |
| 15 | Sector Scanner | Quick | ~345 tok | ~500 tok | <1% | Safe |
| 16 | Industry Deep Dive | Mid | ~4,875 tok | ~7,000 tok | 45% | ✅ Safe |
| 17 | Macro Synthesis | Deep | ~4,325 tok | ~6,500 tok | 35% | ✅ Safe |
| 16 | Smart Money Scanner | Quick | ~620 tok | ~900 tok | <1% | Safe |
| 17 | Industry Deep Dive | Mid | ~5,375 tok | ~7,500 tok | 46% | ✅ Safe |
| 18 | Macro Synthesis | Deep | ~4,875 tok | ~7,000 tok | 45% | ✅ 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,0008,000 additional.
> **Grand total (input + output) ≈ 18,00021,000 tokens per scan.**
> Scanner output tokens ≈ 6,0009,000 additional.
> **Grand total (input + output) ≈ 21,00024,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 = 810 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.

View File

@ -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",

View File

@ -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(),
}

View File

@ -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"

View File

@ -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

View File

@ -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, "

View File

@ -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

View File

@ -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]

View File

@ -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",
)

View File

@ -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": "",

View File

@ -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")