mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-19 19:25:24 +03:00
fix(agents): stop conflict alone from defaulting the verdict to Hold
- both manager prompts and both rating fields listed "materially conflicting" as a Hold condition, and every bull/bear and risk debate is conflicting, so a modest directional edge collapsed to Hold - state one rule at all four sites: conflict alone is not a reason to Hold, commit to the stronger side sized by how decisively it wins, and Hold only when the evidence is still balanced after weighing or too thin for a call - a modest bull edge now resolves to Overweight; a genuinely balanced debate still resolves to Hold #1321
This commit is contained in:
@@ -7,12 +7,14 @@ behavior we added for the Trader, Research Manager, and Sentiment Analyst
|
|||||||
so they share the same deterministic output shape.
|
so they share the same deterministic output shape.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import inspect
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from tradingagents.agents.analysts.sentiment_analyst import create_sentiment_analyst
|
from tradingagents.agents.analysts.sentiment_analyst import create_sentiment_analyst
|
||||||
|
from tradingagents.agents.managers.portfolio_manager import create_portfolio_manager
|
||||||
from tradingagents.agents.managers.research_manager import create_research_manager
|
from tradingagents.agents.managers.research_manager import create_research_manager
|
||||||
from tradingagents.agents.schemas import (
|
from tradingagents.agents.schemas import (
|
||||||
PortfolioDecision,
|
PortfolioDecision,
|
||||||
@@ -484,3 +486,20 @@ class TestSentimentAnalystAgent:
|
|||||||
llm.with_structured_output.return_value = structured
|
llm.with_structured_output.return_value = structured
|
||||||
llm.invoke.return_value = MagicMock(content=plain)
|
llm.invoke.return_value = MagicMock(content=plain)
|
||||||
assert create_sentiment_analyst(llm)(_make_sentiment_state())["sentiment_report"] == plain
|
assert create_sentiment_analyst(llm)(_make_sentiment_state())["sentiment_report"] == plain
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
@pytest.mark.parametrize("source", [
|
||||||
|
pytest.param(lambda: ResearchPlan.model_fields["recommendation"].description, id="ResearchPlan.recommendation"),
|
||||||
|
pytest.param(lambda: PortfolioDecision.model_fields["rating"].description, id="PortfolioDecision.rating"),
|
||||||
|
pytest.param(lambda: inspect.getsource(create_research_manager), id="research_manager prompt"),
|
||||||
|
pytest.param(lambda: inspect.getsource(create_portfolio_manager), id="portfolio_manager prompt"),
|
||||||
|
])
|
||||||
|
def test_conflict_alone_is_not_a_hold_trigger(source):
|
||||||
|
# The debate always contains conflicting arguments, so a Hold condition that
|
||||||
|
# conflict satisfies fires on every run and swallows directional calls
|
||||||
|
# (#1321). All four decision sites must state the same rule.
|
||||||
|
text = " ".join(source().split())
|
||||||
|
assert "conflict alone is not a reason to Hold" in text or \
|
||||||
|
"Conflicting arguments alone are not a reason to Hold" in text
|
||||||
|
assert "materially conflicting" not in text
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ def create_portfolio_manager(llm):
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Ground every conclusion in specific evidence from the analysts. Commit to a directional call only when the evidence clearly supports one; choose Hold when the case is balanced, materially conflicting, ambiguous, or insufficient to justify changing exposure, rather than forcing a direction to appear decisive. Weigh the analysts on their merits, independent of speaking order.
|
Ground every conclusion in specific evidence from the analysts. The risk debate always contains conflicting stances; deciding which is stronger is the job, so conflict alone is not a reason to Hold. Commit to the stronger case, sized by how decisively it wins. Choose Hold only when the evidence is still balanced after that weighing, or too thin to support a call; do not force a direction to appear decisive. Weigh the analysts on their merits, independent of speaking order.
|
||||||
|
|
||||||
{NO_EXTERNAL_TOOLS}{get_language_instruction()}"""
|
{NO_EXTERNAL_TOOLS}{get_language_instruction()}"""
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def create_research_manager(llm):
|
|||||||
- **Underweight**: Cautious view; recommend trimming exposure
|
- **Underweight**: Cautious view; recommend trimming exposure
|
||||||
- **Sell**: Strong conviction in the bear thesis; recommend exiting or avoiding the position
|
- **Sell**: Strong conviction in the bear thesis; recommend exiting or avoiding the position
|
||||||
|
|
||||||
Commit to a directional stance only when the debate's strongest arguments clearly warrant one. Choose Hold when the evidence is balanced, materially conflicting, ambiguous, or insufficient to justify changing exposure; do not manufacture a direction merely to appear decisive. Weigh the bull and bear cases on their merits, independent of which side spoke first or last.
|
The debate always contains conflicting arguments; deciding which side is stronger is the job, so conflict alone is not a reason to Hold. Commit to the side with the stronger case, sized by how decisively it wins. Choose Hold only when the evidence is still balanced after that weighing, or too thin to support a call; do not manufacture a direction to appear decisive. Weigh the bull and bear cases on their merits, independent of which side spoke first or last.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -96,11 +96,10 @@ class ResearchPlan(BaseModel):
|
|||||||
recommendation: PortfolioRating = Field(
|
recommendation: PortfolioRating = Field(
|
||||||
description=(
|
description=(
|
||||||
"The investment recommendation. Exactly one of Buy / Overweight / "
|
"The investment recommendation. Exactly one of Buy / Overweight / "
|
||||||
"Hold / Underweight / Sell. Choose Hold when the evidence is "
|
"Hold / Underweight / Sell. Conflicting arguments alone are not a "
|
||||||
"balanced, materially conflicting, ambiguous, or insufficient to "
|
"reason to Hold: commit to the stronger side, sized by how "
|
||||||
"justify changing exposure; otherwise commit to the side with the "
|
"decisively it wins. Choose Hold only when the evidence is still "
|
||||||
"clearly stronger arguments. Do not pick a direction merely to be "
|
"balanced after weighing, or too thin to support a call."
|
||||||
"decisive."
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rationale: str = Field(
|
rationale: str = Field(
|
||||||
@@ -221,10 +220,11 @@ class PortfolioDecision(BaseModel):
|
|||||||
rating: PortfolioRating = Field(
|
rating: PortfolioRating = Field(
|
||||||
description=(
|
description=(
|
||||||
"The final position rating. Exactly one of Buy / Overweight / Hold / "
|
"The final position rating. Exactly one of Buy / Overweight / Hold / "
|
||||||
"Underweight / Sell, picked based on the analysts' debate. Choose "
|
"Underweight / Sell, picked based on the analysts' debate. "
|
||||||
"Hold when the case is balanced, materially conflicting, ambiguous, "
|
"Conflicting arguments alone are not a reason to Hold: commit to the "
|
||||||
"or insufficient to justify changing exposure, rather than forcing a "
|
"stronger side, sized by how decisively it wins. Choose Hold only "
|
||||||
"direction to appear decisive."
|
"when the evidence is still balanced after weighing, or too thin to "
|
||||||
|
"support a call."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
executive_summary: str = Field(
|
executive_summary: str = Field(
|
||||||
|
|||||||
Reference in New Issue
Block a user