mirror of
https://github.com/farcasclaudiu/TradingAgents.git
synced 2026-06-22 07:01:21 +03:00
0fda24515f
Three related changes that take the rating pipeline from heuristic-only to type-safe at the source. 1) Research Manager prompt now uses the same 5-tier scale (Buy / Overweight / Hold / Underweight / Sell) as the Portfolio Manager, signal_processing, and the memory log. The prior 3-tier wording (Buy / Sell / Hold) was the only remaining inconsistency in the pipeline. 2) Centralise the 5-tier vocabulary and the heuristic prose-rating parser into tradingagents/agents/utils/rating.py. Both the memory log and the signal processor now share the same parser instead of duplicating regex and word-walker logic. 3) Make structured output a first-class part of the Portfolio Manager's primary call. The PM uses llm.with_structured_output(PortfolioDecision) so each provider's native structured-output mode (json_schema for OpenAI/xAI, response_schema for Gemini, tool-use for Anthropic, function_calling for OpenAI-compatible providers) yields a typed Pydantic instance directly. A render helper turns that instance back into the same markdown shape downstream consumers (memory log, CLI display, saved reports) already expect, so no other code has to know the PM now produces structured output. Providers without structured support fall back gracefully to free-text + the deterministic heuristic. The previous SignalProcessor had been making a second LLM call to re-extract the rating from the PM's prose; that round-trip is now eliminated. SignalProcessor is a thin adapter over parse_rating(), makes zero LLM calls, and stays for backwards compatibility with process_signal() callers. Schema (PortfolioDecision) captures rating + executive_summary + investment_thesis + optional price_target + time_horizon, with field descriptions doubling as output instructions. Agent prose remains the primary artifact; structured output is layered onto the PM only because it is the one agent whose output has machine-readable downstream consumers. 15 new tests cover the heuristic parser (markdown-bold edge cases that had no coverage before), the structured PM happy path, the free-text fallback path, and that SignalProcessor never invokes the LLM. Full suite: 77 tests pass in ~2s without API keys.
51 lines
1.7 KiB
Python
51 lines
1.7 KiB
Python
"""Shared 5-tier rating vocabulary and a deterministic heuristic parser.
|
|
|
|
The same five-tier scale (Buy, Overweight, Hold, Underweight, Sell) is used by:
|
|
- The Research Manager (investment plan recommendation)
|
|
- The Portfolio Manager (final position decision)
|
|
- The signal processor (rating extracted for downstream consumers)
|
|
- The memory log (rating tag stored alongside each decision entry)
|
|
|
|
Centralising it here avoids drift between those call sites.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Tuple
|
|
|
|
|
|
# Canonical, ordered 5-tier scale (most bullish to most bearish).
|
|
RATINGS_5_TIER: Tuple[str, ...] = (
|
|
"Buy", "Overweight", "Hold", "Underweight", "Sell",
|
|
)
|
|
|
|
_RATING_SET = {r.lower() for r in RATINGS_5_TIER}
|
|
|
|
# Matches "Rating: X" / "rating - X" / "Rating: **X**" — tolerates markdown
|
|
# bold wrappers and either a colon or hyphen separator.
|
|
_RATING_LABEL_RE = re.compile(r"rating.*?[:\-][\s*]*(\w+)", re.IGNORECASE)
|
|
|
|
|
|
def parse_rating(text: str, default: str = "Hold") -> str:
|
|
"""Heuristically extract a 5-tier rating from prose text.
|
|
|
|
Two-pass strategy:
|
|
1. Look for an explicit "Rating: X" label (tolerant of markdown bold).
|
|
2. Fall back to the first 5-tier rating word found anywhere in the text.
|
|
|
|
Returns a Title-cased rating string, or ``default`` if no rating word appears.
|
|
"""
|
|
for line in text.splitlines():
|
|
m = _RATING_LABEL_RE.search(line)
|
|
if m and m.group(1).lower() in _RATING_SET:
|
|
return m.group(1).capitalize()
|
|
|
|
for line in text.splitlines():
|
|
for word in line.lower().split():
|
|
clean = word.strip("*:.,")
|
|
if clean in _RATING_SET:
|
|
return clean.capitalize()
|
|
|
|
return default
|