How the Portfolio Management Agent Makes Trading Decisions in virattt/ai-hedge-fund
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, the extraction logic pulls three critical components:
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 scorescompact_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:
{
"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:
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:
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-fundpipeline, bridging analyst signals and trade execution. - Deterministic constraints calculated via
compute_allowed_actionsenforce 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 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.
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 →