How to Optimize LangGraph Propagation for Large-Scale Batch Analysis in TradingAgents-CN
Optimize LangGraph propagation for large-scale batch analysis by reusing compiled StateGraph instances across parallel worker processes, replacing single-symbol API calls with bulk data endpoints, and streaming node-level updates to minimize memory overhead.
The TradingAgents-CN repository implements a multi-agent trading analysis system using LangGraph to orchestrate market analysts, news processors, and risk managers. When scaling from single-symbol analysis to thousands of securities, the default sequential propagation pattern in tradingagents/graph/trading_graph.py becomes a significant bottleneck. This guide explains how to optimize LangGraph propagation for large-scale batch analysis while preserving the existing agent architecture and state management logic.
Understanding the Current Propagation Pipeline
State Initialization and Graph Execution
The core execution flow resides in tradingagents/graph/trading_graph.py, where the TradingAgentsGraph.propagate method orchestrates the LangGraph runtime. The process begins with Propagator.create_initial_state in tradingagents/graph/propagation.py (lines 22‑33), which constructs the initial AgentState containing messages, company metadata, trading dates, and debate states.
The Propagator.get_graph_args method (lines 54‑68) determines the streaming configuration. It selects between stream_mode="updates" for per-node progress tracking and stream_mode="values" for full state snapshots, while configuring the recursion limit to prevent infinite loops. The self.graph.stream(init_agent_state, **args) call in trading_graph.py (lines 15‑19, 31‑37) iterates over LangGraph chunks, with each chunk representing either a partial state update or a complete state dictionary depending on the selected mode.
Performance Bottlenecks in Sequential Processing
When processing large batches, the current implementation exhibits three critical bottlenecks:
- Repeated state allocation – While
GraphSetup.setup_graphcompiles the graph once perTradingAgentsGraphinstance, theAgentStateis rebuilt for every symbol, incurring repeated memory allocation and validation overhead. - Sequential LLM execution – Each node's LLM request blocks subsequent symbols, preventing concurrent utilization of API quotas across the batch.
- Serial data fetching – Market data, news, and fundamentals nodes currently invoke provider APIs (such as Tushare or AkShare) on a per-symbol basis, despite these services offering bulk endpoints capable of handling hundreds of symbols in a single request.
Architectural Strategies for Batch Optimization
Reuse Compiled Graph Instances
The StateGraph object produced by GraphSetup.setup_graph is immutable after compilation. Instantiate a single TradingAgentsGraph (or at least its underlying graph attribute) at the batch runner level and reuse it across all symbols. This eliminates the compilation overhead and ensures consistent node configurations throughout the batch.
Parallelize Symbol Processing
Deploy a process pool using concurrent.futures.ProcessPoolExecutor to execute propagate calls concurrently. Each worker process receives its own copy of the compiled graph (deep-copy is inexpensive because the compiled StateGraph is immutable). This architecture sidesteps Python's Global Interpreter Lock (GIL) and maximizes CPU utilization for post-processing tasks such as parsing LLM responses and serializing results.
Implement Bulk Data Fetching
Replace per-symbol tool implementations with batch-aware variants. In web/utils/api_checker.py (or equivalent tool modules), implement functions like get_stock_market_data_batch that accept a list of symbols and return a dictionary mapping symbols to their respective data. For example, utilize TushareProvider.get_realtime_quotes_batch to fetch hundreds of quotes in a single HTTP request, then normalize the response into the standard schema expected by the analyst nodes.
Optimize Stream Modes and Callbacks
Configure Propagator.get_graph_args to use stream_mode="updates" when the client only requires progress tracking. This mode emits {node_name: partial_state} dictionaries rather than full state snapshots, significantly reducing memory overhead and network transfer when streaming results to a monitoring UI. Ensure batch callers provide a progress_callback function to capture these incremental updates without blocking the main execution thread.
Implementation: Production Batch Runner
Parallel Worker Setup
The following skeleton demonstrates a production-ready batch runner that implements the architectural strategies above. Save this as scripts/run_batch_analysis.py:
# scripts/run_batch_analysis.py
import itertools
import json
import os
from concurrent.futures import ProcessPoolExecutor, as_completed
from typing import List, Tuple
from tradingagents.graph.trading_graph import TradingAgentsGraph
from tradingagents.default_config import DEFAULT_CONFIG
# --------------------------------------------------------------------
# Helper: progress callback that emits a tiny JSON line (can be piped to a UI)
def _progress(symbol: str, node: str, elapsed: float):
print(json.dumps({"symbol": symbol, "node": node, "elapsed": elapsed}))
# --------------------------------------------------------------------
# Worker function executed in a separate process
def _process_one(args: Tuple[str, str]) -> dict:
symbol, trade_date = args
# One instance per process (graph compiled once per process)
tg = TradingAgentsGraph(debug=False, config=DEFAULT_CONFIG)
final_state, decision = tg.propagate(
company_name=symbol,
trade_date=trade_date,
progress_callback=lambda chunk: _progress(symbol, list(chunk.keys())[0], 0),
)
# Return only the parts you need (e.g. decision + timing)
return {
"symbol": symbol,
"date": trade_date,
"decision": decision,
"performance": final_state.get("performance_metrics", {}),
}
# --------------------------------------------------------------------
def run_batch(pairs: List[Tuple[str, str]], workers: int = 8) -> List[dict]:
results = []
with ProcessPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(_process_one, pair) for pair in pairs}
for f in as_completed(futures):
results.append(f.result())
return results
# --------------------------------------------------------------------
if __name__ == "__main__":
# Example: read a CSV of symbols + dates
import csv
batch_file = os.getenv("BATCH_FILE", "data/batch_symbols.csv")
pairs = []
with open(batch_file, newline="") as cf:
for row in csv.DictReader(cf):
pairs.append((row["symbol"], row["trade_date"]))
# Run with 8 parallel workers (tune to your CPU / rate‑limit)
all_results = run_batch(pairs, workers=8)
# Persist results
out_path = "batch_results.json"
with open(out_path, "w", encoding="utf-8") as jf:
json.dump(all_results, jf, ensure_ascii=False, indent=2)
print(f"✅ Batch completed – {len(all_results)} symbols saved to {out_path}")
Key implementation details:
- The graph is instantiated once per worker process (
TradingAgentsGraph) to leverage the immutable compiledStateGraphwhile avoiding cross-process sharing issues. - The
progress_callbackreceives partial chunks fromstream_mode="updates"and forwards minimal JSON lines, enabling real-time monitoring without blocking execution. - Workers return only essential data (decision objects and performance metrics) rather than the complete
AgentStatedictionary, minimizing serialization overhead when aggregating results.
Bulk-Aware Tool Nodes
To eliminate the N×M API call explosion, refactor tool implementations to accept symbol lists. The following example illustrates a batch-capable market data fetcher suitable for web/utils/api_checker.py:
# web/utils/api_checker.py (excerpt)
from typing import List, Dict
from tradingagents.utils.logging_init import get_logger
logger = get_logger("api")
def get_stock_market_data_batch(symbols: List[str]) -> Dict[str, dict]:
"""
Bulk version of ``get_stock_market_data_unified``.
Returns a mapping {symbol: market_data_dict}.
"""
# Example using Tushare batch endpoint
from tradingagents.dataflows.providers.china.tushare import TushareProvider
provider = TushareProvider()
raw = provider.get_realtime_quotes_batch(symbols) # <-- batch API
result = {}
for sym, data in raw.items():
# Normalise to the same schema as the single‑symbol version
result[sym] = {
"price": data["close"],
"volume": data["vol"],
"high": data["high"],
"low": data["low"],
}
logger.info(f"🔧 Fetched market data for {len(symbols)} symbols in bulk")
return result
Replace the original single-symbol tool (get_stock_market_data_unified) with this batch variant in the tool_nodes["market"] list inside GraphSetup.setup_graph (lines 92‑97 of tradingagents/graph/setup.py). This modification allows the graph to fetch data for hundreds of symbols in a single HTTP request, reducing network latency by orders of magnitude while maintaining compatibility with existing analyst node schemas.
Why This Architecture Scales
LangGraph's asynchronous node execution allows multiple LLM requests to run concurrently within the same event loop when processing a batch of symbols. By feeding the graph a batch of symbols rather than iterating sequentially, you maximize utilization of LLM provider rate limits without blocking I/O.
The immutable compiled graph pattern eliminates the overhead of rebuilding the DAG for each symbol. Since GraphSetup.setup_graph produces a compiled StateGraph that is immutable after compilation, deep-copying it across worker processes is inexpensive and thread-safe, ensuring consistent execution plans across the entire batch.
Bulk data endpoints reduce the N×M API call explosion (N symbols × M data sources) into single HTTP requests. Providers like Tushare and AkShare expose batch methods such as get_realtime_quotes_batch that return hundreds of records in one round-trip, cutting network latency by 90% or more compared to sequential fetching.
Process-level parallelism sidesteps Python's Global Interpreter Lock (GIL) for CPU-intensive post-processing tasks such as decoding LLM responses, normalizing data schemas, and persisting results to databases or JSON files. This ensures that aggregation logic does not become the bottleneck when scaling to thousands of symbols.
Summary
- Reuse compiled graphs: Instantiate
TradingAgentsGraphonce per worker process to avoid repeated compilation overhead intradingagents/graph/setup.py. - Parallelize execution: Use
ProcessPoolExecutorto runpropagateconcurrently across symbols, with each worker maintaining its own graph instance. - Adopt bulk APIs: Replace per-symbol tool functions with batch variants like
get_stock_market_data_batchinweb/utils/api_checker.pyto minimize HTTP round-trips. - Optimize streaming: Configure
Propagator.get_graph_argsto usestream_mode="updates"for efficient progress tracking without full state serialization. - Minimize payloads: Return only decision objects and
performance_metricsfrom workers, avoiding the serialization overhead of completeAgentStatedictionaries.
Frequently Asked Questions
How does reusing the compiled LangGraph instance improve performance?
Reusing the compiled StateGraph eliminates the overhead of DAG compilation and node registration for every symbol. Since GraphSetup.setup_graph in tradingagents/graph/setup.py produces an immutable compiled graph, instantiating TradingAgentsGraph once per worker process allows thousands of symbols to share the same execution plan without repeated memory allocation or validation logic.
What is the difference between updates and values stream modes in batch processing?
The updates mode returns {node_name: partial_state} dictionaries representing only the nodes that just executed, while values returns the complete AgentState after every node. For large-scale batch analysis, updates mode significantly reduces memory overhead and network transfer when streaming results to monitoring systems, as implemented in Propagator.get_graph_args (lines 54‑68 of tradingagents/graph/propagation.py).
Can I use threading instead of process pools for parallelization?
While threading works for I/O-bound operations, LangGraph's execution involves CPU-intensive tasks such as prompt templating, JSON parsing, and state validation that contend with Python's Global Interpreter Lock (GIL). Using ProcessPoolExecutor as demonstrated in scripts/run_batch_analysis.py sidesteps the GIL, allowing true parallelism for both LLM network I/O and CPU-bound post-processing without risking state corruption across workers.
How do I handle rate limits when processing thousands of symbols concurrently?
Implement an asynchronous semaphore within the worker processes or tune the max_workers parameter in ProcessPoolExecutor to match your LLM provider's rate limits. Additionally, utilize the bulk data fetching patterns in web/utils/api_checker.py to reduce API calls from N per symbol to single batch requests. Monitor the performance_metrics returned by propagate to identify throttling and adjust worker counts or implement exponential backoff in the progress callback.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →