mirror of
https://github.com/farcasclaudiu/TradingAgents.git
synced 2026-06-29 11:01:40 +03:00
feat: structured-output Portfolio Manager + 5-tier rating consistency (#434)
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.
This commit is contained in:
@@ -1,29 +1,53 @@
|
||||
from tradingagents.agents.utils.agent_utils import build_instrument_context, get_language_instruction
|
||||
"""Portfolio Manager: synthesises the risk-analyst debate into the final decision.
|
||||
|
||||
Uses LangChain's ``with_structured_output`` so the LLM produces a typed
|
||||
``PortfolioDecision`` directly, in a single call. The result is rendered
|
||||
back to markdown for storage in ``final_trade_decision`` so memory log,
|
||||
CLI display, and saved reports continue to consume the same shape they do
|
||||
today. When a provider does not expose structured output, the agent falls
|
||||
back to a free-text invocation and the existing heuristic rating parser.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from tradingagents.agents.schemas import PortfolioDecision, render_pm_decision
|
||||
from tradingagents.agents.utils.agent_utils import (
|
||||
build_instrument_context,
|
||||
get_language_instruction,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def create_portfolio_manager(llm):
|
||||
def portfolio_manager_node(state) -> dict:
|
||||
# Wrap once at agent construction; if the provider does not support
|
||||
# structured output we keep ``structured_llm`` as None and use the
|
||||
# free-text fallback for every call.
|
||||
try:
|
||||
structured_llm = llm.with_structured_output(PortfolioDecision)
|
||||
except (NotImplementedError, AttributeError) as exc:
|
||||
logger.warning(
|
||||
"Portfolio Manager: provider does not support with_structured_output (%s); "
|
||||
"falling back to free-text generation",
|
||||
exc,
|
||||
)
|
||||
structured_llm = None
|
||||
|
||||
def portfolio_manager_node(state) -> dict:
|
||||
instrument_context = build_instrument_context(state["company_of_interest"])
|
||||
|
||||
history = state["risk_debate_state"]["history"]
|
||||
risk_debate_state = state["risk_debate_state"]
|
||||
market_research_report = state["market_report"]
|
||||
news_report = state["news_report"]
|
||||
fundamentals_report = state["fundamentals_report"]
|
||||
sentiment_report = state["sentiment_report"]
|
||||
research_plan = state["investment_plan"]
|
||||
trader_plan = state["trader_investment_plan"]
|
||||
|
||||
past_context = state.get("past_context", "")
|
||||
lessons_line = (
|
||||
f"- Lessons from prior decisions and outcomes:\n{past_context}\n"
|
||||
if past_context else ""
|
||||
)
|
||||
thesis_instruction = (
|
||||
"3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate and the lessons from prior decisions."
|
||||
if past_context
|
||||
else "3. **Investment Thesis**: Detailed reasoning anchored in the analysts' debate."
|
||||
else ""
|
||||
)
|
||||
|
||||
prompt = f"""As the Portfolio Manager, synthesize the risk analysts' debate and deliver the final trading decision.
|
||||
@@ -43,14 +67,6 @@ def create_portfolio_manager(llm):
|
||||
- Research Manager's investment plan: **{research_plan}**
|
||||
- Trader's transaction proposal: **{trader_plan}**
|
||||
{lessons_line}
|
||||
|
||||
**Required Output Structure:**
|
||||
1. **Rating**: State one of Buy / Overweight / Hold / Underweight / Sell.
|
||||
2. **Executive Summary**: A concise action plan covering entry strategy, position sizing, key risk levels, and time horizon.
|
||||
{thesis_instruction}
|
||||
|
||||
---
|
||||
|
||||
**Risk Analysts Debate History:**
|
||||
{history}
|
||||
|
||||
@@ -58,10 +74,10 @@ def create_portfolio_manager(llm):
|
||||
|
||||
Be decisive and ground every conclusion in specific evidence from the analysts.{get_language_instruction()}"""
|
||||
|
||||
response = llm.invoke(prompt)
|
||||
final_trade_decision = _invoke_pm(structured_llm, llm, prompt)
|
||||
|
||||
new_risk_debate_state = {
|
||||
"judge_decision": response.content,
|
||||
"judge_decision": final_trade_decision,
|
||||
"history": risk_debate_state["history"],
|
||||
"aggressive_history": risk_debate_state["aggressive_history"],
|
||||
"conservative_history": risk_debate_state["conservative_history"],
|
||||
@@ -75,7 +91,30 @@ Be decisive and ground every conclusion in specific evidence from the analysts.{
|
||||
|
||||
return {
|
||||
"risk_debate_state": new_risk_debate_state,
|
||||
"final_trade_decision": response.content,
|
||||
"final_trade_decision": final_trade_decision,
|
||||
}
|
||||
|
||||
return portfolio_manager_node
|
||||
|
||||
|
||||
def _invoke_pm(structured_llm, plain_llm, prompt: str) -> str:
|
||||
"""Run the PM call and return the markdown-rendered decision.
|
||||
|
||||
Tries the structured-output path first; if it fails for any reason
|
||||
(provider does not support it, model returns malformed JSON, network
|
||||
glitch on the structured endpoint), falls back to the plain free-text
|
||||
invocation so the pipeline still produces a result.
|
||||
"""
|
||||
if structured_llm is not None:
|
||||
try:
|
||||
decision = structured_llm.invoke(prompt)
|
||||
return render_pm_decision(decision)
|
||||
except Exception as exc:
|
||||
logger.warning(
|
||||
"Portfolio Manager: structured-output invocation failed (%s); "
|
||||
"retrying once as free text",
|
||||
exc,
|
||||
)
|
||||
|
||||
response = plain_llm.invoke(prompt)
|
||||
return response.content
|
||||
|
||||
Reference in New Issue
Block a user