mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-04 00:22:43 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user