mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 15:02:39 +03:00
fix(agents): carry the Portfolio Manager's rating through the run (#1383)
- the typed rating is the state's final_rating; propagate returns it, and the memory log tag, the state log and the CLI review check read it - the decision text is parsed only when the Portfolio Manager answered in free text - TradingAgentsGraph.process_signal is removed
This commit is contained in:
@@ -76,10 +76,6 @@ class _FakeGraph:
|
||||
self.calls.append(("create_run_state", ticker, trade_date))
|
||||
return {"messages": [], "company_of_interest": ticker}
|
||||
|
||||
def process_signal(self, text):
|
||||
from tradingagents.agents.rating import parse_rating
|
||||
return parse_rating(text)
|
||||
|
||||
def record_decision(self, ticker, trade_date, final_state):
|
||||
self.calls.append(("record_decision", ticker, trade_date, final_state.get("final_trade_decision")))
|
||||
|
||||
@@ -101,7 +97,7 @@ class _FakeGraph:
|
||||
|
||||
def stream(self, graph_input, **kwargs):
|
||||
yield {"messages": [], "market_report": "M"}
|
||||
yield {"messages": [], "final_trade_decision": "Rating: Buy\n\nBuy NVDA."}
|
||||
yield {"messages": [], "final_trade_decision": "Rating: Buy\n\nBuy NVDA.", "final_rating": "Buy"}
|
||||
|
||||
|
||||
class _NullLive:
|
||||
|
||||
@@ -46,7 +46,7 @@ def _state(ticker, final="评级: 买入"):
|
||||
"company_of_interest": ticker, "trade_date": "2026-09-01",
|
||||
"market_report": "市场", "sentiment_report": "情绪", "news_report": "新闻",
|
||||
"fundamentals_report": "基本面", "investment_plan": "计划",
|
||||
"trader_investment_plan": "交易计划", "final_trade_decision": final,
|
||||
"trader_investment_plan": "交易计划", "final_trade_decision": final, "final_rating": "REVIEW",
|
||||
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
||||
"current_response": "", "judge_decision": "", "count": 0},
|
||||
"risk_debate_state": {"aggressive_history": "", "conservative_history": "",
|
||||
|
||||
@@ -34,8 +34,10 @@ STRUCTURED = {
|
||||
schemas.ResearchPlan: schemas.ResearchPlan(
|
||||
recommendation=schemas.PortfolioRating.OVERWEIGHT, rationale="r", strategic_actions="a"),
|
||||
schemas.TraderProposal: schemas.TraderProposal(action=schemas.TraderAction.BUY, reasoning="r"),
|
||||
# The thesis quotes another party's rating; the decision is still the PM's own.
|
||||
schemas.PortfolioDecision: schemas.PortfolioDecision(
|
||||
rating=schemas.PortfolioRating.OVERWEIGHT, executive_summary="s", investment_thesis="t"),
|
||||
rating=schemas.PortfolioRating.OVERWEIGHT, executive_summary="s",
|
||||
investment_thesis="Street consensus rating: Buy (28 of 35 analysts)."),
|
||||
schemas.SentimentReport: schemas.SentimentReport(
|
||||
overall_band=schemas.SentimentBand.NEUTRAL, overall_score=5.0, confidence="low", narrative="n"),
|
||||
}
|
||||
@@ -126,7 +128,7 @@ def test_a_full_run_reaches_a_logged_decision(tmp_path, monkeypatch, offline, st
|
||||
|
||||
state, signal = graph.propagate("NVDA", TRADE_DATE)
|
||||
|
||||
assert signal == "Overweight"
|
||||
assert signal == state["final_rating"] == "Overweight"
|
||||
for key in ("market_report", "sentiment_report", "news_report", "fundamentals_report",
|
||||
"investment_plan", "trader_investment_plan", "final_trade_decision"):
|
||||
assert state[key].strip(), key
|
||||
|
||||
@@ -898,6 +898,7 @@ class TestLegacyRemoval:
|
||||
|
||||
fake_state = {
|
||||
"final_trade_decision": "Rating: Buy\nBuy NVDA.",
|
||||
"final_rating": "Buy",
|
||||
"company_of_interest": "NVDA",
|
||||
"trade_date": "2026-01-10",
|
||||
"market_report": "",
|
||||
@@ -924,7 +925,6 @@ class TestLegacyRemoval:
|
||||
mock_graph.graph.invoke.return_value = fake_state
|
||||
mock_graph.propagator.create_initial_state.return_value = fake_state
|
||||
mock_graph.propagator.get_graph_args.return_value = {}
|
||||
mock_graph.process_signal.return_value = "Buy"
|
||||
# Bind the real _run_graph so propagate's call to self._run_graph executes
|
||||
# the actual write path instead of the auto-MagicMock.
|
||||
mock_graph._run_graph = functools.partial(
|
||||
|
||||
@@ -174,9 +174,8 @@ def test_completed_run_clears_the_checkpoint_it_wrote(tmp_path, monkeypatch):
|
||||
graph.debug = False
|
||||
graph._resuming = False
|
||||
graph.propagator.get_graph_args = lambda callbacks=None: {}
|
||||
graph.process_signal = lambda d: "Hold"
|
||||
graph._log_state = lambda *a, **k: None
|
||||
graph.graph = type("G", (), {"invoke": lambda self, i, **k: {"final_trade_decision": "Rating: Hold\n\nx"}})()
|
||||
graph.graph = type("G", (), {"invoke": lambda self, i, **k: {"final_trade_decision": "Rating: Hold\n\nx", "final_rating": "Hold"}})()
|
||||
book = PortfolioContext.model_validate(HOLDING)
|
||||
|
||||
written = graph._run_signature("stock", book) # what begin_checkpoint keys on
|
||||
|
||||
@@ -121,10 +121,6 @@ def test_the_cli_says_when_a_run_produced_no_usable_rating(monkeypatch, tmp_path
|
||||
def record_decision(self, *a, **k):
|
||||
pass
|
||||
|
||||
def process_signal(self, text):
|
||||
from tradingagents.agents.rating import parse_rating
|
||||
return parse_rating(text)
|
||||
|
||||
def get_graph_args(self, callbacks=None):
|
||||
return {}
|
||||
|
||||
@@ -141,7 +137,7 @@ def test_the_cli_says_when_a_run_produced_no_usable_rating(monkeypatch, tmp_path
|
||||
pass
|
||||
|
||||
def stream(self, *a, **k):
|
||||
yield {"messages": [], "final_trade_decision": REFUSAL}
|
||||
yield {"messages": [], "final_trade_decision": REFUSAL, "final_rating": RATING_REVIEW}
|
||||
|
||||
fake = _Graph()
|
||||
fake.graph = fake
|
||||
@@ -210,3 +206,15 @@ def test_a_decision_prompt_states_the_shape_of_its_answer(module, factory, must_
|
||||
assert "## Output" in prompt, "no output-format section in the prompt"
|
||||
section = prompt.split("## Output", 1)[1]
|
||||
assert f"**{must_name}**" in section, section[:300]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_a_state_without_the_typed_rating_reads_it_from_the_decision():
|
||||
"""A run finished by an older version and resumed from its checkpoint has
|
||||
no final_rating; every reader falls back the same way instead of one
|
||||
raising and another reporting REVIEW."""
|
||||
from tradingagents.agents.rating import run_rating
|
||||
|
||||
assert run_rating({"final_rating": "Hold", "final_trade_decision": "**Rating**: Buy"}) == "Hold"
|
||||
assert run_rating({"final_trade_decision": "**Rating**: Sell\n\nExit."}) == "Sell"
|
||||
assert run_rating({}) == RATING_REVIEW
|
||||
|
||||
@@ -75,20 +75,3 @@ class TestExtractRating:
|
||||
# The memory log tags an unreadable decision REVIEW, never a tradeable rating.
|
||||
assert parse_rating("No rating here.") == RATING_REVIEW
|
||||
assert parse_rating("No rating here.", default="Underweight") == "Underweight"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
class TestGraphSignalContract:
|
||||
"""The graph-facing signal (TradingAgentsGraph.process_signal) honors the
|
||||
documented "5-tier or REVIEW" contract, not just the parser in isolation."""
|
||||
|
||||
def _bare_graph(self):
|
||||
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
||||
g = object.__new__(TradingAgentsGraph)
|
||||
return g
|
||||
|
||||
def test_graph_surfaces_review(self):
|
||||
assert self._bare_graph().process_signal("no rating in here") == RATING_REVIEW
|
||||
|
||||
def test_graph_returns_rating(self):
|
||||
assert self._bare_graph().process_signal("**Rating**: Sell") == "Sell"
|
||||
|
||||
Reference in New Issue
Block a user