This tutorial builds a portfolio analysis agent langgraph application that ingests holdings, pulls market data, computes risk metrics, and generates a plain-English report using an LLM. You will leave with runnable code and a clear pattern for composing financial workflows as stateful graphs.
Prerequisites
Install the dependencies below. You need Python 3.10+ and an OpenAI-compatible API key.
pip install langgraph langchain-openai yfinance pandas python-dotenv
Create a .env file with OPENAI_API_KEY=sk-.... If you run into provider rate limits, you can point the client at a gateway instead—more on that in the LLM step.
Portfolio data and state schema
Define a small holdings file. The agent reads this JSON and flows through typed state.
{
"holdings": [
{"ticker": "AAPL", "shares": 100},
{"ticker": "MSFT", "shares": 50},
{"ticker": "SPY", "shares": 30}
]
}
LangGraph requires a state container. Use TypedDict so nodes stay explicit about what they read and write.
from typing import TypedDict, List, Dict, Any
class PortfolioState(TypedDict):
holdings: List[Dict[str, Any]]
prices: Dict[str, float]
returns: Dict[str, List[float]]
metrics: Dict[str, Any]
report: str
Step 1: Fetch market data
The first node pulls close prices for the trailing month and derives daily returns. yfinance is fine for a local run; swap in your brokerage API in production.
import yfinance as yf
import pandas as pd
def fetch_market_data(state: PortfolioState) -> dict:
tickers = [h["ticker"] for h in state["holdings"]]
data = yf.download(tickers, period="1mo", interval="1d", auto_adjust=True)["Close"]
if isinstance(data, pd.Series):
data = data.to_frame()
prices = data.iloc[-1].to_dict()
returns = {t: data[t].pct_change().dropna().tolist() for t in tickers}
return {"prices": prices, "returns": returns}
Checkpoint after invocation:
print(app.invoke({"holdings": holdings})["prices"])
# {'AAPL': 212.33, 'MSFT': 430.12, 'SPY': 545.21}
Step 2: Compute portfolio metrics
This node calculates market values, weights, annualized volatility, return, and Sharpe ratio. It assumes 252 trading days.
def compute_metrics(state: PortfolioState) -> dict:
holdings = state["holdings"]
prices = state["prices"]
returns = state["returns"]
values = {h["ticker"]: h["shares"] * prices[h["ticker"]] for h in holdings}
total = sum(values.values())
weights = {t: v / total for t, v in values.items()}
length = min(len(returns[t]) for t in returns)
port_ret = [
sum(weights[t] * returns[t][i] for t in weights)
for i in range(length)
]
vol = pd.Series(port_ret).std() * (252 ** 0.5)
ann_ret = pd.Series(port_ret).mean() * 252
sharpe = ann_ret / vol if vol > 0 else 0.0
return {"metrics": {
"total_value": total,
"weights": weights,
"annualized_vol": vol,
"annualized_return": ann_ret,
"sharpe": sharpe,
}}
Expected metrics shape:
{
"total_value": 38452.1,
"weights": {"AAPL": 0.55, "MSFT": 0.56, "SPY": 0.43}, # sums ~1 after rounding
"annualized_vol": 0.17,
"annualized_return": 0.21,
"sharpe": 1.23
}
Step 3: LLM analysis node
The portfolio analysis agent langgraph design uses an LLM to turn numbers into narrative. We use ChatOpenAI. If you want provider fallback without writing retry logic, n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models with automatic fallback when a provider is rate-limited or degraded—set base_url and keep the rest of the code identical.
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
# llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2, base_url="https://api.n4n.ai/v1")
def analyze_with_llm(state: PortfolioState) -> dict:
m = state["metrics"]
prompt = f"""You are a portfolio analyst. Metrics:
Total value: {m['total_value']:.2f}
Weights: {m['weights']}
Annualized vol: {m['annualized_vol']:.2f}
Annualized return: {m['annualized_return']:.2f}
Sharpe: {m['sharpe']:.2f}
Write a concise risk assessment and one rebalancing action."""
msg = llm.invoke([
SystemMessage(content="You are a disciplined finance expert."),
HumanMessage(content=prompt),
])
return {"report": msg.content}
Step 4: Build the portfolio analysis agent langgraph
Wire the nodes linearly. LangGraph compiles to a runnable app that validates state transitions.
from langgraph.graph import StateGraph, END
def build_graph():
g = StateGraph(PortfolioState)
g.add_node("fetch", fetch_market_data)
g.add_node("metrics", compute_metrics)
g.add_node("analyze", analyze_with_llm)
g.add_edge("fetch", "metrics")
g.add_edge("metrics", "analyze")
g.add_edge("analyze", END)
g.set_entry_point("fetch")
return g.compile()
Run it:
import json, os
from dotenv import load_dotenv
load_dotenv()
with open("portfolio.json") as f:
holdings = json.load(f)["holdings"]
app = build_graph()
result = app.invoke({"holdings": holdings})
print(result["report"])
Sample output:
Your portfolio is concentrated in mega-cap tech (AAPL+MSFT ≈ 70% of value).
Annualized volatility of 17% is moderate, and Sharpe 1.23 shows decent risk-adjusted return.
Trim MSFT by 10 shares and add to SPY to reduce single-name exposure.
Extending the portfolio analysis agent langgraph
The linear graph is a starting point. Add a conditional edge after metrics to branch when sharpe < 0.5 into an alerting node. You can also give the LLM a tool to fetch live news sentiment, or persist metrics to a database inside the analyze node. Because state is explicit, adding nodes does not require refactoring upstream code—just register the node and connect edges.
Keep the LLM call isolated from data computation. That separation lets you unit-test metric math without burning tokens, and swap models by changing one client line.