fix(tools): hand vendors the model's dates as YYYY-MM-DD (#1488)

- as_of and as_of_window return canonical dates, so an unpadded date such as 2024-7-5 no longer compares as later than a filing dated 2024-07-24
- create_run_state validates the run date, as propagate does
This commit is contained in:
Yijia-Xiao
2026-10-27 07:57:32 +00:00
parent 2fe04cc771
commit a5b0f217e3
3 changed files with 27 additions and 8 deletions
+15
View File
@@ -28,6 +28,7 @@ TRADE_DATE = "2026-08-14"
(None, TRADE_DATE), # omitted
("Sept 1", TRADE_DATE), # unparseable
("", TRADE_DATE),
("2026-8-1", "2026-08-01"), # unpadded: served in its canonical form
])
def test_as_of_takes_the_earlier_date(requested, expected):
assert as_of(requested, TRADE_DATE) == expected
@@ -36,6 +37,7 @@ def test_as_of_takes_the_earlier_date(requested, expected):
@pytest.mark.unit
def test_as_of_without_a_trade_date_passes_the_request_through():
assert as_of("2026-09-14", "") == "2026-09-14"
assert as_of("2026-9-14", "") == "2026-09-14"
@pytest.mark.unit
@@ -43,6 +45,7 @@ def test_as_of_without_a_trade_date_passes_the_request_through():
("2026-08-01", "2026-09-14", ("2026-08-01", TRADE_DATE)), # end clamped
("2026-08-01", "2026-08-10", ("2026-08-01", "2026-08-10")), # inside: unchanged
("2026-09-01", "2026-09-08", ("2026-08-07", TRADE_DATE)), # wholly later: span kept, moved back
("2026-8-1", "2026-8-10", ("2026-08-01", "2026-08-10")), # unpadded: canonical
])
def test_as_of_window(start, end, expected):
assert as_of_window(start, end, TRADE_DATE) == expected
@@ -128,6 +131,16 @@ def test_future_window_from_the_model_is_clamped():
assert args == ("get_stock_data", "NVDA", "2026-08-01", TRADE_DATE)
@pytest.mark.unit
def test_an_unpadded_date_reaches_the_vendor_in_canonical_form():
# Vendors compare dates as YYYY-MM-DD text: "2026-07-23" > "2026-7-3" is False,
# so an unpadded date would let a filing made after it through.
args = _run(tools.get_income_statement, {"curr_date": "2026-7-3"}, tools)
assert args[-1] == "2026-07-03"
args = _run(tools.get_news, {"start_date": "2026-7-1", "end_date": "2026-7-3"}, tools)
assert args[-2:] == ("2026-07-01", "2026-07-03")
@pytest.mark.unit
def test_direct_call_without_state_is_unchanged():
with mock.patch.object(tools, "route_to_vendor", return_value="ok") as routed:
@@ -161,6 +174,8 @@ def test_propagate_rejects_a_non_canonical_date(bad):
with pytest.raises(ValueError, match="YYYY-MM-DD"):
object.__new__(TradingAgentsGraph).propagate("AAPL", bad)
with pytest.raises(ValueError, match="YYYY-MM-DD"): # the CLI starts a run here
object.__new__(TradingAgentsGraph).create_run_state("AAPL", bad)
@pytest.mark.unit
+11 -8
View File
@@ -1,7 +1,8 @@
"""Point-in-time rules shared by every dated path: data is served as of the run's date.
- ``as_of`` / ``as_of_window`` clamp a date or window the model asks for to the
trade date, so no tool reaches a vendor with a later one.
trade date, so no tool reaches a vendor with a later one, and always hand it
over as ``YYYY-MM-DD``: vendors compare dates as text.
- ``in_window`` trims dated items (news, StockTwits, Reddit) to the analysis
window: timestamps normalized to UTC, the upper bound exclusive at midnight
after ``end``, and an undated item kept only when the window reaches the
@@ -79,13 +80,14 @@ def as_of(requested: str | None, trade_date: str) -> str | None:
"""The date a tool serves: the model's date, but never later than the run's.
A model can omit the date or pass today's instead of the analysis date, which
would walk past every point-in-time guard behind the tool. An empty
would walk past every point-in-time guard behind the tool. A date is served in
its canonical form: "2026-7-3" compares as later than "2026-07-23". An empty
``trade_date`` (a direct call outside a graph run) passes the request through.
"""
if not trade_date:
return requested
parsed = _parse(requested)
return requested if parsed is not None and parsed <= _parse(trade_date) else trade_date
if not trade_date:
return requested if parsed is None else f"{parsed:%Y-%m-%d}"
return f"{parsed:%Y-%m-%d}" if parsed is not None and parsed <= _parse(trade_date) else trade_date
def as_of_window(start_date: str, end_date: str, trade_date: str) -> tuple[str, str]:
@@ -99,10 +101,11 @@ def as_of_window(start_date: str, end_date: str, trade_date: str) -> tuple[str,
if start is None:
raise ValueError(f"start_date {start_date!r} is not a date; give it as YYYY-MM-DD")
end = as_of(end_date, trade_date)
if end == end_date or start <= _parse(end):
return start_date, end
end_dt = _parse(end)
if end_dt is None or end_dt == old_end or start <= end_dt:
return f"{start:%Y-%m-%d}", end
span = (old_end - start) if old_end is not None and old_end >= start else timedelta(0)
return f"{_parse(end) - span:%Y-%m-%d}", end
return f"{end_dt - span:%Y-%m-%d}", end
def withhold_live_profile(as_of_date: str | None, label: str) -> str | None:
+1
View File
@@ -297,6 +297,7 @@ class TradingAgentsGraph:
memory log's lessons are not here: the graph's Memory Log step settles
and loads them alongside the analysts (see ``_memory_step``).
"""
trade_date = _validate_trade_date(trade_date)
return self.propagator.create_initial_state(
company_name,
trade_date,