Financial Analysis AI Agent
Financial Advisor Agent Architecture
What is a Financial Advisor Agent?
Financial advisor agents analyze market data, portfolio holdings, and economic indicators to provide personalized investment guidance. They combine quantitative analysis with natural language generation to explain complex financial concepts in accessible terms.
Why this matters: Institutional-quality financial analysis was once reserved for wealthy clients. AI agents democratize this by providing portfolio analysis, risk assessment, and rebalancing recommendations to anyone.
Common Misconception
"AI financial advisors can predict the market."
AI agents analyze historical data and current conditions â they cannot predict future returns. Their value is in systematic analysis, risk quantification, and removing emotional bias from investment decisions. Past performance never guarantees future results.
Real-World Analogy
Think of it as having a CFA charterholder on staff who can instantly analyze any portfolio, calculate all risk metrics, compare against benchmarks, and write a professional report â but who never gets tired, never has conflicts of interest, and always discloses limitations.
Project Overview
We will build a financial analysis agent that:
- Fetches real-time and historical market data via yfinance
- Analyzes portfolio composition and performance
- Calculates risk metrics (Sharpe ratio, VaR, CVaR, beta, max drawdown)
- Generates personalized investment advice with disclaimers
- Creates performance reports and visualizations
- Monitors market news for impact analysis
Expected outcome: An agent that provides data-driven financial analysis and advice.
Difficulty: Advanced (requires understanding of finance, statistics, and data visualization)
Tools & Setup
| Tool | Version | Purpose |
|---|---|---|
| Python | 3.11+ | Core language |
| yfinance | 0.2+ | Market data |
| pandas | 2.0+ | Data manipulation |
| numpy | 1.24+ | Numerical computing |
| matplotlib | 3.8+ | Visualization |
| openai | 1.0+ | LLM backbone |
Step 1: Environment Setup
python -m venv venv
source venv/bin/activate
pip install yfinance pandas numpy matplotlib openai
export OPENAI_API_KEY="sk-your-key"
Step 2: Market Data Fetcher
# data/market_data.py
"""Market data retrieval via yfinance with caching and error handling."""
import logging
import yfinance as yf
import pandas as pd
from typing import Dict, List
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class MarketDataFetcher:
"""Fetch real-time and historical market data.
Supports individual stocks, portfolios, and market indices.
"""
def get_stock_info(self, symbol: str) -> Dict:
"""Get current stock information including price and fundamentals."""
try:
stock = yf.Ticker(symbol)
info = stock.info
return {
"symbol": symbol,
"name": info.get("longName", symbol),
"price": info.get("currentPrice", 0),
"change": info.get("regularMarketChangePercent", 0),
"market_cap": info.get("marketCap", 0),
"pe_ratio": info.get("trailingPE"),
"dividend_yield": info.get("dividendYield"),
"52w_high": info.get("fiftyTwoWeekHigh"),
"52w_low": info.get("fiftyTwoWeekLow"),
"volume": info.get("volume", 0),
}
except Exception as e:
logger.error(f"Failed to fetch info for {symbol}: {e}")
return {"symbol": symbol, "error": str(e)}
def get_historical(self, symbol: str, period: str = "1y") -> pd.DataFrame:
"""Get historical price data."""
stock = yf.Ticker(symbol)
return stock.history(period=period)
def get_multiple(self, symbols: List[str], period: str = "1y") -> Dict[str, pd.DataFrame]:
"""Fetch historical data for multiple symbols."""
return {s: self.get_historical(s, period) for s in symbols}
def get_market_overview(self) -> Dict:
"""Get major market indices overview."""
indices = {"^GSPC": "S&P 500", "^DJI": "Dow Jones", "^IXIC": "NASDAQ"}
overview = {}
for symbol, name in indices.items():
stock = yf.Ticker(symbol)
info = stock.info
overview[name] = {
"price": info.get("regularMarketPrice", 0),
"change": info.get("regularMarketChangePercent", 0),
}
return overview
Step 3: Portfolio and Risk Analysis
# analysis/portfolio.py
"""Portfolio analysis with allocation, returns, and diversification metrics."""
import pandas as pd
import numpy as np
from typing import Dict
class PortfolioAnalyzer:
"""Analyze portfolio composition, returns, and diversification."""
def analyze(self, holdings: Dict[str, float], prices: Dict[str, pd.Series]) -> Dict:
"""Perform full portfolio analysis.
Args:
holdings: {symbol: shares} mapping.
prices: {symbol: price_series} mapping.
Returns:
Dict with total value, weights, returns, and holdings detail.
"""
total_value = sum(
holdings[s] * prices[s].iloc[-1] for s in holdings
)
weights = {
s: (holdings[s] * prices[s].iloc[-1]) / total_value
for s in holdings
}
returns = pd.DataFrame({
s: prices[s].pct_change() for s in holdings
}).dropna()
portfolio_returns = sum(returns[s] * weights[s] for s in holdings)
cumulative = (1 + portfolio_returns).cumprod()
total_return = (cumulative.iloc[-1] - 1) * 100
annualized = (
(1 + total_return / 100) ** (252 / len(returns)) - 1
) * 100
return {
"total_value": total_value,
"weights": weights,
"total_return_pct": round(total_return, 2),
"annualized_return_pct": round(annualized, 2),
"daily_returns": portfolio_returns,
"holdings": {
s: {"value": holdings[s] * prices[s].iloc[-1], "weight": weights[s]}
for s in holdings
},
}
def diversification_score(self, weights: Dict[str, float]) -> float:
"""Calculate HHI-based diversification score (0 = concentrated, 1 = diversified)."""
hhi = sum(w ** 2 for w in weights.values())
return 1 - hhi
# analysis/risk.py
"""Risk metrics calculation including Sharpe, VaR, CVaR, and drawdown."""
import pandas as pd
import numpy as np
from typing import Dict
class RiskAnalyzer:
"""Calculate comprehensive risk metrics for portfolios."""
def calculate_metrics(self, returns: pd.Series, risk_free_rate: float = 0.05) -> Dict:
"""Calculate all risk metrics from daily returns.
Args:
returns: Daily portfolio returns.
risk_free_rate: Annual risk-free rate (default 5%).
Returns:
Dict with Sharpe, VaR, CVaR, max drawdown, volatility.
"""
daily_rf = risk_free_rate / 252
excess = returns - daily_rf
sharpe = np.sqrt(252) * excess.mean() / excess.std() if excess.std() > 0 else 0
var_95 = np.percentile(returns, 5)
cvar_95 = returns[returns <= var_95].mean()
cumulative = (1 + returns).cumprod()
running_max = cumulative.cummax()
drawdown = (cumulative - running_max) / running_max
max_drawdown = drawdown.min()
volatility = returns.std() * np.sqrt(252)
return {
"sharpe_ratio": round(sharpe, 3),
"var_95_daily": round(var_95 * 100, 2),
"cvar_95_daily": round(cvar_95 * 100, 2),
"max_drawdown_pct": round(max_drawdown * 100, 2),
"annualized_volatility_pct": round(volatility * 100, 2),
"total_risk_score": self._risk_score(sharpe, max_drawdown, volatility),
}
def _risk_score(self, sharpe: float, max_dd: float, vol: float) -> str:
"""Map metrics to a risk score label."""
score = 0
if sharpe > 1: score += 3
elif sharpe > 0.5: score += 2
elif sharpe > 0: score += 1
if abs(max_dd) < 0.1: score += 3
elif abs(max_dd) < 0.2: score += 2
elif abs(max_dd) < 0.3: score += 1
if vol < 0.15: score += 2
elif vol < 0.25: score += 1
labels = {0: "Very High", 1: "High", 2: "Moderate-High", 3: "Moderate",
4: "Moderate-Low", 5: "Low", 6: "Very Low", 8: "Conservative"}
return labels.get(score, "Moderate")
def beta(self, stock_returns: pd.Series, market_returns: pd.Series) -> float:
"""Calculate beta relative to market."""
covariance = np.cov(stock_returns, market_returns)[0][1]
market_variance = np.var(market_returns)
return covariance / market_variance if market_variance > 0 else 0
Step 4: Report Generator and Agent
# reporting/report_generator.py
"""LLM-powered financial report generation with professional formatting."""
import logging
from typing import Dict
from openai import OpenAI
logger = logging.getLogger(__name__)
class ReportGenerator:
"""Generate professional investment reports using LLM.
Args:
model: OpenAI model for report generation.
"""
def __init__(self, model: str = "gpt-4o"):
self.client = OpenAI()
self.model = model
def generate_report(self, portfolio: Dict, risk: Dict, market: Dict) -> str:
"""Generate a comprehensive investment report.
Args:
portfolio: Portfolio analysis results.
risk: Risk metrics.
market: Market overview data.
Returns:
Formatted investment report with disclaimers.
"""
prompt = f"""Generate a professional investment report based on:
Portfolio Performance:
- Total Value: ${portfolio['total_value']:,.2f}
- Total Return: {portfolio['total_return_pct']}%
- Annualized Return: {portfolio['annualized_return_pct']}%
Risk Metrics:
- Sharpe Ratio: {risk['sharpe_ratio']}
- Max Drawdown: {risk['max_drawdown_pct']}%
- Volatility: {risk['annualized_volatility_pct']}%
- Risk Score: {risk['total_risk_score']}
Market Overview: {market}
Provide:
1. Executive Summary
2. Performance Analysis
3. Risk Assessment
4. Market Context
5. Recommendations
Include appropriate disclaimers: this is educational analysis, not personalized investment advice."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a CFA charterholder writing investment reports. Always include disclaimers."},
{"role": "user", "content": prompt},
],
temperature=0.3,
)
return response.choices[0].message.content
# agent.py
"""Complete financial advisor agent orchestrating all components."""
import logging
from typing import Dict
from openai import OpenAI
from data.market_data import MarketDataFetcher
from analysis.portfolio import PortfolioAnalyzer
from analysis.risk import RiskAnalyzer
from reporting.report_generator import ReportGenerator
logger = logging.getLogger(__name__)
class FinancialAdvisorAgent:
"""Financial analysis and advisory agent.
Args:
model: OpenAI model for advice generation.
"""
def __init__(self, model: str = "gpt-4o"):
self.market = MarketDataFetcher()
self.portfolio_analyzer = PortfolioAnalyzer()
self.risk_analyzer = RiskAnalyzer()
self.report_gen = ReportGenerator(model)
self.client = OpenAI()
self.model = model
def analyze_portfolio(self, holdings: Dict[str, float]) -> Dict:
"""Perform full portfolio analysis with risk metrics."""
symbols = list(holdings.keys())
prices = self.market.get_multiple(symbols)
port = self.portfolio_analyzer.analyze(holdings, prices)
risk = self.risk_analyzer.calculate_metrics(port["daily_returns"])
market = self.market.get_market_overview()
return {"portfolio": port, "risk": risk, "market": market}
def get_advice(self, holdings: Dict[str, float], question: str) -> str:
"""Get personalized investment advice based on portfolio analysis."""
analysis = self.analyze_portfolio(holdings)
prompt = f"""Based on this portfolio analysis:
Portfolio: ${analysis['portfolio']['total_value']:,.2f}
Return: {analysis['portfolio']['total_return_pct']}%
Sharpe: {analysis['risk']['sharpe_ratio']}
Risk Score: {analysis['risk']['total_risk_score']}
User question: {question}
Provide specific, actionable advice with rationale. Include disclaimers."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a financial advisor. Provide educational analysis, not personalized investment advice. Always include: 'This is not financial advice. Consult a licensed financial advisor.'"},
{"role": "user", "content": prompt},
],
temperature=0.5,
)
return response.choices[0].message.content
Mathematical Foundation
Sharpe Ratio:
Where is portfolio return, is risk-free rate, and is portfolio volatility. Higher Sharpe indicates better risk-adjusted returns. A Sharpe > 1 is good, > 2 is excellent.
Value at Risk (VaR):
Where is the z-score for confidence level . A 95% VaR of -50,000.
Maximum Drawdown:
The largest peak-to-trough decline, measuring worst-case loss. A max drawdown of -30% means the portfolio lost 30% from its peak.
Diversification Score (HHI):
Where is the weight of asset . Score ranges from 0 (fully concentrated) to ~1 (perfectly diversified). With 10 equal-weighted stocks, .
Performance Considerations
| Metric | Value | Cost Impact |
|---|---|---|
| Portfolio Analysis | 3s | yfinance API call + computation |
| Risk Calculation | 50ms | Pure numpy/pandas computation |
| Cost per Report | $0.06 | ~2K tokens for report generation |
| Data Accuracy | 99% | yfinance adjusts for splits/dividends |
| Memory Usage | 100MB | For 1-year daily data, 10 stocks |
Security Notes
- API key security â Store yfinance and OpenAI keys in environment variables
- Data validation â Verify prices are within reasonable ranges before analysis
- Disclaimer requirements â Never present analysis as personalized investment advice
- No trading execution â Agent only analyzes, never places trades
- Audit logging â Log all advice generated for compliance
- No PII in prompts â Don't include account numbers or personal financial data
Interview Questions
1. Why is the Sharpe Ratio preferred over simple return?
Simple return ignores risk. Two portfolios with 10% return may have vastly different risk profiles â one with 5% volatility (Sharpe=1.0), another with 25% volatility (Sharpe=0.4). The Sharpe Ratio normalizes return by risk, enabling apples-to-apples comparison.
2. What is the difference between VaR and CVaR?
VaR tells you the minimum loss at a confidence level (e.g., "5% chance of losing >$10K"). CVaR tells you the expected loss given that the VaR threshold is exceeded. CVaR is more informative for tail risk because it captures the magnitude of extreme losses.
3. How does the agent handle survivorship bias?
Survivorship bias occurs when only currently listed stocks are analyzed. The agent uses yfinance which includes delisted stocks if queried directly. For robust analysis, include a broader universe of tickers and account for stocks removed from indices.
4. How would you implement real-time portfolio tracking?
Replace batch get_multiple with WebSocket-based data feeds (e.g., Alpha Vantage or Interactive Brokers API). Use async architecture with asyncio and aiohttp. Store price updates in Redis. Recalculate metrics on each new tick or every 5 minutes.
5. How do you validate the LLM's financial advice?
Implement fact-checking: (1) extract numerical claims from LLM output, (2) verify against computed metrics, (3) flag discrepancies. If the LLM generates numbers not in the data, reject and regenerate.
6. What are limitations of Monte Carlo VaR?
Monte Carlo VaR assumes returns follow a specific distribution (often normal), which underestimates tail risk. Real returns exhibit fat tails and skewness. Solutions: use historical simulation, Student-t distributions, or copula models.
7. How would you extend for multi-currency portfolios?
Add currency conversion using real-time FX rates. Normalize all holdings to base currency before computing returns. Currency hedging analysis compares hedged vs unhedged returns. Risk metrics should be computed on currency-adjusted returns.
8. How do you handle corporate actions in historical data?
yfinance automatically adjusts for splits and dividends with auto_adjust=True. For dividend analysis, use ticker.dividends separately. Always use adjusted close prices for return calculations to avoid misleading results.
Common Pitfalls & Solutions
| Pitfall | Solution |
|---|---|
| Survivorship bias | Include delisted stocks; use comprehensive ticker universes |
| Look-ahead bias | Only use data available at decision time |
| Overfitting historical data | Out-of-sample testing; walk-forward validation |
| Ignoring transaction costs | Factor in commissions, spreads, and slippage |
| Data quality issues | Validate sources; handle missing values with forward-fill |
| Market regime changes | Adaptive models; detect regime shifts with volatility clustering |
| Currency effects | Normalize to base currency; consider FX hedging costs |
| Dividend accounting | Use adjusted prices; separate yield analysis from price returns |
Summary with Key Takeaways
- Market data integration via yfinance enables real-time and historical analysis
- Portfolio analysis provides quantified performance metrics (returns, weights, diversification)
- Risk metrics (Sharpe, VaR, CVaR, drawdown) quantify different dimensions of investment risk
- LLM-generated reports make complex analysis accessible to non-expert investors
- Always include appropriate investment disclaimers â this is educational analysis, not financial advice
- Diversification scoring using HHI provides a simple measure of portfolio concentration risk
- Beta measurement enables understanding of market sensitivity and hedging potential
KnowledgeCheck
-
What does a Sharpe Ratio of 0.5 indicate?
- a) Excellent risk-adjusted returns
- b) Returns are below the risk-free rate
- c) Moderate risk-adjusted returns
- d) The portfolio is too volatile
-
A 95% VaR of -$50,000 means:
- a) The portfolio will lose exactly $50,000
- b) There's a 5% chance of losing more than $50,000
- c) The portfolio has lost $50,000 historically
- d) 95% of days will show a $50,000 loss
-
What is the maximum diversification score achievable with HHI?
- a) 0.0
- b) 0.5
- c) 1.0
- d) Infinity
-
Which metric is most sensitive to extreme outlier losses?
- a) Sharpe Ratio
- b) Annualized Volatility
- c) Maximum Drawdown
- d) Total Return
-
What does a negative beta indicate?
- a) The stock is worthless
- b) The stock moves opposite to the market
- c) The stock has no correlation with the market
- d) The stock is extremely volatile
-
Why should the agent always include disclaimers?
- a) It makes the report longer
- b) Financial advice has legal requirements and fiduciary implications
- c) It improves accuracy
- d) LLMs require it
Answers: 1-c, 2-b, 3-c, 4-c, 5-b, 6-b