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)
This commit is contained in:
Yijia-Xiao
2026-09-17 01:10:48 +00:00
parent 8d64416436
commit 60dcf64723
2 changed files with 56 additions and 7 deletions

View File

@@ -606,6 +606,13 @@ class TestDeferredReflection:
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "RELIANCE.NS") == "^NSEI" assert TradingAgentsGraph._resolve_benchmark(mock_graph, "RELIANCE.NS") == "^NSEI"
assert TradingAgentsGraph._resolve_benchmark(mock_graph, "AZN.L") == "^FTSE" 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): def test_resolve_benchmark_china_a_shares(self):
"""A-share tickers route to their exchange composite (uses the real """A-share tickers route to their exchange composite (uses the real
default benchmark_map, since A-share support relies on it).""" default benchmark_map, since A-share support relies on it)."""
@@ -930,3 +937,36 @@ class TestLegacyRemoval:
assert len(entries) == 1 assert len(entries) == 1
assert entries[0]["ticker"] == "NVDA" assert entries[0]["ticker"] == "NVDA"
assert entries[0]["pending"] is True 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

View File

@@ -278,7 +278,9 @@ class TradingAgentsGraph:
explicit = self.config.get("benchmark_ticker") explicit = self.config.get("benchmark_ticker")
if explicit: 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", {}) benchmark_map = self.config.get("benchmark_map", {})
ticker_upper = normalize_symbol(ticker) ticker_upper = normalize_symbol(ticker)
for suffix, benchmark in benchmark_map.items(): for suffix, benchmark in benchmark_map.items():
@@ -361,12 +363,19 @@ class TradingAgentsGraph:
) )
if raw is None: if raw is None:
continue # price not available yet — try again next run continue # price not available yet — try again next run
reflection = self.reflector.reflect_on_final_decision( try:
final_decision=entry.get("decision", ""), reflection = self.reflector.reflect_on_final_decision(
raw_return=raw, final_decision=entry.get("decision", ""),
alpha_return=alpha, raw_return=raw,
benchmark_name=benchmark, 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({ updates.append({
"ticker": ticker, "ticker": ticker,
"trade_date": entry["date"], "trade_date": entry["date"],