diff --git a/tests/test_alpha_vantage_hardening.py b/tests/test_alpha_vantage_hardening.py index d82ea8dc3..494f2bac6 100644 --- a/tests/test_alpha_vantage_hardening.py +++ b/tests/test_alpha_vantage_hardening.py @@ -3,7 +3,8 @@ Regressions for #990 (no request timeout -> can hang), #991 (invalid-key responses mislabeled as rate limits and silently treated as transient), and #1115 (fundamentals look-ahead filter never ran because the payload is a JSON -string, not a dict). +string, not a dict), and the date trim that keeps post-end_date bars out of a +historical run. """ import json @@ -11,6 +12,7 @@ import pytest import tradingagents.dataflows.alpha_vantage_common as av import tradingagents.dataflows.alpha_vantage_fundamentals as avf +import tradingagents.dataflows.alpha_vantage_stock as avs class _FakeResponse: @@ -94,3 +96,40 @@ def test_fundamentals_no_curr_date_passes_through(monkeypatch): def test_fundamentals_non_json_body_unchanged(monkeypatch): monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: "not-json") assert avf.get_cashflow("AAPL", curr_date="2024-01-01") == "not-json" + + +# --------------------------------------------------------------------------- +# Date trim (see the rationale on the unguarded trim in alpha_vantage_common) +# --------------------------------------------------------------------------- + +_DAILY_CSV = ( + "timestamp,open,high,low,close,volume\n" + "2024-05-13,1,1,1,1,10\n" # after end_date -> must never be served + "2024-05-10,1,1,1,1,10\n" + "2024-05-09,1,1,1,1,10\n" +) + + +@pytest.mark.unit +def test_stock_data_is_trimmed_to_the_requested_window(monkeypatch): + monkeypatch.setattr(avs, "_make_api_request", lambda *a, **k: _DAILY_CSV) + out = avs.get_stock("IBM", "2024-05-09", "2024-05-10") + assert "2024-05-10" in out and "2024-05-09" in out + assert "2024-05-13" not in out, "bar after end_date leaked into the window" + + +@pytest.mark.unit +def test_unparseable_body_is_never_served_untrimmed(monkeypatch): + """The trim used to swallow the failure and return the whole body, putting + bars after end_date into a backtest. It must raise instead.""" + monkeypatch.setattr(avs, "_make_api_request", + lambda *a, **k: "timestamp,close\nnot-a-date,1\n") + + with pytest.raises(ValueError): + avs.get_stock("IBM", "2024-05-09", "2024-05-10") + + +@pytest.mark.unit +def test_empty_body_still_passes_through(monkeypatch): + monkeypatch.setattr(avs, "_make_api_request", lambda *a, **k: "") + assert avs.get_stock("IBM", "2024-05-09", "2024-05-10") == "" diff --git a/tradingagents/dataflows/alpha_vantage_common.py b/tradingagents/dataflows/alpha_vantage_common.py index 237ff54ce..b35feccd9 100644 --- a/tradingagents/dataflows/alpha_vantage_common.py +++ b/tradingagents/dataflows/alpha_vantage_common.py @@ -128,24 +128,18 @@ def _filter_csv_by_date_range(csv_data: str, start_date: str, end_date: str) -> if not csv_data or csv_data.strip() == "": return csv_data - try: - # Parse CSV data - df = pd.read_csv(StringIO(csv_data)) + # Deliberately unguarded: TIME_SERIES_DAILY_ADJUSTED returns the full series + # up to today, so this trim is the only thing keeping bars after end_date out + # of a historical run. Returning the untrimmed body on failure would leak + # future prices, so a parse failure propagates and the caller fails closed. + df = pd.read_csv(StringIO(csv_data)) - # Assume the first column is the date column (timestamp) - date_col = df.columns[0] - df[date_col] = pd.to_datetime(df[date_col]) + # Assume the first column is the date column (timestamp) + date_col = df.columns[0] + df[date_col] = pd.to_datetime(df[date_col]) - # Filter by date range - start_dt = pd.to_datetime(start_date) - end_dt = pd.to_datetime(end_date) + start_dt = pd.to_datetime(start_date) + end_dt = pd.to_datetime(end_date) + filtered_df = df[(df[date_col] >= start_dt) & (df[date_col] <= end_dt)] - filtered_df = df[(df[date_col] >= start_dt) & (df[date_col] <= end_dt)] - - # Convert back to CSV string - return filtered_df.to_csv(index=False) - - except Exception as e: - # If filtering fails, return original data with a warning - print(f"Warning: Failed to filter CSV data by date range: {e}") - return csv_data + return filtered_df.to_csv(index=False)