TalentDelta

Our site is fully accessible by humans and agents (via WebMCP).

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

Provenance

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

Our review

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.yml
    
  • tradingagents-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 API
    
  • tradingagents-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=high
    
  • tradingagents-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: text
    
  • tradingagents-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 issue
    
  • tradingagents-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: true
    
  • tradingagents-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 considered
    
  • tradingagents-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 chatgpt provider 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 with tradingagents login chatgpt. tradingagents logout chatgpt revokes 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=1 turns it off.
    • Release downloads. Each GitHub release carries the source archive and a wheel, so a version installs with pip install from its release page without cloning.

    Upgrading from 0.6.0

    • cryptography 42 and packaging 24 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_id mixes 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-html or save_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_provider or quick_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 and settle_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_10y and eur_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, or html=False to save_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 a Settlement with settled and failed (was None).
    • 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_URL names, 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.txt says the dependencies are declared in pyproject.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_DIR keeps results, reports and the memory log in a host folder. (#865)
    • A cap on an analyst's tool calls. After max_tool_rounds rounds (default 20, TRADINGAGENTS_MAX_TOOL_ROUNDS) the analyst writes its report from what it has. (#1420)
    • tradingagents backtest shows each cell as it starts and the --run-id to continue; --analysts accepts sentiment.
    • Benchmarks for Taiwan, Korea, Singapore and the main European exchanges. (#1392)
    • Python 3.14 support; the Docker image runs Python 3.13.

    Models

    • deepseek-flash is 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 (bedrock extra).
    • 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.memory package (was tradingagents.decision_log); settlement and reflection moved there from tradingagents.graph, and Reflector is no longer exported from tradingagents.graph.
    • process_signal is removed; the rating is propagate()'s second value or final_state["final_rating"].
    • A rating is read only from its Rating: label; a free-text decision without one is REVIEW.
    • Data-layer functions take as_of_date= (was curr_date=); build_instrument_context and resolve_instrument_context take trade_date=.
    • VendorRateLimitError is VendorUnavailableError.
    • Data tools take the instrument from the run's state; tool calls no longer carry symbol or ticker.
    • State log: trader_investment_plan (was trader_investment_decision), judge_decision removed, final_rating and run_settings added.
    • 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_run serves 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.interface is tradingagents.dataflows.router, and dataflows.symbol_utils is dataflows.symbols. dataflows.utils is gone: get_current_date is in dataflows.date_window, safe_ticker_component in dataflows.symbols, and get_scrubbed and vendor_reachable in dataflows.net.
    • Vendor modules live under tradingagents.dataflows.vendors: yahoo (ohlcv, market, fundamentals, news, snapshot, from the former stockstats_utils, y_finance, yfinance_news and market_data_validator), alpha_vantage (a package, from the alpha_vantage_* modules), and sec_edgar, fred, polymarket, reddit, stocktwits.
    • tradingagents.agents.utils is gone: the agent tools are in agents.tools, and agent_utils, agent_states, rating and structured are agents.context, agents.state, agents.rating and agents.structured.
    • The decision log is tradingagents.decision_log (was agents.utils.memory), and cli.utils is cli.prompts.
    • backtest.summarize takes a run_backtest result or the path of a decision log, in place of a TradingMemoryLog.
    • Removed: SignalProcessor (the rating is parsed by process_signal), the create_social_media_analyst alias (use create_sentiment_analyst), symbol_utils.is_yahoo_safe, the unused project_dir config key, and the graph attributes curr_state, ticker and log_states_dict, which held the previous run's state.

    Added

    • Jev post screening. With TYPESAFE_API_KEY set, 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_DIR and 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_backtest runs the pipeline over a ticker and date grid into its own decision log, and summarize scores the settled cells; tradingagents backtest does the same from the CLI.
    • Portfolio context. propagate(..., portfolio=...) and --portfolio let 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-id to 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 REVIEW for a decision with no readable rating, where it previously recorded Hold.
    • 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_context returned 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 REVIEW sentinel, with parse_rating keeping its silent default for compatibility callers. (#1170)
    • --checkpoint was a no-op on the CLI. Checkpoint setup lived only in propagate(); the CLI streamed the checkpointer-less graph. The lifecycle is now shared, and a resume feeds None so LangGraph continues the interrupted run instead of duplicating messages. (#1249)
    • DeepSeek via OpenRouter. deepseek/<id> fell through to default capabilities and had object-form tool_choice forced 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 as max_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 now gpt-5.6 (deep) and gpt-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's BTC-USD 404s) 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_RETRIES is forwarded to every provider, so a transient 429 burst no longer aborts a run. (#1091, @yanggaome)
    • Bedrock API-key auth. AWS_BEARER_TOKEN_BEDROCK authenticates Amazon Bedrock without AWS access keys and takes precedence over an ambient AWS_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_compatible endpoint 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, and TRADINGAGENTS_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 VendorError taxonomy; 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 to None. (#1038, #1051, #1057)

    Removed

    • The no-op analyst_concurrency_limit config 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_analyst now 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 for DEFAULT_CONFIG. Override llm_provider, deep/quick model IDs, backend_url, output_language, debate-round counts, checkpoint flag, and benchmark ticker via .env with 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 .env so the analysis run continues without restart.
    • Remote Ollama support. OLLAMA_BASE_URL points the CLI and the programmatic client at a remote ollama-serve. The CLI surfaces the resolved endpoint and warns on common malformed inputs. Adds a "Custom model ID" option for models pulled via ollama 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); explicit benchmark_ticker override 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_choice per their tool-calling docs; the binding flow now skips it automatically via a capability table.
    • pip install . installations pick up the project .env when 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_config partial updates preserve sibling defaults. (#788)
    • max_recur_limit config 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 LangChainPendingDeprecationWarning from 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_schema for OpenAI / xAI, response_schema for 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-checkpoints resets 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 with TRADINGAGENTS_MEMORY_LOG_PATH. Optional memory_log_max_entries config 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_url default is now None rather 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 overriding backend_url. The CLI flow is unaffected.
    • All file I/O passes explicit encoding="utf-8" so Windows users no longer hit UnicodeEncodeError with the cp1252 default. (#543, #550, #576)
    • Cache and log directories moved to ~/.tradingagents/ to resolve Docker permission issues. (#519)
    • SignalProcessor reads 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 noisy PydanticSerializationUnexpectedValue warnings 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 dead reflect_and_remember() plumbing; subsumed by the persistent decision log.
    • Hardcoded Google endpoint that caused 404 when langchain-google-genai changed 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_url is forwarded to Google and Anthropic clients so corporate proxies work consistently across providers. (#427)
    • Standardised the Google api_key parameter to the unified api_key form.

    Fixed

    • Backtesting fetchers no longer leak look-ahead data when curr_date is 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

    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_manager renamed to portfolio_manager to 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

    0.2.1 — 2026-03-15

    Security

    • Patched langchain-core vulnerability (LangGrinch). (#335)
    • Removed chainlit dependency affected by CVE-2026-22218.

    Added

    • pyproject.toml build-system configuration; the project now installs via modern packaging tooling.

    Removed

    • setup.py — dependencies consolidated to pyproject.toml.

    Fixed

    • Risk manager reads the correct fundamental report source. (#341)
    • All open() calls receive an explicit UTF-8 encoding (initial pass).
    • get_indicators tool handles comma-separated indicator names from the LLM. (#368)
    • Propagation initialises 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-core security 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/announcements for 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 — .env gitignore (#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.
  • 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 --ticker and --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 TradingAgents
    

    TradingAgents 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 tradingagents
    

    Or with uv:

    uv venv --python 3.13
    source .venv/bin/activate
    

    Install 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 tradingagents
    

    After updating the repository, rebuild the image with docker compose build.

    Results, reports, the memory log and the cache live in the tradingagents_data volume. To keep them in a folder on the host instead, create the folder and point TRADINGAGENTS_DATA_DIR at it, in .env or 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-ollama
    

    Required 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.example to .env.enterprise and 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 with tradingagents login chatgpt: it prints an address to open in your browser, which returns to this machine on 127.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, and tradingagents logout chatgpt revokes it. Runs count toward your plan's usage limits, and the plan does not accept temperature or max_tokens.

    For AWS Bedrock, install the extra with pip install ".[bedrock]", set llm_provider: "bedrock", configure AWS credentials (environment variables, ~/.aws/credentials, or an IAM role) and AWS_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 is http://localhost:11434/v1; set OLLAMA_BASE_URL to point at a remote ollama-serve. Pull models with ollama 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 via backend_url (or TRADINGAGENTS_LLM_BACKEND_URL), e.g. http://localhost:8000/v1 for vLLM or http://localhost:1234/v1 for LM Studio. The model is whatever your server serves. No key is needed for local servers; set OPENAI_COMPATIBLE_API_KEY when the endpoint requires one.

    With TYPESAFE_API_KEY set, 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-latest moves with new releases; set TYPESAFE_DEFAULT_MODEL to a versioned ID such as jev-1.13.0 to hold it fixed across runs. To reach Jev through OpenRouter, put an OpenRouter key in TYPESAFE_API_KEY and set TYPESAFE_BASE_URL=https://openrouter.ai/api.

    Alternatively, copy .env.example to .env and fill in your keys:

    cp .env.example .env
    

    CLI Usage

    Launch the interactive CLI:

    tradingagents          # installed command
    python -m cli.main     # alternative: run directly from source
    

    You 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 .env still 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-show
    

    Each 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=1 to 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-html skips it, and so does ta.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.SS for 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 tradingagents module and initialize a TradingAgentsGraph() object. The .propagate() function will return a decision. You can run main.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_url or deep_think_backend_url for a local or relay endpoint. The TRADINGAGENTS_DEEP_THINK_PROVIDER and TRADINGAGENTS_QUICK_THINK_PROVIDER variables set them for the CLI, together with the tier's model variable.

    See tradingagents/default_config.py for 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 positions list 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 with TRADINGAGENTS_CACHE_DIR). Use --clear-checkpoints to reset all of them before a run.

    tradingagents --checkpoint           # enable for this run
    tradingagents --clear-checkpoints    # reset before running
    
    config = 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_backtest runs 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 7
    

    Each 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_id skips 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 temperature in your config (or TRADINGAGENTS_TEMPERATURE in .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 in TRADINGAGENTS_DEEP_THINK_LLM and TRADINGAGENTS_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 tradingagents or python -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 to README.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),
                )
            )
        el
  • tradingagents-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 kept
    
  • tradingagents-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 mo
  • tradingagents-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 = true
    
  • tradingagents-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) == 0o600
    
  • tradingagents-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 held
    
  • tradingagents-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, ma
  • tradingagents-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.

TradingAgents — 12-Role Multi-Agent Trading Desk (Analyst Debate, Trader, Risk Team, Portfolio Manager) · Capital Markets and Trading · AI Agents · TalentDelta