mirror of
https://github.com/farcasclaudiu/TradingAgents.git
synced 2026-06-29 07:01:17 +03:00
feat: structured-output Trader and Research Manager (#434, finishes the trio)
Extends the canonical structured-output pattern from the Portfolio Manager to the other two decision-making agents. Each of the three agents now returns a typed Pydantic instance via llm.with_structured_output() in a single primary call, and a render helper turns the result into the same markdown shape downstream agents and saved reports already consume. - ResearchPlan: 5-tier recommendation, conversational rationale, concrete strategic actions for the trader. - TraderProposal: 3-tier action (transaction direction is naturally Buy / Hold / Sell — position sizing happens later at the Portfolio Manager), reasoning, and optional entry_price / stop_loss / position_sizing. Rendered output preserves the trailing "FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**" line for backward compatibility with the analyst stop-signal text. - PortfolioDecision: 5-tier rating, executive summary, investment thesis, optional price_target / time_horizon (unchanged). The shared try-structured-then-fallback pattern is extracted into tradingagents/agents/utils/structured.py (bind_structured + invoke_structured_or_freetext) so all three agents go through the same code path and log the same warning when a provider lacks structured output and the agent falls back to free-text generation. Net effect for users: every saved markdown report (research/manager.md, trading/trader.md, portfolio/decision.md) now has consistent section headers across runs and providers, easier to scan. Net effect for the runtime: the rating extraction round-trip is gone — the rating comes from the structured response itself, not a second LLM call. SignalProcessor was already simplified to a heuristic adapter in the previous commit. 11 new tests in tests/test_structured_agents.py cover the Trader and Research Manager render functions, structured-output happy paths, and free-text fallback. Full suite: 88 tests pass in ~2s without API keys.
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
"""Pydantic schemas used by agents that produce structured output.
|
||||
|
||||
The framework's primary artifact is still prose: each agent's natural-language
|
||||
reasoning is what users read, what gets stored in the memory log, and what
|
||||
gets saved as markdown reports. Structured output is layered onto agents
|
||||
whose results have downstream machine-readable consumers (currently only
|
||||
the Portfolio Manager) so that:
|
||||
reasoning is what users read in the saved markdown reports and what the
|
||||
downstream agents read as context. Structured output is layered onto the
|
||||
three decision-making agents (Research Manager, Trader, Portfolio Manager)
|
||||
so that:
|
||||
|
||||
- The rating is type-safe and never has to be regex-extracted
|
||||
- Schema field descriptions become the model's output instructions
|
||||
- Their outputs follow consistent section headers across runs and providers
|
||||
- Each provider's native structured-output mode is used (json_schema for
|
||||
OpenAI/xAI, response_schema for Gemini, tool-use for Anthropic)
|
||||
- Schema field descriptions become the model's output instructions, freeing
|
||||
the prompt body to focus on context and the rating-scale guidance
|
||||
- A render helper turns the parsed Pydantic instance back into the same
|
||||
markdown shape the rest of the system already consumes, so display,
|
||||
memory log, and saved reports keep working unchanged
|
||||
@@ -23,8 +24,13 @@ from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared rating types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class PortfolioRating(str, Enum):
|
||||
"""5-tier portfolio rating used by the Research Manager and Portfolio Manager."""
|
||||
"""5-tier rating used by the Research Manager and Portfolio Manager."""
|
||||
|
||||
BUY = "Buy"
|
||||
OVERWEIGHT = "Overweight"
|
||||
@@ -33,6 +39,135 @@ class PortfolioRating(str, Enum):
|
||||
SELL = "Sell"
|
||||
|
||||
|
||||
class TraderAction(str, Enum):
|
||||
"""3-tier transaction direction used by the Trader.
|
||||
|
||||
The Trader's job is to translate the Research Manager's investment plan
|
||||
into a concrete transaction proposal: should the desk execute a Buy, a
|
||||
Sell, or sit on Hold this round. Position sizing and the nuanced
|
||||
Overweight / Underweight calls happen later at the Portfolio Manager.
|
||||
"""
|
||||
|
||||
BUY = "Buy"
|
||||
HOLD = "Hold"
|
||||
SELL = "Sell"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Research Manager
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class ResearchPlan(BaseModel):
|
||||
"""Structured investment plan produced by the Research Manager.
|
||||
|
||||
Hand-off to the Trader: the recommendation pins the directional view,
|
||||
the rationale captures which side of the bull/bear debate carried the
|
||||
argument, and the strategic actions translate that into concrete
|
||||
instructions the trader can execute against.
|
||||
"""
|
||||
|
||||
recommendation: PortfolioRating = Field(
|
||||
description=(
|
||||
"The investment recommendation. Exactly one of Buy / Overweight / "
|
||||
"Hold / Underweight / Sell. Reserve Hold for situations where the "
|
||||
"evidence on both sides is genuinely balanced; otherwise commit to "
|
||||
"the side with the stronger arguments."
|
||||
),
|
||||
)
|
||||
rationale: str = Field(
|
||||
description=(
|
||||
"Conversational summary of the key points from both sides of the "
|
||||
"debate, ending with which arguments led to the recommendation. "
|
||||
"Speak naturally, as if to a teammate."
|
||||
),
|
||||
)
|
||||
strategic_actions: str = Field(
|
||||
description=(
|
||||
"Concrete steps for the trader to implement the recommendation, "
|
||||
"including position sizing guidance consistent with the rating."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def render_research_plan(plan: ResearchPlan) -> str:
|
||||
"""Render a ResearchPlan to markdown for storage and the trader's prompt context."""
|
||||
return "\n".join([
|
||||
f"**Recommendation**: {plan.recommendation.value}",
|
||||
"",
|
||||
f"**Rationale**: {plan.rationale}",
|
||||
"",
|
||||
f"**Strategic Actions**: {plan.strategic_actions}",
|
||||
])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trader
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TraderProposal(BaseModel):
|
||||
"""Structured transaction proposal produced by the Trader.
|
||||
|
||||
The trader reads the Research Manager's investment plan and the analyst
|
||||
reports, then turns them into a concrete transaction: what action to
|
||||
take, the reasoning that justifies it, and the practical levels for
|
||||
entry, stop-loss, and sizing.
|
||||
"""
|
||||
|
||||
action: TraderAction = Field(
|
||||
description="The transaction direction. Exactly one of Buy / Hold / Sell.",
|
||||
)
|
||||
reasoning: str = Field(
|
||||
description=(
|
||||
"The case for this action, anchored in the analysts' reports and "
|
||||
"the research plan. Two to four sentences."
|
||||
),
|
||||
)
|
||||
entry_price: Optional[float] = Field(
|
||||
default=None,
|
||||
description="Optional entry price target in the instrument's quote currency.",
|
||||
)
|
||||
stop_loss: Optional[float] = Field(
|
||||
default=None,
|
||||
description="Optional stop-loss price in the instrument's quote currency.",
|
||||
)
|
||||
position_sizing: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Optional sizing guidance, e.g. '5% of portfolio'.",
|
||||
)
|
||||
|
||||
|
||||
def render_trader_proposal(proposal: TraderProposal) -> str:
|
||||
"""Render a TraderProposal to markdown.
|
||||
|
||||
The trailing ``FINAL TRANSACTION PROPOSAL: **BUY/HOLD/SELL**`` line is
|
||||
preserved for backward compatibility with the analyst stop-signal text
|
||||
and any external code that greps for it.
|
||||
"""
|
||||
parts = [
|
||||
f"**Action**: {proposal.action.value}",
|
||||
"",
|
||||
f"**Reasoning**: {proposal.reasoning}",
|
||||
]
|
||||
if proposal.entry_price is not None:
|
||||
parts.extend(["", f"**Entry Price**: {proposal.entry_price}"])
|
||||
if proposal.stop_loss is not None:
|
||||
parts.extend(["", f"**Stop Loss**: {proposal.stop_loss}"])
|
||||
if proposal.position_sizing:
|
||||
parts.extend(["", f"**Position Sizing**: {proposal.position_sizing}"])
|
||||
parts.extend([
|
||||
"",
|
||||
f"FINAL TRANSACTION PROPOSAL: **{proposal.action.value.upper()}**",
|
||||
])
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Portfolio Manager
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class PortfolioDecision(BaseModel):
|
||||
"""Structured output produced by the Portfolio Manager.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user