A stock research agent langchain project needs more than a prompt and a price API. This tutorial builds a runnable agent that pulls live market data, financial statements, and news, then synthesizes answers with tool calls. You’ll leave with code that drops into a backend service.
Prerequisites
- Python 3.10+ installed locally.
- A virtual environment (recommended).
- Packages:
langchain,langchain-openai,langchain-community,yfinance,python-dotenv. - An OpenAI API key or any OpenAI-compatible endpoint URL and key.
pip install langchain langchain-openai langchain-community yfinance python-dotenv
Create a .env file:
OPENAI_API_KEY=sk-...
# Optional: OPENAI_API_BASE=https://your-gateway/v1
We use yfinance because it requires no API key and returns real structured data. It is unofficial, so rate-limit it in production.
Step 1: Fetch market data with yfinance
Write a small module market_data.py that wraps yfinance calls. Keep functions pure and typed.
import yfinance as yf
def get_price(ticker: str) -> float:
"""Last traded price from fast_info."""
return float(yf.Ticker(ticker).fast_info["last_price"])
def get_income(ticker: str) -> dict:
"""Most recent annual income statement as a flat dict."""
df = yf.Ticker(ticker).financials
if df.empty:
return {}
return {str(k): float(v) for k, v in df.iloc[:, 0].items()}
def get_news(ticker: str, limit: int = 3) -> list:
"""Recent news headlines with links."""
items = yf.Ticker(ticker).news or []
return [{"title": n["title"], "link": n["link"]} for n in items[:limit]]
Test it in a REPL before wiring to LangChain:
>>> import market_data as m
>>> m.get_price("AAPL")
185.32
Your number will differ; the call returns a live float.
Step 2: Expose functions as LangChain tools
LangChain tools are just decorated callables. The docstring becomes the tool description the LLM sees, so be precise.
from langchain_core.tools import tool
import market_data as m
@tool
def stock_price(ticker: str) -> float:
"""Return the latest traded price for a valid ticker symbol like AAPL."""
return m.get_price(ticker)
@tool
def income_statement(ticker: str) -> dict:
"""Return the most recent annual income statement line items for a ticker."""
return m.get_income(ticker)
@tool
def recent_news(ticker: str, limit: int = 3) -> list:
"""Return up to `limit` recent news headlines and links for a ticker."""
return m.get_news(ticker, limit)
tools = [stock_price, income_statement, recent_news]
Step 3: Assemble the stock research agent langchain
We use the OpenAI tools agent pattern. It supports parallel tool calls and is the most reliable for structured data tasks.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain import hub
from langchain.agents import create_openai_tools_agent, AgentExecutor
load_dotenv()
llm = ChatOpenAI(
model="gpt-4o-mini",
temperature=0,
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_API_BASE"), # None falls back to OpenAI
)
prompt = hub.pull("hwchase17/openai-tools-agent")
agent = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
The hub.pull fetches a maintained prompt with system instructions for tool use. You can clone and edit it for domain tone.
Step 4: Run a query and inspect the trace
Invoke with a specific question. Verbose mode shows the agent’s reasoning and tool calls.
response = agent_executor.invoke({
"input": "What is Apple's current price and two recent news headlines?"
})
print(response["output"])
Expected output structure (values are live):
Apple's last price is 185.32. Recent news:
1. "Apple announces new MacBook Pro" - https://example.com/1
2. "Analysts raise AAPL target" - https://example.com/2
In the verbose trace you’ll see the agent call stock_price and recent_news sequentially, then synthesize. If yfinance returns empty news, the agent will say so instead of hallucinating.
Step 5: Add conversation memory
A research session spans follow-ups (“now show me the income statement”). Use ConversationBufferMemory to retain messages.
from langchain.memory import ConversationBufferMemory
memory = ConversationBufferMemory(return_messages=True)
agent_executor = AgentExecutor(
agent=agent, tools=tools, memory=memory, verbose=True
)
agent_executor.invoke({"input": "What is MSFT's price?"})
agent_executor.invoke({"input": "And its recent news?"}) # ticker inferred from context
The second call resolves MSFT because the memory buffer holds the prior exchange. For multi-user services, back this with a per-session store (Redis or Postgres) instead of in-process memory.
Step 6: Production hardening
Tool calls to yfinance will fail under load or geo-restrictions. Wrap each fetcher in a timeout and retry:
from tenacity import retry, stop_after_attempt, wait_fixed
@retry(stop=stop_after_attempt(3), wait=wait_fixed(1))
def get_price(ticker: str) -> float:
return float(yf.Ticker(ticker).fast_info["last_price"])
Set explicit max_iterations on the executor to avoid runaway loops:
agent_executor = AgentExecutor(
agent=agent, tools=tools, memory=memory,
max_iterations=5, early_stopping_method="generate"
)
If you point ChatOpenAI at an OpenAI-compatible gateway such as n4n.ai, you get automatic fallback across providers when one is rate-limited, without changing agent code. The gateway forwards cache-control hints, so repeated financial fetches can hit provider caches.
Finally, never trust raw tool output in customer-facing text. Validate numbers and cite the news links the agent returns. The stock research agent langchain pattern is only as good as the guardrails around its tools.
Where to take it next
Add a vector store for SEC filings, swap gpt-4o-mini for a cheaper model on simple price lookups, or route news through a sentiment classifier before the agent sees it. The architecture above stays the same; you only add tools.