Skip to content

Commit 4193a00

Browse files
klau1011claude
andauthored
fix: repair the outcome-grading loop and ground decisions in evidence (#5)
* fix: repair the outcome-grading loop and ground decisions in evidence Six issues found reviewing the analysis methodology against the 17-entry decision log in ~/.tradingagents/memory/. Outcome grading was training the system on noise. `_fetch_returns` graded a past call with whatever bars happened to exist rather than the full holding window, so re-running a ticker the next day scored the prior call on one day of price action, wrote a confident reflection from it, and injected that reflection into every later analysis of the ticker. The log had entries tagged `| +5.0% | 1d]` and two more at `4d`. Grading is now all-or-nothing on the full window, and pending entries resolve across every ticker instead of only the one being analyzed — a ticker analyzed once previously stayed pending forever and was never scored. The reflector is told its horizon so it stops turning a short-window wobble into a lesson. The judges never saw the primary evidence. The Research Manager received only the bull/bear transcript, and the Trader only the investment plan — while its system prompt claimed it had the analyst reports. Any fact neither advocate chose to cite was lost before the first decision. Both now receive the four reports via a shared `get_analyst_reports_from_state` helper, with the Research Manager instructed to treat reports as primary evidence over advocacy and to surface material facts neither side raised. The rating scale had collapsed. 17 runs produced 11 Overweight, 4 Underweight, 2 Hold and zero Buy or Sell: the anti-Hold nudge appeared twice with nothing defining the tier above it. Buy and Sell now carry an explicit bar, shared through one `RATING_BAR_GUIDANCE` constant so the two stages cannot drift. `what_would_change_it` makes a rating checkable on re-analysis instead of re-derived from scratch. FRED was serving revised data. The observations query passed no realtime window, so a 2026-02-05 analysis saw January and February CPI — published weeks later — and November's revised value rather than the one known at the time. Pinned to the analysis date. Also adds an optional per-run position note (threaded through the single `resolve_instrument_context` chokepoint, with anti-anchoring wording and bounds at the API boundary), batch ticker runs, form defaults that survive a reload, and a track-record page over the decision log. Fixes a latent test-isolation bug while here: test_web_backend patched one `DEFAULT_CONFIG` object while web.backend.runs held another (importlib.reload in test_ollama_base_url splits them), so those tests globbed the developer's real ~/.tradingagents/logs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfW2XdaNF8rtqW3Nyp5enk * fix: address review findings on the outcome-grading PR Six issues raised by automated review, all reproduced before fixing. **avg_alpha was rendering 100x too large.** `_parse_pct` returns percent units ("+2.0%" -> 2.0) while `hit_rate` is a fraction, and the frontend rescales both by 100 — so an average alpha of 0.98% displayed as 98%. Normalized to a fraction so the two fields share units. **Concurrent runs could lose log updates or abort.** The dashboard runs three analyses as threads in one process and each now resolves the full pending set at start-up, so all three raced on the same read-modify-write through a shared `.tmp` path: one worker's `replace` moved another's file into place and the loser raised FileNotFoundError. Adds a reentrant module lock around the mutating methods and gives each write a pid+thread-unique temp name. Resolution holds the lock across the whole read/reflect/write cycle, so the first worker in does the work and the others find nothing pending instead of paying for the same reflections three times. Verified: without the lock, 10 of 12 concurrent writers fail. **A failing reflection aborted the whole analysis.** Resolution runs before the pipeline, so one transient provider error — or one deterministically un-reflectable old entry — killed every subsequent run and retried the same entry forever. Now isolated per entry: log it, leave it pending, keep going. The blast radius grew with cross-ticker resolution, so this matters more than it did. **Track-record entries sorted by log position, not analysis date.** Any non-future date is accepted, so analysing a historical date after a current one put the older call first while the UI claimed "most recent first". **Oversized batches became unmonitorable.** Past `MAX_RECENT_RUNS` (50) the earliest runs drop out of History with no way to cancel them, and the page had already discarded their IDs. Capped at 25 with an explanatory message. **Sticky settings could submit a removed model.** After a deploy trims the model catalog — which this repo does — a persisted value was restored unvalidated; the `<select>` showed its first option while state held the stale one, so submission sent a model the runner cannot construct. Stale provider, model, language and analyst values now snap to current options on load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfW2XdaNF8rtqW3Nyp5enk * fix(ci): guard the fastapi import and sort imports Two CI failures, both mine. `tests/test_position_context.py` imported `web.backend.api` at module level, but the test job installs only the core dev extras — no fastapi — so collection aborted on every Python version. The repo's convention is `pytest.importorskip("fastapi")` at the top of the file, but that would skip the graph and runner coverage in this module too, which needs no fastapi. The import is now lazy inside the API-boundary class, so those four tests skip while the other seven still run. Also applies `ruff check --fix` to the combined import in the test_web_backend fixture. Verified on a real Python 3.10 with fastapi absent: 668 passed, 8 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfW2XdaNF8rtqW3Nyp5enk * fix: address second review pass **A long market closure could strand an entry as pending forever.** Making the holding-window guard strict left a fixed `holding_days + 10` calendar cutoff behind it, and that span can hold fewer than the required sessions. Verified against live data: a 2025-01-27 Shanghai call spans Lunar New Year and returns exactly 5 bars where 6 are needed, so the window never grows and every retry reads the same incomplete slice — the outcome was available since 2025-02-11. Now queries through today and indexes the Nth bar, which is correct at any closure length. Reading past the trade date is fine here: this scores an already-made decision and feeds no analyst. A genuinely too-recent entry still defers. **Buy and Sell had no valid answer for the change triggers.** Asking for the development that would move the rating "one tier more bullish" is unanswerable at the top of the scale, inviting an invented tier like Strong Buy. Endpoints now get explicit handling: the adjacent move that exists, plus what would confirm holding the endpoint rating. **Track-record rows could share a React key.** Date+rating is not unique — once an outcome resolves, re-analysing that date appends a second entry that can match both — so a refetch flipping one row pending -> scored could reconcile the wrong row. Added the occurrence index. **The track-record page rendered nothing on a failed request.** It read neither `error` nor `isError`, so once the skeleton cleared, a failure looked like missing UI. Now shows the same EmptyState treatment the report page uses. Verified: ruff clean, 682 passed on 3.14 and 669 on a real 3.10 with fastapi absent (CI's condition). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UfW2XdaNF8rtqW3Nyp5enk * fix: address third review pass **Average alpha ignored the call's direction.** A correct bearish call has negative alpha, so the unsigned average subtracted it while the hit rate counted it as a win — the page could report "100% hit rate, -2% avg alpha" for a single correct Underweight. Verified on the real log: the 2026-06-23 PLTR Underweight (-1.8% alpha, a hit) was dragging the average down, showing +0.98% where captured alpha is +1.70%. Now averages direction * alpha, which is the same definition backtest.summarize already uses for mean_alpha; a test pins the two together so they cannot drift. Relabelled in the UI as "Avg captured alpha". **Cancellation could not stop the backlog.** Outcome resolution runs before _stream, which is where cancel_event is checked, and it now walks every pending ticker at a price fetch plus an LLM call each while holding LOG_LOCK — so cancelling a run kept spending. Takes an optional should_stop predicate, checked between entries; the runner passes cancel_event.is_set. Entries already resolved are still written rather than discarded. **Persisted research depth was not reconciled.** The catalog check added last pass covered provider, models, language and analysts but not depth, so a stale or hand-edited value (e.g. 2) stayed in state and 422'd at StartRunRequest._v_depth. Now validated against opts.research_depths. Verified: ruff clean, 686 passed on 3.14, 673 on py3.10 with fastapi absent. * fix: skip outcome fetches that cannot have matured A same-day batch made outcome resolution quadratic. Every finished run appends a pending entry, and resolution now walks all tickers, so run N probed all N-1 earlier entries even though five trading sessions plainly could not have elapsed. At the 25-ticker batch cap that is ~600 yfinance requests guaranteed to return None — and because the entries stay pending, the whole sweep repeated on every run for the following week, all serialized under LOG_LOCK and delaying every queued analysis. The batch feature added earlier in this PR is what made it bite. Adds `_window_could_have_closed`: pure calendar arithmetic, no network. Trading sessions can never outnumber calendar days, so a False means the outcome definitely is not available yet, while True only means it might be — the bar count in `_fetch_returns` remains the authority. Conservative in the safe direction, so it never skips a resolvable entry, and an unparseable date fails open rather than dropping a malformed tag forever. The holding period moves to a `HOLDING_DAYS` class constant shared by the pre-check and the fetch default, so the two cannot drift. Confirmed against the real log: all 12 currently pending entries remain eligible; only genuinely immature ones are suppressed. Verified: ruff clean, 688 passed on 3.14, 675 on py3.10 with fastapi absent. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 0ed1d4e commit 4193a00

29 files changed

Lines changed: 1458 additions & 143 deletions

tests/test_fred.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ def _capture(path, params):
150150
self.assertEqual(obs_params["observation_end"], "2025-09-30")
151151
self.assertEqual(obs_params["observation_start"], "2025-07-02") # 90d back
152152

153+
def test_vintage_is_pinned_to_curr_date(self):
154+
"""Without a realtime window FRED returns today's revised figures, and
155+
observation_end filters by the period a figure describes rather than
156+
when it was published — so January CPI leaks into a mid-January date."""
157+
captured = {}
158+
159+
def _capture(path, params):
160+
captured[path] = params
161+
return _META if path == "series" else _OBS
162+
163+
with mock.patch.object(fred, "_request", side_effect=_capture):
164+
fred.get_macro_data("cpi", "2025-09-30", 90)
165+
obs_params = captured["series/observations"]
166+
self.assertEqual(obs_params["realtime_start"], "2025-09-30")
167+
self.assertEqual(obs_params["realtime_end"], "2025-09-30")
168+
153169

154170
@pytest.mark.unit
155171
class FredRoutingTests(unittest.TestCase):

tests/test_memory_log.py

Lines changed: 256 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -432,17 +432,57 @@ def test_update_preserves_other_entries(self, tmp_path):
432432
assert msft["ticker"] == "MSFT" and msft["pending"] is True
433433

434434
def test_update_atomic_write(self, tmp_path):
435-
"""A pre-existing .tmp file is overwritten; the log is correctly updated."""
435+
"""The write leaves no temp file behind and a stale one can't break it.
436+
437+
Temp names are unique per pid+thread, so an unrelated leftover is simply
438+
ignored rather than being moved into place over the real log.
439+
"""
436440
log = make_log(tmp_path)
437441
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
438442
stale_tmp = tmp_path / "trading_memory.tmp"
439-
stale_tmp.write_text("GARBAGE CONTENT — should be overwritten", encoding="utf-8")
443+
stale_tmp.write_text("GARBAGE CONTENT", encoding="utf-8")
440444
log.update_with_outcome("NVDA", "2026-01-10", 0.042, 0.021, 5, "Correct.")
441-
assert not stale_tmp.exists()
442445
entries = log.load_entries()
443446
assert len(entries) == 1
444447
assert entries[0]["reflection"] == "Correct."
445448
assert entries[0]["pending"] is False
449+
# No temp file of ours survives the write.
450+
assert [p.name for p in tmp_path.glob("*.tmp")] == ["trading_memory.tmp"]
451+
452+
def test_concurrent_writers_do_not_lose_updates(self, tmp_path):
453+
"""Three workers resolving at once must not drop each other's writes.
454+
455+
The dashboard runs up to three analyses as threads in one process, and
456+
each resolves pending entries at start-up. With a shared temp path and no
457+
lock, one worker's replace() moves another's file into place and the
458+
loser raises FileNotFoundError.
459+
"""
460+
import threading
461+
462+
log = make_log(tmp_path)
463+
tickers = [f"TICK{i}" for i in range(12)]
464+
for t in tickers:
465+
log.store_decision(t, "2026-01-10", DECISION_BUY)
466+
467+
errors = []
468+
469+
def resolve(ticker):
470+
try:
471+
log.update_with_outcome(ticker, "2026-01-10", 0.01, 0.01, 5, f"r-{ticker}")
472+
except Exception as e: # pragma: no cover - only on a regression
473+
errors.append(e)
474+
475+
threads = [threading.Thread(target=resolve, args=(t,)) for t in tickers]
476+
for th in threads:
477+
th.start()
478+
for th in threads:
479+
th.join()
480+
481+
assert errors == []
482+
entries = log.load_entries()
483+
assert len(entries) == len(tickers)
484+
assert log.get_pending_entries() == [], "an update was lost to a race"
485+
assert {e["reflection"] for e in entries} == {f"r-{t}" for t in tickers}
446486

447487
def test_update_noop_when_no_log_path(self):
448488
log = TradingMemoryLog(config=None)
@@ -529,7 +569,11 @@ def test_fetch_returns_delisted(self):
529569
assert raw is None and alpha is None and days is None
530570

531571
def test_fetch_returns_spy_shorter_than_stock(self):
532-
"""SPY having fewer rows than the stock must not raise IndexError."""
572+
"""A short benchmark series defers the grade instead of truncating it.
573+
574+
Must not raise IndexError, and must not grade a 5-day call on 2 days of
575+
benchmark data.
576+
"""
533577
stock_prices = [100.0, 102.0, 104.0, 103.0, 105.0, 106.0]
534578
spy_prices = [400.0, 402.0, 403.0]
535579
mock_graph = MagicMock(spec=TradingAgentsGraph)
@@ -540,8 +584,57 @@ def _make_ticker(sym):
540584
return m
541585
mock_ticker_cls.side_effect = _make_ticker
542586
raw, alpha, days = TradingAgentsGraph._fetch_returns(mock_graph, "NVDA", "2026-01-05")
543-
assert raw is not None and alpha is not None and days is not None
544-
assert days == 2
587+
assert (raw, alpha, days) == (None, None, None)
588+
589+
def test_fetch_returns_defers_partial_window(self):
590+
"""Re-running a ticker the next day must not grade the prior call on 1 day.
591+
592+
Regression for the `[... | +3.8% | +5.0% | 1d]` entries: partial windows
593+
were graded and their reflections fed back into later analyses.
594+
"""
595+
prices = [100.0, 102.0] # trade date + 1 bar
596+
mock_graph = MagicMock(spec=TradingAgentsGraph)
597+
with patch("yfinance.Ticker") as mock_ticker_cls:
598+
m = MagicMock()
599+
m.history.return_value = _price_df(prices)
600+
mock_ticker_cls.return_value = m
601+
raw, alpha, days = TradingAgentsGraph._fetch_returns(mock_graph, "NOW", "2026-06-22")
602+
assert (raw, alpha, days) == (None, None, None)
603+
604+
def test_fetch_returns_queries_through_today(self):
605+
"""The request window must not be a fixed calendar span.
606+
607+
A long closure (Lunar New Year for a Shanghai listing) can leave fewer
608+
than holding_days sessions inside a fixed cutoff, and because that window
609+
never grows the strict guard would defer the entry forever.
610+
"""
611+
captured = {}
612+
mock_graph = MagicMock(spec=TradingAgentsGraph)
613+
with patch("yfinance.Ticker") as mock_ticker_cls:
614+
def _make_ticker(sym):
615+
m = MagicMock()
616+
617+
def _history(start, end):
618+
captured["end"] = end
619+
return _price_df([100.0] * 6)
620+
m.history.side_effect = _history
621+
return m
622+
mock_ticker_cls.side_effect = _make_ticker
623+
TradingAgentsGraph._fetch_returns(mock_graph, "600519.SS", "2025-01-27")
624+
# Far beyond 2025-01-27 + 15 days, which held only 5 sessions.
625+
assert captured["end"] > "2025-02-11"
626+
627+
def test_fetch_returns_grades_full_window_only(self):
628+
"""Exactly holding_days+1 bars resolves, and always at the full horizon."""
629+
prices = [100.0, 101.0, 102.0, 103.0, 104.0, 110.0]
630+
mock_graph = MagicMock(spec=TradingAgentsGraph)
631+
with patch("yfinance.Ticker") as mock_ticker_cls:
632+
m = MagicMock()
633+
m.history.return_value = _price_df(prices)
634+
mock_ticker_cls.return_value = m
635+
raw, alpha, days = TradingAgentsGraph._fetch_returns(mock_graph, "NOW", "2026-06-22")
636+
assert days == 5
637+
assert raw == pytest.approx(0.10) # measured to the last bar, not an earlier one
545638

546639
# TradingAgentsGraph._resolve_benchmark — picks index for alpha calc
547640

@@ -642,16 +735,166 @@ def test_reflector_defaults_to_spy_for_unupdated_callers(self):
642735

643736
# TradingAgentsGraph._resolve_pending_entries
644737

645-
def test_resolve_skips_other_tickers(self, tmp_path):
646-
"""Pending AAPL entry is not resolved when the run is for NVDA."""
738+
def test_resolve_covers_every_pending_ticker(self, tmp_path):
739+
"""A ticker analyzed once still gets scored on a later run of another ticker.
740+
741+
Scoping resolution to the ticker being analyzed strands single-run
742+
tickers as pending forever.
743+
"""
647744
log = make_log(tmp_path)
648745
log.store_decision("AAPL", "2026-01-10", DECISION_BUY)
746+
log.store_decision("NVDA", "2026-01-10", DECISION_BUY)
747+
mock_reflector = MagicMock()
748+
mock_reflector.reflect_on_final_decision.return_value = "Held up."
649749
mock_graph = MagicMock(spec=TradingAgentsGraph)
650750
mock_graph.memory_log = log
751+
mock_graph.reflector = mock_reflector
752+
mock_graph._resolve_benchmark = MagicMock(return_value="SPY")
651753
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
652-
TradingAgentsGraph._resolve_pending_entries(mock_graph, "NVDA")
653-
mock_graph._fetch_returns.assert_not_called()
654-
assert len(log.get_pending_entries()) == 1
754+
TradingAgentsGraph._resolve_pending_entries(mock_graph)
755+
assert log.get_pending_entries() == []
756+
assert {e["ticker"] for e in log.load_entries()} == {"AAPL", "NVDA"}
757+
758+
def test_reflection_failure_leaves_entry_pending_without_aborting(self, tmp_path):
759+
"""A bad old entry must not block the analysis the user is waiting on.
760+
761+
Resolution runs at the start of every run, so an unhandled provider error
762+
on one stale entry would abort each subsequent run before its pipeline
763+
started — and retry the same entry forever.
764+
"""
765+
log = make_log(tmp_path)
766+
# The reflector only receives the decision text, so identify the entries
767+
# through it rather than by ticker.
768+
log.store_decision("AAPL", "2026-01-10", "Rating: Buy\nAAPL thesis.")
769+
log.store_decision("NVDA", "2026-01-10", "Rating: Buy\nNVDA thesis.")
770+
771+
def _reflect(**kw):
772+
if "AAPL" in kw["final_decision"]:
773+
raise RuntimeError("provider 503")
774+
return "NVDA held up."
775+
776+
mock_reflector = MagicMock()
777+
mock_reflector.reflect_on_final_decision.side_effect = _reflect
778+
mock_graph = MagicMock(spec=TradingAgentsGraph)
779+
mock_graph.memory_log = log
780+
mock_graph.reflector = mock_reflector
781+
mock_graph._resolve_benchmark = MagicMock(return_value="SPY")
782+
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
783+
784+
# Must not raise.
785+
TradingAgentsGraph._resolve_pending_entries(mock_graph)
786+
787+
pending = {e["ticker"] for e in log.get_pending_entries()}
788+
assert pending == {"AAPL"}, "failed entry should stay pending"
789+
resolved = [e for e in log.load_entries() if not e["pending"]]
790+
assert [e["ticker"] for e in resolved] == ["NVDA"]
791+
assert resolved[0]["reflection"] == "NVDA held up."
792+
793+
def test_resolve_skips_immature_entries_without_fetching(self, tmp_path):
794+
"""Same-day entries must cost zero network calls.
795+
796+
Each finished run in a batch appends a pending entry, so without a
797+
maturity pre-check run N re-probes all N-1 earlier ones — quadratic in
798+
batch size, every probe certain to return None, all under LOG_LOCK.
799+
"""
800+
from datetime import datetime, timedelta
801+
802+
log = make_log(tmp_path)
803+
today = datetime.now().date()
804+
# A same-day batch: 8 entries that cannot possibly have matured.
805+
for i in range(8):
806+
log.store_decision(f"T{i}", today.strftime("%Y-%m-%d"), DECISION_BUY)
807+
# Plus one old enough to be worth checking.
808+
old = (today - timedelta(days=30)).strftime("%Y-%m-%d")
809+
log.store_decision("OLD", old, DECISION_BUY)
810+
811+
mock_reflector = MagicMock()
812+
mock_reflector.reflect_on_final_decision.return_value = "done"
813+
mock_graph = MagicMock(spec=TradingAgentsGraph)
814+
mock_graph.HOLDING_DAYS = TradingAgentsGraph.HOLDING_DAYS
815+
mock_graph._window_could_have_closed = (
816+
lambda d: TradingAgentsGraph._window_could_have_closed(mock_graph, d)
817+
)
818+
mock_graph.memory_log = log
819+
mock_graph.reflector = mock_reflector
820+
mock_graph._resolve_benchmark = MagicMock(return_value="SPY")
821+
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
822+
823+
TradingAgentsGraph._resolve_pending_entries(mock_graph)
824+
825+
# Only the mature entry was fetched — not the 8 same-day ones.
826+
assert mock_graph._fetch_returns.call_count == 1
827+
assert mock_graph._fetch_returns.call_args[0][0] == "OLD"
828+
assert {e["ticker"] for e in log.get_pending_entries()} == {
829+
f"T{i}" for i in range(8)
830+
}
831+
832+
def test_window_could_have_closed_is_conservative(self):
833+
"""False only when maturity is impossible; unparseable dates fail open."""
834+
from datetime import datetime, timedelta
835+
836+
mock_graph = MagicMock(spec=TradingAgentsGraph)
837+
mock_graph.HOLDING_DAYS = 5
838+
check = TradingAgentsGraph._window_could_have_closed
839+
today = datetime.now().date()
840+
841+
def at(days_ago):
842+
return check(mock_graph, (today - timedelta(days=days_ago)).strftime("%Y-%m-%d"))
843+
844+
assert at(0) is False
845+
assert at(4) is False
846+
# 5 calendar days is the earliest 5 sessions could have passed; the bar
847+
# count in _fetch_returns is what actually decides.
848+
assert at(5) is True
849+
assert at(30) is True
850+
assert check(mock_graph, "not-a-date") is True
851+
852+
def test_resolve_honours_cancellation_and_keeps_finished_work(self, tmp_path):
853+
"""Cancelling must stop the backlog, not run it to completion.
854+
855+
This phase precedes the graph (where cancellation is handled) and costs a
856+
price fetch plus an LLM call per entry, so a cancelled run would otherwise
857+
keep spending. Work already paid for is still written.
858+
"""
859+
log = make_log(tmp_path)
860+
for i in range(6):
861+
log.store_decision(f"T{i}", "2026-01-10", DECISION_BUY)
862+
863+
mock_reflector = MagicMock()
864+
mock_reflector.reflect_on_final_decision.return_value = "done"
865+
mock_graph = MagicMock(spec=TradingAgentsGraph)
866+
mock_graph.memory_log = log
867+
mock_graph.reflector = mock_reflector
868+
mock_graph._resolve_benchmark = MagicMock(return_value="SPY")
869+
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
870+
871+
calls = {"n": 0}
872+
873+
def _stop():
874+
calls["n"] += 1
875+
return calls["n"] > 2 # allow two entries through, then cancel
876+
877+
TradingAgentsGraph._resolve_pending_entries(mock_graph, should_stop=_stop)
878+
879+
assert mock_reflector.reflect_on_final_decision.call_count == 2
880+
assert len(log.get_pending_entries()) == 4 # remainder retried next run
881+
resolved = [e for e in log.load_entries() if not e["pending"]]
882+
assert len(resolved) == 2, "already-paid-for work must still be written"
883+
884+
def test_resolve_without_should_stop_processes_everything(self, tmp_path):
885+
"""The predicate is optional — omitting it resolves the whole backlog."""
886+
log = make_log(tmp_path)
887+
for i in range(3):
888+
log.store_decision(f"T{i}", "2026-01-10", DECISION_BUY)
889+
mock_reflector = MagicMock()
890+
mock_reflector.reflect_on_final_decision.return_value = "done"
891+
mock_graph = MagicMock(spec=TradingAgentsGraph)
892+
mock_graph.memory_log = log
893+
mock_graph.reflector = mock_reflector
894+
mock_graph._resolve_benchmark = MagicMock(return_value="SPY")
895+
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
896+
TradingAgentsGraph._resolve_pending_entries(mock_graph)
897+
assert log.get_pending_entries() == []
655898

656899
def test_resolve_marks_entry_completed(self, tmp_path):
657900
"""After resolve, get_pending_entries() is empty and the entry has a REFLECTION."""
@@ -662,8 +905,9 @@ def test_resolve_marks_entry_completed(self, tmp_path):
662905
mock_graph = MagicMock(spec=TradingAgentsGraph)
663906
mock_graph.memory_log = log
664907
mock_graph.reflector = mock_reflector
908+
mock_graph._resolve_benchmark = MagicMock(return_value="SPY")
665909
mock_graph._fetch_returns = MagicMock(return_value=(0.05, 0.02, 5))
666-
TradingAgentsGraph._resolve_pending_entries(mock_graph, "NVDA")
910+
TradingAgentsGraph._resolve_pending_entries(mock_graph)
667911
assert log.get_pending_entries() == []
668912
entries = log.load_entries()
669913
assert len(entries) == 1

0 commit comments

Comments
 (0)