mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-11-08 02:17:39 +02:00
fix(rating): read a decision's rating only from its label
- a decision without a rating label is REVIEW; a lone rating word in its prose may be one it argues against ("not a Sell"), so it is no longer read as the call
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user