mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-26 06:22:49 +03:00
chore(graph): remove SignalProcessor
- process_signal reads the rating with parse_rating; the adapter ignored the model it was given
This commit is contained in:
@@ -76,8 +76,8 @@ class _FakeGraph:
|
|||||||
return {"messages": [], "company_of_interest": ticker}
|
return {"messages": [], "company_of_interest": ticker}
|
||||||
|
|
||||||
def process_signal(self, text):
|
def process_signal(self, text):
|
||||||
from tradingagents.graph.signal_processing import SignalProcessor
|
from tradingagents.agents.utils.rating import parse_rating
|
||||||
return SignalProcessor.process_signal(None, text)
|
return parse_rating(text)
|
||||||
|
|
||||||
def record_decision(self, ticker, trade_date, final_state):
|
def record_decision(self, ticker, trade_date, final_state):
|
||||||
self.calls.append(("record_decision", ticker, trade_date, final_state.get("final_trade_decision")))
|
self.calls.append(("record_decision", ticker, trade_date, final_state.get("final_trade_decision")))
|
||||||
|
|||||||
@@ -937,7 +937,7 @@ class TestLegacyRemoval:
|
|||||||
mock_graph.graph.invoke.return_value = fake_state
|
mock_graph.graph.invoke.return_value = fake_state
|
||||||
mock_graph.propagator.create_initial_state.return_value = fake_state
|
mock_graph.propagator.create_initial_state.return_value = fake_state
|
||||||
mock_graph.propagator.get_graph_args.return_value = {}
|
mock_graph.propagator.get_graph_args.return_value = {}
|
||||||
mock_graph.signal_processor.process_signal.return_value = "Buy"
|
mock_graph.process_signal.return_value = "Buy"
|
||||||
# Bind the real _run_graph so propagate's call to self._run_graph executes
|
# Bind the real _run_graph so propagate's call to self._run_graph executes
|
||||||
# the actual write path instead of the auto-MagicMock.
|
# the actual write path instead of the auto-MagicMock.
|
||||||
mock_graph._run_graph = functools.partial(
|
mock_graph._run_graph = functools.partial(
|
||||||
|
|||||||
@@ -75,13 +75,13 @@ def test_the_memory_log_records_review_rather_than_a_tradeable_hold(tmp_path):
|
|||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_the_signal_and_the_log_agree_on_the_same_decision(tmp_path):
|
def test_the_signal_and_the_log_agree_on_the_same_decision(tmp_path):
|
||||||
from tradingagents.agents.utils.memory import TradingMemoryLog
|
from tradingagents.agents.utils.memory import TradingMemoryLog
|
||||||
from tradingagents.graph.signal_processing import SignalProcessor
|
from tradingagents.agents.utils.rating import parse_rating
|
||||||
|
|
||||||
log = TradingMemoryLog({"memory_log_path": str(tmp_path / "m.md")})
|
log = TradingMemoryLog({"memory_log_path": str(tmp_path / "m.md")})
|
||||||
for text in (INVERTED, REFUSAL, "**Rating**: Buy\n\nAccumulate."):
|
for text in (INVERTED, REFUSAL, "**Rating**: Buy\n\nAccumulate."):
|
||||||
log.store_decision("NVDA", f"2026-01-0{len(log.load_entries()) + 1}", text)
|
log.store_decision("NVDA", f"2026-01-0{len(log.load_entries()) + 1}", text)
|
||||||
|
|
||||||
signals = [SignalProcessor.process_signal(None, text)
|
signals = [parse_rating(text)
|
||||||
for text in (INVERTED, REFUSAL, "**Rating**: Buy\n\nAccumulate.")]
|
for text in (INVERTED, REFUSAL, "**Rating**: Buy\n\nAccumulate.")]
|
||||||
assert [e["rating"] for e in log.load_entries()] == signals
|
assert [e["rating"] for e in log.load_entries()] == signals
|
||||||
|
|
||||||
@@ -121,8 +121,8 @@ def test_the_cli_says_when_a_run_produced_no_usable_rating(monkeypatch, tmp_path
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def process_signal(self, text):
|
def process_signal(self, text):
|
||||||
from tradingagents.graph.signal_processing import SignalProcessor
|
from tradingagents.agents.utils.rating import parse_rating
|
||||||
return SignalProcessor.process_signal(None, text)
|
return parse_rating(text)
|
||||||
|
|
||||||
def get_graph_args(self, callbacks=None):
|
def get_graph_args(self, callbacks=None):
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
"""Tests for the shared rating heuristic and the SignalProcessor adapter.
|
"""The rating heuristic that reads the decision's 5-tier rating.
|
||||||
|
|
||||||
The Portfolio Manager produces a typed PortfolioDecision via structured
|
The Portfolio Manager's rendered decision always carries a ``**Rating**: X``
|
||||||
output and renders it to markdown that always contains a ``**Rating**: X``
|
header, so the rating is read deterministically; no second model call is made.
|
||||||
header. The deterministic heuristic in ``tradingagents.agents.utils.rating``
|
|
||||||
is therefore sufficient to extract the rating downstream — no second LLM
|
|
||||||
call is needed — and SignalProcessor is now a thin adapter that delegates
|
|
||||||
to it.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -14,10 +10,8 @@ from tradingagents.agents.utils.rating import (
|
|||||||
RATING_REVIEW,
|
RATING_REVIEW,
|
||||||
RATINGS_5_TIER,
|
RATINGS_5_TIER,
|
||||||
extract_rating,
|
extract_rating,
|
||||||
is_review,
|
|
||||||
parse_rating,
|
parse_rating,
|
||||||
)
|
)
|
||||||
from tradingagents.graph.signal_processing import SignalProcessor
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Heuristic parser
|
# Heuristic parser
|
||||||
@@ -67,44 +61,9 @@ class TestParseRating:
|
|||||||
for r in RATINGS_5_TIER:
|
for r in RATINGS_5_TIER:
|
||||||
assert parse_rating(f"Rating: {r}") == r
|
assert parse_rating(f"Rating: {r}") == r
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# SignalProcessor: thin adapter over the heuristic
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
|
||||||
class TestSignalProcessor:
|
|
||||||
def test_returns_rating_from_pm_markdown(self):
|
|
||||||
sp = SignalProcessor()
|
|
||||||
md = "**Rating**: Overweight\n\n**Executive Summary**: Build gradually."
|
|
||||||
assert sp.process_signal(md) == "Overweight"
|
|
||||||
|
|
||||||
def test_makes_no_llm_calls(self):
|
|
||||||
"""SignalProcessor must not invoke the LLM it was constructed with —
|
|
||||||
the rating is parseable from the rendered PM markdown directly."""
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
llm = MagicMock()
|
|
||||||
sp = SignalProcessor(llm)
|
|
||||||
sp.process_signal("Rating: Buy\nDetails.")
|
|
||||||
llm.invoke.assert_not_called()
|
|
||||||
llm.with_structured_output.assert_not_called()
|
|
||||||
|
|
||||||
def test_unparseable_signal_is_review_not_silent_hold(self):
|
|
||||||
# #1170: an unrecognizable decision must surface REVIEW, not a fabricated
|
|
||||||
# tradeable Hold.
|
|
||||||
sp = SignalProcessor()
|
|
||||||
signal = sp.process_signal("Plain prose without a recommendation.")
|
|
||||||
assert signal == RATING_REVIEW
|
|
||||||
assert is_review(signal)
|
|
||||||
assert signal not in RATINGS_5_TIER
|
|
||||||
|
|
||||||
def test_fullwidth_colon_is_parsed_not_reviewed(self):
|
def test_fullwidth_colon_is_parsed_not_reviewed(self):
|
||||||
# #1170: `Rating:Overweight` (fullwidth colon) used to defeat the regex
|
# `Rating:Overweight` (fullwidth colon) is read, not sent to review (#1170).
|
||||||
# and silently become Hold; NFKC normalization now parses it.
|
assert parse_rating("Rating:Overweight\n理由はこちら。") == "Overweight"
|
||||||
sp = SignalProcessor()
|
|
||||||
assert sp.process_signal("Rating:Overweight\n理由はこちら。") == "Overweight"
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
@@ -131,7 +90,6 @@ class TestGraphSignalContract:
|
|||||||
def _bare_graph(self):
|
def _bare_graph(self):
|
||||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||||
g = object.__new__(TradingAgentsGraph)
|
g = object.__new__(TradingAgentsGraph)
|
||||||
g.signal_processor = SignalProcessor()
|
|
||||||
return g
|
return g
|
||||||
|
|
||||||
def test_graph_surfaces_review(self):
|
def test_graph_surfaces_review(self):
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from .conditional_logic import ConditionalLogic
|
|||||||
from .propagation import Propagator
|
from .propagation import Propagator
|
||||||
from .reflection import Reflector
|
from .reflection import Reflector
|
||||||
from .setup import GraphSetup
|
from .setup import GraphSetup
|
||||||
from .signal_processing import SignalProcessor
|
|
||||||
from .trading_graph import TradingAgentsGraph
|
from .trading_graph import TradingAgentsGraph
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -13,5 +12,4 @@ __all__ = [
|
|||||||
"GraphSetup",
|
"GraphSetup",
|
||||||
"Propagator",
|
"Propagator",
|
||||||
"Reflector",
|
"Reflector",
|
||||||
"SignalProcessor",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
"""Extract the 5-tier portfolio rating from the Portfolio Manager's decision.
|
|
||||||
|
|
||||||
The Portfolio Manager produces a typed ``PortfolioDecision`` via structured
|
|
||||||
output and renders it to markdown that always carries a ``**Rating**: X``
|
|
||||||
header (see :func:`tradingagents.agents.schemas.render_pm_decision`). The
|
|
||||||
deterministic heuristic in :mod:`tradingagents.agents.utils.rating` is more
|
|
||||||
than sufficient to extract that rating; no extra LLM call is needed.
|
|
||||||
|
|
||||||
This module exists for backwards compatibility with callers that expect a
|
|
||||||
``SignalProcessor.process_signal(text)`` interface.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from tradingagents.agents.utils.rating import RATING_REVIEW, extract_rating
|
|
||||||
|
|
||||||
|
|
||||||
class SignalProcessor:
|
|
||||||
"""Read the 5-tier rating out of a Portfolio Manager decision."""
|
|
||||||
|
|
||||||
def __init__(self, quick_thinking_llm: Any = None):
|
|
||||||
# The LLM argument is accepted for backwards compatibility but ignored:
|
|
||||||
# the PM's structured output guarantees the rating is parseable from the
|
|
||||||
# rendered markdown without a second LLM call, so it is not stored.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def process_signal(self, full_signal: str) -> str:
|
|
||||||
"""Return one of Buy / Overweight / Hold / Underweight / Sell, or REVIEW.
|
|
||||||
|
|
||||||
An unrecognizable decision yields ``REVIEW`` rather than a fabricated
|
|
||||||
``Hold``, so a parsing failure is visible instead of masquerading as a
|
|
||||||
tradeable neutral signal (#1170). Consumers that map the result onto the
|
|
||||||
5-tier enum should guard with :func:`~tradingagents.agents.utils.rating.is_review`.
|
|
||||||
"""
|
|
||||||
rating = extract_rating(full_signal)
|
|
||||||
return rating if rating is not None else RATING_REVIEW
|
|
||||||
@@ -28,6 +28,7 @@ from tradingagents.agents.utils.agent_utils import (
|
|||||||
resolve_instrument_identity,
|
resolve_instrument_identity,
|
||||||
)
|
)
|
||||||
from tradingagents.agents.utils.memory import TradingMemoryLog
|
from tradingagents.agents.utils.memory import TradingMemoryLog
|
||||||
|
from tradingagents.agents.utils.rating import parse_rating
|
||||||
from tradingagents.dataflows.config import run_config, set_config
|
from tradingagents.dataflows.config import run_config, set_config
|
||||||
from tradingagents.dataflows.utils import get_current_date, safe_ticker_component
|
from tradingagents.dataflows.utils import get_current_date, safe_ticker_component
|
||||||
from tradingagents.dataflows.y_finance import get_closes
|
from tradingagents.dataflows.y_finance import get_closes
|
||||||
@@ -40,7 +41,6 @@ from .conditional_logic import ConditionalLogic
|
|||||||
from .propagation import Propagator
|
from .propagation import Propagator
|
||||||
from .reflection import Reflector
|
from .reflection import Reflector
|
||||||
from .setup import GraphSetup
|
from .setup import GraphSetup
|
||||||
from .signal_processing import SignalProcessor
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -163,7 +163,6 @@ class TradingAgentsGraph:
|
|||||||
max_recur_limit=self.config.get("max_recur_limit", 100),
|
max_recur_limit=self.config.get("max_recur_limit", 100),
|
||||||
)
|
)
|
||||||
self.reflector = Reflector(self.quick_thinking_llm)
|
self.reflector = Reflector(self.quick_thinking_llm)
|
||||||
self.signal_processor = SignalProcessor(self.quick_thinking_llm)
|
|
||||||
|
|
||||||
# State tracking
|
# State tracking
|
||||||
self.curr_state = None
|
self.curr_state = None
|
||||||
@@ -655,5 +654,5 @@ class TradingAgentsGraph:
|
|||||||
json.dump(entry, f, indent=4, ensure_ascii=False)
|
json.dump(entry, f, indent=4, ensure_ascii=False)
|
||||||
|
|
||||||
def process_signal(self, full_signal):
|
def process_signal(self, full_signal):
|
||||||
"""Process a signal to extract the core decision."""
|
"""The decision's 5-tier rating, or REVIEW when it has none."""
|
||||||
return self.signal_processor.process_signal(full_signal)
|
return parse_rating(full_signal)
|
||||||
|
|||||||
Reference in New Issue
Block a user