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:
Yijia-Xiao
2026-09-24 04:31:05 +00:00
parent a6e92a6b5e
commit 4e1faf6465
7 changed files with 15 additions and 98 deletions
+2 -2
View File
@@ -76,8 +76,8 @@ class _FakeGraph:
return {"messages": [], "company_of_interest": ticker}
def process_signal(self, text):
from tradingagents.graph.signal_processing import SignalProcessor
return SignalProcessor.process_signal(None, text)
from tradingagents.agents.utils.rating import parse_rating
return parse_rating(text)
def record_decision(self, ticker, trade_date, final_state):
self.calls.append(("record_decision", ticker, trade_date, final_state.get("final_trade_decision")))
+1 -1
View File
@@ -937,7 +937,7 @@ class TestLegacyRemoval:
mock_graph.graph.invoke.return_value = fake_state
mock_graph.propagator.create_initial_state.return_value = fake_state
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
# the actual write path instead of the auto-MagicMock.
mock_graph._run_graph = functools.partial(
+4 -4
View File
@@ -75,13 +75,13 @@ def test_the_memory_log_records_review_rather_than_a_tradeable_hold(tmp_path):
@pytest.mark.unit
def test_the_signal_and_the_log_agree_on_the_same_decision(tmp_path):
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")})
for text in (INVERTED, REFUSAL, "**Rating**: Buy\n\nAccumulate."):
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.")]
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
def process_signal(self, text):
from tradingagents.graph.signal_processing import SignalProcessor
return SignalProcessor.process_signal(None, text)
from tradingagents.agents.utils.rating import parse_rating
return parse_rating(text)
def get_graph_args(self, callbacks=None):
return {}
+5 -47
View File
@@ -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
output and renders it to markdown that always contains a ``**Rating**: X``
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.
The Portfolio Manager's rendered decision always carries a ``**Rating**: X``
header, so the rating is read deterministically; no second model call is made.
"""
import pytest
@@ -14,10 +10,8 @@ from tradingagents.agents.utils.rating import (
RATING_REVIEW,
RATINGS_5_TIER,
extract_rating,
is_review,
parse_rating,
)
from tradingagents.graph.signal_processing import SignalProcessor
# ---------------------------------------------------------------------------
# Heuristic parser
@@ -67,44 +61,9 @@ class TestParseRating:
for r in RATINGS_5_TIER:
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):
# #1170: `Rating:Overweight` (fullwidth colon) used to defeat the regex
# and silently become Hold; NFKC normalization now parses it.
sp = SignalProcessor()
assert sp.process_signal("Rating:Overweight\n理由はこちら。") == "Overweight"
# `Rating:Overweight` (fullwidth colon) is read, not sent to review (#1170).
assert parse_rating("Rating:Overweight\n理由はこちら。") == "Overweight"
@pytest.mark.unit
@@ -131,7 +90,6 @@ class TestGraphSignalContract:
def _bare_graph(self):
from tradingagents.graph.trading_graph import TradingAgentsGraph
g = object.__new__(TradingAgentsGraph)
g.signal_processor = SignalProcessor()
return g
def test_graph_surfaces_review(self):