Flow From Analyst Signals to Trading Decisions in the AI Hedge Fund

The AI Hedge Fund orchestrates a three-stage LangGraph workflow where analyst agents emit bullish/bearish signals, a risk manager calculates volatility-adjusted position limits, and a portfolio manager synthesizes both inputs into executable buy/sell orders.

The virattt/ai-hedge-fund repository implements an autonomous trading system that transforms raw market analysis into risk-managed portfolio actions. Understanding the flow from analyst signals to trading decisions reveals how the system balances aggressive alpha generation with strict capital preservation. This article traces the complete data pipeline from signal generation through position sizing to final order execution.

The Three-Layer Architecture

The system implements a directed graph workflow with three distinct logical layers:

  1. Analyst Agents – Multiple specialized agents (e.g., Technical Analyst, Valuation Analyst) examine market data and write structured signals to a shared state.
  2. Risk-Management Agent – Aggregates price histories, computes volatility metrics and correlation matrices, then derives per-ticker position-size limits.
  3. Portfolio-Management Agent – Consumes both analyst signals and risk constraints to determine feasible actions before invoking an LLM for the final decision.

This pipeline ensures that trading decisions are grounded in quantitative risk metrics rather than pure speculation.

Stage 1: Analyst Signal Generation

Each analyst operates as an independent node in the LangGraph workflow. Analysts are registered in src/utils/analysts.py within the ANALYST_CONFIG dictionary, and the get_analyst_nodes() function dynamically instantiates the selected agents.

Individual analysts fetch market data and write structured output to the shared state dictionary:

technical_analysis = {
    "signal": combined_signal["signal"],
    "confidence": round(combined_signal["confidence"] * 100),
    "metrics": normalize_pandas(combined_signal["metrics"]),
}
state["data"]["analyst_signals"][agent_id] = technical_analysis

Source: src/agents/technicals.py

Each signal contains a discrete classification (bullish, bearish, or neutral) and a confidence score. The agent_id serves as the key in state["data"]["analyst_signals"], allowing downstream nodes to aggregate perspectives across multiple analysts.

Stage 2: Risk-Managed Position Limits

After all analysts complete execution, the workflow transitions to the risk-management agent defined in src/agents/risk_manager.py. This node fetches historical prices via get_prices() from src/tools/api.py and performs quantitative risk calculations.

Volatility Adjustment

The agent calculates an inverse-volatility position limit to prevent overexposure to turbulent assets:

def calculate_volatility_adjusted_limit(annualized_volatility: float) -> float:
    # A simple inverse relationship: higher vol → smaller position %.

    # Clamp to a sensible range (0.5 % – 5 % of portfolio).

    base = 0.03  # 3 % default

    adjusted = base / (annualized_volatility * 10)   # scale with vol

    return max(0.005, min(adjusted, 0.05))

Source: src/agents/risk_manager.py

Correlation Penalty

If the portfolio contains multiple tickers, the agent constructs a correlation matrix from returns and computes a calculate_correlation_multiplier() to penalize highly correlated exposures. The final remaining position limit is stored back into the state:

state["data"]["analyst_signals"][risk_manager_id][ticker]["remaining_position_limit"]

This ensures that subsequent trading decisions respect both individual asset volatility and portfolio-level concentration risk.

Stage 3: Portfolio Management and Order Execution

The portfolio manager in src/agents/portfolio_manager.py represents the culmination of the workflow. It retrieves the remaining_position_limit for each ticker, converts dollar limits to max_shares using current prices, and compresses analyst signals into an LLM-friendly format.

Deterministic Constraints

Before invoking the language model, the system computes feasible actions through compute_allowed_actions():

allowed_actions_full = compute_allowed_actions(
    tickers, current_prices, max_shares, portfolio
)

# Pre‑fill pure “hold” decisions to avoid LLM calls

if set(aa.keys()) == {"hold"}:
    prefilled_decisions[t] = PortfolioDecision(
        action="hold", quantity=0, confidence=100.0,
        reasoning="No valid trade available"
    )

Source: src/agents/portfolio_manager.py

This deterministic constraint layer guarantees that the LLM never receives an impossible instruction (e.g., buying shares without available cash), significantly reducing hallucination risk.

Final Decision Generation

For tickers with multiple valid actions, generate_trading_decision() constructs a prompt containing the allowed actions and analyst consensus, then calls call_llm() to produce a PortfolioManagerOutput. The final JSON response includes the action (buy, sell, short, cover, or hold), quantity, and reasoning.

LangGraph Workflow Orchestration

The orchestration logic in src/main.py wires these components into a cohesive StateGraph:

def create_workflow(selected_analysts=None):
    workflow = StateGraph(AgentState)
    workflow.add_node("start_node", start)

    analyst_nodes = get_analyst_nodes()          # ← src/utils/analysts.py

    if selected_analysts is None:
        selected_analysts = list(analyst_nodes.keys())

    for analyst_key in selected_analysts:
        node_name, node_func = analyst_nodes[analyst_key]
        workflow.add_node(node_name, node_func)   # each analyst becomes a node

        workflow.add_edge("start_node", node_name)

    workflow.add_node("risk_management_agent", risk_management_agent)
    workflow.add_node("portfolio_manager", portfolio_management_agent)

    # Wire analysts → risk manager → portfolio manager

    for analyst_key in selected_analysts:
        node_name = analyst_nodes[analyst_key][0]
        workflow.add_edge(node_name, "risk_management_agent")
    workflow.add_edge("risk_management_agent", "portfolio_manager")
    workflow.add_edge("portfolio_manager", END)

    workflow.set_entry_point("start_node")
    return workflow

Source: src/main.py

The entry point run_hedge_fund() executes this graph and returns both the trading decisions and raw analyst signals for auditing:

return {
    "decisions": parse_hedge_fund_response(final_state["messages"][-1].content),
    "analyst_signals": final_state["data"]["analyst_signals"],
}

Summary

  • Modular Analyst System – Signals are generated independently and stored in state["data"]["analyst_signals"], allowing dynamic addition or removal of analysts via ANALYST_CONFIG.
  • Volatility-Adjusted Sizing – The risk manager enforces position limits inversely proportional to annualized volatility, with bounds between 0.5% and 5% of portfolio value.
  • Deterministic Guardrails – compute_allowed_actions() filters impossible trades before LLM invocation, pre-filling "hold" decisions when necessary.
  • Transparent State Management – The AgentState model in src/graph/state.py provides a single source of truth for signals, risk limits, and final decisions throughout the pipeline.

Frequently Asked Questions

How does the risk manager calculate position-size limits?

The risk manager derives limits through calculate_volatility_adjusted_limit() using an inverse relationship with annualized volatility—higher volatility results in smaller maximum positions. This base limit is then modulated by a correlation multiplier derived from the portfolio's returns correlation matrix.

What prevents the LLM from generating invalid trading orders?

The compute_allowed_actions() function in src/agents/portfolio_manager.py evaluates cash balances, margin requirements, and max_shares constraints to determine which actions (buy, sell, short, cover, hold) are feasible for each ticker. If only "hold" is possible, the system bypasses the LLM entirely and pre-fills the decision.

Where are analyst signals stored during workflow execution?

Analysts write signals to state["data"]["analyst_signals"][agent_id] within the shared AgentState object defined in src/graph/state.py. The risk manager additionally writes position limits under the risk_manager_id key within the same dictionary.

How can I add a custom analyst to the pipeline?

Register the new analyst in ANALYST_CONFIG within src/utils/analysts.py and implement the agent function to accept AgentState and return updated state. The create_workflow() function in src/main.py automatically includes new entries via get_analyst_nodes() without requiring changes to the graph wiring logic.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →