This guide is the operational and architectural reference for TradingAgents, a multi-agent LLM financial-trading research framework built on 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
GraphSetupvia theanalyst_factoriesand the explicitcreate_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)
- Record the ticker; resolve pending memory-log entries for it (fetch realized returns, generate reflections, batch-write).
- If
checkpoint_enabled, recompile the graph with a per-tickerSqliteSaverand log whether it is resuming or starting fresh. - Resolve instrument identity deterministically (
resolve_instrument_context) and build the initial state (withpast_contextfrom the memory log). - Stream (debug) or invoke (non-debug) the graph to completion, merging per-node deltas into a final state.
- Write the full state to a JSON log under
results_dir/<TICKER>/TradingAgentsStrategy_logs/. - Store the decision as a pending entry in the memory log.
- Clear the checkpoint on success.
- 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)
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_IDor a pattern to_BY_PATTERNincapabilities.py— not to the client code. Newdeepseek-v5-*orMiniMax-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 |
| (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_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 underdata_cache_dir, and re-fetches on a poisoned (empty) cache.yf_retry()adds exponential backoff onYFRateLimitError. Technical indicators are computed locally viastockstats. - Alpha Vantage (
alpha_vantage_*.py): optional, keyed byALPHA_VANTAGE_API_KEY. Wraps OVERVIEW, BALANCE_SHEET, CASH_FLOW, INCOME_STATEMENT, the indicator endpoints, NEWS_SENTIMENT, and INSIDER_TRANSACTIONS. RaisesAlphaVantageNotConfiguredErrorwhen unkeyed andAlphaVantageRateLimitErroron 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, 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 inagents/utils/rating.pyand 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:
[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_entriesto 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. 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.
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.enterpriseat package import viapython-dotenv).load_dotenvusesoverride=False, so shell-exported values are never clobbered. - The CLI's
ensure_api_key()prompts for a missing key withquestionary.password(masked input) and persists it to.envviaset_key. - Missing keys raise a descriptive
ValueErrornaming the exact env var to set (#680).
Caution
Keys are written to a plaintext
.envin the working directory. Ensure.envis git-ignored (it is, by repo convention) and protected by filesystem permissions. In Docker, the.envis passed viaenv_fileand 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_limitto 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 Windowscp1252failures, #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.pymixes 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__.pycarries 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 |