mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 19:25:24 +03:00
fix(agents): bound tool dates by the run's trade date
- dated tools read trade_date from graph state and clamp later or missing dates #1331 - propagate() rejects non-canonical and future trade dates #1319
This commit is contained in:
140
tests/test_tool_date_enforcement.py
Normal file
140
tests/test_tool_date_enforcement.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
"""Dated tools take the analysis date from graph state, not from the model.
|
||||||
|
|
||||||
|
Every point-in-time guard behind a tool trusts the date it is given. A model that
|
||||||
|
omits the date, or passes today's instead of the analysis date, would otherwise
|
||||||
|
walk past them. The run's trade_date is injected from state and hidden from the
|
||||||
|
model-visible schema.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from langchain_core.messages import AIMessage
|
||||||
|
from langgraph.graph import END, START, MessagesState, StateGraph
|
||||||
|
from langgraph.prebuilt import ToolNode
|
||||||
|
|
||||||
|
from tradingagents.agents.utils import (
|
||||||
|
core_stock_tools,
|
||||||
|
fundamental_data_tools,
|
||||||
|
macro_data_tools,
|
||||||
|
market_data_validation_tools,
|
||||||
|
news_data_tools,
|
||||||
|
technical_indicators_tools,
|
||||||
|
)
|
||||||
|
from tradingagents.dataflows.date_window import as_of, as_of_window
|
||||||
|
|
||||||
|
TRADE_DATE = "2026-08-14"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("requested, expected", [
|
||||||
|
("2026-09-14", TRADE_DATE), # later than the run: clamped
|
||||||
|
("2026-08-01", "2026-08-01"), # earlier: narrows, allowed
|
||||||
|
(None, TRADE_DATE), # omitted
|
||||||
|
("Sept 1", TRADE_DATE), # unparseable
|
||||||
|
("", TRADE_DATE),
|
||||||
|
])
|
||||||
|
def test_as_of_takes_the_earlier_date(requested, expected):
|
||||||
|
assert as_of(requested, TRADE_DATE) == 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"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("start, end, expected", [
|
||||||
|
("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
|
||||||
|
])
|
||||||
|
def test_as_of_window(start, end, expected):
|
||||||
|
assert as_of_window(start, end, TRADE_DATE) == expected
|
||||||
|
|
||||||
|
|
||||||
|
DATED_TOOLS = [
|
||||||
|
core_stock_tools.get_stock_data,
|
||||||
|
fundamental_data_tools.get_fundamentals,
|
||||||
|
fundamental_data_tools.get_balance_sheet,
|
||||||
|
fundamental_data_tools.get_cashflow,
|
||||||
|
fundamental_data_tools.get_income_statement,
|
||||||
|
news_data_tools.get_news,
|
||||||
|
news_data_tools.get_global_news,
|
||||||
|
technical_indicators_tools.get_indicators,
|
||||||
|
macro_data_tools.get_macro_indicators,
|
||||||
|
market_data_validation_tools.get_verified_market_snapshot,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("tool", DATED_TOOLS, ids=lambda t: t.name)
|
||||||
|
def test_trade_date_is_hidden_from_the_model(tool):
|
||||||
|
assert "trade_date" not in tool.tool_call_schema.model_json_schema()["properties"]
|
||||||
|
|
||||||
|
|
||||||
|
class _State(MessagesState):
|
||||||
|
trade_date: str
|
||||||
|
|
||||||
|
|
||||||
|
def _run(tool, args, module):
|
||||||
|
"""Call the tool through a ToolNode in a graph carrying the run's trade_date."""
|
||||||
|
graph = StateGraph(_State)
|
||||||
|
graph.add_node("tools", ToolNode([tool]))
|
||||||
|
graph.add_edge(START, "tools")
|
||||||
|
graph.add_edge("tools", END)
|
||||||
|
with mock.patch.object(module, "route_to_vendor", return_value="ok") as routed:
|
||||||
|
graph.compile().invoke({
|
||||||
|
"messages": [AIMessage("", tool_calls=[{"name": tool.name, "args": args, "id": "1"}])],
|
||||||
|
"trade_date": TRADE_DATE,
|
||||||
|
})
|
||||||
|
return routed.call_args.args
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_statement_tool_with_omitted_date_uses_the_run_date():
|
||||||
|
args = _run(fundamental_data_tools.get_balance_sheet, {"ticker": "AAPL"}, fundamental_data_tools)
|
||||||
|
assert args[-1] == TRADE_DATE # #1331: an omitted date no longer means unfiltered
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_future_curr_date_from_the_model_is_clamped():
|
||||||
|
args = _run(fundamental_data_tools.get_fundamentals,
|
||||||
|
{"ticker": "AAPL", "curr_date": "2026-09-14"}, fundamental_data_tools)
|
||||||
|
assert args == ("get_fundamentals", "AAPL", TRADE_DATE)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_future_window_from_the_model_is_clamped():
|
||||||
|
args = _run(core_stock_tools.get_stock_data,
|
||||||
|
{"symbol": "AAPL", "start_date": "2026-08-01", "end_date": "2026-09-14"}, core_stock_tools)
|
||||||
|
assert args == ("get_stock_data", "AAPL", "2026-08-01", TRADE_DATE)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_direct_call_without_state_is_unchanged():
|
||||||
|
with mock.patch.object(news_data_tools, "route_to_vendor", return_value="ok") as routed:
|
||||||
|
news_data_tools.get_news.func("AAPL", "2026-09-01", "2026-09-08")
|
||||||
|
assert routed.call_args.args == ("get_news", "AAPL", "2026-09-01", "2026-09-08")
|
||||||
|
|
||||||
|
|
||||||
|
# --- the run date itself (#1319) -------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("bad", ["2026-9-10", "2026-09-10 00:00", "Sept 10", None])
|
||||||
|
def test_propagate_rejects_a_non_canonical_date(bad):
|
||||||
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="YYYY-MM-DD"):
|
||||||
|
object.__new__(TradingAgentsGraph).propagate("AAPL", bad)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_propagate_rejects_a_future_date(monkeypatch):
|
||||||
|
import tradingagents.graph.trading_graph as tg
|
||||||
|
|
||||||
|
monkeypatch.setattr(tg, "get_current_date", lambda: "2026-09-10")
|
||||||
|
with pytest.raises(ValueError, match="future"):
|
||||||
|
object.__new__(tg.TradingAgentsGraph).propagate("AAPL", "2026-09-11")
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.prebuilt import InjectedState
|
||||||
|
|
||||||
|
from tradingagents.dataflows.date_window import as_of_window
|
||||||
from tradingagents.dataflows.interface import route_to_vendor
|
from tradingagents.dataflows.interface import route_to_vendor
|
||||||
|
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ def get_stock_data(
|
|||||||
symbol: Annotated[str, "ticker symbol of the company"],
|
symbol: Annotated[str, "ticker symbol of the company"],
|
||||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||||
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve stock price data (OHLCV) for a given ticker symbol.
|
Retrieve stock price data (OHLCV) for a given ticker symbol.
|
||||||
@@ -21,4 +24,5 @@ def get_stock_data(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted dataframe containing the stock price data for the specified ticker symbol in the specified date range.
|
str: A formatted dataframe containing the stock price data for the specified ticker symbol in the specified date range.
|
||||||
"""
|
"""
|
||||||
|
start_date, end_date = as_of_window(start_date, end_date, trade_date)
|
||||||
return route_to_vendor("get_stock_data", symbol, start_date, end_date)
|
return route_to_vendor("get_stock_data", symbol, start_date, end_date)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.prebuilt import InjectedState
|
||||||
|
|
||||||
|
from tradingagents.dataflows.date_window import as_of
|
||||||
from tradingagents.dataflows.interface import route_to_vendor
|
from tradingagents.dataflows.interface import route_to_vendor
|
||||||
|
|
||||||
|
|
||||||
@@ -9,6 +11,7 @@ from tradingagents.dataflows.interface import route_to_vendor
|
|||||||
def get_fundamentals(
|
def get_fundamentals(
|
||||||
ticker: Annotated[str, "ticker symbol"],
|
ticker: Annotated[str, "ticker symbol"],
|
||||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"],
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve comprehensive fundamental data for a given ticker symbol.
|
Retrieve comprehensive fundamental data for a given ticker symbol.
|
||||||
@@ -19,7 +22,7 @@ def get_fundamentals(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted report containing comprehensive fundamental data
|
str: A formatted report containing comprehensive fundamental data
|
||||||
"""
|
"""
|
||||||
return route_to_vendor("get_fundamentals", ticker, curr_date)
|
return route_to_vendor("get_fundamentals", ticker, as_of(curr_date, trade_date))
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
@@ -27,6 +30,7 @@ def get_balance_sheet(
|
|||||||
ticker: Annotated[str, "ticker symbol"],
|
ticker: Annotated[str, "ticker symbol"],
|
||||||
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
||||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve balance sheet data for a given ticker symbol.
|
Retrieve balance sheet data for a given ticker symbol.
|
||||||
@@ -38,7 +42,7 @@ def get_balance_sheet(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted report containing balance sheet data
|
str: A formatted report containing balance sheet data
|
||||||
"""
|
"""
|
||||||
return route_to_vendor("get_balance_sheet", ticker, freq, curr_date)
|
return route_to_vendor("get_balance_sheet", ticker, freq, as_of(curr_date, trade_date))
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
@@ -46,6 +50,7 @@ def get_cashflow(
|
|||||||
ticker: Annotated[str, "ticker symbol"],
|
ticker: Annotated[str, "ticker symbol"],
|
||||||
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
||||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve cash flow statement data for a given ticker symbol.
|
Retrieve cash flow statement data for a given ticker symbol.
|
||||||
@@ -57,7 +62,7 @@ def get_cashflow(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted report containing cash flow statement data
|
str: A formatted report containing cash flow statement data
|
||||||
"""
|
"""
|
||||||
return route_to_vendor("get_cashflow", ticker, freq, curr_date)
|
return route_to_vendor("get_cashflow", ticker, freq, as_of(curr_date, trade_date))
|
||||||
|
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
@@ -65,6 +70,7 @@ def get_income_statement(
|
|||||||
ticker: Annotated[str, "ticker symbol"],
|
ticker: Annotated[str, "ticker symbol"],
|
||||||
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
freq: Annotated[str, "reporting frequency: annual/quarterly"] = "quarterly",
|
||||||
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve income statement data for a given ticker symbol.
|
Retrieve income statement data for a given ticker symbol.
|
||||||
@@ -76,4 +82,4 @@ def get_income_statement(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted report containing income statement data
|
str: A formatted report containing income statement data
|
||||||
"""
|
"""
|
||||||
return route_to_vendor("get_income_statement", ticker, freq, curr_date)
|
return route_to_vendor("get_income_statement", ticker, freq, as_of(curr_date, trade_date))
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.prebuilt import InjectedState
|
||||||
|
|
||||||
|
from tradingagents.dataflows.date_window import as_of
|
||||||
from tradingagents.dataflows.interface import route_to_vendor
|
from tradingagents.dataflows.interface import route_to_vendor
|
||||||
|
|
||||||
|
|
||||||
@@ -17,6 +19,7 @@ def get_macro_indicators(
|
|||||||
look_back_days: Annotated[
|
look_back_days: Annotated[
|
||||||
int | None, "Trailing window length in days; omit for a 1-year window"
|
int | None, "Trailing window length in days; omit for a 1-year window"
|
||||||
] = None,
|
] = None,
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve a macroeconomic indicator time series from FRED (Federal Reserve
|
Retrieve a macroeconomic indicator time series from FRED (Federal Reserve
|
||||||
@@ -33,4 +36,4 @@ def get_macro_indicators(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted markdown report of the macro series
|
str: A formatted markdown report of the macro series
|
||||||
"""
|
"""
|
||||||
return route_to_vendor("get_macro_indicators", indicator, curr_date, look_back_days)
|
return route_to_vendor("get_macro_indicators", indicator, as_of(curr_date, trade_date), look_back_days)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.prebuilt import InjectedState
|
||||||
|
|
||||||
|
from tradingagents.dataflows.date_window import as_of
|
||||||
from tradingagents.dataflows.market_data_validator import build_verified_market_snapshot
|
from tradingagents.dataflows.market_data_validator import build_verified_market_snapshot
|
||||||
|
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ def get_verified_market_snapshot(
|
|||||||
look_back_days: Annotated[
|
look_back_days: Annotated[
|
||||||
int, "number of recent trading rows to include for sanity-checking"
|
int, "number of recent trading rows to include for sanity-checking"
|
||||||
] = 30,
|
] = 30,
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Deterministic verification snapshot for exact market-data claims.
|
"""Deterministic verification snapshot for exact market-data claims.
|
||||||
|
|
||||||
@@ -20,4 +23,4 @@ def get_verified_market_snapshot(
|
|||||||
price levels, Bollinger bands, RSI, MACD, moving averages, support /
|
price levels, Bollinger bands, RSI, MACD, moving averages, support /
|
||||||
resistance, or historical comparisons, and treat it as the source of truth.
|
resistance, or historical comparisons, and treat it as the source of truth.
|
||||||
"""
|
"""
|
||||||
return build_verified_market_snapshot(symbol, curr_date, look_back_days)
|
return build_verified_market_snapshot(symbol, as_of(curr_date, trade_date), look_back_days)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.prebuilt import InjectedState
|
||||||
|
|
||||||
|
from tradingagents.dataflows.date_window import as_of, as_of_window
|
||||||
from tradingagents.dataflows.interface import route_to_vendor
|
from tradingagents.dataflows.interface import route_to_vendor
|
||||||
|
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ def get_news(
|
|||||||
ticker: Annotated[str, "Ticker symbol"],
|
ticker: Annotated[str, "Ticker symbol"],
|
||||||
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
start_date: Annotated[str, "Start date in yyyy-mm-dd format"],
|
||||||
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
end_date: Annotated[str, "End date in yyyy-mm-dd format"],
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve news data for a given ticker symbol.
|
Retrieve news data for a given ticker symbol.
|
||||||
@@ -21,6 +24,7 @@ def get_news(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted string containing news data
|
str: A formatted string containing news data
|
||||||
"""
|
"""
|
||||||
|
start_date, end_date = as_of_window(start_date, end_date, trade_date)
|
||||||
return route_to_vendor("get_news", ticker, start_date, end_date)
|
return route_to_vendor("get_news", ticker, start_date, end_date)
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
@@ -28,6 +32,7 @@ def get_global_news(
|
|||||||
curr_date: Annotated[str, "Current date in yyyy-mm-dd format"],
|
curr_date: Annotated[str, "Current date in yyyy-mm-dd format"],
|
||||||
look_back_days: Annotated[int | None, "Days to look back; omit to use the configured default"] = None,
|
look_back_days: Annotated[int | None, "Days to look back; omit to use the configured default"] = None,
|
||||||
limit: Annotated[int | None, "Max articles to return; omit to use the configured default"] = None,
|
limit: Annotated[int | None, "Max articles to return; omit to use the configured default"] = None,
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve global news data.
|
Retrieve global news data.
|
||||||
@@ -43,7 +48,7 @@ def get_global_news(
|
|||||||
Returns:
|
Returns:
|
||||||
str: A formatted string containing global news data
|
str: A formatted string containing global news data
|
||||||
"""
|
"""
|
||||||
return route_to_vendor("get_global_news", curr_date, look_back_days, limit)
|
return route_to_vendor("get_global_news", as_of(curr_date, trade_date), look_back_days, limit)
|
||||||
|
|
||||||
@tool
|
@tool
|
||||||
def get_insider_transactions(
|
def get_insider_transactions(
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from langchain_core.tools import tool
|
from langchain_core.tools import tool
|
||||||
|
from langgraph.prebuilt import InjectedState
|
||||||
|
|
||||||
|
from tradingagents.dataflows.date_window import as_of
|
||||||
from tradingagents.dataflows.interface import route_to_vendor
|
from tradingagents.dataflows.interface import route_to_vendor
|
||||||
|
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ def get_indicators(
|
|||||||
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[str, "The current trading date you are trading on, YYYY-mm-dd"],
|
curr_date: Annotated[str, "The current trading date you are trading on, YYYY-mm-dd"],
|
||||||
look_back_days: Annotated[int, "how many days to look back"] = 30,
|
look_back_days: Annotated[int, "how many days to look back"] = 30,
|
||||||
|
trade_date: Annotated[str, InjectedState("trade_date")] = "",
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Retrieve a single technical indicator for a given ticker symbol.
|
Retrieve a single technical indicator for a given ticker symbol.
|
||||||
@@ -25,6 +28,7 @@ def get_indicators(
|
|||||||
"""
|
"""
|
||||||
# LLMs sometimes pass multiple indicators as a comma-separated string;
|
# LLMs sometimes pass multiple indicators as a comma-separated string;
|
||||||
# split and process each individually.
|
# split and process each individually.
|
||||||
|
curr_date = as_of(curr_date, trade_date)
|
||||||
indicators = [i.strip().lower() for i in indicator.split(",") if i.strip()]
|
indicators = [i.strip().lower() for i in indicator.split(",") if i.strip()]
|
||||||
results = []
|
results = []
|
||||||
for ind in indicators:
|
for ind in indicators:
|
||||||
|
|||||||
@@ -59,6 +59,39 @@ def coverage_gap(
|
|||||||
return f"<{source} unavailable for {start_date}..{end_date}: {reason}, so this is not an absence of {subject}>"
|
return f"<{source} unavailable for {start_date}..{end_date}: {reason}, so this is not an absence of {subject}>"
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(date: str | None) -> datetime | None:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(date, "%Y-%m-%d")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
``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
|
||||||
|
|
||||||
|
|
||||||
|
def as_of_window(start_date: str, end_date: str, trade_date: str) -> tuple[str, str]:
|
||||||
|
"""``[start, end]`` with its end clamped to the run date.
|
||||||
|
|
||||||
|
A window wholly after the run date keeps its length and moves back to end there.
|
||||||
|
"""
|
||||||
|
end = as_of(end_date, trade_date)
|
||||||
|
start, old_end = _parse(start_date), _parse(end_date)
|
||||||
|
if end == end_date or start is None or start <= _parse(end):
|
||||||
|
return start_date, 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
|
||||||
|
|
||||||
|
|
||||||
def withhold_live_profile(curr_date: str | None, label: str) -> str | None:
|
def withhold_live_profile(curr_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.
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from tradingagents.agents.utils.agent_utils import (
|
|||||||
)
|
)
|
||||||
from tradingagents.agents.utils.memory import TradingMemoryLog
|
from tradingagents.agents.utils.memory import TradingMemoryLog
|
||||||
from tradingagents.dataflows.config import set_config
|
from tradingagents.dataflows.config import set_config
|
||||||
from tradingagents.dataflows.utils import safe_ticker_component
|
from tradingagents.dataflows.utils import get_current_date, safe_ticker_component
|
||||||
from tradingagents.default_config import DEFAULT_CONFIG
|
from tradingagents.default_config import DEFAULT_CONFIG
|
||||||
from tradingagents.llm_clients import create_llm_client
|
from tradingagents.llm_clients import create_llm_client
|
||||||
from tradingagents.reporting import write_report_tree
|
from tradingagents.reporting import write_report_tree
|
||||||
@@ -45,6 +45,20 @@ from .signal_processing import SignalProcessor
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_trade_date(trade_date) -> str:
|
||||||
|
"""The run date as a canonical ``YYYY-MM-DD`` string no later than today."""
|
||||||
|
value = str(trade_date)
|
||||||
|
try:
|
||||||
|
canonical = datetime.strptime(value, "%Y-%m-%d").strftime("%Y-%m-%d") == value
|
||||||
|
except ValueError:
|
||||||
|
canonical = False
|
||||||
|
if not canonical:
|
||||||
|
raise ValueError(f"trade_date must be a date in YYYY-MM-DD format, got {trade_date!r}")
|
||||||
|
if value > get_current_date():
|
||||||
|
raise ValueError(f"trade_date cannot be in the future: {value}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _coerce_max_retries(value):
|
def _coerce_max_retries(value):
|
||||||
"""Validate an ``llm_max_retries`` value to a non-negative int.
|
"""Validate an ``llm_max_retries`` value to a non-negative int.
|
||||||
|
|
||||||
@@ -417,6 +431,7 @@ class TradingAgentsGraph:
|
|||||||
``tradingagents.agents.utils.rating.is_review`` before mapping it to the
|
``tradingagents.agents.utils.rating.is_review`` before mapping it to the
|
||||||
PortfolioRating enum.
|
PortfolioRating enum.
|
||||||
"""
|
"""
|
||||||
|
trade_date = _validate_trade_date(trade_date)
|
||||||
self.ticker = company_name
|
self.ticker = company_name
|
||||||
|
|
||||||
with self.checkpoint_scope(company_name, trade_date, asset_type) as thread_id_value:
|
with self.checkpoint_scope(company_name, trade_date, asset_type) as thread_id_value:
|
||||||
|
|||||||
Reference in New Issue
Block a user