diff --git a/tests/test_alpha_vantage_hardening.py b/tests/test_alpha_vantage_hardening.py index 653c73351..d82ea8dc3 100644 --- a/tests/test_alpha_vantage_hardening.py +++ b/tests/test_alpha_vantage_hardening.py @@ -1,11 +1,16 @@ """Alpha Vantage request hardening. -Regressions for #990 (no request timeout -> can hang) and #991 (invalid-key -responses mislabeled as rate limits and silently treated as transient). +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). """ +import json + import pytest import tradingagents.dataflows.alpha_vantage_common as av +import tradingagents.dataflows.alpha_vantage_fundamentals as avf class _FakeResponse: @@ -52,3 +57,40 @@ def test_invalid_key_not_mislabeled_as_rate_limit(monkeypatch): with pytest.raises(av.AlphaVantageRateLimitError): # sanity: rate-limit path still distinct monkeypatch.setattr(av.requests, "get", _patched_get('{"Note": "API call frequency is 5 calls per minute."}')) av._make_api_request("TIME_SERIES_DAILY", {"symbol": "AAPL"}) + + +_FUNDAMENTALS_JSON = json.dumps({ + "symbol": "AAPL", + "annualReports": [ + {"fiscalDateEnding": "2025-12-31", "totalAssets": "1"}, # future -> must drop + {"fiscalDateEnding": "2023-12-31", "totalAssets": "2"}, # past -> must keep + ], + "quarterlyReports": [ + {"fiscalDateEnding": "2024-06-30", "totalAssets": "3"}, # future -> must drop + {"fiscalDateEnding": "2023-09-30", "totalAssets": "4"}, # past -> must keep + ], +}) + + +@pytest.mark.unit +def test_fundamentals_look_ahead_filter_runs_on_json_string(monkeypatch): + # #1115: the payload arrives as a JSON *string*; the old dict-only guard let + # future-dated fiscal periods leak into historical runs. + monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: _FUNDAMENTALS_JSON) + out = avf.get_balance_sheet("AAPL", curr_date="2024-01-01") + assert isinstance(out, str) # callers still receive a str + parsed = json.loads(out) + assert [r["fiscalDateEnding"] for r in parsed["annualReports"]] == ["2023-12-31"] + assert [r["fiscalDateEnding"] for r in parsed["quarterlyReports"]] == ["2023-09-30"] + + +@pytest.mark.unit +def test_fundamentals_no_curr_date_passes_through(monkeypatch): + monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: _FUNDAMENTALS_JSON) + assert avf.get_income_statement("AAPL") == _FUNDAMENTALS_JSON + + +@pytest.mark.unit +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" diff --git a/tradingagents/dataflows/alpha_vantage_fundamentals.py b/tradingagents/dataflows/alpha_vantage_fundamentals.py index a4ef24c01..90c89f204 100644 --- a/tradingagents/dataflows/alpha_vantage_fundamentals.py +++ b/tradingagents/dataflows/alpha_vantage_fundamentals.py @@ -1,21 +1,30 @@ +import json + from .alpha_vantage_common import _make_api_request def _filter_reports_by_date(result, curr_date: str): - """Filter annualReports/quarterlyReports to exclude entries after curr_date. + """Drop annual/quarterly reports dated after curr_date to prevent look-ahead. - Prevents look-ahead bias by removing fiscal periods that end after - the simulation's current date. + ``_make_api_request`` returns the fundamentals payload as a JSON string, so + parse, filter, and re-serialize. A non-JSON body or an unset ``curr_date`` is + returned unchanged. """ - if not curr_date or not isinstance(result, dict): + if not curr_date or not isinstance(result, str): + return result + try: + payload = json.loads(result) + except json.JSONDecodeError: + return result + if not isinstance(payload, dict): return result for key in ("annualReports", "quarterlyReports"): - if key in result: - result[key] = [ - r for r in result[key] + if isinstance(payload.get(key), list): + payload[key] = [ + r for r in payload[key] if r.get("fiscalDateEnding", "") <= curr_date ] - return result + return json.dumps(payload) def get_fundamentals(ticker: str, curr_date: str = None) -> str: