mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 11:15:24 +03:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
Reference in New Issue
Block a user