mirror of
https://github.com/TauricResearch/TradingAgents.git
synced 2026-09-27 15:02:39 +03:00
refactor: one name per decision in the state and its log
- the Research Manager's plan and the Portfolio Manager's decision live only in investment_plan and final_trade_decision; the debate states no longer copy them as judge_decision - the saved state log writes the Trader's plan as trader_investment_plan, its state key - the report tree, the final display and the live CLI read the decisions from those fields
This commit is contained in:
+6
-6
@@ -404,8 +404,8 @@ def display_complete_report(final_state):
|
|||||||
research.append(("Bull Researcher", debate["bull_history"]))
|
research.append(("Bull Researcher", debate["bull_history"]))
|
||||||
if debate.get("bear_history"):
|
if debate.get("bear_history"):
|
||||||
research.append(("Bear Researcher", debate["bear_history"]))
|
research.append(("Bear Researcher", debate["bear_history"]))
|
||||||
if debate.get("judge_decision"):
|
if final_state.get("investment_plan"):
|
||||||
research.append(("Research Manager", debate["judge_decision"]))
|
research.append(("Research Manager", final_state["investment_plan"]))
|
||||||
if research:
|
if research:
|
||||||
console.print(Panel("[bold]II. Research Team Decision[/bold]", border_style="magenta"))
|
console.print(Panel("[bold]II. Research Team Decision[/bold]", border_style="magenta"))
|
||||||
for title, content in research:
|
for title, content in research:
|
||||||
@@ -431,10 +431,10 @@ def display_complete_report(final_state):
|
|||||||
for title, content in risk_reports:
|
for title, content in risk_reports:
|
||||||
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
|
console.print(Panel(Markdown(content), title=title, border_style="blue", padding=(1, 2)))
|
||||||
|
|
||||||
# V. Portfolio Manager Decision
|
# V. Portfolio Manager Decision
|
||||||
if risk.get("judge_decision"):
|
if final_state.get("final_trade_decision"):
|
||||||
console.print(Panel("[bold]V. Portfolio Manager Decision[/bold]", border_style="green"))
|
console.print(Panel("[bold]V. Portfolio Manager Decision[/bold]", border_style="green"))
|
||||||
console.print(Panel(Markdown(risk["judge_decision"]), title="Portfolio Manager", border_style="blue", padding=(1, 2)))
|
console.print(Panel(Markdown(final_state["final_trade_decision"]), title="Portfolio Manager", border_style="blue", padding=(1, 2)))
|
||||||
|
|
||||||
|
|
||||||
def update_research_team_status(status):
|
def update_research_team_status(status):
|
||||||
|
|||||||
+2
-2
@@ -253,7 +253,7 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
|
|||||||
debate_state = chunk["investment_debate_state"]
|
debate_state = chunk["investment_debate_state"]
|
||||||
bull_hist = debate_state.get("bull_history", "").strip()
|
bull_hist = debate_state.get("bull_history", "").strip()
|
||||||
bear_hist = debate_state.get("bear_history", "").strip()
|
bear_hist = debate_state.get("bear_history", "").strip()
|
||||||
judge = debate_state.get("judge_decision", "").strip()
|
judge = (chunk.get("investment_plan") or "").strip()
|
||||||
|
|
||||||
# Only update status when there's actual content
|
# Only update status when there's actual content
|
||||||
if bull_hist or bear_hist:
|
if bull_hist or bear_hist:
|
||||||
@@ -288,7 +288,7 @@ def run_analysis(checkpoint: bool | None = None, portfolio=None):
|
|||||||
agg_hist = risk_state.get("aggressive_history", "").strip()
|
agg_hist = risk_state.get("aggressive_history", "").strip()
|
||||||
con_hist = risk_state.get("conservative_history", "").strip()
|
con_hist = risk_state.get("conservative_history", "").strip()
|
||||||
neu_hist = risk_state.get("neutral_history", "").strip()
|
neu_hist = risk_state.get("neutral_history", "").strip()
|
||||||
judge = risk_state.get("judge_decision", "").strip()
|
judge = (chunk.get("final_trade_decision") or "").strip()
|
||||||
|
|
||||||
if agg_hist:
|
if agg_hist:
|
||||||
if message_buffer.agent_status.get("Aggressive Analyst") != "completed":
|
if message_buffer.agent_status.get("Aggressive Analyst") != "completed":
|
||||||
|
|||||||
@@ -48,9 +48,9 @@ def _state(ticker, final="评级: 买入"):
|
|||||||
"fundamentals_report": "基本面", "investment_plan": "计划",
|
"fundamentals_report": "基本面", "investment_plan": "计划",
|
||||||
"trader_investment_plan": "交易计划", "final_trade_decision": final, "final_rating": "REVIEW",
|
"trader_investment_plan": "交易计划", "final_trade_decision": final, "final_rating": "REVIEW",
|
||||||
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
||||||
"current_response": "", "judge_decision": "", "count": 0},
|
"current_response": "", "count": 0},
|
||||||
"risk_debate_state": {"aggressive_history": "", "conservative_history": "",
|
"risk_debate_state": {"aggressive_history": "", "conservative_history": "",
|
||||||
"neutral_history": "", "history": "", "judge_decision": "",
|
"neutral_history": "", "history": "",
|
||||||
"latest_speaker": "", "current_aggressive_response": "",
|
"latest_speaker": "", "current_aggressive_response": "",
|
||||||
"current_conservative_response": "", "current_neutral_response": "",
|
"current_conservative_response": "", "current_neutral_response": "",
|
||||||
"count": 0},
|
"count": 0},
|
||||||
@@ -76,6 +76,19 @@ def test_the_state_log_keeps_non_ascii_readable(tmp_path):
|
|||||||
assert json.loads(written) # still valid JSON
|
assert json.loads(written) # still valid JSON
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_the_state_log_names_each_field_as_the_state_does(tmp_path):
|
||||||
|
"""One name per field: the Trader's plan under its state key, and no second
|
||||||
|
copy of the managers' decisions under the debate states."""
|
||||||
|
_bare_graph(tmp_path)._log_state("2026-09-01", _state("NVDA"))
|
||||||
|
|
||||||
|
logged = json.loads(next(tmp_path.rglob("full_states_log*.json")).read_text(encoding="utf-8"))
|
||||||
|
assert logged["trader_investment_plan"] == "交易计划"
|
||||||
|
assert logged["investment_plan"] == "计划"
|
||||||
|
assert "trader_investment_decision" not in logged
|
||||||
|
assert "judge_decision" not in json.dumps(logged)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.unit
|
@pytest.mark.unit
|
||||||
def test_the_live_display_does_not_scroll_the_terminal():
|
def test_the_live_display_does_not_scroll_the_terminal():
|
||||||
"""A layout taller than the window makes rich redraw by scrolling, which
|
"""A layout taller than the window makes rich redraw by scrolling, which
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ def _make_pm_state(past_context=""):
|
|||||||
"aggressive_history": "",
|
"aggressive_history": "",
|
||||||
"conservative_history": "",
|
"conservative_history": "",
|
||||||
"neutral_history": "",
|
"neutral_history": "",
|
||||||
"judge_decision": "",
|
|
||||||
"current_aggressive_response": "",
|
"current_aggressive_response": "",
|
||||||
"current_conservative_response": "",
|
"current_conservative_response": "",
|
||||||
"current_neutral_response": "",
|
"current_neutral_response": "",
|
||||||
@@ -906,13 +905,13 @@ class TestLegacyRemoval:
|
|||||||
"fundamentals_report": "",
|
"fundamentals_report": "",
|
||||||
"investment_debate_state": {
|
"investment_debate_state": {
|
||||||
"bull_history": "", "bear_history": "", "history": "",
|
"bull_history": "", "bear_history": "", "history": "",
|
||||||
"current_response": "", "judge_decision": "",
|
"current_response": "",
|
||||||
},
|
},
|
||||||
"investment_plan": "",
|
"investment_plan": "",
|
||||||
"trader_investment_plan": "",
|
"trader_investment_plan": "",
|
||||||
"risk_debate_state": {
|
"risk_debate_state": {
|
||||||
"aggressive_history": "", "conservative_history": "",
|
"aggressive_history": "", "conservative_history": "",
|
||||||
"neutral_history": "", "history": "", "judge_decision": "",
|
"neutral_history": "", "history": "",
|
||||||
"current_aggressive_response": "", "current_conservative_response": "",
|
"current_aggressive_response": "", "current_conservative_response": "",
|
||||||
"current_neutral_response": "", "count": 1, "latest_speaker": "",
|
"current_neutral_response": "", "count": 1, "latest_speaker": "",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -139,11 +139,11 @@ def test_decision_agents_see_the_portfolio(module, factory, monkeypatch):
|
|||||||
"news_report": "N", "fundamentals_report": "F", "investment_plan": "P",
|
"news_report": "N", "fundamentals_report": "F", "investment_plan": "P",
|
||||||
"trader_investment_plan": "T", "past_context": "",
|
"trader_investment_plan": "T", "past_context": "",
|
||||||
"portfolio_context": "PORTFOLIO_BLOCK_MARKER",
|
"portfolio_context": "PORTFOLIO_BLOCK_MARKER",
|
||||||
"investment_debate_state": {"history": "", "judge_decision": "", "count": 0},
|
"investment_debate_state": {"history": "", "count": 0},
|
||||||
"risk_debate_state": {"history": "", "latest_speaker": "", "count": 0,
|
"risk_debate_state": {"history": "", "latest_speaker": "", "count": 0,
|
||||||
"aggressive_history": "", "conservative_history": "", "neutral_history": "",
|
"aggressive_history": "", "conservative_history": "", "neutral_history": "",
|
||||||
"current_aggressive_response": "", "current_conservative_response": "",
|
"current_aggressive_response": "", "current_conservative_response": "",
|
||||||
"current_neutral_response": "", "judge_decision": ""},
|
"current_neutral_response": ""},
|
||||||
}
|
}
|
||||||
node = getattr(mod, factory)(_LLM())
|
node = getattr(mod, factory)(_LLM())
|
||||||
node(state)
|
node(state)
|
||||||
|
|||||||
@@ -74,11 +74,11 @@ def test_a_report_that_was_never_produced_says_so(module, factory):
|
|||||||
"market_report": "RSI 61, price 178.", "sentiment_report": "", "news_report": "",
|
"market_report": "RSI 61, price 178.", "sentiment_report": "", "news_report": "",
|
||||||
"fundamentals_report": "", "investment_plan": "P", "trader_investment_plan": "T",
|
"fundamentals_report": "", "investment_plan": "P", "trader_investment_plan": "T",
|
||||||
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
"investment_debate_state": {"bull_history": "", "bear_history": "", "history": "",
|
||||||
"current_response": "", "judge_decision": "", "count": 0},
|
"current_response": "", "count": 0},
|
||||||
"risk_debate_state": {"history": "", "latest_speaker": "", "count": 0,
|
"risk_debate_state": {"history": "", "latest_speaker": "", "count": 0,
|
||||||
"aggressive_history": "", "conservative_history": "", "neutral_history": "",
|
"aggressive_history": "", "conservative_history": "", "neutral_history": "",
|
||||||
"current_aggressive_response": "", "current_conservative_response": "",
|
"current_aggressive_response": "", "current_conservative_response": "",
|
||||||
"current_neutral_response": "", "judge_decision": ""},
|
"current_neutral_response": ""},
|
||||||
}
|
}
|
||||||
getattr(mod, factory)(_LLM())(state)
|
getattr(mod, factory)(_LLM())(state)
|
||||||
|
|
||||||
|
|||||||
@@ -194,11 +194,11 @@ def test_a_decision_prompt_states_the_shape_of_its_answer(module, factory, must_
|
|||||||
"news_report": "N", "fundamentals_report": "F", "investment_plan": "P",
|
"news_report": "N", "fundamentals_report": "F", "investment_plan": "P",
|
||||||
"trader_investment_plan": "T", "past_context": "", "portfolio_context": "",
|
"trader_investment_plan": "T", "past_context": "", "portfolio_context": "",
|
||||||
"investment_debate_state": {"bull_history": "b", "bear_history": "r", "history": "h",
|
"investment_debate_state": {"bull_history": "b", "bear_history": "r", "history": "h",
|
||||||
"current_response": "", "judge_decision": "", "count": 2},
|
"current_response": "", "count": 2},
|
||||||
"risk_debate_state": {"history": "h", "latest_speaker": "", "count": 3,
|
"risk_debate_state": {"history": "h", "latest_speaker": "", "count": 3,
|
||||||
"aggressive_history": "", "conservative_history": "", "neutral_history": "",
|
"aggressive_history": "", "conservative_history": "", "neutral_history": "",
|
||||||
"current_aggressive_response": "", "current_conservative_response": "",
|
"current_aggressive_response": "", "current_conservative_response": "",
|
||||||
"current_neutral_response": "", "judge_decision": ""},
|
"current_neutral_response": ""},
|
||||||
}
|
}
|
||||||
getattr(mod, factory)(_LLM())(state)
|
getattr(mod, factory)(_LLM())(state)
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ def _state():
|
|||||||
return {
|
return {
|
||||||
"market_report": "MKT",
|
"market_report": "MKT",
|
||||||
"news_report": "NEWS",
|
"news_report": "NEWS",
|
||||||
"investment_debate_state": {"judge_decision": "RM PLAN"},
|
"investment_debate_state": {"bull_history": "BULL"},
|
||||||
|
"investment_plan": "RM PLAN",
|
||||||
"trader_investment_plan": "TRADE",
|
"trader_investment_plan": "TRADE",
|
||||||
"risk_debate_state": {"judge_decision": "PM DECISION"},
|
"risk_debate_state": {"neutral_history": "NEUTRAL"},
|
||||||
|
"final_trade_decision": "PM DECISION",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def test_research_manager_prompt_states_constraint():
|
|||||||
"company_of_interest": "NVDA",
|
"company_of_interest": "NVDA",
|
||||||
"investment_debate_state": {
|
"investment_debate_state": {
|
||||||
"history": "h", "bull_history": "b", "bear_history": "r",
|
"history": "h", "bull_history": "b", "bear_history": "r",
|
||||||
"current_response": "", "judge_decision": "", "count": 1,
|
"current_response": "", "count": 1,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
assert NO_EXTERNAL_TOOLS in _prompt_text(captured["prompt"])
|
assert NO_EXTERNAL_TOOLS in _prompt_text(captured["prompt"])
|
||||||
|
|||||||
@@ -292,7 +292,6 @@ def _make_rm_state():
|
|||||||
"bull_history": "Bull says...",
|
"bull_history": "Bull says...",
|
||||||
"bear_history": "Bear says...",
|
"bear_history": "Bear says...",
|
||||||
"current_response": "",
|
"current_response": "",
|
||||||
"judge_decision": "",
|
|
||||||
"count": 1,
|
"count": 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ Write these sections, in this order, starting with the rating on its own line:
|
|||||||
final_rating = parse_rating(final_trade_decision)
|
final_rating = parse_rating(final_trade_decision)
|
||||||
|
|
||||||
new_risk_debate_state = {
|
new_risk_debate_state = {
|
||||||
"judge_decision": final_trade_decision,
|
|
||||||
"history": risk_debate_state["history"],
|
"history": risk_debate_state["history"],
|
||||||
"aggressive_history": risk_debate_state["aggressive_history"],
|
"aggressive_history": risk_debate_state["aggressive_history"],
|
||||||
"conservative_history": risk_debate_state["conservative_history"],
|
"conservative_history": risk_debate_state["conservative_history"],
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ Write these sections, in this order, starting with the recommendation on its own
|
|||||||
)
|
)
|
||||||
|
|
||||||
new_investment_debate_state = {
|
new_investment_debate_state = {
|
||||||
"judge_decision": investment_plan,
|
|
||||||
"history": investment_debate_state.get("history", ""),
|
"history": investment_debate_state.get("history", ""),
|
||||||
"bear_history": investment_debate_state.get("bear_history", ""),
|
"bear_history": investment_debate_state.get("bear_history", ""),
|
||||||
"bull_history": investment_debate_state.get("bull_history", ""),
|
"bull_history": investment_debate_state.get("bull_history", ""),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class InvestDebateState(TypedDict):
|
|||||||
]
|
]
|
||||||
history: Annotated[str, "Conversation history"]
|
history: Annotated[str, "Conversation history"]
|
||||||
current_response: Annotated[str, "Latest response"]
|
current_response: Annotated[str, "Latest response"]
|
||||||
judge_decision: Annotated[str, "Final judge decision"]
|
|
||||||
count: Annotated[int, "Length of the current conversation"]
|
count: Annotated[int, "Length of the current conversation"]
|
||||||
|
|
||||||
|
|
||||||
@@ -40,7 +39,6 @@ class RiskDebateState(TypedDict):
|
|||||||
current_neutral_response: Annotated[
|
current_neutral_response: Annotated[
|
||||||
str, "Latest response by the neutral analyst"
|
str, "Latest response by the neutral analyst"
|
||||||
]
|
]
|
||||||
judge_decision: Annotated[str, "Judge's decision"]
|
|
||||||
count: Annotated[int, "Length of the current conversation"]
|
count: Annotated[int, "Length of the current conversation"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ class Propagator:
|
|||||||
"bear_history": "",
|
"bear_history": "",
|
||||||
"history": "",
|
"history": "",
|
||||||
"current_response": "",
|
"current_response": "",
|
||||||
"judge_decision": "",
|
|
||||||
"count": 0,
|
"count": 0,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -55,7 +54,6 @@ class Propagator:
|
|||||||
"current_aggressive_response": "",
|
"current_aggressive_response": "",
|
||||||
"current_conservative_response": "",
|
"current_conservative_response": "",
|
||||||
"current_neutral_response": "",
|
"current_neutral_response": "",
|
||||||
"judge_decision": "",
|
|
||||||
"count": 0,
|
"count": 0,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -359,17 +359,13 @@ class TradingAgentsGraph:
|
|||||||
"current_response": final_state["investment_debate_state"][
|
"current_response": final_state["investment_debate_state"][
|
||||||
"current_response"
|
"current_response"
|
||||||
],
|
],
|
||||||
"judge_decision": final_state["investment_debate_state"][
|
|
||||||
"judge_decision"
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
"trader_investment_decision": final_state["trader_investment_plan"],
|
"trader_investment_plan": final_state["trader_investment_plan"],
|
||||||
"risk_debate_state": {
|
"risk_debate_state": {
|
||||||
"aggressive_history": final_state["risk_debate_state"]["aggressive_history"],
|
"aggressive_history": final_state["risk_debate_state"]["aggressive_history"],
|
||||||
"conservative_history": final_state["risk_debate_state"]["conservative_history"],
|
"conservative_history": final_state["risk_debate_state"]["conservative_history"],
|
||||||
"neutral_history": final_state["risk_debate_state"]["neutral_history"],
|
"neutral_history": final_state["risk_debate_state"]["neutral_history"],
|
||||||
"history": final_state["risk_debate_state"]["history"],
|
"history": final_state["risk_debate_state"]["history"],
|
||||||
"judge_decision": final_state["risk_debate_state"]["judge_decision"],
|
|
||||||
},
|
},
|
||||||
"investment_plan": final_state["investment_plan"],
|
"investment_plan": final_state["investment_plan"],
|
||||||
"final_trade_decision": final_state["final_trade_decision"],
|
"final_trade_decision": final_state["final_trade_decision"],
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ def write_report_tree(final_state: dict, ticker: str, save_path) -> Path:
|
|||||||
research_dir.mkdir(exist_ok=True)
|
research_dir.mkdir(exist_ok=True)
|
||||||
(research_dir / "bear.md").write_text(debate["bear_history"], encoding="utf-8")
|
(research_dir / "bear.md").write_text(debate["bear_history"], encoding="utf-8")
|
||||||
research_parts.append(("Bear Researcher", debate["bear_history"]))
|
research_parts.append(("Bear Researcher", debate["bear_history"]))
|
||||||
if debate.get("judge_decision"):
|
if final_state.get("investment_plan"):
|
||||||
research_dir.mkdir(exist_ok=True)
|
research_dir.mkdir(exist_ok=True)
|
||||||
(research_dir / "manager.md").write_text(debate["judge_decision"], encoding="utf-8")
|
(research_dir / "manager.md").write_text(final_state["investment_plan"], encoding="utf-8")
|
||||||
research_parts.append(("Research Manager", debate["judge_decision"]))
|
research_parts.append(("Research Manager", final_state["investment_plan"]))
|
||||||
if research_parts:
|
if research_parts:
|
||||||
content = "\n\n".join(f"### {name}\n{text}" for name, text in research_parts)
|
content = "\n\n".join(f"### {name}\n{text}" for name, text in research_parts)
|
||||||
sections.append(f"## II. Research Team Decision\n\n{content}")
|
sections.append(f"## II. Research Team Decision\n\n{content}")
|
||||||
@@ -88,12 +88,12 @@ def write_report_tree(final_state: dict, ticker: str, save_path) -> Path:
|
|||||||
content = "\n\n".join(f"### {name}\n{text}" for name, text in risk_parts)
|
content = "\n\n".join(f"### {name}\n{text}" for name, text in risk_parts)
|
||||||
sections.append(f"## IV. Risk Management Team Decision\n\n{content}")
|
sections.append(f"## IV. Risk Management Team Decision\n\n{content}")
|
||||||
|
|
||||||
# 5. Portfolio Manager
|
# 5. Portfolio Manager
|
||||||
if risk.get("judge_decision"):
|
if final_state.get("final_trade_decision"):
|
||||||
portfolio_dir = save_path / "5_portfolio"
|
portfolio_dir = save_path / "5_portfolio"
|
||||||
portfolio_dir.mkdir(exist_ok=True)
|
portfolio_dir.mkdir(exist_ok=True)
|
||||||
(portfolio_dir / "decision.md").write_text(risk["judge_decision"], encoding="utf-8")
|
(portfolio_dir / "decision.md").write_text(final_state["final_trade_decision"], encoding="utf-8")
|
||||||
sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{risk['judge_decision']}")
|
sections.append(f"## V. Portfolio Manager Decision\n\n### Portfolio Manager\n{final_state['final_trade_decision']}")
|
||||||
|
|
||||||
# Write consolidated report
|
# Write consolidated report
|
||||||
header = f"# Trading Analysis Report: {ticker}\n\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
header = f"# Trading Analysis Report: {ticker}\n\nGenerated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user