diff --git a/tests/test_api_key_env.py b/tests/test_api_key_env.py index bdec18441..e973032ce 100644 --- a/tests/test_api_key_env.py +++ b/tests/test_api_key_env.py @@ -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): diff --git a/tests/test_env_overrides.py b/tests/test_env_overrides.py index 8dde7b492..3680f7ad6 100644 --- a/tests/test_env_overrides.py +++ b/tests/test_env_overrides.py @@ -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 diff --git a/tests/test_llm_max_retries.py b/tests/test_llm_max_retries.py index bac0c8bd1..1baad0905 100644 --- a/tests/test_llm_max_retries.py +++ b/tests/test_llm_max_retries.py @@ -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 diff --git a/tests/test_llm_max_tokens.py b/tests/test_llm_max_tokens.py index 2715e179e..d4007f164 100644 --- a/tests/test_llm_max_tokens.py +++ b/tests/test_llm_max_tokens.py @@ -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 diff --git a/tests/test_ollama_base_url.py b/tests/test_ollama_base_url.py index 6cac1f55e..f3511590a 100644 --- a/tests/test_ollama_base_url.py +++ b/tests/test_ollama_base_url.py @@ -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 diff --git a/tests/test_openai_compatible_provider.py b/tests/test_openai_compatible_provider.py index f23c4e64f..f43989709 100644 --- a/tests/test_openai_compatible_provider.py +++ b/tests/test_openai_compatible_provider.py @@ -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 diff --git a/tests/test_temperature_config.py b/tests/test_temperature_config.py index 502182fd1..714eb5244 100644 --- a/tests/test_temperature_config.py +++ b/tests/test_temperature_config.py @@ -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 diff --git a/tradingagents/default_config.py b/tradingagents/default_config.py index d33a206f4..a79c819b2 100644 --- a/tradingagents/default_config.py +++ b/tradingagents/default_config.py @@ -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()