# How the Portfolio Management Agent Makes Trading Decisions in virattt/ai-hedge-fund

> Discover how the virattt/ai-hedge-fund portfolio management agent makes trading decisions using AI language models constrained by risk and position limits.

- Repository: [Virat Singh/ai-hedge-fund](https://github.com/virattt/ai-hedge-fund)
- Tags: deep-dive
- Published: 2026-03-09

---

**The portfolio management agent makes trading decisions by enforcing deterministic risk constraints against cash reserves and position limits, then delegating action selection to a language model operating within those bounded parameters.**

The `portfolio_management_agent` serves as the final execution layer in the `virattt/ai-hedge-fund` repository, transforming multi-source analyst insights into concrete portfolio actions. Operating within the LangGraph-based agent pipeline, this component ensures every trading decision adheres to strict financial boundaries while leveraging AI reasoning for signal interpretation. Understanding how the portfolio management agent makes trading decisions reveals a hybrid architecture that balances computational precision with intelligent strategy selection.

## The Decision-Making Pipeline

The agent processes trading decisions through a deterministic preprocessing phase followed by LLM-assisted action selection. This two-stage approach guarantees that all trades respect hard financial constraints before any AI reasoning occurs.

### Extracting State Data and Analyst Signals

The agent begins by unmarshaling the shared `AgentState` to access current market and portfolio conditions. In [`src/agents/portfolio_manager.py`](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/portfolio_manager.py#L28-L30), the extraction logic pulls three critical components:

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

```

The **portfolio** object contains cash balances, current positions, margin requirements, and equity values. The **analyst_signals** dictionary aggregates recommendations from upstream agents—fundamentals, technicals, sentiment, and risk management—each providing ticker-specific insights. This raw state serves as the foundation for all subsequent constraint calculations.

### Integrating Risk Management Constraints

Before any trading logic executes, the agent queries the risk manager outputs to establish position boundaries. For every ticker, the system retrieves the `remaining_position_limit` and `current_price` from the risk management agent's signal data (L36-L54). These values determine the **max_shares** threshold that limits how many shares the portfolio can hold or short for a specific equity.

The agent compresses complex analyst signals into a lightweight `{signal, confidence}` map per ticker (L56-L64), discarding empty entries to minimize token usage during LLM prompting. This compression step ensures only actionable intelligence reaches the decision engine.

### Computing Deterministic Action Boundaries

The `compute_allowed_actions` function (L96-L158) implements the core financial logic that defines feasible trading actions. This deterministic evaluator considers:

- **Cash availability** against current prices
- **Existing long and short positions**
- **Margin requirements** and used margin capacity
- **Maximum share limits** imposed by risk management

The function returns a dictionary mapping each ticker to its **allowed actions**—`buy`, `sell`, `short`, `cover`, or `hold`—along with the maximum quantity permissible for each action. This creates a bounded action space that prevents the LLM from suggesting trades that violate liquidity or risk parameters.

## LLM Integration and Action Selection

Once deterministic constraints are established, the agent leverages a language model to select optimal actions from the allowed set. This phase combines hard computational limits with soft AI reasoning.

### Optimizing with Prefilled Hold Decisions

To reduce token consumption and ensure system stability, the agent implements a short-circuit optimization (L90-L102). If `compute_allowed_actions` returns only `"hold"` for a ticker—indicating insufficient cash, full positions, or risk limit violations—the agent immediately creates a `PortfolioDecision` with `action="hold"` and bypasses the LLM entirely.

This **prefill strategy** guarantees valid responses for dead-end cases while conserving computational resources for scenarios requiring actual trading decisions.

### Prompt Engineering for Constrained Trading

For tickers with multiple valid actions, the agent constructs a minimal prompt payload containing:

- **`compact_signals`** – Reduced analyst recommendations with signal type and confidence scores
- **`compact_allowed`** – The deterministic action limits and maximum quantities

The `ChatPromptTemplate` (L112-L132) instructs the LLM: *"You are a portfolio manager. Choose one allowed action per ticker, respect the max quantity, keep reasoning ≤ 100 chars, and return pure JSON."* This structured prompt enforces strict output formatting while providing the AI with complete visibility into both analyst sentiment and trading boundaries.

### Handling LLM Failures with Default Factories

The `call_llm` utility (L51-L57) invokes the language model with a **default factory** pattern that automatically assigns `"hold"` actions to any ticker the LLM fails to process. This fail-safe mechanism expects a `PortfolioManagerOutput` Pydantic model but ensures the pipeline never crashes due to model timeouts or malformed responses, defaulting to neutral positions when AI reasoning is unavailable.

## Output Validation and Merging

The final stage aggregates decisions from both deterministic prefill and LLM generation. The agent merges pre-filled `"hold"` decisions with AI-selected actions to produce a complete decision map containing valid entries for every requested ticker (L59-L63).

Each decision conforms to the `PortfolioDecision` Pydantic model (L13-L15), structured as:

```json
{
  "action": "buy|sell|short|cover|hold",
  "quantity": 100,
  "confidence": 85,
  "reasoning": "Concise explanation ≤100 chars"
}

```

The aggregated decisions serialize to JSON and emit as a `HumanMessage` (L79-L88), optionally displaying debug information via `show_agent_reasoning` when `metadata["show_reasoning"]` is enabled.

## Practical Implementation Examples

### Invoking the Portfolio Manager Agent

The following example demonstrates minimal state initialization and agent execution:

```python
from src.graph.state import AgentState
from src.agents.portfolio_manager import portfolio_management_agent

state: AgentState = {
    "messages": [],
    "data": {
        "portfolio": {
            "cash": 100_000.0,
            "positions": {"AAPL": {"long": 50, "short": 0}},
            "margin_requirement": 0.5,
            "margin_used": 0.0,
            "equity": 100_000.0,
        },
        "analyst_signals": {
            "risk_management_agent": {
                "AAPL": {"remaining_position_limit": 200_000, "current_price": 150.0}
            },
            "fundamentals": {"AAPL": {"signal": "buy", "confidence": 80}},
            "technicals": {"AAPL": {"signal": "hold", "confidence": 60}},
        },
        "tickers": ["AAPL"],
    },
    "metadata": {"show_reasoning": True},
}

output = portfolio_management_agent(state)
print(output["messages"][-1].content)

```

### Using Deterministic Constraints Directly

Developers can access the constraint calculation logic independently via `compute_allowed_actions`:

```python
from src.agents.portfolio_manager import compute_allowed_actions

allowed = compute_allowed_actions(
    tickers=["AAPL", "TSLA"],
    current_prices={"AAPL": 150.0, "TSLA": 720.0},
    max_shares={"AAPL": 300, "TSLA": 50},
    portfolio={
        "cash": 50000,
        "positions": {"AAPL": {"long": 20}, "TSLA": {"short": 10}},
        "margin_requirement": 0.5,
        "margin_used": 0,
        "equity": 60000,
    },
)

# Returns: {'AAPL': {'buy': 333, 'sell': 20, 'hold': 0}, 'TSLA': {'short': 10, 'hold': 0}}

```

## Summary

- The **portfolio management agent** operates as the final decision layer in the `virattt/ai-hedge-fund` pipeline, bridging analyst signals and trade execution.
- **Deterministic constraints** calculated via `compute_allowed_actions` enforce cash limits, margin requirements, and position boundaries before AI reasoning occurs.
- **Prefilled hold decisions** optimize token usage by bypassing the LLM for tickers with no valid trading actions.
- The **LLM prompt** includes compressed analyst signals and action boundaries, ensuring the model selects only feasible trades with reasoning limited to 100 characters.
- **Default factory fallbacks** guarantee system reliability by defaulting to `"hold"` actions when the LLM fails to respond.
- All decisions conform to the **PortfolioDecision** schema, producing structured JSON output suitable for downstream execution systems.

## Frequently Asked Questions

### What inputs does the portfolio management agent require to make trading decisions?

The agent requires a complete `AgentState` containing three core data structures: the **portfolio** dictionary (cash, positions, margin status), **analyst_signals** from upstream agents including risk management (position limits and current prices), and the list of **tickers** to evaluate. These inputs are extracted in [`src/agents/portfolio_manager.py`](https://github.com/virattt/ai-hedge-fund/blob/main/src/agents/portfolio_manager.py#L28-L30) and used to compute deterministic constraints before LLM processing.

### How does the agent prevent trades that violate risk limits?

The `compute_allowed_actions` function (L96-L158) enforces hard financial constraints by calculating maximum allowable shares based on available cash, existing positions, margin requirements, and risk manager limits. This function returns a bounded set of allowed actions (`buy`, `sell`, `short`, `cover`, `hold`) with maximum quantities, which the LLM cannot exceed when selecting trades.

### What happens if the language model fails to return a valid decision?

The `call_llm` utility implements a **default factory** pattern that automatically assigns `"hold"` actions to any ticker the LLM fails to process (L51-L57). This ensures the pipeline continues execution without crashes, defaulting to neutral positions when AI reasoning is unavailable or returns malformed output.

### How does the agent handle multiple conflicting analyst signals?

The agent compresses all analyst signals into a simplified `{signal, confidence}` format per ticker (L56-L64), presenting the LLM with a unified view of recommendations without predetermined weighting. The language model interprets these signals within the context of deterministic constraints, effectively acting as the arbiter between conflicting bullish and bearish recommendations from different analytical agents.