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