n4nAI

Building a financial news summarization agent with LangChain

Step-by-step tutorial to build a financial news summarization agent LangChain that pulls headlines, scores relevance, and generates concise briefs.

n4n Team2 min read525 words

Audio narration

Coming soon — every post will get a voice note here.

A financial news summarization agent LangChain implementation turns a firehose of market headlines into a tight, decision-ready morning brief. This tutorial builds one from scratch: pull real RSS feeds, filter for portfolio relevance with an LLM, and generate concise summaries with a LangChain pipeline.

Prerequisites

  • Python 3.10 or newer
  • langchain, langchain-openai, langchain-community, pandas, python-dotenv installed
  • An API key for an OpenAI-compatible chat endpoint (we’ll configure LangChain to use one gateway)
  • A list of tickers or topics you care about (e.g., ["AAPL", "MSFT", "rates"])
  • Basic comfort with async Python and environment variables

1. Install and import

Create a clean virtual environment before touching this code. LangChain moves fast; pin versions in a real project.

pip install langchain langchain-openai langchain-community pandas python-dotenv
import os
import asyncio
import pandas as pd
from langchain_community.document_loaders import RSSFeedLoader
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv

load_dotenv()

We use python-dotenv to keep keys out of source. Set OPENAI_API_KEY and OPENAI_BASE_URL in a .env file.

2. Load live financial news

RSS is still the most reliable free pipe for headlines. RSSFeedLoader wraps feedparser and returns Document objects with metadata.

FEEDS = [
    "https://feeds.a.dj.com/rss/RSSMarketsMain.xml",
    "https://www.cnbc.com/id/100003114/device/rss/rss.html",
]

loader = RSSFeedLoader(urls=FEEDS, browser_user_agent="Mozilla/5.0")
docs = loader.load()
print(f"Loaded {len(docs)} articles")

# Inspect one
sample = docs[0]
print(sample.metadata["title"])
print(sample.metadata["published"])
print(sample.page_content[:200])

Expected output:

Loaded 42 articles
U.S. Stocks Edge Higher as Earnings Season Kicks Off
Mon, 14 Oct 2024 09:30:00 GMT
U.S. equities nudged higher in early trading as major banks reported mixed results...

Each document carries the headline in metadata["title"] and a short blurb in page_content. For a financial news summarization agent LangChain build, this is enough signal to triage.

3. Define a relevance filter

Summarizing every item wastes tokens and buries signal. We use a small model call to label each article against our watchlist. Keep temperature at zero for deterministic YES/NO.

relevance_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a financial analyst. Decide if the text is relevant to {topics}. Answer only 'YES' or 'NO'."),
    ("user", "{text}")
])

llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0,
    api_key=os.environ["OPENAI_API_KEY"],
    base_url=os.environ["OPENAI_BASE_URL"]
)

relevance_chain = relevance_prompt | llm | StrOutputParser()

def is_relevant(doc, topics):
    verdict = relevance_chain.invoke({
        "topics": ", ".join(topics),
        "text": doc.page_content[:500]
    }).strip().upper()
    return verdict == "YES"

topics = ["AAPL", "MSFT", "interest rates"]
filtered = [d for d in docs if is_relevant(d, topics)]
print(f"{len(filtered)} relevant after filter")

If you point OPENAI_BASE_URL at n4n.ai, you get automatic fallback across 240+ models when a provider is rate-limited or degraded, plus per-token metering on each call.

Expected output:

3 relevant after filter

The filter is strict by design. Tune the prompt if you need broader coverage (e.g., “Answer YES if the text mentions macro conditions that could affect {topics}”).

4. Summarization with a tight prompt

For short headlines, the classic load_summarize_chain adds overhead. A single call per item with a focused instruction produces better briefs.

summary_prompt = ChatPromptTemplate.from_messages([
    ("system", "Write a 2-sentence summary of the financial news item for a portfolio manager. Include the ticker if mentioned."),
    ("user", "{text}")
])

summary_chain = summary_prompt | llm | StrOutputParser()

rows = []
for doc in filtered:
    summary = summary_chain.invoke({"text": doc.page_content})
    rows.append({
        "title": doc.metadata.get("title"),
        "link": doc.metadata.get("link"),
        "summary": summary
    })

df = pd.DataFrame(rows)

You can batch these calls with llm.batch to cut latency:

summaries = summary_chain.batch([{"text": d.page_content} for d in filtered])

5. Compose the morning brief

Turn the DataFrame into a markdown report suitable for Slack or email.

brief = "# Morning Financial Brief\n\n"
for _, r in df.iterrows():
    brief += f"## {r['title']}\n{r['summary']}\n[Read more]({r['link']})\n\n"

print(brief)

Sample output:

# Morning Financial Brief

## Apple supplier warns of weak demand
iPhone component orders softened in Q3, signaling potential pressure on AAPL margins ahead of earnings.
[Read more](https://www.cnbc.com/...)

## Microsoft expands Azure AI offerings
MSFT announced new enterprise AI tools, reinforcing cloud growth despite rising rate environment.
[Read more](https://www.wsj.com/...)

6. Production hardening

The synchronous loop blocks on network IO. For a real financial news summarization agent LangChain deployment, run the filter concurrently and cache by article GUID.

async def a_is_relevant(doc, topics):
    chain = relevance_prompt | llm.ainvoke | StrOutputParser()
    verdict = (await chain.ainvoke({
        "topics": ", ".join(topics),
        "text": doc.page_content[:500]
    })).strip().upper()
    return doc if verdict == "YES" else None

async def filter_async(docs, topics):
    tasks = [a_is_relevant(d, topics) for d in docs]
    results = await asyncio.gather(*tasks)
    return [r for r in results if r]

# filtered = asyncio.run(filter_async(docs, topics))

Set model_kwargs={"cache_control": {"type": "ephemeral"}} on the ChatOpenAI constructor if your gateway forwards provider cache hints; n4n.ai honors client routing directives and forwards cache-control to cut repeat-cost on identical headlines.

Also wrap the loader in try/except. RSS feeds go down. Log failures and continue with the remaining feeds.

try:
    docs = loader.load()
except Exception as e:
    print(f"Feed load failed: {e}")
    docs = []

7. Extending the agent

Wrap the pipeline in a LangChain AgentExecutor only if you need tool use—for example, a scraping tool to pull full article text when the RSS blurb is too thin. For most trading desks, the linear chain is easier to test and debug.

Schedule the script with cron or a GitHub Action at 06:00 ET. Persist the DataFrame to S3 or a SQLite DB to build historical briefs. Swap RSSFeedLoader for a paid news API when you need deeper coverage or sentiment scores.

The financial news summarization agent LangChain pattern here scales to any number of feeds and topics. Once the chain is stable, the only real cost is token throughput, so pick a gateway that meters per-token and fails over gracefully.

Full reference script

# combine all steps above into main()
def main():
    load_dotenv()
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0,
                     api_key=os.environ["OPENAI_API_KEY"],
                     base_url=os.environ["OPENAI_BASE_URL"])
    # ... load, filter, summarize, print

Run it daily. Adjust topics as your book changes.

Tagslangchainfinancesummarization

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All framework tutorials: finance & trading analysis agents posts →