mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-09 02:42:37 +02:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user