test: stop reloading modules, so the suite passes in any order

- default_config.build_default_config gives the defaults with the environment as it is now; the overlay tests call it instead of reloading the module, whose reloaded DEFAULT_CONFIG leaked their settings into later tests
- the Ollama and API-key tests import the modules they check, which read the environment when called; two tests that only read os.environ are dropped
This commit is contained in:
Yijia-Xiao
2026-09-29 05:57:18 +00:00
parent 9a568d19b1
commit fa87947c33
8 changed files with 201 additions and 258 deletions
+3 -5
View File
@@ -67,12 +67,10 @@ def test_case_insensitive_lookup():
@pytest.fixture
def prompts(monkeypatch):
"""Import cli.prompts with a fresh environment so module-level state is consistent."""
import importlib
def prompts():
"""cli.prompts, which reads the environment when it is called."""
import cli.prompts as prompts_module
return importlib.reload(prompts_module)
return prompts_module
def test_ensure_api_key_returns_existing(monkeypatch, prompts):
+43 -49
View File
@@ -2,7 +2,6 @@
from __future__ import annotations
import importlib
import os
import pytest
@@ -10,27 +9,27 @@ import pytest
import tradingagents.default_config as default_config_module
def _reload_with_env(monkeypatch, **overrides):
"""Set/clear env vars then reload default_config to re-evaluate DEFAULT_CONFIG."""
def _config_with_env(monkeypatch, **overrides):
"""The defaults as the environment given here would set them."""
for key in list(default_config_module._ENV_OVERRIDES):
monkeypatch.delenv(key, raising=False)
for key, val in overrides.items():
monkeypatch.setenv(key, val)
return importlib.reload(default_config_module)
return default_config_module.build_default_config()
def test_no_env_uses_built_in_defaults(monkeypatch):
dc = _reload_with_env(monkeypatch)
assert dc.DEFAULT_CONFIG["llm_provider"] == "openai"
assert dc.DEFAULT_CONFIG["deep_think_llm"] == "gpt-6-sol"
assert dc.DEFAULT_CONFIG["quick_think_llm"] == "gpt-6-luna"
assert dc.DEFAULT_CONFIG["backend_url"] is None
assert dc.DEFAULT_CONFIG["max_debate_rounds"] == 1
assert dc.DEFAULT_CONFIG["checkpoint_enabled"] is False
config = _config_with_env(monkeypatch)
assert config["llm_provider"] == "openai"
assert config["deep_think_llm"] == "gpt-6-sol"
assert config["quick_think_llm"] == "gpt-6-luna"
assert config["backend_url"] is None
assert config["max_debate_rounds"] == 1
assert config["checkpoint_enabled"] is False
def test_string_overrides(monkeypatch):
dc = _reload_with_env(
config = _config_with_env(
monkeypatch,
TRADINGAGENTS_LLM_PROVIDER="google",
TRADINGAGENTS_DEEP_THINK_LLM="gemini-3-pro-preview",
@@ -38,23 +37,23 @@ def test_string_overrides(monkeypatch):
TRADINGAGENTS_LLM_BACKEND_URL="https://example.invalid/v1",
TRADINGAGENTS_OUTPUT_LANGUAGE="Chinese",
)
assert dc.DEFAULT_CONFIG["llm_provider"] == "google"
assert dc.DEFAULT_CONFIG["deep_think_llm"] == "gemini-3-pro-preview"
assert dc.DEFAULT_CONFIG["quick_think_llm"] == "gemini-3-flash-preview"
assert dc.DEFAULT_CONFIG["backend_url"] == "https://example.invalid/v1"
assert dc.DEFAULT_CONFIG["output_language"] == "Chinese"
assert config["llm_provider"] == "google"
assert config["deep_think_llm"] == "gemini-3-pro-preview"
assert config["quick_think_llm"] == "gemini-3-flash-preview"
assert config["backend_url"] == "https://example.invalid/v1"
assert config["output_language"] == "Chinese"
def test_int_coercion(monkeypatch):
dc = _reload_with_env(
config = _config_with_env(
monkeypatch,
TRADINGAGENTS_MAX_DEBATE_ROUNDS="3",
TRADINGAGENTS_MAX_RISK_ROUNDS="2",
)
assert dc.DEFAULT_CONFIG["max_debate_rounds"] == 3
assert isinstance(dc.DEFAULT_CONFIG["max_debate_rounds"], int)
assert dc.DEFAULT_CONFIG["max_risk_discuss_rounds"] == 2
assert isinstance(dc.DEFAULT_CONFIG["max_risk_discuss_rounds"], int)
assert config["max_debate_rounds"] == 3
assert isinstance(config["max_debate_rounds"], int)
assert config["max_risk_discuss_rounds"] == 2
assert isinstance(config["max_risk_discuss_rounds"], int)
@pytest.mark.parametrize(
@@ -65,65 +64,62 @@ def test_int_coercion(monkeypatch):
],
)
def test_bool_coercion(monkeypatch, raw, expected):
dc = _reload_with_env(monkeypatch, TRADINGAGENTS_CHECKPOINT_ENABLED=raw)
assert dc.DEFAULT_CONFIG["checkpoint_enabled"] is expected
config = _config_with_env(monkeypatch, TRADINGAGENTS_CHECKPOINT_ENABLED=raw)
assert config["checkpoint_enabled"] is expected
def test_reasoning_thinking_overrides(monkeypatch):
"""The provider reasoning/thinking knobs are env-configurable (non-interactive runs)."""
dc = _reload_with_env(
config = _config_with_env(
monkeypatch,
TRADINGAGENTS_OPENAI_REASONING_EFFORT="high",
TRADINGAGENTS_GOOGLE_THINKING_LEVEL="minimal",
TRADINGAGENTS_ANTHROPIC_EFFORT="low",
)
assert dc.DEFAULT_CONFIG["openai_reasoning_effort"] == "high"
assert dc.DEFAULT_CONFIG["google_thinking_level"] == "minimal"
assert dc.DEFAULT_CONFIG["anthropic_effort"] == "low"
assert config["openai_reasoning_effort"] == "high"
assert config["google_thinking_level"] == "minimal"
assert config["anthropic_effort"] == "low"
def test_reasoning_effort_defaults_to_none(monkeypatch):
"""Unset reasoning/thinking knobs stay None so each provider uses its own default."""
dc = _reload_with_env(monkeypatch)
assert dc.DEFAULT_CONFIG["openai_reasoning_effort"] is None
assert dc.DEFAULT_CONFIG["google_thinking_level"] is None
assert dc.DEFAULT_CONFIG["anthropic_effort"] is None
config = _config_with_env(monkeypatch)
assert config["openai_reasoning_effort"] is None
assert config["google_thinking_level"] is None
assert config["anthropic_effort"] is None
def test_empty_env_value_is_passthrough(monkeypatch):
"""Empty TRADINGAGENTS_* values must not clobber the built-in default."""
dc = _reload_with_env(
config = _config_with_env(
monkeypatch,
TRADINGAGENTS_LLM_PROVIDER="",
TRADINGAGENTS_MAX_DEBATE_ROUNDS="",
)
assert dc.DEFAULT_CONFIG["llm_provider"] == "openai"
assert dc.DEFAULT_CONFIG["max_debate_rounds"] == 1
assert config["llm_provider"] == "openai"
assert config["max_debate_rounds"] == 1
def test_empty_path_value_keeps_the_default_path(monkeypatch):
""".env.example lists the path variables blank; uncommenting one made the
path empty, and the graph failed creating its directories."""
dc = _reload_with_env(
config = _config_with_env(
monkeypatch,
TRADINGAGENTS_RESULTS_DIR="",
TRADINGAGENTS_CACHE_DIR="",
TRADINGAGENTS_MEMORY_LOG_PATH="",
)
home = dc._TRADINGAGENTS_HOME
assert dc.DEFAULT_CONFIG["results_dir"] == os.path.join(home, "logs")
assert dc.DEFAULT_CONFIG["data_cache_dir"] == os.path.join(home, "cache")
assert dc.DEFAULT_CONFIG["memory_log_path"] == os.path.join(home, "memory", "trading_memory.md")
home = default_config_module._TRADINGAGENTS_HOME
assert config["results_dir"] == os.path.join(home, "logs")
assert config["data_cache_dir"] == os.path.join(home, "cache")
assert config["memory_log_path"] == os.path.join(home, "memory", "trading_memory.md")
def test_invalid_int_raises(monkeypatch):
"""Garbage int values should surface a ValueError at import, not silently misconfigure."""
monkeypatch.setenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "not-a-number")
with pytest.raises(ValueError, match="TRADINGAGENTS_MAX_DEBATE_ROUNDS"):
importlib.reload(default_config_module)
# Restore module state for subsequent tests in this process
monkeypatch.delenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", raising=False)
importlib.reload(default_config_module)
default_config_module.build_default_config()
@pytest.mark.parametrize("bad", ["treu", "flase", "maybe", "2", "enabled"])
@@ -131,15 +127,13 @@ def test_invalid_bool_raises(monkeypatch, bad):
"""A misspelled boolean must fail loudly (like ints) instead of silently False."""
monkeypatch.setenv("TRADINGAGENTS_CHECKPOINT_ENABLED", bad)
with pytest.raises(ValueError, match="TRADINGAGENTS_CHECKPOINT_ENABLED"):
importlib.reload(default_config_module)
monkeypatch.delenv("TRADINGAGENTS_CHECKPOINT_ENABLED", raising=False)
importlib.reload(default_config_module)
default_config_module.build_default_config()
def test_unknown_env_var_is_ignored(monkeypatch):
"""Env vars outside _ENV_OVERRIDES must not bleed into DEFAULT_CONFIG."""
dc = _reload_with_env(
config = _config_with_env(
monkeypatch,
TRADINGAGENTS_NONEXISTENT_KEY="oops",
)
assert "nonexistent_key" not in dc.DEFAULT_CONFIG
assert "nonexistent_key" not in config
+8 -9
View File
@@ -6,8 +6,6 @@ opt-in llm_max_retries knob forwarded to every provider chat client.
"""
from __future__ import annotations
import importlib
import pytest
import tradingagents.default_config as default_config_module
@@ -74,23 +72,24 @@ def test_invalid_config_value_fails_loudly():
# --- env overlay -----------------------------------------------------------
def _reload_with_env(monkeypatch, **overrides):
def _config_with_env(monkeypatch, **overrides):
"""The defaults as the environment given here would set them."""
for key in list(default_config_module._ENV_OVERRIDES):
monkeypatch.delenv(key, raising=False)
for key, val in overrides.items():
monkeypatch.setenv(key, val)
return importlib.reload(default_config_module)
return default_config_module.build_default_config()
@pytest.mark.unit
def test_default_is_none(monkeypatch):
dc = _reload_with_env(monkeypatch)
assert dc.DEFAULT_CONFIG["llm_max_retries"] is None
config = _config_with_env(monkeypatch)
assert config["llm_max_retries"] is None
@pytest.mark.unit
def test_env_override_sets_config(monkeypatch):
dc = _reload_with_env(monkeypatch, TRADINGAGENTS_LLM_MAX_RETRIES="8")
config = _config_with_env(monkeypatch, TRADINGAGENTS_LLM_MAX_RETRIES="8")
# None-default key: env value arrives as a string and is coerced downstream.
assert dc.DEFAULT_CONFIG["llm_max_retries"] == "8"
assert _coerce_max_retries(dc.DEFAULT_CONFIG["llm_max_retries"]) == 8
assert config["llm_max_retries"] == "8"
assert _coerce_max_retries(config["llm_max_retries"]) == 8
+8 -9
View File
@@ -8,8 +8,6 @@ right key per provider.
"""
from __future__ import annotations
import importlib
import pytest
import tradingagents.default_config as default_config_module
@@ -97,22 +95,23 @@ def test_openai_and_google_clients_accept_the_kwarg():
# --- env overlay -----------------------------------------------------------
def _reload_with_env(monkeypatch, **overrides):
def _config_with_env(monkeypatch, **overrides):
"""The defaults as the environment given here would set them."""
for key in list(default_config_module._ENV_OVERRIDES):
monkeypatch.delenv(key, raising=False)
for key, val in overrides.items():
monkeypatch.setenv(key, val)
return importlib.reload(default_config_module)
return default_config_module.build_default_config()
@pytest.mark.unit
def test_default_is_none(monkeypatch):
dc = _reload_with_env(monkeypatch)
assert dc.DEFAULT_CONFIG["max_tokens"] is None
config = _config_with_env(monkeypatch)
assert config["max_tokens"] is None
@pytest.mark.unit
def test_env_override_sets_config(monkeypatch):
dc = _reload_with_env(monkeypatch, TRADINGAGENTS_MAX_TOKENS="8192")
assert dc.DEFAULT_CONFIG["max_tokens"] == "8192"
assert _coerce_max_tokens(dc.DEFAULT_CONFIG["max_tokens"]) == 8192
config = _config_with_env(monkeypatch, TRADINGAGENTS_MAX_TOKENS="8192")
assert config["max_tokens"] == "8192"
assert _coerce_max_tokens(config["max_tokens"]) == 8192
+8 -59
View File
@@ -2,7 +2,6 @@
from __future__ import annotations
import importlib
import re
import pytest
@@ -18,30 +17,12 @@ def _console_out(capsys) -> str:
return _ANSI.sub("", capsys.readouterr().out)
@pytest.fixture(scope="module", autouse=True)
def _resync_reloaded_modules():
"""Restore module state after this file's importlib.reload() calls.
Several tests below reload ``cli.prompts`` to re-evaluate OLLAMA_BASE_URL.
That leaves the modules importing from it (cli.selections, then cli.run and
cli.main) bound to the pre-reload functions, which breaks identity checks in
unrelated tests that run afterward. Re-sync them in import order on teardown.
"""
yield
import cli.main
import cli.prompts
import cli.run
import cli.selections
for module in (cli.prompts, cli.selections, cli.run, cli.main):
importlib.reload(module)
# ---- openai_client side: registry-driven base_url resolution --------------
def _reload_client():
def _client_module():
import tradingagents.llm_clients.openai_client as mod
return importlib.reload(mod)
return mod
def _base_url(mod, provider, **kwargs):
@@ -50,20 +31,20 @@ def _base_url(mod, provider, **kwargs):
def test_resolver_returns_default_when_env_unset(monkeypatch):
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
mod = _reload_client()
mod = _client_module()
assert _base_url(mod, "ollama") == "http://localhost:11434/v1"
def test_resolver_returns_env_when_set(monkeypatch):
monkeypatch.setenv("OLLAMA_BASE_URL", "http://remote-ollama:11434/v1")
mod = _reload_client()
mod = _client_module()
assert _base_url(mod, "ollama") == "http://remote-ollama:11434/v1"
def test_resolver_evaluation_is_call_time(monkeypatch):
"""Setting the env AFTER module import must still take effect."""
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
mod = _reload_client()
mod = _client_module()
monkeypatch.setenv("OLLAMA_BASE_URL", "http://late-set:11434/v1")
assert _base_url(mod, "ollama") == "http://late-set:11434/v1"
@@ -71,7 +52,7 @@ def test_resolver_evaluation_is_call_time(monkeypatch):
def test_resolver_does_not_affect_other_providers(monkeypatch):
"""OLLAMA_BASE_URL should NOT leak into xai/deepseek/etc."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://elsewhere/v1")
mod = _reload_client()
mod = _client_module()
assert _base_url(mod, "xai") == "https://api.x.ai/v1"
assert _base_url(mod, "deepseek") == "https://api.deepseek.com"
@@ -79,7 +60,7 @@ def test_resolver_does_not_affect_other_providers(monkeypatch):
def test_client_get_llm_picks_up_env(monkeypatch):
"""End-to-end: OllamaClient.get_llm() respects OLLAMA_BASE_URL."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://my-ollama:11434/v1")
mod = _reload_client()
mod = _client_module()
client = mod.OpenAIClient(model="llama3.1", provider="ollama")
llm = client.get_llm()
assert "my-ollama" in str(llm.openai_api_base)
@@ -88,7 +69,7 @@ def test_client_get_llm_picks_up_env(monkeypatch):
def test_explicit_base_url_overrides_env(monkeypatch):
"""An explicit base_url passed to the client wins over the env var."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://env-set:11434/v1")
mod = _reload_client()
mod = _client_module()
client = mod.OpenAIClient(
model="llama3.1",
provider="ollama",
@@ -99,40 +80,12 @@ def test_explicit_base_url_overrides_env(monkeypatch):
assert "env-set" not in str(llm.openai_api_base)
# ---- cli.prompts side: select_llm_provider dropdown -------------------------
def test_cli_dropdown_uses_env(monkeypatch):
"""The Ollama entry in the CLI dropdown must reflect OLLAMA_BASE_URL."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://cli-remote:11434/v1")
from cli import prompts
importlib.reload(prompts)
# Reach inside the function via the same env-read it does at call time
ollama_url = (
__import__("os").environ.get("OLLAMA_BASE_URL")
or "http://localhost:11434/v1"
)
assert ollama_url == "http://cli-remote:11434/v1"
def test_cli_dropdown_default_when_unset(monkeypatch):
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
from cli import prompts
importlib.reload(prompts)
ollama_url = (
__import__("os").environ.get("OLLAMA_BASE_URL")
or "http://localhost:11434/v1"
)
assert ollama_url == "http://localhost:11434/v1"
# ---- confirm_ollama_endpoint UX -------------------------------------------
def test_confirm_endpoint_shows_default(monkeypatch, capsys):
monkeypatch.delenv("OLLAMA_BASE_URL", raising=False)
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://localhost:11434/v1")
out = _console_out(capsys)
assert "http://localhost:11434/v1" in out
@@ -143,7 +96,6 @@ def test_confirm_endpoint_shows_default(monkeypatch, capsys):
def test_confirm_endpoint_marks_env_origin(monkeypatch, capsys):
monkeypatch.setenv("OLLAMA_BASE_URL", "http://remote-host:11434/v1")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://remote-host:11434/v1")
out = _console_out(capsys)
assert "http://remote-host:11434/v1" in out
@@ -154,7 +106,6 @@ def test_confirm_endpoint_warns_on_missing_scheme(monkeypatch, capsys):
"""If user sets OLLAMA_BASE_URL=0.0.0.128, advise on the expected shape."""
monkeypatch.setenv("OLLAMA_BASE_URL", "0.0.0.128")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("0.0.0.128")
out = _console_out(capsys)
assert "missing a scheme" in out
@@ -165,7 +116,6 @@ def test_confirm_endpoint_warns_on_non_default_port_remote(monkeypatch, capsys):
"""A remote host with no :11434 gets a soft hint about port mismatch."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://remote-host/v1")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://remote-host/v1")
out = _console_out(capsys)
assert "port 11434" in out
@@ -175,7 +125,6 @@ def test_confirm_endpoint_quiet_on_local_no_port(monkeypatch, capsys):
"""Local host without port shouldn't trigger the remote-port hint."""
monkeypatch.setenv("OLLAMA_BASE_URL", "http://localhost/v1")
from cli import prompts
importlib.reload(prompts)
prompts.confirm_ollama_endpoint("http://localhost/v1")
out = _console_out(capsys)
assert "Note" not in out # localhost is fine without explicit port
+3 -5
View File
@@ -9,18 +9,16 @@ 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.openai_client import LocalCompatibleChatOpenAI, OpenAIClient
from tradingagents.llm_clients.validators import validate_model
# Note: assert by class NAME, not isinstance — other tests reload the
# openai_client module, which would otherwise create a second class identity.
@pytest.mark.unit
def test_factory_routes_to_openai_client():
client = create_llm_client(
provider="openai_compatible", model="my-model", base_url="http://localhost:8000/v1"
)
assert type(client).__name__ == "OpenAIClient"
assert isinstance(client, OpenAIClient)
@pytest.mark.unit
@@ -36,7 +34,7 @@ def test_keyless_local_uses_placeholder_and_chat_completions(monkeypatch):
llm = create_llm_client(
provider="openai_compatible", model="qwen2.5", base_url="http://localhost:8000/v1"
).get_llm()
assert type(llm).__name__ == "LocalCompatibleChatOpenAI"
assert isinstance(llm, LocalCompatibleChatOpenAI)
assert str(llm.openai_api_base) == "http://localhost:8000/v1"
# keyless local servers: a placeholder key is sent
key = llm.openai_api_key.get_secret_value() if hasattr(llm.openai_api_key, "get_secret_value") else llm.openai_api_key
+4 -8
View File
@@ -4,7 +4,6 @@ Temperature is a cross-provider knob: when set it must reach the underlying
chat client; when unset the provider keeps its own default.
"""
import importlib
import pytest
@@ -45,18 +44,15 @@ class TestTemperatureEnvOverlay:
def test_env_sets_temperature(self, monkeypatch):
import tradingagents.default_config as dc
monkeypatch.setenv("TRADINGAGENTS_TEMPERATURE", "0.2")
importlib.reload(dc)
config = dc.build_default_config()
# Stored on config (string from env is fine; consumed via float()).
assert dc.DEFAULT_CONFIG["temperature"] in ("0.2", 0.2)
assert float(dc.DEFAULT_CONFIG["temperature"]) == 0.2
monkeypatch.delenv("TRADINGAGENTS_TEMPERATURE", raising=False)
importlib.reload(dc)
assert config["temperature"] in ("0.2", 0.2)
assert float(config["temperature"]) == 0.2
def test_default_temperature_is_none(self, monkeypatch):
import tradingagents.default_config as dc
monkeypatch.delenv("TRADINGAGENTS_TEMPERATURE", raising=False)
importlib.reload(dc)
assert dc.DEFAULT_CONFIG["temperature"] is None
assert dc.build_default_config()["temperature"] is None
@pytest.mark.unit
+124 -114
View File
@@ -70,117 +70,127 @@ def _apply_env_overrides(config: dict) -> dict:
return config
DEFAULT_CONFIG = _apply_env_overrides({
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR") or os.path.join(_TRADINGAGENTS_HOME, "logs"),
"data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR") or os.path.join(_TRADINGAGENTS_HOME, "cache"),
"memory_log_path": os.getenv("TRADINGAGENTS_MEMORY_LOG_PATH") or os.path.join(_TRADINGAGENTS_HOME, "memory", "trading_memory.md"),
# Optional cap on the number of resolved memory log entries. When set,
# the oldest resolved entries are pruned once this limit is exceeded.
# Pending entries are never pruned. None disables rotation entirely.
"memory_log_max_entries": None,
# LLM settings
"llm_provider": "openai",
"deep_think_llm": "gpt-6-sol",
"quick_think_llm": "gpt-6-luna",
# When None, each provider's client falls back to its own default endpoint
# (api.openai.com for OpenAI, generativelanguage.googleapis.com for Gemini, ...).
# The CLI overrides this per provider when the user picks one. Keeping a
# provider-specific URL here would leak (e.g. OpenAI's /v1 was previously
# being forwarded to Gemini, producing malformed request URLs).
"backend_url": None,
# Provider-specific thinking configuration
"google_thinking_level": None, # "high", "minimal", etc.
"openai_reasoning_effort": None, # "medium", "high", "low"
"anthropic_effort": None, # "high", "medium", "low"
# Sampling temperature, forwarded to every provider when set. None leaves
# each provider at its own default. Lower values reduce run-to-run
# variation on models that honor it; reasoning models largely ignore it
# and no setting makes LLM output bit-identical across runs (see README).
"temperature": None,
# SDK retry budget forwarded to every provider chat client. None leaves each
# provider/SDK at its own default (usually 2). Raise it to ride out bursty
# 429 throttling on rate-limited deployments instead of aborting a run (#1091).
"llm_max_retries": None,
# Cap on output tokens forwarded to every provider chat client. None leaves
# each provider at its own default. Set it to bound a model that emits
# unbounded reasoning/output and hangs or trips a gateway idle timeout
# (e.g. some deepseek-v4-flash deployments, #1204).
"max_tokens": None,
# Checkpoint/resume: when True, LangGraph saves state after each node
# so a crashed run can resume from the last successful step.
"checkpoint_enabled": False,
# Output language for analyst reports and final decision
# Internal agent debate stays in English for reasoning quality
"output_language": "English",
# Debate and discussion settings
"max_debate_rounds": 1,
"max_risk_discuss_rounds": 1,
"max_recur_limit": 100,
# Rounds of tool calls an analyst may make before it is asked for its report.
"max_tool_rounds": 20,
# News / data fetching parameters
# Increase for longer lookback strategies or to broaden macro coverage;
# decrease to reduce token usage in agent prompts.
"news_article_limit": 20, # max articles per ticker (ticker-news)
"global_news_article_limit": 10, # max articles for global/macro news
"global_news_lookback_days": 7, # macro news lookback window
# Search queries used by get_global_news for macro headlines. Extend or
# replace to broaden geographic / sector coverage.
"global_news_queries": [
"Federal Reserve interest rates inflation",
"S&P 500 earnings GDP economic outlook",
"geopolitical risk trade war sanctions",
"ECB Bank of England BOJ central bank policy",
"oil commodities supply chain energy",
],
# Data vendor configuration
# Category-level configuration (default for all tools in category).
# The configured value is the exact vendor chain — requests are NOT silently
# routed to vendors you didn't choose. For ordered fallback, list several,
# e.g. "yfinance,alpha_vantage". "default" uses all available vendors.
"data_vendors": {
"core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance
"technical_indicators": "yfinance", # Options: alpha_vantage, yfinance
"fundamental_data": "yfinance", # Options: alpha_vantage, yfinance
"news_data": "yfinance", # Options: alpha_vantage, yfinance
"macro_data": "fred", # Options: fred (needs FRED_API_KEY)
"prediction_markets": "polymarket", # Options: polymarket (keyless)
},
# Tool-level configuration (takes precedence over category-level)
"tool_vendors": {
# Example: "get_stock_data": "alpha_vantage", # Override category default
},
# Benchmark for alpha calculation in the reflection layer.
# ``benchmark_ticker`` (when set) overrides the suffix map for all
# tickers; leave it None to use ``benchmark_map`` for auto-detection
# based on the ticker's exchange suffix. SPY remains the US default
# so the reflection label keeps reading "Alpha vs SPY" for US tickers
# while non-US tickers get their regional index automatically.
# Trading days after the analysis date over which a decision's outcome is
# measured, for reflection and for the backtest figures.
"holding_period_days": 5,
"benchmark_ticker": None,
"benchmark_map": {
".NS": "^NSEI", # NSE India (Nifty 50)
".BO": "^BSESN", # BSE India (Sensex)
".T": "^N225", # Tokyo (Nikkei 225)
".TW": "^TWII", # Taiwan (TAIEX)
".TWO": "^TWII", # Taipei OTC (TPEx has no Yahoo index; TAIEX)
".KS": "^KS11", # Korea (KOSPI)
".KQ": "^KQ11", # Korea (KOSDAQ)
".HK": "^HSI", # Hong Kong (Hang Seng)
".SI": "^STI", # Singapore (Straits Times)
".L": "^FTSE", # London (FTSE 100)
".DE": "^GDAXI", # Germany (DAX)
".PA": "^FCHI", # Paris (CAC 40)
".AS": "^AEX", # Amsterdam (AEX)
".SW": "^SSMI", # Switzerland (SMI)
".MI": "FTSEMIB.MI", # Milan (FTSE MIB)
".TO": "^GSPTSE", # Toronto (TSX Composite)
".AX": "^AXJO", # Australia (ASX 200)
".SS": "000001.SS", # Shanghai (SSE Composite)
".SZ": "399001.SZ", # Shenzhen (SZSE Component)
".SA": "^BVSP", # B3 Brazil (Ibovespa)
"": "SPY", # default for US-listed tickers (no suffix)
},
})
def build_default_config() -> dict:
"""The built-in defaults with the TRADINGAGENTS_* environment folded in.
Read when the package is imported, as DEFAULT_CONFIG; call it again to see
the environment as it is now.
"""
return _apply_env_overrides({
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR") or os.path.join(_TRADINGAGENTS_HOME, "logs"),
"data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR") or os.path.join(_TRADINGAGENTS_HOME, "cache"),
"memory_log_path": os.getenv("TRADINGAGENTS_MEMORY_LOG_PATH") or os.path.join(_TRADINGAGENTS_HOME, "memory", "trading_memory.md"),
# Optional cap on the number of resolved memory log entries. When set,
# the oldest resolved entries are pruned once this limit is exceeded.
# Pending entries are never pruned. None disables rotation entirely.
"memory_log_max_entries": None,
# LLM settings
"llm_provider": "openai",
"deep_think_llm": "gpt-6-sol",
"quick_think_llm": "gpt-6-luna",
# When None, each provider's client falls back to its own default endpoint
# (api.openai.com for OpenAI, generativelanguage.googleapis.com for Gemini, ...).
# The CLI overrides this per provider when the user picks one. Keeping a
# provider-specific URL here would leak (e.g. OpenAI's /v1 was previously
# being forwarded to Gemini, producing malformed request URLs).
"backend_url": None,
# Provider-specific thinking configuration
"google_thinking_level": None, # "high", "minimal", etc.
"openai_reasoning_effort": None, # "medium", "high", "low"
"anthropic_effort": None, # "high", "medium", "low"
# Sampling temperature, forwarded to every provider when set. None leaves
# each provider at its own default. Lower values reduce run-to-run
# variation on models that honor it; reasoning models largely ignore it
# and no setting makes LLM output bit-identical across runs (see README).
"temperature": None,
# SDK retry budget forwarded to every provider chat client. None leaves each
# provider/SDK at its own default (usually 2). Raise it to ride out bursty
# 429 throttling on rate-limited deployments instead of aborting a run (#1091).
"llm_max_retries": None,
# Cap on output tokens forwarded to every provider chat client. None leaves
# each provider at its own default. Set it to bound a model that emits
# unbounded reasoning/output and hangs or trips a gateway idle timeout
# (e.g. some deepseek-v4-flash deployments, #1204).
"max_tokens": None,
# Checkpoint/resume: when True, LangGraph saves state after each node
# so a crashed run can resume from the last successful step.
"checkpoint_enabled": False,
# Output language for analyst reports and final decision
# Internal agent debate stays in English for reasoning quality
"output_language": "English",
# Debate and discussion settings
"max_debate_rounds": 1,
"max_risk_discuss_rounds": 1,
"max_recur_limit": 100,
# Rounds of tool calls an analyst may make before it is asked for its report.
"max_tool_rounds": 20,
# News / data fetching parameters
# Increase for longer lookback strategies or to broaden macro coverage;
# decrease to reduce token usage in agent prompts.
"news_article_limit": 20, # max articles per ticker (ticker-news)
"global_news_article_limit": 10, # max articles for global/macro news
"global_news_lookback_days": 7, # macro news lookback window
# Search queries used by get_global_news for macro headlines. Extend or
# replace to broaden geographic / sector coverage.
"global_news_queries": [
"Federal Reserve interest rates inflation",
"S&P 500 earnings GDP economic outlook",
"geopolitical risk trade war sanctions",
"ECB Bank of England BOJ central bank policy",
"oil commodities supply chain energy",
],
# Data vendor configuration
# Category-level configuration (default for all tools in category).
# The configured value is the exact vendor chain — requests are NOT silently
# routed to vendors you didn't choose. For ordered fallback, list several,
# e.g. "yfinance,alpha_vantage". "default" uses all available vendors.
"data_vendors": {
"core_stock_apis": "yfinance", # Options: alpha_vantage, yfinance
"technical_indicators": "yfinance", # Options: alpha_vantage, yfinance
"fundamental_data": "yfinance", # Options: alpha_vantage, yfinance
"news_data": "yfinance", # Options: alpha_vantage, yfinance
"macro_data": "fred", # Options: fred (needs FRED_API_KEY)
"prediction_markets": "polymarket", # Options: polymarket (keyless)
},
# Tool-level configuration (takes precedence over category-level)
"tool_vendors": {
# Example: "get_stock_data": "alpha_vantage", # Override category default
},
# Benchmark for alpha calculation in the reflection layer.
# ``benchmark_ticker`` (when set) overrides the suffix map for all
# tickers; leave it None to use ``benchmark_map`` for auto-detection
# based on the ticker's exchange suffix. SPY remains the US default
# so the reflection label keeps reading "Alpha vs SPY" for US tickers
# while non-US tickers get their regional index automatically.
# Trading days after the analysis date over which a decision's outcome is
# measured, for reflection and for the backtest figures.
"holding_period_days": 5,
"benchmark_ticker": None,
"benchmark_map": {
".NS": "^NSEI", # NSE India (Nifty 50)
".BO": "^BSESN", # BSE India (Sensex)
".T": "^N225", # Tokyo (Nikkei 225)
".TW": "^TWII", # Taiwan (TAIEX)
".TWO": "^TWII", # Taipei OTC (TPEx has no Yahoo index; TAIEX)
".KS": "^KS11", # Korea (KOSPI)
".KQ": "^KQ11", # Korea (KOSDAQ)
".HK": "^HSI", # Hong Kong (Hang Seng)
".SI": "^STI", # Singapore (Straits Times)
".L": "^FTSE", # London (FTSE 100)
".DE": "^GDAXI", # Germany (DAX)
".PA": "^FCHI", # Paris (CAC 40)
".AS": "^AEX", # Amsterdam (AEX)
".SW": "^SSMI", # Switzerland (SMI)
".MI": "FTSEMIB.MI", # Milan (FTSE MIB)
".TO": "^GSPTSE", # Toronto (TSX Composite)
".AX": "^AXJO", # Australia (ASX 200)
".SS": "000001.SS", # Shanghai (SSE Composite)
".SZ": "399001.SZ", # Shenzhen (SZSE Component)
".SA": "^BVSP", # B3 Brazil (Ibovespa)
"": "SPY", # default for US-listed tickers (no suffix)
},
})
DEFAULT_CONFIG = build_default_config()