diff --git a/tests/test_rating_integrity.py b/tests/test_rating_integrity.py index 61def3de2..9fad9d5fe 100644 --- a/tests/test_rating_integrity.py +++ b/tests/test_rating_integrity.py @@ -41,8 +41,15 @@ def test_prose_naming_several_ratings_without_a_label_needs_review(): @pytest.mark.unit -def test_prose_naming_one_rating_is_taken_as_the_call(): - assert extract_rating("On balance we stay Underweight until margins recover.") == "Underweight" +@pytest.mark.parametrize("prose", [ + "We would not Sell here; the dip is a chance to add.", + "**评级**:买入\n\n不建议卖出 (Sell)。", # a label in another language, and a negated Sell + "On balance we stay Underweight until margins recover.", +]) +def test_a_rating_word_without_a_label_is_not_read_as_the_call(prose): + """A rating word in prose may be one the text argues against (#1435).""" + assert extract_rating(prose) is None + assert parse_rating(prose) == RATING_REVIEW @pytest.mark.unit diff --git a/tradingagents/agents/rating.py b/tradingagents/agents/rating.py index 16d7689dd..c82d5cb28 100644 --- a/tradingagents/agents/rating.py +++ b/tradingagents/agents/rating.py @@ -49,20 +49,13 @@ _RATING_LINE_RE = re.compile( # A line presenting the scale rather than a decision ("Rating Scale: Buy, ..."). _RATING_SCALE_RE = re.compile(r"rating\s*(scale|options|legend)", re.IGNORECASE) -# Standalone 5-tier word anywhere (word boundaries so "Buyer"/"Holding" don't match). -_RATING_WORD_RE = re.compile( - r"\b(" + "|".join(RATINGS_5_TIER) + r")\b", re.IGNORECASE -) - - def extract_rating(text: str) -> str | None: - """Extract a 5-tier rating from prose, or ``None`` if none is present. + """Extract a 5-tier rating from its label, or ``None`` if there is none. - Two-pass strategy on the NFKC-normalized text (so fullwidth punctuation like - ``Rating:Overweight`` is matched the same as ASCII): - 1. An explicit "Rating: X" label (tolerant of markdown bold): the first one - opening its own line, else the last one anywhere. - 2. A single 5-tier rating word, when the text names only one. + Reads an explicit "Rating: X" label (tolerant of markdown bold) in the + NFKC-normalized text, so fullwidth punctuation like ``Rating:Overweight`` + matches as ASCII does: the first one opening its own line, else the last + one anywhere. """ if not text: return None @@ -83,14 +76,10 @@ def extract_rating(text: str) -> str | None: m = _RATING_LABEL_RE.search(line) if m and m.group(1).lower() in _RATING_SET: anywhere = m.group(1).capitalize() - if on_own_line or anywhere: - return on_own_line or anywhere - - # No label. A single rating word in the text is the call; several are an - # argument, and picking one of them reports a direction nobody decided -- - # prose that rejects a Buy before concluding Underweight read as Buy. - named = {m.group(1).capitalize() for m in _RATING_WORD_RE.finditer(norm)} - return named.pop() if len(named) == 1 else None + # Without a label there is no call to read: a rating word in the prose may be + # one the text argues against ("not a Sell"), and reading it reports a + # direction nobody decided. + return on_own_line or anywhere def parse_rating(text: str, default: str = RATING_REVIEW) -> str: