mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-03 16:12:44 +02:00
feat(graph): settle past decisions alongside the analysts (#1428)
- a Memory Log step runs beside the analysts, settles every ticker's due decisions and loads the lessons known by the trade date for the Portfolio Manager; the run no longer settles before it starts - a settlement failure is logged and noted in the report's header instead of ending the run - the checkpoint signature records the new layout
This commit is contained in:
@@ -25,27 +25,6 @@ def _bare_graph(tmp_path):
|
||||
return graph
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_create_run_state_settles_pending_and_carries_context(tmp_path, monkeypatch):
|
||||
from tradingagents.graph.propagation import Propagator
|
||||
|
||||
graph = _bare_graph(tmp_path)
|
||||
graph.propagator = Propagator()
|
||||
settled = []
|
||||
monkeypatch.setattr(graph, "settle_pending", settled.append, raising=False)
|
||||
monkeypatch.setattr(graph, "resolve_instrument_context", lambda t, a="stock", d=None: f"id:{t}", raising=False)
|
||||
monkeypatch.setattr(graph, "_memory_as_of", lambda d: d, raising=False)
|
||||
graph.memory_log.store_decision("NVDA", "2026-01-05", "Rating: Buy\nold call")
|
||||
graph.memory_log.update_with_outcome("NVDA", "2026-01-05", 0.01, 0.005, 5, "great trade", "2026-01-12")
|
||||
|
||||
state = graph.create_run_state("NVDA", "2026-02-01")
|
||||
|
||||
assert settled == ["NVDA"]
|
||||
assert "great trade" in state["past_context"]
|
||||
assert state["instrument_context"] == "id:NVDA"
|
||||
assert state["company_of_interest"] == "NVDA"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_record_decision_appends_a_pending_entry(tmp_path):
|
||||
graph = _bare_graph(tmp_path)
|
||||
|
||||
@@ -8,6 +8,7 @@ the wiring: a restructure that drops a node, a tool or an edge fails here.
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import time
|
||||
|
||||
import pandas as pd
|
||||
import pytest
|
||||
@@ -287,3 +288,102 @@ def test_the_last_turn_is_offered_no_tools_and_reads_its_tool_results_as_text(tm
|
||||
def test_a_tool_limit_the_recursion_limit_cannot_hold_is_refused(tmp_path, monkeypatch):
|
||||
with pytest.raises(ValueError, match="max_tool_rounds"):
|
||||
_graph(tmp_path, monkeypatch, ScriptedModel(), max_tool_rounds=60, max_recur_limit=100)
|
||||
|
||||
|
||||
class TimedModel(ScriptedModel):
|
||||
"""ScriptedModel that records each call's graph step, prompt and timing."""
|
||||
|
||||
seen: list = Field(default_factory=list) # shared across bound copies
|
||||
|
||||
def _generate(self, messages, stop=None, run_manager=None, **kwargs) -> ChatResult:
|
||||
step = (run_manager.metadata or {}).get("langgraph_node") if run_manager else None
|
||||
started = time.monotonic()
|
||||
if step in {"Market Analyst", "agent"} or step == "Memory Log":
|
||||
time.sleep(0.2) # long enough to see what overlaps
|
||||
result = super()._generate(messages, stop, run_manager, **kwargs)
|
||||
prompt = "\n".join(str(m.content) for m in messages)
|
||||
self.seen.append((step, prompt, started, time.monotonic()))
|
||||
return result
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_past_decisions_settle_alongside_the_analysts(tmp_path, monkeypatch, offline):
|
||||
from tradingagents.memory import settlement
|
||||
|
||||
model = TimedModel()
|
||||
graph = _graph(tmp_path, monkeypatch, model)
|
||||
graph.memory_log.store_decision("NVDA", "2026-01-02", "Rating: Buy\n\nBuy NVDA.")
|
||||
closes = pd.Series([100.0 + i for i in range(10)], index=pd.bdate_range("2026-01-02", periods=10))
|
||||
monkeypatch.setattr(settlement, "get_closes", lambda *a, **k: closes)
|
||||
|
||||
graph.propagate("NVDA", TRADE_DATE)
|
||||
|
||||
reflections = [c for c in model.seen if c[0] == "Memory Log"]
|
||||
assert len(reflections) == 1 # settled once, inside the run
|
||||
analyst_turns = [c for c in model.seen if c[0] == "agent"]
|
||||
assert reflections[0][2] < max(c[3] for c in analyst_turns), "settled before the analysts, not alongside"
|
||||
settled = next(e for e in graph.memory_log.load_entries() if e["date"] == "2026-01-02")
|
||||
assert not settled["pending"]
|
||||
lessons = graph.memory_log.get_past_context("NVDA", as_of=TRADE_DATE)
|
||||
assert lessons
|
||||
pm_prompt = next(c[1] for c in model.seen if c[0] == "Portfolio Manager")
|
||||
assert lessons in pm_prompt
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_settlement_failure_is_noted_and_the_run_goes_on(tmp_path, monkeypatch, offline):
|
||||
"""The analysts have spent their tokens by the time the join waits on settlement."""
|
||||
from tradingagents.reporting import write_report_tree
|
||||
|
||||
graph = _graph(tmp_path, monkeypatch, ScriptedModel(structured=True))
|
||||
|
||||
def locked():
|
||||
raise OSError("memory log is locked")
|
||||
|
||||
monkeypatch.setattr(graph, "settle_all_pending", locked)
|
||||
final_state, rating = graph.propagate("NVDA", TRADE_DATE)
|
||||
|
||||
assert rating == "Overweight"
|
||||
assert "could not be settled" in final_state["memory_note"]
|
||||
report = write_report_tree(final_state, "NVDA", tmp_path / "report").read_text()
|
||||
assert "Memory log: Past decisions could not be settled" in report
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_run_settles_other_tickers_due_decisions_too(tmp_path, monkeypatch, offline):
|
||||
"""A ticker a scheduler stopped analysing still gets its decisions settled (#1445)."""
|
||||
from tradingagents.memory import settlement
|
||||
|
||||
graph = _graph(tmp_path, monkeypatch, ScriptedModel(structured=True))
|
||||
graph.memory_log.store_decision("MSFT", "2026-01-02", "Rating: Buy\n\nBuy MSFT.")
|
||||
closes = pd.Series([100.0 + i for i in range(10)], index=pd.bdate_range("2026-01-02", periods=10))
|
||||
monkeypatch.setattr(settlement, "get_closes", lambda *a, **k: closes)
|
||||
|
||||
graph.propagate("NVDA", TRADE_DATE)
|
||||
|
||||
msft = next(e for e in graph.memory_log.load_entries() if e["ticker"] == "MSFT")
|
||||
assert not msft["pending"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_the_memory_step_reads_lessons_as_of_the_trade_date_and_settles_once(tmp_path, monkeypatch, offline):
|
||||
"""A past-dated run reads only lessons known by its date (#1251); a resumed run
|
||||
that re-runs the step settles nothing twice."""
|
||||
from tradingagents.memory import settlement
|
||||
|
||||
graph = _graph(tmp_path, monkeypatch, ScriptedModel(structured=True))
|
||||
graph.memory_log.store_decision("NVDA", "2026-01-02", "Rating: Buy\n\nBuy NVDA.")
|
||||
closes = pd.Series([100.0 + i for i in range(10)], index=pd.bdate_range("2026-01-02", periods=10))
|
||||
monkeypatch.setattr(settlement, "get_closes", lambda *a, **k: closes)
|
||||
asked = []
|
||||
real = graph.memory_log.get_past_context
|
||||
monkeypatch.setattr(graph.memory_log, "get_past_context",
|
||||
lambda ticker, as_of=None, **k: asked.append(as_of) or real(ticker, as_of=as_of, **k))
|
||||
state = {"company_of_interest": "NVDA", "trade_date": "2026-01-20"}
|
||||
|
||||
graph._memory_step(state)
|
||||
log_after_first = (tmp_path / "log.md").read_text()
|
||||
graph._memory_step(state)
|
||||
|
||||
assert asked == ["2026-01-20", "2026-01-20"]
|
||||
assert (tmp_path / "log.md").read_text() == log_after_first
|
||||
|
||||
Reference in New Issue
Block a user