# TradingAgents Admin & Architecture Guide

This guide is the operational and architectural reference for **TradingAgents**, a multi-agent LLM financial-trading research framework built on [LangGraph](https://github.com/langchain-ai/langgraph). It targets operators deploying the framework, developers extending it, and architects evaluating its design. It documents the agent topology, the LLM-provider abstraction, the data-vendor routing layer, the anti-hallucination mechanisms, configuration, persistence, security posture, and the codebase's internal structure in enough depth that a competent engineer (or AI) could reconstruct the system.

For installation and end-user CLI walkthroughs, see the companion **User Guide**.

> [!IMPORTANT]
> TradingAgents is a research framework. It does not connect to a live brokerage; the Portfolio Manager "executes" against a simulated exchange. Treat its outputs as research artifacts. Trading performance varies with model choice, temperature, data quality, and other non-deterministic factors.

---

## 1. Executive Summary

TradingAgents decomposes a trading decision into specialized roles, each backed by a language model, and wires them into a directed graph executed by LangGraph. A run for a given ticker and date proceeds through five stages:

```
Analyst Team  →  Research Debate  →  Trader  →  Risk Debate  →  Portfolio Manager
(market,         (bull vs bear,      (drafts    (aggressive/    (final 5-tier
 sentiment,       judged by          a trade)    conservative/   Buy/.../Sell
 news,            Research Manager)              neutral, +PM)   decision)
 fundamentals)
```

| Attribute | Value |
|---|---|
| Language | Python 3.10+ |
| Orchestration | LangGraph `StateGraph` |
| Package version | 0.2.5 |
| Entry points | `cli.main:app` (console script `tradingagents`), `main.py` (programmatic) |
| LLM providers | 11 (OpenAI, Google, Anthropic, xAI, DeepSeek, Qwen, GLM, MiniMax, OpenRouter, Azure, Ollama) |
| Data vendors | yfinance (default), Alpha Vantage; plus StockTwits & Reddit for sentiment |
| Persistence | Markdown decision log + per-ticker SQLite checkpoints |
| External infra | None required beyond the LLM provider + data-vendor HTTP APIs |
| License | See repository |

The framework's defining design choices are: a **provider-agnostic LLM abstraction** that normalizes the quirks of eleven model APIs behind one interface; a **vendor-routing data layer** with automatic fallback; and a **multi-layer anti-hallucination system** that deterministically resolves company identity and grounds numeric claims in verified data snapshots.

---

## 2. Architecture

### 2.1 Component map

```
                          ┌─────────────────────────────────────────┐
   CLI (cli/main.py)      │           TradingAgentsGraph              │
   or main.py             │      (tradingagents/graph/trading_graph)  │
        │                 │                                           │
        │  config + ──────▶  • builds quick/deep LLMs via factory     │
        │  selected           • builds ToolNodes (data tools)         │
        │  analysts           • GraphSetup.setup_graph() → StateGraph │
        │                     • Propagator (initial state)            │
        │                     • ConditionalLogic (edge routing)       │
        │                     • Reflector / SignalProcessor           │
        │                     • TradingMemoryLog (decision log)       │
        └─────────────────┴───────────────────┬───────────────────────┘
                                               │ graph.stream(...)
                ┌──────────────────────────────┼──────────────────────────────┐
                ▼                               ▼                              ▼
       llm_clients/ (factory +         agents/ (node factories         dataflows/ (vendor
       per-provider clients +          + Pydantic schemas)             routing + yfinance/
       capability table)                                               alpha_vantage + reddit/
                                                                       stocktwits + validators)
```

### 2.2 The agent graph topology

`GraphSetup.setup_graph()` (`tradingagents/graph/setup.py`) compiles a LangGraph `StateGraph(AgentState)`. The topology is built dynamically from the selected analysts:

**Analyst loop (per selected analyst, in fixed order market → social → news → fundamentals):**

```
START → [Analyst] ──conditional──▶ tools_<analyst> ──▶ back to [Analyst]
                  └──conditional──▶ Msg Clear <analyst> ──▶ next analyst (or Bull Researcher)
```

Each analyst is a node that may emit tool calls. `ConditionalLogic.should_continue_<analyst>` inspects the last message: if it contains tool calls, the graph routes to that analyst's `ToolNode` (which executes the data tools and loops back); otherwise it routes to a "Msg Clear" node that wipes the message history and advances to the next analyst.

**Research debate:**

```
Bull Researcher ⇄ Bear Researcher  → Research Manager → Trader
```

`should_continue_debate` alternates between Bull and Bear until `investment_debate_state["count"]` reaches `2 × max_debate_rounds`, then hands to the Research Manager (deep-thinking LLM), who issues the investment plan and routes to the Trader.

**Risk debate:**

```
Aggressive → Conservative → Neutral  (round-robin)  → Portfolio Manager → END
```

`should_continue_risk_analysis` cycles the three risk debaters until `risk_debate_state["count"]` reaches `3 × max_risk_discuss_rounds`, then hands to the Portfolio Manager (deep-thinking LLM), who issues the final decision and ends the graph.

> [!NOTE]
> The analysts and high-volume agents use the **quick-thinking** LLM; the Research Manager and Portfolio Manager use the **deep-thinking** LLM. This split is wired in `GraphSetup` via the `analyst_factories` and the explicit `create_research_manager(self.deep_thinking_llm)` / `create_portfolio_manager(self.deep_thinking_llm)` calls.

### 2.3 Graph state

The graph state is `AgentState` (`tradingagents/agents/utils/agent_states.py`), a `MessagesState` subclass (so the `messages` channel gets LangGraph's reducer) extended with:

| Field | Purpose |
|---|---|
| `company_of_interest` | The ticker under analysis |
| `asset_type` | `stock` or `crypto` |
| `instrument_context` | Deterministically resolved ticker identity injected at run start |
| `trade_date` | Analysis date (data filtered to this date) |
| `market_report`, `sentiment_report`, `news_report`, `fundamentals_report` | Per-analyst outputs |
| `investment_debate_state` | `InvestDebateState` (bull/bear history, judge decision, count) |
| `investment_plan` | Research Manager's plan |
| `trader_investment_plan` | Trader's proposal |
| `risk_debate_state` | `RiskDebateState` (aggressive/conservative/neutral history, latest speaker, count) |
| `final_trade_decision` | Portfolio Manager's decision |
| `past_context` | Memory-log context injected at run start |

### 2.4 Execution lifecycle (`TradingAgentsGraph.propagate`)

1. Record the ticker; **resolve pending memory-log entries** for it (fetch realized returns, generate reflections, batch-write).
2. If `checkpoint_enabled`, recompile the graph with a per-ticker `SqliteSaver` and log whether it is resuming or starting fresh.
3. **Resolve instrument identity** deterministically (`resolve_instrument_context`) and build the initial state (with `past_context` from the memory log).
4. Stream (debug) or invoke (non-debug) the graph to completion, merging per-node deltas into a final state.
5. Write the full state to a JSON log under `results_dir/<TICKER>/TradingAgentsStrategy_logs/`.
6. **Store the decision** as a pending entry in the memory log.
7. Clear the checkpoint on success.
8. Return `(final_state, process_signal(final_decision))`.

---

## 3. The LLM Provider Abstraction

`tradingagents/llm_clients/` is the heart of the framework's portability. A factory dispatches on provider name; per-provider client classes normalize each API's quirks behind a single `BaseLLMClient` interface.

### 3.1 Factory dispatch (`factory.py`)

```python
create_llm_client(provider, model, base_url=None, **kwargs) -> BaseLLMClient
```

| Provider key(s) | Client class | API style |
|---|---|---|
| `openai`, `xai`, `deepseek`, `qwen`, `qwen-cn`, `glm`, `glm-cn`, `minimax`, `minimax-cn`, `ollama`, `openrouter` | `OpenAIClient` (+ `DeepSeekChatOpenAI` / `MinimaxChatOpenAI` subclasses) | OpenAI-compatible |
| `anthropic` | `AnthropicClient` | Anthropic Messages |
| `google` | `GoogleClient` | Gemini |
| `azure` | `AzureOpenAIClient` | Azure OpenAI |

An unsupported provider raises `ValueError: Unsupported LLM provider: {provider}`. Provider modules are imported **lazily** so merely importing the factory (e.g. during test collection) does not pull in heavy SDKs or fail on missing keys.

### 3.2 Content normalization

Several providers (OpenAI Responses API, Gemini 3) return content as a list of typed blocks (reasoning, text, …). Every client subclasses its langchain chat model and overrides `invoke()` to call `normalize_content()` (`base_client.py`), which joins the text blocks into a plain string so downstream agents always see `response.content` as a string.

### 3.3 The capability table (`capabilities.py`)

OpenAI-compatible providers differ in which API parameters they accept. Rather than scattering model-name `if` ladders through the client code, a declarative table resolves capabilities by exact model ID, then regex pattern, then a default:

| Model class | `supports_tool_choice` | Structured method | Special handling |
|---|---|---|---|
| Default | yes | `function_calling` | — |
| DeepSeek chat | yes | `function_calling` | — |
| DeepSeek thinking (`deepseek-reasoner`, `deepseek-v4-*`, `deepseek-v\d*`) | **no** | `function_calling` | requires `reasoning_content` round-trip |
| MiniMax M2.x (`MiniMax-M2*`) | **no** | `function_calling` | requires `reasoning_split` via `extra_body` |

When a model rejects `tool_choice`, `NormalizedChatOpenAI.with_structured_output()` still binds the schema as a tool but suppresses the `tool_choice` kwarg — mirroring those providers' official tool-calling examples. `DeepSeekChatOpenAI` re-attaches `reasoning_content` on the next request (DeepSeek thinking models 400 otherwise); `MinimaxChatOpenAI` sets `reasoning_split=True` via `extra_body` so `<think>` blocks land in `reasoning_details` instead of polluting `content`.

> [!TIP]
> To onboard a new OpenAI-compatible model with API quirks, add a row to `_BY_ID` or a pattern to `_BY_PATTERN` in `capabilities.py` — not to the client code. New `deepseek-v5-*` or `MiniMax-M3*` variants inherit the thinking-mode handling automatically via the forward-compat patterns.

### 3.4 Provider endpoints and keys

Base URLs live in `_PROVIDER_BASE_URL` (`openai_client.py`); the canonical provider→env-var key map lives in `api_key_env.py` (consumed by both the client and the CLI's key prompt):

| Provider | Base URL | API-key env var |
|---|---|---|
| openai | (Responses API default) | `OPENAI_API_KEY` |
| anthropic | `https://api.anthropic.com/` | `ANTHROPIC_API_KEY` |
| google | (Gemini default) | `GOOGLE_API_KEY` |
| azure | (from `AZURE_OPENAI_ENDPOINT`) | `AZURE_OPENAI_API_KEY` |
| xai | `https://api.x.ai/v1` | `XAI_API_KEY` |
| deepseek | `https://api.deepseek.com` | `DEEPSEEK_API_KEY` |
| qwen | `https://dashscope-intl.aliyuncs.com/compatible-mode/v1` | `DASHSCOPE_API_KEY` |
| qwen-cn | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `DASHSCOPE_CN_API_KEY` |
| glm | `https://api.z.ai/api/paas/v4/` | `ZHIPU_API_KEY` |
| glm-cn | `https://open.bigmodel.cn/api/paas/v4/` | `ZHIPU_CN_API_KEY` |
| minimax | `https://api.minimax.io/v1` | `MINIMAX_API_KEY` |
| minimax-cn | `https://api.minimaxi.com/v1` | `MINIMAX_CN_API_KEY` |
| openrouter | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` |
| ollama | `http://localhost:11434/v1` (override via `OLLAMA_BASE_URL`) | none |

> [!IMPORTANT]
> Dual-region providers (qwen/glm/minimax) keep separate endpoints and separate keys because international and China accounts cannot share credentials. Selecting the wrong region for a key produces auth failures.

### 3.5 Model catalog & validation

`model_catalog.py` is the single source of truth for the CLI dropdowns and the `validators.py` model-name validation. `get_known_models()` derives the valid set from the catalog. Ollama and OpenRouter accept any model name (validation always returns `True`); for other providers, an unknown model emits a `RuntimeWarning` but does **not** block the run (`warn_if_unknown_model`).

### 3.6 Provider-specific thinking configuration

`TradingAgentsGraph._get_provider_kwargs()` forwards reasoning/thinking settings only to the provider that consumes them:

| Provider | Config key | Forwarded kwarg |
|---|---|---|
| google | `google_thinking_level` | `thinking_level` (Gemini 3) or mapped to `thinking_budget` (Gemini 2.5) |
| openai | `openai_reasoning_effort` | `reasoning_effort` |
| anthropic | `anthropic_effort` | `effort` (only for Opus/Sonnet 4.5+; Haiku rejects it) |
| all | `temperature` | `temperature` (when set) |

---

## 4. The Data Layer

`tradingagents/dataflows/` abstracts market data behind a category-based routing layer with vendor fallback.

### 4.1 Tool categories and vendor routing (`interface.py`)

Agent-facing tools (in `agents/utils/*_tools.py`) are thin `@tool` wrappers that call `route_to_vendor(method, *args)`. Routing resolves the configured vendor for the tool's category, builds a fallback chain, and tries each vendor in turn.

| Category | Tools | Default vendor |
|---|---|---|
| `core_stock_apis` | `get_stock_data` | yfinance |
| `technical_indicators` | `get_indicators` | yfinance |
| `fundamental_data` | `get_fundamentals`, `get_balance_sheet`, `get_cashflow`, `get_income_statement` | yfinance |
| `news_data` | `get_news`, `get_global_news`, `get_insider_transactions` | yfinance |

`get_vendor()` checks tool-level config (`tool_vendors`) first, then category-level (`data_vendors`). The fallback chain puts configured vendors first, then any remaining available vendor.

### 4.2 Fallback semantics

`route_to_vendor()` iterates the fallback chain and handles failures distinctly:

- `AlphaVantageRateLimitError` → try the next vendor.
- `NoMarketDataError` → remember it; another vendor may have data.
- Any other exception → remember the *first* error but keep trying.

If at least one vendor reported "no data", it returns a single explicit `NO_DATA_AVAILABLE: ...` sentinel instructing the agent **not** to fabricate values. If no vendor returned data and none reported clean "no data", the first real error is raised. This guarantees an agent never receives a silently-empty result it could hallucinate around.

### 4.3 Vendors

- **yfinance** (`y_finance.py`, `yfinance_news.py`, `stockstats_utils.py`): default. `load_ohlcv()` downloads a 5-year window, caches per symbol under `data_cache_dir`, and re-fetches on a poisoned (empty) cache. `yf_retry()` adds exponential backoff on `YFRateLimitError`. Technical indicators are computed locally via `stockstats`.
- **Alpha Vantage** (`alpha_vantage_*.py`): optional, keyed by `ALPHA_VANTAGE_API_KEY`. Wraps OVERVIEW, BALANCE_SHEET, CASH_FLOW, INCOME_STATEMENT, the indicator endpoints, NEWS_SENTIMENT, and INSIDER_TRANSACTIONS. Raises `AlphaVantageNotConfiguredError` when unkeyed and `AlphaVantageRateLimitError` on rate limits.
- **StockTwits** (`stocktwits.py`): keyless per-symbol message stream with bullish/bearish labels; degrades to a placeholder string on any failure.
- **Reddit** (`reddit.py`): keyless search across finance subreddits, with automatic Atom/RSS fallback when the JSON endpoint 403s. RSS-sourced posts omit score/comment counts rather than printing fake zeros.

### 4.4 Symbol normalization (`symbol_utils.py`)

`normalize_symbol()` maps broker/TradingView/MT5-style symbols to Yahoo's convention before any fetch:

| User types | Yahoo symbol | Rule |
|---|---|---|
| `XAUUSD`, `GOLD` | `GC=F` | metals → COMEX/NYMEX futures (alias table) |
| `EURUSD` | `EURUSD=X` | two known ISO-4217 halves → spot forex `=X` |
| `BTCUSD` | `BTC-USD` | known crypto base → `-USD` |
| `SPX500`, `US500` | `^GSPC` | index CFD → Yahoo index (alias table) |

Add new instruments by appending an alias row — no call-site changes. A failed lookup raises `NoMarketDataError`, which carries both the requested and canonical symbol for clear messaging.

---

## 5. Anti-Hallucination Architecture

This is the framework's most important quality system. Three mechanisms prevent the LLM from inventing companies, prices, or data.

### 5.1 Deterministic instrument identity

Before any agent runs, `resolve_instrument_identity()` (`agent_utils.py`) performs a cached, fail-open yfinance lookup of the ticker's real company name, sector, industry, and exchange. `build_instrument_context()` injects this into `AgentState.instrument_context`, instructing every agent to use the exact ticker and not substitute a different company. This fixed the failure mode where the Market Analyst would pattern-match a chart to a narrative and invent an identity that cascaded downstream (issue #814). If yfinance is unavailable, the function returns `{}` and the run falls back to ticker-only context rather than failing.

### 5.2 Verified market snapshot

`build_verified_market_snapshot()` (`market_data_validator.py`) computes a deterministic, LLM-free ground-truth snapshot: the latest OHLCV row on or before the analysis date, a fixed set of indicators (SMA/EMA/RSI/Bollinger/MACD/ATR via `stockstats`), and recent closes. The Market Analyst is told to treat this as the source of truth for any exact numeric claim and to flag — not reconcile — conflicting tool output. This stops confabulated price levels and "historically validated bounces" (issue #830).

### 5.3 `NO_DATA_AVAILABLE` sentinel

As described in [4.2](#42-fallback-semantics), the vendor router returns an explicit "data unavailable, do not fabricate" string rather than an empty result when a symbol has no data anywhere.

### 5.4 Look-ahead-bias prevention

Backtests must not see future data. Every data path filters to the analysis date: `load_ohlcv()` drops rows after `curr_date`; `filter_financials_by_date()` and `_filter_reports_by_date()` drop fiscal periods after it; the verified-snapshot path re-applies the cutoff defensively.

---

## 6. Structured Output & the Decision Schema

The three decision-making agents (Research Manager, Trader, Portfolio Manager) produce typed Pydantic output via `llm.with_structured_output(Schema)`, then render it back to markdown so the rest of the system (memory log, CLI, saved reports) is unchanged.

| Agent | Schema | Key fields |
|---|---|---|
| Research Manager | `ResearchPlan` | `recommendation` (5-tier), `rationale`, `strategic_actions` |
| Trader | `TraderProposal` | `action` (3-tier Buy/Hold/Sell), `reasoning`, optional `entry_price`/`stop_loss`/`position_sizing` |
| Portfolio Manager | `PortfolioDecision` | `rating` (5-tier), `executive_summary`, `investment_thesis`, optional `price_target`/`time_horizon` |
| Sentiment Analyst | `SentimentReport` | `overall_band` (6-tier), `overall_score` (0–10), `confidence`, `narrative` |

The `structured.py` helper centralizes the pattern: `bind_structured()` wraps the LLM (returning `None` if the provider lacks structured output, logging a warning), and `invoke_structured_or_freetext()` runs the structured call and falls back to a plain `llm.invoke()` on any failure — so a weak model or transient error never blocks the pipeline.

`SignalProcessor.process_signal()` extracts the final rating from the rendered markdown using the deterministic `parse_rating()` heuristic — **no extra LLM call** — because the PM's structured output guarantees a parseable `**Rating**: X` line.

> [!NOTE]
> The five-tier scale (`Buy / Overweight / Hold / Underweight / Sell`) is centralized in `agents/utils/rating.py` and used identically by the Research Manager, Portfolio Manager, signal processor, and memory log, preventing vocabulary drift.

---

## 7. Persistence

### 7.1 Decision log (`TradingMemoryLog`)

An append-only markdown log (`memory.py`) at `memory_log_path` (default `~/.tradingagents/memory/trading_memory.md`). Each decision is appended as a tagged block:

```text
[2026-01-15 | NVDA | Buy | pending]

DECISION:
...portfolio manager decision markdown...

<!-- ENTRY_END -->
```

The HTML-comment separator cannot appear in LLM prose, making it a safe delimiter. On the next same-ticker run, `_resolve_pending_entries()` fetches the realized raw and alpha return, generates a reflection via the `Reflector`, and rewrites the tag with the figures (`[date | ticker | rating | +4.2% | +1.1% | 5d]`) plus a `REFLECTION:` section — using an atomic temp-file + `os.replace()` write. `get_past_context()` injects the most recent same-ticker decisions and cross-ticker lessons into the PM prompt.

> [!TIP]
> Set `memory_log_max_entries` to cap the number of **resolved** entries; the oldest are rotated out once the cap is exceeded. Pending entries are never pruned. `None` (default) disables rotation.

### 7.2 Checkpoint resume (`checkpointer.py`)

Opt-in via `checkpoint_enabled` / `--checkpoint`. Uses LangGraph's `SqliteSaver` with **one SQLite database per ticker** under `data_cache_dir/checkpoints/<TICKER>.db`, so concurrent tickers do not contend. The thread ID is a SHA-256 of `ticker:date` (truncated to 16 chars), so the same ticker+date resumes and a new date starts fresh. Checkpoints are cleared on successful completion. `clear_all_checkpoints()` deletes every DB; `--clear-checkpoints` invokes it before a run.

> [!CAUTION]
> Ticker values are validated through `safe_ticker_component()` before being used as a filename in the checkpoint, cache, and results paths — see [Section 9](#9-security). A malicious ticker cannot escape its intended directory.

---

## 8. Configuration Mastery

### 8.1 `DEFAULT_CONFIG` reference

Defined in `tradingagents/default_config.py`. Copy it (`DEFAULT_CONFIG.copy()`) and override individual keys.

| Key | Default | Purpose |
|---|---|---|
| `results_dir` | `~/.tradingagents/logs` | Per-run state logs (env: `TRADINGAGENTS_RESULTS_DIR`) |
| `data_cache_dir` | `~/.tradingagents/cache` | Price cache + checkpoints (env: `TRADINGAGENTS_CACHE_DIR`) |
| `memory_log_path` | `~/.tradingagents/memory/trading_memory.md` | Decision log (env: `TRADINGAGENTS_MEMORY_LOG_PATH`) |
| `memory_log_max_entries` | `None` | Cap on resolved log entries; `None` = no rotation |
| `llm_provider` | `openai` | Provider key |
| `deep_think_llm` | `gpt-5.5` | Research Manager + Portfolio Manager model |
| `quick_think_llm` | `gpt-5.4-mini` | Analysts/researchers/trader model |
| `backend_url` | `None` | Override endpoint; `None` = provider default |
| `google_thinking_level` | `None` | Gemini thinking level |
| `openai_reasoning_effort` | `None` | OpenAI reasoning effort |
| `anthropic_effort` | `None` | Anthropic effort |
| `temperature` | `None` | Cross-provider sampling temperature |
| `checkpoint_enabled` | `False` | Enable resume checkpoints |
| `output_language` | `English` | Report language (debate stays English) |
| `max_debate_rounds` | `1` | Research-team debate rounds |
| `max_risk_discuss_rounds` | `1` | Risk-team debate rounds |
| `max_recur_limit` | `100` | LangGraph recursion limit |
| `analyst_concurrency_limit` | `1` | Analyst execution concurrency |
| `news_article_limit` | `20` | Max articles per ticker |
| `global_news_article_limit` | `10` | Max macro articles |
| `global_news_lookback_days` | `7` | Macro news lookback |
| `global_news_queries` | (5 macro queries) | Macro headline search terms |
| `data_vendors` | all `yfinance` | Per-category vendor |
| `tool_vendors` | `{}` | Per-tool vendor override (wins over category) |
| `benchmark_ticker` | `None` | Force a single alpha benchmark |
| `benchmark_map` | (suffix→index map) | Per-exchange alpha benchmark |

### 8.2 Environment-variable overrides

`_apply_env_overrides()` overlays `TRADINGAGENTS_*` variables onto `DEFAULT_CONFIG` at import, coercing to the existing default's type (bool/int/float/str). To expose a new key for env override, add a row to `_ENV_OVERRIDES` — no entry-point changes needed.

```bash
TRADINGAGENTS_LLM_PROVIDER=anthropic \
TRADINGAGENTS_DEEP_THINK_LLM=claude-opus-4-8 \
TRADINGAGENTS_MAX_DEBATE_ROUNDS=3 \
tradingagents analyze
```

Setting provider/models/backend-URL/language via env also skips the matching CLI prompt, enabling unattended runs.

### 8.3 Config mutation semantics (`dataflows/config.py`)

`set_config()` merges dict-valued keys (e.g. `data_vendors`) one level deep, so a partial update like `{"data_vendors": {"core_stock_apis": "alpha_vantage"}}` preserves the other nested defaults; scalar keys are replaced. This fixed config state leaking between runs when sub-dicts were mutated (#788).

### 8.4 Alpha benchmark configuration

`_resolve_benchmark()` picks the alpha baseline: `benchmark_ticker` overrides everything; otherwise `benchmark_map` matches the ticker's exchange suffix (`.T`→`^N225`, `.HK`→`^HSI`, `.L`→`^FTSE`, etc.), falling back to `SPY` for US tickers. This replaced a hardcoded SPY benchmark that let FX drift dominate alpha for non-USD listings (#628, #684).

---

## 9. Security

> [!IMPORTANT]
> This section reflects the framework's actual security posture as implemented in source. Review it before deploying in any shared or automated environment.

### 9.1 Path-traversal hardening

`safe_ticker_component()` (`dataflows/utils.py`) validates every ticker used as a filesystem path component (cache files, checkpoint DBs, results directories). It rejects tickers that are empty, exceed a length cap, contain characters illegal in a path, or consist solely of dots — raising `ValueError`. This prevents a malicious ticker like `../../etc/x` from escaping its intended directory (#618). Applied in `load_ohlcv()`, `_db_path()`, and `_log_state()`.

### 9.2 Secrets handling

- API keys are read from environment variables (loaded from `.env` / `.env.enterprise` at package import via `python-dotenv`). `load_dotenv` uses `override=False`, so shell-exported values are never clobbered.
- The CLI's `ensure_api_key()` prompts for a missing key with `questionary.password` (masked input) and persists it to `.env` via `set_key`.
- Missing keys raise a descriptive `ValueError` naming the exact env var to set (#680).

> [!CAUTION]
> Keys are written to a plaintext `.env` in the working directory. Ensure `.env` is git-ignored (it is, by repo convention) and protected by filesystem permissions. In Docker, the `.env` is passed via `env_file` and never baked into the image.

### 9.3 Network egress

The framework makes outbound HTTPS calls to: the selected LLM provider, Yahoo Finance, Alpha Vantage (if keyed), StockTwits, Reddit, and `api.tauric.ai/v1/announcements` (CLI welcome panel, 1s timeout, silent on failure). All data fetchers degrade gracefully on network failure rather than crashing.

### 9.4 Untrusted-input surface

LLM output is treated as untrusted: tool arguments are validated at the tool boundary (e.g. invalid indicator names raise `ValueError` and are caught), and the structured-output schemas constrain the decision agents. The verified-snapshot and instrument-identity layers prevent the model from acting on fabricated data.

### 9.5 Docker runtime

The `Dockerfile` runs as a non-root `appuser` and pre-creates the writable `~/.tradingagents` directory with correct ownership, resolving first-run permission errors (#519, #771).

---

## 10. Operations

### 10.1 Deployment models

| Model | How |
|---|---|
| Local CLI | `pip install .` then `tradingagents` |
| Docker (cloud LLM) | `docker compose run --rm tradingagents` |
| Docker (local Ollama) | `docker compose --profile ollama run --rm tradingagents-ollama` |
| Programmatic / embedded | Import `TradingAgentsGraph`, call `.propagate()` |
| Unattended / batch | Set `TRADINGAGENTS_*` env vars to skip all prompts |

### 10.2 Runtime artifacts

| Path | Contents |
|---|---|
| `~/.tradingagents/logs/<TICKER>/TradingAgentsStrategy_logs/full_states_log_<date>.json` | Full final state per run |
| `~/.tradingagents/cache/*.csv` | Cached OHLCV per symbol |
| `~/.tradingagents/cache/checkpoints/<TICKER>.db` | Resume checkpoints |
| `~/.tradingagents/memory/trading_memory.md` | Decision log |
| `./reports/<TICKER>_<timestamp>/` | User-saved report tree (when prompted) |

### 10.3 Observability

The CLI's `StatsCallbackHandler` (a langchain `BaseCallbackHandler`) counts LLM calls, tool calls, and input/output tokens (thread-safe via a lock) and surfaces them in the footer. The `AnalystWallTimeTracker` reports per-analyst wall time. A `message_tool.log` is written per run capturing every message and tool call.

### 10.4 Cost & latency controls

- Lower `max_debate_rounds` / `max_risk_discuss_rounds` (Shallow depth = 1) to reduce LLM calls.
- Reduce `news_article_limit` / `global_news_article_limit` to shrink agent prompt size.
- Use cheaper quick-thinking models for analysts; reserve frontier models for the deep-thinking decision agents.
- Use a local Ollama provider to eliminate per-token cost.

### 10.5 Backup & recovery

The decision log and price cache are plain files under `~/.tradingagents/`; back up that directory to preserve learned context. The Docker Compose setup persists it in the `tradingagents_data` named volume. Checkpoints are disposable (cleared on success); they exist only to survive a crash mid-run.

---

## 11. Repository Structure

```
TradingAgents/
├── main.py                       # programmatic example entry point
├── pyproject.toml                # build + deps + console script
├── Dockerfile, docker-compose.yml
├── .env.example, .env.enterprise.example
├── cli/                          # interactive Typer CLI
│   ├── main.py                   # app, run_analysis(), live dashboard, analyze command
│   ├── utils.py                  # questionary prompts, model menus, key handling
│   ├── models.py                 # AnalystType, AssetType enums
│   ├── stats_handler.py          # LLM/tool/token callback counter
│   ├── announcements.py, config.py
│   └── static/welcome.txt
└── tradingagents/
    ├── __init__.py               # loads .env, suppresses upstream warnings
    ├── default_config.py         # DEFAULT_CONFIG + env-override overlay
    ├── graph/                    # orchestration
    │   ├── trading_graph.py      # TradingAgentsGraph (the orchestrator)
    │   ├── setup.py              # GraphSetup.setup_graph() builds the StateGraph
    │   ├── conditional_logic.py  # edge routing (debate/risk loops)
    │   ├── propagation.py        # initial-state construction
    │   ├── analyst_execution.py  # analyst node specs + wall-time tracker
    │   ├── checkpointer.py       # per-ticker SqliteSaver
    │   ├── reflection.py         # post-outcome reflection LLM call
    │   └── signal_processing.py  # rating extraction (no LLM)
    ├── llm_clients/              # provider abstraction
    │   ├── factory.py, base_client.py
    │   ├── openai_client.py, anthropic_client.py, google_client.py, azure_client.py
    │   ├── capabilities.py       # declarative per-model quirk table
    │   ├── model_catalog.py, validators.py, api_key_env.py
    ├── agents/                   # node factories + schemas
    │   ├── __init__.py           # exports every create_* factory
    │   ├── schemas.py            # Pydantic decision schemas + renderers
    │   ├── analysts/, researchers/, managers/, risk_mgmt/, trader/
    │   └── utils/                # agent_states, agent_utils, tools, memory, rating, structured
    └── dataflows/                # data layer
        ├── interface.py          # vendor routing + fallback
        ├── y_finance.py, yfinance_news.py, stockstats_utils.py
        ├── alpha_vantage*.py
        ├── reddit.py, stocktwits.py
        ├── market_data_validator.py, symbol_utils.py, utils.py, config.py
```

---

## 12. Extending the Framework

| To add… | Do this |
|---|---|
| A new LLM provider (OpenAI-compatible) | Add it to `_OPENAI_COMPATIBLE` and `_PROVIDER_BASE_URL`, register its key in `PROVIDER_API_KEY_ENV`, and add models to `MODEL_OPTIONS`. |
| A new LLM provider (custom API) | Add a `BaseLLMClient` subclass and a branch in `create_llm_client()`. |
| A model with API quirks | Add a row to `capabilities._BY_ID` or a pattern to `_BY_PATTERN`. |
| A new data vendor | Implement the vendor functions and register them per method in `VENDOR_METHODS`. |
| A new symbol mapping | Add a row to `_ALIASES` (or rely on the forex/crypto rules) in `symbol_utils.py`. |
| A new env-overridable config key | Add a row to `_ENV_OVERRIDES` in `default_config.py`. |
| A new data tool | Add a `@tool` wrapper in `agents/utils/`, register it in a category in `TOOLS_CATEGORIES` + `VENDOR_METHODS`, and add it to the relevant `ToolNode` in `_create_tool_nodes()`. |

---

## 13. Code Quality & Patterns

- **Declarative tables over conditionals.** Provider quirks (`capabilities.py`), env overrides (`_ENV_OVERRIDES`), symbol aliases (`_ALIASES`), and model catalogs are data, not code branches — new cases are table rows.
- **Single source of truth.** The 5-tier rating (`rating.py`), the provider→key map (`api_key_env.py`), and the model catalog (`model_catalog.py`) each live in exactly one place to prevent drift.
- **Fail-open data, fail-loud config.** Network/data layers degrade gracefully (placeholders, fallbacks); missing config (API keys) fails loudly with an actionable message.
- **Atomic writes.** The decision log uses temp-file + `os.replace()` so a crash never corrupts it.
- **Lazy imports** in the factory keep test collection and partial use from pulling in heavy SDKs.
- **UTF-8 everywhere.** All file I/O passes explicit `encoding="utf-8"` (fixed Windows `cp1252` failures, #543/#550/#576).

---

## 14. Known Issues & Community-Reported Behavior

Drawn from the project's issue tracker; useful for operators triaging support.

| Issue | Status | Notes |
|---|---|---|
| Reddit 403 blocked (#935) | Mitigated | Automatic RSS fallback; scores/comments omitted |
| Local Ollama 404 / connection (#664, #843, #760) | Configuration | Set `OLLAMA_BASE_URL`, pull the model, use exact tag |
| Ollama model-tag mismatch (#657) | Configuration | `glm-4.7-flash-30b:latest` vs `glm-4.7-flash:latest` — use `ollama list` |
| OpenRouter "no OpenAI key" (#655) | Fixed | Each provider names its own key in the error |
| MiniMax connection errors (#851) | Open | Reasoning-split + capability handling added; verify region/key |
| DeepSeek Chinese output when localized (#740) | Addressed | Multi-language now covers every user-facing agent |
| Non-US benchmark broke alpha (#628) | Fixed | Per-exchange `benchmark_map` |
| Reports not saving end-to-end (#719, #736) | Fixed | Streamed chunks merged before save |

### Architectural notes & tech debt observed in source

- `cli/main.py` mixes the live-dashboard rendering, the streaming event loop, and report I/O in one ~1,300-line module; the display logic could be extracted from the orchestration loop.
- `tradingagents/__init__.py` carries two upstream-warning suppressions (langchain-core pending-deprecation, langgraph-checkpoint Reviver) explicitly flagged for removal once the upstream fixes ship — track those dependency bumps.
- The `reflect_and_remember()` BM25 memory system was removed in 0.2.4 in favor of the decision log; ensure no external callers still expect it.

---

## 15. Quick Reference

### Decision flow

```
START → analysts (loop with tools) → Bull⇄Bear → Research Manager
      → Trader → Aggressive→Conservative→Neutral (loop) → Portfolio Manager → END
```

### Debate-round math

- Research debate ends at `count ≥ 2 × max_debate_rounds`.
- Risk debate ends at `count ≥ 3 × max_risk_discuss_rounds`.

### Provider key env vars

`OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_API_KEY`, `AZURE_OPENAI_API_KEY`, `XAI_API_KEY`, `DEEPSEEK_API_KEY`, `DASHSCOPE_API_KEY`, `DASHSCOPE_CN_API_KEY`, `ZHIPU_API_KEY`, `ZHIPU_CN_API_KEY`, `MINIMAX_API_KEY`, `MINIMAX_CN_API_KEY`, `OPENROUTER_API_KEY`, `ALPHA_VANTAGE_API_KEY`

### Config env overrides

`TRADINGAGENTS_LLM_PROVIDER`, `TRADINGAGENTS_DEEP_THINK_LLM`, `TRADINGAGENTS_QUICK_THINK_LLM`, `TRADINGAGENTS_LLM_BACKEND_URL`, `TRADINGAGENTS_OUTPUT_LANGUAGE`, `TRADINGAGENTS_MAX_DEBATE_ROUNDS`, `TRADINGAGENTS_MAX_RISK_ROUNDS`, `TRADINGAGENTS_CHECKPOINT_ENABLED`, `TRADINGAGENTS_BENCHMARK_TICKER`, `TRADINGAGENTS_TEMPERATURE`, `TRADINGAGENTS_RESULTS_DIR`, `TRADINGAGENTS_CACHE_DIR`, `TRADINGAGENTS_MEMORY_LOG_PATH`

### Key exceptions

| Exception | Raised when |
|---|---|
| `ValueError: Unsupported LLM provider` | Unknown provider key |
| `ValueError: API key for provider '…' is not set` | Missing provider key |
| `AlphaVantageNotConfiguredError` | Alpha Vantage selected without `ALPHA_VANTAGE_API_KEY` |
| `AlphaVantageRateLimitError` | Alpha Vantage rate limit hit (triggers fallback) |
| `NoMarketDataError` | A vendor returned no rows for a symbol |
| `ValueError` (ticker validation) | Ticker fails `safe_ticker_component` path checks |

---

_Generated June 2026 from TauricResearch/TradingAgents at 04f434e. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
