How the Portfolio Manager Aggregates Analyst Signals in AI Hedge Fund

The Portfolio Manager aggregates analyst signals by collecting raw signal-confidence pairs from the shared agent state and compressing them into a compact per-ticker map before passing them to the LLM for deterministic trading decisions.

In the virattt/ai-hedge-fund repository, the Portfolio Manager serves as the central orchestrator that transforms dispersed analyst outputs into actionable trading instructions. Understanding how the Portfolio Manager aggregates analyst signals reveals the deterministic pipeline that bridges quantitative analysis with LLM-based decision making. The entire workflow operates within src/agents/portfolio_manager.py, where raw signals from valuation, technical, and sentiment analysts are normalized into a structured payload.

Signal Aggregation Pipeline

The aggregation process follows a strict two-phase architecture designed to minimize token consumption while preserving signal fidelity.

Step 1: Collecting Raw Signals from Agent State

The Portfolio Manager begins by accessing the shared state object that persists across the agent workflow. Every specialized analyst—including valuation, technicals, and sentiment agents—writes its conclusions to state["data"]["analyst_signals"] as a dictionary mapping agent IDs to per-ticker payloads.

At the start of the portfolio_management_agent function, the manager extracts three critical elements from the state:

portfolio = state["data"]["portfolio"]
analyst_signals = state["data"]["analyst_signals"]
tickers = state["data"]["tickers"]

(source: src/agents/portfolio_manager.py, lines 28‑31)

Each entry in analyst_signals contains at minimum a signal field (e.g., "bullish", "bearish", or "neutral") and a confidence score representing the analyst's certainty.

Step 2: Compressing Signals by Ticker

The core aggregation logic iterates over every ticker and compacts the scattered analyst outputs into a hierarchical map. The manager explicitly filters out risk-management agents to maintain separation between analytical signals and risk constraints.

The implementation in portfolio_manager.py (lines 58‑64) constructs the final structure:

signals_by_ticker = {}
for ticker in tickers:
    ticker_signals = {}
    for agent, signals in analyst_signals.items():
        # Exclude risk-management agents from signal aggregation

        if not agent.startswith("risk_management_agent") and ticker in signals:
            sig = signals[ticker].get("signal")
            conf = signals[ticker].get("confidence")
            if sig is not None and conf is not None:
                ticker_signals[agent] = {"sig": sig, "conf": conf}
    signals_by_ticker[ticker] = ticker_signals

This produces a JSON-serializable structure like:

{
  "AAPL": {
    "valuation": {"sig": "bullish", "conf": 85},
    "technicals": {"sig": "neutral", "conf": 60},
    "sentiment": {"sig": "bearish", "conf": 70}
  },
  "TSLA": {
    "valuation": {"sig": "bullish", "conf": 72},
    "fundamentals": {"sig": "bullish", "conf": 65}
  }
}

Risk Management Data Separation

The aggregation pipeline maintains strict isolation between analytical signals and risk management constraints. While risk data resides in the same analyst_signals dictionary under keys prefixed with risk_management_agent, the Portfolio Manager retrieves this data separately for position sizing and limit checks.

According to the source code at lines 46‑48, risk-specific data—including current prices and position limits—is accessed via dedicated keys without being mixed into the analyst signal map intended for the LLM.

LLM Payload Construction

After aggregation, the Portfolio Manager further optimizes the payload through the _compact_signals helper function (defined at lines 60‑74). This utility trims empty agent entries and normalizes the data structure to reduce token usage.

The final payload is constructed within generate_trading_decision (lines 84‑88 and 107‑112):

compact_signals = _compact_signals({t: signals_by_ticker.get(t, {}) for t in tickers_for_llm})

prompt_data = {
    "signals": json.dumps(compact_signals, separators=(",", ":"), ensure_ascii=False),
    "allowed": json.dumps(compact_allowed, separators=(",", ":"), ensure_ascii=False),
}
prompt = template.invoke(prompt_data)

This compact serialization removes whitespace and unnecessary metadata, ensuring the LLM receives only the essential signal-confidence pairs required for trading decisions.

Summary

  • The Portfolio Manager reads from state["data"]["analyst_signals"] to access outputs from all analyst agents including valuation, technicals, and sentiment modules.
  • Aggregation excludes risk-management agents by filtering keys starting with risk_management_agent, keeping risk data separate from analytical signals.
  • The _compact_signals helper and JSON minification dramatically reduce token usage when transmitting data to the LLM via generate_trading_decision.
  • The final structure maps tickers to agents, with each agent containing sig (signal direction) and conf (confidence score) fields.
  • Source files involved: src/agents/portfolio_manager.py (main aggregation), src/agents/risk_manager.py (excluded risk data), and various analyst agents that populate the shared state.

Frequently Asked Questions

What data structure stores analyst signals in the AI Hedge Fund?

Analyst signals are stored in state["data"]["analyst_signals"], a nested dictionary where top-level keys represent agent IDs (e.g., "valuation", "technicals") and values contain per-ticker objects with signal and confidence fields. The Portfolio Manager accesses this structure at lines 28‑31 of src/agents/portfolio_manager.py.

Why does the Portfolio Manager exclude risk management signals from aggregation?

Risk management signals are excluded to maintain a clean separation between analytical opinions and constraint-based risk data. The Portfolio Manager specifically checks if not agent.startswith("risk_management_agent") during aggregation (lines 58‑64) because risk data—such as position limits and current prices—is handled separately for pre-trade validation rather than LLM decision-making.

How does the Portfolio Manager reduce token usage when sending data to the LLM?

The manager employs two optimization strategies: first, the _compact_signals helper function (lines 60‑74) removes empty agent entries and normalizes the structure; second, the generate_trading_decision function uses json.dumps with separators=(",", ":") to eliminate whitespace, creating a minified payload that preserves all signal-confidence data while minimizing token consumption.

Which source file contains the main aggregation logic for trading decisions?

The primary aggregation logic resides in src/agents/portfolio_manager.py, specifically within the portfolio_management_agent function (lines 28‑112). This file handles state retrieval, signal compression, risk data separation, and the final LLM prompt construction through generate_trading_decision.

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 →