mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 11:15:24 +03:00
fix(memory): don't settle a decision before its holding window trades
- _fetch_returns settled on min(holding_days, available), so a rerun a day or two after a decision reflected on a 1-2 day partial return as if final - require the full holding window in both the stock and benchmark series before resolving; otherwise leave the entry pending to retry next run - this also makes the #1251 resolution date the full-window date, not a partial bar's #1169
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user