mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-06 17:37:11 +02:00
fix(cli): write the state log, and name the analysts as users know them
- record_decision writes the run's state log, so CLI runs save full_states_log like propagate() does - with both round counts from the environment, the CLI no longer says a chosen research depth was overridden - an unknown --analysts name lists sentiment, not social
This commit is contained in:
+2
-1
@@ -90,7 +90,8 @@ def parse_analysts(value: str, asset_type: AssetType) -> list[AnalystType]:
|
||||
known = {a.value: a for a in AnalystType}
|
||||
unknown = [n for n in names if n not in known]
|
||||
if unknown:
|
||||
raise ValueError(f"unknown analyst {', '.join(unknown)}; choose from {', '.join(known)}")
|
||||
choices = ", ".join("sentiment" if name == "social" else name for name in known)
|
||||
raise ValueError(f"unknown analyst {', '.join(unknown)}; choose from {choices}")
|
||||
available = filter_analysts_for_asset_type(list(known.values()), asset_type)
|
||||
unavailable = [n for n in names if known[n] not in available]
|
||||
if unavailable:
|
||||
|
||||
+9
-7
@@ -22,7 +22,7 @@ from cli.display import (
|
||||
update_display,
|
||||
update_research_team_status,
|
||||
)
|
||||
from cli.selections import get_user_selections, unattended_gaps
|
||||
from cli.selections import depth_from_env, get_user_selections, unattended_gaps
|
||||
from cli.stats_handler import StatsCallbackHandler
|
||||
from tradingagents.agents.rating import is_review, run_rating
|
||||
from tradingagents.dataflows.symbols import safe_ticker_component
|
||||
@@ -67,17 +67,19 @@ def _build_run_config(selections: dict, checkpoint: bool | None) -> dict:
|
||||
# Research depth sets both round counts, but an explicit env override
|
||||
# (TRADINGAGENTS_MAX_DEBATE_ROUNDS / _MAX_RISK_ROUNDS) wins over the
|
||||
# interactive selection — leave the env-applied value in place (#977).
|
||||
for env_var, key in (("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "max_debate_rounds"),
|
||||
("TRADINGAGENTS_MAX_RISK_ROUNDS", "max_risk_discuss_rounds")):
|
||||
if os.environ.get(env_var):
|
||||
# The depth prompt still appeared (it is skipped only when both are
|
||||
rounds = (("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "max_debate_rounds"),
|
||||
("TRADINGAGENTS_MAX_RISK_ROUNDS", "max_risk_discuss_rounds"))
|
||||
depth_was_asked = not depth_from_env()
|
||||
for env_var, key in rounds:
|
||||
if not os.environ.get(env_var):
|
||||
config[key] = selections["research_depth"]
|
||||
elif depth_was_asked:
|
||||
# The depth question appeared (it is skipped only when both are
|
||||
# set), so say which half of the answer the environment overrode.
|
||||
console.print(
|
||||
f"[green]✓ {key} from environment:[/green] {config[key]} "
|
||||
f"(set by {env_var}, so the research depth you chose does not apply to it)"
|
||||
)
|
||||
else:
|
||||
config[key] = selections["research_depth"]
|
||||
config["quick_think_llm"] = selections["quick_think_llm"]
|
||||
config["deep_think_llm"] = selections["deep_think_llm"]
|
||||
config["backend_url"] = selections["backend_url"]
|
||||
|
||||
+8
-5
@@ -46,6 +46,12 @@ def get_user_selections(flags=None):
|
||||
return selections
|
||||
|
||||
|
||||
def depth_from_env() -> bool:
|
||||
"""Both round counts come from the environment, so the depth question is skipped."""
|
||||
return bool(os.environ.get("TRADINGAGENTS_MAX_DEBATE_ROUNDS")
|
||||
and os.environ.get("TRADINGAGENTS_MAX_RISK_ROUNDS"))
|
||||
|
||||
|
||||
def unattended_gaps(flags) -> list[str]:
|
||||
"""The flags and environment variables a run with no terminal still needs."""
|
||||
env = os.environ.get
|
||||
@@ -53,7 +59,7 @@ def unattended_gaps(flags) -> list[str]:
|
||||
gaps += [f"--{name} or --no-{name}" for name in ("save", "show") if flags.get(name) is None]
|
||||
if not env("TRADINGAGENTS_OUTPUT_LANGUAGE"):
|
||||
gaps.append("TRADINGAGENTS_OUTPUT_LANGUAGE")
|
||||
if not (env("TRADINGAGENTS_MAX_DEBATE_ROUNDS") and env("TRADINGAGENTS_MAX_RISK_ROUNDS")):
|
||||
if not depth_from_env():
|
||||
gaps.append("TRADINGAGENTS_MAX_DEBATE_ROUNDS and TRADINGAGENTS_MAX_RISK_ROUNDS")
|
||||
if not env("TRADINGAGENTS_LLM_PROVIDER"):
|
||||
gaps.append("TRADINGAGENTS_LLM_PROVIDER")
|
||||
@@ -190,10 +196,7 @@ def _prompt_selections(prefs, flags):
|
||||
# Research depth maps to the debate + risk round counts; when both are
|
||||
# supplied through TRADINGAGENTS_MAX_DEBATE_ROUNDS / _MAX_RISK_ROUNDS we keep
|
||||
# the run non-interactive and honor the env values (#977).
|
||||
depth_from_env = bool(os.environ.get("TRADINGAGENTS_MAX_DEBATE_ROUNDS")) and bool(
|
||||
os.environ.get("TRADINGAGENTS_MAX_RISK_ROUNDS")
|
||||
)
|
||||
if depth_from_env:
|
||||
if depth_from_env():
|
||||
selected_research_depth = DEFAULT_CONFIG["max_debate_rounds"]
|
||||
console.print(
|
||||
f"[green]✓ Research depth from environment:[/green] "
|
||||
|
||||
@@ -38,7 +38,7 @@ class TestFlagValues:
|
||||
|
||||
def test_analysts_are_named_and_checked_against_the_asset(self):
|
||||
assert prompts.parse_analysts(" Market, news ", AssetType.STOCK) == [AnalystType.MARKET, AnalystType.NEWS]
|
||||
with pytest.raises(ValueError, match="market, social, news, fundamentals"):
|
||||
with pytest.raises(ValueError, match="market, sentiment, news, fundamentals"):
|
||||
prompts.parse_analysts("market,macro", AssetType.STOCK)
|
||||
with pytest.raises(ValueError, match="crypto"):
|
||||
prompts.parse_analysts("fundamentals", AssetType.CRYPTO)
|
||||
@@ -160,3 +160,24 @@ def test_a_closed_stdin_counts_as_no_terminal(monkeypatch, capsys):
|
||||
with pytest.raises(typer.Exit):
|
||||
run.run_analysis(flags={})
|
||||
assert "--ticker" in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_rounds_from_the_environment_do_not_claim_a_depth_was_chosen(monkeypatch, capsys):
|
||||
"""With both round counts set, the depth question never appeared."""
|
||||
monkeypatch.setenv("TRADINGAGENTS_MAX_DEBATE_ROUNDS", "1")
|
||||
monkeypatch.setenv("TRADINGAGENTS_MAX_RISK_ROUNDS", "1")
|
||||
selections = {"research_depth": 3, "quick_think_llm": "q", "deep_think_llm": "d", "backend_url": None,
|
||||
"llm_provider": "openai", "google_thinking_level": None, "openai_reasoning_effort": None,
|
||||
"anthropic_effort": None, "output_language": "English"}
|
||||
|
||||
run._build_run_config(selections, None)
|
||||
|
||||
assert "the research depth you chose" not in capsys.readouterr().out
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_an_unknown_analyst_error_names_the_analysts_as_users_know_them():
|
||||
with pytest.raises(ValueError) as caught:
|
||||
prompts.parse_analysts("macro", AssetType.STOCK)
|
||||
assert "sentiment" in str(caught.value) and "social" not in str(caught.value)
|
||||
|
||||
@@ -21,6 +21,7 @@ def _bare_graph(tmp_path):
|
||||
graph = object.__new__(TradingAgentsGraph)
|
||||
graph.config = {"memory_log_path": str(tmp_path / "trading_memory.md")}
|
||||
graph.memory_log = TradingMemoryLog(graph.config)
|
||||
graph._log_state = lambda *a: None # these tests are about the memory log
|
||||
return graph
|
||||
|
||||
|
||||
@@ -187,3 +188,26 @@ def test_a_run_without_checkpointing_says_nothing_about_resuming(tmp_path, monke
|
||||
buffer = _run_cli(monkeypatch, tmp_path, _FakeGraph())
|
||||
|
||||
assert not any("resum" in text.lower() or "fresh" in text.lower() for _, _, text in buffer.messages)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_recording_a_run_writes_its_state_log(tmp_path):
|
||||
"""The CLI records a run through record_decision, so the state log is written there."""
|
||||
graph = object.__new__(TradingAgentsGraph)
|
||||
graph.config = {"results_dir": str(tmp_path), "llm_provider": "openai", "deep_think_llm": "d",
|
||||
"quick_think_llm": "q", "max_debate_rounds": 1, "max_risk_discuss_rounds": 1,
|
||||
"output_language": "English", "data_vendors": {}, "tool_vendors": {}}
|
||||
graph.selected_analysts = ("market",)
|
||||
graph.memory_log = TradingMemoryLog({"memory_log_path": str(tmp_path / "m.md")})
|
||||
state = {"company_of_interest": "NVDA", "trade_date": "2026-09-23", "market_report": "M",
|
||||
"sentiment_report": "", "news_report": "", "fundamentals_report": "",
|
||||
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
||||
"current_response": ""},
|
||||
"trader_investment_plan": "T", "investment_plan": "P",
|
||||
"risk_debate_state": {"aggressive_history": "", "conservative_history": "",
|
||||
"neutral_history": "", "history": ""},
|
||||
"final_trade_decision": "**Rating**: Hold", "final_rating": "Hold"}
|
||||
|
||||
graph.record_decision("NVDA", "2026-09-23", state)
|
||||
|
||||
assert list(tmp_path.glob("NVDA/TradingAgentsStrategy_logs/full_states_log_2026-09-23.json"))
|
||||
|
||||
@@ -312,10 +312,13 @@ class TradingAgentsGraph:
|
||||
settlement.settle_pending(company_name, self.memory_log, self.reflector, self.config)
|
||||
|
||||
def record_decision(self, company_name, trade_date, final_state):
|
||||
"""Log a finished run's decision for reflection on the next same-ticker run."""
|
||||
"""Record a finished run: its state log, and its decision in the memory log
|
||||
for reflection on the next same-ticker run. propagate() and the CLI both end here."""
|
||||
self._log_state(trade_date, final_state)
|
||||
decision = final_state.get("final_trade_decision")
|
||||
if not decision:
|
||||
logger.warning("No final decision for %s on %s; nothing logged", company_name, trade_date)
|
||||
logger.warning("No final decision for %s on %s; nothing added to the memory log",
|
||||
company_name, trade_date)
|
||||
return
|
||||
self.memory_log.store_decision(
|
||||
ticker=company_name, trade_date=trade_date, final_trade_decision=decision,
|
||||
@@ -349,9 +352,6 @@ class TradingAgentsGraph:
|
||||
else:
|
||||
final_state = self.graph.invoke(graph_input, **args)
|
||||
|
||||
# Log state to disk.
|
||||
self._log_state(trade_date, final_state)
|
||||
|
||||
self.record_decision(company_name, trade_date, final_state)
|
||||
|
||||
# Clear checkpoint on successful completion to avoid stale state.
|
||||
|
||||
Reference in New Issue
Block a user