Multi-agent financial analysis system powered by Claude. A user submits a natural-language request (e.g. "Analyse AAPL") and a pipeline of specialised AI agents gathers data, analyses it, critiques the analysis, and writes a final report.
flowchart TD
subgraph WEB["Web UI — Starlette · web.py · localhost:8000"]
AUTH["Auth<br/>register · login"]
RUN["POST /run<br/>start job · SSE /events"]
REP["Reports<br/>per-tenant"]
ART["Articles<br/>list / download (shared)"]
ISO["Bearer-token auth · per-tenant isolation<br/>tenants.py → .data/tenants.db (SQLite)"]
end
subgraph ORCH["Orchestrator · agents/orchestrator.py"]
PL["Phase 0 — Planning<br/>PlannerAgent: request → mode, targets, tasks"]
subgraph P1["Phase 1 — Parallel Data Gathering (ThreadPool, 4 workers)"]
MD["MarketDataAgent<br/>Haiku 4.5"]
GR["GeopoliticalRiskAgent<br/>Haiku 4.5"]
end
BK["Backdrops (always injected)<br/>macro / liquidity / news — not Phase-1 agents<br/>see architecture_agents.md"]
subgraph P2["Phase 2 — Sequential Analysis (each sees all prior output)"]
EA["EquityAnalyst<br/>Sonnet 5"]
SW["SwingTradingAnalyst<br/>Sonnet 5"]
SC["ScenarioAgent<br/>Sonnet 5"]
EA --> SW --> SC
end
CR["Phase 3 — CriticAgent (Opus 5)<br/>reviews all outputs for gaps, contradictions, risks"]
RW["Phase 4 — ReportWriterAgent (Sonnet 5, 16k max)<br/>synthesises everything into final markdown"]
OUT["reports/{tenant_id}/*.md + .meta.json + .log"]
PL --> P1 --> BK --> P2 --> CR --> RW --> OUT
end
AR["AgentRunner · agents/runner.py<br/>loads skill md as system prompt · wires tools<br/>Claude tool-use loop · retries · prompt caching · usage tracking"]
subgraph TOOLS["Tool Modules — all route through tool_cache"]
T1["fmp · fred · imf"]
T2["newsapi* · newsapi_ai · gdelt · newsdata"]
T3["polygon · sec · openfigi"]
T4["articles · macro_liquidity · finviz · uw"]
T5["alpaca_mktdata (agents) · alpaca (trades)"]
TJ["tools.json — per-module enable/disable · cache.py (SQLite TTL)"]
end
EXT["External APIs<br/>Claude · FMP · FRED · Benzinga · IMF/ECB/BIS<br/>GDELT · NewsData · SEC EDGAR · OpenFIGI<br/>Alpaca (paper+live) · Polygon (Advanced) · NewsAPI.org*"]
RUN --> ORCH
ORCH --> AR --> TOOLS --> EXT
Authoritative source: agent_model + agent_tools in agents/config.json. This
covers the report pipeline; route-invoked agents (Trades/Dashboard/Today) pass
tools at the call site. For the full per-skill view/LLM/input/tools breakdown see
the Skills overview and Data sources per agent tables in
architecture_agents.md.
| Agent | Model | Tools |
|---|---|---|
| planner | Sonnet 5 | (none) |
| market_data | Haiku 4.5 | fmp, polygon, finra_short, sec, openfigi, finviz (screener subset), 13f, uw, alpaca_mktdata (movers, most-active, snapshots) |
| geopolitical_risk | Haiku 4.5 | newsapi_ai, newsdata, benzinga (news), gdelt (news + geo), web_search |
| equity_analyst | Sonnet 5 | fmp (batch_quotes, key_metrics, financial_statements) |
| swing_trading_analyst | Sonnet 5 | fmp (technical_indicator), uw (oi_changes, options_screener) — larger set in Trades propose flow |
| scenario | Sonnet 5 | (none) |
| critic | Opus 5 | (none) |
| report_writer | Sonnet 5 | (none) — single writer for every mode |
planner and scenario are not in agent_model — they
inherit the orchestrator default (Sonnet 5). The retired news, macro, and
macro_liquidity_analyst agents were replaced by the always-injected Market
Snapshot / Symbol Snapshot / Market News backdrops.
uw = local Unusual Whales daily CSV snapshots in uw/. Seeded on deploy, refreshed daily by scheduler. Returns empty gracefully when files absent.
alpaca_mktdata = tools/data_ingestion_alpaca_mktdata.py. Provides market movers, most-active stocks, batch snapshots, and Alpaca news via the Alpaca Market Data REST API. Uses env-var keys (not per-tenant TLS), so no auth setup needed per agent call.
localStorage.Authorization: Bearer <key>. SSE and
window.open calls pass the key as a ?token= query parameter instead.Job scoped to the tenant, runs the pipeline
in a background thread, and streams progress events back via SSE.reports/{tenant_id}/ with metadata and a log file.
Usage (tokens, cost, elapsed time) is recorded in .data/tenants.db.Tool modules can be enabled or disabled via tools.json in the project root.
Each key is a module short name matching TOOL_MODULES in agents/runner.py:
{
"fmp": { "enabled": true },
"fred": { "enabled": true },
"imf": { "enabled": true },
"newsapi": { "enabled": false },
"newsapi_ai": { "enabled": true },
"gdelt": { "enabled": true },
"newsdata": { "enabled": true },
"articles": { "enabled": true },
"sec": { "enabled": true },
"openfigi": { "enabled": true },
"macro_liquidity": { "enabled": true },
"uw": { "enabled": true },
"finviz": { "enabled": true },
"unusualwhales": { "enabled": false }
}
Disabled modules are skipped when the runner wires tools for an agent. This allows swapping data sources without code changes (e.g. NewsAPI.org → GDELT + NewsData.io).
Four background daemon threads started when web.py boots:
| Thread | Interval | Purpose |
|---|---|---|
| Thread 1 — Daily Reports | 06:00 UTC daily | Generates reports for watchlist items and periodic requests; reuses existing where valid; weekly items on Mondays |
| Thread 2 — Data Refresh | Every 15 min | Refreshes UW CSV snapshots and stale market data |
| Thread 3 — Trade Monitor | Every 60s | Polls all open DB positions; auto-closes filled bracket exit legs; reconciles positions Alpaca no longer holds |
| Thread 4 — Candidate Search sweep | Continuous, 30s floor between passes; starts 10s after boot | For VIP tenants with auto_cfg.enabled, per auto_cfg.mode (premarket = once per trading day 04:00–09:30 ET / loop), runs the Dashboard symbol analysis over every symbol in the selected screeners (routes/auto_analysis.run_sweep). Produces candidates only — it places no orders |
| Thread 8 — Candidate verifier | Continuous, 30s floor; starts 20s after boot | For VIP tenants with auto_cfg.orders_enabled, inside auto_cfg.verify_window, reviews every standing candidate with the candidate_verifier agent and keeps / re-prices / removes / orders it (routes/candidate_verify.run_verify) |
Server-side, independent of the browser: Threads 4 and 8 run inside the server process, so both the search and the review happen whether or not anyone has the UI open — provided the tenant is VIP and the relevant half is switched on. With Candidate Search off nothing is generated at all; a trade ticket can still be raised by hand from any screener's Order button. The sweep only runs while the server process does.
Candidate Search's On/Off button alone changes auto_cfg.enabled. Its schedule,
screeners and provider remain saved while off. Changing those controls, removing
the last screener, completing a pass and restarting the server preserve the switch.
The progress modal only observes the search; turn it off in the cockpit to stop
after the current symbol.
Candidates to Orders follows the same rule: its On/Off button alone changes
auto_cfg.orders_enabled. Direction (auto_cfg.orders: long / short / all),
verification window and model are independent preferences and remain saved while
off. Server restarts preserve both switches. The verifier rechecks its switch
after each AI review, and automatic submission checks it again before ordering.
routes/trades.py + trade_db.py + tools/data_ingestion_alpaca.py
Database: SQLite at .data/trades.db. Tables: propositions (AI trade ideas) and positions (active/closed trades).
Source of truth: Alpaca holds the authoritative position list. The positions table stores bracket context (stop/target/opened_at/proposition_id) but does not gate whether a position is displayed. Any symbol Alpaca holds that has no DB record is auto-registered on the next trades load via get_bracket_context().
Two generation paths:
- Auto (server-side): Thread 4 analyses the selected screeners into candidates; Thread 8's verifier is the only thing that turns one into an order. Closing the UI stops neither; only stopping the server does.
- Manual (UI): an Order button on any screener row opens a prefilled ticket in the Trading view (tradesCreateManualOrder), which you edit and submit yourself.
Lifecycle:
1. Swing Trading Analyst generates a trade card → saved to propositions table.
2. User clicks Buy/Sell on the proposition → bracket limit order placed on Alpaca via place_bracket_order().
3. positions record created in DB with stop/target from the proposition.
4. Trade Monitor (Thread 3) polls get_exit_fill() every 60s; closes the DB record when a bracket leg fills.
Key functions in tools/data_ingestion_alpaca.py:
- get_snapshots(symbols) — batch StockSnapshotRequest SDK call; returns price, bid, ask, OHLCV, change% per symbol.
- get_bracket_context(symbol, mode) — reconstructs stop/target from open SELL orders, opened_at from earliest filled BUY.
- set_tenant_keys() / clear_tenant_keys() — load/clear per-tenant Alpaca keys into thread-local _tls.
/api/trades/snapshots endpoint: batch price endpoint used by the frontend to poll live prices for all open positions once per 60s.
templates/app.html (Screener view) + routes/watchlist.py (/api/screener/signals).
A client-side screener over the static stock universe (CSV) with three filter layers: asset type, market cap, sector, plus a set of Universe rules (price > $20, avg volume > 2M, dollar volume > $10M, not a leveraged ETF/SPAC) evaluated directly from the CSV. Two further rule groups — Trend and Crossover — are evaluated from live signals.
Live signals: /api/screener/signals?symbols=... returns per-symbol boolean flags (EMA stack, ADX, fresh 8/21 cross, etc.) computed in _compute_signals() from ~220 days of Polygon daily bars. Each symbol's result is cached 24h in the tool cache (screener_signals:{symbol}), so the bars are fetched at most once per symbol per day. The frontend fetches signals in 50-symbol chunks only for the pool that already passes the static filters.
Swing Trade Focus is a one-click preset that sets the regular controls to a swing-trading configuration (Common Stock only, Mid–Large cap, the Universe rules above). The include sectors are data-driven:
above_all_emas | emas_stacked | cross_fresh | adx_over_30), using the already-cached signals._scrRankSwingSectors).< 30 sampled, or no sector trending), it keeps the static fallback.This replaces the previous hardcoded include list, which could silently miss sector rotation.
templates/app.html (#masterView) + routes/master_screener.py + indicators/. A top-down funnel ported from an external site's Master Screener (documentation/tmp/Master-Screener-Code, Pine sources in documentation/tmp/pine/):
INDUSTRIES: XSD, XBI, IHI, KRE, XOP, XES, XAR — KIE is excluded as in the source; one tuple per row, extend there) inside the selected sectors. A sector with a selected industry is scanned by those industries (benchmark = industry ETF); otherwise whole (benchmark = sector ETF).screen_stocks(sector, industry); REQUIRED market-cap floors are pushed to FMP. Price floors are pushed upstream only for Regular hours; Extended/24h use their session candle's price.routes.watchlist.earnings_soon). Only stocks passing every REQUIRED daily filter go on.indicators/channels.py; Primary = daily candles, Secondary = 1H, as the Pine wires them on an hourly chart), EMA Screener V4 (indicators/ema_v4.py, Pine raw[1] semantics), RSI Classy (preference only), RS 1H/daily vs SPY and vs the industry/sector ETF. 1H candles are built from 30-minute bars using the selected trading hours (indicators/bars.py).Trading hours is a saved single choice in Selection Settings, defaulting to
Regular (09:30–16:00 ET). Extended covers 04:00–20:00 ET; 24h adds overnight trades
from 20:00 on the preceding day to 20:00 on the trading date, where available.
Exactly one radio option can be selected. Older combinations retain their broadest
coverage (Regular + 24h becomes 24h). Extended uses Polygon;
24h merges it with split-adjusted Alpaca Blue Ocean (boats) bars. Hourly candles
are anchored at the selected session's open. Extended/24h daily OHLCV, ATR, ADR and
volume filters are rebuilt from the same session bars and wait for the 20:00 close;
Regular keeps the existing provider daily candles. Benchmarks and channel charts
use the same saved coverage as the run. The separate smaller-companies daily list
remains shared market context.
Context panels above the funnel: Selection settings (sectors per direction, RS window in completed candles — saved explicitly), RSP / SPY breadth and Market caps (IWM/MDY/SPY/XLG), both read as the change in the ETF/SPY ratio, and the Top 50 outperforming smaller S&P 500 companies (smaller_companies): the 100 smallest company weights in SPY from State Street's daily holdings XLSX (share classes combined by CUSIP issuer prefix; start-of-day weights when the file is dated the measured session; routes/master_smaller_members.json is only the offline fallback), ranked by 1-day ratio change vs SPY.
The RSP / SPY, Market caps, Sectors and Industries sections each open with a headline + one sentence written by Haiku (agents/skills/master_reading.md, role DASHBOARD_BRIEF) from that table's numbers — GET /api/master/reading?section=, tool-cached as master_reading keyed by the rounded table payload, so it re-runs only when a shown number changes. Spend is recorded under feature interpret, agent master_reading.
Every condition is REQUIRED / PREFERRED / OFF; missing data never passes (on Extended/24h, an hour with no trades is not missing: RS and panel comparisons reuse the last close for up to _CARRY_CANDLES benchmark candles, while indicators still see only real candles); ranking = weighted RS score, then preferred count. The Filters subsection of Filters & rules adds 20 optional min/max, date and text conditions (FILTERS, cfg["filters"], all OFF by default): blank is inactive, Required gates at the daily stage (channel quality after the hourly stage), Preferred adds a ranking point. Next earnings (90 days of FMP calendar) and pre-market % (one Polygon call a symbol) are fetched only while their filter is active. Settings: tenants.screener_cfg["master"] (/api/master/settings, clamped by validate_cfg). A run is a background thread per tenant (POST /api/master/run, progress via GET, POST ?stop=1 to stop it after the calls in flight and keep the previous snapshot), writing snapshots/master_screener_<tenant>.json. That snapshot feeds the master_long / master_short system lists Candidate Search can sweep (routes.trades.symbols_for_list). Bars are tool-cached as master_bars_30m / master_bars_day; the Extended/24h history (master_history_extended_30m, master_history_overnight_30m) is stored per symbol and topped up with the last few days each time an hourly candle closes (candle_aligned in tool_cache_config.json); a close that disagrees with the stored bar means a split, and that symbol is refetched in full, as each is every 7-14 days. Overnight bars are fetched in 100-symbol batches (_prefetch_overnight) to keep the request count down (the batching dates from the free plan's 200 requests a minute; Algo Trader Plus allows 10,000). Daily candles built from them are cached as master_daily_candles.
The indicator ports are not yet parity-validated against TradingView (the UI shows an "unvalidated" badge); EMA V4 was ported from the JS port because its .pine was not supplied. The source's never-connected conditions (rvol/adx/trend4h/entry5m/regime/momentum/overshoot) and its "smaller companies" basket list were not carried over.
Each user is a tenant with an isolated data namespace:
Authorization header. Tokens are
secrets.token_urlsafe(32) stored in SQLite.reports/{tenant_id}/. Each tenant only sees their
own reports.jobs dict is filtered by tenant_id.reports/*.md files are moved
into the default admin tenant's directory. A .data/.migrated sentinel prevents
re-running.Stripe integration exists for subscription checkout and management but can be
disabled via STRIPE_CHECKOUT_ENABLED=false in .env. When disabled:
/stripe/checkout) returns a 400 error./api/request-upgrade.Admins can manually assign plans via the admin panel or CLI
(python tenants.py set-plan user@example.com pro).
.
├── web.py # Starlette web server + HTML/JS UI + auth
├── tenants.py # Multi-tenancy: TenantDB (SQLite), migration
├── tools.json # Per-module enabled/disabled config
├── install.sh # macOS installer (venv, API keys, LaunchAgent)
├── uninstall.sh # Removes installation + LaunchAgent
├── run_server.sh # Kills port 8000, starts web.py
├── agents/
│ ├── orchestrator.py # Multi-phase pipeline controller
│ ├── runner.py # Single-agent Claude API loop + retry logic
│ └── skills/ # Markdown system prompts per agent
│ ├── planner.md
│ ├── market_data.md
│ ├── geopolitical_risk.md
│ ├── equity_analyst.md
│ ├── swing_trading_analyst.md # Two-sided momentum funnel; reports + Trades view
│ ├── scenario.md
│ ├── critic.md
│ ├── report_writer.md # Single writer for every mode
│ ├── order_brief.md # Trades: condenses proposition to bullets
│ ├── dashboard_brief.md # Dashboard: card AI briefs
│ ├── dashboard_brief_symbol.md # Dashboard: symbol bias/brief
│ ├── today_brief.md # Today view briefing
│ └── today_macro.md # Today view: 8 macro drivers
├── tools/
│ ├── cache.py # SQLite TTL cache (shared across tenants)
│ ├── data_ingestion_fmp.py # Financial Modeling Prep
│ ├── data_ingestion_fred.py # FRED (Federal Reserve)
│ ├── data_ingestion_imf.py # IMF / ECB / BIS
│ ├── data_ingestion_newsapi.py # NewsAPI.org (disabled by default)
│ ├── data_ingestion_newsapi_ai.py # NewsAPI.ai / Event Registry
│ ├── data_ingestion_gdelt.py # GDELT DOC 2.0 (free, no key)
│ ├── data_ingestion_newsdata.py # NewsData.io
│ ├── data_ingestion_sec.py # SEC EDGAR (filings, facts, free)
│ ├── data_ingestion_openfigi.py # OpenFIGI (identifier mapping)
│ ├── data_ingestion_articles.py # Local articles + web scraping
│ ├── data_ingestion_external_data.py # UW daily CSV snapshots (module key: "uw")
│ ├── data_ingestion_finviz.py # FinViz Elite (screener, news, sectors)
│ ├── data_ingestion_alpaca.py # Alpaca Trading API (orders, positions, snapshots, bracket context)
│ ├── data_ingestion_alpaca_mktdata.py# Alpaca Market Data (movers, most-active, snapshots, news; agent module key: "alpaca_mktdata")
│ └── macro_liquidity.py # Composite liquidity scoring
├── uw/ # Unusual Whales daily CSV snapshots
│ ├── bot-eod-report-*.csv # Options flow (EOD)
│ ├── chain-oi-changes-*.csv # Open interest changes
│ ├── dp-eod-report-*.csv # Dark pool prints
│ ├── stock-screener-*.csv # Stock/options screener
│ └── analysis/ # Output from generate_*.py scripts
├── uw_download.py # Playwright downloader for UW daily files
├── trade_db.py # Trade database (propositions + positions, SQLite at .data/trades.db)
├── routes/
│ └── trades.py # Trade routes (propositions, positions, order flow, snapshots endpoint)
├── .data/ # Tenant database + per-tenant data
│ ├── tenants.db # SQLite: tenants + usage tables
│ ├── trades.db # SQLite: propositions + positions tables
│ └── .migrated # Migration sentinel
├── articles/ # Downloaded article store (shared)
└── reports/ # Generated reports (per-tenant)
└── {tenant_id}/ # Each tenant's reports + .meta.json + .log
Each agent is driven by a markdown skill file (agents/skills/*.md) loaded as
the system prompt by AgentRunner. The skill defines the agent's persona, tools,
workflow, and output format.
Role: Orchestration brain — decomposes a natural-language request into a structured JSON execution plan (mode, targets, per-agent tasks).
Tools: None (pure reasoning).
Output: A single JSON object:
{
"mode": "equity_deep_dive | swing_trade | market_brief | macro_outlook | news_digest",
"targets": ["AAPL"],
"agents": { "<name>": { "activate": true, "task": "..." } }
}
Mode selection rules:
- equity_deep_dive — user names specific stock(s); activates equity_analyst,
scenario, swing_trading_analyst (+ market_data / geopolitical_risk only when
the request needs cross-ticker discovery or a geo angle).
- swing_trade — user asks about trading, entries, short-term positioning;
activates market_data + swing_trading_analyst; skips equity_analyst and
scenario.
- market_brief — broad market/sector questions; market_data only; skips
equity_analyst, swing_trading_analyst, scenario.
- macro_outlook — economy, rates, liquidity; served by the Market Snapshot +
Market News backdrops; skips market_data and geopolitical_risk (unless
request-specific risk analysis is needed).
- news_digest — "what's happening" requests; served by the Market News +
Geopolitics backdrops; skips market_data and geopolitical_risk.
- data_pull — raw table/screener; activates market_data + report_writer only,
skips critic.
A single report_writer runs in every mode. critic runs in every mode
except data_pull. Macro, liquidity, and news are supplied by the
always-injected backdrops rather than by Phase-1 agents.
Role: Cross-ticker discovery specialist — screening, market movers, 13F filings, and peer-universe data that a per-symbol snapshot can't provide (named tickers get their data from the auto-injected Symbol Snapshot instead).
Tools (config.json): fmp (all), polygon, finra_short, sec,
openfigi, finviz (screener, latest_filings, quote_history, group_performance),
13f, uw, alpaca_mktdata (market movers, most-active, stock snapshots).
Workflow: Multi-company → get_batch_quotes + individual metrics; sector →
get_sector_performance + screen_stocks; institutional → 13F. Minimal
interpretation — raw data for analysts.
Output: Structured tables — Quote Summary, Financial Summary (3-year), Key Metrics (TTM), Analyst Consensus, Technical Levels. Minimal interpretation; raw data for analysts.
Role: Geopolitical risk analyst — assesses how political events and international tensions affect financial markets.
Tools (config.json): newsapi_ai, newsdata, benzinga (news), gdelt
(search_gdelt_news, get_gdelt_geo_news), web_search.
Performance budget: ≤ 8 tool calls total.
Workflow: Geopolitical headlines → central bank news → targeted topic searches → article summaries (prefer over full text).
Output: Geopolitical Risk Register (probability × impact × timeframe × affected assets table), Risk Detail (description, trigger signals, transmission mechanism, affected sectors, historical analogue, hedging implications), Geopolitical Sentiment (risk level, direction, key uncertainty), Central Bank Watch.
Role: Senior equity research analyst — synthesises all prior agent output into a rigorous investment thesis with valuation and recommendation.
Tools (config.json): fmp (get_batch_quotes, get_key_metrics,
get_financial_statements) — most data arrives via the injected Symbol Snapshot,
so the live toolset is deliberately narrow.
Inputs: Phase-1 summaries (market_data, geopolitical_risk) + Market Snapshot + per-symbol Symbol Snapshots.
Workflow: Review all context → fetch missing data → build thesis → cross-check conclusions against data.
Output: Business Quality Assessment (moat, revenue quality, capital allocation, management), Financial Analysis (growth, margins, balance sheet, FCF, capital returns), Valuation Assessment (DCF + relative methods → blended fair value), Investment Thesis (recommendation, fair value range, conviction), Key Catalysts, Risk Factors.
Role: Institutional-level swing trading analyst — top-down analysis for 1–3 day trades in US equities/ETFs with concrete entry/stop/target.
Tools: In the report pipeline (config.json) — fmp
(get_technical_indicator), uw (ext_get_oi_changes, ext_get_options_screener).
In the Trades propose flow (routes/trades.py) it gets a larger set:
fmp, fred, finra_short, newsapi_ai, symbol_snapshot, newsdata (search),
uw (options flow, OI changes, options screener). It is the sole swing analyst skill,
used for both reports and the Trades propose flow (the tenant analyst setting is
retained as proposition metadata but no longer selects a variant).
Performance budget: ≤ 8 tool calls total.
Workflow (strict 9-step process): 1. Market Context — index trend, VIX, rates → Risk ON/OFF/CHOP 2. Geopolitics + News — wars, macro events, earnings 3. Sector & Industry Rotation — relative strength 4. Sentiment / Positioning — crowd positioning 5. Technical Analysis — structure, key levels, indicators, patterns 6. Trade Decision — LONG or SHORT (no hedging) 7. Execution — exact entry, stop, target, risk %, R:R 8. Invalidation — price/condition that cancels trade 9. Final Trade Summary block
Output: Ends with a clean trade card:
TRADE: LONG / SHORT
TICKER: ___
ENTRY / STOP / TARGET: $___
R:R = ___
PROBABILITY: _/5
Role: Scenario analyst — constructs probability-weighted bull/base/bear scenarios grounded in valuation frameworks.
Tools: None (pure reasoning from prior agent context).
Inputs: Equity analyst thesis + all Phase 1 data.
Output: Scenario Summary table (probability, target price, return, key driver), detailed Bull/Base/Bear cases (thesis, key assumptions for revenue/margins/multiples, triggers, signposts), Risk/Reward Assessment (EV vs. price, skew, tail risk, optionality).
Constraints: Probabilities sum to 100%. Base case is most probable (40–60%). Scenarios are internally consistent. Target prices grounded in DCF / multiples.
Role: Devil's advocate and quality controller — stress-tests the full analysis for weaknesses.
Tools: None (pure reasoning from prior agent context).
Inputs: All prior Phase-1/2 outputs (market data, geopolitical risk, equity thesis, scenarios) + Market Snapshot + Symbol Snapshots.
Output: Assumption Challenges table (assumption, challenge, severity), Data Quality Concerns (stale, missing, contradictions, sample bias), Analytical Gaps, Counter-Arguments, Bias Check (anchoring, recency, confirmation, narrative, consensus), Confidence Assessment table (data quality, analytical rigour, valuation range, scenario completeness → overall), Recommendations for Improvement.
Role: Financial report writer — synthesises all agent outputs into a single, polished, reader-friendly markdown report.
Tools: None (pure writing from prior agent context + critic feedback).
Writing style: ADHD-friendly, non-technical. Lead with the bottom line, short paragraphs (2–3 sentences), plain English, liberal bullet points, bold key takeaways, analogies for context, small tables with emoji trend/signal indicators (🟢🔴🟡⬆️⬇️➡️⚠️✅❌📊).
Templates by mode:
- equity_deep_dive (1500–2500 words) — The Big Picture, What the Stock Is
Doing, The Business, The Numbers That Matter, What's It Worth, What Could Go
Right/Wrong, Risks to Watch, The Bigger Picture, What People Are Saying,
Fine Print.
- market_brief (500–1000 words) — The One-Minute Version, Markets at a
Glance, Winners & Losers, The Economy Right Now, What Could Shake Things Up.
- macro_outlook (1000–1800 words) — Bottom Line, Growth, Inflation & Interest
Rates, Around the World, Political & Geopolitical Risks, What This Means for
Investing.
- news_digest (400–800 words) — The Headlines That Matter, What's Moving
Prices, Worth Watching.
Incorporates critic feedback in the fine print. Report stands alone without needing raw agent outputs.