fix(dataflows): fail closed when the Alpha Vantage date trim fails

- get_stock requests the full daily series up to today, so trimming to the
  requested window is the only thing keeping bars after end_date out of a
  historical run
- the trim caught every exception, warned, and returned the untrimmed body, so
  a parse failure fed future prices into a backtest with no usable signal that
  it had happened
- let a parse failure propagate instead: the routing layer already logs the
  vendor failure, falls through to the next vendor, and surfaces the real error
  if none can serve
This commit is contained in:
Yijia-Xiao
2026-09-07 21:28:52 +00:00
parent 260c899c72
commit 16f7fd613c
2 changed files with 52 additions and 19 deletions

View File

@@ -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") == ""

View File

@@ -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
# 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])
# Filter by date range
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)]
# 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