diff --git a/tests/test_memory_log.py b/tests/test_memory_log.py index 34bf0c4d1..3db0ac531 100644 --- a/tests/test_memory_log.py +++ b/tests/test_memory_log.py @@ -58,7 +58,7 @@ def _price_df(prices, start="2026-01-05"): """Minimal DataFrame matching yfinance .history() output shape. Uses a DatetimeIndex like real yfinance output, so resolution-date - extraction (stock.index[actual_days]) works (#1251). + extraction (stock.index[holding_days]) works (#1251). """ idx = pd.date_range(start=start, periods=len(prices), freq="D") return pd.DataFrame({"Close": prices}, index=idx) @@ -529,9 +529,10 @@ class TestDeferredReflection: assert (raw, alpha, days, resolved) == (None, None, None, None) def test_fetch_returns_spy_shorter_than_stock(self): - """SPY having fewer rows than the stock must not raise IndexError.""" - stock_prices = [100.0, 102.0, 104.0, 103.0, 105.0, 106.0] - spy_prices = [400.0, 402.0, 403.0] + """SPY having fewer rows than the stock (but still a full window) must + not raise IndexError.""" + stock_prices = [100.0, 102.0, 104.0, 103.0, 105.0, 106.0, 107.0, 108.0] # 8 rows + spy_prices = [400.0, 402.0, 403.0, 405.0, 406.0, 407.0] # 6 rows mock_graph = MagicMock(spec=TradingAgentsGraph) with patch("yfinance.Ticker") as mock_ticker_cls: def _make_ticker(sym): @@ -540,9 +541,25 @@ class TestDeferredReflection: return m mock_ticker_cls.side_effect = _make_ticker raw, alpha, days, resolved = TradingAgentsGraph._fetch_returns(mock_graph, "NVDA", "2026-01-05") - assert raw is not None and alpha is not None and days is not None - assert days == 2 - assert resolved == "2026-01-07" # 2 sessions after the trade date + assert raw is not None and alpha is not None + assert days == 5 # full holding window used for both series + assert resolved == "2026-01-10" + + def test_fetch_returns_incomplete_window_stays_pending(self): + """#1169: a rerun before the full holding window has traded returns + unavailable (all-None) so the entry stays pending, rather than settling + on a premature partial return.""" + stock_prices = [100.0, 102.0, 104.0] # only 3 rows; holding window is 5 + spy_prices = [400.0, 402.0, 404.0] + mock_graph = MagicMock(spec=TradingAgentsGraph) + with patch("yfinance.Ticker") as mock_ticker_cls: + def _make_ticker(sym): + m = MagicMock() + m.history.return_value = _price_df(spy_prices if sym == "SPY" else stock_prices) + return m + mock_ticker_cls.side_effect = _make_ticker + result = TradingAgentsGraph._fetch_returns(mock_graph, "NVDA", "2026-01-05") + assert result == (None, None, None, None) # TradingAgentsGraph._resolve_benchmark — picks index for alpha calc @@ -673,6 +690,20 @@ class TestDeferredReflection: assert "+5.0%" in entries[0]["raw"] assert "+2.0%" in entries[0]["alpha"] + def test_resolve_leaves_premature_entry_pending(self, tmp_path): + """#1169: when the outcome can't be settled yet (_fetch_returns None), + the entry stays pending and the reflector is never called.""" + log = make_log(tmp_path) + log.store_decision("NVDA", "2026-01-05", DECISION_BUY) + mock_reflector = MagicMock() + mock_graph = MagicMock(spec=TradingAgentsGraph) + mock_graph.memory_log = log + mock_graph.reflector = mock_reflector + mock_graph._fetch_returns = MagicMock(return_value=(None, None, None, None)) + TradingAgentsGraph._resolve_pending_entries(mock_graph, "NVDA") + assert len(log.get_pending_entries()) == 1 # still pending + mock_reflector.reflect_on_final_decision.assert_not_called() + # --------------------------------------------------------------------------- # Portfolio Manager injection: past_context in state and prompt diff --git a/tradingagents/graph/trading_graph.py b/tradingagents/graph/trading_graph.py index c2a9418c1..bf6c02827 100644 --- a/tradingagents/graph/trading_graph.py +++ b/tradingagents/graph/trading_graph.py @@ -278,10 +278,11 @@ class TradingAgentsGraph: ``benchmark`` is the index used as the alpha baseline (resolved by the caller via ``_resolve_benchmark``). Returns ``(raw_return, alpha_return, - actual_holding_days, resolution_date)`` — where ``resolution_date`` is - the date of the last price bar used, i.e. when the outcome became known - (#1251) — or ``(None, None, None, None)`` if price data is unavailable - (too recent, delisted, or network error). + holding_days, resolution_date)`` — where ``resolution_date`` is the date + of the last price bar used, i.e. when the outcome became known (#1251) — + or ``(None, None, None, None)`` when the outcome cannot be settled yet: + the full holding window has not traded (#1169), or the symbol is delisted + or unreachable. """ from tradingagents.dataflows.symbol_utils import normalize_symbol @@ -296,23 +297,25 @@ class TradingAgentsGraph: stock = yf.Ticker(normalize_symbol(ticker)).history(start=trade_date, end=end_str) bench = yf.Ticker(benchmark).history(start=trade_date, end=end_str) - if len(stock) < 2 or len(bench) < 2: + # Require the full holding window in both series. A rerun before it + # has traded leaves the entry pending to retry next run, rather than + # settling on a premature partial return (#1169). + if len(stock) <= holding_days or len(bench) <= holding_days: return None, None, None, None - actual_days = min(holding_days, len(stock) - 1, len(bench) - 1) raw = float( - (stock["Close"].iloc[actual_days] - stock["Close"].iloc[0]) + (stock["Close"].iloc[holding_days] - stock["Close"].iloc[0]) / stock["Close"].iloc[0] ) bench_ret = float( - (bench["Close"].iloc[actual_days] - bench["Close"].iloc[0]) + (bench["Close"].iloc[holding_days] - bench["Close"].iloc[0]) / bench["Close"].iloc[0] ) alpha = raw - bench_ret # The date of the last price bar used is when this outcome became # known — the point-in-time cutoff for injecting the lesson (#1251). - resolution_date = stock.index[actual_days].strftime("%Y-%m-%d") - return raw, alpha, actual_days, resolution_date + resolution_date = stock.index[holding_days].strftime("%Y-%m-%d") + return raw, alpha, holding_days, resolution_date except Exception as e: logger.warning( "Could not resolve outcome for %s on %s vs %s (will retry next run): %s",