mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 15:02:39 +03:00
refactor: name the data layer's date argument as_of_date
- vendor functions and date_window take as_of_date, the date data is served as of; the model-facing tool arguments are unchanged - build_instrument_context and resolve_instrument_context take trade_date, which is what they receive
This commit is contained in:
@@ -82,7 +82,7 @@ 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
|
# #1115: the payload arrives as a JSON *string*; the old dict-only guard let
|
||||||
# future-dated fiscal periods leak into historical runs.
|
# future-dated fiscal periods leak into historical runs.
|
||||||
monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: _FUNDAMENTALS_JSON)
|
monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: _FUNDAMENTALS_JSON)
|
||||||
out = avf.get_balance_sheet("AAPL", curr_date="2024-01-01")
|
out = avf.get_balance_sheet("AAPL", as_of_date="2024-01-01")
|
||||||
assert isinstance(out, str) # callers still receive a str
|
assert isinstance(out, str) # callers still receive a str
|
||||||
parsed = json.loads(out)
|
parsed = json.loads(out)
|
||||||
assert [r["fiscalDateEnding"] for r in parsed["annualReports"]] == ["2023-12-31"]
|
assert [r["fiscalDateEnding"] for r in parsed["annualReports"]] == ["2023-12-31"]
|
||||||
@@ -98,7 +98,7 @@ def test_fundamentals_no_curr_date_passes_through(monkeypatch):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_fundamentals_non_json_body_unchanged(monkeypatch):
|
def test_fundamentals_non_json_body_unchanged(monkeypatch):
|
||||||
monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: "not-json")
|
monkeypatch.setattr(avf, "_make_api_request", lambda fn, params: "not-json")
|
||||||
assert avf.get_cashflow("AAPL", curr_date="2024-01-01") == "not-json"
|
assert avf.get_cashflow("AAPL", as_of_date="2024-01-01") == "not-json"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
+6
-6
@@ -141,7 +141,7 @@ class FredFormattingTests(unittest.TestCase):
|
|||||||
self.assertEqual(len(body_rows), fred.MAX_ROWS)
|
self.assertEqual(len(body_rows), fred.MAX_ROWS)
|
||||||
|
|
||||||
def test_window_is_lookahead_safe(self):
|
def test_window_is_lookahead_safe(self):
|
||||||
# observation_end must equal curr_date so a past date never pulls future data.
|
# observation_end must equal as_of_date so a past date never pulls future data.
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def _capture(path, params):
|
def _capture(path, params):
|
||||||
@@ -156,9 +156,9 @@ class FredFormattingTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_requests_pin_the_data_vintage(self):
|
def test_requests_pin_the_data_vintage(self):
|
||||||
# #1275: both the metadata and observations requests must pin the vintage
|
# #1275: both the metadata and observations requests must pin the vintage
|
||||||
# to curr_date (clamped to FRED's today), or FRED serves the latest
|
# to as_of_date (clamped to FRED's today), or FRED serves the latest
|
||||||
# revision and revision-prone series leak future information. A past
|
# revision and revision-prone series leak future information. A past
|
||||||
# curr_date sits below FRED's today, so it pins through unchanged.
|
# as_of_date sits below FRED's today, so it pins through unchanged.
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def _capture(path, params):
|
def _capture(path, params):
|
||||||
@@ -174,11 +174,11 @@ class FredFormattingTests(unittest.TestCase):
|
|||||||
self.assertEqual(captured[path]["realtime_end"], "2025-09-30", path)
|
self.assertEqual(captured[path]["realtime_end"], "2025-09-30", path)
|
||||||
|
|
||||||
def test_future_curr_date_clamps_vintage_to_fred_today(self):
|
def test_future_curr_date_clamps_vintage_to_fred_today(self):
|
||||||
# #1275 regression: on a live run curr_date is the caller's LOCAL date,
|
# #1275 regression: on a live run as_of_date is the caller's LOCAL date,
|
||||||
# which can be a day ahead of FRED's US-Central clock. Pinning the vintage
|
# which can be a day ahead of FRED's US-Central clock. Pinning the vintage
|
||||||
# to that future date 400s, and the routing layer then drops macro data
|
# to that future date 400s, and the routing layer then drops macro data
|
||||||
# silently. The pin must clamp to FRED's today; the observation window
|
# silently. The pin must clamp to FRED's today; the observation window
|
||||||
# (future bars can't exist yet) stays at curr_date.
|
# (future bars can't exist yet) stays at as_of_date.
|
||||||
captured = {}
|
captured = {}
|
||||||
|
|
||||||
def _capture(path, params):
|
def _capture(path, params):
|
||||||
@@ -192,7 +192,7 @@ class FredFormattingTests(unittest.TestCase):
|
|||||||
for path in ("series", "series/observations"):
|
for path in ("series", "series/observations"):
|
||||||
self.assertEqual(captured[path]["realtime_start"], "2026-08-31", path)
|
self.assertEqual(captured[path]["realtime_start"], "2026-08-31", path)
|
||||||
self.assertEqual(captured[path]["realtime_end"], "2026-08-31", path)
|
self.assertEqual(captured[path]["realtime_end"], "2026-08-31", path)
|
||||||
# the observation window still tracks curr_date, not the clamped vintage
|
# the observation window still tracks as_of_date, not the clamped vintage
|
||||||
self.assertEqual(captured["series/observations"]["observation_end"], "2026-09-01")
|
self.assertEqual(captured["series/observations"]["observation_end"], "2026-09-01")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ FRED (#1275), social (#1220) and memory (#1251) leaks.
|
|||||||
|
|
||||||
Both vendors withhold on one shared rule (``date_window.withhold_live_profile``)
|
Both vendors withhold on one shared rule (``date_window.withhold_live_profile``)
|
||||||
so switching ``fundamental_data`` between them cannot reintroduce the leak. The
|
so switching ``fundamental_data`` between them cannot reintroduce the leak. The
|
||||||
statement tools stay point-in-time by filtering on ``curr_date``, and a live run
|
statement tools stay point-in-time by filtering on ``as_of_date``, and a live run
|
||||||
is unchanged. All API access is mocked.
|
is unchanged. All API access is mocked.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -45,19 +45,19 @@ _LEAKY = ("3500000000000", "34.2", "260.1", "391000000000",
|
|||||||
"Apple Inc.", "Technology", "Consumer Electronics")
|
"Apple Inc.", "Technology", "Consumer Electronics")
|
||||||
|
|
||||||
|
|
||||||
def _yf(curr_date, info=_INFO, today=_TODAY):
|
def _yf(as_of_date, info=_INFO, today=_TODAY):
|
||||||
with mock.patch.object(date_window, "get_current_date", return_value=today), \
|
with mock.patch.object(date_window, "get_current_date", return_value=today), \
|
||||||
mock.patch.object(yahoo_fundamentals, "yf_retry", lambda fn: info), \
|
mock.patch.object(yahoo_fundamentals, "yf_retry", lambda fn: info), \
|
||||||
mock.patch.object(yahoo_market.yf, "Ticker"):
|
mock.patch.object(yahoo_market.yf, "Ticker"):
|
||||||
return yahoo_fundamentals.get_fundamentals("AAPL", curr_date)
|
return yahoo_fundamentals.get_fundamentals("AAPL", as_of_date)
|
||||||
|
|
||||||
|
|
||||||
def _av(curr_date, today=_TODAY):
|
def _av(as_of_date, today=_TODAY):
|
||||||
"""Alpha Vantage path; the API call is mocked so a leak would be visible."""
|
"""Alpha Vantage path; the API call is mocked so a leak would be visible."""
|
||||||
with mock.patch.object(date_window, "get_current_date", return_value=today), \
|
with mock.patch.object(date_window, "get_current_date", return_value=today), \
|
||||||
mock.patch.object(av, "_make_api_request",
|
mock.patch.object(av, "_make_api_request",
|
||||||
return_value="MarketCapitalization: 3500000000000") as req:
|
return_value="MarketCapitalization: 3500000000000") as req:
|
||||||
return av.get_fundamentals("AAPL", curr_date), req
|
return av.get_fundamentals("AAPL", as_of_date), req
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
|
|||||||
@@ -102,4 +102,4 @@ def test_an_unreachable_yahoo_is_not_reported_as_a_symbol_without_insider_data()
|
|||||||
with mock.patch.object(fundamentals.yf, "Ticker", return_value=ticker), \
|
with mock.patch.object(fundamentals.yf, "Ticker", return_value=ticker), \
|
||||||
mock.patch.object(fundamentals, "vendor_reachable", return_value=False), \
|
mock.patch.object(fundamentals, "vendor_reachable", return_value=False), \
|
||||||
pytest.raises(VendorRateLimitError):
|
pytest.raises(VendorRateLimitError):
|
||||||
fundamentals.get_insider_transactions("AAPL", curr_date="2026-09-21")
|
fundamentals.get_insider_transactions("AAPL", as_of_date="2026-09-21")
|
||||||
|
|||||||
@@ -70,14 +70,14 @@ def test_alpha_vantage_insider_filings_after_the_date_are_dropped():
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_polymarket_withholds_live_odds_from_a_historical_run():
|
def test_polymarket_withholds_live_odds_from_a_historical_run():
|
||||||
with mock.patch.object(polymarket, "_request", side_effect=AssertionError("must not fetch")):
|
with mock.patch.object(polymarket, "_request", side_effect=AssertionError("must not fetch")):
|
||||||
out = polymarket.get_prediction_markets("Fed rate cut", curr_date="2025-06-01")
|
out = polymarket.get_prediction_markets("Fed rate cut", as_of_date="2025-06-01")
|
||||||
assert "withheld" in out
|
assert "withheld" in out
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_polymarket_serves_a_current_run():
|
def test_polymarket_serves_a_current_run():
|
||||||
with mock.patch.object(polymarket, "_request", return_value={"events": []}) as req:
|
with mock.patch.object(polymarket, "_request", return_value={"events": []}) as req:
|
||||||
polymarket.get_prediction_markets("Fed rate cut", curr_date=polymarket.get_current_date())
|
polymarket.get_prediction_markets("Fed rate cut", as_of_date=polymarket.get_current_date())
|
||||||
req.assert_called_once()
|
req.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ def test_a_historical_run_is_told_the_identity_is_current(monkeypatch):
|
|||||||
identity = {"company_name": "Example Corp", "sector": "Technology",
|
identity = {"company_name": "Example Corp", "sector": "Technology",
|
||||||
"industry": "Software", "exchange": "NMS"}
|
"industry": "Software", "exchange": "NMS"}
|
||||||
|
|
||||||
historical = build_instrument_context("EXMP", "stock", identity, curr_date="2024-03-14")
|
historical = build_instrument_context("EXMP", "stock", identity, trade_date="2024-03-14")
|
||||||
assert "Example Corp" in historical
|
assert "Example Corp" in historical
|
||||||
assert "2024-03-14" in historical and "today" in historical.lower()
|
assert "2024-03-14" in historical and "today" in historical.lower()
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ def test_a_current_run_is_not_cluttered_with_a_vintage_note(monkeypatch):
|
|||||||
from tradingagents.dataflows.date_window import get_current_date
|
from tradingagents.dataflows.date_window import get_current_date
|
||||||
|
|
||||||
today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"},
|
today = build_instrument_context("EXMP", "stock", {"company_name": "Example Corp"},
|
||||||
curr_date=get_current_date())
|
trade_date=get_current_date())
|
||||||
assert "Example Corp" in today
|
assert "Example Corp" in today
|
||||||
assert "resolved today" not in today.lower()
|
assert "resolved today" not in today.lower()
|
||||||
|
|
||||||
@@ -297,7 +297,7 @@ def test_an_unavailable_notice_names_no_date_after_the_run():
|
|||||||
coverage_gap([pd.Timestamp(today, tz="UTC")], "2025-01-01", "2025-01-07", "Feed", "news"),
|
coverage_gap([pd.Timestamp(today, tz="UTC")], "2025-01-01", "2025-01-07", "Feed", "news"),
|
||||||
withhold_live_profile("2025-01-07", "AAPL"),
|
withhold_live_profile("2025-01-07", "AAPL"),
|
||||||
_yf_insider(_insider_frame(today), "2025-01-07"),
|
_yf_insider(_insider_frame(today), "2025-01-07"),
|
||||||
build_instrument_context("EXMP", "stock", {"company_name": "Example"}, curr_date="2025-01-07"),
|
build_instrument_context("EXMP", "stock", {"company_name": "Example"}, trade_date="2025-01-07"),
|
||||||
]
|
]
|
||||||
for notice in notices:
|
for notice in notices:
|
||||||
assert _dates_after(notice, "2025-01-07") == [], notice
|
assert _dates_after(notice, "2025-01-07") == [], notice
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ def build_instrument_context(
|
|||||||
ticker: str,
|
ticker: str,
|
||||||
asset_type: str = "stock",
|
asset_type: str = "stock",
|
||||||
identity: Mapping[str, str] | None = None,
|
identity: Mapping[str, str] | None = None,
|
||||||
curr_date: str | None = None,
|
trade_date: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Describe the exact instrument so agents preserve identity and ticker.
|
"""Describe the exact instrument so agents preserve identity and ticker.
|
||||||
|
|
||||||
@@ -144,10 +144,10 @@ def build_instrument_context(
|
|||||||
"result explicitly disproves this resolved identity."
|
"result explicitly disproves this resolved identity."
|
||||||
)
|
)
|
||||||
today = get_current_date()
|
today = get_current_date()
|
||||||
if curr_date and str(curr_date) < today:
|
if trade_date and str(trade_date) < today:
|
||||||
context += (
|
context += (
|
||||||
f" This identity is how the vendor describes the instrument today, "
|
f" This identity is how the vendor describes the instrument today, "
|
||||||
f"not necessarily on {curr_date}: a name or classification changed "
|
f"not necessarily on {trade_date}: a name or classification changed "
|
||||||
f"since then would read as the current one."
|
f"since then would read as the current one."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ def as_of_window(start_date: str, end_date: str, trade_date: str) -> tuple[str,
|
|||||||
return f"{_parse(end) - span:%Y-%m-%d}", end
|
return f"{_parse(end) - span:%Y-%m-%d}", end
|
||||||
|
|
||||||
|
|
||||||
def withhold_live_profile(curr_date: str | None, label: str) -> str | None:
|
def withhold_live_profile(as_of_date: str | None, label: str) -> str | None:
|
||||||
"""Notice to serve instead of a live-only company profile, or None to serve it.
|
"""Notice to serve instead of a live-only company profile, or None to serve it.
|
||||||
|
|
||||||
Vendor "company overview" endpoints (yfinance ``Ticker.info``, Alpha Vantage
|
Vendor "company overview" endpoints (yfinance ``Ticker.info``, Alpha Vantage
|
||||||
@@ -105,20 +105,20 @@ def withhold_live_profile(curr_date: str | None, label: str) -> str | None:
|
|||||||
Every fundamentals vendor withholds on this rule, so switching between them
|
Every fundamentals vendor withholds on this rule, so switching between them
|
||||||
cannot reintroduce the leak.
|
cannot reintroduce the leak.
|
||||||
"""
|
"""
|
||||||
if not curr_date:
|
if not as_of_date:
|
||||||
return None
|
return None
|
||||||
today = get_current_date()
|
today = get_current_date()
|
||||||
if curr_date >= today:
|
if as_of_date >= today:
|
||||||
return None
|
return None
|
||||||
return (
|
return (
|
||||||
f"# Company Fundamentals for {label}\n"
|
f"# Company Fundamentals for {label}\n"
|
||||||
f"# Point-in-time as of: {curr_date}\n\n"
|
f"# Point-in-time as of: {as_of_date}\n\n"
|
||||||
f"Profile fundamentals are withheld for this date. This vendor serves "
|
f"Profile fundamentals are withheld for this date. This vendor serves "
|
||||||
f"only present-day values with no historical vintage: market "
|
f"only present-day values with no historical vintage: market "
|
||||||
f"cap, valuation multiples, the 52-week range and TTM income move with "
|
f"cap, valuation multiples, the 52-week range and TTM income move with "
|
||||||
f"today's quote, and even the name, sector and industry reflect today "
|
f"today's quote, and even the name, sector and industry reflect today "
|
||||||
f"rather than {curr_date} (companies rename and get reclassified). "
|
f"rather than {as_of_date} (companies rename and get reclassified). "
|
||||||
f"Serving them would put post-decision information into a {curr_date} "
|
f"Serving them would put post-decision information into a {as_of_date} "
|
||||||
f"analysis. Point-in-time fundamentals for {curr_date} are available "
|
f"analysis. Point-in-time fundamentals for {as_of_date} are available "
|
||||||
f"from the balance sheet, income statement, and cash flow tools."
|
f"from the balance sheet, income statement, and cash flow tools."
|
||||||
)
|
)
|
||||||
|
|||||||
+15
-15
@@ -4,14 +4,14 @@ from tradingagents.dataflows.date_window import withhold_live_profile
|
|||||||
from tradingagents.dataflows.vendors.alpha_vantage.common import _make_api_request
|
from tradingagents.dataflows.vendors.alpha_vantage.common import _make_api_request
|
||||||
|
|
||||||
|
|
||||||
def _filter_reports_by_date(result, curr_date: str):
|
def _filter_reports_by_date(result, as_of_date: str):
|
||||||
"""Drop annual/quarterly reports dated after curr_date to prevent look-ahead.
|
"""Drop annual/quarterly reports dated after as_of_date to prevent look-ahead.
|
||||||
|
|
||||||
``_make_api_request`` returns the fundamentals payload as a JSON string, so
|
``_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
|
parse, filter, and re-serialize. A non-JSON body or an unset ``as_of_date`` is
|
||||||
returned unchanged.
|
returned unchanged.
|
||||||
"""
|
"""
|
||||||
if not curr_date or not isinstance(result, str):
|
if not as_of_date or not isinstance(result, str):
|
||||||
return result
|
return result
|
||||||
try:
|
try:
|
||||||
payload = json.loads(result)
|
payload = json.loads(result)
|
||||||
@@ -23,28 +23,28 @@ def _filter_reports_by_date(result, curr_date: str):
|
|||||||
if isinstance(payload.get(key), list):
|
if isinstance(payload.get(key), list):
|
||||||
payload[key] = [
|
payload[key] = [
|
||||||
r for r in payload[key]
|
r for r in payload[key]
|
||||||
if r.get("fiscalDateEnding", "") <= curr_date
|
if r.get("fiscalDateEnding", "") <= as_of_date
|
||||||
]
|
]
|
||||||
return json.dumps(payload)
|
return json.dumps(payload)
|
||||||
|
|
||||||
|
|
||||||
def get_fundamentals(ticker: str, curr_date: str = None) -> str:
|
def get_fundamentals(ticker: str, as_of_date: str = None) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve comprehensive fundamental data for a given ticker symbol using Alpha Vantage.
|
Retrieve comprehensive fundamental data for a given ticker symbol using Alpha Vantage.
|
||||||
|
|
||||||
OVERVIEW serves only present-day values and carries no historical vintage, so
|
OVERVIEW serves only present-day values and carries no historical vintage, so
|
||||||
a past ``curr_date`` withholds it rather than leaking post-decision figures
|
a past ``as_of_date`` withholds it rather than leaking post-decision figures
|
||||||
into a backtest (#1300); the statement endpoints below stay point-in-time via
|
into a backtest (#1300); the statement endpoints below stay point-in-time via
|
||||||
``_filter_reports_by_date``.
|
``_filter_reports_by_date``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker (str): Ticker symbol of the company
|
ticker (str): Ticker symbol of the company
|
||||||
curr_date (str): Analysis date, yyyy-mm-dd
|
as_of_date (str): Analysis date, yyyy-mm-dd
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: Company overview data including financial ratios and key metrics
|
str: Company overview data including financial ratios and key metrics
|
||||||
"""
|
"""
|
||||||
withheld = withhold_live_profile(curr_date, ticker)
|
withheld = withhold_live_profile(as_of_date, ticker)
|
||||||
if withheld:
|
if withheld:
|
||||||
return withheld
|
return withheld
|
||||||
|
|
||||||
@@ -55,20 +55,20 @@ def get_fundamentals(ticker: str, curr_date: str = None) -> str:
|
|||||||
return _make_api_request("OVERVIEW", params)
|
return _make_api_request("OVERVIEW", params)
|
||||||
|
|
||||||
|
|
||||||
def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str = None):
|
def get_balance_sheet(ticker: str, freq: str = "quarterly", as_of_date: str = None):
|
||||||
"""Retrieve balance sheet data for a given ticker symbol using Alpha Vantage."""
|
"""Retrieve balance sheet data for a given ticker symbol using Alpha Vantage."""
|
||||||
result = _make_api_request("BALANCE_SHEET", {"symbol": ticker})
|
result = _make_api_request("BALANCE_SHEET", {"symbol": ticker})
|
||||||
return _filter_reports_by_date(result, curr_date)
|
return _filter_reports_by_date(result, as_of_date)
|
||||||
|
|
||||||
|
|
||||||
def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str = None):
|
def get_cashflow(ticker: str, freq: str = "quarterly", as_of_date: str = None):
|
||||||
"""Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage."""
|
"""Retrieve cash flow statement data for a given ticker symbol using Alpha Vantage."""
|
||||||
result = _make_api_request("CASH_FLOW", {"symbol": ticker})
|
result = _make_api_request("CASH_FLOW", {"symbol": ticker})
|
||||||
return _filter_reports_by_date(result, curr_date)
|
return _filter_reports_by_date(result, as_of_date)
|
||||||
|
|
||||||
|
|
||||||
def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str = None):
|
def get_income_statement(ticker: str, freq: str = "quarterly", as_of_date: str = None):
|
||||||
"""Retrieve income statement data for a given ticker symbol using Alpha Vantage."""
|
"""Retrieve income statement data for a given ticker symbol using Alpha Vantage."""
|
||||||
result = _make_api_request("INCOME_STATEMENT", {"symbol": ticker})
|
result = _make_api_request("INCOME_STATEMENT", {"symbol": ticker})
|
||||||
return _filter_reports_by_date(result, curr_date)
|
return _filter_reports_by_date(result, as_of_date)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ logger = logging.getLogger(__name__)
|
|||||||
def get_indicator(
|
def get_indicator(
|
||||||
symbol: str,
|
symbol: str,
|
||||||
indicator: str,
|
indicator: str,
|
||||||
curr_date: str,
|
as_of_date: str,
|
||||||
look_back_days: int,
|
look_back_days: int,
|
||||||
interval: str = "daily",
|
interval: str = "daily",
|
||||||
time_period: int = 14,
|
time_period: int = 14,
|
||||||
@@ -21,7 +21,7 @@ def get_indicator(
|
|||||||
Args:
|
Args:
|
||||||
symbol: ticker symbol of the company
|
symbol: ticker symbol of the company
|
||||||
indicator: technical indicator to get the analysis and report of
|
indicator: technical indicator to get the analysis and report of
|
||||||
curr_date: The current trading date you are trading on, YYYY-mm-dd
|
as_of_date: The current trading date you are trading on, YYYY-mm-dd
|
||||||
look_back_days: how many days to look back
|
look_back_days: how many days to look back
|
||||||
interval: Time interval (daily, weekly, monthly)
|
interval: Time interval (daily, weekly, monthly)
|
||||||
time_period: Number of data points for calculation
|
time_period: Number of data points for calculation
|
||||||
@@ -72,8 +72,8 @@ def get_indicator(
|
|||||||
f"Alpha Vantage does not serve {indicator}; it serves {list(supported_indicators)}"
|
f"Alpha Vantage does not serve {indicator}; it serves {list(supported_indicators)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
||||||
before = curr_date_dt - relativedelta(days=look_back_days)
|
before = as_of_dt - relativedelta(days=look_back_days)
|
||||||
|
|
||||||
# Get the full data for the period instead of making individual calls
|
# Get the full data for the period instead of making individual calls
|
||||||
_, required_series_type = supported_indicators[indicator]
|
_, required_series_type = supported_indicators[indicator]
|
||||||
@@ -184,7 +184,7 @@ def get_indicator(
|
|||||||
date_str = values[date_col_idx].strip()
|
date_str = values[date_col_idx].strip()
|
||||||
date_dt = datetime.strptime(date_str, "%Y-%m-%d")
|
date_dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||||||
|
|
||||||
if before <= date_dt <= curr_date_dt:
|
if before <= date_dt <= as_of_dt:
|
||||||
value = values[value_col_idx].strip()
|
value = values[value_col_idx].strip()
|
||||||
result_data.append((date_dt, value))
|
result_data.append((date_dt, value))
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
@@ -201,7 +201,7 @@ def get_indicator(
|
|||||||
ind_string = "No data available for the specified date range.\n"
|
ind_string = "No data available for the specified date range.\n"
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
f"## {indicator.upper()} values from {before.strftime('%Y-%m-%d')} to {curr_date}:\n\n"
|
f"## {indicator.upper()} values from {before.strftime('%Y-%m-%d')} to {as_of_date}:\n\n"
|
||||||
+ ind_string
|
+ ind_string
|
||||||
+ "\n\n"
|
+ "\n\n"
|
||||||
+ indicator_descriptions.get(indicator, "No description available.")
|
+ indicator_descriptions.get(indicator, "No description available.")
|
||||||
|
|||||||
+8
-8
@@ -33,13 +33,13 @@ def get_news(ticker, start_date, end_date) -> dict[str, str] | str:
|
|||||||
return _make_api_request("NEWS_SENTIMENT", params)
|
return _make_api_request("NEWS_SENTIMENT", params)
|
||||||
|
|
||||||
|
|
||||||
def get_global_news(curr_date, look_back_days: int | None = None, limit: int | None = None) -> dict[str, str] | str:
|
def get_global_news(as_of_date, look_back_days: int | None = None, limit: int | None = None) -> dict[str, str] | str:
|
||||||
"""Returns global market news & sentiment data without ticker-specific filtering.
|
"""Returns global market news & sentiment data without ticker-specific filtering.
|
||||||
|
|
||||||
Covers broad market topics like financial markets, economy, and more.
|
Covers broad market topics like financial markets, economy, and more.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
curr_date: Current date in yyyy-mm-dd format.
|
as_of_date: Current date in yyyy-mm-dd format.
|
||||||
look_back_days: Number of days to look back; ``None`` uses
|
look_back_days: Number of days to look back; ``None`` uses
|
||||||
``global_news_lookback_days`` from the active config.
|
``global_news_lookback_days`` from the active config.
|
||||||
limit: Maximum number of articles; ``None`` uses
|
limit: Maximum number of articles; ``None`` uses
|
||||||
@@ -56,28 +56,28 @@ def get_global_news(curr_date, look_back_days: int | None = None, limit: int | N
|
|||||||
if limit is None:
|
if limit is None:
|
||||||
limit = config["global_news_article_limit"]
|
limit = config["global_news_article_limit"]
|
||||||
|
|
||||||
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
curr_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
||||||
start_dt = curr_dt - timedelta(days=look_back_days)
|
start_dt = curr_dt - timedelta(days=look_back_days)
|
||||||
start_date = start_dt.strftime("%Y-%m-%d")
|
start_date = start_dt.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
"topics": "financial_markets,economy_macro,economy_monetary",
|
"topics": "financial_markets,economy_macro,economy_monetary",
|
||||||
"time_from": format_datetime_for_api(start_date),
|
"time_from": format_datetime_for_api(start_date),
|
||||||
"time_to": format_datetime_for_api(curr_date, end_of_day=True),
|
"time_to": format_datetime_for_api(as_of_date, end_of_day=True),
|
||||||
"limit": str(limit),
|
"limit": str(limit),
|
||||||
}
|
}
|
||||||
|
|
||||||
return _make_api_request("NEWS_SENTIMENT", params)
|
return _make_api_request("NEWS_SENTIMENT", params)
|
||||||
|
|
||||||
|
|
||||||
def get_insider_transactions(symbol: str, curr_date: str | None = None) -> dict[str, str] | str:
|
def get_insider_transactions(symbol: str, as_of_date: str | None = None) -> dict[str, str] | str:
|
||||||
"""Returns latest and historical insider transactions by key stakeholders.
|
"""Returns latest and historical insider transactions by key stakeholders.
|
||||||
|
|
||||||
Covers transactions by founders, executives, board members, etc.
|
Covers transactions by founders, executives, board members, etc.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
symbol: Ticker symbol. Example: "IBM".
|
symbol: Ticker symbol. Example: "IBM".
|
||||||
curr_date: When given, only transactions on or before it (yyyy-mm-dd).
|
as_of_date: When given, only transactions on or before it (yyyy-mm-dd).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing insider transaction data or JSON string.
|
Dictionary containing insider transaction data or JSON string.
|
||||||
@@ -88,8 +88,8 @@ def get_insider_transactions(symbol: str, curr_date: str | None = None) -> dict[
|
|||||||
}
|
}
|
||||||
|
|
||||||
response = _make_api_request("INSIDER_TRANSACTIONS", params)
|
response = _make_api_request("INSIDER_TRANSACTIONS", params)
|
||||||
if not curr_date:
|
if not as_of_date:
|
||||||
return response
|
return response
|
||||||
payload = json.loads(response)
|
payload = json.loads(response)
|
||||||
payload["data"] = [t for t in payload["data"] if t["transaction_date"] <= curr_date]
|
payload["data"] = [t for t in payload["data"] if t["transaction_date"] <= as_of_date]
|
||||||
return json.dumps(payload)
|
return json.dumps(payload)
|
||||||
|
|||||||
+10
-10
@@ -126,7 +126,7 @@ def _fred_today() -> str:
|
|||||||
"""FRED's current calendar date (US Central) as ``yyyy-mm-dd``.
|
"""FRED's current calendar date (US Central) as ``yyyy-mm-dd``.
|
||||||
|
|
||||||
The vintage pin is clamped to this: FRED rejects a ``realtime_start`` after
|
The vintage pin is clamped to this: FRED rejects a ``realtime_start`` after
|
||||||
its own today with a 400, and ``curr_date`` on a live run comes from the
|
its own today with a 400, and ``as_of_date`` on a live run comes from the
|
||||||
caller's local clock, which can already be tomorrow in Chicago.
|
caller's local clock, which can already be tomorrow in Chicago.
|
||||||
"""
|
"""
|
||||||
return datetime.now(FRED_TZ).strftime("%Y-%m-%d")
|
return datetime.now(FRED_TZ).strftime("%Y-%m-%d")
|
||||||
@@ -155,7 +155,7 @@ def _request(path: str, params: dict) -> dict:
|
|||||||
|
|
||||||
def get_macro_data(
|
def get_macro_data(
|
||||||
indicator: str,
|
indicator: str,
|
||||||
curr_date: str,
|
as_of_date: str,
|
||||||
look_back_days: int | None = None,
|
look_back_days: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Fetch a FRED macroeconomic series as a formatted markdown report.
|
"""Fetch a FRED macroeconomic series as a formatted markdown report.
|
||||||
@@ -163,9 +163,9 @@ def get_macro_data(
|
|||||||
Args:
|
Args:
|
||||||
indicator: A friendly alias (e.g. "cpi", "unemployment", "10y_treasury")
|
indicator: A friendly alias (e.g. "cpi", "unemployment", "10y_treasury")
|
||||||
or a raw FRED series ID (e.g. "CPIAUCSL", "DGS10").
|
or a raw FRED series ID (e.g. "CPIAUCSL", "DGS10").
|
||||||
curr_date: The as-of date (yyyy-mm-dd). It bounds the observation window
|
as_of_date: The as-of date (yyyy-mm-dd). It bounds the observation window
|
||||||
AND pins the data vintage: FRED is queried with the realtime bounds
|
AND pins the data vintage: FRED is queried with the realtime bounds
|
||||||
set to ``curr_date`` (clamped to FRED's own today) so a historical
|
set to ``as_of_date`` (clamped to FRED's own today) so a historical
|
||||||
run sees the values that were actually published by that date, not
|
run sees the values that were actually published by that date, not
|
||||||
later revisions. Without this, revision-prone series (CPI, GDP, ...)
|
later revisions. Without this, revision-prone series (CPI, GDP, ...)
|
||||||
would leak future information into a backtest (#1275).
|
would leak future information into a backtest (#1275).
|
||||||
@@ -178,18 +178,18 @@ def get_macro_data(
|
|||||||
if look_back_days is None:
|
if look_back_days is None:
|
||||||
look_back_days = DEFAULT_LOOKBACK_DAYS
|
look_back_days = DEFAULT_LOOKBACK_DAYS
|
||||||
|
|
||||||
end_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
end_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
||||||
start_date = (end_dt - timedelta(days=look_back_days)).strftime("%Y-%m-%d")
|
start_date = (end_dt - timedelta(days=look_back_days)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
# Pin the data vintage. FRED defaults both realtime bounds to today, serving
|
# Pin the data vintage. FRED defaults both realtime bounds to today, serving
|
||||||
# the LATEST revision of every observation; a single-day realtime interval
|
# the LATEST revision of every observation; a single-day realtime interval
|
||||||
# asks for the values known as of the pin instead, on both the metadata and
|
# asks for the values known as of the pin instead, on both the metadata and
|
||||||
# observations requests (#1275). Clamp to FRED's today: on a live run
|
# observations requests (#1275). Clamp to FRED's today: on a live run
|
||||||
# curr_date is the caller's local date, which can be a day ahead of Chicago,
|
# as_of_date is the caller's local date, which can be a day ahead of Chicago,
|
||||||
# and a realtime date in FRED's future 400s -> the routing layer would then
|
# and a realtime date in FRED's future 400s -> the routing layer would then
|
||||||
# drop macro data silently. A past curr_date is unaffected, so historical
|
# drop macro data silently. A past as_of_date is unaffected, so historical
|
||||||
# point-in-time behaviour is preserved.
|
# point-in-time behaviour is preserved.
|
||||||
pit = min(curr_date, _fred_today())
|
pit = min(as_of_date, _fred_today())
|
||||||
realtime = {"realtime_start": pit, "realtime_end": pit}
|
realtime = {"realtime_start": pit, "realtime_end": pit}
|
||||||
|
|
||||||
# Invalid LLM-supplied indicator: return guidance rather than raising, so a
|
# Invalid LLM-supplied indicator: return guidance rather than raising, so a
|
||||||
@@ -217,7 +217,7 @@ def get_macro_data(
|
|||||||
{
|
{
|
||||||
"series_id": series_id,
|
"series_id": series_id,
|
||||||
"observation_start": start_date,
|
"observation_start": start_date,
|
||||||
"observation_end": curr_date,
|
"observation_end": as_of_date,
|
||||||
"sort_order": "asc",
|
"sort_order": "asc",
|
||||||
**realtime,
|
**realtime,
|
||||||
},
|
},
|
||||||
@@ -235,7 +235,7 @@ def get_macro_data(
|
|||||||
f"- Units: {units}\n"
|
f"- Units: {units}\n"
|
||||||
f"- Frequency: {frequency}"
|
f"- Frequency: {frequency}"
|
||||||
f"{f' ({seasonal})' if seasonal else ''}\n"
|
f"{f' ({seasonal})' if seasonal else ''}\n"
|
||||||
f"- Window: {start_date} to {curr_date}\n"
|
f"- Window: {start_date} to {as_of_date}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not points:
|
if not points:
|
||||||
|
|||||||
+5
-5
@@ -67,7 +67,7 @@ def _is_forward_looking(market: dict, now: datetime) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_prediction_markets(topic: str, limit: int | None = None, curr_date: str | None = None) -> str:
|
def get_prediction_markets(topic: str, limit: int | None = None, as_of_date: str | None = None) -> str:
|
||||||
"""Return live prediction-market probabilities for an event topic.
|
"""Return live prediction-market probabilities for an event topic.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -75,7 +75,7 @@ def get_prediction_markets(topic: str, limit: int | None = None, curr_date: str
|
|||||||
"US election", or a sector/company event.
|
"US election", or a sector/company event.
|
||||||
limit: Max markets to return (ranked by traded volume); ``None`` uses
|
limit: Max markets to return (ranked by traded volume); ``None`` uses
|
||||||
DEFAULT_LIMIT.
|
DEFAULT_LIMIT.
|
||||||
curr_date: The analysis date. Polymarket serves only live odds, so a
|
as_of_date: The analysis date. Polymarket serves only live odds, so a
|
||||||
date before today withholds them.
|
date before today withholds them.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -83,11 +83,11 @@ def get_prediction_markets(topic: str, limit: int | None = None, curr_date: str
|
|||||||
each with its implied probability, traded volume, resolution date, and
|
each with its implied probability, traded volume, resolution date, and
|
||||||
recent (1-week) move.
|
recent (1-week) move.
|
||||||
"""
|
"""
|
||||||
if curr_date and curr_date < get_current_date():
|
if as_of_date and as_of_date < get_current_date():
|
||||||
return (
|
return (
|
||||||
f"Prediction-market odds are withheld for {curr_date}. Polymarket serves "
|
f"Prediction-market odds are withheld for {as_of_date}. Polymarket serves "
|
||||||
f"only live odds on open markets, with no historical vintage, so serving "
|
f"only live odds on open markets, with no historical vintage, so serving "
|
||||||
f"them would put post-decision information into a {curr_date} analysis."
|
f"them would put post-decision information into a {as_of_date} analysis."
|
||||||
)
|
)
|
||||||
if limit is None:
|
if limit is None:
|
||||||
limit = DEFAULT_LIMIT
|
limit = DEFAULT_LIMIT
|
||||||
|
|||||||
+17
-17
@@ -5,7 +5,7 @@ statement at the fiscal period end. That is two claims a run should not make: a
|
|||||||
period that has ended is not public until the company files, weeks later, and a
|
period that has ended is not public until the company files, weeks later, and a
|
||||||
figure that was later restated is not what investors saw at the time.
|
figure that was later restated is not what investors saw at the time.
|
||||||
|
|
||||||
EDGAR reports every fact with the date it was filed, so a run dated ``curr_date``
|
EDGAR reports every fact with the date it was filed, so a run dated ``as_of_date``
|
||||||
serves exactly what was on file by then, restatements included at the vintage
|
serves exactly what was on file by then, restatements included at the vintage
|
||||||
that was current: Apple's 2008 total assets read 39.6B until the 2010 amendment
|
that was current: Apple's 2008 total assets read 39.6B until the 2010 amendment
|
||||||
restated them to 36.2B.
|
restated them to 36.2B.
|
||||||
@@ -144,7 +144,7 @@ def cik_for(ticker: str) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _as_of(facts: dict, tags: tuple[str, ...], curr_date: str, span: tuple[int, int],
|
def _as_of(facts: dict, tags: tuple[str, ...], as_of_date: str, span: tuple[int, int],
|
||||||
forms: tuple[str, ...] = ()) -> tuple[dict, str]:
|
forms: tuple[str, ...] = ()) -> tuple[dict, str]:
|
||||||
"""({period end: value}, unit) for the first tag the filer reports, as known then.
|
"""({period end: value}, unit) for the first tag the filer reports, as known then.
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ def _as_of(facts: dict, tags: tuple[str, ...], curr_date: str, span: tuple[int,
|
|||||||
latest: dict[str, dict] = {}
|
latest: dict[str, dict] = {}
|
||||||
covered: set[str] = set() # period ends a filing of ``forms`` reports
|
covered: set[str] = set() # period ends a filing of ``forms`` reports
|
||||||
for fact in unit_values:
|
for fact in unit_values:
|
||||||
if fact["filed"] > curr_date or fact["end"] in values:
|
if fact["filed"] > as_of_date or fact["end"] in values:
|
||||||
continue
|
continue
|
||||||
# A duration fact (revenue, cash flow) must cover the span asked
|
# A duration fact (revenue, cash flow) must cover the span asked
|
||||||
# for. An instant fact (a balance) has no span and serves both.
|
# for. An instant fact (a balance) has no span and serves both.
|
||||||
@@ -184,8 +184,8 @@ def _as_of(facts: dict, tags: tuple[str, ...], curr_date: str, span: tuple[int,
|
|||||||
return dict(sorted(values.items())), chosen_unit
|
return dict(sorted(values.items())), chosen_unit
|
||||||
|
|
||||||
|
|
||||||
def _statement(kind: str, ticker: str, freq: str, curr_date: str, title: str) -> str:
|
def _statement(kind: str, ticker: str, freq: str, as_of_date: str, title: str) -> str:
|
||||||
curr_date = curr_date or datetime.now().strftime("%Y-%m-%d")
|
as_of_date = as_of_date or datetime.now().strftime("%Y-%m-%d")
|
||||||
cik = cik_for(ticker)
|
cik = cik_for(ticker)
|
||||||
if cik is None:
|
if cik is None:
|
||||||
raise NoMarketDataError(ticker, ticker, "not a US SEC filer")
|
raise NoMarketDataError(ticker, ticker, "not a US SEC filer")
|
||||||
@@ -198,14 +198,14 @@ def _statement(kind: str, ticker: str, freq: str, curr_date: str, title: str) ->
|
|||||||
quarterly = freq.lower() == "quarterly"
|
quarterly = freq.lower() == "quarterly"
|
||||||
span = _SPANS["quarterly" if quarterly else "annual"]
|
span = _SPANS["quarterly" if quarterly else "annual"]
|
||||||
forms = () if quarterly else _ANNUAL_FORMS
|
forms = () if quarterly else _ANNUAL_FORMS
|
||||||
lines = {label: _as_of(us_gaap, tags, curr_date, span, forms) for label, tags in _STATEMENTS[kind]}
|
lines = {label: _as_of(us_gaap, tags, as_of_date, span, forms) for label, tags in _STATEMENTS[kind]}
|
||||||
periods = sorted({end for values, _ in lines.values() for end in values})
|
periods = sorted({end for values, _ in lines.values() for end in values})
|
||||||
if not periods:
|
if not periods:
|
||||||
raise NoMarketDataError(ticker, ticker, f"no {freq} {title.lower()} filed by {curr_date}")
|
raise NoMarketDataError(ticker, ticker, f"no {freq} {title.lower()} filed by {as_of_date}")
|
||||||
|
|
||||||
header = (
|
header = (
|
||||||
f"# {title} for {ticker.upper()} ({freq}), USD in millions unless the row says otherwise\n"
|
f"# {title} for {ticker.upper()} ({freq}), USD in millions unless the row says otherwise\n"
|
||||||
f"# SEC EDGAR facts filed on or before {curr_date}, at the values filed then\n\n"
|
f"# SEC EDGAR facts filed on or before {as_of_date}, at the values filed then\n\n"
|
||||||
)
|
)
|
||||||
rows = [",".join([""] + periods)]
|
rows = [",".join([""] + periods)]
|
||||||
for label, (values, unit) in lines.items():
|
for label, (values, unit) in lines.items():
|
||||||
@@ -223,21 +223,21 @@ def _statement(kind: str, ticker: str, freq: str, curr_date: str, title: str) ->
|
|||||||
return header + "\n".join(rows) + "\n"
|
return header + "\n".join(rows) + "\n"
|
||||||
|
|
||||||
|
|
||||||
def get_balance_sheet(ticker: str, freq: str = "quarterly", curr_date: str | None = None) -> str:
|
def get_balance_sheet(ticker: str, freq: str = "quarterly", as_of_date: str | None = None) -> str:
|
||||||
"""Balance sheet as filed on or before ``curr_date``."""
|
"""Balance sheet as filed on or before ``as_of_date``."""
|
||||||
return _statement("balance_sheet", ticker, freq, curr_date, "Balance Sheet")
|
return _statement("balance_sheet", ticker, freq, as_of_date, "Balance Sheet")
|
||||||
|
|
||||||
|
|
||||||
def get_income_statement(ticker: str, freq: str = "quarterly", curr_date: str | None = None) -> str:
|
def get_income_statement(ticker: str, freq: str = "quarterly", as_of_date: str | None = None) -> str:
|
||||||
"""Income statement as filed on or before ``curr_date``.
|
"""Income statement as filed on or before ``as_of_date``.
|
||||||
|
|
||||||
A fourth quarter is never derived: filers report it only inside the annual
|
A fourth quarter is never derived: filers report it only inside the annual
|
||||||
figure, and subtracting three separately filed quarters would invent a number
|
figure, and subtracting three separately filed quarters would invent a number
|
||||||
with no filing date behind it.
|
with no filing date behind it.
|
||||||
"""
|
"""
|
||||||
return _statement("income_statement", ticker, freq, curr_date, "Income Statement")
|
return _statement("income_statement", ticker, freq, as_of_date, "Income Statement")
|
||||||
|
|
||||||
|
|
||||||
def get_cashflow(ticker: str, freq: str = "quarterly", curr_date: str | None = None) -> str:
|
def get_cashflow(ticker: str, freq: str = "quarterly", as_of_date: str | None = None) -> str:
|
||||||
"""Cash flow statement as filed on or before ``curr_date``."""
|
"""Cash flow statement as filed on or before ``as_of_date``."""
|
||||||
return _statement("cashflow", ticker, freq, curr_date, "Cash Flow Statement")
|
return _statement("cashflow", ticker, freq, as_of_date, "Cash Flow Statement")
|
||||||
|
|||||||
+21
-21
@@ -16,19 +16,19 @@ from tradingagents.dataflows.vendors.yahoo.ohlcv import (
|
|||||||
|
|
||||||
def get_fundamentals(
|
def get_fundamentals(
|
||||||
ticker: Annotated[str, "ticker symbol of the company"],
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
curr_date: Annotated[str, "analysis date in YYYY-MM-DD format"] = None
|
as_of_date: Annotated[str, "analysis date in YYYY-MM-DD format"] = None
|
||||||
):
|
):
|
||||||
"""Get company fundamentals overview from yfinance.
|
"""Get company fundamentals overview from yfinance.
|
||||||
|
|
||||||
``Ticker.info`` is a present-day snapshot with no historical vintage, so a
|
``Ticker.info`` is a present-day snapshot with no historical vintage, so a
|
||||||
past ``curr_date`` withholds it through the shared point-in-time guard
|
past ``as_of_date`` withholds it through the shared point-in-time guard
|
||||||
(``date_window.withhold_live_profile``, #1300).
|
(``date_window.withhold_live_profile``, #1300).
|
||||||
"""
|
"""
|
||||||
canonical = normalize_symbol(ticker)
|
canonical = normalize_symbol(ticker)
|
||||||
|
|
||||||
# Guard before the request: the response would only be discarded, and the
|
# Guard before the request: the response would only be discarded, and the
|
||||||
# answer does not depend on it.
|
# answer does not depend on it.
|
||||||
withheld = withhold_live_profile(curr_date, canonical)
|
withheld = withhold_live_profile(as_of_date, canonical)
|
||||||
if withheld:
|
if withheld:
|
||||||
return withheld
|
return withheld
|
||||||
|
|
||||||
@@ -99,14 +99,14 @@ _PERIOD_END_VINTAGE = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _statement(ticker, freq, curr_date, title, quarterly_attr, annual_attr) -> str:
|
def _statement(ticker, freq, as_of_date, title, quarterly_attr, annual_attr) -> str:
|
||||||
"""One financial statement as CSV, cut at ``curr_date`` by period end."""
|
"""One financial statement as CSV, cut at ``as_of_date`` by period end."""
|
||||||
canonical = normalize_symbol(ticker)
|
canonical = normalize_symbol(ticker)
|
||||||
what = title.lower()
|
what = title.lower()
|
||||||
try:
|
try:
|
||||||
ticker_obj = yf.Ticker(canonical)
|
ticker_obj = yf.Ticker(canonical)
|
||||||
attr = quarterly_attr if freq.lower() == "quarterly" else annual_attr
|
attr = quarterly_attr if freq.lower() == "quarterly" else annual_attr
|
||||||
data = filter_financials_by_date(yf_retry(lambda: getattr(ticker_obj, attr)), curr_date)
|
data = filter_financials_by_date(yf_retry(lambda: getattr(ticker_obj, attr)), as_of_date)
|
||||||
if data.empty:
|
if data.empty:
|
||||||
raise_for_empty(ticker, canonical, f"{what} data")
|
raise_for_empty(ticker, canonical, f"{what} data")
|
||||||
return f"# {title} data for {canonical} ({freq})\n" + _PERIOD_END_VINTAGE + data.to_csv()
|
return f"# {title} data for {canonical} ({freq})\n" + _PERIOD_END_VINTAGE + data.to_csv()
|
||||||
@@ -119,28 +119,28 @@ def _statement(ticker, freq, curr_date, title, quarterly_attr, annual_attr) -> s
|
|||||||
def get_balance_sheet(
|
def get_balance_sheet(
|
||||||
ticker: Annotated[str, "ticker symbol of the company"],
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
as_of_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||||
):
|
):
|
||||||
"""Get balance sheet data from yfinance."""
|
"""Get balance sheet data from yfinance."""
|
||||||
return _statement(ticker, freq, curr_date, "Balance Sheet", "quarterly_balance_sheet", "balance_sheet")
|
return _statement(ticker, freq, as_of_date, "Balance Sheet", "quarterly_balance_sheet", "balance_sheet")
|
||||||
|
|
||||||
|
|
||||||
def get_cashflow(
|
def get_cashflow(
|
||||||
ticker: Annotated[str, "ticker symbol of the company"],
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
as_of_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||||
):
|
):
|
||||||
"""Get cash flow data from yfinance."""
|
"""Get cash flow data from yfinance."""
|
||||||
return _statement(ticker, freq, curr_date, "Cash Flow", "quarterly_cashflow", "cashflow")
|
return _statement(ticker, freq, as_of_date, "Cash Flow", "quarterly_cashflow", "cashflow")
|
||||||
|
|
||||||
|
|
||||||
def get_income_statement(
|
def get_income_statement(
|
||||||
ticker: Annotated[str, "ticker symbol of the company"],
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
freq: Annotated[str, "frequency of data: 'annual' or 'quarterly'"] = "quarterly",
|
||||||
curr_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
as_of_date: Annotated[str, "current date in YYYY-MM-DD format"] = None
|
||||||
):
|
):
|
||||||
"""Get income statement data from yfinance."""
|
"""Get income statement data from yfinance."""
|
||||||
return _statement(ticker, freq, curr_date, "Income Statement", "quarterly_income_stmt", "income_stmt")
|
return _statement(ticker, freq, as_of_date, "Income Statement", "quarterly_income_stmt", "income_stmt")
|
||||||
|
|
||||||
|
|
||||||
# Rows are dated by the transaction, which is when the insider traded, not when
|
# Rows are dated by the transaction, which is when the insider traded, not when
|
||||||
@@ -156,7 +156,7 @@ _TRANSACTION_DATE_VINTAGE = (
|
|||||||
|
|
||||||
def get_insider_transactions(
|
def get_insider_transactions(
|
||||||
ticker: Annotated[str, "ticker symbol of the company"],
|
ticker: Annotated[str, "ticker symbol of the company"],
|
||||||
curr_date: Annotated[str | None, "only transactions on or before this date, yyyy-mm-dd"] = None,
|
as_of_date: Annotated[str | None, "only transactions on or before this date, yyyy-mm-dd"] = None,
|
||||||
):
|
):
|
||||||
"""Get insider transactions data from yfinance."""
|
"""Get insider transactions data from yfinance."""
|
||||||
canonical = normalize_symbol(ticker)
|
canonical = normalize_symbol(ticker)
|
||||||
@@ -171,12 +171,12 @@ def get_insider_transactions(
|
|||||||
raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved")
|
raise VendorRateLimitError("Yahoo Finance is unreachable; insider filings were not retrieved")
|
||||||
return f"No insider transactions reported for symbol '{canonical}'"
|
return f"No insider transactions reported for symbol '{canonical}'"
|
||||||
|
|
||||||
if curr_date:
|
if as_of_date:
|
||||||
traded = data["Start Date"]
|
traded = data["Start Date"]
|
||||||
kept = data[traded <= pd.Timestamp(curr_date)]
|
kept = data[traded <= pd.Timestamp(as_of_date)]
|
||||||
if kept.empty:
|
if kept.empty:
|
||||||
return (
|
return (
|
||||||
f"<insider transactions unavailable for {canonical} as of {curr_date}: "
|
f"<insider transactions unavailable for {canonical} as of {as_of_date}: "
|
||||||
"Yahoo serves recent transactions only>"
|
"Yahoo serves recent transactions only>"
|
||||||
)
|
)
|
||||||
data = kept
|
data = kept
|
||||||
@@ -198,15 +198,15 @@ def get_company_profile(ticker: str) -> dict:
|
|||||||
raise NoMarketDataError(ticker, canonical, f"profile unavailable: {e}") from e
|
raise NoMarketDataError(ticker, canonical, f"profile unavailable: {e}") from e
|
||||||
|
|
||||||
|
|
||||||
def filter_financials_by_date(data: pd.DataFrame, curr_date: str) -> pd.DataFrame:
|
def filter_financials_by_date(data: pd.DataFrame, as_of_date: str) -> pd.DataFrame:
|
||||||
"""Drop financial statement columns (fiscal period timestamps) after curr_date.
|
"""Drop financial statement columns (fiscal period timestamps) after as_of_date.
|
||||||
|
|
||||||
yfinance financial statements use fiscal period end dates as columns.
|
yfinance financial statements use fiscal period end dates as columns.
|
||||||
Columns after curr_date represent future data and are removed to
|
Columns after as_of_date represent future data and are removed to
|
||||||
prevent look-ahead bias.
|
prevent look-ahead bias.
|
||||||
"""
|
"""
|
||||||
if not curr_date or data.empty:
|
if not as_of_date or data.empty:
|
||||||
return data
|
return data
|
||||||
cutoff = pd.Timestamp(curr_date)
|
cutoff = pd.Timestamp(as_of_date)
|
||||||
mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff
|
mask = pd.to_datetime(data.columns, errors="coerce") <= cutoff
|
||||||
return data.loc[:, mask]
|
return data.loc[:, mask]
|
||||||
|
|||||||
+22
-22
@@ -73,7 +73,7 @@ def get_YFin_data_online(
|
|||||||
def get_stock_stats_indicators_window(
|
def get_stock_stats_indicators_window(
|
||||||
symbol: Annotated[str, "ticker symbol of the company"],
|
symbol: Annotated[str, "ticker symbol of the company"],
|
||||||
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
||||||
curr_date: Annotated[
|
as_of_date: Annotated[
|
||||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||||
],
|
],
|
||||||
look_back_days: Annotated[int, "how many days to look back"],
|
look_back_days: Annotated[int, "how many days to look back"],
|
||||||
@@ -157,16 +157,16 @@ def get_stock_stats_indicators_window(
|
|||||||
f"Indicator {indicator} is not supported. Please choose from: {list(best_ind_params.keys())}"
|
f"Indicator {indicator} is not supported. Please choose from: {list(best_ind_params.keys())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
end_date = curr_date
|
end_date = as_of_date
|
||||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
||||||
before = curr_date_dt - relativedelta(days=look_back_days)
|
before = as_of_dt - relativedelta(days=look_back_days)
|
||||||
|
|
||||||
# Optimized: Get stock data once and calculate indicators for all dates
|
# Optimized: Get stock data once and calculate indicators for all dates
|
||||||
try:
|
try:
|
||||||
indicator_data = _get_stock_stats_bulk(symbol, indicator, curr_date)
|
indicator_data = _get_stock_stats_bulk(symbol, indicator, as_of_date)
|
||||||
|
|
||||||
# Generate the date range we need
|
# Generate the date range we need
|
||||||
current_dt = curr_date_dt
|
current_dt = as_of_dt
|
||||||
date_values = []
|
date_values = []
|
||||||
|
|
||||||
while current_dt >= before:
|
while current_dt >= before:
|
||||||
@@ -191,13 +191,13 @@ def get_stock_stats_indicators_window(
|
|||||||
logger.warning("Bulk stockstats fetch failed, falling back per-day: %s", e)
|
logger.warning("Bulk stockstats fetch failed, falling back per-day: %s", e)
|
||||||
# Fallback to original implementation if bulk method fails
|
# Fallback to original implementation if bulk method fails
|
||||||
ind_string = ""
|
ind_string = ""
|
||||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
||||||
while curr_date_dt >= before:
|
while as_of_dt >= before:
|
||||||
indicator_value = get_stockstats_indicator(
|
indicator_value = get_stockstats_indicator(
|
||||||
symbol, indicator, curr_date_dt.strftime("%Y-%m-%d")
|
symbol, indicator, as_of_dt.strftime("%Y-%m-%d")
|
||||||
)
|
)
|
||||||
ind_string += f"{curr_date_dt.strftime('%Y-%m-%d')}: {indicator_value}\n"
|
ind_string += f"{as_of_dt.strftime('%Y-%m-%d')}: {indicator_value}\n"
|
||||||
curr_date_dt = curr_date_dt - relativedelta(days=1)
|
as_of_dt = as_of_dt - relativedelta(days=1)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n"
|
f"## {indicator} values from {before.strftime('%Y-%m-%d')} to {end_date}:\n\n"
|
||||||
@@ -212,7 +212,7 @@ def get_stock_stats_indicators_window(
|
|||||||
def _get_stock_stats_bulk(
|
def _get_stock_stats_bulk(
|
||||||
symbol: Annotated[str, "ticker symbol of the company"],
|
symbol: Annotated[str, "ticker symbol of the company"],
|
||||||
indicator: Annotated[str, "technical indicator to calculate"],
|
indicator: Annotated[str, "technical indicator to calculate"],
|
||||||
curr_date: Annotated[str, "current date for reference"]
|
as_of_date: Annotated[str, "current date for reference"]
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Optimized bulk calculation of stock stats indicators.
|
Optimized bulk calculation of stock stats indicators.
|
||||||
@@ -221,7 +221,7 @@ def _get_stock_stats_bulk(
|
|||||||
"""
|
"""
|
||||||
from stockstats import wrap
|
from stockstats import wrap
|
||||||
|
|
||||||
data = load_ohlcv(symbol, curr_date)
|
data = load_ohlcv(symbol, as_of_date)
|
||||||
df = wrap(data)
|
df = wrap(data)
|
||||||
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
@@ -243,19 +243,19 @@ def _get_stock_stats_bulk(
|
|||||||
def get_stockstats_indicator(
|
def get_stockstats_indicator(
|
||||||
symbol: Annotated[str, "ticker symbol of the company"],
|
symbol: Annotated[str, "ticker symbol of the company"],
|
||||||
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
indicator: Annotated[str, "technical indicator to get the analysis and report of"],
|
||||||
curr_date: Annotated[
|
as_of_date: Annotated[
|
||||||
str, "The current trading date you are trading on, YYYY-mm-dd"
|
str, "The current trading date you are trading on, YYYY-mm-dd"
|
||||||
],
|
],
|
||||||
) -> str:
|
) -> str:
|
||||||
|
|
||||||
curr_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
as_of_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
||||||
curr_date = curr_date_dt.strftime("%Y-%m-%d")
|
as_of_date = as_of_dt.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
indicator_value = get_stock_stats(
|
indicator_value = get_stock_stats(
|
||||||
symbol,
|
symbol,
|
||||||
indicator,
|
indicator,
|
||||||
curr_date,
|
as_of_date,
|
||||||
)
|
)
|
||||||
except VendorError:
|
except VendorError:
|
||||||
raise # Unknown/delisted symbol — let the router emit the sentinel
|
raise # Unknown/delisted symbol — let the router emit the sentinel
|
||||||
@@ -264,7 +264,7 @@ def get_stockstats_indicator(
|
|||||||
# reads as no value that day rather than a read that failed. Raise so the
|
# reads as no value that day rather than a read that failed. Raise so the
|
||||||
# router can try the next vendor or report the series unavailable.
|
# router can try the next vendor or report the series unavailable.
|
||||||
raise NoMarketDataError(
|
raise NoMarketDataError(
|
||||||
symbol, symbol, f"{indicator} could not be read for {curr_date}: {e}"
|
symbol, symbol, f"{indicator} could not be read for {as_of_date}: {e}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
return str(indicator_value)
|
return str(indicator_value)
|
||||||
@@ -285,17 +285,17 @@ def get_stock_stats(
|
|||||||
indicator: Annotated[
|
indicator: Annotated[
|
||||||
str, "quantitative indicators based off of the stock data for the company"
|
str, "quantitative indicators based off of the stock data for the company"
|
||||||
],
|
],
|
||||||
curr_date: Annotated[
|
as_of_date: Annotated[
|
||||||
str, "curr date for retrieving stock price data, YYYY-mm-dd"
|
str, "curr date for retrieving stock price data, YYYY-mm-dd"
|
||||||
],
|
],
|
||||||
):
|
):
|
||||||
data = load_ohlcv(symbol, curr_date)
|
data = load_ohlcv(symbol, as_of_date)
|
||||||
df = wrap(data)
|
df = wrap(data)
|
||||||
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
df["Date"] = df["Date"].dt.strftime("%Y-%m-%d")
|
||||||
curr_date_str = pd.to_datetime(curr_date).strftime("%Y-%m-%d")
|
as_of_str = pd.to_datetime(as_of_date).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
df[indicator] # trigger stockstats to calculate the indicator
|
df[indicator] # trigger stockstats to calculate the indicator
|
||||||
matching_rows = df[df["Date"].str.startswith(curr_date_str)]
|
matching_rows = df[df["Date"].str.startswith(as_of_str)]
|
||||||
|
|
||||||
if not matching_rows.empty:
|
if not matching_rows.empty:
|
||||||
indicator_value = matching_rows[indicator].values[0]
|
indicator_value = matching_rows[indicator].values[0]
|
||||||
|
|||||||
+6
-6
@@ -121,7 +121,7 @@ def get_news_yfinance(
|
|||||||
|
|
||||||
|
|
||||||
def get_global_news_yfinance(
|
def get_global_news_yfinance(
|
||||||
curr_date: str,
|
as_of_date: str,
|
||||||
look_back_days: int | None = None,
|
look_back_days: int | None = None,
|
||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -129,7 +129,7 @@ def get_global_news_yfinance(
|
|||||||
Retrieve global/macro economic news using yfinance Search.
|
Retrieve global/macro economic news using yfinance Search.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
curr_date: Current date in yyyy-mm-dd format
|
as_of_date: Current date in yyyy-mm-dd format
|
||||||
look_back_days: Number of days to look back. ``None`` falls back to
|
look_back_days: Number of days to look back. ``None`` falls back to
|
||||||
``global_news_lookback_days`` from the active config.
|
``global_news_lookback_days`` from the active config.
|
||||||
limit: Maximum number of articles to return. ``None`` falls back to
|
limit: Maximum number of articles to return. ``None`` falls back to
|
||||||
@@ -145,7 +145,7 @@ def get_global_news_yfinance(
|
|||||||
limit = config["global_news_article_limit"]
|
limit = config["global_news_article_limit"]
|
||||||
search_queries = config["global_news_queries"]
|
search_queries = config["global_news_queries"]
|
||||||
|
|
||||||
curr_dt = datetime.strptime(curr_date, "%Y-%m-%d")
|
curr_dt = datetime.strptime(as_of_date, "%Y-%m-%d")
|
||||||
start_dt = curr_dt - relativedelta(days=look_back_days)
|
start_dt = curr_dt - relativedelta(days=look_back_days)
|
||||||
start_date = start_dt.strftime("%Y-%m-%d")
|
start_date = start_dt.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
@@ -189,10 +189,10 @@ def get_global_news_yfinance(
|
|||||||
if not news_str:
|
if not news_str:
|
||||||
# Results merge several fuzzy searches, so their timestamps prove no
|
# Results merge several fuzzy searches, so their timestamps prove no
|
||||||
# continuous coverage; judge the window against the present only.
|
# continuous coverage; judge the window against the present only.
|
||||||
gap = coverage_gap((), start_date, curr_date, "Yahoo Finance global news", "market news")
|
gap = coverage_gap((), start_date, as_of_date, "Yahoo Finance global news", "market news")
|
||||||
return gap or f"No global news found between {start_date} and {curr_date}"
|
return gap or f"No global news found between {start_date} and {as_of_date}"
|
||||||
|
|
||||||
return f"## Global Market News, from {start_date} to {curr_date}:\n\n{news_str}"
|
return f"## Global Market News, from {start_date} to {as_of_date}:\n\n{news_str}"
|
||||||
|
|
||||||
except VendorError:
|
except VendorError:
|
||||||
raise
|
raise
|
||||||
|
|||||||
+16
-16
@@ -93,7 +93,7 @@ def _local_midnight(value) -> pd.Timestamp:
|
|||||||
|
|
||||||
def _normalize_dates(dates) -> pd.Series:
|
def _normalize_dates(dates) -> pd.Series:
|
||||||
"""Parse to naive, midnight-normalized dates so tz-aware or intraday
|
"""Parse to naive, midnight-normalized dates so tz-aware or intraday
|
||||||
timestamps compare correctly against the naive ``curr_date`` cutoff (#1201).
|
timestamps compare correctly against the naive ``as_of_date`` cutoff (#1201).
|
||||||
|
|
||||||
Normalized per element: 5 years of yfinance bars span daylight-saving
|
Normalized per element: 5 years of yfinance bars span daylight-saving
|
||||||
changes (and cache CSVs round-trip the offsets as strings), so the series can
|
changes (and cache CSVs round-trip the offsets as strings), so the series can
|
||||||
@@ -147,13 +147,13 @@ def _coerce_ohlcv_dates(data: pd.DataFrame) -> pd.Series:
|
|||||||
|
|
||||||
def _assert_ohlcv_not_stale(
|
def _assert_ohlcv_not_stale(
|
||||||
data: pd.DataFrame,
|
data: pd.DataFrame,
|
||||||
curr_date: str,
|
as_of_date: str,
|
||||||
symbol: str,
|
symbol: str,
|
||||||
canonical: str | None = None,
|
canonical: str | None = None,
|
||||||
*,
|
*,
|
||||||
max_stale_days: int = MAX_OHLCV_STALE_DAYS,
|
max_stale_days: int = MAX_OHLCV_STALE_DAYS,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Reject OHLCV whose latest row is far older than curr_date.
|
"""Reject OHLCV whose latest row is far older than as_of_date.
|
||||||
|
|
||||||
Raises NoMarketDataError (with a stale-specific detail) so the router treats
|
Raises NoMarketDataError (with a stale-specific detail) so the router treats
|
||||||
it like any other "no usable data from this vendor" — try the next vendor,
|
it like any other "no usable data from this vendor" — try the next vendor,
|
||||||
@@ -164,7 +164,7 @@ def _assert_ohlcv_not_stale(
|
|||||||
"""
|
"""
|
||||||
if data is None or data.empty:
|
if data is None or data.empty:
|
||||||
return
|
return
|
||||||
requested = pd.to_datetime(curr_date, errors="coerce")
|
requested = pd.to_datetime(as_of_date, errors="coerce")
|
||||||
if pd.isna(requested):
|
if pd.isna(requested):
|
||||||
return
|
return
|
||||||
requested = requested.normalize()
|
requested = requested.normalize()
|
||||||
@@ -182,7 +182,7 @@ def _assert_ohlcv_not_stale(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _cache_is_fresh(data_file, curr_date_dt, now) -> bool:
|
def _cache_is_fresh(data_file, as_of_dt, now) -> bool:
|
||||||
"""Whether the symbol's cached download can serve this request.
|
"""Whether the symbol's cached download can serve this request.
|
||||||
|
|
||||||
The file holds the download made on the day it was written, so it serves
|
The file holds the download made on the day it was written, so it serves
|
||||||
@@ -194,14 +194,14 @@ def _cache_is_fresh(data_file, curr_date_dt, now) -> bool:
|
|||||||
written = pd.Timestamp.fromtimestamp(os.path.getmtime(data_file))
|
written = pd.Timestamp.fromtimestamp(os.path.getmtime(data_file))
|
||||||
if written.date() != now.date():
|
if written.date() != now.date():
|
||||||
return False
|
return False
|
||||||
return curr_date_dt.date() < now.date() or (now - written).total_seconds() <= OHLCV_CACHE_TTL_SECONDS
|
return as_of_dt.date() < now.date() or (now - written).total_seconds() <= OHLCV_CACHE_TTL_SECONDS
|
||||||
|
|
||||||
|
|
||||||
def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFrame:
|
def load_ohlcv(symbol: str, as_of_date: str, fill_gaps: bool = True) -> pd.DataFrame:
|
||||||
"""Fetch OHLCV data with caching, filtered to prevent look-ahead bias.
|
"""Fetch OHLCV data with caching, filtered to prevent look-ahead bias.
|
||||||
|
|
||||||
Downloads 5 years of data up to today and caches per symbol. On
|
Downloads 5 years of data up to today and caches per symbol. On
|
||||||
subsequent calls the cache is reused. Rows after curr_date are
|
subsequent calls the cache is reused. Rows after as_of_date are
|
||||||
filtered out so backtests never see future prices.
|
filtered out so backtests never see future prices.
|
||||||
|
|
||||||
``fill_gaps`` carries prices forward over gaps so indicators compute on a
|
``fill_gaps`` carries prices forward over gaps so indicators compute on a
|
||||||
@@ -215,15 +215,15 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
|
|||||||
safe_symbol = safe_ticker_component(canonical)
|
safe_symbol = safe_ticker_component(canonical)
|
||||||
|
|
||||||
config = get_config()
|
config = get_config()
|
||||||
curr_date_dt = pd.to_datetime(curr_date).normalize()
|
as_of_dt = pd.to_datetime(as_of_date).normalize()
|
||||||
|
|
||||||
# One cache file per symbol, holding the latest 5y-to-today download.
|
# One cache file per symbol, holding the latest 5y-to-today download.
|
||||||
now = pd.Timestamp.today()
|
now = pd.Timestamp.today()
|
||||||
start_date = now - pd.DateOffset(years=5)
|
start_date = now - pd.DateOffset(years=5)
|
||||||
start_str = start_date.strftime("%Y-%m-%d")
|
start_str = start_date.strftime("%Y-%m-%d")
|
||||||
# yfinance ``end`` is EXCLUSIVE; request tomorrow so today's row is included
|
# yfinance ``end`` is EXCLUSIVE; request tomorrow so today's row is included
|
||||||
# when curr_date is the current day (#986). Look-ahead is still prevented by
|
# when as_of_date is the current day (#986). Look-ahead is still prevented by
|
||||||
# the curr_date filter below.
|
# the as_of_date filter below.
|
||||||
end_str = (now + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
|
end_str = (now + pd.Timedelta(days=1)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
os.makedirs(config["data_cache_dir"], exist_ok=True)
|
os.makedirs(config["data_cache_dir"], exist_ok=True)
|
||||||
@@ -241,7 +241,7 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
|
|||||||
if (
|
if (
|
||||||
not cached.empty
|
not cached.empty
|
||||||
and "Close" in cached.columns
|
and "Close" in cached.columns
|
||||||
and _cache_is_fresh(data_file, curr_date_dt, now)
|
and _cache_is_fresh(data_file, as_of_dt, now)
|
||||||
):
|
):
|
||||||
data = cached
|
data = cached
|
||||||
|
|
||||||
@@ -271,8 +271,8 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
|
|||||||
|
|
||||||
data = _clean_dataframe(data)
|
data = _clean_dataframe(data)
|
||||||
|
|
||||||
# Filter to curr_date to prevent look-ahead bias in backtesting.
|
# Filter to as_of_date to prevent look-ahead bias in backtesting.
|
||||||
data = data[data["Date"] <= curr_date_dt]
|
data = data[data["Date"] <= as_of_dt]
|
||||||
|
|
||||||
# A closeless newest bar is an unsettled session, not a symbol without data.
|
# A closeless newest bar is an unsettled session, not a symbol without data.
|
||||||
# _fill_price_gaps below drops it, here and mid-series alike, so the frame
|
# _fill_price_gaps below drops it, here and mid-series alike, so the frame
|
||||||
@@ -295,9 +295,9 @@ def load_ohlcv(symbol: str, curr_date: str, fill_gaps: bool = True) -> pd.DataFr
|
|||||||
# a filled cell is the previous session's price under this session's date.
|
# a filled cell is the previous session's price under this session's date.
|
||||||
data = _fill_price_gaps(data) if fill_gaps else data.dropna(subset=["Close"]).copy()
|
data = _fill_price_gaps(data) if fill_gaps else data.dropna(subset=["Close"]).copy()
|
||||||
|
|
||||||
# Reject a stale frame (latest row far older than curr_date) rather than
|
# Reject a stale frame (latest row far older than as_of_date) rather than
|
||||||
# feeding year-old prices into indicators (#1021).
|
# feeding year-old prices into indicators (#1021).
|
||||||
_assert_ohlcv_not_stale(data, curr_date, symbol, canonical)
|
_assert_ohlcv_not_stale(data, as_of_date, symbol, canonical)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -25,8 +25,8 @@ DEFAULT_SNAPSHOT_INDICATORS: tuple[str, ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _verified_rows(symbol: str, curr_date: str) -> pd.DataFrame:
|
def _verified_rows(symbol: str, as_of_date: str) -> pd.DataFrame:
|
||||||
"""OHLCV on or before curr_date, date-sorted. Raises if nothing usable.
|
"""OHLCV on or before as_of_date, date-sorted. Raises if nothing usable.
|
||||||
|
|
||||||
``load_ohlcv`` already normalizes the Date column and filters out
|
``load_ohlcv`` already normalizes the Date column and filters out
|
||||||
look-ahead rows, but we re-apply the cutoff defensively — this is a
|
look-ahead rows, but we re-apply the cutoff defensively — this is a
|
||||||
@@ -34,16 +34,16 @@ def _verified_rows(symbol: str, curr_date: str) -> pd.DataFrame:
|
|||||||
"""
|
"""
|
||||||
# As reported: this snapshot is quoted by the agents as exact prices, so a
|
# As reported: this snapshot is quoted by the agents as exact prices, so a
|
||||||
# gap-filled cell would put the previous session's number under this date.
|
# gap-filled cell would put the previous session's number under this date.
|
||||||
data = load_ohlcv(symbol, curr_date, fill_gaps=False)
|
data = load_ohlcv(symbol, as_of_date, fill_gaps=False)
|
||||||
if data is None or data.empty:
|
if data is None or data.empty:
|
||||||
raise ValueError(f"No OHLCV data available for {symbol}.")
|
raise ValueError(f"No OHLCV data available for {symbol}.")
|
||||||
|
|
||||||
df = data.copy()
|
df = data.copy()
|
||||||
df["Date"] = pd.to_datetime(df["Date"], errors="coerce")
|
df["Date"] = pd.to_datetime(df["Date"], errors="coerce")
|
||||||
df = df.dropna(subset=["Date"])
|
df = df.dropna(subset=["Date"])
|
||||||
df = df[df["Date"] <= pd.to_datetime(curr_date)].sort_values("Date")
|
df = df[df["Date"] <= pd.to_datetime(as_of_date)].sort_values("Date")
|
||||||
if df.empty:
|
if df.empty:
|
||||||
raise ValueError(f"No OHLCV rows on or before {curr_date} for {symbol}.")
|
raise ValueError(f"No OHLCV rows on or before {as_of_date} for {symbol}.")
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ def _fmt(value) -> str:
|
|||||||
|
|
||||||
def build_verified_market_snapshot(
|
def build_verified_market_snapshot(
|
||||||
symbol: str,
|
symbol: str,
|
||||||
curr_date: str,
|
as_of_date: str,
|
||||||
look_back_days: int = 30,
|
look_back_days: int = 30,
|
||||||
indicators: Iterable[str] | None = None,
|
indicators: Iterable[str] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
@@ -71,7 +71,7 @@ def build_verified_market_snapshot(
|
|||||||
# `df` keeps the original capitalized OHLCV columns (Open/High/Low/Close/
|
# `df` keeps the original capitalized OHLCV columns (Open/High/Low/Close/
|
||||||
# Volume); stockstats `wrap()` lowercases columns and adds indicator
|
# Volume); stockstats `wrap()` lowercases columns and adds indicator
|
||||||
# columns, so read raw prices from `df` and indicators from `stock_df`.
|
# columns, so read raw prices from `df` and indicators from `stock_df`.
|
||||||
df = _verified_rows(symbol, curr_date)
|
df = _verified_rows(symbol, as_of_date)
|
||||||
stock_df = wrap(df.copy())
|
stock_df = wrap(df.copy())
|
||||||
|
|
||||||
selected = tuple(indicators or DEFAULT_SNAPSHOT_INDICATORS)
|
selected = tuple(indicators or DEFAULT_SNAPSHOT_INDICATORS)
|
||||||
@@ -91,7 +91,7 @@ def build_verified_market_snapshot(
|
|||||||
lines = [
|
lines = [
|
||||||
f"## Verified market data snapshot for {symbol.upper()}",
|
f"## Verified market data snapshot for {symbol.upper()}",
|
||||||
"",
|
"",
|
||||||
f"- Requested analysis date: {curr_date}",
|
f"- Requested analysis date: {as_of_date}",
|
||||||
f"- Latest trading row used: {latest_date}",
|
f"- Latest trading row used: {latest_date}",
|
||||||
"- Rows after the requested analysis date are excluded before verification.",
|
"- Rows after the requested analysis date are excluded before verification.",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class TradingAgentsGraph:
|
|||||||
self._resuming = False
|
self._resuming = False
|
||||||
|
|
||||||
def resolve_instrument_context(self, ticker: str, asset_type: str = "stock",
|
def resolve_instrument_context(self, ticker: str, asset_type: str = "stock",
|
||||||
curr_date: str | None = None) -> str:
|
trade_date: str | None = None) -> str:
|
||||||
"""Resolve ticker identity once and return the full instrument context.
|
"""Resolve ticker identity once and return the full instrument context.
|
||||||
|
|
||||||
Deterministic yfinance lookup (cached, fail-open) injected into a
|
Deterministic yfinance lookup (cached, fail-open) injected into a
|
||||||
@@ -124,7 +124,7 @@ class TradingAgentsGraph:
|
|||||||
graph regardless of entry point.
|
graph regardless of entry point.
|
||||||
"""
|
"""
|
||||||
identity = resolve_instrument_identity(ticker)
|
identity = resolve_instrument_identity(ticker)
|
||||||
return build_instrument_context(ticker, asset_type, identity, curr_date)
|
return build_instrument_context(ticker, asset_type, identity, trade_date)
|
||||||
|
|
||||||
def _memory_as_of(self, trade_date) -> str | None:
|
def _memory_as_of(self, trade_date) -> str | None:
|
||||||
"""Point-in-time cutoff for past-context lessons (#1251).
|
"""Point-in-time cutoff for past-context lessons (#1251).
|
||||||
|
|||||||
Reference in New Issue
Block a user