DEV Community

Ayrat Murtazin
Ayrat Murtazin

Posted on

Stock Recommendation System Using Anthropic MCP and Python

The Model Context Protocol (MCP) is an open standard developed by Anthropic that gives large language models a structured, reliable way to connect with external tools and data sources. For quantitative finance, this is a meaningful shift: instead of copy-pasting price data into a chat window and asking for analysis, you can build a system where Claude autonomously fetches live stock data, runs calculations, and returns structured recommendations — all through a standardized interface.

In this article, we implement a stock recommendation pipeline that uses Anthropic's MCP architecture to bridge a Python-based market data engine with a Claude language model. We will build the data-fetching layer using yfinance and pandas, define MCP-compatible tool schemas, and wire everything together so Claude can query real price history, compute momentum and volatility signals, and return ranked buy/hold/sell recommendations.


Most algo trading content gives you theory.
This gives you the code.

3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.

5,000 quant traders already run these:

Subscribe | AlgoEdge Insights

Stock Recommendation System Using Anthropic MCP and Python

This article covers:

  • Section 1 — What Is MCP and Why It Matters for Quant Finance:** The USB-C analogy explained. How MCP standardizes tool-calling for LLMs and why that removes friction in financial AI pipelines.
  • Section 2 — Python Implementation:** Full setup, data layer, MCP tool definitions, signal computation, and a visualization of the recommendation output.
  • Section 3 — Results and Signal Interpretation:** What the system returns, how to read the output, and realistic expectations for signal quality.
  • Section 4 — Use Cases:** Portfolio screening, watchlist automation, research augmentation, and alerting pipelines.
  • Section 5 — Limitations and Edge Cases:** Honest assessment of LLM-as-analyst, data latency, overfitting risk, and operational constraints.

1. What Is MCP and Why It Matters for Quant Finance

The official MCP documentation offers a concise analogy: think of MCP like a USB-C port for AI applications. Just as USB-C provides a single standardized connector that works across laptops, phones, monitors, and power banks, MCP provides a single standardized protocol that lets an LLM connect to databases, APIs, calculators, and external services — without requiring a custom integration for every pairing. Before MCP, wiring Claude or GPT-4 to a live data source meant hand-rolling function-calling schemas, managing context windows manually, and rebuilding the plumbing for each new tool. MCP replaces that with a consistent contract.

In practice, an MCP server exposes a set of named tools, each described by a JSON schema specifying its inputs and outputs. The LLM client discovers these tools at runtime, decides which to call based on the user's query, and receives structured results it can reason over. For finance, this means you can expose tools like get_price_history, compute_momentum, or screen_by_volatility and let Claude call them in sequence to answer a question like "Which of these five stocks looks strongest going into next week?"

The quantitative value is in the composability. A single MCP server can host a dozen signal-computation tools. Claude can chain them — fetching data, computing signals, filtering by criteria — without any orchestration code on your side. You define the tools; the model decides the workflow. This turns Claude from a passive text generator into an active research agent operating on real market data.

For this implementation, we keep the MCP server lightweight and local. We define three tools: one that downloads OHLCV data from Yahoo Finance, one that computes a momentum and volatility score for a ticker, and one that returns a ranked recommendation. Claude receives the tool schemas, calls them in order for a list of tickers, and returns a structured buy/hold/sell output with brief reasoning.

2. Python Implementation

2.1 Setup and Parameters

The implementation requires four libraries: yfinance for market data, pandas and numpy for signal computation, anthropic for the Claude API client, and json for schema handling. The configurable parameters at the top of the notebook control which tickers to screen, the lookback window for momentum, and the volatility normalization period.

# ── Dependencies ──────────────────────────────────────────────────────────────
# pip install yfinance anthropic pandas numpy matplotlib

import json
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib
import matplotlib.pyplot as plt
import anthropic

# ── Parameters ────────────────────────────────────────────────────────────────
TICKERS         = ["AAPL", "MSFT", "NVDA", "ASML", "TSLA"]
LOOKBACK_DAYS   = 90        # calendar days of price history to download
MOMENTUM_WINDOW = 20        # trading days for momentum return calculation
VOL_WINDOW      = 20        # trading days for rolling volatility
RISK_FREE_RATE  = 0.05      # annualised, used in Sharpe approximation
ANTHROPIC_MODEL = "claude-3-5-sonnet-20241022"

# ── API client ────────────────────────────────────────────────────────────────
client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from environment
Enter fullscreen mode Exit fullscreen mode

Implementation chart

Each parameter is intentionally exposed at the top so you can swap the ticker list to your own watchlist, tighten or widen the momentum window, or plug in a different risk-free rate without touching the core logic.

2.2 Market Data and Signal Engine

This section defines the two core functions that back our MCP tools. fetch_price_history downloads adjusted close prices and returns a clean DataFrame. compute_signals derives three values from that history: a momentum return over the last MOMENTUM_WINDOW days, a 20-day annualised volatility, and a simple Sharpe-proxy score (momentum divided by volatility) used to rank tickers.

def fetch_price_history(ticker: str, lookback_days: int = LOOKBACK_DAYS) -> pd.DataFrame:
    """Download adjusted close prices for a single ticker."""
    end   = pd.Timestamp.today()
    start = end - pd.Timedelta(days=lookback_days)
    raw   = yf.download(ticker, start=start, end=end, auto_adjust=True, progress=False)
    if raw.empty:
        raise ValueError(f"No data returned for {ticker}")
    df = raw[["Close"]].rename(columns={"Close": "price"})
    df["daily_return"] = df["price"].pct_change()
    return df.dropna()


def compute_signals(ticker: str) -> dict:
    """Compute momentum, volatility, and a Sharpe-proxy score."""
    df = fetch_price_history(ticker)

    # Momentum: total return over the last MOMENTUM_WINDOW trading days
    if len(df) < MOMENTUM_WINDOW:
        raise ValueError(f"Insufficient data for {ticker}")
    momentum = (df["price"].iloc[-1] / df["price"].iloc[-MOMENTUM_WINDOW] - 1) * 100

    # Volatility: annualised std of daily returns
    ann_vol = df["daily_return"].rolling(VOL_WINDOW).std().iloc[-1] * np.sqrt(252) * 100

    # Sharpe proxy: momentum per unit of annualised volatility
    sharpe_proxy = (momentum / ann_vol) if ann_vol > 0 else 0.0

    return {
        "ticker":        ticker,
        "momentum_pct":  round(momentum, 2),
        "ann_vol_pct":   round(ann_vol, 2),
        "sharpe_proxy":  round(sharpe_proxy, 4),
        "last_price":    round(float(df["price"].iloc[-1]), 2),
    }


# ── Pre-compute all signals (used both for MCP and for charting) ──────────────
all_signals = {t: compute_signals(t) for t in TICKERS}
print(pd.DataFrame(all_signals).T.to_string())
Enter fullscreen mode Exit fullscreen mode

2.3 MCP Tool Schema and Claude Integration

Here we define the three MCP-compatible tool schemas and the dispatch function that executes them when Claude calls them. The screen_stocks tool is the entry point: it accepts a list of tickers, calls compute_signals for each, and returns the full signal table. Claude then reasons over that table to generate ranked recommendations.

# ── Tool definitions (MCP-compatible JSON schemas) ────────────────────────────
tools = [
    {
        "name": "get_stock_signals",
        "description": (
            "Fetch momentum, annualised volatility, and a Sharpe-proxy score "
            "for a list of stock tickers. Returns a JSON array of signal objects."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "tickers": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "List of ticker symbols, e.g. ['AAPL', 'MSFT']",
                }
            },
            "required": ["tickers"],
        },
    },
    {
        "name": "rank_and_recommend",
        "description": (
            "Given a JSON array of signal objects, rank tickers by Sharpe-proxy "
            "and return structured buy/hold/sell recommendations with brief reasoning."
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "signals_json": {
                    "type": "string",
                    "description": "JSON string of signal objects from get_stock_signals.",
                }
            },
            "required": ["signals_json"],
        },
    },
]


def execute_tool(name: str, inputs: dict) -> str:
    """Dispatch MCP tool calls to local Python functions."""
    if name == "get_stock_signals":
        results = [compute_signals(t) for t in inputs["tickers"]]
        return json.dumps(results)

    if name == "rank_and_recommend":
        signals = json.loads(inputs["signals_json"])
        ranked  = sorted(signals, key=lambda x: x["sharpe_proxy"], reverse=True)
        recs    = []
        for i, s in enumerate(ranked):
            label = "BUY" if i == 0 else ("HOLD" if i <= 2 else "SELL")
            recs.append({**s, "rank": i + 1, "recommendation": label})
        return json.dumps(recs, indent=2)

    return json.dumps({"error": f"Unknown tool: {name}"})


# ── Agentic loop: Claude calls tools until it has a final answer ──────────────
def run_mcp_recommendation(tickers: list[str]) -> str:
    messages = [
        {
            "role": "user",
            "content": (
                f"Use the available tools to fetch signals for {tickers}, "
                "rank them, and provide structured buy/hold/sell recommendations "
                "with a one-sentence rationale for each ticker."
            ),
        }
    ]

    while True:
        response = client.messages.create(
            model=ANTHROPIC_MODEL,
            max_tokens=1024,
            tools=tools,
            messages=messages,
        )

        # If Claude is done reasoning, return the final text
        if response.stop_reason == "end_turn":
            return next(b.text for b in response.content if hasattr(b, "text"))

        # Otherwise, execute the tool calls Claude requested
        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = execute_tool(block.name, block.input)
                tool_results.append({
                    "type":        "tool_result",
                    "tool_use_id": block.id,
                    "content":     result,
                })

        # Append assistant turn and tool results, then loop
        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user",      "content": tool_results})


recommendation_output = run_mcp_recommendation(TICKERS)
print(recommendation_output)
Enter fullscreen mode Exit fullscreen mode

2.4 Visualization

The chart below plots each ticker's momentum return against its annualised volatility, with point size proportional to the Sharpe-proxy score. This gives an immediate visual read of which names offer the best risk-adjusted momentum — exactly what Claude is reasoning over when it calls the ranking tool.

plt.style.use("dark_background")
fig, ax = plt.subplots(figsize=(9, 6))

signals_list = list(all_signals.values())
x    = [s["ann_vol_pct"]   for s in signals_list]
y    = [s["momentum_pct"]  for s in signals_list]
sz   = [max(abs(s["sharpe_proxy"]) * 400, 80) for s in signals_list]
cols = ["#00e676" if s["momentum_pct"] > 0 else "#ff1744" for s in signals_list]

ax.scatter(x, y, s=sz, c=cols, alpha=0.85, edgecolors="white", linewidths=0.5)

for s in signals_list:
    ax.annotate(
        s["ticker"],
        xy=(s["ann_vol_pct"], s["momentum_pct"]),
        xytext=(6, 4),
        textcoords="offset points",
        color="white",
        fontsize=10,
        fontweight="bold",
    )

ax.axhline(0, color="grey", linewidth=0.8, linestyle="--")
ax.set_xlabel("Annualised Volatility (%)", color="white", fontsize=11)
ax.set_ylabel(f"{MOMENTUM_WINDOW}-Day Momentum Return (%)", color="white", fontsize=11)
ax.set_title("MCP Stock Screener — Momentum vs Volatility", color="white", fontsize=13, pad=14)
ax.tick_params(colors="white")
for spine in ax.spines.values():
    spine.set_edgecolor("#444444")

plt.tight_layout()
plt.savefig("mcp_screener.png", dpi=150, bbox_inches="tight")
plt.show()
Enter fullscreen mode Exit fullscreen mode

Figure 1. Momentum return vs annualised volatility for each screened ticker; bubble size encodes the Sharpe-proxy score, green indicates positive momentum, and red indicates negative — tickers in the upper-left quadrant (high momentum, low volatility) are the strongest candidates.


Enjoying this strategy so far? This is only a taste of what's possible.

Go deeper with my newsletter: longer, more detailed articles + full Google Colab implementations for every approach.

Or get everything in one powerful package with AlgoEdge Insights: 30+ Python-Powered Trading Strategies — The Complete 2026 Playbook — it comes with detailed write-ups + dedicated Google Colab code/links for each of the 30+ strategies, so you can code, test, and trade them yourself immediately.

Exclusive for readers: 20% off the book with code MEDIUM20.

Join newsletter for free or Claim Your Discounted Book and take your trading to the next level!

3. Results and Signal Interpretation

Running the pipeline against a five-stock watchlist — AAPL, MSFT, NVDA, ASML, TSLA — over a 90-day lookback will typically produce a ranked table where high-momentum, lower-volatility names like MSFT or ASML surface near the top, while high-beta names like TSLA and NVDA rank lower despite sometimes having stronger raw momentum, precisely because their volatility penalty in the Sharpe-proxy is large. The Sharpe-proxy is intentionally simple: it is not a risk-adjusted return in the full sense, but it filters out situations where momentum is driven almost entirely by outsized risk.

Claude's natural language output layers reasoning on top of the numbers. A typical response will note which ticker leads on momentum, flag any name where volatility is elevated relative to peers, and offer a brief macro or sector observation if the signal pattern warrants it. That reasoning is non-deterministic and should be treated as a qualitative gloss on the quantitative ranking, not as independent confirmation. The numbers are the ground truth; Claude's prose adds context.

The key metric to monitor in live deployment is the Sharpe-proxy consistency across rolling windows. A ticker that ranks first over 20 days but third over 60 days is exhibiting momentum instability — a signal worth surfacing to the model explicitly by adding a multi-window tool to the MCP server.

4. Use Cases

  • Watchlist screening: Run the pipeline nightly against a curated list of 20–50 names, store the ranked output in a database, and alert on tickers that move from HOLD to BUY across consecutive sessions.
  • Earnings and event filtering: Wrap the MCP tool layer around an earnings calendar API. Ask Claude to screen only tickers reporting in the next five days and rank them by pre-earnings momentum.
  • Portfolio rebalancing support: Feed the current portfolio holdings as the ticker list. The system identifies which positions have deteriorating momentum scores and which off-portfolio names have stronger signals, providing a structured basis for rotation decisions.
  • Research augmentation: Use Claude's tool-calling loop to pull multiple data layers — price signals, sector ETF performance, macro indicators — and synthesize them into a single research memo, replacing hours of manual data assembly.

5. Limitations and Edge Cases

LLM reasoning is probabilistic, not auditable. Claude's recommendation rationale can sound authoritative while being factually soft. Always treat the prose output as supplementary to the quantitative signal, never as its source.

Momentum signals decay quickly. A 20-day momentum score computed at market close is stale by the next afternoon if a material news event occurs overnight. The pipeline has no real-time awareness unless you build a streaming data layer into the MCP server.

Yahoo Finance data quality. yfinance occasionally returns split-adjusted anomalies or missing sessions for thinly traded names. The dropna() call in fetch_price_history silently drops those rows, which can shorten the effective lookback without warning. Add an explicit length check before computing signals.

The Sharpe-proxy is not a Sharpe ratio. It uses raw momentum return rather than excess return over the risk-free rate, and it does not account for serial correlation in returns. For production use, replace it with a proper rolling Sharpe computed from daily excess returns.

API rate limits and cost. Each call to run_mcp_recommendation makes at least two round-trips to the Claude API (one for tool selection, one for final synthesis). For large ticker universes, this accumulates both latency and token cost. Batch signals into a single tool call where possible and cache results that do not change intraday.

Concluding Thoughts

The Model Context Protocol offers quantitative developers a clean, maintainable way to connect LLM reasoning to live financial data without hand-coding a new integration for every tool. The pipeline built here — a three-tool MCP server backed by yfinance and pandas, wrapped in a Claude agentic loop — takes under 150 lines of Python and already handles the full cycle from raw price data to ranked recommendations.

The most productive next step is expanding the tool library. Adding a fundamental data tool (P/E, revenue growth, analyst consensus), a sector ETF momentum tool, or a macro regime indicator gives Claude richer context and meaningfully improves the quality of its synthesis. Each new tool is a new JSON schema and a new Python function; the agentic loop handles the rest.

If you want to go deeper on the quantitative infrastructure behind systems like this — stochastic models, volatility surfaces, options pricing, and full Python backtesting frameworks — the AlgoEdge Insights newsletter covers exactly that, with complete notebooks in every issue.


Most algo trading content gives you theory.
This gives you the code.

3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.

5,000 quant traders already run these:

Subscribe | AlgoEdge Insights

Top comments (0)