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.
|
"""Minimal DataFrame matching yfinance .history() output shape.
|
||||||
|
|
||||||
Uses a DatetimeIndex like real yfinance output, so resolution-date
|
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")
|
idx = pd.date_range(start=start, periods=len(prices), freq="D")
|
||||||
return pd.DataFrame({"Close": prices}, index=idx)
|
return pd.DataFrame({"Close": prices}, index=idx)
|
||||||
@@ -529,9 +529,10 @@ class TestDeferredReflection:
|
|||||||
assert (raw, alpha, days, resolved) == (None, None, None, None)
|
assert (raw, alpha, days, resolved) == (None, None, None, None)
|
||||||
|
|
||||||
def test_fetch_returns_spy_shorter_than_stock(self):
|
def test_fetch_returns_spy_shorter_than_stock(self):
|
||||||
"""SPY having fewer rows than the stock must not raise IndexError."""
|
"""SPY having fewer rows than the stock (but still a full window) must
|
||||||
stock_prices = [100.0, 102.0, 104.0, 103.0, 105.0, 106.0]
|
not raise IndexError."""
|
||||||
spy_prices = [400.0, 402.0, 403.0]
|
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)
|
mock_graph = MagicMock(spec=TradingAgentsGraph)
|
||||||
with patch("yfinance.Ticker") as mock_ticker_cls:
|
with patch("yfinance.Ticker") as mock_ticker_cls:
|
||||||
def _make_ticker(sym):
|
def _make_ticker(sym):
|
||||||
@@ -540,9 +541,25 @@ class TestDeferredReflection:
|
|||||||
return m
|
return m
|
||||||
mock_ticker_cls.side_effect = _make_ticker
|
mock_ticker_cls.side_effect = _make_ticker
|
||||||
raw, alpha, days, resolved = TradingAgentsGraph._fetch_returns(mock_graph, "NVDA", "2026-01-05")
|
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 raw is not None and alpha is not None
|
||||||
assert days == 2
|
assert days == 5 # full holding window used for both series
|
||||||
assert resolved == "2026-01-07" # 2 sessions after the trade date
|
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
|
# TradingAgentsGraph._resolve_benchmark — picks index for alpha calc
|
||||||
|
|
||||||
@@ -673,6 +690,20 @@ class TestDeferredReflection:
|
|||||||
assert "+5.0%" in entries[0]["raw"]
|
assert "+5.0%" in entries[0]["raw"]
|
||||||
assert "+2.0%" in entries[0]["alpha"]
|
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
|
# 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
|
``benchmark`` is the index used as the alpha baseline (resolved by the
|
||||||
caller via ``_resolve_benchmark``). Returns ``(raw_return, alpha_return,
|
caller via ``_resolve_benchmark``). Returns ``(raw_return, alpha_return,
|
||||||
actual_holding_days, resolution_date)`` — where ``resolution_date`` is
|
holding_days, resolution_date)`` — where ``resolution_date`` is the date
|
||||||
the date of the last price bar used, i.e. when the outcome became known
|
of the last price bar used, i.e. when the outcome became known (#1251) —
|
||||||
(#1251) — or ``(None, None, None, None)`` if price data is unavailable
|
or ``(None, None, None, None)`` when the outcome cannot be settled yet:
|
||||||
(too recent, delisted, or network error).
|
the full holding window has not traded (#1169), or the symbol is delisted
|
||||||
|
or unreachable.
|
||||||
"""
|
"""
|
||||||
from tradingagents.dataflows.symbol_utils import normalize_symbol
|
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)
|
stock = yf.Ticker(normalize_symbol(ticker)).history(start=trade_date, end=end_str)
|
||||||
bench = yf.Ticker(benchmark).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
|
return None, None, None, None
|
||||||
|
|
||||||
actual_days = min(holding_days, len(stock) - 1, len(bench) - 1)
|
|
||||||
raw = float(
|
raw = float(
|
||||||
(stock["Close"].iloc[actual_days] - stock["Close"].iloc[0])
|
(stock["Close"].iloc[holding_days] - stock["Close"].iloc[0])
|
||||||
/ stock["Close"].iloc[0]
|
/ stock["Close"].iloc[0]
|
||||||
)
|
)
|
||||||
bench_ret = float(
|
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]
|
/ bench["Close"].iloc[0]
|
||||||
)
|
)
|
||||||
alpha = raw - bench_ret
|
alpha = raw - bench_ret
|
||||||
# The date of the last price bar used is when this outcome became
|
# The date of the last price bar used is when this outcome became
|
||||||
# known — the point-in-time cutoff for injecting the lesson (#1251).
|
# known — the point-in-time cutoff for injecting the lesson (#1251).
|
||||||
resolution_date = stock.index[actual_days].strftime("%Y-%m-%d")
|
resolution_date = stock.index[holding_days].strftime("%Y-%m-%d")
|
||||||
return raw, alpha, actual_days, resolution_date
|
return raw, alpha, holding_days, resolution_date
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Could not resolve outcome for %s on %s vs %s (will retry next run): %s",
|
"Could not resolve outcome for %s on %s vs %s (will retry next run): %s",
|
||||||
|
|||||||
Reference in New Issue
Block a user