TradingAgents — 12-Role Multi-Agent Trading Desk (Analyst Debate, Trader, Risk Team, Portfolio Manager)
Firm-style multi-agent trading desk: four analysts (market, fundamentals, news, sentiment) run in parallel, bull and bear researchers debate them, a trader drafts the plan, three risk debaters argue it and a portfolio manager approves or rejects. Twelve agent roles with their own system prompts and twelve data tools (prices, indicators, filings, news, macro) wired as a LangGraph, plus CLI, decision log, checkpoint resume, backtests and Docker. Python 3.11+, research only: it places no orders.
Uploaded by TalentDelta Team Member (Scout) · via TalentDelta
build 1.0.0 · 3593.5 KB · published 2026-10-19 · 212 files
Licence: Apache-2.0
Scouted from github.com/TauricResearch/TradingAgents at commit e8f5503e9b86d5c2710b0c76b1f686703968c9df (2026-10-16). Licence Apache-2.0. Upstream credit: Tauric Research (TauricResearch). Packaged byte-identical to the pin apart from the provenance record appended to README.md. Upstream code and licence stay upstream's.
Brought into the catalogue by TalentDelta Team Member (Scout).
What we like
A whole public research tree packaged unedited: twelve role modules with their own system prompts, twelve tool-decorated data functions bound to the four analyst nodes, plus graph wiring, CLI, decision log, checkpoints and backtests. The Apache-2.0 licence ships unmodified; the appended provenance section names the upstream project, commit and licence.
Potential improvements
Container-internal build paths in the Dockerfile and compose file read as the packer's own; dummy key strings in test fixtures keep tripping secret scanners.
Minimum requirements
Docker, or Python 3.11+ with the listed requirements, plus model and provider keys; some dataflows need a paid data subscription.
Security risks
No live credential, personal data or host path found. Research only — nothing places an order.
Tested: archive integrity, per-file match to the pinned upstream tree, licence, packaging read. Not tested: a live run — no provider keys or paid subscription here.
212 files · 4997.7 KB · previews are generated at approval and never executed
tradingagents-multi-agent-firm/.dockerignoretext · 0.1 KB
.git .venv .env .claude .idea .vscode .DS_Store __pycache__ *.egg-info build dist results eval_results Dockerfile docker-compose.ymltradingagents-multi-agent-firm/.env.enterprise.exampletext · 0.2 KB
# Azure OpenAI AZURE_OPENAI_API_KEY= AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ AZURE_OPENAI_DEPLOYMENT_NAME= # OPENAI_API_VERSION=2024-10-21 # optional, required for non-v1 APItradingagents-multi-agent-firm/.env.exampletext · 2.9 KB
# LLM Providers (set the one you use) OPENAI_API_KEY= GOOGLE_API_KEY= ANTHROPIC_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= MISTRAL_API_KEY= MOONSHOT_API_KEY= GROQ_API_KEY= NVIDIA_API_KEY= # SEC EDGAR (US company filings, point-in-time). No key; a contact address SEC can reach you at. #SEC_EDGAR_USER_AGENT=Your Name your@email.com # FRED (Federal Reserve macro data). Free key: https://fred.stlouisfed.org/docs/api/api_key.html #FRED_API_KEY= # TypeSafe Jev: screens StockTwits and Reddit posts for the Sentiment Analyst. Optional. #TYPESAFE_API_KEY= #TYPESAFE_DEFAULT_MODEL=jev-latest #TYPESAFE_BASE_URL=https://openrouter.ai/api # Jev through OpenRouter, with an OpenRouter key above # Custom OpenAI-compatible endpoint (vLLM, LM Studio, llama.cpp). Local servers need no key. #OPENAI_COMPATIBLE_API_KEY= # AWS Bedrock (pip install ".[bedrock]"). Bearer token, or the AWS credential chain; set the region either way. #AWS_BEARER_TOKEN_BEDROCK= #AWS_DEFAULT_REGION=us-west-2 #AWS_PROFILE= # ChatGPT Plus or Pro plan: no key; sign in once with `tradingagents login chatgpt`. #TRADINGAGENTS_LLM_PROVIDER=chatgpt # Remote Ollama server. Unset uses http://localhost:11434/v1. #OLLAMA_BASE_URL=http://your-ollama-host:11434/v1 # Override these DEFAULT_CONFIG keys. Provider, models, language and round counts also skip their CLI prompt. #TRADINGAGENTS_LLM_PROVIDER=openai #TRADINGAGENTS_DEEP_THINK_LLM=gpt-6-sol #TRADINGAGENTS_QUICK_THINK_LLM=gpt-6-luna # A tier on its own provider (set its model above too); its URL only for a local or relay endpoint. #TRADINGAGENTS_DEEP_THINK_PROVIDER= #TRADINGAGENTS_QUICK_THINK_PROVIDER= #TRADINGAGENTS_DEEP_THINK_BACKEND_URL= #TRADINGAGENTS_QUICK_THINK_BACKEND_URL= #TRADINGAGENTS_LLM_BACKEND_URL= #TRADINGAGENTS_OUTPUT_LANGUAGE=English #TRADINGAGENTS_MAX_DEBATE_ROUNDS=1 #TRADINGAGENTS_MAX_RISK_ROUNDS=1 #TRADINGAGENTS_MAX_TOOL_ROUNDS=20 #TRADINGAGENTS_CHECKPOINT_ENABLED=false # Turn off the start-up note about a newer release and Tauric Research notices. #TRADINGAGENTS_NO_UPDATE_CHECK=1 # Docker: a host folder for results, reports and the memory log (unset: the tradingagents_data volume). #TRADINGAGENTS_DATA_DIR=./data # Paths and alpha benchmark. Unset uses ~/.tradingagents and the regional index. #TRADINGAGENTS_RESULTS_DIR= #TRADINGAGENTS_CACHE_DIR= #TRADINGAGENTS_MEMORY_LOG_PATH= #TRADINGAGENTS_BENCHMARK_TICKER=SPY # Lower temperature means less run-to-run variation on models that honor it. #TRADINGAGENTS_TEMPERATURE=0.0 # Retry budget for every LLM SDK. Raise it to ride out 429 throttling. #TRADINGAGENTS_LLM_MAX_RETRIES=6 # Cap output tokens to bound a model that runs long and trips a timeout. #TRADINGAGENTS_MAX_TOKENS=8192 # Reasoning depth per provider; setting one skips its prompt. #TRADINGAGENTS_OPENAI_REASONING_EFFORT=medium #TRADINGAGENTS_GOOGLE_THINKING_LEVEL=high #TRADINGAGENTS_ANTHROPIC_EFFORT=hightradingagents-multi-agent-firm/.github/ISSUE_TEMPLATE/bug-report.ymlcode · 1.7 KB
name: Bug report description: Something fails, crashes, or behaves differently from the docs. labels: ["bug"] body: - type: markdown attributes: value: | Search [existing issues](https://github.com/TauricResearch/TradingAgents/issues?q=is%3Aissue) first, and check that the bug reproduces on the latest release. Remove API keys and other secrets from anything you paste. - type: input id: version attributes: label: Version description: Output of `pip show tradingagents | grep Version`, or `git rev-parse --short HEAD` for a source checkout. placeholder: "0.5.1" validations: required: true - type: dropdown id: entry attributes: label: How you run it options: - CLI (tradingagents) - Python package (TradingAgentsGraph) - Backtest (run_backtest or tradingagents backtest) - Docker validations: required: true - type: input id: models attributes: label: LLM provider and models placeholder: "openai, gpt-6-sol / gpt-6-luna" validations: required: true - type: input id: run attributes: label: Ticker and analysis date placeholder: "NVDA, 2026-09-23" - type: textarea id: what attributes: label: What happened, and what you expected validations: required: true - type: textarea id: repro attributes: label: Steps or code to reproduce description: The smallest steps or snippet that shows the problem. render: python - type: textarea id: logs attributes: label: Error output description: The full traceback or log lines, as text rather than a screenshot. render: texttradingagents-multi-agent-firm/.github/ISSUE_TEMPLATE/config.ymlcode · 0.4 KB
blank_issues_enabled: false contact_links: - name: Questions and showcases url: https://github.com/TauricResearch/TradingAgents/discussions about: Usage questions, ideas, and projects built on TradingAgents - name: Security vulnerability url: https://github.com/TauricResearch/TradingAgents/security/advisories/new about: Report privately; please do not open a public issuetradingagents-multi-agent-firm/.github/ISSUE_TEMPLATE/data-issue.ymlcode · 1.5 KB
name: Wrong or missing data description: A report shows a wrong price, figure, date or company, or data is missing for a symbol. labels: ["bug", "data"] body: - type: markdown attributes: value: | Data is served as of the analysis date: a historical run does not see later prices, filings or news, and some sources (news, StockTwits, Reddit) only serve recent items. A report that says a source is unavailable for a past window is expected. - type: input id: symbol attributes: label: Ticker and market placeholder: "0700.HK (Hong Kong)" validations: required: true - type: input id: date attributes: label: Analysis date placeholder: "2026-09-23" validations: required: true - type: dropdown id: kind attributes: label: Which data multiple: true options: - Prices or indicators - Fundamentals or statements - News - Social (StockTwits, Reddit) - Macro (FRED) - Insider transactions - Prediction markets - Company identity (wrong company or name) validations: required: true - type: textarea id: shown attributes: label: What the report shows, and what is correct description: Quote the report line, and give the correct value with a public source (exchange page, filing, Yahoo Finance link). validations: required: true - type: input id: version attributes: label: Version placeholder: "0.5.1" validations: required: truetradingagents-multi-agent-firm/.github/ISSUE_TEMPLATE/feature-request.ymlcode · 0.8 KB
name: Feature request description: A capability TradingAgents lacks, or a change to how it works. labels: ["enhancement"] body: - type: markdown attributes: value: | Any OpenAI-compatible endpoint already works through the `openai_compatible` provider, and OpenRouter serves many hosted models, so a new LLM provider is often not needed. - type: textarea id: problem attributes: label: The problem description: What you are trying to do, and where TradingAgents falls short today. validations: required: true - type: textarea id: proposal attributes: label: Proposed change validations: required: true - type: textarea id: alternatives attributes: label: Alternatives consideredtradingagents-multi-agent-firm/.github/workflows/ci.ymlcode · 2.0 KB
name: CI on: push: branches: [main, 'v[0-9]+.[0-9]+.[0-9]+'] pull_request: concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: test: name: tests (py${{ matrix.python-version }}${{ matrix.tz && format(', {0}', matrix.tz) || '' }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: ["3.11", "3.12", "3.13", "3.14"] # One run away from UTC, so a test that only passes in UTC fails here. include: - python-version: "3.12" tz: America/New_York env: TZ: ${{ matrix.tz }} steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install (with dev extras) run: | python -m pip install --upgrade pip pip install -e ".[dev]" - name: Run test suite run: pytest -q smoke-install: name: clean-install smoke runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.12" - name: Fresh install (no dev extras) and import run: | python -m pip install --upgrade pip pip install . # Catches undeclared runtime deps (e.g. #994 python-dotenv): a bare # install must import the package and the CLI module. python -c "import tradingagents, cli.main; print('clean-install import OK')" lint: name: ruff (strict, full repo) runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-python@v7 with: python-version: "3.12" - name: Install ruff run: pip install "ruff>=0.16" - name: Lint the repository # The repo is fully clean under the strict select, so we lint everything # (generated results/ is excluded via pyproject extend-exclude). run: ruff check .tradingagents-multi-agent-firm/.github/workflows/release-assets.ymlcode · 2.2 KB
name: Release assets # Builds the source archive and wheel from the release tag and attaches them to # the GitHub release, so a version can be installed without cloning: # pip install https://github.com/TauricResearch/TradingAgents/releases/download/<tag>/<wheel> # Run it by hand with a tag to attach builds to an earlier release. on: release: types: [published] workflow_dispatch: inputs: tag: description: "Release tag to build and attach, for example v0.6.0" required: true permissions: {} concurrency: group: release-assets-${{ inputs.tag || github.event.release.tag_name }} cancel-in-progress: false jobs: build: name: build and attach runs-on: ubuntu-latest permissions: contents: write env: TAG: ${{ inputs.tag || github.event.release.tag_name }} GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v7 with: # Release branches share their tag's name, so name the tag explicitly. ref: refs/tags/${{ env.TAG }} - uses: actions/setup-python@v7 with: python-version: "3.13" - name: Check the package version matches the tag run: | version=$(python -c "import re, pathlib; print(re.search(r'__version__ = \"([^\"]+)\"', pathlib.Path('tradingagents/__init__.py').read_text()).group(1))") if [ "v$version" != "$TAG" ]; then echo "tradingagents/__init__.py says $version, but the tag is $TAG" exit 1 fi - name: Build the source archive and wheel run: | python -m pip install --upgrade build python -m build - name: Install the wheel in a clean environment run: | python -m venv /tmp/smoke /tmp/smoke/bin/pip install dist/*.whl # From another directory, so the imports come from the wheel, not the checkout. cd /tmp /tmp/smoke/bin/python -c "import importlib.resources as r, tradingagents, cli.main; assert (r.files('cli') / 'static' / 'welcome.txt').is_file(); print('wheel install OK', tradingagents.__version__)" - name: Attach them to the release run: gh release upload "$TAG" dist/* --clobber --repo "$GITHUB_REPOSITORY"tradingagents-multi-agent-firm/.gitignoretext · 4.6 KB
# Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py.cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. # Pipfile.lock # UV # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # uv.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control # poetry.lock # poetry.toml # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. # https://pdm-project.org/en/latest/usage/project/#working-with-version-control # pdm.lock # pdm.toml .pdm-python .pdm-build/ # pixi # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. # pixi.lock # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one # in the .venv directory. It is recommended not to include this directory in version control. .pixi # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # Redis *.rdb *.aof *.pid # RabbitMQ mnesia/ rabbitmq/ rabbitmq-data/ # ActiveMQ activemq-data/ # SageMath parsed files *.sage.py # Environments .env .envrc .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. # .idea/ # Abstra # Abstra is an AI-powered process automation framework. # Ignore directories containing user credentials, local state, and settings. # Learn more at https://abstra.io/docs .abstra/ # Visual Studio Code # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore # and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ # Ruff stuff: .ruff_cache/ # PyPI configuration file .pypirc # Marimo marimo/_static/ marimo/_lsp/ __marimo__/ # Streamlit .streamlit/secrets.toml # Cache **/data_cache/ # Enterprise env file (secrets) and generated run reports .env.enterprise reports/tradingagents-multi-agent-firm/CHANGELOG.mdmarkdown · 49.2 KB
Changelog
All notable changes to TradingAgents are documented here.
The format is based on Keep a Changelog, and this project follows Semantic Versioning. Changes that need action when upgrading are listed under "Upgrading from" in their release.
[0.7.0] — 2026-10-16
Runs on your ChatGPT plan, backtests scored from the first close after each decision, a note at start-up when a newer release is out, and a wheel attached to each GitHub release.
Added
- Your ChatGPT plan. The
chatgptprovider runs TradingAgents on a ChatGPT Plus or Pro plan instead of an API key, through OpenAI's sign-in for open-source apps. Choose "Subscription" in the CLI's provider menu; for unattended runs, Docker and Python (llm_provider="chatgpt"), sign in once withtradingagents login chatgpt.tradingagents logout chatgptrevokes the sign-in. The CLI lists the plan's models, and reasoning effort applies as it does for OpenAI. (#1194, #1475) - What's new at start-up. The interactive CLI mentions a newer release, with the command that upgrades this install when it can tell how TradingAgents was installed, and notices from Tauric Research for the running version. It checks at most once a day in the background, so a run never waits on it; runs without a terminal never check, and
TRADINGAGENTS_NO_UPDATE_CHECK=1turns it off. - Release downloads. Each GitHub release carries the source archive and a wheel, so a version installs with
pip installfrom its release page without cloning.
Upgrading from 0.6.0
cryptography42 andpackaging24 or later are installed.- Decisions are now scored from the first close after their date. Outcomes already settled in a memory log keep the earlier entry, the close of the decision date, so a backtest resumed with
run_idmixes the two; start a new run to compare like with like.
Fixed
- A decision is no longer scored against news it could not have traded on: a run dated d reads news published after the close on d, so the memory log and backtests enter it at the first close after d, not the close of d. (#1518)
- A run without a terminal, such as a scheduled job or CI, no longer contacts the announcements server before it starts.
- A model tier on another provider keeps its configured reasoning effort, thinking level or Claude effort when the main provider has none. (#1440)
Contributors
@LeonWTW, @OleksanderSS, @sovorn-c.
[0.6.0] — 2026-10-03
HTML reports, a provider per model tier, past decisions settled for every ticker while the analysts work, and company news read while Yahoo's news feed is down.
Added
- HTML report. A saved report also includes
complete_report.html, the report as one page with its sections listed beside the text, that reads well in a browser, on a phone and in print. Answering the save question at the prompt also asks about the page and can open it in your browser;--no-htmlorsave_reports(..., html=False)skips it. The page is self-contained: it loads nothing and runs nothing. (#1419) - A provider per model tier. The quick and deep models can run on different providers, for example the managers on Claude while the analysts run on OpenAI: set
deep_think_providerorquick_think_provider(TRADINGAGENTS_DEEP_THINK_PROVIDER,TRADINGAGENTS_QUICK_THINK_PROVIDER) with the tier's model. The report header names each tier's provider. (#1440) - Past decisions settle alongside the analysts. While a run's analysts work, every logged decision whose holding period has passed is settled, for any ticker, not only the one being analyzed. If settling fails, the run goes on and the report header says so. (#1428, #1445)
- Crypto on Reddit. For a crypto ticker the Sentiment Analyst reads r/CryptoCurrency and r/CryptoMarkets, plus r/Bitcoin, r/ethereum or r/solana for those coins, instead of the stock subreddits. (#1461)
settle_all_pending()settles every due decision without running an analysis, for a scheduled job; it andsettle_pending()return the decisions settled and any that could not be. (#1445)- The report header names the run's rating.
- Euro-area macro series by name:
ecb_deposit_rate,ecb_main_refi_rate,euro_hicp,euro_core_hicp,euro_real_gdp,germany_10y,france_10yandeur_usd. (#1465)
Models
- A Gemini call times out after 600 seconds, as OpenAI and Anthropic calls do, so a stalled call no longer hangs the run. (#1417)
Upgrading from 0.5.2
- markdown-it-py 4.0 or later is installed.
- A saved report also writes
complete_report.html; pass--no-html, orhtml=Falsetosave_reports(), to keep only the Markdown. - A run no longer settles its ticker's decisions before it starts; the Memory Log step settles every ticker's during the run.
settle_pending()returns aSettlementwithsettledandfailed(wasNone).- Checkpoints from 0.5.2 are not resumed.
Fixed
- Company news is read from Yahoo search while Yahoo's news feed returns nothing. When Yahoo has no article about the symbol, its news is reported unavailable, not absent, and a configured next news vendor is tried. (#1467, #1469)
- A rating quoted from someone else, such as "Consensus rating: Buy", no longer replaces the Portfolio Manager's own. (#1466)
- A Yahoo news article with a null field no longer loses the ticker's news. (#1458)
- A start date a model writes with stray characters is sent back to it to correct, instead of failing the news or price request. (#1476)
- A request Alpha Vantage rejects is reported as unavailable, not served as data; the API key is kept out of every Alpha Vantage error. (#1442)
- Yahoo fundamentals print each money figure in its own currency: London prices in GBp, market cap and per-share figures in GBP, and an ADR's income figures in its reporting currency, such as TWD for TSMC. (#1456)
- Jev screening uses the endpoint
TYPESAFE_BASE_URLnames, so it can run through OpenRouter. (#1416) - Runs sharing a memory log settle each decision once, and an interrupted settlement keeps the outcomes it finished.
- The report header names the sentiment analyst
sentiment, as the CLI does. requirements.txtsays the dependencies are declared inpyproject.toml.
Contributors
@amitbhansali, @AyushJain-601, @BichengWang, @byte7m, @Dangerboi, @davidalmeida90, @Jackzigen, @jhill-cmd, @Recnelis0, @seaaxe, @vaegev.
[0.5.2] — 2026-09-29
Parallel analysts, unattended CLI runs, reports that record what produced them, and past runs served only what was known on their date.
Added
- Parallel analysts. The selected analysts run at the same time, so the analyst phase takes about as long as the slowest one; the CLI shows each analyst's progress and time as it finishes, and the debate starts when all reports are in. (#1255, #433)
- Unattended CLI runs. Flags answer the per-run questions, and
TRADINGAGENTS_*variables the rest, so a scheduled job or script asks nothing:tradingagents --ticker NVDA --date 2026-09-26 --analysts market,news --save --no-show. Without a terminal, a missing answer stops the run before it starts and names what to set. (#1127, #1133) - Reports record what produced them. Each report opens with the analysis date, version, provider, models, analysts, rounds and data vendors; the state log keeps them as
run_settings. (#752, #1197) - Docker data folder.
TRADINGAGENTS_DATA_DIRkeeps results, reports and the memory log in a host folder. (#865) - A cap on an analyst's tool calls. After
max_tool_roundsrounds (default 20,TRADINGAGENTS_MAX_TOOL_ROUNDS) the analyst writes its report from what it has. (#1420) tradingagents backtestshows each cell as it starts and the--run-idto continue;--analystsacceptssentiment.- Benchmarks for Taiwan, Korea, Singapore and the main European exchanges. (#1392)
- Python 3.14 support; the Docker image runs Python 3.13.
Models
deepseek-flashis treated as a thinking model, so structured output works. (#1388)
Upgrading from 0.5.1
- Python 3.11 or later is required; pandas 3 is installed.
- Minimum versions: yfinance 1.7.0, langchain-core 1.6.5, langchain-openai 1.6.6, langchain-anthropic 1.7.4, langchain-google-genai 4.4.0, langgraph 1.2.12, langgraph-checkpoint-sqlite 3.1.1, langchain-aws 1.7.9 (
bedrockextra). - Statements default to SEC EDGAR, then Yahoo (
fundamental_data = "sec_edgar,yfinance"). - A past-dated run gets no Yahoo or Alpha Vantage statements or insider trades, which carry no filing date, and only the company's current name, marked as such.
- The memory log is the
tradingagents.memorypackage (wastradingagents.decision_log); settlement and reflection moved there fromtradingagents.graph, andReflectoris no longer exported fromtradingagents.graph. process_signalis removed; the rating ispropagate()'s second value orfinal_state["final_rating"].- A rating is read only from its
Rating:label; a free-text decision without one isREVIEW. - Data-layer functions take
as_of_date=(wascurr_date=);build_instrument_contextandresolve_instrument_contexttaketrade_date=. VendorRateLimitErrorisVendorUnavailableError.- Data tools take the instrument from the run's state; tool calls no longer carry
symbolorticker. - State log:
trader_investment_plan(wastrader_investment_decision),judge_decisionremoved,final_ratingandrun_settingsadded. - Checkpoints from 0.5.1, or saved under different settings, are not resumed.
Fixed
- Data tools serve the run's instrument, even when the model passes another symbol.
- A failed vendor request is reported as unavailable, not as a missing or delisted symbol; one vendor's "no data" no longer speaks for a chain in which another failed. (#1386)
- The rating is the Portfolio Manager's own, not one its thesis quotes (#1383); its line keeps an English label in other output languages (#1435).
- Backtest cells given twice run once (#1415), and settlement compares a decision with its benchmark over the same dates.
- A snapshot for a symbol with no prices no longer ends the run.
- Concurrent runs no longer drop memory log entries.
- SEC EDGAR quarterly figures filed only year to date are served, labelled with their span.
- Yahoo dividend yield and debt-to-equity print as percentages. (#1414)
- The FRED change line names the dates it spans. (#1397)
stream_runserves the graph's own config to its tools.- The test suite stays off the network and out of the user's files, and passes in any order; integration tests run with
-m integration. (#1395)
Contributors
@1Wizzy, @BichengWang, @Chaoqi31, @davidalmeida90, @djconnexion77, @fuzing, @hailampy123, @hesam-shams, @HEYALT, @Jackzigen, @kagura-agent, @loulanyue, @mannubaveja007, @olivergpt, @rita112025-cpu, @shuyan-code, @SingTheCode, @sjq597, @xiaodu55, @Youholdme.
[0.5.1] — 2026-09-24
A package layout organised by what each module holds, social posts screened by TypeSafe's Jev when a key is set, GPT-6 Sol and Luna as the default models, and fixes to run isolation, SEC EDGAR statements and historical runs.
Upgrading from 0.5.0
Some modules moved, and the old import paths are gone. Update imports as follows:
tradingagents.dataflows.interfaceistradingagents.dataflows.router, anddataflows.symbol_utilsisdataflows.symbols.dataflows.utilsis gone:get_current_dateis indataflows.date_window,safe_ticker_componentindataflows.symbols, andget_scrubbedandvendor_reachableindataflows.net.- Vendor modules live under
tradingagents.dataflows.vendors:yahoo(ohlcv,market,fundamentals,news,snapshot, from the formerstockstats_utils,y_finance,yfinance_newsandmarket_data_validator),alpha_vantage(a package, from thealpha_vantage_*modules), andsec_edgar,fred,polymarket,reddit,stocktwits. tradingagents.agents.utilsis gone: the agent tools are inagents.tools, andagent_utils,agent_states,ratingandstructuredareagents.context,agents.state,agents.ratingandagents.structured.- The decision log is
tradingagents.decision_log(wasagents.utils.memory), andcli.utilsiscli.prompts. backtest.summarizetakes arun_backtestresult or the path of a decision log, in place of aTradingMemoryLog.- Removed:
SignalProcessor(the rating is parsed byprocess_signal), thecreate_social_media_analystalias (usecreate_sentiment_analyst),symbol_utils.is_yahoo_safe, the unusedproject_dirconfig key, and the graph attributescurr_state,tickerandlog_states_dict, which held the previous run's state.
Added
- Jev post screening. With
TYPESAFE_API_KEYset, TypeSafe's Jev reads each StockTwits and Reddit post the Sentiment Analyst fetches: posts that are not about the company are dropped, and each source opens with a count of the rest by stance. Without the key nothing changes. (#1376) - B3 tickers (
.SA) are benchmarked against the Ibovespa. (#1366)
Models
- GPT-6 Sol and GPT-6 Luna are the default deep and quick models, and Claude Opus 5.5 replaces Opus 5 in the picker. Opus 5 and GPT-5.4 Mini remain valid model IDs.
Fixed
- Several graphs in one process each read their own data vendors. (#1369)
- SEC EDGAR cash flow statements find capital expenditure for filers that moved it to purchases of productive assets (NVIDIA since fiscal 2022, Amazon since 2016), whose recent periods read as empty. (#1370)
- SEC EDGAR annual statements list fiscal years only.
- A historical run is no longer told today's date, or a date after the run, in coverage notices and the instrument context.
- The Fundamentals Analyst can call the insider transactions tool.
- An unreachable Yahoo on insider transactions is reported as unavailable, not as a symbol without data.
- A graph reused across runs, as in a backtest, no longer keeps every run's full state.
- A checkpointed CLI run says whether it resumed or started fresh.
- A blank path variable (
TRADINGAGENTS_RESULTS_DIRand the like) keeps the default path. - StockTwits messages reach the prompt as plain text rather than HTML-escaped.
- The test suite is independent of the developer's
.env, time zone and network. (#1368, #1372)
Contributors
Thanks to everyone who reported these or sent a fix:
@codify88, @davidalmeida90, @duongylinh, @jccl2, @yuina368.
[0.5.0] — 2026-09-18
Point-in-time integrity across every dated path, decisions that are recorded as they were made, backtesting over a grid of tickers and dates, the caller's portfolio as run input, and SEC EDGAR fundamentals served as filed.
Highlights
- Fundamentals as filed. SEC EDGAR serves US company statements as they stood on the run's date: a period that has ended but has not been filed is not served, and a figure restated later still reads as first reported. Keyless, opt-in via the vendor chain.
- Backtesting.
run_backtestruns the pipeline over a ticker and date grid into its own decision log, andsummarizescores the settled cells;tradingagents backtestdoes the same from the CLI. - Portfolio context.
propagate(..., portfolio=...)and--portfoliolet the trader, risk and portfolio agents size against real holdings. A run without one is never treated as a flat book. - Decisions are recorded as made. An unreadable decision is flagged for review everywhere instead of becoming a tradeable Hold, and a rating argued against is no longer read as the call.
Point-in-time and honest attribution
- Dated tools take the run's date from graph state, so an omitted or later date cannot reach a vendor. (#1331, #1319, #1118)
- Insider filings and prediction-market odds are bounded by the run date; insider rows state that a trade becomes public when its Form 4 is filed.
- A feed that never observed a window reports it as unavailable rather than as an absence, across news, Reddit and StockTwits.
- The resolved company identity says when it describes today rather than the run date.
- The verification snapshot quotes the prices the vendor reported, never a gap-filled value.
- A vendor failure is a vendor failure: yfinance raises instead of returning its errors as text, an outage is not reported as a company with no data, and a chain where every vendor is unavailable says so instead of ending the run.
- The macro vintage pin is clamped to the vendor's own clock, so a run dated today cannot ask for a vintage it does not have.
- A historical run is not served a present-day company profile by either fundamentals vendor. (#1300)
Decisions and evaluation
- The labelled rating decides, whatever separates it, and prose naming several ratings is reviewed rather than guessed.
- Decision prompts state the shape of their answer, so a provider without structured output still returns a readable decision.
- A report that was not produced says so, instead of appearing as an empty section.
- Backtest scoring reads the direction each rating claimed: a Sell that fell is a hit, and Hold reports no hit rate.
- The outcome window is configurable (
holding_period_days), and reflection states the window it judges. - A settled decision is not logged twice, and a failed reflection no longer stops the next run. (#645)
- The trader states entry and stop levels as prices, so a percentage no longer fails the whole proposal. (#1288)
CLI
tradingagents backtest, with--run-idto continue an interrupted sweep. (#1234)- The previous run's selections come back as prompt defaults. (#1236, #920)
- A run with no readable rating says so; the live view no longer scrolls; messages that read like Python values are shown. (#649, #784)
- The state log keeps non-ASCII readable. (#1081)
Data sources
- SEC EDGAR fundamentals vendor (US filers, keyless).
- Hong Kong and Shanghai tickers resolve to the symbols Yahoo serves. (#1342, #957, #1260)
- Reddit is fetched as one combined request per run. (#1286)
- One OHLCV cache file per symbol. (#1330)
Models
- Current lineups for every provider: GPT-6 Astra and the GPT-5.6 family, Gemini 3.8 Flash, Claude Opus 5 and Fable 5.1, Grok 4.6, DeepSeek Flash, Qwen 3.8, GLM-5.3, MiniMax M3, Kimi K3 and the current Mistral snapshots.
- Every provider accepts a model ID the picker does not list.
- GLM traffic goes to the platform its key belongs to, and Ollama structured output no longer sends a tool_choice it rejects. (#1062)
Changed
- The memory log records
REVIEWfor a decision with no readable rating, where it previously recordedHold. - Optional fields the model did not provide are named as such rather than omitted.
- Removed dependencies nothing imports: backtrader, redis, setuptools, langchain-experimental, parsel, tqdm. (#1353, #1070)
Contributors
Thanks to everyone who reported these or sent a fix:
@akashkpfreelancer, @angziii, @anupamme, @AyushKar2005, @bulkypanda, @CadeYu, @chiang21fcb, @dajiaohuang, @dewrama, @DogInfantry, @emitov, @farukerdem34, @flydragon2018, @fusshell, @Ganesh1729-ui, @gyx09212214-prog, @hamzabudeir, @ihsieh31, @jaylew20250206, @kaushik-yadav, @kbnnf, @kevinkda, @LudwigJMarx, @lx7720, @malandrindev, @mhd325ic-hash, @minhdn90, @miznan, @mmssix, @mrbob-git, @newnewself, @prithvirajrh, @PyriteResearch, @Rajatendu1, @Recnelis0, @Rodvask, @samhoooo, @sheiun-xu, @shivsin25, @SmileShaun, @SonnyRajagopalan, @taro0915, @wupengbo125, @wxggzz, @Yixiang-Wu, @ZahirBodrike, @ZHUYAWEI, @zkwang616.
[0.4.0] — 2026-08-31
Look-ahead and point-in-time fixes across the data and memory layers, clearer decision signals, working CLI checkpoint resume, and the GPT-5.6 / GLM-5.3 models.
Fixed
- FRED macro look-ahead. Historical macro requests were served from today's data vintage, leaking later revisions into a backtest; both the observations and metadata requests now pin the vintage to the as-of date. (#1275)
- Social sentiment look-ahead. StockTwits and Reddit were fetched with no
date, so a historical run showed today's chatter as if it were from the as-of
date; the social path is now trimmed to the analysis window, via one shared
UTC half-open window rule (
dataflows/date_window) used by news too. (#1220) - Memory point-in-time guard.
get_past_contextreturned every resolved lesson regardless of the run date; each resolved entry now records the date its outcome became known, and a historical run only sees lessons resolved by the trade date. (#1251) - Premature reflection. A decision was settled on a partial return if a rerun happened before its holding window fully traded; resolution now waits for the full window. (#1169)
- Latest OHLCV bar dropped. The newest bar with a NaN close was silently dropped before the date cutoff, making the previous trading day look like the latest; dates are normalized per element (DST- and non-US-market safe) and a missing latest close raises rather than falling back. (#1201)
- Debate opening fabrication. The first speaker in each debate round rebutted an empty opponent response, fabricating the other side; all five debators now open with their own case when no opponent has spoken. (#1176)
- Silent Hold. An unparseable Portfolio Manager rating (including a fullwidth
colon) was coerced to a tradeable Hold; it now surfaces a
REVIEWsentinel, withparse_ratingkeeping its silent default for compatibility callers. (#1170) --checkpointwas a no-op on the CLI. Checkpoint setup lived only inpropagate(); the CLI streamed the checkpointer-less graph. The lifecycle is now shared, and a resume feedsNoneso LangGraph continues the interrupted run instead of duplicating messages. (#1249)- DeepSeek via OpenRouter.
deepseek/<id>fell through to default capabilities and had object-formtool_choiceforced on it; the official namespace is stripped so it reuses the native DeepSeek quirks. (#1199) - Trader price grounding. The Trader saw only the digested plan; it now also receives the technical market report so entry/stop levels anchor to real price structure. (#1167)
Added
- Configurable output-token cap.
max_tokens/TRADINGAGENTS_MAX_TOKENS, forwarded to every provider (Gemini asmax_output_tokens), so a model that emits unbounded reasoning can be bounded instead of hanging. (#1204) - Latest models. Added the GPT-5.6 family (
gpt-5.6/gpt-5.6-terra/gpt-5.6-luna) and GLM-5.3 (glm-5.3,glm-5.3-flash). The default models are nowgpt-5.6(deep) andgpt-5.6-luna(quick).
Contributors
Thanks to everyone who reported these or sent a fix:
@PyriteResearch, @yiran1268, @fabiolenine, @lx7720, @taro0915, @Jaswanth-Sriram-Veturi, @ariesy, @liangzj1999, @zkwang616, @aniketshukla1, @loulanyue, @hudsonwa, @daleselaji-dev, @wolfoswald777-crypto.
[0.3.1] — 2026-07-05
Correctness and stability patch: data look-ahead, graph-router crash-safety, checkpoint identity, crypto sentiment sources, and configurable resilience.
Fixed
- Alpha Vantage look-ahead filter now runs. The fundamentals payload is a JSON string, so the dict-only guard skipped filtering and future-dated reports leaked into historical runs; parse before filtering. (#1115, @zachthebird)
- News analyst prompt matches the tool. The prompt advertised
get_news(query, ...)but the tool takes a ticker; aligned to stop hallucinated free-text query calls. (#1116, @shcheuk) - Shared debate/risk routers can't crash mid-run. Both routers return more targets than any one edge mapped; every edge now shares the complete path map, so a fall-through under prompt/i18n/refactor drift stays routable. (#1088, @Fr3ya, @sa7an7, @Sushanth012)
- Checkpoint resume respects graph shape. The thread id folds in selected analysts, debate/risk depth, and asset mode, so a resume under different choices no longer continues the wrong graph. (#1089, @bossjoker1, @Ghraven)
- Crypto sentiment sources resolve. StockTwits lists crypto as
<BASE>.X(Yahoo'sBTC-USD404s) and Reddit needs the base symbol to match; the social path now maps crypto correctly for both. (#1113, @suremadoreai)
Added
- Configurable LLM retry budget.
llm_max_retries/TRADINGAGENTS_LLM_MAX_RETRIESis forwarded to every provider, so a transient 429 burst no longer aborts a run. (#1091, @yanggaome) - Bedrock API-key auth.
AWS_BEARER_TOKEN_BEDROCKauthenticates Amazon Bedrock without AWS access keys and takes precedence over an ambientAWS_PROFILE. (#1103, @praxstack) - Latest Claude models. Added Claude Sonnet 5 (
claude-sonnet-5) and Fable 5 (claude-fable-5); effort control now covers the Claude 5 line.
[0.3.0] — 2026-06-22
Stabilization and extensibility release: a CI gate, a unified verified data-access contract, a provider and data-vendor registry, and a maintenance sweep that hardened config precedence, the model catalog, data resilience, and structured output.
Added
- CI gate. GitHub Actions runs the pytest suite across Python 3.10-3.13,
strict
ruff, and a clean-install smoke that imports the package and CLI to catch undeclared dependencies. (#994, #197) - Provider registry. OpenAI-compatible providers register as a single spec,
and a generic
openai_compatibleendpoint covers vLLM, LM Studio, and relays. Adds NVIDIA NIM, Kimi, Groq, Mistral, and a native Amazon Bedrock client. - Macro and prediction-market vendors. FRED macro indicators and Polymarket event probabilities, surfaced to the news and macro analysts.
- Programmatic report output.
TradingAgentsGraph.save_reports()writes the same report tree the CLI produces, for headless and API runs. (#1037) - Env-configurable reasoning depth via
TRADINGAGENTS_OPENAI_REASONING_EFFORT,TRADINGAGENTS_GOOGLE_THINKING_LEVEL, andTRADINGAGENTS_ANTHROPIC_EFFORT, each gated to the models that accept it.
Changed
- Verified data-access contract. Symbol normalization on every vendor path
(identity, returns, CLI, news); the configured vendor list is the exact
resolution chain with no silent fallback to unselected vendors; a typed
VendorErrortaxonomy; look-ahead-safe news windows; stale-OHLCV rejection; inclusive yfinance date ranges. - Config precedence. An explicit
TRADINGAGENTS_*value or CLI flag now wins over interactive defaults for debate and risk round counts,--checkpoint / --no-checkpoint, and the Docker provider profile; invalid boolean env values fail loudly. (#975, #976, #977) - Current-generation model catalog. Refreshed provider lineups; retired
gpt-4.1, Claude Sonnet 4.5, and the Gemini 2.5 line. - Optional vendors degrade instead of aborting a run: a failed macro or prediction-market lookup returns a no-data sentinel.
- Analyst prompts lead with the current date so tool-call date ranges anchor to the run date rather than the model's training cutoff. (#836)
Fixed
- Instrument identity. Deterministic ticker-to-company resolution prevents wrong-company hallucination, and a verified market-data snapshot grounds price and indicator claims. (#814, #830)
- Social and market data sources. Reddit RSS-first with 429 backoff, StockTwits transport hardening, and Alpha Vantage timeout plus key-versus-rate-limit handling.
- Structured output. Local OpenAI-compatible servers no longer reject
object-form
tool_choice; a thinking model that returns no parsed result falls back to free text; null-ish strings in optional price fields coerce toNone. (#1038, #1051, #1057)
Removed
- The no-op
analyst_concurrency_limitconfig knob; parallel analyst execution is planned for a later release. (#979) - The unused committed
uv.lock. (#1030)
Contributors
Thanks to everyone who shaped this release through code, design, and reports:
@CadeYu, @Zavianx, @weijianz-opc, @naltun, @brahmasky, @nik2208, @thieucong98, @Derekko-web, @LukiPrince, @Eddieargenal, @Ghraven, @ms32035, @yting27, @nyxst4ck, @KenCheung-AIxFinance, @yangyusheng2n, @fareloj, @haosenwang1018, @octo-patch, @seifenk, @CaoYuhaoCarl, @mihailnica10, @Dado-hash, @Handsomemikezzz, @ydhawesome, @macd2, @AyushKar2005, @wildhuman, @robert23kim, @bngness, @tedix-rodrigo, @malaccan, @rfalken78, @dengli1971-droid, @proofconcept39, @prasta1, @liximin, @jeffhuen, @mazar, @soyangelromero, @CNQQC, @dovetaill, @fperdigon, @gyx09212214-prog, @RSXLX.
[0.2.5] — 2026-05-11
Added
- Grounded Sentiment Analyst. The renamed
sentiment_analystnow reads real Yahoo News, StockTwits, and Reddit data before generating its report, replacing the prior flow that could fabricate social posts under prompt pressure. (#557, #607) - MiniMax provider with the full M2.x catalog (M2.7 / M2.5 / M2.1 / M2
plus highspeed variants, 204K context). Dual-region: Global
(
MINIMAX_API_KEY) and China (MINIMAX_CN_API_KEY). - Dual-region Qwen and GLM with separate keys per region — international
(
DASHSCOPE_API_KEY,ZHIPU_API_KEY) and China (DASHSCOPE_CN_API_KEY,ZHIPU_CN_API_KEY), selectable via a secondary region prompt. (#758) TRADINGAGENTS_*env-var configurability forDEFAULT_CONFIG. Overridellm_provider, deep/quick model IDs,backend_url,output_language, debate-round counts, checkpoint flag, and benchmark ticker via.envwith type-aware coercion (string / int / bool). (#602)- Interactive API-key detection in the CLI. When the selected provider's
key is missing, the CLI prompts for it and persists the value to
.envso the analysis run continues without restart. - Remote Ollama support.
OLLAMA_BASE_URLpoints the CLI and the programmatic client at a remoteollama-serve. The CLI surfaces the resolved endpoint and warns on common malformed inputs. Adds a"Custom model ID"option for models pulled viaollama pull. (#648, #768) - Configurable news-fetch parameters in
DEFAULT_CONFIG— per-ticker article limit, macro headline limit, lookback window, and macro search queries. (#606, #683) - Configurable alpha benchmark for non-US tickers. Replaces hardcoded
SPY with regional indices for
.NS(^NSEI),.T(^N225),.HK(^HSI),.L(^FTSE),.TO(^GSPTSE),.AX(^AXJO),.BO(^BSESN); explicitbenchmark_tickeroverride available. Eliminates FX drift dominating alpha for non-USD listings. (#628, #684) - Multi-language output covers every user-facing agent — researchers, risk debators, research manager, and trader, ending the previous partial-localization reports. (#575)
- Model catalog refresh. OpenAI GPT-5.5 frontier, Anthropic Claude Opus
4.7, Gemini 3.1 Flash-Lite GA, xAI Grok 4.20, Qwen 3.6 line. Versioned IDs
only; auto-shifting aliases moved to the
"Custom model ID"option.
Changed
- Sentiment Analyst is now consistently named across the CLI dropdown,
status panel, and final reports (previously the backend was renamed but
the CLI still said "Social Analyst"). The
AnalystType.SOCIAL = "social"wire value is kept for saved-config back-compat.
Fixed
- Structured output works on DeepSeek V4 / reasoner and MiniMax M2.x.
Those providers reject
tool_choiceper their tool-calling docs; the binding flow now skips it automatically via a capability table. pip install .installations pick up the project.envwhen running the CLI as a console script. (#747)- Reports save end-to-end — streamed chunks were previously dropped from
complete_report.md. (#719, #736) - Ticker prompt preserves exchange suffixes (
.SH,.SZ,.SS,.HK,.T, etc.) for A-share, HK, Tokyo, and other non-US flows. (#770) - Docker permission errors no longer block first-run write to
~/.tradingagents/. (#519, #627, #672, #771) - Config state no longer leaks between runs when sub-dicts are mutated;
set_configpartial updates preserve sibling defaults. (#788) max_recur_limitconfig actually applies — previously read but not forwarded to the propagator. (#764)- Missing-API-key error names the exact env var to set. (#680)
- Quieter startup — suppressed the noisy upstream
LangChainPendingDeprecationWarningfrom langgraph-checkpoint; will be removed once that package ships its fix.
Security
- Ticker path-traversal validation at every filesystem-path site (cache, checkpoint database, results) so a malicious ticker cannot escape its intended directory. (#618)
0.2.4 — 2026-04-25
Added
- Structured-output decision agents. Research Manager, Trader, and Portfolio
Manager now use
llm.with_structured_output(Schema)on their primary call and return typed Pydantic instances. Each provider's native structured-output mode is used (json_schemafor OpenAI / xAI,response_schemafor Gemini, tool-use for Anthropic, function-calling for OpenAI-compatible providers). Render helpers preserve the existing markdown shape so memory log, CLI display, and saved reports keep working unchanged. (#434) - LangGraph checkpoint resume — opt-in via
--checkpoint. State is saved after each node so crashed or interrupted runs resume from the last successful step. Per-ticker SQLite databases under~/.tradingagents/cache/checkpoints/.--clear-checkpointsresets them. (#594) - Persistent decision log replacing the per-agent BM25 memory. Decisions
are stored automatically at the end of
propagate(); the next same-ticker run resolves prior pending entries with realised return, alpha vs SPY, and a one-paragraph reflection. Override path withTRADINGAGENTS_MEMORY_LOG_PATH. Optionalmemory_log_max_entriesconfig caps resolved entries; pending entries are never pruned. (#578, #563, #564, #579) - DeepSeek, Qwen (Alibaba DashScope), GLM (Zhipu), and Azure OpenAI providers, plus dynamic OpenRouter model selection.
- Docker support — multi-stage build with separate dev and runtime images.
scripts/smoke_structured_output.py— diagnostic that exercises the three structured-output agents against any provider so contributors can verify their setup with one command.- 5-tier rating scale (Buy / Overweight / Hold / Underweight / Sell) used consistently by Research Manager, Portfolio Manager, signal processor, and the memory log; Trader keeps 3-tier (Buy / Hold / Sell) since transaction direction is naturally ternary.
- Pytest fixtures — lazy LLM client imports plus placeholder API keys so the test suite runs cleanly without credentials. (#588)
Changed
backend_urldefault is nowNonerather than the OpenAI URL. Each provider client falls back to its native default. The previous default leaked the OpenAI URL into non-OpenAI clients (e.g. Gemini), producing malformed request URLs for Python users who switched providers without overridingbackend_url. The CLI flow is unaffected.- All file I/O passes explicit
encoding="utf-8"so Windows users no longer hitUnicodeEncodeErrorwith the cp1252 default. (#543, #550, #576) - Cache and log directories moved to
~/.tradingagents/to resolve Docker permission issues. (#519) SignalProcessorreads the rating from the Portfolio Manager's rendered markdown via a deterministic heuristic — no extra LLM call.- OpenAI structured-output calls default to
method="function_calling"to avoid noisyPydanticSerializationUnexpectedValuewarnings emitted by langchain-openai's Responses-API parse path. Same typed result, no warnings.
Fixed
- Empty memory no longer triggers fabricated past-lessons in agent prompts; the memory-log redesign makes this structurally impossible since only the Portfolio Manager consults memory and only when entries exist. (#572)
- Tool-call logging processes every chunk message, not just the last one, and memory score normalization handles empty score arrays. (#534, #531)
Removed
FinancialSituationMemory(the per-agent BM25 system) and the deadreflect_and_remember()plumbing; subsumed by the persistent decision log.- Hardcoded Google endpoint that caused 404 when
langchain-google-genaichanged its API path. (#493, #496)
Contributors
Thanks to everyone who shaped this release through code, design, and reports:
- @claytonbrown — checkpoint resume (#594), test fixtures (#588), design feedback on cost tracking (#582) and structured validation (#583)
- @Bcardo — memory-log redesign (#579), empty-memory hallucination report (#572), encoding fix proposal (#570)
- @voidborne-d — memory persistence design (#564), portfolio manager state fix (#503)
- @mannubaveja007 — structured-output feature request (#434)
- @kelder66 — RAM-only memory issue (#563)
- @Gujiassh — tool-call logging fix (#534), test stub PR (#533)
- @iuyup — memory score normalization fix (#531)
- @kaihg — Google base_url fix (#496)
- @32ryh98yfe — Gemini 404 report (#493)
- @uppb — OpenRouter dynamic model selection (#482)
- @guoz14 — OpenRouter limited-model report (#337)
- @samchenku — indicator name normalization (#490)
- @JasonOA888 — y_finance pandas import fix (#488)
- @tiffanychum — stale import cleanup (#499)
- @zaizou — Docker permission issue (#519)
- @Stosman123, @mauropuga, @hotwind2015 — Windows encoding bug reports (#543, #550, #576)
- @nnishad, @atharvajoshi01 — encoding fix proposals (#568, #549)
0.2.3 — 2026-03-29
Added
- Multi-language output for analyst reports and final decisions, with a CLI selector. Internal agent debate stays in English for reasoning quality. (#472)
- GPT-5.4 family models in the default catalog, with deep/quick model split.
- Unified model catalog as a single source of truth for CLI options and provider validation.
Changed
base_urlis forwarded to Google and Anthropic clients so corporate proxies work consistently across providers. (#427)- Standardised the Google
api_keyparameter to the unifiedapi_keyform.
Fixed
- Backtesting fetchers no longer leak look-ahead data when
curr_dateis in the middle of a fetched window. (#475) - Invalid indicator names from the LLM are caught at the tool boundary instead of crashing the run. (#429)
- yfinance news fetchers respect the same exponential-backoff retry as price fetchers. (#445)
Contributors
- @ahmedk20 — multi-language output (#472)
- @CadeYu — model catalog typing (#464)
- @javierdejesusda — unified Google API key parameter (#453)
- @voidborne-d — yfinance news retry (#445)
- @kostakost2 — look-ahead bias report (#475)
- @lu-zhengda — proxy/base_url support request (#427)
- @VamsiKrishna2021 — invalid indicator crash report (#429)
0.2.2 — 2026-03-22
Added
- Five-tier rating scale (Buy / Overweight / Hold / Underweight / Sell) introduced for the Portfolio Manager.
- Anthropic effort level support for Claude models.
- OpenAI Responses API path for native OpenAI models.
Changed
risk_managerrenamed toportfolio_managerto match the role description shown in the CLI display.- Exchange-qualified tickers (e.g.
7203.T,BRK.B) preserved across all agent prompts and tool calls. - Process-level UTF-8 default attempted for cross-platform consistency
(note: this approach did not actually take effect; replaced in v0.2.4 with
explicit per-call
encoding="utf-8"arguments).
Fixed
- yfinance rate-limit errors are retried with exponential backoff. (#426)
- HTTP client SSL customisation is supported for environments that need custom certificate bundles. (#379)
- Report-section writes handle list-of-string content gracefully.
Contributors
- @CadeYu — exchange-qualified ticker preservation (#413)
- @yang1002378395-cmyk — HTTP client SSL customisation (#379)
0.2.1 — 2026-03-15
Security
- Patched
langchain-corevulnerability (LangGrinch). (#335) - Removed
chainlitdependency affected by CVE-2026-22218.
Added
pyproject.tomlbuild-system configuration; the project now installs via modern packaging tooling.
Removed
setup.py— dependencies consolidated topyproject.toml.
Fixed
- Risk manager reads the correct fundamental report source. (#341)
- All
open()calls receive an explicit UTF-8 encoding (initial pass). get_indicatorstool handles comma-separated indicator names from the LLM. (#368)Propagationinitialises every debate-state field so risk debaters never see missing keys.- Stock data parsing tolerates malformed CSVs and NaN values.
- Conditional debate logic respects the configured round count. (#361)
Contributors
- @RinZ27 —
langchain-coresecurity patch (#335) - @Ljx-007 — risk manager fundamental-report fix (#341)
- @makk9 — debate-rounds config issue (#361)
0.2.0 — 2026-02-04
This is the largest release since the initial public version. The framework moved from single-provider to a multi-provider architecture and grew several production-ready surfaces.
Added
- Multi-provider LLM support (OpenAI, Google, Anthropic, xAI, OpenRouter, Ollama) via a factory pattern, with provider-specific thinking configurations.
- Alpha Vantage integration as a configurable primary data provider, with yfinance as a community-stability fallback.
- Footer statistics in the CLI: real-time tracking of LLM calls, tool calls, and token usage via LangChain callbacks.
- Post-analysis report saving — the framework writes per-section markdown files (analyst reports, debate transcripts, final decision) when a run completes.
- Announcements panel — fetches updates from
api.tauric.ai/v1/announcementsfor the CLI welcome screen. - Tool fallbacks so a single vendor outage does not stop the pipeline.
Changed
- Risky / Safe risk debaters renamed to Aggressive / Conservative for consistency with the displayed agent labels.
- Default data vendor switched to balance reliability and quota across community deployments.
- Ollama and OpenRouter model lists updated; default endpoints clarified.
Fixed
- Analyst status tracking and message deduplication in the live display.
- Infinite-loop guard in the agent loop; reflection and logging hardened.
- Various data-vendor implementation bugs and tool-signature mismatches.
Contributors
This release is the first with substantial outside contributions; many community PRs from late 2025 also landed here.
- @luohy15 — Alpha Vantage data-vendor integration (#235)
- @EdwardoSunny — yfinance fetching optimisations (#245)
- @Mirza-Samad-Ahmed-Baig — infinite-loop guard, reflection, and logging fixes (#89)
- @ZeroAct — saved results path support (#29)
- @Zhongyi-Lu —
.envgitignore (#49) - @csoboy — local Ollama setup (#53)
- @chauhang — initial Docker support attempt (#47, later reverted; the merged Docker support shipped in v0.2.4)
0.1.1 — 2025-06-07
Removed
- Static site assets that had been bundled with v0.1.0; the public site now lives separately.
0.1.0 — 2025-06-05
Added
- Initial public release of the TradingAgents multi-agent trading framework: market / sentiment / news / fundamentals analysts; bull and bear researchers; trader; aggressive, conservative, and neutral risk debaters; portfolio manager. LangGraph orchestration, yfinance data, per-agent BM25 memory, single-provider OpenAI integration, interactive CLI.
- Your ChatGPT plan. The
tradingagents-multi-agent-firm/Dockerfiletext · 0.7 KB
FROM python:3.13-slim AS builder ENV PYTHONDONTWRITEBYTECODE=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 RUN python -m venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" WORKDIR /build COPY . . RUN pip install --no-cache-dir . FROM python:3.13-slim # TRADINGAGENTS_IN_DOCKER lets an update notice name the rebuild command. ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ TRADINGAGENTS_IN_DOCKER=1 COPY --from=builder /opt/venv /opt/venv ENV PATH="/opt/venv/bin:$PATH" RUN useradd --create-home appuser \ && install -d -m 0755 -o appuser -g appuser /home/appuser/.tradingagents USER appuser WORKDIR /home/appuser/app COPY --from=builder --chown=appuser:appuser /build . ENTRYPOINT ["tradingagents"]tradingagents-multi-agent-firm/LICENSEtext · 11.1 KB
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.tradingagents-multi-agent-firm/README.mdmarkdown · 29.6 KB
TradingAgents: Multi-Agents LLM Financial Trading Framework
News
- [2026-10] TradingAgents v0.7.0 released with runs on your own ChatGPT Plus or Pro plan, backtests scored from the first close after each decision, a note at start-up when a newer release is out, and a wheel attached to each GitHub release.
- [2026-10] TradingAgents v0.6.0 released with reports saved as one HTML page, a provider per model tier so the managers and analysts can run on different models, past decisions settled for every ticker while the analysts work, and company news read from Yahoo search while Yahoo's news feed is down.
- [2026-09] TradingAgents v0.5.2 released with parallel analysts for a faster analysis, a CLI that runs without prompts from flags such as
--tickerand--date, the run's settings recorded in every report, and backtests that see only data published by each analysis date.
Full release notes are in CHANGELOG.md.
- [2026-09] TradingAgents v0.5.1 released with a package layout organised by what each module holds (import paths moved), optional Jev screening of social posts, GPT-6 Sol and Luna as the default models, and fixes to run isolation and SEC EDGAR statements.
- [2026-09] TradingAgents v0.5.0 released with point-in-time integrity across every dated path, SEC EDGAR fundamentals served as filed, backtesting over a ticker and date grid, portfolio-aware runs, and current model lineups across every provider.
- [2026-08] TradingAgents v0.4.0 released with look-ahead / point-in-time fixes across FRED macro, social sentiment, and the decision-log memory; clearer decision signals; working CLI checkpoint resume; Trader price grounding; and the GPT-5.6 and GLM-5.3 models.
- [2026-07] TradingAgents v0.3.1 released with correctness and stability fixes: Alpha Vantage look-ahead filtering, graph-router crash-safety, graph-shape-aware checkpoint resume, working crypto sentiment sources, a configurable LLM retry budget, Bedrock API-key auth, and Claude Sonnet 5 / Fable 5 support.
- [2026-06] TradingAgents v0.3.0 released with a verified data-access contract, an expanded provider registry (NVIDIA, Kimi, Groq, Mistral, Bedrock, and any OpenAI-compatible endpoint), FRED and Polymarket data vendors, a current-generation model catalog, and a CI gate.
- [2026-05] TradingAgents v0.2.5 released with the grounded Sentiment Analyst, GPT-5.5 etc. model coverage, Qwen/GLM/MiniMax dual-region support,
TRADINGAGENTS_*env-var configurability with API-key auto-detection, remote Ollama support, non-US alpha benchmarks, and ticker path-traversal hardening. - [2026-04] TradingAgents v0.2.4 released with structured-output agents (Research Manager, Trader, Portfolio Manager), LangGraph checkpoint resume, persistent decision log, DeepSeek/Qwen/GLM/Azure provider support, Docker, and a Windows UTF-8 encoding fix.
- [2026-03] TradingAgents v0.2.3 released with multi-language support, GPT-5.4 family models, unified model catalog, backtesting date fidelity, and proxy support.
- [2026-03] TradingAgents v0.2.2 released with GPT-5.4/Gemini 3.1/Claude 4.6 model coverage, five-tier rating scale, OpenAI Responses API, Anthropic effort control, and cross-platform stability.
- [2026-02] TradingAgents v0.2.0 released with multi-provider LLM support (GPT-5.x, Gemini 3.x, Claude 4.x, Grok 4.x) and improved system architecture.
- [2026-01] Trading-R1 Technical Report released, with Terminal expected to land soon.
🚀 TradingAgents | ⚡ Installation & CLI | 🎬 Demo | 📦 Package Usage | 🤝 Contributing | 📄 Citation
🎉 TradingAgents officially released! We have received numerous inquiries about the work, and we would like to express our thanks for the enthusiasm in our community.
So we decided to fully open-source the framework. Looking forward to building impactful projects with you!
TradingAgents Framework
TradingAgents is a multi-agent trading framework that mirrors the dynamics of real-world trading firms. By deploying specialized LLM-powered agents: from fundamental analysts, sentiment experts, and technical analysts, to trader, risk management team, the platform collaboratively evaluates market conditions and informs trading decisions. Moreover, these agents engage in dynamic discussions to pinpoint the optimal strategy.
TradingAgents framework is designed for research purposes. Trading performance may vary based on many factors, including the chosen backbone language models, model temperature, trading periods, the quality of data, and other non-deterministic factors. It is not intended as financial, investment, or trading advice.
Our framework decomposes complex trading tasks into specialized roles.
Analyst Team
- Fundamentals Analyst: Evaluates company financials and performance metrics, identifying intrinsic values and potential red flags.
- Sentiment Analyst: Aggregates news headlines, StockTwits, and Reddit chatter into a single sentiment read to gauge short-term market mood.
- News Analyst: Monitors global news and macroeconomic indicators, interpreting the impact of events on market conditions.
- Technical Analyst: Utilizes technical indicators (like MACD and RSI) to detect trading patterns and forecast price movements.
The selected analysts work at the same time, each on its own tools, and the research debate starts once all of their reports are in.
Researcher Team
- Comprises both bullish and bearish researchers who critically assess the insights provided by the Analyst Team. Through structured debates, they balance potential gains against inherent risks.
Trader Agent
- Composes reports from the analysts and researchers to make informed trading decisions, determining the timing and magnitude of trades.
Risk Management and Portfolio Manager
- Continuously evaluates portfolio risk by assessing market volatility, liquidity, and other risk factors. The risk management team evaluates and adjusts trading strategies, providing assessment reports to the Portfolio Manager for final decision.
- The Portfolio Manager approves/rejects the transaction proposal. If approved, the order will be sent to the simulated exchange and executed.
Installation and CLI
Installation
Clone TradingAgents:
git clone https://github.com/TauricResearch/TradingAgents.git cd TradingAgentsTradingAgents needs Python 3.11 or later. Create a virtual environment in any of your favorite environment managers:
conda create -n tradingagents python=3.13 conda activate tradingagentsOr with uv:
uv venv --python 3.13 source .venv/bin/activateInstall the package and its dependencies (
uv pip install .with uv):pip install .Docker
Alternatively, run with Docker:
cp .env.example .env # add your API keys docker compose run --rm tradingagentsAfter updating the repository, rebuild the image with
docker compose build.Results, reports, the memory log and the cache live in the
tradingagents_datavolume. To keep them in a folder on the host instead, create the folder and pointTRADINGAGENTS_DATA_DIRat it, in.envor the shell:mkdir -p data && TRADINGAGENTS_DATA_DIR=./data docker compose run --rm tradingagents.For local models with Ollama:
docker compose --profile ollama run --rm tradingagents-ollamaRequired APIs
TradingAgents supports multiple LLM providers. Set the API key for your chosen provider:
export OPENAI_API_KEY=... # OpenAI (GPT) export GOOGLE_API_KEY=... # Google (Gemini) export ANTHROPIC_API_KEY=... # Anthropic (Claude) export XAI_API_KEY=... # xAI (Grok) export DEEPSEEK_API_KEY=... # DeepSeek export DASHSCOPE_API_KEY=... # Qwen (international, dashscope-intl.aliyuncs.com) export DASHSCOPE_CN_API_KEY=... # Qwen (China, dashscope.aliyuncs.com) export ZHIPU_API_KEY=... # GLM via Z.AI (international) export ZHIPU_CN_API_KEY=... # GLM via BigModel (China, open.bigmodel.cn) export MINIMAX_API_KEY=... # MiniMax (global, api.minimax.io) export MINIMAX_CN_API_KEY=... # MiniMax (China, api.minimaxi.com) export OPENROUTER_API_KEY=... # OpenRouter export MISTRAL_API_KEY=... # Mistral export MOONSHOT_API_KEY=... # Kimi (Moonshot) export GROQ_API_KEY=... # Groq export NVIDIA_API_KEY=... # NVIDIA NIM export FRED_API_KEY=... # FRED macro data (free, optional) export ALPHA_VANTAGE_API_KEY=... # Alpha Vantage export TYPESAFE_API_KEY=... # Jev social-post screening (optional)For Azure OpenAI, copy
.env.enterprise.exampleto.env.enterpriseand fill in your credentials.To run on your ChatGPT Plus or Pro plan instead of an API key, choose "Subscription" in the CLI's provider menu, or set
llm_provider: "chatgpt". Sign in once withtradingagents login chatgpt: it prints an address to open in your browser, which returns to this machine on127.0.0.1. When the browser runs on another machine, or TradingAgents runs in Docker (docker compose run --rm tradingagents login chatgpt), paste the address the browser ends up on. The sign-in is kept in~/.tradingagents/chatgpt-auth.json, andtradingagents logout chatgptrevokes it. Runs count toward your plan's usage limits, and the plan does not accepttemperatureormax_tokens.For AWS Bedrock, install the extra with
pip install ".[bedrock]", setllm_provider: "bedrock", configure AWS credentials (environment variables,~/.aws/credentials, or an IAM role) andAWS_DEFAULT_REGION, and use a Bedrock model ID, e.g.us.anthropic.claude-opus-5-5.For local models, configure Ollama with
llm_provider: "ollama". The default endpoint ishttp://localhost:11434/v1; setOLLAMA_BASE_URLto point at a remoteollama-serve. Pull models withollama pull <name>, and pick "Custom model ID" in the CLI for any model not listed by default.For any other OpenAI-compatible server (vLLM, LM Studio, llama.cpp, or a custom relay), use
llm_provider: "openai_compatible"and set the endpoint viabackend_url(orTRADINGAGENTS_LLM_BACKEND_URL), e.g.http://localhost:8000/v1for vLLM orhttp://localhost:1234/v1for LM Studio. The model is whatever your server serves. No key is needed for local servers; setOPENAI_COMPATIBLE_API_KEYwhen the endpoint requires one.With
TYPESAFE_API_KEYset, the Sentiment Analyst screens StockTwits and Reddit posts with TypeSafe's Jev before reading them. Posts that are not about the company are dropped, and each source opens with a count of the remaining posts by stance: bullish, bearish, neutral, or unclear. Without the key, posts pass through unscreened.jev-latestmoves with new releases; setTYPESAFE_DEFAULT_MODELto a versioned ID such asjev-1.13.0to hold it fixed across runs. To reach Jev through OpenRouter, put an OpenRouter key inTYPESAFE_API_KEYand setTYPESAFE_BASE_URL=https://openrouter.ai/api.Alternatively, copy
.env.exampleto.envand fill in your keys:cp .env.example .envCLI Usage
Launch the interactive CLI:
tradingagents # installed command python -m cli.main # alternative: run directly from sourceYou will see a screen where you can select your desired tickers, analysis date, LLM provider, research depth, and more. Your previous run's answers come back as the defaults, so pressing Enter accepts them. The
TRADINGAGENTS_*variables in.envstill skip their step entirely.To run without questions, for a scheduled job or a script, answer the per-run steps with flags and the rest with
TRADINGAGENTS_*variables:export TRADINGAGENTS_LLM_PROVIDER=openai TRADINGAGENTS_QUICK_THINK_LLM=gpt-6-luna TRADINGAGENTS_DEEP_THINK_LLM=gpt-6-sol export TRADINGAGENTS_OUTPUT_LANGUAGE=English TRADINGAGENTS_MAX_DEBATE_ROUNDS=1 TRADINGAGENTS_MAX_RISK_ROUNDS=1 tradingagents --ticker NVDA --date 2026-09-23 --analysts market,news,fundamentals --save --no-showEach flag skips only its own question. Run without a terminal, a missing answer stops the run before it starts and names the flag or variable to set.
At start-up the interactive CLI mentions a newer release and notices from Tauric Research. It checks at most once a day, in the background, and sends nothing about your runs; set
TRADINGAGENTS_NO_UPDATE_CHECK=1to turn it off. Runs without a terminal never check.A saved report also includes
complete_report.html, the report as one page with its sections listed beside the text, for reading in a browser, on a phone or in print. Answering the save question at the prompt also asks about the page and can open it in your browser;--no-htmlskips it, and so doesta.save_reports(state, "NVDA", html=False)from Python.Markets and tickers
TradingAgents works with any market Yahoo Finance covers, using the exchange-suffixed ticker. Company identity and the alpha benchmark resolve automatically per market.
- US:
AAPL,SPY - Hong Kong:
0700.HK· Tokyo:7203.T· London:AZN.L - India:
RELIANCE.NS,.BO· Canada:.TO· Australia:.AX - China A-shares: Shanghai
.SS, Shenzhen.SZ(e.g.600519.SSfor Kweichow Moutai) - Crypto:
BTC-USD,ETH-USD
An interface will appear showing results as they load, letting you track the agent's progress as it runs.
TradingAgents Package
Implementation Details
We built TradingAgents with LangGraph to ensure flexibility and modularity. The framework supports multiple LLM providers: OpenAI, Google, Anthropic, xAI, DeepSeek, Qwen (Alibaba DashScope, international and China endpoints), GLM (Zhipu), MiniMax (global + China), OpenRouter, Ollama for local models, Azure OpenAI for enterprise, and your own ChatGPT plan.
Python Usage
To use TradingAgents inside your code, you can import the
tradingagentsmodule and initialize aTradingAgentsGraph()object. The.propagate()function will return a decision. You can runmain.py, here's also a quick example:from tradingagents.graph.trading_graph import TradingAgentsGraph from tradingagents.default_config import DEFAULT_CONFIG ta = TradingAgentsGraph(debug=True, config=DEFAULT_CONFIG.copy()) # forward propagate state, decision = ta.propagate("NVDA", "2026-09-01") print(decision) # the same report tree the CLI saves, under results_dir/reports ta.save_reports(state, "NVDA")You can also adjust the default configuration to set your own choice of LLMs, debate rounds, etc.
from tradingagents.graph.trading_graph import TradingAgentsGraph from tradingagents.default_config import DEFAULT_CONFIG config = DEFAULT_CONFIG.copy() config["llm_provider"] = "openai" # e.g. openai, google, anthropic, deepseek, groq, ollama; openai_compatible covers any OpenAI-compatible endpoint (vLLM, LM Studio, llama.cpp, ...) config["deep_think_llm"] = "gpt-6-sol" # Model for complex reasoning config["quick_think_llm"] = "gpt-6-luna" # Model for quick tasks config["max_debate_rounds"] = 2 ta = TradingAgentsGraph(debug=True, config=config) _, decision = ta.propagate("NVDA", "2026-09-01") print(decision)The quick model serves the analysts, researchers, debaters and trader; the deep model serves the research and portfolio managers. Each can run on its own provider, for example the managers on Claude while the rest run on OpenAI:
config["deep_think_provider"] = "anthropic" config["deep_think_llm"] = "claude-opus-5-5"A tier on its own provider uses that provider's key and default endpoint; set
quick_think_backend_urlordeep_think_backend_urlfor a local or relay endpoint. TheTRADINGAGENTS_DEEP_THINK_PROVIDERandTRADINGAGENTS_QUICK_THINK_PROVIDERvariables set them for the CLI, together with the tier's model variable.See
tradingagents/default_config.pyfor all configuration options.Fundamentals as filed
US company statements come from SEC EDGAR, which records the date every figure was filed. A run dated in the past reads the statements exactly as they stood that day: a fiscal year that has ended but has not been filed yet is not served, and a figure restated later still reads as first reported. Apple's 2008 total assets were filed as $39.6B and restated to $36.2B in 2010, so a run dated in between reads $39.6B. EDGAR needs no account or API key.
Other companies' statements come from Yahoo Finance, which dates a statement by the period it covers rather than by when it was published. A run dated today reads them; a run dated in the past is told they are withheld, since Yahoo cannot say which figures were public by then.
Insider trades are dated by when they happened, not when they were filed, so a run dated in the past is told they are withheld as well.
SEC asks callers to identify themselves and refuses requests that carry no contact address, so a default one is sent. Set your own so SEC can reach you rather than the project:
SEC_EDGAR_USER_AGENT="Your Name your@email.com"It covers companies that file with the SEC, including foreign companies listed in the US. Anything else, such as Hong Kong or A-share listings, falls through to the next vendor in the chain. EDGAR's machine-readable filings begin in 2009, and a fourth quarter is reported as unavailable rather than derived, because filers publish it only inside the annual figure.
Current holdings
By default the agents do not know what you hold, so their guidance is written for a reader who applies it to their own position. Pass a portfolio to have the trader, the risk analysts and the portfolio manager work against your actual book.
from tradingagents.portfolio import PortfolioContext portfolio = PortfolioContext.model_validate({ "cash": 25000.0, "currency": "USD", "positions": [{"ticker": "NVDA", "quantity": 120, "average_price": 150.0}], }) _, decision = ta.propagate("NVDA", "2026-09-01", portfolio=portfolio)The CLI takes the same content as a JSON file:
tradingagents --portfolio my_book.json.An empty
positionslist means a flat book, which is different from passing nothing. A run without a portfolio is never treated as flat.Persistence and Recovery
TradingAgents persists two kinds of state across runs.
Memory log
The memory log is always on. Each completed run appends its decision to
~/.tradingagents/memory/trading_memory.md. While the analysts of a later run work, TradingAgents settles every logged decision whose holding period has passed: it fetches the realised return (raw, and alpha against the instrument's regional benchmark) and generates a one-paragraph reflection. The Portfolio Manager then reads the most recent decisions for the same ticker plus recent lessons from other tickers, so each analysis carries forward what worked and what didn't. If settling fails, the run goes on and its report says so.Override the path with
TRADINGAGENTS_MEMORY_LOG_PATH.To settle decisions without running an analysis, for a scheduled job, call
ta.settle_all_pending(); it returns the decisions it settled and any it could not.Checkpoint resume
Checkpoint resume is opt-in via
--checkpoint. When enabled, LangGraph saves state after each node so a crashed or interrupted run resumes from the last successful step instead of starting over. The run view says whether it resumed a saved run or started fresh. Checkpoints are cleared automatically on successful completion.Per-ticker SQLite databases live at
~/.tradingagents/cache/checkpoints/<TICKER>.db(override the base withTRADINGAGENTS_CACHE_DIR). Use--clear-checkpointsto reset all of them before a run.tradingagents --checkpoint # enable for this run tradingagents --clear-checkpoints # reset before runningconfig = DEFAULT_CONFIG.copy() config["checkpoint_enabled"] = True ta = TradingAgentsGraph(config=config) _, decision = ta.propagate("NVDA", "2026-09-01")Evaluating decisions over time
One run gives one decision, which cannot tell you whether the system decides well.
run_backtestruns the same pipeline over a grid of tickers and dates, writes to a memory log of its own, and scores the decisions whose holding window has since traded.from tradingagents.backtest import iter_grid, run_backtest, summarize dates = iter_grid("2026-06-01", "2026-08-01", every_n_days=7) result = run_backtest(["NVDA", "AAPL"], dates, config, selected_analysts=["market", "news"]) print(summarize(result).render())From the CLI:
tradingagents backtest NVDA,AAPL --start 2026-06-01 --end 2026-08-01 --every 7Each cell is scored on realized alpha against the instrument's regional benchmark, grouped by rating. A decision is entered at the first close after its date, because a run reads news published after that day's close. Your own memory log is never written to, and re-running the same grid with
run_id=result.run_idskips the cells that already ran, so an interrupted sweep continues where it stopped.Reproducibility
TradingAgents is LLM-driven, so two runs of the same ticker and date can differ. This is expected for a research tool built on language models, not a defect. The variation comes from a few distinct sources, and it helps to separate them.
Language model sampling is non-deterministic. Even at a fixed temperature, providers do not guarantee byte-identical output across calls, and reasoning models (the default GPT-6 family, and any thinking-mode model) vary the most because their internal reasoning is itself sampled.
Live data moves. News, StockTwits, and Reddit return different content as time passes, so a run today sees different inputs than a run last week even for the same historical trade date. Pin the analysis date to hold the price and indicator window fixed, but the social and news sources still reflect "now".
To reduce variation you can lower the sampling temperature. Set
temperaturein your config (orTRADINGAGENTS_TEMPERATUREin.env); lower values make models that honor it more repeatable. The current curated models are reasoning-first and largely ignore temperature, so for tighter reproducibility name a non-reasoning model in your config, or inTRADINGAGENTS_DEEP_THINK_LLMandTRADINGAGENTS_QUICK_THINK_LLM. Any model ID your provider serves is accepted, whether or not the picker lists it.config = DEFAULT_CONFIG.copy() config["llm_provider"] = "openai" config["temperature"] = 0.0 # Reasoning models ignore temperature. For tighter reproducibility, name a # non-reasoning model in deep_think_llm / quick_think_llm.What does not vary anymore: the analyzed company identity is resolved deterministically from the ticker before any agent runs, and the market analyst grounds exact price and indicator claims in a verified data snapshot. Earlier reports of "different companies" or fabricated price levels across runs are addressed by these two mechanisms.
Backtest results are not guaranteed to match any published figure. Returns depend on the model, the temperature, the date range, data quality, and the sampling above. Treat the framework as a research scaffold for studying multi-agent analysis, not as a strategy with a fixed, replicable return.
Contributing
Contributions are welcome: bug fixes, documentation, and feature ideas; past contributions are credited per release in
CHANGELOG.md.Citation
Please reference our work if you find TradingAgents provides you with some help :)
@misc{xiao2025tradingagentsmultiagentsllmfinancial, title={TradingAgents: Multi-Agents LLM Financial Trading Framework}, author={Yijia Xiao and Edward Sun and Di Luo and Wei Wang}, year={2025}, eprint={2412.20138}, archivePrefix={arXiv}, primaryClass={q-fin.TR}, url={https://arxiv.org/abs/2412.20138}, }
TalentDelta provenance record (added at packaging)
- Packaged for: TalentDelta catalogue (draft; gated by review)
- Upstream: TauricResearch/TradingAgents
- Pinned commit:
e8f5503e9b86d5c2710b0c76b1f686703968c9df(2026-10-16) - Licence: Apache-2.0 — full text at the repository root, shipped byte-identical to the pin. Permissive: use, modification and redistribution allowed with attribution and the licence text; no NOTICE file exists upstream and no per-file headers are added. Attribution to Tauric Research is recorded in the pack README's provenance section and in this upload record.
- Upstream author credit: Tauric Research (TauricResearch)
- Runtime: Python 3.11+ with a LangGraph stack (pip install .); CLI
tradingagentsorpython -m cli.main; Docker via docker-compose; any of ~15 LLM providers, a ChatGPT Plus/Pro subscription, or a local Ollama / OpenAI-compatible endpoint - Changes made at packaging: this section was appended to
README.md. No upstream path is excluded. Apart from this provenance section appended toREADME.md, no file was added, removed, renamed or edited; every upstream file is byte-identical to the pinned commit. - Safety read of the packaged archive (packaging time, 2026-10-19): Leak scan of the packaged archive: 36 matching lines across 201 text files, every one reviewed with values masked. 34 come from the upstream tree — 16 email matches are placeholders (your@email.com in .env.example and README; a@example.com, research@example.com, contact@example.com in tests and the SEC EDGAR user-agent default) and 18 api-key-shaped matches are test fixtures only (sk-existing, sk-typed-in, sk-relay-123 and redacted sk-… strings), plus one user:secret@relay.example URL in a reporting test. The other two are placeholder addresses quoted in this note. No live credentials, keys or personal data; .env.example is empty and .env.enterprise.example carries only a placeholder Azure endpoint URL.
Packaging notes (TalentDelta)
- Runnable system, not a prompt library: every role is a module with its own system prompt and an explicitly bound tool tuple under tradingagents/agents/ — four analysts, bull and bear researchers, a trader, three risk debators and two managers.
- No credentials, keys or market data are bundled. .env.example is empty placeholders and .env.enterprise.example carries only a placeholder Azure endpoint URL; prices, filings, news and macro series are fetched at run time from Yahoo Finance, SEC EDGAR, FRED, Alpha Vantage and similar providers.
- Upstream states that the framework is for research and is not financial, investment or trading advice; that disclaimer is preserved verbatim in the shipped README.
- Version 0.7.0, read from tradingagents/init.py at the pin; the README's release notes for 0.5.2 through 0.7.0 are preserved.
- The CLI checks for a newer release at most once a day and sends nothing about the user's runs; TRADINGAGENTS_NO_UPDATE_CHECK=1 disables it, and non-interactive runs never check.
tradingagents-multi-agent-firm/assets/TauricResearch.pngbinary · 62.1 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/analyst.pngbinary · 211.5 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/cli/cli_init.pngbinary · 239.5 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/cli/cli_news.pngbinary · 811.7 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/cli/cli_technical.pngbinary · 774.5 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/cli/cli_transaction.pngbinary · 791.3 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/researcher.pngbinary · 137.2 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/risk.pngbinary · 161.8 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/schema.pngbinary · 356.8 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/assets/trader.pngbinary · 158.0 KB
Binary — download to inspect.
tradingagents-multi-agent-firm/cli/__init__.pycode · 0.0 KB
tradingagents-multi-agent-firm/cli/display.pycode · 23.0 KB · preview truncated
"""The live view of a run: message log, agent status, report panels and timings.""" import datetime import time from collections import deque from time import monotonic from rich import box from rich.console import Console from rich.layout import Layout from rich.markdown import Markdown from rich.panel import Panel from rich.rule import Rule from rich.spinner import Spinner from rich.table import Table from rich.text import Text from tradingagents.graph.analyst_execution import ( ANALYST_NODE_SPECS, AnalystExecutionPlan, ) console = Console() class MessageBuffer: # Fixed teams that always run (not user-selectable) FIXED_AGENTS = { "Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"], "Trading Team": ["Trader"], "Risk Management": ["Aggressive Analyst", "Neutral Analyst", "Conservative Analyst"], "Portfolio Management": ["Portfolio Manager"], } # Analyst name mapping ANALYST_MAPPING = { "market": "Market Analyst", "social": "Sentiment Analyst", "news": "News Analyst", "fundamentals": "Fundamentals Analyst", } # Report section mapping: section -> (analyst_key for filtering, finalizing_agent) # analyst_key: which analyst selection controls this section (None = always included) # finalizing_agent: which agent must be "completed" for this report to count as done REPORT_SECTIONS = { "market_report": ("market", "Market Analyst"), "sentiment_report": ("social", "Sentiment Analyst"), "news_report": ("news", "News Analyst"), "fundamentals_report": ("fundamentals", "Fundamentals Analyst"), "investment_plan": (None, "Research Manager"), "trader_investment_plan": (None, "Trader"), "final_trade_decision": (None, "Portfolio Manager"), } def __init__(self, max_length=100): self.messages = deque(maxlen=max_length) self.tool_calls = deque(maxlen=max_length) self.current_report = None self.agent_status = {} self.report_sections = {} self.selected_analysts = [] self._processed_message_ids = set() def init_for_analysis(self, selected_analysts): """Initialize agent status and report sections based on selected analysts. Args: selected_analysts: List of analyst type strings (e.g., ["market", "news"]) """ self.selected_analysts = [a.lower() for a in selected_analysts] self.agent_status = {} for analyst_key in self.selected_analysts: if analyst_key in self.ANALYST_MAPPING: self.agent_status[self.ANALYST_MAPPING[analyst_key]] = "pending" for team_agents in self.FIXED_AGENTS.values(): for agent in team_agents: self.agent_status[agent] = "pending" self.report_sections = {} for section, (analyst_key, _) in self.REPORT_SECTIONS.items(): if analyst_key is None or analyst_key in self.selected_analysts: self.report_sections[section] = None # Reset other state self.current_report = None self.messages.clear() self.tool_calls.clear() self._processed_message_ids.clear() def get_completed_reports_count(self): """Count reports that are finalized (their finalizing agent is completed). A report is considered complete when: 1. The report section has content (not None), AND 2. The agent responsible for finalizing that report has status "completed" This prevents interim updates (like debate rounds) from counting as completed. """ count = 0 for section in self.report_sections: if section not in self.REPORT_SECTIONS: continue _, finalizing_agent = self.REPORT_SECTIONS[section] # Report is complete if it has content AND its finalizing agent is done has_content = self.report_sections.get(section) is not None agent_done = self.agent_status.get(finalizing_agent) == "completed" if has_content and agent_done: count += 1 return count def add_message(self, message_type, content): timestamp = datetime.datetime.now().strftime("%H:%M:%S") self.messages.append((timestamp, message_type, content)) def add_tool_call(self, tool_name, args): timestamp = datetime.datetime.now().strftime("%H:%M:%S") self.tool_calls.append((timestamp, tool_name, args)) def update_agent_status(self, agent, status): if agent in self.agent_status: self.agent_status[agent] = status def update_report_section(self, section_name, content): if section_name in self.report_sections: self.report_sections[section_name] = content self._update_current_report() def _update_current_report(self): # For the panel display, only show the most recently updated section latest_section = None latest_content = None for section, content in self.report_sections.items(): if content is not None: latest_section = section latest_content = content if latest_section and latest_content: section_titles = { "market_report": "Market Analysis", "sentiment_report": "Social Sentiment", "news_report": "News Analysis", "fundamentals_report": "Fundamentals Analysis", "investment_plan": "Research Team Decision", "trader_investment_plan": "Trading Team Plan", "final_trade_decision": "Portfolio Management Decision", } self.current_report = ( f"### {section_titles[latest_section]}\n{latest_content}" ) message_buffer = MessageBuffer() def create_layout(): layout = Layout() layout.split_column( Layout(name="header", size=3), Layout(name="main"), Layout(name="footer", size=3), ) layout["main"].split_column( Layout(name="upper", ratio=3), Layout(name="analysis", ratio=5) ) layout["upper"].split_row( Layout(name="progress", ratio=2), Layout(name="messages", ratio=3) ) return layout def format_tokens(n): """Format token count for display.""" if n >= 1000: return f"{n/1000:.1f}k" return str(n) def update_display(layout, spinner_text=None, stats_handler=None, start_time=None): # Header with welcome message layout["header"].update( Panel( "[bold green]Welcome to TradingAgents CLI[/bold green]\n" "[dim]© [Tauric Research](https://github.com/TauricResearch)[/dim]", title="Welcome to TradingAgents", border_style="green", padding=(1, 2), expand=True, ) ) # Progress panel showing agent status progress_table = Table( show_header=True, header_style="bold magenta", show_footer=False, box=box.SIMPLE_HEAD, # Use simple header with horizontal lines title=None, # Remove the redundant Progress title padding=(0, 2), # Add horizontal padding expand=True, # Make table expand to fill available space ) progress_table.add_column("Team", style="cyan", justify="center", width=20) progress_table.add_column("Agent", style="green", justify="center", width=20) progress_table.add_column("Status", style="yellow", justify="center", width=20) # Group agents by team - filter to only include agents in agent_status all_teams = { "Analyst Team": [ "Market Analyst", "Sentiment Analyst", "News Analyst", "Fundamentals Analyst", ], "Research Team": ["Bull Researcher", "Bear Researcher", "Research Manager"], "Trading Team": ["Trader"], "Risk Management": ["Aggressive Analyst", "Neutral Analyst", "Conservative Analyst"], "Portfolio Management": ["Portfolio Manager"], } teams = {} for team, agents in all_teams.items(): active_agents = [a for a in agents if a in message_buffer.agent_status] if active_agents: teams[team] = active_agents for team, agents in teams.items(): first_agent = agents[0] status = message_buffer.agent_status.get(first_agent, "pending") if status == "in_progress": spinner = Spinner( "dots", text="[blue]in_progress[/blue]", style="bold cyan" ) status_cell = spinner else: status_color = { "pending": "yellow", "completed": "green", "error": "red", }.get(status, "white") status_cell = f"[{status_color}]{status}[/{status_color}]" progress_table.add_row(team, first_agent, status_cell) for agent in agents[1:]: status = message_buffer.agent_status.get(agent, "pending") if status == "in_progress": spinner = Spinner( "dots", text="[blue]in_progress[/blue]", style="bold cyan" ) status_cell = spinner else: status_color = { "pending": "yellow", "completed": "green", "error": "red", }.get(status, "white") status_cell = f"[{status_color}]{status}[/{status_color}]" progress_table.add_row("", agent, status_cell) progress_table.add_row("─" * 20, "─" * 20, "─" * 20, style="dim") layout["progress"].update( Panel(progress_table, title="Progress", border_style="cyan", padding=(1, 2)) ) # Messages panel showing recent messages and tool calls messages_table = Table( show_header=True, header_style="bold magenta", show_footer=False, expand=True, # Make table expand to fill available space box=box.MINIMAL, # Use minimal box style for a lighter look show_lines=True, # Keep horizontal lines padding=(0, 1), # Add some padding between columns ) messages_table.add_column("Time", style="cyan", width=8, justify="center") messages_table.add_column("Type", style="green", width=10, justify="center") messages_table.add_column( "Content", style="white", no_wrap=False, ratio=1 ) # Make content column expand # Combine tool calls and messages all_messages = [] for timestamp, tool_name, args in message_buffer.tool_calls: formatted_args = format_tool_args(args) all_messages.append((timestamp, "Tool", f"{tool_name}: {formatted_args}")) for timestamp, msg_type, content in message_buffer.messages: content_str = str(content) if content else "" if len(content_str) > 200: content_str = content_str[:197] + "..." all_messages.append((timestamp, msg_type, content_str)) # Sort by timestamp descending (newest first) all_messages.sort(key=lambda x: x[0], reverse=True) max_messages = 12 recent_messages = all_messages[:max_messages] for timestamp, msg_type, content in recent_messages: wrapped_content = Text(content, overflow="fold") messages_table.add_row(timestamp, msg_type, wrapped_content) layout["messages"].update( Panel( messages_table, title="Messages & Tools", border_style="blue", padding=(1, 2), ) ) # Analysis panel showing current report if message_buffer.current_report: layout["analysis"].update( Panel( Markdown(message_buffer.current_report), title="Current Report", border_style="green", padding=(1, 2), ) ) eltradingagents-multi-agent-firm/cli/main.pycode · 8.6 KB
import sys from enum import StrEnum from typing import Annotated import typer from rich.markup import escape from cli.display import console from cli.models import AnalystType, AssetType from cli.prompts import filter_analysts_for_asset_type, parse_analysts from cli.run import run_analysis from tradingagents.backtest import iter_grid, run_backtest, summarize from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.llm_clients.openai_client import SIGNED_IN_PROVIDERS from tradingagents.portfolio import load_portfolio # prompt_toolkit's win32 output module is importable only on Windows (it asserts # the platform at import time), so gate on the platform rather than catching the # failure — that way a genuinely broken prompt_toolkit on Windows still surfaces # instead of silently disabling the handler below. Off Windows this stays an # empty tuple, which `except` accepts and never matches (#1138). if sys.platform == "win32": # pragma: no cover - platform dependent from prompt_toolkit.output.win32 import NoConsoleScreenBufferError _NO_CONSOLE_ERRORS: tuple[type[BaseException], ...] = (NoConsoleScreenBufferError,) else: _NO_CONSOLE_ERRORS = () app = typer.Typer( name="TradingAgents", help="TradingAgents CLI: Multi-Agents LLM Financial Trading Framework", add_completion=True, # Enable shell completion ) @app.callback(invoke_without_command=True) def analyze( ctx: typer.Context, checkpoint: bool | None = typer.Option( None, "--checkpoint/--no-checkpoint", help="Enable/disable checkpoint-resume (save state after each node so a " "crashed run can resume). Omit to honor TRADINGAGENTS_CHECKPOINT_ENABLED.", ), clear_checkpoints: bool = typer.Option( False, "--clear-checkpoints", help="Delete all saved checkpoints before running (force fresh start).", ), portfolio: str = typer.Option( None, "--portfolio", help="JSON file with current holdings and cash, so the trader, risk and " "portfolio agents size against your actual position.", ), ticker: str = typer.Option(None, "--ticker", help="Ticker to analyze, e.g. NVDA or 0700.HK; skips the prompt"), date: str = typer.Option(None, "--date", help="Analysis date, YYYY-MM-DD; skips the prompt"), analysts: str = typer.Option( None, "--analysts", help="Comma-separated analysts, e.g. market,news; skips the prompt" ), save: bool | None = typer.Option( None, "--save/--no-save", help="Save the report under results_dir without asking" ), show: bool | None = typer.Option( None, "--show/--no-show", help="Show the full report at the end without asking" ), html: bool | None = typer.Option( None, "--html/--no-html", help="Save the report as one HTML page too, complete_report.html, without asking (default: yes)", ), ): """Run an analysis. This is what a bare `tradingagents` does. Flags answer their questions; with provider, models, depth and language also set through TRADINGAGENTS_* variables, the run asks nothing. """ if ctx.invoked_subcommand is not None: return if clear_checkpoints: from tradingagents.graph.checkpointer import clear_all_checkpoints n = clear_all_checkpoints(DEFAULT_CONFIG["data_cache_dir"]) console.print(f"[yellow]Cleared {n} checkpoint(s).[/yellow]") portfolio_context = None if portfolio: try: portfolio_context = load_portfolio(portfolio) except ValueError as exc: console.print(f"[red]{exc}[/red]") raise typer.Exit(code=1) from None try: flags = {"ticker": ticker, "date": date, "analysts": analysts, "save": save, "show": show, "html": html} run_analysis(checkpoint=checkpoint, portfolio=portfolio_context, flags=flags) except _NO_CONSOLE_ERRORS: # A terminal with no console buffer cannot host the interactive prompts. # Emit one actionable line on stderr instead of a prompt_toolkit # traceback; plain text, since rich may not render here either (#1138). typer.echo( "Error: no Windows console available. The interactive CLI needs a real " "console buffer — run it from Windows Terminal, PowerShell, or cmd.exe " "rather than a piped or embedded terminal.", err=True, ) raise typer.Exit(code=1) from None @app.command() def backtest( tickers: str = typer.Argument(..., help="Comma-separated tickers, e.g. NVDA,AAPL"), start: str = typer.Option(..., "--start", help="First analysis date, YYYY-MM-DD"), end: str = typer.Option(..., "--end", help="Last analysis date, YYYY-MM-DD"), every: int = typer.Option(7, "--every", help="Days between analysis dates"), analysts: str = typer.Option( None, "--analysts", help="Comma-separated analysts to run: market, sentiment, news, fundamentals; omit for all the asset type allows" ), asset_type: str = typer.Option("stock", "--asset-type", help="stock or crypto"), portfolio: str = typer.Option( None, "--portfolio", help="JSON file with holdings and cash, held constant across the grid" ), run_id: str = typer.Option( None, "--run-id", help="Continue an earlier sweep: its cells are skipped and its log reused" ), ): """Score past decisions over a grid of tickers and dates.""" try: dates = iter_grid(start, end, every) book = load_portfolio(portfolio) if portfolio else None kind = AssetType(asset_type.strip().lower()) # The analysts are named and checked as for an analysis; without a # choice, every analyst the asset type allows runs. chosen = (parse_analysts(analysts, kind) if analysts else filter_analysts_for_asset_type(list(AnalystType), kind)) except ValueError as exc: console.print(f"[red]{exc}[/red]") raise typer.Exit(code=1) from None names = [t.strip() for t in tickers.split(",") if t.strip()] if not names: console.print("[red]No ticker to analyze; pass them comma-separated, e.g. NVDA,AAPL[/red]") raise typer.Exit(code=1) def show_progress(done, total, ticker, date): console.print(f"[dim][{done}/{total}] {ticker} {date}[/dim]") kwargs = {"asset_type": kind.value, "portfolio": book, "run_id": run_id, "progress": show_progress, "selected_analysts": [a.value for a in chosen]} try: result = run_backtest(names, dates, DEFAULT_CONFIG, **kwargs) except Exception as exc: # a missing key or an unknown analyst is a setup error console.print(f"[red]{exc}[/red]") raise typer.Exit(code=1) from None console.print(summarize(result).render()) console.print(f"\nRan {result.cells_run} cells, skipped {result.skipped}. Log: {result.log_path}") console.print(f"Continue or settle this sweep: --run-id {result.run_id}") for ticker, date, reason in result.failures: console.print(f"[yellow]failed:[/yellow] {ticker} {date}: {reason}") for ticker, reason in result.settlement_failures: console.print(f"[yellow]unsettled:[/yellow] {ticker}: {reason}") # The plans `login` and `logout` take: the providers signed in to, not keyed. Plan = StrEnum("Plan", {name: name for name in SIGNED_IN_PROVIDERS}) @app.command() def login(plan: Annotated[Plan, typer.Argument(help="The plan to sign in with")]): """Sign in with your ChatGPT Plus or Pro plan, for the chatgpt provider.""" from tradingagents.llm_clients import chatgpt_auth try: # One unwrapped line, so the sign-in address can be copied whole. email = chatgpt_auth.sign_in( say=lambda line: console.print(line, soft_wrap=True, markup=False, highlight=False)) except chatgpt_auth.ChatGPTAuthError as exc: console.print(f"[red]{escape(str(exc))}[/red]") raise typer.Exit(code=1) from None console.print(f"[green]Signed in to ChatGPT as {escape(email)}.[/green]") @app.command() def logout(plan: Annotated[Plan, typer.Argument(help="The plan to sign out of")]): """Sign out of your ChatGPT plan: revoke this machine's access and forget it.""" from tradingagents.llm_clients import chatgpt_auth outcome = chatgpt_auth.sign_out() if outcome == "revoked": console.print("Signed out of ChatGPT.") elif outcome == "forgotten": console.print("[yellow]Signed out here, but OpenAI could not be reached to revoke the sign-in; " "it stays valid at OpenAI until it expires.[/yellow]") else: console.print("Not signed in to ChatGPT.") if __name__ == "__main__": app()tradingagents-multi-agent-firm/cli/models.pycode · 0.4 KB
from enum import StrEnum class AnalystType(StrEnum): MARKET = "market" # Wire value stays "social" for saved-config and string-keyed-caller # back-compat; the user-facing label is "Sentiment Analyst". SOCIAL = "social" NEWS = "news" FUNDAMENTALS = "fundamentals" class AssetType(StrEnum): STOCK = "stock" CRYPTO = "crypto"tradingagents-multi-agent-firm/cli/prefs.pycode · 3.9 KB
"""What the last run chose, offered back as the next run's defaults. The interactive flow asks the same questions every time, and only some of them have an environment variable to skip them (the analyst set has none). Remembered answers prefill the prompts so Enter accepts them; they never skip a step, so a run always starts on choices the user has seen. Only answers that are stable between runs are kept. The ticker and the analysis date are not: they change every run, and a remembered date would quietly offer a stale one. Every value is checked against the current choices on the way out, because models and providers are added and retired between versions. A remembered model that is no longer offered is dropped rather than shown. """ from __future__ import annotations import json import os from pathlib import Path from cli.models import AnalystType, AssetType from cli.prompts import LISTED_LIVE, _llm_provider_table, filter_analysts_for_asset_type from tradingagents.dataflows.files import replace_file from tradingagents.llm_clients.model_catalog import get_model_options _PREFS_PATH = Path(os.path.expanduser("~")) / ".tradingagents" / "cli_prefs.json" REMEMBERED = ( "output_language", "analysts", "research_depth", "llm_provider", "quick_think_llm", "deep_think_llm", "backend_url", ) def load_last_run() -> dict: """The previous run's answers, or an empty dict when there is nothing usable. Convenience state: an unreadable or corrupt file means no defaults, never an error in the user's way. """ try: data = json.loads(_PREFS_PATH.read_text(encoding="utf-8")) return data if isinstance(data, dict) else {} except (OSError, ValueError): return {} def save_last_run(selections: dict) -> None: """Record the answers worth offering next time; failure is never fatal.""" kept = {k: v for k, v in selections.items() if k in REMEMBERED and v not in (None, "", [])} kept["analysts"] = [getattr(a, "value", a) for a in kept.get("analysts", [])] or None kept = {k: v for k, v in kept.items() if v is not None} try: _PREFS_PATH.parent.mkdir(parents=True, exist_ok=True) # A concurrent run reads one file or the other. replace_file(_PREFS_PATH, lambda temp: Path(temp).write_text(json.dumps(kept, indent=2), encoding="utf-8")) except OSError: return def sanitize(prefs: dict, asset_type) -> dict: """Keep only the remembered answers that are still choosable now.""" kept: dict = {} if isinstance(prefs.get("output_language"), str): kept["output_language"] = prefs["output_language"] if prefs.get("research_depth") in (1, 3, 5): kept["research_depth"] = prefs["research_depth"] known = {a.value for a in AnalystType} analysts = [a for a in prefs.get("analysts") or [] if a in known] allowed = filter_analysts_for_asset_type([AnalystType(a) for a in analysts], AssetType(asset_type)) if allowed: kept["analysts"] = [a.value for a in allowed] provider = prefs.get("llm_provider") # Region-specific providers (qwen-cn) are picked in a second prompt, so the # base key is what the provider menu matches. base = (provider or "").split("-cn")[0] if base and base in {key for _, key, _ in _llm_provider_table()}: kept["llm_provider"] = provider if isinstance(prefs.get("backend_url"), str) and prefs["backend_url"]: kept["backend_url"] = prefs["backend_url"] for field, mode in (("quick_think_llm", "quick"), ("deep_think_llm", "deep")): try: offered = {model for _, model in get_model_options(base, mode)} except KeyError: continue # Models listed live are not in the catalog, so any model ID is kept. listed_live = base in LISTED_LIVE and isinstance(prefs.get(field), str) if prefs.get(field) in offered or listed_live: kept[field] = prefs[field] return kepttradingagents-multi-agent-firm/cli/prompts.pycode · 30.3 KB · preview truncated
import datetime import os import subprocess import sys from pathlib import Path import questionary import typer from dotenv import find_dotenv, set_key from rich.markup import escape from cli.display import console from cli.models import AnalystType, AssetType from tradingagents.llm_clients.api_key_env import get_api_key_env from tradingagents.llm_clients.model_catalog import get_model_options from tradingagents.llm_clients.openai_client import OPENAI_COMPATIBLE_PROVIDERS, SIGNED_IN_PROVIDERS TICKER_INPUT_EXAMPLES = "SPY, 0700.HK, BTC-USD" ANALYST_CHOICES = [ ("Market Analyst", AnalystType.MARKET), ("Sentiment Analyst", AnalystType.SOCIAL), ("News Analyst", AnalystType.NEWS), ("Fundamentals Analyst", AnalystType.FUNDAMENTALS), ] CRYPTO_SUFFIXES = ("-USD", "-USDT", "-USDC", "-BTC", "-ETH") # The style of every selection menu. _MENU_STYLE = questionary.Style([ ("selected", "fg:magenta noinherit"), ("highlighted", "fg:magenta noinherit"), ("pointer", "fg:magenta noinherit"), ]) def is_valid_ticker_input(value: str) -> bool: """Whether a ticker entry is acceptable (charset + length). Allows the characters Yahoo symbols use, including ``=`` for futures/forex like ``GC=F`` and ``EURUSD=X`` (#980), and ``^`` for indices. Empty input is allowed (it defaults to SPY downstream). """ v = value.strip() return not v or (all(ch.isalnum() or ch in "._-^=" for ch in v) and len(v) <= 32) def get_ticker() -> str: """Prompt the user to enter a ticker symbol, preserving exchange suffixes. Uses questionary.text (not typer.prompt, which strips trailing dot-suffixes like ``000404.SH`` on some shells) and validates the symbol charset so an obvious typo is caught before the run starts. """ ticker = questionary.text( f"Enter ticker symbol (e.g. {TICKER_INPUT_EXAMPLES}):", validate=lambda x: ( is_valid_ticker_input(x) or "Please enter a valid ticker symbol, e.g. AAPL, 000404.SZ, 0700.HK, GC=F." ), style=questionary.Style( [ ("text", "fg:green"), ("highlighted", "noinherit"), ] ), ).ask() if ticker is None: console.print("\n[red]No ticker symbol provided. Exiting...[/red]") exit(1) return normalize_ticker_symbol(ticker) if ticker.strip() else "SPY" def parse_ticker(value: str) -> str: """A ticker given on the command line, canonical; empty or malformed is refused.""" if not value.strip() or not is_valid_ticker_input(value): raise ValueError(f"not a ticker symbol: {value!r} (e.g. {TICKER_INPUT_EXAMPLES})") return normalize_ticker_symbol(value) def parse_analysis_date(value: str) -> str: """An analysis date as YYYY-MM-DD, today or earlier.""" try: day = datetime.datetime.strptime(value.strip(), "%Y-%m-%d").date() except ValueError: raise ValueError(f"not a date: {value!r}; use YYYY-MM-DD") from None if day > datetime.date.today(): raise ValueError(f"{value} is in the future") return day.isoformat() def parse_analysts(value: str, asset_type: AssetType) -> list[AnalystType]: """Comma-separated analyst names, in the canonical order, checked against the asset.""" # "sentiment" is the name users see; the analyst's key is "social". names = [{"sentiment": "social"}.get(n, n) for n in (n.strip().lower() for n in value.split(",")) if n] if not names: raise ValueError("name at least one analyst") known = {a.value: a for a in AnalystType} unknown = [n for n in names if n not in known] if unknown: choices = ", ".join("sentiment" if name == "social" else name for name in known) raise ValueError(f"unknown analyst {', '.join(unknown)}; choose from {choices}") available = filter_analysts_for_asset_type(list(known.values()), asset_type) unavailable = [n for n in names if known[n] not in available] if unavailable: raise ValueError(f"{', '.join(unavailable)} is not available for {asset_type.value}") return [a for a in available if a.value in names] def normalize_ticker_symbol(ticker: str) -> str: """Resolve user input to its canonical Yahoo symbol (single source of truth). Delegates to the data layer's ``normalize_symbol`` so the symbol the CLI passes through the pipeline is exactly the one the data path will price (e.g. ``BTCUSD`` -> ``BTC-USD``, ``XAUUSD`` -> ``GC=F``). Falls back to the plain upper-case if the data layer is unavailable. """ try: from tradingagents.dataflows.symbols import normalize_symbol return normalize_symbol(ticker) except Exception: return ticker.strip().upper() def detect_asset_type(ticker: str) -> AssetType: """Classify on the canonical symbol so e.g. BTCUSD and BTC-USDT both read as crypto (#981/#982), matching what the data path will actually fetch.""" canonical = normalize_ticker_symbol(ticker) if canonical.endswith(CRYPTO_SUFFIXES): return AssetType.CRYPTO return AssetType.STOCK def filter_analysts_for_asset_type( analysts: list[AnalystType], asset_type: AssetType ) -> list[AnalystType]: if asset_type != AssetType.CRYPTO: return analysts return [ analyst for analyst in analysts if analyst != AnalystType.FUNDAMENTALS ] def _matching_choice(options, default): """The option value equal to ``default``, or None to leave the menu as is.""" return next((value for _, value in options if value == default), None) def select_analysts(asset_type: AssetType = AssetType.STOCK, default=None) -> list[AnalystType]: """Select analysts using an interactive checkbox. ``default`` pre-checks the previous run's analysts; the prompt still shows. """ available_analysts = filter_analysts_for_asset_type( [value for _, value in ANALYST_CHOICES], asset_type, ) choices = questionary.checkbox( "Select Your [Analysts Team]:", choices=[ questionary.Choice(display, value=value, checked=value.value in (default or [])) for display, value in ANALYST_CHOICES if value in available_analysts ], instruction="\n- Press Space to select/unselect analysts\n- Press 'a' to select/unselect all\n- Press Enter when done", validate=lambda x: len(x) > 0 or "You must select at least one analyst.", style=questionary.Style( [ ("checkbox-selected", "fg:green"), ("selected", "fg:green noinherit"), ("highlighted", "noinherit"), ("pointer", "noinherit"), ] ), ).ask() if not choices: console.print("\n[red]No analysts selected. Exiting...[/red]") exit(1) return choices def select_research_depth(default=None) -> int: """Select research depth using an interactive selection.""" DEPTH_OPTIONS = [ ("Shallow - Quick research, few debate and strategy discussion rounds", 1), ("Medium - Middle ground, moderate debate rounds and strategy discussion", 3), ("Deep - Comprehensive research, in depth debate and strategy discussion", 5), ] choice = questionary.select( "Select Your [Research Depth]:", choices=[ questionary.Choice(display, value=value) for display, value in DEPTH_OPTIONS ], default=_matching_choice(DEPTH_OPTIONS, default), instruction="\n- Use arrow keys to navigate\n- Press Enter to select", style=questionary.Style( [ ("selected", "fg:yellow noinherit"), ("highlighted", "fg:yellow noinherit"), ("pointer", "fg:yellow noinherit"), ] ), ).ask() if choice is None: console.print("\n[red]No research depth selected. Exiting...[/red]") exit(1) return choice # Mainstream OpenRouter chat-LLM provider namespaces. We surface the newest # models from these rather than the universal-newest, which is dominated by # niche/experimental releases. These are the general-purpose chat providers; # more enterprise/specialised namespaces (nvidia, cohere, amazon, ...) tend to # ship research/safety variants as their newest, so they're left out of the # shortlist. Provider names are stable (unlike model IDs), so this rarely needs # touching; anything not here is still reachable via Custom ID. _OPENROUTER_MAINSTREAM = { "openai", "anthropic", "google", "deepseek", "qwen", "mistralai", "meta-llama", "x-ai", "z-ai", "minimax", "moonshotai", } def _fetch_openrouter_models() -> list[tuple[str, str]]: """Fetch available models from the OpenRouter API.""" import requests try: resp = requests.get("https://openrouter.ai/api/v1/models", timeout=10) resp.raise_for_status() models = resp.json().get("data", []) # Newest first so the top-N shown really is the latest available — the # API currently returns this order, but sort explicitly so the prompt's # "latest available" label holds regardless of response ordering. models.sort(key=lambda m: m.get("created") or 0, reverse=True) return [(m.get("name") or m["id"], m["id"]) for m in models] except Exception as e: console.print(f"\n[yellow]Could not fetch OpenRouter models: {e}[/yellow]") return [] def select_chatgpt_model(mode: str, default=None) -> str: """Select one of the ChatGPT plan's models, or enter a model ID.""" from tradingagents.llm_clients import chatgpt_auth try: models = chatgpt_auth.list_models() except Exception as e: console.print(f"\n[yellow]Could not list your ChatGPT plan's models: {escape(str(e))}[/yellow]") models = [] return _select_listed_model(f"Select Your [{mode.title()}-Thinking] ChatGPT Plan Model:", models, default, _prompt_custom_model_id) def _select_listed_model(title: str, models, default, custom) -> str: """Select from ``models`` ((name, model ID) pairs, listed live) or enter one with ``custom``.""" choices = [questionary.Choice(name, value=mid) for name, mid in models] choices.append(questionary.Choice("Custom model ID", value="custom")) choice = questionary.select( title, choices=choices, default=default if any(c.value == default for c in choices) else None, instruction="\n- Use arrow keys to navigate\n- Press Enter to select", style=_MENU_STYLE, ).ask() if choice is None: console.print("\n[red]No model selected. Exiting...[/red]") exit(1) return custom() if choice == "custom" else choice def _require_text(message: str, hint: str) -> str: """Prompt for a required value; exit cleanly if the user cancels. ``questionary.text(...).ask()`` returns None on Ctrl-C/Esc; mirror the exit-on-cancel behavior of the other required selections so a cancelled prompt never returns an empty model/deployment that would fail downstream. """ response = questionary.text( message, validate=lambda x: len(x.strip()) > 0 or hint, ).ask() if response is None: console.print("\n[red]Cancelled. Exiting...[/red]") exit(1) return response.strip() def select_openrouter_model(mode: str, default=None) -> str: """Select an OpenRouter model from the newest available, or enter a custom ID. ``mode`` ("quick"/"deep") labels the prompt so the two consecutive OpenRouter selections are distinguishable, like the other providers (#1000). """ models = _fetch_openrouter_models() # newest first # Prefer the newest from mainstream providers so the shortlist isn't crowded # out by niche/experimental releases; fall back to all if none match. mainstream = [ (name, mid) for name, mid in motradingagents-multi-agent-firm/cli/run.pycode · 21.7 KB · preview truncated
"""Running one analysis from the CLI: build the graph, stream it into the live view, save the report.""" import os import sys import time import webbrowser from functools import wraps from pathlib import Path import typer from rich.live import Live from cli.display import ( ANALYST_ORDER, AnalystWallTimeTracker, classify_message_type, console, create_layout, display_complete_report, message_buffer, update_analyst_statuses, update_display, update_research_team_status, ) from cli.selections import depth_from_env, get_user_selections, unattended_gaps from cli.stats_handler import StatsCallbackHandler from tradingagents.agents.rating import is_review, run_rating from tradingagents.dataflows.symbols import safe_ticker_component from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.graph.analyst_execution import ( build_analyst_execution_plan, ) from tradingagents.graph.trading_graph import TradingAgentsGraph def _run_directory(config: dict, ticker: str, trade_date: str) -> Path: """Where this run writes, with the ticker validated as a path component. Every other path that interpolates a ticker checks it first; a value of ".." here would place the run outside the results directory. """ return Path(config["results_dir"]) / safe_ticker_component(ticker) / trade_date def _announce_checkpoint_state(graph, ticker: str, trade_date: str) -> None: """Say whether this run resumed a saved one, where the user can see it. The graph logs this, but nothing in the CLI configures logging and the live view owns the screen, so a resume was invisible. """ if getattr(graph, "_resuming", False): message_buffer.add_message( "System", f"Resuming the saved run for {ticker} on {trade_date}" ) else: message_buffer.add_message("System", f"Starting fresh for {ticker} on {trade_date}") def _build_run_config(selections: dict, checkpoint: bool | None) -> dict: """Assemble the run config from interactive selections, honoring env precedence. Round counts and checkpoint follow "explicit env/flag wins": an env-applied value on DEFAULT_CONFIG is preserved unless the user overrode it on the CLI. """ config = DEFAULT_CONFIG.copy() # Research depth sets both round counts, but an explicit env override # (TRADINGAGENTS_MAX_DEBATE_ROUNDS / _MAX_RISK_ROUNDS) wins over the # interactive selection — leave the env-applied value in place (#977). rounds = (("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "max_debate_rounds"), ("TRADINGAGENTS_MAX_RISK_ROUNDS", "max_risk_discuss_rounds")) depth_was_asked = not depth_from_env() for env_var, key in rounds: if not os.environ.get(env_var): config[key] = selections["research_depth"] elif depth_was_asked: # The depth question appeared (it is skipped only when both are # set), so say which half of the answer the environment overrode. console.print( f"[green]✓ {key} from environment:[/green] {config[key]} " f"(set by {env_var}, so the research depth you chose does not apply to it)" ) config["quick_think_llm"] = selections["quick_think_llm"] config["deep_think_llm"] = selections["deep_think_llm"] config["backend_url"] = selections["backend_url"] config["llm_provider"] = selections["llm_provider"].lower() # Provider-specific thinking configuration. Only the main provider's is # asked; a tier on another provider keeps its configured value (#1440). for key in ("google_thinking_level", "openai_reasoning_effort", "anthropic_effort"): if selections.get(key) is not None: config[key] = selections[key] config["output_language"] = selections.get("output_language", "English") # --checkpoint/--no-checkpoint overrides only when explicitly given; omitting # the flag preserves TRADINGAGENTS_CHECKPOINT_ENABLED / the default (#976). if checkpoint is not None: config["checkpoint_enabled"] = checkpoint return config def run_analysis(checkpoint: bool | None = None, portfolio=None, flags=None): flags = flags or {} # With no terminal nothing can answer a prompt: name every question still # open before any model is called, rather than stopping at the first one. if not (sys.stdin and sys.stdin.isatty()): gaps = unattended_gaps(flags) if gaps: console.print("[red]No terminal to answer the setup questions. Set:[/red]") for gap in gaps: console.print(f" {gap}") raise typer.Exit(code=1) selections = get_user_selections(flags) config = _build_run_config(selections, checkpoint) stats_handler = StatsCallbackHandler() # Normalize analyst selection to predefined order (selection is a 'set', order is fixed) selected_set = {analyst.value for analyst in selections["analysts"]} selected_analyst_keys = [a for a in ANALYST_ORDER if a in selected_set] analyst_execution_plan = build_analyst_execution_plan(selected_analyst_keys) analyst_wall_time_tracker = AnalystWallTimeTracker(analyst_execution_plan) graph = TradingAgentsGraph( selected_analyst_keys, config=config, debug=True, callbacks=[stats_handler], ) message_buffer.init_for_analysis(selected_analyst_keys) # Track start time for elapsed display start_time = time.time() results_dir = _run_directory(config, selections["ticker"], selections["analysis_date"]) results_dir.mkdir(parents=True, exist_ok=True) report_dir = results_dir / "reports" report_dir.mkdir(parents=True, exist_ok=True) log_file = results_dir / "message_tool.log" log_file.touch(exist_ok=True) def save_message_decorator(obj, func_name): func = getattr(obj, func_name) @wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) timestamp, message_type, content = obj.messages[-1] content = content.replace("\n", " ") # Replace newlines with spaces with open(log_file, "a", encoding="utf-8") as f: f.write(f"{timestamp} [{message_type}] {content}\n") return wrapper def save_tool_call_decorator(obj, func_name): func = getattr(obj, func_name) @wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) timestamp, tool_name, args = obj.tool_calls[-1] args_str = ", ".join(f"{k}={v}" for k, v in args.items()) with open(log_file, "a", encoding="utf-8") as f: f.write(f"{timestamp} [Tool Call] {tool_name}({args_str})\n") return wrapper def save_report_section_decorator(obj, func_name): func = getattr(obj, func_name) @wraps(func) def wrapper(section_name, content): func(section_name, content) if section_name in obj.report_sections and obj.report_sections[section_name] is not None: content = obj.report_sections[section_name] if content: file_name = f"{section_name}.md" text = "\n".join(str(item) for item in content) if isinstance(content, list) else content with open(report_dir / file_name, "w", encoding="utf-8") as f: f.write(text) return wrapper message_buffer.add_message = save_message_decorator(message_buffer, "add_message") message_buffer.add_tool_call = save_tool_call_decorator(message_buffer, "add_tool_call") message_buffer.update_report_section = save_report_section_decorator(message_buffer, "update_report_section") layout = create_layout() # The alternate screen keeps a layout taller than the window from redrawing # by scrolling; the final report prints after this block, on the normal screen. with Live(layout, refresh_per_second=4, screen=True): # Initial display update_display(layout, stats_handler=stats_handler, start_time=start_time) message_buffer.add_message("System", f"Selected ticker: {selections['ticker']}") if selections["asset_type"] != "stock": message_buffer.add_message("System", f"Detected asset type: {selections['asset_type']}") message_buffer.add_message( "System", f"Analysis date: {selections['analysis_date']}" ) message_buffer.add_message( "System", f"Selected analysts: {', '.join(analyst.value for analyst in selections['analysts'])}", ) update_display(layout, stats_handler=stats_handler, start_time=start_time) # The analysts start together. for spec in analyst_execution_plan.specs: message_buffer.update_agent_status(spec.agent_node, "in_progress") analyst_wall_time_tracker.mark_started(spec.key) update_display(layout, stats_handler=stats_handler, start_time=start_time) spinner_text = ( f"Analyzing {selections['ticker']} on {selections['analysis_date']}..." ) update_display(layout, spinner_text, stats_handler=stats_handler, start_time=start_time) # The same initial state propagate() builds, with the resolved # instrument identity; the graph's Memory Log step settles past # decisions and loads their lessons alongside the analysts. init_agent_state = graph.create_run_state( selections["ticker"], selections["analysis_date"], selections["asset_type"], portfolio ) # Pass callbacks to graph config for tool execution tracking # (LLM tracking is handled separately via LLM constructor) args = graph.propagator.get_graph_args(callbacks=[stats_handler]) # Recompile with a checkpointer and inject the thread_id so --checkpoint # actually saves and resumes on the CLI path (#1249); a no-op when # checkpointing is disabled. Torn down in the finally below. checkpoint_tid = graph.begin_checkpoint( selections["ticker"], selections["analysis_date"], selections["asset_type"], portfolio ) if checkpoint_tid is not None: args.setdefault("config", {}).setdefault("configurable", {})["thread_id"] = checkpoint_tid _announce_checkpoint_state(graph, selections["ticker"], selections["analysis_date"]) # Stream the analysis. On resume, feed None so LangGraph continues the # interrupted run instead of re-appending the initial state (#1249); the # try/finally tears the checkpointer down even if the stream raises. trace = [] try: for messages, chunk in graph.stream_run(graph.checkpoint_input(init_agent_state), **args): for message in messages: msg_id = getattr(message, "id", None) if msg_id is not None: if msg_id in message_buffer._processed_message_ids: continue message_buffer._processed_message_ids.add(msg_id) msg_type, content = classify_message_type(message) if content and content.strip(): message_buffer.add_message(msg_type, content) if hasattr(message, "tool_calls") and message.tool_calls: for tool_call in message.tool_calls: if isinstance(tool_call, dict): message_buffer.add_tool_call(tool_call["name"], tool_call["args"]) else: message_buffer.add_tool_call(tool_call.name, tool_call.args) if chunk is None: # a step inside an analyst's graph: messages only update_display(layout, stats_handler=stats_handler, start_time=start_time)tradingagents-multi-agent-firm/cli/selections.pycode · 15.9 KB · preview truncated
"""The interactive choices for a run: ticker, date, analysts, depth, provider and models.""" import datetime import os from pathlib import Path import typer from rich.align import Align from rich.panel import Panel from cli import updates from cli.display import ( console, ) from cli.prefs import load_last_run, sanitize, save_last_run from cli.prompts import ( ask_anthropic_effort, ask_gemini_thinking_config, ask_glm_region, ask_minimax_region, ask_openai_reasoning_effort, ask_output_language, ask_qwen_region, confirm_ollama_endpoint, detect_asset_type, ensure_api_key, ensure_signed_in, get_ticker, parse_analysis_date, parse_analysts, parse_ticker, prompt_openai_compatible_url, resolve_backend_url, select_analysts, select_deep_thinking_agent, select_llm_provider, select_research_depth, select_shallow_thinking_agent, ) from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.llm_clients.factory import REASONING_EFFORT_PROVIDERS from tradingagents.llm_clients.openai_client import check_endpoint def get_user_selections(flags=None): """Ask for the run's settings, offering the previous run's answers.""" selections = _prompt_selections(load_last_run(), flags or {}) save_last_run(selections) return selections def depth_from_env() -> bool: """Both round counts come from the environment, so the depth question is skipped.""" return bool(os.environ.get("TRADINGAGENTS_MAX_DEBATE_ROUNDS") and os.environ.get("TRADINGAGENTS_MAX_RISK_ROUNDS")) def unattended_gaps(flags) -> list[str]: """The flags and environment variables a run with no terminal still needs.""" env = os.environ.get gaps = [f"--{name}" for name in ("ticker", "date", "analysts") if flags.get(name) is None] gaps += [f"--{name} or --no-{name}" for name in ("save", "show") if flags.get(name) is None] if not env("TRADINGAGENTS_OUTPUT_LANGUAGE"): gaps.append("TRADINGAGENTS_OUTPUT_LANGUAGE") if not depth_from_env(): gaps.append("TRADINGAGENTS_MAX_DEBATE_ROUNDS and TRADINGAGENTS_MAX_RISK_ROUNDS") if not env("TRADINGAGENTS_LLM_PROVIDER"): gaps.append("TRADINGAGENTS_LLM_PROVIDER") if not (env("TRADINGAGENTS_QUICK_THINK_LLM") or env("TRADINGAGENTS_DEEP_THINK_LLM")): gaps.append("TRADINGAGENTS_QUICK_THINK_LLM or TRADINGAGENTS_DEEP_THINK_LLM") return gaps def _check_tier_providers(main_provider: str, backend_url: str | None = None) -> None: """Check each provider the model tiers use (#1440) before the run. A tier on another provider needs its model from its variable, since the model question offers the main provider's models. Each provider a tier uses has its endpoint, sign-in and key checked now, prompting for a missing key, rather than at its first call, which for the deep tier comes after everything else has been paid for; a provider no tier uses needs no key. """ used = [] for tier in ("quick", "deep"): provider = (DEFAULT_CONFIG.get(f"{tier}_think_provider") or main_provider).lower() on_main = provider == main_provider.lower() if not on_main: variable = f"TRADINGAGENTS_{tier.upper()}_THINK_LLM" if not os.environ.get(variable): console.print(f"[red]The {tier} tier runs on {provider}; set {variable} to one of its models.[/red]") raise typer.Exit(code=1) # The tier's endpoint, as create_tier_client resolves it. url = DEFAULT_CONFIG.get(f"{tier}_think_backend_url") or (backend_url if on_main else None) try: check_endpoint(provider, url) except ValueError as exc: console.print(f"[red]{exc}[/red]") raise typer.Exit(code=1) from None if provider not in used: used.append(provider) for provider in used: ensure_signed_in(provider) ensure_api_key(provider) def _from_flag(parse, value, *args): """A flag's value through the same check its prompt applies; a bad one ends the run.""" try: return parse(value, *args) except ValueError as exc: console.print(f"[red]{exc}[/red]") raise typer.Exit(code=1) from None def _prompt_selections(prefs, flags): """Walk the selection steps. ``prefs`` prefills; flags and the environment skip.""" with open(Path(__file__).parent / "static" / "welcome.txt", encoding="utf-8") as f: welcome_ascii = f.read() welcome_content = f"{welcome_ascii}\n" welcome_content += "[bold green]TradingAgents: Multi-Agents LLM Financial Trading Framework - CLI[/bold green]\n\n" welcome_content += "[bold]Workflow Steps:[/bold]\n" welcome_content += "I. Analyst Team → II. Research Team → III. Trader → IV. Risk Management → V. Portfolio Management\n\n" welcome_content += ( "[dim]Built by [Tauric Research](https://github.com/TauricResearch)[/dim]" ) welcome_box = Panel( welcome_content, border_style="green", padding=(1, 2), title="Welcome to TradingAgents", subtitle="Multi-Agents LLM Financial Trading Framework", ) console.print(Align.center(welcome_box)) console.print() console.print() # Add vertical space before announcements # What's new comes from a cache refreshed in the background, and only for # someone at a terminal: a run never waits on it and unattended runs skip it. if updates.enabled(): updates.show(console) updates.start_background_refresh() def create_question_box(title, prompt, default=None): box_content = f"[bold]{title}[/bold]\n" box_content += f"[dim]{prompt}[/dim]" if default: box_content += f"\n[dim]Default: {default}[/dim]" return Panel(box_content, border_style="blue", padding=(1, 2)) def thinking_value_or_prompt(env_var, config_key, label, box_title, box_body, prompt_fn): """Return the env-configured reasoning/thinking value, or prompt for it. When ``env_var`` is set the interactive choice is skipped and the value the env overlay placed on DEFAULT_CONFIG is used — mirroring the env-precedence rule applied to the other selection steps. """ if os.environ.get(env_var): value = DEFAULT_CONFIG[config_key] console.print(f"[green]✓ {label} from environment:[/green] {value}") return value console.print(create_question_box(box_title, box_body)) return prompt_fn() # Step 1: Ticker symbol if flags.get("ticker") is not None: selected_ticker = _from_flag(parse_ticker, flags["ticker"]) console.print(f"[green]✓ Ticker from --ticker:[/green] {selected_ticker}") else: console.print( create_question_box( "Step 1: Ticker Symbol", "Enter the ticker, with exchange suffix when needed (e.g. SPY, 0700.HK, BTC-USD)", "SPY", ) ) selected_ticker = get_ticker() asset_type = detect_asset_type(selected_ticker) # Only announce when it's not the default stock path, to avoid printing # "stock" on every run. if asset_type.value != "stock": console.print( f"[green]Detected asset type:[/green] {asset_type.value}" ) # Step 2: Analysis date if flags.get("date") is not None: analysis_date = _from_flag(parse_analysis_date, flags["date"]) console.print(f"[green]✓ Analysis date from --date:[/green] {analysis_date}") else: default_date = datetime.datetime.now().strftime("%Y-%m-%d") console.print( create_question_box( "Step 2: Analysis Date", "Enter the analysis date (YYYY-MM-DD)", default_date, ) ) analysis_date = get_analysis_date() # Step 3: Output language (skipped when set via TRADINGAGENTS_OUTPUT_LANGUAGE) if os.environ.get("TRADINGAGENTS_OUTPUT_LANGUAGE"): output_language = DEFAULT_CONFIG["output_language"] console.print( f"[green]✓ Output language from environment:[/green] {output_language}" ) else: console.print( create_question_box( "Step 3: Output Language", "Select the language for analyst reports and final decision" ) ) output_language = ask_output_language(prefs.get("output_language")) # Step 4: Select analysts prefs = sanitize(prefs, asset_type.value) if flags.get("analysts") is not None: selected_analysts = _from_flag(parse_analysts, flags["analysts"], asset_type) else: console.print( create_question_box( "Step 4: Analysts Team", "Select your LLM analyst agents for the analysis" ) ) selected_analysts = select_analysts(asset_type, prefs.get("analysts")) console.print( f"[green]Selected analysts:[/green] {', '.join(analyst.value for analyst in selected_analysts)}" ) # Step 5: Research depth (skipped when both round counts are set via env). # Research depth maps to the debate + risk round counts; when both are # supplied through TRADINGAGENTS_MAX_DEBATE_ROUNDS / _MAX_RISK_ROUNDS we keep # the run non-interactive and honor the env values (#977). if depth_from_env(): selected_research_depth = DEFAULT_CONFIG["max_debate_rounds"] console.print( f"[green]✓ Research depth from environment:[/green] " f"{DEFAULT_CONFIG['max_debate_rounds']} debate / " f"{DEFAULT_CONFIG['max_risk_discuss_rounds']} risk rounds" ) else: console.print( create_question_box( "Step 5: Research Depth", "Select your research depth level" ) ) selected_research_depth = select_research_depth(prefs.get("research_depth")) # Step 6: LLM Provider (skipped when set via TRADINGAGENTS_LLM_PROVIDER). # The backend URL comes from TRADINGAGENTS_LLM_BACKEND_URL when set, # otherwise the provider's default endpoint — the same value the menu # would have picked. provider_from_env = bool(os.environ.get("TRADINGAGENTS_LLM_PROVIDER")) if provider_from_env: selected_llm_provider = DEFAULT_CONFIG["llm_provider"].lower() backend_url = resolve_backend_url( selected_llm_provider, env_url=DEFAULT_CONFIG["backend_url"] ) console.print(f"[green]✓ LLM provider from environment:[/green] {selected_llm_provider}") console.print(f"[green]✓ Backend URL:[/green] {backend_url}") else: console.print( create_question_box( "Step 6: LLM Provider", "Select your LLM provider" ) ) selected_llm_provider, backend_url = select_llm_provider(prefs.get("llm_provider")) # Providers with regional endpoints prompt for the region as a secondary # step so the main dropdown stays clean (mainland China and international # accounts cannot share API keys). if selected_llm_provider == "qwen": selected_llm_provider, backend_url = ask_qwen_region() elif selected_llm_provider == "minimax": selected_llm_provider, backend_url = ask_minimax_region() elif selected_llm_provider == "glm": selected_llm_provider, backend_url = ask_glm_region() # Honor an explicit env backend URL even when the provider was chosen # interactively, so it isn't overwritten by the menu default (#978). backend_url = resolve_backend_url( selected_llm_provider, backend_url, env_url=DEFAULT_CONFIG["backend_url"] ) # The generic OpenAI-compatible endpoint has no default; ask for it if # neither the menu nor the environment supplied one.tradingagents-multi-agent-firm/cli/static/welcome.txtcode · 0.4 KB
______ ___ ___ __ /_ __/________ _____/ (_)___ ____ _/ | ____ ____ ____ / /______ / / / ___/ __ `/ __ / / __ \/ __ `/ /| |/ __ `/ _ \/ __ \/ __/ ___/ / / / / / /_/ / /_/ / / / / / /_/ / ___ / /_/ / __/ / / / /_(__ ) /_/ /_/ \__,_/\__,_/_/_/ /_/\__, /_/ |_\__, /\___/_/ /_/\__/____/ /____/ /____/tradingagents-multi-agent-firm/cli/stats_handler.pycode · 2.3 KB
import threading from typing import Any from langchain_core.callbacks import BaseCallbackHandler from langchain_core.messages import AIMessage from langchain_core.outputs import LLMResult class StatsCallbackHandler(BaseCallbackHandler): """Callback handler that tracks LLM calls, tool calls, and token usage.""" def __init__(self) -> None: super().__init__() self._lock = threading.Lock() self.llm_calls = 0 self.tool_calls = 0 self.tokens_in = 0 self.tokens_out = 0 def on_llm_start( self, serialized: dict[str, Any], prompts: list[str], **kwargs: Any, ) -> None: """Increment LLM call counter when an LLM starts.""" with self._lock: self.llm_calls += 1 def on_chat_model_start( self, serialized: dict[str, Any], messages: list[list[Any]], **kwargs: Any, ) -> None: """Increment LLM call counter when a chat model starts.""" with self._lock: self.llm_calls += 1 def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None: """Extract token usage from LLM response.""" try: generation = response.generations[0][0] except (IndexError, TypeError): return usage_metadata = None if hasattr(generation, "message"): message = generation.message if isinstance(message, AIMessage) and hasattr(message, "usage_metadata"): usage_metadata = message.usage_metadata if usage_metadata: with self._lock: self.tokens_in += usage_metadata.get("input_tokens", 0) self.tokens_out += usage_metadata.get("output_tokens", 0) def on_tool_start( self, serialized: dict[str, Any], input_str: str, **kwargs: Any, ) -> None: """Increment tool call counter when a tool starts.""" with self._lock: self.tool_calls += 1 def get_stats(self) -> dict[str, Any]: """Return current statistics.""" with self._lock: return { "llm_calls": self.llm_calls, "tool_calls": self.tool_calls, "tokens_in": self.tokens_in, "tokens_out": self.tokens_out, }tradingagents-multi-agent-firm/cli/updates.pycode · 8.6 KB
"""What's new, for the person at the terminal: a newer release, and notices from Tauric Research. At most once a day a background thread asks GitHub for the latest release and the announcements server for notices, and writes the answers to a small cache. The lines shown at start-up are read from that cache, so a run never waits on the network and news shows up on the next run. Nothing identifying is sent. Unattended, scripted and CI runs contact no server; set TRADINGAGENTS_NO_UPDATE_CHECK=1 to turn the check off. """ import importlib.metadata import json import os import re import shlex import sys import threading import time from collections.abc import Callable from datetime import UTC, date, datetime from pathlib import Path from urllib.parse import unquote, urlparse import requests from packaging.version import InvalidVersion, Version from tradingagents.dataflows.files import replace_file from tradingagents.default_config import _TRADINGAGENTS_HOME RELEASES_URL = "https://api.github.com/repos/TauricResearch/TradingAgents/releases/latest" NOTICES_URL = "https://api.tauric.ai/v1/announcements" CACHE = Path(_TRADINGAGENTS_HOME) / "update_check.json" CHECK_EVERY = 24 * 3600 TIMEOUT = 2.0 SUMMARY_LIMIT = 160 OPT_OUT = "TRADINGAGENTS_NO_UPDATE_CHECK" # Control characters, C0 and C1, read as spaces; invisible spaces and direction marks are dropped. CONTROL = re.compile(r"[\x00-\x1f\x7f-\x9f]") INVISIBLE = re.compile(r"[\u061c\u200b\u200e\u200f\u202a-\u202e\u2066-\u2069\ufeff]") DATE = re.compile(r"\d{4}-\d{2}-\d{2}") def enabled() -> bool: """Whether a person is at the terminal and has not turned the check off.""" at_a_terminal = bool(sys.stdin and sys.stdin.isatty() and sys.stdout and sys.stdout.isatty()) return at_a_terminal and not os.environ.get("CI") and not os.environ.get(OPT_OUT) # --- the cache and the check ------------------------------------------------------- def read_cache() -> dict: """The cache, keeping only fields of the shape this version writes.""" try: data = json.loads(CACHE.read_text(encoding="utf-8")) except (OSError, ValueError): return {} if not isinstance(data, dict): return {} shapes = {"checked_at": (int, float), "latest": dict, "notices": list} return {key: data[key] for key, shape in shapes.items() if isinstance(data.get(key), shape)} def due(cache: dict, now: float) -> bool: checked = cache.get("checked_at") return not isinstance(checked, (int, float)) or not 0 <= now - checked < CHECK_EVERY def summary_of(body: str) -> str: """The release's one-line summary: its first paragraph on one line (sanitised when shown).""" first = re.split(r"\n\s*\n", (body or "").strip(), maxsplit=1)[0] text = " ".join(first.split()) if len(text) > SUMMARY_LIMIT: text = text[: SUMMARY_LIMIT - 3].rsplit(" ", 1)[0] + "..." return text def parse_notices(payload) -> list | None: """The targeted notices in an announcements answer; None when it has no notices list.""" notices = payload.get("notices") if isinstance(payload, dict) else None return notices if isinstance(notices, list) else None def _get_json(url: str): response = requests.get(url, timeout=TIMEOUT) return response.json() if response.ok else None def refresh(now: float | None = None) -> None: """Ask for the latest release and notices and record the answers. Never raises or prints; a failed check keeps what was known and is not retried for a day.""" now = time.time() if now is None else now cache = read_cache() cache["checked_at"] = now try: release = _get_json(RELEASES_URL) if isinstance(release, dict) and release.get("tag_name"): cache["latest"] = { "version": str(release["tag_name"]).removeprefix("v"), "url": release.get("html_url"), "summary": summary_of(release.get("body") or ""), } except Exception: pass try: notices = parse_notices(_get_json(NOTICES_URL)) if notices is not None: cache["notices"] = notices except Exception: pass try: CACHE.parent.mkdir(parents=True, exist_ok=True) replace_file(CACHE, lambda temp: Path(temp).write_text(json.dumps(cache), encoding="utf-8")) except Exception: pass def start_background_refresh() -> None: """Refresh the cache in a daemon thread when a check is due.""" if due(read_cache(), time.time()): threading.Thread(target=refresh, name="tradingagents-update-check", daemon=True).start() # --- what is shown --------------------------------------------------------------- def _version(text) -> Version | None: try: return Version(str(text)) except (InvalidVersion, TypeError): return None def _plain(text) -> str: return " ".join(CONTROL.sub(" ", INVISIBLE.sub("", str(text or ""))).split()) def _applies(notice: dict, running: Version, today: date) -> bool: """Whether a notice is for this version and unexpired; a bound or date that cannot be read keeps it hidden. A notice is shown through its ``expires`` date (UTC).""" low, high = notice.get("min_version"), notice.get("max_version") if low is not None and ((floor := _version(low)) is None or running < floor): return False if high is not None and ((ceiling := _version(high)) is None or running > ceiling): return False expires = notice.get("expires") if expires is None: return True try: return isinstance(expires, str) and bool(DATE.fullmatch(expires)) and date.fromisoformat(expires) >= today except ValueError: return False def messages(cache: dict, running: str, upgrade: Callable[[], str | None] = lambda: None) -> list[str]: """The lines to show: a newer release with its upgrade command (``upgrade()``, asked only when there is one), then the notices for this version.""" current = _version(running) if current is None: return [] lines = [] latest = cache.get("latest") or {} newest = _version(latest.get("version")) if newest is not None and newest > current: lines.append(f"TradingAgents {_plain(latest['version'])} is available (you have {_plain(running)}): " f"{_plain(latest.get('summary'))}") if latest.get("url"): lines.append(_plain(latest["url"])) command = upgrade() if command: lines.append(f"Upgrade: {_plain(command)}") today = datetime.now(UTC).date() for notice in cache.get("notices") or []: if not (isinstance(notice, dict) and isinstance(notice.get("message"), str) and _plain(notice["message"]) and _applies(notice, current, today)): continue url = notice.get("url") link = url if isinstance(url, str) and url.lower().startswith(("http://", "https://")) else None lines.append(_plain(notice["message"]) + (f" {_plain(link)}" if link else "")) return lines def upgrade_command(dist=None) -> str | None: """The command that upgrades this install, from how pip or uv recorded it; None when unsure.""" if os.environ.get("TRADINGAGENTS_IN_DOCKER"): return "git pull && docker compose build (on the host)" if os.name == "nt": # the commands below are POSIX shell return None try: dist = dist or importlib.metadata.distribution("tradingagents") direct = json.loads(dist.read_text("direct_url.json") or "null") installer = (dist.read_text("INSTALLER") or "").strip() except (importlib.metadata.PackageNotFoundError, OSError, ValueError): return None if not isinstance(direct, dict) or not direct.get("url"): return None python = shlex.quote(sys.executable) install = f"uv pip install --python {python}" if installer == "uv" else f"{python} -m pip install" if "vcs_info" in direct: return f"{install} -U git+{direct['url']}" parsed = urlparse(direct["url"]) if parsed.scheme != "file": return None clone = Path(unquote(parsed.path)) if not (clone / ".git").exists(): return None editable = (direct.get("dir_info") or {}).get("editable") target = shlex.quote(str(clone)) return f"git -C {target} pull && {install} {'-e ' if editable else ''}{target}" def show(console) -> None: """Print what's new from the cache, under the welcome panel.""" from rich.panel import Panel from rich.text import Text from tradingagents import __version__ lines = messages(read_cache(), running=__version__, upgrade=upgrade_command) if lines: console.print(Panel(Text("\n".join(lines)), border_style="cyan", padding=(1, 2), title="What's new")) console.print()tradingagents-multi-agent-firm/docker-compose.ymlcode · 0.7 KB
services: tradingagents: build: . env_file: - .env volumes: - ${TRADINGAGENTS_DATA_DIR:-tradingagents_data}:/home/appuser/.tradingagents tty: true stdin_open: true ollama: image: ollama/ollama:latest volumes: - ollama_data:/root/.ollama profiles: - ollama tradingagents-ollama: build: . env_file: - .env environment: - TRADINGAGENTS_LLM_PROVIDER=ollama - OLLAMA_BASE_URL=http://ollama:11434/v1 volumes: - ${TRADINGAGENTS_DATA_DIR:-tradingagents_data}:/home/appuser/.tradingagents depends_on: - ollama tty: true stdin_open: true profiles: - ollama volumes: tradingagents_data: ollama_data:tradingagents-multi-agent-firm/main.pycode · 0.6 KB
from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.graph.trading_graph import TradingAgentsGraph # DEFAULT_CONFIG already applies TRADINGAGENTS_* env-var overrides # (llm_provider, deep_think_llm, quick_think_llm, backend_url, etc.), # so users can switch models or endpoints purely via .env without # editing this script. Override individual keys here only when you # want a hard-coded value that should ignore the environment. config = DEFAULT_CONFIG.copy() # Initialize with custom config ta = TradingAgentsGraph(debug=True, config=config) # forward propagate _, decision = ta.propagate("NVDA", "2026-09-01") print(decision)tradingagents-multi-agent-firm/pyproject.tomlcode · 2.4 KB
[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "tradingagents" dynamic = ["version"] description = "TradingAgents: Multi-Agents LLM Financial Trading Framework" readme = "README.md" requires-python = ">=3.11" dependencies = [ "cryptography>=42.0", "langchain-core>=1.6.5", "langchain-anthropic>=1.7.4", "langchain-google-genai>=4.4.0", "langchain-openai>=1.6.6", "langgraph>=1.2.12", "langgraph-checkpoint-sqlite>=3.1.1", "markdown-it-py>=4.0", "packaging>=24.0", "pandas>=3.0.6", "python-dotenv>=1.0.0", "pytz>=2025.2", "questionary>=2.1.0", "requests>=2.32.4", "rich>=14.0.0", "typer>=0.21.0", "stockstats>=0.6.5", "typing-extensions>=4.14.0", "yfinance>=1.7.0", ] [project.optional-dependencies] dev = [ "ruff>=0.16", "pytest>=8.0", "pytest-subtests>=0.13", ] # Amazon Bedrock support (AWS SigV4 auth + boto3). Optional so the core install # stays lean: pip install "tradingagents[bedrock]". bedrock = [ "langchain-aws>=1.7.9", ] [project.scripts] tradingagents = "cli.main:app" [tool.setuptools.dynamic] version = {attr = "tradingagents.__version__"} [tool.setuptools.packages.find] include = ["tradingagents*", "cli*"] [tool.setuptools.package-data] cli = ["static/*"] tradingagents = ["assets/*.svg"] [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-ra --strict-markers -m \"not integration\"" markers = [ "unit: fast isolated unit tests", "integration: tests requiring external services; run only when asked for, with -m integration", "smoke: quick sanity-check tests", ] filterwarnings = [ "ignore::DeprecationWarning", ] [tool.ruff] line-length = 100 target-version = "py311" extend-exclude = ["results"] [tool.ruff.lint] # Standard "good defaults" rule set (pyflakes + pycodestyle + isort + bugbear + # pyupgrade + comprehensions/simplify). Line length (E501) and layout are owned # by the formatter; whole-repo `ruff format` adoption is deferred until the # open-PR backlog clears, to avoid mass merge conflicts. select = ["E", "W", "F", "I", "B", "UP", "C4", "SIM"] ignore = ["E501"] [tool.ruff.lint.per-file-ignores] "**/__init__.py" = ["F401"] # intentional re-exports [tool.ruff.lint.isort] # Keep multiple aliased names from one module in a single combined import block # (e.g. the vendor imports in router.py) instead of one statement per name. combine-as-imports = truetradingagents-multi-agent-firm/requirements.txtcode · 0.1 KB
# The dependencies are declared in pyproject.toml. From the repository root this # installs TradingAgents with them, the same as `pip install .` .tradingagents-multi-agent-firm/tests/__init__.pycode · 0.0 KB
tradingagents-multi-agent-firm/tests/conftest.pycode · 4.6 KB
"""Shared pytest fixtures that prevent CI hangs when API keys are absent.""" import os import socket import pytest def _blank_settings_overlay(): """Blank every TRADINGAGENTS_* setting before the package is imported. The package loads .env on import and folds these variables into DEFAULT_CONFIG, so a contributor's own settings would become the defaults the suite asserts on. A blank value is still present, so load_dotenv leaves it alone, and the overlay reads it as unset. Tests of the overlay set their own. """ from dotenv import dotenv_values, find_dotenv names = set(os.environ) for filename in (".env", ".env.enterprise"): names |= set(dotenv_values(find_dotenv(filename, usecwd=True))) for name in names: if name.startswith("TRADINGAGENTS_"): os.environ[name] = "" _blank_settings_overlay() def _own_file_locations(): """Keep the suite's results, cache and memory log in a directory of its own. Set before the package is imported, whose defaults put them in the user's home: a test reading the user's cache would see what their runs fetched. """ import tempfile home = tempfile.mkdtemp(prefix="tradingagents-tests-") os.environ["TRADINGAGENTS_RESULTS_DIR"] = os.path.join(home, "logs") os.environ["TRADINGAGENTS_CACHE_DIR"] = os.path.join(home, "cache") os.environ["TRADINGAGENTS_MEMORY_LOG_PATH"] = os.path.join(home, "memory", "trading_memory.md") _own_file_locations() def pytest_configure(config): for marker in ("unit", "integration", "smoke"): config.addinivalue_line("markers", f"{marker}: {marker}-level tests") # yfinance caches each symbol's time zone on disk, in the home directory # by default; the suite keeps it in a directory of its own. import tempfile import yfinance yfinance.set_tz_cache_location(tempfile.mkdtemp(prefix="tradingagents-tests-yf-")) @pytest.fixture(autouse=True) def _no_network(request, monkeypatch): """Tests do not reach the network; one that must is marked integration.""" if request.node.get_closest_marker("integration"): return def refuse(*args, **kwargs): raise OSError(f"test tried to reach the network: {args[1:] or kwargs}") monkeypatch.setattr(socket.socket, "connect", refuse) monkeypatch.setattr(socket.socket, "connect_ex", refuse) monkeypatch.setattr(socket, "getaddrinfo", lambda *a, **k: refuse(None, *a)) # yfinance's HTTP client is libcurl, which never touches Python's sockets. from curl_cffi import requests as curl_requests monkeypatch.setattr(curl_requests.Session, "request", refuse) monkeypatch.setattr(curl_requests.AsyncSession, "request", refuse) @pytest.fixture(autouse=True) def _at_a_terminal(monkeypatch): """Tests of the interactive steps run as if at a terminal; pytest's stdin is not one. A test of an unattended run sets isatty to False itself.""" import sys monkeypatch.setattr(sys.stdin, "isatty", lambda: True) @pytest.fixture(autouse=True) def _own_cli_prefs(tmp_path, monkeypatch): """The CLI keeps the last run's selections in the user's home; tests keep theirs apart.""" monkeypatch.setattr("cli.prefs._PREFS_PATH", tmp_path / "cli_prefs.json") _API_KEY_ENV_VARS = ( "OPENAI_API_KEY", "GOOGLE_API_KEY", "ANTHROPIC_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", "AZURE_OPENAI_API_KEY", "ALPHA_VANTAGE_API_KEY", ) @pytest.fixture(autouse=True) def _dummy_api_keys(monkeypatch): for env_var in _API_KEY_ENV_VARS: # `or` not a .get default: an env var present but empty (e.g. a key left # blank in a .env copied from .env.example) must still get the placeholder. monkeypatch.setenv(env_var, os.environ.get(env_var) or "placeholder") @pytest.fixture(autouse=True) def _isolate_config(): """Reset the global dataflows config before and after each test. ``set_config`` merges (it never clears keys absent from the override), so a test that sets e.g. ``tool_vendors`` would otherwise leak into later tests and make routing behavior order-dependent. Replace the global outright so every test starts from a clean DEFAULT_CONFIG. """ import copy import tradingagents.dataflows.config as config_module import tradingagents.default_config as default_config config_module._config = copy.deepcopy(default_config.DEFAULT_CONFIG) yield config_module._config = copy.deepcopy(default_config.DEFAULT_CONFIG)tradingagents-multi-agent-firm/tests/test_alpha_vantage_hardening.pycode · 9.6 KB
"""Alpha Vantage request hardening. Regressions for #990 (no request timeout -> can hang), #991 (invalid-key responses mislabeled as rate limits and silently treated as transient), and #1115 (fundamentals look-ahead filter never ran because the payload is a JSON string, not a dict), and the date trim that keeps post-end_date bars out of a historical run. """ import json import pytest import tradingagents.dataflows.net as net import tradingagents.dataflows.vendors.alpha_vantage.common as av import tradingagents.dataflows.vendors.alpha_vantage.fundamentals as avf import tradingagents.dataflows.vendors.alpha_vantage.stock as avs class _FakeResponse: status_code = 200 def __init__(self, text): self.text = text def raise_for_status(self): pass def _patched_get(body, capture=None): def fake_get(url, params=None, **kwargs): if capture is not None: capture.update(kwargs) return _FakeResponse(body) return fake_get @pytest.mark.unit def test_request_passes_timeout(monkeypatch): captured = {} monkeypatch.setattr(net.requests, "get", _patched_get("Date,Close\n2025-01-02,1.0", captured)) av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"}) assert captured.get("timeout") == av.REQUEST_TIMEOUT # #990 @pytest.mark.unit def test_rate_limit_detected(monkeypatch): body = '{"Information": "Our standard API rate limit is 25 requests per day. ... your API key ..."}' monkeypatch.setattr(net.requests, "get", _patched_get(body)) with pytest.raises(av.AlphaVantageRateLimitError): av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"}) @pytest.mark.unit def test_invalid_key_not_mislabeled_as_rate_limit(monkeypatch): # AV's invalid-key notice mentions "API key"; it must NOT be treated as a # (transient) rate limit, but surface as a real configuration error (#991). body = ('{"Information": "the parameter apikey is invalid or missing. ' 'Please claim your free API key on (https://www.alphavantage.co/support/#api-key)."}') monkeypatch.setattr(net.requests, "get", _patched_get(body)) with pytest.raises(av.AlphaVantageNotConfiguredError): av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"}) with pytest.raises(av.AlphaVantageRateLimitError): # sanity: rate-limit path still distinct monkeypatch.setattr(net.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}')) av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"}) @pytest.mark.unit def test_a_rejected_request_is_unavailable_not_data(monkeypatch): """Alpha Vantage answers a call it rejects with {"Error Message": ...}, for an unknown symbol and for a malformed call alike, so it says nothing about the instrument; served as data it reads as a one-line CSV or as "no data" (#1442).""" from tradingagents.dataflows import router from tradingagents.dataflows.config import set_config from tradingagents.dataflows.errors import VendorUnavailableError monkeypatch.setenv("ALPHA_VANTAGE_API_KEY", "k-secret") body = '{"Error Message": "Invalid API call. Please retry or visit the documentation for TIME_SERIES_DAILY."}' monkeypatch.setattr(net.requests, "get", _patched_get(body)) with pytest.raises(VendorUnavailableError) as raised: av._make_api_request("TIME_SERIES_DAILY", {"symbol": "ZZZZ"}) assert "k-secret" not in str(raised.value) set_config({"data_vendors": {"core_stock_apis": "alpha_vantage"}}) out = router.route_to_vendor("get_stock_data", "ZZZZ", "2026-01-02", "2026-01-09") assert out.startswith("DATA_UNAVAILABLE"), out @pytest.mark.unit @pytest.mark.parametrize("body", [ '{"Note": "Thank you for using key k-secret. API call frequency is 5 calls per minute."}', '{"Information": "the apikey k-secret is invalid or missing."}', ]) def test_the_key_never_reaches_an_error_message(monkeypatch, body): """Alpha Vantage can echo the key in its notices; error text reaches logs and reports.""" from tradingagents.dataflows.errors import VendorError monkeypatch.setenv("ALPHA_VANTAGE_API_KEY", "k-secret") monkeypatch.setattr(net.requests, "get", _patched_get(body)) with pytest.raises(VendorError) as raised: av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"}) assert "k-secret" not in str(raised.value) _FUNDAMENTALS_JSON = json.dumps({ "symbol": "AAPL", "annualReports": [ {"fiscalDateEnding": "2025-12-31", "totalAssets": "1"}, # future -> must drop {"fiscalDateEnding": "2023-12-31", "totalAssets": "2"}, # past -> must keep ], "quarterlyReports": [ {"fiscalDateEnding": "2024-06-30", "totalAssets": "3"}, # future -> must drop {"fiscalDateEnding": "2023-09-30", "totalAssets": "4"}, # past -> must keep ], }) @pytest.mark.unit def test_fundamentals_no_curr_date_passes_through(monkeypatch): monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: _FUNDAMENTALS_JSON) assert avf.get_income_statement("AAPL") == _FUNDAMENTALS_JSON # --------------------------------------------------------------------------- # Date trim (see the rationale on the unguarded trim in alpha_vantage.common) # --------------------------------------------------------------------------- _DAILY_CSV = ( "timestamp,open,high,low,close,volume\n" "2024-05-13,1,1,1,1,10\n" # after end_date -> must never be served "2024-05-10,1,1,1,1,10\n" "2024-05-09,1,1,1,1,10\n" ) @pytest.mark.unit def test_stock_data_is_trimmed_to_the_requested_window(monkeypatch): monkeypatch.setattr(avs, "_make_api_request", lambda *a, **k: _DAILY_CSV) out = avs.get_stock("IBM", "2024-05-09", "2024-05-10") assert "2024-05-10" in out and "2024-05-09" in out assert "2024-05-13" not in out, "bar after end_date leaked into the window" @pytest.mark.unit def test_unparseable_body_is_never_served_untrimmed(monkeypatch): """The trim used to swallow the failure and return the whole body, putting bars after end_date into a backtest. It must raise instead.""" monkeypatch.setattr(avs, "_make_api_request", lambda *a, **k: "timestamp,close\nnot-a-date,1\n") with pytest.raises(ValueError): avs.get_stock("IBM", "2024-05-09", "2024-05-10") @pytest.mark.unit def test_empty_body_still_passes_through(monkeypatch): monkeypatch.setattr(avs, "_make_api_request", lambda *a, **k: "") assert avs.get_stock("IBM", "2024-05-09", "2024-05-10") == "" def test_request_error_message_carries_no_key(monkeypatch): # Alpha Vantage also sends its key in the URL (#1324). import requests key = "AVKEY1234567890XYZ" monkeypatch.setenv("ALPHA_VANTAGE_API_KEY", key) def boom(*a, **k): raise requests.Timeout(f"Read timed out. url: https://www.alphavantage.co/query?apikey={key}") monkeypatch.setattr(net.requests, "get", boom) with pytest.raises(requests.Timeout) as caught: av._make_api_request("OVERVIEW", {"symbol": "IBM"}) assert key not in str(caught.value) @pytest.mark.unit def test_global_news_omitted_optionals_use_the_configured_defaults(monkeypatch): """The tool passes None for an omitted look_back_days or limit (#1326).""" from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news monkeypatch.setattr(alpha_vantage_news, "get_config", lambda: {"global_news_lookback_days": 3, "global_news_article_limit": 9}) seen = {} monkeypatch.setattr(alpha_vantage_news, "_make_api_request", lambda fn, params: seen.update(params) or "{}") alpha_vantage_news.get_global_news("2026-08-14", None, None) assert seen["time_from"].startswith("20260811") and seen["limit"] == "9" @pytest.mark.unit def test_the_news_window_includes_the_analysis_day(monkeypatch): """time_to was midnight at the start of the end date, so everything published during the analysis day, the most decision-relevant day, was excluded. The yfinance path includes it.""" from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news seen = {} monkeypatch.setattr(alpha_vantage_news, "_make_api_request", lambda fn, params: seen.update(params) or "{}") alpha_vantage_news.get_news("AAPL", "2026-03-10", "2026-03-14") assert seen["time_from"] == "20260310T0000" assert seen["time_to"] == "20260314T2359" @pytest.mark.unit @pytest.mark.parametrize("indicator", ["vwma", "mfi"]) def test_an_indicator_this_vendor_lacks_lets_the_next_one_serve_it(indicator): """Returning prose counts as success to the router, so the chain stops at a vendor that cannot compute the indicator while the next one can.""" from tradingagents.dataflows.errors import VendorError from tradingagents.dataflows.vendors.alpha_vantage import indicator as alpha_vantage_indicator with pytest.raises(VendorError): alpha_vantage_indicator.get_indicator("AAPL", indicator, "2026-05-08", 30) @pytest.mark.unit def test_ticker_news_asks_for_only_as_many_articles_as_configured(monkeypatch): """The endpoint returns 50 articles with per-article sentiment arrays by default, and the whole payload went into the prompt.""" from tradingagents.dataflows.vendors.alpha_vantage import news as alpha_vantage_news monkeypatch.setattr(alpha_vantage_news, "get_config", lambda: {"news_article_limit": 8}) seen = {} monkeypatch.setattr(alpha_vantage_news, "_make_api_request", lambda fn, params: seen.update(params) or "{}") alpha_vantage_news.get_news("AAPL", "2026-03-10", "2026-03-14") assert seen["limit"] == "8"tradingagents-multi-agent-firm/tests/test_analyst_execution.pycode · 1.3 KB
import unittest from tradingagents.graph.analyst_execution import ( build_analyst_execution_plan, ) class AnalystExecutionPlanTests(unittest.TestCase): def test_build_plan_preserves_selected_order(self): plan = build_analyst_execution_plan(["news", "market"]) self.assertEqual([spec.key for spec in plan.specs], ["news", "market"]) self.assertEqual(plan.specs[0].agent_node, "News Analyst") self.assertEqual(plan.specs[0].report_key, "news_report") self.assertFalse(hasattr(plan.specs[0], "clear_node")) def test_rejects_unknown_analyst_keys(self): with self.assertRaises(ValueError): build_analyst_execution_plan(["market", "macro"]) def test_social_key_displays_as_sentiment_analyst(self): # The wire key stays "social" for saved-config back-compat, but the # user-visible agent_node label must match the v0.2.5 rename so the # wall-time summary and any future consumer of agent_node says # "Sentiment Analyst" rather than the legacy "Social Analyst". plan = build_analyst_execution_plan(["social"]) spec = plan.specs[0] self.assertEqual(spec.key, "social") self.assertEqual(spec.agent_node, "Sentiment Analyst") self.assertEqual(spec.report_key, "sentiment_report")tradingagents-multi-agent-firm/tests/test_anthropic_effort.pycode · 3.8 KB
"""Tests for Anthropic effort-parameter gating (#831). Haiku (any version) and Sonnet 4.5 reject the ``effort`` parameter with a 400. Only Opus 4.5+ and Sonnet 4.6+ accept it. The gate uses a per-family minimum version so future ``claude-{opus,sonnet}-X-Y`` releases inherit support automatically. """ import pytest from tradingagents.llm_clients import anthropic_client as mod def _capture_kwargs(monkeypatch): captured: dict = {} monkeypatch.setattr( mod, "NormalizedChatAnthropic", lambda **kwargs: captured.setdefault("kwargs", kwargs), ) return captured @pytest.mark.unit class TestEffortGate: @pytest.mark.parametrize( "model", [ "claude-haiku-4-5", "claude-haiku-5-0", "claude-haiku-4-7-preview", # Sonnet 4.5 (and earlier) 400 on effort — only Sonnet 4.6+ supports it. "claude-sonnet-4-5", "claude-sonnet-4-0", ], ) def test_unsupported_models_do_not_receive_effort(self, monkeypatch, model): captured = _capture_kwargs(monkeypatch) mod.AnthropicClient(model=model, effort="medium", api_key="x").get_llm() assert "effort" not in captured["kwargs"] @pytest.mark.parametrize( "model", [ "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-sonnet-4-6", ], ) def test_current_opus_and_sonnet_receive_effort(self, monkeypatch, model): captured = _capture_kwargs(monkeypatch) mod.AnthropicClient(model=model, effort="high", api_key="x").get_llm() assert captured["kwargs"]["effort"] == "high" @pytest.mark.parametrize( "model", ["claude-opus-5-0", "claude-opus-4-8", "claude-sonnet-5-0"], ) def test_future_opus_sonnet_inherit_effort_via_pattern(self, monkeypatch, model): """Forward-compat: new Opus/Sonnet versions don't need a code change.""" captured = _capture_kwargs(monkeypatch) mod.AnthropicClient(model=model, effort="low", api_key="x").get_llm() assert captured["kwargs"]["effort"] == "low" @pytest.mark.parametrize( "model", # Claude 5 family uses single-number version IDs; all are effort-capable. ["claude-sonnet-5", "claude-fable-5", "claude-mythos-5", "claude-opus-5", "claude-opus-5-5", "claude-fable-5-1"], ) def test_claude_5_family_receives_effort(self, monkeypatch, model): captured = _capture_kwargs(monkeypatch) mod.AnthropicClient(model=model, effort="high", api_key="x").get_llm() assert captured["kwargs"]["effort"] == "high" def test_mythos_preview_receives_effort(self, monkeypatch): captured = _capture_kwargs(monkeypatch) mod.AnthropicClient( model="claude-mythos-preview", effort="medium", api_key="x" ).get_llm() assert captured["kwargs"]["effort"] == "medium" def test_unknown_anthropic_model_does_not_receive_effort(self, monkeypatch): """Default is conservative — unknown models don't get effort to avoid 400s.""" captured = _capture_kwargs(monkeypatch) mod.AnthropicClient( model="claude-experimental-x", effort="medium", api_key="x" ).get_llm() assert "effort" not in captured["kwargs"] def test_other_kwargs_still_forwarded_when_effort_skipped(self, monkeypatch): """Skipping effort must not break other passthrough kwargs.""" captured = _capture_kwargs(monkeypatch) mod.AnthropicClient( model="claude-haiku-4-5", effort="medium", api_key="placeholder", max_tokens=1024, timeout=30, ).get_llm() assert captured["kwargs"]["api_key"] == "placeholder" assert captured["kwargs"]["max_tokens"] == 1024 assert captured["kwargs"]["timeout"] == 30 assert "effort" not in captured["kwargs"]tradingagents-multi-agent-firm/tests/test_api_key_env.pycode · 7.0 KB
"""Tests for the canonical provider->env-var mapping and the CLI key-prompt helper.""" from __future__ import annotations import os import stat from unittest.mock import patch import pytest from tradingagents.llm_clients.api_key_env import PROVIDER_API_KEY_ENV, get_api_key_env # ---- Mapping coverage ----------------------------------------------------- def test_every_select_llm_provider_choice_has_an_entry(): """select_llm_provider() must not present a provider the mapping doesn't know about.""" # Mirrors the dropdown order in cli/prompts.select_llm_provider so the two # stay in lockstep. Region-specific keys (qwen-cn / minimax-cn / glm-cn) # are reached via the secondary region prompt, so they must also be present. expected = { "openai", "google", "anthropic", "xai", "deepseek", "qwen", "qwen-cn", "glm", "glm-cn", "minimax", "minimax-cn", "openrouter", "azure", "ollama", } assert expected.issubset(PROVIDER_API_KEY_ENV.keys()) @pytest.mark.parametrize( "provider,env_var", [ ("openai", "OPENAI_API_KEY"), ("anthropic", "ANTHROPIC_API_KEY"), ("google", "GOOGLE_API_KEY"), ("azure", "AZURE_OPENAI_API_KEY"), ("xai", "XAI_API_KEY"), ("deepseek", "DEEPSEEK_API_KEY"), ("qwen", "DASHSCOPE_API_KEY"), ("qwen-cn", "DASHSCOPE_CN_API_KEY"), ("glm", "ZHIPU_API_KEY"), ("glm-cn", "ZHIPU_CN_API_KEY"), ("minimax", "MINIMAX_API_KEY"), ("minimax-cn", "MINIMAX_CN_API_KEY"), ("openrouter", "OPENROUTER_API_KEY"), ], ) def test_known_providers_resolve(provider, env_var): assert get_api_key_env(provider) == env_var def test_ollama_has_no_key(): assert get_api_key_env("ollama") is None def test_unknown_provider_returns_none(): assert get_api_key_env("not-a-real-provider") is None def test_case_insensitive_lookup(): assert get_api_key_env("OpenAI") == "OPENAI_API_KEY" assert get_api_key_env("QWEN-CN") == "DASHSCOPE_CN_API_KEY" # ---- ensure_api_key behavior --------------------------------------------- @pytest.fixture def prompts(): """cli.prompts, which reads the environment when it is called.""" import cli.prompts as prompts_module return prompts_module def test_ensure_api_key_returns_existing(monkeypatch, prompts): monkeypatch.setenv("OPENAI_API_KEY", "sk-already-set") result = prompts.ensure_api_key("openai") assert result == "sk-already-set" def test_ensure_api_key_no_op_for_ollama(monkeypatch, prompts): # Even with no env var set, ollama should not prompt and should return None. monkeypatch.delenv("OPENAI_API_KEY", raising=False) with patch.object(prompts, "questionary") as mock_q: result = prompts.ensure_api_key("ollama") assert result is None mock_q.password.assert_not_called() def test_ensure_api_key_unknown_provider_no_prompt(monkeypatch, prompts): with patch.object(prompts, "questionary") as mock_q: result = prompts.ensure_api_key("totally-fake-provider") assert result is None mock_q.password.assert_not_called() def test_ensure_api_key_prompts_and_writes_to_env(monkeypatch, tmp_path, prompts): """When key is missing, user-pasted value must be written to .env AND os.environ.""" monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False) monkeypatch.chdir(tmp_path) fake_prompt = type("P", (), {"ask": staticmethod(lambda: "sk-deepseek-test")})() with patch.object(prompts.questionary, "password", return_value=fake_prompt): result = prompts.ensure_api_key("deepseek") assert result == "sk-deepseek-test" assert os.environ["DEEPSEEK_API_KEY"] == "sk-deepseek-test" env_file = tmp_path / ".env" assert env_file.exists() assert "DEEPSEEK_API_KEY" in env_file.read_text() assert "sk-deepseek-test" in env_file.read_text() def test_ensure_api_key_user_cancels_returns_none(monkeypatch, tmp_path, prompts): """Empty prompt response (user cancelled) must not write to .env.""" monkeypatch.delenv("XAI_API_KEY", raising=False) monkeypatch.chdir(tmp_path) fake_prompt = type("P", (), {"ask": staticmethod(lambda: None)})() with patch.object(prompts.questionary, "password", return_value=fake_prompt): result = prompts.ensure_api_key("xai") assert result is None assert "XAI_API_KEY" not in os.environ # .env may or may not exist depending on find_dotenv's walk, but if it # does it must not contain the key. env_file = tmp_path / ".env" if env_file.exists(): assert "XAI_API_KEY" not in env_file.read_text() def test_ensure_api_key_updates_existing_env_file(monkeypatch, tmp_path, prompts): """An existing .env with other keys must be preserved on writeback.""" monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) monkeypatch.chdir(tmp_path) env_file = tmp_path / ".env" env_file.write_text("OPENAI_API_KEY=sk-existing\nOTHER=value\n") fake_prompt = type("P", (), {"ask": staticmethod(lambda: "sk-openrouter-new")})() with patch.object(prompts.questionary, "password", return_value=fake_prompt): prompts.ensure_api_key("openrouter") content = env_file.read_text() assert "OPENAI_API_KEY" in content and "sk-existing" in content assert "OTHER=value" in content assert "OPENROUTER_API_KEY" in content and "sk-openrouter-new" in content def _prompt_key(prompts, monkeypatch, tmp_path, key="sk-typed-in"): monkeypatch.chdir(tmp_path) monkeypatch.delenv("OPENAI_API_KEY", raising=False) monkeypatch.setattr(prompts, "find_dotenv", lambda **k: "") with patch.object(prompts, "questionary") as mock_q: mock_q.password.return_value.ask.return_value = key prompts.ensure_api_key("openai") @pytest.mark.skipif(os.name == "nt", reason="POSIX file modes") def test_saved_key_file_is_owner_only(monkeypatch, prompts, tmp_path): # The prompt writes a real credential; the file must not be readable by # other local users whatever the umask is. old = os.umask(0o002) try: _prompt_key(prompts, monkeypatch, tmp_path) finally: os.umask(old) env = tmp_path / ".env" assert "sk-typed-in" in env.read_text() assert stat.S_IMODE(env.stat().st_mode) == 0o600 @pytest.mark.skipif(os.name == "nt", reason="POSIX file modes") def test_existing_key_file_is_tightened_before_writing(monkeypatch, prompts, tmp_path): env = tmp_path / ".env" env.write_text("OTHER=1\n") os.chmod(env, 0o664) _prompt_key(prompts, monkeypatch, tmp_path) assert stat.S_IMODE(env.stat().st_mode) == 0o600 assert "OTHER=1" in env.read_text() @pytest.mark.skipif(os.name == "nt", reason="POSIX file modes") def test_read_only_key_file_is_still_updated(monkeypatch, prompts, tmp_path): env = tmp_path / ".env" env.write_text("OTHER=1\n") os.chmod(env, 0o400) _prompt_key(prompts, monkeypatch, tmp_path) assert "sk-typed-in" in env.read_text() assert stat.S_IMODE(env.stat().st_mode) == 0o600tradingagents-multi-agent-firm/tests/test_backtest.pycode · 11.2 KB
"""Backtesting: many single-shot decisions, scored by the memory log. A run already records its rating and later settles it with realized and alpha return against the regional benchmark. A backtest is that machinery over a grid of tickers and dates, aggregated. It evaluates decision quality; it does not simulate a portfolio, so there is no execution, no fees and no equity curve. """ from __future__ import annotations import pytest from tradingagents.backtest import iter_grid, run_backtest, summarize from tradingagents.memory import TradingMemoryLog DECISION = "Rating: Buy\n\nbuy it" @pytest.mark.unit def test_grid_spacing_and_canonical_dates(): assert iter_grid("2026-01-05", "2026-01-20", every_n_days=7) == ["2026-01-05", "2026-01-12", "2026-01-19"] @pytest.mark.unit def test_grid_stops_at_today(monkeypatch): import tradingagents.backtest as bt monkeypatch.setattr(bt, "get_current_date", lambda: "2026-01-10") assert iter_grid("2026-01-05", "2026-02-20", every_n_days=5) == ["2026-01-05", "2026-01-10"] @pytest.mark.unit def test_grid_rejects_a_non_canonical_date(): with pytest.raises(ValueError, match="YYYY-MM-DD"): iter_grid("2026-1-5", "2026-01-20") class _FakeGraph: """Stands in for TradingAgentsGraph, writing to the log the harness gave it.""" instances: list = [] fail_on: set = set() def __init__(self, selected_analysts=None, config=None, **kw): self.analysts = list(selected_analysts) if selected_analysts else None self.config = config self.memory_log = TradingMemoryLog(config) self.calls = [] self.settled = [] _FakeGraph.instances.append(self) def propagate(self, ticker, trade_date, asset_type="stock", portfolio=None): self.calls.append((ticker, trade_date)) if (ticker, trade_date) in _FakeGraph.fail_on: raise RuntimeError("vendor exploded") self.memory_log.store_decision(ticker, trade_date, DECISION) return {"final_trade_decision": DECISION}, "Buy" def settle_pending(self, ticker): self.settled.append(ticker) @pytest.fixture(autouse=True) def _fake_graph(monkeypatch, tmp_path): import tradingagents.backtest as bt _FakeGraph.instances = [] _FakeGraph.fail_on = set() monkeypatch.setattr(bt, "TradingAgentsGraph", _FakeGraph) return _FakeGraph def _config(tmp_path): return {"results_dir": str(tmp_path / "results"), "memory_log_path": str(tmp_path / "live_trading_memory.md")} @pytest.mark.unit def test_the_live_memory_log_is_never_written(tmp_path): config = _config(tmp_path) result = run_backtest(["NVDA"], ["2026-01-05", "2026-01-12"], config) assert not (tmp_path / "live_trading_memory.md").exists() assert result.log_path.exists() and result.cells_run == 2 @pytest.mark.unit def test_a_cell_already_in_the_log_is_not_run_again(tmp_path): config = _config(tmp_path) first = run_backtest(["NVDA"], ["2026-01-05"], config) again = run_backtest(["NVDA"], ["2026-01-05", "2026-01-12"], config, run_id=first.run_id) assert again.cells_run == 1 and again.skipped == 1 assert _FakeGraph.instances[-1].calls == [("NVDA", "2026-01-12")] @pytest.mark.unit def test_every_ticker_is_settled_after_the_grid(tmp_path): """Settlement runs at the start of the next same-ticker run, so the last date of each ticker would stay pending without an explicit pass.""" run_backtest(["NVDA", "AAPL"], ["2026-01-05", "2026-01-12"], _config(tmp_path)) assert sorted(_FakeGraph.instances[-1].settled) == ["AAPL", "NVDA"] @pytest.mark.unit def test_a_failed_cell_does_not_abort_the_sweep(tmp_path): _FakeGraph.fail_on = {("NVDA", "2026-01-05")} result = run_backtest(["NVDA"], ["2026-01-05", "2026-01-12"], _config(tmp_path)) assert result.cells_run == 1 assert result.failures == [("NVDA", "2026-01-05", "vendor exploded")] # --- reading the result ------------------------------------------------------ def _log_with(tmp_path, rows): log = TradingMemoryLog({"memory_log_path": str(tmp_path / "m.md")}) for ticker, date, decision, outcome in rows: log.store_decision(ticker, date, decision) if outcome is not None: log.update_with_outcome(ticker, date, outcome[0], outcome[1], 5, "note", "2026-02-01") return tmp_path / "m.md" @pytest.mark.unit def test_summary_scores_resolved_cells_and_keeps_pending_out_of_the_average(tmp_path): log = _log_with(tmp_path, [ ("NVDA", "2026-01-05", "Rating: Buy\n\nx", (0.10, 0.04)), ("NVDA", "2026-01-12", "Rating: Buy\n\nx", (-0.02, -0.02)), ("AAPL", "2026-01-05", "Rating: Sell\n\nx", None), ]) summary = summarize(log) assert summary.resolved == 2 and summary.pending == 1 buys = summary.by_rating["Buy"] assert buys.count == 2 and buys.hit_rate == 0.5 and round(buys.mean_alpha, 4) == 0.01 assert "Sell" not in summary.by_rating # unsettled: nothing to score yet @pytest.mark.unit def test_summary_states_what_it_cannot_prove(tmp_path): text = summarize(_log_with(tmp_path, [("NVDA", "2026-01-05", DECISION, (0.1, 0.05))])).render() assert "not archived" in text assert "one" in text.lower() and "sampl" in text.lower() @pytest.mark.unit def test_the_analyst_set_under_test_is_the_one_that_runs(tmp_path): """A backtest of a two-analyst setup must not silently run four.""" run_backtest(["NVDA"], ["2026-01-05"], _config(tmp_path), selected_analysts=["market", "news"]) assert _FakeGraph.instances[-1].analysts == ["market", "news"] @pytest.mark.unit def test_a_run_id_cannot_escape_the_results_directory(tmp_path): """run_id becomes a path segment, so it is validated like a ticker is.""" with pytest.raises(ValueError): run_backtest(["NVDA"], ["2026-01-05"], _config(tmp_path), run_id="../../escaped") with pytest.raises(ValueError): run_backtest(["NVDA"], ["2026-01-05"], _config(tmp_path), run_id="/etc/cron.d/x") @pytest.mark.unit def test_a_failed_settlement_does_not_lose_the_remaining_tickers(tmp_path, monkeypatch): """Settlement reflects with an LLM, so it can fail; the sweep still returns its result and every other ticker still gets settled.""" settled = [] def _settle(self, ticker): if ticker == "NVDA": raise RuntimeError("reflector timed out") settled.append(ticker) monkeypatch.setattr(_FakeGraph, "settle_pending", _settle, raising=False) result = run_backtest(["NVDA", "AAPL"], ["2026-01-05"], _config(tmp_path)) assert result.cells_run == 2 assert settled == ["AAPL"] assert result.settlement_failures == [("NVDA", "reflector timed out")] @pytest.mark.unit def test_pending_note_appears_only_when_something_is_pending(tmp_path): settled = [("NVDA", "2026-01-05", DECISION, (0.1, 0.05))] assert "Pending" not in summarize(_log_with(tmp_path, settled)).render() assert "Pending" in summarize(_log_with(tmp_path, settled + [("AAPL", "2026-01-05", DECISION, None)])).render() # --- scoring reads the direction the rating claimed --------------------------- def _scored(tmp_path, rows): log = _log_with(tmp_path, rows) return summarize(log).by_rating @pytest.mark.unit def test_a_bearish_call_that_fell_counts_as_right(tmp_path): """Alpha below the benchmark is the outcome a Sell predicted; scoring it as a miss reported the system as wrong exactly when it was right.""" scores = _scored(tmp_path, [ ("NVDA", "2026-01-05", "**Rating**: Sell\n\nx", (-0.08, -0.05)), ("AAPL", "2026-01-05", "**Rating**: Underweight\n\nx", (-0.03, -0.02)), ]) assert scores["Sell"].hit_rate == 1.0 assert scores["Underweight"].hit_rate == 1.0 @pytest.mark.unit def test_a_bearish_call_that_rose_counts_as_wrong(tmp_path): scores = _scored(tmp_path, [("NVDA", "2026-01-05", "**Rating**: Sell\n\nx", (0.08, 0.05))]) assert scores["Sell"].hit_rate == 0.0 @pytest.mark.unit def test_a_bullish_call_is_scored_the_same_way_as_before(tmp_path): scores = _scored(tmp_path, [ ("NVDA", "2026-01-05", "**Rating**: Buy\n\nx", (0.10, 0.04)), ("AAPL", "2026-01-05", "**Rating**: Buy\n\nx", (-0.02, -0.02)), ]) assert scores["Buy"].hit_rate == 0.5 @pytest.mark.unit def test_hold_claims_no_direction_so_it_gets_no_hit_rate(tmp_path): scores = _scored(tmp_path, [("NVDA", "2026-01-05", "**Rating**: Hold\n\nx", (0.01, 0.005))]) assert scores["Hold"].hit_rate is None assert scores["Hold"].mean_alpha == 0.005 @pytest.mark.unit def test_the_report_names_the_window_the_scores_cover(tmp_path): text = summarize(_log_with(tmp_path, [ ("NVDA", "2026-01-05", "**Rating**: Buy\n\nx", (0.1, 0.05))])).render() assert "5" in text and "day" in text.lower() assert "Hold" not in text or "no direction" in text.lower() @pytest.mark.unit def test_the_window_reported_is_the_one_the_outcomes_used(tmp_path): """The log records the window each outcome was measured over; the summary must not claim a different one.""" log = TradingMemoryLog({"memory_log_path": str(tmp_path / "m.md")}) log.store_decision("NVDA", "2026-01-05", "**Rating**: Buy\n\nx") log.update_with_outcome("NVDA", "2026-01-05", 0.1, 0.04, 21, "note", "2026-02-01") assert "21 trading days" in summarize(tmp_path / "m.md").render() @pytest.mark.unit def test_a_backtest_result_is_summarized_directly(tmp_path): """The result names its own log, so a caller never builds the log to score it.""" from tradingagents.backtest import BacktestResult path = _log_with(tmp_path, [("NVDA", "2026-01-05", "Rating: Buy\n\nx", (0.10, 0.04))]) assert summarize(BacktestResult(run_id="r", log_path=path)).resolved == 1 @pytest.mark.unit def test_a_log_path_that_does_not_exist_is_an_error_not_an_empty_summary(tmp_path): missing = tmp_path / "no-such-dir" / "m.md" with pytest.raises(FileNotFoundError): summarize(missing) assert not missing.parent.exists() @pytest.mark.unit def test_a_result_whose_cells_all_failed_summarizes_as_empty(tmp_path): from tradingagents.backtest import BacktestResult result = BacktestResult(run_id="r", log_path=tmp_path / "never-written.md") assert summarize(result).resolved == 0 @pytest.mark.unit def test_progress_is_reported_before_each_cell(tmp_path): seen = [] run_backtest(["NVDA", "AAPL"], ["2026-01-05", "2026-01-12"], _config(tmp_path), progress=lambda done, total, ticker, date: seen.append((done, total, ticker, date))) assert seen == [(1, 4, "NVDA", "2026-01-05"), (2, 4, "NVDA", "2026-01-12"), (3, 4, "AAPL", "2026-01-05"), (4, 4, "AAPL", "2026-01-12")] @pytest.mark.unit def test_a_resumed_sweep_reports_only_the_cells_it_runs(tmp_path): first = run_backtest(["NVDA"], ["2026-01-05"], _config(tmp_path)) seen = [] run_backtest(["NVDA"], ["2026-01-05", "2026-01-12"], _config(tmp_path), run_id=first.run_id, progress=lambda done, total, ticker, date: seen.append((done, total, date))) assert seen == [(1, 1, "2026-01-12")] @pytest.mark.unit def test_a_ticker_or_date_given_twice_runs_and_settles_once(tmp_path): run_backtest(["NVDA", "NVDA"], ["2026-01-05", "2026-01-05"], _config(tmp_path)) graph = _FakeGraph.instances[-1] assert graph.calls == [("NVDA", "2026-01-05")] assert graph.settled == ["NVDA"]tradingagents-multi-agent-firm/tests/test_bedrock_provider.pycode · 3.0 KB
"""Amazon Bedrock — first-class native client via the optional langchain-aws extra. Auth uses the AWS credential chain (no single key env); the model is a Bedrock model ID / inference profile ID; langchain-aws is imported lazily with a clear install hint when the [bedrock] extra is absent. """ import sys import pytest from tradingagents.llm_clients.api_key_env import get_api_key_env from tradingagents.llm_clients.factory import create_llm_client from tradingagents.llm_clients.validators import validate_model @pytest.mark.unit def test_factory_routes_bedrock(): client = create_llm_client("bedrock", "us.anthropic.claude-opus-4-8-v1:0") assert type(client).__name__ == "BedrockClient" @pytest.mark.unit def test_bedrock_any_model_and_no_key_env(): assert validate_model("bedrock", "any.model-id:0") is True # Bedrock uses the AWS credential chain, so there is no single key env. assert get_api_key_env("bedrock") is None @pytest.mark.unit def test_helpful_error_when_langchain_aws_absent(monkeypatch): import tradingagents.llm_clients.bedrock_client as bc monkeypatch.setattr(bc, "_BEDROCK_CLASS", None) monkeypatch.setitem(sys.modules, "langchain_aws", None) # force ImportError on import with pytest.raises(ImportError, match=r"bedrock"): create_llm_client("bedrock", "m").get_llm() def _capture_kwargs(monkeypatch): """Stub _bedrock_class so the constructor kwargs are testable without the optional langchain-aws extra installed.""" import tradingagents.llm_clients.bedrock_client as bc captured = {} class _FakeChat: def __init__(self, **kwargs): captured.update(kwargs) monkeypatch.setattr(bc, "_bedrock_class", lambda: _FakeChat) return captured @pytest.mark.unit def test_bearer_token_passed_as_api_key(monkeypatch): # #1103: a Bedrock API key authenticates without AWS access keys. captured = _capture_kwargs(monkeypatch) monkeypatch.setenv("AWS_BEARER_TOKEN_BEDROCK", "bt-secret") monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") create_llm_client("bedrock", "us.anthropic.claude-opus-4-8-v1:0").get_llm() assert captured["api_key"] == "bt-secret" assert captured["region_name"] == "us-east-1" @pytest.mark.unit def test_no_bearer_token_omits_api_key(monkeypatch): # Without a token, fall back to the AWS credential chain (no api_key kwarg). captured = _capture_kwargs(monkeypatch) monkeypatch.delenv("AWS_BEARER_TOKEN_BEDROCK", raising=False) create_llm_client("bedrock", "us.anthropic.claude-opus-4-8-v1:0").get_llm() assert "api_key" not in captured @pytest.mark.unit def test_construction_when_extra_installed(monkeypatch): pytest.importorskip("langchain_aws") import tradingagents.llm_clients.bedrock_client as bc monkeypatch.setattr(bc, "_BEDROCK_CLASS", None) monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1") llm = create_llm_client("bedrock", "us.anthropic.claude-sonnet-5").get_llm() assert type(llm).__name__ == "NormalizedChatBedrockConverse" assert llm.region_name == "eu-west-1"tradingagents-multi-agent-firm/tests/test_cache_writes.pycode · 5.3 KB
"""Cache files are replaced whole: a reader never sees a half-written file, and concurrent writers (tool calls run in parallel) never share a temp file.""" import os from pathlib import Path import pandas as pd import pytest from tradingagents.dataflows import files from tradingagents.dataflows.vendors import sec_edgar from tradingagents.dataflows.vendors.yahoo import ohlcv def _recording_replace(monkeypatch): moves = [] real = os.replace def replace(src, dst): moves.append((Path(src), Path(dst))) real(src, dst) monkeypatch.setattr(os, "replace", replace) return moves @pytest.mark.unit def test_sec_edgar_writers_never_share_a_temp_file(monkeypatch, tmp_path): monkeypatch.setattr(sec_edgar, "get_config", lambda: {"data_cache_dir": str(tmp_path)}) monkeypatch.setattr(sec_edgar, "_fetch_json", lambda url: {"url": url}) moves = _recording_replace(monkeypatch) sec_edgar._cached_json("https://example/a", "facts.json") (tmp_path / "sec_edgar" / "facts.json").unlink() sec_edgar._cached_json("https://example/a", "facts.json") temps = [src for src, _ in moves] assert len(set(temps)) == 2 assert all(src.parent == dst.parent for src, dst in moves) @pytest.mark.unit def test_the_price_cache_is_replaced_whole(monkeypatch, tmp_path): frame = pd.DataFrame({"Date": pd.bdate_range("2026-01-02", periods=3), "Open": 1.0, "High": 1.0, "Low": 1.0, "Close": 1.0, "Volume": 1}) monkeypatch.setattr(ohlcv, "get_config", lambda: {"data_cache_dir": str(tmp_path)}) monkeypatch.setattr(ohlcv.yf, "Ticker", lambda s: type("T", (), {"history": lambda self, **k: frame.set_index("Date")})()) moves = _recording_replace(monkeypatch) ohlcv.load_ohlcv("AAPL", "2026-01-06") assert [dst.name for _, dst in moves] == ["AAPL-YFin-data.csv"] assert moves[0][0].parent == tmp_path and moves[0][0] != moves[0][1] assert not list(tmp_path.glob("*.tmp*")) @pytest.mark.unit def test_a_replaced_file_keeps_the_usual_permissions(tmp_path): from tradingagents.dataflows.files import replace_file plain = tmp_path / "plain.txt" plain.write_text("x") replaced = tmp_path / "replaced.txt" replace_file(replaced, lambda temp: Path(temp).write_text("x")) assert replaced.stat().st_mode & 0o777 == plain.stat().st_mode & 0o777 @pytest.mark.unit def test_a_file_held_open_elsewhere_keeps_its_old_content(monkeypatch, tmp_path): """On Windows a file another reader has open cannot be replaced; the cache write is skipped rather than failing the call that produced the data.""" from tradingagents.dataflows import files target = tmp_path / "cache.csv" target.write_text("old") def locked(src, dst): raise PermissionError("in use") monkeypatch.setattr(files.os, "replace", locked) files.replace_file(target, lambda temp: Path(temp).write_text("new")) assert target.read_text() == "old" assert list(tmp_path.iterdir()) == [target] def _fake_msvcrt(results): """msvcrt whose locking() raises the next error in ``results``, or succeeds on None.""" import types calls = [] def locking(fd, mode, nbytes): calls.append(mode) if mode in (1, 2) and results: # LK_NBLCK, LK_LOCK; unlocking always succeeds error = results.pop(0) if error is not None: raise error return types.SimpleNamespace(LK_LOCK=2, LK_NBLCK=1, LK_UNLCK=0, locking=locking), calls @pytest.mark.unit def test_on_windows_the_lock_is_waited_for_while_another_writer_holds_it(tmp_path, monkeypatch): import errno import sys fake, calls = _fake_msvcrt([OSError(errno.EDEADLOCK, "held"), None]) monkeypatch.setitem(sys.modules, "msvcrt", fake) with open(tmp_path / "log.md.lock", "a+b") as handle, files._hold_windows(handle): pass assert calls == [2, 2, 0] @pytest.mark.unit def test_on_windows_a_lock_that_cannot_be_taken_is_reported_not_retried_forever(tmp_path, monkeypatch): import errno import sys fake, _ = _fake_msvcrt([OSError(errno.EINVAL, "bad handle")] * 3) monkeypatch.setitem(sys.modules, "msvcrt", fake) with pytest.raises(OSError, match="bad handle"), open(tmp_path / "log.md.lock", "a+b") as handle, \ files._hold_windows(handle): pass @pytest.mark.unit def test_a_lock_held_elsewhere_is_reported_when_not_waiting(tmp_path): import threading path = tmp_path / "log.md" held, release = threading.Event(), threading.Event() def holder(): with files.locked(path): held.set() release.wait(5) worker = threading.Thread(target=holder) worker.start() held.wait(5) with files.locked(path, wait=False) as acquired: assert acquired is False release.set() worker.join() with files.locked(path, wait=False) as acquired: assert acquired is True @pytest.mark.unit def test_on_windows_a_held_lock_is_reported_when_not_waiting(tmp_path, monkeypatch): import errno import sys fake, calls = _fake_msvcrt([OSError(errno.EACCES, "held")]) monkeypatch.setitem(sys.modules, "msvcrt", fake) with open(tmp_path / "log.md.lock", "a+b") as handle, files._hold_windows(handle, wait=False) as acquired: assert acquired is False assert calls == [1] # one try, no unlock of a lock it never heldtradingagents-multi-agent-firm/tests/test_capabilities.pycode · 7.2 KB
"""Unit tests for the LLM capability table.""" from dataclasses import FrozenInstanceError import pytest from tradingagents.llm_clients.capabilities import ( get_capabilities, ) @pytest.mark.unit class TestExactIdMatches: def test_deepseek_chat_supports_tool_choice(self): caps = get_capabilities("deepseek-chat") assert caps.supports_tool_choice is True def test_deepseek_reasoner_rejects_tool_choice(self): caps = get_capabilities("deepseek-reasoner") assert caps.supports_tool_choice is False assert caps.requires_reasoning_content_roundtrip is True def test_deepseek_v4_flash_rejects_tool_choice(self): caps = get_capabilities("deepseek-v4-flash") assert caps.supports_tool_choice is False assert caps.requires_reasoning_content_roundtrip is True def test_deepseek_flash_alias_rejects_tool_choice(self): """``deepseek-flash`` is what the model picker offers for V4.1 Flash. It was falling through to _DEFAULT (tool_choice on), so every structured-output call 400'd with "Thinking mode does not support this tool_choice" and burned a retry as free text. """ caps = get_capabilities("deepseek-flash") assert caps.supports_tool_choice is False assert caps.requires_reasoning_content_roundtrip is True def test_deepseek_v4_pro_rejects_tool_choice(self): caps = get_capabilities("deepseek-v4-pro") assert caps.supports_tool_choice is False assert caps.requires_reasoning_content_roundtrip is True @pytest.mark.unit class TestPatternMatches: """Forward-compat regex patterns catch unknown DeepSeek and MiniMax variants.""" def test_future_deepseek_v5_inherits_thinking_quirks(self): caps = get_capabilities("deepseek-v5-flash") assert caps.supports_tool_choice is False assert caps.requires_reasoning_content_roundtrip is True def test_future_deepseek_v9_inherits_thinking_quirks(self): caps = get_capabilities("deepseek-v9-anything") assert caps.supports_tool_choice is False def test_reasoner_variant_inherits_thinking_quirks(self): caps = get_capabilities("deepseek-reasoner-pro") assert caps.supports_tool_choice is False def test_future_flash_variant_inherits_thinking_quirks(self): caps = get_capabilities("deepseek-flash-lite") assert caps.supports_tool_choice is False def test_minimax_m3_inherits_thinking_quirks(self): caps = get_capabilities("MiniMax-M3") assert caps.supports_tool_choice is False def test_future_minimax_m4_highspeed_inherits_thinking_quirks(self): caps = get_capabilities("MiniMax-M4-highspeed") assert caps.supports_tool_choice is False @pytest.mark.unit class TestMinimaxExactMatches: """MiniMax M2.x models reject langchain's function-spec dict tool_choice (official API enum: none/auto only).""" def test_m2_7_rejects_tool_choice(self): caps = get_capabilities("MiniMax-M2.7") assert caps.supports_tool_choice is False assert caps.supports_json_mode is False # only MiniMax-Text-01 supports json_object def test_m2_7_highspeed_rejects_tool_choice(self): assert get_capabilities("MiniMax-M2.7-highspeed").supports_tool_choice is False def test_m2_1_rejects_tool_choice(self): assert get_capabilities("MiniMax-M2.1").supports_tool_choice is False def test_m2_base_rejects_tool_choice(self): assert get_capabilities("MiniMax-M2").supports_tool_choice is False def test_m2_x_requires_reasoning_split(self): # M2.x reasoning models need reasoning_split=True so <think> blocks # land in reasoning_details instead of content (#826). for model in ("MiniMax-M2.7", "MiniMax-M2.5-highspeed", "MiniMax-M2"): assert get_capabilities(model).requires_reasoning_split is True def test_future_m3_inherits_reasoning_split(self): assert get_capabilities("MiniMax-M3-highspeed").requires_reasoning_split is True def test_non_reasoning_minimax_does_not_get_reasoning_split(self): # Coding Plan, MiniMax-Text-01, and any non-M2-prefixed MiniMax model # reject the reasoning_split kwarg via the openai SDK's strict # validation (#826). Default capability has it disabled. for model in ("minimax-text-01", "MiniMax-Coding-Plan", "abab6.5-chat"): assert get_capabilities(model).requires_reasoning_split is False @pytest.mark.unit class TestDefault: """Unknown / non-DeepSeek models get the permissive default.""" def test_gpt_default(self): caps = get_capabilities("gpt-4.1") assert caps.supports_tool_choice is True assert caps.preferred_structured_method == "function_calling" def test_grok_default(self): caps = get_capabilities("grok-4-0709") assert caps.supports_tool_choice is True def test_unknown_model_default(self): caps = get_capabilities("totally-made-up-model-id") assert caps.supports_tool_choice is True def test_exact_match_precedes_pattern(self): """deepseek-chat must NOT match the v\\d regex.""" caps = get_capabilities("deepseek-chat") assert caps.supports_tool_choice is True @pytest.mark.unit class TestOpenRouterDeepSeekNamespace: """OpenRouter namespaces DeepSeek as ``deepseek/<id>``; strip it so the same quirks apply as the native provider (#1199).""" def test_prefixed_v4_flash_suppresses_tool_choice(self): # Was falling through to _DEFAULT (tool_choice on) -> slow object-form call. assert get_capabilities("deepseek/deepseek-v4-flash").supports_tool_choice is False def test_prefixed_reasoner_suppresses_tool_choice(self): assert get_capabilities("deepseek/deepseek-reasoner").supports_tool_choice is False def test_prefixed_flash_alias_suppresses_tool_choice(self): assert get_capabilities("deepseek/deepseek-flash").supports_tool_choice is False def test_prefixed_chat_selects_deepseek_chat_not_default(self): # Must resolve to _DEEPSEEK_CHAT, not _DEFAULT: supports_json_schema=False # is what distinguishes them (both keep tool_choice). caps = get_capabilities("deepseek/deepseek-chat") assert caps.supports_tool_choice is True assert caps.supports_json_schema is False # _DEEPSEEK_CHAT, not _DEFAULT def test_only_official_namespace_is_stripped(self): # A third-party publisher whose model name WOULD match a deepseek pattern # must stay _DEFAULT: proves we strip only "deepseek/", not any "*/". caps = get_capabilities("tngtech/deepseek-v4-flash") assert caps.supports_tool_choice is True # not thinking assert caps.supports_json_schema is True # _DEFAULT def test_native_ids_unchanged(self): assert get_capabilities("deepseek-v4-flash").supports_tool_choice is False assert get_capabilities("deepseek-chat").supports_tool_choice is True @pytest.mark.unit def test_capabilities_dataclass_is_frozen(): """Capability rows are immutable so they can be safely shared.""" caps = get_capabilities("deepseek-chat") with pytest.raises(FrozenInstanceError): caps.supports_tool_choice = False # type: ignore[misc]tradingagents-multi-agent-firm/tests/test_chatgpt_auth.pycode · 17.1 KB · preview truncated
"""Sign in with ChatGPT: the sign-in is checked, kept private, renewed once, and revoked. OpenAI is replaced by a fake that signs ID tokens with a key made here, so every check of the sign-in (signature, issuer, audience, nonce, plan scope) runs for real. """ import base64 import json import socket import stat import threading import time import urllib.error import urllib.parse import urllib.request import pytest from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding, rsa from tradingagents.llm_clients import chatgpt_auth as auth # The suite refuses all connections; the loopback test lets 127.0.0.1 through only. _CONNECT, _GETADDRINFO = socket.socket.connect, socket.getaddrinfo KEY = rsa.generate_private_key(public_exponent=65537, key_size=2048) CLIENT = "oaiapp_test" SUBJECT = "user-123" def _b64(raw: bytes) -> str: return base64.urlsafe_b64encode(raw).rstrip(b"=").decode() def _id_token(key=KEY, **claims) -> str: payload = {"iss": auth.ISSUER, "aud": CLIENT, "sub": SUBJECT, "email": "a@example.com", "exp": time.time() + 3600, **claims} head = _b64(json.dumps({"alg": "RS256", "kid": "k1"}).encode()) body = _b64(json.dumps(payload).encode()) signature = key.sign(f"{head}.{body}".encode(), padding.PKCS1v15(), hashes.SHA256()) return f"{head}.{body}.{_b64(signature)}" def _jwks(): numbers = KEY.public_key().public_numbers() return {"keys": [{"kid": "k1", "kty": "RSA", "e": _b64(numbers.e.to_bytes(3, "big")), "n": _b64(numbers.n.to_bytes(256, "big"))}]} class _Response: def __init__(self, body, status=200): self.body, self.status_code = body, status def json(self): return self.body class FakeOpenAI: """Answers the JWKS, token and revoke endpoints and records what was sent.""" def __init__(self, token_body=None, status=200): self.posts = [] self.token_body = token_body self.status = status self.lock = threading.Lock() self.keys_reachable = True self.revoke_status = 200 self.post_error = None def get(self, url, timeout): assert url == auth.JWKS_URL if not self.keys_reachable: raise auth.requests.ConnectionError("down") return _Response(_jwks()) def post(self, url, timeout, data): with self.lock: self.posts.append((url, data)) if url == auth.REVOKE_URL: return _Response({}, self.revoke_status) if self.post_error is not None: raise self.post_error time.sleep(0.05) # let a racing refresher reach the lock body = self.token_body(data) if callable(self.token_body) else self.token_body return _Response(body, self.status) @pytest.fixture def home(tmp_path, monkeypatch): monkeypatch.setattr(auth, "AUTH_FILE", tmp_path / "home" / "chatgpt-auth.json") monkeypatch.setattr(auth, "_retry_after", 0.0) return tmp_path @pytest.fixture def openai(monkeypatch): fake = FakeOpenAI() monkeypatch.setattr(auth.requests, "get", fake.get) monkeypatch.setattr(auth.requests, "post", fake.post) return fake def _signed_in(expires_at, earliest=None, refresh_token="r1"): auth.AUTH_FILE.parent.mkdir(parents=True, exist_ok=True) auth._write({"host_id": "urn:uuid:h", "account": {"client_id": CLIENT, "email": "a@example.com", "subject": SUBJECT}, "tokens": {"access_token": "a1", "refresh_token": refresh_token, "expires_at": expires_at, "earliest_refresh_at": earliest}}) def _renewal(data): return {"access_token": "a2", "refresh_token": "r2", "expires_in": 3600, "id_token": _id_token(), "scope": auth.SCOPE} # --- the ID token ---------------------------------------------------------------- @pytest.mark.unit def test_a_genuine_id_token_is_accepted(): assert auth._validated_claims(_id_token(nonce="n"), CLIENT, "n", _jwks()["keys"])["sub"] == SUBJECT @pytest.mark.unit @pytest.mark.parametrize("token, nonce", [ (lambda: _id_token(nonce="other"), "n"), (lambda: _id_token(aud="oaiapp_someone_else", nonce="n"), "n"), (lambda: _id_token(iss="https://evil.example", nonce="n"), "n"), (lambda: _id_token(exp=time.time() - 600, nonce="n"), "n"), ]) def test_an_id_token_for_another_sign_in_is_refused(token, nonce): with pytest.raises(auth.ChatGPTAuthError): auth._validated_claims(token(), CLIENT, nonce, _jwks()["keys"]) @pytest.mark.unit def test_an_id_token_signed_by_another_key_is_refused(): stranger = rsa.generate_private_key(public_exponent=65537, key_size=2048) with pytest.raises(auth.ChatGPTAuthError, match="do not verify"): auth._validated_claims(_id_token(key=stranger, nonce="n"), CLIENT, "n", _jwks()["keys"]) # --- tokens ---------------------------------------------------------------------- @pytest.mark.unit def test_nobody_signed_in_is_told_how_to_sign_in(home, openai): with pytest.raises(auth.ChatGPTAuthError, match="tradingagents login chatgpt"): auth.credential() with pytest.raises(auth.ChatGPTAuthError, match="tradingagents login chatgpt"): auth.access_token() assert openai.posts == [] @pytest.mark.unit def test_a_fresh_token_is_used_without_asking_openai(home, openai): _signed_in(expires_at=time.time() + 3000) assert auth.credential()() == "a1" assert openai.posts == [] @pytest.mark.unit def test_a_token_near_expiry_is_renewed_with_the_stored_refresh_token(home, openai): _signed_in(expires_at=time.time() + 60) openai.token_body = _renewal assert auth.access_token() == "a2" (url, sent), = openai.posts assert url == auth.TOKEN_URL and sent["refresh_token"] == "r1" and sent["client_id"] == CLIENT stored = json.loads(auth.AUTH_FILE.read_text()) assert stored["tokens"]["refresh_token"] == "r2" and stored["account"]["subject"] == SUBJECT assert stat.S_IMODE(auth.AUTH_FILE.stat().st_mode) == 0o600 @pytest.mark.unit def test_renewal_waits_for_the_time_openai_allows_while_the_token_still_works(home, openai): now = time.time() _signed_in(expires_at=now + 200, earliest=now + 100) assert auth.access_token() == "a1" assert openai.posts == [] @pytest.mark.unit def test_concurrent_runs_renew_once_and_never_replay_a_used_refresh_token(home, openai): _signed_in(expires_at=time.time() + 60) openai.token_body = _renewal tokens = [] threads = [threading.Thread(target=lambda: tokens.append(auth.access_token())) for _ in range(4)] for thread in threads: thread.start() for thread in threads: thread.join() assert tokens == ["a2"] * 4 assert [sent["refresh_token"] for _, sent in openai.posts] == ["r1"] @pytest.mark.unit def test_a_refused_renewal_asks_to_sign_in_again_and_keeps_the_file(home, openai): _signed_in(expires_at=time.time() + 60) openai.token_body, openai.status = {"error": "invalid_grant"}, 400 with pytest.raises(auth.ChatGPTAuthError, match="invalid_grant.*login chatgpt"): auth.access_token() assert json.loads(auth.AUTH_FILE.read_text())["tokens"]["refresh_token"] == "r1" @pytest.mark.unit def test_a_renewal_for_another_account_is_refused(home, openai): _signed_in(expires_at=time.time() + 60) openai.token_body = lambda data: {**_renewal(data), "id_token": _id_token(sub="someone-else")} with pytest.raises(auth.ChatGPTAuthError, match="another account"): auth.access_token() # --- signing in and out ------------------------------------------------------------ def _nonce_of(printed): url = next(line for line in printed if line.startswith(auth.AUTHORIZE_URL)) return dict(urllib.parse.parse_qsl(urllib.parse.urlparse(url).query))["nonce"] def _paster(printed, edit=lambda query: query): calls = [] def read_line(): calls.append(1) if len(calls) > 1: threading.Event().wait() url = next(line for line in printed if line.startswith(auth.AUTHORIZE_URL)) asked = dict(urllib.parse.parse_qsl(urllib.parse.urlparse(url).query)) query = edit({"code": "c1", "state": asked["state"], "client_id": CLIENT}) return f"{asked['redirect_uri']}?{urllib.parse.urlencode(query)}" return read_line @pytest.mark.unit def test_signing_in_by_pasting_the_redirect_saves_a_private_sign_in(home, openai): printed = [] def token(data): assert data["grant_type"] == "authorization_code" and data["client_id"] == CLIENT assert data["code"] == "c1" and data["code_verifier"] return {"access_token": "a1", "refresh_token": "r1", "expires_in": 3600, "id_token": _id_token(nonce=_nonce_of(printed)), "scope": auth.SCOPE} openai.token_body = token email = auth.sign_in(say=printed.append, read_line=_paster(printed), timeout=10, port=0) assert email == "a@example.com" stored = json.loads(auth.AUTH_FILE.read_text()) assert stored["account"] == {"client_id": CLIENT, "email": "a@example.com", "subject": SUBJECT} assert stored["host_id"].startswith("urn:uuid:") assert stat.S_IMODE(auth.AUTH_FILE.stat().st_mode) == 0o600 asked = dict(urllib.parse.parse_qsl(urllib.parse.urlparse(printed[1]).query)) assert asked["client_id"] == auth.DYNAMIC_CLIENT_ID and asked["agent_name_hint"] == "TradingAgents" assert asked["code_challenge_method"] == "S256" and asked["redirect_uri"].startswith("http://127.0.0.1:") @pytest.mark.unit def test_an_answer_for_another_sign_in_attempt_is_ignored(home, openai): printed = [] with pytest.raises(auth.ChatGPTAuthError, match="timed out"): auth.sign_in(say=printed.append, read_line=_paster(printed, lambda q: {**q, "state": "forged"}), timeout=1, port=0) assert any("not the address" in line for line in printed) assert openai.posts == [] and not auth.AUTH_FILE.exists() def _status(url): try: return urllib.request.urlopen(url, timeout=5).status except urllib.error.HTTPError as exc: return exc.code @pytest.mark.unit def test_the_browser_answer_reaches_the_loopback_and_strays_are_ignored(home, openai, monkeypatch): refused_connect, refused_lookup = socket.socket.connect, socket.getaddrinfo monkeypatch.setattr(socket.socket, "connect", lambda sock, address: ( _CONNECT if address[0] == "127.0.0.1" else refused_connect)(sock, address)) monkeypatch.setattr(socket, "getaddrinfo", lambda host, *a, **k: ( _GETADDRINFO if host == "127.0.0.1" else refused_lookup)(host, *a, **k)) printed = [] openai.token_body = lambda data: {"access_token": "a1", "refresh_token": "r1", "expires_in": 3600, "id_token": _id_token(nonce=_nonce_of(printed)), "scope": auth.SCOPE} def browser(): while not any(line.startswith(auth.AUTHORIZE_URL) for line in printed): time.sleep(0.01) asked = dict(urllib.parse.parse_qsl(urllib.parse.urlparse(printed[1]).query)) results.append(_status(f"{asked['redirect_uri']}?error=access_denied")) results.append(_status(f"{asked['redirect_uri']}?code=c1&state={asked['state']}&client_id={CLIENT}")) results = [] threading.Thread(target=browser, daemon=True).start() email = auth.sign_in(say=printed.append, read_line=threading.Event().wait, timeout=10, port=0) assert email == "a@example.com" and results == [400, 200] @pytest.mark.unit def test_a_sign_in_without_plan_usage_is_refused(home, openai): printed = [] openai.token_body = lambda data: {"access_token": "a1", "refresh_token": "r1", "expires_in": 3600, "id_token": _id_token(nonce=_nonce_of(printed)), "scope": "openid email offline_access"} with pytest.raises(auth.ChatGPTAuthError, matradingagents-multi-agent-firm/tests/test_chatgpt_provider.pycode · 11.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_checkpoint_lifecycle.pycode · 5.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_checkpoint_resume.pycode · 10.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_commands.pycode · 8.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_config_precedence.pycode · 5.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_display.pycode · 6.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_env_skip.pycode · 7.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_headless.pycode · 14.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_memory_log.pycode · 7.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_no_console.pycode · 2.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_prefs.pycode · 6.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_symbol_handling.pycode · 2.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_cli_updates.pycode · 15.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_crypto_asset_mode.pycode · 1.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_dataflows_config.pycode · 7.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_date_boundaries.pycode · 2.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_debate_opening.pycode · 4.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_deepseek_reasoning.pycode · 9.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_env_overrides.pycode · 5.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_fred.pycode · 13.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_fundamentals_lookahead.pycode · 5.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_fundamentals_units.pycode · 3.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_get_scrubbed.pycode · 2.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_google_api_key.pycode · 1.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_google_thinking_level.pycode · 2.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_graph_end_to_end.pycode · 17.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_i18n_coverage.pycode · 2.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_instrument_identity.pycode · 5.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_layering.pycode · 1.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_llm_max_retries.pycode · 3.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_llm_max_tokens.pycode · 4.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_memory_log.pycode · 58.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_memory_pointintime.pycode · 3.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_minimax.pycode · 2.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_model_validation.pycode · 3.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_news_analyst_prompt.pycode · 0.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_news_lookahead.pycode · 17.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_no_data_handling.pycode · 4.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_ohlcv_cache_freshness.pycode · 4.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_ohlcv_date_column.pycode · 3.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_ohlcv_latest_bar.pycode · 8.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_ollama_base_url.pycode · 6.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_openai_compatible_provider.pycode · 4.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_openai_reasoning_effort.pycode · 1.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_openai_responses_base_url.pycode · 1.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_openrouter_model_select.pycode · 5.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_polymarket.pycode · 5.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_portfolio_context.pycode · 9.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_post_screen.pycode · 10.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_prompt_integrity.pycode · 3.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_provider_registry.pycode · 5.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_rating_integrity.pycode · 13.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_reddit_fallback.pycode · 14.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_report_html.pycode · 8.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_reporting.pycode · 5.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_risk_router_path_map.pycode · 3.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_safe_ticker_component.pycode · 2.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_sec_edgar.pycode · 14.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_signal_processing.pycode · 3.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_social_lookahead.pycode · 9.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_statement_knowledge_time.pycode · 3.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_stocktwits_resilience.pycode · 3.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_structured_agent_prompts.pycode · 6.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_structured_agents.pycode · 22.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_suite_isolation.pycode · 2.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_symbol_normalization_paths.pycode · 3.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_symbols.pycode · 4.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_temperature_config.pycode · 2.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_ticker_symbol_handling.pycode · 0.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_tier_providers.pycode · 3.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_tool_date_enforcement.pycode · 6.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_undated_tools_as_of.pycode · 11.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_vendor_errors.pycode · 4.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_vendor_routing.pycode · 5.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_version.pycode · 0.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_yahoo_rate_limit.pycode · 10.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_yahoo_snapshot.pycode · 3.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tests/test_yfinance_stale_ohlcv_guard.pycode · 3.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/__init__.pycode · 0.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/__init__.pycode · 1.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/analysts/__init__.pycode · 0.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/analysts/fundamentals_analyst.pycode · 3.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/analysts/market_analyst.pycode · 6.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/analysts/news_analyst.pycode · 3.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/analysts/sentiment_analyst.pycode · 9.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/analysts/turn.pycode · 2.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/context.pycode · 8.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/managers/__init__.pycode · 0.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/managers/portfolio_manager.pycode · 4.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/managers/research_manager.pycode · 3.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/post_screen.pycode · 7.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/rating.pycode · 5.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/researchers/__init__.pycode · 0.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/researchers/bear_researcher.pycode · 3.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/researchers/bull_researcher.pycode · 3.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/risk_mgmt/__init__.pycode · 0.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/risk_mgmt/aggressive_debator.pycode · 4.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/risk_mgmt/conservative_debator.pycode · 4.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/risk_mgmt/neutral_debator.pycode · 4.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/schemas.pycode · 14.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/state.pycode · 3.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/structured.pycode · 3.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/tools.pycode · 11.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/trader/__init__.pycode · 0.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/agents/trader/trader.pycode · 4.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/assets/tauric-logo.svgtext · 16.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/backtest.pycode · 9.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/__init__.pycode · 0.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/config.pycode · 2.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/date_window.pycode · 7.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/errors.pycode · 2.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/files.pycode · 2.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/net.pycode · 1.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/router.pycode · 11.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/symbols.pycode · 7.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/__init__.pycode · 0.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/alpha_vantage/__init__.pycode · 0.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/alpha_vantage/common.pycode · 6.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/alpha_vantage/fundamentals.pycode · 2.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/alpha_vantage/indicator.pycode · 10.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/alpha_vantage/news.pycode · 3.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/alpha_vantage/stock.pycode · 1.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/fred.pycode · 10.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/polymarket.pycode · 5.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/reddit.pycode · 14.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/sec_edgar.pycode · 12.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/stocktwits.pycode · 6.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/yahoo/__init__.pycode · 0.1 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/yahoo/common.pycode · 3.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/yahoo/fundamentals.pycode · 7.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/yahoo/market.pycode · 12.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/yahoo/news.pycode · 8.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/yahoo/ohlcv.pycode · 10.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/dataflows/vendors/yahoo/snapshot.pycode · 5.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/default_config.pycode · 10.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/graph/__init__.pycode · 0.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/graph/analyst_execution.pycode · 1.7 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/graph/checkpointer.pycode · 3.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/graph/conditional_logic.pycode · 1.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/graph/propagation.pycode · 2.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/graph/setup.pycode · 7.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/graph/trading_graph.pycode · 23.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/__init__.pycode · 0.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/anthropic_client.pycode · 2.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/api_key_env.pycode · 2.3 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/azure_client.pycode · 1.6 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/base_client.pycode · 2.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/bedrock_client.pycode · 3.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/capabilities.pycode · 5.8 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/chatgpt_auth.pycode · 18.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/factory.pycode · 5.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/google_client.pycode · 3.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/model_catalog.pycode · 11.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/openai_client.pycode · 19.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/llm_clients/validators.pycode · 1.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/memory/__init__.pycode · 0.4 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/memory/log.pycode · 14.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/memory/reflection.pycode · 3.0 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/memory/settlement.pycode · 8.2 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/portfolio.pycode · 2.9 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/report_html.pycode · 14.5 KB
No inline preview for this entry.
tradingagents-multi-agent-firm/tradingagents/reporting.pycode · 5.6 KB
No inline preview for this entry.