Parallelize research & risk debate stages for ~25% faster analysis
Run Bull+Bear researchers concurrently and all 3 risk analysts (Aggressive/Conservative/Neutral) concurrently instead of sequentially. With max_debate_rounds=1, there's no back-and-forth so parallel execution is safe. Sequential mode is completely unchanged. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
aa654c9425
commit
3cd0c19b35
2
app.py
2
app.py
|
|
@ -239,6 +239,8 @@ async def run_analysis(analysis_id: str, ticker: str, trade_date: str):
|
||||||
trader_emitted = True
|
trader_emitted = True
|
||||||
buf.update_agent_status("Trader", "completed")
|
buf.update_agent_status("Trader", "completed")
|
||||||
buf.update_agent_status("Aggressive Analyst", "in_progress")
|
buf.update_agent_status("Aggressive Analyst", "in_progress")
|
||||||
|
buf.update_agent_status("Conservative Analyst", "in_progress")
|
||||||
|
buf.update_agent_status("Neutral Analyst", "in_progress")
|
||||||
buf.update_report_section("trader_investment_plan", chunk["trader_investment_plan"])
|
buf.update_report_section("trader_investment_plan", chunk["trader_investment_plan"])
|
||||||
evt = {
|
evt = {
|
||||||
"type": "trader",
|
"type": "trader",
|
||||||
|
|
|
||||||
|
|
@ -820,6 +820,7 @@ def update_analyst_statuses(message_buffer, chunk):
|
||||||
if not found_active and selected:
|
if not found_active and selected:
|
||||||
if message_buffer.agent_status.get("Bull Researcher") == "pending":
|
if message_buffer.agent_status.get("Bull Researcher") == "pending":
|
||||||
message_buffer.update_agent_status("Bull Researcher", "in_progress")
|
message_buffer.update_agent_status("Bull Researcher", "in_progress")
|
||||||
|
message_buffer.update_agent_status("Bear Researcher", "in_progress")
|
||||||
|
|
||||||
def extract_content_string(content):
|
def extract_content_string(content):
|
||||||
"""Extract string content from various message formats.
|
"""Extract string content from various message formats.
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"""Parallel analyst execution for TradingAgents.
|
"""Parallel execution nodes for TradingAgents.
|
||||||
|
|
||||||
Runs all analyst agents (Market, Social, News, Fundamentals) concurrently
|
Provides parallel wrappers for:
|
||||||
instead of sequentially, cutting the analyst phase from ~8-9 min to ~2-3 min.
|
- Analyst phase (Market, Social, News, Fundamentals)
|
||||||
|
- Research debate phase (Bull + Bear)
|
||||||
|
- Risk debate phase (Aggressive + Conservative + Neutral)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
@ -74,3 +76,81 @@ def create_parallel_analyst_node(analyst_fns, tool_nodes, selected_analysts):
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
return parallel_analysts_node
|
return parallel_analysts_node
|
||||||
|
|
||||||
|
|
||||||
|
def create_parallel_research_node(bull_fn, bear_fn):
|
||||||
|
"""Create a node that runs Bull and Bear researchers in parallel.
|
||||||
|
|
||||||
|
Both agents receive the same state (reports + empty debate state) and
|
||||||
|
produce independent arguments. Results are merged into a single
|
||||||
|
investment_debate_state with both histories and count=2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def parallel_research_node(state):
|
||||||
|
bull_result, bear_result = await asyncio.gather(
|
||||||
|
asyncio.to_thread(bull_fn, state),
|
||||||
|
asyncio.to_thread(bear_fn, state),
|
||||||
|
)
|
||||||
|
|
||||||
|
bull_debate = bull_result["investment_debate_state"]
|
||||||
|
bear_debate = bear_result["investment_debate_state"]
|
||||||
|
|
||||||
|
merged_debate = {
|
||||||
|
"bull_history": bull_debate.get("bull_history", ""),
|
||||||
|
"bear_history": bear_debate.get("bear_history", ""),
|
||||||
|
"history": bull_debate.get("bull_history", "")
|
||||||
|
+ "\n"
|
||||||
|
+ bear_debate.get("bear_history", ""),
|
||||||
|
"current_response": bear_debate.get("current_response", ""),
|
||||||
|
"judge_decision": "",
|
||||||
|
"count": 2,
|
||||||
|
}
|
||||||
|
return {"investment_debate_state": merged_debate}
|
||||||
|
|
||||||
|
return parallel_research_node
|
||||||
|
|
||||||
|
|
||||||
|
def create_parallel_risk_node(aggressive_fn, conservative_fn, neutral_fn):
|
||||||
|
"""Create a node that runs all 3 risk analysts in parallel.
|
||||||
|
|
||||||
|
All agents receive the same state (trader plan + empty risk debate state)
|
||||||
|
and produce independent arguments. Results are merged into a single
|
||||||
|
risk_debate_state with all histories and count=3.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def parallel_risk_node(state):
|
||||||
|
agg_result, con_result, neu_result = await asyncio.gather(
|
||||||
|
asyncio.to_thread(aggressive_fn, state),
|
||||||
|
asyncio.to_thread(conservative_fn, state),
|
||||||
|
asyncio.to_thread(neutral_fn, state),
|
||||||
|
)
|
||||||
|
|
||||||
|
agg_debate = agg_result["risk_debate_state"]
|
||||||
|
con_debate = con_result["risk_debate_state"]
|
||||||
|
neu_debate = neu_result["risk_debate_state"]
|
||||||
|
|
||||||
|
merged_debate = {
|
||||||
|
"aggressive_history": agg_debate.get("aggressive_history", ""),
|
||||||
|
"conservative_history": con_debate.get("conservative_history", ""),
|
||||||
|
"neutral_history": neu_debate.get("neutral_history", ""),
|
||||||
|
"history": agg_debate.get("aggressive_history", "")
|
||||||
|
+ "\n"
|
||||||
|
+ con_debate.get("conservative_history", "")
|
||||||
|
+ "\n"
|
||||||
|
+ neu_debate.get("neutral_history", ""),
|
||||||
|
"latest_speaker": "Neutral",
|
||||||
|
"current_aggressive_response": agg_debate.get(
|
||||||
|
"current_aggressive_response", ""
|
||||||
|
),
|
||||||
|
"current_conservative_response": con_debate.get(
|
||||||
|
"current_conservative_response", ""
|
||||||
|
),
|
||||||
|
"current_neutral_response": neu_debate.get(
|
||||||
|
"current_neutral_response", ""
|
||||||
|
),
|
||||||
|
"judge_decision": "",
|
||||||
|
"count": 3,
|
||||||
|
}
|
||||||
|
return {"risk_debate_state": merged_debate}
|
||||||
|
|
||||||
|
return parallel_risk_node
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,11 @@ from tradingagents.agents import *
|
||||||
from tradingagents.agents.utils.agent_states import AgentState
|
from tradingagents.agents.utils.agent_states import AgentState
|
||||||
|
|
||||||
from .conditional_logic import ConditionalLogic
|
from .conditional_logic import ConditionalLogic
|
||||||
from .parallel_analysts import create_parallel_analyst_node
|
from .parallel_analysts import (
|
||||||
|
create_parallel_analyst_node,
|
||||||
|
create_parallel_research_node,
|
||||||
|
create_parallel_risk_node,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class GraphSetup:
|
class GraphSetup:
|
||||||
|
|
@ -126,23 +130,49 @@ class GraphSetup:
|
||||||
)
|
)
|
||||||
workflow.add_node(f"tools_{analyst_type}", tool_nodes[analyst_type])
|
workflow.add_node(f"tools_{analyst_type}", tool_nodes[analyst_type])
|
||||||
|
|
||||||
# Add other nodes
|
|
||||||
workflow.add_node("Bull Researcher", bull_researcher_node)
|
|
||||||
workflow.add_node("Bear Researcher", bear_researcher_node)
|
|
||||||
workflow.add_node("Research Manager", research_manager_node)
|
|
||||||
workflow.add_node("Trader", trader_node)
|
|
||||||
workflow.add_node("Aggressive Analyst", aggressive_analyst)
|
|
||||||
workflow.add_node("Neutral Analyst", neutral_analyst)
|
|
||||||
workflow.add_node("Conservative Analyst", conservative_analyst)
|
|
||||||
workflow.add_node("Risk Judge", risk_manager_node)
|
|
||||||
|
|
||||||
# Define edges
|
|
||||||
if parallel:
|
if parallel:
|
||||||
# Parallel: START → Parallel Analysts → Bull Researcher
|
# --- Parallel mode ---
|
||||||
|
# Analysts: single parallel node
|
||||||
|
# Research: Bull+Bear run concurrently in one node
|
||||||
|
# Risk: Agg+Con+Neu run concurrently in one node
|
||||||
|
|
||||||
|
parallel_research = create_parallel_research_node(
|
||||||
|
bull_researcher_node, bear_researcher_node
|
||||||
|
)
|
||||||
|
parallel_risk = create_parallel_risk_node(
|
||||||
|
aggressive_analyst, conservative_analyst, neutral_analyst
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow.add_node("Research Manager", research_manager_node)
|
||||||
|
workflow.add_node("Trader", trader_node)
|
||||||
|
workflow.add_node("Parallel Research", parallel_research)
|
||||||
|
workflow.add_node("Parallel Risk", parallel_risk)
|
||||||
|
workflow.add_node("Risk Judge", risk_manager_node)
|
||||||
|
|
||||||
|
# Parallel Analysts → Parallel Research → Manager → Trader → Parallel Risk → Judge → END
|
||||||
workflow.add_edge(START, "Parallel Analysts")
|
workflow.add_edge(START, "Parallel Analysts")
|
||||||
workflow.add_edge("Parallel Analysts", "Bull Researcher")
|
workflow.add_edge("Parallel Analysts", "Parallel Research")
|
||||||
|
workflow.add_edge("Parallel Research", "Research Manager")
|
||||||
|
workflow.add_edge("Research Manager", "Trader")
|
||||||
|
workflow.add_edge("Trader", "Parallel Risk")
|
||||||
|
workflow.add_edge("Parallel Risk", "Risk Judge")
|
||||||
|
workflow.add_edge("Risk Judge", END)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Sequential: START → Analyst 1 → ... → Analyst N → Bull Researcher
|
# --- Sequential mode ---
|
||||||
|
# Individual analyst nodes with tool-calling loops
|
||||||
|
# Bull/Bear debate with conditional routing
|
||||||
|
# Agg/Con/Neu risk debate with conditional routing
|
||||||
|
|
||||||
|
workflow.add_node("Bull Researcher", bull_researcher_node)
|
||||||
|
workflow.add_node("Bear Researcher", bear_researcher_node)
|
||||||
|
workflow.add_node("Research Manager", research_manager_node)
|
||||||
|
workflow.add_node("Trader", trader_node)
|
||||||
|
workflow.add_node("Aggressive Analyst", aggressive_analyst)
|
||||||
|
workflow.add_node("Neutral Analyst", neutral_analyst)
|
||||||
|
workflow.add_node("Conservative Analyst", conservative_analyst)
|
||||||
|
workflow.add_node("Risk Judge", risk_manager_node)
|
||||||
|
|
||||||
first_analyst = selected_analysts[0]
|
first_analyst = selected_analysts[0]
|
||||||
workflow.add_edge(START, f"{first_analyst.capitalize()} Analyst")
|
workflow.add_edge(START, f"{first_analyst.capitalize()} Analyst")
|
||||||
|
|
||||||
|
|
@ -164,51 +194,50 @@ class GraphSetup:
|
||||||
else:
|
else:
|
||||||
workflow.add_edge(current_clear, "Bull Researcher")
|
workflow.add_edge(current_clear, "Bull Researcher")
|
||||||
|
|
||||||
# Add remaining edges (same for both modes)
|
workflow.add_conditional_edges(
|
||||||
workflow.add_conditional_edges(
|
"Bull Researcher",
|
||||||
"Bull Researcher",
|
self.conditional_logic.should_continue_debate,
|
||||||
self.conditional_logic.should_continue_debate,
|
{
|
||||||
{
|
"Bear Researcher": "Bear Researcher",
|
||||||
"Bear Researcher": "Bear Researcher",
|
"Research Manager": "Research Manager",
|
||||||
"Research Manager": "Research Manager",
|
},
|
||||||
},
|
)
|
||||||
)
|
workflow.add_conditional_edges(
|
||||||
workflow.add_conditional_edges(
|
"Bear Researcher",
|
||||||
"Bear Researcher",
|
self.conditional_logic.should_continue_debate,
|
||||||
self.conditional_logic.should_continue_debate,
|
{
|
||||||
{
|
"Bull Researcher": "Bull Researcher",
|
||||||
"Bull Researcher": "Bull Researcher",
|
"Research Manager": "Research Manager",
|
||||||
"Research Manager": "Research Manager",
|
},
|
||||||
},
|
)
|
||||||
)
|
workflow.add_edge("Research Manager", "Trader")
|
||||||
workflow.add_edge("Research Manager", "Trader")
|
workflow.add_edge("Trader", "Aggressive Analyst")
|
||||||
workflow.add_edge("Trader", "Aggressive Analyst")
|
workflow.add_conditional_edges(
|
||||||
workflow.add_conditional_edges(
|
"Aggressive Analyst",
|
||||||
"Aggressive Analyst",
|
self.conditional_logic.should_continue_risk_analysis,
|
||||||
self.conditional_logic.should_continue_risk_analysis,
|
{
|
||||||
{
|
"Conservative Analyst": "Conservative Analyst",
|
||||||
"Conservative Analyst": "Conservative Analyst",
|
"Risk Judge": "Risk Judge",
|
||||||
"Risk Judge": "Risk Judge",
|
},
|
||||||
},
|
)
|
||||||
)
|
workflow.add_conditional_edges(
|
||||||
workflow.add_conditional_edges(
|
"Conservative Analyst",
|
||||||
"Conservative Analyst",
|
self.conditional_logic.should_continue_risk_analysis,
|
||||||
self.conditional_logic.should_continue_risk_analysis,
|
{
|
||||||
{
|
"Neutral Analyst": "Neutral Analyst",
|
||||||
"Neutral Analyst": "Neutral Analyst",
|
"Risk Judge": "Risk Judge",
|
||||||
"Risk Judge": "Risk Judge",
|
},
|
||||||
},
|
)
|
||||||
)
|
workflow.add_conditional_edges(
|
||||||
workflow.add_conditional_edges(
|
"Neutral Analyst",
|
||||||
"Neutral Analyst",
|
self.conditional_logic.should_continue_risk_analysis,
|
||||||
self.conditional_logic.should_continue_risk_analysis,
|
{
|
||||||
{
|
"Aggressive Analyst": "Aggressive Analyst",
|
||||||
"Aggressive Analyst": "Aggressive Analyst",
|
"Risk Judge": "Risk Judge",
|
||||||
"Risk Judge": "Risk Judge",
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
workflow.add_edge("Risk Judge", END)
|
workflow.add_edge("Risk Judge", END)
|
||||||
|
|
||||||
# Compile and return
|
# Compile and return
|
||||||
return workflow.compile()
|
return workflow.compile()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue