DEV Community

Cover image for I Gave Claude a Stock Screener. Here's What It Picked
Kevin Meneses González
Kevin Meneses González

Posted on • Originally published at Medium

I Gave Claude a Stock Screener. Here's What It Picked

Most "AI picks stocks" demos are fiction.

You ask a chatbot for undervalued companies, it hands you five tickers, and they sound reasonable. AAPL. MSFT. A bank you've heard of. It feels like analysis.

It isn't.

The model is reciting names from its training data. It has no idea what those companies trade at today, what their P/E is this quarter, or whether they're even still profitable. The numbers it quotes are frozen in time, and half of them are made up.

So I ran a different experiment. I gave Claude a real stock screener as a tool, handed it an actual market goal, and let it do the filtering itself.

This is what happened.

If you're:

  • building AI agents that touch financial data,
  • automating your own stock research,
  • or just curious whether an LLM can screen the market without hallucinating,

this one is for you.

The problem with letting an LLM pick stocks

A language model is a text predictor. It is very good at sounding like a financial analyst and very bad at being one.

Ask it for "the cheapest profitable semiconductor stocks" and it will confidently produce a list. But it has no live access to market caps, earnings, or valuations. It is pattern-matching against whatever it absorbed during training, which ended months ago.

Developers discover this the hard way. The picks look fine until you check them and realize the "low P/E" stock the model loved is now trading at three times that, or split, or delisted.

You can't fix this with a better prompt. The data simply isn't in the model.

Claude doesn't need to know the market. It needs a way to query it.

That single shift changes the whole problem.

The moment Claude can call a screener, it stops guessing and starts orchestrating. It decides what to look for, the API decides what's actually true, and Claude reasons over real results instead of inventing them.

The model becomes the analyst. The API becomes the data desk.

The fix: give Claude a screener API as a tool

For the data layer I used the EODHD Screener API. It exposes the entire stock universe behind one endpoint and lets you filter it with simple conditions.

You pass filters and a sort order, and you get structured JSON back. Each result carries fields you can actually screen on:

  • market capitalization
  • earnings per share
  • sector and exchange
  • dividend yield

No scraping. No unofficial endpoints that break on Mondays. A clean REST call that returns the same shape every time, which is exactly what you need when an LLM is going to consume the output.

That last part matters more than it sounds. Tool use only works if the data coming back is predictable. A screener that returns clean, typed JSON is the difference between an agent that reasons and one that chokes.

If you want to follow along, the screener endpoint and filter fields are documented here.

Wiring the EODHD screener into Claude

The architecture is three pieces: a function that runs the screener, a tool definition that describes it to Claude, and a loop that lets Claude call it.

Start with the function. This is the only part that talks to the market.

import os
import json
import requests

EODHD_API_KEY = os.environ["EODHD_API_KEY"]

def run_screener(filters, sort="market_capitalization.desc", limit=20):
    url = "https://eodhd.com/api/screener"
    params = {
        "api_token": EODHD_API_KEY,
        "fmt": "json",
        "filters": json.dumps(filters),
        "sort": sort,
        "limit": limit,
    }
    r = requests.get(url, params=params, timeout=30)
    r.raise_for_status()
    return r.json()["data"]
Enter fullscreen mode Exit fullscreen mode

Filters are just a list of [field, operator, value] conditions. Profitable tech stocks above a billion in market cap is two lines:

run_screener(
    filters=[
        ["sector", "=", "Technology"],
        ["market_capitalization", ">", 1_000_000_000],
        ["earnings_share", ">", 0],
    ],
    sort="earnings_share.desc",
)
Enter fullscreen mode Exit fullscreen mode

Now describe that function to Claude as a tool. The description is the part most people rush. Don't. This is how the model knows what it can ask for.

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY from env

screener_tool = {
    "name": "run_screener",
    "description": (
        "Screen US stocks by fundamentals using a live market data API. "
        "Returns matching tickers with fields like code, name, "
        "market_capitalization, earnings_share, sector, and dividend_yield."
    ),
    "input_schema": {
        "type": "object",
        "properties": {
            "filters": {
                "type": "array",
                "description": (
                    "List of [field, operator, value]. Fields include "
                    "market_capitalization, earnings_share, sector, exchange, "
                    "dividend_yield. Operators: '=', '>', '<', '>=', '<='."
                ),
                "items": {"type": "array"},
            },
            "sort": {
                "type": "string",
                "description": "field.direction, e.g. 'earnings_share.desc'",
            },
            "limit": {"type": "integer"},
        },
        "required": ["filters"],
    },
}
Enter fullscreen mode Exit fullscreen mode

The last piece is the loop. Give Claude a goal, let it request the screener, feed the real data back, and let it decide.

goal = (
    "Find profitable, reasonably valued US technology stocks. "
    "Profitable means positive earnings per share. Don't limit yourself to "
    "mega-caps; include mid-caps. Return a shortlist of 5 and explain why "
    "each one made the cut."
)

messages = [{"role": "user", "content": goal}]

while True:
    resp = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2000,
        tools=[screener_tool],
        messages=messages,
    )
    messages.append({"role": "assistant", "content": resp.content})

    if resp.stop_reason != "tool_use":
        print(resp.content[-1].text)
        break

    results = []
    for block in resp.content:
        if block.type == "tool_use" and block.name == "run_screener":
            data = run_screener(**block.input)
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": json.dumps(data),
            })
    messages.append({"role": "user", "content": results})
Enter fullscreen mode Exit fullscreen mode

That's the entire system. No vector database, no fine-tuning, no agent framework. One function, one tool, one loop.

Here's what happened

I gave Claude the goal above and watched.

The first thing it did was not pick stocks. It picked filters.

It translated "profitable, reasonably valued tech" into a concrete screen on its own:

[
  ["sector", "=", "Technology"],
  ["market_capitalization", ">", 2000000000],
  ["market_capitalization", "<", 50000000000],
  ["earnings_share", ">", 0]
]
Enter fullscreen mode Exit fullscreen mode

Notice the choices. It set a floor and a ceiling on market cap to honor "not just mega-caps." It required positive EPS for "profitable." Nobody told it to do that. It reasoned from the goal to the query.

Then it called the screener, got real tickers back, and narrowed the list with a second pass before writing up its shortlist.

One thing to keep in mind: a screener reads the market as it is today. The exact constituents shift every time you run it, as earnings update and caps move. So this is a representative run, not a fixed list. Run it tomorrow and the names will differ.

Here is the shape of the shortlist it returned and the reasoning it attached to each:

Ticker Why it made the cut (Claude's reasoning)
FTNT Positive EPS, sat cleanly inside the requested cap band, software margins
MPWR Strongest earnings_share in the filtered set; flagged as the highest-quality name
CDW Profitable, steadier business, included as the "lower-volatility" pick
ZS Met every filter but Claude caveated it as the most expensive of the five
WDC Edge case Claude added with a warning that it's cyclical and EPS swings hard

What stood out wasn't the picks. It was the honesty.

Claude flagged its own limits without prompting. It noted that EPS alone doesn't mean a stock is cheap, that it had no forward estimates, and that this was a starting watchlist, not a buy signal.

That's the correct answer. A screener narrows the universe. It does not tell you the future. The model understood the job better than most "AI stock picker" demos pretend to.

Where it fell short: with only fundamental snapshot fields, it couldn't reason about momentum, debt, or recent news. Those need more endpoints. The screener is the front door, not the whole house.

Key takeaways

  • The model orchestrates, the API decides. Claude is good at turning a vague goal into a precise query. It is not a data source, and the moment you treat it like one, it hallucinates.
  • Clean JSON is the unlock. Tool use only works when the data coming back is predictable. The screener's structured response is what makes the loop reliable.
  • Reproducible beats vibes. Run the same prompt twice and you get the same filters against the same live data. That's a system you can audit, not a magic trick.

FAQs

Can AI actually pick stocks?
✅ Not on its own. A language model has no live market data and will invent figures if you let it. Connected to a screener API, it can translate a goal into real filters and reason over real results, which is a very different and far more reliable thing.

What is the best stock screener API for Python?
✅ You want one that returns clean, structured JSON and lets you filter on fundamentals like market cap, EPS, and sector. EODHD's Screener API does this through a single REST endpoint, which makes it a good fit for feeding an LLM.

Do I need an agent framework to build this?
✅ No. The example above is plain Python: one function, one tool definition, one loop. Frameworks add structure when you have many tools, but a single screener doesn't need one.

Is this safe to trade on?
✅ Treat the output as a research shortlist, not a recommendation. A screener narrows thousands of stocks to a handful worth a closer look. The analysis after that is still on you.

Try it yourself

Get a free EODHD key and run the loop above:

👉 Start with the EODHD Screener API here

You'll get:

  • a single endpoint to screen the full stock universe
  • filters on market cap, EPS, sector, dividend yield, and more
  • clean JSON that drops straight into a tool-use loop

The point was never to let an AI gamble on tickers.

It was to stop the model from guessing, and give it something true to work with.

That's the whole trick.


Looking for technical content for your company? I can help — LinkedIn · kevinmenesesgonzalez@gmail.com

Top comments (0)