mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-06-16 21:06:15 +03:00
Clear the deferred full-repo lint backlog so the whole tree passes the strict ruff select (E,W,F,I,B,UP,C4,SIM). Mechanical fixes dominate: import sorting, pep585/604 annotations, dropped dead imports, and whitespace. The few semantic changes are behavior-preserving: declare __all__ on the agent_utils and alpha_vantage re-export hubs; expand 'from x import *' to explicit names; use immutable tuple defaults instead of mutable list defaults; contextlib.suppress for try/except/pass; and narrow an over-broad assertRaises.
90 lines
3.5 KiB
Python
90 lines
3.5 KiB
Python
"""Tests for the shared rating heuristic and the SignalProcessor adapter.
|
|
|
|
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.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from tradingagents.agents.utils.rating import RATINGS_5_TIER, parse_rating
|
|
from tradingagents.graph.signal_processing import SignalProcessor
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Heuristic parser
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestParseRating:
|
|
def test_explicit_label_buy(self):
|
|
assert parse_rating("Rating: Buy\nReasoning here.") == "Buy"
|
|
|
|
def test_explicit_label_overweight(self):
|
|
assert parse_rating("Rating: Overweight\nDetails.") == "Overweight"
|
|
|
|
def test_explicit_label_with_markdown_bold_value(self):
|
|
# Regression: Rating: **Sell** — markdown around the value.
|
|
assert parse_rating("Rating: **Sell**\nExit immediately.") == "Sell"
|
|
|
|
def test_explicit_label_with_markdown_bold_label(self):
|
|
assert parse_rating("**Rating**: Underweight\nTrim exposure.") == "Underweight"
|
|
|
|
def test_rendered_pm_markdown_shape(self):
|
|
# The exact shape produced by render_pm_decision must always parse.
|
|
text = (
|
|
"**Rating**: Buy\n\n"
|
|
"**Executive Summary**: Enter at $189-192, 6% portfolio cap.\n\n"
|
|
"**Investment Thesis**: AI capex cycle intact; institutional flows constructive."
|
|
)
|
|
assert parse_rating(text) == "Buy"
|
|
|
|
def test_explicit_label_wins_over_prose_with_markdown(self):
|
|
text = (
|
|
"The buy thesis is weakened by guidance.\n"
|
|
"Rating: **Sell**\n"
|
|
"Exit before earnings."
|
|
)
|
|
assert parse_rating(text) == "Sell"
|
|
|
|
def test_no_rating_returns_default(self):
|
|
assert parse_rating("No clear directional signal at this time.") == "Hold"
|
|
|
|
def test_no_rating_custom_default(self):
|
|
assert parse_rating("Plain prose.", default="Underweight") == "Underweight"
|
|
|
|
def test_all_five_tiers_recognised(self):
|
|
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_default_when_no_rating_present(self):
|
|
sp = SignalProcessor()
|
|
assert sp.process_signal("Plain prose without a recommendation.") == "Hold"
|