agents — AI Investor Persona Agents
AI Investor Persona Agents
19 independent AI agents embodying famous investors' philosophies, plus a risk manager and Portfolio Tracking & Performance Metrics" href="../backtesting_doc/portfolio/index.html">portfolio manager that aggregate signals into trading decisions.
🎯 Why This Module Exists
Without this module, the system would be a single monolithic trading algorithm with one perspective — a brittle approach that fails whenever market conditions shift away from its narrow worldview. The agents module exists because investment decision-making is inherently multi-perspective: different market conditions favor different philosophies. By encoding 19 distinct investor personas (Warren Buffett’s value focus, Cathie Wood’s innovation thesis, Michael Burry’s contrarian analysis, etc.) as independent AI agents, the system creates an ensemble of diverse viewpoints. Each agent independently analyzes the same stock data through its unique philosophical lens, producing a signal (bullish/bearish/neutral with confidence). This parallel, independent analysis mirrors how real hedge funds operate: analysts research independently, then an investment committee (here, the risk + portfolio managers) synthesizes their views. The module is designed for maximum extensibility — adding a new investor persona requires only creating a new Python file with a system prompt and registering it in analysts.py. The ANALYST_CONFIG dictionary in src/utils/analysts.py serves as the single source of truth for all agent metadata, display names, and function references.
💡 Real-world analogy
Think of a panel of expert judges at a cooking competition. Each judge (agent) evaluates the same dish (stock) through their unique expertise and taste preferences (investing philosophy). They score independently — one judge focuses on technique, another on flavor balance, another on innovation. No judge sees the others’ scores until all have submitted. Then the head judge (portfolio manager) weighs their opinions, checks for consensus or strong disagreement, and makes the final decision. The risk manager acts as the health inspector — regardless of how delicious the dish is, it still has to pass safety standards before being served.
LangGraph Nodes Fan-Out Parallel Persona-Based Prompts
Architecture
flowchart LR
subgraph Input["Entry Point"]
start_node["start_node"]
AgentState["AgentState\n(tickers, dates, portfolio)"]
end
subgraph Personas["Investor Personas (13)"]
buffett["warren_buffett"]
graham["ben_graham"]
munger["charlie_munger"]
ackman["bill_ackman"]
wood["cathie_wood"]
burry["michael_burry"]
pabrai["mohnish_pabrai"]
taleb["nassim_taleb"]
lynch["peter_lynch"]
fisher["phil_fisher"]
jhunjhunwala["rakesh_jhunjhunwala"]
druckenmiller["stanley_druckenmiller"]
growth["growth_agent"]
end
subgraph Analysis["Quantitative Analysis (6)"]
fundamentals["fundamentals"]
technicals["technicals"]
sentiment["sentiment"]
valuation["valuation"]
news_sentiment["news_sentiment"]
damodaran["aswath_damodaran"]
end
subgraph Management["Decision Making"]
risk_manager["risk_management_agent"]
portfolio_manager["portfolio_manager"]
end
AgentState --> start_node
start_node -->|"fan-out"| buffett
start_node -->|"fan-out"| graham
start_node -->|"fan-out"| munger
start_node -->|"fan-out"| ackman
start_node -->|"fan-out"| wood
start_node -->|"fan-out"| burry
start_node -->|"fan-out"| pabrai
start_node -->|"fan-out"| taleb
start_node -->|"fan-out"| lynch
start_node -->|"fan-out"| fisher
start_node -->|"fan-out"| jhunjhunwala
start_node -->|"fan-out"| druckenmiller
start_node -->|"fan-out"| growth
start_node -->|"fan-out"| fundamentals
start_node -->|"fan-out"| technicals
start_node -->|"fan-out"| sentiment
start_node -->|"fan-out"| valuation
start_node -->|"fan-out"| news_sentiment
start_node -->|"fan-out"| damodaran
buffett -->|"signal"| risk_manager
graham -->|"signal"| risk_manager
munger -->|"signal"| risk_manager
ackman -->|"signal"| risk_manager
wood -->|"signal"| risk_manager
burry -->|"signal"| risk_manager
pabrai -->|"signal"| risk_manager
taleb -->|"signal"| risk_manager
lynch -->|"signal"| risk_manager
fisher -->|"signal"| risk_manager
jhunjhunwala -->|"signal"| risk_manager
druckenmiller -->|"signal"| risk_manager
growth -->|"signal"| risk_manager
fundamentals -->|"signal"| risk_manager
technicals -->|"signal"| risk_manager
sentiment -->|"signal"| risk_manager
valuation -->|"signal"| risk_manager
news_sentiment -->|"signal"| risk_manager
damodaran -->|"signal"| risk_manager
risk_manager -->|"risk-adjusted limits"| portfolio_manager
Data Flow
sequenceDiagram
participant S as AgentState
participant A as Investor Agent (e.g. Buffett)
participant API as Financial Data API
participant LLM as LLM (GPT-4 / Groq)
participant RM as risk_management_agent
participant PM as portfolio_manager
S->>A: Dispatch with tickers + date range
A->>API: get_financial_metrics(ticker, end_date)
API-->>A: Financial metrics (ROE, margins, debt)
A->>API: search_line_items(ticker, line_items)
API-->>A: Income, balance sheet, cash flow data
A->>API: get_market_cap(ticker, end_date)
API-->>A: Current market capitalization
A->>A: Analyze fundamentals + build persona prompt
A->>LLM: System prompt (persona) + financial data
LLM-->>A: JSON: signal, confidence, reasoning
A->>S: Write to state.data.analyst_signals[agent_id]
Note over S: All 19 agents converge
S->>RM: All analyst_signals + portfolio state
RM->>API: get_prices(ticker) for volatility calc
RM->>RM: Calculate volatility + correlation limits
RM->>S: Write risk_analysis with position limits
S->>PM: analyst_signals + risk limits
PM->>PM: Compute allowed actions per ticker
PM->>LLM: Compressed signals + allowed actions
LLM-->>PM: Trading decisions (action, qty, confidence)
PM->>S: Final portfolio decisions
🚶 Step-by-Step Narrated Walkthrough
Follow a single Warren Buffett agent analyzing AAPL as it flows through the entire agents pipeline:
- State Initialization —
AgentStateis created withtickers=["AAPL"], a date range, an emptyanalyst_signalsdict, and the current portfolio holdings. This shared state object is the only communication channel between agents. - Fan-Out Dispatch — LangGraph dispatches all 19 agents in parallel from
start_node. Each agent receives the sameAgentStatereference but writes to its own key inanalyst_signals. - Financial Metrics Fetch —
warren_buffett_agentcallsget_financial_metrics("AAPL", end_date, period="ttm", limit=10)to retrieve trailing-twelve-month fundamentals: ROE, debt ratios, profit margins, current ratio. - Line Items Retrieval — The agent calls
search_line_items()for 12 specific items: capital expenditure, depreciation, net income, outstanding shares, total assets/liabilities, shareholders’ equity, dividends, share buybacks, gross profit, revenue, and free cash flow. - Market Cap Lookup —
get_market_cap("AAPL", end_date)returns current market capitalization for intrinsic value comparison. - Multi-Factor Analysis — The agent runs seven analysis functions sequentially:
analyze_fundamentals,analyze_consistency,analyze_moat,analyze_pricing_power,analyze_book_value_growth,analyze_management_quality, andcalculate_intrinsic_value. Each returns a score and detailed breakdown. - Prompt Construction — A system prompt encodes Buffett’s value investing philosophy (moat analysis, margin of safety, management quality, circle of competence). The human message contains the raw analysis data and scores for the LLM to interpret.
- LLM Invocation —
call_llm()sends the prompt to GPT-4 withWarrenBuffettSignalas the Pydantic output model, ensuring structured JSON:{"signal": "bullish", "confidence": 75, "reasoning": "..."}. - Signal Storage — The parsed signal is written to
state["data"]["analyst_signals"]["warren_buffett_agent"]. The agent returns updated state with its message appended. - Fan-In Convergence — Once all 19 agents complete, LangGraph routes to
risk_management_agent. The risk manager fetches price data for each ticker and calculates volatility metrics (daily vol, annualized vol, percentile rank) and cross-ticker correlation matrices. - Risk Assessment — For each ticker, the risk manager computes a volatility-adjusted position limit (low vol: up to 25%, high vol: down to 5%) multiplied by a correlation adjustment factor (high correlation reduces limits to prevent concentrated bets). The result is a dollar-denominated
remaining_position_limit. - Portfolio Decision —
portfolio_management_agentreads all analyst signals and risk limits, computes deterministic allowed actions per ticker (buy/sell/short/cover/hold with max quantities), then sends a compact prompt to the LLM. The LLM returnsPortfolioDecisionobjects with action, quantity, confidence, and reasoning for each ticker.
🏗️ Design Decisions
| Decision | Choice Made | Alternatives Considered | Why This Approach |
|---|---|---|---|
| Agent granularity | One Python file per investor | Multi-agent classes, config-driven templates | Maximum readability — each file is self-documenting with its own analysis functions and system prompt. New contributors can understand a single agent without reading framework code. |
| Agent interface | Function returning state dict | Class with methods, abstract base class | Simpler for LangGraph node registration. No inheritance complexity — each agent is a pure function (AgentState, str) -> dict. The ANALYST_CONFIG dict maps keys to functions directly. |
| Persona encoding | LLM system prompt | Hardcoded scoring rules, weighted factor models | LLMs can reason about complex, qualitative philosophies (e.g., “circle of competence”) that are nearly impossible to encode as numerical rules. The system prompt approach preserves nuance. |
| Signal format | Pydantic model: signal + confidence + reasoning | Numerical score only, unstructured text | Structured output via Pydantic ensures type safety. The reasoning field provides transparency for debugging and audit trails. Confidence as 0–100 int avoids floating-point ambiguity. |
| Data passing | Shared AgentState dict |
Direct function calls between agents, message queues | Agents stay fully independent with no coupling. Each writes to its own key in analyst_signals. The risk and portfolio managers read all keys, creating a clean fan-out/fan-in topology. |
| Risk calculation | Volatility + correlation-adjusted position limits | Fixed percentage limits, VaR-based | Volatility-adjusted limits automatically scale down exposure for risky stocks. Correlation adjustment prevents concentrated bets in correlated assets. All computed deterministically before the LLM step. |
🔬 Algorithm Spotlight
Signal Aggregation & Risk-Adjusted Position Sizing
The Problem: Given 19 independent agent signals (bullish/bearish/neutral with varying confidence levels) and market data for multiple tickers, determine safe position sizes that account for both individual stock risk and portfolio-level correlation risk.
The Approach: The risk manager operates in three phases. First, it fetches price history for all tickers (including existing portfolio positions) and computes volatility metrics: daily standard deviation of returns, annualized volatility (daily vol × √252), and a percentile rank comparing recent volatility to historical rolling 30-day windows. Second, it builds a cross-ticker correlation matrix from aligned daily returns and computes an average correlation for each ticker against actively held positions. Third, it combines these into a position limit: a base 20% allocation is multiplied by a volatility factor (1.25× for low vol <15%, down to 0.50× for extreme vol >50%) and then by a correlation factor (0.70× for highly correlated assets ≥0.8, up to 1.10× for uncorrelated assets <0.2).
Why This Algorithm: Simple VaR calculations would require distributional assumptions. Fixed percentages ignore market conditions entirely. This tiered approach is interpretable, deterministic, and adapts to both individual asset risk and portfolio concentration — and all computation happens before the LLM step, so position limits are never hallucinated.
graph TD
A["Fetch prices for all tickers"] --> B["Calculate daily returns"]
B --> C{"Sufficient data?"}
C -->|"Yes"| D["Compute annualized volatility"]
C -->|"No"| E["Use default 5% daily vol"]
D --> F["Build correlation matrix"]
E --> F
F --> G{"Multiple tickers?"}
G -->|"Yes"| H["Compute avg correlation with active positions"]
G -->|"No"| I["Correlation multiplier = 1.0"]
H --> J["Volatility-adjusted limit pct"]
I --> J
J --> K["Combined limit = base x vol_factor x corr_factor"]
K --> L["Position limit in dollars"]
L --> M["Remaining limit = limit - current exposure"]
Trace through an example
Given: Portfolio value = $100,000. AAPL annualized volatility = 22% (medium). Average correlation with existing positions = 0.35 (low-moderate). Current AAPL exposure = $5,000.
Step 1: Volatility factor: 22% falls in the 15–30% range, so vol_multiplier = 1.0 - (0.22 - 0.15) * 0.5 = 0.965
Step 2: Correlation factor: 0.35 falls in the 0.2–0.4 range, so corr_multiplier = 1.05
Step 3: Combined limit = 0.20 * 0.965 * 1.05 = 20.3% of portfolio = $20,265
Step 4: Remaining limit = $20,265 - $5,000 = $15,265, capped by available cash
Result: The portfolio manager can buy up to $15,265 worth of AAPL. A naive fixed-percentage approach would allow $20,000 regardless of volatility or correlation.
⚠️ What Could Go Wrong
| Failure Mode | Trigger | Detection | Recovery / Mitigation |
|---|---|---|---|
| LLM API timeout | OpenAI/Groq provider outage or rate limiting | HTTP timeout in call_llm() |
Agent returns empty signal via default_factory; remaining agents continue independently. Portfolio manager treats missing signals as abstentions. |
| Invalid JSON response | LLM hallucinating malformed output despite Pydantic schema | Pydantic validation error on WarrenBuffettSignal parse |
call_llm() retries with structured output enforcement. On persistent failure, default_factory returns a safe default (hold, confidence=0). |
| Financial data unavailable | API rate limit (HTTP 429) or ticker not found | Empty response from get_financial_metrics() or get_prices() |
Risk manager falls back to conservative defaults: 5% daily volatility, 100th percentile risk. Agent proceeds with partial data and lower confidence. |
| All agents agree (unanimous signal) | Extreme market conditions (crash or euphoria) | All 19 signals identical in direction | Risk manager still applies volatility-adjusted position limits. Portfolio manager respects max_shares constraints computed deterministically. No single trade can exceed the combined limit percentage. |
| Correlation matrix failure | Insufficient overlapping price data across tickers | returns_df.shape[0] < 5 or exception in pd.DataFrame.corr() |
Correlation multiplier defaults to 1.0 (neutral). Position limits are still bounded by volatility adjustment alone. |
⚡ Performance Profile
Hot Paths
The most performance-sensitive code paths in this module:
- Parallel LLM calls (fan-out phase): 19 agents each making 1–3 API calls to OpenAI/Groq. Total latency is bounded by the slowest agent (typically 3–10 seconds), not the sum, thanks to LangGraph’s parallel execution model.
- Financial data fetching: Each agent makes 2–4 HTTP calls to the Financial Datasets API. With 19 agents × 4 calls × N tickers, this can generate significant API traffic. Agents fetch data independently (no shared cache at the agent level).
- Volatility calculation:
calculate_volatility_metrics()computes rolling 30-day standard deviation over the full price history — O(n) where n is the number of price points. Fast for typical 60-day windows.
Resource Usage
- Memory:
AgentStateholds financial data for all tickers in memory simultaneously. Each agent’s analysis data (metrics, line items, market cap) is scoped to its function and garbage-collected after the signal is stored. The correlation matrix is O(k²) where k is the number of tickers. - I/O: Dominated by external API calls — Financial Datasets API for market data, OpenAI/Groq for LLM inference. The sequential risk_manager → portfolio_manager phase is fast (single LLM call each) since it operates on pre-computed signals, not raw data.
- Token usage: Each persona agent sends 1 LLM request with ~2,000–5,000 tokens (system prompt + financial data). The portfolio manager sends a compressed prompt (<1,000 tokens) containing only signal/confidence pairs and allowed actions.
Sub-Modules
investor-personas/
13 famous investor AI agents: Buffett, Graham, Munger, Ackman, Wood, Burry, Pabrai, Taleb, Lynch, Fisher, Jhunjhunwala, Druckenmiller, and a generic growth analyst.
- Persona system prompts encoding investing philosophies
- Value / Growth / Contrarian / Macro styles
- Pydantic signal models per agent
quantitative/
6 quantitative analysis agents: fundamentals, technicals, sentiment, valuation, news sentiment, and Aswath Damodaran’s valuation framework.
- Data-driven analysis without persona bias
- Technical indicators & chart patterns
- Market sentiment & news analysis
management/
Risk manager + Portfolio manager: signal aggregation, volatility-adjusted position sizing, and final trading decisions.
- Volatility & correlation-based risk limits
- Deterministic position sizing before LLM
- Allowed-action computation & trade execution
Directory Map
Source tree for agents
src/agents/
├── __init__.py
├── warren_buffett.py # Value investing, moat analysis, intrinsic value
├── ben_graham.py # Deep value, margin of safety
├── charlie_munger.py # Quality at fair price, rational thinking
├── bill_ackman.py # Activist investing, unlocking value
├── cathie_wood.py # Innovation/disruption growth thesis
├── michael_burry.py # Contrarian deep value, short selling
├── mohnish_pabrai.py # Dhandho framework, low-risk doubles
├── nassim_taleb.py # Tail risk, antifragility, barbell strategy
├── peter_lynch.py # Ten-baggers, buy what you know
├── phil_fisher.py # Scuttlebutt growth research
├── rakesh_jhunjhunwala.py # Emerging market macro bull
├── stanley_druckenmiller.py # Macro opportunities, currency/rates
├── aswath_damodaran.py # Dean of Valuation, DCF models
├── growth_agent.py # Generic growth analysis
├── fundamentals.py # Fundamental data analysis
├── technicals.py # Technical indicator analysis
├── sentiment.py # Market sentiment analysis
├── valuation.py # Intrinsic value calculation
├── news_sentiment.py # News-based sentiment scoring
├── risk_manager.py # Volatility + correlation risk limits
└── portfolio_manager.py # Final trading decisions + order generation
Key Code Patterns
Agent Function Signature
Every investor agent follows the same interface pattern — a function that accepts AgentState and an agent ID string, returning updated state:
class WarrenBuffettSignal(BaseModel):
signal: Literal["bullish", "bearish", "neutral"]
confidence: int = Field(description="Confidence 0-100")
reasoning: str = Field(description="Reasoning for the decision")
def warren_buffett_agent(state: AgentState, agent_id: str = "warren_buffett_agent"):
"""Analyzes stocks using Buffett's principles and LLM reasoning."""
data = state["data"]
tickers = data["tickers"]
# ... fetch data, analyze, call LLM ...
state["data"]["analyst_signals"][agent_id] = buffett_analysis
return {"messages": state["messages"] + [message], "data": data}
Analyst Registry
All agents are registered in src/utils/analysts.py via the ANALYST_CONFIG dictionary — the single source of truth for agent metadata:
ANALYST_CONFIG = {
"warren_buffett": {
"display_name": "Warren Buffett",
"description": "The Oracle of Omaha",
"agent_func": warren_buffett_agent,
"type": "analyst",
"order": 12,
},
# ... 18 more entries ...
}
def get_analyst_nodes():
"""Get mapping of analyst keys to (node_name, agent_func) tuples."""
return {key: (f"{key}_agent", config["agent_func"])
for key, config in ANALYST_CONFIG.items()}