feat(config): TRADINGAGENTS_* env-var overlay for DEFAULT_CONFIG

Adds a single _ENV_OVERRIDES table in default_config.py with type-aware
coercion (str/int/bool), so users can switch llm_provider, deep/quick
models, backend URL, output language, debate rounds, and the checkpoint
flag purely via .env. Centralizes load_dotenv in the package __init__
so the overlay applies for every entry point (CLI, main.py, programmatic).
Drops the hardcoded model assignments and duplicate dotenv loads in
main.py and cli/main.py. Verified live with OpenAI and Gemini.

#602
This commit is contained in:
Yijia-Xiao
2026-05-11 06:12:31 +00:00
parent 6b384f74f9
commit d13e9b7946
6 changed files with 176 additions and 25 deletions

View File

@@ -11,3 +11,16 @@ ZHIPU_CN_API_KEY=
MINIMAX_API_KEY= MINIMAX_API_KEY=
MINIMAX_CN_API_KEY= MINIMAX_CN_API_KEY=
OPENROUTER_API_KEY= OPENROUTER_API_KEY=
# Optional: override DEFAULT_CONFIG without editing code.
# Any TRADINGAGENTS_* variable below, when set, replaces the matching key
# in tradingagents/default_config.py. Values are coerced to the type of
# the existing default (bool / int / str), so "true"/"3" work as expected.
#TRADINGAGENTS_LLM_PROVIDER=openai
#TRADINGAGENTS_DEEP_THINK_LLM=gpt-5.4
#TRADINGAGENTS_QUICK_THINK_LLM=gpt-5.4-mini
#TRADINGAGENTS_LLM_BACKEND_URL=
#TRADINGAGENTS_OUTPUT_LANGUAGE=English
#TRADINGAGENTS_MAX_DEBATE_ROUNDS=1
#TRADINGAGENTS_MAX_RISK_ROUNDS=1
#TRADINGAGENTS_CHECKPOINT_ENABLED=false

View File

@@ -5,13 +5,6 @@ import questionary
from pathlib import Path from pathlib import Path
from functools import wraps from functools import wraps
from rich.console import Console from rich.console import Console
from dotenv import find_dotenv, load_dotenv
# Search starts from the user's CWD so the installed `tradingagents`
# console script picks up the project's .env instead of walking up from
# site-packages.
load_dotenv(find_dotenv(usecwd=True))
load_dotenv(find_dotenv(".env.enterprise", usecwd=True), override=False)
from rich.panel import Panel from rich.panel import Panel
from rich.spinner import Spinner from rich.spinner import Spinner
from rich.live import Live from rich.live import Live
@@ -569,6 +562,11 @@ def get_user_selections():
elif selected_llm_provider == "glm": elif selected_llm_provider == "glm":
selected_llm_provider, backend_url = ask_glm_region() selected_llm_provider, backend_url = ask_glm_region()
# Confirm the provider's API key is present; prompt the user to paste
# one and persist it to .env if it's missing, so the analysis run
# doesn't fail later at the first API call.
ensure_api_key(selected_llm_provider)
# Step 7: Thinking agents # Step 7: Thinking agents
console.print( console.print(
create_question_box( create_question_box(

21
main.py
View File

@@ -1,23 +1,12 @@
from tradingagents.graph.trading_graph import TradingAgentsGraph from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG from tradingagents.default_config import DEFAULT_CONFIG
from dotenv import find_dotenv, load_dotenv # DEFAULT_CONFIG already applies TRADINGAGENTS_* env-var overrides
# (llm_provider, deep_think_llm, quick_think_llm, backend_url, etc.),
load_dotenv(find_dotenv(usecwd=True)) # so users can switch models or endpoints purely via .env without
# editing this script. Override individual keys here only when you
# Create a custom config # want a hard-coded value that should ignore the environment.
config = DEFAULT_CONFIG.copy() config = DEFAULT_CONFIG.copy()
config["deep_think_llm"] = "gpt-5.4-mini" # Use a different model
config["quick_think_llm"] = "gpt-5.4-mini" # Use a different model
config["max_debate_rounds"] = 1 # Increase debate rounds
# Configure data vendors (default uses yfinance, no extra API keys needed)
config["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
}
# Initialize with custom config # Initialize with custom config
ta = TradingAgentsGraph(debug=True, config=config) ta = TradingAgentsGraph(debug=True, config=config)

View File

@@ -0,0 +1,98 @@
"""Tests for TRADINGAGENTS_* env-var overlay onto DEFAULT_CONFIG."""
from __future__ import annotations
import importlib
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."""
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)
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-5.4"
assert dc.DEFAULT_CONFIG["quick_think_llm"] == "gpt-5.4-mini"
assert dc.DEFAULT_CONFIG["backend_url"] is None
assert dc.DEFAULT_CONFIG["max_debate_rounds"] == 1
assert dc.DEFAULT_CONFIG["checkpoint_enabled"] is False
def test_string_overrides(monkeypatch):
dc = _reload_with_env(
monkeypatch,
TRADINGAGENTS_LLM_PROVIDER="google",
TRADINGAGENTS_DEEP_THINK_LLM="gemini-3-pro-preview",
TRADINGAGENTS_QUICK_THINK_LLM="gemini-3-flash-preview",
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"
def test_int_coercion(monkeypatch):
dc = _reload_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)
@pytest.mark.parametrize(
"raw,expected",
[
("true", True), ("True", True), ("1", True), ("yes", True), ("on", True),
("false", False), ("False", False), ("0", False), ("no", False), ("off", False),
],
)
def test_bool_coercion(monkeypatch, raw, expected):
dc = _reload_with_env(monkeypatch, TRADINGAGENTS_CHECKPOINT_ENABLED=raw)
assert dc.DEFAULT_CONFIG["checkpoint_enabled"] is expected
def test_empty_env_value_is_passthrough(monkeypatch):
"""Empty TRADINGAGENTS_* values must not clobber the built-in default."""
dc = _reload_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
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):
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)
def test_unknown_env_var_is_ignored(monkeypatch):
"""Env vars outside _ENV_OVERRIDES must not bleed into DEFAULT_CONFIG."""
dc = _reload_with_env(
monkeypatch,
TRADINGAGENTS_NONEXISTENT_KEY="oops",
)
assert "nonexistent_key" not in dc.DEFAULT_CONFIG

View File

@@ -1,5 +1,20 @@
import warnings import warnings
# Load .env files at package import so DEFAULT_CONFIG's env-var overlay
# (and every llm_clients consumer) sees the user's keys regardless of
# which entry point started the process. find_dotenv(usecwd=True) walks
# from the CWD, so the installed `tradingagents` console script picks up
# the project's .env instead of stepping up from site-packages.
# load_dotenv defaults to override=False, so it never clobbers values
# the caller has already exported.
try:
from dotenv import find_dotenv, load_dotenv
load_dotenv(find_dotenv(usecwd=True))
load_dotenv(find_dotenv(".env.enterprise", usecwd=True), override=False)
except ImportError:
pass
# langchain-core 1.3.3 calls surface_langchain_deprecation_warnings() in # langchain-core 1.3.3 calls surface_langchain_deprecation_warnings() in
# its own __init__, which prepends default-action filters for its # its own __init__, which prepends default-action filters for its
# subclassed warning categories. To suppress a specific warning we must # subclassed warning categories. To suppress a specific warning we must

View File

@@ -2,7 +2,45 @@ import os
_TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents") _TRADINGAGENTS_HOME = os.path.join(os.path.expanduser("~"), ".tradingagents")
DEFAULT_CONFIG = { # Single source of truth for env-var → config-key overrides. To expose
# a new config key for environment-based override, add a row here — no
# entry-point script changes required. Coercion is driven by the type
# of the existing default, so users can keep writing plain strings in
# their .env file.
_ENV_OVERRIDES = {
"TRADINGAGENTS_LLM_PROVIDER": "llm_provider",
"TRADINGAGENTS_DEEP_THINK_LLM": "deep_think_llm",
"TRADINGAGENTS_QUICK_THINK_LLM": "quick_think_llm",
"TRADINGAGENTS_LLM_BACKEND_URL": "backend_url",
"TRADINGAGENTS_OUTPUT_LANGUAGE": "output_language",
"TRADINGAGENTS_MAX_DEBATE_ROUNDS": "max_debate_rounds",
"TRADINGAGENTS_MAX_RISK_ROUNDS": "max_risk_discuss_rounds",
"TRADINGAGENTS_CHECKPOINT_ENABLED": "checkpoint_enabled",
}
def _coerce(value: str, reference):
"""Coerce env-var string to the type of the existing default value."""
if isinstance(reference, bool):
return value.strip().lower() in ("true", "1", "yes", "on")
if isinstance(reference, int) and not isinstance(reference, bool):
return int(value)
if isinstance(reference, float):
return float(value)
return value
def _apply_env_overrides(config: dict) -> dict:
"""Apply TRADINGAGENTS_* env vars to the config dict in-place."""
for env_var, key in _ENV_OVERRIDES.items():
raw = os.environ.get(env_var)
if raw is None or raw == "":
continue
config[key] = _coerce(raw, config.get(key))
return config
DEFAULT_CONFIG = _apply_env_overrides({
"project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")), "project_dir": os.path.abspath(os.path.join(os.path.dirname(__file__), ".")),
"results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", os.path.join(_TRADINGAGENTS_HOME, "logs")), "results_dir": os.getenv("TRADINGAGENTS_RESULTS_DIR", os.path.join(_TRADINGAGENTS_HOME, "logs")),
"data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR", os.path.join(_TRADINGAGENTS_HOME, "cache")), "data_cache_dir": os.getenv("TRADINGAGENTS_CACHE_DIR", os.path.join(_TRADINGAGENTS_HOME, "cache")),
@@ -62,4 +100,4 @@ DEFAULT_CONFIG = {
"tool_vendors": { "tool_vendors": {
# Example: "get_stock_data": "alpha_vantage", # Override category default # Example: "get_stock_data": "alpha_vantage", # Override category default
}, },
} })