mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-08-01 19:34:24 +03:00
fix(dataflows): apply the Alpha Vantage fundamentals look-ahead filter
- the payload is a JSON string, so the dict-only guard skipped filtering and future-dated reports leaked into historical runs, breaking the #475 guarantee - parse before filtering; non-JSON bodies and an unset curr_date pass through #1115
This commit is contained in:
@@ -1,11 +1,16 @@
|
|||||||
"""Alpha Vantage request hardening.
|
"""Alpha Vantage request hardening.
|
||||||
|
|
||||||
Regressions for #990 (no request timeout -> can hang) and #991 (invalid-key
|
Regressions for #990 (no request timeout -> can hang), #991 (invalid-key
|
||||||
responses mislabeled as rate limits and silently treated as transient).
|
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 pytest
|
||||||
|
|
||||||
import tradingagents.dataflows.alpha_vantage_common as av
|
import tradingagents.dataflows.alpha_vantage_common as av
|
||||||
|
import tradingagents.dataflows.alpha_vantage_fundamentals as avf
|
||||||
|
|
||||||
|
|
||||||
class _FakeResponse:
|
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
|
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."}'))
|
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"})
|
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"
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
from .alpha_vantage_common import _make_api_request
|
from .alpha_vantage_common import _make_api_request
|
||||||
|
|
||||||
|
|
||||||
def _filter_reports_by_date(result, curr_date: str):
|
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
|
``_make_api_request`` returns the fundamentals payload as a JSON string, so
|
||||||
the simulation's current date.
|
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
|
return result
|
||||||
for key in ("annualReports", "quarterlyReports"):
|
for key in ("annualReports", "quarterlyReports"):
|
||||||
if key in result:
|
if isinstance(payload.get(key), list):
|
||||||
result[key] = [
|
payload[key] = [
|
||||||
r for r in result[key]
|
r for r in payload[key]
|
||||||
if r.get("fiscalDateEnding", "") <= curr_date
|
if r.get("fiscalDateEnding", "") <= curr_date
|
||||||
]
|
]
|
||||||
return result
|
return json.dumps(payload)
|
||||||
|
|
||||||
|
|
||||||
def get_fundamentals(ticker: str, curr_date: str = None) -> str:
|
def get_fundamentals(ticker: str, curr_date: str = None) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user