From 60dcf64723be7b411da569df6d67cc3a4cc5f654 Mon Sep 17 00:00:00 2001 From: Yijia-Xiao Date: Thu, 17 Sep 2026 01:10:48 +0000 Subject: [PATCH] fix(graph): keep a failed reflection from stopping the next run (#645) - settling a past decision is per entry; a provider error leaves it pending - an explicitly configured benchmark ticker is normalized like any other symbol (#1075) --- tests/test_memory_log.py | 40 ++++++++++++++++++++++++++++ tradingagents/graph/trading_graph.py | 23 +++++++++++----- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/tests/test_memory_log.py b/tests/test_memory_log.py index d05fc3c5a..78f615297 100644 --- a/tests/test_memory_log.py +++ b/tests/test_memory_log.py @@ -606,6 +606,13 @@ class TestDeferredReflection: assert TradingAgentsGraph._resolve_benchmark(mock_graph, "RELIANCE.NS") == "^NSEI" assert TradingAgentsGraph._resolve_benchmark(mock_graph, "AZN.L") == "^FTSE" + def test_explicit_benchmark_is_resolved_like_any_other_symbol(self): + """A configured benchmark takes the same alias mapping as the ticker, or + the return lookup finds nothing and the decision never settles.""" + mock_graph = MagicMock(spec=TradingAgentsGraph) + mock_graph.config = {"benchmark_ticker": "SPX500", "benchmark_map": {"": "SPY"}} + assert TradingAgentsGraph._resolve_benchmark(mock_graph, "NVDA") == "^GSPC" + def test_resolve_benchmark_china_a_shares(self): """A-share tickers route to their exchange composite (uses the real default benchmark_map, since A-share support relies on it).""" @@ -930,3 +937,36 @@ class TestLegacyRemoval: assert len(entries) == 1 assert entries[0]["ticker"] == "NVDA" assert entries[0]["pending"] is True + + +@pytest.mark.unit +def test_a_failed_reflection_leaves_the_entry_pending_and_lets_the_run_start(tmp_path, monkeypatch): + """Settling past decisions happens on the way into a new run, and reflection + calls an LLM. A transient failure there must not stop the new analysis.""" + from tradingagents.agents.utils.memory import TradingMemoryLog + from tradingagents.graph.trading_graph import TradingAgentsGraph + + graph = object.__new__(TradingAgentsGraph) + graph.config = {"memory_log_path": str(tmp_path / "m.md")} + graph.memory_log = TradingMemoryLog(graph.config) + graph.memory_log.store_decision("NVDA", "2026-01-05", "Rating: Buy\n\nx") + graph.memory_log.store_decision("NVDA", "2026-01-12", "Rating: Sell\n\ny") + monkeypatch.setattr(graph, "_resolve_benchmark", lambda t: "SPY", raising=False) + monkeypatch.setattr(graph, "_fetch_returns", + lambda t, d, benchmark=None: (0.01, 0.005, 5, "2026-01-19"), raising=False) + + class _Reflector: + calls = 0 + + def reflect_on_final_decision(self, **kw): + _Reflector.calls += 1 + if _Reflector.calls == 1: + raise RuntimeError("provider timed out") + return "second one worked" + + graph.reflector = _Reflector() + + graph._resolve_pending_entries("NVDA") # must not raise + + entries = graph.memory_log.load_entries() + assert [e["pending"] for e in entries] == [True, False] # the failed one waits for next time diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index 120bc141f..ad4d268f3 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -278,7 +278,9 @@ class TradingAgentsGraph: explicit = self.config.get("benchmark_ticker") if explicit: - return explicit + # Same alias mapping as the analyzed ticker; an unmapped alias finds + # no prices, and the decision would stay pending for good. + return normalize_symbol(explicit) benchmark_map = self.config.get("benchmark_map", {}) ticker_upper = normalize_symbol(ticker) for suffix, benchmark in benchmark_map.items(): @@ -361,12 +363,19 @@ class TradingAgentsGraph: ) if raw is None: continue # price not available yet — try again next run - reflection = self.reflector.reflect_on_final_decision( - final_decision=entry.get("decision", ""), - raw_return=raw, - alpha_return=alpha, - benchmark_name=benchmark, - ) + try: + reflection = self.reflector.reflect_on_final_decision( + final_decision=entry.get("decision", ""), + raw_return=raw, + alpha_return=alpha, + benchmark_name=benchmark, + ) + except Exception as exc: + # Reflection calls a provider, and this runs on the way into a + # new run: a transient failure leaves the entry pending for the + # next one rather than stopping the analysis that was asked for. + logger.warning("Reflection failed for %s on %s: %s", ticker, entry["date"], exc) + continue updates.append({ "ticker": ticker, "trade_date": entry["date"],